flyte 2.0.0b5__py3-none-any.whl → 2.0.0b7__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/_bin/runtime.py CHANGED
@@ -26,7 +26,6 @@ DOMAIN_NAME = "FLYTE_INTERNAL_TASK_DOMAIN"
26
26
  ORG_NAME = "_U_ORG_NAME"
27
27
  ENDPOINT_OVERRIDE = "_U_EP_OVERRIDE"
28
28
  RUN_OUTPUT_BASE_DIR = "_U_RUN_BASE"
29
- ENABLE_REF_TASKS = "_REF_TASKS" # This is a temporary flag to enable reference tasks in the runtime.
30
29
 
31
30
  # TODO: Remove this after proper auth is implemented
32
31
  _UNION_EAGER_API_KEY_ENV_VAR = "_UNION_EAGER_API_KEY"
@@ -87,6 +86,7 @@ def main(
87
86
 
88
87
  import flyte
89
88
  import flyte._utils as utils
89
+ import flyte.errors
90
90
  from flyte._initialize import init
91
91
  from flyte._internal.controllers import create_controller
92
92
  from flyte._internal.imagebuild.image_builder import ImageCache
@@ -123,22 +123,7 @@ def main(
123
123
  logger.debug(f"Using controller endpoint: {ep} with kwargs: {controller_kwargs}")
124
124
 
125
125
  bundle = CodeBundle(tgz=tgz, pkl=pkl, destination=dest, computed_version=version)
126
- enable_ref_tasks = os.getenv(ENABLE_REF_TASKS, "false").lower() in ("true", "1", "yes")
127
- # We init regular client here so that reference tasks can work
128
- # Current reference tasks will not work with remote controller, because we create 2 different
129
- # channels on different threads and this is not supported by grpcio or the auth system. It ends up leading
130
- # File "src/python/grpcio/grpc/_cython/_cygrpc/aio/completion_queue.pyx.pxi", line 147,
131
- # in grpc._cython.cygrpc.PollerCompletionQueue._handle_events
132
- # BlockingIOError: [Errno 11] Resource temporarily unavailable
133
- # TODO solution is to use a single channel for both controller and reference tasks, but this requires a refactor
134
- if enable_ref_tasks:
135
- logger.warning(
136
- "Reference tasks are enabled. This will initialize client and you will see a BlockIOError. "
137
- "This is harmless, but a nuisance. We are working on a fix."
138
- )
139
- init(org=org, project=project, domain=domain, **controller_kwargs)
140
- else:
141
- init()
126
+ init(org=org, project=project, domain=domain, **controller_kwargs)
142
127
  # Controller is created with the same kwargs as init, so that it can be used to run tasks
143
128
  controller = create_controller(ct="remote", **controller_kwargs)
144
129
 
@@ -164,6 +149,8 @@ def main(
164
149
 
165
150
  # Run both coroutines concurrently and wait for first to finish and cancel the other
166
151
  async def _run_and_stop():
152
+ loop = asyncio.get_event_loop()
153
+ loop.set_exception_handler(flyte.errors.silence_grpc_polling_error)
167
154
  await utils.run_coros(controller_failure, task_coroutine)
168
155
  await controller.stop()
169
156
 
flyte/_cache/cache.py CHANGED
@@ -21,7 +21,7 @@ from flyte.models import CodeBundle
21
21
  P = ParamSpec("P")
22
22
  FuncOut = TypeVar("FuncOut")
23
23
 
24
- CacheBehavior = Literal["auto", "override", "disable", "enabled"]
24
+ CacheBehavior = Literal["auto", "override", "disable"]
25
25
 
26
26
 
27
27
  @dataclass
flyte/_deploy.py CHANGED
@@ -5,7 +5,6 @@ import typing
5
5
  from dataclasses import dataclass
6
6
  from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
7
7
 
8
- import grpc.aio
9
8
  import rich.repr
10
9
 
11
10
  import flyte.errors
@@ -86,6 +85,8 @@ async def _deploy_task(
86
85
  Deploy the given task.
87
86
  """
88
87
  ensure_client()
88
+ import grpc.aio
89
+
89
90
  from ._internal.runtime.convert import convert_upload_default_inputs
90
91
  from ._internal.runtime.task_serde import translate_task_to_wire
91
92
  from ._protos.workflow import task_definition_pb2, task_service_pb2
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)
@@ -184,7 +207,11 @@ class UVProject(PipOption, Layer):
184
207
  @rich.repr.auto
185
208
  @dataclass(frozen=True, repr=True)
186
209
  class UVScript(PipOption, Layer):
187
- script: Path
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)
188
215
 
189
216
  def validate(self):
190
217
  if not self.script.exists():
@@ -196,10 +223,13 @@ class UVScript(PipOption, Layer):
196
223
  super().validate()
197
224
 
198
225
  def update_hash(self, hasher: hashlib._Hash):
199
- from ._utils import filehash_update
226
+ from ._utils import parse_uv_script_file
200
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"))
201
232
  super().update_hash(hasher)
202
- filehash_update(self.script, hasher)
203
233
 
204
234
 
205
235
  @rich.repr.auto
@@ -247,9 +277,15 @@ class DockerIgnore(Layer):
247
277
  @rich.repr.auto
248
278
  @dataclass(frozen=True, repr=True)
249
279
  class CopyConfig(Layer):
250
- path_type: CopyConfigType
251
- src: Path
252
- 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)
253
289
 
254
290
  def validate(self):
255
291
  if not self.src.exists():
@@ -393,7 +429,7 @@ class Image:
393
429
  # across different SDK versions.
394
430
  # Layers can specify a _compute_identifier optionally, but the default will just stringify
395
431
  image_dict = asdict(self, dict_factory=lambda x: {k: v for (k, v) in x if v is not None and k != "_layers"})
396
- 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])
397
433
  image_dict["layers"] = layers_str_repr
398
434
  spec_bytes = image_dict.__str__().encode("utf-8")
399
435
  return base64.urlsafe_b64encode(hashlib.md5(spec_bytes).digest()).decode("ascii").rstrip("=")
@@ -430,6 +466,7 @@ class Image:
430
466
  base_image=f"python:{python_version[0]}.{python_version[1]}-slim-bookworm",
431
467
  registry=_BASE_REGISTRY,
432
468
  name=_DEFAULT_IMAGE_NAME,
469
+ python_version=python_version,
433
470
  platform=("linux/amd64", "linux/arm64") if platform is None else platform,
434
471
  )
435
472
  labels_and_user = _DockerLines(
@@ -572,8 +609,12 @@ class Image:
572
609
  :param extra_index_urls: extra index urls to use for pip install, default is None
573
610
  :param pre: whether to allow pre-release versions, default is False
574
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.
575
613
 
576
614
  :return: Image
615
+
616
+ Args:
617
+ secret_mounts:
577
618
  """
578
619
  ll = UVScript(
579
620
  script=Path(script),
@@ -801,7 +842,7 @@ class Image:
801
842
  :param dst: destination folder in the image
802
843
  :return: Image
803
844
  """
804
- 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))
805
846
  return new_image
806
847
 
807
848
  def with_source_file(self, src: Path, dst: str = ".") -> Image:
@@ -813,7 +854,7 @@ class Image:
813
854
  :param dst: destination folder in the image
814
855
  :return: Image
815
856
  """
816
- 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))
817
858
  return new_image
818
859
 
819
860
  def with_dockerignore(self, path: Path) -> Image:
@@ -899,7 +940,7 @@ class Image:
899
940
  dist_folder = Path(__file__).parent.parent.parent / "dist"
900
941
  # Manually declare the PythonWheel so we can set the hashing
901
942
  # used to compute the identifier. Can remove if we ever decide to expose the lambda in with_ commands
902
- 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))
903
944
 
904
945
  return with_dist
905
946
 
@@ -253,6 +253,21 @@ class RemoteController(Controller):
253
253
  await self.cancel_action(action)
254
254
  raise
255
255
 
256
+ # If the action is aborted, we should abort the controller as well
257
+ if n.phase == run_definition_pb2.PHASE_ABORTED:
258
+ logger.warning(f"Action {n.action_id.name} was aborted, aborting current Action{current_action_id.name}")
259
+ raise flyte.errors.RunAbortedError(
260
+ f"Action {n.action_id.name} was aborted, aborting current Action {current_action_id.name}"
261
+ )
262
+
263
+ if n.phase == run_definition_pb2.PHASE_TIMED_OUT:
264
+ logger.warning(
265
+ f"Action {n.action_id.name} timed out, raising timeout exception Action {current_action_id.name}"
266
+ )
267
+ raise flyte.errors.TaskTimeoutError(
268
+ f"Action {n.action_id.name} timed out, raising exception in current Action {current_action_id.name}"
269
+ )
270
+
256
271
  if n.has_error() or n.phase == run_definition_pb2.PHASE_FAILED:
257
272
  exc = await handle_action_failure(action, _task.name)
258
273
  raise exc
@@ -16,7 +16,6 @@ from flyte._protos.workflow import (
16
16
  queue_service_pb2,
17
17
  task_definition_pb2,
18
18
  )
19
- from flyte.errors import RuntimeSystemError
20
19
 
21
20
  from ._action import Action
22
21
  from ._informer import InformerCache
@@ -125,7 +124,7 @@ class Controller:
125
124
  async def watch_for_errors(self):
126
125
  """Watch for errors in the background thread"""
127
126
  await self._run_coroutine_in_controller_thread(self._bg_watch_for_errors())
128
- raise RuntimeSystemError(
127
+ raise flyte.errors.RuntimeSystemError(
129
128
  code="InformerWatchFailure",
130
129
  message=f"Controller thread failed with exception: {self._get_exception()}",
131
130
  )
@@ -164,7 +163,7 @@ class Controller:
164
163
  raise TimeoutError("Controller thread failed to start in time")
165
164
 
166
165
  if self._get_exception():
167
- raise RuntimeSystemError(
166
+ raise flyte.errors.RuntimeSystemError(
168
167
  type(self._get_exception()).__name__,
169
168
  f"Controller thread startup failed: {self._get_exception()}",
170
169
  )
@@ -212,6 +211,7 @@ class Controller:
212
211
  # Create a new event loop for this thread
213
212
  self._loop = asyncio.new_event_loop()
214
213
  asyncio.set_event_loop(self._loop)
214
+ self._loop.set_exception_handler(flyte.errors.silence_grpc_polling_error)
215
215
  logger.debug(f"Controller thread started with new event loop: {threading.current_thread().name}")
216
216
 
217
217
  # Create an event to signal the errors were observed in the thread's loop
@@ -1,4 +1,6 @@
1
+ import asyncio
1
2
  import sys
3
+ import time
2
4
  from typing import Any, List, Tuple
3
5
 
4
6
  from flyte._context import contextual_run
@@ -75,19 +77,23 @@ async def create_controller(
75
77
  :return:
76
78
  """
77
79
  logger.info(f"[rusty] Creating controller with endpoint {endpoint}")
80
+ import flyte.errors
78
81
  from flyte._initialize import init
79
82
 
80
- # TODO Currently refrence tasks are not supported in Rusty.
83
+ loop = asyncio.get_event_loop()
84
+ loop.set_exception_handler(flyte.errors.silence_grpc_polling_error)
85
+
86
+ # TODO Currently reference tasks are not supported in Rusty.
81
87
  await init.aio()
82
88
  controller_kwargs: dict[str, Any] = {"insecure": insecure}
83
89
  if api_key:
84
- logger.info("Using api key from environment")
90
+ logger.info("[rusty] Using api key from environment")
85
91
  controller_kwargs["api_key"] = api_key
86
92
  else:
87
93
  controller_kwargs["endpoint"] = endpoint
88
94
  if "localhost" in endpoint or "docker" in endpoint:
89
95
  controller_kwargs["insecure"] = True
90
- logger.debug(f"Using controller endpoint: {endpoint} with kwargs: {controller_kwargs}")
96
+ logger.debug(f"[rusty] Using controller endpoint: {endpoint} with kwargs: {controller_kwargs}")
91
97
 
92
98
  return _create_controller(ct="remote", **controller_kwargs)
93
99
 
@@ -130,22 +136,39 @@ async def run_task(
130
136
  :param input_path: Optional input path for the task.
131
137
  :return: The loaded task template.
132
138
  """
133
- logger.info(f"[rusty] Running task {task.name}")
134
- await contextual_run(
135
- extract_download_run_upload,
136
- task,
137
- action=ActionID(name=name, org=org, project=project, domain=domain, run_name=run_name),
138
- version=version,
139
- controller=controller,
140
- raw_data_path=RawDataPath(path=raw_data_path),
141
- output_path=output_path,
142
- run_base_dir=run_base_dir,
143
- checkpoints=Checkpoints(prev_checkpoint_path=prev_checkpoint, checkpoint_path=checkpoint_path),
144
- code_bundle=code_bundle,
145
- input_path=input_path,
146
- image_cache=ImageCache.from_transport(image_cache) if image_cache else None,
139
+ start_time = time.time()
140
+ action_id = f"{org}/{project}/{domain}/{run_name}/{name}"
141
+
142
+ logger.info(
143
+ f"[rusty] Running task '{task.name}' (action: {action_id})"
144
+ f" at {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(start_time))}"
147
145
  )
148
- logger.info(f"[rusty] Finished task {task.name}")
146
+
147
+ try:
148
+ await contextual_run(
149
+ extract_download_run_upload,
150
+ task,
151
+ action=ActionID(name=name, org=org, project=project, domain=domain, run_name=run_name),
152
+ version=version,
153
+ controller=controller,
154
+ raw_data_path=RawDataPath(path=raw_data_path),
155
+ output_path=output_path,
156
+ run_base_dir=run_base_dir,
157
+ checkpoints=Checkpoints(prev_checkpoint_path=prev_checkpoint, checkpoint_path=checkpoint_path),
158
+ code_bundle=code_bundle,
159
+ input_path=input_path,
160
+ image_cache=ImageCache.from_transport(image_cache) if image_cache else None,
161
+ )
162
+ except Exception as e:
163
+ logger.error(f"[rusty] Task failed: {e!s}")
164
+ raise
165
+ finally:
166
+ end_time = time.time()
167
+ duration = end_time - start_time
168
+ logger.info(
169
+ f"[rusty] TASK_EXECUTION_END: Task '{task.name}' (action: {action_id})"
170
+ f" done after {duration:.2f}s at {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(end_time))}"
171
+ )
149
172
 
150
173
 
151
174
  async def ping(name: str) -> str:
flyte/_run.py CHANGED
@@ -18,7 +18,6 @@ from flyte._initialize import (
18
18
  requires_storage,
19
19
  )
20
20
  from flyte._logging import logger
21
- from flyte._protos.common import identifier_pb2
22
21
  from flyte._task import P, R, TaskTemplate
23
22
  from flyte._tools import ipython_check
24
23
  from flyte.errors import InitializationError
@@ -411,6 +410,7 @@ class _Runner:
411
410
  async def _run_local(self, obj: TaskTemplate[P, R], *args: P.args, **kwargs: P.kwargs) -> Run:
412
411
  from flyte._internal.controllers import create_controller
413
412
  from flyte._internal.controllers._local_controller import LocalController
413
+ from flyte._protos.common import identifier_pb2
414
414
  from flyte.remote import Run
415
415
  from flyte.report import Report
416
416
 
@@ -62,7 +62,7 @@ class TaskEnvironment(Environment):
62
62
  :param reusable: Reuse policy for the environment, if set, a python process may be reused for multiple tasks.
63
63
  """
64
64
 
65
- cache: Union[CacheRequest] = "disable"
65
+ cache: CacheRequest = "disable"
66
66
  reusable: ReusePolicy | None = None
67
67
  plugin_config: Optional[Any] = None
68
68
  # TODO Shall we make this union of string or env? This way we can lookup the env by module/file:name
@@ -134,7 +134,7 @@ class TaskEnvironment(Environment):
134
134
  _func=None,
135
135
  *,
136
136
  name: Optional[str] = None,
137
- cache: Union[CacheRequest] | None = None,
137
+ cache: CacheRequest | None = None,
138
138
  retries: Union[int, RetryStrategy] = 0,
139
139
  timeout: Union[timedelta, int] = 0,
140
140
  docs: Optional[Documentation] = None,
flyte/_version.py CHANGED
@@ -1,7 +1,14 @@
1
1
  # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
3
 
4
- __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
5
12
 
6
13
  TYPE_CHECKING = False
7
14
  if TYPE_CHECKING:
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
9
16
  from typing import Union
10
17
 
11
18
  VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
12
20
  else:
13
21
  VERSION_TUPLE = object
22
+ COMMIT_ID = object
14
23
 
15
24
  version: str
16
25
  __version__: str
17
26
  __version_tuple__: VERSION_TUPLE
18
27
  version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
19
30
 
20
- __version__ = version = '2.0.0b5'
21
- __version_tuple__ = version_tuple = (2, 0, 0, 'b5')
31
+ __version__ = version = '2.0.0b7'
32
+ __version_tuple__ = version_tuple = (2, 0, 0, 'b7')
33
+
34
+ __commit_id__ = commit_id = 'g5cfd1e5ec'
flyte/cli/_build.py CHANGED
@@ -54,7 +54,7 @@ class BuildEnvCommand(click.Command):
54
54
  with console.status("Building...", spinner="dots"):
55
55
  image_cache = flyte.build_images(self.obj)
56
56
 
57
- console.print(common.get_table("Images", image_cache.repr(), simple=obj.simple))
57
+ console.print(common.format("Images", image_cache.repr(), obj.output_format))
58
58
 
59
59
 
60
60
  class EnvPerFileGroup(common.ObjectsPerFileGroup):
flyte/cli/_common.py CHANGED
@@ -8,19 +8,22 @@ from abc import abstractmethod
8
8
  from dataclasses import dataclass, replace
9
9
  from pathlib import Path
10
10
  from types import MappingProxyType, ModuleType
11
- from typing import Any, Dict, Iterable, List, Optional
11
+ from typing import Any, Dict, Iterable, List, Literal, Optional
12
12
 
13
13
  import rich.box
14
14
  import rich.repr
15
15
  import rich_click as click
16
16
  from rich.console import Console
17
17
  from rich.panel import Panel
18
+ from rich.pretty import pretty_repr
18
19
  from rich.table import Table
19
20
  from rich.traceback import Traceback
20
21
 
21
22
  import flyte.errors
22
23
  from flyte.config import Config
23
24
 
25
+ OutputFormat = Literal["table", "json", "table-simple"]
26
+
24
27
  PREFERRED_BORDER_COLOR = "dim cyan"
25
28
  PREFERRED_ACCENT_COLOR = "bold #FFD700"
26
29
  HEADER_STYLE = f"{PREFERRED_ACCENT_COLOR} on black"
@@ -99,8 +102,8 @@ class CLIConfig:
99
102
  endpoint: str | None = None
100
103
  insecure: bool = False
101
104
  org: str | None = None
102
- simple: bool = False
103
105
  auth_type: str | None = None
106
+ output_format: OutputFormat = "table"
104
107
 
105
108
  def replace(self, **kwargs) -> CLIConfig:
106
109
  """
@@ -327,20 +330,7 @@ class FileGroup(GroupBase):
327
330
  raise NotImplementedError
328
331
 
329
332
 
330
- def get_table(title: str, vals: Iterable[Any], simple: bool = False) -> Table:
331
- """
332
- Get a table from a list of values.
333
- """
334
- if simple:
335
- table = Table(title, box=None)
336
- else:
337
- table = Table(
338
- title=title,
339
- box=rich.box.SQUARE_DOUBLE_HEAD,
340
- header_style=HEADER_STYLE,
341
- show_header=True,
342
- border_style=PREFERRED_BORDER_COLOR,
343
- )
333
+ def _table_format(table: Table, vals: Iterable[Any]) -> Table:
344
334
  headers = None
345
335
  has_rich_repr = False
346
336
  for p in vals:
@@ -357,11 +347,37 @@ def get_table(title: str, vals: Iterable[Any], simple: bool = False) -> Table:
357
347
  return table
358
348
 
359
349
 
360
- def get_panel(title: str, renderable: Any, simple: bool = False) -> Panel:
350
+ def format(title: str, vals: Iterable[Any], of: OutputFormat = "table") -> Table | Any:
351
+ """
352
+ Get a table from a list of values.
353
+ """
354
+
355
+ match of:
356
+ case "table-simple":
357
+ return _table_format(Table(title, box=None), vals)
358
+ case "table":
359
+ return _table_format(
360
+ Table(
361
+ title=title,
362
+ box=rich.box.SQUARE_DOUBLE_HEAD,
363
+ header_style=HEADER_STYLE,
364
+ show_header=True,
365
+ border_style=PREFERRED_BORDER_COLOR,
366
+ ),
367
+ vals,
368
+ )
369
+ case "json":
370
+ if not vals:
371
+ return pretty_repr([])
372
+ return pretty_repr([v.to_dict() for v in vals])
373
+ raise click.ClickException("Unknown output format. Supported formats are: table, table-simple, json.")
374
+
375
+
376
+ def get_panel(title: str, renderable: Any, of: OutputFormat = "table") -> Panel:
361
377
  """
362
378
  Get a panel from a list of values.
363
379
  """
364
- if simple:
380
+ if of in ["table-simple", "json"]:
365
381
  return renderable
366
382
  return Panel.fit(
367
383
  renderable,
flyte/cli/_create.py CHANGED
@@ -4,6 +4,7 @@ from typing import Any, Dict, get_args
4
4
  import rich_click as click
5
5
 
6
6
  import flyte.cli._common as common
7
+ from flyte.cli._option import MutuallyExclusiveOption
7
8
  from flyte.remote import SecretTypes
8
9
 
9
10
 
@@ -16,8 +17,21 @@ def create():
16
17
 
17
18
  @create.command(cls=common.CommandBase)
18
19
  @click.argument("name", type=str, required=True)
19
- @click.argument("value", type=str, required=False)
20
- @click.option("--from-file", type=click.Path(exists=True), help="Path to the file with the binary secret.")
20
+ @click.option(
21
+ "--value",
22
+ help="Secret value",
23
+ prompt="Enter secret value",
24
+ hide_input=True,
25
+ cls=MutuallyExclusiveOption,
26
+ mutually_exclusive=["from_file"],
27
+ )
28
+ @click.option(
29
+ "--from-file",
30
+ type=click.Path(exists=True),
31
+ help="Path to the file with the binary secret.",
32
+ cls=MutuallyExclusiveOption,
33
+ mutually_exclusive=["value"],
34
+ )
21
35
  @click.option(
22
36
  "--type", type=click.Choice(get_args(SecretTypes)), default="regular", help="Type of the secret.", show_default=True
23
37
  )
@@ -38,6 +52,14 @@ def secret(
38
52
  $ flyte create secret my_secret --value my_value
39
53
  ```
40
54
 
55
+ If you don't provide a `--value` flag, you will be prompted to enter the
56
+ secret value in the terminal.
57
+
58
+ ```bash
59
+ $ flyte create secret my_secret
60
+ Enter secret value:
61
+ ```
62
+
41
63
  If `--from-file` is specified, the value will be read from the file instead of being provided directly:
42
64
 
43
65
  ```bash
flyte/cli/_deploy.py CHANGED
@@ -109,8 +109,8 @@ class DeployEnvCommand(click.Command):
109
109
  version=self.deploy_args.version,
110
110
  )
111
111
 
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))
112
+ console.print(common.format("Environments", deployment[0].env_repr(), obj.output_format))
113
+ console.print(common.format("Tasks", deployment[0].task_repr(), obj.output_format))
114
114
 
115
115
 
116
116
  class DeployEnvRecursiveCommand(click.Command):
@@ -139,7 +139,7 @@ class DeployEnvRecursiveCommand(click.Command):
139
139
  if failed_paths:
140
140
  console.print(f"Loaded {len(loaded_modules)} modules with, but failed to load {len(failed_paths)} paths:")
141
141
  console.print(
142
- common.get_table("Modules", [[("Path", p), ("Err", e)] for p, e in failed_paths], simple=obj.simple)
142
+ common.format("Modules", [[("Path", p), ("Err", e)] for p, e in failed_paths], obj.output_format)
143
143
  )
144
144
  else:
145
145
  console.print(f"Loaded {len(loaded_modules)} modules")
@@ -149,9 +149,7 @@ class DeployEnvRecursiveCommand(click.Command):
149
149
  if not all_envs:
150
150
  console.print("No environments found to deploy")
151
151
  return
152
- console.print(
153
- common.get_table("Loaded Environments", [[("name", e.name)] for e in all_envs], simple=obj.simple)
154
- )
152
+ console.print(common.format("Loaded Environments", [[("name", e.name)] for e in all_envs], obj.output_format))
155
153
 
156
154
  if not self.deploy_args.ignore_load_errors and len(failed_paths) > 0:
157
155
  raise click.ClickException(
@@ -168,11 +166,9 @@ class DeployEnvRecursiveCommand(click.Command):
168
166
  )
169
167
 
170
168
  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)
169
+ common.format("Environments", [env for d in deployments for env in d.env_repr()], obj.output_format)
175
170
  )
171
+ console.print(common.format("Tasks", [task for d in deployments for task in d.task_repr()], obj.output_format))
176
172
 
177
173
 
178
174
  class EnvPerFileGroup(common.ObjectsPerFileGroup):
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)