Add version-aware documentation resolution
This commit is contained in:
@@ -20,6 +20,11 @@ from govoplan_core.core.modules import (
|
||||
user_workflow_scope_condition_issues,
|
||||
)
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
from govoplan_core.core.versioning import (
|
||||
format_version_range,
|
||||
version_satisfies_range,
|
||||
version_tuple,
|
||||
)
|
||||
from govoplan_core.db.session import get_database
|
||||
|
||||
from govoplan_docs.backend.manifest import DOCS_ADMIN_READ_SCOPES, DOCS_READ_SCOPES
|
||||
@@ -42,6 +47,7 @@ def docs_context(
|
||||
request: Request,
|
||||
documentation_type: DocumentationType = Query(default="user", alias="type", pattern="^(admin|user)$"),
|
||||
locale: str | None = Query(default=None, min_length=2, max_length=20),
|
||||
version: str | None = Query(default=None, min_length=1, max_length=40),
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*DOCS_READ_SCOPES)),
|
||||
) -> dict[str, Any]:
|
||||
can_read_admin_documentation = _has_any_scope(principal, DOCS_ADMIN_READ_SCOPES)
|
||||
@@ -51,10 +57,18 @@ def docs_context(
|
||||
detail="Administrative documentation requires documentation-administrator authority",
|
||||
)
|
||||
registry = _registry(request)
|
||||
target_version = version if isinstance(version, str) else None
|
||||
resolved_locale = _preferred_locale(request, locale)
|
||||
route_items = _route_items(registry.manifests(), principal)
|
||||
visible_route_items = [item for item in route_items if item["visible"]]
|
||||
documentation_layers = _documentation_layers(request, registry, principal, documentation_type=documentation_type, locale=resolved_locale)
|
||||
documentation_layers = _documentation_layers(
|
||||
request,
|
||||
registry,
|
||||
principal,
|
||||
documentation_type=documentation_type,
|
||||
locale=resolved_locale,
|
||||
target_version=target_version,
|
||||
)
|
||||
evidence_sources = (
|
||||
_documentation_source_summaries(
|
||||
request,
|
||||
@@ -81,6 +95,10 @@ def docs_context(
|
||||
)
|
||||
topic_groups = _documentation_topic_groups(documentation_layers)
|
||||
return {
|
||||
"versions": _documentation_version_context(
|
||||
registry,
|
||||
target_version=target_version,
|
||||
),
|
||||
"actor": _documentation_actor(
|
||||
principal,
|
||||
documentation_type=documentation_type,
|
||||
@@ -243,6 +261,50 @@ def _documentation_actor(
|
||||
return actor
|
||||
|
||||
|
||||
def _documentation_version_context(
|
||||
registry: PlatformRegistry,
|
||||
*,
|
||||
target_version: str | None,
|
||||
) -> dict[str, Any]:
|
||||
manifests = registry.manifests()
|
||||
installed_versions = {manifest.id: manifest.version for manifest in manifests}
|
||||
supported_versions = sorted(
|
||||
{
|
||||
*(manifest.version for manifest in manifests),
|
||||
*(
|
||||
topic.version_min
|
||||
for manifest in manifests
|
||||
for topic in manifest.documentation
|
||||
if topic.version_min
|
||||
),
|
||||
},
|
||||
key=version_tuple,
|
||||
reverse=True,
|
||||
)
|
||||
latest = supported_versions[0] if supported_versions else None
|
||||
if target_version is None:
|
||||
status_name = "installed"
|
||||
elif target_version == latest:
|
||||
status_name = "stable"
|
||||
elif target_version in supported_versions:
|
||||
status_name = "older_supported"
|
||||
else:
|
||||
status_name = "unsupported"
|
||||
return {
|
||||
"mode": "selected" if target_version else "installed",
|
||||
"selected_version": target_version,
|
||||
"status": status_name,
|
||||
"latest_version": latest,
|
||||
"stable_version": latest,
|
||||
"supported_versions": supported_versions,
|
||||
"installed_versions": installed_versions,
|
||||
"fallback_policy": (
|
||||
"Topics without bounds apply to every version. Bounded topics are "
|
||||
"hidden outside their declared half-open version range."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _documentation_summary(
|
||||
catalog: Mapping[str, list[dict[str, Any]]],
|
||||
*,
|
||||
@@ -471,13 +533,14 @@ def _documentation_layers(
|
||||
*,
|
||||
documentation_type: DocumentationType,
|
||||
locale: str,
|
||||
target_version: str | None = None,
|
||||
) -> dict[str, list[dict[str, Any]]]:
|
||||
settings = _settings(request)
|
||||
try:
|
||||
with get_database().SessionLocal() as session:
|
||||
return _classify_documentation(registry, principal, settings=settings, session=session, documentation_type=documentation_type, locale=locale)
|
||||
return _classify_documentation(registry, principal, settings=settings, session=session, documentation_type=documentation_type, locale=locale, target_version=target_version)
|
||||
except RuntimeError:
|
||||
return _classify_documentation(registry, principal, settings=settings, session=None, documentation_type=documentation_type, locale=locale)
|
||||
return _classify_documentation(registry, principal, settings=settings, session=None, documentation_type=documentation_type, locale=locale, target_version=target_version)
|
||||
|
||||
|
||||
def _settings(request: Request) -> object | None:
|
||||
@@ -628,6 +691,7 @@ def _classify_documentation(
|
||||
session: object | None,
|
||||
documentation_type: DocumentationType,
|
||||
locale: str,
|
||||
target_version: str | None = None,
|
||||
) -> dict[str, list[dict[str, Any]]]:
|
||||
layers: dict[str, list[dict[str, Any]]] = {"always": [], "configured": [], "available": [], "evidence": []}
|
||||
installed = {manifest.id for manifest in registry.manifests()}
|
||||
@@ -646,11 +710,34 @@ def _classify_documentation(
|
||||
session=session,
|
||||
documentation_type=documentation_type,
|
||||
locale=locale,
|
||||
data={
|
||||
"target_version": target_version,
|
||||
"installed_versions": {
|
||||
manifest.id: manifest.version for manifest in registry.manifests()
|
||||
},
|
||||
},
|
||||
)
|
||||
topics = _collect_documentation_topics(
|
||||
registry,
|
||||
principal,
|
||||
settings=settings,
|
||||
session=session,
|
||||
documentation_type=documentation_type,
|
||||
locale=locale,
|
||||
target_version=target_version,
|
||||
)
|
||||
topics = _collect_documentation_topics(registry, principal, settings=settings, session=session, documentation_type=documentation_type, locale=locale)
|
||||
for source_module_id, topic in sorted(topics, key=lambda item: (item[1].order, item[0], item[1].id)):
|
||||
if not _topic_matches_documentation_type(topic, documentation_type):
|
||||
continue
|
||||
module_id = topic.source_module_id or source_module_id
|
||||
manifest = registry.get(module_id)
|
||||
resolved_version = target_version or (manifest.version if manifest else "0")
|
||||
if not version_satisfies_range(
|
||||
resolved_version,
|
||||
version_min=topic.version_min,
|
||||
version_max_exclusive=topic.version_max_exclusive,
|
||||
):
|
||||
continue
|
||||
configuration_keys = _documentation_configuration_keys(topic)
|
||||
configuration = _resolve_documentation_configuration(
|
||||
registry,
|
||||
@@ -686,6 +773,7 @@ def _classify_documentation(
|
||||
documentation_type=documentation_type,
|
||||
visible_runtime_paths=visible_runtime_paths,
|
||||
configuration=configuration,
|
||||
resolved_version=resolved_version,
|
||||
))
|
||||
return layers
|
||||
|
||||
@@ -698,6 +786,7 @@ def _collect_documentation_topics(
|
||||
session: object | None,
|
||||
documentation_type: DocumentationType,
|
||||
locale: str,
|
||||
target_version: str | None = None,
|
||||
) -> list[tuple[str, DocumentationTopic]]:
|
||||
topics: list[tuple[str, DocumentationTopic]] = []
|
||||
for manifest in registry.manifests():
|
||||
@@ -713,7 +802,13 @@ def _collect_documentation_topics(
|
||||
session=session,
|
||||
documentation_type=documentation_type,
|
||||
locale=locale,
|
||||
data={"source_module_id": manifest.id},
|
||||
data={
|
||||
"source_module_id": manifest.id,
|
||||
"target_version": target_version,
|
||||
"installed_versions": {
|
||||
item.id: item.version for item in registry.manifests()
|
||||
},
|
||||
},
|
||||
)
|
||||
for provider in manifest.documentation_providers:
|
||||
try:
|
||||
@@ -1060,6 +1155,7 @@ def _documentation_topic_payload(
|
||||
documentation_type: DocumentationType,
|
||||
visible_runtime_paths: frozenset[str],
|
||||
configuration: Mapping[str, DocumentationConfigurationDecision],
|
||||
resolved_version: str,
|
||||
) -> dict[str, Any]:
|
||||
module_id = topic.source_module_id or source_module_id
|
||||
translation_locale, translation = _translation_for_locale(topic, locale)
|
||||
@@ -1090,6 +1186,21 @@ def _documentation_topic_payload(
|
||||
"i18n_key": topic.i18n_key or topic.id,
|
||||
"locale": locale,
|
||||
"translation_locale": translation_locale,
|
||||
"version": {
|
||||
"resolved": resolved_version,
|
||||
"minimum": topic.version_min,
|
||||
"maximum_exclusive": topic.version_max_exclusive,
|
||||
"range": format_version_range(
|
||||
version_min=topic.version_min,
|
||||
version_max_exclusive=topic.version_max_exclusive,
|
||||
),
|
||||
"fallback": (
|
||||
"unversioned"
|
||||
if topic.version_min is None
|
||||
and topic.version_max_exclusive is None
|
||||
else "matching_range"
|
||||
),
|
||||
},
|
||||
"conditions": [_documentation_condition_payload(condition) for condition in topic.conditions],
|
||||
"links": [_documentation_link_payload(link) for link in topic.links],
|
||||
"related_modules": list(topic.related_modules),
|
||||
|
||||
Reference in New Issue
Block a user