feat: project Nexus SBOM state into summaries
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a028f0-a42f-7582-89a8-ebaad7343834
This commit is contained in:
parent
e1e259cf87
commit
76e6eda086
4 changed files with 172 additions and 42 deletions
|
|
@ -48,6 +48,9 @@ from api.schemas.managed_repo import (
|
|||
classification_fields_set,
|
||||
validate_repo_classification_fields,
|
||||
)
|
||||
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 hub_core.routers.repos import create_repos_router
|
||||
|
||||
router = APIRouter(prefix="/repos", tags=["repos"])
|
||||
|
|
@ -108,7 +111,7 @@ async def list_repos(
|
|||
capability_tag: str | None = None,
|
||||
business_stake: str | None = None,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> list[ManagedRepo]:
|
||||
) -> list[RepoRead]:
|
||||
"""List repos with optional domain and classification filters."""
|
||||
response.headers["Cache-Control"] = "max-age=60, stale-while-revalidate=30"
|
||||
q = (
|
||||
|
|
@ -134,7 +137,7 @@ async def list_repos(
|
|||
if business_stake:
|
||||
q = q.where(ManagedRepo.business_stake.contains([business_stake]))
|
||||
result = await session.execute(q)
|
||||
return list(result.scalars().all())
|
||||
return await _project_repo_reads(list(result.scalars().all()))
|
||||
|
||||
|
||||
@router.post("/", response_model=RepoRead, status_code=status.HTTP_201_CREATED)
|
||||
|
|
@ -330,6 +333,7 @@ async def doi_summary(session: AsyncSession = Depends(get_session)) -> list[DoIS
|
|||
select(ManagedRepo).where(ManagedRepo.status == "active").order_by(ManagedRepo.name)
|
||||
)
|
||||
repos = list(repos_result.scalars().all())
|
||||
sbom_projections = await _sbom_projection_map()
|
||||
repo_ids = [r.id for r in repos]
|
||||
id_to_slug = {r.id: r.slug for r in repos}
|
||||
|
||||
|
|
@ -385,7 +389,7 @@ async def doi_summary(session: AsyncSession = Depends(get_session)) -> list[DoIS
|
|||
"local_path": repo.local_path,
|
||||
"remote_url": repo.remote_url,
|
||||
"host_paths": repo.host_paths or {},
|
||||
"last_sbom_at": str(repo.last_sbom_at) if repo.last_sbom_at else None,
|
||||
"last_sbom_at": _projected_last_sbom_at(repo, sbom_projections),
|
||||
"updated_at": str(repo.updated_at) if repo.updated_at else "",
|
||||
}
|
||||
fp = compute_fingerprint(
|
||||
|
|
@ -465,6 +469,7 @@ 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)
|
||||
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()
|
||||
|
||||
|
|
@ -484,7 +489,7 @@ async def get_repo_doi(
|
|||
"local_path": repo.local_path,
|
||||
"remote_url": repo.remote_url,
|
||||
"host_paths": repo.host_paths or {},
|
||||
"last_sbom_at": str(repo.last_sbom_at) if repo.last_sbom_at else None,
|
||||
"last_sbom_at": _projected_last_sbom_at(repo, sbom_projections),
|
||||
"updated_at": str(repo.updated_at) if repo.updated_at else "",
|
||||
}
|
||||
fp = compute_fingerprint(repo_dict, str(tpsc_row.latest) if tpsc_row.latest else None,
|
||||
|
|
@ -622,6 +627,15 @@ async def update_repo_with_classification(
|
|||
return repo
|
||||
|
||||
|
||||
@router.get("/{slug}", response_model=RepoRead)
|
||||
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]
|
||||
|
||||
|
||||
router.include_router(
|
||||
_core_repo_router(
|
||||
include_collection_routes=False,
|
||||
|
|
@ -822,3 +836,48 @@ def _repo_doi_dict(repo: ManagedRepo, domain_slug: str | None) -> dict:
|
|||
"last_sbom_at": str(repo.last_sbom_at) if repo.last_sbom_at else None,
|
||||
"updated_at": str(repo.updated_at) if repo.updated_at else "",
|
||||
}
|
||||
|
||||
|
||||
async def _sbom_projection_map() -> dict[str, datetime | None]:
|
||||
if not reads_from_nexus():
|
||||
return {}
|
||||
try:
|
||||
repositories = await get_sbom_nexus_json("/repositories/")
|
||||
except SBOMNexusError as exc:
|
||||
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
|
||||
return {
|
||||
repository["slug"]: (
|
||||
datetime.fromisoformat(repository["last_attempt_at"].replace("Z", "+00:00"))
|
||||
if repository.get("last_attempt_at")
|
||||
else None
|
||||
)
|
||||
for repository in repositories
|
||||
}
|
||||
|
||||
|
||||
def _projected_last_sbom_at(
|
||||
repo: ManagedRepo,
|
||||
projections: dict[str, datetime | None],
|
||||
) -> str | None:
|
||||
if repo.slug in projections:
|
||||
projected = projections[repo.slug]
|
||||
return projected.isoformat() if projected else None
|
||||
return str(repo.last_sbom_at) if repo.last_sbom_at else None
|
||||
|
||||
|
||||
async def _project_repo_reads(repositories: list[ManagedRepo]) -> list[RepoRead]:
|
||||
projections = await _sbom_projection_map()
|
||||
result: list[RepoRead] = []
|
||||
for repository in repositories:
|
||||
read = RepoRead.model_validate(repository)
|
||||
if repository.slug in projections:
|
||||
read = read.model_copy(
|
||||
update={
|
||||
"last_sbom_at": projections[repository.slug],
|
||||
"sbom_source": "sbom-nexus"
|
||||
if projections[repository.slug]
|
||||
else None,
|
||||
}
|
||||
)
|
||||
result.append(read)
|
||||
return result
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue