Implement typed template library and rendering

This commit is contained in:
2026-08-02 12:38:38 +02:00
parent 142c3a26f1
commit b65b905b6e
28 changed files with 4385 additions and 104 deletions
@@ -0,0 +1,152 @@
from __future__ import annotations
from collections.abc import Mapping, Sequence
from sqlalchemy.orm import Session
from govoplan_core.auth import ApiPrincipal
from govoplan_core.core.modules import ModuleContext
from govoplan_core.core.templates import (
TemplateCatalogProvider,
TemplateCompatibility,
TemplateRef,
)
from govoplan_templates.backend.rendering import SqlTemplateRenderer
from govoplan_templates.backend.service import (
READ_SCOPE,
compatibility,
get_template,
get_template_revision,
list_templates,
template_ref,
)
class SqlTemplateCatalog(TemplateCatalogProvider):
def list_templates(
self,
session: object,
principal: object,
*,
query: str = "",
usage: str | None = None,
template_type: str | None = None,
locale: str | None = None,
limit: int = 100,
) -> Sequence[TemplateRef]:
sql_session, api_principal = _context(session, principal)
_require_read(api_principal)
rows = list_templates(
sql_session,
api_principal,
query=query,
usage=usage,
template_type=template_type,
locale=locale,
limit=limit,
)
return tuple(
template_ref(
row,
get_template_revision(sql_session, row, published_preferred=True),
read_only=_read_only(api_principal, row.scope_type, row.scope_id),
)
for row in rows
)
def get_template(
self,
session: object,
principal: object,
*,
template_id: str,
revision: int | None = None,
) -> TemplateRef | None:
sql_session, api_principal = _context(session, principal)
_require_read(api_principal)
try:
row = get_template(sql_session, api_principal, template_id)
item_revision = get_template_revision(
sql_session,
row,
revision=revision,
published_preferred=revision is None,
)
except ValueError:
return None
return template_ref(
row,
item_revision,
read_only=_read_only(api_principal, row.scope_type, row.scope_id),
)
def check_compatibility(
self,
session: object,
principal: object,
*,
template_id: str,
revision: int | None = None,
usage: str | None = None,
output_format: str | None = None,
available_fields: Mapping[str, str] | Sequence[str] = (),
) -> TemplateCompatibility:
sql_session, api_principal = _context(session, principal)
_require_read(api_principal)
row = get_template(sql_session, api_principal, template_id)
item_revision = get_template_revision(
sql_session,
row,
revision=revision,
published_preferred=revision is None,
)
return compatibility(
item_revision,
usage=usage,
output_format=output_format,
available_fields=available_fields,
)
def catalog_capability(_context: ModuleContext) -> SqlTemplateCatalog:
return SqlTemplateCatalog()
def renderer_capability(context: ModuleContext) -> SqlTemplateRenderer:
return SqlTemplateRenderer(context.registry)
def _context(session: object, principal: object) -> tuple[Session, ApiPrincipal]:
if not isinstance(session, Session):
raise TypeError("Template catalogue access requires a SQLAlchemy session.")
if not isinstance(principal, ApiPrincipal):
raise TypeError("Template catalogue access requires an API principal.")
return session, principal
def _require_read(principal: ApiPrincipal) -> None:
if not any(
principal.has(scope)
for scope in (
READ_SCOPE,
"templates:template:write",
"templates:template:publish",
"templates:template:admin",
)
):
raise PermissionError(f"Template catalogue access requires {READ_SCOPE}.")
def _read_only(principal: ApiPrincipal, scope_type: str, scope_id: str | None) -> bool:
if principal.has("templates:template:admin") or scope_type == "tenant":
return False
if scope_type == "user":
return scope_id != principal.account_id
return scope_id not in principal.group_ids
__all__ = [
"SqlTemplateCatalog",
"catalog_capability",
"renderer_capability",
]