0c654b173d
POST /api/projects/<id>/collections, owner/admin-gated, commits a .collection.yaml to the content repo main via bot.create_collection, then re-mirrors the registry so the collections row appears (§22.2). Adds GET list/one collection routes. Extends FakeGitea to model directory listings so the mirror's content-repo walk is exercised end to end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
113 lines
4.7 KiB
Python
113 lines
4.7 KiB
Python
"""§22 S2 — collection directory + create-collection.
|
|
|
|
GET /api/projects/:id/collections — list the project's visible collections.
|
|
GET /api/projects/:id/collections/:cid — one collection's settings.
|
|
POST /api/projects/:id/collections — create a collection. Authorized by a
|
|
deployment owner/admin (S2; scoped
|
|
{owner, contributor} roles at the
|
|
collection axis land in S3). The bot
|
|
commits a `.collection.yaml` to the
|
|
content repo, then the registry mirror
|
|
upserts the collections row — §22.2
|
|
keeps the registry the source of truth.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
import yaml
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
from pydantic import BaseModel
|
|
|
|
from . import (
|
|
auth,
|
|
collections as collections_mod,
|
|
projects as projects_mod,
|
|
registry as registry_mod,
|
|
)
|
|
from .bot import Bot
|
|
from .config import Config
|
|
from .gitea import Gitea, GiteaError
|
|
|
|
_SLUG_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$")
|
|
|
|
|
|
class CreateCollectionBody(BaseModel):
|
|
collection_id: str
|
|
type: str
|
|
name: str | None = None
|
|
visibility: str | None = None
|
|
initial_state: str | None = None
|
|
|
|
|
|
def make_router(config: Config, gitea: Gitea, bot: Bot) -> APIRouter:
|
|
router = APIRouter()
|
|
|
|
@router.get("/api/projects/{project_id}/collections")
|
|
async def list_cols(project_id: str, request: Request) -> dict[str, Any]:
|
|
viewer = auth.current_user(request)
|
|
# §22.5 read gate: a gated project 404s a non-member.
|
|
auth.require_project_readable(viewer, project_id)
|
|
return {"items": collections_mod.list_collections(project_id)}
|
|
|
|
@router.get("/api/projects/{project_id}/collections/{collection_id}")
|
|
async def get_col(project_id: str, collection_id: str, request: Request) -> dict[str, Any]:
|
|
viewer = auth.current_user(request)
|
|
auth.require_project_readable(viewer, project_id)
|
|
col = collections_mod.get_collection(collection_id)
|
|
if col is None or col["project_id"] != project_id:
|
|
raise HTTPException(404, "Not found")
|
|
return col
|
|
|
|
@router.post("/api/projects/{project_id}/collections")
|
|
async def create_col(
|
|
project_id: str, body: CreateCollectionBody, request: Request
|
|
) -> dict[str, Any]:
|
|
# S2 authority: deployment owner/admin (the scoped-role surface is S3).
|
|
user = auth.require_admin(request)
|
|
auth.require_project_readable(user, project_id)
|
|
cid = body.collection_id.strip().lower()
|
|
if not _SLUG_RE.match(cid) or cid == "default":
|
|
raise HTTPException(422, "collection id must be a slug and not 'default'")
|
|
if body.type not in registry_mod.VALID_TYPES:
|
|
raise HTTPException(422, f"invalid type {body.type!r}")
|
|
if body.visibility is not None and body.visibility not in registry_mod.VALID_VISIBILITY:
|
|
raise HTTPException(422, f"invalid visibility {body.visibility!r}")
|
|
if body.initial_state is not None and body.initial_state not in registry_mod.VALID_INITIAL_STATE:
|
|
raise HTTPException(422, f"invalid initial_state {body.initial_state!r}")
|
|
if collections_mod.get_collection(cid) is not None:
|
|
raise HTTPException(409, f"collection `{cid}` already exists")
|
|
content_repo = projects_mod.content_repo(project_id)
|
|
if not content_repo:
|
|
raise HTTPException(409, "project has no content repo")
|
|
|
|
manifest: dict[str, Any] = {"type": body.type}
|
|
if body.name:
|
|
manifest["name"] = body.name
|
|
if body.visibility:
|
|
manifest["visibility"] = body.visibility
|
|
if body.initial_state:
|
|
manifest["initial_state"] = body.initial_state
|
|
manifest_yaml = yaml.safe_dump(manifest, sort_keys=False)
|
|
|
|
try:
|
|
await bot.create_collection(
|
|
user.as_actor(),
|
|
org=config.gitea_org,
|
|
content_repo=content_repo,
|
|
collection_id=cid,
|
|
manifest_yaml=manifest_yaml,
|
|
)
|
|
except GiteaError as e:
|
|
raise HTTPException(502, f"Gitea: {e.detail}")
|
|
|
|
# §22.2: re-read the registry so the new manifest becomes a row.
|
|
await registry_mod.refresh_registry(config, gitea)
|
|
col = collections_mod.get_collection(cid)
|
|
if col is None:
|
|
raise HTTPException(500, "collection committed but not mirrored")
|
|
return col
|
|
|
|
return router
|