flyte 2.0.0b3__py3-none-any.whl → 2.0.0b5__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 flyte might be problematic. Click here for more details.
- flyte/_code_bundle/bundle.py +2 -0
- flyte/_deploy.py +71 -50
- flyte/_environment.py +14 -2
- flyte/_image.py +102 -50
- flyte/_internal/controllers/remote/_controller.py +29 -21
- flyte/_internal/controllers/remote/_core.py +1 -4
- flyte/_internal/imagebuild/docker_builder.py +96 -12
- flyte/_internal/imagebuild/remote_builder.py +12 -2
- flyte/_internal/runtime/convert.py +3 -5
- flyte/_run.py +48 -14
- flyte/_secret.py +3 -2
- flyte/_task.py +13 -0
- flyte/_utils/__init__.py +2 -0
- flyte/_utils/module_loader.py +89 -0
- flyte/_version.py +2 -2
- flyte/cli/_common.py +5 -1
- flyte/cli/_deploy.py +120 -12
- flyte/cli/_run.py +1 -1
- flyte/errors.py +10 -0
- flyte/models.py +9 -1
- flyte/report/_report.py +5 -1
- {flyte-2.0.0b3.dist-info → flyte-2.0.0b5.dist-info}/METADATA +3 -3
- {flyte-2.0.0b3.dist-info → flyte-2.0.0b5.dist-info}/RECORD +28 -27
- {flyte-2.0.0b3.data → flyte-2.0.0b5.data}/scripts/runtime.py +0 -0
- {flyte-2.0.0b3.dist-info → flyte-2.0.0b5.dist-info}/WHEEL +0 -0
- {flyte-2.0.0b3.dist-info → flyte-2.0.0b5.dist-info}/entry_points.txt +0 -0
- {flyte-2.0.0b3.dist-info → flyte-2.0.0b5.dist-info}/licenses/LICENSE +0 -0
- {flyte-2.0.0b3.dist-info → flyte-2.0.0b5.dist-info}/top_level.txt +0 -0
|
@@ -11,6 +11,7 @@ from typing import ClassVar, Optional, Protocol, cast
|
|
|
11
11
|
import aiofiles
|
|
12
12
|
import click
|
|
13
13
|
|
|
14
|
+
from flyte import Secret
|
|
14
15
|
from flyte._image import (
|
|
15
16
|
AptPackages,
|
|
16
17
|
Commands,
|
|
@@ -19,12 +20,15 @@ from flyte._image import (
|
|
|
19
20
|
Env,
|
|
20
21
|
Image,
|
|
21
22
|
Layer,
|
|
23
|
+
PipOption,
|
|
22
24
|
PipPackages,
|
|
23
25
|
PythonWheels,
|
|
24
26
|
Requirements,
|
|
25
27
|
UVProject,
|
|
28
|
+
UVScript,
|
|
26
29
|
WorkDir,
|
|
27
30
|
_DockerLines,
|
|
31
|
+
_ensure_tuple,
|
|
28
32
|
)
|
|
29
33
|
from flyte._internal.imagebuild.image_builder import (
|
|
30
34
|
DockerAPIImageChecker,
|
|
@@ -44,6 +48,7 @@ WORKDIR /root
|
|
|
44
48
|
RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
|
|
45
49
|
--mount=type=bind,target=uv.lock,src=uv.lock \
|
|
46
50
|
--mount=type=bind,target=pyproject.toml,src=pyproject.toml \
|
|
51
|
+
$SECRET_MOUNT \
|
|
47
52
|
uv sync $PIP_INSTALL_ARGS
|
|
48
53
|
WORKDIR /
|
|
49
54
|
|
|
@@ -56,31 +61,35 @@ ENV PATH="/root/.venv/bin:$$PATH" \
|
|
|
56
61
|
UV_PACKAGE_INSTALL_COMMAND_TEMPLATE = Template("""\
|
|
57
62
|
RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
|
|
58
63
|
--mount=type=bind,target=requirements_uv.txt,src=requirements_uv.txt \
|
|
59
|
-
|
|
64
|
+
$SECRET_MOUNT \
|
|
65
|
+
uv pip install --python $$UV_PYTHON $PIP_INSTALL_ARGS
|
|
60
66
|
""")
|
|
61
67
|
|
|
62
68
|
UV_WHEEL_INSTALL_COMMAND_TEMPLATE = Template("""\
|
|
63
69
|
RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=wheel \
|
|
64
70
|
--mount=source=/dist,target=/dist,type=bind \
|
|
65
|
-
|
|
71
|
+
$SECRET_MOUNT \
|
|
72
|
+
uv pip install --python $$UV_PYTHON $PIP_INSTALL_ARGS
|
|
66
73
|
""")
|
|
67
74
|
|
|
68
75
|
APT_INSTALL_COMMAND_TEMPLATE = Template("""\
|
|
69
76
|
RUN --mount=type=cache,sharing=locked,mode=0777,target=/var/cache/apt,id=apt \
|
|
77
|
+
$SECRET_MOUNT \
|
|
70
78
|
apt-get update && apt-get install -y --no-install-recommends \
|
|
71
79
|
$APT_PACKAGES
|
|
72
80
|
""")
|
|
73
81
|
|
|
74
82
|
UV_PYTHON_INSTALL_COMMAND = Template("""\
|
|
75
83
|
RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
|
|
84
|
+
$SECRET_MOUNT \
|
|
76
85
|
uv pip install $PIP_INSTALL_ARGS
|
|
77
86
|
""")
|
|
78
87
|
|
|
79
88
|
# uv pip install --python /root/env/bin/python
|
|
80
89
|
# new template
|
|
81
90
|
DOCKER_FILE_UV_BASE_TEMPLATE = Template("""\
|
|
82
|
-
#syntax=docker/dockerfile:1.
|
|
83
|
-
FROM ghcr.io/astral-sh/uv:0.6.12
|
|
91
|
+
# syntax=docker/dockerfile:1.10
|
|
92
|
+
FROM ghcr.io/astral-sh/uv:0.6.12 AS uv
|
|
84
93
|
FROM $BASE_IMAGE
|
|
85
94
|
|
|
86
95
|
USER root
|
|
@@ -139,8 +148,10 @@ class PipAndRequirementsHandler:
|
|
|
139
148
|
|
|
140
149
|
pip_install_args = layer.get_pip_install_args()
|
|
141
150
|
pip_install_args.extend(["--requirement", "requirements_uv.txt"])
|
|
142
|
-
|
|
143
|
-
delta = UV_PACKAGE_INSTALL_COMMAND_TEMPLATE.substitute(
|
|
151
|
+
secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
|
|
152
|
+
delta = UV_PACKAGE_INSTALL_COMMAND_TEMPLATE.substitute(
|
|
153
|
+
PIP_INSTALL_ARGS=" ".join(pip_install_args), SECRET_MOUNT=secret_mounts
|
|
154
|
+
)
|
|
144
155
|
dockerfile += delta
|
|
145
156
|
|
|
146
157
|
return dockerfile
|
|
@@ -152,8 +163,10 @@ class PythonWheelHandler:
|
|
|
152
163
|
shutil.copytree(layer.wheel_dir, context_path / "dist", dirs_exist_ok=True)
|
|
153
164
|
pip_install_args = layer.get_pip_install_args()
|
|
154
165
|
pip_install_args.extend(["/dist/*.whl"])
|
|
155
|
-
|
|
156
|
-
delta = UV_WHEEL_INSTALL_COMMAND_TEMPLATE.substitute(
|
|
166
|
+
secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
|
|
167
|
+
delta = UV_WHEEL_INSTALL_COMMAND_TEMPLATE.substitute(
|
|
168
|
+
PIP_INSTALL_ARGS=" ".join(pip_install_args), SECRET_MOUNT=secret_mounts
|
|
169
|
+
)
|
|
157
170
|
dockerfile += delta
|
|
158
171
|
|
|
159
172
|
return dockerfile
|
|
@@ -181,9 +194,10 @@ class EnvHandler:
|
|
|
181
194
|
|
|
182
195
|
class AptPackagesHandler:
|
|
183
196
|
@staticmethod
|
|
184
|
-
async def handle(layer: AptPackages,
|
|
197
|
+
async def handle(layer: AptPackages, _: Path, dockerfile: str) -> str:
|
|
185
198
|
packages = layer.packages
|
|
186
|
-
|
|
199
|
+
secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
|
|
200
|
+
delta = APT_INSTALL_COMMAND_TEMPLATE.substitute(APT_PACKAGES=" ".join(packages), SECRET_MOUNT=secret_mounts)
|
|
187
201
|
dockerfile += delta
|
|
188
202
|
|
|
189
203
|
return dockerfile
|
|
@@ -200,7 +214,10 @@ class UVProjectHandler:
|
|
|
200
214
|
# --no-dev: Omit the development dependency group
|
|
201
215
|
# --no-install-project: Do not install the current project
|
|
202
216
|
additional_pip_install_args = ["--locked", "--no-dev", "--no-install-project"]
|
|
203
|
-
|
|
217
|
+
secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
|
|
218
|
+
delta = UV_LOCK_INSTALL_TEMPLATE.substitute(
|
|
219
|
+
PIP_INSTALL_ARGS=" ".join(additional_pip_install_args), SECRET_MOUNT=secret_mounts
|
|
220
|
+
)
|
|
204
221
|
dockerfile += delta
|
|
205
222
|
|
|
206
223
|
return dockerfile
|
|
@@ -250,19 +267,82 @@ class CommandsHandler:
|
|
|
250
267
|
|
|
251
268
|
class WorkDirHandler:
|
|
252
269
|
@staticmethod
|
|
253
|
-
async def handle(layer: WorkDir,
|
|
270
|
+
async def handle(layer: WorkDir, _: Path, dockerfile: str) -> str:
|
|
254
271
|
# cd to the workdir
|
|
255
272
|
dockerfile += f"\nWORKDIR {layer.workdir}\n"
|
|
256
273
|
|
|
257
274
|
return dockerfile
|
|
258
275
|
|
|
259
276
|
|
|
277
|
+
def _get_secret_commands(layers: typing.Tuple[Layer, ...]) -> typing.List[str]:
|
|
278
|
+
commands = []
|
|
279
|
+
|
|
280
|
+
def _get_secret_command(secret: str | Secret) -> typing.List[str]:
|
|
281
|
+
secret_id = hash(secret)
|
|
282
|
+
if isinstance(secret, str):
|
|
283
|
+
if not os.path.exists(secret):
|
|
284
|
+
raise FileNotFoundError(f"Secret file '{secret}' not found")
|
|
285
|
+
return ["--secret", f"id={secret_id},src={secret}"]
|
|
286
|
+
secret_env_key = "_".join([k.upper() for k in filter(None, (secret.group, secret.key))])
|
|
287
|
+
secret_env = os.getenv(secret_env_key)
|
|
288
|
+
if secret_env:
|
|
289
|
+
return ["--secret", f"id={secret_id},env={secret_env}"]
|
|
290
|
+
secret_file_name = "_".join(list(filter(None, (secret.group, secret.key))))
|
|
291
|
+
secret_file_path = f"/etc/secrets/{secret_file_name}"
|
|
292
|
+
if not os.path.exists(secret_file_path):
|
|
293
|
+
raise FileNotFoundError(f"Secret not found in Env Var {secret_env_key} or file {secret_file_path}")
|
|
294
|
+
return ["--secret", f"id={secret_id},src={secret_file_path}"]
|
|
295
|
+
|
|
296
|
+
for layer in layers:
|
|
297
|
+
if isinstance(layer, (PipOption, AptPackages)):
|
|
298
|
+
if layer.secret_mounts:
|
|
299
|
+
for secret_mount in layer.secret_mounts:
|
|
300
|
+
commands.extend(_get_secret_command(secret_mount))
|
|
301
|
+
return commands
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _get_secret_mounts_layer(secrets: typing.Tuple[str | Secret, ...] | None) -> str:
|
|
305
|
+
if secrets is None:
|
|
306
|
+
return ""
|
|
307
|
+
secret_mounts_layer = ""
|
|
308
|
+
for secret in secrets:
|
|
309
|
+
secret_id = hash(secret)
|
|
310
|
+
if isinstance(secret, str):
|
|
311
|
+
secret_mounts_layer += f"--mount=type=secret,id={secret_id},target=/run/secrets/{os.path.basename(secret)}"
|
|
312
|
+
elif isinstance(secret, Secret):
|
|
313
|
+
if secret.mount:
|
|
314
|
+
secret_mounts_layer += f"--mount=type=secret,id={secret_id},target={secret.mount}"
|
|
315
|
+
elif secret.as_env_var:
|
|
316
|
+
secret_mounts_layer += f"--mount=type=secret,id={secret_id},env={secret.as_env_var}"
|
|
317
|
+
else:
|
|
318
|
+
secret_file_name = "_".join(list(filter(None, (secret.group, secret.key))))
|
|
319
|
+
secret_mounts_layer += f"--mount=type=secret,id={secret_id},src=/run/secrets/{secret_file_name}"
|
|
320
|
+
|
|
321
|
+
return secret_mounts_layer
|
|
322
|
+
|
|
323
|
+
|
|
260
324
|
async def _process_layer(layer: Layer, context_path: Path, dockerfile: str) -> str:
|
|
261
325
|
match layer:
|
|
262
326
|
case PythonWheels():
|
|
263
327
|
# Handle Python wheels
|
|
264
328
|
dockerfile = await PythonWheelHandler.handle(layer, context_path, dockerfile)
|
|
265
329
|
|
|
330
|
+
case UVScript():
|
|
331
|
+
# Handle UV script
|
|
332
|
+
from flyte._utils import parse_uv_script_file
|
|
333
|
+
|
|
334
|
+
header = parse_uv_script_file(layer.script)
|
|
335
|
+
if header.dependencies:
|
|
336
|
+
pip = PipPackages(
|
|
337
|
+
packages=_ensure_tuple(header.dependencies) if header.dependencies else None,
|
|
338
|
+
secret_mounts=layer.secret_mounts,
|
|
339
|
+
index_url=layer.index_url,
|
|
340
|
+
extra_args=layer.extra_args,
|
|
341
|
+
pre=layer.pre,
|
|
342
|
+
extra_index_urls=layer.extra_index_urls,
|
|
343
|
+
)
|
|
344
|
+
dockerfile = await PipAndRequirementsHandler.handle(pip, context_path, dockerfile)
|
|
345
|
+
|
|
266
346
|
case Requirements() | PipPackages():
|
|
267
347
|
# Handle pip packages and requirements
|
|
268
348
|
dockerfile = await PipAndRequirementsHandler.handle(layer, context_path, dockerfile)
|
|
@@ -357,6 +437,8 @@ class DockerImageBuilder(ImageBuilder):
|
|
|
357
437
|
else:
|
|
358
438
|
command.append("--load")
|
|
359
439
|
|
|
440
|
+
command.extend(_get_secret_commands(layers=image._layers))
|
|
441
|
+
|
|
360
442
|
concat_command = " ".join(command)
|
|
361
443
|
logger.debug(f"Build command: {concat_command}")
|
|
362
444
|
click.secho(f"Run command: {concat_command} ", fg="blue")
|
|
@@ -464,6 +546,8 @@ class DockerImageBuilder(ImageBuilder):
|
|
|
464
546
|
command.append("--push")
|
|
465
547
|
else:
|
|
466
548
|
command.append("--load")
|
|
549
|
+
|
|
550
|
+
command.extend(_get_secret_commands(layers=image._layers))
|
|
467
551
|
command.append(tmp_dir)
|
|
468
552
|
|
|
469
553
|
concat_command = " ".join(command)
|
|
@@ -23,6 +23,7 @@ from flyte._image import (
|
|
|
23
23
|
PythonWheels,
|
|
24
24
|
Requirements,
|
|
25
25
|
UVProject,
|
|
26
|
+
UVScript,
|
|
26
27
|
)
|
|
27
28
|
from flyte._internal.imagebuild.image_builder import ImageBuilder, ImageChecker
|
|
28
29
|
from flyte._logging import logger
|
|
@@ -196,10 +197,19 @@ def _get_layers_proto(image: Image, context_path: Path) -> "image_definition_pb2
|
|
|
196
197
|
)
|
|
197
198
|
)
|
|
198
199
|
layers.append(requirements_layer)
|
|
199
|
-
elif isinstance(layer, PipPackages):
|
|
200
|
+
elif isinstance(layer, PipPackages) or isinstance(layer, UVScript):
|
|
201
|
+
if isinstance(layer, UVScript):
|
|
202
|
+
from flyte._utils import parse_uv_script_file
|
|
203
|
+
|
|
204
|
+
header = parse_uv_script_file(layer.script)
|
|
205
|
+
if not header.dependencies:
|
|
206
|
+
continue
|
|
207
|
+
packages: typing.Iterable[str] = header.dependencies
|
|
208
|
+
else:
|
|
209
|
+
packages = layer.packages or []
|
|
200
210
|
pip_layer = image_definition_pb2.Layer(
|
|
201
211
|
pip_packages=image_definition_pb2.PipPackages(
|
|
202
|
-
packages=
|
|
212
|
+
packages=packages,
|
|
203
213
|
options=image_definition_pb2.PipOptions(
|
|
204
214
|
index_url=layer.index_url,
|
|
205
215
|
extra_index_urls=layer.extra_index_urls,
|
|
@@ -105,11 +105,9 @@ def is_optional_type(tp) -> bool:
|
|
|
105
105
|
async def convert_from_native_to_inputs(interface: NativeInterface, *args, **kwargs) -> Inputs:
|
|
106
106
|
kwargs = interface.convert_to_kwargs(*args, **kwargs)
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
f"Please provide all required inputs. Inputs received: {kwargs}, interface: {interface}"
|
|
112
|
-
)
|
|
108
|
+
missing = [key for key in interface.required_inputs() if key not in kwargs]
|
|
109
|
+
if missing:
|
|
110
|
+
raise ValueError(f"Missing required inputs: {', '.join(missing)}")
|
|
113
111
|
|
|
114
112
|
if len(interface.inputs) == 0:
|
|
115
113
|
return Inputs.empty()
|
flyte/_run.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import pathlib
|
|
5
5
|
import uuid
|
|
6
|
+
from dataclasses import dataclass
|
|
6
7
|
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union, cast
|
|
7
8
|
|
|
8
9
|
import flyte.errors
|
|
@@ -36,10 +37,26 @@ if TYPE_CHECKING:
|
|
|
36
37
|
from flyte.remote._task import LazyEntity
|
|
37
38
|
|
|
38
39
|
from ._code_bundle import CopyFiles
|
|
40
|
+
from ._internal.imagebuild.image_builder import ImageCache
|
|
39
41
|
|
|
40
42
|
Mode = Literal["local", "remote", "hybrid"]
|
|
41
43
|
|
|
42
44
|
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class _CacheKey:
|
|
47
|
+
obj_id: int
|
|
48
|
+
dry_run: bool
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class _CacheValue:
|
|
53
|
+
code_bundle: CodeBundle | None
|
|
54
|
+
image_cache: Optional[ImageCache]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_RUN_CACHE: Dict[_CacheKey, _CacheValue] = {}
|
|
58
|
+
|
|
59
|
+
|
|
43
60
|
async def _get_code_bundle_for_run(name: str) -> CodeBundle | None:
|
|
44
61
|
"""
|
|
45
62
|
Get the code bundle for the run with the given name.
|
|
@@ -78,6 +95,7 @@ class _Runner:
|
|
|
78
95
|
annotations: Dict[str, str] | None = None,
|
|
79
96
|
interruptible: bool = False,
|
|
80
97
|
log_level: int | None = None,
|
|
98
|
+
disable_run_cache: bool = False,
|
|
81
99
|
):
|
|
82
100
|
init_config = _get_init_config()
|
|
83
101
|
client = init_config.client if init_config else None
|
|
@@ -104,6 +122,7 @@ class _Runner:
|
|
|
104
122
|
self._annotations = annotations
|
|
105
123
|
self._interruptible = interruptible
|
|
106
124
|
self._log_level = log_level
|
|
125
|
+
self._disable_run_cache = disable_run_cache
|
|
107
126
|
|
|
108
127
|
@requires_initialization
|
|
109
128
|
async def _run_remote(self, obj: TaskTemplate[P, R] | LazyEntity, *args: P.args, **kwargs: P.kwargs) -> Run:
|
|
@@ -135,24 +154,36 @@ class _Runner:
|
|
|
135
154
|
if obj.parent_env is None:
|
|
136
155
|
raise ValueError("Task is not attached to an environment. Please attach the task to an environment")
|
|
137
156
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
)
|
|
157
|
+
if (
|
|
158
|
+
not self._disable_run_cache
|
|
159
|
+
and _RUN_CACHE.get(_CacheKey(obj_id=id(obj), dry_run=self._dry_run)) is not None
|
|
160
|
+
):
|
|
161
|
+
cached_value = _RUN_CACHE[_CacheKey(obj_id=id(obj), dry_run=self._dry_run)]
|
|
162
|
+
code_bundle = cached_value.code_bundle
|
|
163
|
+
image_cache = cached_value.image_cache
|
|
146
164
|
else:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
165
|
+
image_cache = await build_images.aio(cast(Environment, obj.parent_env()))
|
|
166
|
+
|
|
167
|
+
if self._interactive_mode:
|
|
168
|
+
code_bundle = await build_pkl_bundle(
|
|
169
|
+
obj,
|
|
170
|
+
upload_to_controlplane=not self._dry_run,
|
|
151
171
|
copy_bundle_to=self._copy_bundle_to,
|
|
152
|
-
copy_style=self._copy_files,
|
|
153
172
|
)
|
|
154
173
|
else:
|
|
155
|
-
|
|
174
|
+
if self._copy_files != "none":
|
|
175
|
+
code_bundle = await build_code_bundle(
|
|
176
|
+
from_dir=cfg.root_dir,
|
|
177
|
+
dryrun=self._dry_run,
|
|
178
|
+
copy_bundle_to=self._copy_bundle_to,
|
|
179
|
+
copy_style=self._copy_files,
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
code_bundle = None
|
|
183
|
+
if not self._disable_run_cache:
|
|
184
|
+
_RUN_CACHE[_CacheKey(obj_id=id(obj), dry_run=self._dry_run)] = _CacheValue(
|
|
185
|
+
code_bundle=code_bundle, image_cache=image_cache
|
|
186
|
+
)
|
|
156
187
|
|
|
157
188
|
version = self._version or (
|
|
158
189
|
code_bundle.computed_version if code_bundle and code_bundle.computed_version else None
|
|
@@ -516,6 +547,7 @@ def with_runcontext(
|
|
|
516
547
|
annotations: Dict[str, str] | None = None,
|
|
517
548
|
interruptible: bool = False,
|
|
518
549
|
log_level: int | None = None,
|
|
550
|
+
disable_run_cache: bool = False,
|
|
519
551
|
) -> _Runner:
|
|
520
552
|
"""
|
|
521
553
|
Launch a new run with the given parameters as the context.
|
|
@@ -556,6 +588,7 @@ def with_runcontext(
|
|
|
556
588
|
:param interruptible: Optional If true, the run can be interrupted by the user.
|
|
557
589
|
:param log_level: Optional Log level to set for the run. If not provided, it will be set to the default log level
|
|
558
590
|
set using `flyte.init()`
|
|
591
|
+
:param disable_run_cache: Optional If true, the run cache will be disabled. This is useful for testing purposes.
|
|
559
592
|
|
|
560
593
|
:return: runner
|
|
561
594
|
"""
|
|
@@ -580,6 +613,7 @@ def with_runcontext(
|
|
|
580
613
|
project=project,
|
|
581
614
|
domain=domain,
|
|
582
615
|
log_level=log_level,
|
|
616
|
+
disable_run_cache=disable_run_cache,
|
|
583
617
|
)
|
|
584
618
|
|
|
585
619
|
|
flyte/_secret.py
CHANGED
|
@@ -7,8 +7,9 @@ from typing import List, Optional, Union
|
|
|
7
7
|
@dataclass
|
|
8
8
|
class Secret:
|
|
9
9
|
"""
|
|
10
|
-
Secrets are used to inject sensitive information into tasks
|
|
11
|
-
|
|
10
|
+
Secrets are used to inject sensitive information into tasks or image build context.
|
|
11
|
+
Secrets can be mounted as environment variables or files.
|
|
12
|
+
The secret key is the name of the secret in the secret store. The group is optional and maybe used with some
|
|
12
13
|
secret stores to organize secrets. The secret_mount is used to specify how the secret should be mounted. If the
|
|
13
14
|
secret_mount is set to "env" the secret will be mounted as an environment variable. If the secret_mount is set to
|
|
14
15
|
"file" the secret will be mounted as a file. The as_env_var is an optional parameter that can be used to specify the
|
flyte/_task.py
CHANGED
|
@@ -150,6 +150,10 @@ class TaskTemplate(Generic[P, R]):
|
|
|
150
150
|
self.__dict__.update(state)
|
|
151
151
|
self.parent_env = None
|
|
152
152
|
|
|
153
|
+
@property
|
|
154
|
+
def source_file(self) -> Optional[str]:
|
|
155
|
+
return None
|
|
156
|
+
|
|
153
157
|
async def pre(self, *args, **kwargs) -> Dict[str, Any]:
|
|
154
158
|
"""
|
|
155
159
|
This is the preexecute function that will be
|
|
@@ -395,6 +399,15 @@ class AsyncFunctionTaskTemplate(TaskTemplate[P, R]):
|
|
|
395
399
|
if not iscoroutinefunction(self.func):
|
|
396
400
|
self._call_as_synchronous = True
|
|
397
401
|
|
|
402
|
+
@property
|
|
403
|
+
def source_file(self) -> Optional[str]:
|
|
404
|
+
"""
|
|
405
|
+
Returns the source file of the function, if available. This is useful for debugging and tracing.
|
|
406
|
+
"""
|
|
407
|
+
if hasattr(self.func, "__code__") and self.func.__code__:
|
|
408
|
+
return self.func.__code__.co_filename
|
|
409
|
+
return None
|
|
410
|
+
|
|
398
411
|
def forward(self, *args: P.args, **kwargs: P.kwargs) -> Coroutine[Any, Any, R] | R:
|
|
399
412
|
# In local execution, we want to just call the function. Note we're not awaiting anything here.
|
|
400
413
|
# If the function was a coroutine function, the coroutine is returned and the await that the caller has
|
flyte/_utils/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from .coro_management import run_coros
|
|
|
9
9
|
from .file_handling import filehash_update, update_hasher_for_source
|
|
10
10
|
from .helpers import get_cwd_editable_install
|
|
11
11
|
from .lazy_module import lazy_module
|
|
12
|
+
from .module_loader import load_python_modules
|
|
12
13
|
from .org_discovery import hostname_from_url, org_from_endpoint, sanitize_endpoint
|
|
13
14
|
from .uv_script_parser import parse_uv_script_file
|
|
14
15
|
|
|
@@ -18,6 +19,7 @@ __all__ = [
|
|
|
18
19
|
"get_cwd_editable_install",
|
|
19
20
|
"hostname_from_url",
|
|
20
21
|
"lazy_module",
|
|
22
|
+
"load_python_modules",
|
|
21
23
|
"org_from_endpoint",
|
|
22
24
|
"parse_uv_script_file",
|
|
23
25
|
"run_coros",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import importlib.util
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Tuple
|
|
7
|
+
|
|
8
|
+
from rich.progress import BarColumn, Progress, TextColumn, TimeElapsedColumn, TimeRemainingColumn
|
|
9
|
+
|
|
10
|
+
import flyte.errors
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_python_modules(path: Path, recursive: bool = False) -> Tuple[List[str], List[Tuple[Path, str]]]:
|
|
14
|
+
"""
|
|
15
|
+
Load all Python modules from a path and return list of loaded module names.
|
|
16
|
+
|
|
17
|
+
:param path: File or directory path
|
|
18
|
+
:param recursive: If True, load modules recursively from subdirectories
|
|
19
|
+
:return: List of loaded module names, and list of file paths that failed to load
|
|
20
|
+
"""
|
|
21
|
+
loaded_modules = []
|
|
22
|
+
failed_paths = []
|
|
23
|
+
|
|
24
|
+
if path.is_file() and path.suffix == ".py":
|
|
25
|
+
# Single file case
|
|
26
|
+
module_name = _load_module_from_file(path)
|
|
27
|
+
if module_name:
|
|
28
|
+
loaded_modules.append(module_name)
|
|
29
|
+
|
|
30
|
+
elif path.is_dir():
|
|
31
|
+
# Directory case
|
|
32
|
+
pattern = "**/*.py" if recursive else "*.py"
|
|
33
|
+
python_files = glob.glob(str(path / pattern), recursive=recursive)
|
|
34
|
+
|
|
35
|
+
with Progress(
|
|
36
|
+
TextColumn("[progress.description]{task.description}"),
|
|
37
|
+
BarColumn(),
|
|
38
|
+
"[progress.percentage]{task.percentage:>3.0f}%",
|
|
39
|
+
TimeElapsedColumn(),
|
|
40
|
+
TimeRemainingColumn(),
|
|
41
|
+
TextColumn("• {task.fields[current_file]}"),
|
|
42
|
+
) as progress:
|
|
43
|
+
task = progress.add_task(f"Loading {len(python_files)} files", total=len(python_files), current_file="")
|
|
44
|
+
for file_path in python_files:
|
|
45
|
+
p = Path(file_path)
|
|
46
|
+
progress.update(task, advance=1, current_file=p.name)
|
|
47
|
+
# Skip __init__.py files
|
|
48
|
+
if p.name == "__init__.py":
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
module_name = _load_module_from_file(p)
|
|
53
|
+
if module_name:
|
|
54
|
+
loaded_modules.append(module_name)
|
|
55
|
+
except flyte.errors.ModuleLoadError as e:
|
|
56
|
+
failed_paths.append((p, str(e)))
|
|
57
|
+
|
|
58
|
+
progress.update(task, advance=1, current_file="[green]Done[/green]")
|
|
59
|
+
|
|
60
|
+
return loaded_modules, failed_paths
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _load_module_from_file(file_path: Path) -> str | None:
|
|
64
|
+
"""
|
|
65
|
+
Load a Python module from a file path.
|
|
66
|
+
|
|
67
|
+
:param file_path: Path to the Python file
|
|
68
|
+
:return: Module name if successfully loaded, None otherwise
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
# Use the file stem as module name
|
|
72
|
+
module_name = file_path.stem
|
|
73
|
+
|
|
74
|
+
# Load the module specification
|
|
75
|
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
|
76
|
+
if spec is None or spec.loader is None:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
# Create and execute the module
|
|
80
|
+
module = importlib.util.module_from_spec(spec)
|
|
81
|
+
sys.modules[module_name] = module
|
|
82
|
+
module_path = os.path.dirname(os.path.abspath(file_path))
|
|
83
|
+
sys.path.append(module_path)
|
|
84
|
+
spec.loader.exec_module(module)
|
|
85
|
+
|
|
86
|
+
return module_name
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
raise flyte.errors.ModuleLoadError(f"Failed to load module from {file_path}: {e}") from e
|
flyte/_version.py
CHANGED
|
@@ -17,5 +17,5 @@ __version__: str
|
|
|
17
17
|
__version_tuple__: VERSION_TUPLE
|
|
18
18
|
version_tuple: VERSION_TUPLE
|
|
19
19
|
|
|
20
|
-
__version__ = version = '2.0.
|
|
21
|
-
__version_tuple__ = version_tuple = (2, 0, 0, '
|
|
20
|
+
__version__ = version = '2.0.0b5'
|
|
21
|
+
__version_tuple__ = version_tuple = (2, 0, 0, 'b5')
|
flyte/cli/_common.py
CHANGED
|
@@ -265,7 +265,7 @@ class ObjectsPerFileGroup(GroupBase):
|
|
|
265
265
|
|
|
266
266
|
spec = importlib.util.spec_from_file_location(module_name, self.filename)
|
|
267
267
|
if spec is None or spec.loader is None:
|
|
268
|
-
raise click.ClickException(f"Could not load module {module_name} from {self.filename}")
|
|
268
|
+
raise click.ClickException(f"Could not load module {module_name} from path [{self.filename}]")
|
|
269
269
|
|
|
270
270
|
module = importlib.util.module_from_spec(spec)
|
|
271
271
|
sys.modules[module_name] = module
|
|
@@ -314,6 +314,10 @@ class FileGroup(GroupBase):
|
|
|
314
314
|
if self._files is None:
|
|
315
315
|
directory = self._dir or Path(".").absolute()
|
|
316
316
|
self._files = [os.fspath(p) for p in directory.glob("*.py") if p.name != "__init__.py"]
|
|
317
|
+
if not self._files:
|
|
318
|
+
self._files = [os.fspath(".")] + [
|
|
319
|
+
os.fspath(p.name) for p in directory.iterdir() if not p.name.startswith(("_", ".")) and p.is_dir()
|
|
320
|
+
]
|
|
317
321
|
return self._files
|
|
318
322
|
|
|
319
323
|
def list_commands(self, ctx):
|