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 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
- # init(org=org, project=project, domain=domain, **controller_kwargs)
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
 
@@ -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 digest: {ls_digest}")
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 spec
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.info(f"Building Image for environment {env_name}, image: {env.image}")
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.auto()
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.info(f"Built Image for environment {env_name}, image: {image_uri}")
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
- if copy_style == "none":
155
- code_bundle = None
156
- assert deployment.version is not None, "Version must be set when copy_style is none"
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
- deployment.version = code_bundle.computed_version
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=deployment.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.env_dep_hints:
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 env_dep_hints: Environment dependencies to hint, so when you deploy the environment, the dependencies are
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
- env_dep_hints: List[Environment] = field(default_factory=list)
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.env_dep_hints.extend(env)
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
- env_dep_hints: Optional[List[Environment]] = None,
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
- "env_dep_hints": self.env_dep_hints,
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 ImageSpec to ensure the hash is consistent
294
+ # Only get the non-None values in the Image to ensure the hash is consistent
277
295
  # across different SDK versions.
278
- # Can potentially add a second hashing function to the Layer protocol, but relying on just asdict/str
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
- tag=preset_tag,
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(["build-essential", "ca-certificates"])
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.with_pip_packages(base_packages)
333
- image = image.with_local_v2()
347
+ image = image._with_local_v2()
334
348
  else:
335
- base_packages.append(f"flyte=={flyte_version}")
336
- image = image.with_pip_packages(base_packages)
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 from_uv_debian(
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 from_prebuilt(cls, image_uri: str) -> Image:
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
- img = cls.from_uv_debian(registry=registry, name=name, arch=arch, python_version=python_version)
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, tag: Optional[str] = None) -> Image:
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
- tag = tag or "latest"
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.tag if self.tag else self._get_hash_digest()
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: Union[str, List[str], Tuple[str, ...]],
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(["requests", "numpy"])))
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: Union[str, List[str], Tuple[str, ...]]) -> Image:
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
- pkgs = _ensure_tuple(packages)
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 with_local_v2(self) -> Image:
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
- logger.debug(f"Task returns named tuple {return_annotation}")
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
- logger.debug(f"Task returns unnamed typing.Tuple {return_annotation}")
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
- logger.debug(f"Task returns unnamed native tuple {return_annotation}")
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(_task.spec.task_template.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.is_final:
273
- if image._layers:
274
- raise ValueError("Image is a default image and should already be built")
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
- raise Exception(f"Failed to get manifest: {manifest_response.status_code}")
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"{x['platform']['os']}/{x['platform']['architecture']}" for x in manifest_list]
68
+ architectures = [f"{m['platform']['os']}/{m['platform']['architecture']}" for m in manifest_list]
69
69
 
70
- if set(architectures) >= set(arch):
71
- logger.debug(f"Image {repository}:{tag} found for architecture(s) {arch}, has {architectures}")
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} not found for architecture(s) {arch}, only has {architectures}")
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.auto().uri,
126
+ "auto": Image.from_debian_base().uri,
127
127
  }
128
128
 
129
129
  @classmethod