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.
- airflow_provider_rivet-0.1.0/.gitignore +8 -0
- airflow_provider_rivet-0.1.0/CHANGELOG.md +214 -0
- airflow_provider_rivet-0.1.0/LICENSE +21 -0
- airflow_provider_rivet-0.1.0/PKG-INFO +142 -0
- airflow_provider_rivet-0.1.0/README.md +94 -0
- airflow_provider_rivet-0.1.0/airflow_provider_rivet/__init__.py +15 -0
- airflow_provider_rivet-0.1.0/airflow_provider_rivet/_compat.py +27 -0
- airflow_provider_rivet-0.1.0/airflow_provider_rivet/_yaml.py +285 -0
- airflow_provider_rivet-0.1.0/airflow_provider_rivet/callbacks.py +252 -0
- airflow_provider_rivet-0.1.0/airflow_provider_rivet/capabilities.py +177 -0
- airflow_provider_rivet-0.1.0/airflow_provider_rivet/connections.py +422 -0
- airflow_provider_rivet-0.1.0/airflow_provider_rivet/dags.py +200 -0
- airflow_provider_rivet-0.1.0/airflow_provider_rivet/invocation.py +1116 -0
- airflow_provider_rivet-0.1.0/airflow_provider_rivet/operators.py +352 -0
- airflow_provider_rivet-0.1.0/airflow_provider_rivet/preflight.py +283 -0
- airflow_provider_rivet-0.1.0/airflow_provider_rivet/py.typed +0 -0
- airflow_provider_rivet-0.1.0/airflow_provider_rivet/result.py +128 -0
- airflow_provider_rivet-0.1.0/airflow_provider_rivet/tasklog.py +256 -0
- airflow_provider_rivet-0.1.0/examples/configs/postgres_batch.yaml +23 -0
- airflow_provider_rivet-0.1.0/examples/configs/postgres_cdc.yaml +18 -0
- airflow_provider_rivet-0.1.0/examples/rivet_provider_batch.py +32 -0
- airflow_provider_rivet-0.1.0/examples/rivet_provider_cdc.py +33 -0
- airflow_provider_rivet-0.1.0/examples/rivet_slack_alerts.py +58 -0
- airflow_provider_rivet-0.1.0/pyproject.toml +81 -0
|
@@ -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
|
+
]
|