apache-airflow-providers-standard 1.9.0__py3-none-any.whl → 1.9.2rc1__py3-none-any.whl
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/providers/standard/__init__.py +1 -1
- airflow/providers/standard/decorators/bash.py +7 -13
- airflow/providers/standard/decorators/branch_external_python.py +2 -8
- airflow/providers/standard/decorators/branch_python.py +2 -7
- airflow/providers/standard/decorators/branch_virtualenv.py +2 -7
- airflow/providers/standard/decorators/external_python.py +2 -7
- airflow/providers/standard/decorators/python.py +2 -7
- airflow/providers/standard/decorators/python_virtualenv.py +2 -9
- airflow/providers/standard/decorators/sensor.py +2 -9
- airflow/providers/standard/decorators/short_circuit.py +2 -8
- airflow/providers/standard/decorators/stub.py +6 -12
- airflow/providers/standard/example_dags/example_bash_decorator.py +1 -6
- airflow/providers/standard/example_dags/example_branch_operator.py +1 -6
- airflow/providers/standard/example_dags/example_branch_operator_decorator.py +1 -6
- airflow/providers/standard/example_dags/example_external_task_parent_deferrable.py +2 -6
- airflow/providers/standard/example_dags/example_hitl_operator.py +1 -1
- airflow/providers/standard/example_dags/example_sensors.py +1 -6
- airflow/providers/standard/example_dags/example_short_circuit_decorator.py +1 -6
- airflow/providers/standard/example_dags/example_short_circuit_operator.py +1 -6
- airflow/providers/standard/hooks/filesystem.py +1 -1
- airflow/providers/standard/hooks/package_index.py +1 -1
- airflow/providers/standard/hooks/subprocess.py +3 -10
- airflow/providers/standard/operators/bash.py +3 -12
- airflow/providers/standard/operators/branch.py +1 -1
- airflow/providers/standard/operators/datetime.py +2 -6
- airflow/providers/standard/operators/empty.py +1 -1
- airflow/providers/standard/operators/hitl.py +12 -9
- airflow/providers/standard/operators/latest_only.py +3 -8
- airflow/providers/standard/operators/python.py +9 -9
- airflow/providers/standard/operators/smooth.py +1 -1
- airflow/providers/standard/operators/trigger_dagrun.py +16 -21
- airflow/providers/standard/operators/weekday.py +2 -6
- airflow/providers/standard/sensors/bash.py +3 -8
- airflow/providers/standard/sensors/date_time.py +2 -6
- airflow/providers/standard/sensors/external_task.py +77 -55
- airflow/providers/standard/sensors/filesystem.py +1 -1
- airflow/providers/standard/sensors/python.py +2 -6
- airflow/providers/standard/sensors/time.py +1 -6
- airflow/providers/standard/sensors/time_delta.py +3 -7
- airflow/providers/standard/sensors/weekday.py +2 -7
- airflow/providers/standard/triggers/external_task.py +36 -36
- airflow/providers/standard/triggers/file.py +1 -1
- airflow/providers/standard/triggers/hitl.py +135 -86
- airflow/providers/standard/triggers/temporal.py +1 -5
- airflow/providers/standard/utils/python_virtualenv.py +36 -3
- airflow/providers/standard/utils/sensor_helper.py +19 -8
- airflow/providers/standard/utils/skipmixin.py +1 -7
- airflow/providers/standard/version_compat.py +4 -21
- {apache_airflow_providers_standard-1.9.0.dist-info → apache_airflow_providers_standard-1.9.2rc1.dist-info}/METADATA +36 -13
- apache_airflow_providers_standard-1.9.2rc1.dist-info/RECORD +78 -0
- apache_airflow_providers_standard-1.9.2rc1.dist-info/licenses/NOTICE +5 -0
- apache_airflow_providers_standard-1.9.0.dist-info/RECORD +0 -77
- {apache_airflow_providers_standard-1.9.0.dist-info → apache_airflow_providers_standard-1.9.2rc1.dist-info}/WHEEL +0 -0
- {apache_airflow_providers_standard-1.9.0.dist-info → apache_airflow_providers_standard-1.9.2rc1.dist-info}/entry_points.txt +0 -0
- {airflow/providers/standard → apache_airflow_providers_standard-1.9.2rc1.dist-info/licenses}/LICENSE +0 -0
|
@@ -30,6 +30,9 @@ from uuid import UUID
|
|
|
30
30
|
|
|
31
31
|
from asgiref.sync import sync_to_async
|
|
32
32
|
|
|
33
|
+
from airflow.exceptions import ParamValidationError
|
|
34
|
+
from airflow.sdk import Param
|
|
35
|
+
from airflow.sdk.definitions.param import ParamsDict
|
|
33
36
|
from airflow.sdk.execution_time.hitl import (
|
|
34
37
|
HITLUser,
|
|
35
38
|
get_hitl_detail_content_detail,
|
|
@@ -43,7 +46,7 @@ class HITLTriggerEventSuccessPayload(TypedDict, total=False):
|
|
|
43
46
|
"""Minimum required keys for a success Human-in-the-loop TriggerEvent."""
|
|
44
47
|
|
|
45
48
|
chosen_options: list[str]
|
|
46
|
-
params_input: dict[str, Any]
|
|
49
|
+
params_input: dict[str, dict[str, Any]]
|
|
47
50
|
responded_by_user: HITLUser | None
|
|
48
51
|
responded_at: datetime
|
|
49
52
|
timedout: bool
|
|
@@ -53,7 +56,7 @@ class HITLTriggerEventFailurePayload(TypedDict):
|
|
|
53
56
|
"""Minimum required keys for a failed Human-in-the-loop TriggerEvent."""
|
|
54
57
|
|
|
55
58
|
error: str
|
|
56
|
-
error_type: Literal["timeout", "unknown"]
|
|
59
|
+
error_type: Literal["timeout", "unknown", "validation"]
|
|
57
60
|
|
|
58
61
|
|
|
59
62
|
class HITLTrigger(BaseTrigger):
|
|
@@ -64,7 +67,7 @@ class HITLTrigger(BaseTrigger):
|
|
|
64
67
|
*,
|
|
65
68
|
ti_id: UUID,
|
|
66
69
|
options: list[str],
|
|
67
|
-
params: dict[str, Any],
|
|
70
|
+
params: dict[str, dict[str, Any]],
|
|
68
71
|
defaults: list[str] | None = None,
|
|
69
72
|
multiple: bool = False,
|
|
70
73
|
timeout_datetime: datetime | None,
|
|
@@ -80,7 +83,21 @@ class HITLTrigger(BaseTrigger):
|
|
|
80
83
|
self.defaults = defaults
|
|
81
84
|
self.timeout_datetime = timeout_datetime
|
|
82
85
|
|
|
83
|
-
self.params =
|
|
86
|
+
self.params = ParamsDict(
|
|
87
|
+
{
|
|
88
|
+
k: Param(
|
|
89
|
+
v.pop("value"),
|
|
90
|
+
**v,
|
|
91
|
+
)
|
|
92
|
+
if HITLTrigger._is_param(v)
|
|
93
|
+
else Param(v)
|
|
94
|
+
for k, v in params.items()
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def _is_param(value: Any) -> bool:
|
|
100
|
+
return isinstance(value, dict) and all(key in value for key in ("description", "schema", "value"))
|
|
84
101
|
|
|
85
102
|
def serialize(self) -> tuple[str, dict[str, Any]]:
|
|
86
103
|
"""Serialize HITLTrigger arguments and classpath."""
|
|
@@ -90,99 +107,131 @@ class HITLTrigger(BaseTrigger):
|
|
|
90
107
|
"ti_id": self.ti_id,
|
|
91
108
|
"options": self.options,
|
|
92
109
|
"defaults": self.defaults,
|
|
93
|
-
"params": self.params,
|
|
110
|
+
"params": {k: self.params.get_param(k).serialize() for k in self.params},
|
|
94
111
|
"multiple": self.multiple,
|
|
95
112
|
"timeout_datetime": self.timeout_datetime,
|
|
96
113
|
"poke_interval": self.poke_interval,
|
|
97
114
|
},
|
|
98
115
|
)
|
|
99
116
|
|
|
100
|
-
async def
|
|
101
|
-
"""
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
timedout=False,
|
|
129
|
-
)
|
|
130
|
-
)
|
|
131
|
-
return
|
|
132
|
-
|
|
133
|
-
if self.defaults is None:
|
|
134
|
-
yield TriggerEvent(
|
|
135
|
-
HITLTriggerEventFailurePayload(
|
|
136
|
-
error="The timeout has passed, and the response has not yet been received.",
|
|
137
|
-
error_type="timeout",
|
|
138
|
-
)
|
|
139
|
-
)
|
|
140
|
-
return
|
|
141
|
-
|
|
142
|
-
resp = await sync_to_async(update_hitl_detail_response)(
|
|
143
|
-
ti_id=self.ti_id,
|
|
144
|
-
chosen_options=self.defaults,
|
|
145
|
-
params_input=self.params,
|
|
117
|
+
async def _handle_timeout(self) -> TriggerEvent:
|
|
118
|
+
"""Handle HITL timeout logic and yield appropriate event."""
|
|
119
|
+
resp = await sync_to_async(get_hitl_detail_content_detail)(ti_id=self.ti_id)
|
|
120
|
+
|
|
121
|
+
# Case 1: Response arrived just before timeout
|
|
122
|
+
if resp.response_received and resp.chosen_options:
|
|
123
|
+
if TYPE_CHECKING:
|
|
124
|
+
assert resp.responded_by_user is not None
|
|
125
|
+
assert resp.responded_at is not None
|
|
126
|
+
|
|
127
|
+
chosen_options_list = list(resp.chosen_options or [])
|
|
128
|
+
self.log.info(
|
|
129
|
+
"[HITL] responded_by=%s (id=%s) options=%s at %s (timeout fallback skipped)",
|
|
130
|
+
resp.responded_by_user.name,
|
|
131
|
+
resp.responded_by_user.id,
|
|
132
|
+
chosen_options_list,
|
|
133
|
+
resp.responded_at,
|
|
134
|
+
)
|
|
135
|
+
return TriggerEvent(
|
|
136
|
+
HITLTriggerEventSuccessPayload(
|
|
137
|
+
chosen_options=chosen_options_list,
|
|
138
|
+
params_input=resp.params_input or {},
|
|
139
|
+
responded_at=resp.responded_at,
|
|
140
|
+
responded_by_user=HITLUser(
|
|
141
|
+
id=resp.responded_by_user.id,
|
|
142
|
+
name=resp.responded_by_user.name,
|
|
143
|
+
),
|
|
144
|
+
timedout=False,
|
|
146
145
|
)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Case 2: No defaults defined → failure
|
|
149
|
+
if self.defaults is None:
|
|
150
|
+
return TriggerEvent(
|
|
151
|
+
HITLTriggerEventFailurePayload(
|
|
152
|
+
error="The timeout has passed, and the response has not yet been received.",
|
|
153
|
+
error_type="timeout",
|
|
151
154
|
)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Case 3: Timeout fallback to default
|
|
158
|
+
resp = await sync_to_async(update_hitl_detail_response)(
|
|
159
|
+
ti_id=self.ti_id,
|
|
160
|
+
chosen_options=self.defaults,
|
|
161
|
+
params_input=self.params.dump(),
|
|
162
|
+
)
|
|
163
|
+
if TYPE_CHECKING:
|
|
164
|
+
assert resp.responded_at is not None
|
|
165
|
+
|
|
166
|
+
self.log.info(
|
|
167
|
+
"[HITL] timeout reached before receiving response, fallback to default %s",
|
|
168
|
+
self.defaults,
|
|
169
|
+
)
|
|
170
|
+
return TriggerEvent(
|
|
171
|
+
HITLTriggerEventSuccessPayload(
|
|
172
|
+
chosen_options=self.defaults,
|
|
173
|
+
params_input=self.params.dump(),
|
|
174
|
+
responded_by_user=None,
|
|
175
|
+
responded_at=resp.responded_at,
|
|
176
|
+
timedout=True,
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
async def _handle_response(self):
|
|
181
|
+
"""Check if HITL response is ready and yield success if so."""
|
|
182
|
+
resp = await sync_to_async(get_hitl_detail_content_detail)(ti_id=self.ti_id)
|
|
183
|
+
if TYPE_CHECKING:
|
|
184
|
+
assert resp.responded_by_user is not None
|
|
185
|
+
assert resp.responded_at is not None
|
|
186
|
+
|
|
187
|
+
if not (resp.response_received and resp.chosen_options):
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
# validate input
|
|
191
|
+
if params_input := resp.params_input:
|
|
192
|
+
try:
|
|
193
|
+
for key, value in params_input.items():
|
|
194
|
+
self.params[key] = value
|
|
195
|
+
except ParamValidationError as err:
|
|
196
|
+
return TriggerEvent(
|
|
197
|
+
HITLTriggerEventFailurePayload(
|
|
198
|
+
error=str(err),
|
|
199
|
+
error_type="validation",
|
|
159
200
|
)
|
|
160
201
|
)
|
|
202
|
+
|
|
203
|
+
chosen_options_list = list(resp.chosen_options or [])
|
|
204
|
+
self.log.info(
|
|
205
|
+
"[HITL] responded_by=%s (id=%s) options=%s at %s",
|
|
206
|
+
resp.responded_by_user.name,
|
|
207
|
+
resp.responded_by_user.id,
|
|
208
|
+
chosen_options_list,
|
|
209
|
+
resp.responded_at,
|
|
210
|
+
)
|
|
211
|
+
return TriggerEvent(
|
|
212
|
+
HITLTriggerEventSuccessPayload(
|
|
213
|
+
chosen_options=chosen_options_list,
|
|
214
|
+
params_input=params_input or {},
|
|
215
|
+
responded_at=resp.responded_at,
|
|
216
|
+
responded_by_user=HITLUser(
|
|
217
|
+
id=resp.responded_by_user.id,
|
|
218
|
+
name=resp.responded_by_user.name,
|
|
219
|
+
),
|
|
220
|
+
timedout=False,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
async def run(self) -> AsyncIterator[TriggerEvent]:
|
|
225
|
+
"""Loop until the Human-in-the-loop response received or timeout reached."""
|
|
226
|
+
while True:
|
|
227
|
+
if self.timeout_datetime and self.timeout_datetime < utcnow():
|
|
228
|
+
event = await self._handle_timeout()
|
|
229
|
+
yield event
|
|
161
230
|
return
|
|
162
231
|
|
|
163
|
-
|
|
164
|
-
if
|
|
165
|
-
|
|
166
|
-
assert resp.responded_by_user is not None
|
|
167
|
-
assert resp.responded_at is not None
|
|
168
|
-
self.log.info(
|
|
169
|
-
"[HITL] responded_by=%s (id=%s) options=%s at %s",
|
|
170
|
-
resp.responded_by_user.name,
|
|
171
|
-
resp.responded_by_user.id,
|
|
172
|
-
resp.chosen_options,
|
|
173
|
-
resp.responded_at,
|
|
174
|
-
)
|
|
175
|
-
yield TriggerEvent(
|
|
176
|
-
HITLTriggerEventSuccessPayload(
|
|
177
|
-
chosen_options=resp.chosen_options,
|
|
178
|
-
params_input=resp.params_input or {},
|
|
179
|
-
responded_at=resp.responded_at,
|
|
180
|
-
responded_by_user=HITLUser(
|
|
181
|
-
id=resp.responded_by_user.id,
|
|
182
|
-
name=resp.responded_by_user.name,
|
|
183
|
-
),
|
|
184
|
-
timedout=False,
|
|
185
|
-
)
|
|
186
|
-
)
|
|
232
|
+
event = await self._handle_response()
|
|
233
|
+
if event:
|
|
234
|
+
yield event
|
|
187
235
|
return
|
|
236
|
+
|
|
188
237
|
await asyncio.sleep(self.poke_interval)
|
|
@@ -23,13 +23,9 @@ from typing import Any
|
|
|
23
23
|
|
|
24
24
|
import pendulum
|
|
25
25
|
|
|
26
|
+
from airflow.providers.common.compat.sdk import timezone
|
|
26
27
|
from airflow.triggers.base import BaseTrigger, TaskSuccessEvent, TriggerEvent
|
|
27
28
|
|
|
28
|
-
try:
|
|
29
|
-
from airflow.sdk import timezone
|
|
30
|
-
except ImportError:
|
|
31
|
-
from airflow.utils import timezone # type: ignore[attr-defined,no-redef]
|
|
32
|
-
|
|
33
29
|
|
|
34
30
|
class DateTimeTrigger(BaseTrigger):
|
|
35
31
|
"""
|
|
@@ -19,8 +19,11 @@
|
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
|
+
import logging
|
|
22
23
|
import os
|
|
24
|
+
import shlex
|
|
23
25
|
import shutil
|
|
26
|
+
import subprocess
|
|
24
27
|
import warnings
|
|
25
28
|
from pathlib import Path
|
|
26
29
|
|
|
@@ -28,7 +31,6 @@ import jinja2
|
|
|
28
31
|
from jinja2 import select_autoescape
|
|
29
32
|
|
|
30
33
|
from airflow.configuration import conf
|
|
31
|
-
from airflow.utils.process_utils import execute_in_subprocess
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
def _is_uv_installed() -> bool:
|
|
@@ -132,6 +134,37 @@ def _index_urls_to_uv_env_vars(index_urls: list[str] | None = None) -> dict[str,
|
|
|
132
134
|
return uv_index_env_vars
|
|
133
135
|
|
|
134
136
|
|
|
137
|
+
def _execute_in_subprocess(cmd: list[str], cwd: str | None = None, env: dict[str, str] | None = None) -> None:
|
|
138
|
+
"""
|
|
139
|
+
Execute a process and stream output to logger.
|
|
140
|
+
|
|
141
|
+
:param cmd: command and arguments to run
|
|
142
|
+
:param cwd: Current working directory passed to the Popen constructor
|
|
143
|
+
:param env: Additional environment variables to set for the subprocess.
|
|
144
|
+
"""
|
|
145
|
+
log = logging.getLogger(__name__)
|
|
146
|
+
|
|
147
|
+
log.info("Executing cmd: %s", " ".join(shlex.quote(c) for c in cmd))
|
|
148
|
+
with subprocess.Popen(
|
|
149
|
+
cmd,
|
|
150
|
+
stdout=subprocess.PIPE,
|
|
151
|
+
stderr=subprocess.STDOUT,
|
|
152
|
+
bufsize=0,
|
|
153
|
+
close_fds=True,
|
|
154
|
+
cwd=cwd,
|
|
155
|
+
env=env,
|
|
156
|
+
) as proc:
|
|
157
|
+
log.info("Output:")
|
|
158
|
+
if proc.stdout:
|
|
159
|
+
with proc.stdout:
|
|
160
|
+
for line in iter(proc.stdout.readline, b""):
|
|
161
|
+
log.info("%s", line.decode().rstrip())
|
|
162
|
+
|
|
163
|
+
exit_code = proc.wait()
|
|
164
|
+
if exit_code != 0:
|
|
165
|
+
raise subprocess.CalledProcessError(exit_code, cmd)
|
|
166
|
+
|
|
167
|
+
|
|
135
168
|
def prepare_virtualenv(
|
|
136
169
|
venv_directory: str,
|
|
137
170
|
python_bin: str,
|
|
@@ -169,7 +202,7 @@ def prepare_virtualenv(
|
|
|
169
202
|
venv_cmd = _generate_uv_cmd(venv_directory, python_bin, system_site_packages)
|
|
170
203
|
else:
|
|
171
204
|
venv_cmd = _generate_venv_cmd(venv_directory, python_bin, system_site_packages)
|
|
172
|
-
|
|
205
|
+
_execute_in_subprocess(venv_cmd)
|
|
173
206
|
|
|
174
207
|
pip_cmd = None
|
|
175
208
|
if requirements is not None and len(requirements) != 0:
|
|
@@ -188,7 +221,7 @@ def prepare_virtualenv(
|
|
|
188
221
|
)
|
|
189
222
|
|
|
190
223
|
if pip_cmd:
|
|
191
|
-
|
|
224
|
+
_execute_in_subprocess(pip_cmd, env={**os.environ, **_index_urls_to_uv_env_vars(index_urls)})
|
|
192
225
|
|
|
193
226
|
return f"{venv_directory}/bin/python"
|
|
194
227
|
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
# under the License.
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
from collections.abc import Collection
|
|
19
20
|
from typing import TYPE_CHECKING, Any, cast
|
|
20
21
|
|
|
21
22
|
from sqlalchemy import func, select, tuple_
|
|
@@ -27,7 +28,7 @@ from airflow.utils.session import NEW_SESSION, provide_session
|
|
|
27
28
|
|
|
28
29
|
if TYPE_CHECKING:
|
|
29
30
|
from sqlalchemy.orm import Session
|
|
30
|
-
from sqlalchemy.sql import
|
|
31
|
+
from sqlalchemy.sql import Select
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
@provide_session
|
|
@@ -59,6 +60,7 @@ def _get_count(
|
|
|
59
60
|
session.scalar(
|
|
60
61
|
_count_stmt(TI, states, dttm_filter, external_dag_id).where(TI.task_id.in_(external_task_ids))
|
|
61
62
|
)
|
|
63
|
+
or 0
|
|
62
64
|
) / len(external_task_ids)
|
|
63
65
|
elif external_task_group_id:
|
|
64
66
|
external_task_group_task_ids = _get_external_task_group_task_ids(
|
|
@@ -68,20 +70,25 @@ def _get_count(
|
|
|
68
70
|
count = 0
|
|
69
71
|
else:
|
|
70
72
|
count = (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
(
|
|
74
|
+
session.scalar(
|
|
75
|
+
_count_stmt(TI, states, dttm_filter, external_dag_id).where(
|
|
76
|
+
tuple_(TI.task_id, TI.map_index).in_(external_task_group_task_ids)
|
|
77
|
+
)
|
|
74
78
|
)
|
|
79
|
+
or 0
|
|
75
80
|
)
|
|
76
81
|
/ len(external_task_group_task_ids)
|
|
77
82
|
* len(dttm_filter)
|
|
78
83
|
)
|
|
79
84
|
else:
|
|
80
|
-
count = session.scalar(_count_stmt(DR, states, dttm_filter, external_dag_id))
|
|
85
|
+
count = session.scalar(_count_stmt(DR, states, dttm_filter, external_dag_id)) or 0
|
|
81
86
|
return cast("int", count)
|
|
82
87
|
|
|
83
88
|
|
|
84
|
-
def _count_stmt(
|
|
89
|
+
def _count_stmt(
|
|
90
|
+
model: type[DagRun] | type[TaskInstance], states: list[str], dttm_filter: list[Any], external_dag_id: str
|
|
91
|
+
) -> Select[tuple[int]]:
|
|
85
92
|
"""
|
|
86
93
|
Get the count of records against dttm filter and states.
|
|
87
94
|
|
|
@@ -97,7 +104,9 @@ def _count_stmt(model, states, dttm_filter, external_dag_id) -> Executable:
|
|
|
97
104
|
)
|
|
98
105
|
|
|
99
106
|
|
|
100
|
-
def _get_external_task_group_task_ids(
|
|
107
|
+
def _get_external_task_group_task_ids(
|
|
108
|
+
dttm_filter: list[Any], external_task_group_id: str, external_dag_id: str, session: Session
|
|
109
|
+
) -> list[tuple[str, int]]:
|
|
101
110
|
"""
|
|
102
111
|
Get the count of records against dttm filter and states.
|
|
103
112
|
|
|
@@ -107,6 +116,8 @@ def _get_external_task_group_task_ids(dttm_filter, external_task_group_id, exter
|
|
|
107
116
|
:param session: airflow session object
|
|
108
117
|
"""
|
|
109
118
|
refreshed_dag_info = SerializedDagModel.get_dag(external_dag_id, session=session)
|
|
119
|
+
if not refreshed_dag_info:
|
|
120
|
+
return [(external_task_group_id, -1)]
|
|
110
121
|
task_group = refreshed_dag_info.task_group_dict.get(external_task_group_id)
|
|
111
122
|
|
|
112
123
|
if task_group:
|
|
@@ -129,7 +140,7 @@ def _get_external_task_group_task_ids(dttm_filter, external_task_group_id, exter
|
|
|
129
140
|
|
|
130
141
|
def _get_count_by_matched_states(
|
|
131
142
|
run_id_task_state_map: dict[str, dict[str, Any]],
|
|
132
|
-
states:
|
|
143
|
+
states: Collection[str],
|
|
133
144
|
):
|
|
134
145
|
count = 0
|
|
135
146
|
for _, task_states in run_id_task_state_map.items():
|
|
@@ -22,7 +22,6 @@ from types import GeneratorType
|
|
|
22
22
|
from typing import TYPE_CHECKING
|
|
23
23
|
|
|
24
24
|
from airflow.exceptions import AirflowException
|
|
25
|
-
from airflow.providers.standard.version_compat import AIRFLOW_V_3_0_PLUS
|
|
26
25
|
from airflow.utils.log.logging_mixin import LoggingMixin
|
|
27
26
|
|
|
28
27
|
if TYPE_CHECKING:
|
|
@@ -40,12 +39,7 @@ XCOM_SKIPMIXIN_FOLLOWED = "followed"
|
|
|
40
39
|
|
|
41
40
|
|
|
42
41
|
def _ensure_tasks(nodes: Iterable[DAGNode]) -> Sequence[Operator]:
|
|
43
|
-
|
|
44
|
-
from airflow.sdk import BaseOperator
|
|
45
|
-
from airflow.sdk.definitions.mappedoperator import MappedOperator
|
|
46
|
-
else:
|
|
47
|
-
from airflow.models.baseoperator import BaseOperator # type: ignore[no-redef]
|
|
48
|
-
from airflow.models.mappedoperator import MappedOperator # type: ignore[assignment,no-redef]
|
|
42
|
+
from airflow.providers.common.compat.sdk import BaseOperator, MappedOperator
|
|
49
43
|
|
|
50
44
|
return [n for n in nodes if isinstance(n, (BaseOperator, MappedOperator))]
|
|
51
45
|
|
|
@@ -34,36 +34,19 @@ def get_base_airflow_version_tuple() -> tuple[int, int, int]:
|
|
|
34
34
|
|
|
35
35
|
AIRFLOW_V_3_0_PLUS: bool = get_base_airflow_version_tuple() >= (3, 0, 0)
|
|
36
36
|
AIRFLOW_V_3_1_PLUS: bool = get_base_airflow_version_tuple() >= (3, 1, 0)
|
|
37
|
+
AIRFLOW_V_3_1_3_PLUS: bool = get_base_airflow_version_tuple() >= (3, 1, 3)
|
|
37
38
|
AIRFLOW_V_3_2_PLUS: bool = get_base_airflow_version_tuple() >= (3, 2, 0)
|
|
38
39
|
|
|
39
|
-
# BaseOperator
|
|
40
|
-
#
|
|
41
|
-
# even though it wasn't used.
|
|
40
|
+
# BaseOperator: Use 3.1+ due to xcom_push method missing in SDK BaseOperator 3.0.x
|
|
41
|
+
# This is needed for DecoratedOperator compatibility
|
|
42
42
|
if AIRFLOW_V_3_1_PLUS:
|
|
43
|
-
from airflow.sdk import
|
|
44
|
-
from airflow.sdk.definitions.context import context_merge
|
|
43
|
+
from airflow.sdk import BaseOperator
|
|
45
44
|
else:
|
|
46
|
-
from airflow.hooks.base import BaseHook # type: ignore[attr-defined,no-redef]
|
|
47
45
|
from airflow.models.baseoperator import BaseOperator # type: ignore[no-redef]
|
|
48
|
-
from airflow.utils import timezone # type: ignore[attr-defined,no-redef]
|
|
49
|
-
from airflow.utils.context import context_merge # type: ignore[no-redef, attr-defined]
|
|
50
|
-
|
|
51
|
-
if AIRFLOW_V_3_0_PLUS:
|
|
52
|
-
from airflow.sdk import BaseOperatorLink
|
|
53
|
-
from airflow.sdk.bases.sensor import BaseSensorOperator, PokeReturnValue
|
|
54
|
-
else:
|
|
55
|
-
from airflow.models.baseoperatorlink import BaseOperatorLink # type: ignore[no-redef]
|
|
56
|
-
from airflow.sensors.base import BaseSensorOperator, PokeReturnValue # type: ignore[no-redef]
|
|
57
46
|
|
|
58
47
|
__all__ = [
|
|
59
48
|
"AIRFLOW_V_3_0_PLUS",
|
|
60
49
|
"AIRFLOW_V_3_1_PLUS",
|
|
61
50
|
"AIRFLOW_V_3_2_PLUS",
|
|
62
51
|
"BaseOperator",
|
|
63
|
-
"BaseOperatorLink",
|
|
64
|
-
"BaseHook",
|
|
65
|
-
"BaseSensorOperator",
|
|
66
|
-
"PokeReturnValue",
|
|
67
|
-
"context_merge",
|
|
68
|
-
"timezone",
|
|
69
52
|
]
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: apache-airflow-providers-standard
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.2rc1
|
|
4
4
|
Summary: Provider package apache-airflow-providers-standard for Apache Airflow
|
|
5
5
|
Keywords: airflow-provider,standard,airflow,integration
|
|
6
6
|
Author-email: Apache Software Foundation <dev@airflow.apache.org>
|
|
7
7
|
Maintainer-email: Apache Software Foundation <dev@airflow.apache.org>
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
9
|
Description-Content-Type: text/x-rst
|
|
10
|
+
License-Expression: Apache-2.0
|
|
10
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
11
12
|
Classifier: Environment :: Console
|
|
12
13
|
Classifier: Environment :: Web Environment
|
|
@@ -14,16 +15,18 @@ Classifier: Intended Audience :: Developers
|
|
|
14
15
|
Classifier: Intended Audience :: System Administrators
|
|
15
16
|
Classifier: Framework :: Apache Airflow
|
|
16
17
|
Classifier: Framework :: Apache Airflow :: Provider
|
|
17
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
22
|
Classifier: Topic :: System :: Monitoring
|
|
23
|
-
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
License-File: NOTICE
|
|
25
|
+
Requires-Dist: apache-airflow>=2.10.0rc1
|
|
26
|
+
Requires-Dist: apache-airflow-providers-common-compat>=1.8.0rc1
|
|
24
27
|
Project-URL: Bug Tracker, https://github.com/apache/airflow/issues
|
|
25
|
-
Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-standard/1.9.
|
|
26
|
-
Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-standard/1.9.
|
|
28
|
+
Project-URL: Changelog, https://airflow.staged.apache.org/docs/apache-airflow-providers-standard/1.9.2/changelog.html
|
|
29
|
+
Project-URL: Documentation, https://airflow.staged.apache.org/docs/apache-airflow-providers-standard/1.9.2
|
|
27
30
|
Project-URL: Mastodon, https://fosstodon.org/@airflow
|
|
28
31
|
Project-URL: Slack Chat, https://s.apache.org/airflow-slack
|
|
29
32
|
Project-URL: Source Code, https://github.com/apache/airflow
|
|
@@ -54,7 +57,7 @@ Project-URL: YouTube, https://www.youtube.com/channel/UCSXwxpWZQ7XZ1WL3wqevChA/
|
|
|
54
57
|
|
|
55
58
|
Package ``apache-airflow-providers-standard``
|
|
56
59
|
|
|
57
|
-
Release: ``1.9.
|
|
60
|
+
Release: ``1.9.2``
|
|
58
61
|
|
|
59
62
|
|
|
60
63
|
Airflow Standard Provider
|
|
@@ -67,7 +70,7 @@ This is a provider package for ``standard`` provider. All classes for this provi
|
|
|
67
70
|
are in ``airflow.providers.standard`` python package.
|
|
68
71
|
|
|
69
72
|
You can find package information and changelog for the provider
|
|
70
|
-
in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-standard/1.9.
|
|
73
|
+
in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-standard/1.9.2/>`_.
|
|
71
74
|
|
|
72
75
|
Installation
|
|
73
76
|
------------
|
|
@@ -81,12 +84,32 @@ The package supports the following python versions: 3.10,3.11,3.12,3.13
|
|
|
81
84
|
Requirements
|
|
82
85
|
------------
|
|
83
86
|
|
|
84
|
-
|
|
85
|
-
PIP package
|
|
86
|
-
|
|
87
|
-
``apache-airflow``
|
|
88
|
-
|
|
87
|
+
========================================== ==================
|
|
88
|
+
PIP package Version required
|
|
89
|
+
========================================== ==================
|
|
90
|
+
``apache-airflow`` ``>=2.10.0``
|
|
91
|
+
``apache-airflow-providers-common-compat`` ``>=1.8.0``
|
|
92
|
+
========================================== ==================
|
|
93
|
+
|
|
94
|
+
Cross provider package dependencies
|
|
95
|
+
-----------------------------------
|
|
96
|
+
|
|
97
|
+
Those are dependencies that might be needed in order to use all the features of the package.
|
|
98
|
+
You need to install the specified providers in order to use them.
|
|
99
|
+
|
|
100
|
+
You can install such cross-provider dependencies when installing from PyPI. For example:
|
|
101
|
+
|
|
102
|
+
.. code-block:: bash
|
|
103
|
+
|
|
104
|
+
pip install apache-airflow-providers-standard[common.compat]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
================================================================================================================== =================
|
|
108
|
+
Dependent package Extra
|
|
109
|
+
================================================================================================================== =================
|
|
110
|
+
`apache-airflow-providers-common-compat <https://airflow.apache.org/docs/apache-airflow-providers-common-compat>`_ ``common.compat``
|
|
111
|
+
================================================================================================================== =================
|
|
89
112
|
|
|
90
113
|
The changelog for the provider package can be found in the
|
|
91
|
-
`changelog <https://airflow.apache.org/docs/apache-airflow-providers-standard/1.9.
|
|
114
|
+
`changelog <https://airflow.apache.org/docs/apache-airflow-providers-standard/1.9.2/changelog.html>`_.
|
|
92
115
|
|