feat: expose connector datasource origins
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from govoplan_core.core.datasources import (
|
||||
DatasourceAccessError,
|
||||
DatasourceField,
|
||||
DatasourceNotFoundError,
|
||||
DatasourceOrigin,
|
||||
DatasourceOriginReadRequest,
|
||||
DatasourceOriginReadResult,
|
||||
DatasourceValidationError,
|
||||
)
|
||||
from govoplan_core.core.tabular_sources import (
|
||||
TabularReadRequest,
|
||||
TabularSource,
|
||||
TabularSourceAccessError,
|
||||
TabularSourceError,
|
||||
TabularSourceNotFoundError,
|
||||
TabularSourceValidationError,
|
||||
)
|
||||
from govoplan_connectors.backend.tabular_sources import SqlTabularSourceProvider
|
||||
|
||||
|
||||
class ConnectorDatasourceOriginProvider:
|
||||
"""Expose connector-owned sources through the Datasources origin contract."""
|
||||
|
||||
def __init__(self, provider: SqlTabularSourceProvider | None = None) -> None:
|
||||
self._provider = provider or SqlTabularSourceProvider()
|
||||
|
||||
def list_origins(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
query: str = "",
|
||||
limit: int = 100,
|
||||
):
|
||||
try:
|
||||
rows = self._provider.list_sources(
|
||||
session,
|
||||
principal,
|
||||
query=query,
|
||||
limit=limit,
|
||||
)
|
||||
except TabularSourceError as exc:
|
||||
raise _datasource_error(exc) from exc
|
||||
return tuple(_origin(source) for source in rows)
|
||||
|
||||
def get_origin(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
origin_ref: str,
|
||||
) -> DatasourceOrigin | None:
|
||||
try:
|
||||
source = self._provider.get_source(
|
||||
session,
|
||||
principal,
|
||||
source_ref=origin_ref,
|
||||
)
|
||||
except TabularSourceError as exc:
|
||||
raise _datasource_error(exc) from exc
|
||||
return _origin(source) if source is not None else None
|
||||
|
||||
def read_origin(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: DatasourceOriginReadRequest,
|
||||
) -> DatasourceOriginReadResult:
|
||||
try:
|
||||
result = self._provider.read_source(
|
||||
session,
|
||||
principal,
|
||||
request=TabularReadRequest(
|
||||
source_ref=request.origin_ref,
|
||||
limit=request.limit,
|
||||
offset=request.offset,
|
||||
columns=request.columns,
|
||||
expected_fingerprint=request.expected_fingerprint,
|
||||
),
|
||||
)
|
||||
except TabularSourceError as exc:
|
||||
raise _datasource_error(exc) from exc
|
||||
return DatasourceOriginReadResult(
|
||||
origin=_origin(result.source),
|
||||
rows=result.rows,
|
||||
total_rows=result.total_rows,
|
||||
truncated=result.truncated,
|
||||
)
|
||||
|
||||
|
||||
def _origin(source: TabularSource) -> DatasourceOrigin:
|
||||
return DatasourceOrigin(
|
||||
ref=source.ref,
|
||||
source_name=source.source_name,
|
||||
name=source.name,
|
||||
description=source.description,
|
||||
kind="upload",
|
||||
shape="tabular",
|
||||
supported_modes=("live", "cached"),
|
||||
provider=f"connectors.{source.provider}",
|
||||
schema=tuple(
|
||||
DatasourceField(
|
||||
name=column.name,
|
||||
data_type=column.data_type,
|
||||
nullable=column.nullable,
|
||||
)
|
||||
for column in source.schema
|
||||
),
|
||||
schema_version=source.schema_version,
|
||||
fingerprint=source.fingerprint,
|
||||
row_count=source.row_count,
|
||||
byte_count=source.byte_count,
|
||||
updated_at=source.updated_at,
|
||||
capabilities=source.capabilities,
|
||||
metadata=dict(source.metadata),
|
||||
)
|
||||
|
||||
|
||||
def _datasource_error(exc: TabularSourceError):
|
||||
if isinstance(exc, TabularSourceAccessError):
|
||||
return DatasourceAccessError(str(exc))
|
||||
if isinstance(exc, TabularSourceNotFoundError):
|
||||
return DatasourceNotFoundError(str(exc))
|
||||
if isinstance(exc, TabularSourceValidationError):
|
||||
return DatasourceValidationError(str(exc))
|
||||
return DatasourceValidationError(str(exc))
|
||||
|
||||
|
||||
__all__ = ["ConnectorDatasourceOriginProvider"]
|
||||
@@ -10,6 +10,7 @@ from govoplan_core.core.module_guards import (
|
||||
drop_table_retirement_provider,
|
||||
persistent_table_uninstall_guard,
|
||||
)
|
||||
from govoplan_core.core.datasources import CAPABILITY_DATASOURCE_ORIGINS
|
||||
from govoplan_core.core.modules import (
|
||||
DocumentationTopic,
|
||||
MigrationSpec,
|
||||
@@ -30,11 +31,15 @@ from govoplan_connectors.backend.tabular_sources import (
|
||||
WRITE_SCOPE,
|
||||
SqlTabularSourceProvider,
|
||||
)
|
||||
from govoplan_connectors.backend.datasource_origins import (
|
||||
ConnectorDatasourceOriginProvider,
|
||||
)
|
||||
|
||||
|
||||
MODULE_ID = "connectors"
|
||||
MODULE_VERSION = "0.1.14"
|
||||
TABULAR_SOURCE_INTERFACE_VERSION = "0.1.0"
|
||||
DATASOURCE_ORIGIN_INTERFACE_VERSION = "0.1.0"
|
||||
|
||||
|
||||
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
|
||||
@@ -95,6 +100,10 @@ def _provider(_context) -> SqlTabularSourceProvider:
|
||||
return SqlTabularSourceProvider()
|
||||
|
||||
|
||||
def _datasource_origin_provider(_context) -> ConnectorDatasourceOriginProvider:
|
||||
return ConnectorDatasourceOriginProvider()
|
||||
|
||||
|
||||
def _tenant_summary(session, tenant_id: str) -> dict[str, int]:
|
||||
return {
|
||||
"connector_tabular_sources": (
|
||||
@@ -126,6 +135,10 @@ manifest = ModuleManifest(
|
||||
name="connectors.tabular_snapshot_writer",
|
||||
version=TABULAR_SOURCE_INTERFACE_VERSION,
|
||||
),
|
||||
ModuleInterfaceProvider(
|
||||
name="connectors.datasource_origins",
|
||||
version=DATASOURCE_ORIGIN_INTERFACE_VERSION,
|
||||
),
|
||||
),
|
||||
permissions=PERMISSIONS,
|
||||
role_templates=ROLE_TEMPLATES,
|
||||
@@ -133,6 +146,7 @@ manifest = ModuleManifest(
|
||||
capability_factories={
|
||||
CAPABILITY_CONNECTORS_TABULAR_SOURCES: _provider,
|
||||
CAPABILITY_CONNECTORS_TABULAR_SNAPSHOT_WRITER: _provider,
|
||||
CAPABILITY_DATASOURCE_ORIGINS: _datasource_origin_provider,
|
||||
},
|
||||
tenant_summary_providers=(_tenant_summary,),
|
||||
migration_spec=MigrationSpec(
|
||||
@@ -164,8 +178,9 @@ manifest = ModuleManifest(
|
||||
"Connectors owns source configuration, access checks, schema discovery, "
|
||||
"fingerprints, and bounded reads. Dataflow stores only opaque source "
|
||||
"references and expected fingerprints. The first executable provider "
|
||||
"imports immutable JSON or CSV snapshots; database and API providers "
|
||||
"can implement the same capability without changing Dataflow."
|
||||
"imports immutable JSON or CSV snapshots and exposes them as Datasource "
|
||||
"origins. Database and API providers can implement the same origin "
|
||||
"contract without changing Datasources or Dataflow."
|
||||
),
|
||||
layer="available",
|
||||
documentation_types=("admin", "user"),
|
||||
@@ -184,6 +199,7 @@ def get_manifest() -> ModuleManifest:
|
||||
__all__ = [
|
||||
"MODULE_ID",
|
||||
"MODULE_VERSION",
|
||||
"DATASOURCE_ORIGIN_INTERFACE_VERSION",
|
||||
"TABULAR_SOURCE_INTERFACE_VERSION",
|
||||
"get_manifest",
|
||||
"manifest",
|
||||
|
||||
@@ -292,7 +292,16 @@ def _context(
|
||||
raise TypeError("Tabular source providers require a SQLAlchemy session.")
|
||||
if not isinstance(principal, ApiPrincipal):
|
||||
raise TabularSourceAccessError("A tenant API principal is required.")
|
||||
if not (has_scope(principal, required_scope) or has_scope(principal, ADMIN_SCOPE)):
|
||||
accepted_scopes = {required_scope, ADMIN_SCOPE}
|
||||
if required_scope == READ_SCOPE:
|
||||
accepted_scopes.update(
|
||||
{
|
||||
"datasources:catalogue:read",
|
||||
"datasources:source:write",
|
||||
"datasources:source:admin",
|
||||
}
|
||||
)
|
||||
if not any(has_scope(principal, scope) for scope in accepted_scopes):
|
||||
raise TabularSourceAccessError(f"Missing scope: {required_scope}")
|
||||
return session, principal
|
||||
|
||||
|
||||
Reference in New Issue
Block a user