flyte 2.0.0b32__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 +108 -0
- flyte/_bin/__init__.py +0 -0
- flyte/_bin/debug.py +38 -0
- flyte/_bin/runtime.py +195 -0
- flyte/_bin/serve.py +178 -0
- flyte/_build.py +26 -0
- flyte/_cache/__init__.py +12 -0
- flyte/_cache/cache.py +147 -0
- flyte/_cache/defaults.py +9 -0
- flyte/_cache/local_cache.py +216 -0
- flyte/_cache/policy_function_body.py +42 -0
- flyte/_code_bundle/__init__.py +8 -0
- flyte/_code_bundle/_ignore.py +121 -0
- flyte/_code_bundle/_packaging.py +218 -0
- flyte/_code_bundle/_utils.py +347 -0
- flyte/_code_bundle/bundle.py +266 -0
- flyte/_constants.py +1 -0
- flyte/_context.py +155 -0
- flyte/_custom_context.py +73 -0
- flyte/_debug/__init__.py +0 -0
- flyte/_debug/constants.py +38 -0
- flyte/_debug/utils.py +17 -0
- flyte/_debug/vscode.py +307 -0
- flyte/_deploy.py +408 -0
- flyte/_deployer.py +109 -0
- flyte/_doc.py +29 -0
- flyte/_docstring.py +32 -0
- flyte/_environment.py +122 -0
- flyte/_excepthook.py +37 -0
- flyte/_group.py +32 -0
- flyte/_hash.py +8 -0
- flyte/_image.py +1055 -0
- flyte/_initialize.py +628 -0
- flyte/_interface.py +119 -0
- flyte/_internal/__init__.py +3 -0
- flyte/_internal/controllers/__init__.py +129 -0
- flyte/_internal/controllers/_local_controller.py +239 -0
- flyte/_internal/controllers/_trace.py +48 -0
- flyte/_internal/controllers/remote/__init__.py +58 -0
- flyte/_internal/controllers/remote/_action.py +211 -0
- flyte/_internal/controllers/remote/_client.py +47 -0
- flyte/_internal/controllers/remote/_controller.py +583 -0
- flyte/_internal/controllers/remote/_core.py +465 -0
- flyte/_internal/controllers/remote/_informer.py +381 -0
- flyte/_internal/controllers/remote/_service_protocol.py +50 -0
- flyte/_internal/imagebuild/__init__.py +3 -0
- flyte/_internal/imagebuild/docker_builder.py +706 -0
- flyte/_internal/imagebuild/image_builder.py +277 -0
- flyte/_internal/imagebuild/remote_builder.py +386 -0
- flyte/_internal/imagebuild/utils.py +78 -0
- flyte/_internal/resolvers/__init__.py +0 -0
- flyte/_internal/resolvers/_task_module.py +21 -0
- flyte/_internal/resolvers/common.py +31 -0
- flyte/_internal/resolvers/default.py +28 -0
- flyte/_internal/runtime/__init__.py +0 -0
- flyte/_internal/runtime/convert.py +486 -0
- flyte/_internal/runtime/entrypoints.py +204 -0
- flyte/_internal/runtime/io.py +188 -0
- flyte/_internal/runtime/resources_serde.py +152 -0
- flyte/_internal/runtime/reuse.py +125 -0
- flyte/_internal/runtime/rusty.py +193 -0
- flyte/_internal/runtime/task_serde.py +362 -0
- flyte/_internal/runtime/taskrunner.py +209 -0
- flyte/_internal/runtime/trigger_serde.py +160 -0
- flyte/_internal/runtime/types_serde.py +54 -0
- flyte/_keyring/__init__.py +0 -0
- flyte/_keyring/file.py +115 -0
- flyte/_logging.py +300 -0
- flyte/_map.py +312 -0
- flyte/_module.py +72 -0
- flyte/_pod.py +30 -0
- flyte/_resources.py +473 -0
- flyte/_retry.py +32 -0
- flyte/_reusable_environment.py +102 -0
- flyte/_run.py +724 -0
- flyte/_secret.py +96 -0
- flyte/_task.py +550 -0
- flyte/_task_environment.py +316 -0
- flyte/_task_plugins.py +47 -0
- flyte/_timeout.py +47 -0
- flyte/_tools.py +27 -0
- flyte/_trace.py +119 -0
- flyte/_trigger.py +1000 -0
- flyte/_utils/__init__.py +30 -0
- flyte/_utils/asyn.py +121 -0
- flyte/_utils/async_cache.py +139 -0
- flyte/_utils/coro_management.py +27 -0
- flyte/_utils/docker_credentials.py +173 -0
- flyte/_utils/file_handling.py +72 -0
- flyte/_utils/helpers.py +134 -0
- flyte/_utils/lazy_module.py +54 -0
- flyte/_utils/module_loader.py +104 -0
- flyte/_utils/org_discovery.py +57 -0
- flyte/_utils/uv_script_parser.py +49 -0
- flyte/_version.py +34 -0
- flyte/app/__init__.py +22 -0
- flyte/app/_app_environment.py +157 -0
- flyte/app/_deploy.py +125 -0
- flyte/app/_input.py +160 -0
- flyte/app/_runtime/__init__.py +3 -0
- flyte/app/_runtime/app_serde.py +347 -0
- flyte/app/_types.py +101 -0
- flyte/app/extras/__init__.py +3 -0
- flyte/app/extras/_fastapi.py +151 -0
- flyte/cli/__init__.py +12 -0
- flyte/cli/_abort.py +28 -0
- flyte/cli/_build.py +114 -0
- flyte/cli/_common.py +468 -0
- flyte/cli/_create.py +371 -0
- flyte/cli/_delete.py +45 -0
- flyte/cli/_deploy.py +293 -0
- flyte/cli/_gen.py +176 -0
- flyte/cli/_get.py +370 -0
- flyte/cli/_option.py +33 -0
- flyte/cli/_params.py +554 -0
- flyte/cli/_plugins.py +209 -0
- flyte/cli/_run.py +597 -0
- flyte/cli/_serve.py +64 -0
- flyte/cli/_update.py +37 -0
- flyte/cli/_user.py +17 -0
- flyte/cli/main.py +221 -0
- flyte/config/__init__.py +3 -0
- flyte/config/_config.py +248 -0
- flyte/config/_internal.py +73 -0
- flyte/config/_reader.py +225 -0
- flyte/connectors/__init__.py +11 -0
- flyte/connectors/_connector.py +270 -0
- flyte/connectors/_server.py +197 -0
- flyte/connectors/utils.py +135 -0
- flyte/errors.py +243 -0
- flyte/extend.py +19 -0
- flyte/extras/__init__.py +5 -0
- flyte/extras/_container.py +286 -0
- flyte/git/__init__.py +3 -0
- flyte/git/_config.py +21 -0
- flyte/io/__init__.py +29 -0
- flyte/io/_dataframe/__init__.py +131 -0
- flyte/io/_dataframe/basic_dfs.py +223 -0
- flyte/io/_dataframe/dataframe.py +1026 -0
- flyte/io/_dir.py +910 -0
- flyte/io/_file.py +914 -0
- flyte/io/_hashing_io.py +342 -0
- flyte/models.py +479 -0
- flyte/py.typed +0 -0
- flyte/remote/__init__.py +35 -0
- flyte/remote/_action.py +738 -0
- flyte/remote/_app.py +57 -0
- flyte/remote/_client/__init__.py +0 -0
- flyte/remote/_client/_protocols.py +189 -0
- flyte/remote/_client/auth/__init__.py +12 -0
- flyte/remote/_client/auth/_auth_utils.py +14 -0
- flyte/remote/_client/auth/_authenticators/__init__.py +0 -0
- flyte/remote/_client/auth/_authenticators/base.py +403 -0
- flyte/remote/_client/auth/_authenticators/client_credentials.py +73 -0
- flyte/remote/_client/auth/_authenticators/device_code.py +117 -0
- flyte/remote/_client/auth/_authenticators/external_command.py +79 -0
- flyte/remote/_client/auth/_authenticators/factory.py +200 -0
- flyte/remote/_client/auth/_authenticators/pkce.py +516 -0
- flyte/remote/_client/auth/_channel.py +213 -0
- flyte/remote/_client/auth/_client_config.py +85 -0
- flyte/remote/_client/auth/_default_html.py +32 -0
- flyte/remote/_client/auth/_grpc_utils/__init__.py +0 -0
- flyte/remote/_client/auth/_grpc_utils/auth_interceptor.py +288 -0
- flyte/remote/_client/auth/_grpc_utils/default_metadata_interceptor.py +151 -0
- flyte/remote/_client/auth/_keyring.py +152 -0
- flyte/remote/_client/auth/_token_client.py +260 -0
- flyte/remote/_client/auth/errors.py +16 -0
- flyte/remote/_client/controlplane.py +128 -0
- flyte/remote/_common.py +30 -0
- flyte/remote/_console.py +19 -0
- flyte/remote/_data.py +161 -0
- flyte/remote/_logs.py +185 -0
- flyte/remote/_project.py +88 -0
- flyte/remote/_run.py +386 -0
- flyte/remote/_secret.py +142 -0
- flyte/remote/_task.py +527 -0
- flyte/remote/_trigger.py +306 -0
- flyte/remote/_user.py +33 -0
- flyte/report/__init__.py +3 -0
- flyte/report/_report.py +182 -0
- flyte/report/_template.html +124 -0
- flyte/storage/__init__.py +36 -0
- flyte/storage/_config.py +237 -0
- flyte/storage/_parallel_reader.py +274 -0
- flyte/storage/_remote_fs.py +34 -0
- flyte/storage/_storage.py +456 -0
- flyte/storage/_utils.py +5 -0
- flyte/syncify/__init__.py +56 -0
- flyte/syncify/_api.py +375 -0
- flyte/types/__init__.py +52 -0
- flyte/types/_interface.py +40 -0
- flyte/types/_pickle.py +145 -0
- flyte/types/_renderer.py +162 -0
- flyte/types/_string_literals.py +119 -0
- flyte/types/_type_engine.py +2254 -0
- flyte/types/_utils.py +80 -0
- flyte-2.0.0b32.data/scripts/debug.py +38 -0
- flyte-2.0.0b32.data/scripts/runtime.py +195 -0
- flyte-2.0.0b32.dist-info/METADATA +351 -0
- flyte-2.0.0b32.dist-info/RECORD +204 -0
- flyte-2.0.0b32.dist-info/WHEEL +5 -0
- flyte-2.0.0b32.dist-info/entry_points.txt +7 -0
- flyte-2.0.0b32.dist-info/licenses/LICENSE +201 -0
- flyte-2.0.0b32.dist-info/top_level.txt +1 -0
flyte/_deploy.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hashlib
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
|
|
7
|
+
|
|
8
|
+
import cloudpickle
|
|
9
|
+
import rich.repr
|
|
10
|
+
|
|
11
|
+
from flyte.models import SerializationContext
|
|
12
|
+
from flyte.syncify import syncify
|
|
13
|
+
|
|
14
|
+
from ._environment import Environment
|
|
15
|
+
from ._image import Image
|
|
16
|
+
from ._initialize import ensure_client, get_client, get_init_config, requires_initialization
|
|
17
|
+
from ._logging import logger
|
|
18
|
+
from ._task import TaskTemplate
|
|
19
|
+
from ._task_environment import TaskEnvironment
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from flyteidl2.task import task_definition_pb2
|
|
23
|
+
|
|
24
|
+
from ._code_bundle import CopyFiles
|
|
25
|
+
from ._deployer import DeployedEnvironment, DeploymentContext
|
|
26
|
+
from ._internal.imagebuild.image_builder import ImageCache
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@rich.repr.auto
|
|
30
|
+
@dataclass
|
|
31
|
+
class DeploymentPlan:
|
|
32
|
+
envs: Dict[str, Environment]
|
|
33
|
+
version: Optional[str] = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@rich.repr.auto
|
|
37
|
+
@dataclass
|
|
38
|
+
class DeployedTask:
|
|
39
|
+
deployed_task: task_definition_pb2.TaskSpec
|
|
40
|
+
deployed_triggers: List[task_definition_pb2.TaskTrigger]
|
|
41
|
+
|
|
42
|
+
def get_name(self) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Returns the name of the deployed environment.
|
|
45
|
+
Returns:
|
|
46
|
+
"""
|
|
47
|
+
return self.deployed_task.task_template.id.name
|
|
48
|
+
|
|
49
|
+
def summary_repr(self) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Returns a summary representation of the deployed task.
|
|
52
|
+
"""
|
|
53
|
+
return (
|
|
54
|
+
f"DeployedTask(name={self.deployed_task.task_template.id.name}, "
|
|
55
|
+
f"version={self.deployed_task.task_template.id.version})"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def table_repr(self) -> List[Tuple[str, ...]]:
|
|
59
|
+
"""
|
|
60
|
+
Returns a table representation of the deployed task.
|
|
61
|
+
"""
|
|
62
|
+
return [
|
|
63
|
+
("type", "task"),
|
|
64
|
+
("name", self.deployed_task.task_template.id.name),
|
|
65
|
+
("version", self.deployed_task.task_template.id.version),
|
|
66
|
+
("triggers", ",".join([t.name for t in self.deployed_triggers])),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@rich.repr.auto
|
|
71
|
+
@dataclass
|
|
72
|
+
class DeployedTaskEnvironment:
|
|
73
|
+
env: TaskEnvironment
|
|
74
|
+
deployed_entities: List[DeployedTask]
|
|
75
|
+
|
|
76
|
+
def get_name(self) -> str:
|
|
77
|
+
"""
|
|
78
|
+
Returns the name of the deployed environment.
|
|
79
|
+
"""
|
|
80
|
+
return self.env.name
|
|
81
|
+
|
|
82
|
+
def summary_repr(self) -> str:
|
|
83
|
+
"""
|
|
84
|
+
Returns a summary representation of the deployment.
|
|
85
|
+
"""
|
|
86
|
+
entities = ", ".join(f"{e.summary_repr()}" for e in self.deployed_entities or [])
|
|
87
|
+
return f"Deployment(env=[{self.env.name}], entities=[{entities}])"
|
|
88
|
+
|
|
89
|
+
def table_repr(self) -> List[List[Tuple[str, ...]]]:
|
|
90
|
+
"""
|
|
91
|
+
Returns a detailed representation of the deployed tasks.
|
|
92
|
+
"""
|
|
93
|
+
tuples = []
|
|
94
|
+
if self.deployed_entities:
|
|
95
|
+
for e in self.deployed_entities:
|
|
96
|
+
tuples.append(e.table_repr())
|
|
97
|
+
return tuples
|
|
98
|
+
|
|
99
|
+
def env_repr(self) -> List[Tuple[str, ...]]:
|
|
100
|
+
"""
|
|
101
|
+
Returns a detailed representation of the deployed environments.
|
|
102
|
+
"""
|
|
103
|
+
env = self.env
|
|
104
|
+
return [
|
|
105
|
+
("environment", env.name),
|
|
106
|
+
("image", env.image.uri if isinstance(env.image, Image) else env.image or ""),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@rich.repr.auto
|
|
111
|
+
@dataclass(frozen=True)
|
|
112
|
+
class Deployment:
|
|
113
|
+
envs: Dict[str, DeployedEnvironment]
|
|
114
|
+
|
|
115
|
+
def summary_repr(self) -> str:
|
|
116
|
+
"""
|
|
117
|
+
Returns a summary representation of the deployment.
|
|
118
|
+
"""
|
|
119
|
+
envs = ", ".join(f"{e.summary_repr()}" for e in self.envs.values() or [])
|
|
120
|
+
return f"Deployment(envs=[{envs}])"
|
|
121
|
+
|
|
122
|
+
def table_repr(self) -> List[List[Tuple[str, ...]]]:
|
|
123
|
+
"""
|
|
124
|
+
Returns a detailed representation of the deployed tasks.
|
|
125
|
+
"""
|
|
126
|
+
tuples = []
|
|
127
|
+
for d in self.envs.values():
|
|
128
|
+
tuples.extend(d.table_repr())
|
|
129
|
+
return tuples
|
|
130
|
+
|
|
131
|
+
def env_repr(self) -> List[List[Tuple[str, ...]]]:
|
|
132
|
+
"""
|
|
133
|
+
Returns a detailed representation of the deployed environments.
|
|
134
|
+
"""
|
|
135
|
+
tuples = []
|
|
136
|
+
for d in self.envs.values():
|
|
137
|
+
tuples.append(d.env_repr())
|
|
138
|
+
return tuples
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def _deploy_task(
|
|
142
|
+
task: TaskTemplate, serialization_context: SerializationContext, dryrun: bool = False
|
|
143
|
+
) -> DeployedTask:
|
|
144
|
+
"""
|
|
145
|
+
Deploy the given task.
|
|
146
|
+
"""
|
|
147
|
+
ensure_client()
|
|
148
|
+
import grpc.aio
|
|
149
|
+
from flyteidl2.task import task_definition_pb2, task_service_pb2
|
|
150
|
+
|
|
151
|
+
import flyte.errors
|
|
152
|
+
|
|
153
|
+
from ._internal.runtime.convert import convert_upload_default_inputs
|
|
154
|
+
from ._internal.runtime.task_serde import translate_task_to_wire
|
|
155
|
+
from ._internal.runtime.trigger_serde import to_task_trigger
|
|
156
|
+
|
|
157
|
+
image_uri = task.image.uri if isinstance(task.image, Image) else task.image
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
if dryrun:
|
|
161
|
+
return DeployedTask(translate_task_to_wire(task, serialization_context), [])
|
|
162
|
+
|
|
163
|
+
default_inputs = await convert_upload_default_inputs(task.interface)
|
|
164
|
+
spec = translate_task_to_wire(task, serialization_context, default_inputs=default_inputs)
|
|
165
|
+
|
|
166
|
+
msg = f"Deploying task {task.name}, with image {image_uri} version {serialization_context.version}"
|
|
167
|
+
if spec.task_template.HasField("container") and spec.task_template.container.args:
|
|
168
|
+
msg += f" from {spec.task_template.container.args[-3]}.{spec.task_template.container.args[-1]}"
|
|
169
|
+
logger.info(msg)
|
|
170
|
+
task_id = task_definition_pb2.TaskIdentifier(
|
|
171
|
+
org=spec.task_template.id.org,
|
|
172
|
+
project=spec.task_template.id.project,
|
|
173
|
+
domain=spec.task_template.id.domain,
|
|
174
|
+
version=spec.task_template.id.version,
|
|
175
|
+
name=spec.task_template.id.name,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
deployable_triggers = []
|
|
179
|
+
for t in task.triggers:
|
|
180
|
+
inputs = spec.task_template.interface.inputs
|
|
181
|
+
default_inputs = spec.default_inputs
|
|
182
|
+
deployable_triggers.append(
|
|
183
|
+
await to_task_trigger(
|
|
184
|
+
t=t, task_name=task.name, task_inputs=inputs, task_default_inputs=list(default_inputs)
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
await get_client().task_service.DeployTask(
|
|
190
|
+
task_service_pb2.DeployTaskRequest(
|
|
191
|
+
task_id=task_id,
|
|
192
|
+
spec=spec,
|
|
193
|
+
triggers=deployable_triggers,
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
logger.info(f"Deployed task {task.name} with version {task_id.version}")
|
|
197
|
+
except grpc.aio.AioRpcError as e:
|
|
198
|
+
if e.code() == grpc.StatusCode.ALREADY_EXISTS:
|
|
199
|
+
logger.info(f"Task {task.name} with image {image_uri} already exists, skipping deployment.")
|
|
200
|
+
return DeployedTask(spec, deployable_triggers)
|
|
201
|
+
raise
|
|
202
|
+
|
|
203
|
+
return DeployedTask(spec, deployable_triggers)
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.error(f"Failed to deploy task {task.name} with image {image_uri}: {e}")
|
|
206
|
+
raise flyte.errors.DeploymentError(
|
|
207
|
+
f"Failed to deploy task {task.name} file{task.source_file} with image {image_uri}, Error: {e!s}"
|
|
208
|
+
) from e
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
async def _build_image_bg(env_name: str, image: Image) -> Tuple[str, str]:
|
|
212
|
+
"""
|
|
213
|
+
Build the image in the background and return the environment name and the built image.
|
|
214
|
+
"""
|
|
215
|
+
from ._build import build
|
|
216
|
+
|
|
217
|
+
logger.info(f"Building image {image.name} for environment {env_name}")
|
|
218
|
+
return env_name, await build.aio(image)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
async def _build_images(deployment: DeploymentPlan, image_refs: Dict[str, str] | None = None) -> ImageCache:
|
|
222
|
+
"""
|
|
223
|
+
Build the images for the given deployment plan and update the environment with the built image.
|
|
224
|
+
"""
|
|
225
|
+
from ._internal.imagebuild.image_builder import ImageCache
|
|
226
|
+
|
|
227
|
+
if image_refs is None:
|
|
228
|
+
image_refs = {}
|
|
229
|
+
|
|
230
|
+
images = []
|
|
231
|
+
image_identifier_map = {}
|
|
232
|
+
for env_name, env in deployment.envs.items():
|
|
233
|
+
if not isinstance(env.image, str):
|
|
234
|
+
if env.image._ref_name is not None:
|
|
235
|
+
if env.image._ref_name in image_refs:
|
|
236
|
+
# If the image is set in the config, set it as the base_image
|
|
237
|
+
image_uri = image_refs[env.image._ref_name]
|
|
238
|
+
env.image = env.image.clone(base_image=image_uri)
|
|
239
|
+
else:
|
|
240
|
+
raise ValueError(
|
|
241
|
+
f"Image name '{env.image._ref_name}' not found in config. Available: {list(image_refs.keys())}"
|
|
242
|
+
)
|
|
243
|
+
if not env.image._layers:
|
|
244
|
+
# No additional layers, use the base_image directly without building
|
|
245
|
+
image_identifier_map[env_name] = image_uri
|
|
246
|
+
continue
|
|
247
|
+
logger.debug(f"Building Image for environment {env_name}, image: {env.image}")
|
|
248
|
+
images.append(_build_image_bg(env_name, env.image))
|
|
249
|
+
|
|
250
|
+
elif env.image == "auto" and "auto" not in image_identifier_map:
|
|
251
|
+
if "default" in image_refs:
|
|
252
|
+
# If the default image is set through CLI, use it instead
|
|
253
|
+
image_uri = image_refs["default"]
|
|
254
|
+
image_identifier_map[env_name] = image_uri
|
|
255
|
+
continue
|
|
256
|
+
auto_image = Image.from_debian_base()
|
|
257
|
+
images.append(_build_image_bg(env_name, auto_image))
|
|
258
|
+
final_images = await asyncio.gather(*images)
|
|
259
|
+
|
|
260
|
+
for env_name, image_uri in final_images:
|
|
261
|
+
logger.warning(f"Built Image for environment {env_name}, image: {image_uri}")
|
|
262
|
+
image_identifier_map[env_name] = image_uri
|
|
263
|
+
|
|
264
|
+
return ImageCache(image_lookup=image_identifier_map)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
async def _deploy_task_env(context: DeploymentContext) -> DeployedTaskEnvironment:
|
|
268
|
+
"""
|
|
269
|
+
Deploy the given task environment.
|
|
270
|
+
"""
|
|
271
|
+
ensure_client()
|
|
272
|
+
env = context.environment
|
|
273
|
+
if not isinstance(env, TaskEnvironment):
|
|
274
|
+
raise ValueError(f"Expected TaskEnvironment, got {type(env)}")
|
|
275
|
+
|
|
276
|
+
task_coros = []
|
|
277
|
+
for task in env.tasks.values():
|
|
278
|
+
task_coros.append(_deploy_task(task, context.serialization_context, dryrun=context.dryrun))
|
|
279
|
+
deployed_task_vals = await asyncio.gather(*task_coros)
|
|
280
|
+
deployed_tasks = []
|
|
281
|
+
for t in deployed_task_vals:
|
|
282
|
+
deployed_tasks.append(t)
|
|
283
|
+
return DeployedTaskEnvironment(env=env, deployed_entities=deployed_tasks)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@requires_initialization
|
|
287
|
+
async def apply(deployment_plan: DeploymentPlan, copy_style: CopyFiles, dryrun: bool = False) -> Deployment:
|
|
288
|
+
import flyte.errors
|
|
289
|
+
|
|
290
|
+
from ._code_bundle import build_code_bundle
|
|
291
|
+
from ._deployer import DeploymentContext, get_deployer
|
|
292
|
+
|
|
293
|
+
cfg = get_init_config()
|
|
294
|
+
|
|
295
|
+
image_cache = await _build_images(deployment_plan, cfg.images)
|
|
296
|
+
|
|
297
|
+
if copy_style == "none" and not deployment_plan.version:
|
|
298
|
+
raise flyte.errors.DeploymentError("Version must be set when copy_style is none")
|
|
299
|
+
else:
|
|
300
|
+
# if this is an AppEnvironment.include, skip code bundling here and build a code bundle at the
|
|
301
|
+
# app._deploy._deploy_app function
|
|
302
|
+
code_bundle = await build_code_bundle(from_dir=cfg.root_dir, dryrun=dryrun, copy_style=copy_style)
|
|
303
|
+
if deployment_plan.version:
|
|
304
|
+
version = deployment_plan.version
|
|
305
|
+
else:
|
|
306
|
+
h = hashlib.md5()
|
|
307
|
+
h.update(cloudpickle.dumps(deployment_plan.envs))
|
|
308
|
+
h.update(code_bundle.computed_version.encode("utf-8"))
|
|
309
|
+
h.update(cloudpickle.dumps(image_cache))
|
|
310
|
+
version = h.hexdigest()
|
|
311
|
+
|
|
312
|
+
sc = SerializationContext(
|
|
313
|
+
project=cfg.project,
|
|
314
|
+
domain=cfg.domain,
|
|
315
|
+
org=cfg.org,
|
|
316
|
+
code_bundle=code_bundle,
|
|
317
|
+
version=version,
|
|
318
|
+
image_cache=image_cache,
|
|
319
|
+
root_dir=cfg.root_dir,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
deployment_coros = []
|
|
323
|
+
for env_name, env in deployment_plan.envs.items():
|
|
324
|
+
logger.info(f"Deploying environment {env_name}")
|
|
325
|
+
deployer = get_deployer(type(env))
|
|
326
|
+
context = DeploymentContext(environment=env, serialization_context=sc, dryrun=dryrun)
|
|
327
|
+
deployment_coros.append(deployer(context))
|
|
328
|
+
deployed_envs = await asyncio.gather(*deployment_coros)
|
|
329
|
+
envs = {}
|
|
330
|
+
for d in deployed_envs:
|
|
331
|
+
envs[d.get_name()] = d
|
|
332
|
+
|
|
333
|
+
return Deployment(envs)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _recursive_discover(planned_envs: Dict[str, Environment], env: Environment) -> Dict[str, Environment]:
|
|
337
|
+
"""
|
|
338
|
+
Recursively deploy the environment and its dependencies, if not already deployed (present in env_tasks) and
|
|
339
|
+
return the updated env_tasks.
|
|
340
|
+
"""
|
|
341
|
+
if env.name in planned_envs:
|
|
342
|
+
if planned_envs[env.name] is not env:
|
|
343
|
+
# Raise error if different TaskEnvironment objects have the same name
|
|
344
|
+
raise ValueError(f"Duplicate environment name '{env.name}' found")
|
|
345
|
+
# Add the environment to the existing envs
|
|
346
|
+
planned_envs[env.name] = env
|
|
347
|
+
|
|
348
|
+
# Recursively discover dependent environments
|
|
349
|
+
for dependent_env in env.depends_on:
|
|
350
|
+
_recursive_discover(planned_envs, dependent_env)
|
|
351
|
+
return planned_envs
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def plan_deploy(*envs: Environment, version: Optional[str] = None) -> List[DeploymentPlan]:
|
|
355
|
+
if envs is None:
|
|
356
|
+
return [DeploymentPlan({})]
|
|
357
|
+
deployment_plans = []
|
|
358
|
+
visited_envs: Set[str] = set()
|
|
359
|
+
for env in envs:
|
|
360
|
+
if env.name in visited_envs:
|
|
361
|
+
raise ValueError(f"Duplicate environment name '{env.name}' found")
|
|
362
|
+
planned_envs = _recursive_discover({}, env)
|
|
363
|
+
deployment_plans.append(DeploymentPlan(planned_envs, version=version))
|
|
364
|
+
visited_envs.update(planned_envs.keys())
|
|
365
|
+
return deployment_plans
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@syncify
|
|
369
|
+
async def deploy(
|
|
370
|
+
*envs: Environment,
|
|
371
|
+
dryrun: bool = False,
|
|
372
|
+
version: str | None = None,
|
|
373
|
+
interactive_mode: bool | None = None,
|
|
374
|
+
copy_style: CopyFiles = "loaded_modules",
|
|
375
|
+
) -> List[Deployment]:
|
|
376
|
+
"""
|
|
377
|
+
Deploy the given environment or list of environments.
|
|
378
|
+
:param envs: Environment or list of environments to deploy.
|
|
379
|
+
:param dryrun: dryrun mode, if True, the deployment will not be applied to the control plane.
|
|
380
|
+
:param version: version of the deployment, if None, the version will be computed from the code bundle.
|
|
381
|
+
TODO: Support for interactive_mode
|
|
382
|
+
:param interactive_mode: Optional, can be forced to True or False.
|
|
383
|
+
If not provided, it will be set based on the current environment. For example Jupyter notebooks are considered
|
|
384
|
+
interactive mode, while scripts are not. This is used to determine how the code bundle is created.
|
|
385
|
+
:param copy_style: Copy style to use when running the task
|
|
386
|
+
|
|
387
|
+
:return: Deployment object containing the deployed environments and tasks.
|
|
388
|
+
"""
|
|
389
|
+
if interactive_mode:
|
|
390
|
+
raise NotImplementedError("Interactive mode not yet implemented for deployment")
|
|
391
|
+
deployment_plans = plan_deploy(*envs, version=version)
|
|
392
|
+
deployments = []
|
|
393
|
+
for deployment_plan in deployment_plans:
|
|
394
|
+
deployments.append(apply(deployment_plan, copy_style=copy_style, dryrun=dryrun))
|
|
395
|
+
return await asyncio.gather(*deployments)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@syncify
|
|
399
|
+
async def build_images(envs: Environment) -> ImageCache:
|
|
400
|
+
"""
|
|
401
|
+
Build the images for the given environments.
|
|
402
|
+
:param envs: Environment to build images for.
|
|
403
|
+
:return: ImageCache containing the built images.
|
|
404
|
+
"""
|
|
405
|
+
cfg = get_init_config()
|
|
406
|
+
images = cfg.images if cfg else {}
|
|
407
|
+
deployment = plan_deploy(envs)
|
|
408
|
+
return await _build_images(deployment[0], images)
|
flyte/_deployer.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Dict, List, Protocol, Tuple, Type
|
|
5
|
+
|
|
6
|
+
import rich.repr
|
|
7
|
+
|
|
8
|
+
from flyte.models import SerializationContext
|
|
9
|
+
|
|
10
|
+
from ._deploy import _deploy_task_env
|
|
11
|
+
from ._environment import Environment
|
|
12
|
+
from ._task_environment import TaskEnvironment
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@rich.repr.auto
|
|
16
|
+
@dataclass
|
|
17
|
+
class DeploymentContext:
|
|
18
|
+
"""
|
|
19
|
+
Context for deployment operations.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
environment: Environment
|
|
23
|
+
serialization_context: SerializationContext
|
|
24
|
+
dryrun: bool = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DeployedEnvironment(Protocol):
|
|
28
|
+
"""
|
|
29
|
+
Protocol for deployed environment representations.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def get_name(self) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Returns the name of the deployed environment.
|
|
35
|
+
Returns:
|
|
36
|
+
"""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
def env_repr(self) -> List[Tuple[str, ...]]:
|
|
40
|
+
"""
|
|
41
|
+
Returns a detailed representation of the deployed environment.
|
|
42
|
+
Returns:
|
|
43
|
+
"""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
def table_repr(self) -> List[List[Tuple[str, ...]]]:
|
|
47
|
+
"""
|
|
48
|
+
Returns a detailed representation of the deployed entities in the environment, useful for tabular display.
|
|
49
|
+
Returns:
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
def summary_repr(self) -> str:
|
|
55
|
+
"""
|
|
56
|
+
Returns a summary representation of the deployed environment.
|
|
57
|
+
Returns:
|
|
58
|
+
"""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Deployer(Protocol):
|
|
63
|
+
"""
|
|
64
|
+
Protocol for deployment callables.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
async def __call__(self, context: DeploymentContext) -> DeployedEnvironment:
|
|
68
|
+
"""
|
|
69
|
+
Deploy the environment described in the context.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
context: Deployment context containing environment, serialization context, and dryrun flag
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Deployment result
|
|
76
|
+
"""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
_ENVTYPE_REGISTRY: Dict[Type[Environment], Deployer] = {
|
|
81
|
+
TaskEnvironment: _deploy_task_env,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def register_deployer(env_type: Type[Environment], deployer: Deployer) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Register a deployer for a specific environment type.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
env_type: Type of environment this deployer handles
|
|
91
|
+
deployer: Deployment callable that conforms to the Deployer protocol
|
|
92
|
+
"""
|
|
93
|
+
_ENVTYPE_REGISTRY[env_type] = deployer
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_deployer(env_type: Type[Environment | TaskEnvironment]) -> Deployer:
|
|
97
|
+
"""
|
|
98
|
+
Get the registered deployer for an environment type.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
env_type: Type of environment to get deployer for
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Deployer for the environment type, defaults to task environment deployer
|
|
105
|
+
"""
|
|
106
|
+
for tpe, v in _ENVTYPE_REGISTRY.items():
|
|
107
|
+
if issubclass(env_type, tpe):
|
|
108
|
+
return v
|
|
109
|
+
raise ValueError(f"No deployer registered for environment type {env_type}")
|
flyte/_doc.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Callable, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Documentation:
|
|
8
|
+
"""
|
|
9
|
+
This class is used to store the documentation of a task.
|
|
10
|
+
|
|
11
|
+
It can be set explicitly or extracted from the docstring of the task.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
description: str
|
|
15
|
+
|
|
16
|
+
def __help__str__(self):
|
|
17
|
+
return self.description
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def extract_docstring(func: Optional[Callable]) -> Documentation:
|
|
21
|
+
"""
|
|
22
|
+
Extracts the description from a docstring.
|
|
23
|
+
"""
|
|
24
|
+
if func is None:
|
|
25
|
+
return Documentation(description="")
|
|
26
|
+
docstring = inspect.getdoc(func)
|
|
27
|
+
if not docstring:
|
|
28
|
+
return Documentation(description="")
|
|
29
|
+
return Documentation(description=docstring)
|
flyte/_docstring.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Callable, Dict, Optional
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Docstring(object):
|
|
8
|
+
def __init__(self, docstring: Optional[str] = None, callable_: Optional[Callable] = None):
|
|
9
|
+
import docstring_parser
|
|
10
|
+
|
|
11
|
+
self._parsed_docstring: docstring_parser.Docstring
|
|
12
|
+
|
|
13
|
+
if docstring is not None:
|
|
14
|
+
self._parsed_docstring = docstring_parser.parse(docstring)
|
|
15
|
+
elif callable_.__doc__ is not None:
|
|
16
|
+
self._parsed_docstring = docstring_parser.parse(callable_.__doc__)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def input_descriptions(self) -> Dict[str, Optional[str]]:
|
|
20
|
+
return {p.arg_name: p.description for p in self._parsed_docstring.params}
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def output_descriptions(self) -> Dict[str, Optional[str]]:
|
|
24
|
+
return {p.return_name: p.description for p in self._parsed_docstring.many_returns if p.return_name is not None}
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def short_description(self) -> Optional[str]:
|
|
28
|
+
return self._parsed_docstring.short_description
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def long_description(self) -> Optional[str]:
|
|
32
|
+
return self._parsed_docstring.long_description
|
flyte/_environment.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
6
|
+
|
|
7
|
+
import rich.repr
|
|
8
|
+
|
|
9
|
+
from ._image import Image
|
|
10
|
+
from ._pod import PodTemplate
|
|
11
|
+
from ._resources import Resources
|
|
12
|
+
from ._secret import Secret, SecretRequest
|
|
13
|
+
|
|
14
|
+
# Global registry to track all Environment instances in load order
|
|
15
|
+
_ENVIRONMENT_REGISTRY: List[Environment] = []
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def list_loaded_environments() -> List[Environment]:
|
|
19
|
+
"""
|
|
20
|
+
Return a list of all Environment objects in the order they were loaded.
|
|
21
|
+
This is useful for deploying environments in the order they were defined.
|
|
22
|
+
"""
|
|
23
|
+
return _ENVIRONMENT_REGISTRY
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_snake_or_kebab_with_numbers(s: str) -> bool:
|
|
27
|
+
return re.fullmatch(r"^[a-z0-9]+([_-][a-z0-9]+)*$", s) is not None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@rich.repr.auto
|
|
31
|
+
@dataclass(init=True, repr=True)
|
|
32
|
+
class Environment:
|
|
33
|
+
"""
|
|
34
|
+
:param name: Name of the environment
|
|
35
|
+
:param image: Docker image to use for the environment. If set to "auto", will use the default image.
|
|
36
|
+
:param resources: Resources to allocate for the environment.
|
|
37
|
+
:param env_vars: Environment variables to set for the environment.
|
|
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
|
|
42
|
+
:param depends_on: Environment dependencies to hint, so when you deploy the environment, the dependencies are
|
|
43
|
+
also deployed. This is useful when you have a set of environments that depend on each other.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
name: str
|
|
47
|
+
depends_on: List[Environment] = field(default_factory=list)
|
|
48
|
+
pod_template: Optional[Union[str, PodTemplate]] = None
|
|
49
|
+
description: Optional[str] = None
|
|
50
|
+
secrets: Optional[SecretRequest] = None
|
|
51
|
+
env_vars: Optional[Dict[str, str]] = None
|
|
52
|
+
resources: Optional[Resources] = None
|
|
53
|
+
interruptible: bool = False
|
|
54
|
+
image: Union[str, Image, Literal["auto"]] = "auto"
|
|
55
|
+
|
|
56
|
+
def _validate_name(self):
|
|
57
|
+
if not is_snake_or_kebab_with_numbers(self.name):
|
|
58
|
+
raise ValueError(f"Environment name '{self.name}' must be in snake_case or kebab-case format.")
|
|
59
|
+
|
|
60
|
+
def __post_init__(self):
|
|
61
|
+
if not isinstance(self.image, (Image, str)):
|
|
62
|
+
raise TypeError(f"Expected image to be of type str or Image, got {type(self.image)}")
|
|
63
|
+
if self.secrets and not isinstance(self.secrets, (str, Secret, List)):
|
|
64
|
+
raise TypeError(f"Expected secrets to be of type SecretRequest, got {type(self.secrets)}")
|
|
65
|
+
for dep in self.depends_on:
|
|
66
|
+
if not isinstance(dep, Environment):
|
|
67
|
+
raise TypeError(f"Expected depends_on to be of type List[Environment], got {type(dep)}")
|
|
68
|
+
if self.resources is not None and not isinstance(self.resources, Resources):
|
|
69
|
+
raise TypeError(f"Expected resources to be of type Resources, got {type(self.resources)}")
|
|
70
|
+
if self.env_vars is not None and not isinstance(self.env_vars, dict):
|
|
71
|
+
raise TypeError(f"Expected env_vars to be of type Dict[str, str], got {type(self.env_vars)}")
|
|
72
|
+
if self.pod_template is not None and not isinstance(self.pod_template, (str, PodTemplate)):
|
|
73
|
+
raise TypeError(f"Expected pod_template to be of type str or PodTemplate, got {type(self.pod_template)}")
|
|
74
|
+
self._validate_name()
|
|
75
|
+
# Automatically register this environment instance in load order
|
|
76
|
+
_ENVIRONMENT_REGISTRY.append(self)
|
|
77
|
+
|
|
78
|
+
def add_dependency(self, *env: Environment):
|
|
79
|
+
"""
|
|
80
|
+
Add a dependency to the environment.
|
|
81
|
+
"""
|
|
82
|
+
for e in env:
|
|
83
|
+
if not isinstance(e, Environment):
|
|
84
|
+
raise TypeError(f"Expected Environment, got {type(e)}")
|
|
85
|
+
if e.name == self.name:
|
|
86
|
+
raise ValueError("Cannot add self as a dependency")
|
|
87
|
+
if e in self.depends_on:
|
|
88
|
+
continue
|
|
89
|
+
self.depends_on.extend(env)
|
|
90
|
+
|
|
91
|
+
def clone_with(
|
|
92
|
+
self,
|
|
93
|
+
name: str,
|
|
94
|
+
image: Optional[Union[str, Image, Literal["auto"]]] = None,
|
|
95
|
+
resources: Optional[Resources] = None,
|
|
96
|
+
env_vars: Optional[Dict[str, str]] = None,
|
|
97
|
+
secrets: Optional[SecretRequest] = None,
|
|
98
|
+
depends_on: Optional[List[Environment]] = None,
|
|
99
|
+
description: Optional[str] = None,
|
|
100
|
+
**kwargs: Any,
|
|
101
|
+
) -> Environment:
|
|
102
|
+
raise NotImplementedError
|
|
103
|
+
|
|
104
|
+
def _get_kwargs(self) -> Dict[str, Any]:
|
|
105
|
+
"""
|
|
106
|
+
Get the keyword arguments for the environment.
|
|
107
|
+
"""
|
|
108
|
+
kwargs: Dict[str, Any] = {
|
|
109
|
+
"depends_on": self.depends_on,
|
|
110
|
+
"image": self.image,
|
|
111
|
+
}
|
|
112
|
+
if self.resources is not None:
|
|
113
|
+
kwargs["resources"] = self.resources
|
|
114
|
+
if self.secrets is not None:
|
|
115
|
+
kwargs["secrets"] = self.secrets
|
|
116
|
+
if self.env_vars is not None:
|
|
117
|
+
kwargs["env_vars"] = self.env_vars
|
|
118
|
+
if self.pod_template is not None:
|
|
119
|
+
kwargs["pod_template"] = self.pod_template
|
|
120
|
+
if self.description is not None:
|
|
121
|
+
kwargs["description"] = self.description
|
|
122
|
+
return kwargs
|