flyte 2.0.0b4__py3-none-any.whl → 2.0.0b6__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.

@@ -7,6 +7,7 @@ import tempfile
7
7
  from pathlib import Path
8
8
  from typing import ClassVar, Type
9
9
 
10
+ from async_lru import alru_cache
10
11
  from flyteidl.core.tasks_pb2 import TaskTemplate
11
12
 
12
13
  from flyte._logging import log, logger
@@ -109,6 +110,7 @@ async def build_pkl_bundle(
109
110
  return CodeBundle(pkl=str(dest), computed_version=str_digest)
110
111
 
111
112
 
113
+ @alru_cache
112
114
  async def build_code_bundle(
113
115
  from_dir: Path,
114
116
  *ignore: Type[Ignore],
flyte/_deploy.py CHANGED
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import typing
4
5
  from dataclasses import dataclass
5
6
  from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
6
7
 
8
+ import grpc.aio
7
9
  import rich.repr
8
10
 
9
11
  import flyte.errors
@@ -90,27 +92,39 @@ async def _deploy_task(
90
92
 
91
93
  image_uri = task.image.uri if isinstance(task.image, Image) else task.image
92
94
 
93
- if dryrun:
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
-
99
- msg = f"Deploying task {task.name}, with image {image_uri} version {serialization_context.version}"
100
- if spec.task_template.HasField("container") and spec.task_template.container.args:
101
- msg += f" from {spec.task_template.container.args[-3]}.{spec.task_template.container.args[-1]}"
102
- logger.info(msg)
103
- task_id = task_definition_pb2.TaskIdentifier(
104
- org=spec.task_template.id.org,
105
- project=spec.task_template.id.project,
106
- domain=spec.task_template.id.domain,
107
- version=spec.task_template.id.version,
108
- name=spec.task_template.id.name,
109
- )
95
+ try:
96
+ if dryrun:
97
+ return translate_task_to_wire(task, serialization_context)
98
+
99
+ default_inputs = await convert_upload_default_inputs(task.interface)
100
+ spec = translate_task_to_wire(task, serialization_context, default_inputs=default_inputs)
101
+
102
+ msg = f"Deploying task {task.name}, with image {image_uri} version {serialization_context.version}"
103
+ if spec.task_template.HasField("container") and spec.task_template.container.args:
104
+ msg += f" from {spec.task_template.container.args[-3]}.{spec.task_template.container.args[-1]}"
105
+ logger.info(msg)
106
+ task_id = task_definition_pb2.TaskIdentifier(
107
+ org=spec.task_template.id.org,
108
+ project=spec.task_template.id.project,
109
+ domain=spec.task_template.id.domain,
110
+ version=spec.task_template.id.version,
111
+ name=spec.task_template.id.name,
112
+ )
110
113
 
111
- await get_client().task_service.DeployTask(task_service_pb2.DeployTaskRequest(task_id=task_id, spec=spec))
112
- logger.info(f"Deployed task {task.name} with version {task_id.version}")
113
- return spec
114
+ try:
115
+ await get_client().task_service.DeployTask(task_service_pb2.DeployTaskRequest(task_id=task_id, spec=spec))
116
+ logger.info(f"Deployed task {task.name} with version {task_id.version}")
117
+ except grpc.aio.AioRpcError as e:
118
+ if e.code() == grpc.StatusCode.ALREADY_EXISTS:
119
+ logger.info(f"Task {task.name} with image {image_uri} already exists, skipping deployment.")
120
+ return spec
121
+ raise
122
+ return spec
123
+ except Exception as e:
124
+ logger.error(f"Failed to deploy task {task.name} with image {image_uri}: {e}")
125
+ raise flyte.errors.DeploymentError(
126
+ f"Failed to deploy task {task.name} file{task.source_file} with image {image_uri}, Error: {e!s}"
127
+ ) from e
114
128
 
115
129
 
116
130
  async def _build_image_bg(env_name: str, image: Image) -> Tuple[str, str]:
@@ -151,13 +165,14 @@ async def _build_images(deployment: DeploymentPlan) -> ImageCache:
151
165
 
152
166
 
153
167
  @requires_initialization
154
- async def apply(deployment: DeploymentPlan, copy_style: CopyFiles, dryrun: bool = False) -> Deployment:
168
+ async def apply(deployment_plan: DeploymentPlan, copy_style: CopyFiles, dryrun: bool = False) -> Deployment:
155
169
  from ._code_bundle import build_code_bundle
156
170
 
157
171
  cfg = get_common_config()
158
- image_cache = await _build_images(deployment)
159
172
 
160
- version = deployment.version
173
+ image_cache = await _build_images(deployment_plan)
174
+
175
+ version = deployment_plan.version
161
176
  if copy_style == "none" and not version:
162
177
  raise flyte.errors.DeploymentError("Version must be set when copy_style is none")
163
178
  else:
@@ -178,41 +193,44 @@ async def apply(deployment: DeploymentPlan, copy_style: CopyFiles, dryrun: bool
178
193
  )
179
194
 
180
195
  tasks = []
181
- for env_name, env in deployment.envs.items():
196
+
197
+ for env_name, env in deployment_plan.envs.items():
182
198
  logger.info(f"Deploying environment {env_name}")
183
199
  # TODO Make this pluggable based on the environment type
184
200
  if isinstance(env, TaskEnvironment):
185
201
  for task in env.tasks.values():
186
202
  tasks.append(_deploy_task(task, dryrun=dryrun, serialization_context=sc))
187
- return Deployment(envs=deployment.envs, deployed_tasks=await asyncio.gather(*tasks))
203
+ return Deployment(envs=deployment_plan.envs, deployed_tasks=await asyncio.gather(*tasks))
188
204
 
189
205
 
190
- def _recursive_discover(
191
- planned_envs: Dict[str, Environment], envs: Environment | List[Environment]
192
- ) -> Dict[str, Environment]:
206
+ def _recursive_discover(planned_envs: Dict[str, Environment], env: Environment) -> Dict[str, Environment]:
193
207
  """
194
208
  Recursively deploy the environment and its dependencies, if not already deployed (present in env_tasks) and
195
209
  return the updated env_tasks.
196
210
  """
197
- if isinstance(envs, Environment):
198
- envs = [envs]
199
- for env in envs:
200
- # Skip if the environment is already planned
201
- if env.name in planned_envs:
202
- continue
203
- # Recursively discover dependent environments
204
- for dependent_env in env.depends_on:
205
- _recursive_discover(planned_envs, dependent_env)
206
- # Add the environment to the existing envs
207
- planned_envs[env.name] = env
211
+ # Skip if the environment is already planned
212
+ if env.name in planned_envs:
213
+ return planned_envs
214
+ # Recursively discover dependent environments
215
+ for dependent_env in env.depends_on:
216
+ _recursive_discover(planned_envs, dependent_env)
217
+ # Add the environment to the existing envs
218
+ planned_envs[env.name] = env
208
219
  return planned_envs
209
220
 
210
221
 
211
- def plan_deploy(*envs: Environment, version: Optional[str] = None) -> DeploymentPlan:
222
+ def plan_deploy(*envs: Environment, version: Optional[str] = None) -> List[DeploymentPlan]:
212
223
  if envs is None:
213
- return DeploymentPlan({})
214
- planned_envs = _recursive_discover({}, *envs)
215
- return DeploymentPlan(planned_envs, version=version)
224
+ return [DeploymentPlan({})]
225
+ deployment_plans = []
226
+ visited_envs: typing.Set[str] = set()
227
+ for env in envs:
228
+ if env.name in visited_envs:
229
+ continue
230
+ planned_envs = _recursive_discover({}, env)
231
+ deployment_plans.append(DeploymentPlan(planned_envs, version=version))
232
+ visited_envs.update(planned_envs.keys())
233
+ return deployment_plans
216
234
 
217
235
 
218
236
  @syncify
@@ -222,7 +240,7 @@ async def deploy(
222
240
  version: str | None = None,
223
241
  interactive_mode: bool | None = None,
224
242
  copy_style: CopyFiles = "loaded_modules",
225
- ) -> Deployment:
243
+ ) -> List[Deployment]:
226
244
  """
227
245
  Deploy the given environment or list of environments.
228
246
  :param envs: Environment or list of environments to deploy.
@@ -238,16 +256,19 @@ async def deploy(
238
256
  """
239
257
  if interactive_mode:
240
258
  raise NotImplementedError("Interactive mode not yet implemented for deployment")
241
- deployment = plan_deploy(*envs, version=version)
242
- return await apply(deployment, copy_style=copy_style, dryrun=dryrun)
259
+ deployment_plans = plan_deploy(*envs, version=version)
260
+ deployments = []
261
+ for deployment_plan in deployment_plans:
262
+ deployments.append(apply(deployment_plan, copy_style=copy_style, dryrun=dryrun))
263
+ return await asyncio.gather(*deployments)
243
264
 
244
265
 
245
266
  @syncify
246
- async def build_images(*envs: Environment) -> ImageCache:
267
+ async def build_images(envs: Environment) -> ImageCache:
247
268
  """
248
269
  Build the images for the given environments.
249
- :param envs: Environment or list of environments to build images for.
270
+ :param envs: Environment to build images for.
250
271
  :return: ImageCache containing the built images.
251
272
  """
252
- deployment = plan_deploy(*envs)
253
- return await _build_images(deployment)
273
+ deployment = plan_deploy(envs)
274
+ return await _build_images(deployment[0])
flyte/_environment.py CHANGED
@@ -6,14 +6,24 @@ from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union
6
6
 
7
7
  import rich.repr
8
8
 
9
- from flyte._secret import SecretRequest
10
-
11
9
  from ._image import Image
12
10
  from ._resources import Resources
11
+ from ._secret import SecretRequest
13
12
 
14
13
  if TYPE_CHECKING:
15
14
  from kubernetes.client import V1PodTemplate
16
15
 
16
+ # Global registry to track all Environment instances in load order
17
+ _ENVIRONMENT_REGISTRY: List[Environment] = []
18
+
19
+
20
+ def list_loaded_environments() -> List[Environment]:
21
+ """
22
+ Return a list of all Environment objects in the order they were loaded.
23
+ This is useful for deploying environments in the order they were defined.
24
+ """
25
+ return _ENVIRONMENT_REGISTRY
26
+
17
27
 
18
28
  def is_snake_or_kebab_with_numbers(s: str) -> bool:
19
29
  return re.fullmatch(r"^[a-z0-9]+([_-][a-z0-9]+)*$", s) is not None
@@ -44,6 +54,8 @@ class Environment:
44
54
  def __post_init__(self):
45
55
  if not is_snake_or_kebab_with_numbers(self.name):
46
56
  raise ValueError(f"Environment name '{self.name}' must be in snake_case or kebab-case format.")
57
+ # Automatically register this environment instance in load order
58
+ _ENVIRONMENT_REGISTRY.append(self)
47
59
 
48
60
  def add_dependency(self, *env: Environment):
49
61
  """
flyte/_image.py CHANGED
@@ -5,10 +5,10 @@ import hashlib
5
5
  import sys
6
6
  import typing
7
7
  from abc import abstractmethod
8
- from dataclasses import asdict, dataclass, field
8
+ from dataclasses import asdict, dataclass, field, fields
9
9
  from functools import cached_property
10
10
  from pathlib import Path
11
- from typing import TYPE_CHECKING, Callable, ClassVar, Dict, List, Literal, Optional, Tuple, TypeVar, Union
11
+ from typing import TYPE_CHECKING, ClassVar, Dict, List, Literal, Optional, Tuple, TypeVar, Union
12
12
 
13
13
  import rich.repr
14
14
  from packaging.version import Version
@@ -49,8 +49,6 @@ class Layer:
49
49
  layered images programmatically.
50
50
  """
51
51
 
52
- _compute_identifier: Callable[[Layer], str] = field(default=lambda x: x.__str__(), init=True)
53
-
54
52
  @abstractmethod
55
53
  def update_hash(self, hasher: hashlib._Hash):
56
54
  """
@@ -66,6 +64,27 @@ class Layer:
66
64
  :return:
67
65
  """
68
66
 
67
+ def identifier(self) -> str:
68
+ """
69
+ This method computes a unique identifier for the layer based on its properties.
70
+ It is used to identify the layer in the image cache.
71
+
72
+ It is also used to compute a unique identifier for the image itself, which is a combination of all the layers.
73
+ This identifier is used to look up previously built images in the image cache. So having a consistent
74
+ identifier is important for the image cache to work correctly.
75
+
76
+ :return: A unique identifier for the layer.
77
+ """
78
+ ignore_fields: list[str] = []
79
+ for f in fields(self):
80
+ if f.metadata.get("identifier", True) is False:
81
+ ignore_fields.append(f.name)
82
+ d = asdict(self)
83
+ for v in ignore_fields:
84
+ d.pop(v)
85
+
86
+ return str(d)
87
+
69
88
 
70
89
  @rich.repr.auto
71
90
  @dataclass(kw_only=True, frozen=True, repr=True)
@@ -133,7 +152,11 @@ class PipPackages(PipOption, Layer):
133
152
  @rich.repr.auto
134
153
  @dataclass(kw_only=True, frozen=True, repr=True)
135
154
  class PythonWheels(PipOption, Layer):
136
- wheel_dir: Path
155
+ wheel_dir: Path = field(metadata={"identifier": False})
156
+ wheel_dir_name: str = field(init=False)
157
+
158
+ def __post_init__(self):
159
+ object.__setattr__(self, "wheel_dir_name", self.wheel_dir.name)
137
160
 
138
161
  def update_hash(self, hasher: hashlib._Hash):
139
162
  super().update_hash(hasher)
@@ -165,6 +188,15 @@ class UVProject(PipOption, Layer):
165
188
  pyproject: Path
166
189
  uvlock: Path
167
190
 
191
+ def validate(self):
192
+ if not self.pyproject.exists():
193
+ raise FileNotFoundError(f"pyproject.toml file {self.pyproject} does not exist")
194
+ if not self.pyproject.is_file():
195
+ raise ValueError(f"Pyproject file {self.pyproject} is not a file")
196
+ if not self.uvlock.exists():
197
+ raise ValueError(f"UVLock file {self.uvlock} does not exist")
198
+ super().validate()
199
+
168
200
  def update_hash(self, hasher: hashlib._Hash):
169
201
  from ._utils import filehash_update
170
202
 
@@ -172,6 +204,34 @@ class UVProject(PipOption, Layer):
172
204
  filehash_update(self.uvlock, hasher)
173
205
 
174
206
 
207
+ @rich.repr.auto
208
+ @dataclass(frozen=True, repr=True)
209
+ class UVScript(PipOption, Layer):
210
+ script: Path = field(metadata={"identifier": False})
211
+ script_name: str = field(init=False)
212
+
213
+ def __post_init__(self):
214
+ object.__setattr__(self, "script_name", self.script.name)
215
+
216
+ def validate(self):
217
+ if not self.script.exists():
218
+ raise FileNotFoundError(f"UV script {self.script} does not exist")
219
+ if not self.script.is_file():
220
+ raise ValueError(f"UV script {self.script} is not a file")
221
+ if not self.script.suffix == ".py":
222
+ raise ValueError(f"UV script {self.script} must have a .py extension")
223
+ super().validate()
224
+
225
+ def update_hash(self, hasher: hashlib._Hash):
226
+ from ._utils import parse_uv_script_file
227
+
228
+ header = parse_uv_script_file(self.script)
229
+ h_tuple = _ensure_tuple(header)
230
+ if h_tuple:
231
+ hasher.update(h_tuple.__str__().encode("utf-8"))
232
+ super().update_hash(hasher)
233
+
234
+
175
235
  @rich.repr.auto
176
236
  @dataclass(frozen=True, repr=True)
177
237
  class AptPackages(Layer):
@@ -217,9 +277,15 @@ class DockerIgnore(Layer):
217
277
  @rich.repr.auto
218
278
  @dataclass(frozen=True, repr=True)
219
279
  class CopyConfig(Layer):
220
- path_type: CopyConfigType
221
- src: Path
222
- dst: str = "."
280
+ path_type: CopyConfigType = field(metadata={"identifier": True})
281
+ src: Path = field(metadata={"identifier": True})
282
+ dst: str
283
+ src_name: str = field(init=False)
284
+
285
+ def __post_init__(self):
286
+ if self.path_type not in (0, 1):
287
+ raise ValueError(f"Invalid path_type {self.path_type}, must be 0 (file) or 1 (directory)")
288
+ object.__setattr__(self, "src_name", self.src.name)
223
289
 
224
290
  def validate(self):
225
291
  if not self.src.exists():
@@ -363,7 +429,7 @@ class Image:
363
429
  # across different SDK versions.
364
430
  # Layers can specify a _compute_identifier optionally, but the default will just stringify
365
431
  image_dict = asdict(self, dict_factory=lambda x: {k: v for (k, v) in x if v is not None and k != "_layers"})
366
- layers_str_repr = "".join([layer._compute_identifier(layer) for layer in self._layers])
432
+ layers_str_repr = "".join([layer.identifier() for layer in self._layers])
367
433
  image_dict["layers"] = layers_str_repr
368
434
  spec_bytes = image_dict.__str__().encode("utf-8")
369
435
  return base64.urlsafe_b64encode(hashlib.md5(spec_bytes).digest()).decode("ascii").rstrip("=")
@@ -400,12 +466,13 @@ class Image:
400
466
  base_image=f"python:{python_version[0]}.{python_version[1]}-slim-bookworm",
401
467
  registry=_BASE_REGISTRY,
402
468
  name=_DEFAULT_IMAGE_NAME,
469
+ python_version=python_version,
403
470
  platform=("linux/amd64", "linux/arm64") if platform is None else platform,
404
471
  )
405
472
  labels_and_user = _DockerLines(
406
473
  (
407
- "LABEL org.opencontainers.image.authors='Union.AI <sales@union.ai>'",
408
- "LABEL org.opencontainers.image.source=https://github.com/unionai/unionv2",
474
+ "LABEL org.opencontainers.image.authors='Union.AI <info@union.ai>'",
475
+ "LABEL org.opencontainers.image.source=https://github.com/flyteorg/flyte",
409
476
  "RUN useradd --create-home --shell /bin/bash flytekit &&"
410
477
  " chown -R flytekit /root && chown -R flytekit /home",
411
478
  "WORKDIR /root",
@@ -511,6 +578,7 @@ class Image:
511
578
  pre: bool = False,
512
579
  extra_args: Optional[str] = None,
513
580
  platform: Optional[Tuple[Architecture, ...]] = None,
581
+ secret_mounts: Optional[SecretRequest] = None,
514
582
  ) -> Image:
515
583
  """
516
584
  Use this method to create a new image with the specified uv script.
@@ -541,39 +609,25 @@ class Image:
541
609
  :param extra_index_urls: extra index urls to use for pip install, default is None
542
610
  :param pre: whether to allow pre-release versions, default is False
543
611
  :param extra_args: extra arguments to pass to pip install, default is None
612
+ :param secret_mounts: Secret mounts to use for the image, default is None.
544
613
 
545
614
  :return: Image
615
+
616
+ Args:
617
+ secret_mounts:
546
618
  """
547
- from ._utils import parse_uv_script_file
619
+ ll = UVScript(
620
+ script=Path(script),
621
+ index_url=index_url,
622
+ extra_index_urls=_ensure_tuple(extra_index_urls) if extra_index_urls else None,
623
+ pre=pre,
624
+ extra_args=extra_args,
625
+ secret_mounts=_ensure_tuple(secret_mounts) if secret_mounts else None,
626
+ )
548
627
 
549
- if isinstance(script, str):
550
- script = Path(script)
551
- if not script.exists():
552
- raise FileNotFoundError(f"UV script {script} does not exist")
553
- if not script.is_file():
554
- raise ValueError(f"UV script {script} is not a file")
555
- if not script.suffix == ".py":
556
- raise ValueError(f"UV script {script} must have a .py extension")
557
- header = parse_uv_script_file(script)
558
-
559
- # todo: arch
560
628
  img = cls.from_debian_base(registry=registry, name=name, python_version=python_version, platform=platform)
561
629
 
562
- # add ca-certificates to the image by default
563
- img = img.with_apt_packages("ca-certificates")
564
-
565
- if header.dependencies:
566
- return img.with_pip_packages(
567
- *header.dependencies,
568
- index_url=index_url,
569
- extra_index_urls=extra_index_urls,
570
- pre=pre,
571
- extra_args=extra_args,
572
- )
573
-
574
- # todo: override the _identifier_override to be the script name or a hash of the script contents
575
- # This is needed because inside the image, the identifier will be computed to be something different.
576
- return img
630
+ return img.clone(addl_layer=ll)
577
631
 
578
632
  def clone(
579
633
  self,
@@ -788,7 +842,7 @@ class Image:
788
842
  :param dst: destination folder in the image
789
843
  :return: Image
790
844
  """
791
- new_image = self.clone(addl_layer=CopyConfig(path_type=1, src=src, dst=dst, _compute_identifier=lambda x: dst))
845
+ new_image = self.clone(addl_layer=CopyConfig(path_type=1, src=src, dst=dst))
792
846
  return new_image
793
847
 
794
848
  def with_source_file(self, src: Path, dst: str = ".") -> Image:
@@ -800,7 +854,7 @@ class Image:
800
854
  :param dst: destination folder in the image
801
855
  :return: Image
802
856
  """
803
- new_image = self.clone(addl_layer=CopyConfig(path_type=0, src=src, dst=dst, _compute_identifier=lambda x: dst))
857
+ new_image = self.clone(addl_layer=CopyConfig(path_type=0, src=src, dst=dst))
804
858
  return new_image
805
859
 
806
860
  def with_dockerignore(self, path: Path) -> Image:
@@ -809,7 +863,8 @@ class Image:
809
863
 
810
864
  def with_uv_project(
811
865
  self,
812
- pyproject_file: Path,
866
+ pyproject_file: str | Path,
867
+ uvlock: Path | None = None,
813
868
  index_url: Optional[str] = None,
814
869
  extra_index_urls: Union[List[str], Tuple[str, ...], None] = None,
815
870
  pre: bool = False,
@@ -823,6 +878,8 @@ class Image:
823
878
  In the Union builders, using this will change the virtual env to /root/.venv
824
879
 
825
880
  :param pyproject_file: path to the pyproject.toml file, needs to have a corresponding uv.lock file
881
+ :param uvlock: path to the uv.lock file, if not specified, will use the default uv.lock file in the same
882
+ directory as the pyproject.toml file. (pyproject.parent / uv.lock)
826
883
  :param index_url: index url to use for pip install, default is None
827
884
  :param extra_index_urls: extra index urls to use for pip install, default is None
828
885
  :param pre: whether to allow pre-release versions, default is False
@@ -830,17 +887,12 @@ class Image:
830
887
  :param secret_mounts: list of secret mounts to use for the build process.
831
888
  :return: Image
832
889
  """
833
- if not pyproject_file.exists():
834
- raise FileNotFoundError(f"UVLock file {pyproject_file} does not exist")
835
- if not pyproject_file.is_file():
836
- raise ValueError(f"UVLock file {pyproject_file} is not a file")
837
- lock = pyproject_file.parent / "uv.lock"
838
- if not lock.exists():
839
- raise ValueError(f"UVLock file {lock} does not exist")
890
+ if isinstance(pyproject_file, str):
891
+ pyproject_file = Path(pyproject_file)
840
892
  new_image = self.clone(
841
893
  addl_layer=UVProject(
842
894
  pyproject=pyproject_file,
843
- uvlock=lock,
895
+ uvlock=uvlock or (pyproject_file.parent / "uv.lock"),
844
896
  index_url=index_url,
845
897
  extra_index_urls=extra_index_urls,
846
898
  pre=pre,
@@ -888,7 +940,7 @@ class Image:
888
940
  dist_folder = Path(__file__).parent.parent.parent / "dist"
889
941
  # Manually declare the PythonWheel so we can set the hashing
890
942
  # used to compute the identifier. Can remove if we ever decide to expose the lambda in with_ commands
891
- with_dist = self.clone(addl_layer=PythonWheels(wheel_dir=dist_folder, _compute_identifier=lambda x: "/dist"))
943
+ with_dist = self.clone(addl_layer=PythonWheels(wheel_dir=dist_folder))
892
944
 
893
945
  return with_dist
894
946
 
@@ -25,8 +25,10 @@ from flyte._image import (
25
25
  PythonWheels,
26
26
  Requirements,
27
27
  UVProject,
28
+ UVScript,
28
29
  WorkDir,
29
30
  _DockerLines,
31
+ _ensure_tuple,
30
32
  )
31
33
  from flyte._internal.imagebuild.image_builder import (
32
34
  DockerAPIImageChecker,
@@ -325,6 +327,22 @@ async def _process_layer(layer: Layer, context_path: Path, dockerfile: str) -> s
325
327
  # Handle Python wheels
326
328
  dockerfile = await PythonWheelHandler.handle(layer, context_path, dockerfile)
327
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
+
328
346
  case Requirements() | PipPackages():
329
347
  # Handle pip packages and requirements
330
348
  dockerfile = await PipAndRequirementsHandler.handle(layer, context_path, dockerfile)
@@ -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=layer.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
- if len(kwargs) < interface.num_required_inputs():
109
- raise ValueError(
110
- f"Received {len(kwargs)} inputs but interface has {interface.num_required_inputs()} required inputs. "
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
- image_cache = await build_images.aio(cast(Environment, obj.parent_env()))
139
-
140
- if self._interactive_mode:
141
- code_bundle = await build_pkl_bundle(
142
- obj,
143
- upload_to_controlplane=not self._dry_run,
144
- copy_bundle_to=self._copy_bundle_to,
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
- if self._copy_files != "none":
148
- code_bundle = await build_code_bundle(
149
- from_dir=cfg.root_dir,
150
- dryrun=self._dry_run,
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
- code_bundle = None
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/_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.0b4'
21
- __version_tuple__ = version_tuple = (2, 0, 0, 'b4')
20
+ __version__ = version = '2.0.0b6'
21
+ __version_tuple__ = version_tuple = (2, 0, 0, 'b6')
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):
flyte/cli/_deploy.py CHANGED
@@ -1,3 +1,4 @@
1
+ import pathlib
1
2
  from dataclasses import dataclass, field, fields
2
3
  from pathlib import Path
3
4
  from types import ModuleType
@@ -43,6 +44,36 @@ class DeployArguments:
43
44
  )
44
45
  },
45
46
  )
47
+ recursive: bool = field(
48
+ default=False,
49
+ metadata={
50
+ "click.option": click.Option(
51
+ ["--recursive", "-r"],
52
+ is_flag=True,
53
+ help="Recursively deploy all environments in the current directory",
54
+ )
55
+ },
56
+ )
57
+ all: bool = field(
58
+ default=False,
59
+ metadata={
60
+ "click.option": click.Option(
61
+ ["--all"],
62
+ is_flag=True,
63
+ help="Deploy all environments in the current directory, ignoring the file name",
64
+ )
65
+ },
66
+ )
67
+ ignore_load_errors: bool = field(
68
+ default=False,
69
+ metadata={
70
+ "click.option": click.Option(
71
+ ["--ignore-load-errors", "-i"],
72
+ is_flag=True,
73
+ help="Ignore errors when loading environments especially when using --recursive or --all.",
74
+ )
75
+ },
76
+ )
46
77
 
47
78
  @classmethod
48
79
  def from_dict(cls, d: Dict[str, Any]) -> "DeployArguments":
@@ -57,9 +88,9 @@ class DeployArguments:
57
88
 
58
89
 
59
90
  class DeployEnvCommand(click.Command):
60
- def __init__(self, obj_name: str, obj: Any, deploy_args: DeployArguments, *args, **kwargs):
61
- self.obj_name = obj_name
62
- self.obj = obj
91
+ def __init__(self, env_name: str, env: Any, deploy_args: DeployArguments, *args, **kwargs):
92
+ self.env_name = env_name
93
+ self.env = env
63
94
  self.deploy_args = deploy_args
64
95
  super().__init__(*args, **kwargs)
65
96
 
@@ -67,19 +98,81 @@ class DeployEnvCommand(click.Command):
67
98
  from rich.console import Console
68
99
 
69
100
  console = Console()
70
- console.print(f"Deploying root - environment: {self.obj_name}")
101
+ console.print(f"Deploying root - environment: {self.env_name}")
71
102
  obj: CLIConfig = ctx.obj
72
103
  obj.init(self.deploy_args.project, self.deploy_args.domain)
73
104
  with console.status("Deploying...", spinner="dots"):
74
105
  deployment = flyte.deploy(
75
- self.obj,
106
+ self.env,
76
107
  dryrun=self.deploy_args.dry_run,
77
108
  copy_style=self.deploy_args.copy_style,
78
109
  version=self.deploy_args.version,
79
110
  )
80
111
 
81
- console.print(common.get_table("Environments", deployment.env_repr(), simple=obj.simple))
82
- console.print(common.get_table("Tasks", deployment.task_repr(), simple=obj.simple))
112
+ console.print(common.get_table("Environments", deployment[0].env_repr(), simple=obj.simple))
113
+ console.print(common.get_table("Tasks", deployment[0].task_repr(), simple=obj.simple))
114
+
115
+
116
+ class DeployEnvRecursiveCommand(click.Command):
117
+ """
118
+ Command to deploy all loaded environments in a directory or a file, optionally recursively.
119
+ This command will load all python files in the directory, and deploy all environments found in them.
120
+ If the path is a file, it will deploy all environments in that file.
121
+ """
122
+
123
+ def __init__(self, path: pathlib.Path, deploy_args: DeployArguments, *args, **kwargs):
124
+ self.path = path
125
+ self.deploy_args = deploy_args
126
+ super().__init__(*args, **kwargs)
127
+
128
+ def invoke(self, ctx: Context):
129
+ from rich.console import Console
130
+
131
+ from flyte._environment import list_loaded_environments
132
+ from flyte._utils import load_python_modules
133
+
134
+ console = Console()
135
+ obj: CLIConfig = ctx.obj
136
+
137
+ # Load all python modules
138
+ loaded_modules, failed_paths = load_python_modules(self.path, self.deploy_args.recursive)
139
+ if failed_paths:
140
+ console.print(f"Loaded {len(loaded_modules)} modules with, but failed to load {len(failed_paths)} paths:")
141
+ console.print(
142
+ common.get_table("Modules", [[("Path", p), ("Err", e)] for p, e in failed_paths], simple=obj.simple)
143
+ )
144
+ else:
145
+ console.print(f"Loaded {len(loaded_modules)} modules")
146
+
147
+ # Get newly loaded environments
148
+ all_envs = list_loaded_environments()
149
+ if not all_envs:
150
+ console.print("No environments found to deploy")
151
+ return
152
+ console.print(
153
+ common.get_table("Loaded Environments", [[("name", e.name)] for e in all_envs], simple=obj.simple)
154
+ )
155
+
156
+ if not self.deploy_args.ignore_load_errors and len(failed_paths) > 0:
157
+ raise click.ClickException(
158
+ f"Failed to load {len(failed_paths)} files. Use --ignore-load-errors to ignore these errors."
159
+ )
160
+ # Now start connection and deploy all environments
161
+ obj.init(self.deploy_args.project, self.deploy_args.domain)
162
+ with console.status("Deploying...", spinner="dots"):
163
+ deployments = flyte.deploy(
164
+ *all_envs,
165
+ dryrun=self.deploy_args.dry_run,
166
+ copy_style=self.deploy_args.copy_style,
167
+ version=self.deploy_args.version,
168
+ )
169
+
170
+ console.print(
171
+ common.get_table("Environments", [env for d in deployments for env in d.env_repr()], simple=obj.simple)
172
+ )
173
+ console.print(
174
+ common.get_table("Tasks", [task for d in deployments for task in d.task_repr()], simple=obj.simple)
175
+ )
83
176
 
84
177
 
85
178
  class EnvPerFileGroup(common.ObjectsPerFileGroup):
@@ -99,8 +192,8 @@ class EnvPerFileGroup(common.ObjectsPerFileGroup):
99
192
  obj = cast(flyte.Environment, obj)
100
193
  return DeployEnvCommand(
101
194
  name=obj_name,
102
- obj_name=obj_name,
103
- obj=obj,
195
+ env_name=obj_name,
196
+ env=obj,
104
197
  help=f"{obj.name}" + (f": {obj.description}" if obj.description else ""),
105
198
  deploy_args=self.deploy_args,
106
199
  )
@@ -116,20 +209,35 @@ class EnvFiles(common.FileGroup):
116
209
  def __init__(
117
210
  self,
118
211
  *args,
212
+ directory: Path | None = None,
119
213
  **kwargs,
120
214
  ):
121
215
  if "params" not in kwargs:
122
216
  kwargs["params"] = []
123
217
  kwargs["params"].extend(DeployArguments.options())
124
- super().__init__(*args, **kwargs)
218
+ super().__init__(*args, directory=directory, **kwargs)
125
219
 
126
220
  def get_command(self, ctx, filename):
127
221
  deploy_args = DeployArguments.from_dict(ctx.params)
222
+ fp = Path(filename)
223
+ if not fp.exists():
224
+ raise click.BadParameter(f"File {filename} does not exist")
225
+ if deploy_args.recursive or deploy_args.all:
226
+ # If recursive or all, we want to deploy all environments in the current directory
227
+ return DeployEnvRecursiveCommand(
228
+ path=fp,
229
+ deploy_args=deploy_args,
230
+ name=filename,
231
+ help="Deploy all loaded environments from the file, or directory (optional recursively)",
232
+ )
233
+ if fp.is_dir():
234
+ # If the path is a directory, we want to deploy all environments in that directory
235
+ return EnvFiles(directory=fp)
128
236
  return EnvPerFileGroup(
129
- filename=Path(filename),
237
+ filename=fp,
130
238
  deploy_args=deploy_args,
131
239
  name=filename,
132
- help=f"Run, functions decorated `env.task` or instances of Tasks in {filename}",
240
+ help="Deploy a single environment and all its dependencies, from the file.",
133
241
  )
134
242
 
135
243
 
flyte/cli/_gen.py CHANGED
@@ -38,6 +38,10 @@ def walk_commands(ctx: click.Context) -> Generator[Tuple[str, click.Command], No
38
38
 
39
39
  if not isinstance(command, click.Group):
40
40
  yield ctx.command_path, command
41
+ elif isinstance(command, common.FileGroup):
42
+ # If the command is a FileGroup, yield its file path and the command itself
43
+ # No need to recurse further into FileGroup as it doesn't have subcommands, they are dynamically generated
44
+ yield ctx.command_path, command
41
45
  else:
42
46
  for name in command.list_commands(ctx):
43
47
  subcommand = command.get_command(ctx, name)
flyte/cli/_run.py CHANGED
@@ -199,7 +199,7 @@ class TaskFiles(common.FileGroup):
199
199
  if fp.is_dir():
200
200
  return TaskFiles(directory=fp)
201
201
  return TaskPerFileGroup(
202
- filename=Path(filename),
202
+ filename=fp,
203
203
  run_args=run_args,
204
204
  name=filename,
205
205
  help=f"Run, functions decorated with `env.task` in {filename}",
flyte/errors.py CHANGED
@@ -181,6 +181,16 @@ class ImageBuildError(RuntimeUserError):
181
181
  super().__init__("ImageBuildError", message, "user")
182
182
 
183
183
 
184
+ class ModuleLoadError(RuntimeUserError):
185
+ """
186
+ This error is raised when the module cannot be loaded, either because it does not exist or because of a
187
+ syntax error.
188
+ """
189
+
190
+ def __init__(self, message: str):
191
+ super().__init__("ModuleLoadError", message, "user")
192
+
193
+
184
194
  class InlineIOMaxBytesBreached(RuntimeUserError):
185
195
  """
186
196
  This error is raised when the inline IO max bytes limit is breached.
flyte/models.py CHANGED
@@ -4,7 +4,7 @@ import inspect
4
4
  import os
5
5
  import pathlib
6
6
  from dataclasses import dataclass, field, replace
7
- from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, Literal, Optional, Tuple, Type
7
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, List, Literal, Optional, Tuple, Type
8
8
 
9
9
  import rich.repr
10
10
 
@@ -270,6 +270,14 @@ class NativeInterface:
270
270
  """
271
271
  return self.outputs is not None and len(self.outputs) > 0
272
272
 
273
+ def required_inputs(self) -> List[str]:
274
+ """
275
+ Get the names of the required inputs for the task. This is used to determine which inputs are required for the
276
+ task execution.
277
+ :return: A list of required input names.
278
+ """
279
+ return [k for k, v in self.inputs.items() if v[1] is inspect.Parameter.empty]
280
+
273
281
  def num_required_inputs(self) -> int:
274
282
  """
275
283
  Get the number of required inputs for the task. This is used to determine how many inputs are required for the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flyte
3
- Version: 2.0.0b4
3
+ Version: 2.0.0b6
4
4
  Summary: Add your description here
5
5
  Author-email: Ketan Umare <kumare3@users.noreply.github.com>
6
6
  Requires-Python: >=3.10
@@ -1,14 +1,14 @@
1
1
  flyte/__init__.py,sha256=jWSynBJyJ0WuSjE8HID2MT386ZNlRzx7LzCHW1y_XNw,1468
2
2
  flyte/_build.py,sha256=MkgfLAPeL56YeVrGRNZUCZgbwzlEzVP3wLbl5Qru4yk,578
3
3
  flyte/_context.py,sha256=K0-TCt-_pHOoE5Xni87_8uIe2vCBOhfNQEtjGT4Hu4k,5239
4
- flyte/_deploy.py,sha256=HU2ksZUvHj77DrJm7MryN0n26DUJqo4h7-op4fUTBUg,9593
4
+ flyte/_deploy.py,sha256=v4QYa7L9AeFxZh1Ya5Wn8OSPBA3YAX5favQStdD-x-s,10536
5
5
  flyte/_doc.py,sha256=_OPCf3t_git6UT7kSJISFaWO9cfNzJhhoe6JjVdyCJo,706
6
6
  flyte/_docstring.py,sha256=SsG0Ab_YMAwy2ABJlEo3eBKlyC3kwPdnDJ1FIms-ZBQ,1127
7
- flyte/_environment.py,sha256=oKVXLBX0ky2eE_wjBdzvQGI_2LiT2Nbx58ur7GMt50c,3231
7
+ flyte/_environment.py,sha256=6ks0lkvGt4oSqM5EFPFlhWC3eoUghxUvCn0wstcAD2E,3713
8
8
  flyte/_excepthook.py,sha256=nXts84rzEg6-7RtFarbKzOsRZTQR4plnbWVIFMAEprs,1310
9
9
  flyte/_group.py,sha256=7o1j16sZyUmYB50mOiq1ui4TBAKhRpDqLakV8Ya1kw4,803
10
10
  flyte/_hash.py,sha256=Of_Zl_DzzzF2jp4ZsLm-3o-xJFCCJ8_GubmLI1htx78,504
11
- flyte/_image.py,sha256=G6o2z3JY1epk8oSDuUkTbj-5bNuSetC4w-ciG64FoHQ,35322
11
+ flyte/_image.py,sha256=dSChbZeXBSz77wN-AjcXyWLkVUR0kqOtjUuNsflayJM,37310
12
12
  flyte/_initialize.py,sha256=xKl_LYMluRt21wWqa6RTKuLo0_DCbSaTfUk27_brtNk,18232
13
13
  flyte/_interface.py,sha256=1B9zIwFDjiVp_3l_mk8EpA4g3Re-6DUBEBi9z9vDvPs,3504
14
14
  flyte/_logging.py,sha256=QrT4Z30C2tsZ-yIojisQODTuq6Y6zSJYuTrLgF58UYc,3664
@@ -17,18 +17,18 @@ flyte/_pod.py,sha256=--72b0c6IkOEbBwZPLmgl-ll-j7ECfG-kh75LzBnNN8,1068
17
17
  flyte/_resources.py,sha256=L2JuvQDlMo1JLJeUmJPRwtWbunhR2xJEhFgQW5yc72c,9690
18
18
  flyte/_retry.py,sha256=rfLv0MvWxzPByKESTglEmjPsytEAKiIvvmzlJxXwsfE,941
19
19
  flyte/_reusable_environment.py,sha256=f8Y1GilUwGcXH4n2Fckrnx0SrZmhk3nCfoe-TqUKivI,3740
20
- flyte/_run.py,sha256=HkTD3rHL34pAwvn1WPN6OXYmk-GWX0txLdRH1OIMvEA,24338
20
+ flyte/_run.py,sha256=SSD35ICaYaqt7Ep4SNAi7BLESIullo68g0g4x_dfrW4,25654
21
21
  flyte/_secret.py,sha256=89VIihdXI03irHb217GMfipt7jzXBafm17YYmyv6gHo,3245
22
- flyte/_task.py,sha256=rYR7SVGihfBp7UYrhdmqhHp0ngl2K7Za8IJfhv1K2oc,19402
22
+ flyte/_task.py,sha256=FUqGDtDmhOVPdv-UVko4h0oecoAcc3JZKu8S__cwUpY,19805
23
23
  flyte/_task_environment.py,sha256=Zpfr8gjwXg5KuCfIbT4s3l2mtJCFqDxXwv6ZStHWBuc,9840
24
24
  flyte/_task_plugins.py,sha256=9MH3nFPOH_e8_92BT4sFk4oyAnj6GJFvaPYWaraX7yE,1037
25
25
  flyte/_timeout.py,sha256=zx5sFcbYmjJAJbZWSGzzX-BpC9HC7Jfs35T7vVhKwkk,1571
26
26
  flyte/_tools.py,sha256=tWb0sx3t3mm4jbaQVjCTc9y39oR_Ibo3z_KHToP3Lto,966
27
27
  flyte/_trace.py,sha256=SSE1nzUgmVTS2xFNtchEOjEjlRavMOIInasXzY8i9lU,4911
28
- flyte/_version.py,sha256=_khCeGxTPxDDeGWBrQG01YziZI4eo5tW-iu_1TCc42w,519
29
- flyte/errors.py,sha256=MsVZxq6oeIOXe6AVWpxqA3ZiyxCGM0A6UqnYuvmSUCU,5514
28
+ flyte/_version.py,sha256=c1--_MDUEVzy0HOAoU4GvWafX-tUEg8fSks16cbT7nQ,519
29
+ flyte/errors.py,sha256=DKKw0LVywBNuiWR9WMkODxd_dwWRn-NtyN1TMs6HHd0,5800
30
30
  flyte/extend.py,sha256=GB4ZedGzKa30vYWRVPOdxEeK62xnUVFY4z2tD6H9eEw,376
31
- flyte/models.py,sha256=AJZXL8eRmZ2RHEvjL6VSdpTmgF5S1Ekh_tAKRn0b6mM,15321
31
+ flyte/models.py,sha256=2TgfrkPPgcnnk1P_MO5SEmOYAUbsMKl3gxIDwhW2yEU,15674
32
32
  flyte/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  flyte/_bin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  flyte/_bin/runtime.py,sha256=2jTy3ccvrJ__Xrfdo2t0Fxhsojc5o2zIxDHt98RE_eU,6475
@@ -40,7 +40,7 @@ flyte/_code_bundle/__init__.py,sha256=G7DJTQ0UN_ETvdh55pYcWsTrZJKXEcyQl9iQQNQOBX
40
40
  flyte/_code_bundle/_ignore.py,sha256=Tfaoa62CQVTH17kBHD6Xv6xEh1FhcAyvXivl9m-MEE0,3853
41
41
  flyte/_code_bundle/_packaging.py,sha256=5QUuea6kg9s-ebBg7gFAHaxOMchxR5MhTQ8KohWsjPk,6909
42
42
  flyte/_code_bundle/_utils.py,sha256=qlAVmik9rLasfd1oNrCxhL870w5ntk5ZlNGeaKSKaAU,12028
43
- flyte/_code_bundle/bundle.py,sha256=nUAwYTVAE3Z9dfgkBtsqCoKJImjSl4AicG36yweWHLc,8797
43
+ flyte/_code_bundle/bundle.py,sha256=QbodfyX1RW_V8v0lW8kNwJ8lf4JCL3_uVE43_9ePAzo,8842
44
44
  flyte/_internal/__init__.py,sha256=vjXgGzAAjy609YFkAy9_RVPuUlslsHSJBXCLNTVnqOY,136
45
45
  flyte/_internal/controllers/__init__.py,sha256=TVAc4ydsldcIFmN3PW9-IX5UkKeD8oOmuIukIgEae9M,4341
46
46
  flyte/_internal/controllers/_local_controller.py,sha256=__-eEira0k18DsBu1LBXeEjhFGFcp1Uai9K0YEBbwKM,7300
@@ -53,15 +53,15 @@ flyte/_internal/controllers/remote/_core.py,sha256=PhqI_qwKieH0abOzXYzUZt3v166Dx
53
53
  flyte/_internal/controllers/remote/_informer.py,sha256=w4p29_dzS_ns762eNBljvnbJLgCm36d1Ogo2ZkgV1yg,14418
54
54
  flyte/_internal/controllers/remote/_service_protocol.py,sha256=B9qbIg6DiGeac-iSccLmX_AL2xUgX4ezNUOiAbSy4V0,1357
55
55
  flyte/_internal/imagebuild/__init__.py,sha256=dwXdJ1jMhw9RF8itF7jkPLanvX1yCviSns7hE5eoIts,102
56
- flyte/_internal/imagebuild/docker_builder.py,sha256=8k8YPqhr22p_SRx3_EaOlaXB67CGWFP3reRdQ0iOS8E,19770
56
+ flyte/_internal/imagebuild/docker_builder.py,sha256=tTA5Wg479ThCB2qwttrZcAqUWBjzzOVUwVg1wNKY4u4,20508
57
57
  flyte/_internal/imagebuild/image_builder.py,sha256=dXBXl62qcPabus6dR3eP8P9mBGNhpZHZ2Xm12AymKkk,11150
58
- flyte/_internal/imagebuild/remote_builder.py,sha256=vxPhosMbFXQGQY0Um6il4C8I4vZEWsDyBK3uJQN3icg,10602
58
+ flyte/_internal/imagebuild/remote_builder.py,sha256=oP8JgnkRgkEMvIdcakxvXjOZiKEYyt5h6CvgEbakSAM,11016
59
59
  flyte/_internal/resolvers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
60
  flyte/_internal/resolvers/_task_module.py,sha256=jwy1QYygUK7xmpCZLt1SPTfJCkfox3Ck3mTlTsm66UI,1973
61
61
  flyte/_internal/resolvers/common.py,sha256=ADQLRoyGsJ4vuUkitffMGrMKKjy0vpk6X53g4FuKDLc,993
62
62
  flyte/_internal/resolvers/default.py,sha256=nX4DHUYod1nRvEsl_vSgutQVEdExu2xL8pRkyi4VWbY,981
63
63
  flyte/_internal/runtime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
- flyte/_internal/runtime/convert.py,sha256=yK5Fy25-CVSqTtWF8BuBel2jwlVoh8R5F4UhIMYpgmg,16086
64
+ flyte/_internal/runtime/convert.py,sha256=dX3PfIhuYvuCpRAFGRo-uTRy8Ys3Rgs9cowO6XUjHMo,15951
65
65
  flyte/_internal/runtime/entrypoints.py,sha256=9Ng-aQ45M-_MMWeOe9uGmgx69qO9b0xaMRiu542ZI9g,6581
66
66
  flyte/_internal/runtime/io.py,sha256=ysL7hMpfVumvsEYWOM-_VPa8MXn5_X_CZorKbOThyv4,5935
67
67
  flyte/_internal/runtime/resources_serde.py,sha256=TObMVsSjVcQhcY8-nY81pbvrz7TP-adDD5xV-LqAaxM,4813
@@ -145,26 +145,27 @@ flyte/_protos/workflow/task_definition_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8g
145
145
  flyte/_protos/workflow/task_service_pb2.py,sha256=7kCVgR8Is9MzlbdoGd1kVCwz1ot39r2qyY3oZzE_Xuo,5781
146
146
  flyte/_protos/workflow/task_service_pb2.pyi,sha256=W0OZWui3TbQANi0GL7lCVmbpJKRwJ0X-8vjXj1qNP5k,3100
147
147
  flyte/_protos/workflow/task_service_pb2_grpc.py,sha256=whmfmOTiNhz6_CBsXm8aXUCwtA5bncOikqKYz-bKdok,5992
148
- flyte/_utils/__init__.py,sha256=RByuYa2hSqsKrpLkFfjKspKxH3K7DzfVzTVnZsxkO0A,757
148
+ flyte/_utils/__init__.py,sha256=Lwn9_fLxNF4YR0oCUIKCwM_aXYT5UFjUFInofTHnQTs,831
149
149
  flyte/_utils/asyn.py,sha256=KeJKarXNIyD16g6oPM0T9cH7JDmh1KY7JLbwo7i0IlQ,3673
150
150
  flyte/_utils/async_cache.py,sha256=JtZJmWO62OowJ0QFNl6wryWqh-kuDi76aAASMie87QY,4596
151
151
  flyte/_utils/coro_management.py,sha256=wIsul4XY-tQbH9bjqZ3A0jKluE19xSzLlkMeYu_dk_A,934
152
152
  flyte/_utils/file_handling.py,sha256=iU4TxW--fCho_Eg5xTMODn96P03SxzF-V-5f-7bZAZY,2233
153
153
  flyte/_utils/helpers.py,sha256=9N70yzfLF4lLGEEdOv5OcweEpYtrCvZqqhtzkjZUXNY,4779
154
154
  flyte/_utils/lazy_module.py,sha256=fvXPjvZLzCfcI8Vzs4pKedUDdY0U_RQ1ZVrp9b8qBQY,1994
155
+ flyte/_utils/module_loader.py,sha256=XDmK2qndI2Lx-7JUvPi0LW33_zr5HgCgt-FsyXJzccI,3124
155
156
  flyte/_utils/org_discovery.py,sha256=C7aJa0LfnWBkDtSU9M7bE60zp27qEhJC58piqOErZ94,2088
156
157
  flyte/_utils/uv_script_parser.py,sha256=PxqD8lSMi6xv0uDd1s8LKB2IPZr4ttZJCUweqlyMTKk,1483
157
158
  flyte/cli/__init__.py,sha256=aeCcumeP9xD_5aCmaRYUPCe2QRJSGCaxcUbTZ3co768,341
158
159
  flyte/cli/_abort.py,sha256=Ty-63Gtd2PUn6lCuL5AaasfBoPu7TDSU5EQKVbkF4qw,661
159
160
  flyte/cli/_build.py,sha256=SBgybTVWOZ22VBHFL8CVFB_oo34lF9wvlwNirYFFyk0,3543
160
- flyte/cli/_common.py,sha256=k5_DwLNm2Nqps99ry3Bkbnw9tVxeQd3Ya7fSpReaUn4,12315
161
+ flyte/cli/_common.py,sha256=SLY3M7ganzLCf2MaybJFRkk677IoZrkZKjIR5Qoc0cE,12542
161
162
  flyte/cli/_create.py,sha256=Rv_Ox_OA9TqdSI6zaTzLp9vwiqOanOk-Oasnbgx1Q3M,5081
162
163
  flyte/cli/_delete.py,sha256=VTmXv09PBjkdtyl23mbSjIQQlN7Y1AI_bO0GkHP-f9E,546
163
- flyte/cli/_deploy.py,sha256=Zxm7vn1zrqmll73CJTiVGYJI95P-XBI1AlhOlmbmkD0,4635
164
- flyte/cli/_gen.py,sha256=vlE5l8UR1zz4RSdaRyUfYFvGR0TLxGcTYcP4dhA3Pvg,5458
164
+ flyte/cli/_deploy.py,sha256=h-laMIbqNskxPzqtBNxIhgamjt2MxGoNongi9_nTQbo,8939
165
+ flyte/cli/_gen.py,sha256=ni3E65_wSBc9x5NNbq1REuxfZCJz-ioLMVQnZIgwyYg,5745
165
166
  flyte/cli/_get.py,sha256=fvoJaBmZuD4sDs33dMo94dvBJVk3MaWPJe24e3cG0Ps,10200
166
167
  flyte/cli/_params.py,sha256=8Gj8UYGHwu-SUXGWCTRX5QsVf19NiajhaUMMae6FF9o,19466
167
- flyte/cli/_run.py,sha256=fyxGt5ZbW84EcjPVJq5ADK4254kzYHUa2ggp7MRKarI,7776
168
+ flyte/cli/_run.py,sha256=x1BRMK4M0kUboZVOKNuufi8B0cFjsOE7b36zbHT40Cc,7764
168
169
  flyte/cli/main.py,sha256=9sLH-xaGdF9HamUQrTxmGTGNScdCtBfNmyYyPHiW3vU,5180
169
170
  flyte/config/__init__.py,sha256=MiwEYK5Iv7MRR22z61nzbsbvZ9Q6MdmAU_g9If1Pmb8,144
170
171
  flyte/config/_config.py,sha256=WElU--Kw4MM9zx1v-rLD8qYu2T5Zk0-1QbTpkEc27bc,10779
@@ -226,10 +227,10 @@ flyte/types/_renderer.py,sha256=ygcCo5l60lHufyQISFddZfWwLlQ8kJAKxUT_XnR_6dY,4818
226
227
  flyte/types/_string_literals.py,sha256=NlG1xV8RSA-sZ-n-IFQCAsdB6jXJOAKkHWtnopxVVDk,4231
227
228
  flyte/types/_type_engine.py,sha256=Tas_OXYddOi0nDuORjqan2SkJ96wKD8937I2l1bo8vk,97916
228
229
  flyte/types/_utils.py,sha256=pbts9E1_2LTdLygAY0UYTLYJ8AsN3BZyviSXvrtcutc,2626
229
- flyte-2.0.0b4.data/scripts/runtime.py,sha256=2jTy3ccvrJ__Xrfdo2t0Fxhsojc5o2zIxDHt98RE_eU,6475
230
- flyte-2.0.0b4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
231
- flyte-2.0.0b4.dist-info/METADATA,sha256=hrYyZeScWhM_utQk4oz22SQDBQi6V4W5OqOa2rRnSTw,10004
232
- flyte-2.0.0b4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
233
- flyte-2.0.0b4.dist-info/entry_points.txt,sha256=MIq2z5dBurdCJfpXfMKzgBv7sJOakKRYxr8G0cMiTrg,75
234
- flyte-2.0.0b4.dist-info/top_level.txt,sha256=7dkyFbikvA12LEZEqawx8oDG1CMod6hTliPj7iWzgYo,6
235
- flyte-2.0.0b4.dist-info/RECORD,,
230
+ flyte-2.0.0b6.data/scripts/runtime.py,sha256=2jTy3ccvrJ__Xrfdo2t0Fxhsojc5o2zIxDHt98RE_eU,6475
231
+ flyte-2.0.0b6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
232
+ flyte-2.0.0b6.dist-info/METADATA,sha256=AfK1udiYYhGWg2MmKRBZGsVOe7Ff3QrPdMozeqv3feY,10004
233
+ flyte-2.0.0b6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
234
+ flyte-2.0.0b6.dist-info/entry_points.txt,sha256=MIq2z5dBurdCJfpXfMKzgBv7sJOakKRYxr8G0cMiTrg,75
235
+ flyte-2.0.0b6.dist-info/top_level.txt,sha256=7dkyFbikvA12LEZEqawx8oDG1CMod6hTliPj7iWzgYo,6
236
+ flyte-2.0.0b6.dist-info/RECORD,,