flyte 2.0.0b22__py3-none-any.whl → 2.0.0b23__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/__init__.py +5 -0
- flyte/_bin/runtime.py +35 -5
- flyte/_cache/cache.py +4 -2
- flyte/_cache/local_cache.py +215 -0
- flyte/_code_bundle/bundle.py +1 -0
- flyte/_debug/constants.py +0 -1
- flyte/_debug/vscode.py +6 -1
- flyte/_deploy.py +193 -52
- flyte/_environment.py +5 -0
- flyte/_excepthook.py +1 -1
- flyte/_image.py +101 -72
- flyte/_initialize.py +23 -0
- flyte/_internal/controllers/_local_controller.py +64 -24
- flyte/_internal/controllers/remote/_action.py +4 -1
- flyte/_internal/controllers/remote/_controller.py +5 -2
- flyte/_internal/controllers/remote/_core.py +6 -3
- flyte/_internal/controllers/remote/_informer.py +1 -1
- flyte/_internal/imagebuild/docker_builder.py +92 -28
- flyte/_internal/imagebuild/image_builder.py +7 -13
- flyte/_internal/imagebuild/remote_builder.py +6 -1
- flyte/_internal/runtime/io.py +13 -1
- flyte/_internal/runtime/rusty.py +17 -2
- flyte/_internal/runtime/task_serde.py +14 -20
- flyte/_internal/runtime/taskrunner.py +1 -1
- flyte/_internal/runtime/trigger_serde.py +153 -0
- flyte/_logging.py +1 -1
- flyte/_protos/common/identifier_pb2.py +19 -1
- flyte/_protos/common/identifier_pb2.pyi +22 -0
- flyte/_protos/workflow/common_pb2.py +14 -3
- flyte/_protos/workflow/common_pb2.pyi +49 -0
- flyte/_protos/workflow/queue_service_pb2.py +41 -35
- flyte/_protos/workflow/queue_service_pb2.pyi +26 -12
- flyte/_protos/workflow/queue_service_pb2_grpc.py +34 -0
- flyte/_protos/workflow/run_definition_pb2.py +38 -38
- flyte/_protos/workflow/run_definition_pb2.pyi +4 -2
- flyte/_protos/workflow/run_service_pb2.py +60 -50
- flyte/_protos/workflow/run_service_pb2.pyi +24 -6
- flyte/_protos/workflow/run_service_pb2_grpc.py +34 -0
- flyte/_protos/workflow/task_definition_pb2.py +15 -11
- flyte/_protos/workflow/task_definition_pb2.pyi +19 -2
- flyte/_protos/workflow/task_service_pb2.py +18 -17
- flyte/_protos/workflow/task_service_pb2.pyi +5 -2
- flyte/_protos/workflow/trigger_definition_pb2.py +66 -0
- flyte/_protos/workflow/trigger_definition_pb2.pyi +117 -0
- flyte/_protos/workflow/trigger_definition_pb2_grpc.py +4 -0
- flyte/_protos/workflow/trigger_service_pb2.py +96 -0
- flyte/_protos/workflow/trigger_service_pb2.pyi +110 -0
- flyte/_protos/workflow/trigger_service_pb2_grpc.py +281 -0
- flyte/_run.py +42 -15
- flyte/_task.py +35 -4
- flyte/_task_environment.py +60 -15
- flyte/_trigger.py +382 -0
- flyte/_version.py +3 -3
- flyte/cli/_abort.py +3 -3
- flyte/cli/_build.py +1 -3
- flyte/cli/_common.py +15 -2
- flyte/cli/_create.py +74 -0
- flyte/cli/_delete.py +23 -1
- flyte/cli/_deploy.py +5 -9
- flyte/cli/_get.py +75 -34
- flyte/cli/_params.py +4 -2
- flyte/cli/_run.py +12 -3
- flyte/cli/_update.py +36 -0
- flyte/cli/_user.py +17 -0
- flyte/cli/main.py +9 -1
- flyte/errors.py +9 -0
- flyte/io/_dir.py +513 -115
- flyte/io/_file.py +495 -135
- flyte/models.py +32 -0
- flyte/remote/__init__.py +6 -1
- flyte/remote/_client/_protocols.py +36 -2
- flyte/remote/_client/controlplane.py +19 -3
- flyte/remote/_run.py +42 -2
- flyte/remote/_task.py +14 -1
- flyte/remote/_trigger.py +308 -0
- flyte/remote/_user.py +33 -0
- flyte/storage/__init__.py +6 -1
- flyte/storage/_storage.py +119 -101
- flyte/types/_pickle.py +16 -3
- {flyte-2.0.0b22.data → flyte-2.0.0b23.data}/scripts/runtime.py +35 -5
- {flyte-2.0.0b22.dist-info → flyte-2.0.0b23.dist-info}/METADATA +3 -1
- {flyte-2.0.0b22.dist-info → flyte-2.0.0b23.dist-info}/RECORD +87 -75
- flyte/_protos/secret/secret_pb2_grpc_grpc.py +0 -198
- {flyte-2.0.0b22.data → flyte-2.0.0b23.data}/scripts/debug.py +0 -0
- {flyte-2.0.0b22.dist-info → flyte-2.0.0b23.dist-info}/WHEEL +0 -0
- {flyte-2.0.0b22.dist-info → flyte-2.0.0b23.dist-info}/entry_points.txt +0 -0
- {flyte-2.0.0b22.dist-info → flyte-2.0.0b23.dist-info}/licenses/LICENSE +0 -0
- {flyte-2.0.0b22.dist-info → flyte-2.0.0b23.dist-info}/top_level.txt +0 -0
flyte/_deploy.py
CHANGED
|
@@ -2,10 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import hashlib
|
|
5
|
-
import sys
|
|
6
|
-
import typing
|
|
7
5
|
from dataclasses import dataclass
|
|
8
|
-
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
|
6
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Protocol, Set, Tuple, Type
|
|
9
7
|
|
|
10
8
|
import cloudpickle
|
|
11
9
|
import rich.repr
|
|
@@ -22,7 +20,7 @@ from ._task import TaskTemplate
|
|
|
22
20
|
from ._task_environment import TaskEnvironment
|
|
23
21
|
|
|
24
22
|
if TYPE_CHECKING:
|
|
25
|
-
from flyte._protos.workflow import task_definition_pb2
|
|
23
|
+
from flyte._protos.workflow import task_definition_pb2, trigger_definition_pb2
|
|
26
24
|
|
|
27
25
|
from ._code_bundle import CopyFiles
|
|
28
26
|
from ._internal.imagebuild.image_builder import ImageCache
|
|
@@ -37,53 +35,110 @@ class DeploymentPlan:
|
|
|
37
35
|
|
|
38
36
|
@rich.repr.auto
|
|
39
37
|
@dataclass
|
|
38
|
+
class DeploymentContext:
|
|
39
|
+
"""
|
|
40
|
+
Context for deployment operations.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
environment: Environment | TaskEnvironment
|
|
44
|
+
serialization_context: SerializationContext
|
|
45
|
+
dryrun: bool = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@rich.repr.auto
|
|
49
|
+
@dataclass
|
|
50
|
+
class DeployedTask:
|
|
51
|
+
deployed_task: task_definition_pb2.TaskSpec
|
|
52
|
+
deployed_triggers: List[trigger_definition_pb2.TaskTrigger]
|
|
53
|
+
|
|
54
|
+
def summary_repr(self) -> str:
|
|
55
|
+
"""
|
|
56
|
+
Returns a summary representation of the deployed task.
|
|
57
|
+
"""
|
|
58
|
+
return (
|
|
59
|
+
f"DeployedTask(name={self.deployed_task.task_template.id.name}, "
|
|
60
|
+
f"version={self.deployed_task.task_template.id.version})"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def table_repr(self) -> List[Tuple[str, ...]]:
|
|
64
|
+
"""
|
|
65
|
+
Returns a table representation of the deployed task.
|
|
66
|
+
"""
|
|
67
|
+
return [
|
|
68
|
+
("name", self.deployed_task.task_template.id.name),
|
|
69
|
+
("version", self.deployed_task.task_template.id.version),
|
|
70
|
+
("triggers", ",".join([t.name for t in self.deployed_triggers])),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@rich.repr.auto
|
|
75
|
+
@dataclass
|
|
76
|
+
class DeployedEnv:
|
|
77
|
+
env: Environment
|
|
78
|
+
deployed_entities: List[DeployedTask]
|
|
79
|
+
|
|
80
|
+
def summary_repr(self) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Returns a summary representation of the deployment.
|
|
83
|
+
"""
|
|
84
|
+
entities = ", ".join(f"{e.summary_repr()}" for e in self.deployed_entities or [])
|
|
85
|
+
return f"Deployment(env=[{self.env.name}], entities=[{entities}])"
|
|
86
|
+
|
|
87
|
+
def table_repr(self) -> List[List[Tuple[str, ...]]]:
|
|
88
|
+
"""
|
|
89
|
+
Returns a detailed representation of the deployed tasks.
|
|
90
|
+
"""
|
|
91
|
+
tuples = []
|
|
92
|
+
if self.deployed_entities:
|
|
93
|
+
for e in self.deployed_entities:
|
|
94
|
+
tuples.append(e.table_repr())
|
|
95
|
+
return tuples
|
|
96
|
+
|
|
97
|
+
def env_repr(self) -> List[Tuple[str, ...]]:
|
|
98
|
+
"""
|
|
99
|
+
Returns a detailed representation of the deployed environments.
|
|
100
|
+
"""
|
|
101
|
+
env = self.env
|
|
102
|
+
return [
|
|
103
|
+
("environment", env.name),
|
|
104
|
+
("image", env.image.uri if isinstance(env.image, Image) else env.image or ""),
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@rich.repr.auto
|
|
109
|
+
@dataclass(frozen=True)
|
|
40
110
|
class Deployment:
|
|
41
|
-
envs: Dict[str,
|
|
42
|
-
deployed_tasks: List[task_definition_pb2.TaskSpec] | None = None
|
|
111
|
+
envs: Dict[str, DeployedEnv]
|
|
43
112
|
|
|
44
113
|
def summary_repr(self) -> str:
|
|
45
114
|
"""
|
|
46
115
|
Returns a summary representation of the deployment.
|
|
47
116
|
"""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
f"{task.task_template.id.name} (v{task.task_template.id.version})" for task in self.deployed_tasks or []
|
|
51
|
-
)
|
|
52
|
-
return f"Deployment(envs=[{env_names}], tasks=[{task_names_versions}])"
|
|
117
|
+
envs = ", ".join(f"{e.summary_repr()}" for e in self.envs.values() or [])
|
|
118
|
+
return f"Deployment(envs=[{envs}])"
|
|
53
119
|
|
|
54
|
-
def
|
|
120
|
+
def table_repr(self) -> List[List[Tuple[str, ...]]]:
|
|
55
121
|
"""
|
|
56
122
|
Returns a detailed representation of the deployed tasks.
|
|
57
123
|
"""
|
|
58
124
|
tuples = []
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
tuples.append(
|
|
62
|
-
[
|
|
63
|
-
("name", task.task_template.id.name),
|
|
64
|
-
("version", task.task_template.id.version),
|
|
65
|
-
]
|
|
66
|
-
)
|
|
125
|
+
for d in self.envs.values():
|
|
126
|
+
tuples.extend(d.table_repr())
|
|
67
127
|
return tuples
|
|
68
128
|
|
|
69
|
-
def env_repr(self) -> List[List[Tuple[str,
|
|
129
|
+
def env_repr(self) -> List[List[Tuple[str, ...]]]:
|
|
70
130
|
"""
|
|
71
131
|
Returns a detailed representation of the deployed environments.
|
|
72
132
|
"""
|
|
73
133
|
tuples = []
|
|
74
|
-
for
|
|
75
|
-
tuples.append(
|
|
76
|
-
[
|
|
77
|
-
("environment", env_name),
|
|
78
|
-
("image", env.image.uri if isinstance(env.image, Image) else env.image or ""),
|
|
79
|
-
]
|
|
80
|
-
)
|
|
134
|
+
for d in self.envs.values():
|
|
135
|
+
tuples.append(d.env_repr())
|
|
81
136
|
return tuples
|
|
82
137
|
|
|
83
138
|
|
|
84
139
|
async def _deploy_task(
|
|
85
140
|
task: TaskTemplate, serialization_context: SerializationContext, dryrun: bool = False
|
|
86
|
-
) ->
|
|
141
|
+
) -> DeployedTask:
|
|
87
142
|
"""
|
|
88
143
|
Deploy the given task.
|
|
89
144
|
"""
|
|
@@ -92,13 +147,14 @@ async def _deploy_task(
|
|
|
92
147
|
|
|
93
148
|
from ._internal.runtime.convert import convert_upload_default_inputs
|
|
94
149
|
from ._internal.runtime.task_serde import translate_task_to_wire
|
|
150
|
+
from ._internal.runtime.trigger_serde import to_task_trigger
|
|
95
151
|
from ._protos.workflow import task_definition_pb2, task_service_pb2
|
|
96
152
|
|
|
97
153
|
image_uri = task.image.uri if isinstance(task.image, Image) else task.image
|
|
98
154
|
|
|
99
155
|
try:
|
|
100
156
|
if dryrun:
|
|
101
|
-
return translate_task_to_wire(task, serialization_context)
|
|
157
|
+
return DeployedTask(translate_task_to_wire(task, serialization_context), [])
|
|
102
158
|
|
|
103
159
|
default_inputs = await convert_upload_default_inputs(task.interface)
|
|
104
160
|
spec = translate_task_to_wire(task, serialization_context, default_inputs=default_inputs)
|
|
@@ -115,15 +171,31 @@ async def _deploy_task(
|
|
|
115
171
|
name=spec.task_template.id.name,
|
|
116
172
|
)
|
|
117
173
|
|
|
174
|
+
deployable_triggers_coros = []
|
|
175
|
+
for t in task.triggers:
|
|
176
|
+
inputs = spec.task_template.interface.inputs
|
|
177
|
+
default_inputs = spec.default_inputs
|
|
178
|
+
deployable_triggers_coros.append(
|
|
179
|
+
to_task_trigger(t=t, task_name=task.name, task_inputs=inputs, task_default_inputs=list(default_inputs))
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
deployable_triggers = await asyncio.gather(*deployable_triggers_coros)
|
|
118
183
|
try:
|
|
119
|
-
await get_client().task_service.DeployTask(
|
|
184
|
+
await get_client().task_service.DeployTask(
|
|
185
|
+
task_service_pb2.DeployTaskRequest(
|
|
186
|
+
task_id=task_id,
|
|
187
|
+
spec=spec,
|
|
188
|
+
triggers=deployable_triggers,
|
|
189
|
+
)
|
|
190
|
+
)
|
|
120
191
|
logger.info(f"Deployed task {task.name} with version {task_id.version}")
|
|
121
192
|
except grpc.aio.AioRpcError as e:
|
|
122
193
|
if e.code() == grpc.StatusCode.ALREADY_EXISTS:
|
|
123
194
|
logger.info(f"Task {task.name} with image {image_uri} already exists, skipping deployment.")
|
|
124
|
-
return spec
|
|
195
|
+
return DeployedTask(spec, deployable_triggers)
|
|
125
196
|
raise
|
|
126
|
-
|
|
197
|
+
|
|
198
|
+
return DeployedTask(spec, deployable_triggers)
|
|
127
199
|
except Exception as e:
|
|
128
200
|
logger.error(f"Failed to deploy task {task.name} with image {image_uri}: {e}")
|
|
129
201
|
raise flyte.errors.DeploymentError(
|
|
@@ -162,16 +234,80 @@ async def _build_images(deployment: DeploymentPlan) -> ImageCache:
|
|
|
162
234
|
for env_name, image_uri in final_images:
|
|
163
235
|
logger.warning(f"Built Image for environment {env_name}, image: {image_uri}")
|
|
164
236
|
env = deployment.envs[env_name]
|
|
165
|
-
|
|
166
|
-
py_version = "{}.{}".format(*env.image.python_version)
|
|
167
|
-
image_identifier_map[env.image.identifier] = {py_version: image_uri}
|
|
168
|
-
elif env.image == "auto":
|
|
169
|
-
py_version = "{}.{}".format(sys.version_info.major, sys.version_info.minor)
|
|
170
|
-
image_identifier_map["auto"] = {py_version: image_uri}
|
|
237
|
+
image_identifier_map[env_name] = image_uri
|
|
171
238
|
|
|
172
239
|
return ImageCache(image_lookup=image_identifier_map)
|
|
173
240
|
|
|
174
241
|
|
|
242
|
+
class Deployer(Protocol):
|
|
243
|
+
"""
|
|
244
|
+
Protocol for deployment callables.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
async def __call__(self, context: DeploymentContext) -> DeployedEnv:
|
|
248
|
+
"""
|
|
249
|
+
Deploy the environment described in the context.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
context: Deployment context containing environment, serialization context, and dryrun flag
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Deployment result
|
|
256
|
+
"""
|
|
257
|
+
...
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
async def _deploy_task_env(context: DeploymentContext) -> DeployedEnv:
|
|
261
|
+
"""
|
|
262
|
+
Deploy the given task environment.
|
|
263
|
+
"""
|
|
264
|
+
ensure_client()
|
|
265
|
+
env = context.environment
|
|
266
|
+
if not isinstance(env, TaskEnvironment):
|
|
267
|
+
raise ValueError(f"Expected TaskEnvironment, got {type(env)}")
|
|
268
|
+
|
|
269
|
+
task_coros = []
|
|
270
|
+
for task in env.tasks.values():
|
|
271
|
+
task_coros.append(_deploy_task(task, context.serialization_context, dryrun=context.dryrun))
|
|
272
|
+
deployed_task_vals = await asyncio.gather(*task_coros)
|
|
273
|
+
deployed_tasks = []
|
|
274
|
+
for t in deployed_task_vals:
|
|
275
|
+
deployed_tasks.append(t)
|
|
276
|
+
return DeployedEnv(env=env, deployed_entities=deployed_tasks)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
_ENVTYPE_REGISTRY: Dict[Type[Environment | TaskEnvironment], Deployer] = {
|
|
280
|
+
TaskEnvironment: _deploy_task_env,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def register_deployer(env_type: Type[Environment | TaskEnvironment], deployer: Deployer) -> None:
|
|
285
|
+
"""
|
|
286
|
+
Register a deployer for a specific environment type.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
env_type: Type of environment this deployer handles
|
|
290
|
+
deployer: Deployment callable that conforms to the Deployer protocol
|
|
291
|
+
"""
|
|
292
|
+
_ENVTYPE_REGISTRY[env_type] = deployer
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def get_deployer(env_type: Type[Environment | TaskEnvironment]) -> Deployer:
|
|
296
|
+
"""
|
|
297
|
+
Get the registered deployer for an environment type.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
env_type: Type of environment to get deployer for
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Deployer for the environment type, defaults to task environment deployer
|
|
304
|
+
"""
|
|
305
|
+
v = _ENVTYPE_REGISTRY.get(env_type)
|
|
306
|
+
if v is None:
|
|
307
|
+
raise ValueError(f"No deployer registered for environment type {env_type}")
|
|
308
|
+
return v
|
|
309
|
+
|
|
310
|
+
|
|
175
311
|
@requires_initialization
|
|
176
312
|
async def apply(deployment_plan: DeploymentPlan, copy_style: CopyFiles, dryrun: bool = False) -> Deployment:
|
|
177
313
|
from ._code_bundle import build_code_bundle
|
|
@@ -203,15 +339,18 @@ async def apply(deployment_plan: DeploymentPlan, copy_style: CopyFiles, dryrun:
|
|
|
203
339
|
root_dir=cfg.root_dir,
|
|
204
340
|
)
|
|
205
341
|
|
|
206
|
-
|
|
207
|
-
|
|
342
|
+
deployment_coros = []
|
|
208
343
|
for env_name, env in deployment_plan.envs.items():
|
|
209
344
|
logger.info(f"Deploying environment {env_name}")
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
345
|
+
deployer = get_deployer(type(env))
|
|
346
|
+
context = DeploymentContext(environment=env, serialization_context=sc, dryrun=dryrun)
|
|
347
|
+
deployment_coros.append(deployer(context))
|
|
348
|
+
deployed_envs = await asyncio.gather(*deployment_coros)
|
|
349
|
+
envs = {}
|
|
350
|
+
for d in deployed_envs:
|
|
351
|
+
envs[d.env.name] = d
|
|
352
|
+
|
|
353
|
+
return Deployment(envs)
|
|
215
354
|
|
|
216
355
|
|
|
217
356
|
def _recursive_discover(planned_envs: Dict[str, Environment], env: Environment) -> Dict[str, Environment]:
|
|
@@ -219,14 +358,16 @@ def _recursive_discover(planned_envs: Dict[str, Environment], env: Environment)
|
|
|
219
358
|
Recursively deploy the environment and its dependencies, if not already deployed (present in env_tasks) and
|
|
220
359
|
return the updated env_tasks.
|
|
221
360
|
"""
|
|
222
|
-
# Skip if the environment is already planned
|
|
223
361
|
if env.name in planned_envs:
|
|
224
|
-
|
|
362
|
+
if planned_envs[env.name] is not env:
|
|
363
|
+
# Raise error if different TaskEnvironment objects have the same name
|
|
364
|
+
raise ValueError(f"Duplicate environment name '{env.name}' found")
|
|
365
|
+
# Add the environment to the existing envs
|
|
366
|
+
planned_envs[env.name] = env
|
|
367
|
+
|
|
225
368
|
# Recursively discover dependent environments
|
|
226
369
|
for dependent_env in env.depends_on:
|
|
227
370
|
_recursive_discover(planned_envs, dependent_env)
|
|
228
|
-
# Add the environment to the existing envs
|
|
229
|
-
planned_envs[env.name] = env
|
|
230
371
|
return planned_envs
|
|
231
372
|
|
|
232
373
|
|
|
@@ -234,10 +375,10 @@ def plan_deploy(*envs: Environment, version: Optional[str] = None) -> List[Deplo
|
|
|
234
375
|
if envs is None:
|
|
235
376
|
return [DeploymentPlan({})]
|
|
236
377
|
deployment_plans = []
|
|
237
|
-
visited_envs:
|
|
378
|
+
visited_envs: Set[str] = set()
|
|
238
379
|
for env in envs:
|
|
239
380
|
if env.name in visited_envs:
|
|
240
|
-
|
|
381
|
+
raise ValueError(f"Duplicate environment name '{env.name}' found")
|
|
241
382
|
planned_envs = _recursive_discover({}, env)
|
|
242
383
|
deployment_plans.append(DeploymentPlan(planned_envs, version=version))
|
|
243
384
|
visited_envs.update(planned_envs.keys())
|
flyte/_environment.py
CHANGED
|
@@ -36,6 +36,9 @@ class Environment:
|
|
|
36
36
|
:param resources: Resources to allocate for the environment.
|
|
37
37
|
:param env_vars: Environment variables to set for the environment.
|
|
38
38
|
:param secrets: Secrets to inject into the environment.
|
|
39
|
+
:param pod_template: Pod template to use for the environment.
|
|
40
|
+
:param description: Description of the environment.
|
|
41
|
+
:param interruptible: Whether the environment is interruptible and can be scheduled on spot/preemptible instances
|
|
39
42
|
:param depends_on: Environment dependencies to hint, so when you deploy the environment, the dependencies are
|
|
40
43
|
also deployed. This is useful when you have a set of environments that depend on each other.
|
|
41
44
|
"""
|
|
@@ -47,6 +50,7 @@ class Environment:
|
|
|
47
50
|
secrets: Optional[SecretRequest] = None
|
|
48
51
|
env_vars: Optional[Dict[str, str]] = None
|
|
49
52
|
resources: Optional[Resources] = None
|
|
53
|
+
interruptible: bool = False
|
|
50
54
|
image: Union[str, Image, Literal["auto"]] = "auto"
|
|
51
55
|
|
|
52
56
|
def __post_init__(self):
|
|
@@ -87,6 +91,7 @@ class Environment:
|
|
|
87
91
|
env_vars: Optional[Dict[str, str]] = None,
|
|
88
92
|
secrets: Optional[SecretRequest] = None,
|
|
89
93
|
depends_on: Optional[List[Environment]] = None,
|
|
94
|
+
description: Optional[str] = None,
|
|
90
95
|
**kwargs: Any,
|
|
91
96
|
) -> Environment:
|
|
92
97
|
raise NotImplementedError
|
flyte/_excepthook.py
CHANGED
|
@@ -33,5 +33,5 @@ def custom_excepthook(exc_type, exc_value, exc_tb):
|
|
|
33
33
|
filtered_tb = [frame for frame in tb_list if should_include_frame(frame)]
|
|
34
34
|
# Print the filtered version (custom format)
|
|
35
35
|
print("Filtered traceback (most recent call last):")
|
|
36
|
-
|
|
36
|
+
traceback.print_tb(filtered_tb)
|
|
37
37
|
print(f"{exc_type.__name__}: {exc_value}\n")
|
flyte/_image.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import base64
|
|
4
3
|
import hashlib
|
|
5
4
|
import sys
|
|
6
5
|
import typing
|
|
7
6
|
from abc import abstractmethod
|
|
8
|
-
from dataclasses import
|
|
7
|
+
from dataclasses import dataclass, field
|
|
9
8
|
from functools import cached_property
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
from typing import TYPE_CHECKING, ClassVar, Dict, List, Literal, Optional, Tuple, TypeVar, Union
|
|
@@ -13,6 +12,8 @@ from typing import TYPE_CHECKING, ClassVar, Dict, List, Literal, Optional, Tuple
|
|
|
13
12
|
import rich.repr
|
|
14
13
|
from packaging.version import Version
|
|
15
14
|
|
|
15
|
+
from flyte._utils import update_hasher_for_source
|
|
16
|
+
|
|
16
17
|
if TYPE_CHECKING:
|
|
17
18
|
from flyte import Secret, SecretRequest
|
|
18
19
|
|
|
@@ -56,7 +57,6 @@ class Layer:
|
|
|
56
57
|
|
|
57
58
|
:param hasher: The hash object to update with the layer's data.
|
|
58
59
|
"""
|
|
59
|
-
print("hash hash")
|
|
60
60
|
|
|
61
61
|
def validate(self):
|
|
62
62
|
"""
|
|
@@ -64,27 +64,6 @@ class Layer:
|
|
|
64
64
|
:return:
|
|
65
65
|
"""
|
|
66
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
|
-
|
|
88
67
|
|
|
89
68
|
@rich.repr.auto
|
|
90
69
|
@dataclass(kw_only=True, frozen=True, repr=True)
|
|
@@ -152,7 +131,7 @@ class PipPackages(PipOption, Layer):
|
|
|
152
131
|
@rich.repr.auto
|
|
153
132
|
@dataclass(kw_only=True, frozen=True, repr=True)
|
|
154
133
|
class PythonWheels(PipOption, Layer):
|
|
155
|
-
wheel_dir: Path
|
|
134
|
+
wheel_dir: Path
|
|
156
135
|
wheel_dir_name: str = field(init=False)
|
|
157
136
|
package_name: str
|
|
158
137
|
|
|
@@ -202,14 +181,56 @@ class UVProject(PipOption, Layer):
|
|
|
202
181
|
from ._utils import filehash_update
|
|
203
182
|
|
|
204
183
|
super().update_hash(hasher)
|
|
205
|
-
|
|
206
|
-
|
|
184
|
+
if self.extra_args and "--no-install-project" in self.extra_args:
|
|
185
|
+
filehash_update(self.uvlock, hasher)
|
|
186
|
+
filehash_update(self.pyproject, hasher)
|
|
187
|
+
else:
|
|
188
|
+
update_hasher_for_source(self.pyproject.parent, hasher)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@rich.repr.auto
|
|
192
|
+
@dataclass(frozen=True, repr=True)
|
|
193
|
+
class PoetryProject(Layer):
|
|
194
|
+
"""
|
|
195
|
+
Poetry does not use pip options, so the PoetryProject class do not inherits PipOption class
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
pyproject: Path
|
|
199
|
+
poetry_lock: Path
|
|
200
|
+
extra_args: Optional[str] = None
|
|
201
|
+
secret_mounts: Optional[Tuple[str | Secret, ...]] = None
|
|
202
|
+
|
|
203
|
+
def validate(self):
|
|
204
|
+
if not self.pyproject.exists():
|
|
205
|
+
raise FileNotFoundError(f"pyproject.toml file {self.pyproject} does not exist")
|
|
206
|
+
if not self.pyproject.is_file():
|
|
207
|
+
raise ValueError(f"Pyproject file {self.pyproject} is not a file")
|
|
208
|
+
if not self.poetry_lock.exists():
|
|
209
|
+
raise ValueError(f"poetry.lock file {self.poetry_lock} does not exist")
|
|
210
|
+
super().validate()
|
|
211
|
+
|
|
212
|
+
def update_hash(self, hasher: hashlib._Hash):
|
|
213
|
+
from ._utils import filehash_update
|
|
214
|
+
|
|
215
|
+
hash_input = ""
|
|
216
|
+
if self.extra_args:
|
|
217
|
+
hash_input += self.extra_args
|
|
218
|
+
if self.secret_mounts:
|
|
219
|
+
for secret_mount in self.secret_mounts:
|
|
220
|
+
hash_input += str(secret_mount)
|
|
221
|
+
hasher.update(hash_input.encode("utf-8"))
|
|
222
|
+
|
|
223
|
+
if self.extra_args and "--no-root" in self.extra_args:
|
|
224
|
+
filehash_update(self.poetry_lock, hasher)
|
|
225
|
+
filehash_update(self.pyproject, hasher)
|
|
226
|
+
else:
|
|
227
|
+
update_hasher_for_source(self.pyproject.parent, hasher)
|
|
207
228
|
|
|
208
229
|
|
|
209
230
|
@rich.repr.auto
|
|
210
231
|
@dataclass(frozen=True, repr=True)
|
|
211
232
|
class UVScript(PipOption, Layer):
|
|
212
|
-
script: Path
|
|
233
|
+
script: Path
|
|
213
234
|
script_name: str = field(init=False)
|
|
214
235
|
|
|
215
236
|
def __post_init__(self):
|
|
@@ -285,15 +306,14 @@ class DockerIgnore(Layer):
|
|
|
285
306
|
@rich.repr.auto
|
|
286
307
|
@dataclass(frozen=True, repr=True)
|
|
287
308
|
class CopyConfig(Layer):
|
|
288
|
-
path_type: CopyConfigType
|
|
289
|
-
src: Path
|
|
309
|
+
path_type: CopyConfigType
|
|
310
|
+
src: Path
|
|
290
311
|
dst: str
|
|
291
|
-
src_name: str
|
|
312
|
+
src_name: str
|
|
292
313
|
|
|
293
314
|
def __post_init__(self):
|
|
294
315
|
if self.path_type not in (0, 1):
|
|
295
316
|
raise ValueError(f"Invalid path_type {self.path_type}, must be 0 (file) or 1 (directory)")
|
|
296
|
-
object.__setattr__(self, "src_name", self.src.name)
|
|
297
317
|
|
|
298
318
|
def validate(self):
|
|
299
319
|
if not self.src.exists():
|
|
@@ -382,9 +402,6 @@ class Image:
|
|
|
382
402
|
platform: Tuple[Architecture, ...] = field(default=("linux/amd64",))
|
|
383
403
|
python_version: Tuple[int, int] = field(default_factory=_detect_python_version)
|
|
384
404
|
|
|
385
|
-
# For .auto() images. Don't compute an actual identifier.
|
|
386
|
-
_identifier_override: Optional[str] = field(default=None, init=False)
|
|
387
|
-
|
|
388
405
|
# Layers to be added to the image. In init, because frozen, but users shouldn't access, so underscore.
|
|
389
406
|
_layers: Tuple[Layer, ...] = field(default_factory=tuple)
|
|
390
407
|
|
|
@@ -417,34 +434,6 @@ class Image:
|
|
|
417
434
|
cls.__init__(obj, **kwargs) # run dataclass generated __init__
|
|
418
435
|
return obj
|
|
419
436
|
|
|
420
|
-
@cached_property
|
|
421
|
-
def identifier(self) -> str:
|
|
422
|
-
"""
|
|
423
|
-
This identifier is a hash of the layers and properties of the image. It is used to look up previously built
|
|
424
|
-
images. Why is this useful? For example, if a user has Image.from_uv_base().with_source_file("a/local/file"),
|
|
425
|
-
it's not necessarily the case that that file exists within the image (further commands may have removed/changed
|
|
426
|
-
it), and certainly not the case that the path to the file, inside the image (which is used as part of the layer
|
|
427
|
-
hash computation), is the same. That is, inside the image when a task runs, as we come across the same Image
|
|
428
|
-
declaration, we need a way of identifying the image and its uri, without hashing all the layers again. This
|
|
429
|
-
is what this identifier is for. See the ImageCache object for additional information.
|
|
430
|
-
|
|
431
|
-
:return: A unique identifier of the Image
|
|
432
|
-
"""
|
|
433
|
-
if self._identifier_override:
|
|
434
|
-
return self._identifier_override
|
|
435
|
-
|
|
436
|
-
# Only get the non-None values in the Image to ensure the hash is consistent
|
|
437
|
-
# across different SDK versions.
|
|
438
|
-
# Layers can specify a _compute_identifier optionally, but the default will just stringify
|
|
439
|
-
image_dict = asdict(
|
|
440
|
-
self,
|
|
441
|
-
dict_factory=lambda x: {k: v for (k, v) in x if v is not None and k not in ("_layers", "python_version")},
|
|
442
|
-
)
|
|
443
|
-
layers_str_repr = "".join([layer.identifier() for layer in self._layers])
|
|
444
|
-
image_dict["layers"] = layers_str_repr
|
|
445
|
-
spec_bytes = image_dict.__str__().encode("utf-8")
|
|
446
|
-
return base64.urlsafe_b64encode(hashlib.md5(spec_bytes).digest()).decode("ascii").rstrip("=")
|
|
447
|
-
|
|
448
437
|
def validate(self):
|
|
449
438
|
for layer in self._layers:
|
|
450
439
|
layer.validate()
|
|
@@ -507,9 +496,6 @@ class Image:
|
|
|
507
496
|
image = image.with_pip_packages(f"flyte=={flyte_version}")
|
|
508
497
|
if not dev_mode:
|
|
509
498
|
object.__setattr__(image, "_tag", preset_tag)
|
|
510
|
-
# Set this to auto for all auto images because the meaning of "auto" can change (based on logic inside
|
|
511
|
-
# _get_default_image_for, acts differently in a running task container) so let's make sure it stays auto.
|
|
512
|
-
object.__setattr__(image, "_identifier_override", "auto")
|
|
513
499
|
|
|
514
500
|
return image
|
|
515
501
|
|
|
@@ -550,9 +536,6 @@ class Image:
|
|
|
550
536
|
if registry or name:
|
|
551
537
|
return base_image.clone(registry=registry, name=name)
|
|
552
538
|
|
|
553
|
-
# # Set this to auto for all auto images because the meaning of "auto" can change (based on logic inside
|
|
554
|
-
# # _get_default_image_for, acts differently in a running task container) so let's make sure it stays auto.
|
|
555
|
-
# object.__setattr__(base_image, "_identifier_override", "auto")
|
|
556
539
|
return base_image
|
|
557
540
|
|
|
558
541
|
@classmethod
|
|
@@ -849,16 +832,23 @@ class Image:
|
|
|
849
832
|
new_image = self.clone(addl_layer=Env.from_dict(env_vars))
|
|
850
833
|
return new_image
|
|
851
834
|
|
|
852
|
-
def with_source_folder(self, src: Path, dst: str = ".") -> Image:
|
|
835
|
+
def with_source_folder(self, src: Path, dst: str = ".", copy_contents_only: bool = False) -> Image:
|
|
853
836
|
"""
|
|
854
837
|
Use this method to create a new image with the specified local directory layered on top of the current image.
|
|
855
838
|
If dest is not specified, it will be copied to the working directory of the image
|
|
856
839
|
|
|
857
840
|
:param src: root folder of the source code from the build context to be copied
|
|
858
841
|
:param dst: destination folder in the image
|
|
842
|
+
:param copy_contents_only: If True, will copy the contents of the source folder to the destination folder,
|
|
843
|
+
instead of the folder itself. Default is False.
|
|
859
844
|
:return: Image
|
|
860
845
|
"""
|
|
861
|
-
|
|
846
|
+
src_name = src.name
|
|
847
|
+
if copy_contents_only:
|
|
848
|
+
src_name = "."
|
|
849
|
+
else:
|
|
850
|
+
dst = str("./" + src_name)
|
|
851
|
+
new_image = self.clone(addl_layer=CopyConfig(path_type=1, src=src, dst=dst, src_name=src_name))
|
|
862
852
|
return new_image
|
|
863
853
|
|
|
864
854
|
def with_source_file(self, src: Path, dst: str = ".") -> Image:
|
|
@@ -870,7 +860,7 @@ class Image:
|
|
|
870
860
|
:param dst: destination folder in the image
|
|
871
861
|
:return: Image
|
|
872
862
|
"""
|
|
873
|
-
new_image = self.clone(addl_layer=CopyConfig(path_type=0, src=src, dst=dst))
|
|
863
|
+
new_image = self.clone(addl_layer=CopyConfig(path_type=0, src=src, dst=dst, src_name=src.name))
|
|
874
864
|
return new_image
|
|
875
865
|
|
|
876
866
|
def with_dockerignore(self, path: Path) -> Image:
|
|
@@ -923,6 +913,45 @@ class Image:
|
|
|
923
913
|
)
|
|
924
914
|
return new_image
|
|
925
915
|
|
|
916
|
+
def with_poetry_project(
|
|
917
|
+
self,
|
|
918
|
+
pyproject_file: str | Path,
|
|
919
|
+
poetry_lock: Path | None = None,
|
|
920
|
+
extra_args: Optional[str] = None,
|
|
921
|
+
secret_mounts: Optional[SecretRequest] = None,
|
|
922
|
+
):
|
|
923
|
+
"""
|
|
924
|
+
Use this method to create a new image with the specified pyproject.toml layered on top of the current image.
|
|
925
|
+
Must have a corresponding pyproject.toml file in the same directory.
|
|
926
|
+
Cannot be used in conjunction with conda.
|
|
927
|
+
|
|
928
|
+
By default, this method copies the entire project into the image,
|
|
929
|
+
including files such as pyproject.toml, poetry.lock, and the src/ directory.
|
|
930
|
+
|
|
931
|
+
If you prefer not to install the current project, you can pass through `extra_args`
|
|
932
|
+
`--no-root`. In this case, the image builder will only copy pyproject.toml and poetry.lock
|
|
933
|
+
into the image.
|
|
934
|
+
|
|
935
|
+
:param pyproject_file: Path to the pyproject.toml file. A poetry.lock file must exist in the same directory
|
|
936
|
+
unless `poetry_lock` is explicitly provided.
|
|
937
|
+
:param poetry_lock: Path to the poetry.lock file. If not specified, the default is the file named
|
|
938
|
+
'poetry.lock' in the same directory as `pyproject_file` (pyproject.parent / "poetry.lock").
|
|
939
|
+
:param extra_args: Extra arguments to pass through to the package installer/resolver, default is None.
|
|
940
|
+
:param secret_mounts: Secrets to make available during dependency resolution/build (e.g., private indexes).
|
|
941
|
+
:return: Image
|
|
942
|
+
"""
|
|
943
|
+
if isinstance(pyproject_file, str):
|
|
944
|
+
pyproject_file = Path(pyproject_file)
|
|
945
|
+
new_image = self.clone(
|
|
946
|
+
addl_layer=PoetryProject(
|
|
947
|
+
pyproject=pyproject_file,
|
|
948
|
+
poetry_lock=poetry_lock or (pyproject_file.parent / "poetry.lock"),
|
|
949
|
+
extra_args=extra_args,
|
|
950
|
+
secret_mounts=_ensure_tuple(secret_mounts) if secret_mounts else None,
|
|
951
|
+
)
|
|
952
|
+
)
|
|
953
|
+
return new_image
|
|
954
|
+
|
|
926
955
|
def with_apt_packages(self, *packages: str, secret_mounts: Optional[SecretRequest] = None) -> Image:
|
|
927
956
|
"""
|
|
928
957
|
Use this method to create a new image with the specified apt packages layered on top of the current image
|