from __future__ import annotations from collections.abc import Mapping, Sequence from urllib.parse import quote from sqlalchemy import func, select from sqlalchemy.orm import Session from govoplan_core.auth import ApiPrincipal from govoplan_core.core.modules import ModuleContext from govoplan_core.core.search import ( SearchAuthorizationRequest, SearchBackfillPage, SearchBackfillRequest, SearchDocument, SearchResourceType, ) from govoplan_datasources.backend.db.models import DatasourceRecord from govoplan_datasources.backend.service import ADMIN_SCOPE, CATALOGUE_READ_SCOPE PROVIDER_ID = "datasources.catalogue" RESOURCE_TYPE = "datasource" class DatasourcesSearchSource: def resource_types(self) -> Sequence[SearchResourceType]: return ( SearchResourceType( provider_id=PROVIDER_ID, module_id="datasources", resource_type=RESOURCE_TYPE, label="Datasources", requires_authorization_recheck=True, ), ) def backfill( self, session: object, *, request: SearchBackfillRequest, ) -> SearchBackfillPage: _assert_source(request.provider_id, request.resource_type) db = _session(session) statement = select(DatasourceRecord).where( DatasourceRecord.tenant_id == request.tenant_id, DatasourceRecord.deleted_at.is_(None), ) if request.cursor: statement = statement.where(DatasourceRecord.id > request.cursor) rows = list( db.scalars( statement.order_by(DatasourceRecord.id).limit(request.limit + 1) ).all() ) has_more = len(rows) > request.limit selected = rows[: request.limit] high_watermark = db.scalar( select(func.max(DatasourceRecord.updated_at)).where( DatasourceRecord.tenant_id == request.tenant_id, DatasourceRecord.deleted_at.is_(None), ) ) return SearchBackfillPage( documents=tuple(_document(row) for row in selected), next_cursor=selected[-1].id if has_more and selected else None, complete=not has_more, high_watermark=high_watermark.isoformat() if high_watermark else None, ) def authorize( self, session: object, principal: object, *, requests: Sequence[SearchAuthorizationRequest], ) -> Mapping[str, bool]: decisions = {item.reference.key: False for item in requests} if not isinstance(principal, ApiPrincipal) or not ( principal.has(CATALOGUE_READ_SCOPE) or principal.has(ADMIN_SCOPE) ): return decisions db = _session(session) eligible = [ request for request in requests if request.reference.tenant_id == principal.tenant_id and request.reference.module_id == "datasources" and request.reference.resource_type == RESOURCE_TYPE ] resource_ids = {request.reference.resource_id for request in eligible} available_ids = ( set( db.scalars( select(DatasourceRecord.id).where( DatasourceRecord.tenant_id == principal.tenant_id, DatasourceRecord.id.in_(resource_ids), DatasourceRecord.deleted_at.is_(None), ) ).all() ) if resource_ids else set() ) for request in eligible: decisions[request.reference.key] = ( request.reference.resource_id in available_ids ) return decisions def create_datasources_search_source( _context: ModuleContext, ) -> DatasourcesSearchSource: return DatasourcesSearchSource() def _document(row: DatasourceRecord) -> SearchDocument: datasource_ref = quote(f"datasource:{row.id}", safe="") return SearchDocument( tenant_id=row.tenant_id, module_id="datasources", provider_id=PROVIDER_ID, resource_type=RESOURCE_TYPE, resource_id=row.id, title=row.name, url=f"/datasources?datasource={datasource_ref}", summary=(row.description or row.source_name)[:4000], keywords=tuple( value[:200] for value in ( row.source_name, row.kind, row.mode, row.shape, row.status, row.classification, row.publication_state, ) if value ), visibility="restricted", acl_tokens=( f"scope:{CATALOGUE_READ_SCOPE}", f"scope:{ADMIN_SCOPE}", ), metadata={ "kind": row.kind, "mode": row.mode, "shape": row.shape, "status": row.status, "classification": row.classification, "publication_state": row.publication_state, "authority_mode": row.authority_mode, }, source_revision=f"{row.schema_version}:{row.updated_at.isoformat()}", source_updated_at=row.updated_at, requires_authorization_recheck=True, ) def _assert_source(provider_id: str, resource_type: str) -> None: if provider_id != PROVIDER_ID or resource_type != RESOURCE_TYPE: raise ValueError("Unsupported Datasources search source.") def _session(value: object) -> Session: if not isinstance(value, Session): raise TypeError("Datasources search requires a SQLAlchemy session.") return value __all__ = [ "DatasourcesSearchSource", "PROVIDER_ID", "RESOURCE_TYPE", "create_datasources_search_source", ]