culvert 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. culvert/__init__.py +27 -0
  2. culvert-0.1.0.dist-info/METADATA +112 -0
  3. culvert-0.1.0.dist-info/RECORD +134 -0
  4. culvert-0.1.0.dist-info/WHEEL +4 -0
  5. culvert-0.1.0.dist-info/entry_points.txt +10 -0
  6. data_pipeline_contract_tests/__init__.py +28 -0
  7. data_pipeline_contract_tests/blob_store.py +34 -0
  8. data_pipeline_contract_tests/secrets.py +41 -0
  9. data_pipeline_contract_tests/stage_metrics_hook.py +109 -0
  10. data_pipeline_contract_tests/warehouse.py +36 -0
  11. data_pipeline_core/__init__.py +60 -0
  12. data_pipeline_core/audit/__init__.py +5 -0
  13. data_pipeline_core/audit/records.py +34 -0
  14. data_pipeline_core/autoconfig.py +144 -0
  15. data_pipeline_core/contracts/__init__.py +41 -0
  16. data_pipeline_core/contracts/audit.py +37 -0
  17. data_pipeline_core/contracts/blob_store.py +81 -0
  18. data_pipeline_core/contracts/finops.py +33 -0
  19. data_pipeline_core/contracts/governance.py +46 -0
  20. data_pipeline_core/contracts/job_control.py +125 -0
  21. data_pipeline_core/contracts/lineage.py +28 -0
  22. data_pipeline_core/contracts/observability.py +78 -0
  23. data_pipeline_core/contracts/pipeline.py +46 -0
  24. data_pipeline_core/contracts/runtime.py +97 -0
  25. data_pipeline_core/contracts/secrets.py +31 -0
  26. data_pipeline_core/contracts/source.py +60 -0
  27. data_pipeline_core/contracts/stage_metrics.py +100 -0
  28. data_pipeline_core/contracts/warehouse.py +94 -0
  29. data_pipeline_core/dataquality/__init__.py +44 -0
  30. data_pipeline_core/dataquality/data_quality_transform.py +368 -0
  31. data_pipeline_core/dataquality/field_violation.py +48 -0
  32. data_pipeline_core/dataquality/numeric_range.py +54 -0
  33. data_pipeline_core/dataquality/validation_result.py +90 -0
  34. data_pipeline_core/dataquality/violation_kind.py +35 -0
  35. data_pipeline_core/decorators.py +77 -0
  36. data_pipeline_core/finops_api/__init__.py +27 -0
  37. data_pipeline_core/finops_api/budget.py +235 -0
  38. data_pipeline_core/finops_api/labels.py +30 -0
  39. data_pipeline_core/finops_api/models.py +40 -0
  40. data_pipeline_core/governance_api/__init__.py +21 -0
  41. data_pipeline_core/governance_api/classification.py +18 -0
  42. data_pipeline_core/governance_api/masker.py +102 -0
  43. data_pipeline_core/governance_api/pii_masking_governance_policy.py +173 -0
  44. data_pipeline_core/governance_api/policies.py +56 -0
  45. data_pipeline_core/job_control_api/__init__.py +26 -0
  46. data_pipeline_core/job_control_api/models.py +106 -0
  47. data_pipeline_core/job_control_api/types.py +44 -0
  48. data_pipeline_core/lineage/__init__.py +5 -0
  49. data_pipeline_core/lineage/events.py +67 -0
  50. data_pipeline_core/py.typed +0 -0
  51. data_pipeline_core/runtime.py +414 -0
  52. data_pipeline_core/schema/__init__.py +5 -0
  53. data_pipeline_core/schema/entity.py +70 -0
  54. data_pipeline_gcp_bigquery/__init__.py +18 -0
  55. data_pipeline_gcp_bigquery/cost_tracker.py +240 -0
  56. data_pipeline_gcp_bigquery/finops_sink.py +158 -0
  57. data_pipeline_gcp_bigquery/warehouse.py +133 -0
  58. data_pipeline_gcp_gcs/__init__.py +17 -0
  59. data_pipeline_gcp_gcs/blob_store.py +108 -0
  60. data_pipeline_gcp_gcs/cost_tracker.py +249 -0
  61. data_pipeline_gcp_observability/__init__.py +53 -0
  62. data_pipeline_gcp_observability/cloud_monitoring_metrics_hook.py +287 -0
  63. data_pipeline_gcp_observability/cloud_trace_observability_hook.py +236 -0
  64. data_pipeline_gcp_observability/culvert_mdc_populator.py +186 -0
  65. data_pipeline_gcp_observability/data_catalog_lineage_emitter.py +189 -0
  66. data_pipeline_gcp_pubsub/__init__.py +18 -0
  67. data_pipeline_gcp_pubsub/cost_tracker.py +205 -0
  68. data_pipeline_gcp_pubsub/io.py +93 -0
  69. data_pipeline_gcp_secrets/__init__.py +13 -0
  70. data_pipeline_gcp_secrets/secret_manager_provider.py +164 -0
  71. data_pipeline_orchestration/__init__.py +105 -0
  72. data_pipeline_orchestration/_job_control.py +91 -0
  73. data_pipeline_orchestration/callbacks/__init__.py +54 -0
  74. data_pipeline_orchestration/callbacks/dlq.py +174 -0
  75. data_pipeline_orchestration/callbacks/factory.py +128 -0
  76. data_pipeline_orchestration/callbacks/handlers.py +223 -0
  77. data_pipeline_orchestration/callbacks/quarantine.py +100 -0
  78. data_pipeline_orchestration/callbacks/types.py +66 -0
  79. data_pipeline_orchestration/dependency.py +250 -0
  80. data_pipeline_orchestration/factories/__init__.py +91 -0
  81. data_pipeline_orchestration/factories/_dag_builders.py +917 -0
  82. data_pipeline_orchestration/factories/base_dag_factory.py +206 -0
  83. data_pipeline_orchestration/factories/config.py +372 -0
  84. data_pipeline_orchestration/factories/dag_factory.py +146 -0
  85. data_pipeline_orchestration/factories/dag_factory_alias.py +154 -0
  86. data_pipeline_orchestration/factories/validators.py +366 -0
  87. data_pipeline_orchestration/hooks/secrets.py +84 -0
  88. data_pipeline_orchestration/operators/__init__.py +24 -0
  89. data_pipeline_orchestration/operators/dataflow.py +592 -0
  90. data_pipeline_orchestration/routing/__init__.py +54 -0
  91. data_pipeline_orchestration/routing/config.py +70 -0
  92. data_pipeline_orchestration/routing/router.py +167 -0
  93. data_pipeline_orchestration/routing/yaml_selector.py +228 -0
  94. data_pipeline_orchestration/sensors/__init__.py +15 -0
  95. data_pipeline_orchestration/sensors/dataflow.py +90 -0
  96. data_pipeline_orchestration/sensors/pubsub.py +311 -0
  97. data_pipeline_tester/__init__.py +41 -0
  98. data_pipeline_tester/assertions/__init__.py +79 -0
  99. data_pipeline_tester/assertions/beam_assertions.py +88 -0
  100. data_pipeline_tester/assertions/pipeline_assertions.py +106 -0
  101. data_pipeline_tester/assertions/record_assertions.py +109 -0
  102. data_pipeline_tester/base/__init__.py +37 -0
  103. data_pipeline_tester/base/beam_test.py +152 -0
  104. data_pipeline_tester/base/gdw_test.py +1 -0
  105. data_pipeline_tester/base/pipeline_test.py +134 -0
  106. data_pipeline_tester/base/result.py +70 -0
  107. data_pipeline_tester/base/scenario_test.py +107 -0
  108. data_pipeline_tester/base/validation_test.py +143 -0
  109. data_pipeline_tester/bdd/__init__.py +6 -0
  110. data_pipeline_tester/bdd/base.py +38 -0
  111. data_pipeline_tester/bdd/steps/__init__.py +0 -0
  112. data_pipeline_tester/bdd/steps/common_steps.py +18 -0
  113. data_pipeline_tester/bdd/steps/dq_steps.py +51 -0
  114. data_pipeline_tester/bdd/steps/pipeline_steps.py +54 -0
  115. data_pipeline_tester/builders/__init__.py +41 -0
  116. data_pipeline_tester/builders/config_builder.py +128 -0
  117. data_pipeline_tester/builders/pipeline_builder.py +102 -0
  118. data_pipeline_tester/builders/record_builder.py +166 -0
  119. data_pipeline_tester/comparison/__init__.py +16 -0
  120. data_pipeline_tester/comparison/dual_run.py +373 -0
  121. data_pipeline_tester/fixtures/__init__.py +88 -0
  122. data_pipeline_tester/fixtures/beam.py +64 -0
  123. data_pipeline_tester/fixtures/bigquery.py +83 -0
  124. data_pipeline_tester/fixtures/common.py +123 -0
  125. data_pipeline_tester/fixtures/gcs.py +79 -0
  126. data_pipeline_tester/mocks/__init__.py +39 -0
  127. data_pipeline_tester/mocks/bigquery_mock.py +99 -0
  128. data_pipeline_tester/mocks/gcs_mock.py +95 -0
  129. data_pipeline_tester/mocks/pubsub_mock.py +384 -0
  130. data_pipeline_transform/__init__.py +4 -0
  131. data_pipeline_transform/dbt_shared/macros/audit_columns.sql +50 -0
  132. data_pipeline_transform/dbt_shared/macros/data_quality_check.sql +121 -0
  133. data_pipeline_transform/dbt_shared/macros/enrichment.sql +48 -0
  134. data_pipeline_transform/dbt_shared/macros/pii_masking.sql +181 -0
culvert/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ """Culvert — a cloud-agnostic, polyglot data-pipeline framework.
2
+
3
+ One language-neutral contract set (Source, Sink, Transform, Pipeline,
4
+ RuntimeContext, BlobStore, Warehouse, JobControlRepository, ...), realised in
5
+ Java and Python, with cloud specifics behind adapters. This distribution is the
6
+ Python side; the Java twin ships as ``com.enrichmeai.culvert:*`` on Maven
7
+ Central.
8
+
9
+ For 0.1.0 the import packages keep their library names — start at
10
+ ``data_pipeline_core`` for the contracts::
11
+
12
+ from data_pipeline_core import autoconfig
13
+ from data_pipeline_core.contracts.blob_store import BlobStore
14
+
15
+ Install extras for the adapters you need::
16
+
17
+ pip install culvert[gcp] # BigQuery, GCS, Pub/Sub, Secret Manager, observability
18
+ pip install culvert[orchestration] # Airflow-side DAG factory, operators, sensors
19
+ pip install culvert[transform] # dbt runner assets
20
+ pip install culvert[all]
21
+
22
+ Docs and worked examples: https://github.com/enrichmeai/culvert
23
+ """
24
+
25
+ __version__ = "0.1.0"
26
+
27
+ __all__ = ["__version__"]
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: culvert
3
+ Version: 0.1.0
4
+ Summary: Cloud-agnostic, polyglot data-pipeline framework: one contract set, adapters per cloud. Python side of Culvert (Java twin on Maven Central: com.enrichmeai.culvert).
5
+ Project-URL: Homepage, https://github.com/enrichmeai/culvert
6
+ Project-URL: Repository, https://github.com/enrichmeai/culvert
7
+ Project-URL: Issues, https://github.com/enrichmeai/culvert/issues
8
+ Author: Joseph Aruja
9
+ License: MIT
10
+ Keywords: airflow,bigquery,cloud-agnostic,contracts,data-pipeline,dbt,framework,gcp
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: typing-extensions>=4.7
20
+ Provides-Extra: all
21
+ Requires-Dist: dbt-bigquery>=1.5.0; extra == 'all'
22
+ Requires-Dist: google-cloud-bigquery>=3.11.0; extra == 'all'
23
+ Requires-Dist: google-cloud-datacatalog>=3.14.0; extra == 'all'
24
+ Requires-Dist: google-cloud-monitoring>=2.14.0; extra == 'all'
25
+ Requires-Dist: google-cloud-pubsub>=2.18.0; extra == 'all'
26
+ Requires-Dist: google-cloud-secret-manager>=2.16.0; extra == 'all'
27
+ Requires-Dist: google-cloud-storage>=2.10.0; extra == 'all'
28
+ Requires-Dist: opentelemetry-api>=1.20.0; extra == 'all'
29
+ Requires-Dist: pyyaml>=6.0; extra == 'all'
30
+ Provides-Extra: contract-tests
31
+ Requires-Dist: pytest>=7.4; extra == 'contract-tests'
32
+ Provides-Extra: gcp
33
+ Requires-Dist: google-cloud-bigquery>=3.11.0; extra == 'gcp'
34
+ Requires-Dist: google-cloud-datacatalog>=3.14.0; extra == 'gcp'
35
+ Requires-Dist: google-cloud-monitoring>=2.14.0; extra == 'gcp'
36
+ Requires-Dist: google-cloud-pubsub>=2.18.0; extra == 'gcp'
37
+ Requires-Dist: google-cloud-secret-manager>=2.16.0; extra == 'gcp'
38
+ Requires-Dist: google-cloud-storage>=2.10.0; extra == 'gcp'
39
+ Requires-Dist: opentelemetry-api>=1.20.0; extra == 'gcp'
40
+ Provides-Extra: orchestration
41
+ Requires-Dist: google-cloud-bigquery>=3.11.0; extra == 'orchestration'
42
+ Requires-Dist: google-cloud-pubsub>=2.18.0; extra == 'orchestration'
43
+ Requires-Dist: google-cloud-storage>=2.10.0; extra == 'orchestration'
44
+ Requires-Dist: pyyaml>=6.0; extra == 'orchestration'
45
+ Provides-Extra: tester
46
+ Requires-Dist: apache-beam[gcp]==2.56.0; extra == 'tester'
47
+ Requires-Dist: google-cloud-bigquery>=3.11.0; extra == 'tester'
48
+ Requires-Dist: google-cloud-pubsub>=2.18.0; extra == 'tester'
49
+ Requires-Dist: google-cloud-storage>=2.10.0; extra == 'tester'
50
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'tester'
51
+ Requires-Dist: pytest-mock>=3.10.0; extra == 'tester'
52
+ Requires-Dist: pytest>=7.0.0; extra == 'tester'
53
+ Provides-Extra: transform
54
+ Requires-Dist: dbt-bigquery>=1.5.0; extra == 'transform'
55
+ Description-Content-Type: text/markdown
56
+
57
+ # Culvert
58
+
59
+ **A cloud-agnostic, polyglot data-pipeline framework.** One contract set —
60
+ `Source`, `Sink`, `Transform`, `Pipeline`, `RuntimeContext`, `BlobStore`,
61
+ `Warehouse`, `JobControlRepository`, and friends — realised in **Java and
62
+ Python**, with cloud specifics behind adapters. Define the pipeline once
63
+ against contracts; point it at an emulator on your laptop, a dev project, or
64
+ production by wiring, not rewriting.
65
+
66
+ - **Contract-driven** — the [language-neutral spec](https://github.com/enrichmeai/culvert/blob/main/docs/CONTRACT.md)
67
+ is the portability boundary. Business logic depends only on contracts.
68
+ - **Two clouds today** — GCP is the first full implementation; AWS is a real
69
+ (Java) adapter family with the same pipeline proven on both. Azure is on the
70
+ roadmap.
71
+ - **No application framework in the core** — plain Python Protocols +
72
+ entry-point auto-discovery; composes into Beam pipelines, Airflow DAGs, or a
73
+ script on your laptop.
74
+ - **Local-first** — the whole stack runs against emulators with zero cloud
75
+ account; cloud is where you prove and ship, not where you develop.
76
+
77
+ ## Install
78
+
79
+ ```bash
80
+ pip install culvert # core contracts only (no cloud SDKs)
81
+ pip install culvert[gcp] # + BigQuery, GCS, Pub/Sub, Secret Manager, observability
82
+ pip install culvert[orchestration] # + Airflow-side DAG factory, operators, sensors
83
+ pip install culvert[transform] # + dbt integration
84
+ pip install culvert[all]
85
+ ```
86
+
87
+ Python ≥ 3.10. For 0.1.0 the import packages keep their library names — the
88
+ contracts live in `data_pipeline_core`:
89
+
90
+ ```python
91
+ from data_pipeline_core import autoconfig
92
+
93
+ config = autoconfig.discover() # entry-point adapter discovery
94
+ blob_store = config.blob_store() # GcsBlobStore if culvert[gcp] installed
95
+ ```
96
+
97
+ The Java twin ships as `com.enrichmeai.culvert:*` on Maven Central — same
98
+ contracts, Java owns the Beam/Dataflow execution layer; Python owns the
99
+ Airflow runtime and dbt packaging.
100
+
101
+ ## Status
102
+
103
+ `0.1.0` — first public release. Built and validated against a real GCP
104
+ project (Cloud Run, BigQuery, Pub/Sub, event-driven end-to-end) before
105
+ publishing. GCP adapters are production-shaped; the AWS family is Java-side;
106
+ Azure is a roadmap skeleton. Honest limitations are documented per adapter in
107
+ the source.
108
+
109
+ Docs, architecture, worked example deployments, and the engineering story:
110
+ **https://github.com/enrichmeai/culvert**
111
+
112
+ MIT licensed.
@@ -0,0 +1,134 @@
1
+ culvert/__init__.py,sha256=I0wbpJgTo-c0xEif_5i_tfNqklyXf13KH-NSnDNMM2s,1040
2
+ data_pipeline_contract_tests/__init__.py,sha256=VcJjW6mrWX0HO64gozESfO076CZijWI52zTJeHt1sn8,986
3
+ data_pipeline_contract_tests/blob_store.py,sha256=JZUcZs1gt0WULzNUEIHZdCdYyv06rDlx3x1r6ayh9Oo,1073
4
+ data_pipeline_contract_tests/secrets.py,sha256=pzlJnl_-x6VfcHpcAtCJ9OmrkhEmzj46-loNaE46SwI,1354
5
+ data_pipeline_contract_tests/stage_metrics_hook.py,sha256=tCFxrD5BT8nq-NhtNkI9MujVHKSnX0gjwHV1uaKyOVc,4372
6
+ data_pipeline_contract_tests/warehouse.py,sha256=5B10dA9y46oI_qI7-pfch24oBiimVnAx6vjRr0AHDtc,1323
7
+ data_pipeline_core/__init__.py,sha256=ScNCKd2CwkaDL6YOim2BCDylwTaP-jtn9HeOgyRKkYw,2329
8
+ data_pipeline_core/autoconfig.py,sha256=IOmwu11AhuFsy8yn3SOiqDCDBa8h_k3x_JsNV_dOf7A,5406
9
+ data_pipeline_core/decorators.py,sha256=2yvjDcnDWH4w2PxFVecBURpp8kQgfI0BTV5cc9FBv3k,2512
10
+ data_pipeline_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ data_pipeline_core/runtime.py,sha256=-u7ncxGAE2ibh8nJ9mmXMcJ13sBfnJ4dz7XsXSb2GcY,15728
12
+ data_pipeline_core/audit/__init__.py,sha256=OHQCTUlOGMer4IYyr5TRNTV7NR6alPIIodnHKe7BiSo,168
13
+ data_pipeline_core/audit/records.py,sha256=cKwd4x8pgNze7Q9U1Czao2Mu59OsMK-m0srWYRHCkek,997
14
+ data_pipeline_core/contracts/__init__.py,sha256=qs6RUsA95Vdy6vstNiR891-BCquw_22Scy_IjJ3gcLg,1575
15
+ data_pipeline_core/contracts/audit.py,sha256=XCx-HeI1BgGy082gT0lhmPAboaPChSjwMrW-rdD9hlo,1237
16
+ data_pipeline_core/contracts/blob_store.py,sha256=JV_YAxE_WDZihGU95zY-m3CtE-F9xAF-5tpZLcYy9_s,2641
17
+ data_pipeline_core/contracts/finops.py,sha256=ym8QDiI9EA012p5M_sOngSITcdVz-hITJfo1T9Jco5c,1233
18
+ data_pipeline_core/contracts/governance.py,sha256=EkXpuJfOOWqCU1zpCs7ma_bZKAErF2KlW3xQI0F4Dpk,1633
19
+ data_pipeline_core/contracts/job_control.py,sha256=WouoYykydGfjrxDdbRZAfNinQZXhQkPWCam89Di8lTc,4254
20
+ data_pipeline_core/contracts/lineage.py,sha256=994hyvoO_LJZSzEFZD35cmCXAN6gRrZAHuoSq28lcJg,946
21
+ data_pipeline_core/contracts/observability.py,sha256=CWZzaj1BdNPRK-9m91HY-vabIDuRGXkJs7MfNG_Xy80,2481
22
+ data_pipeline_core/contracts/pipeline.py,sha256=3bhCvi9Luw5LspFzVoxNfE6s4hDWmAN5CEP3SQJ0x8s,1369
23
+ data_pipeline_core/contracts/runtime.py,sha256=PVYaOYHClOZdEdyjUAOZS7iKyzdz61EXgwkl78efvxk,4317
24
+ data_pipeline_core/contracts/secrets.py,sha256=ZQkU-leLqGAqPgkKFOuBrb1M7jXBBZLhejBPtXG4jJQ,1143
25
+ data_pipeline_core/contracts/source.py,sha256=1-Ngdd0iyPqHh73cIweP4MErtSWS74JAzujRUZv5xaQ,2037
26
+ data_pipeline_core/contracts/stage_metrics.py,sha256=OXbF5Ie2vgr8eGAhJqgEkwJjgpy1p-OX0C_MEZhm6kY,3900
27
+ data_pipeline_core/contracts/warehouse.py,sha256=StJiDm6ySK1vVxVT9hKTJiN5qEbcADs-eUeq2M5rUdk,3144
28
+ data_pipeline_core/dataquality/__init__.py,sha256=igfv9RvTQJ_nkl5c_Sx4Aw4GEXhVw3clEDU304su400,1507
29
+ data_pipeline_core/dataquality/data_quality_transform.py,sha256=3iookCH7oI-l7_D1Nnb27zxJZ8A0XNZTvPaqWbueqRU,15370
30
+ data_pipeline_core/dataquality/field_violation.py,sha256=eeaRkxsknJ-VuZLX5e9n9K8ri-B30Fq9YNi-fYnYDcQ,1672
31
+ data_pipeline_core/dataquality/numeric_range.py,sha256=cLocX6GvWteo-jU76lEv7080yqufgv8_yjpBdu-4QXY,1862
32
+ data_pipeline_core/dataquality/validation_result.py,sha256=H2m2M3I0w6mspCPTjD4lOGmcXVJUpdAIFeNT60KtQ6s,3124
33
+ data_pipeline_core/dataquality/violation_kind.py,sha256=mzbhQI-igbML5ErF5doR4fhHjk8lxyhfU1Rl4_HQVyY,1220
34
+ data_pipeline_core/finops_api/__init__.py,sha256=LF7h5aDW5mL_E2nPXoxaunId5aGGvKHFfaNPUABIVaw,855
35
+ data_pipeline_core/finops_api/budget.py,sha256=yDrkG4IxOXqFJ8kaG2AkQ_8sR9hSI-HLfy-S5DBe9fw,9170
36
+ data_pipeline_core/finops_api/labels.py,sha256=4TGX-Z4M0vuu3VxtSmaupmhuUSSbGm5PHJf5ERHhsc8,961
37
+ data_pipeline_core/finops_api/models.py,sha256=WUoZvycA3TvIuq6dhTQrjN3KP8pvdC_3H_oJCqGfX4Q,1374
38
+ data_pipeline_core/governance_api/__init__.py,sha256=z-GSVht8drj1ppuIx8qP0KmfwADKkrTKtobwvhnQtnQ,596
39
+ data_pipeline_core/governance_api/classification.py,sha256=R4bXcad0NXTq0MEmE78U7f6OXJuaNwSQbioaEdcNMfA,572
40
+ data_pipeline_core/governance_api/masker.py,sha256=RZurFdESsc2s3cUzT9zHwSkzJEt5jtZQqmszpUn7s3k,3522
41
+ data_pipeline_core/governance_api/pii_masking_governance_policy.py,sha256=epuJ7W7nvvzjGbHy31qqy2GqY11xXWN7QFzF_RRKb4c,6910
42
+ data_pipeline_core/governance_api/policies.py,sha256=qgDvXKB95G3fV1YdX__5YUzT-ZN97tBWKITgtN7jlno,1686
43
+ data_pipeline_core/job_control_api/__init__.py,sha256=0iYej02zSMa4_-1Rr-gbm8II2MfyJesp_9nP8F0m4fQ,561
44
+ data_pipeline_core/job_control_api/models.py,sha256=kMgwW9F9lZswgAdDf9IfbAsHSCiYgrvZsRIxdLBoqTs,3219
45
+ data_pipeline_core/job_control_api/types.py,sha256=sttpoVR3A2r3HcGsU1qPQFse-eGnY36kc9FNmW-HsE4,1086
46
+ data_pipeline_core/lineage/__init__.py,sha256=shMojrPXu2hgJCnmfGlOVTJhMFD2sHEM1CmbEqeimzI,171
47
+ data_pipeline_core/lineage/events.py,sha256=6BZ2bJB_nfCSGIYwdJCpjGukq-3KZL36yDVhVQdzHGc,1982
48
+ data_pipeline_core/schema/__init__.py,sha256=FDFlB6K1yw4VsJ-XjfSwo91b4wiTmI7aNREZ1BPgiPo,178
49
+ data_pipeline_core/schema/entity.py,sha256=Na-fS5GaQC0k5Nz5jKdfYk22cEtVQNdZ3t1Hwh_ZZf0,2866
50
+ data_pipeline_gcp_bigquery/__init__.py,sha256=-fOkVAvRw7Lcy9h999qRR6-K-tRCahhyKE_Gkif4RYE,526
51
+ data_pipeline_gcp_bigquery/cost_tracker.py,sha256=9CuxpNVpsxDJPXMe0jNlJYubOGtijHTdVTecJeLQUno,9366
52
+ data_pipeline_gcp_bigquery/finops_sink.py,sha256=3Z1yt6s6yjOM-vsa5n9ndMjfKS-Zz-9kLVIyVDQCz7I,6327
53
+ data_pipeline_gcp_bigquery/warehouse.py,sha256=3uXuB5wfitpdgcOe3j4osuQ03-0Kk9aWW4Ou1kAyrD4,5076
54
+ data_pipeline_gcp_gcs/__init__.py,sha256=Yzj81NMiv8CslB2qT8DHGSzh1koWKQksyJQ2ccUAx4g,426
55
+ data_pipeline_gcp_gcs/blob_store.py,sha256=Fn5cCf_zQ1heSsK6og2lT5H5WUMUTY_9tHUtlZSOWKA,3918
56
+ data_pipeline_gcp_gcs/cost_tracker.py,sha256=15w6jXJdD6rhGJ8f2YDZONgWNxoSOErFwEGb-Irgyl0,9246
57
+ data_pipeline_gcp_observability/__init__.py,sha256=y4HMRDeGhncSEoJvJxbb6cpxYUFeehFTjOOdX0DaIiA,1783
58
+ data_pipeline_gcp_observability/cloud_monitoring_metrics_hook.py,sha256=ScjYnAESrelqA7TR3J4lqcDu4hli81RMWGzrzyCRmoY,10937
59
+ data_pipeline_gcp_observability/cloud_trace_observability_hook.py,sha256=Tw1RPV4UczeCmsCje185PF3zzXivbpplVXdLEbJ4fvA,8967
60
+ data_pipeline_gcp_observability/culvert_mdc_populator.py,sha256=7M2lv8AuXGujMaKmF_1UvRN8A1hmBiV5jXfud1Gc9Wo,6003
61
+ data_pipeline_gcp_observability/data_catalog_lineage_emitter.py,sha256=8mU583_MC-x0OjysLeEEqB3DyE3UVpSK_joHVeT5aVU,7015
62
+ data_pipeline_gcp_pubsub/__init__.py,sha256=GPtmBp4bo0MEkOjLjJ-1gtYbwCJpzfmH0wYPD-JhZzk,470
63
+ data_pipeline_gcp_pubsub/cost_tracker.py,sha256=NRDJIGp52ToZK47vVDarCL3cVFA-nUUf19KEToL9ME4,7573
64
+ data_pipeline_gcp_pubsub/io.py,sha256=yfndM1HvACzyEhHxURKifPtZjRjVaphOi55TgmdKbw8,3462
65
+ data_pipeline_gcp_secrets/__init__.py,sha256=qFAge9_PFFmmG7671E6pj0WYcoXlhK4V9tresL-CAZQ,389
66
+ data_pipeline_gcp_secrets/secret_manager_provider.py,sha256=6syIkm9rwykJ_DvxtaKMffGtRJledOjKLJyBGNqj6eg,6444
67
+ data_pipeline_orchestration/__init__.py,sha256=JPG-vKX9vN5p27FC6tLPoHImouLwxc96Ukf4edEdcWE,2496
68
+ data_pipeline_orchestration/_job_control.py,sha256=KEG0yES5SXvy7zVnWt6MncS7IXWjRalvvKYCK_1WLv0,3696
69
+ data_pipeline_orchestration/dependency.py,sha256=lH-wyMIlO9FNHE7gRn-YH36ZPcLZdWyRJo25xl_WxqQ,9231
70
+ data_pipeline_orchestration/callbacks/__init__.py,sha256=B6ec6OSY1ovhXZQ5hnjBSgSgH16DiJNZaCnHak4HnPo,961
71
+ data_pipeline_orchestration/callbacks/dlq.py,sha256=VUGq7V_8wKhEieDz3CksJN7ivsKPrEFvq2CbjEw8gXs,5529
72
+ data_pipeline_orchestration/callbacks/factory.py,sha256=pSmoevqtTCABpvQseQLQWVQGCP_kAv43m8wzqlunBrY,3676
73
+ data_pipeline_orchestration/callbacks/handlers.py,sha256=d6m-Pn5j03DBMWpuOHnU39XEvmjt09-4VKNv5OJIjNU,6582
74
+ data_pipeline_orchestration/callbacks/quarantine.py,sha256=CM5xoFzF7iP-qYDCSb2VObqkcfK0LU_linDyaYlaNao,2972
75
+ data_pipeline_orchestration/callbacks/types.py,sha256=6qByCaLPIibhFY3yyYRkcpWNt7HvekG_npBU00QFrKU,1791
76
+ data_pipeline_orchestration/factories/__init__.py,sha256=Cx585Uz0N5dug_ML25K4S1UCbh6PRryHlV2tDnqwPQc,2213
77
+ data_pipeline_orchestration/factories/_dag_builders.py,sha256=4AsspjaWOWhB3_ThSrNFiLoYUgpsAfSoSUYorzNyOGg,38663
78
+ data_pipeline_orchestration/factories/base_dag_factory.py,sha256=XciK6QffNdH6UK5MQ8ZrcEK3tFHIxtaeJh2KOoku1U8,5830
79
+ data_pipeline_orchestration/factories/config.py,sha256=LJHqbEQcI_MmNVwwDhGSUCyMUayWD_uJ6cI2irJjaPg,12737
80
+ data_pipeline_orchestration/factories/dag_factory.py,sha256=NUoK7X5aKsg_6BJxAKhwO9xFVPmF-cTUjyPpxsOXSXU,5987
81
+ data_pipeline_orchestration/factories/dag_factory_alias.py,sha256=ec220Ai0fIdkgwZ0Wukx58ahhR9P7F9iPxaRZLTZEls,5938
82
+ data_pipeline_orchestration/factories/validators.py,sha256=0pOKRXHpVVsNEQ8SfUNMEfpid2Lwn8OOGRLVB-Gy-Rg,12316
83
+ data_pipeline_orchestration/hooks/secrets.py,sha256=UVAfJucwHxs7zfmFll5DbRQ6Sn3SmD8jQteD-4RWtgA,2652
84
+ data_pipeline_orchestration/operators/__init__.py,sha256=51zUKPKdJIqJ_ZVqbERhD8Tcn_vd_ASuNWXPEBOFqoA,454
85
+ data_pipeline_orchestration/operators/dataflow.py,sha256=u4pl0ff7D0WEkWlKmiQGd5crnXxRDxslqxP91jyqtVs,20927
86
+ data_pipeline_orchestration/routing/__init__.py,sha256=SzahekNMWT5xuYlTcjQUbk9zHvqx7afGD-UXvLwsNjE,1393
87
+ data_pipeline_orchestration/routing/config.py,sha256=cWu7W84oXcxYWlDu5GbXo9-FhDamx1q8ip8poBuC32Q,1710
88
+ data_pipeline_orchestration/routing/router.py,sha256=y16wVUCJvQ94th6diqa8ZXBRUeK7R8cp94Gh1RgtG6c,4633
89
+ data_pipeline_orchestration/routing/yaml_selector.py,sha256=xMhuA__MLYVYDNM-jKSWVV7cPll7EjVPPxYZ2gJduFQ,7354
90
+ data_pipeline_orchestration/sensors/__init__.py,sha256=xf91EHczJouGtbskWIZZvhDEq7bNnLTvFlP1GtP3miQ,284
91
+ data_pipeline_orchestration/sensors/dataflow.py,sha256=AVN_F16dNxjtuOY0HVYKBDsYc4OzmCM6UHjCFFMzz4s,3091
92
+ data_pipeline_orchestration/sensors/pubsub.py,sha256=RJVEmCFfh05hXdE6vrlyqa1xJ1bO2R2vkHC5sPy0C8k,11470
93
+ data_pipeline_tester/__init__.py,sha256=NaopYycE680DfS3M3qeJD7AS6lx-bwo2K6KJagZnsmA,966
94
+ data_pipeline_tester/assertions/__init__.py,sha256=tDUPUiQJ3_mSPsFkkaIf6khiBMkzgUFz0ZLi2IWcHeU,1975
95
+ data_pipeline_tester/assertions/beam_assertions.py,sha256=UkHrizI07kt2yCiHcujyM33fviKMtTRwQ6WGyQAPtyc,2443
96
+ data_pipeline_tester/assertions/pipeline_assertions.py,sha256=zjkAaLqqXZDj7FxbmPOJStzyZajp_YcdLEq_yYIn-b0,3016
97
+ data_pipeline_tester/assertions/record_assertions.py,sha256=YgTOeXIg0Cl7rSVafQumCs4i2Aixi6LBYFv1HDGXYks,2886
98
+ data_pipeline_tester/base/__init__.py,sha256=rCkw2DUZKT5dLPP61fjiMWbFDTX60LJ7AiVve6OGH5g,1207
99
+ data_pipeline_tester/base/beam_test.py,sha256=G5f7XuHFawQiodas1cYWYPmBQqHJY5CUqT_jDSge9js,4999
100
+ data_pipeline_tester/base/gdw_test.py,sha256=DILKFeGoVm_G5gOsN5bOvlI9ILjiwfcGKHp_X2_FHCk,66
101
+ data_pipeline_tester/base/pipeline_test.py,sha256=IqCCqsgmEVRYZruYKsZAylQYiRcf7mF4zFs8A2rCwdM,4119
102
+ data_pipeline_tester/base/result.py,sha256=O2kbl9k--V3mF6vWqYoCU-1HLAV0guddo4Nxbb8T--M,1695
103
+ data_pipeline_tester/base/scenario_test.py,sha256=vKKjw9RhMwpa8ERjNtGeQ3Ldak6nnnN8vQAOWOVPrvw,3175
104
+ data_pipeline_tester/base/validation_test.py,sha256=DsP7-rm-KEEdXUzEu4HrZ5UBB2Hjpkp5SXXNdthANp4,4706
105
+ data_pipeline_tester/bdd/__init__.py,sha256=rT6bDPHNEAR2uPO802KCZ3efQNtCFii6f-GMRsUQveg,124
106
+ data_pipeline_tester/bdd/base.py,sha256=7gp82365icc1qqSisV367Ht_4u6SIx_xtTWJbp3od50,1042
107
+ data_pipeline_tester/bdd/steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
108
+ data_pipeline_tester/bdd/steps/common_steps.py,sha256=aPXY3VcHaSm4EIRGxPNoO8bYPNf8zHzr--O8tuHUJDk,642
109
+ data_pipeline_tester/bdd/steps/dq_steps.py,sha256=T12OnBj_uQrojAMq4DjLN2n2n_Y12Nblp0eNt2fEvvg,1936
110
+ data_pipeline_tester/bdd/steps/pipeline_steps.py,sha256=BR1d950ydJwGQOnmGxZFc10U6hxKlnt4wJkiMYlPksM,2113
111
+ data_pipeline_tester/builders/__init__.py,sha256=WQiGMw41iH2S1nIUles4-CATqSs3Aj6qwlnM2pBecng,1141
112
+ data_pipeline_tester/builders/config_builder.py,sha256=CIVrGodzrQuUiAp9kWvn-1ti9D0Rwj_j946L16ronhc,3804
113
+ data_pipeline_tester/builders/pipeline_builder.py,sha256=ohVzeOXiNQeZTsrwuZP3LYWIgjRbfVcfl5XZ0zOIFHU,2579
114
+ data_pipeline_tester/builders/record_builder.py,sha256=LWy_klIox7ggAlL9ZKJiJCZULVqOdYvlt-69d8Kfjds,4000
115
+ data_pipeline_tester/comparison/__init__.py,sha256=HWQawx5sIpW8DstAsk-tIJ2O2BowAVjAYpq58LTAE0g,238
116
+ data_pipeline_tester/comparison/dual_run.py,sha256=R_lvryr3RFu5R3FxklHZo7X7fkVzV6900Avp8g6la7E,12155
117
+ data_pipeline_tester/fixtures/__init__.py,sha256=Z_jUwDI3JEeBWQcHDDkzheBYYhOVajAvJcbByzJ2e3Q,2194
118
+ data_pipeline_tester/fixtures/beam.py,sha256=5ySAIl2ONwgJSooR7D-31INswDLucMeF4rg28jPvzQc,1438
119
+ data_pipeline_tester/fixtures/bigquery.py,sha256=3ef6mC2rsCyIfHGMZj1_EzGIHC3AzjkxWziTNP0MSfw,2127
120
+ data_pipeline_tester/fixtures/common.py,sha256=FeVIWbdu6tI6Mry02uYIG8NFAljNG81F5GYUY6bhAio,3125
121
+ data_pipeline_tester/fixtures/gcs.py,sha256=oZnSXDz_5D035YcOt6j1fUn8F3FTO8AVnKQh-K6De2Q,1650
122
+ data_pipeline_tester/mocks/__init__.py,sha256=jM9LwxLCTB4vyMOZ7gMr2zL0bJ-TMen1mUuglZ7v1tw,1097
123
+ data_pipeline_tester/mocks/bigquery_mock.py,sha256=9i4LtDnkdVU3OGCEAQwNJO66AgEC1J61xIMcvV76iiE,2639
124
+ data_pipeline_tester/mocks/gcs_mock.py,sha256=bOG2EUXlvHUBYDMfdVDqa3vPW8i6VCEM_4A7kSywZso,2558
125
+ data_pipeline_tester/mocks/pubsub_mock.py,sha256=mAfZ08ypGlRJWch6rwva5IKgCgqcLMFDA3YmbgsnOQk,11839
126
+ data_pipeline_transform/__init__.py,sha256=ltAvCHCTw0ylJorp_yI363VF7xsxr6fNyGFSm-Nx87g,61
127
+ data_pipeline_transform/dbt_shared/macros/audit_columns.sql,sha256=KilvtCY65KmQV_me4qKPV5-591pjt1S7ZKGmAz1anAg,1468
128
+ data_pipeline_transform/dbt_shared/macros/data_quality_check.sql,sha256=7_uO9kNfIFc4ocM0OzT-gGdTLU55Qw0N-MZndx1LVAg,4456
129
+ data_pipeline_transform/dbt_shared/macros/enrichment.sql,sha256=EjXAnM0SSkSs2RqCR1FP4g7YE3hFhsiVgxiD7pJxS8s,2414
130
+ data_pipeline_transform/dbt_shared/macros/pii_masking.sql,sha256=MGmtrLdYiVfZJFdBx69dkeubapmOUMoxYEJEkMzxX9c,6155
131
+ culvert-0.1.0.dist-info/METADATA,sha256=WAB1KLIcu0DDS0zLbkKhfkSi2KiymWi3zTdbAnXd0Fg,5331
132
+ culvert-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
133
+ culvert-0.1.0.dist-info/entry_points.txt,sha256=BRGRVTNRy5mXOv8VdZckmGKMm4W34s9E0lw_5n8kvIM,557
134
+ culvert-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,10 @@
1
+ [data_pipeline_core.adapters]
2
+ blob_store = data_pipeline_gcp_gcs:GcsBlobStore
3
+ finops = data_pipeline_gcp_bigquery:BigQueryFinOpsSink
4
+ lineage = data_pipeline_gcp_observability:DataCatalogLineageEmitter
5
+ observability = data_pipeline_gcp_observability:CloudTraceObservabilityHook
6
+ secrets = data_pipeline_gcp_secrets:SecretManagerProvider
7
+ sink = data_pipeline_gcp_pubsub:PubSubSink
8
+ source = data_pipeline_gcp_pubsub:PubSubSource
9
+ stage_metrics = data_pipeline_gcp_observability:CloudMonitoringMetricsHook
10
+ warehouse = data_pipeline_gcp_bigquery:BigQueryWarehouse
@@ -0,0 +1,28 @@
1
+ """Abstract pytest contract tests for the Culvert data pipeline framework.
2
+
3
+ Each cloud adapter library inherits the relevant mixin and supplies the
4
+ adapter under test. The mixin tests prove the adapter honours the
5
+ Protocol's documented behaviour.
6
+
7
+ Mirror of the Java ``data-pipeline-contract-tests`` library.
8
+
9
+ Sprint-5 deliverable. Sprint-17 (T17.3): added
10
+ :class:`StageMetricsHookContract` and brought existing mixins to 1:1
11
+ method-level parity with their Java counterparts (null-rejection tests).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from data_pipeline_contract_tests.blob_store import BlobStoreContract
17
+ from data_pipeline_contract_tests.secrets import SecretProviderContract
18
+ from data_pipeline_contract_tests.stage_metrics_hook import StageMetricsHookContract
19
+ from data_pipeline_contract_tests.warehouse import WarehouseContract
20
+
21
+ __version__ = "0.1.0"
22
+
23
+ __all__ = [
24
+ "BlobStoreContract",
25
+ "SecretProviderContract",
26
+ "StageMetricsHookContract",
27
+ "WarehouseContract",
28
+ ]
@@ -0,0 +1,34 @@
1
+ """BlobStore contract test mixin.
2
+
3
+ Java mirror: ``com.enrichmeai.culvert.contracttests.BlobStoreContractTest``
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import pytest
9
+
10
+
11
+ class BlobStoreContract:
12
+ """Mixin — subclasses provide ``store``, ``known_uri``, ``missing_uri``
13
+ fixtures. ``known_uri`` resolves to bytes equal to ``b"hello"``.
14
+
15
+ Java mirror: ``BlobStoreContractTest`` (Sprint-5 deliverable).
16
+ """
17
+
18
+ def test_get_known_returns_bytes(self, store, known_uri):
19
+ assert store.get(known_uri) == b"hello"
20
+
21
+ def test_exists_known_true(self, store, known_uri):
22
+ assert store.exists(known_uri) is True
23
+
24
+ def test_exists_missing_false(self, store, missing_uri):
25
+ assert store.exists(missing_uri) is False
26
+
27
+ def test_delete_missing_idempotent(self, store, missing_uri):
28
+ # Should not raise.
29
+ store.delete(missing_uri)
30
+
31
+ def test_null_arguments_rejected(self, store):
32
+ # Java: ``nullArgumentsRejected`` — get(null) must raise.
33
+ with pytest.raises((TypeError, ValueError)):
34
+ store.get(None)
@@ -0,0 +1,41 @@
1
+ """SecretProvider contract test mixin.
2
+
3
+ Java mirror: ``com.enrichmeai.culvert.contracttests.SecretProviderContractTest``
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import pytest
9
+
10
+
11
+ class SecretProviderContract:
12
+ """Mixin — subclasses provide ``self.provider`` via a pytest fixture
13
+ named ``provider`` (and configure it so ``get("known")`` returns
14
+ ``"the-secret"`` and ``get("missing")`` raises ``KeyError``).
15
+
16
+ Java mirror: ``SecretProviderContractTest`` (Sprint-5 deliverable).
17
+
18
+ Example wiring in a cloud adapter's test sources:
19
+
20
+ .. code-block:: python
21
+
22
+ from data_pipeline_contract_tests import SecretProviderContract
23
+
24
+ class TestMySecretProvider(SecretProviderContract):
25
+ @pytest.fixture
26
+ def provider(self):
27
+ client = make_mock_client({"known": "the-secret"})
28
+ return MySecretProvider("my-project", client)
29
+ """
30
+
31
+ def test_get_known_returns_value(self, provider):
32
+ assert provider.get("known") == "the-secret"
33
+
34
+ def test_get_missing_raises(self, provider):
35
+ with pytest.raises((KeyError, LookupError)):
36
+ provider.get("missing")
37
+
38
+ def test_null_name_rejected(self, provider):
39
+ # Java: ``nullNameRejected`` — get(null, ...) must raise.
40
+ with pytest.raises((TypeError, ValueError)):
41
+ provider.get(None)
@@ -0,0 +1,109 @@
1
+ """StageMetricsHook contract test mixin.
2
+
3
+ Java mirror: no ``Abstract*ContractTest`` class exists on the Java side —
4
+ this mixin is derived directly from the ``StageMetricsHook`` interface contract
5
+ and its accompanying ``StageMetrics`` record
6
+ (``com.enrichmeai.culvert.contracts.StageMetricsHook``,
7
+ ``com.enrichmeai.culvert.contracts.StageMetrics``, Sprint-12 / issue #65).
8
+
9
+ The defining guarantee of ``StageMetricsHook`` is:
10
+ "Implementations must not propagate monitoring-backend failures to the
11
+ caller. If the backend is unavailable the implementation logs and
12
+ swallows the exception so the pipeline continues uninterrupted."
13
+
14
+ That guarantee is the primary behavioral assertion this mixin checks.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import pytest
20
+
21
+ from data_pipeline_core.contracts.stage_metrics import StageMetrics
22
+
23
+
24
+ def _valid_metrics(**overrides) -> StageMetrics:
25
+ """Build a minimal valid :class:`StageMetrics` snapshot."""
26
+ defaults = dict(
27
+ pipeline_id="pipe-1",
28
+ run_id="run-abc",
29
+ stage_name="my-stage",
30
+ rows_processed=100,
31
+ stage_latency_ms=42.5,
32
+ error_count=0,
33
+ )
34
+ defaults.update(overrides)
35
+ return StageMetrics(**defaults)
36
+
37
+
38
+ class StageMetricsHookContract:
39
+ """Mixin — subclasses provide two pytest fixtures:
40
+
41
+ - ``hook`` — the :class:`~data_pipeline_core.contracts.stage_metrics.StageMetricsHook`
42
+ implementation under test (happy-path instance).
43
+ - ``failing_hook`` — the same implementation wired to a backend that
44
+ raises an exception on every ``record_stage_metrics`` call, used to
45
+ verify the swallow-and-continue guarantee.
46
+
47
+ Java mirror: no ``AbstractStageMetricsHookContractTest`` exists in
48
+ ``data-pipeline-contract-tests-java``; guarantees are derived directly
49
+ from the interface contract in ``StageMetricsHook.java`` (Sprint-12
50
+ / issue #65).
51
+
52
+ Example wiring:
53
+
54
+ .. code-block:: python
55
+
56
+ from data_pipeline_contract_tests import StageMetricsHookContract
57
+ from data_pipeline_core.contracts.stage_metrics import StageMetrics
58
+ from unittest.mock import MagicMock
59
+
60
+ class TestMyHook(StageMetricsHookContract):
61
+ @pytest.fixture
62
+ def hook(self):
63
+ client = make_mock_monitoring_client()
64
+ return MyCloudMetricsHook(client)
65
+
66
+ @pytest.fixture
67
+ def failing_hook(self):
68
+ client = MagicMock()
69
+ client.create_time_series.side_effect = RuntimeError("backend down")
70
+ return MyCloudMetricsHook(client)
71
+ """
72
+
73
+ def test_record_stage_metrics_returns_none(self, hook):
74
+ """Happy-path: ``record_stage_metrics`` with a valid snapshot returns
75
+ ``None`` (i.e. does not raise and has no meaningful return value).
76
+ """
77
+ result = hook.record_stage_metrics(_valid_metrics())
78
+ assert result is None
79
+
80
+ def test_record_stage_metrics_all_fields(self, hook):
81
+ """Happy-path: all three label fields and all three metric fields are
82
+ accepted, including edge values (zero rows, zero errors, non-zero latency).
83
+ """
84
+ hook.record_stage_metrics(
85
+ _valid_metrics(rows_processed=0, stage_latency_ms=0.0, error_count=0)
86
+ )
87
+
88
+ def test_record_stage_metrics_with_errors(self, hook):
89
+ """Happy-path: non-zero ``error_count`` is accepted without raising."""
90
+ hook.record_stage_metrics(_valid_metrics(error_count=5))
91
+
92
+ def test_backend_failure_is_swallowed(self, failing_hook):
93
+ """Core contract guarantee: if the monitoring backend is unavailable
94
+ the implementation must NOT propagate the exception to the caller.
95
+ The pipeline continues uninterrupted.
96
+
97
+ This is the primary behavioral invariant stated in both
98
+ ``StageMetricsHook.java`` and the Python ``StageMetricsHook`` Protocol.
99
+ """
100
+ # Must not raise even though the backend raises internally.
101
+ failing_hook.record_stage_metrics(_valid_metrics())
102
+
103
+ def test_null_metrics_rejected(self, hook):
104
+ """``record_stage_metrics(None)`` must raise ``TypeError`` or
105
+ ``ValueError`` — implementations should not silently accept ``None``
106
+ in place of a ``StageMetrics`` snapshot.
107
+ """
108
+ with pytest.raises((TypeError, ValueError)):
109
+ hook.record_stage_metrics(None)
@@ -0,0 +1,36 @@
1
+ """Warehouse contract test mixin.
2
+
3
+ Java mirror: ``com.enrichmeai.culvert.contracttests.WarehouseContractTest``
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import pytest
9
+
10
+
11
+ class WarehouseContract:
12
+ """Mixin — subclasses provide ``warehouse`` fixture configured so that
13
+ ``query("SELECT id FROM contract_test_table")`` yields ``{"id": 1}``,
14
+ ``table_exists("contract_test_table")`` is True, and
15
+ ``table_exists("contract_missing_table")`` is False.
16
+
17
+ Java mirror: ``WarehouseContractTest`` (Sprint-5 deliverable; T15.4
18
+ added ``known_table``/``missing_table`` hook — Python subclasses
19
+ override the fixture to supply qualified names if needed).
20
+ """
21
+
22
+ def test_query_streams_rows(self, warehouse):
23
+ rows = list(warehouse.query("SELECT id FROM contract_test_table"))
24
+ assert len(rows) >= 1
25
+ assert "id" in rows[0]
26
+
27
+ def test_table_exists_known_true(self, warehouse):
28
+ assert warehouse.table_exists("contract_test_table") is True
29
+
30
+ def test_table_exists_missing_false(self, warehouse):
31
+ assert warehouse.table_exists("contract_missing_table") is False
32
+
33
+ def test_null_sql_rejected(self, warehouse):
34
+ # Java: ``nullSqlRejected`` — query(null, ...) must raise.
35
+ with pytest.raises((TypeError, ValueError)):
36
+ warehouse.query(None)
@@ -0,0 +1,60 @@
1
+ """data-pipeline-core: the cloud-neutral contracts for the Culvert framework.
2
+
3
+ This package contains only Protocols, dataclasses, enums, and TypedDicts.
4
+ Zero dependencies on google.cloud, boto3, or azure. Cloud-specific
5
+ implementations of the Protocols live in sibling distributions
6
+ (`data-pipeline-gcp-*`, future `data-pipeline-aws-*`, `data-pipeline-azure-*`).
7
+
8
+ The public surface is the `contracts` package plus the supporting types.
9
+ Most users will import the Protocols from the top level:
10
+
11
+ from data_pipeline_core import (
12
+ Source, Sink, Transform, Pipeline, PipelineStage, RuntimeContext,
13
+ JobControlRepository, BlobStore, Warehouse,
14
+ AuditEventPublisher, GovernancePolicy, LineageEmitter,
15
+ ObservabilityHook, FinOpsSink, SecretProvider,
16
+ StageMetrics, StageMetricsHook,
17
+ )
18
+ """
19
+
20
+ from data_pipeline_core.contracts.audit import AuditEventPublisher
21
+ from data_pipeline_core.contracts.blob_store import BlobStore
22
+ from data_pipeline_core.contracts.finops import FinOpsSink
23
+ from data_pipeline_core.contracts.governance import GovernancePolicy
24
+ from data_pipeline_core.contracts.job_control import JobControlRepository
25
+ from data_pipeline_core.contracts.lineage import LineageEmitter
26
+ from data_pipeline_core.contracts.observability import ObservabilityHook
27
+ from data_pipeline_core.contracts.pipeline import Pipeline, PipelineStage
28
+ from data_pipeline_core.contracts.runtime import RuntimeContext
29
+ from data_pipeline_core.contracts.secrets import SecretProvider
30
+ from data_pipeline_core.contracts.source import Sink, Source, Transform
31
+ from data_pipeline_core.contracts.stage_metrics import StageMetrics, StageMetricsHook
32
+ from data_pipeline_core.contracts.warehouse import Warehouse
33
+
34
+ __version__ = "0.1.0"
35
+
36
+ __all__ = [
37
+ "__version__",
38
+ # Source / Sink / Transform — the I/O primitives
39
+ "Source",
40
+ "Sink",
41
+ "Transform",
42
+ # Pipeline composition
43
+ "Pipeline",
44
+ "PipelineStage",
45
+ # The DI container / run metadata carrier
46
+ "RuntimeContext",
47
+ # Cloud-pluggable contracts
48
+ "JobControlRepository",
49
+ "BlobStore",
50
+ "Warehouse",
51
+ "AuditEventPublisher",
52
+ "GovernancePolicy",
53
+ "LineageEmitter",
54
+ "ObservabilityHook",
55
+ "FinOpsSink",
56
+ "SecretProvider",
57
+ # Typed per-stage metrics (Sprint-12 / T17.1)
58
+ "StageMetrics",
59
+ "StageMetricsHook",
60
+ ]
@@ -0,0 +1,5 @@
1
+ """Audit records — the cloud-neutral data shape an AuditEventPublisher emits."""
2
+
3
+ from data_pipeline_core.audit.records import AuditRecord
4
+
5
+ __all__ = ["AuditRecord"]
@@ -0,0 +1,34 @@
1
+ """The AuditRecord dataclass — the unit of audit emission.
2
+
3
+ Mirrors the existing `gcp_pipeline_core.audit.records.AuditRecord` shape
4
+ verbatim (eleven fields). Stage 1 will collapse the two into one.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime
11
+ from typing import Any, Dict
12
+
13
+
14
+ @dataclass
15
+ class AuditRecord:
16
+ """A single audit event emitted at a pipeline boundary (e.g. an
17
+ ingestion stage completing).
18
+
19
+ `audit_hash` is a deterministic content hash that downstream
20
+ reconciliation uses to dedupe replays. `metadata` is the catch-all
21
+ for stage-specific context (table identifiers, partition keys, etc.)
22
+ """
23
+
24
+ run_id: str
25
+ pipeline_name: str
26
+ entity_type: str
27
+ source_file: str
28
+ record_count: int
29
+ processed_timestamp: datetime
30
+ processing_duration_seconds: float
31
+ success: bool
32
+ error_count: int = 0
33
+ audit_hash: str = ""
34
+ metadata: Dict[str, Any] = field(default_factory=dict)