feat: complete governed documentation sources
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from dataclasses import dataclass
|
||||
from typing import Annotated, Any, Literal, Mapping
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from govoplan_core.core.modules import (
|
||||
DocumentationCondition,
|
||||
DocumentationLink,
|
||||
DocumentationSourceDefinition,
|
||||
DocumentationType,
|
||||
ModuleManifest,
|
||||
)
|
||||
from govoplan_core.security.redaction import redact_secret_values
|
||||
|
||||
|
||||
class _SourceModel(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class DocumentationSourceProvenance(_SourceModel):
|
||||
source: str
|
||||
version: str | None = None
|
||||
revision: str | None = None
|
||||
published_at: str | None = None
|
||||
checksum: str | None = None
|
||||
|
||||
|
||||
class DocumentationSourceVisibility(_SourceModel):
|
||||
documentation_types: list[DocumentationType]
|
||||
required_modules: list[str] = Field(default_factory=list)
|
||||
any_modules: list[str] = Field(default_factory=list)
|
||||
missing_modules: list[str] = Field(default_factory=list)
|
||||
required_capabilities: list[str] = Field(default_factory=list)
|
||||
required_scopes: list[str] = Field(default_factory=list)
|
||||
any_scopes: list[str] = Field(default_factory=list)
|
||||
configuration_keys: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ManifestInspection(_SourceModel):
|
||||
kind: Literal["manifest"] = "manifest"
|
||||
module_id: str
|
||||
name: str
|
||||
version: str
|
||||
dependencies: list[str]
|
||||
optional_dependencies: list[str]
|
||||
|
||||
|
||||
class RouteInspection(_SourceModel):
|
||||
kind: Literal["route"] = "route"
|
||||
path: str
|
||||
component: str | None = None
|
||||
order: int
|
||||
|
||||
|
||||
class CapabilityInspection(_SourceModel):
|
||||
kind: Literal["capability"] = "capability"
|
||||
capability: str
|
||||
|
||||
|
||||
class PolicyInspection(_SourceModel):
|
||||
kind: Literal["policy"] = "policy"
|
||||
capability: str
|
||||
|
||||
|
||||
class ConfigurationPackageInspection(_SourceModel):
|
||||
kind: Literal["configuration_package"] = "configuration_package"
|
||||
package_id: str
|
||||
schema_version: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class WikiInspection(_SourceModel):
|
||||
kind: Literal["wiki"] = "wiki"
|
||||
href: str
|
||||
published_at: str | None = None
|
||||
revision: str | None = None
|
||||
|
||||
|
||||
class RepositoryInspection(_SourceModel):
|
||||
kind: Literal["repository"] = "repository"
|
||||
href: str
|
||||
revision: str | None = None
|
||||
|
||||
|
||||
DocumentationSourceInspection = Annotated[
|
||||
ManifestInspection
|
||||
| RouteInspection
|
||||
| CapabilityInspection
|
||||
| PolicyInspection
|
||||
| ConfigurationPackageInspection
|
||||
| WikiInspection
|
||||
| RepositoryInspection,
|
||||
Field(discriminator="kind"),
|
||||
]
|
||||
|
||||
|
||||
class DocumentationSourceItem(_SourceModel):
|
||||
id: str
|
||||
kind: str
|
||||
owner_module_id: str
|
||||
label: str
|
||||
state: Literal["configured", "disabled", "unavailable"]
|
||||
state_reason: str | None = None
|
||||
inspection_url: str
|
||||
provenance: DocumentationSourceProvenance
|
||||
visibility: DocumentationSourceVisibility
|
||||
inspection: DocumentationSourceInspection
|
||||
|
||||
def summary(self) -> dict[str, Any]:
|
||||
return self.model_dump(mode="json", exclude={"inspection"})
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RegisteredDocumentationSource:
|
||||
item: DocumentationSourceItem
|
||||
condition: DocumentationCondition
|
||||
documentation_types: tuple[DocumentationType, ...]
|
||||
configuration_key: str | None = None
|
||||
|
||||
|
||||
class DocumentationSourceRegistry:
|
||||
def __init__(self) -> None:
|
||||
self._sources: dict[str, RegisteredDocumentationSource] = {}
|
||||
|
||||
def register(self, source: RegisteredDocumentationSource) -> None:
|
||||
if source.item.id in self._sources:
|
||||
raise ValueError(f"Duplicate documentation source id: {source.item.id}")
|
||||
self._sources[source.item.id] = source
|
||||
|
||||
def sources(self) -> tuple[RegisteredDocumentationSource, ...]:
|
||||
return tuple(
|
||||
self._sources[source_id]
|
||||
for source_id in sorted(self._sources)
|
||||
)
|
||||
|
||||
def get(self, source_id: str) -> RegisteredDocumentationSource | None:
|
||||
return self._sources.get(source_id)
|
||||
|
||||
|
||||
def build_documentation_source_registry(
|
||||
manifests: tuple[ModuleManifest, ...],
|
||||
) -> DocumentationSourceRegistry:
|
||||
registry = DocumentationSourceRegistry()
|
||||
for manifest in manifests:
|
||||
registry.register(_manifest_source(manifest))
|
||||
for source in _route_sources(manifest):
|
||||
registry.register(source)
|
||||
for source in _capability_sources(manifest):
|
||||
registry.register(source)
|
||||
for definition in manifest.documentation_sources:
|
||||
registry.register(_defined_source(manifest, definition))
|
||||
for source in _linked_sources(manifest):
|
||||
if registry.get(source.item.id) is None:
|
||||
registry.register(source)
|
||||
return registry
|
||||
|
||||
|
||||
def _manifest_source(manifest: ModuleManifest) -> RegisteredDocumentationSource:
|
||||
return _registered_source(
|
||||
source_id=f"{manifest.id}.manifest",
|
||||
kind="manifest",
|
||||
owner_module_id=manifest.id,
|
||||
label=f"{manifest.name} module manifest",
|
||||
provenance={"source": "module_manifest", "version": manifest.version},
|
||||
inspection=ManifestInspection(
|
||||
module_id=manifest.id,
|
||||
name=manifest.name,
|
||||
version=manifest.version,
|
||||
dependencies=list(manifest.dependencies),
|
||||
optional_dependencies=list(manifest.optional_dependencies),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _route_sources(manifest: ModuleManifest) -> tuple[RegisteredDocumentationSource, ...]:
|
||||
if manifest.frontend is None:
|
||||
return ()
|
||||
return tuple(
|
||||
_registered_source(
|
||||
source_id=_derived_source_id(manifest.id, "route", route.path),
|
||||
kind="route",
|
||||
owner_module_id=manifest.id,
|
||||
label=f"{manifest.name} route {route.path}",
|
||||
provenance={"source": "frontend_manifest", "version": manifest.version},
|
||||
inspection=RouteInspection(
|
||||
path=route.path,
|
||||
component=route.component,
|
||||
order=route.order,
|
||||
),
|
||||
condition=DocumentationCondition(
|
||||
required_modules=(manifest.id,),
|
||||
required_scopes=route.required_all,
|
||||
any_scopes=route.required_any,
|
||||
),
|
||||
)
|
||||
for route in manifest.frontend.routes
|
||||
)
|
||||
|
||||
|
||||
def _capability_sources(manifest: ModuleManifest) -> tuple[RegisteredDocumentationSource, ...]:
|
||||
sources: list[RegisteredDocumentationSource] = []
|
||||
for capability in sorted(manifest.capability_factories):
|
||||
is_policy = capability.startswith("policy.")
|
||||
kind = "policy" if is_policy else "capability"
|
||||
inspection: DocumentationSourceInspection = (
|
||||
PolicyInspection(capability=capability)
|
||||
if is_policy
|
||||
else CapabilityInspection(capability=capability)
|
||||
)
|
||||
sources.append(_registered_source(
|
||||
source_id=_derived_source_id(manifest.id, kind, capability),
|
||||
kind=kind,
|
||||
owner_module_id=manifest.id,
|
||||
label=f"{manifest.name} {kind} {capability}",
|
||||
provenance={"source": "module_manifest", "version": manifest.version},
|
||||
inspection=inspection,
|
||||
condition=DocumentationCondition(required_modules=(manifest.id,)),
|
||||
))
|
||||
return tuple(sources)
|
||||
|
||||
|
||||
def _defined_source(
|
||||
manifest: ModuleManifest,
|
||||
definition: DocumentationSourceDefinition,
|
||||
) -> RegisteredDocumentationSource:
|
||||
inspection = _defined_inspection(definition)
|
||||
return _registered_source(
|
||||
source_id=definition.id,
|
||||
kind=definition.kind,
|
||||
owner_module_id=manifest.id,
|
||||
label=definition.label,
|
||||
provenance={
|
||||
"source": str(definition.provenance.get("source") or "module_manifest"),
|
||||
**_safe_source_fields(definition.provenance),
|
||||
},
|
||||
inspection=inspection,
|
||||
condition=definition.condition,
|
||||
documentation_types=definition.documentation_types,
|
||||
state=definition.state,
|
||||
configuration_key=definition.configuration_key,
|
||||
)
|
||||
|
||||
|
||||
def _defined_inspection(definition: DocumentationSourceDefinition) -> DocumentationSourceInspection:
|
||||
safe = _safe_source_fields(definition.inspection)
|
||||
if definition.kind == "configuration_package":
|
||||
return ConfigurationPackageInspection(
|
||||
package_id=str(safe.get("package_id") or definition.id),
|
||||
schema_version=_optional_text(safe.get("schema_version")),
|
||||
description=_optional_text(safe.get("description")),
|
||||
)
|
||||
href = definition.link.href if definition.link else str(safe.get("href") or "")
|
||||
if definition.kind == "wiki":
|
||||
return WikiInspection(
|
||||
href=href,
|
||||
published_at=_optional_text(safe.get("published_at")),
|
||||
revision=_optional_text(safe.get("revision")),
|
||||
)
|
||||
if definition.kind == "repository":
|
||||
return RepositoryInspection(
|
||||
href=href,
|
||||
revision=_optional_text(safe.get("revision")),
|
||||
)
|
||||
if definition.kind == "route":
|
||||
return RouteInspection(
|
||||
path=str(safe.get("path") or ""),
|
||||
component=_optional_text(safe.get("component")),
|
||||
order=int(safe.get("order") or 0),
|
||||
)
|
||||
if definition.kind == "policy":
|
||||
return PolicyInspection(capability=str(safe.get("capability") or definition.id))
|
||||
if definition.kind == "capability":
|
||||
return CapabilityInspection(capability=str(safe.get("capability") or definition.id))
|
||||
return ManifestInspection(
|
||||
module_id=str(safe.get("module_id") or definition.id.split(".", 1)[0]),
|
||||
name=str(safe.get("name") or definition.label),
|
||||
version=str(safe.get("version") or ""),
|
||||
dependencies=_string_list(safe.get("dependencies")),
|
||||
optional_dependencies=_string_list(safe.get("optional_dependencies")),
|
||||
)
|
||||
|
||||
|
||||
def _linked_sources(manifest: ModuleManifest) -> tuple[RegisteredDocumentationSource, ...]:
|
||||
sources: list[RegisteredDocumentationSource] = []
|
||||
for topic in manifest.documentation:
|
||||
for link in topic.links:
|
||||
if link.kind not in {"wiki", "repository"}:
|
||||
continue
|
||||
source_id = _derived_source_id(manifest.id, link.kind, link.href)
|
||||
inspection: DocumentationSourceInspection = (
|
||||
WikiInspection(href=link.href)
|
||||
if link.kind == "wiki"
|
||||
else RepositoryInspection(href=link.href)
|
||||
)
|
||||
sources.append(_registered_source(
|
||||
source_id=source_id,
|
||||
kind=link.kind,
|
||||
owner_module_id=manifest.id,
|
||||
label=link.label,
|
||||
provenance={"source": "documentation_link", "version": manifest.version},
|
||||
inspection=inspection,
|
||||
condition=topic.conditions[0] if len(topic.conditions) == 1 else DocumentationCondition(),
|
||||
documentation_types=topic.documentation_types,
|
||||
))
|
||||
return tuple(sources)
|
||||
|
||||
|
||||
def _registered_source(
|
||||
*,
|
||||
source_id: str,
|
||||
kind: str,
|
||||
owner_module_id: str,
|
||||
label: str,
|
||||
provenance: Mapping[str, Any],
|
||||
inspection: DocumentationSourceInspection,
|
||||
condition: DocumentationCondition | None = None,
|
||||
documentation_types: tuple[DocumentationType, ...] = ("admin",),
|
||||
state: Literal["configured", "disabled", "unavailable"] = "configured",
|
||||
configuration_key: str | None = None,
|
||||
) -> RegisteredDocumentationSource:
|
||||
clean_provenance = _safe_source_fields(provenance)
|
||||
return RegisteredDocumentationSource(
|
||||
item=DocumentationSourceItem(
|
||||
id=source_id,
|
||||
kind=kind,
|
||||
owner_module_id=owner_module_id,
|
||||
label=label,
|
||||
state=state,
|
||||
inspection_url=f"/api/v1/docs/sources/{source_id}",
|
||||
provenance=DocumentationSourceProvenance(
|
||||
source=str(clean_provenance.get("source") or "unknown"),
|
||||
version=_optional_text(clean_provenance.get("version")),
|
||||
revision=_optional_text(clean_provenance.get("revision")),
|
||||
published_at=_optional_text(clean_provenance.get("published_at")),
|
||||
checksum=_optional_text(clean_provenance.get("checksum")),
|
||||
),
|
||||
visibility=_visibility_payload(condition or DocumentationCondition(), documentation_types),
|
||||
inspection=inspection,
|
||||
),
|
||||
condition=condition or DocumentationCondition(),
|
||||
documentation_types=documentation_types,
|
||||
configuration_key=configuration_key,
|
||||
)
|
||||
|
||||
|
||||
def _visibility_payload(
|
||||
condition: DocumentationCondition,
|
||||
documentation_types: tuple[DocumentationType, ...],
|
||||
) -> DocumentationSourceVisibility:
|
||||
return DocumentationSourceVisibility(
|
||||
documentation_types=list(documentation_types),
|
||||
required_modules=list(condition.required_modules),
|
||||
any_modules=list(condition.any_modules),
|
||||
missing_modules=list(condition.missing_modules),
|
||||
required_capabilities=list(condition.required_capabilities),
|
||||
required_scopes=list(condition.required_scopes),
|
||||
any_scopes=list(condition.any_scopes),
|
||||
configuration_keys=list(condition.configuration_keys),
|
||||
)
|
||||
|
||||
|
||||
def _derived_source_id(module_id: str, kind: str, value: str) -> str:
|
||||
digest = hashlib.sha256(value.encode("utf-8")).hexdigest()[:16]
|
||||
return f"{module_id}.{kind}.{digest}"
|
||||
|
||||
|
||||
def _safe_source_fields(value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
redacted = redact_secret_values(dict(value))
|
||||
if not isinstance(redacted, Mapping):
|
||||
return {}
|
||||
return {
|
||||
str(key): item
|
||||
for key, item in redacted.items()
|
||||
if isinstance(item, (str, int, float, bool, list, tuple)) or item is None
|
||||
}
|
||||
|
||||
|
||||
def _optional_text(value: object) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
text = str(value).strip()
|
||||
return text[:2_000] if text else None
|
||||
|
||||
|
||||
def _string_list(value: object) -> list[str]:
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return []
|
||||
return [str(item)[:500] for item in value[:100]]
|
||||
Reference in New Issue
Block a user