feat: add tenant semantic documentation lifecycle
This commit is contained in:
@@ -32,12 +32,19 @@ from govoplan_core.core.versioning import (
|
||||
from govoplan_core.db.session import get_database
|
||||
|
||||
from govoplan_docs.backend.manifest import DOCS_ADMIN_READ_SCOPES, DOCS_READ_SCOPES
|
||||
from govoplan_docs.backend.api.v1.semantic_routes import router as semantic_router
|
||||
from govoplan_docs.backend.sources import (
|
||||
RegisteredDocumentationSource,
|
||||
build_documentation_source_registry,
|
||||
)
|
||||
from govoplan_docs.backend.semantic_service import (
|
||||
list_semantic_entries,
|
||||
select_locale_entries,
|
||||
semantic_entry_payload,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/docs", tags=["docs"])
|
||||
router.include_router(semantic_router)
|
||||
|
||||
TOPIC_KINDS = ("workflow", "reference", "pattern", "system")
|
||||
_CONFIGURATION_ACTIVE_STATES = frozenset({"enabled", "inherited"})
|
||||
@@ -958,9 +965,149 @@ def _collect_documentation_topics(
|
||||
for topic in provided_topics:
|
||||
if not user_workflow_scope_condition_issues(topic):
|
||||
topics.append((manifest.id, topic))
|
||||
if session is not None:
|
||||
try:
|
||||
topics.extend(
|
||||
("docs", topic)
|
||||
for topic in _semantic_documentation_topics(
|
||||
registry,
|
||||
principal,
|
||||
session=session,
|
||||
locale=locale,
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
if documentation_type == "admin":
|
||||
topics.append(
|
||||
(
|
||||
"docs",
|
||||
DocumentationTopic(
|
||||
id="docs.semantic-runtime-unavailable",
|
||||
title="Tenant semantic documentation unavailable",
|
||||
summary="Stored tenant semantics could not be projected for this request.",
|
||||
body="Static module documentation remains available. Check the Docs database migration, subject providers, and application logs.",
|
||||
layer="evidence",
|
||||
documentation_types=("admin",),
|
||||
source_module_id="docs",
|
||||
metadata={
|
||||
"kind": "system",
|
||||
"error_type": type(exc).__name__,
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
return topics
|
||||
|
||||
|
||||
def _semantic_documentation_topics(
|
||||
registry: PlatformRegistry,
|
||||
principal: ApiPrincipal,
|
||||
*,
|
||||
session: object,
|
||||
locale: str,
|
||||
) -> tuple[DocumentationTopic, ...]:
|
||||
entries = select_locale_entries(
|
||||
list_semantic_entries(session, principal),
|
||||
locale=locale,
|
||||
)
|
||||
topics: list[DocumentationTopic] = []
|
||||
for entry in entries:
|
||||
payload = semantic_entry_payload(
|
||||
session,
|
||||
registry,
|
||||
principal,
|
||||
entry=entry,
|
||||
editor=False,
|
||||
requested_locale=locale,
|
||||
)
|
||||
if payload is None:
|
||||
continue
|
||||
content = payload["content"]
|
||||
resolution = payload["subject_resolution"]
|
||||
subject = resolution.get("subject") if isinstance(resolution, Mapping) else None
|
||||
route = subject.get("route") if isinstance(subject, Mapping) else None
|
||||
route_anchor = (
|
||||
subject.get("route_anchor") if isinstance(subject, Mapping) else None
|
||||
)
|
||||
links = [
|
||||
DocumentationLink(
|
||||
label=str(item["label"]),
|
||||
href=str(item["href"]),
|
||||
kind="runtime" if str(item["href"]).startswith("/") else "external",
|
||||
)
|
||||
for item in content.get("links", ())
|
||||
if isinstance(item, Mapping) and item.get("label") and item.get("href")
|
||||
]
|
||||
if isinstance(route, str) and route.startswith("/"):
|
||||
links.insert(
|
||||
0,
|
||||
DocumentationLink(
|
||||
label="Open configured subject",
|
||||
href=(f"{route}#{route_anchor}" if route_anchor else route),
|
||||
kind="runtime",
|
||||
),
|
||||
)
|
||||
body_parts = [
|
||||
str(content.get(key) or "").strip()
|
||||
for key in (
|
||||
"meaning",
|
||||
"body",
|
||||
"intended_use",
|
||||
"non_intended_use",
|
||||
)
|
||||
]
|
||||
topics.append(
|
||||
DocumentationTopic(
|
||||
id=f"docs.semantic.{entry.id}",
|
||||
title=str(content.get("title") or "Semantic documentation"),
|
||||
summary=str(content.get("summary") or "Tenant semantic guidance"),
|
||||
body="\n\n".join(part for part in body_parts if part),
|
||||
layer="configured",
|
||||
documentation_types=("admin", "user"),
|
||||
source_module_id="docs",
|
||||
order=200,
|
||||
links=tuple(links),
|
||||
related_modules=(entry.subject_module_id,),
|
||||
metadata={
|
||||
"kind": "reference",
|
||||
"source_badge": "tenant_semantic",
|
||||
"semantic_entry_id": entry.id,
|
||||
"semantic_subject": payload["subject"],
|
||||
"semantic_subject_stable_key": entry.subject_stable_key,
|
||||
"subject_availability": resolution.get("availability"),
|
||||
"locale": payload["locale"],
|
||||
"requested_locale": payload["requested_locale"],
|
||||
"locale_fallback": payload["locale_fallback"],
|
||||
"lifecycle_state": payload["lifecycle_state"],
|
||||
"pending_draft": payload["pending_draft"],
|
||||
"route": route,
|
||||
"route_anchor": route_anchor,
|
||||
"help_contexts": [
|
||||
_semantic_help_context(entry),
|
||||
],
|
||||
},
|
||||
)
|
||||
)
|
||||
return tuple(topics)
|
||||
|
||||
|
||||
def _semantic_help_context(entry: object) -> str:
|
||||
values = [
|
||||
"semantic",
|
||||
str(getattr(entry, "subject_module_id")),
|
||||
str(getattr(entry, "subject_kind")),
|
||||
str(getattr(entry, "subject_id")),
|
||||
]
|
||||
if getattr(entry, "anchor_kind", None) and getattr(entry, "anchor_id", None):
|
||||
values.extend(
|
||||
(
|
||||
str(getattr(entry, "anchor_kind")),
|
||||
str(getattr(entry, "anchor_id")),
|
||||
)
|
||||
)
|
||||
return ".".join(values)
|
||||
|
||||
|
||||
def _topic_matches_documentation_type(topic: DocumentationTopic, documentation_type: DocumentationType) -> bool:
|
||||
return documentation_type in (topic.documentation_types or ("admin",))
|
||||
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.audit.logging import audit_from_principal
|
||||
from govoplan_core.auth import ApiPrincipal, require_any_scope
|
||||
from govoplan_core.core.semantic_documentation import (
|
||||
SemanticDocumentationContractError,
|
||||
SemanticDocumentationSubjectQuery,
|
||||
SemanticDocumentationSubjectReference,
|
||||
list_semantic_documentation_subjects,
|
||||
)
|
||||
from govoplan_core.db.session import get_session
|
||||
|
||||
from govoplan_docs.backend.manifest import (
|
||||
DOCS_ADMIN_READ_SCOPE,
|
||||
DOCS_READ_SCOPE,
|
||||
DOCS_SEMANTIC_CREATE_SCOPE,
|
||||
DOCS_SEMANTIC_EDIT_SCOPE,
|
||||
DOCS_SEMANTIC_EXPORT_SCOPE,
|
||||
DOCS_SEMANTIC_POLICY_SCOPE,
|
||||
DOCS_SEMANTIC_PUBLISH_SCOPE,
|
||||
DOCS_SEMANTIC_RETIRE_SCOPE,
|
||||
DOCS_SEMANTIC_SUPERSEDE_SCOPE,
|
||||
)
|
||||
from govoplan_docs.backend.semantic_schemas import (
|
||||
SemanticDocumentationCreateRequest,
|
||||
SemanticDocumentationPolicyUpdateRequest,
|
||||
SemanticDocumentationSupersedeRequest,
|
||||
SemanticDocumentationTransitionRequest,
|
||||
SemanticDocumentationUpdateRequest,
|
||||
)
|
||||
from govoplan_docs.backend.semantic_service import (
|
||||
SemanticDocumentationAuthorizationError,
|
||||
SemanticDocumentationConflictError,
|
||||
SemanticDocumentationError,
|
||||
SemanticDocumentationNotFoundError,
|
||||
content_visible_to_principal,
|
||||
create_semantic_entry,
|
||||
get_semantic_entry,
|
||||
list_semantic_entries,
|
||||
publication_policy,
|
||||
publish_semantic_entry,
|
||||
retire_semantic_entry,
|
||||
revision_payload,
|
||||
select_locale_entries,
|
||||
semantic_entry_history,
|
||||
semantic_entry_payload,
|
||||
set_publication_policy,
|
||||
supersede_semantic_entry,
|
||||
update_semantic_entry,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter(prefix="/semantic", tags=["docs-semantic"])
|
||||
READ_SCOPES = (
|
||||
DOCS_READ_SCOPE,
|
||||
DOCS_ADMIN_READ_SCOPE,
|
||||
DOCS_SEMANTIC_CREATE_SCOPE,
|
||||
DOCS_SEMANTIC_EDIT_SCOPE,
|
||||
DOCS_SEMANTIC_PUBLISH_SCOPE,
|
||||
DOCS_SEMANTIC_SUPERSEDE_SCOPE,
|
||||
DOCS_SEMANTIC_RETIRE_SCOPE,
|
||||
)
|
||||
EDITOR_SCOPES = READ_SCOPES[2:]
|
||||
|
||||
SessionDep = Annotated[Session, Depends(get_session)]
|
||||
ReadPrincipal = Annotated[ApiPrincipal, Depends(require_any_scope(*READ_SCOPES))]
|
||||
|
||||
|
||||
@router.get("/policy")
|
||||
def get_policy(
|
||||
response: Response,
|
||||
session: SessionDep,
|
||||
principal: ReadPrincipal,
|
||||
) -> dict[str, str]:
|
||||
_private(response)
|
||||
return {"mode": publication_policy(session, principal.tenant_id)}
|
||||
|
||||
|
||||
@router.put("/policy")
|
||||
def update_policy(
|
||||
payload: SemanticDocumentationPolicyUpdateRequest,
|
||||
response: Response,
|
||||
session: SessionDep,
|
||||
principal: Annotated[
|
||||
ApiPrincipal, Depends(require_any_scope(DOCS_SEMANTIC_POLICY_SCOPE))
|
||||
],
|
||||
) -> dict[str, str]:
|
||||
_private(response)
|
||||
try:
|
||||
mode = set_publication_policy(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
mode=payload.mode,
|
||||
)
|
||||
_audit(session, principal, "docs.semantic.policy.updated", None, {"mode": mode})
|
||||
session.commit()
|
||||
return {"mode": mode}
|
||||
except SemanticDocumentationError as exc:
|
||||
session.rollback()
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/subjects")
|
||||
def list_subjects(
|
||||
request: Request,
|
||||
response: Response,
|
||||
session: SessionDep,
|
||||
principal: ReadPrincipal,
|
||||
query: str = Query(default="", max_length=300),
|
||||
subject_kind: list[str] | None = Query(default=None, max_length=120),
|
||||
module_id: str | None = Query(default=None, max_length=80),
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
cursor: str | None = Query(default=None, max_length=1000),
|
||||
) -> dict[str, object]:
|
||||
_private(response)
|
||||
try:
|
||||
pages = list_semantic_documentation_subjects(
|
||||
_registry(request),
|
||||
session,
|
||||
principal,
|
||||
request=SemanticDocumentationSubjectQuery(
|
||||
tenant_id=principal.tenant_id,
|
||||
query=query,
|
||||
subject_kinds=tuple(subject_kind or ()),
|
||||
limit=limit,
|
||||
cursor=cursor,
|
||||
),
|
||||
)
|
||||
except (SemanticDocumentationContractError, TypeError) as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
items = [
|
||||
{
|
||||
"module_id": provider_module_id,
|
||||
"subjects": [item.to_dict() for item in page.subjects],
|
||||
"next_cursor": page.next_cursor,
|
||||
"has_more": page.has_more,
|
||||
}
|
||||
for provider_module_id, page in pages
|
||||
if module_id is None or provider_module_id == module_id
|
||||
]
|
||||
return {"providers": items, "total": sum(len(item["subjects"]) for item in items)}
|
||||
|
||||
|
||||
@router.get("/entries")
|
||||
def list_entries(
|
||||
request: Request,
|
||||
response: Response,
|
||||
session: SessionDep,
|
||||
principal: ReadPrincipal,
|
||||
locale: str = Query(default="de", min_length=2, max_length=20),
|
||||
module_id: str | None = Query(default=None, max_length=80),
|
||||
subject_kind: str | None = Query(default=None, max_length=120),
|
||||
include_drafts: bool = Query(default=False),
|
||||
) -> dict[str, object]:
|
||||
_private(response)
|
||||
editor = include_drafts and _has_any(principal, EDITOR_SCOPES)
|
||||
entries = list_semantic_entries(
|
||||
session,
|
||||
principal,
|
||||
module_id=module_id,
|
||||
subject_kind=subject_kind,
|
||||
)
|
||||
selected = entries if editor else select_locale_entries(entries, locale=locale)
|
||||
items = [
|
||||
item
|
||||
for entry in selected
|
||||
if (
|
||||
item := semantic_entry_payload(
|
||||
session,
|
||||
_registry(request),
|
||||
principal,
|
||||
entry=entry,
|
||||
editor=editor,
|
||||
requested_locale=locale,
|
||||
)
|
||||
)
|
||||
is not None
|
||||
]
|
||||
return {"items": items, "total": len(items)}
|
||||
|
||||
|
||||
@router.get("/entries/{entry_id}")
|
||||
def inspect_entry(
|
||||
entry_id: str,
|
||||
request: Request,
|
||||
response: Response,
|
||||
session: SessionDep,
|
||||
principal: ReadPrincipal,
|
||||
include_draft: bool = Query(default=False),
|
||||
) -> dict[str, object]:
|
||||
_private(response)
|
||||
try:
|
||||
entry = get_semantic_entry(session, principal, entry_id=entry_id)
|
||||
item = semantic_entry_payload(
|
||||
session,
|
||||
_registry(request),
|
||||
principal,
|
||||
entry=entry,
|
||||
editor=include_draft and _has_any(principal, EDITOR_SCOPES),
|
||||
)
|
||||
if item is None:
|
||||
raise SemanticDocumentationNotFoundError("Entry not found.")
|
||||
return item
|
||||
except SemanticDocumentationError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/entries/{entry_id}/history")
|
||||
def inspect_history(
|
||||
entry_id: str,
|
||||
response: Response,
|
||||
session: SessionDep,
|
||||
principal: Annotated[ApiPrincipal, Depends(require_any_scope(*EDITOR_SCOPES))],
|
||||
) -> dict[str, object]:
|
||||
_private(response)
|
||||
try:
|
||||
items = semantic_entry_history(session, principal, entry_id=entry_id)
|
||||
return {"items": [revision_payload(item) for item in items], "total": len(items)}
|
||||
except SemanticDocumentationError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/entries", status_code=status.HTTP_201_CREATED)
|
||||
def create_entry(
|
||||
payload: SemanticDocumentationCreateRequest,
|
||||
request: Request,
|
||||
response: Response,
|
||||
session: SessionDep,
|
||||
principal: Annotated[
|
||||
ApiPrincipal, Depends(require_any_scope(DOCS_SEMANTIC_CREATE_SCOPE))
|
||||
],
|
||||
) -> dict[str, object]:
|
||||
return _mutate(
|
||||
response,
|
||||
session,
|
||||
principal,
|
||||
action="created",
|
||||
callback=lambda: create_semantic_entry(
|
||||
session,
|
||||
_registry(request),
|
||||
principal,
|
||||
subject=SemanticDocumentationSubjectReference.from_mapping(
|
||||
payload.subject.model_dump(mode="json")
|
||||
),
|
||||
locale=payload.locale,
|
||||
content=payload.content.model_dump(mode="json"),
|
||||
change_reason=payload.change_reason,
|
||||
),
|
||||
registry=_registry(request),
|
||||
)
|
||||
|
||||
|
||||
@router.put("/entries/{entry_id}")
|
||||
def update_entry(
|
||||
entry_id: str,
|
||||
payload: SemanticDocumentationUpdateRequest,
|
||||
request: Request,
|
||||
response: Response,
|
||||
session: SessionDep,
|
||||
principal: Annotated[
|
||||
ApiPrincipal, Depends(require_any_scope(DOCS_SEMANTIC_EDIT_SCOPE))
|
||||
],
|
||||
) -> dict[str, object]:
|
||||
return _mutate(
|
||||
response,
|
||||
session,
|
||||
principal,
|
||||
action="updated",
|
||||
callback=lambda: update_semantic_entry(
|
||||
session,
|
||||
_registry(request),
|
||||
principal,
|
||||
entry_id=entry_id,
|
||||
expected_revision=payload.expected_revision,
|
||||
content=payload.content.model_dump(mode="json"),
|
||||
change_reason=payload.change_reason,
|
||||
),
|
||||
registry=_registry(request),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/entries/{entry_id}/publish")
|
||||
def publish_entry(
|
||||
entry_id: str,
|
||||
payload: SemanticDocumentationTransitionRequest,
|
||||
request: Request,
|
||||
response: Response,
|
||||
session: SessionDep,
|
||||
principal: Annotated[
|
||||
ApiPrincipal, Depends(require_any_scope(DOCS_SEMANTIC_PUBLISH_SCOPE))
|
||||
],
|
||||
) -> dict[str, object]:
|
||||
return _mutate(
|
||||
response,
|
||||
session,
|
||||
principal,
|
||||
action="published",
|
||||
callback=lambda: publish_semantic_entry(
|
||||
session,
|
||||
_registry(request),
|
||||
principal,
|
||||
entry_id=entry_id,
|
||||
expected_revision=payload.expected_revision,
|
||||
change_reason=payload.change_reason,
|
||||
),
|
||||
registry=_registry(request),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/entries/{entry_id}/supersede")
|
||||
def supersede_entry(
|
||||
entry_id: str,
|
||||
payload: SemanticDocumentationSupersedeRequest,
|
||||
request: Request,
|
||||
response: Response,
|
||||
session: SessionDep,
|
||||
principal: Annotated[
|
||||
ApiPrincipal, Depends(require_any_scope(DOCS_SEMANTIC_SUPERSEDE_SCOPE))
|
||||
],
|
||||
) -> dict[str, object]:
|
||||
return _mutate(
|
||||
response,
|
||||
session,
|
||||
principal,
|
||||
action="superseded",
|
||||
callback=lambda: supersede_semantic_entry(
|
||||
session,
|
||||
principal,
|
||||
entry_id=entry_id,
|
||||
replacement_entry_id=payload.replacement_entry_id,
|
||||
expected_revision=payload.expected_revision,
|
||||
change_reason=payload.change_reason,
|
||||
),
|
||||
registry=_registry(request),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/entries/{entry_id}/retire")
|
||||
def retire_entry(
|
||||
entry_id: str,
|
||||
payload: SemanticDocumentationTransitionRequest,
|
||||
request: Request,
|
||||
response: Response,
|
||||
session: SessionDep,
|
||||
principal: Annotated[
|
||||
ApiPrincipal, Depends(require_any_scope(DOCS_SEMANTIC_RETIRE_SCOPE))
|
||||
],
|
||||
) -> dict[str, object]:
|
||||
return _mutate(
|
||||
response,
|
||||
session,
|
||||
principal,
|
||||
action="retired",
|
||||
callback=lambda: retire_semantic_entry(
|
||||
session,
|
||||
principal,
|
||||
entry_id=entry_id,
|
||||
expected_revision=payload.expected_revision,
|
||||
change_reason=payload.change_reason,
|
||||
),
|
||||
registry=_registry(request),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/export")
|
||||
def export_tenant_semantics(
|
||||
request: Request,
|
||||
response: Response,
|
||||
session: SessionDep,
|
||||
principal: Annotated[
|
||||
ApiPrincipal, Depends(require_any_scope(DOCS_SEMANTIC_EXPORT_SCOPE))
|
||||
],
|
||||
) -> dict[str, object]:
|
||||
_private(response)
|
||||
entries = list_semantic_entries(session, principal)
|
||||
response.headers["Content-Disposition"] = (
|
||||
'attachment; filename="govoplan-semantic-documentation.json"'
|
||||
)
|
||||
exported: list[dict[str, object]] = []
|
||||
for entry in entries:
|
||||
payload = semantic_entry_payload(
|
||||
session,
|
||||
_registry(request),
|
||||
principal,
|
||||
entry=entry,
|
||||
editor=True,
|
||||
)
|
||||
if payload is None or payload.get("content_redacted"):
|
||||
continue
|
||||
exported.append(
|
||||
{
|
||||
"entry": payload,
|
||||
"history": [
|
||||
revision_payload(item)
|
||||
for item in semantic_entry_history(
|
||||
session, principal, entry_id=entry.id
|
||||
)
|
||||
if content_visible_to_principal(item.content, principal)
|
||||
],
|
||||
}
|
||||
)
|
||||
return {
|
||||
"schema_version": "1",
|
||||
"tenant_id": principal.tenant_id,
|
||||
"entries": exported,
|
||||
}
|
||||
|
||||
|
||||
def _mutate(
|
||||
response: Response,
|
||||
session: Session,
|
||||
principal: ApiPrincipal,
|
||||
*,
|
||||
action: str,
|
||||
callback: Callable[[], Any],
|
||||
registry: object,
|
||||
) -> dict[str, object]:
|
||||
_private(response)
|
||||
try:
|
||||
entry = callback()
|
||||
_audit(
|
||||
session,
|
||||
principal,
|
||||
f"docs.semantic.{action}",
|
||||
entry.id,
|
||||
{
|
||||
"revision": entry.current_revision,
|
||||
"subject_stable_key": entry.subject_stable_key,
|
||||
"locale": entry.locale,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
result = semantic_entry_payload(
|
||||
session,
|
||||
registry,
|
||||
principal,
|
||||
entry=entry,
|
||||
editor=True,
|
||||
)
|
||||
if result is None:
|
||||
raise SemanticDocumentationNotFoundError("Entry not found.")
|
||||
return result
|
||||
except (SemanticDocumentationError, SemanticDocumentationContractError) as exc:
|
||||
session.rollback()
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
def _audit(
|
||||
session: Session,
|
||||
principal: ApiPrincipal,
|
||||
action: str,
|
||||
entry_id: str | None,
|
||||
details: dict[str, object],
|
||||
) -> None:
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action=action,
|
||||
object_type="semantic_documentation",
|
||||
object_id=entry_id,
|
||||
details=details,
|
||||
)
|
||||
|
||||
|
||||
def _private(response: Response) -> None:
|
||||
response.headers["Cache-Control"] = "private, no-store"
|
||||
|
||||
|
||||
def _registry(request: Request) -> object:
|
||||
registry = getattr(request.app.state, "govoplan_registry", None)
|
||||
if registry is None or not hasattr(registry, "capability_names"):
|
||||
raise HTTPException(status_code=500, detail="Module registry is unavailable.")
|
||||
return registry
|
||||
|
||||
|
||||
def _has_any(principal: ApiPrincipal, scopes: tuple[str, ...]) -> bool:
|
||||
return any(principal.has(scope) for scope in scopes)
|
||||
|
||||
|
||||
def _http_error(exc: Exception) -> HTTPException:
|
||||
if isinstance(exc, SemanticDocumentationNotFoundError):
|
||||
return HTTPException(status_code=404, detail=str(exc))
|
||||
if isinstance(exc, SemanticDocumentationAuthorizationError):
|
||||
return HTTPException(status_code=403, detail=str(exc))
|
||||
if isinstance(exc, SemanticDocumentationConflictError):
|
||||
return HTTPException(status_code=409, detail=str(exc))
|
||||
return HTTPException(status_code=422, detail=str(exc))
|
||||
|
||||
|
||||
__all__ = ["router"]
|
||||
Reference in New Issue
Block a user