feat: run pinned pipelines and publish outputs

This commit is contained in:
2026-07-28 13:47:59 +02:00
parent 6ca3058021
commit 6305ef9cef
13 changed files with 1461 additions and 7 deletions

View File

@@ -5,6 +5,18 @@ import unittest
from sqlalchemy import create_engine, func, select
from sqlalchemy.orm import Session, sessionmaker
from govoplan_core.auth import ApiPrincipal
from govoplan_core.core.access import PrincipalRef
from govoplan_core.core.dataflows import (
DataflowPublicationTarget,
DataflowRunRequest,
)
from govoplan_core.core.datasources import (
CAPABILITY_DATASOURCE_PUBLICATION,
DatasourceDescriptor,
DatasourceMaterialization,
DatasourcePublicationResult,
)
from govoplan_core.db.base import Base
from govoplan_dataflow.backend.db.models import (
DataflowPipeline,
@@ -28,6 +40,7 @@ from govoplan_dataflow.backend.service import (
get_pipeline,
list_pipelines,
preview_pipeline,
start_pipeline_run,
update_pipeline,
)
@@ -71,6 +84,61 @@ def sample_graph(*, minimum: int = 10) -> PipelineGraph:
)
def principal(tenant_id: str = "tenant-1") -> ApiPrincipal:
return ApiPrincipal(
principal=PrincipalRef(
account_id="account-1",
membership_id="membership-1",
tenant_id=tenant_id,
scopes=frozenset(),
),
account=object(),
user=object(),
)
class FakePublicationProvider:
def __init__(self) -> None:
self.requests = []
def publish_rows(self, _session, _principal, *, request):
self.requests.append(request)
descriptor = DatasourceDescriptor(
ref=request.target_datasource_ref or "datasource:output-1",
source_name=request.source_name or "existing_output",
name=request.name or "Existing output",
kind="custom",
mode="static",
shape="tabular",
fingerprint="published-fingerprint",
)
return DatasourcePublicationResult(
ref="publication:publication-1",
status="published",
datasource=descriptor,
materialization=DatasourceMaterialization(
ref="materialization:materialization-1",
datasource_ref=descriptor.ref,
revision=1,
state="published",
fingerprint=descriptor.fingerprint,
),
)
class FakeRegistry:
def __init__(self, publication_provider: FakePublicationProvider) -> None:
self.publication_provider = publication_provider
def has_capability(self, name: str) -> bool:
return name == CAPABILITY_DATASOURCE_PUBLICATION
def capability(self, name: str):
if not self.has_capability(name):
raise KeyError(name)
return self.publication_provider
class DataflowServiceTests(unittest.TestCase):
def setUp(self) -> None:
self.engine = create_engine("sqlite:///:memory:")
@@ -263,6 +331,109 @@ class DataflowServiceTests(unittest.TestCase):
self.assertEqual(run.source_fingerprints, response.source_fingerprints)
self.assertEqual("preview.execution", run.diagnostics[-1]["code"])
def test_pinned_run_publishes_once_and_replays_idempotently(self) -> None:
pipeline = self._create()
publication_provider = FakePublicationProvider()
request = DataflowRunRequest(
pipeline_ref=f"pipeline:{pipeline.id}",
revision=1,
idempotency_key="monthly-2026-07",
publication=DataflowPublicationTarget(
name="Monthly result",
source_name="monthly_result",
freeze=True,
frozen_label="July 2026",
),
)
first, first_replayed = start_pipeline_run(
self.session,
tenant_id="tenant-1",
actor_id="user-1",
principal=principal(),
registry=FakeRegistry(publication_provider),
request=request,
)
replay, replayed = start_pipeline_run(
self.session,
tenant_id="tenant-1",
actor_id="user-1",
principal=principal(),
registry=FakeRegistry(publication_provider),
request=request,
)
self.session.commit()
self.assertFalse(first_replayed)
self.assertTrue(replayed)
self.assertEqual(first.id, replay.id)
self.assertEqual("succeeded", first.status)
self.assertEqual("datasource:output-1", first.output_datasource_ref)
self.assertEqual(
"materialization:materialization-1",
first.output_materialization_ref,
)
self.assertEqual(
[{"id": 2, "amount": 15}, {"id": 3, "amount": 25}],
list(publication_provider.requests[0].rows),
)
self.assertEqual(1, len(publication_provider.requests))
def test_run_idempotency_key_rejects_changed_parameters(self) -> None:
pipeline = self._create()
publication_provider = FakePublicationProvider()
registry = FakeRegistry(publication_provider)
start_pipeline_run(
self.session,
tenant_id="tenant-1",
actor_id="user-1",
principal=principal(),
registry=registry,
request=DataflowRunRequest(
pipeline_ref=f"pipeline:{pipeline.id}",
revision=1,
idempotency_key="stable-key",
row_limit=100,
),
)
with self.assertRaises(DataflowConflictError):
start_pipeline_run(
self.session,
tenant_id="tenant-1",
actor_id="user-1",
principal=principal(),
registry=registry,
request=DataflowRunRequest(
pipeline_ref=f"pipeline:{pipeline.id}",
revision=1,
idempotency_key="stable-key",
row_limit=10,
),
)
def test_publication_without_datasources_finishes_as_failed_run(self) -> None:
pipeline = self._create()
run, _ = start_pipeline_run(
self.session,
tenant_id="tenant-1",
actor_id="user-1",
principal=principal(),
registry=None,
request=DataflowRunRequest(
pipeline_ref=f"pipeline:{pipeline.id}",
revision=1,
idempotency_key="missing-publisher",
publication=DataflowPublicationTarget(
name="Output",
source_name="output",
),
),
)
self.assertEqual("failed", run.status)
self.assertIn("Datasources publication capability", run.error)
if __name__ == "__main__":
unittest.main()