Repository navigation
fix(airflow): the init DAG helper writes what generate schedule writes, and Snowflake python tasks no longer lose the DAG - #739
Merged
Conversation
fas89
force-pushed
the
feat/airflow3-dags
branch
from
October 22, 2026 13:22
f29bf7b to
90447ed
Compare
providers/snowflake/orchestration/airflow_generator.py emitted `python_callable=<callable>` for a function the generated module never defined, so Airflow raised NameError parsing the file and dropped the whole DAG. Reproduced in a real DagBag on Airflow 3.2.2 and 2.10.5: "NameError: name 'score_customers' is not defined". The generator is a library function: no fluid command calls it (fluid export uses providers/snowflake/codegen). It now defines a stub of that callable right above its PythonOperator. The stub raises AirflowFailException naming the task and the callable, imported when it runs (airflow.sdk.exceptions on Airflow 3, airflow.exceptions on 2), so the DAG loads and that one task fails at once, without the retries a stub cannot pass, until someone writes the function. `airflow tasks test` marks it failed on both versions; a NotImplementedError left it up_for_retry. A task that names no callable gets `<task>_func`, as before. Injection safety is unchanged: the stub's name goes through sanitize_identifier and its message is one py_str_literal. A stub `def` would rebind a name the module already uses above it, so a callable named `dag`, `DAG`, `default_args`, an operator class, an earlier task's variable or a `__dunder__` is refused with a ValueError at generation. A task that only becomes an ERROR or TODO comment binds nothing and reserves no name. Sharing its own task's name works: `x = PythonOperator(python_callable=x)` reads the stub before the rebind. Refusing python tasks outright was the alternative. A stub keeps the DAG a usable scaffold, the way this generator already marks what it cannot generate (TODO and ERROR comments). Airflow's @task.stub is for tasks an external worker runs and needs a recent standard provider, so it is not used. The new tests load the generated module against stub Airflow modules (the pattern of tests/cli/_schedule_dag_fixtures.load_dag) whose operators, like Airflow's, refuse a `dag=` that is not the DAG and `>>` on anything but a task, so the NameError and any rebinding reproduce without Airflow installed.
… or nothing
generate_dag_for_project ran `fluid generate-airflow --output-dir ...`, a
flag generate-airflow never had. argparse exited 2 ("unrecognized arguments:
--output-dir"), so every project got the basic fallback DAG under a warning
that the command was "not available"; with no `fluid` on PATH the
FileNotFoundError meant no DAG at all. Its tests mocked subprocess.run, so
nothing ever parsed that argv.
The helper now runs `fluid generate schedule --scheduler airflow --output
<abs dags> --contract-path contract.fluid.yaml --overwrite`, the command
generate-airflow's own deprecation notice points to. (With valid flags,
generate-airflow's DAG for the shipped templates is all `echo` placeholder
tasks.) It runs in-process, parsed by the CLI's own build_parser, as stage 3
(artifact_fanout) runs the same command: a flag the command lacks fails here
as on the command line, and with no child process there is no PATH, cwd,
sys.path or output-decoding assumption. --overwrite lets a re-run
regenerate its DAGs.
It writes no DAG, and says why, when:
- the contract schedules nothing: no build with a cron trigger and no
orchestration.tasks (fluid_apply.has_scheduled_builds/has_explicit_tasks).
customer-360's build is `trigger.type: manual`; generate schedule would
have synthesized a nightly DAG whose only task logs and passes.
- the command refuses the contract, an invalid cron for instance. The basic
validate/plan/apply DAG it used to fall back to ran on a schedule the
contract never declared and hid the error; stage 3 treats the same exit as
a hard failure.
Both are judged on contract.fluid.yaml as the command reads it: its loader
resolves `$ref` builds and the legacy singular `build:` key, which the raw
YAML hides. should_generate_dag, the caller's gate, now also accepts a build
with a cron trigger, so the two gates agree.
dags/README.md lists each DAG this run wrote (dag_id and schedule read from
the file with ast), not other files already in dags/, says to edit the
contract and regenerate rather than edit the generated DAG, and says that
regenerating does not delete the DAG of a renamed or removed build.
create_basic_dag stays as a Python helper. Its apply task ran `fluid apply
contract.fluid.yaml --auto-approve`, which fluid apply rejects (exit 2); it
passes --yes now, and its project path is shell-quoted and then emitted as a
py_str_literal, so a path holding a quote no longer breaks the DAG or runs
what follows it.
No CLI command reaches this helper with DAGs enabled today: `fluid demo`
passes no_dag=True and no `fluid init` mode calls it.
tests/test_init_generate_schedule_dag.py parses the helper's argv with the
real build_parser() and runs the real command on shipped templates:
scheduled builds get their fluid apply DAG and README, a manual or
continuous build gets none, a refused contract gets none plus the command's
reason, a re-run regenerates, a file already in dags/ stays out of the
README, a relative project dir works, and a singular `build:` contract gets
its DAG. Older tests that mocked
subprocess.run now mock the in-process call.
…2.7+/3 shape The example was AwsProvider.export output from before the Airflow 3 work: DAG(schedule_interval=...) and bare Airflow 2 imports, which fail to import on Airflow 3, and helper bodies hollowed out by hand. Regenerated with the call examples/generate_example_dags.py makes (AwsProvider.export, engine airflow) and black-formatted like the file it replaces: schedule=, and DAG and PythonOperator imported from airflow.sdk and apache-airflow-providers-standard with the Airflow 2 fallback. It loads in a real DagBag on Airflow 3.2.2 and 2.10.5 with the amazon provider installed.
fas89
force-pushed
the
fix/airflow-init-and-python-tasks
branch
from
October 22, 2026 13:56
bee71ff to
e9bffb0
Compare
fas89
force-pushed
the
fix/airflow-init-and-python-tasks
branch
from
October 22, 2026 13:56
bee71ff to
e9bffb0
Compare
fas89
marked this pull request as ready for review
October 22, 2026 13:56
📄 Documentation ReminderThis PR appears to be missing a documentation reference. Our docs live in a separate repo. Please update the PR description with one of:
See the Contributing Guide for details. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #736 (
feat/airflow3-dags): it rewrites the same generators. Draft until #736 merges; this branch then gets rebased ontomainand retargeted. Do not merge before.What and why
Two defects the #736 review turned up, plus the non-shipped example.
1.
fluid init's DAG helper never ran the generator it called.generate_dag_for_projectranfluid generate-airflow --output-dir ..., a flag that command never had. argparse exited 2 and every project got the basic fallback DAG. Whenfluidwas not on PATH, it got no DAG at all. The helper's tests mockedsubprocess.run, so nothing parsed that argv. With valid flags,generate-airflow(deprecated) writes onlyechoplaceholder tasks for the shipped templates. It now does the following, per the decisions made during review:fluid generate schedule --scheduler airflow --output <abs dags> --contract-path contract.fluid.yaml --overwrite. This is the commandgenerate-airflow's own deprecation notice names. It runs in-process through the CLI's ownbuild_parser, as stage 3 (artifact_fanout) runs it.orchestration.tasks. For example, customer-360's build istrigger.type: manual, and the command would synthesize a nightly DAG that only logs.contract.fluid.yamlas the command loads it, including$refbuilds and the legacy singularbuild:.dags/README.mdthat lists each DAG this run wrote and says to edit the contract, not the generated DAG.create_basic_dagstays a Python helper. Itsfluid apply --auto-approve(rejected, exit 2) is now--yes, and its project path is shell-quoted and then emitted as a Python string literal. No CLI command reaches the helper with DAGs on today:fluid demopassesno_dag=True.2. Snowflake orchestration python tasks lost the whole DAG.
providers/snowflake/orchestration/airflow_generator.pyemittedpython_callable=<name>for a function the module never defined. Airflow raisedNameErrorat parse and dropped the DAG; this reproduces in a real DagBag on 3.2.2 and 2.10.5. The generator now defines a stub of the callable above the task. The stub raisesAirflowFailExceptionnaming the task, so the DAG loads and that one task fails at once instead of burning retries. A callable name that would rebind something the DAG uses (dag, an operator class, an earlier task,ImportError, a dunder) is refused with aValueError.sanitize_identifierandpy_str_literalescaping are unchanged. This generator is library-only (fluid exportusesproviders/snowflake/codegen).3.
examples/generated_dags/financial_analytics_dag.pyis regenerated throughAwsProvider.exportand black at width 100. It now usesschedule=and Airflow-3-first imports with the Airflow 2 fallback.Borrow before build
ship.py::_resolve_fluid_bin; stage 3's in-processgenerate_schedule.run;fluid_apply.has_scheduled_builds/has_explicit_tasks;codegen_utilsescaping; thetests/cli/_schedule_dag_fixtures.load_dagstub-Airflow harness (adapted).@task.stubis for externally executed tasks and needs a recent standard provider, so it is not used.import_errorsintegrity checks follow Astronomer's testing guide.Validation
DagBag(uv venvs with the snowflake, amazon and common-sql providers; 3.2.2 also has the standard provider): the Snowflake python-task DAG, first-dag and pipeline-orchestrationfluid applyDAGs, and the example all load with 0 import errors. The base code givesNameError: name 'score_customers' is not definedon both versions.airflow tasks teston the stub task: 3.2.2new_state=failed, 2.10.5Marking task as FAILED. WithNotImplementedErrorboth wentUP_FOR_RETRY.dags/helpers.pystays out of the README.build:contract both work.apply --yesran for real in a project path containing a space and'(all rc 0).lint-imports(6/6).mainCI is red; the DuckDB 2.0 port session owns that.Reviews
$refconfined, nothing written outsidedags/.orchestration.engine: noneopt-out is not honoured, andshould_generate_dagcan miss$refbuilds (Trello).task_varsfalse positive, retries of the stub, and harness blind spots. Round 2 left 5 Low latent items: engine opt-out; the outer gate on$ref; the message when the command fails after writing; the reason printed twice; the command's Tip missing--overwrite(Trello).Follow-ups (on the Trello board)
orchestration.tasks.--no-dag, flatdags/vs schedule-sync, stale DAGs on regenerate, Jinja in the project path.