flyte 2.0.0b6__py3-none-any.whl → 2.0.0b8__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
@@ -154,6 +154,7 @@ class PipPackages(PipOption, Layer):
154
154
  class PythonWheels(PipOption, Layer):
155
155
  wheel_dir: Path = field(metadata={"identifier": False})
156
156
  wheel_dir_name: str = field(init=False)
157
+ package_name: str
157
158
 
158
159
  def __post_init__(self):
159
160
  object.__setattr__(self, "wheel_dir_name", self.wheel_dir.name)
@@ -498,7 +499,8 @@ class Image:
498
499
  image = image.with_pip_packages(f"flyte=={flyte_version}", pre=True)
499
500
  else:
500
501
  image = image.with_pip_packages(f"flyte=={flyte_version}")
501
- object.__setattr__(image, "_tag", preset_tag)
502
+ if not dev_mode:
503
+ object.__setattr__(image, "_tag", preset_tag)
502
504
  # Set this to auto for all auto images because the meaning of "auto" can change (based on logic inside
503
505
  # _get_default_image_for, acts differently in a running task container) so let's make sure it stays auto.
504
506
  object.__setattr__(image, "_identifier_override", "auto")
@@ -546,7 +548,7 @@ class Image:
546
548
  platform=platform,
547
549
  )
548
550
 
549
- if registry and name:
551
+ if registry or name:
550
552
  return base_image.clone(registry=registry, name=name)
551
553
 
552
554
  # # Set this to auto for all auto images because the meaning of "auto" can change (based on logic inside
@@ -940,7 +942,7 @@ class Image:
940
942
  dist_folder = Path(__file__).parent.parent.parent / "dist"
941
943
  # Manually declare the PythonWheel so we can set the hashing
942
944
  # used to compute the identifier. Can remove if we ever decide to expose the lambda in with_ commands
943
- with_dist = self.clone(addl_layer=PythonWheels(wheel_dir=dist_folder))
945
+ with_dist = self.clone(addl_layer=PythonWheels(wheel_dir=dist_folder, package_name="flyte", pre=True))
944
946
 
945
947
  return with_dist
946
948
 
@@ -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
@@ -37,6 +37,7 @@ from flyte._internal.imagebuild.image_builder import (
37
37
  LocalDockerCommandImageChecker,
38
38
  LocalPodmanCommandImageChecker,
39
39
  )
40
+ from flyte._internal.imagebuild.utils import copy_files_to_context
40
41
  from flyte._logging import logger
41
42
 
42
43
  _F_IMG_ID = "_F_IMG_ID"
@@ -109,7 +110,7 @@ RUN uv venv $$VIRTUALENV --python=$PYTHON_VERSION
109
110
 
110
111
  # Adds nvidia just in case it exists
111
112
  ENV PATH="$$PATH:/usr/local/nvidia/bin:/usr/local/cuda/bin" \
112
- LD_LIBRARY_PATH="/usr/local/nvidia/lib64:$$LD_LIBRARY_PATH"
113
+ LD_LIBRARY_PATH="/usr/local/nvidia/lib64"
113
114
  """)
114
115
 
115
116
  # This gets added on to the end of the dockerfile
@@ -128,30 +129,29 @@ class Handler(Protocol):
128
129
  class PipAndRequirementsHandler:
129
130
  @staticmethod
130
131
  async def handle(layer: PipPackages, context_path: Path, dockerfile: str) -> str:
132
+ # Set pip_install_args based on the layer type - either a requirements file or a list of packages
131
133
  if isinstance(layer, Requirements):
132
134
  if not layer.file.exists():
133
135
  raise FileNotFoundError(f"Requirements file {layer.file} does not exist")
134
136
  if not layer.file.is_file():
135
137
  raise ValueError(f"Requirements file {layer.file} is not a file")
136
138
 
137
- async with aiofiles.open(layer.file) as f:
138
- requirements = []
139
- async for line in f:
140
- requirement = line
141
- requirements.append(requirement.strip())
139
+ # Copy the requirements file to the context path
140
+ requirements_path = copy_files_to_context(layer.file, context_path)
141
+ pip_install_args = layer.get_pip_install_args()
142
+ pip_install_args.extend(["--requirement", str(requirements_path)])
142
143
  else:
143
144
  requirements = list(layer.packages) if layer.packages else []
144
- requirements_uv_path = context_path / "requirements_uv.txt"
145
- async with aiofiles.open(requirements_uv_path, "w") as f:
146
- reqs = "\n".join(requirements)
147
- await f.write(reqs)
145
+ reqs = " ".join(requirements)
146
+ pip_install_args = layer.get_pip_install_args()
147
+ pip_install_args.append(reqs)
148
148
 
149
- pip_install_args = layer.get_pip_install_args()
150
- pip_install_args.extend(["--requirement", "requirements_uv.txt"])
151
149
  secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
152
150
  delta = UV_PACKAGE_INSTALL_COMMAND_TEMPLATE.substitute(
153
- PIP_INSTALL_ARGS=" ".join(pip_install_args), SECRET_MOUNT=secret_mounts
151
+ SECRET_MOUNT=secret_mounts,
152
+ PIP_INSTALL_ARGS=" ".join(pip_install_args),
154
153
  )
154
+
155
155
  dockerfile += delta
156
156
 
157
157
  return dockerfile
@@ -162,12 +162,31 @@ class PythonWheelHandler:
162
162
  async def handle(layer: PythonWheels, context_path: Path, dockerfile: str) -> str:
163
163
  shutil.copytree(layer.wheel_dir, context_path / "dist", dirs_exist_ok=True)
164
164
  pip_install_args = layer.get_pip_install_args()
165
- pip_install_args.extend(["/dist/*.whl"])
166
165
  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
166
+
167
+ # First install: Install the wheel without dependencies using --no-deps
168
+ pip_install_args_no_deps = [
169
+ *pip_install_args,
170
+ *[
171
+ "--find-links",
172
+ "/dist",
173
+ "--no-deps",
174
+ "--no-index",
175
+ layer.package_name,
176
+ ],
177
+ ]
178
+
179
+ delta1 = UV_WHEEL_INSTALL_COMMAND_TEMPLATE.substitute(
180
+ PIP_INSTALL_ARGS=" ".join(pip_install_args_no_deps), SECRET_MOUNT=secret_mounts
169
181
  )
170
- dockerfile += delta
182
+ dockerfile += delta1
183
+
184
+ # Second install: Install dependencies from PyPI
185
+ pip_install_args_deps = [*pip_install_args, layer.package_name]
186
+ delta2 = UV_WHEEL_INSTALL_COMMAND_TEMPLATE.substitute(
187
+ PIP_INSTALL_ARGS=" ".join(pip_install_args_deps), SECRET_MOUNT=secret_mounts
188
+ )
189
+ dockerfile += delta2
171
190
 
172
191
  return dockerfile
173
192
 
@@ -26,6 +26,7 @@ from flyte._image import (
26
26
  UVScript,
27
27
  )
28
28
  from flyte._internal.imagebuild.image_builder import ImageBuilder, ImageChecker
29
+ from flyte._internal.imagebuild.utils import copy_files_to_context
29
30
  from flyte._logging import logger
30
31
  from flyte.remote import ActionOutputs, Run
31
32
 
@@ -169,7 +170,7 @@ def _get_layers_proto(image: Image, context_path: Path) -> "image_definition_pb2
169
170
  )
170
171
  layers.append(apt_layer)
171
172
  elif isinstance(layer, PythonWheels):
172
- dst_path = _copy_files_to_context(layer.wheel_dir, context_path)
173
+ dst_path = copy_files_to_context(layer.wheel_dir, context_path)
173
174
  wheel_layer = image_definition_pb2.Layer(
174
175
  python_wheels=image_definition_pb2.PythonWheels(
175
176
  dir=str(dst_path.relative_to(context_path)),
@@ -184,7 +185,7 @@ def _get_layers_proto(image: Image, context_path: Path) -> "image_definition_pb2
184
185
  layers.append(wheel_layer)
185
186
 
186
187
  elif isinstance(layer, Requirements):
187
- dst_path = _copy_files_to_context(layer.file, context_path)
188
+ dst_path = copy_files_to_context(layer.file, context_path)
188
189
  requirements_layer = image_definition_pb2.Layer(
189
190
  requirements=image_definition_pb2.Requirements(
190
191
  file=str(dst_path.relative_to(context_path)),
@@ -240,7 +241,7 @@ def _get_layers_proto(image: Image, context_path: Path) -> "image_definition_pb2
240
241
  elif isinstance(layer, DockerIgnore):
241
242
  shutil.copy(layer.path, context_path)
242
243
  elif isinstance(layer, CopyConfig):
243
- dst_path = _copy_files_to_context(layer.src, context_path)
244
+ dst_path = copy_files_to_context(layer.src, context_path)
244
245
 
245
246
  copy_layer = image_definition_pb2.Layer(
246
247
  copy_config=image_definition_pb2.CopyConfig(
@@ -264,18 +265,5 @@ def _get_layers_proto(image: Image, context_path: Path) -> "image_definition_pb2
264
265
  )
265
266
 
266
267
 
267
- def _copy_files_to_context(src: Path, context_path: Path) -> Path:
268
- if src.is_absolute() or ".." in str(src):
269
- dst_path = context_path / str(src.absolute()).replace("/", "./_flyte_abs_context/", 1)
270
- else:
271
- dst_path = context_path / src
272
- dst_path.parent.mkdir(parents=True, exist_ok=True)
273
- if src.is_dir():
274
- shutil.copytree(src, dst_path, dirs_exist_ok=True)
275
- else:
276
- shutil.copy(src, dst_path)
277
- return dst_path
278
-
279
-
280
268
  def _get_fully_qualified_image_name(outputs: ActionOutputs) -> str:
281
269
  return outputs.pb2.literals[0].value.scalar.primitive.string_value
@@ -0,0 +1,29 @@
1
+ import shutil
2
+ from pathlib import Path
3
+
4
+
5
+ def copy_files_to_context(src: Path, context_path: Path) -> Path:
6
+ """
7
+ This helper function ensures that absolute paths that users specify are converted correctly to a path in the
8
+ context directory. Doing this prevents collisions while ensuring files are available in the context.
9
+
10
+ For example, if a user has
11
+ img.with_requirements(Path("/Users/username/requirements.txt"))
12
+ .with_requirements(Path("requirements.txt"))
13
+ .with_requirements(Path("../requirements.txt"))
14
+
15
+ copying with this function ensures that the Docker context folder has all three files.
16
+
17
+ :param src: The source path to copy
18
+ :param context_path: The context path where the files should be copied to
19
+ """
20
+ if src.is_absolute() or ".." in str(src):
21
+ dst_path = context_path / str(src.absolute()).replace("/", "./_flyte_abs_context/", 1)
22
+ else:
23
+ dst_path = context_path / src
24
+ dst_path.parent.mkdir(parents=True, exist_ok=True)
25
+ if src.is_dir():
26
+ shutil.copytree(src, dst_path, dirs_exist_ok=True)
27
+ else:
28
+ shutil.copy(src, dst_path)
29
+ return dst_path
@@ -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
 
@@ -512,7 +512,7 @@ class _Runner:
512
512
  raise ValueError("Remote task can only be run in remote mode.")
513
513
 
514
514
  if not isinstance(task, TaskTemplate) and not isinstance(task, LazyEntity):
515
- raise TypeError("On Flyte tasks can be run, not generic functions or methods.")
515
+ raise TypeError(f"On Flyte tasks can be run, not generic functions or methods '{type(task)}'.")
516
516
 
517
517
  if self._mode == "remote":
518
518
  return await self._run_remote(task, *args, **kwargs)
@@ -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
@@ -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.0b6'
21
- __version_tuple__ = version_tuple = (2, 0, 0, 'b6')
20
+ __version__ = version = '2.0.0b8'
21
+ __version_tuple__ = version_tuple = (2, 0, 0, 'b8')
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