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 +4 -17
- flyte/_cache/cache.py +1 -1
- flyte/_deploy.py +2 -1
- flyte/_image.py +56 -15
- flyte/_internal/controllers/remote/_controller.py +15 -0
- flyte/_internal/controllers/remote/_core.py +3 -3
- flyte/_internal/runtime/rusty.py +41 -18
- flyte/_run.py +1 -1
- flyte/_task_environment.py +2 -2
- flyte/_version.py +16 -3
- flyte/cli/_build.py +1 -1
- flyte/cli/_common.py +34 -18
- flyte/cli/_create.py +24 -2
- flyte/cli/_deploy.py +6 -10
- flyte/cli/_gen.py +4 -0
- flyte/cli/_get.py +17 -10
- flyte/cli/_option.py +33 -0
- flyte/cli/_run.py +1 -1
- flyte/cli/main.py +10 -6
- flyte/errors.py +22 -0
- flyte/remote/_action.py +5 -4
- flyte/remote/_common.py +30 -0
- flyte/remote/_project.py +9 -7
- flyte/remote/_run.py +10 -3
- flyte/remote/_secret.py +2 -1
- flyte/remote/_task.py +18 -6
- flyte/syncify/_api.py +4 -0
- {flyte-2.0.0b5.data → flyte-2.0.0b7.data}/scripts/runtime.py +4 -17
- {flyte-2.0.0b5.dist-info → flyte-2.0.0b7.dist-info}/METADATA +1 -1
- {flyte-2.0.0b5.dist-info → flyte-2.0.0b7.dist-info}/RECORD +34 -32
- {flyte-2.0.0b5.dist-info → flyte-2.0.0b7.dist-info}/WHEEL +0 -0
- {flyte-2.0.0b5.dist-info → flyte-2.0.0b7.dist-info}/entry_points.txt +0 -0
- {flyte-2.0.0b5.dist-info → flyte-2.0.0b7.dist-info}/licenses/LICENSE +0 -0
- {flyte-2.0.0b5.dist-info → flyte-2.0.0b7.dist-info}/top_level.txt +0 -0
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
|
-
|
|
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
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,
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
flyte/_internal/runtime/rusty.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
|
flyte/_task_environment.py
CHANGED
|
@@ -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:
|
|
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:
|
|
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__ = [
|
|
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.
|
|
21
|
-
__version_tuple__ = version_tuple = (2, 0, 0, '
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
20
|
-
|
|
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.
|
|
113
|
-
console.print(common.
|
|
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.
|
|
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.
|
|
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)
|