generated from coulomb/repo-seed
feat: use hub-core repo registry routes
This commit is contained in:
@@ -9,9 +9,8 @@ import uuid
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy import case, func, select
|
from sqlalchemy import case, func, select
|
||||||
from sqlalchemy.orm import noload
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from api.config import settings
|
from api.config import settings
|
||||||
@@ -46,71 +45,44 @@ from api.schemas.managed_repo import (
|
|||||||
RepoUpdate,
|
RepoUpdate,
|
||||||
ScopeIssueDetail,
|
ScopeIssueDetail,
|
||||||
)
|
)
|
||||||
|
from hub_core.routers.repos import create_repos_router
|
||||||
|
|
||||||
router = APIRouter(prefix="/repos", tags=["repos"])
|
router = APIRouter(prefix="/repos", tags=["repos"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list[RepoRead])
|
async def _publish_repo_registered(repo: ManagedRepo, body: RepoCreate, domain: Domain) -> None:
|
||||||
async def list_repos(
|
|
||||||
response: Response,
|
|
||||||
domain: str | None = None,
|
|
||||||
session: AsyncSession = Depends(get_session),
|
|
||||||
) -> list[ManagedRepo]:
|
|
||||||
response.headers["Cache-Control"] = "max-age=60, stale-while-revalidate=30"
|
|
||||||
q = select(ManagedRepo).options(noload(ManagedRepo.goals)).order_by(ManagedRepo.name)
|
|
||||||
if domain:
|
|
||||||
domain_row = await session.execute(select(Domain).where(Domain.slug == domain))
|
|
||||||
domain_obj = domain_row.scalar_one_or_none()
|
|
||||||
if domain_obj is None:
|
|
||||||
raise HTTPException(status_code=404, detail=f"Domain '{domain}' not found")
|
|
||||||
q = q.where(ManagedRepo.domain_id == domain_obj.id)
|
|
||||||
result = await session.execute(q)
|
|
||||||
return list(result.scalars().all())
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=RepoRead, status_code=status.HTTP_201_CREATED)
|
|
||||||
async def register_repo(
|
|
||||||
body: RepoCreate,
|
|
||||||
session: AsyncSession = Depends(get_session),
|
|
||||||
) -> ManagedRepo:
|
|
||||||
domain_row = await session.execute(select(Domain).where(Domain.slug == body.domain_slug))
|
|
||||||
domain_obj = domain_row.scalar_one_or_none()
|
|
||||||
if domain_obj is None:
|
|
||||||
raise HTTPException(status_code=404, detail=f"Domain '{body.domain_slug}' not found")
|
|
||||||
|
|
||||||
existing = await session.execute(select(ManagedRepo).where(ManagedRepo.slug == body.slug))
|
|
||||||
if existing.scalar_one_or_none():
|
|
||||||
raise HTTPException(status_code=409, detail=f"Repo slug '{body.slug}' already exists")
|
|
||||||
|
|
||||||
repo = ManagedRepo(
|
|
||||||
domain_id=domain_obj.id,
|
|
||||||
slug=body.slug,
|
|
||||||
name=body.name,
|
|
||||||
local_path=body.local_path,
|
|
||||||
host_paths=body.host_paths,
|
|
||||||
remote_url=body.remote_url,
|
|
||||||
git_fingerprint=body.git_fingerprint,
|
|
||||||
description=body.description,
|
|
||||||
topic_id=body.topic_id,
|
|
||||||
)
|
|
||||||
session.add(repo)
|
|
||||||
await session.commit()
|
|
||||||
await session.refresh(repo)
|
|
||||||
|
|
||||||
subject = "org.statehub.repo.registered"
|
subject = "org.statehub.repo.registered"
|
||||||
envelope = EventEnvelope.new(
|
envelope = EventEnvelope.new(
|
||||||
subject,
|
subject,
|
||||||
attributes={
|
attributes={
|
||||||
"repo_id": str(repo.id),
|
"repo_id": str(repo.id),
|
||||||
"repo_slug": repo.slug,
|
"repo_slug": repo.slug,
|
||||||
"domain_slug": body.domain_slug,
|
"domain_slug": domain.slug,
|
||||||
"remote_url": repo.remote_url,
|
"remote_url": repo.remote_url,
|
||||||
"local_path": repo.local_path,
|
"local_path": repo.local_path,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
asyncio.create_task(publish_event(subject, envelope))
|
asyncio.create_task(publish_event(subject, envelope))
|
||||||
|
|
||||||
return repo
|
|
||||||
|
def _core_repo_router(**route_flags) -> APIRouter:
|
||||||
|
return create_repos_router(
|
||||||
|
get_session,
|
||||||
|
prefix="",
|
||||||
|
domain_model=Domain,
|
||||||
|
repo_model=ManagedRepo,
|
||||||
|
repo_create_schema=RepoCreate,
|
||||||
|
repo_update_schema=RepoUpdate,
|
||||||
|
repo_read_schema=RepoRead,
|
||||||
|
repo_path_register_schema=RepoPathRegister,
|
||||||
|
list_noload_fields=("goals",),
|
||||||
|
create_extension_fields=("topic_id",),
|
||||||
|
after_register=_publish_repo_registered,
|
||||||
|
**route_flags,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
router.include_router(_core_repo_router(include_slug_routes=False))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/onboard", response_model=RepoOnboardResult)
|
@router.post("/onboard", response_model=RepoOnboardResult)
|
||||||
@@ -184,43 +156,6 @@ async def onboard_repo(body: RepoOnboardRequest) -> RepoOnboardResult:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/by-fingerprint", response_model=list[RepoRead])
|
|
||||||
async def get_repo_by_fingerprint(
|
|
||||||
hash: str,
|
|
||||||
remote_url: str | None = None,
|
|
||||||
session: AsyncSession = Depends(get_session),
|
|
||||||
) -> list[ManagedRepo]:
|
|
||||||
"""Look up repos by git root-commit SHA-1 fingerprint.
|
|
||||||
|
|
||||||
The fingerprint is the output of ``git rev-list --max-parents=0 HEAD`` and
|
|
||||||
is identical across every clone of the same repository. Repos that share
|
|
||||||
git history (forks, monorepo splits) will have the same fingerprint.
|
|
||||||
|
|
||||||
Pass ``remote_url`` to narrow results to a specific remote — useful when
|
|
||||||
multiple repos share the same ancestor commit.
|
|
||||||
|
|
||||||
Returns an empty list if no match is found.
|
|
||||||
"""
|
|
||||||
q = select(ManagedRepo).where(ManagedRepo.git_fingerprint == hash)
|
|
||||||
if remote_url:
|
|
||||||
q = q.where(ManagedRepo.remote_url == remote_url)
|
|
||||||
result = await session.execute(q)
|
|
||||||
return list(result.scalars().all())
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/by-remote", response_model=RepoRead)
|
|
||||||
async def get_repo_by_remote_url(
|
|
||||||
url: str,
|
|
||||||
session: AsyncSession = Depends(get_session),
|
|
||||||
) -> ManagedRepo:
|
|
||||||
"""Look up a repo by its git remote URL (fallback; prefer /by-fingerprint)."""
|
|
||||||
result = await session.execute(select(ManagedRepo).where(ManagedRepo.remote_url == url))
|
|
||||||
repo = result.scalar_one_or_none()
|
|
||||||
if repo is None:
|
|
||||||
raise HTTPException(status_code=404, detail=f"No repo with remote_url '{url}' found")
|
|
||||||
return repo
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/doi/summary", response_model=list[DoISummaryEntry])
|
@router.get("/doi/summary", response_model=list[DoISummaryEntry])
|
||||||
async def doi_summary(session: AsyncSession = Depends(get_session)) -> list[DoISummaryEntry]:
|
async def doi_summary(session: AsyncSession = Depends(get_session)) -> list[DoISummaryEntry]:
|
||||||
"""Return DoI tier for all active repos, worst tier first.
|
"""Return DoI tier for all active repos, worst tier first.
|
||||||
@@ -493,46 +428,12 @@ async def list_repo_scope_health(
|
|||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{slug}", response_model=RepoRead)
|
router.include_router(
|
||||||
async def get_repo(
|
_core_repo_router(
|
||||||
slug: str,
|
include_collection_routes=False,
|
||||||
session: AsyncSession = Depends(get_session),
|
include_lookup_routes=False,
|
||||||
) -> ManagedRepo:
|
)
|
||||||
return await _get_repo_by_slug(slug, session)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{slug}", response_model=RepoRead)
|
|
||||||
async def update_repo(
|
|
||||||
slug: str,
|
|
||||||
body: RepoUpdate,
|
|
||||||
session: AsyncSession = Depends(get_session),
|
|
||||||
) -> ManagedRepo:
|
|
||||||
repo = await _get_repo_by_slug(slug, session)
|
|
||||||
for field, value in body.model_dump(exclude_unset=True).items():
|
|
||||||
setattr(repo, field, value)
|
|
||||||
await session.commit()
|
|
||||||
await session.refresh(repo)
|
|
||||||
return repo
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{slug}/paths", response_model=RepoRead)
|
|
||||||
async def register_host_path(
|
|
||||||
slug: str,
|
|
||||||
body: RepoPathRegister,
|
|
||||||
session: AsyncSession = Depends(get_session),
|
|
||||||
) -> ManagedRepo:
|
|
||||||
"""Register or update the local path for a specific host.
|
|
||||||
|
|
||||||
Merges {"host": path} into host_paths without overwriting other entries.
|
|
||||||
Use this when a repo lives at a different absolute path on different machines.
|
|
||||||
"""
|
|
||||||
repo = await _get_repo_by_slug(slug, session)
|
|
||||||
updated = dict(repo.host_paths or {})
|
|
||||||
updated[body.host] = body.path
|
|
||||||
repo.host_paths = updated
|
|
||||||
await session.commit()
|
|
||||||
await session.refresh(repo)
|
|
||||||
return repo
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{slug}/archive", response_model=RepoRead)
|
@router.patch("/{slug}/archive", response_model=RepoRead)
|
||||||
|
|||||||
Reference in New Issue
Block a user