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.
- airflow_dq-0.2.0/.claude/launch.json +11 -0
- airflow_dq-0.2.0/.env.example +83 -0
- airflow_dq-0.2.0/.gitattributes +7 -0
- airflow_dq-0.2.0/.github/ISSUE_TEMPLATE/bug_report.md +22 -0
- airflow_dq-0.2.0/.github/ISSUE_TEMPLATE/feature_request.md +14 -0
- airflow_dq-0.2.0/.github/workflows/ci.yml +84 -0
- airflow_dq-0.2.0/.github/workflows/release.yml +57 -0
- airflow_dq-0.2.0/.gitignore +38 -0
- airflow_dq-0.2.0/CHANGELOG.md +51 -0
- airflow_dq-0.2.0/CODE_OF_CONDUCT.md +9 -0
- airflow_dq-0.2.0/CONTRIBUTING.md +47 -0
- airflow_dq-0.2.0/DESIGN.md +151 -0
- airflow_dq-0.2.0/LICENSE +202 -0
- airflow_dq-0.2.0/Makefile +75 -0
- airflow_dq-0.2.0/NOTICE +16 -0
- airflow_dq-0.2.0/PKG-INFO +294 -0
- airflow_dq-0.2.0/README.md +244 -0
- airflow_dq-0.2.0/SECURITY.md +28 -0
- airflow_dq-0.2.0/contracts/orders/orders.v3.yaml +48 -0
- airflow_dq-0.2.0/contracts/orders/orders.v5.yaml +23 -0
- airflow_dq-0.2.0/contracts/orders/orders.v6.yaml +62 -0
- airflow_dq-0.2.0/contracts/payments/payments.v1.yaml +69 -0
- airflow_dq-0.2.0/contracts/web_events/web_events.v1.yaml +67 -0
- airflow_dq-0.2.0/dags/demo_contract_5.py +52 -0
- airflow_dq-0.2.0/dags/demo_medallion.py +52 -0
- airflow_dq-0.2.0/dags/dq_assets.py +14 -0
- airflow_dq-0.2.0/dags/dq_monitor.py +29 -0
- airflow_dq-0.2.0/dags/examples/01_quickstart_gate.py +85 -0
- airflow_dq-0.2.0/dags/examples/02_quarantine_triage.py +122 -0
- airflow_dq-0.2.0/dags/examples/03_incremental_and_freshness.py +131 -0
- airflow_dq-0.2.0/dags/examples/04_sla_and_anomaly.py +130 -0
- airflow_dq-0.2.0/dags/examples/05_monitor_on_asset.py +122 -0
- airflow_dq-0.2.0/docker-compose.wheel.yaml +39 -0
- airflow_dq-0.2.0/docker-compose.yaml +78 -0
- airflow_dq-0.2.0/docs/FEATURE-INVENTORY.md +213 -0
- airflow_dq-0.2.0/docs/IMPLEMENTATION-SPEC.md +151 -0
- airflow_dq-0.2.0/docs/PROJECT_DESCRIPTION.md +183 -0
- airflow_dq-0.2.0/docs/REFACTOR-ROADMAP.md +127 -0
- airflow_dq-0.2.0/docs/medium-article.md +230 -0
- airflow_dq-0.2.0/examples/README.md +110 -0
- airflow_dq-0.2.0/examples/dbt_schema.yml +33 -0
- airflow_dq-0.2.0/pyproject.toml +105 -0
- airflow_dq-0.2.0/scripts/integration_test.sh +75 -0
- airflow_dq-0.2.0/scripts/odcs_lint.sh +34 -0
- airflow_dq-0.2.0/seeds/dirty_orders.sql +19 -0
- airflow_dq-0.2.0/src/airflow_dq/__init__.py +13 -0
- airflow_dq-0.2.0/src/airflow_dq/alerting/__init__.py +25 -0
- airflow_dq-0.2.0/src/airflow_dq/alerting/channels.py +138 -0
- airflow_dq-0.2.0/src/airflow_dq/alerting/notifier.py +128 -0
- airflow_dq-0.2.0/src/airflow_dq/alerting/routing.py +65 -0
- airflow_dq-0.2.0/src/airflow_dq/authoring.py +440 -0
- airflow_dq-0.2.0/src/airflow_dq/compiler/__init__.py +6 -0
- airflow_dq-0.2.0/src/airflow_dq/compiler/compiler.py +231 -0
- airflow_dq-0.2.0/src/airflow_dq/compiler/specs.py +55 -0
- airflow_dq-0.2.0/src/airflow_dq/config.py +103 -0
- airflow_dq-0.2.0/src/airflow_dq/contracts/__init__.py +13 -0
- airflow_dq-0.2.0/src/airflow_dq/contracts/dbt_import.py +151 -0
- airflow_dq-0.2.0/src/airflow_dq/contracts/git_flow.py +109 -0
- airflow_dq-0.2.0/src/airflow_dq/contracts/models.py +288 -0
- airflow_dq-0.2.0/src/airflow_dq/contracts/registry.py +148 -0
- airflow_dq-0.2.0/src/airflow_dq/contracts/store.py +75 -0
- airflow_dq-0.2.0/src/airflow_dq/contracts/versioning.py +196 -0
- airflow_dq-0.2.0/src/airflow_dq/db.py +41 -0
- airflow_dq-0.2.0/src/airflow_dq/executors/__init__.py +15 -0
- airflow_dq-0.2.0/src/airflow_dq/executors/_evaluate.py +247 -0
- airflow_dq-0.2.0/src/airflow_dq/executors/_sa.py +96 -0
- airflow_dq-0.2.0/src/airflow_dq/executors/base.py +114 -0
- airflow_dq-0.2.0/src/airflow_dq/executors/bigquery.py +24 -0
- airflow_dq-0.2.0/src/airflow_dq/executors/connections.py +206 -0
- airflow_dq-0.2.0/src/airflow_dq/executors/databricks.py +21 -0
- airflow_dq-0.2.0/src/airflow_dq/executors/dialects.py +173 -0
- airflow_dq-0.2.0/src/airflow_dq/executors/duckdb.py +96 -0
- airflow_dq-0.2.0/src/airflow_dq/executors/postgres.py +14 -0
- airflow_dq-0.2.0/src/airflow_dq/executors/quarantine.py +84 -0
- airflow_dq-0.2.0/src/airflow_dq/executors/schema_check.py +63 -0
- airflow_dq-0.2.0/src/airflow_dq/executors/snowflake.py +21 -0
- airflow_dq-0.2.0/src/airflow_dq/executors/sql.py +330 -0
- airflow_dq-0.2.0/src/airflow_dq/executors/trino.py +20 -0
- airflow_dq-0.2.0/src/airflow_dq/lineage.py +205 -0
- airflow_dq-0.2.0/src/airflow_dq/operators/__init__.py +6 -0
- airflow_dq-0.2.0/src/airflow_dq/operators/check_operator.py +267 -0
- airflow_dq-0.2.0/src/airflow_dq/operators/factory.py +92 -0
- airflow_dq-0.2.0/src/airflow_dq/plugin/__init__.py +50 -0
- airflow_dq-0.2.0/src/airflow_dq/plugin/api/__init__.py +0 -0
- airflow_dq-0.2.0/src/airflow_dq/plugin/api/app.py +263 -0
- airflow_dq-0.2.0/src/airflow_dq/plugin/auth.py +202 -0
- airflow_dq-0.2.0/src/airflow_dq/py.typed +0 -0
- airflow_dq-0.2.0/src/airflow_dq/results/__init__.py +6 -0
- airflow_dq-0.2.0/src/airflow_dq/results/migrations/0001_baseline.sql +69 -0
- airflow_dq-0.2.0/src/airflow_dq/results/migrations/0002_wave1.sql +68 -0
- airflow_dq-0.2.0/src/airflow_dq/results/migrator.py +72 -0
- airflow_dq-0.2.0/src/airflow_dq/results/schema.sql +69 -0
- airflow_dq-0.2.0/src/airflow_dq/results/store.py +717 -0
- airflow_dq-0.2.0/tests/__init__.py +0 -0
- airflow_dq-0.2.0/tests/data/odcs-json-schema-v3.1.0.json +2929 -0
- airflow_dq-0.2.0/tests/test_alerting.py +44 -0
- airflow_dq-0.2.0/tests/test_alerting_v2.py +301 -0
- airflow_dq-0.2.0/tests/test_anomaly_baseline.py +99 -0
- airflow_dq-0.2.0/tests/test_api_auth.py +152 -0
- airflow_dq-0.2.0/tests/test_api_routes.py +273 -0
- airflow_dq-0.2.0/tests/test_authoring.py +176 -0
- airflow_dq-0.2.0/tests/test_compiler.py +34 -0
- airflow_dq-0.2.0/tests/test_compiler_sla.py +131 -0
- airflow_dq-0.2.0/tests/test_contracts_authoring_save.py +181 -0
- airflow_dq-0.2.0/tests/test_contracts_git_flow.py +102 -0
- airflow_dq-0.2.0/tests/test_contracts_models.py +141 -0
- airflow_dq-0.2.0/tests/test_contracts_registry.py +91 -0
- airflow_dq-0.2.0/tests/test_contracts_store_v2.py +64 -0
- airflow_dq-0.2.0/tests/test_contracts_versioning_diff.py +185 -0
- airflow_dq-0.2.0/tests/test_dbt_import.py +155 -0
- airflow_dq-0.2.0/tests/test_executor.py +397 -0
- airflow_dq-0.2.0/tests/test_executor_adapters.py +73 -0
- airflow_dq-0.2.0/tests/test_executor_connections.py +186 -0
- airflow_dq-0.2.0/tests/test_executor_dialects.py +140 -0
- airflow_dq-0.2.0/tests/test_executor_quarantine.py +140 -0
- airflow_dq-0.2.0/tests/test_executor_registry.py +97 -0
- airflow_dq-0.2.0/tests/test_freshness_semantics.py +274 -0
- airflow_dq-0.2.0/tests/test_lineage.py +50 -0
- airflow_dq-0.2.0/tests/test_odcs_compliance.py +93 -0
- airflow_dq-0.2.0/tests/test_operator_execute.py +396 -0
- airflow_dq-0.2.0/tests/test_results_migrator.py +129 -0
- airflow_dq-0.2.0/tests/test_results_store_wave1.py +352 -0
- airflow_dq-0.2.0/tests/test_store_triage.py +199 -0
- airflow_dq-0.2.0/tests/test_versioning.py +61 -0
- airflow_dq-0.2.0/ui/README.md +44 -0
- airflow_dq-0.2.0/ui/dist/assets/index-C3lrIcGb.js +107 -0
- airflow_dq-0.2.0/ui/dist/index.html +12 -0
- airflow_dq-0.2.0/ui/index.html +12 -0
- airflow_dq-0.2.0/ui/package-lock.json +4294 -0
- airflow_dq-0.2.0/ui/package.json +35 -0
- airflow_dq-0.2.0/ui/src/api.ts +277 -0
- airflow_dq-0.2.0/ui/src/components/NavTabs.tsx +46 -0
- airflow_dq-0.2.0/ui/src/components/SampleTable.tsx +51 -0
- airflow_dq-0.2.0/ui/src/components/StateBadge.tsx +50 -0
- airflow_dq-0.2.0/ui/src/components/StatsCard.tsx +28 -0
- airflow_dq-0.2.0/ui/src/components/ui/Alert.tsx +24 -0
- airflow_dq-0.2.0/ui/src/components/ui/Checkbox.tsx +15 -0
- airflow_dq-0.2.0/ui/src/components/ui/ProgressBar.tsx +15 -0
- airflow_dq-0.2.0/ui/src/components/ui/Select.tsx +42 -0
- airflow_dq-0.2.0/ui/src/context/colorMode.tsx +21 -0
- airflow_dq-0.2.0/ui/src/dashboard/Dashboard.tsx +470 -0
- airflow_dq-0.2.0/ui/src/dashboard/DimensionChart.tsx +62 -0
- airflow_dq-0.2.0/ui/src/dashboard/TrendChart.tsx +91 -0
- airflow_dq-0.2.0/ui/src/editor/Editor.tsx +996 -0
- airflow_dq-0.2.0/ui/src/main.tsx +67 -0
- airflow_dq-0.2.0/ui/src/overview/Overview.tsx +147 -0
- airflow_dq-0.2.0/ui/src/quarantine/Quarantine.tsx +336 -0
- airflow_dq-0.2.0/ui/src/theme.ts +390 -0
- airflow_dq-0.2.0/ui/src/vite-env.d.ts +1 -0
- airflow_dq-0.2.0/ui/tsconfig.json +20 -0
- airflow_dq-0.2.0/ui/tsconfig.node.json +10 -0
- airflow_dq-0.2.0/ui/vite.config.ts +15 -0
|
@@ -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,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).
|