flyte 0.2.0b14__py3-none-any.whl → 0.2.0b15__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/__init__.py +9 -0
- flyte/_bin/runtime.py +2 -2
- flyte/_code_bundle/_packaging.py +1 -1
- flyte/_deploy.py +18 -11
- flyte/_environment.py +5 -5
- flyte/_image.py +68 -70
- flyte/_interface.py +3 -5
- flyte/_internal/controllers/remote/_controller.py +3 -2
- flyte/_internal/imagebuild/docker_builder.py +17 -5
- flyte/_internal/imagebuild/image_builder.py +11 -11
- flyte/_internal/runtime/convert.py +69 -15
- flyte/_internal/runtime/task_serde.py +28 -8
- flyte/_logging.py +11 -0
- flyte/_run.py +1 -1
- flyte/_task.py +7 -2
- flyte/_task_environment.py +8 -7
- flyte/_utils/coro_management.py +3 -0
- flyte/_utils/helpers.py +30 -19
- flyte/_version.py +2 -2
- flyte/cli/_common.py +2 -4
- flyte/cli/_deploy.py +1 -1
- flyte/cli/main.py +2 -2
- flyte/errors.py +9 -0
- flyte/extras/_container.py +2 -2
- flyte/io/_structured_dataset/basic_dfs.py +28 -32
- flyte/models.py +43 -5
- flyte/remote/_data.py +2 -1
- flyte/remote/_run.py +8 -3
- flyte/remote/_task.py +33 -1
- flyte/storage/__init__.py +2 -0
- flyte/storage/_storage.py +30 -19
- flyte/types/_interface.py +21 -6
- {flyte-0.2.0b14.dist-info → flyte-0.2.0b15.dist-info}/METADATA +2 -2
- {flyte-0.2.0b14.dist-info → flyte-0.2.0b15.dist-info}/RECORD +37 -37
- {flyte-0.2.0b14.dist-info → flyte-0.2.0b15.dist-info}/WHEEL +0 -0
- {flyte-0.2.0b14.dist-info → flyte-0.2.0b15.dist-info}/entry_points.txt +0 -0
- {flyte-0.2.0b14.dist-info → flyte-0.2.0b15.dist-info}/top_level.txt +0 -0
flyte/__init__.py
CHANGED
|
@@ -35,6 +35,7 @@ __all__ = [
|
|
|
35
35
|
"Timeout",
|
|
36
36
|
"TimeoutType",
|
|
37
37
|
"__version__",
|
|
38
|
+
"build",
|
|
38
39
|
"ctx",
|
|
39
40
|
"deploy",
|
|
40
41
|
"group",
|
|
@@ -48,6 +49,7 @@ __all__ = [
|
|
|
48
49
|
|
|
49
50
|
import sys
|
|
50
51
|
|
|
52
|
+
from ._build import build
|
|
51
53
|
from ._cache import Cache, CachePolicy, CacheRequest
|
|
52
54
|
from ._context import ctx
|
|
53
55
|
from ._deploy import deploy
|
|
@@ -69,3 +71,10 @@ from ._trace import trace
|
|
|
69
71
|
from ._version import __version__
|
|
70
72
|
|
|
71
73
|
sys.excepthook = custom_excepthook
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def version() -> str:
|
|
77
|
+
"""
|
|
78
|
+
Returns the version of the Flyte SDK.
|
|
79
|
+
"""
|
|
80
|
+
return __version__
|
flyte/_bin/runtime.py
CHANGED
|
@@ -118,9 +118,9 @@ def main(
|
|
|
118
118
|
# File "src/python/grpcio/grpc/_cython/_cygrpc/aio/completion_queue.pyx.pxi", line 147,
|
|
119
119
|
# in grpc._cython.cygrpc.PollerCompletionQueue._handle_events
|
|
120
120
|
# BlockingIOError: [Errno 11] Resource temporarily unavailable
|
|
121
|
-
|
|
121
|
+
init(org=org, project=project, domain=domain, **controller_kwargs)
|
|
122
122
|
# TODO solution is to use a single channel for both controller and reference tasks, but this requires a refactor
|
|
123
|
-
init()
|
|
123
|
+
# init()
|
|
124
124
|
# Controller is created with the same kwargs as init, so that it can be used to run tasks
|
|
125
125
|
controller = create_controller(ct="remote", **controller_kwargs)
|
|
126
126
|
|
flyte/_code_bundle/_packaging.py
CHANGED
|
@@ -91,7 +91,7 @@ def list_files_to_bundle(
|
|
|
91
91
|
ignore = IgnoreGroup(source, *ignores)
|
|
92
92
|
|
|
93
93
|
ls, ls_digest = ls_files(source, copy_style, deref_symlinks, ignore)
|
|
94
|
-
logger.debug(f"Hash
|
|
94
|
+
logger.debug(f"Hash of files to be included in the code bundle: {ls_digest}")
|
|
95
95
|
return ls, ls_digest
|
|
96
96
|
|
|
97
97
|
|
flyte/_deploy.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
|
|
6
6
|
|
|
7
7
|
import rich.repr
|
|
8
8
|
|
|
9
|
+
import flyte.errors
|
|
9
10
|
from flyte.models import SerializationContext
|
|
10
11
|
from flyte.syncify import syncify
|
|
11
12
|
|
|
@@ -83,14 +84,18 @@ async def _deploy_task(
|
|
|
83
84
|
Deploy the given task.
|
|
84
85
|
"""
|
|
85
86
|
ensure_client()
|
|
87
|
+
from ._internal.runtime.convert import convert_upload_default_inputs
|
|
86
88
|
from ._internal.runtime.task_serde import translate_task_to_wire
|
|
87
89
|
from ._protos.workflow import task_definition_pb2, task_service_pb2
|
|
88
90
|
|
|
89
91
|
image_uri = task.image.uri if isinstance(task.image, Image) else task.image
|
|
90
92
|
|
|
91
|
-
spec = translate_task_to_wire(task, serialization_context)
|
|
92
93
|
if dryrun:
|
|
93
|
-
return
|
|
94
|
+
return translate_task_to_wire(task, serialization_context)
|
|
95
|
+
|
|
96
|
+
default_inputs = await convert_upload_default_inputs(task.interface)
|
|
97
|
+
spec = translate_task_to_wire(task, serialization_context, default_inputs=default_inputs)
|
|
98
|
+
|
|
94
99
|
msg = f"Deploying task {task.name}, with image {image_uri} version {serialization_context.version}"
|
|
95
100
|
if spec.task_template.HasField("container") and spec.task_template.container.args:
|
|
96
101
|
msg += f" from {spec.task_template.container.args[-3]}.{spec.task_template.container.args[-1]}"
|
|
@@ -128,16 +133,16 @@ async def build_images(deployment: DeploymentPlan) -> ImageCache:
|
|
|
128
133
|
image_identifier_map = {}
|
|
129
134
|
for env_name, env in deployment.envs.items():
|
|
130
135
|
if not isinstance(env.image, str):
|
|
131
|
-
logger.
|
|
136
|
+
logger.warning(f"Building Image for environment {env_name}, image: {env.image}")
|
|
132
137
|
images.append(_build_image_bg(env_name, env.image))
|
|
133
138
|
|
|
134
139
|
elif env.image == "auto" and "auto" not in image_identifier_map:
|
|
135
|
-
auto_image = Image.
|
|
140
|
+
auto_image = Image.from_debian_base()
|
|
136
141
|
image_identifier_map["auto"] = auto_image.uri
|
|
137
142
|
final_images = await asyncio.gather(*images)
|
|
138
143
|
|
|
139
144
|
for env_name, image_uri in final_images:
|
|
140
|
-
logger.
|
|
145
|
+
logger.warning(f"Built Image for environment {env_name}, image: {image_uri}")
|
|
141
146
|
env = deployment.envs[env_name]
|
|
142
147
|
if isinstance(env.image, Image):
|
|
143
148
|
image_identifier_map[env.image.identifier] = env.image.uri
|
|
@@ -151,12 +156,14 @@ async def apply(deployment: DeploymentPlan, copy_style: CopyFiles, dryrun: bool
|
|
|
151
156
|
|
|
152
157
|
cfg = get_common_config()
|
|
153
158
|
image_cache = await build_images(deployment)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
|
|
160
|
+
version = deployment.version
|
|
161
|
+
code_bundle = None
|
|
162
|
+
if copy_style == "none" and not version:
|
|
163
|
+
raise flyte.errors.DeploymentError("Version must be set when copy_style is none")
|
|
157
164
|
else:
|
|
158
165
|
code_bundle = await build_code_bundle(from_dir=cfg.root_dir, dryrun=dryrun, copy_style=copy_style)
|
|
159
|
-
|
|
166
|
+
version = version or code_bundle.computed_version
|
|
160
167
|
# TODO we should update the version to include the image cache digest and code bundle digest. This is
|
|
161
168
|
# to ensure that changes in image dependencies, cause an update to the deployment version.
|
|
162
169
|
# TODO Also hash the environment and tasks to ensure that changes in the environment or tasks
|
|
@@ -166,7 +173,7 @@ async def apply(deployment: DeploymentPlan, copy_style: CopyFiles, dryrun: bool
|
|
|
166
173
|
domain=cfg.domain,
|
|
167
174
|
org=cfg.org,
|
|
168
175
|
code_bundle=code_bundle,
|
|
169
|
-
version=
|
|
176
|
+
version=version,
|
|
170
177
|
image_cache=image_cache,
|
|
171
178
|
root_dir=cfg.root_dir,
|
|
172
179
|
)
|
|
@@ -195,7 +202,7 @@ def _recursive_discover(
|
|
|
195
202
|
if env.name in planned_envs:
|
|
196
203
|
continue
|
|
197
204
|
# Recursively discover dependent environments
|
|
198
|
-
for dependent_env in env.
|
|
205
|
+
for dependent_env in env.depends_on:
|
|
199
206
|
_recursive_discover(planned_envs, dependent_env)
|
|
200
207
|
# Add the environment to the existing envs
|
|
201
208
|
planned_envs[env.name] = env
|
flyte/_environment.py
CHANGED
|
@@ -28,12 +28,12 @@ class Environment:
|
|
|
28
28
|
:param resources: Resources to allocate for the environment.
|
|
29
29
|
:param env: Environment variables to set for the environment.
|
|
30
30
|
:param secrets: Secrets to inject into the environment.
|
|
31
|
-
:param
|
|
31
|
+
:param depends_on: Environment dependencies to hint, so when you deploy the environment, the dependencies are
|
|
32
32
|
also deployed. This is useful when you have a set of environments that depend on each other.
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
35
|
name: str
|
|
36
|
-
|
|
36
|
+
depends_on: List[Environment] = field(default_factory=list)
|
|
37
37
|
pod_template: Optional[Union[str, "V1PodTemplate"]] = None
|
|
38
38
|
description: Optional[str] = None
|
|
39
39
|
secrets: Optional[SecretRequest] = None
|
|
@@ -49,7 +49,7 @@ class Environment:
|
|
|
49
49
|
"""
|
|
50
50
|
Add a dependency to the environment.
|
|
51
51
|
"""
|
|
52
|
-
self.
|
|
52
|
+
self.depends_on.extend(env)
|
|
53
53
|
|
|
54
54
|
def clone_with(
|
|
55
55
|
self,
|
|
@@ -58,7 +58,7 @@ class Environment:
|
|
|
58
58
|
resources: Optional[Resources] = None,
|
|
59
59
|
env: Optional[Dict[str, str]] = None,
|
|
60
60
|
secrets: Optional[SecretRequest] = None,
|
|
61
|
-
|
|
61
|
+
depends_on: Optional[List[Environment]] = None,
|
|
62
62
|
**kwargs: Any,
|
|
63
63
|
) -> Environment:
|
|
64
64
|
raise NotImplementedError
|
|
@@ -68,7 +68,7 @@ class Environment:
|
|
|
68
68
|
Get the keyword arguments for the environment.
|
|
69
69
|
"""
|
|
70
70
|
kwargs: Dict[str, Any] = {
|
|
71
|
-
"
|
|
71
|
+
"depends_on": self.depends_on,
|
|
72
72
|
"image": self.image,
|
|
73
73
|
}
|
|
74
74
|
if self.resources is not None:
|
flyte/_image.py
CHANGED
|
@@ -238,18 +238,17 @@ class Image:
|
|
|
238
238
|
registry: Optional[str] = field(default=None)
|
|
239
239
|
name: Optional[str] = field(default=None)
|
|
240
240
|
platform: Tuple[Architecture, ...] = field(default=("linux/amd64",))
|
|
241
|
-
tag: Optional[str] = field(default=None)
|
|
242
241
|
python_version: Tuple[int, int] = field(default_factory=_detect_python_version)
|
|
243
242
|
|
|
244
243
|
# For .auto() images. Don't compute an actual identifier.
|
|
245
244
|
_identifier_override: Optional[str] = field(default=None, init=False)
|
|
246
|
-
# This is set on default images. These images are built from the base Dockerfile in this library and shouldn't be
|
|
247
|
-
# modified with additional layers.
|
|
248
|
-
is_final: bool = field(default=False)
|
|
249
245
|
|
|
250
246
|
# Layers to be added to the image. In init, because frozen, but users shouldn't access, so underscore.
|
|
251
247
|
_layers: Tuple[Layer, ...] = field(default_factory=tuple)
|
|
252
248
|
|
|
249
|
+
# Only settable internally.
|
|
250
|
+
_tag: Optional[str] = field(default=None, init=False)
|
|
251
|
+
|
|
253
252
|
_DEFAULT_IMAGE_PREFIXES: ClassVar = {
|
|
254
253
|
PYTHON_3_10: "py3.10-",
|
|
255
254
|
PYTHON_3_11: "py3.11-",
|
|
@@ -257,6 +256,25 @@ class Image:
|
|
|
257
256
|
PYTHON_3_13: "py3.13-",
|
|
258
257
|
}
|
|
259
258
|
|
|
259
|
+
# class-level token not included in __init__
|
|
260
|
+
_token: ClassVar[object] = object()
|
|
261
|
+
|
|
262
|
+
# check for the guard that we put in place
|
|
263
|
+
def __post_init__(self):
|
|
264
|
+
if object.__getattribute__(self, "__dict__").pop("_guard", None) is not Image._token:
|
|
265
|
+
raise TypeError(
|
|
266
|
+
"Direct instantiation of Image not allowed, please use one of the various from_...() methods instead"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Private constructor for internal use only
|
|
270
|
+
@classmethod
|
|
271
|
+
def _new(cls, **kwargs) -> Image:
|
|
272
|
+
# call the normal __init__, injecting a private keyword that users won't know
|
|
273
|
+
obj = cls.__new__(cls) # allocate
|
|
274
|
+
object.__setattr__(obj, "_guard", cls._token) # set guard to prevent direct construction
|
|
275
|
+
cls.__init__(obj, **kwargs) # run dataclass generated __init__
|
|
276
|
+
return obj
|
|
277
|
+
|
|
260
278
|
@cached_property
|
|
261
279
|
def identifier(self) -> str:
|
|
262
280
|
"""
|
|
@@ -273,10 +291,9 @@ class Image:
|
|
|
273
291
|
if self._identifier_override:
|
|
274
292
|
return self._identifier_override
|
|
275
293
|
|
|
276
|
-
# Only get the non-None values in the
|
|
294
|
+
# Only get the non-None values in the Image to ensure the hash is consistent
|
|
277
295
|
# across different SDK versions.
|
|
278
|
-
#
|
|
279
|
-
# representation for now.
|
|
296
|
+
# Layers can specify a _compute_identifier optionally, but the default will just stringify
|
|
280
297
|
image_dict = asdict(self, dict_factory=lambda x: {k: v for (k, v) in x if v is not None and k != "_layers"})
|
|
281
298
|
layers_str_repr = "".join([layer._compute_identifier(layer) for layer in self._layers])
|
|
282
299
|
image_dict["layers"] = layers_str_repr
|
|
@@ -293,16 +310,16 @@ class Image:
|
|
|
293
310
|
# this default image definition may need to be updated once there is a released pypi version
|
|
294
311
|
from flyte._version import __version__
|
|
295
312
|
|
|
296
|
-
dev_mode = cls._is_editable_install() or (__version__ and "dev" in __version__)
|
|
313
|
+
dev_mode = (cls._is_editable_install() or (__version__ and "dev" in __version__)) and not flyte_version
|
|
297
314
|
if flyte_version is None:
|
|
298
315
|
flyte_version = __version__.replace("+", "-")
|
|
299
316
|
preset_tag = flyte_version if flyte_version.startswith("v") else f"v{flyte_version}"
|
|
300
317
|
preset_tag = f"py{python_version[0]}.{python_version[1]}-{preset_tag}"
|
|
301
|
-
image = Image(
|
|
318
|
+
image = Image._new(
|
|
302
319
|
base_image=f"python:{python_version[0]}.{python_version[1]}-slim-bookworm",
|
|
303
320
|
registry=_BASE_REGISTRY,
|
|
304
321
|
name=_DEFAULT_IMAGE_NAME,
|
|
305
|
-
|
|
322
|
+
# _tag=preset_tag,
|
|
306
323
|
platform=("linux/amd64", "linux/arm64"),
|
|
307
324
|
)
|
|
308
325
|
labels_and_user = _DockerLines(
|
|
@@ -323,17 +340,17 @@ class Image:
|
|
|
323
340
|
"UV_LINK_MODE": "copy",
|
|
324
341
|
}
|
|
325
342
|
)
|
|
326
|
-
image = image.with_apt_packages(
|
|
327
|
-
|
|
328
|
-
base_packages = ["kubernetes", "msgpack", "mashumaro"]
|
|
343
|
+
image = image.with_apt_packages("build-essential", "ca-certificates")
|
|
329
344
|
|
|
330
345
|
# Add in flyte library
|
|
331
346
|
if dev_mode:
|
|
332
|
-
image = image.
|
|
333
|
-
image = image.with_local_v2()
|
|
347
|
+
image = image._with_local_v2()
|
|
334
348
|
else:
|
|
335
|
-
|
|
336
|
-
|
|
349
|
+
image = image.with_pip_packages("flyte=={flyte_version}")
|
|
350
|
+
object.__setattr__(image, "_tag", preset_tag)
|
|
351
|
+
# Set this to auto for all auto images because the meaning of "auto" can change (based on logic inside
|
|
352
|
+
# _get_default_image_for, acts differently in a running task container) so let's make sure it stays auto.
|
|
353
|
+
object.__setattr__(image, "_identifier_override", "auto")
|
|
337
354
|
|
|
338
355
|
return image
|
|
339
356
|
|
|
@@ -345,36 +362,7 @@ class Image:
|
|
|
345
362
|
return pyproject.exists()
|
|
346
363
|
|
|
347
364
|
@classmethod
|
|
348
|
-
def
|
|
349
|
-
cls,
|
|
350
|
-
registry: str,
|
|
351
|
-
name: str,
|
|
352
|
-
tag: Optional[str] = None,
|
|
353
|
-
python_version: Optional[Tuple[int, int]] = None,
|
|
354
|
-
arch: Union[Architecture, Tuple[Architecture, ...]] = "linux/amd64",
|
|
355
|
-
) -> Image:
|
|
356
|
-
"""
|
|
357
|
-
This creates a new debian-based base image.
|
|
358
|
-
If using the Union or docker builders, image will have uv available and a virtualenv created at /opt/venv.
|
|
359
|
-
|
|
360
|
-
:param registry: Registry to use for the image
|
|
361
|
-
:param name: Name of the image
|
|
362
|
-
:param tag: Tag to use for the image
|
|
363
|
-
:param python_version: Python version to use for the image
|
|
364
|
-
:param arch: Architecture to use for the image, default is linux/amd64
|
|
365
|
-
:return: Image
|
|
366
|
-
"""
|
|
367
|
-
base_image = "debian:bookworm-slim"
|
|
368
|
-
plat = arch if isinstance(arch, tuple) else (arch,)
|
|
369
|
-
if python_version is None:
|
|
370
|
-
python_version = _detect_python_version()
|
|
371
|
-
img = cls(
|
|
372
|
-
base_image=base_image, name=name, registry=registry, tag=tag, platform=plat, python_version=python_version
|
|
373
|
-
)
|
|
374
|
-
return img
|
|
375
|
-
|
|
376
|
-
@classmethod
|
|
377
|
-
def auto(
|
|
365
|
+
def from_debian_base(
|
|
378
366
|
cls,
|
|
379
367
|
python_version: Optional[Tuple[int, int]] = None,
|
|
380
368
|
flyte_version: Optional[str] = None,
|
|
@@ -402,20 +390,20 @@ class Image:
|
|
|
402
390
|
if registry and name:
|
|
403
391
|
return base_image.clone(registry=registry, name=name)
|
|
404
392
|
|
|
405
|
-
# Set this to auto for all auto images because the meaning of "auto" can change (based on logic inside
|
|
406
|
-
# _get_default_image_for, acts differently in a running task container) so let's make sure it stays auto.
|
|
407
|
-
object.__setattr__(base_image, "_identifier_override", "auto")
|
|
393
|
+
# # Set this to auto for all auto images because the meaning of "auto" can change (based on logic inside
|
|
394
|
+
# # _get_default_image_for, acts differently in a running task container) so let's make sure it stays auto.
|
|
395
|
+
# object.__setattr__(base_image, "_identifier_override", "auto")
|
|
408
396
|
return base_image
|
|
409
397
|
|
|
410
398
|
@classmethod
|
|
411
|
-
def
|
|
399
|
+
def from_base(cls, image_uri: str) -> Image:
|
|
412
400
|
"""
|
|
413
401
|
Use this method to start with a pre-built base image. This image must already exist in the registry of course.
|
|
414
402
|
|
|
415
403
|
:param image_uri: The full URI of the image, in the format <registry>/<name>:<tag>
|
|
416
404
|
:return:
|
|
417
405
|
"""
|
|
418
|
-
img = cls(base_image=image_uri)
|
|
406
|
+
img = cls._new(base_image=image_uri)
|
|
419
407
|
return img
|
|
420
408
|
|
|
421
409
|
@classmethod
|
|
@@ -450,6 +438,7 @@ class Image:
|
|
|
450
438
|
:param registry: registry to use for the image
|
|
451
439
|
:param script: path to the uv script
|
|
452
440
|
:param arch: architecture to use for the image, default is linux/amd64, use tuple for multiple values
|
|
441
|
+
:param python_version: Python version for the image, if not specified, will use the current Python version
|
|
453
442
|
|
|
454
443
|
:return: Image
|
|
455
444
|
"""
|
|
@@ -466,9 +455,16 @@ class Image:
|
|
|
466
455
|
header = parse_uv_script_file(script)
|
|
467
456
|
if registry is None:
|
|
468
457
|
raise ValueError("registry must be specified")
|
|
469
|
-
|
|
458
|
+
|
|
459
|
+
# todo: arch
|
|
460
|
+
img = cls.from_debian_base(registry=registry, name=name, python_version=python_version)
|
|
461
|
+
|
|
462
|
+
# add ca-certificates to the image by default
|
|
463
|
+
img = img.with_apt_packages("ca-certificates")
|
|
464
|
+
|
|
470
465
|
if header.dependencies:
|
|
471
|
-
return img.with_pip_packages(header.dependencies)
|
|
466
|
+
return img.with_pip_packages(*header.dependencies)
|
|
467
|
+
|
|
472
468
|
# todo: override the _identifier_override to be the script name or a hash of the script contents
|
|
473
469
|
# This is needed because inside the image, the identifier will be computed to be something different.
|
|
474
470
|
return img
|
|
@@ -481,40 +477,42 @@ class Image:
|
|
|
481
477
|
|
|
482
478
|
:param registry: Registry to use for the image
|
|
483
479
|
:param name: Name of the image
|
|
480
|
+
:param addl_layer: Additional layer to add to the image. This will be added on top of the existing layers.
|
|
484
481
|
|
|
485
482
|
:return:
|
|
486
483
|
"""
|
|
487
484
|
registry = registry if registry else self.registry
|
|
488
485
|
name = name if name else self.name
|
|
486
|
+
if addl_layer and (not name or not registry):
|
|
487
|
+
raise ValueError(
|
|
488
|
+
f"Cannot add additional layer {addl_layer} to an image"
|
|
489
|
+
f" without a registry and name. Please first clone()."
|
|
490
|
+
)
|
|
489
491
|
new_layers = (*self._layers, addl_layer) if addl_layer else self._layers
|
|
490
|
-
img = Image(
|
|
492
|
+
img = Image._new(
|
|
491
493
|
base_image=self.base_image,
|
|
492
494
|
dockerfile=self.dockerfile,
|
|
493
495
|
registry=registry,
|
|
494
496
|
name=name,
|
|
495
|
-
tag=self.tag,
|
|
496
497
|
platform=self.platform,
|
|
497
498
|
python_version=self.python_version,
|
|
498
|
-
is_final=self.is_final,
|
|
499
499
|
_layers=new_layers,
|
|
500
500
|
)
|
|
501
501
|
|
|
502
502
|
return img
|
|
503
503
|
|
|
504
504
|
@classmethod
|
|
505
|
-
def from_dockerfile(cls, file: Path, registry: str, name: str
|
|
505
|
+
def from_dockerfile(cls, file: Path, registry: str, name: str) -> Image:
|
|
506
506
|
"""
|
|
507
507
|
Use this method to create a new image with the specified dockerfile
|
|
508
508
|
|
|
509
509
|
:param file: path to the dockerfile
|
|
510
510
|
:param name: name of the image
|
|
511
511
|
:param registry: registry to use for the image
|
|
512
|
-
:param tag: tag to use for the image
|
|
513
512
|
|
|
514
513
|
:return:
|
|
515
514
|
"""
|
|
516
|
-
|
|
517
|
-
img = cls(dockerfile=file, registry=registry, name=name, tag=tag)
|
|
515
|
+
img = cls(dockerfile=file, registry=registry, name=name)
|
|
518
516
|
|
|
519
517
|
return img
|
|
520
518
|
|
|
@@ -527,6 +525,8 @@ class Image:
|
|
|
527
525
|
from ._utils import filehash_update
|
|
528
526
|
|
|
529
527
|
hasher = hashlib.md5()
|
|
528
|
+
if self.base_image:
|
|
529
|
+
hasher.update(self.base_image.encode("utf-8"))
|
|
530
530
|
if self.dockerfile:
|
|
531
531
|
# Note the location of the dockerfile shouldn't matter, only the contents
|
|
532
532
|
filehash_update(self.dockerfile, hasher)
|
|
@@ -537,7 +537,7 @@ class Image:
|
|
|
537
537
|
|
|
538
538
|
@property
|
|
539
539
|
def _final_tag(self) -> str:
|
|
540
|
-
t = self.
|
|
540
|
+
t = self._tag if self._tag else self._get_hash_digest()
|
|
541
541
|
return t or "latest"
|
|
542
542
|
|
|
543
543
|
@cached_property
|
|
@@ -585,7 +585,7 @@ class Image:
|
|
|
585
585
|
|
|
586
586
|
def with_pip_packages(
|
|
587
587
|
self,
|
|
588
|
-
packages:
|
|
588
|
+
*packages: str,
|
|
589
589
|
index_url: Optional[str] = None,
|
|
590
590
|
extra_index_urls: Union[str, List[str], Tuple[str, ...], None] = None,
|
|
591
591
|
pre: bool = False,
|
|
@@ -599,7 +599,7 @@ class Image:
|
|
|
599
599
|
```python
|
|
600
600
|
@flyte.task(image=(flyte.Image
|
|
601
601
|
.ubuntu_python()
|
|
602
|
-
.with_pip_packages(
|
|
602
|
+
.with_pip_packages("requests", "numpy")))
|
|
603
603
|
def my_task(x: int) -> int:
|
|
604
604
|
import numpy as np
|
|
605
605
|
return np.sum([x, 1])
|
|
@@ -614,8 +614,7 @@ class Image:
|
|
|
614
614
|
:param extra_args: extra arguments to pass to pip install, default is None
|
|
615
615
|
:return: Image
|
|
616
616
|
"""
|
|
617
|
-
|
|
618
|
-
new_packages: Optional[Tuple] = _ensure_tuple(packages) if packages else None
|
|
617
|
+
new_packages: Optional[Tuple] = packages or None
|
|
619
618
|
new_extra_index_urls: Optional[Tuple] = _ensure_tuple(extra_index_urls) if extra_index_urls else None
|
|
620
619
|
|
|
621
620
|
ll = PipPackages(
|
|
@@ -685,15 +684,14 @@ class Image:
|
|
|
685
684
|
new_image = self.clone(addl_layer=UVProject(pyproject=pyproject_file, uvlock=lock))
|
|
686
685
|
return new_image
|
|
687
686
|
|
|
688
|
-
def with_apt_packages(self, packages:
|
|
687
|
+
def with_apt_packages(self, *packages: str) -> Image:
|
|
689
688
|
"""
|
|
690
689
|
Use this method to create a new image with the specified apt packages layered on top of the current image
|
|
691
690
|
|
|
692
691
|
:param packages: list of apt packages to install
|
|
693
692
|
:return: Image
|
|
694
693
|
"""
|
|
695
|
-
|
|
696
|
-
new_image = self.clone(addl_layer=AptPackages(packages=pkgs))
|
|
694
|
+
new_image = self.clone(addl_layer=AptPackages(packages=packages))
|
|
697
695
|
return new_image
|
|
698
696
|
|
|
699
697
|
def with_commands(self, commands: List[str]) -> Image:
|
|
@@ -708,7 +706,7 @@ class Image:
|
|
|
708
706
|
new_image = self.clone(addl_layer=Commands(commands=new_commands))
|
|
709
707
|
return new_image
|
|
710
708
|
|
|
711
|
-
def
|
|
709
|
+
def _with_local_v2(self) -> Image:
|
|
712
710
|
"""
|
|
713
711
|
Use this method to create a new image with the local v2 builder
|
|
714
712
|
This will override any existing builder
|
flyte/_interface.py
CHANGED
|
@@ -3,8 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
import inspect
|
|
4
4
|
from typing import Dict, Generator, Tuple, Type, TypeVar, Union, cast, get_args, get_type_hints
|
|
5
5
|
|
|
6
|
-
from flyte._logging import logger
|
|
7
|
-
|
|
8
6
|
|
|
9
7
|
def default_output_name(index: int = 0) -> str:
|
|
10
8
|
return f"o{index}"
|
|
@@ -62,12 +60,12 @@ def extract_return_annotation(return_annotation: Union[Type, Tuple, None]) -> Di
|
|
|
62
60
|
# Options 1 and 2
|
|
63
61
|
bases = return_annotation.__bases__ # type: ignore
|
|
64
62
|
if len(bases) == 1 and bases[0] is tuple and hasattr(return_annotation, "_fields"):
|
|
65
|
-
|
|
63
|
+
# Task returns named tuple
|
|
66
64
|
return dict(get_type_hints(cast(Type, return_annotation), include_extras=True))
|
|
67
65
|
|
|
68
66
|
if hasattr(return_annotation, "__origin__") and return_annotation.__origin__ is tuple: # type: ignore
|
|
69
67
|
# Handle option 3
|
|
70
|
-
|
|
68
|
+
# Task returns unnamed typing.Tuple
|
|
71
69
|
if len(return_annotation.__args__) == 1: # type: ignore
|
|
72
70
|
raise TypeError("Tuples should be used to indicate multiple return values, found only one return variable.")
|
|
73
71
|
ra = get_args(return_annotation)
|
|
@@ -80,5 +78,5 @@ def extract_return_annotation(return_annotation: Union[Type, Tuple, None]) -> Di
|
|
|
80
78
|
|
|
81
79
|
else:
|
|
82
80
|
# Handle all other single return types
|
|
83
|
-
|
|
81
|
+
# Task returns unnamed native tuple
|
|
84
82
|
return {default_output_name(): cast(Type, return_annotation)}
|
|
@@ -416,10 +416,11 @@ class RemoteController(Controller):
|
|
|
416
416
|
|
|
417
417
|
invoke_seq_num = self.generate_task_call_sequence(_task, current_action_id)
|
|
418
418
|
|
|
419
|
-
native_interface = types.guess_interface(
|
|
419
|
+
native_interface = types.guess_interface(
|
|
420
|
+
_task.spec.task_template.interface, default_inputs=_task.spec.default_inputs
|
|
421
|
+
)
|
|
420
422
|
inputs = await convert.convert_from_native_to_inputs(native_interface, *args, **kwargs)
|
|
421
423
|
serialized_inputs = inputs.proto_inputs.SerializeToString(deterministic=True)
|
|
422
|
-
|
|
423
424
|
inputs_hash = convert.generate_inputs_hash(serialized_inputs)
|
|
424
425
|
sub_action_id, sub_action_output_path = convert.generate_sub_action_id_and_output_path(
|
|
425
426
|
tctx, task_name, inputs_hash, invoke_seq_num
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import os
|
|
2
3
|
import shutil
|
|
3
4
|
import subprocess
|
|
4
5
|
import tempfile
|
|
@@ -25,6 +26,8 @@ from flyte._image import (
|
|
|
25
26
|
from flyte._logging import logger
|
|
26
27
|
|
|
27
28
|
_F_IMG_ID = "_F_IMG_ID"
|
|
29
|
+
FLYTE_DOCKER_BUILDER_CACHE_FROM = "FLYTE_DOCKER_BUILDER_CACHE_FROM"
|
|
30
|
+
FLYTE_DOCKER_BUILDER_CACHE_TO = "FLYTE_DOCKER_BUILDER_CACHE_TO"
|
|
28
31
|
|
|
29
32
|
UV_LOCK_INSTALL_TEMPLATE = Template("""\
|
|
30
33
|
WORKDIR /root
|
|
@@ -193,7 +196,7 @@ class CopyConfigHandler:
|
|
|
193
196
|
shutil.copy(abs_path, dest_path)
|
|
194
197
|
elif layer.context_source.is_dir():
|
|
195
198
|
# Copy the entire directory
|
|
196
|
-
shutil.copytree(abs_path, dest_path)
|
|
199
|
+
shutil.copytree(abs_path, dest_path, dirs_exist_ok=True)
|
|
197
200
|
else:
|
|
198
201
|
raise ValueError(f"Source path is neither file nor directory: {layer.context_source}")
|
|
199
202
|
|
|
@@ -269,9 +272,9 @@ class DockerImageBuilder:
|
|
|
269
272
|
_builder_name: ClassVar = "flytex"
|
|
270
273
|
|
|
271
274
|
async def build_image(self, image: Image, dry_run: bool = False) -> str:
|
|
272
|
-
if image.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
+
if len(image._layers) == 0:
|
|
276
|
+
logger.warning("No layers to build, returning the image URI as is.")
|
|
277
|
+
return image.uri
|
|
275
278
|
|
|
276
279
|
if image.dockerfile:
|
|
277
280
|
# If a dockerfile is provided, use it directly
|
|
@@ -393,11 +396,20 @@ class DockerImageBuilder:
|
|
|
393
396
|
f"{image.uri}",
|
|
394
397
|
"--platform",
|
|
395
398
|
",".join(image.platform),
|
|
396
|
-
"--push" if push else "--load",
|
|
397
399
|
]
|
|
398
400
|
|
|
401
|
+
cache_from = os.getenv(FLYTE_DOCKER_BUILDER_CACHE_FROM)
|
|
402
|
+
cache_to = os.getenv(FLYTE_DOCKER_BUILDER_CACHE_TO)
|
|
403
|
+
if cache_from and cache_to:
|
|
404
|
+
command[3:3] = [
|
|
405
|
+
f"--cache-from={cache_from}",
|
|
406
|
+
f"--cache-to={cache_to}",
|
|
407
|
+
]
|
|
408
|
+
|
|
399
409
|
if image.registry and push:
|
|
400
410
|
command.append("--push")
|
|
411
|
+
else:
|
|
412
|
+
command.append("--load")
|
|
401
413
|
command.append(tmp_dir)
|
|
402
414
|
|
|
403
415
|
concat_command = " ".join(command)
|
|
@@ -35,10 +35,7 @@ class DockerAPIImageChecker(ImageChecker):
|
|
|
35
35
|
async def image_exists(cls, repository: str, tag: str, arch: Tuple[Architecture, ...] = ("linux/amd64",)) -> bool:
|
|
36
36
|
import httpx
|
|
37
37
|
|
|
38
|
-
if "/" in repository:
|
|
39
|
-
if not repository.startswith("library/"):
|
|
40
|
-
raise ValueError("This checker only works with Docker Hub")
|
|
41
|
-
else:
|
|
38
|
+
if "/" not in repository:
|
|
42
39
|
repository = f"library/{repository}"
|
|
43
40
|
|
|
44
41
|
auth_url = "https://auth.docker.io/token"
|
|
@@ -50,6 +47,7 @@ class DockerAPIImageChecker(ImageChecker):
|
|
|
50
47
|
auth_response = await client.get(auth_url, params={"service": service, "scope": scope})
|
|
51
48
|
if auth_response.status_code != 200:
|
|
52
49
|
raise Exception(f"Failed to get auth token: {auth_response.status_code}")
|
|
50
|
+
|
|
53
51
|
token = auth_response.json()["token"]
|
|
54
52
|
|
|
55
53
|
manifest_url = f"https://registry-1.docker.io/v2/{repository}/manifests/{tag}"
|
|
@@ -60,18 +58,20 @@ class DockerAPIImageChecker(ImageChecker):
|
|
|
60
58
|
"application/vnd.docker.distribution.manifest.list.v2+json"
|
|
61
59
|
),
|
|
62
60
|
}
|
|
63
|
-
manifest_response = await client.get(manifest_url, headers=headers)
|
|
64
61
|
|
|
62
|
+
manifest_response = await client.get(manifest_url, headers=headers)
|
|
65
63
|
if manifest_response.status_code != 200:
|
|
66
|
-
|
|
64
|
+
logger.warning(f"Image not found: {repository}:{tag} (HTTP {manifest_response.status_code})")
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
67
|
manifest_list = manifest_response.json()["manifests"]
|
|
68
|
-
architectures = [f"{
|
|
68
|
+
architectures = [f"{m['platform']['os']}/{m['platform']['architecture']}" for m in manifest_list]
|
|
69
69
|
|
|
70
|
-
if set(
|
|
71
|
-
logger.debug(f"Image {repository}:{tag} found
|
|
70
|
+
if set(arch).issubset(set(architectures)):
|
|
71
|
+
logger.debug(f"Image {repository}:{tag} found with arch {architectures}")
|
|
72
72
|
return True
|
|
73
73
|
else:
|
|
74
|
-
logger.debug(f"Image {repository}:{tag}
|
|
74
|
+
logger.debug(f"Image {repository}:{tag} has {architectures}, but missing {arch}")
|
|
75
75
|
return False
|
|
76
76
|
|
|
77
77
|
|
|
@@ -123,7 +123,7 @@ class ImageBuildEngine:
|
|
|
123
123
|
_REGISTRY: typing.ClassVar[typing.Dict[str, Tuple[ImageBuilder, int]]] = {}
|
|
124
124
|
_SEEN_IMAGES: typing.ClassVar[typing.Dict[str, str]] = {
|
|
125
125
|
# Set default for the auto container. See Image._identifier_override for more info.
|
|
126
|
-
"auto": Image.
|
|
126
|
+
"auto": Image.from_debian_base().uri,
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
@classmethod
|