feat: make repository reads alias-aware
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a049a4-ee9f-78e1-9d66-2cb0f9bea3e3
This commit is contained in:
parent
639b9aed08
commit
2e2ae1e5d0
19 changed files with 982 additions and 94 deletions
|
|
@ -54,6 +54,13 @@ from api.services.sbom_nexus import SBOMNexusError
|
|||
from api.services.sbom_nexus import get_json as get_sbom_nexus_json
|
||||
from api.services.sbom_nexus import reads_from_nexus
|
||||
from api.services.repository_identity import stage_initial_repository_identity
|
||||
from api.services.repository_aliases import (
|
||||
RepositorySlugResolution,
|
||||
affected_slug_predicate,
|
||||
repository_resolution_for_id,
|
||||
resolve_repository_slug,
|
||||
stale_external_references,
|
||||
)
|
||||
from hub_core.routers.repos import create_repos_router
|
||||
|
||||
router = APIRouter(prefix="/repos", tags=["repos"])
|
||||
|
|
@ -140,14 +147,14 @@ async def list_repos(
|
|||
if business_stake:
|
||||
q = q.where(ManagedRepo.business_stake.contains([business_stake]))
|
||||
result = await session.execute(q)
|
||||
return await _project_repo_reads(list(result.scalars().all()))
|
||||
return await _project_repo_reads(session, 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:
|
||||
) -> RepoRead:
|
||||
domain_result = await session.execute(select(Domain).where(Domain.slug == body.domain_slug))
|
||||
domain_obj = domain_result.scalar_one_or_none()
|
||||
if domain_obj is None:
|
||||
|
|
@ -197,7 +204,7 @@ async def register_repo(
|
|||
) from exc
|
||||
await session.refresh(repo)
|
||||
await _publish_repo_registered(repo, body, domain_obj)
|
||||
return repo
|
||||
return (await _project_repo_reads(session, [repo]))[0]
|
||||
|
||||
|
||||
@router.post("/onboard", response_model=RepoOnboardResult)
|
||||
|
|
@ -483,7 +490,8 @@ async def get_repo_doi(
|
|||
|
||||
Results are cached by fingerprint. Pass ?force_refresh=true to bypass the cache.
|
||||
"""
|
||||
repo = await _get_repo_by_slug(slug, session)
|
||||
resolution = await resolve_repository_slug(session, slug)
|
||||
repo = resolution.repo
|
||||
sbom_projections = await _sbom_projection_map()
|
||||
domain_result = await session.execute(select(Domain).where(Domain.id == repo.domain_id))
|
||||
domain_obj = domain_result.scalar_one_or_none()
|
||||
|
|
@ -517,7 +525,7 @@ async def get_repo_doi(
|
|||
|
||||
if not force_refresh and cached and cached.fingerprint == fp and cached.criteria:
|
||||
return DoIReport(
|
||||
repo_slug=slug,
|
||||
repo_slug=repo.slug,
|
||||
tier=cached.tier,
|
||||
core_pass=cached.core_pass,
|
||||
standard_pass=cached.standard_pass,
|
||||
|
|
@ -556,11 +564,11 @@ async def get_repo_doi(
|
|||
async def get_repo_by_id(
|
||||
repo_id: uuid.UUID,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> ManagedRepo:
|
||||
) -> RepoRead:
|
||||
repo = await session.get(ManagedRepo, repo_id)
|
||||
if repo is None:
|
||||
raise HTTPException(status_code=404, detail=f"Repo '{repo_id}' not found")
|
||||
return repo
|
||||
return (await _project_repo_reads(session, [repo]))[0]
|
||||
|
||||
|
||||
@router.get("/scope-health", response_model=list[RepoScopeHealth])
|
||||
|
|
@ -615,9 +623,10 @@ async def update_repo_with_classification(
|
|||
slug: str,
|
||||
body: RepoUpdate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> ManagedRepo:
|
||||
) -> RepoRead:
|
||||
"""Patch repo metadata including classification spine fields."""
|
||||
repo = await _get_repo_by_slug(slug, session)
|
||||
resolution = await resolve_repository_slug(session, slug)
|
||||
repo = resolution.repo
|
||||
payload = body.model_dump(exclude_unset=True)
|
||||
requested_domain_slug = payload.pop("domain_slug", None)
|
||||
if requested_domain_slug is not None:
|
||||
|
|
@ -658,7 +667,7 @@ async def update_repo_with_classification(
|
|||
setattr(repo, field, value)
|
||||
await session.commit()
|
||||
await session.refresh(repo)
|
||||
return repo
|
||||
return (await _project_repo_reads(session, [repo], requested=resolution))[0]
|
||||
|
||||
|
||||
@router.get("/{slug}", response_model=RepoRead)
|
||||
|
|
@ -666,28 +675,53 @@ async def get_repo_with_sbom_projection(
|
|||
slug: str,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> RepoRead:
|
||||
repo = await _get_repo_by_slug(slug, session)
|
||||
return (await _project_repo_reads([repo]))[0]
|
||||
resolution = await resolve_repository_slug(session, slug)
|
||||
return (
|
||||
await _project_repo_reads(
|
||||
session,
|
||||
[resolution.repo],
|
||||
requested=resolution,
|
||||
include_stale_external=True,
|
||||
)
|
||||
)[0]
|
||||
|
||||
|
||||
router.include_router(
|
||||
_core_repo_router(
|
||||
include_collection_routes=False,
|
||||
include_lookup_routes=False,
|
||||
include_slug_routes=False,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{slug}/paths", response_model=RepoRead)
|
||||
async def register_repo_path(
|
||||
slug: str,
|
||||
body: RepoPathRegister,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> RepoRead:
|
||||
resolution = await resolve_repository_slug(session, slug)
|
||||
repo = resolution.repo
|
||||
host_paths = dict(repo.host_paths or {})
|
||||
host_paths[body.host] = body.path
|
||||
repo.host_paths = host_paths
|
||||
await session.commit()
|
||||
await session.refresh(repo)
|
||||
return (await _project_repo_reads(session, [repo], requested=resolution))[0]
|
||||
|
||||
|
||||
@router.patch("/{slug}/archive", response_model=RepoRead)
|
||||
async def archive_repo(
|
||||
slug: str,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> ManagedRepo:
|
||||
repo = await _get_repo_by_slug(slug, session)
|
||||
) -> RepoRead:
|
||||
resolution = await resolve_repository_slug(session, slug)
|
||||
repo = resolution.repo
|
||||
repo.status = "archived"
|
||||
await session.commit()
|
||||
await session.refresh(repo)
|
||||
return repo
|
||||
return (await _project_repo_reads(session, [repo], requested=resolution))[0]
|
||||
|
||||
|
||||
@router.get("/{slug}/dispatch", response_model=RepoDispatch)
|
||||
|
|
@ -701,7 +735,8 @@ async def get_repo_dispatch(
|
|||
call it at session start to discover what work is pending without needing to
|
||||
read state-hub summary or scan workplan files manually.
|
||||
"""
|
||||
repo = await _get_repo_by_slug(slug, session)
|
||||
resolution = await resolve_repository_slug(session, slug)
|
||||
repo = resolution.repo
|
||||
|
||||
# Active goal
|
||||
goal_result = await session.execute(
|
||||
|
|
@ -765,7 +800,9 @@ async def get_repo_dispatch(
|
|||
ic_result = await session.execute(
|
||||
select(InterfaceChange).where(
|
||||
InterfaceChange.status == "published",
|
||||
InterfaceChange.affected_repo_slugs.contains([slug]),
|
||||
affected_slug_predicate(
|
||||
InterfaceChange.affected_repo_slugs, resolution.slug_values
|
||||
),
|
||||
).order_by(InterfaceChange.published_at.desc())
|
||||
)
|
||||
pending_changes = [
|
||||
|
|
@ -794,7 +831,12 @@ async def get_repo_dispatch(
|
|||
)
|
||||
|
||||
return RepoDispatch(
|
||||
repo_slug=slug,
|
||||
repo_slug=resolution.canonical_slug,
|
||||
requested_slug=resolution.requested_slug,
|
||||
canonical_slug=resolution.canonical_slug,
|
||||
slug_status=resolution.slug_status,
|
||||
aliases=list(resolution.aliases),
|
||||
stale_external_references=await stale_external_references(session, resolution),
|
||||
active_goal=active_goal,
|
||||
active_workplans=dispatch_workstreams,
|
||||
human_interventions=all_interventions,
|
||||
|
|
@ -819,7 +861,8 @@ async def sync_repo_consistency(
|
|||
Returns the raw JSON output from consistency_check.py.
|
||||
Query param ?fix=false to run check-only without writing.
|
||||
"""
|
||||
repo = await _get_repo_by_slug(slug, session)
|
||||
resolution = await resolve_repository_slug(session, slug)
|
||||
repo = resolution.repo
|
||||
|
||||
hostname = socket.gethostname()
|
||||
host_paths = repo.host_paths or {}
|
||||
|
|
@ -828,13 +871,13 @@ async def sync_repo_consistency(
|
|||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail=(
|
||||
f"No accessible path for repo '{slug}' on host '{hostname}'. "
|
||||
f"Register with: POST /repos/{slug}/paths/"
|
||||
f"No accessible path for repo '{repo.slug}' on host '{hostname}'. "
|
||||
f"Register with: POST /repos/{repo.slug}/paths/"
|
||||
),
|
||||
)
|
||||
|
||||
script = Path(__file__).parent.parent.parent / "scripts" / "consistency_check.py"
|
||||
cmd = [sys.executable, str(script), "--repo", slug, "--json",
|
||||
cmd = [sys.executable, str(script), "--repo", repo.slug, "--json",
|
||||
"--api-base", settings.api_base]
|
||||
if fix:
|
||||
cmd.append("--fix")
|
||||
|
|
@ -853,11 +896,8 @@ async def sync_repo_consistency(
|
|||
|
||||
|
||||
async def _get_repo_by_slug(slug: str, session: AsyncSession) -> ManagedRepo:
|
||||
result = await session.execute(select(ManagedRepo).where(ManagedRepo.slug == slug))
|
||||
repo = result.scalar_one_or_none()
|
||||
if repo is None:
|
||||
raise HTTPException(status_code=404, detail=f"Repo '{slug}' not found")
|
||||
return repo
|
||||
resolution = await resolve_repository_slug(session, slug)
|
||||
return resolution.repo
|
||||
|
||||
|
||||
def _repo_doi_dict(repo: ManagedRepo, domain_slug: str | None) -> dict:
|
||||
|
|
@ -899,11 +939,35 @@ def _projected_last_sbom_at(
|
|||
return str(repo.last_sbom_at) if repo.last_sbom_at else None
|
||||
|
||||
|
||||
async def _project_repo_reads(repositories: list[ManagedRepo]) -> list[RepoRead]:
|
||||
async def _project_repo_reads(
|
||||
session: AsyncSession,
|
||||
repositories: list[ManagedRepo],
|
||||
*,
|
||||
requested: RepositorySlugResolution | None = None,
|
||||
include_stale_external: bool = False,
|
||||
) -> list[RepoRead]:
|
||||
projections = await _sbom_projection_map()
|
||||
result: list[RepoRead] = []
|
||||
for repository in repositories:
|
||||
resolution = (
|
||||
requested
|
||||
if requested is not None and requested.repo.id == repository.id
|
||||
else await repository_resolution_for_id(session, repository)
|
||||
)
|
||||
read = RepoRead.model_validate(repository)
|
||||
read = read.model_copy(
|
||||
update={
|
||||
"requested_slug": resolution.requested_slug,
|
||||
"canonical_slug": resolution.canonical_slug,
|
||||
"slug_status": resolution.slug_status,
|
||||
"aliases": list(resolution.aliases),
|
||||
"stale_external_references": (
|
||||
await stale_external_references(session, resolution)
|
||||
if include_stale_external
|
||||
else []
|
||||
),
|
||||
}
|
||||
)
|
||||
if repository.slug in projections:
|
||||
read = read.model_copy(
|
||||
update={
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue