apache-airflow-providers-standard 0.0.1__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.
Potentially problematic release.
This version of apache-airflow-providers-standard might be problematic. Click here for more details.
- airflow/providers/standard/LICENSE +253 -0
- airflow/providers/standard/__init__.py +39 -0
- airflow/providers/standard/get_provider_info.py +95 -0
- airflow/providers/standard/hooks/__init__.py +16 -0
- airflow/providers/standard/hooks/filesystem.py +89 -0
- airflow/providers/standard/hooks/package_index.py +95 -0
- airflow/providers/standard/hooks/subprocess.py +119 -0
- airflow/providers/standard/operators/__init__.py +16 -0
- airflow/providers/standard/operators/bash.py +304 -0
- airflow/providers/standard/operators/datetime.py +110 -0
- airflow/providers/standard/operators/generic_transfer.py +133 -0
- airflow/providers/standard/operators/python.py +1172 -0
- airflow/providers/standard/operators/weekday.py +120 -0
- airflow/providers/standard/sensors/__init__.py +16 -0
- airflow/providers/standard/sensors/bash.py +114 -0
- airflow/providers/standard/sensors/date_time.py +152 -0
- airflow/providers/standard/sensors/python.py +80 -0
- airflow/providers/standard/sensors/time.py +130 -0
- airflow/providers/standard/sensors/time_delta.py +134 -0
- airflow/providers/standard/sensors/weekday.py +104 -0
- airflow/providers/standard/utils/__init__.py +16 -0
- airflow/providers/standard/utils/python_virtualenv.py +209 -0
- airflow/providers/standard/utils/python_virtualenv_script.jinja2 +101 -0
- airflow/providers/standard/utils/version_references.py +26 -0
- apache_airflow_providers_standard-0.0.1.dist-info/METADATA +112 -0
- apache_airflow_providers_standard-0.0.1.dist-info/RECORD +28 -0
- apache_airflow_providers_standard-0.0.1.dist-info/WHEEL +4 -0
- apache_airflow_providers_standard-0.0.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
# or more contributor license agreements. See the NOTICE file
|
|
4
|
+
# distributed with this work for additional information
|
|
5
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
# to you under the Apache License, Version 2.0 (the
|
|
7
|
+
# "License"); you may not use this file except in compliance
|
|
8
|
+
# with the License. You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing,
|
|
13
|
+
# software distributed under the License is distributed on an
|
|
14
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
# KIND, either express or implied. See the License for the
|
|
16
|
+
# specific language governing permissions and limitations
|
|
17
|
+
# under the License.
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from datetime import timedelta
|
|
21
|
+
from time import sleep
|
|
22
|
+
from typing import TYPE_CHECKING, Any, NoReturn
|
|
23
|
+
|
|
24
|
+
from airflow.configuration import conf
|
|
25
|
+
from airflow.exceptions import AirflowSkipException
|
|
26
|
+
from airflow.providers.standard.utils.version_references import AIRFLOW_V_3_0_PLUS
|
|
27
|
+
from airflow.sensors.base import BaseSensorOperator
|
|
28
|
+
from airflow.triggers.temporal import DateTimeTrigger, TimeDeltaTrigger
|
|
29
|
+
from airflow.utils import timezone
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from airflow.utils.context import Context
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TimeDeltaSensor(BaseSensorOperator):
|
|
36
|
+
"""
|
|
37
|
+
Waits for a timedelta after the run's data interval.
|
|
38
|
+
|
|
39
|
+
:param delta: time length to wait after the data interval before succeeding.
|
|
40
|
+
|
|
41
|
+
.. seealso::
|
|
42
|
+
For more information on how to use this sensor, take a look at the guide:
|
|
43
|
+
:ref:`howto/operator:TimeDeltaSensor`
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, *, delta, **kwargs):
|
|
49
|
+
super().__init__(**kwargs)
|
|
50
|
+
self.delta = delta
|
|
51
|
+
|
|
52
|
+
def poke(self, context: Context):
|
|
53
|
+
target_dttm = context["data_interval_end"]
|
|
54
|
+
target_dttm += self.delta
|
|
55
|
+
self.log.info("Checking if the time (%s) has come", target_dttm)
|
|
56
|
+
return timezone.utcnow() > target_dttm
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TimeDeltaSensorAsync(TimeDeltaSensor):
|
|
60
|
+
"""
|
|
61
|
+
A deferrable drop-in replacement for TimeDeltaSensor.
|
|
62
|
+
|
|
63
|
+
Will defers itself to avoid taking up a worker slot while it is waiting.
|
|
64
|
+
|
|
65
|
+
:param delta: time length to wait after the data interval before succeeding.
|
|
66
|
+
:param end_from_trigger: End the task directly from the triggerer without going into the worker.
|
|
67
|
+
|
|
68
|
+
.. seealso::
|
|
69
|
+
For more information on how to use this sensor, take a look at the guide:
|
|
70
|
+
:ref:`howto/operator:TimeDeltaSensorAsync`
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, *, end_from_trigger: bool = False, delta, **kwargs) -> None:
|
|
75
|
+
super().__init__(delta=delta, **kwargs)
|
|
76
|
+
self.end_from_trigger = end_from_trigger
|
|
77
|
+
|
|
78
|
+
def execute(self, context: Context) -> bool | NoReturn:
|
|
79
|
+
target_dttm = context["data_interval_end"]
|
|
80
|
+
target_dttm += self.delta
|
|
81
|
+
if timezone.utcnow() > target_dttm:
|
|
82
|
+
# If the target datetime is in the past, return immediately
|
|
83
|
+
return True
|
|
84
|
+
try:
|
|
85
|
+
if AIRFLOW_V_3_0_PLUS:
|
|
86
|
+
trigger = DateTimeTrigger(moment=target_dttm, end_from_trigger=self.end_from_trigger)
|
|
87
|
+
else:
|
|
88
|
+
trigger = DateTimeTrigger(moment=target_dttm)
|
|
89
|
+
except (TypeError, ValueError) as e:
|
|
90
|
+
if self.soft_fail:
|
|
91
|
+
raise AirflowSkipException("Skipping due to soft_fail is set to True.") from e
|
|
92
|
+
raise
|
|
93
|
+
|
|
94
|
+
self.defer(trigger=trigger, method_name="execute_complete")
|
|
95
|
+
|
|
96
|
+
def execute_complete(self, context: Context, event: Any = None) -> None:
|
|
97
|
+
"""Handle the event when the trigger fires and return immediately."""
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class WaitSensor(BaseSensorOperator):
|
|
102
|
+
"""
|
|
103
|
+
A sensor that waits a specified period of time before completing.
|
|
104
|
+
|
|
105
|
+
This differs from TimeDeltaSensor because the time to wait is measured from the start of the task, not
|
|
106
|
+
the data_interval_end of the DAG run.
|
|
107
|
+
|
|
108
|
+
:param time_to_wait: time length to wait after the task starts before succeeding.
|
|
109
|
+
:param deferrable: Run sensor in deferrable mode
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
time_to_wait: timedelta | int,
|
|
115
|
+
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
|
|
116
|
+
**kwargs,
|
|
117
|
+
) -> None:
|
|
118
|
+
super().__init__(**kwargs)
|
|
119
|
+
self.deferrable = deferrable
|
|
120
|
+
if isinstance(time_to_wait, int):
|
|
121
|
+
self.time_to_wait = timedelta(minutes=time_to_wait)
|
|
122
|
+
else:
|
|
123
|
+
self.time_to_wait = time_to_wait
|
|
124
|
+
|
|
125
|
+
def execute(self, context: Context) -> None:
|
|
126
|
+
if self.deferrable:
|
|
127
|
+
self.defer(
|
|
128
|
+
trigger=TimeDeltaTrigger(self.time_to_wait, end_from_trigger=True)
|
|
129
|
+
if AIRFLOW_V_3_0_PLUS
|
|
130
|
+
else TimeDeltaTrigger(self.time_to_wait),
|
|
131
|
+
method_name="execute_complete",
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
sleep(int(self.time_to_wait.total_seconds()))
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
# or more contributor license agreements. See the NOTICE file
|
|
4
|
+
# distributed with this work for additional information
|
|
5
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
# to you under the Apache License, Version 2.0 (the
|
|
7
|
+
# "License"); you may not use this file except in compliance
|
|
8
|
+
# with the License. You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing,
|
|
13
|
+
# software distributed under the License is distributed on an
|
|
14
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
# KIND, either express or implied. See the License for the
|
|
16
|
+
# specific language governing permissions and limitations
|
|
17
|
+
# under the License.
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING, Iterable
|
|
21
|
+
|
|
22
|
+
from airflow.sensors.base import BaseSensorOperator
|
|
23
|
+
from airflow.utils import timezone
|
|
24
|
+
from airflow.utils.weekday import WeekDay
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from airflow.utils.context import Context
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DayOfWeekSensor(BaseSensorOperator):
|
|
31
|
+
"""
|
|
32
|
+
Waits until the first specified day of the week.
|
|
33
|
+
|
|
34
|
+
For example, if the execution day of the task is '2018-12-22' (Saturday)
|
|
35
|
+
and you pass 'FRIDAY', the task will wait until next Friday.
|
|
36
|
+
|
|
37
|
+
**Example** (with single day): ::
|
|
38
|
+
|
|
39
|
+
weekend_check = DayOfWeekSensor(
|
|
40
|
+
task_id="weekend_check", week_day="Saturday", use_task_logical_date=True, dag=dag
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
**Example** (with multiple day using set): ::
|
|
44
|
+
|
|
45
|
+
weekend_check = DayOfWeekSensor(
|
|
46
|
+
task_id="weekend_check", week_day={"Saturday", "Sunday"}, use_task_logical_date=True, dag=dag
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
**Example** (with :class:`~airflow.utils.weekday.WeekDay` enum): ::
|
|
50
|
+
|
|
51
|
+
# import WeekDay Enum
|
|
52
|
+
from airflow.utils.weekday import WeekDay
|
|
53
|
+
|
|
54
|
+
weekend_check = DayOfWeekSensor(
|
|
55
|
+
task_id="weekend_check",
|
|
56
|
+
week_day={WeekDay.SATURDAY, WeekDay.SUNDAY},
|
|
57
|
+
use_task_logical_date=True,
|
|
58
|
+
dag=dag,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
:param week_day: Day of the week to check (full name). Optionally, a set
|
|
62
|
+
of days can also be provided using a set.
|
|
63
|
+
Example values:
|
|
64
|
+
|
|
65
|
+
* ``"MONDAY"``,
|
|
66
|
+
* ``{"Saturday", "Sunday"}``
|
|
67
|
+
* ``{WeekDay.TUESDAY}``
|
|
68
|
+
* ``{WeekDay.SATURDAY, WeekDay.SUNDAY}``
|
|
69
|
+
|
|
70
|
+
To use ``WeekDay`` enum, import it from ``airflow.utils.weekday``
|
|
71
|
+
|
|
72
|
+
:param use_task_logical_date: If ``True``, uses task's logical date to compare
|
|
73
|
+
with week_day. Execution Date is Useful for backfilling.
|
|
74
|
+
If ``False``, uses system's day of the week. Useful when you
|
|
75
|
+
don't want to run anything on weekdays on the system.
|
|
76
|
+
|
|
77
|
+
.. seealso::
|
|
78
|
+
For more information on how to use this sensor, take a look at the guide:
|
|
79
|
+
:ref:`howto/operator:DayOfWeekSensor`
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
*,
|
|
86
|
+
week_day: str | Iterable[str] | WeekDay | Iterable[WeekDay],
|
|
87
|
+
use_task_logical_date: bool = False,
|
|
88
|
+
**kwargs,
|
|
89
|
+
) -> None:
|
|
90
|
+
super().__init__(**kwargs)
|
|
91
|
+
self.week_day = week_day
|
|
92
|
+
self.use_task_logical_date = use_task_logical_date
|
|
93
|
+
self._week_day_num = WeekDay.validate_week_day(week_day)
|
|
94
|
+
|
|
95
|
+
def poke(self, context: Context) -> bool:
|
|
96
|
+
self.log.info(
|
|
97
|
+
"Poking until weekday is in %s, Today is %s",
|
|
98
|
+
self.week_day,
|
|
99
|
+
WeekDay(timezone.utcnow().isoweekday()).name,
|
|
100
|
+
)
|
|
101
|
+
if self.use_task_logical_date:
|
|
102
|
+
return context["logical_date"].isoweekday() in self._week_day_num
|
|
103
|
+
else:
|
|
104
|
+
return timezone.utcnow().isoweekday() in self._week_day_num
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
# or more contributor license agreements. See the NOTICE file
|
|
4
|
+
# distributed with this work for additional information
|
|
5
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
# to you under the Apache License, Version 2.0 (the
|
|
7
|
+
# "License"); you may not use this file except in compliance
|
|
8
|
+
# with the License. You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing,
|
|
13
|
+
# software distributed under the License is distributed on an
|
|
14
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
# KIND, either express or implied. See the License for the
|
|
16
|
+
# specific language governing permissions and limitations
|
|
17
|
+
# under the License.
|
|
18
|
+
"""Utilities for creating a virtual environment."""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import shutil
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
import jinja2
|
|
28
|
+
from jinja2 import select_autoescape
|
|
29
|
+
|
|
30
|
+
from airflow.configuration import conf
|
|
31
|
+
from airflow.utils.process_utils import execute_in_subprocess
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _is_uv_installed() -> bool:
|
|
35
|
+
"""
|
|
36
|
+
Verify whether the uv tool is installed by checking if it's included in the system PATH or installed as a package.
|
|
37
|
+
|
|
38
|
+
:return: True if it is. Whichever way of checking it works, is fine.
|
|
39
|
+
"""
|
|
40
|
+
return bool(shutil.which("uv"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _use_uv() -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Check if the uv tool should be used.
|
|
46
|
+
|
|
47
|
+
:return: True if uv should be used.
|
|
48
|
+
"""
|
|
49
|
+
venv_install_method = conf.get("standard", "venv_install_method", fallback="auto").lower()
|
|
50
|
+
if venv_install_method == "auto":
|
|
51
|
+
return _is_uv_installed()
|
|
52
|
+
elif venv_install_method == "uv":
|
|
53
|
+
return True
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _generate_uv_cmd(tmp_dir: str, python_bin: str, system_site_packages: bool) -> list[str]:
|
|
58
|
+
"""Build the command to install the venv via UV."""
|
|
59
|
+
cmd = ["uv", "venv", "--allow-existing", "--seed"]
|
|
60
|
+
if python_bin is not None:
|
|
61
|
+
cmd += ["--python", python_bin]
|
|
62
|
+
if system_site_packages:
|
|
63
|
+
cmd.append("--system-site-packages")
|
|
64
|
+
cmd.append(tmp_dir)
|
|
65
|
+
return cmd
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _generate_venv_cmd(tmp_dir: str, python_bin: str, system_site_packages: bool) -> list[str]:
|
|
69
|
+
"""We are using venv command instead of venv module to allow creation of venv for different python versions."""
|
|
70
|
+
if python_bin is None:
|
|
71
|
+
python_bin = sys.executable
|
|
72
|
+
cmd = [python_bin, "-m", "venv", tmp_dir]
|
|
73
|
+
if system_site_packages:
|
|
74
|
+
cmd.append("--system-site-packages")
|
|
75
|
+
return cmd
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _generate_uv_install_cmd_from_file(
|
|
79
|
+
tmp_dir: str, requirements_file_path: str, pip_install_options: list[str]
|
|
80
|
+
) -> list[str]:
|
|
81
|
+
return [
|
|
82
|
+
"uv",
|
|
83
|
+
"pip",
|
|
84
|
+
"install",
|
|
85
|
+
"--python",
|
|
86
|
+
f"{tmp_dir}/bin/python",
|
|
87
|
+
*pip_install_options,
|
|
88
|
+
"-r",
|
|
89
|
+
requirements_file_path,
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _generate_pip_install_cmd_from_file(
|
|
94
|
+
tmp_dir: str, requirements_file_path: str, pip_install_options: list[str]
|
|
95
|
+
) -> list[str]:
|
|
96
|
+
return [f"{tmp_dir}/bin/pip", "install", *pip_install_options, "-r", requirements_file_path]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _generate_uv_install_cmd_from_list(
|
|
100
|
+
tmp_dir: str, requirements: list[str], pip_install_options: list[str]
|
|
101
|
+
) -> list[str]:
|
|
102
|
+
return ["uv", "pip", "install", "--python", f"{tmp_dir}/bin/python", *pip_install_options, *requirements]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _generate_pip_install_cmd_from_list(
|
|
106
|
+
tmp_dir: str, requirements: list[str], pip_install_options: list[str]
|
|
107
|
+
) -> list[str]:
|
|
108
|
+
return [f"{tmp_dir}/bin/pip", "install", *pip_install_options, *requirements]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _generate_pip_conf(conf_file: Path, index_urls: list[str]) -> None:
|
|
112
|
+
if index_urls:
|
|
113
|
+
pip_conf_options = f"index-url = {index_urls[0]}"
|
|
114
|
+
if len(index_urls) > 1:
|
|
115
|
+
pip_conf_options += f"\nextra-index-url = {' '.join(x for x in index_urls[1:])}"
|
|
116
|
+
else:
|
|
117
|
+
pip_conf_options = "no-index = true"
|
|
118
|
+
conf_file.write_text(f"[global]\n{pip_conf_options}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def prepare_virtualenv(
|
|
122
|
+
venv_directory: str,
|
|
123
|
+
python_bin: str,
|
|
124
|
+
system_site_packages: bool,
|
|
125
|
+
requirements: list[str] | None = None,
|
|
126
|
+
requirements_file_path: str | None = None,
|
|
127
|
+
pip_install_options: list[str] | None = None,
|
|
128
|
+
index_urls: list[str] | None = None,
|
|
129
|
+
) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Create a virtual environment and install the additional python packages.
|
|
132
|
+
|
|
133
|
+
:param venv_directory: The path for directory where the environment will be created.
|
|
134
|
+
:param python_bin: Path to the Python executable.
|
|
135
|
+
:param system_site_packages: Whether to include system_site_packages in your virtualenv.
|
|
136
|
+
See virtualenv documentation for more information.
|
|
137
|
+
:param requirements: List of additional python packages.
|
|
138
|
+
:param requirements_file_path: Path to the ``requirements.txt`` file.
|
|
139
|
+
:param pip_install_options: a list of pip install options when installing requirements
|
|
140
|
+
See 'pip install -h' for available options
|
|
141
|
+
:param index_urls: an optional list of index urls to load Python packages from.
|
|
142
|
+
If not provided the system pip conf will be used to source packages from.
|
|
143
|
+
:return: Path to a binary file with Python in a virtual environment.
|
|
144
|
+
"""
|
|
145
|
+
if pip_install_options is None:
|
|
146
|
+
pip_install_options = []
|
|
147
|
+
|
|
148
|
+
if requirements is not None and requirements_file_path is not None:
|
|
149
|
+
raise ValueError("Either requirements OR requirements_file_path has to be passed, but not both")
|
|
150
|
+
|
|
151
|
+
if index_urls is not None:
|
|
152
|
+
_generate_pip_conf(Path(venv_directory) / "pip.conf", index_urls)
|
|
153
|
+
|
|
154
|
+
if _use_uv():
|
|
155
|
+
venv_cmd = _generate_uv_cmd(venv_directory, python_bin, system_site_packages)
|
|
156
|
+
else:
|
|
157
|
+
venv_cmd = _generate_venv_cmd(venv_directory, python_bin, system_site_packages)
|
|
158
|
+
execute_in_subprocess(venv_cmd)
|
|
159
|
+
|
|
160
|
+
pip_cmd = None
|
|
161
|
+
if requirements is not None and len(requirements) != 0:
|
|
162
|
+
if _use_uv():
|
|
163
|
+
pip_cmd = _generate_uv_install_cmd_from_list(venv_directory, requirements, pip_install_options)
|
|
164
|
+
else:
|
|
165
|
+
pip_cmd = _generate_pip_install_cmd_from_list(venv_directory, requirements, pip_install_options)
|
|
166
|
+
if requirements_file_path is not None and requirements_file_path:
|
|
167
|
+
if _use_uv():
|
|
168
|
+
pip_cmd = _generate_uv_install_cmd_from_file(
|
|
169
|
+
venv_directory, requirements_file_path, pip_install_options
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
pip_cmd = _generate_pip_install_cmd_from_file(
|
|
173
|
+
venv_directory, requirements_file_path, pip_install_options
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if pip_cmd:
|
|
177
|
+
execute_in_subprocess(pip_cmd)
|
|
178
|
+
|
|
179
|
+
return f"{venv_directory}/bin/python"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def write_python_script(
|
|
183
|
+
jinja_context: dict,
|
|
184
|
+
filename: str,
|
|
185
|
+
render_template_as_native_obj: bool = False,
|
|
186
|
+
):
|
|
187
|
+
"""
|
|
188
|
+
Render the python script to a file to execute in the virtual environment.
|
|
189
|
+
|
|
190
|
+
:param jinja_context: The jinja context variables to unpack and replace with its placeholders in the
|
|
191
|
+
template file.
|
|
192
|
+
:param filename: The name of the file to dump the rendered script to.
|
|
193
|
+
:param render_template_as_native_obj: If ``True``, rendered Jinja template would be converted
|
|
194
|
+
to a native Python object
|
|
195
|
+
"""
|
|
196
|
+
template_loader = jinja2.FileSystemLoader(searchpath=os.path.dirname(__file__))
|
|
197
|
+
template_env: jinja2.Environment
|
|
198
|
+
if render_template_as_native_obj:
|
|
199
|
+
template_env = jinja2.nativetypes.NativeEnvironment(
|
|
200
|
+
loader=template_loader, undefined=jinja2.StrictUndefined
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
template_env = jinja2.Environment(
|
|
204
|
+
loader=template_loader,
|
|
205
|
+
undefined=jinja2.StrictUndefined,
|
|
206
|
+
autoescape=select_autoescape(["html", "xml"]),
|
|
207
|
+
)
|
|
208
|
+
template = template_env.get_template("python_virtualenv_script.jinja2")
|
|
209
|
+
template.stream(**jinja_context).dump(filename)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{#
|
|
2
|
+
Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
or more contributor license agreements. See the NOTICE file
|
|
4
|
+
distributed with this work for additional information
|
|
5
|
+
regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
to you under the Apache License, Version 2.0 (the
|
|
7
|
+
"License"); you may not use this file except in compliance
|
|
8
|
+
with the License. You may obtain a copy of the License at
|
|
9
|
+
|
|
10
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
|
|
12
|
+
Unless required by applicable law or agreed to in writing,
|
|
13
|
+
software distributed under the License is distributed on an
|
|
14
|
+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
KIND, either express or implied. See the License for the
|
|
16
|
+
specific language governing permissions and limitations
|
|
17
|
+
under the License.
|
|
18
|
+
-#}
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import {{ pickling_library }}
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
{% if expect_airflow %}
|
|
25
|
+
{# Check whether Airflow is available in the environment.
|
|
26
|
+
# If it is, we'll want to ensure that we integrate any macros that are being provided
|
|
27
|
+
# by plugins prior to unpickling the task context. #}
|
|
28
|
+
if sys.version_info >= (3,6):
|
|
29
|
+
try:
|
|
30
|
+
from airflow.plugins_manager import integrate_macros_plugins
|
|
31
|
+
integrate_macros_plugins()
|
|
32
|
+
except ImportError:
|
|
33
|
+
{# Airflow is not available in this environment, therefore we won't
|
|
34
|
+
# be able to integrate any plugin macros. #}
|
|
35
|
+
pass
|
|
36
|
+
{% endif %}
|
|
37
|
+
|
|
38
|
+
# Script
|
|
39
|
+
{{ python_callable_source }}
|
|
40
|
+
|
|
41
|
+
# monkey patching for the cases when python_callable is part of the dag module.
|
|
42
|
+
{% if modified_dag_module_name is defined %}
|
|
43
|
+
|
|
44
|
+
import types
|
|
45
|
+
|
|
46
|
+
{{ modified_dag_module_name }} = types.ModuleType("{{ modified_dag_module_name }}")
|
|
47
|
+
|
|
48
|
+
{{ modified_dag_module_name }}.{{ python_callable }} = {{ python_callable }}
|
|
49
|
+
|
|
50
|
+
sys.modules["{{modified_dag_module_name}}"] = {{modified_dag_module_name}}
|
|
51
|
+
|
|
52
|
+
{% endif%}
|
|
53
|
+
|
|
54
|
+
{% if op_args or op_kwargs %}
|
|
55
|
+
with open(sys.argv[1], "rb") as file:
|
|
56
|
+
arg_dict = {{ pickling_library }}.load(file)
|
|
57
|
+
{% else %}
|
|
58
|
+
arg_dict = {"args": [], "kwargs": {}}
|
|
59
|
+
{% endif %}
|
|
60
|
+
|
|
61
|
+
{% if string_args_global | default(true) -%}
|
|
62
|
+
# Read string args
|
|
63
|
+
with open(sys.argv[3], "r") as file:
|
|
64
|
+
virtualenv_string_args = list(map(lambda x: x.strip(), list(file)))
|
|
65
|
+
{% endif %}
|
|
66
|
+
|
|
67
|
+
{% if use_airflow_context | default(false) -%}
|
|
68
|
+
if len(sys.argv) > 5:
|
|
69
|
+
import json
|
|
70
|
+
from types import ModuleType
|
|
71
|
+
|
|
72
|
+
from airflow.providers.standard.operators import python as airflow_python
|
|
73
|
+
from airflow.serialization.serialized_objects import BaseSerialization
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _MockPython(ModuleType):
|
|
77
|
+
@staticmethod
|
|
78
|
+
def get_current_context():
|
|
79
|
+
with open(sys.argv[5]) as file:
|
|
80
|
+
context = json.load(file)
|
|
81
|
+
return BaseSerialization.deserialize(context, use_pydantic_models=True)
|
|
82
|
+
|
|
83
|
+
def __getattr__(self, name: str):
|
|
84
|
+
return getattr(airflow_python, name)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
MockPython = _MockPython("MockPython")
|
|
88
|
+
sys.modules["airflow.providers.standard.operators.python"] = MockPython
|
|
89
|
+
{% endif %}
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
res = {{ python_callable }}(*arg_dict["args"], **arg_dict["kwargs"])
|
|
93
|
+
except Exception as e:
|
|
94
|
+
with open(sys.argv[4], "w") as file:
|
|
95
|
+
file.write(str(e))
|
|
96
|
+
raise
|
|
97
|
+
|
|
98
|
+
# Write output
|
|
99
|
+
with open(sys.argv[2], "wb") as file:
|
|
100
|
+
if res is not None:
|
|
101
|
+
{{ pickling_library }}.dump(res, file)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
# or more contributor license agreements. See the NOTICE file
|
|
4
|
+
# distributed with this work for additional information
|
|
5
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
# to you under the Apache License, Version 2.0 (the
|
|
7
|
+
# "License"); you may not use this file except in compliance
|
|
8
|
+
# with the License. You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing,
|
|
13
|
+
# software distributed under the License is distributed on an
|
|
14
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
# KIND, either express or implied. See the License for the
|
|
16
|
+
# specific language governing permissions and limitations
|
|
17
|
+
# under the License.
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from packaging.version import Version
|
|
21
|
+
|
|
22
|
+
from airflow import __version__ as airflow_version
|
|
23
|
+
|
|
24
|
+
AIRFLOW_VERSION = Version(airflow_version)
|
|
25
|
+
AIRFLOW_V_2_10_PLUS = Version(AIRFLOW_VERSION.base_version) >= Version("2.10.0")
|
|
26
|
+
AIRFLOW_V_3_0_PLUS = Version(AIRFLOW_VERSION.base_version) >= Version("3.0.0")
|