airflow-dq 0.2.0__tar.gz

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 (152) hide show
  1. airflow_dq-0.2.0/.claude/launch.json +11 -0
  2. airflow_dq-0.2.0/.env.example +83 -0
  3. airflow_dq-0.2.0/.gitattributes +7 -0
  4. airflow_dq-0.2.0/.github/ISSUE_TEMPLATE/bug_report.md +22 -0
  5. airflow_dq-0.2.0/.github/ISSUE_TEMPLATE/feature_request.md +14 -0
  6. airflow_dq-0.2.0/.github/workflows/ci.yml +84 -0
  7. airflow_dq-0.2.0/.github/workflows/release.yml +57 -0
  8. airflow_dq-0.2.0/.gitignore +38 -0
  9. airflow_dq-0.2.0/CHANGELOG.md +51 -0
  10. airflow_dq-0.2.0/CODE_OF_CONDUCT.md +9 -0
  11. airflow_dq-0.2.0/CONTRIBUTING.md +47 -0
  12. airflow_dq-0.2.0/DESIGN.md +151 -0
  13. airflow_dq-0.2.0/LICENSE +202 -0
  14. airflow_dq-0.2.0/Makefile +75 -0
  15. airflow_dq-0.2.0/NOTICE +16 -0
  16. airflow_dq-0.2.0/PKG-INFO +294 -0
  17. airflow_dq-0.2.0/README.md +244 -0
  18. airflow_dq-0.2.0/SECURITY.md +28 -0
  19. airflow_dq-0.2.0/contracts/orders/orders.v3.yaml +48 -0
  20. airflow_dq-0.2.0/contracts/orders/orders.v5.yaml +23 -0
  21. airflow_dq-0.2.0/contracts/orders/orders.v6.yaml +62 -0
  22. airflow_dq-0.2.0/contracts/payments/payments.v1.yaml +69 -0
  23. airflow_dq-0.2.0/contracts/web_events/web_events.v1.yaml +67 -0
  24. airflow_dq-0.2.0/dags/demo_contract_5.py +52 -0
  25. airflow_dq-0.2.0/dags/demo_medallion.py +52 -0
  26. airflow_dq-0.2.0/dags/dq_assets.py +14 -0
  27. airflow_dq-0.2.0/dags/dq_monitor.py +29 -0
  28. airflow_dq-0.2.0/dags/examples/01_quickstart_gate.py +85 -0
  29. airflow_dq-0.2.0/dags/examples/02_quarantine_triage.py +122 -0
  30. airflow_dq-0.2.0/dags/examples/03_incremental_and_freshness.py +131 -0
  31. airflow_dq-0.2.0/dags/examples/04_sla_and_anomaly.py +130 -0
  32. airflow_dq-0.2.0/dags/examples/05_monitor_on_asset.py +122 -0
  33. airflow_dq-0.2.0/docker-compose.wheel.yaml +39 -0
  34. airflow_dq-0.2.0/docker-compose.yaml +78 -0
  35. airflow_dq-0.2.0/docs/FEATURE-INVENTORY.md +213 -0
  36. airflow_dq-0.2.0/docs/IMPLEMENTATION-SPEC.md +151 -0
  37. airflow_dq-0.2.0/docs/PROJECT_DESCRIPTION.md +183 -0
  38. airflow_dq-0.2.0/docs/REFACTOR-ROADMAP.md +127 -0
  39. airflow_dq-0.2.0/docs/medium-article.md +230 -0
  40. airflow_dq-0.2.0/examples/README.md +110 -0
  41. airflow_dq-0.2.0/examples/dbt_schema.yml +33 -0
  42. airflow_dq-0.2.0/pyproject.toml +105 -0
  43. airflow_dq-0.2.0/scripts/integration_test.sh +75 -0
  44. airflow_dq-0.2.0/scripts/odcs_lint.sh +34 -0
  45. airflow_dq-0.2.0/seeds/dirty_orders.sql +19 -0
  46. airflow_dq-0.2.0/src/airflow_dq/__init__.py +13 -0
  47. airflow_dq-0.2.0/src/airflow_dq/alerting/__init__.py +25 -0
  48. airflow_dq-0.2.0/src/airflow_dq/alerting/channels.py +138 -0
  49. airflow_dq-0.2.0/src/airflow_dq/alerting/notifier.py +128 -0
  50. airflow_dq-0.2.0/src/airflow_dq/alerting/routing.py +65 -0
  51. airflow_dq-0.2.0/src/airflow_dq/authoring.py +440 -0
  52. airflow_dq-0.2.0/src/airflow_dq/compiler/__init__.py +6 -0
  53. airflow_dq-0.2.0/src/airflow_dq/compiler/compiler.py +231 -0
  54. airflow_dq-0.2.0/src/airflow_dq/compiler/specs.py +55 -0
  55. airflow_dq-0.2.0/src/airflow_dq/config.py +103 -0
  56. airflow_dq-0.2.0/src/airflow_dq/contracts/__init__.py +13 -0
  57. airflow_dq-0.2.0/src/airflow_dq/contracts/dbt_import.py +151 -0
  58. airflow_dq-0.2.0/src/airflow_dq/contracts/git_flow.py +109 -0
  59. airflow_dq-0.2.0/src/airflow_dq/contracts/models.py +288 -0
  60. airflow_dq-0.2.0/src/airflow_dq/contracts/registry.py +148 -0
  61. airflow_dq-0.2.0/src/airflow_dq/contracts/store.py +75 -0
  62. airflow_dq-0.2.0/src/airflow_dq/contracts/versioning.py +196 -0
  63. airflow_dq-0.2.0/src/airflow_dq/db.py +41 -0
  64. airflow_dq-0.2.0/src/airflow_dq/executors/__init__.py +15 -0
  65. airflow_dq-0.2.0/src/airflow_dq/executors/_evaluate.py +247 -0
  66. airflow_dq-0.2.0/src/airflow_dq/executors/_sa.py +96 -0
  67. airflow_dq-0.2.0/src/airflow_dq/executors/base.py +114 -0
  68. airflow_dq-0.2.0/src/airflow_dq/executors/bigquery.py +24 -0
  69. airflow_dq-0.2.0/src/airflow_dq/executors/connections.py +206 -0
  70. airflow_dq-0.2.0/src/airflow_dq/executors/databricks.py +21 -0
  71. airflow_dq-0.2.0/src/airflow_dq/executors/dialects.py +173 -0
  72. airflow_dq-0.2.0/src/airflow_dq/executors/duckdb.py +96 -0
  73. airflow_dq-0.2.0/src/airflow_dq/executors/postgres.py +14 -0
  74. airflow_dq-0.2.0/src/airflow_dq/executors/quarantine.py +84 -0
  75. airflow_dq-0.2.0/src/airflow_dq/executors/schema_check.py +63 -0
  76. airflow_dq-0.2.0/src/airflow_dq/executors/snowflake.py +21 -0
  77. airflow_dq-0.2.0/src/airflow_dq/executors/sql.py +330 -0
  78. airflow_dq-0.2.0/src/airflow_dq/executors/trino.py +20 -0
  79. airflow_dq-0.2.0/src/airflow_dq/lineage.py +205 -0
  80. airflow_dq-0.2.0/src/airflow_dq/operators/__init__.py +6 -0
  81. airflow_dq-0.2.0/src/airflow_dq/operators/check_operator.py +267 -0
  82. airflow_dq-0.2.0/src/airflow_dq/operators/factory.py +92 -0
  83. airflow_dq-0.2.0/src/airflow_dq/plugin/__init__.py +50 -0
  84. airflow_dq-0.2.0/src/airflow_dq/plugin/api/__init__.py +0 -0
  85. airflow_dq-0.2.0/src/airflow_dq/plugin/api/app.py +263 -0
  86. airflow_dq-0.2.0/src/airflow_dq/plugin/auth.py +202 -0
  87. airflow_dq-0.2.0/src/airflow_dq/py.typed +0 -0
  88. airflow_dq-0.2.0/src/airflow_dq/results/__init__.py +6 -0
  89. airflow_dq-0.2.0/src/airflow_dq/results/migrations/0001_baseline.sql +69 -0
  90. airflow_dq-0.2.0/src/airflow_dq/results/migrations/0002_wave1.sql +68 -0
  91. airflow_dq-0.2.0/src/airflow_dq/results/migrator.py +72 -0
  92. airflow_dq-0.2.0/src/airflow_dq/results/schema.sql +69 -0
  93. airflow_dq-0.2.0/src/airflow_dq/results/store.py +717 -0
  94. airflow_dq-0.2.0/tests/__init__.py +0 -0
  95. airflow_dq-0.2.0/tests/data/odcs-json-schema-v3.1.0.json +2929 -0
  96. airflow_dq-0.2.0/tests/test_alerting.py +44 -0
  97. airflow_dq-0.2.0/tests/test_alerting_v2.py +301 -0
  98. airflow_dq-0.2.0/tests/test_anomaly_baseline.py +99 -0
  99. airflow_dq-0.2.0/tests/test_api_auth.py +152 -0
  100. airflow_dq-0.2.0/tests/test_api_routes.py +273 -0
  101. airflow_dq-0.2.0/tests/test_authoring.py +176 -0
  102. airflow_dq-0.2.0/tests/test_compiler.py +34 -0
  103. airflow_dq-0.2.0/tests/test_compiler_sla.py +131 -0
  104. airflow_dq-0.2.0/tests/test_contracts_authoring_save.py +181 -0
  105. airflow_dq-0.2.0/tests/test_contracts_git_flow.py +102 -0
  106. airflow_dq-0.2.0/tests/test_contracts_models.py +141 -0
  107. airflow_dq-0.2.0/tests/test_contracts_registry.py +91 -0
  108. airflow_dq-0.2.0/tests/test_contracts_store_v2.py +64 -0
  109. airflow_dq-0.2.0/tests/test_contracts_versioning_diff.py +185 -0
  110. airflow_dq-0.2.0/tests/test_dbt_import.py +155 -0
  111. airflow_dq-0.2.0/tests/test_executor.py +397 -0
  112. airflow_dq-0.2.0/tests/test_executor_adapters.py +73 -0
  113. airflow_dq-0.2.0/tests/test_executor_connections.py +186 -0
  114. airflow_dq-0.2.0/tests/test_executor_dialects.py +140 -0
  115. airflow_dq-0.2.0/tests/test_executor_quarantine.py +140 -0
  116. airflow_dq-0.2.0/tests/test_executor_registry.py +97 -0
  117. airflow_dq-0.2.0/tests/test_freshness_semantics.py +274 -0
  118. airflow_dq-0.2.0/tests/test_lineage.py +50 -0
  119. airflow_dq-0.2.0/tests/test_odcs_compliance.py +93 -0
  120. airflow_dq-0.2.0/tests/test_operator_execute.py +396 -0
  121. airflow_dq-0.2.0/tests/test_results_migrator.py +129 -0
  122. airflow_dq-0.2.0/tests/test_results_store_wave1.py +352 -0
  123. airflow_dq-0.2.0/tests/test_store_triage.py +199 -0
  124. airflow_dq-0.2.0/tests/test_versioning.py +61 -0
  125. airflow_dq-0.2.0/ui/README.md +44 -0
  126. airflow_dq-0.2.0/ui/dist/assets/index-C3lrIcGb.js +107 -0
  127. airflow_dq-0.2.0/ui/dist/index.html +12 -0
  128. airflow_dq-0.2.0/ui/index.html +12 -0
  129. airflow_dq-0.2.0/ui/package-lock.json +4294 -0
  130. airflow_dq-0.2.0/ui/package.json +35 -0
  131. airflow_dq-0.2.0/ui/src/api.ts +277 -0
  132. airflow_dq-0.2.0/ui/src/components/NavTabs.tsx +46 -0
  133. airflow_dq-0.2.0/ui/src/components/SampleTable.tsx +51 -0
  134. airflow_dq-0.2.0/ui/src/components/StateBadge.tsx +50 -0
  135. airflow_dq-0.2.0/ui/src/components/StatsCard.tsx +28 -0
  136. airflow_dq-0.2.0/ui/src/components/ui/Alert.tsx +24 -0
  137. airflow_dq-0.2.0/ui/src/components/ui/Checkbox.tsx +15 -0
  138. airflow_dq-0.2.0/ui/src/components/ui/ProgressBar.tsx +15 -0
  139. airflow_dq-0.2.0/ui/src/components/ui/Select.tsx +42 -0
  140. airflow_dq-0.2.0/ui/src/context/colorMode.tsx +21 -0
  141. airflow_dq-0.2.0/ui/src/dashboard/Dashboard.tsx +470 -0
  142. airflow_dq-0.2.0/ui/src/dashboard/DimensionChart.tsx +62 -0
  143. airflow_dq-0.2.0/ui/src/dashboard/TrendChart.tsx +91 -0
  144. airflow_dq-0.2.0/ui/src/editor/Editor.tsx +996 -0
  145. airflow_dq-0.2.0/ui/src/main.tsx +67 -0
  146. airflow_dq-0.2.0/ui/src/overview/Overview.tsx +147 -0
  147. airflow_dq-0.2.0/ui/src/quarantine/Quarantine.tsx +336 -0
  148. airflow_dq-0.2.0/ui/src/theme.ts +390 -0
  149. airflow_dq-0.2.0/ui/src/vite-env.d.ts +1 -0
  150. airflow_dq-0.2.0/ui/tsconfig.json +20 -0
  151. airflow_dq-0.2.0/ui/tsconfig.node.json +10 -0
  152. airflow_dq-0.2.0/ui/vite.config.ts +15 -0
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": "0.0.1",
3
+ "configurations": [
4
+ {
5
+ "name": "dq-ui",
6
+ "runtimeExecutable": "npm",
7
+ "runtimeArgs": ["run", "dev", "--prefix", "/Users/fabianschneider/projects/claude_projects/airflow-dq/ui"],
8
+ "port": 5173
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,83 @@
1
+ # Copy to .env and adjust. Every DQ_* knob the framework reads (src/airflow_dq/config.py)
2
+ # is listed here with its default. NOTE: docker-compose.yaml forwards only DQ_API_TOKEN,
3
+ # DQ_ALERT_WEBHOOK, DQ_ALERT_ROUTES and DQ_BASE_URL into the Airflow container — the rest
4
+ # apply when you run the package outside the demo stack (or extend the compose env block).
5
+
6
+ # --- docker-compose only -------------------------------------------------------------
7
+ # Airflow image used by the demo stack.
8
+ AIRFLOW_IMAGE_NAME=apache/airflow:3.1.0
9
+
10
+ # --- storage ---------------------------------------------------------------------------
11
+ # SQLAlchemy DSN of the DQ results/registry store (its own schema `dq`, NEVER Airflow's
12
+ # metadata DB). Default: the compose demo Postgres.
13
+ # DQ_RESULTS_DSN=postgresql+psycopg2://dq:dq@dq-postgres:5432/dq
14
+
15
+ # Fallback warehouse DSN for `postgres*` backends when no Airflow Connection with
16
+ # conn_id == backend exists. Default: the compose demo Postgres.
17
+ # DQ_DWH_DSN=postgresql+psycopg2://dq:dq@dq-postgres:5432/dq
18
+
19
+ # DuckDB database path for `duckdb*` backends (":memory:" = throwaway in-process DB).
20
+ # DQ_DUCKDB_PATH=:memory:
21
+
22
+ # Directory holding the versioned contract YAML files (must be shared between the
23
+ # API server and every worker).
24
+ # DQ_CONTRACTS_DIR=contracts
25
+
26
+ # --- experimental warehouse adapters (DSN fallbacks; Airflow Connections preferred) ----
27
+ # DQ_SNOWFLAKE_DSN=snowflake://user:pass@account/db/schema
28
+ # DQ_BIGQUERY_DSN=bigquery://project/dataset
29
+ # DQ_DATABRICKS_DSN=databricks://token:...@host?http_path=...
30
+ # DQ_TRINO_DSN=trino://user@host:8080/catalog/schema
31
+
32
+ # --- plugin / API ----------------------------------------------------------------------
33
+ # Legacy shared secret for machine clients (CI, curl). Unset = Airflow auth-manager
34
+ # JWT / session-cookie auth only. Grants read AND write.
35
+ DQ_API_TOKEN=change-me-in-prod
36
+
37
+ # Override for the built React bundle location (rarely needed: wheel installs resolve
38
+ # the packaged airflow_dq/ui_dist, editable installs the repo's ui/dist).
39
+ # DQ_UI_DIST=/opt/airflow/ui/dist
40
+
41
+ # Absolute base URL used to build clickable links in alerts,
42
+ # e.g. https://airflow.example.com (empty = relative links).
43
+ # DQ_BASE_URL=
44
+
45
+ # --- execution -------------------------------------------------------------------------
46
+ # Per-statement timeout applied to check/preview/profile queries (seconds).
47
+ # DQ_STATEMENT_TIMEOUT_S=300
48
+ # Max bad rows captured as the failure sample per check.
49
+ # DQ_SAMPLE_LIMIT=10
50
+ # Max rows copied into dq.dq_quarantine per failing check.
51
+ # DQ_QUARANTINE_LIMIT=1000
52
+
53
+ # --- alerting --------------------------------------------------------------------------
54
+ # Default alert channel for failing/erroring checks. Channels are selected by URL scheme:
55
+ # https:// = Slack-compatible webhook, mailto:<to>, pagerduty://<routing_key>,
56
+ # opsgenie://<api_key>.
57
+ # DQ_ALERT_WEBHOOK=https://hooks.slack.com/services/XXX/YYY/ZZZ
58
+
59
+ # Per-owner routes (JSON) take precedence over the default. Values are either a channel
60
+ # URL or a {"fail": url, "error": url} object for severity-aware routing.
61
+ # DQ_ALERT_ROUTES={"data-platform@example.com": "https://hooks.slack.com/services/A/B/C"}
62
+
63
+ # SMTP settings for mailto: alert routes.
64
+ # DQ_SMTP_HOST=
65
+ # DQ_SMTP_PORT=587
66
+ # DQ_SMTP_FROM=
67
+ # DQ_SMTP_USER=
68
+ # DQ_SMTP_PASSWORD=
69
+
70
+ # --- lineage ---------------------------------------------------------------------------
71
+ # Explicit OpenLineage dataset namespace override. Default: derived from the executor's
72
+ # connection (e.g. postgres://host:5432), falling back to "airflow-dq".
73
+ # DQ_OL_NAMESPACE=airflow-dq
74
+
75
+ # --- contracts git flow (PR-based approval) ---------------------------------------------
76
+ # When true, "propose" saves commit the new contract version on a dq/contract-<id>-v<N>
77
+ # branch instead of writing to the working tree.
78
+ # DQ_CONTRACTS_GIT=false
79
+ # DQ_CONTRACTS_GIT_REMOTE=origin
80
+
81
+ # --- retention -------------------------------------------------------------------------
82
+ # Days to keep raw check results / metric anomalies (0 = keep forever).
83
+ # DQ_RESULTS_RETENTION_DAYS=0
@@ -0,0 +1,7 @@
1
+ # Normalize line endings to LF in the repo (avoids CRLF churn on Windows).
2
+ * text=auto eol=lf
3
+
4
+ # Treat lockfiles/build output sensibly
5
+ *.png binary
6
+ *.jpg binary
7
+ *.gif binary
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: Bug report
3
+ about: Something broke or behaves differently than documented
4
+ labels: bug
5
+ ---
6
+
7
+ **What happened / what did you expect?**
8
+
9
+ **Reproduction**
10
+ - Contract YAML (or minimal snippet):
11
+ - DAG wiring (`build_contract_checks(...)` call):
12
+ - Trigger: scheduled / manual / asset-triggered / retry
13
+
14
+ **Environment**
15
+ - airflow-dq version:
16
+ - Apache Airflow version:
17
+ - Auth manager (Simple/FAB/other):
18
+ - Backend (postgres / duckdb / experimental adapter):
19
+ - Deployment (docker compose demo / K8s / other):
20
+
21
+ **Logs / results rows**
22
+ (check task log excerpt; relevant rows from `dq.dq_check_results` if applicable)
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: Feature request
3
+ about: A capability the framework should have
4
+ labels: enhancement
5
+ ---
6
+
7
+ **Use case** — what are you trying to accomplish, in your pipeline's terms?
8
+
9
+ **Proposal** — what should airflow-dq do? (rule type, UI surface, API, backend…)
10
+
11
+ **Workaround today** — how do you solve this now (custom expression rule, external tool, not at all)?
12
+
13
+ **Context** — see `docs/REFACTOR-ROADMAP.md` for what's already planned; note if this
14
+ matches or contradicts an item there.
@@ -0,0 +1,84 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ env:
9
+ AIRFLOW_VERSION: "3.1.0"
10
+
11
+ jobs:
12
+ python:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python-version: ["3.10", "3.12"]
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+ cache: pip
24
+ cache-dependency-path: pyproject.toml
25
+ - name: Install (with Airflow constraints)
26
+ # The Airflow constraints file pins the whole transitive dependency set to the
27
+ # combination the Airflow release was tested with — same install as production.
28
+ run: |
29
+ CONSTRAINTS="https://raw.githubusercontent.com/apache/airflow/constraints-${AIRFLOW_VERSION}/constraints-${{ matrix.python-version }}.txt"
30
+ python -m pip install --upgrade pip
31
+ pip install "apache-airflow==${AIRFLOW_VERSION}" -e ".[dev]" --constraint "$CONSTRAINTS"
32
+ - name: Lint
33
+ run: |
34
+ ruff check src tests
35
+ mypy src
36
+ - name: Unit tests (coverage gate 70%)
37
+ run: pytest --cov=airflow_dq --cov-fail-under=70
38
+
39
+ ui:
40
+ runs-on: ubuntu-latest
41
+ steps:
42
+ - uses: actions/checkout@v4
43
+ - uses: actions/setup-node@v4
44
+ with:
45
+ node-version: "20"
46
+ cache: npm
47
+ cache-dependency-path: ui/package-lock.json
48
+ - name: Build UI (npm ci + tsc + vite build)
49
+ run: |
50
+ cd ui
51
+ npm ci
52
+ npm run build # runs `tsc && vite build`
53
+
54
+ # ODCS v3.1 spec compliance: lint the committed fixture AND a freshly emitted contract
55
+ # with the official datacontract-cli (the fast suite covers the same schema via
56
+ # tests/test_odcs_compliance.py; this proves it against the real ecosystem linter).
57
+ odcs-lint:
58
+ runs-on: ubuntu-latest
59
+ steps:
60
+ - uses: actions/checkout@v4
61
+ - uses: actions/setup-python@v5
62
+ with:
63
+ python-version: "3.12"
64
+ cache: pip
65
+ - name: Install airflow-dq (no Airflow needed) + datacontract-cli
66
+ run: |
67
+ pip install pyyaml pydantic sqlalchemy
68
+ pip install --no-deps -e .
69
+ pip install datacontract-cli
70
+ - name: Lint contracts with datacontract-cli
71
+ run: bash scripts/odcs_lint.sh
72
+
73
+ # Real end-to-end test: compose stack up, wait for the api-server, trigger the demo
74
+ # DAG, assert that check results landed in dq-postgres. The script tears the stack
75
+ # down (down -v) on exit, pass or fail.
76
+ integration:
77
+ runs-on: ubuntu-latest
78
+ needs: [python]
79
+ timeout-minutes: 20
80
+ continue-on-error: false
81
+ steps:
82
+ - uses: actions/checkout@v4
83
+ - name: Run integration test
84
+ run: bash scripts/integration_test.sh
@@ -0,0 +1,57 @@
1
+ name: Release
2
+
3
+ # Publishes to PyPI via trusted publishing (OIDC) — configure the publisher on
4
+ # https://pypi.org/manage/project/airflow-dq/settings/publishing/ with this repo,
5
+ # workflow name "release.yml", environment "pypi". No API token secrets needed.
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-node@v4
16
+ with:
17
+ node-version: 20
18
+ cache: npm
19
+ cache-dependency-path: ui/package-lock.json
20
+ - name: Build UI (ships inside the wheel)
21
+ run: cd ui && npm ci && npm run build
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: "3.12"
25
+ - name: Build sdist + wheel
26
+ run: |
27
+ python -m pip install build
28
+ python -m build
29
+ - name: Verify wheel contents
30
+ run: |
31
+ python - <<'EOF'
32
+ import zipfile, glob
33
+ whl = glob.glob("dist/*.whl")[0]
34
+ names = zipfile.ZipFile(whl).namelist()
35
+ required = ["airflow_dq/py.typed", "airflow_dq/ui_dist/index.html"]
36
+ missing = [r for r in required if r not in names]
37
+ assert not missing, f"wheel is missing {missing}"
38
+ assert any(n.startswith("airflow_dq/results/migrations/") for n in names), "migrations missing"
39
+ print(f"{whl}: {len(names)} files OK")
40
+ EOF
41
+ - uses: actions/upload-artifact@v4
42
+ with:
43
+ name: dist
44
+ path: dist/
45
+
46
+ publish:
47
+ needs: build
48
+ runs-on: ubuntu-latest
49
+ environment: pypi
50
+ permissions:
51
+ id-token: write # trusted publishing
52
+ steps:
53
+ - uses: actions/download-artifact@v4
54
+ with:
55
+ name: dist
56
+ path: dist/
57
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,38 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+ .pytest_cache/
13
+ .coverage
14
+ htmlcov/
15
+
16
+ # Node / React
17
+ node_modules/
18
+ ui/dist/
19
+ ui/.vite/
20
+
21
+ # Airflow
22
+ logs/
23
+ airflow.db
24
+ airflow.cfg
25
+ standalone_admin_password.txt
26
+
27
+ # Local data files (DuckDB scratch, etc.)
28
+ *.duckdb
29
+ *.duckdb.wal
30
+
31
+ # Env / secrets
32
+ .env
33
+ *.local
34
+
35
+ # OS / editor
36
+ .DS_Store
37
+ .idea/
38
+ .vscode/
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
+ [Semantic Versioning](https://semver.org/) (0.x: minor bumps may break).
6
+
7
+ ## [0.2.0] - 2026-07-12
8
+
9
+ ### Added
10
+ - **Auth**: Airflow auth-manager JWT validation (Bearer header or `_token` session cookie),
11
+ verified live against Airflow 3.1.0; legacy `DQ_API_TOKEN` shared secret kept for machine
12
+ clients; write routes consult `is_authorized_custom_view`.
13
+ - **UI**: three new views on the Airflow-native design system — Overview (per-contract
14
+ rollup with SLA scorecard + anomaly counts), Quarantine triage (filters, pagination,
15
+ bulk resolve), and dashboard upgrades (server-flagged anomalies, dag/status filters);
16
+ the built UI now ships inside the wheel.
17
+ - **SLA checks** compiled from ODCS `slaProperties` (latency, frequency, retention).
18
+ - **Anomaly detection**: leave-current-out baselines for the z-score rule; persisted
19
+ server-side metric anomalies (`dq_metric_anomalies`) surfaced in trends and alerts.
20
+ - **Alerting**: per-(run, contract) grouped alerts with scheme-routed channels
21
+ (webhook, `mailto:`, `pagerduty://`, `opsgenie://`) and severity-aware routing.
22
+ - **Contracts**: immutable versions (409 on conflict), atomic writes, registry sync with
23
+ version timeline API, param/severity-aware breaking-change detection, git propose-as-PR
24
+ flow (`propose: true`), dbt `schema.yml` import.
25
+ - **Executors**: Airflow Connection resolution, pooled engines with statement timeouts,
26
+ dialect layer, experimental Snowflake/BigQuery/Databricks/Trino adapters (SQL-generation
27
+ tested; not yet run against live warehouses).
28
+ - **Results store**: SQL migration runner, idempotent upserts keyed by
29
+ (dag_id, run_id, task_id, check_name), paginated/filterable results API, retention helper.
30
+ - **OpenLineage**: dataset naming derived from the resolved connection per the OL naming spec.
31
+ - Packaging: `py.typed`, Python 3.10–3.12 classifiers, Airflow-constraints CI matrix,
32
+ real docker-compose integration test (`scripts/integration_test.sh`, `make integration`).
33
+
34
+ ### Changed (breaking — see README "Upgrading from 0.1.x")
35
+ - `freshness` defaults to `mode: max_age` (table-level newest-row age); the old per-row
36
+ behavior is `mode: row_age`. The `unit` param (day/hour) is now honored.
37
+ - Executor errors produce `status='error'` results instead of only crashing the task;
38
+ record-only monitors no longer fail on infra blips.
39
+ - Quarantine no longer fires for tolerated (`pass`-graded) results.
40
+ - Check tasks no longer push result XComs by default (`return_result=True` restores).
41
+ - The API no longer injects a bearer token into the served UI; auth is cookie/JWT based.
42
+ - Contract YAML output follows ODCS v3.1 and is validated by the official JSON Schema and
43
+ `datacontract lint` (CI job): quality rules are emitted as `custom` engine blocks
44
+ (`engine: airflow-dq`), column-scoped checks nest under their schema property, and
45
+ legacy logical types normalize on read (`decimal`→`number`).
46
+ - Trends `rows_failed` is now serialized as an integer (was a JSON string on Postgres).
47
+
48
+ ## [0.1.0] - 2026-07 (unreleased baseline)
49
+ - Initial framework: ODCS-dialect contracts, 12 rule types, dynamic-task-mapped checks,
50
+ fail/warn/quarantine policies, results store, embedded dashboard + low-code editor,
51
+ webhook alerting, OpenLineage facets, DuckDB/Postgres executors.
@@ -0,0 +1,9 @@
1
+ # Code of Conduct
2
+
3
+ This project follows the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
4
+
5
+ In short: be respectful, be constructive, assume good faith. Harassment or personal attacks
6
+ of any kind are not tolerated.
7
+
8
+ Instances of unacceptable behavior may be reported to the maintainer via GitHub
9
+ (@Fabian-Schneider01). All reports will be reviewed confidentially.
@@ -0,0 +1,47 @@
1
+ # Contributing to airflow-dq
2
+
3
+ Thanks for your interest! This project aims to be the most complete data-quality framework
4
+ that lives *inside* Apache Airflow — issues, ideas and PRs are welcome.
5
+
6
+ ## Development setup
7
+
8
+ ```bash
9
+ make install # pip install -e '.[dev]'
10
+ make test # pytest (unit tests, no services needed)
11
+ make lint # ruff + mypy
12
+ make up # docker compose demo stack (Airflow 3.1 + seeded Postgres)
13
+ make ui-build # build the React UI into ui/dist
14
+ make integration # full end-to-end test against the compose stack
15
+ ```
16
+
17
+ UI development: `cd ui && npm install && npm run dev` — the Vite dev server proxies
18
+ `/dq/api` to the compose stack on `localhost:8080`.
19
+
20
+ ## Ground rules
21
+
22
+ - **Tests are the bar.** Every behavior change lands with pytest coverage; UI changes must
23
+ keep `npm run build` (tsc strict) green. CI runs ruff, mypy, pytest (70% coverage gate)
24
+ and a docker integration test.
25
+ - **Architecture decisions** live in `docs/DESIGN.md` (D1–D16) and
26
+ `docs/IMPLEMENTATION-SPEC.md` (D-IMPL-1…13). Read them before changing interfaces —
27
+ PRs that silently deviate will be asked to update the spec first.
28
+ - **UI styling** is Airflow's own design system — see `ui/README.md`. Don't hand-tune
29
+ colors; component patterns are ports of Airflow's.
30
+ - **SQL safety**: everything spec-derived goes through the guards in
31
+ `src/airflow_dq/executors/sql.py`. New rule types must keep count/sample/quarantine on
32
+ a single shared predicate.
33
+ - **Backends**: new executors implement the `CheckExecutor` protocol (run + query +
34
+ quarantine) and register via the `airflow_dq.executors` entry-point group. Mark them
35
+ experimental until they've run against a live warehouse.
36
+
37
+ ## Pull requests
38
+
39
+ 1. Fork, branch from `main`, keep PRs focused.
40
+ 2. `make test lint` green locally.
41
+ 3. Describe the user-visible behavior change; update `CHANGELOG.md` under "Unreleased".
42
+ 4. Breaking changes need a README "Upgrading" note.
43
+
44
+ ## Reporting issues
45
+
46
+ Use the issue templates. For anything security-sensitive, take a look at [SECURITY.md](SECURITY.md) —
47
+ please do not open public issues for vulnerabilities.
@@ -0,0 +1,151 @@
1
+ # DESIGN.md — Decisions & Tradeoffs
2
+
3
+ This document captures the *why* behind the architecture. It is intentionally the senior-signal artifact:
4
+ "I know the alternatives and I made a deliberate choice."
5
+
6
+ ---
7
+
8
+ ## D1 — Why Dynamic Task Mapping instead of one opaque check task?
9
+
10
+ Each check becomes its **own visible task** in the DAG graph via `.expand()`, rather than a black-box
11
+ "validate" task. Reviewers and on-call engineers see exactly which check failed in the Airflow grid,
12
+ get per-check retries, and get per-check logs/lineage.
13
+
14
+ **Tradeoff:** more tasks in the graph and a small mapping overhead. Acceptable — observability wins.
15
+
16
+ ## D2 — Why a pluggable `CheckExecutor` (Protocol) instead of one SQL engine?
17
+
18
+ We do **not** build a SQL engine. The executor is a `Protocol` with adapters that *generate* SQL or
19
+ *delegate* to a backend:
20
+
21
+ - `DuckDBExecutor` / `PostgresExecutor` — generate SQL, run everywhere (demo + CI).
22
+ - `DatabricksExecutor` (stretch) — Databricks SQL + Delta, optionally via DQX (enterprise flex).
23
+
24
+ This single abstraction is the core design decision: *"runs locally on DuckDB for reviewers, scales to
25
+ Databricks/Delta for the real world."*
26
+
27
+ **Tradeoff:** dialect differences leak into adapters. Contained by keeping `CheckSpec` backend-agnostic
28
+ and pushing dialect into the adapter only.
29
+
30
+ ## D3 — Why ODCS instead of a home-grown contract format?
31
+
32
+ ODCS (Open Data Contract Standard) is an existing, governed standard. Inventing a format would be
33
+ undifferentiated work and a portability dead-end. Using ODCS plays to real-world experience.
34
+
35
+ ## D4 — Why a separate metadata DB schema, not Airflow's metadata DB?
36
+
37
+ Contract registry, versions and check-run history live in **their own Postgres schema** — never pollute
38
+ Airflow's internal metadata DB. Keeps upgrades safe and the product portable across Airflow deployments.
39
+
40
+ ## D5 — Why not just use Soda / Great Expectations?
41
+
42
+ They are checks-as-code in YAML next to the pipeline, frequently backed by external SaaS. The wedge here
43
+ is **visual authoring inside the orchestrator + codegen to real tasks + an integrated dashboard**, fully
44
+ self-hosted OSS. The **breaking-change detector** ("git for data") is specifically the part those tools
45
+ don't cover as Airflow-native OSS — the differentiator.
46
+
47
+ ## D6 — Failure semantics: `fail` | `warn` | `quarantine`
48
+
49
+ - `fail` — stops the critical pipeline (check task fails, downstream blocked).
50
+ - `warn` — pipeline continues, result recorded as `warn`.
51
+ - `quarantine` — copies failing rows (capped) into `dq.dq_quarantine` for triage; pipeline continues.
52
+
53
+ ## D8 — Tolerances over binary pass/fail (anti alert-fatigue)
54
+
55
+ Real data is never 100% clean; a check that goes red on a single null gets disabled. A rule may
56
+ carry `warn/failIfGreaterThan` (absolute) or `warn/failIfPercentGreaterThan` (share) thresholds.
57
+ With thresholds, status is graded (pass → warn → fail); without them it falls back to binary-by-
58
+ severity. Example: `amount.valueRange` is `blocking` but tolerated, so 2/7 bad (29%) only
59
+ **warns**, while `freshness` (no tolerance) hard-fails. The single biggest lever against the #1
60
+ reason DQ tools get switched off.
61
+
62
+ ## D9 — Schema-drift is one metadata check, not per-column rules
63
+
64
+ Upstream renaming a column / changing a type is the silent killer. One `schema` check per dataset
65
+ compares `information_schema` to the contract: missing required columns + coarse type-family
66
+ mismatch (string/number/temporal/boolean), so `decimal` vs `numeric` doesn't false-positive while
67
+ string↔number drift is caught. Also seeds the breaking-change story (stretch).
68
+
69
+ ## D10 — Failures must be actionable
70
+
71
+ Every check captures a small **sample** of the offending rows inline (one query, same predicate as
72
+ the count, so they can't disagree). The dashboard shows it in an expandable "bad rows" panel —
73
+ actionable triage, not just a red count. Quarantine persists the rows; it does **not** rewrite the
74
+ user's downstream table (owning that write is out of scope).
75
+
76
+ ## D11 — Low-code authoring: form library + one expression escape hatch + live preview
77
+
78
+ The editor is no-code for the closed rule library (pick column + rule + params; columns are
79
+ introspected from the live table) and low-code via a single `expression` rule — a boolean SQL
80
+ predicate that flags bad rows. Crucially, that predicate plugs straight into `failing_predicate`,
81
+ so count, bad-row sample and quarantine come **for free**; no parallel code path. We deliberately
82
+ do *not* build a drag-drop block engine (the form already is no-code) nor accept arbitrary full
83
+ `SELECT`s (breaks portability + safety). The differentiator is **live preview**: every block has a
84
+ "Test" button that runs against the real table and shows pass/fail + sample *before saving* —
85
+ interactive authoring no other in-Airflow OSS does. Expressions are guarded (identifiers only,
86
+ read-only predicate, no DDL/DML); trust model = data engineers, same as dbt tests.
87
+
88
+ ## D12 — One contract, two deployment patterns (inline gate + asset-triggered monitor)
89
+
90
+ The unit is the contract-per-dataset, not the DAG. The same contract is consumed two ways:
91
+ (A) **inline in the producer DAG** as a gate (`on_fail="quarantine"`, blocks/quarantines before
92
+ data flows on); and (C) an **asset-triggered monitor** DAG (`dq_monitor`) that `schedule=[asset]`
93
+ and runs whenever the table is reproduced (`on_fail="warn"`, record-only). The producer marks the
94
+ table updated via `outlets=[orders_asset]`. This uses Airflow 3 **data-aware scheduling** so DQ
95
+ coverage doesn't require wiring checks into every pipeline — and it makes "gate + observability
96
+ from one definition" literal: both feed the same results store / dashboard. (A central scheduled
97
+ "sweep" DAG is the third option, better when there's no producer to hang the asset off.)
98
+
99
+ ## D13 — Authoring intelligence: profile to draft, diff to warn
100
+
101
+ Two features make the contract editor more than a form. **Profiling** points at a live table and
102
+ suggests a starter draft (fully-populated → notNull, distinct==rows → unique, numeric → valueRange
103
+ from observed min/max, low-cardinality string → acceptedValues) — you refine from a draft, not a
104
+ blank page. **Breaking-change detection** diffs the new version against the previous one on save
105
+ and classifies each change (column removed / type changed / new required / uniqueness added =
106
+ breaking; optional add / relaxed constraint / rule change = info), surfacing a red warning in the
107
+ editor. The detector reuses the schema type-family logic (D9); it's the "git for data" wedge that
108
+ Soda/GE don't ship as Airflow-native OSS.
109
+
110
+ ## D14 — Anomaly detection: statistical, push-down, two levels
111
+
112
+ Anomaly detection here is **unsupervised and statistical** (z-score / baseline), not deep
113
+ learning — the same family as Databricks Lakehouse Monitoring, and honest about it. Two levels:
114
+ (1) **row-level** — the `anomaly` rule flags rows beyond `k·σ` of the column mean via pure SQL
115
+ (`ABS(x - (SELECT avg(x))) > k * (SELECT stddev_samp(x))`), so it scales (no data pulled into
116
+ Python) and reuses the sample/quarantine machinery to surface the weird records; (2) **metric-
117
+ level** — the dashboard's "quality over time" chart flags a *run* whose failing-row count exceeds
118
+ the historical mean + 2σ. A heavier model (e.g. IsolationForest) is a drop-in extension behind the
119
+ same rule, with the explicit trade-off that it pulls a sample into Python.
120
+
121
+ ## D15 — Incremental by scope, backfill-safe by logical date
122
+
123
+ Real tables are too big for full scans on every run, and backfills must not false-alarm. Two
124
+ mechanisms: (1) every row-level check accepts a guarded `where` scope (partition filter) that
125
+ count, sample and quarantine all share — check only the new slice, compare anomaly baselines
126
+ against full history; (2) freshness evaluates against the run's `logical_date` (injected by the
127
+ operator as `asOfDate`), so a backfill for last month doesn't flag last month's data as stale.
128
+
129
+ ## D16 — Lineage: OpenLineage facets, never in the critical path
130
+
131
+ The check operator implements `get_openlineage_facets_on_complete` and emits each result as
132
+ `DataQualityAssertionsDatasetFacet` (+ `DataQualityMetricsInputDatasetFacet`) on the checked
133
+ dataset — lineage UIs (Marquez etc.) then show "the pipeline ran AND the dataset met its
134
+ expectations". The mapping core is pure Python (unit-testable without Airflow); the client
135
+ conversion uses defensive imports and returns None on any failure — lineage must never break a
136
+ pipeline. Dialect note: SQL generation now takes a `dialect` parameter (regex is the only split
137
+ so far), and all free-form predicates/identifiers/numeric params are guarded at SQL-build time
138
+ (defense in depth beyond the authoring-side validation).
139
+
140
+ ## D7 — Auth is explicit, on purpose
141
+
142
+ Airflow 3 FastAPI plugin endpoints are not auto-protected by Airflow auth. We secure them explicitly
143
+ (token validated against Airflow's auth manager). A consciously handled auth concern is itself a good signal.
144
+
145
+ ---
146
+
147
+ ## Open questions / parked
148
+
149
+ - Exact ODCS v3 rule coverage in the MVP form (~6 most common check types).
150
+ - OpenLineage facet shape for check results (stretch).
151
+ - Whether the dashboard also feeds an external BI tool (optional, not the centerpiece).