flyte 2.0.0b3__py3-none-any.whl → 2.0.0b5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flyte might be problematic. Click here for more details.

@@ -11,6 +11,7 @@ from typing import ClassVar, Optional, Protocol, cast
11
11
  import aiofiles
12
12
  import click
13
13
 
14
+ from flyte import Secret
14
15
  from flyte._image import (
15
16
  AptPackages,
16
17
  Commands,
@@ -19,12 +20,15 @@ from flyte._image import (
19
20
  Env,
20
21
  Image,
21
22
  Layer,
23
+ PipOption,
22
24
  PipPackages,
23
25
  PythonWheels,
24
26
  Requirements,
25
27
  UVProject,
28
+ UVScript,
26
29
  WorkDir,
27
30
  _DockerLines,
31
+ _ensure_tuple,
28
32
  )
29
33
  from flyte._internal.imagebuild.image_builder import (
30
34
  DockerAPIImageChecker,
@@ -44,6 +48,7 @@ WORKDIR /root
44
48
  RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
45
49
  --mount=type=bind,target=uv.lock,src=uv.lock \
46
50
  --mount=type=bind,target=pyproject.toml,src=pyproject.toml \
51
+ $SECRET_MOUNT \
47
52
  uv sync $PIP_INSTALL_ARGS
48
53
  WORKDIR /
49
54
 
@@ -56,31 +61,35 @@ ENV PATH="/root/.venv/bin:$$PATH" \
56
61
  UV_PACKAGE_INSTALL_COMMAND_TEMPLATE = Template("""\
57
62
  RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
58
63
  --mount=type=bind,target=requirements_uv.txt,src=requirements_uv.txt \
59
- uv pip install --prerelease=allow --python $$UV_PYTHON $PIP_INSTALL_ARGS
64
+ $SECRET_MOUNT \
65
+ uv pip install --python $$UV_PYTHON $PIP_INSTALL_ARGS
60
66
  """)
61
67
 
62
68
  UV_WHEEL_INSTALL_COMMAND_TEMPLATE = Template("""\
63
69
  RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=wheel \
64
70
  --mount=source=/dist,target=/dist,type=bind \
65
- uv pip install --prerelease=allow --python $$UV_PYTHON $PIP_INSTALL_ARGS
71
+ $SECRET_MOUNT \
72
+ uv pip install --python $$UV_PYTHON $PIP_INSTALL_ARGS
66
73
  """)
67
74
 
68
75
  APT_INSTALL_COMMAND_TEMPLATE = Template("""\
69
76
  RUN --mount=type=cache,sharing=locked,mode=0777,target=/var/cache/apt,id=apt \
77
+ $SECRET_MOUNT \
70
78
  apt-get update && apt-get install -y --no-install-recommends \
71
79
  $APT_PACKAGES
72
80
  """)
73
81
 
74
82
  UV_PYTHON_INSTALL_COMMAND = Template("""\
75
83
  RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
84
+ $SECRET_MOUNT \
76
85
  uv pip install $PIP_INSTALL_ARGS
77
86
  """)
78
87
 
79
88
  # uv pip install --python /root/env/bin/python
80
89
  # new template
81
90
  DOCKER_FILE_UV_BASE_TEMPLATE = Template("""\
82
- #syntax=docker/dockerfile:1.5
83
- FROM ghcr.io/astral-sh/uv:0.6.12 as uv
91
+ # syntax=docker/dockerfile:1.10
92
+ FROM ghcr.io/astral-sh/uv:0.6.12 AS uv
84
93
  FROM $BASE_IMAGE
85
94
 
86
95
  USER root
@@ -139,8 +148,10 @@ class PipAndRequirementsHandler:
139
148
 
140
149
  pip_install_args = layer.get_pip_install_args()
141
150
  pip_install_args.extend(["--requirement", "requirements_uv.txt"])
142
-
143
- delta = UV_PACKAGE_INSTALL_COMMAND_TEMPLATE.substitute(PIP_INSTALL_ARGS=" ".join(pip_install_args))
151
+ secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
152
+ delta = UV_PACKAGE_INSTALL_COMMAND_TEMPLATE.substitute(
153
+ PIP_INSTALL_ARGS=" ".join(pip_install_args), SECRET_MOUNT=secret_mounts
154
+ )
144
155
  dockerfile += delta
145
156
 
146
157
  return dockerfile
@@ -152,8 +163,10 @@ class PythonWheelHandler:
152
163
  shutil.copytree(layer.wheel_dir, context_path / "dist", dirs_exist_ok=True)
153
164
  pip_install_args = layer.get_pip_install_args()
154
165
  pip_install_args.extend(["/dist/*.whl"])
155
-
156
- delta = UV_WHEEL_INSTALL_COMMAND_TEMPLATE.substitute(PIP_INSTALL_ARGS=" ".join(pip_install_args))
166
+ secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
167
+ delta = UV_WHEEL_INSTALL_COMMAND_TEMPLATE.substitute(
168
+ PIP_INSTALL_ARGS=" ".join(pip_install_args), SECRET_MOUNT=secret_mounts
169
+ )
157
170
  dockerfile += delta
158
171
 
159
172
  return dockerfile
@@ -181,9 +194,10 @@ class EnvHandler:
181
194
 
182
195
  class AptPackagesHandler:
183
196
  @staticmethod
184
- async def handle(layer: AptPackages, context_path: Path, dockerfile: str) -> str:
197
+ async def handle(layer: AptPackages, _: Path, dockerfile: str) -> str:
185
198
  packages = layer.packages
186
- delta = APT_INSTALL_COMMAND_TEMPLATE.substitute(APT_PACKAGES=" ".join(packages))
199
+ secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
200
+ delta = APT_INSTALL_COMMAND_TEMPLATE.substitute(APT_PACKAGES=" ".join(packages), SECRET_MOUNT=secret_mounts)
187
201
  dockerfile += delta
188
202
 
189
203
  return dockerfile
@@ -200,7 +214,10 @@ class UVProjectHandler:
200
214
  # --no-dev: Omit the development dependency group
201
215
  # --no-install-project: Do not install the current project
202
216
  additional_pip_install_args = ["--locked", "--no-dev", "--no-install-project"]
203
- delta = UV_LOCK_INSTALL_TEMPLATE.substitute(PIP_INSTALL_ARGS=" ".join(additional_pip_install_args))
217
+ secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
218
+ delta = UV_LOCK_INSTALL_TEMPLATE.substitute(
219
+ PIP_INSTALL_ARGS=" ".join(additional_pip_install_args), SECRET_MOUNT=secret_mounts
220
+ )
204
221
  dockerfile += delta
205
222
 
206
223
  return dockerfile
@@ -250,19 +267,82 @@ class CommandsHandler:
250
267
 
251
268
  class WorkDirHandler:
252
269
  @staticmethod
253
- async def handle(layer: WorkDir, context_path: Path, dockerfile: str) -> str:
270
+ async def handle(layer: WorkDir, _: Path, dockerfile: str) -> str:
254
271
  # cd to the workdir
255
272
  dockerfile += f"\nWORKDIR {layer.workdir}\n"
256
273
 
257
274
  return dockerfile
258
275
 
259
276
 
277
+ def _get_secret_commands(layers: typing.Tuple[Layer, ...]) -> typing.List[str]:
278
+ commands = []
279
+
280
+ def _get_secret_command(secret: str | Secret) -> typing.List[str]:
281
+ secret_id = hash(secret)
282
+ if isinstance(secret, str):
283
+ if not os.path.exists(secret):
284
+ raise FileNotFoundError(f"Secret file '{secret}' not found")
285
+ return ["--secret", f"id={secret_id},src={secret}"]
286
+ secret_env_key = "_".join([k.upper() for k in filter(None, (secret.group, secret.key))])
287
+ secret_env = os.getenv(secret_env_key)
288
+ if secret_env:
289
+ return ["--secret", f"id={secret_id},env={secret_env}"]
290
+ secret_file_name = "_".join(list(filter(None, (secret.group, secret.key))))
291
+ secret_file_path = f"/etc/secrets/{secret_file_name}"
292
+ if not os.path.exists(secret_file_path):
293
+ raise FileNotFoundError(f"Secret not found in Env Var {secret_env_key} or file {secret_file_path}")
294
+ return ["--secret", f"id={secret_id},src={secret_file_path}"]
295
+
296
+ for layer in layers:
297
+ if isinstance(layer, (PipOption, AptPackages)):
298
+ if layer.secret_mounts:
299
+ for secret_mount in layer.secret_mounts:
300
+ commands.extend(_get_secret_command(secret_mount))
301
+ return commands
302
+
303
+
304
+ def _get_secret_mounts_layer(secrets: typing.Tuple[str | Secret, ...] | None) -> str:
305
+ if secrets is None:
306
+ return ""
307
+ secret_mounts_layer = ""
308
+ for secret in secrets:
309
+ secret_id = hash(secret)
310
+ if isinstance(secret, str):
311
+ secret_mounts_layer += f"--mount=type=secret,id={secret_id},target=/run/secrets/{os.path.basename(secret)}"
312
+ elif isinstance(secret, Secret):
313
+ if secret.mount:
314
+ secret_mounts_layer += f"--mount=type=secret,id={secret_id},target={secret.mount}"
315
+ elif secret.as_env_var:
316
+ secret_mounts_layer += f"--mount=type=secret,id={secret_id},env={secret.as_env_var}"
317
+ else:
318
+ secret_file_name = "_".join(list(filter(None, (secret.group, secret.key))))
319
+ secret_mounts_layer += f"--mount=type=secret,id={secret_id},src=/run/secrets/{secret_file_name}"
320
+
321
+ return secret_mounts_layer
322
+
323
+
260
324
  async def _process_layer(layer: Layer, context_path: Path, dockerfile: str) -> str:
261
325
  match layer:
262
326
  case PythonWheels():
263
327
  # Handle Python wheels
264
328
  dockerfile = await PythonWheelHandler.handle(layer, context_path, dockerfile)
265
329
 
330
+ case UVScript():
331
+ # Handle UV script
332
+ from flyte._utils import parse_uv_script_file
333
+
334
+ header = parse_uv_script_file(layer.script)
335
+ if header.dependencies:
336
+ pip = PipPackages(
337
+ packages=_ensure_tuple(header.dependencies) if header.dependencies else None,
338
+ secret_mounts=layer.secret_mounts,
339
+ index_url=layer.index_url,
340
+ extra_args=layer.extra_args,
341
+ pre=layer.pre,
342
+ extra_index_urls=layer.extra_index_urls,
343
+ )
344
+ dockerfile = await PipAndRequirementsHandler.handle(pip, context_path, dockerfile)
345
+
266
346
  case Requirements() | PipPackages():
267
347
  # Handle pip packages and requirements
268
348
  dockerfile = await PipAndRequirementsHandler.handle(layer, context_path, dockerfile)
@@ -357,6 +437,8 @@ class DockerImageBuilder(ImageBuilder):
357
437
  else:
358
438
  command.append("--load")
359
439
 
440
+ command.extend(_get_secret_commands(layers=image._layers))
441
+
360
442
  concat_command = " ".join(command)
361
443
  logger.debug(f"Build command: {concat_command}")
362
444
  click.secho(f"Run command: {concat_command} ", fg="blue")
@@ -464,6 +546,8 @@ class DockerImageBuilder(ImageBuilder):
464
546
  command.append("--push")
465
547
  else:
466
548
  command.append("--load")
549
+
550
+ command.extend(_get_secret_commands(layers=image._layers))
467
551
  command.append(tmp_dir)
468
552
 
469
553
  concat_command = " ".join(command)
@@ -23,6 +23,7 @@ from flyte._image import (
23
23
  PythonWheels,
24
24
  Requirements,
25
25
  UVProject,
26
+ UVScript,
26
27
  )
27
28
  from flyte._internal.imagebuild.image_builder import ImageBuilder, ImageChecker
28
29
  from flyte._logging import logger
@@ -196,10 +197,19 @@ def _get_layers_proto(image: Image, context_path: Path) -> "image_definition_pb2
196
197
  )
197
198
  )
198
199
  layers.append(requirements_layer)
199
- elif isinstance(layer, PipPackages):
200
+ elif isinstance(layer, PipPackages) or isinstance(layer, UVScript):
201
+ if isinstance(layer, UVScript):
202
+ from flyte._utils import parse_uv_script_file
203
+
204
+ header = parse_uv_script_file(layer.script)
205
+ if not header.dependencies:
206
+ continue
207
+ packages: typing.Iterable[str] = header.dependencies
208
+ else:
209
+ packages = layer.packages or []
200
210
  pip_layer = image_definition_pb2.Layer(
201
211
  pip_packages=image_definition_pb2.PipPackages(
202
- packages=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/_secret.py CHANGED
@@ -7,8 +7,9 @@ from typing import List, Optional, Union
7
7
  @dataclass
8
8
  class Secret:
9
9
  """
10
- Secrets are used to inject sensitive information into tasks. Secrets can be mounted as environment variables or
11
- files. The secret key is the name of the secret in the secret store. The group is optional and maybe used with some
10
+ Secrets are used to inject sensitive information into tasks or image build context.
11
+ Secrets can be mounted as environment variables or files.
12
+ The secret key is the name of the secret in the secret store. The group is optional and maybe used with some
12
13
  secret stores to organize secrets. The secret_mount is used to specify how the secret should be mounted. If the
13
14
  secret_mount is set to "env" the secret will be mounted as an environment variable. If the secret_mount is set to
14
15
  "file" the secret will be mounted as a file. The as_env_var is an optional parameter that can be used to specify the
flyte/_task.py CHANGED
@@ -150,6 +150,10 @@ class TaskTemplate(Generic[P, R]):
150
150
  self.__dict__.update(state)
151
151
  self.parent_env = None
152
152
 
153
+ @property
154
+ def source_file(self) -> Optional[str]:
155
+ return None
156
+
153
157
  async def pre(self, *args, **kwargs) -> Dict[str, Any]:
154
158
  """
155
159
  This is the preexecute function that will be
@@ -395,6 +399,15 @@ class AsyncFunctionTaskTemplate(TaskTemplate[P, R]):
395
399
  if not iscoroutinefunction(self.func):
396
400
  self._call_as_synchronous = True
397
401
 
402
+ @property
403
+ def source_file(self) -> Optional[str]:
404
+ """
405
+ Returns the source file of the function, if available. This is useful for debugging and tracing.
406
+ """
407
+ if hasattr(self.func, "__code__") and self.func.__code__:
408
+ return self.func.__code__.co_filename
409
+ return None
410
+
398
411
  def forward(self, *args: P.args, **kwargs: P.kwargs) -> Coroutine[Any, Any, R] | R:
399
412
  # In local execution, we want to just call the function. Note we're not awaiting anything here.
400
413
  # If the function was a coroutine function, the coroutine is returned and the await that the caller has
flyte/_utils/__init__.py CHANGED
@@ -9,6 +9,7 @@ from .coro_management import run_coros
9
9
  from .file_handling import filehash_update, update_hasher_for_source
10
10
  from .helpers import get_cwd_editable_install
11
11
  from .lazy_module import lazy_module
12
+ from .module_loader import load_python_modules
12
13
  from .org_discovery import hostname_from_url, org_from_endpoint, sanitize_endpoint
13
14
  from .uv_script_parser import parse_uv_script_file
14
15
 
@@ -18,6 +19,7 @@ __all__ = [
18
19
  "get_cwd_editable_install",
19
20
  "hostname_from_url",
20
21
  "lazy_module",
22
+ "load_python_modules",
21
23
  "org_from_endpoint",
22
24
  "parse_uv_script_file",
23
25
  "run_coros",
@@ -0,0 +1,89 @@
1
+ import glob
2
+ import importlib.util
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import List, Tuple
7
+
8
+ from rich.progress import BarColumn, Progress, TextColumn, TimeElapsedColumn, TimeRemainingColumn
9
+
10
+ import flyte.errors
11
+
12
+
13
+ def load_python_modules(path: Path, recursive: bool = False) -> Tuple[List[str], List[Tuple[Path, str]]]:
14
+ """
15
+ Load all Python modules from a path and return list of loaded module names.
16
+
17
+ :param path: File or directory path
18
+ :param recursive: If True, load modules recursively from subdirectories
19
+ :return: List of loaded module names, and list of file paths that failed to load
20
+ """
21
+ loaded_modules = []
22
+ failed_paths = []
23
+
24
+ if path.is_file() and path.suffix == ".py":
25
+ # Single file case
26
+ module_name = _load_module_from_file(path)
27
+ if module_name:
28
+ loaded_modules.append(module_name)
29
+
30
+ elif path.is_dir():
31
+ # Directory case
32
+ pattern = "**/*.py" if recursive else "*.py"
33
+ python_files = glob.glob(str(path / pattern), recursive=recursive)
34
+
35
+ with Progress(
36
+ TextColumn("[progress.description]{task.description}"),
37
+ BarColumn(),
38
+ "[progress.percentage]{task.percentage:>3.0f}%",
39
+ TimeElapsedColumn(),
40
+ TimeRemainingColumn(),
41
+ TextColumn("• {task.fields[current_file]}"),
42
+ ) as progress:
43
+ task = progress.add_task(f"Loading {len(python_files)} files", total=len(python_files), current_file="")
44
+ for file_path in python_files:
45
+ p = Path(file_path)
46
+ progress.update(task, advance=1, current_file=p.name)
47
+ # Skip __init__.py files
48
+ if p.name == "__init__.py":
49
+ continue
50
+
51
+ try:
52
+ module_name = _load_module_from_file(p)
53
+ if module_name:
54
+ loaded_modules.append(module_name)
55
+ except flyte.errors.ModuleLoadError as e:
56
+ failed_paths.append((p, str(e)))
57
+
58
+ progress.update(task, advance=1, current_file="[green]Done[/green]")
59
+
60
+ return loaded_modules, failed_paths
61
+
62
+
63
+ def _load_module_from_file(file_path: Path) -> str | None:
64
+ """
65
+ Load a Python module from a file path.
66
+
67
+ :param file_path: Path to the Python file
68
+ :return: Module name if successfully loaded, None otherwise
69
+ """
70
+ try:
71
+ # Use the file stem as module name
72
+ module_name = file_path.stem
73
+
74
+ # Load the module specification
75
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
76
+ if spec is None or spec.loader is None:
77
+ return None
78
+
79
+ # Create and execute the module
80
+ module = importlib.util.module_from_spec(spec)
81
+ sys.modules[module_name] = module
82
+ module_path = os.path.dirname(os.path.abspath(file_path))
83
+ sys.path.append(module_path)
84
+ spec.loader.exec_module(module)
85
+
86
+ return module_name
87
+
88
+ except Exception as e:
89
+ raise flyte.errors.ModuleLoadError(f"Failed to load module from {file_path}: {e}") from e
flyte/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.0.0b3'
21
- __version_tuple__ = version_tuple = (2, 0, 0, 'b3')
20
+ __version__ = version = '2.0.0b5'
21
+ __version_tuple__ = version_tuple = (2, 0, 0, 'b5')
flyte/cli/_common.py CHANGED
@@ -265,7 +265,7 @@ class ObjectsPerFileGroup(GroupBase):
265
265
 
266
266
  spec = importlib.util.spec_from_file_location(module_name, self.filename)
267
267
  if spec is None or spec.loader is None:
268
- raise click.ClickException(f"Could not load module {module_name} from {self.filename}")
268
+ raise click.ClickException(f"Could not load module {module_name} from path [{self.filename}]")
269
269
 
270
270
  module = importlib.util.module_from_spec(spec)
271
271
  sys.modules[module_name] = module
@@ -314,6 +314,10 @@ class FileGroup(GroupBase):
314
314
  if self._files is None:
315
315
  directory = self._dir or Path(".").absolute()
316
316
  self._files = [os.fspath(p) for p in directory.glob("*.py") if p.name != "__init__.py"]
317
+ if not self._files:
318
+ self._files = [os.fspath(".")] + [
319
+ os.fspath(p.name) for p in directory.iterdir() if not p.name.startswith(("_", ".")) and p.is_dir()
320
+ ]
317
321
  return self._files
318
322
 
319
323
  def list_commands(self, ctx):