airflow-provider-rivet 0.1.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 (24) hide show
  1. airflow_provider_rivet-0.1.0/.gitignore +8 -0
  2. airflow_provider_rivet-0.1.0/CHANGELOG.md +214 -0
  3. airflow_provider_rivet-0.1.0/LICENSE +21 -0
  4. airflow_provider_rivet-0.1.0/PKG-INFO +142 -0
  5. airflow_provider_rivet-0.1.0/README.md +94 -0
  6. airflow_provider_rivet-0.1.0/airflow_provider_rivet/__init__.py +15 -0
  7. airflow_provider_rivet-0.1.0/airflow_provider_rivet/_compat.py +27 -0
  8. airflow_provider_rivet-0.1.0/airflow_provider_rivet/_yaml.py +285 -0
  9. airflow_provider_rivet-0.1.0/airflow_provider_rivet/callbacks.py +252 -0
  10. airflow_provider_rivet-0.1.0/airflow_provider_rivet/capabilities.py +177 -0
  11. airflow_provider_rivet-0.1.0/airflow_provider_rivet/connections.py +422 -0
  12. airflow_provider_rivet-0.1.0/airflow_provider_rivet/dags.py +200 -0
  13. airflow_provider_rivet-0.1.0/airflow_provider_rivet/invocation.py +1116 -0
  14. airflow_provider_rivet-0.1.0/airflow_provider_rivet/operators.py +352 -0
  15. airflow_provider_rivet-0.1.0/airflow_provider_rivet/preflight.py +283 -0
  16. airflow_provider_rivet-0.1.0/airflow_provider_rivet/py.typed +0 -0
  17. airflow_provider_rivet-0.1.0/airflow_provider_rivet/result.py +128 -0
  18. airflow_provider_rivet-0.1.0/airflow_provider_rivet/tasklog.py +256 -0
  19. airflow_provider_rivet-0.1.0/examples/configs/postgres_batch.yaml +23 -0
  20. airflow_provider_rivet-0.1.0/examples/configs/postgres_cdc.yaml +18 -0
  21. airflow_provider_rivet-0.1.0/examples/rivet_provider_batch.py +32 -0
  22. airflow_provider_rivet-0.1.0/examples/rivet_provider_cdc.py +33 -0
  23. airflow_provider_rivet-0.1.0/examples/rivet_slack_alerts.py +58 -0
  24. airflow_provider_rivet-0.1.0/pyproject.toml +81 -0
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ mutants/
@@ -0,0 +1,214 @@
1
+ # Changelog
2
+
3
+ All notable changes to `airflow-provider-rivet`. The package is versioned independently of the
4
+ rivet binary; its release tags will use the prefix `airflow-v`.
5
+
6
+ ## 0.1.0 (2026-10-18)
7
+
8
+ First release, against rivet 0.32.0 and the contract of ADR-0039; Python 3.12+, Airflow 2.10 and 3.x.
9
+
10
+ - Depends on `rivet-cli>=0.32.0`, which installs the rivet binary; without `rivet` on the worker's
11
+ `PATH` the operators run the binary it installed into Airflow's environment.
12
+
13
+ - Operators: `RivetApplyOperator`, `RivetRunOperator`, `RivetCdcRunOperator`, `RivetLoadOperator`,
14
+ `RivetCompactOperator`, `RivetPlanOperator`; one invocation module builds argv and env, runs
15
+ the process, reads its outputs and classifies the result.
16
+ - DAG builders: `build_batch_dag` (plan, then apply / load / compact groups with per-table
17
+ edges and ordered waves) and `build_cdc_dag` (one run task, load and compact per table).
18
+ - Retries by the contract: a printed retryable failure is retried, exits 1 and 3-6 are not, an
19
+ exit 2 with no error object is not, a kill is retried under its own budget.
20
+ - Preflight: binary present and at least 0.32.0, state location by deployment (a pod needs
21
+ PostgreSQL state and refuses CDC), state directory checks, a warning for `notifications:`.
22
+ - Secrecy: XCom and the task log never carry the error message; raw stderr goes to a file.
23
+ - `slack_failure_callback` for the Slack provider: a one-line title per failed task,
24
+ `dag.task — <rivet code> (final|retryable)` (else the exception's class), with the run id and
25
+ the task-log link; with a bot token and `channel=` the error text (rivet's error line for the
26
+ failed step, else the exception's message; through Airflow's secrets masker, capped) is a code
27
+ block in its thread; with an incoming webhook, title and error text are one message. The task
28
+ log says what was sent and what Slack answered. Setup guide `docs/slack.md`, test DAGs
29
+ `examples/rivet_slack_alerts.py`.
30
+ - `connections=[conn_id, ...]` on every operator: storage and warehouse credentials from Airflow
31
+ Connections of kind gcp, aws, azure, snowflake and clickhouse (the connection type, or the
32
+ extra `rivet_kind`); key files are written `0600` into a private folder removed when the task
33
+ ends, and an unknown kind or two Connections of one kind are refused before rivet starts.
34
+ ("Storage and warehouse connections")
35
+ - Every fallback taken against a binary that lacks a contract feature is recorded as
36
+ `degraded` in the task's result and listed in the README.
37
+
38
+ Changed before the first release, after review (see the README sections named):
39
+
40
+ - The task log is readable: one headline per task with the outcome first (`OK`, `SKIPPED`,
41
+ `REFUSED`, `FAILED`, `CRASHED`, `STOPPED`, `BUSY`), counts, duration and run id; one line per
42
+ rivet step; `note` lines for degradations and single-host SQLite. A successful task's lines
43
+ were raw JSON and red in the Airflow UI (argv held `--json-errors`); argv and the JSON records
44
+ are now at DEBUG. ("Reading the task log")
45
+ - A retry after the task process died past a successful `rivet apply` no longer applies the plan
46
+ again: a `mode: full` export was extracted twice and an incremental one was refused
47
+ `RIVET_PLAN_ARTIFACT_STALE` (a failed task, its load skipped). rivet 0.32.0, the minimum, answers
48
+ the replay itself (a full plan: `already_applied` with the run that applied it; an incremental
49
+ one reads from the cursor its own apply moved), so the package's own record of the apply beside
50
+ the sealed plan and its search of `rivet metrics` are gone, as is the `apply_summary` fallback:
51
+ every apply reads its unit from `apply --summary-output`. ("Retries")
52
+ - A failure Airflow will not try again (no try left) is logged at ERROR, also when rivet called it retryable, a
53
+ crash under the crash budget or a busy lock; WARNING stays for a failure Airflow retries.
54
+ - rivet 0.32's own answer to a replay of an applied point-in-time plan (`per_export[].status:
55
+ already_applied`, the answering run's `run_id` and counts) is a success with that run's unit,
56
+ logged `SKIPPED: an earlier try already applied this plan`; the word read as `failed` before. The contract test now
57
+ requires every status word of rivet's `run_entry_*.json` fixtures to be known.
58
+ - Two tries of one export no longer run rivet at once: every task but `plan` holds a per-export
59
+ (per-table for a load with `--table`) lock file whose descriptor rivet inherits, so a rivet that
60
+ outlives its task keeps it; a task that finds it held for `lock_wait` seconds starts no rivet and
61
+ is retried (`RIVET_AIRFLOW_EXPORT_BUSY`). The degraded per-table load lock was freed when the task
62
+ process died while its rivet ran on. On Linux rivet gets SIGTERM when its task process dies.
63
+ ("Overlap")
64
+ - rivet's busy refusals `RIVET_STATE_RUN_IN_PROGRESS` and `RIVET_LOAD_TABLE_BUSY` (exit 5, "wait,
65
+ then repeat") are retried instead of failing the task. ("Retries")
66
+ - **A DAG run with a failed task is a failed run.** Both builders end in a `watcher` task
67
+ (`one_failed`). A failed `plan` blocks every extract; a failed CDC `run` blocks every load and
68
+ compaction (`load.T` was `all_done`); extract tasks are `all_success` and ordering goes through
69
+ no-op tasks (`apply.wave_<n>_done`, new `apply.after_<export>`). ("DAG builders")
70
+ - A multi-wave batch DAG no longer fails on Airflow 2.10 when an upstream published no XCom.
71
+ - A relative `query_file` stays relative and the file is copied beside the config's copy; it was
72
+ rewritten to an absolute path, which rivet refuses. The copy is named
73
+ `<name>.<hash of the source path>.yaml`. State or a CDC checkpoint left beside the original
74
+ config is refused (`RIVET_AIRFLOW_STATE_BESIDE_CONFIG`, `RIVET_AIRFLOW_CHECKPOINT_BESIDE_CONFIG`,
75
+ `RIVET_AIRFLOW_QUERY_FILE`). ("Local worker setup guide")
76
+ - The subprocess gets an allow-listed environment instead of the worker's; `env_passthrough`
77
+ and `cloud_credentials` extend it. `env` is no longer a templated field. ("Secrets")
78
+ - Raw stderr is never written to the worker's own stderr; without a state directory it goes to
79
+ a private temp directory (`stderr_temp`). Log files are `0600` in `0700` directories, under
80
+ `logs/<dag_id>/<task_id>/`, and only the newest `log_keep` tries of a task are kept. ("Secrets")
81
+ - Exit 0 with a failed unit in the summary fails the task (`exit_zero_failed_unit`); a malformed
82
+ error object or result entry falls back to the exit-code table (`result_malformed`) instead of
83
+ raising; the error line is searched in the last 1 MiB of stderr, not the last 200 lines.
84
+ - The crashed-budget ledger fails closed (`crashed_ledger`) and is keyed by `max_tries`, so
85
+ lowering `retries` cannot reset it. ("Retries")
86
+ - `RivetPlanOperator` swaps the layout file in only when a builder can read it.
87
+ - Every path component made from a DAG id, task id, run id or export name is unique to that
88
+ name: a name that is not one lower-case word of safe characters gets a hash of the exact name
89
+ appended. Two exports or two DAGs whose names differed only in replaced characters shared one
90
+ sealed plan, so one export applied the other's plan and was never extracted. `apply` also
91
+ refuses an artifact that records another export (`RIVET_AIRFLOW_PLAN_ARTIFACT_FOREIGN`).
92
+ ("Local worker setup guide")
93
+ - A sealed plan belongs to one task, one config and one export: its name carries the task id
94
+ and a digest of the config's path, the config's bytes and the export's query file. Two apply
95
+ tasks of one DAG run with two configs and one export name shared `plans/<run>/<export>.json`,
96
+ so the second applied the first config's plan and was never extracted. `apply` refuses an
97
+ artifact whose `export_name` or `config_path` is missing or not the task's own, expired or
98
+ not. The scratch file of `RivetPlanOperator` is no longer shared by two DAGs that refresh
99
+ one layout file. ("Local worker setup guide")
100
+ - A task's log folder is `logs/<dag_id>/<task_id>-<hash>/` for every task id. Retention looks
101
+ only there: files of an older layout are never pruned or deleted (on a file system that
102
+ ignores case, DAG `sales` removed log files of DAG `Sales`). ("Upgrading a state directory")
103
+ - A `RIVET_STATE_URL` in the worker's environment that rivet would use (it starts with
104
+ `postgres`) and that the task would not pass to rivet is refused before rivet starts
105
+ (`RIVET_AIRFLOW_STATE_ENV_NOT_PASSED`). A declared value, also an empty one, is not refused.
106
+ - The README said a Snowflake or ClickHouse compact task "is skipped"; on rivet 0.31.0 it ends
107
+ `compacted` having merged nothing. It now says so, documents `build_cdc_dag(compact=False)` for
108
+ those warehouses, and names every operator and builder parameter (a test holds the lists to the
109
+ signatures). ("Storage and warehouse connections", "DAG builders", "Operators")
110
+ - `build_batch_dag(exports=[])` raises `ValueError`; the watcher carries no failure callback, so
111
+ a callback in `default_args` fires once per failed task.
112
+ - The config is read as rivet reads it. `${name}` is resolved from `-p` and the task's env before
113
+ the guards look at `mode`, `name` and `cdc.checkpoint` (a templated CDC mode passed the
114
+ CDC-on-pod guard; a templated checkpoint was looked for under its literal name); an unresolved
115
+ mode counts as `cdc`, an unresolved name or checkpoint is refused
116
+ (`RIVET_AIRFLOW_CONFIG_UNRESOLVED`). Scalars follow serde_yaml_ng: `08` / `09` no longer crash
117
+ the preflight, `017` is not 15, and `name: 2024` is found. The single-export copy for load /
118
+ compact is cut out of the original text instead of re-serialised, so a quoted `'${SECRET}'`
119
+ keeps its quotes; a text that cannot be cut safely is refused
120
+ (`RIVET_AIRFLOW_CONFIG_NARROW_UNSAFE`). (D06, D07, D09)
121
+ - `connection_url`: a password without a login, IPv6 hosts, Oracle `service_name`, an encoded
122
+ database name. The version has one source, `airflow_provider_rivet/__init__.py`.
123
+ - `slack_failure_callback` on a mapped task reports its own index's code and class; it said "no
124
+ rivet result was published" because the XCom pull returned every index's payload.
125
+ - DAG builders: every rivet task is `all_success` whatever `default_args` says, and a
126
+ `trigger_rule` in `operator_kwargs` is refused (with `all_done`, load and compact ran after a
127
+ refused drain); user `default_args` are merged over the builders' `retries=2`; a bare string
128
+ for `exports`, `compact_exports` or `tables` is refused; a name that is no ASCII task id
129
+ (`daily orders`, `orders:v2`, `заказы`) gets a sanitised task id with a hash and keeps its real name;
130
+ in a wave, each heavy export runs alone and the cheap ones after them, like `rivet apply`.
131
+ ("DAG builders")
132
+ - On Airflow 3 the exceptions are imported from `airflow.sdk.exceptions`, not the deprecated
133
+ `airflow.exceptions`.
134
+ - A clear of a finished `RivetApplyOperator` plans again instead of replaying a sealed plan whose
135
+ cursor the successful apply already moved (rivet refused it `RIVET_PLAN_ARTIFACT_STALE` on
136
+ every clear for 24 h): the artifact is named by the try series (`max_tries`). (D04)
137
+ - `RivetPlanOperator` and `build_batch_dag` read a plan of one export, which rivet prints as a
138
+ bare object with no campaign; it was a `KeyError` and an `internal` failure. (D10)
139
+ - The sealed plan artifact is mode `0600` whatever rivet's umask, as the README said. (D38)
140
+ - Expired plan artifacts and crashed ledgers untouched for 24 h are pruned. (D33)
141
+ - An apply whose counts come from `rivet metrics --json` reports the row's status: a
142
+ `skip_empty` export that delivered nothing is `skipped` with its `skip_reason`. (D31)
143
+
144
+ The environment built from Connections, after the 2026-10 bug hunt (`connections.build_environment`
145
+ is now the one place rivet's environment is assembled; README "Storage and warehouse connections"):
146
+
147
+ - A storage Connection that supplies a credential replaces the worker's variables of its kind
148
+ (`cloud_credentials`, `env_passthrough`): the worker's `AWS_SESSION_TOKEN` no longer rides beside
149
+ a Connection's static keys, nor its Azure account key beside a Connection's SAS token. (D12)
150
+ - An AWS Connection with `role_arn`, `assume_role_*`, `profile_name` or keys in extras is refused
151
+ (`RIVET_AIRFLOW_CONNECTION_UNSUPPORTED`) instead of running as the worker's identity. (D13)
152
+ - Snowflake: with a private key, the Connection's password reaches `snow` as
153
+ `PRIVATE_KEY_PASSPHRASE` (D14); a base64 `private_key_content` is decoded to PEM, and content
154
+ that is neither is refused (D15); the extra `region` is written to the `snow` connection (D19);
155
+ `config.toml` is written as TOML, so a non-BMP or control character in any field reads back
156
+ unchanged (D20).
157
+ - `connection_url` maps the standard extras with a URL form (PostgreSQL `sslmode`, MongoDB `srv`,
158
+ `authSource`, ...) and refuses `iam`, `redshift`, `unix_socket` (D21); only a real IPv6 literal
159
+ is bracketed, with its zone encoded as `%25` (D23).
160
+ - A ClickHouse Connection without a host is refused (`RIVET_AIRFLOW_CONNECTION_INCOMPLETE`)
161
+ instead of becoming `http://None`. (D22)
162
+ - On a binary without `apply --summary-output`, an apply's counts come from its own run (rivet's run id with this
163
+ apply's pid), not the export's newest `rivet metrics` row, which may be another process's run.
164
+ - A load/compact of a config whose export item ends in an alias (`destination: *s3`) narrows the config
165
+ instead of refusing it `RIVET_AIRFLOW_CONFIG_NARROW_UNSAFE` ("shares its last line with other text").
166
+ - A rivet child whose task process died before the parent-death signal took effect stops itself with SIGKILL;
167
+ its SIGTERM ran the inherited Airflow 3 handler, which returns, and the child went on to run rivet as an orphan.
168
+ - A database Connection without a host is refused (`RIVET_AIRFLOW_CONNECTION_INCOMPLETE`) like a ClickHouse one;
169
+ it became `postgresql://u:p@/d` and the driver dialled its default socket or host. One check, in `url_host`.
170
+ - README: an Azure config reads `account_name: ${AZURE_STORAGE_ACCOUNT}`. (D32)
171
+ - A Connection id no backend defines (`env_from_connections`, `connections`) is refused,
172
+ `RIVET_AIRFLOW_CONNECTION_MISSING`, with the refusal in XCom; it was retried with no XCom.
173
+ - A config that is not UTF-8, nests too deep, holds a scalar Python cannot convert, or whose
174
+ `exports` is not a list of mappings with a scalar `name` (read as its text: `name: 2024` is the
175
+ export `2024`), is refused `RIVET_AIRFLOW_CONFIG_UNREADABLE`; it crashed with a traceback and
176
+ was retried. The Connection resolution reads the config through the same reader, with `${...}`
177
+ resolved from `-p` and the task's env, so a templated `load.password_env` / `load.connection`
178
+ names the variable rivet reads.
179
+ - A rivet binary whose `--version` or `<sub> --help` probe fails or times out, in the preflight
180
+ or while argv is built, is refused `RIVET_AIRFLOW_BINARY_MISSING` with the refusal in XCom; it
181
+ raised a raw `RuntimeError` / `TimeoutExpired` and was retried.
182
+ - `on_kill` only sends SIGTERM; the step that started rivet reaps it and kills it after 30 s.
183
+ It used to wait inside Airflow's SIGTERM handler on the lock the interrupted `wait` held, so
184
+ the task hung until the supervisor's SIGKILL and its secrets folder was never removed.
185
+ - A stop Airflow requests while no rivet process runs (between `rivet plan` and `rivet apply`, or
186
+ before the first step) keeps every later step from starting: the task fails with a retryable
187
+ error and `stopped_before` in XCom. On Airflow 3, whose SIGTERM handler returns, the next step
188
+ used to start rivet and run it to completion after the stop. (D40)
189
+ - rivet's stderr is cut into lines at `\n` only: a `--json-errors` line whose message holds
190
+ U+2028, U+2029 or U+0085 was cut apart, so a retryable failure lost its code and its retry.
191
+ A stderr line that starts with `{` and is not JSON is recorded as `result_malformed`.
192
+ - A summary file that is missing (`result_missing`) or not a JSON object (`result_malformed`)
193
+ after an exit-0 process is recorded in `degraded`. ("Works today / needs a newer rivet")
194
+ - Live gate (`python -m gate`, not part of the package): the warehouse leg is graded by values a
195
+ second client reads back (google-cloud-bigquery, snowflake-connector-python, ClickHouse HTTP)
196
+ against the source, per warehouse, mode and table; it printed an inventory and graded nothing.
197
+ Only the BigQuery batch compaction may not be skipped; the cloud cleanup lists again and fails
198
+ on a leftover; the Snowflake sweep drops only the gate's own schema shape, quoted; the run tag
199
+ names the compose project and image; the `Live gate` workflow takes the `-glibc2.28` asset or
200
+ the ghcr.io image, which start on Debian 12.
201
+ - README "Connections and secrets: recommended setup": keyless GCP (Workload Identity, a Connection
202
+ with only `project`), Google Secret Manager as the secrets backend for keys and passwords, a
203
+ metastore with a Fernet key, and `AIRFLOW_CONN_*` variables last; the IAM grants, and what the
204
+ operator does with a key file.
205
+ - Live gate: every stack runs with random passwords and a Fernet key of its own (the source and
206
+ state password `rivet` and the metastore's `airflow` made Airflow mask those words in every task
207
+ log, `/home/***/`), graded by `hygiene/*` cells. `--connections gsm` serves the Connections from
208
+ Google Secret Manager through a reader limited to the gate's secrets, `--gcp-auth adc` runs
209
+ rivet keyless as the containers' identity; each has cells proving where the Connection came
210
+ from, and the cleanup and sweep delete the secrets.
211
+ - Documentation: the README is a front page (the flow picture with rivet inside Airflow, badges,
212
+ how the operators work, install, one example) and links the pages under `docs/`: operators,
213
+ DAG builders, retries, state, connections and secrets, Slack alerts, capabilities, overlap and
214
+ limits, development. The setup-guide link the operators print points at `docs/state.md`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rivet Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.5
2
+ Name: airflow-provider-rivet
3
+ Version: 0.1.0
4
+ Summary: Apache Airflow operators and DAG builders that run the rivet binary on the worker
5
+ Project-URL: Homepage, https://github.com/panchenkoai/airflow-provider-rivet
6
+ Project-URL: Source, https://github.com/panchenkoai/airflow-provider-rivet
7
+ Project-URL: Issues, https://github.com/panchenkoai/airflow-provider-rivet/issues
8
+ Project-URL: rivet, https://github.com/panchenkoai/rivet
9
+ Author: Andrii Panchenko
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 Rivet Contributors
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: airflow,airflow-provider,cdc,etl,rivet
33
+ Classifier: Development Status :: 3 - Alpha
34
+ Classifier: Framework :: Apache Airflow
35
+ Classifier: Framework :: Apache Airflow :: Provider
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Programming Language :: Python :: 3.14
41
+ Classifier: Typing :: Typed
42
+ Requires-Python: >=3.12
43
+ Requires-Dist: apache-airflow>=2.10
44
+ Requires-Dist: rivet-cli>=0.32.0
45
+ Provides-Extra: slack
46
+ Requires-Dist: apache-airflow-providers-slack>=8.0; extra == 'slack'
47
+ Description-Content-Type: text/markdown
48
+
49
+ # airflow-provider-rivet
50
+
51
+ <p align="center">
52
+ <picture>
53
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/panchenkoai/airflow-provider-rivet/airflow-v0.1.0/docs/assets/provider-flow-dark.svg">
54
+ <img src="https://raw.githubusercontent.com/panchenkoai/airflow-provider-rivet/airflow-v0.1.0/docs/assets/provider-flow.svg" alt="Apache Airflow runs rivet as tasks: PostgreSQL, MySQL, SQL Server, MongoDB and Oracle → rivet → S3, GCS, Azure Blob or local disk → BigQuery, Snowflake or ClickHouse" width="900">
55
+ </picture>
56
+ </p>
57
+
58
+ <p align="center">
59
+ <a href="https://github.com/panchenkoai/airflow-provider-rivet/blob/main/airflow_provider_rivet/__init__.py"><img src="https://img.shields.io/badge/version-0.1.0%20alpha-orange" alt="version"></a>
60
+ <a href="https://github.com/panchenkoai/airflow-provider-rivet/blob/main/pyproject.toml"><img src="https://img.shields.io/badge/python-3.12%20%7C%203.13%20%7C%203.14-blue" alt="python"></a>
61
+ <a href="https://github.com/panchenkoai/airflow-provider-rivet/blob/main/pyproject.toml"><img src="https://img.shields.io/badge/airflow-2.10%2B%20%7C%203.x-017CEE" alt="airflow"></a>
62
+ <a href="https://github.com/panchenkoai/airflow-provider-rivet/actions/workflows/ci.yml?query=branch%3Amain"><img src="https://github.com/panchenkoai/airflow-provider-rivet/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
63
+ <a href="https://github.com/panchenkoai/airflow-provider-rivet/blob/main/LICENSE"><img src="https://img.shields.io/github/license/panchenkoai/airflow-provider-rivet" alt="license"></a>
64
+ </p>
65
+
66
+ Apache Airflow operators and DAG builders that run the [rivet](https://github.com/panchenkoai/rivet)
67
+ binary on the worker as a subprocess and turn its result into Airflow's: retry or stop, which
68
+ unit failed, and what to do about it. The contract it implements is
69
+ [ADR-0039](https://github.com/panchenkoai/rivet/blob/main/docs/adr/0039-scheduler-contract.md).
70
+
71
+ rivet itself (sources, destinations, warehouses, CDC, configs): **<https://github.com/panchenkoai/rivet>**.
72
+
73
+ ## How it works
74
+
75
+ - **A task runs the rivet binary on the worker**, as a subprocess with a minimal environment; no
76
+ rivet code runs inside Airflow.
77
+ - **Plan, then apply.** `RivetApplyOperator` seals a plan (`rivet plan`) and applies it
78
+ (`rivet apply`); a retry replays the same sealed plan, a cleared task plans again. `load` and
79
+ `compact` tasks follow per table.
80
+ - **rivet's exit becomes Airflow's decision**: success, a retry for what may pass by itself, a
81
+ final stop for a refusal. The code, class and action go to XCom and to a short block in the
82
+ task log; rivet's raw output stays in files.
83
+ - **DAG builders** make the whole batch DAG (plan, apply, load, compact, in waves) or the CDC DAG
84
+ (one bounded drain, then load and compact per table) from a config.
85
+ - **State** lives in a declared directory on the worker (`state_dir`, SQLite) or in PostgreSQL
86
+ (`RIVET_STATE_URL`); the operator refuses a missing or mismatched one instead of starting from
87
+ empty state.
88
+ - **Credentials come from Airflow Connections** (databases, storage, warehouses), resolved inside
89
+ the task; nothing secret is in argv, XCom or the task log.
90
+
91
+ Status: alpha. It is versioned independently of the binary and checks
92
+ the binary's version at run time (minimum **rivet 0.32.0**; a lower one is refused with both
93
+ versions in the message). Supported: Airflow 2.10+ and 3.x, Python 3.12+ (Airflow 2.10 only on 3.12).
94
+
95
+ ## Install
96
+
97
+ ```bash
98
+ pip install airflow-provider-rivet # or airflow-provider-rivet[slack]
99
+ ```
100
+
101
+ Install it into the Airflow environment with Airflow's own constraints file, so it does not move
102
+ Airflow's pins. It depends on [`rivet-cli`](https://pypi.org/project/rivet-cli/), which installs the
103
+ rivet binary (wheels for Linux glibc 2.28+ x86_64/aarch64 and macOS), so the official Airflow images
104
+ need nothing else:
105
+
106
+ ```dockerfile
107
+ FROM apache/airflow:3.3.2
108
+ RUN pip install --no-cache-dir airflow-provider-rivet
109
+ ```
110
+
111
+ The operators run `rivet` from the worker's `PATH`, else the binary `rivet-cli` installed into
112
+ Airflow's environment. To run another build, pass `rivet_bin="/abs/path/rivet"` (for example one
113
+ copied from `ghcr.io/panchenkoai/rivet:v0.32.0`).
114
+
115
+ ## Example
116
+
117
+ ```python
118
+ from airflow_provider_rivet.dags import build_batch_dag
119
+
120
+ dag = build_batch_dag(
121
+ "rivet_postgres",
122
+ config="/opt/pipelines/postgres.yaml",
123
+ state_dir="/var/lib/rivet/postgres",
124
+ exports=["users", "orders"],
125
+ operator_kwargs={"env_from_connections": {"RIVET_PG_URL": "rivet_postgres"}},
126
+ )
127
+ ```
128
+
129
+ ## Documentation
130
+
131
+ | Topic | |
132
+ |---|---|
133
+ | [Operators](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/operators.md) | each operator, its parameters, the XCom value, reading the task log |
134
+ | [DAG builders](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/dag-builders.md) | `build_batch_dag`, `build_cdc_dag`: tasks, waves, trigger rules, the `watcher` |
135
+ | [Retries](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/retries.md) | exit status → success, retry or stop; the crashed budget; a retry of an apply that succeeded |
136
+ | [State](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/state.md) | state by deployment, the local worker setup guide, moving to PostgreSQL |
137
+ | [Connections and secrets](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/connections-and-secrets.md) | the subprocess environment, database / storage / warehouse Connections, where to keep them |
138
+ | [Slack alerts](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/slack.md) | the failure callback and its setup guide |
139
+ | [Capabilities](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/capabilities.md) | what works with today's rivet and what waits for a newer one |
140
+ | [Overlap and known limits](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/limits.md) | locks between runs of one export; what the package cannot see |
141
+ | [Development](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/development.md) | tests, mutation testing, the live gate |
142
+ | [Changelog](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/CHANGELOG.md) | |
@@ -0,0 +1,94 @@
1
+ # airflow-provider-rivet
2
+
3
+ <p align="center">
4
+ <picture>
5
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/panchenkoai/airflow-provider-rivet/airflow-v0.1.0/docs/assets/provider-flow-dark.svg">
6
+ <img src="https://raw.githubusercontent.com/panchenkoai/airflow-provider-rivet/airflow-v0.1.0/docs/assets/provider-flow.svg" alt="Apache Airflow runs rivet as tasks: PostgreSQL, MySQL, SQL Server, MongoDB and Oracle → rivet → S3, GCS, Azure Blob or local disk → BigQuery, Snowflake or ClickHouse" width="900">
7
+ </picture>
8
+ </p>
9
+
10
+ <p align="center">
11
+ <a href="https://github.com/panchenkoai/airflow-provider-rivet/blob/main/airflow_provider_rivet/__init__.py"><img src="https://img.shields.io/badge/version-0.1.0%20alpha-orange" alt="version"></a>
12
+ <a href="https://github.com/panchenkoai/airflow-provider-rivet/blob/main/pyproject.toml"><img src="https://img.shields.io/badge/python-3.12%20%7C%203.13%20%7C%203.14-blue" alt="python"></a>
13
+ <a href="https://github.com/panchenkoai/airflow-provider-rivet/blob/main/pyproject.toml"><img src="https://img.shields.io/badge/airflow-2.10%2B%20%7C%203.x-017CEE" alt="airflow"></a>
14
+ <a href="https://github.com/panchenkoai/airflow-provider-rivet/actions/workflows/ci.yml?query=branch%3Amain"><img src="https://github.com/panchenkoai/airflow-provider-rivet/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
15
+ <a href="https://github.com/panchenkoai/airflow-provider-rivet/blob/main/LICENSE"><img src="https://img.shields.io/github/license/panchenkoai/airflow-provider-rivet" alt="license"></a>
16
+ </p>
17
+
18
+ Apache Airflow operators and DAG builders that run the [rivet](https://github.com/panchenkoai/rivet)
19
+ binary on the worker as a subprocess and turn its result into Airflow's: retry or stop, which
20
+ unit failed, and what to do about it. The contract it implements is
21
+ [ADR-0039](https://github.com/panchenkoai/rivet/blob/main/docs/adr/0039-scheduler-contract.md).
22
+
23
+ rivet itself (sources, destinations, warehouses, CDC, configs): **<https://github.com/panchenkoai/rivet>**.
24
+
25
+ ## How it works
26
+
27
+ - **A task runs the rivet binary on the worker**, as a subprocess with a minimal environment; no
28
+ rivet code runs inside Airflow.
29
+ - **Plan, then apply.** `RivetApplyOperator` seals a plan (`rivet plan`) and applies it
30
+ (`rivet apply`); a retry replays the same sealed plan, a cleared task plans again. `load` and
31
+ `compact` tasks follow per table.
32
+ - **rivet's exit becomes Airflow's decision**: success, a retry for what may pass by itself, a
33
+ final stop for a refusal. The code, class and action go to XCom and to a short block in the
34
+ task log; rivet's raw output stays in files.
35
+ - **DAG builders** make the whole batch DAG (plan, apply, load, compact, in waves) or the CDC DAG
36
+ (one bounded drain, then load and compact per table) from a config.
37
+ - **State** lives in a declared directory on the worker (`state_dir`, SQLite) or in PostgreSQL
38
+ (`RIVET_STATE_URL`); the operator refuses a missing or mismatched one instead of starting from
39
+ empty state.
40
+ - **Credentials come from Airflow Connections** (databases, storage, warehouses), resolved inside
41
+ the task; nothing secret is in argv, XCom or the task log.
42
+
43
+ Status: alpha. It is versioned independently of the binary and checks
44
+ the binary's version at run time (minimum **rivet 0.32.0**; a lower one is refused with both
45
+ versions in the message). Supported: Airflow 2.10+ and 3.x, Python 3.12+ (Airflow 2.10 only on 3.12).
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install airflow-provider-rivet # or airflow-provider-rivet[slack]
51
+ ```
52
+
53
+ Install it into the Airflow environment with Airflow's own constraints file, so it does not move
54
+ Airflow's pins. It depends on [`rivet-cli`](https://pypi.org/project/rivet-cli/), which installs the
55
+ rivet binary (wheels for Linux glibc 2.28+ x86_64/aarch64 and macOS), so the official Airflow images
56
+ need nothing else:
57
+
58
+ ```dockerfile
59
+ FROM apache/airflow:3.3.2
60
+ RUN pip install --no-cache-dir airflow-provider-rivet
61
+ ```
62
+
63
+ The operators run `rivet` from the worker's `PATH`, else the binary `rivet-cli` installed into
64
+ Airflow's environment. To run another build, pass `rivet_bin="/abs/path/rivet"` (for example one
65
+ copied from `ghcr.io/panchenkoai/rivet:v0.32.0`).
66
+
67
+ ## Example
68
+
69
+ ```python
70
+ from airflow_provider_rivet.dags import build_batch_dag
71
+
72
+ dag = build_batch_dag(
73
+ "rivet_postgres",
74
+ config="/opt/pipelines/postgres.yaml",
75
+ state_dir="/var/lib/rivet/postgres",
76
+ exports=["users", "orders"],
77
+ operator_kwargs={"env_from_connections": {"RIVET_PG_URL": "rivet_postgres"}},
78
+ )
79
+ ```
80
+
81
+ ## Documentation
82
+
83
+ | Topic | |
84
+ |---|---|
85
+ | [Operators](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/operators.md) | each operator, its parameters, the XCom value, reading the task log |
86
+ | [DAG builders](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/dag-builders.md) | `build_batch_dag`, `build_cdc_dag`: tasks, waves, trigger rules, the `watcher` |
87
+ | [Retries](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/retries.md) | exit status → success, retry or stop; the crashed budget; a retry of an apply that succeeded |
88
+ | [State](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/state.md) | state by deployment, the local worker setup guide, moving to PostgreSQL |
89
+ | [Connections and secrets](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/connections-and-secrets.md) | the subprocess environment, database / storage / warehouse Connections, where to keep them |
90
+ | [Slack alerts](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/slack.md) | the failure callback and its setup guide |
91
+ | [Capabilities](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/capabilities.md) | what works with today's rivet and what waits for a newer one |
92
+ | [Overlap and known limits](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/limits.md) | locks between runs of one export; what the package cannot see |
93
+ | [Development](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/docs/development.md) | tests, mutation testing, the live gate |
94
+ | [Changelog](https://github.com/panchenkoai/airflow-provider-rivet/blob/main/CHANGELOG.md) | |
@@ -0,0 +1,15 @@
1
+ """Apache Airflow provider that runs the rivet binary on the worker (ADR-0039)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
6
+
7
+
8
+ def get_provider_info() -> dict:
9
+ """Provider metadata Airflow reads through the `apache_airflow_provider` entry point."""
10
+ return {
11
+ "package-name": "airflow-provider-rivet",
12
+ "name": "Rivet",
13
+ "description": "Run rivet exports, CDC drains, loads and compactions as Airflow tasks.",
14
+ "versions": [__version__],
15
+ }
@@ -0,0 +1,27 @@
1
+ """The names that moved between Airflow 2.10 and 3.x, imported in one place."""
2
+
3
+ from __future__ import annotations
4
+
5
+ try: # Airflow 3.x: the task SDK is the public authoring interface
6
+ from airflow.sdk import DAG, BaseHook, BaseOperator, TaskGroup
7
+ except ImportError: # Airflow 2.10+
8
+ from airflow.hooks.base import BaseHook
9
+ from airflow.models.baseoperator import BaseOperator
10
+ from airflow.models.dag import DAG
11
+ from airflow.utils.task_group import TaskGroup
12
+
13
+ try: # Airflow 3.x: `airflow.exceptions` is a deprecated shim for these (D27)
14
+ from airflow.sdk.exceptions import AirflowException, AirflowFailException, AirflowNotFoundException, AirflowSkipException
15
+ except ImportError: # Airflow 2.10+
16
+ from airflow.exceptions import AirflowException, AirflowFailException, AirflowNotFoundException, AirflowSkipException
17
+
18
+ __all__ = [
19
+ "DAG",
20
+ "AirflowException",
21
+ "AirflowFailException",
22
+ "AirflowNotFoundException",
23
+ "AirflowSkipException",
24
+ "BaseHook",
25
+ "BaseOperator",
26
+ "TaskGroup",
27
+ ]