from __future__ import annotations from collections.abc import Mapping, Sequence from datetime import UTC, date, datetime import hashlib import json from typing import Any import uuid from sqlalchemy.orm import Session from govoplan_core.core.dataflows import ( DataflowDatasetRequest, dataflow_dataset_output, ) from govoplan_core.core.events import ( EventActorRef, EventObjectRef, EventTenantRef, PlatformEvent, emit_platform_event, ) from govoplan_core.security.module_permissions import scopes_grant_compatible from govoplan_core.security.time import utc_now from govoplan_reporting.backend.contracts import ( CAPABILITY_REPORTING_CHART_RENDERER, ReportingChartRenderer, ReportingDatasetReadRequest, ReportingDatasetReadResult, ReportingReadModelProvider, ReportingRowPolicyProvider, ReportingRowPolicyRequest, capability, ) from govoplan_reporting.backend.db.models import ( ReportingExecution, ReportingQualityResult, ) from govoplan_reporting.backend.definitions import get_definition, list_definitions from govoplan_reporting.backend.domain import ReportingDefinitionRecord from govoplan_reporting.backend.governance import require_definition_action from govoplan_reporting.backend.postgres_planner import ( POSTGRES_PLANNER_VERSION, execute_postgres_query, ) from govoplan_reporting.backend.query_engine import ( QUERY_ENGINE_VERSION, DefaultChartRenderer, QueryResult, execute_semantic_query, ) from govoplan_reporting.backend.schemas import ( DatasetDefinition, QualityPlanDefinition, ReportDefinition, ReportQuery, SemanticModelDefinition, ) RUN_SCOPE = "reporting:report:run" QUALITY_SCOPE = "reporting:quality:run" class ReportingExecutionError(ValueError): pass class ReportingExecutionFailure(ReportingExecutionError): def __init__(self, message: str, execution_id: str) -> None: super().__init__(message) self.execution_id = execution_id class SqlReportingRunner: def __init__(self, registry: object | None) -> None: self.registry = registry def execute( self, session: object, principal: object, *, report_id: str, report_revision: int | None = None, parameters: Mapping[str, object] | None = None, query: ReportQuery | None = None, idempotency_key: str, ) -> Mapping[str, object]: return execute_report( _session(session), principal, registry=self.registry, report_id=report_id, report_revision=report_revision, parameters=parameters or {}, query=query, idempotency_key=idempotency_key, ) def get_execution( self, session: object, principal: object, *, execution_id: str, ) -> Mapping[str, object] | None: return get_execution( _session(session), principal, execution_id=execution_id, registry=self.registry, ) def execute_report( session: Session, principal: object, *, registry: object | None, report_id: str, report_revision: int | None, parameters: Mapping[str, object], query: ReportQuery | None, idempotency_key: str, ) -> dict[str, object]: _require_scope(principal, RUN_SCOPE) report_record = get_definition( session, principal, definition_kind="report", definition_id=report_id, revision=report_revision, ) if report_record is None: raise LookupError("Reporting report definition not found.") if report_record.status != "active": raise ReportingExecutionError("Only active report definitions can run.") report = ReportDefinition.model_validate(report_record.payload) report_decision = require_definition_action( session, principal, registry=registry, record=report_record, action="run", ) semantic_record = get_definition( session, principal, definition_kind="semantic_model", definition_id=report.semantic_model_id, revision=report.semantic_model_revision, ) if semantic_record is None or semantic_record.status != "active": raise ReportingExecutionError( "The report's pinned semantic model is unavailable or inactive." ) semantic = SemanticModelDefinition.model_validate(semantic_record.payload) semantic_decision = require_definition_action( session, principal, registry=registry, record=semantic_record, action="view", ) dataset_record = get_definition( session, principal, definition_kind="dataset", definition_id=semantic.dataset_id, revision=semantic.dataset_revision, ) if dataset_record is None or dataset_record.status != "active": raise ReportingExecutionError( "The report's pinned analytical dataset is unavailable or inactive." ) dataset = DatasetDefinition.model_validate(dataset_record.payload) dataset_decision = require_definition_action( session, principal, registry=registry, record=dataset_record, action="view", ) bound_parameters = _bind_parameters(report, parameters) effective_query = _enforce_query_access(report, query or report.default_query) clean_idempotency_key = _required( idempotency_key, "Reporting execution idempotency key", 255, ) request_sha256 = _sha256( { "report_id": report_id, "report_revision": report_record.revision, "semantic_model_id": semantic_record.definition_id, "semantic_model_revision": semantic_record.revision, "dataset_id": dataset_record.definition_id, "dataset_revision": dataset_record.revision, "parameters": bound_parameters, "query": effective_query.model_dump(mode="json"), } ) replay = _execution_replay( session, tenant_id=_tenant(principal), idempotency_key=clean_idempotency_key, request_sha256=request_sha256, ) if replay is not None: delivery = _authorize_execution_delivery( session, principal, registry=registry, row=replay, report_record=report_record, ) return _execution_payload( replay, report=report, registry=registry, delivery_authorization=delivery, ) started_at = utc_now() execution = ReportingExecution( tenant_id=_tenant(principal), execution_id=str(uuid.uuid4()), report_id=report_record.definition_id, report_revision=report_record.revision, semantic_model_id=semantic_record.definition_id, semantic_model_revision=semantic_record.revision, dataset_id=dataset_record.definition_id, dataset_revision=dataset_record.revision, status="running", idempotency_key=clean_idempotency_key, request_sha256=request_sha256, parameters=_json_value(bound_parameters), query=effective_query.model_dump(mode="json"), definition_hashes={ "report": report_record.content_hash, "semantic_model": semantic_record.content_hash, "dataset": dataset_record.content_hash, }, started_at=started_at, actor_id=_actor(principal), ) session.add(execution) session.flush() try: source = _read_dataset( session, principal, registry=registry, dataset=dataset, parameters=bound_parameters, ) diagnostics = list(source.diagnostics) diagnostics.extend(_freshness_diagnostics(dataset, source, now=started_at)) normalized_rows = tuple(_json_value(dict(row)) for row in source.rows) diagnostics.extend(_validate_schema(dataset, normalized_rows)) authorized_rows, policy_provenance = _apply_row_policy( session, principal, registry=registry, dataset_id=dataset_record.definition_id, dataset_revision=dataset_record.revision, dataset=dataset, rows=normalized_rows, ) quality_results = _evaluate_blocking_quality_plans( session, principal, dataset_id=dataset_record.definition_id, dataset_revision=dataset_record.revision, rows=authorized_rows, output_hash=source.output_hash, source_fingerprints=source.source_fingerprints, ) result = execute_postgres_query( session, rows=authorized_rows, dataset=dataset, semantic_model=semantic, query=effective_query, ) or execute_semantic_query(authorized_rows, semantic, effective_query) diagnostics.extend(result.diagnostics) output_hash = _sha256( { "rows": result.rows, "schema": result.schema, "query": effective_query.model_dump(mode="json"), "source_output_hash": source.output_hash, } ) execution.status = "succeeded" execution.source_fingerprints = _json_value(source.source_fingerprints) execution.output_hash = output_hash planner_version = ( POSTGRES_PLANNER_VERSION if any(item.get("code") == "postgresql_semantic_plan" for item in result.diagnostics) else QUERY_ENGINE_VERSION ) execution.executor_version = f"{planner_version}+{source.executor_version}" execution.result_schema = list(result.schema) execution.result_rows = list(result.rows) execution.total_rows = result.total_rows execution.truncated = result.truncated or source.truncated execution.diagnostics = _json_value(diagnostics) execution.provenance = { "source": _json_value(source.provenance), "source_output_hash": source.output_hash, "row_policy": _json_value(policy_provenance), "quality_result_ids": [item.result_id for item in quality_results], "authorized_source_rows": len(authorized_rows), "report_content_hash": report_record.content_hash, "semantic_model_content_hash": semantic_record.content_hash, "dataset_content_hash": dataset_record.content_hash, "definition_governance": { "report": report_decision.to_dict(), "semantic_model": semantic_decision.to_dict(), "dataset": dataset_decision.to_dict(), }, "access_explanation": _access_explanation( report, effective_query, source_rows=len(normalized_rows), authorized_rows=len(authorized_rows), row_policy=policy_provenance, ), } execution.finished_at = utc_now() session.flush() _emit_execution_event(session, execution, report_record.name) delivery = _authorize_execution_delivery( session, principal, registry=registry, row=execution, report_record=report_record, ) return _execution_payload( execution, report=report, registry=registry, delivery_authorization=delivery, ) except Exception as exc: execution.status = "failed" execution.finished_at = utc_now() execution.diagnostics = [ { "severity": "error", "code": "report_execution_failed", "message": str(exc), } ] session.flush() _emit_execution_event(session, execution, report_record.name) raise ReportingExecutionFailure(str(exc), execution.execution_id) from exc def get_execution( session: Session, principal: object, *, execution_id: str, registry: object | None = None, ) -> dict[str, object] | None: row = ( session.query(ReportingExecution) .filter( ReportingExecution.tenant_id == _tenant(principal), ReportingExecution.execution_id == execution_id, ) .one_or_none() ) if row is None: return None report_record = get_definition( session, principal, definition_kind="report", definition_id=row.report_id, revision=row.report_revision, ) if report_record is None: return None delivery = _authorize_execution_delivery( session, principal, registry=registry, row=row, report_record=report_record, ) return _execution_payload( row, report=ReportDefinition.model_validate(report_record.payload), registry=registry, delivery_authorization=delivery, ) def list_executions( session: Session, principal: object, *, report_id: str, limit: int = 100, registry: object | None = None, ) -> tuple[dict[str, object], ...]: current_report = get_definition( session, principal, definition_kind="report", definition_id=report_id, ) if current_report is None: return () rows = ( session.query(ReportingExecution) .filter( ReportingExecution.tenant_id == _tenant(principal), ReportingExecution.report_id == report_id, ) .order_by(ReportingExecution.started_at.desc()) .limit(max(1, min(limit, 200))) .all() ) authorization_cache: dict[tuple[int, int, int], dict[str, object]] = {} payloads: list[dict[str, object]] = [] for row in rows: key = ( row.report_revision, row.semantic_model_revision, row.dataset_revision, ) report_record = ( current_report if current_report.revision == row.report_revision else get_definition( session, principal, definition_kind="report", definition_id=row.report_id, revision=row.report_revision, ) ) if report_record is None: continue delivery = authorization_cache.get(key) if delivery is None: delivery = _authorize_execution_delivery( session, principal, registry=registry, row=row, report_record=report_record, ) authorization_cache[key] = delivery payloads.append( _execution_payload( row, report=ReportDefinition.model_validate(report_record.payload), registry=registry, delivery_authorization=delivery, ) ) return tuple(payloads) def _read_dataset( session: Session, principal: object, *, registry: object | None, dataset: DatasetDefinition, parameters: Mapping[str, object], ) -> ReportingDatasetReadResult: source_parameters = {**dataset.source_parameters, **parameters} if dataset.source_kind == "static": rows = tuple(dict(item) for item in dataset.static_rows) return ReportingDatasetReadResult( rows=rows, total_rows=len(rows), truncated=False, output_hash=_sha256(rows), executor_version="reporting-static-v1", definition_hash=dataset.definition_hash, generated_at=utc_now(), provenance={"source_kind": "static", "source_ref": dataset.source_ref}, ) if dataset.source_kind == "dataflow": provider = dataflow_dataset_output(registry) if provider is None: raise ReportingExecutionError( "The Dataflow dataset provider is not enabled." ) result = provider.read_output( session, principal, request=DataflowDatasetRequest( pipeline_ref=dataset.source_ref, revision=dataset.source_revision or 0, run_ref=dataset.source_run_ref, parameters=source_parameters, row_limit=2_000, expected_definition_hash=dataset.definition_hash, expected_source_fingerprints=tuple( dataset.expected_source_fingerprints ), ), ) return ReportingDatasetReadResult( rows=result.rows, total_rows=result.total_rows, truncated=result.truncated, output_hash=result.output_hash, executor_version=result.executor_version, definition_hash=result.definition_hash, source_fingerprints=result.source_fingerprints, diagnostics=result.diagnostics, generated_at=result.generated_at, provenance=result.provenance, ) provider = capability(registry, dataset.source_ref) if not isinstance(provider, ReportingReadModelProvider): raise ReportingExecutionError( f"Reporting read-model provider {dataset.source_ref!r} is unavailable." ) return provider.read_dataset( session, principal, request=ReportingDatasetReadRequest( source_ref=dataset.source_ref, source_revision=dataset.source_revision, parameters=source_parameters, expected_definition_hash=dataset.definition_hash, expected_source_fingerprints=tuple(dataset.expected_source_fingerprints), ), ) def _apply_row_policy( session: Session, principal: object, *, registry: object | None, dataset_id: str, dataset_revision: int, dataset: DatasetDefinition, rows: tuple[Mapping[str, object], ...], ) -> tuple[tuple[Mapping[str, object], ...], Mapping[str, object]]: if dataset.row_policy_ref is None: return rows, {"mode": "dataset_access_only"} provider = capability(registry, dataset.row_policy_ref) if not isinstance(provider, ReportingRowPolicyProvider): raise ReportingExecutionError( f"Required row-policy provider {dataset.row_policy_ref!r} is unavailable." ) result = provider.authorize_rows( session, principal, request=ReportingRowPolicyRequest( dataset_id=dataset_id, dataset_revision=dataset_revision, policy_ref=dataset.row_policy_ref, rows=rows, ), ) if len(result.rows) > len(rows): raise ReportingExecutionError( "A row-policy provider returned more rows than it received." ) return result.rows, { "mode": "provider", "provider": dataset.row_policy_ref, "decision_ref": result.decision_ref, **dict(result.provenance), } def _freshness_diagnostics( dataset: DatasetDefinition, source: ReportingDatasetReadResult, *, now: datetime, ) -> tuple[dict[str, object], ...]: policy = dataset.freshness if ( policy.require_source_fingerprints and not source.source_fingerprints and dataset.source_kind != "static" ): raise ReportingExecutionError( "The analytical dataset requires source fingerprints, but the provider returned none." ) if policy.max_age_seconds is None: return () generated_at = source.generated_at if generated_at is None: message = "The dataset provider did not report a generation timestamp." if policy.stale_action == "block": raise ReportingExecutionError(message) return ( {"severity": "warning", "code": "freshness_unknown", "message": message}, ) if generated_at.tzinfo is None: generated_at = generated_at.replace(tzinfo=UTC) age_seconds = max(0, int((now - generated_at).total_seconds())) if age_seconds <= policy.max_age_seconds: return () message = ( f"Dataset age {age_seconds}s exceeds the configured " f"{policy.max_age_seconds}s freshness limit." ) if policy.stale_action == "block": raise ReportingExecutionError(message) if policy.stale_action == "warn": return ({"severity": "warning", "code": "dataset_stale", "message": message},) return () def _validate_schema( dataset: DatasetDefinition, rows: Sequence[Mapping[str, object]], ) -> tuple[dict[str, object], ...]: if not dataset.fields: return ( { "severity": "info", "code": "schema_inferred", "message": "Dataset schema is inferred because no explicit fields are pinned.", }, ) errors: list[str] = [] for field in dataset.fields: for index, row in enumerate(rows): value = row.get(field.name) if value is None: if not field.nullable: errors.append(f"row {index + 1}: {field.name} is null") continue if not _matches_type(value, field.type): errors.append( f"row {index + 1}: {field.name} does not match {field.type}" ) if len(errors) >= 20: break if len(errors) >= 20: break if errors: raise ReportingExecutionError( "Analytical dataset schema validation failed: " + "; ".join(errors) ) return () def _evaluate_blocking_quality_plans( session: Session, principal: object, *, dataset_id: str, dataset_revision: int, rows: tuple[Mapping[str, object], ...], output_hash: str, source_fingerprints: Sequence[Mapping[str, object]], ) -> tuple[ReportingQualityResult, ...]: plans, _total = list_definitions( session, principal, definition_kinds=("quality_plan",), statuses=("active",), limit=200, ) relevant = tuple( plan for plan in plans if plan.parent_id == dataset_id and plan.parent_revision == dataset_revision ) results: list[ReportingQualityResult] = [] for plan_record in relevant: plan = QualityPlanDefinition.model_validate(plan_record.payload) result = _record_quality_result( session, principal, plan_id=plan_record.definition_id, plan_revision=plan_record.revision, dataset_id=dataset_id, dataset_revision=dataset_revision, plan=plan, rows=rows, output_hash=output_hash, source_fingerprints=source_fingerprints, ) results.append(result) if plan.block_report_execution and result.status == "failed": raise ReportingExecutionError( f"Quality plan {plan_record.name!r} blocked report execution." ) return tuple(results) def run_quality_plan( session: Session, principal: object, *, registry: object | None, quality_plan_id: str, quality_plan_revision: int | None, parameters: Mapping[str, object], ) -> dict[str, object]: _require_scope(principal, QUALITY_SCOPE) record = get_definition( session, principal, definition_kind="quality_plan", definition_id=quality_plan_id, revision=quality_plan_revision, ) if record is None or record.status != "active": raise LookupError("Active Reporting quality plan not found.") plan = QualityPlanDefinition.model_validate(record.payload) dataset_record = get_definition( session, principal, definition_kind="dataset", definition_id=plan.dataset_id, revision=plan.dataset_revision, ) if dataset_record is None or dataset_record.status != "active": raise ReportingExecutionError("Quality plan dataset is unavailable.") dataset = DatasetDefinition.model_validate(dataset_record.payload) source = _read_dataset( session, principal, registry=registry, dataset=dataset, parameters=parameters, ) rows = tuple(_json_value(dict(item)) for item in source.rows) result = _record_quality_result( session, principal, plan_id=record.definition_id, plan_revision=record.revision, dataset_id=dataset_record.definition_id, dataset_revision=dataset_record.revision, plan=plan, rows=rows, output_hash=source.output_hash, source_fingerprints=source.source_fingerprints, ) return _quality_payload(result) def _record_quality_result( session: Session, principal: object, *, plan_id: str, plan_revision: int, dataset_id: str, dataset_revision: int, plan: QualityPlanDefinition, rows: Sequence[Mapping[str, object]], output_hash: str, source_fingerprints: Sequence[Mapping[str, object]], ) -> ReportingQualityResult: assertions = [_evaluate_assertion(item, rows) for item in plan.assertions] failed = any( not item["passed"] and item["severity"] in {"error", "blocker"} for item in assertions ) warning = any(not item["passed"] for item in assertions) row = ReportingQualityResult( tenant_id=_tenant(principal), result_id=str(uuid.uuid4()), quality_plan_id=plan_id, quality_plan_revision=plan_revision, dataset_id=dataset_id, dataset_revision=dataset_revision, status="failed" if failed else "warning" if warning else "passed", output_hash=output_hash, assertions=assertions, source_fingerprints=_json_value(source_fingerprints), evaluated_at=utc_now(), actor_id=_actor(principal), ) session.add(row) session.flush() return row def _evaluate_assertion( assertion, rows: Sequence[Mapping[str, object]] ) -> dict[str, object]: field = assertion.field config = assertion.config failures = 0 if assertion.kind == "not_null": failures = sum(item.get(field or "") is None for item in rows) elif assertion.kind == "unique": values = [item.get(field or "") for item in rows] failures = len(values) - len({_stable_value(item) for item in values}) elif assertion.kind == "range": minimum = config.get("minimum") maximum = config.get("maximum") failures = sum( value is not None and ( (minimum is not None and value < minimum) or (maximum is not None and value > maximum) ) for value in (item.get(field or "") for item in rows) ) elif assertion.kind == "accepted_values": accepted = {_stable_value(item) for item in config.get("values", [])} failures = sum( _stable_value(item.get(field or "")) not in accepted for item in rows ) elif assertion.kind == "row_count": minimum = int(config.get("minimum", 0)) maximum = int(config.get("maximum", 2_000_000_000)) failures = 0 if minimum <= len(rows) <= maximum else 1 elif assertion.kind == "comparison": other_field = str(config.get("other_field") or "") operator = str(config.get("operator") or "eq") if not field or not other_field or operator not in {"eq", "ne"}: raise ReportingExecutionError("Comparison quality assertion is invalid.") failures = sum( (item.get(field) == item.get(other_field)) != (operator == "eq") for item in rows ) else: raise ReportingExecutionError( f"Unsupported quality assertion: {assertion.kind!r}." ) return { "key": assertion.key, "kind": assertion.kind, "severity": assertion.severity, "passed": failures == 0, "failure_count": failures, "row_count": len(rows), } def _enforce_query_access( report: ReportDefinition, query: ReportQuery, ) -> ReportQuery: policy = report.access_policy hidden_dimensions = _policy_strings(policy, "hidden_dimensions") hidden_measures = _policy_strings(policy, "hidden_measures") requested_dimensions = set(query.dimensions) requested_dimensions.update(item.dimension for item in query.filters) if query.pivot is not None: requested_dimensions.update(query.pivot.rows) requested_dimensions.update(query.pivot.columns) requested_measures = set(query.measures) if query.pivot is not None: requested_measures.update(query.pivot.measures) blocked = (requested_dimensions & hidden_dimensions) | ( requested_measures & hidden_measures ) if blocked: raise PermissionError( "Policy hides requested Reporting fields: " + ", ".join(sorted(blocked)) ) if "run" in _policy_strings(policy, "disabled_actions"): raise PermissionError(_policy_reason(policy, "run")) return query def _access_explanation( report: ReportDefinition, query: ReportQuery, *, source_rows: int, authorized_rows: int, row_policy: Mapping[str, object], ) -> dict[str, object]: policy = report.access_policy hidden_dimensions = sorted(_policy_strings(policy, "hidden_dimensions")) hidden_measures = sorted(_policy_strings(policy, "hidden_measures")) disabled_actions = sorted(_policy_strings(policy, "disabled_actions")) reasons = policy.get("reasons") return { "hidden_dimensions": hidden_dimensions, "hidden_measures": hidden_measures, "hidden_rows": max(0, source_rows - authorized_rows), "disabled_actions": disabled_actions, "reasons": dict(reasons) if isinstance(reasons, Mapping) else {}, "row_policy": dict(row_policy), "effective_query": query.model_dump(mode="json"), } def _authorize_execution_delivery( session: Session, principal: object, *, registry: object | None, row: ReportingExecution, report_record: ReportingDefinitionRecord, ) -> dict[str, object]: report_decision = require_definition_action( session, principal, registry=registry, record=report_record, action="view", ) semantic_record = get_definition( session, principal, definition_kind="semantic_model", definition_id=row.semantic_model_id, revision=row.semantic_model_revision, ) dataset_record = get_definition( session, principal, definition_kind="dataset", definition_id=row.dataset_id, revision=row.dataset_revision, ) if semantic_record is None or dataset_record is None: raise PermissionError( "The source definitions for this report result are no longer accessible." ) semantic_decision = require_definition_action( session, principal, registry=registry, record=semantic_record, action="view", ) dataset_decision = require_definition_action( session, principal, registry=registry, record=dataset_record, action="view", ) dataset = DatasetDefinition.model_validate(dataset_record.payload) _empty, row_policy = _apply_row_policy( session, principal, registry=registry, dataset_id=dataset_record.definition_id, dataset_revision=dataset_record.revision, dataset=dataset, rows=(), ) return { "checked": True, "report": report_decision.to_dict(), "semantic_model": semantic_decision.to_dict(), "dataset": dataset_decision.to_dict(), "row_policy": dict(row_policy), } def _policy_strings(policy: Mapping[str, object], key: str) -> set[str]: raw = policy.get(key, ()) if not isinstance(raw, (list, tuple, set, frozenset)): return set() return {str(item) for item in raw if str(item).strip()} def _policy_reason(policy: Mapping[str, object], action: str) -> str: reasons = policy.get("reasons") if isinstance(reasons, Mapping) and str(reasons.get(action) or "").strip(): return str(reasons[action]) return f"Policy disables the Reporting {action} action." def _execution_payload( row: ReportingExecution, *, report: ReportDefinition | None, registry: object | None, delivery_authorization: Mapping[str, object] | None = None, ) -> dict[str, object]: payload: dict[str, object] = { "execution_id": row.execution_id, "report_id": row.report_id, "report_revision": row.report_revision, "semantic_model_id": row.semantic_model_id, "semantic_model_revision": row.semantic_model_revision, "dataset_id": row.dataset_id, "dataset_revision": row.dataset_revision, "status": row.status, "parameters": dict(row.parameters or {}), "query": dict(row.query or {}), "definition_hashes": dict(row.definition_hashes or {}), "source_fingerprints": list(row.source_fingerprints or []), "output_hash": row.output_hash, "executor_version": row.executor_version, "schema": list(row.result_schema or []), "rows": list(row.result_rows or []), "total_rows": row.total_rows, "truncated": row.truncated, "diagnostics": list(row.diagnostics or []), "provenance": dict(row.provenance or {}), "started_at": _datetime_text(row.started_at), "finished_at": _datetime_text(row.finished_at), "actor_id": row.actor_id, "delivery_authorization": dict(delivery_authorization or {}), } if row.status == "succeeded" and report is not None: result = QueryResult( rows=tuple(dict(item) for item in row.result_rows or []), total_rows=row.total_rows, schema=tuple(dict(item) for item in row.result_schema or []), truncated=row.truncated, ) renderer = capability(registry, CAPABILITY_REPORTING_CHART_RENDERER) if not isinstance(renderer, ReportingChartRenderer): renderer = DefaultChartRenderer() payload["visualization"] = dict( renderer.render(visualization=report.visualization, result=result) ) return payload def _quality_payload(row: ReportingQualityResult) -> dict[str, object]: return { "result_id": row.result_id, "quality_plan_id": row.quality_plan_id, "quality_plan_revision": row.quality_plan_revision, "dataset_id": row.dataset_id, "dataset_revision": row.dataset_revision, "status": row.status, "output_hash": row.output_hash, "assertions": list(row.assertions or []), "source_fingerprints": list(row.source_fingerprints or []), "evaluated_at": _datetime_text(row.evaluated_at), } def _execution_replay( session: Session, *, tenant_id: str, idempotency_key: str, request_sha256: str, ) -> ReportingExecution | None: row = ( session.query(ReportingExecution) .filter( ReportingExecution.tenant_id == tenant_id, ReportingExecution.idempotency_key == idempotency_key, ) .one_or_none() ) if row is not None and row.request_sha256 != request_sha256: raise ReportingExecutionError( "Reporting execution idempotency conflict: the key belongs to another request." ) return row def _bind_parameters( report: ReportDefinition, supplied: Mapping[str, object], ) -> dict[str, object]: definitions = {item.key: item for item in report.parameters} unknown = set(supplied) - set(definitions) if unknown: raise ReportingExecutionError( "Unknown report parameters: " + ", ".join(sorted(unknown)) ) result: dict[str, object] = {} for key, definition in definitions.items(): value = supplied.get(key, definition.default) if value is None and definition.required: raise ReportingExecutionError(f"Report parameter {key!r} is required.") if definition.allowed_values and value not in definition.allowed_values: raise ReportingExecutionError( f"Report parameter {key!r} is outside its allowed values." ) if value is not None: result[key] = value return result def _emit_execution_event( session: Session, execution: ReportingExecution, report_name: str, ) -> None: emit_platform_event( session, PlatformEvent( type=f"reporting.execution.{execution.status}", module_id="reporting", payload={ "execution_id": execution.execution_id, "report_id": execution.report_id, "report_revision": execution.report_revision, "output_hash": execution.output_hash, "total_rows": execution.total_rows, "truncated": execution.truncated, }, occurred_at=execution.finished_at or execution.started_at, actor=EventActorRef(type="account", id=execution.actor_id), tenant=EventTenantRef(id=execution.tenant_id), resource=EventObjectRef( type="report_execution", id=execution.execution_id, label=report_name, ), classification="internal", ), ) def _matches_type(value: object, field_type: str) -> bool: if field_type == "string": return isinstance(value, str) if field_type == "integer": return isinstance(value, int) and not isinstance(value, bool) if field_type == "number": return isinstance(value, (int, float)) and not isinstance(value, bool) if field_type == "boolean": return isinstance(value, bool) if field_type in {"date", "datetime"}: if isinstance(value, (date, datetime)): return True if isinstance(value, str): try: datetime.fromisoformat(value.replace("Z", "+00:00")) return True except ValueError: return False return False if field_type == "json": return isinstance(value, (dict, list, tuple)) return False def _stable_value(value: object) -> str: return json.dumps(_json_value(value), sort_keys=True, separators=(",", ":")) def _required(value: object, label: str, maximum: int) -> str: result = str(value or "").strip() if not result: raise ReportingExecutionError(f"{label} is required.") if len(result) > maximum: raise ReportingExecutionError(f"{label} is limited to {maximum} characters.") return result def _require_scope(principal: object, scope: str) -> None: method = getattr(principal, "has", None) allowed = ( bool(method(scope)) if callable(method) else scopes_grant_compatible( frozenset(getattr(principal, "scopes", ()) or ()), scope ) ) if not allowed: raise PermissionError(f"Missing scope: {scope}") def _tenant(principal: object) -> str: tenant_id = str(getattr(principal, "tenant_id", "") or "").strip() if not tenant_id: raise ReportingExecutionError( "Reporting execution requires a tenant-bound principal." ) return tenant_id def _actor(principal: object) -> str | None: for value in ( getattr(principal, "account_id", None), getattr(principal, "identity_id", None), getattr(principal, "membership_id", None), ): if str(value or "").strip(): return str(value) return None def _session(value: object) -> Session: if not isinstance(value, Session): raise TypeError("Reporting execution requires a SQLAlchemy session.") return value def _sha256(value: object) -> str: return hashlib.sha256( json.dumps( _json_value(value), sort_keys=True, separators=(",", ":"), ensure_ascii=True, ).encode("utf-8") ).hexdigest() def _json_value(value: object) -> Any: if isinstance(value, (date, datetime)): return value.isoformat() if hasattr(value, "model_dump"): return _json_value(value.model_dump(mode="json")) if isinstance(value, Mapping): return {str(key): _json_value(item) for key, item in value.items()} if isinstance(value, (list, tuple)): return [_json_value(item) for item in value] return value def _datetime_text(value: datetime | None) -> str | None: if value is None: return None if value.tzinfo is None: value = value.replace(tzinfo=UTC) return value.isoformat() __all__ = [ "QUALITY_SCOPE", "RUN_SCOPE", "ReportingExecutionError", "ReportingExecutionFailure", "SqlReportingRunner", "execute_report", "get_execution", "list_executions", "run_quality_plan", ]