mirror of
https://github.com/astral-sh/ruff.git
synced 2026-05-06 08:56:57 -04:00
[airflow] Flag runtime-varying values in DAG/task constructor arguments (AIR304) (#23631)
## Summary Add rule AIR304 that flags runtime-varying function calls (e.g., `datetime.now()`, `pendulum.now()`, `random.randint()`, `uuid.uuid4()`) used as arguments in Airflow DAG/task constructors. These calls cause the serialized DAG hash to change on every parse, creating infinite DAG versions in the `dag_version` and `serialized_dag` tables, leading to unbounded database growth and eventual OOM conditions. Reference: https://github.com/apache/airflow/pull/59430 The rule checks: - `DAG(...)` constructors and `@dag(...)` decorator calls - Operator and sensor constructors (via `is_airflow_builtin_or_provider`) - `@task(...)` decorator calls It recursively inspects keyword argument values through binary/unary ops, dicts, lists, sets, tuples, and f-strings to find calls to known runtime-varying functions from `datetime`, `pendulum`, `time`, `uuid`, and `random`. ## Test Plan `cargo test -p ruff_linter -- airflow::tests` — all 43 tests pass, including the new AIR304 test case with 17 violation and 6 non-violation scenarios covering direct calls, binary ops, `default_args` dicts, f-strings, operators, sensors, decorators, and non-airflow calls. --- Related: https://github.com/apache/airflow/issues/43176 CC: @Lee-W @sjyangkevin @wjddn279 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import random
|
||||
import time
|
||||
import uuid
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
import pendulum
|
||||
from airflow import DAG, dag
|
||||
from airflow.decorators import task
|
||||
from airflow.operators.bash import BashOperator
|
||||
from airflow.providers.standard.sensors.python import PythonSensor
|
||||
|
||||
|
||||
# Violations
|
||||
|
||||
DAG(dag_id="a", start_date=datetime.now())
|
||||
DAG(dag_id="b", start_date=datetime.now() - timedelta(days=1))
|
||||
DAG(dag_id="c", default_args={"start_date": datetime.now()})
|
||||
DAG(dag_id="d", start_date=datetime.utcnow())
|
||||
DAG(dag_id="e", start_date=datetime.today())
|
||||
DAG(dag_id="f", start_date=date.today())
|
||||
DAG(dag_id="g", tags=[f"v{random.randint(1, 9)}"])
|
||||
DAG(dag_id="h", start_date=pendulum.now())
|
||||
DAG(dag_id="i", start_date=pendulum.yesterday())
|
||||
DAG(dag_id="j", start_date=pendulum.tomorrow())
|
||||
DAG(dag_id="k", start_date=pendulum.today())
|
||||
DAG(dag_id="l", owner=f"team-{uuid.uuid4()}")
|
||||
DAG(dag_id="m", description=f"built at {time.time()}")
|
||||
|
||||
# Wrapped calls
|
||||
DAG(dag_id="n", start_date=str(datetime.now()))
|
||||
DAG(dag_id="o", start_date=int(time.time()))
|
||||
|
||||
# Method chain
|
||||
DAG(dag_id="p", start_date=datetime.now().isoformat())
|
||||
|
||||
# Ternary
|
||||
DAG(dag_id="q", start_date=datetime.now() if True else None)
|
||||
|
||||
# BoolOp
|
||||
DAG(dag_id="r", start_date=None or datetime.now())
|
||||
|
||||
# Walrus operator
|
||||
DAG(dag_id="s", start_date=(x := datetime.now()))
|
||||
|
||||
|
||||
@dag(start_date=pendulum.now())
|
||||
def my_dag():
|
||||
pass
|
||||
|
||||
|
||||
BashOperator(task_id="t", bash_command="echo hi", start_date=datetime.today())
|
||||
|
||||
PythonSensor(task_id="s", start_date=datetime.now())
|
||||
|
||||
|
||||
@task(start_date=datetime.utcnow())
|
||||
def my_task():
|
||||
pass
|
||||
|
||||
|
||||
# Non-violations
|
||||
|
||||
DAG(dag_id="ok_a", start_date=datetime(2024, 1, 1))
|
||||
DAG(dag_id="ok_b", start_date=pendulum.datetime(2024, 1, 1))
|
||||
DAG(dag_id="ok_c", schedule=timedelta(hours=1))
|
||||
DAG(dag_id="ok_d", default_args={"retries": 3})
|
||||
|
||||
BashOperator(task_id="t_ok", bash_command="echo hello")
|
||||
|
||||
|
||||
@task(retries=3)
|
||||
def my_static_task():
|
||||
pass
|
||||
|
||||
|
||||
# Non-airflow function call with dynamic arg — should not trigger
|
||||
def not_airflow(start_date):
|
||||
pass
|
||||
|
||||
|
||||
not_airflow(start_date=datetime.now())
|
||||
|
||||
# Lambda defers execution — not a violation
|
||||
DAG(dag_id="ok_e", on_failure_callback=lambda ctx: datetime.now())
|
||||
@@ -1287,6 +1287,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
if checker.is_rule_enabled(Rule::Airflow3IncompatibleFunctionSignature) {
|
||||
airflow::rules::airflow_3_incompatible_function_signature(checker, expr);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::Airflow3DagDynamicValue) {
|
||||
airflow::rules::airflow_3_dag_dynamic_value(checker, call);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::UnnecessaryCastToInt) {
|
||||
ruff::rules::unnecessary_cast_to_int(checker, call);
|
||||
}
|
||||
|
||||
@@ -1132,6 +1132,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Airflow, "301") => rules::airflow::rules::Airflow3Removal,
|
||||
(Airflow, "302") => rules::airflow::rules::Airflow3MovedToProvider,
|
||||
(Airflow, "303") => rules::airflow::rules::Airflow3IncompatibleFunctionSignature,
|
||||
(Airflow, "304") => rules::airflow::rules::Airflow3DagDynamicValue,
|
||||
(Airflow, "311") => rules::airflow::rules::Airflow3SuggestedUpdate,
|
||||
(Airflow, "312") => rules::airflow::rules::Airflow3SuggestedToMoveToProvider,
|
||||
(Airflow, "321") => rules::airflow::rules::Airflow31Moved,
|
||||
|
||||
@@ -50,6 +50,7 @@ mod tests {
|
||||
#[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_standard.py"))]
|
||||
#[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_try.py"))]
|
||||
#[test_case(Rule::Airflow3IncompatibleFunctionSignature, Path::new("AIR303.py"))]
|
||||
#[test_case(Rule::Airflow3DagDynamicValue, Path::new("AIR304.py"))]
|
||||
#[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_args.py"))]
|
||||
#[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_names.py"))]
|
||||
#[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_try.py"))]
|
||||
|
||||
@@ -15,11 +15,11 @@ use crate::checkers::ast::Checker;
|
||||
/// The default value of the `schedule` parameter on Airflow 2 and
|
||||
/// `schedule_interval` on Airflow 1 is `timedelta(days=1)`, which is almost
|
||||
/// never what a user is looking for. Airflow 3 changed the default value to `None`,
|
||||
/// and would break existing dags using the implicit default.
|
||||
/// and would break existing Dags using the implicit default.
|
||||
///
|
||||
/// If your dag does not have an explicit `schedule` / `schedule_interval` argument,
|
||||
/// If your Dag does not have an explicit `schedule` / `schedule_interval` argument,
|
||||
/// Airflow 2 schedules a run for it every day (at the time determined by `start_date`).
|
||||
/// Such a dag will no longer be scheduled on Airflow 3 at all, without any
|
||||
/// Such a Dag will no longer be scheduled on Airflow 3 at all, without any
|
||||
/// exceptions or other messages visible to the user.
|
||||
///
|
||||
/// ## Example
|
||||
|
||||
@@ -3,6 +3,7 @@ pub(crate) use function_signature_change_in_3::*;
|
||||
pub(crate) use moved_in_3_1::*;
|
||||
pub(crate) use moved_to_provider_in_3::*;
|
||||
pub(crate) use removal_in_3::*;
|
||||
pub(crate) use runtime_value_in_dag_or_task::*;
|
||||
pub(crate) use suggested_to_move_to_provider_in_3::*;
|
||||
pub(crate) use suggested_to_update_3_0::*;
|
||||
pub(crate) use task_variable_name::*;
|
||||
@@ -12,6 +13,7 @@ mod function_signature_change_in_3;
|
||||
mod moved_in_3_1;
|
||||
mod moved_to_provider_in_3;
|
||||
mod removal_in_3;
|
||||
mod runtime_value_in_dag_or_task;
|
||||
mod suggested_to_move_to_provider_in_3;
|
||||
mod suggested_to_update_3_0;
|
||||
mod task_variable_name;
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::name::QualifiedName;
|
||||
use ruff_python_ast::{self as ast, Expr, ExprCall, InterpolatedStringElement};
|
||||
use ruff_python_semantic::{Modules, SemanticModel};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::airflow::helpers::is_airflow_builtin_or_provider;
|
||||
use crate::{FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for calls to runtime-varying functions (such as `datetime.now()`)
|
||||
/// used as arguments in Airflow Dag or task constructors.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Using runtime-varying values as arguments to Dag or task constructors
|
||||
/// causes the serialized Dag hash to change on every parse, creating
|
||||
/// infinite Dag versions in the `dag_version` and `serialized_dag` tables.
|
||||
/// This leads to unbounded database growth and can eventually cause
|
||||
/// out-of-memory conditions.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from datetime import datetime
|
||||
///
|
||||
/// from airflow import DAG
|
||||
///
|
||||
/// dag = DAG(dag_id="my_dag", start_date=datetime.now())
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from datetime import datetime
|
||||
///
|
||||
/// from airflow import DAG
|
||||
///
|
||||
/// dag = DAG(dag_id="my_dag", start_date=datetime(2024, 1, 1))
|
||||
/// ```
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "NEXT_RUFF_VERSION")]
|
||||
pub(crate) struct Airflow3DagDynamicValue {
|
||||
function_name: String,
|
||||
}
|
||||
|
||||
impl Violation for Airflow3DagDynamicValue {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let Airflow3DagDynamicValue { function_name } = self;
|
||||
format!(
|
||||
"`{function_name}()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// AIR304
|
||||
pub(crate) fn airflow_3_dag_dynamic_value(checker: &Checker, call: &ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::AIRFLOW) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(qualified_name) = checker.semantic().resolve_qualified_name(&call.func) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !is_dag_or_task_constructor(&qualified_name) {
|
||||
return;
|
||||
}
|
||||
|
||||
for keyword in &call.arguments.keywords {
|
||||
if let Some((expr, name)) = find_runtime_varying_call(&keyword.value, checker.semantic()) {
|
||||
checker.report_diagnostic(
|
||||
Airflow3DagDynamicValue {
|
||||
function_name: name.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the qualified name refers to a Dag constructor, `@dag` decorator,
|
||||
/// operator, sensor, or `@task` decorator.
|
||||
fn is_dag_or_task_constructor(qualified_name: &QualifiedName) -> bool {
|
||||
let segments = qualified_name.segments();
|
||||
matches!(segments, ["airflow", .., "DAG" | "dag"])
|
||||
|| matches!(segments, ["airflow", "decorators" | "sdk", "task"])
|
||||
|| is_airflow_builtin_or_provider(segments, "operators", "Operator")
|
||||
|| is_airflow_builtin_or_provider(segments, "sensors", "Sensor")
|
||||
}
|
||||
|
||||
/// Recursively check an expression for calls to known runtime-varying functions.
|
||||
/// Returns the call expression and a display name (e.g., `"datetime.now"`) if found.
|
||||
///
|
||||
/// The set of traversed expression variants mirrors [`any_over_expr`] in
|
||||
/// `ruff_python_ast::helpers` so that no nested sub-expression is silently skipped.
|
||||
fn find_runtime_varying_call<'a>(
|
||||
expr: &'a Expr,
|
||||
semantic: &SemanticModel,
|
||||
) -> Option<(&'a Expr, &'static str)> {
|
||||
match expr {
|
||||
Expr::Call(ExprCall {
|
||||
func, arguments, ..
|
||||
}) => {
|
||||
if let Some(qualified_name) = semantic.resolve_qualified_name(func) {
|
||||
let name = match qualified_name.segments() {
|
||||
["datetime", "datetime", "now"] => Some("datetime.now"),
|
||||
["datetime", "datetime", "utcnow"] => Some("datetime.utcnow"),
|
||||
["datetime", "datetime", "today"] => Some("datetime.today"),
|
||||
["datetime", "date", "today"] => Some("date.today"),
|
||||
["pendulum", "now"] => Some("pendulum.now"),
|
||||
["pendulum", "today"] => Some("pendulum.today"),
|
||||
["pendulum", "yesterday"] => Some("pendulum.yesterday"),
|
||||
["pendulum", "tomorrow"] => Some("pendulum.tomorrow"),
|
||||
["time", "time"] => Some("time.time"),
|
||||
["uuid", "uuid1"] => Some("uuid.uuid1"),
|
||||
["uuid", "uuid4"] => Some("uuid.uuid4"),
|
||||
["random", "random"] => Some("random.random"),
|
||||
["random", "randint"] => Some("random.randint"),
|
||||
["random", "choice"] => Some("random.choice"),
|
||||
["random", "uniform"] => Some("random.uniform"),
|
||||
["random", "randrange"] => Some("random.randrange"),
|
||||
["random", "sample"] => Some("random.sample"),
|
||||
["random", "getrandbits"] => Some("random.getrandbits"),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(name) = name {
|
||||
return Some((expr, name));
|
||||
}
|
||||
}
|
||||
// Recurse into the callee (catches method chains like `datetime.now().isoformat()`)
|
||||
// and into arguments (catches wrappers like `str(datetime.now())`).
|
||||
find_runtime_varying_call(func, semantic)
|
||||
.or_else(|| {
|
||||
arguments
|
||||
.args
|
||||
.iter()
|
||||
.find_map(|arg| find_runtime_varying_call(arg, semantic))
|
||||
})
|
||||
.or_else(|| {
|
||||
arguments
|
||||
.keywords
|
||||
.iter()
|
||||
.find_map(|kw| find_runtime_varying_call(&kw.value, semantic))
|
||||
})
|
||||
}
|
||||
Expr::BoolOp(ast::ExprBoolOp { values, .. }) => values
|
||||
.iter()
|
||||
.find_map(|value| find_runtime_varying_call(value, semantic)),
|
||||
Expr::Named(ast::ExprNamed { target, value, .. }) => {
|
||||
find_runtime_varying_call(target, semantic)
|
||||
.or_else(|| find_runtime_varying_call(value, semantic))
|
||||
}
|
||||
Expr::BinOp(ast::ExprBinOp { left, right, .. }) => {
|
||||
find_runtime_varying_call(left, semantic)
|
||||
.or_else(|| find_runtime_varying_call(right, semantic))
|
||||
}
|
||||
Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => {
|
||||
find_runtime_varying_call(operand, semantic)
|
||||
}
|
||||
// Lambda defers execution — `lambda: datetime.now()` does not call
|
||||
// `now()` at DAG parse time, so traversing into the body would be a
|
||||
// false positive.
|
||||
Expr::Lambda(_) => None,
|
||||
Expr::If(ast::ExprIf {
|
||||
test, body, orelse, ..
|
||||
}) => find_runtime_varying_call(test, semantic)
|
||||
.or_else(|| find_runtime_varying_call(body, semantic))
|
||||
.or_else(|| find_runtime_varying_call(orelse, semantic)),
|
||||
Expr::Dict(ast::ExprDict { items, .. }) => items.iter().find_map(|item| {
|
||||
item.key
|
||||
.as_ref()
|
||||
.and_then(|k| find_runtime_varying_call(k, semantic))
|
||||
.or_else(|| find_runtime_varying_call(&item.value, semantic))
|
||||
}),
|
||||
Expr::List(ast::ExprList { elts, .. })
|
||||
| Expr::Tuple(ast::ExprTuple { elts, .. })
|
||||
| Expr::Set(ast::ExprSet { elts, .. }) => elts
|
||||
.iter()
|
||||
.find_map(|elt| find_runtime_varying_call(elt, semantic)),
|
||||
Expr::ListComp(ast::ExprListComp {
|
||||
elt, generators, ..
|
||||
})
|
||||
| Expr::SetComp(ast::ExprSetComp {
|
||||
elt, generators, ..
|
||||
})
|
||||
| Expr::Generator(ast::ExprGenerator {
|
||||
elt, generators, ..
|
||||
}) => find_runtime_varying_call(elt, semantic).or_else(|| {
|
||||
generators.iter().find_map(|generator| {
|
||||
find_runtime_varying_call(&generator.target, semantic)
|
||||
.or_else(|| find_runtime_varying_call(&generator.iter, semantic))
|
||||
.or_else(|| {
|
||||
generator
|
||||
.ifs
|
||||
.iter()
|
||||
.find_map(|e| find_runtime_varying_call(e, semantic))
|
||||
})
|
||||
})
|
||||
}),
|
||||
Expr::DictComp(ast::ExprDictComp {
|
||||
key,
|
||||
value,
|
||||
generators,
|
||||
..
|
||||
}) => find_runtime_varying_call(key, semantic)
|
||||
.or_else(|| find_runtime_varying_call(value, semantic))
|
||||
.or_else(|| {
|
||||
generators.iter().find_map(|generator| {
|
||||
find_runtime_varying_call(&generator.target, semantic)
|
||||
.or_else(|| find_runtime_varying_call(&generator.iter, semantic))
|
||||
.or_else(|| {
|
||||
generator
|
||||
.ifs
|
||||
.iter()
|
||||
.find_map(|e| find_runtime_varying_call(e, semantic))
|
||||
})
|
||||
})
|
||||
}),
|
||||
Expr::Await(ast::ExprAwait { value, .. })
|
||||
| Expr::YieldFrom(ast::ExprYieldFrom { value, .. })
|
||||
| Expr::Attribute(ast::ExprAttribute { value, .. })
|
||||
| Expr::Starred(ast::ExprStarred { value, .. }) => {
|
||||
find_runtime_varying_call(value, semantic)
|
||||
}
|
||||
Expr::Yield(ast::ExprYield { value, .. }) => value
|
||||
.as_ref()
|
||||
.and_then(|v| find_runtime_varying_call(v, semantic)),
|
||||
Expr::Compare(ast::ExprCompare {
|
||||
left, comparators, ..
|
||||
}) => find_runtime_varying_call(left, semantic).or_else(|| {
|
||||
comparators
|
||||
.iter()
|
||||
.find_map(|c| find_runtime_varying_call(c, semantic))
|
||||
}),
|
||||
Expr::FString(ast::ExprFString { value, .. }) => value
|
||||
.elements()
|
||||
.find_map(|element| find_runtime_in_interpolated_element(element, semantic)),
|
||||
Expr::TString(ast::ExprTString { value, .. }) => value
|
||||
.elements()
|
||||
.find_map(|element| find_runtime_in_interpolated_element(element, semantic)),
|
||||
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
||||
find_runtime_varying_call(value, semantic)
|
||||
.or_else(|| find_runtime_varying_call(slice, semantic))
|
||||
}
|
||||
Expr::Slice(ast::ExprSlice {
|
||||
lower, upper, step, ..
|
||||
}) => lower
|
||||
.as_ref()
|
||||
.and_then(|v| find_runtime_varying_call(v, semantic))
|
||||
.or_else(|| {
|
||||
upper
|
||||
.as_ref()
|
||||
.and_then(|v| find_runtime_varying_call(v, semantic))
|
||||
})
|
||||
.or_else(|| {
|
||||
step.as_ref()
|
||||
.and_then(|v| find_runtime_varying_call(v, semantic))
|
||||
}),
|
||||
Expr::Name(_)
|
||||
| Expr::StringLiteral(_)
|
||||
| Expr::BytesLiteral(_)
|
||||
| Expr::NumberLiteral(_)
|
||||
| Expr::BooleanLiteral(_)
|
||||
| Expr::NoneLiteral(_)
|
||||
| Expr::EllipsisLiteral(_)
|
||||
| Expr::IpyEscapeCommand(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check an interpolated string element (f-string or t-string) for runtime-varying calls,
|
||||
/// including format specifications.
|
||||
fn find_runtime_in_interpolated_element<'a>(
|
||||
element: &'a InterpolatedStringElement,
|
||||
semantic: &SemanticModel,
|
||||
) -> Option<(&'a Expr, &'static str)> {
|
||||
match element {
|
||||
InterpolatedStringElement::Literal(_) => None,
|
||||
InterpolatedStringElement::Interpolation(interpolation) => {
|
||||
find_runtime_varying_call(&interpolation.expression, semantic).or_else(|| {
|
||||
interpolation.format_spec.as_ref().and_then(|spec| {
|
||||
spec.elements
|
||||
.iter()
|
||||
.find_map(|el| find_runtime_in_interpolated_element(el, semantic))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ## Why is this bad?
|
||||
/// When initializing an Airflow Operator, for consistency, the variable
|
||||
/// name should match the `task_id` value. This makes it easier to
|
||||
/// follow the flow of the DAG.
|
||||
/// follow the flow of the Dag.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
---
|
||||
AIR304 `datetime.now()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:15:28
|
||||
|
|
||||
13 | # Violations
|
||||
14 |
|
||||
15 | DAG(dag_id="a", start_date=datetime.now())
|
||||
| ^^^^^^^^^^^^^^
|
||||
16 | DAG(dag_id="b", start_date=datetime.now() - timedelta(days=1))
|
||||
17 | DAG(dag_id="c", default_args={"start_date": datetime.now()})
|
||||
|
|
||||
|
||||
AIR304 `datetime.now()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:16:28
|
||||
|
|
||||
15 | DAG(dag_id="a", start_date=datetime.now())
|
||||
16 | DAG(dag_id="b", start_date=datetime.now() - timedelta(days=1))
|
||||
| ^^^^^^^^^^^^^^
|
||||
17 | DAG(dag_id="c", default_args={"start_date": datetime.now()})
|
||||
18 | DAG(dag_id="d", start_date=datetime.utcnow())
|
||||
|
|
||||
|
||||
AIR304 `datetime.now()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:17:45
|
||||
|
|
||||
15 | DAG(dag_id="a", start_date=datetime.now())
|
||||
16 | DAG(dag_id="b", start_date=datetime.now() - timedelta(days=1))
|
||||
17 | DAG(dag_id="c", default_args={"start_date": datetime.now()})
|
||||
| ^^^^^^^^^^^^^^
|
||||
18 | DAG(dag_id="d", start_date=datetime.utcnow())
|
||||
19 | DAG(dag_id="e", start_date=datetime.today())
|
||||
|
|
||||
|
||||
AIR304 `datetime.utcnow()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:18:28
|
||||
|
|
||||
16 | DAG(dag_id="b", start_date=datetime.now() - timedelta(days=1))
|
||||
17 | DAG(dag_id="c", default_args={"start_date": datetime.now()})
|
||||
18 | DAG(dag_id="d", start_date=datetime.utcnow())
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
19 | DAG(dag_id="e", start_date=datetime.today())
|
||||
20 | DAG(dag_id="f", start_date=date.today())
|
||||
|
|
||||
|
||||
AIR304 `datetime.today()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:19:28
|
||||
|
|
||||
17 | DAG(dag_id="c", default_args={"start_date": datetime.now()})
|
||||
18 | DAG(dag_id="d", start_date=datetime.utcnow())
|
||||
19 | DAG(dag_id="e", start_date=datetime.today())
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
20 | DAG(dag_id="f", start_date=date.today())
|
||||
21 | DAG(dag_id="g", tags=[f"v{random.randint(1, 9)}"])
|
||||
|
|
||||
|
||||
AIR304 `date.today()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:20:28
|
||||
|
|
||||
18 | DAG(dag_id="d", start_date=datetime.utcnow())
|
||||
19 | DAG(dag_id="e", start_date=datetime.today())
|
||||
20 | DAG(dag_id="f", start_date=date.today())
|
||||
| ^^^^^^^^^^^^
|
||||
21 | DAG(dag_id="g", tags=[f"v{random.randint(1, 9)}"])
|
||||
22 | DAG(dag_id="h", start_date=pendulum.now())
|
||||
|
|
||||
|
||||
AIR304 `random.randint()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:21:27
|
||||
|
|
||||
19 | DAG(dag_id="e", start_date=datetime.today())
|
||||
20 | DAG(dag_id="f", start_date=date.today())
|
||||
21 | DAG(dag_id="g", tags=[f"v{random.randint(1, 9)}"])
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
22 | DAG(dag_id="h", start_date=pendulum.now())
|
||||
23 | DAG(dag_id="i", start_date=pendulum.yesterday())
|
||||
|
|
||||
|
||||
AIR304 `pendulum.now()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:22:28
|
||||
|
|
||||
20 | DAG(dag_id="f", start_date=date.today())
|
||||
21 | DAG(dag_id="g", tags=[f"v{random.randint(1, 9)}"])
|
||||
22 | DAG(dag_id="h", start_date=pendulum.now())
|
||||
| ^^^^^^^^^^^^^^
|
||||
23 | DAG(dag_id="i", start_date=pendulum.yesterday())
|
||||
24 | DAG(dag_id="j", start_date=pendulum.tomorrow())
|
||||
|
|
||||
|
||||
AIR304 `pendulum.yesterday()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:23:28
|
||||
|
|
||||
21 | DAG(dag_id="g", tags=[f"v{random.randint(1, 9)}"])
|
||||
22 | DAG(dag_id="h", start_date=pendulum.now())
|
||||
23 | DAG(dag_id="i", start_date=pendulum.yesterday())
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
24 | DAG(dag_id="j", start_date=pendulum.tomorrow())
|
||||
25 | DAG(dag_id="k", start_date=pendulum.today())
|
||||
|
|
||||
|
||||
AIR304 `pendulum.tomorrow()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:24:28
|
||||
|
|
||||
22 | DAG(dag_id="h", start_date=pendulum.now())
|
||||
23 | DAG(dag_id="i", start_date=pendulum.yesterday())
|
||||
24 | DAG(dag_id="j", start_date=pendulum.tomorrow())
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
25 | DAG(dag_id="k", start_date=pendulum.today())
|
||||
26 | DAG(dag_id="l", owner=f"team-{uuid.uuid4()}")
|
||||
|
|
||||
|
||||
AIR304 `pendulum.today()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:25:28
|
||||
|
|
||||
23 | DAG(dag_id="i", start_date=pendulum.yesterday())
|
||||
24 | DAG(dag_id="j", start_date=pendulum.tomorrow())
|
||||
25 | DAG(dag_id="k", start_date=pendulum.today())
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
26 | DAG(dag_id="l", owner=f"team-{uuid.uuid4()}")
|
||||
27 | DAG(dag_id="m", description=f"built at {time.time()}")
|
||||
|
|
||||
|
||||
AIR304 `uuid.uuid4()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:26:31
|
||||
|
|
||||
24 | DAG(dag_id="j", start_date=pendulum.tomorrow())
|
||||
25 | DAG(dag_id="k", start_date=pendulum.today())
|
||||
26 | DAG(dag_id="l", owner=f"team-{uuid.uuid4()}")
|
||||
| ^^^^^^^^^^^^
|
||||
27 | DAG(dag_id="m", description=f"built at {time.time()}")
|
||||
|
|
||||
|
||||
AIR304 `time.time()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:27:41
|
||||
|
|
||||
25 | DAG(dag_id="k", start_date=pendulum.today())
|
||||
26 | DAG(dag_id="l", owner=f"team-{uuid.uuid4()}")
|
||||
27 | DAG(dag_id="m", description=f"built at {time.time()}")
|
||||
| ^^^^^^^^^^^
|
||||
28 |
|
||||
29 | # Wrapped calls
|
||||
|
|
||||
|
||||
AIR304 `datetime.now()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:30:32
|
||||
|
|
||||
29 | # Wrapped calls
|
||||
30 | DAG(dag_id="n", start_date=str(datetime.now()))
|
||||
| ^^^^^^^^^^^^^^
|
||||
31 | DAG(dag_id="o", start_date=int(time.time()))
|
||||
|
|
||||
|
||||
AIR304 `time.time()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:31:32
|
||||
|
|
||||
29 | # Wrapped calls
|
||||
30 | DAG(dag_id="n", start_date=str(datetime.now()))
|
||||
31 | DAG(dag_id="o", start_date=int(time.time()))
|
||||
| ^^^^^^^^^^^
|
||||
32 |
|
||||
33 | # Method chain
|
||||
|
|
||||
|
||||
AIR304 `datetime.now()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:34:28
|
||||
|
|
||||
33 | # Method chain
|
||||
34 | DAG(dag_id="p", start_date=datetime.now().isoformat())
|
||||
| ^^^^^^^^^^^^^^
|
||||
35 |
|
||||
36 | # Ternary
|
||||
|
|
||||
|
||||
AIR304 `datetime.now()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:37:28
|
||||
|
|
||||
36 | # Ternary
|
||||
37 | DAG(dag_id="q", start_date=datetime.now() if True else None)
|
||||
| ^^^^^^^^^^^^^^
|
||||
38 |
|
||||
39 | # BoolOp
|
||||
|
|
||||
|
||||
AIR304 `datetime.now()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:40:36
|
||||
|
|
||||
39 | # BoolOp
|
||||
40 | DAG(dag_id="r", start_date=None or datetime.now())
|
||||
| ^^^^^^^^^^^^^^
|
||||
41 |
|
||||
42 | # Walrus operator
|
||||
|
|
||||
|
||||
AIR304 `datetime.now()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:43:34
|
||||
|
|
||||
42 | # Walrus operator
|
||||
43 | DAG(dag_id="s", start_date=(x := datetime.now()))
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
AIR304 `pendulum.now()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:46:17
|
||||
|
|
||||
46 | @dag(start_date=pendulum.now())
|
||||
| ^^^^^^^^^^^^^^
|
||||
47 | def my_dag():
|
||||
48 | pass
|
||||
|
|
||||
|
||||
AIR304 `datetime.today()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:51:62
|
||||
|
|
||||
51 | BashOperator(task_id="t", bash_command="echo hi", start_date=datetime.today())
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
52 |
|
||||
53 | PythonSensor(task_id="s", start_date=datetime.now())
|
||||
|
|
||||
|
||||
AIR304 `datetime.now()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:53:38
|
||||
|
|
||||
51 | BashOperator(task_id="t", bash_command="echo hi", start_date=datetime.today())
|
||||
52 |
|
||||
53 | PythonSensor(task_id="s", start_date=datetime.now())
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
AIR304 `datetime.utcnow()` produces a value that changes at runtime; using it in a Dag or task argument causes infinite Dag version creation
|
||||
--> AIR304.py:56:18
|
||||
|
|
||||
56 | @task(start_date=datetime.utcnow())
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
57 | def my_task():
|
||||
58 | pass
|
||||
|
|
||||
Generated
+1
@@ -3068,6 +3068,7 @@
|
||||
"AIR301",
|
||||
"AIR302",
|
||||
"AIR303",
|
||||
"AIR304",
|
||||
"AIR31",
|
||||
"AIR311",
|
||||
"AIR312",
|
||||
|
||||
Reference in New Issue
Block a user