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", ]