flyte 0.2.0b1__py3-none-any.whl → 2.0.0b46__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.
- flyte/__init__.py +83 -30
- flyte/_bin/connect.py +61 -0
- flyte/_bin/debug.py +38 -0
- flyte/_bin/runtime.py +87 -19
- flyte/_bin/serve.py +351 -0
- flyte/_build.py +3 -2
- flyte/_cache/cache.py +6 -5
- flyte/_cache/local_cache.py +216 -0
- flyte/_code_bundle/_ignore.py +31 -5
- flyte/_code_bundle/_packaging.py +42 -11
- flyte/_code_bundle/_utils.py +57 -34
- flyte/_code_bundle/bundle.py +130 -27
- flyte/_constants.py +1 -0
- flyte/_context.py +21 -5
- flyte/_custom_context.py +73 -0
- flyte/_debug/constants.py +37 -0
- flyte/_debug/utils.py +17 -0
- flyte/_debug/vscode.py +315 -0
- flyte/_deploy.py +396 -75
- flyte/_deployer.py +109 -0
- flyte/_environment.py +94 -11
- flyte/_excepthook.py +37 -0
- flyte/_group.py +2 -1
- flyte/_hash.py +1 -16
- flyte/_image.py +544 -231
- flyte/_initialize.py +456 -316
- flyte/_interface.py +40 -5
- flyte/_internal/controllers/__init__.py +22 -8
- flyte/_internal/controllers/_local_controller.py +159 -35
- flyte/_internal/controllers/_trace.py +18 -10
- flyte/_internal/controllers/remote/__init__.py +38 -9
- flyte/_internal/controllers/remote/_action.py +82 -12
- flyte/_internal/controllers/remote/_client.py +6 -2
- flyte/_internal/controllers/remote/_controller.py +290 -64
- flyte/_internal/controllers/remote/_core.py +155 -95
- flyte/_internal/controllers/remote/_informer.py +40 -20
- flyte/_internal/controllers/remote/_service_protocol.py +2 -2
- flyte/_internal/imagebuild/__init__.py +2 -10
- flyte/_internal/imagebuild/docker_builder.py +391 -84
- flyte/_internal/imagebuild/image_builder.py +111 -55
- flyte/_internal/imagebuild/remote_builder.py +409 -0
- flyte/_internal/imagebuild/utils.py +79 -0
- flyte/_internal/resolvers/_app_env_module.py +92 -0
- flyte/_internal/resolvers/_task_module.py +5 -38
- flyte/_internal/resolvers/app_env.py +26 -0
- flyte/_internal/resolvers/common.py +8 -1
- flyte/_internal/resolvers/default.py +2 -2
- flyte/_internal/runtime/convert.py +319 -36
- flyte/_internal/runtime/entrypoints.py +106 -18
- flyte/_internal/runtime/io.py +71 -23
- flyte/_internal/runtime/resources_serde.py +21 -7
- flyte/_internal/runtime/reuse.py +125 -0
- flyte/_internal/runtime/rusty.py +196 -0
- flyte/_internal/runtime/task_serde.py +239 -66
- flyte/_internal/runtime/taskrunner.py +48 -8
- flyte/_internal/runtime/trigger_serde.py +162 -0
- flyte/_internal/runtime/types_serde.py +7 -16
- flyte/_keyring/file.py +115 -0
- flyte/_link.py +30 -0
- flyte/_logging.py +241 -42
- flyte/_map.py +312 -0
- flyte/_metrics.py +59 -0
- flyte/_module.py +74 -0
- flyte/_pod.py +30 -0
- flyte/_resources.py +296 -33
- flyte/_retry.py +1 -7
- flyte/_reusable_environment.py +72 -7
- flyte/_run.py +462 -132
- flyte/_secret.py +47 -11
- flyte/_serve.py +333 -0
- flyte/_task.py +245 -56
- flyte/_task_environment.py +219 -97
- flyte/_task_plugins.py +47 -0
- flyte/_tools.py +8 -8
- flyte/_trace.py +15 -24
- flyte/_trigger.py +1027 -0
- flyte/_utils/__init__.py +12 -1
- flyte/_utils/asyn.py +3 -1
- flyte/_utils/async_cache.py +139 -0
- flyte/_utils/coro_management.py +5 -4
- flyte/_utils/description_parser.py +19 -0
- flyte/_utils/docker_credentials.py +173 -0
- flyte/_utils/helpers.py +45 -19
- flyte/_utils/module_loader.py +123 -0
- flyte/_utils/org_discovery.py +57 -0
- flyte/_utils/uv_script_parser.py +8 -1
- flyte/_version.py +16 -3
- flyte/app/__init__.py +27 -0
- flyte/app/_app_environment.py +362 -0
- flyte/app/_connector_environment.py +40 -0
- flyte/app/_deploy.py +130 -0
- flyte/app/_parameter.py +343 -0
- flyte/app/_runtime/__init__.py +3 -0
- flyte/app/_runtime/app_serde.py +383 -0
- flyte/app/_types.py +113 -0
- flyte/app/extras/__init__.py +9 -0
- flyte/app/extras/_auth_middleware.py +217 -0
- flyte/app/extras/_fastapi.py +93 -0
- flyte/app/extras/_model_loader/__init__.py +3 -0
- flyte/app/extras/_model_loader/config.py +7 -0
- flyte/app/extras/_model_loader/loader.py +288 -0
- flyte/cli/__init__.py +12 -0
- flyte/cli/_abort.py +28 -0
- flyte/cli/_build.py +114 -0
- flyte/cli/_common.py +493 -0
- flyte/cli/_create.py +371 -0
- flyte/cli/_delete.py +45 -0
- flyte/cli/_deploy.py +401 -0
- flyte/cli/_gen.py +316 -0
- flyte/cli/_get.py +446 -0
- flyte/cli/_option.py +33 -0
- flyte/{_cli → cli}/_params.py +57 -17
- flyte/cli/_plugins.py +209 -0
- flyte/cli/_prefetch.py +292 -0
- flyte/cli/_run.py +690 -0
- flyte/cli/_serve.py +338 -0
- flyte/cli/_update.py +86 -0
- flyte/cli/_user.py +20 -0
- flyte/cli/main.py +246 -0
- flyte/config/__init__.py +2 -167
- flyte/config/_config.py +215 -163
- flyte/config/_internal.py +10 -1
- flyte/config/_reader.py +225 -0
- flyte/connectors/__init__.py +11 -0
- flyte/connectors/_connector.py +330 -0
- flyte/connectors/_server.py +194 -0
- flyte/connectors/utils.py +159 -0
- flyte/errors.py +134 -2
- flyte/extend.py +24 -0
- flyte/extras/_container.py +69 -56
- flyte/git/__init__.py +3 -0
- flyte/git/_config.py +279 -0
- flyte/io/__init__.py +8 -1
- flyte/io/{structured_dataset → _dataframe}/__init__.py +32 -30
- flyte/io/{structured_dataset → _dataframe}/basic_dfs.py +75 -68
- flyte/io/{structured_dataset/structured_dataset.py → _dataframe/dataframe.py} +207 -242
- flyte/io/_dir.py +575 -113
- flyte/io/_file.py +587 -141
- flyte/io/_hashing_io.py +342 -0
- flyte/io/extend.py +7 -0
- flyte/models.py +635 -0
- flyte/prefetch/__init__.py +22 -0
- flyte/prefetch/_hf_model.py +563 -0
- flyte/remote/__init__.py +14 -3
- flyte/remote/_action.py +879 -0
- flyte/remote/_app.py +346 -0
- flyte/remote/_auth_metadata.py +42 -0
- flyte/remote/_client/_protocols.py +62 -4
- flyte/remote/_client/auth/_auth_utils.py +19 -0
- flyte/remote/_client/auth/_authenticators/base.py +8 -2
- flyte/remote/_client/auth/_authenticators/device_code.py +4 -5
- flyte/remote/_client/auth/_authenticators/factory.py +4 -0
- flyte/remote/_client/auth/_authenticators/passthrough.py +79 -0
- flyte/remote/_client/auth/_authenticators/pkce.py +17 -18
- flyte/remote/_client/auth/_channel.py +47 -18
- flyte/remote/_client/auth/_client_config.py +5 -3
- flyte/remote/_client/auth/_keyring.py +15 -2
- flyte/remote/_client/auth/_token_client.py +3 -3
- flyte/remote/_client/controlplane.py +206 -18
- flyte/remote/_common.py +66 -0
- flyte/remote/_data.py +107 -22
- flyte/remote/_logs.py +116 -33
- flyte/remote/_project.py +21 -19
- flyte/remote/_run.py +164 -631
- flyte/remote/_secret.py +72 -29
- flyte/remote/_task.py +387 -46
- flyte/remote/_trigger.py +368 -0
- flyte/remote/_user.py +43 -0
- flyte/report/_report.py +10 -6
- flyte/storage/__init__.py +13 -1
- flyte/storage/_config.py +237 -0
- flyte/storage/_parallel_reader.py +289 -0
- flyte/storage/_storage.py +268 -59
- flyte/syncify/__init__.py +56 -0
- flyte/syncify/_api.py +414 -0
- flyte/types/__init__.py +39 -0
- flyte/types/_interface.py +22 -7
- flyte/{io/pickle/transformer.py → types/_pickle.py} +37 -9
- flyte/types/_string_literals.py +8 -9
- flyte/types/_type_engine.py +226 -126
- flyte/types/_utils.py +1 -1
- flyte-2.0.0b46.data/scripts/debug.py +38 -0
- flyte-2.0.0b46.data/scripts/runtime.py +194 -0
- flyte-2.0.0b46.dist-info/METADATA +352 -0
- flyte-2.0.0b46.dist-info/RECORD +221 -0
- flyte-2.0.0b46.dist-info/entry_points.txt +8 -0
- flyte-2.0.0b46.dist-info/licenses/LICENSE +201 -0
- flyte/_api_commons.py +0 -3
- flyte/_cli/_common.py +0 -299
- flyte/_cli/_create.py +0 -42
- flyte/_cli/_delete.py +0 -23
- flyte/_cli/_deploy.py +0 -140
- flyte/_cli/_get.py +0 -235
- flyte/_cli/_run.py +0 -174
- flyte/_cli/main.py +0 -98
- flyte/_datastructures.py +0 -342
- flyte/_internal/controllers/pbhash.py +0 -39
- flyte/_protos/common/authorization_pb2.py +0 -66
- flyte/_protos/common/authorization_pb2.pyi +0 -108
- flyte/_protos/common/authorization_pb2_grpc.py +0 -4
- flyte/_protos/common/identifier_pb2.py +0 -71
- flyte/_protos/common/identifier_pb2.pyi +0 -82
- flyte/_protos/common/identifier_pb2_grpc.py +0 -4
- flyte/_protos/common/identity_pb2.py +0 -48
- flyte/_protos/common/identity_pb2.pyi +0 -72
- flyte/_protos/common/identity_pb2_grpc.py +0 -4
- flyte/_protos/common/list_pb2.py +0 -36
- flyte/_protos/common/list_pb2.pyi +0 -69
- flyte/_protos/common/list_pb2_grpc.py +0 -4
- flyte/_protos/common/policy_pb2.py +0 -37
- flyte/_protos/common/policy_pb2.pyi +0 -27
- flyte/_protos/common/policy_pb2_grpc.py +0 -4
- flyte/_protos/common/role_pb2.py +0 -37
- flyte/_protos/common/role_pb2.pyi +0 -53
- flyte/_protos/common/role_pb2_grpc.py +0 -4
- flyte/_protos/common/runtime_version_pb2.py +0 -28
- flyte/_protos/common/runtime_version_pb2.pyi +0 -24
- flyte/_protos/common/runtime_version_pb2_grpc.py +0 -4
- flyte/_protos/logs/dataplane/payload_pb2.py +0 -96
- flyte/_protos/logs/dataplane/payload_pb2.pyi +0 -168
- flyte/_protos/logs/dataplane/payload_pb2_grpc.py +0 -4
- flyte/_protos/secret/definition_pb2.py +0 -49
- flyte/_protos/secret/definition_pb2.pyi +0 -93
- flyte/_protos/secret/definition_pb2_grpc.py +0 -4
- flyte/_protos/secret/payload_pb2.py +0 -62
- flyte/_protos/secret/payload_pb2.pyi +0 -94
- flyte/_protos/secret/payload_pb2_grpc.py +0 -4
- flyte/_protos/secret/secret_pb2.py +0 -38
- flyte/_protos/secret/secret_pb2.pyi +0 -6
- flyte/_protos/secret/secret_pb2_grpc.py +0 -198
- flyte/_protos/secret/secret_pb2_grpc_grpc.py +0 -198
- flyte/_protos/validate/validate/validate_pb2.py +0 -76
- flyte/_protos/workflow/node_execution_service_pb2.py +0 -26
- flyte/_protos/workflow/node_execution_service_pb2.pyi +0 -4
- flyte/_protos/workflow/node_execution_service_pb2_grpc.py +0 -32
- flyte/_protos/workflow/queue_service_pb2.py +0 -106
- flyte/_protos/workflow/queue_service_pb2.pyi +0 -141
- flyte/_protos/workflow/queue_service_pb2_grpc.py +0 -172
- flyte/_protos/workflow/run_definition_pb2.py +0 -128
- flyte/_protos/workflow/run_definition_pb2.pyi +0 -310
- flyte/_protos/workflow/run_definition_pb2_grpc.py +0 -4
- flyte/_protos/workflow/run_logs_service_pb2.py +0 -41
- flyte/_protos/workflow/run_logs_service_pb2.pyi +0 -28
- flyte/_protos/workflow/run_logs_service_pb2_grpc.py +0 -69
- flyte/_protos/workflow/run_service_pb2.py +0 -133
- flyte/_protos/workflow/run_service_pb2.pyi +0 -175
- flyte/_protos/workflow/run_service_pb2_grpc.py +0 -412
- flyte/_protos/workflow/state_service_pb2.py +0 -58
- flyte/_protos/workflow/state_service_pb2.pyi +0 -71
- flyte/_protos/workflow/state_service_pb2_grpc.py +0 -138
- flyte/_protos/workflow/task_definition_pb2.py +0 -72
- flyte/_protos/workflow/task_definition_pb2.pyi +0 -65
- flyte/_protos/workflow/task_definition_pb2_grpc.py +0 -4
- flyte/_protos/workflow/task_service_pb2.py +0 -44
- flyte/_protos/workflow/task_service_pb2.pyi +0 -31
- flyte/_protos/workflow/task_service_pb2_grpc.py +0 -104
- flyte/io/_dataframe.py +0 -0
- flyte/io/pickle/__init__.py +0 -0
- flyte/remote/_console.py +0 -18
- flyte-0.2.0b1.dist-info/METADATA +0 -179
- flyte-0.2.0b1.dist-info/RECORD +0 -204
- flyte-0.2.0b1.dist-info/entry_points.txt +0 -3
- /flyte/{_cli → _debug}/__init__.py +0 -0
- /flyte/{_protos → _keyring}/__init__.py +0 -0
- {flyte-0.2.0b1.dist-info → flyte-2.0.0b46.dist-info}/WHEEL +0 -0
- {flyte-0.2.0b1.dist-info → flyte-2.0.0b46.dist-info}/top_level.txt +0 -0
flyte/_image.py
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import base64
|
|
4
3
|
import hashlib
|
|
4
|
+
import os.path
|
|
5
5
|
import sys
|
|
6
|
+
import typing
|
|
6
7
|
from abc import abstractmethod
|
|
7
|
-
from dataclasses import
|
|
8
|
+
from dataclasses import dataclass, field
|
|
8
9
|
from functools import cached_property
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import
|
|
11
|
+
from typing import TYPE_CHECKING, ClassVar, Dict, List, Literal, Optional, Tuple, TypeVar, Union
|
|
11
12
|
|
|
12
13
|
import rich.repr
|
|
14
|
+
from packaging.version import Version
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from flyte import Secret, SecretRequest
|
|
13
18
|
|
|
14
19
|
# Supported Python versions
|
|
15
20
|
PYTHON_3_10 = (3, 10)
|
|
16
21
|
PYTHON_3_11 = (3, 11)
|
|
17
22
|
PYTHON_3_12 = (3, 12)
|
|
18
23
|
PYTHON_3_13 = (3, 13)
|
|
24
|
+
PYTHON_3_14 = (3, 14)
|
|
19
25
|
|
|
20
26
|
# 0 is a file, 1 is a directory
|
|
21
27
|
CopyConfigType = Literal[0, 1]
|
|
28
|
+
SOURCE_ROOT = Path(__file__).parent.parent.parent
|
|
29
|
+
DIST_FOLDER = SOURCE_ROOT / "dist"
|
|
22
30
|
|
|
23
31
|
T = TypeVar("T")
|
|
24
32
|
|
|
@@ -44,8 +52,6 @@ class Layer:
|
|
|
44
52
|
layered images programmatically.
|
|
45
53
|
"""
|
|
46
54
|
|
|
47
|
-
_compute_identifier: Callable[[Layer], str] = field(default=lambda x: x.__str__(), init=True)
|
|
48
|
-
|
|
49
55
|
@abstractmethod
|
|
50
56
|
def update_hash(self, hasher: hashlib._Hash):
|
|
51
57
|
"""
|
|
@@ -53,7 +59,6 @@ class Layer:
|
|
|
53
59
|
|
|
54
60
|
:param hasher: The hash object to update with the layer's data.
|
|
55
61
|
"""
|
|
56
|
-
...
|
|
57
62
|
|
|
58
63
|
def validate(self):
|
|
59
64
|
"""
|
|
@@ -64,24 +69,33 @@ class Layer:
|
|
|
64
69
|
|
|
65
70
|
@rich.repr.auto
|
|
66
71
|
@dataclass(kw_only=True, frozen=True, repr=True)
|
|
67
|
-
class
|
|
68
|
-
packages: Optional[Tuple[str, ...]] = None
|
|
72
|
+
class PipOption:
|
|
69
73
|
index_url: Optional[str] = None
|
|
70
74
|
extra_index_urls: Optional[Tuple[str] | Tuple[str, ...] | List[str]] = None
|
|
71
75
|
pre: bool = False
|
|
72
76
|
extra_args: Optional[str] = None
|
|
77
|
+
secret_mounts: Optional[Tuple[str | Secret, ...]] = None
|
|
78
|
+
|
|
79
|
+
def get_pip_install_args(self) -> List[str]:
|
|
80
|
+
pip_install_args = []
|
|
81
|
+
if self.index_url:
|
|
82
|
+
pip_install_args.append(f"--index-url {self.index_url}")
|
|
83
|
+
|
|
84
|
+
if self.extra_index_urls:
|
|
85
|
+
pip_install_args.extend([f"--extra-index-url {url}" for url in self.extra_index_urls])
|
|
73
86
|
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
if self.pre:
|
|
88
|
+
pip_install_args.append("--pre")
|
|
89
|
+
|
|
90
|
+
if self.extra_args:
|
|
91
|
+
pip_install_args.append(self.extra_args)
|
|
92
|
+
return pip_install_args
|
|
76
93
|
|
|
77
94
|
def update_hash(self, hasher: hashlib._Hash):
|
|
78
95
|
"""
|
|
79
|
-
Update the hash with the
|
|
96
|
+
Update the hash with the PipOption
|
|
80
97
|
"""
|
|
81
98
|
hash_input = ""
|
|
82
|
-
if self.packages:
|
|
83
|
-
for package in self.packages:
|
|
84
|
-
hash_input += package
|
|
85
99
|
if self.index_url:
|
|
86
100
|
hash_input += self.index_url
|
|
87
101
|
if self.extra_index_urls:
|
|
@@ -91,10 +105,53 @@ class PipPackages(Layer):
|
|
|
91
105
|
hash_input += str(self.pre)
|
|
92
106
|
if self.extra_args:
|
|
93
107
|
hash_input += self.extra_args
|
|
108
|
+
if self.secret_mounts:
|
|
109
|
+
for secret_mount in self.secret_mounts:
|
|
110
|
+
hash_input += str(secret_mount)
|
|
111
|
+
|
|
112
|
+
hasher.update(hash_input.encode("utf-8"))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@rich.repr.auto
|
|
116
|
+
@dataclass(kw_only=True, frozen=True, repr=True)
|
|
117
|
+
class PipPackages(PipOption, Layer):
|
|
118
|
+
packages: Optional[Tuple[str, ...]] = None
|
|
119
|
+
|
|
120
|
+
def update_hash(self, hasher: hashlib._Hash):
|
|
121
|
+
"""
|
|
122
|
+
Update the hash with the pip packages
|
|
123
|
+
"""
|
|
124
|
+
super().update_hash(hasher)
|
|
125
|
+
hash_input = ""
|
|
126
|
+
if self.packages:
|
|
127
|
+
for package in self.packages:
|
|
128
|
+
hash_input += package
|
|
94
129
|
|
|
95
130
|
hasher.update(hash_input.encode("utf-8"))
|
|
96
131
|
|
|
97
132
|
|
|
133
|
+
@rich.repr.auto
|
|
134
|
+
@dataclass(kw_only=True, frozen=True, repr=True)
|
|
135
|
+
class PythonWheels(PipOption, Layer):
|
|
136
|
+
wheel_dir: Path
|
|
137
|
+
wheel_dir_name: str = field(init=False)
|
|
138
|
+
package_name: str
|
|
139
|
+
|
|
140
|
+
def __post_init__(self):
|
|
141
|
+
object.__setattr__(self, "wheel_dir_name", self.wheel_dir.name)
|
|
142
|
+
|
|
143
|
+
def update_hash(self, hasher: hashlib._Hash):
|
|
144
|
+
super().update_hash(hasher)
|
|
145
|
+
from ._utils import filehash_update
|
|
146
|
+
|
|
147
|
+
# Iterate through all the wheel files in the directory and update the hash
|
|
148
|
+
for wheel_file in self.wheel_dir.glob("*.whl"):
|
|
149
|
+
if not wheel_file.is_file():
|
|
150
|
+
# Skip if it's not a file (e.g., directory or symlink)
|
|
151
|
+
continue
|
|
152
|
+
filehash_update(wheel_file, hasher)
|
|
153
|
+
|
|
154
|
+
|
|
98
155
|
@rich.repr.auto
|
|
99
156
|
@dataclass(kw_only=True, frozen=True, repr=True)
|
|
100
157
|
class Requirements(PipPackages):
|
|
@@ -107,22 +164,134 @@ class Requirements(PipPackages):
|
|
|
107
164
|
filehash_update(self.file, hasher)
|
|
108
165
|
|
|
109
166
|
|
|
167
|
+
@rich.repr.auto
|
|
168
|
+
@dataclass(frozen=True, repr=True)
|
|
169
|
+
class UVProject(PipOption, Layer):
|
|
170
|
+
pyproject: Path
|
|
171
|
+
uvlock: Path
|
|
172
|
+
project_install_mode: typing.Literal["dependencies_only", "install_project"] = "dependencies_only"
|
|
173
|
+
|
|
174
|
+
def validate(self):
|
|
175
|
+
if not self.pyproject.exists():
|
|
176
|
+
raise FileNotFoundError(f"pyproject.toml file {self.pyproject.resolve()} does not exist")
|
|
177
|
+
if not self.pyproject.is_file():
|
|
178
|
+
raise ValueError(f"Pyproject file {self.pyproject.resolve()} is not a file")
|
|
179
|
+
if not self.uvlock.exists():
|
|
180
|
+
raise ValueError(f"UVLock file {self.uvlock.resolve()} does not exist")
|
|
181
|
+
super().validate()
|
|
182
|
+
|
|
183
|
+
def update_hash(self, hasher: hashlib._Hash):
|
|
184
|
+
from ._utils import filehash_update, update_hasher_for_source
|
|
185
|
+
|
|
186
|
+
super().update_hash(hasher)
|
|
187
|
+
if self.project_install_mode == "dependencies_only":
|
|
188
|
+
filehash_update(self.uvlock, hasher)
|
|
189
|
+
filehash_update(self.pyproject, hasher)
|
|
190
|
+
else:
|
|
191
|
+
update_hasher_for_source(self.pyproject.parent, hasher)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@rich.repr.auto
|
|
195
|
+
@dataclass(frozen=True, repr=True)
|
|
196
|
+
class PoetryProject(Layer):
|
|
197
|
+
"""
|
|
198
|
+
Poetry does not use pip options, so the PoetryProject class do not inherits PipOption class
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
pyproject: Path
|
|
202
|
+
poetry_lock: Path
|
|
203
|
+
extra_args: Optional[str] = None
|
|
204
|
+
project_install_mode: typing.Literal["dependencies_only", "install_project"] = "dependencies_only"
|
|
205
|
+
secret_mounts: Optional[Tuple[str | Secret, ...]] = None
|
|
206
|
+
|
|
207
|
+
def validate(self):
|
|
208
|
+
if not self.pyproject.exists():
|
|
209
|
+
raise FileNotFoundError(f"pyproject.toml file {self.pyproject} does not exist")
|
|
210
|
+
if not self.pyproject.is_file():
|
|
211
|
+
raise ValueError(f"Pyproject file {self.pyproject} is not a file")
|
|
212
|
+
if not self.poetry_lock.exists():
|
|
213
|
+
raise ValueError(f"poetry.lock file {self.poetry_lock} does not exist")
|
|
214
|
+
super().validate()
|
|
215
|
+
|
|
216
|
+
def update_hash(self, hasher: hashlib._Hash):
|
|
217
|
+
from ._utils import filehash_update, update_hasher_for_source
|
|
218
|
+
|
|
219
|
+
hash_input = ""
|
|
220
|
+
if self.extra_args:
|
|
221
|
+
hash_input += self.extra_args
|
|
222
|
+
if self.secret_mounts:
|
|
223
|
+
for secret_mount in self.secret_mounts:
|
|
224
|
+
hash_input += str(secret_mount)
|
|
225
|
+
hasher.update(hash_input.encode("utf-8"))
|
|
226
|
+
|
|
227
|
+
if self.project_install_mode == "dependencies_only":
|
|
228
|
+
filehash_update(self.poetry_lock, hasher)
|
|
229
|
+
filehash_update(self.pyproject, hasher)
|
|
230
|
+
else:
|
|
231
|
+
update_hasher_for_source(self.pyproject.parent, hasher)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@rich.repr.auto
|
|
235
|
+
@dataclass(frozen=True, repr=True)
|
|
236
|
+
class UVScript(PipOption, Layer):
|
|
237
|
+
script: Path
|
|
238
|
+
script_name: str = field(init=False)
|
|
239
|
+
|
|
240
|
+
def __post_init__(self):
|
|
241
|
+
object.__setattr__(self, "script_name", self.script.name)
|
|
242
|
+
|
|
243
|
+
def validate(self):
|
|
244
|
+
if not self.script.exists():
|
|
245
|
+
raise FileNotFoundError(f"UV script {self.script} does not exist")
|
|
246
|
+
if not self.script.is_file():
|
|
247
|
+
raise ValueError(f"UV script {self.script} is not a file")
|
|
248
|
+
if not self.script.suffix == ".py":
|
|
249
|
+
raise ValueError(f"UV script {self.script} must have a .py extension")
|
|
250
|
+
super().validate()
|
|
251
|
+
|
|
252
|
+
def update_hash(self, hasher: hashlib._Hash):
|
|
253
|
+
from ._utils import parse_uv_script_file
|
|
254
|
+
|
|
255
|
+
header = parse_uv_script_file(self.script)
|
|
256
|
+
h_tuple = _ensure_tuple(header)
|
|
257
|
+
if h_tuple:
|
|
258
|
+
hasher.update(h_tuple.__str__().encode("utf-8"))
|
|
259
|
+
super().update_hash(hasher)
|
|
260
|
+
if header.pyprojects:
|
|
261
|
+
for pyproject in header.pyprojects:
|
|
262
|
+
UVProject(
|
|
263
|
+
Path(pyproject) / "pyproject.toml", Path(pyproject) / "uv.lock", "install_project"
|
|
264
|
+
).update_hash(hasher)
|
|
265
|
+
|
|
266
|
+
|
|
110
267
|
@rich.repr.auto
|
|
111
268
|
@dataclass(frozen=True, repr=True)
|
|
112
269
|
class AptPackages(Layer):
|
|
113
270
|
packages: Tuple[str, ...]
|
|
271
|
+
secret_mounts: Optional[Tuple[str | Secret, ...]] = None
|
|
114
272
|
|
|
115
273
|
def update_hash(self, hasher: hashlib._Hash):
|
|
116
|
-
|
|
274
|
+
hash_input = "".join(self.packages)
|
|
275
|
+
|
|
276
|
+
if self.secret_mounts:
|
|
277
|
+
for secret_mount in self.secret_mounts:
|
|
278
|
+
hash_input += str(secret_mount)
|
|
279
|
+
hasher.update(hash_input.encode("utf-8"))
|
|
117
280
|
|
|
118
281
|
|
|
119
282
|
@rich.repr.auto
|
|
120
283
|
@dataclass(frozen=True, repr=True)
|
|
121
284
|
class Commands(Layer):
|
|
122
285
|
commands: Tuple[str, ...]
|
|
286
|
+
secret_mounts: Optional[Tuple[str | Secret, ...]] = None
|
|
123
287
|
|
|
124
288
|
def update_hash(self, hasher: hashlib._Hash):
|
|
125
|
-
|
|
289
|
+
hash_input = "".join(self.commands)
|
|
290
|
+
|
|
291
|
+
if self.secret_mounts:
|
|
292
|
+
for secret_mount in self.secret_mounts:
|
|
293
|
+
hash_input += str(secret_mount)
|
|
294
|
+
hasher.update(hash_input.encode("utf-8"))
|
|
126
295
|
|
|
127
296
|
|
|
128
297
|
@rich.repr.auto
|
|
@@ -136,37 +305,38 @@ class WorkDir(Layer):
|
|
|
136
305
|
|
|
137
306
|
@rich.repr.auto
|
|
138
307
|
@dataclass(frozen=True, repr=True)
|
|
139
|
-
class
|
|
140
|
-
|
|
141
|
-
context_source: Path
|
|
142
|
-
image_dest: str = "."
|
|
143
|
-
|
|
144
|
-
def validate(self):
|
|
145
|
-
if not self.context_source.exists():
|
|
146
|
-
raise ValueError(f"Source folder {self.context_source.absolute()} does not exist")
|
|
147
|
-
if not self.context_source.is_dir() and self.path_type == 1:
|
|
148
|
-
raise ValueError(f"Source folder {self.context_source.absolute()} is not a directory")
|
|
149
|
-
if not self.context_source.is_file() and self.path_type == 0:
|
|
150
|
-
raise ValueError(f"Source file {self.context_source.absolute()} is not a file")
|
|
308
|
+
class DockerIgnore(Layer):
|
|
309
|
+
path: str
|
|
151
310
|
|
|
152
311
|
def update_hash(self, hasher: hashlib._Hash):
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
update_hasher_for_source(self.context_source, hasher)
|
|
156
|
-
if self.image_dest:
|
|
157
|
-
hasher.update(self.image_dest.encode("utf-8"))
|
|
312
|
+
hasher.update(self.path.encode("utf-8"))
|
|
158
313
|
|
|
159
314
|
|
|
160
315
|
@rich.repr.auto
|
|
161
316
|
@dataclass(frozen=True, repr=True)
|
|
162
|
-
class
|
|
163
|
-
|
|
164
|
-
|
|
317
|
+
class CopyConfig(Layer):
|
|
318
|
+
path_type: CopyConfigType
|
|
319
|
+
src: Path
|
|
320
|
+
dst: str
|
|
321
|
+
|
|
322
|
+
def __post_init__(self):
|
|
323
|
+
if self.path_type not in (0, 1):
|
|
324
|
+
raise ValueError(f"Invalid path_type {self.path_type}, must be 0 (file) or 1 (directory)")
|
|
325
|
+
|
|
326
|
+
def validate(self):
|
|
327
|
+
if not self.src.exists():
|
|
328
|
+
raise ValueError(f"Source folder {self.src.absolute()} does not exist")
|
|
329
|
+
if not self.src.is_dir() and self.path_type == 1:
|
|
330
|
+
raise ValueError(f"Source folder {self.src.absolute()} is not a directory")
|
|
331
|
+
if not self.src.is_file() and self.path_type == 0:
|
|
332
|
+
raise ValueError(f"Source file {self.src.absolute()} is not a file")
|
|
165
333
|
|
|
166
334
|
def update_hash(self, hasher: hashlib._Hash):
|
|
167
|
-
from ._utils import
|
|
335
|
+
from ._utils import update_hasher_for_source
|
|
168
336
|
|
|
169
|
-
|
|
337
|
+
update_hasher_for_source(self.src, hasher)
|
|
338
|
+
if self.dst:
|
|
339
|
+
hasher.update(self.dst.encode("utf-8"))
|
|
170
340
|
|
|
171
341
|
|
|
172
342
|
@rich.repr.auto
|
|
@@ -204,8 +374,9 @@ class Env(Layer):
|
|
|
204
374
|
|
|
205
375
|
Architecture = Literal["linux/amd64", "linux/arm64"]
|
|
206
376
|
|
|
207
|
-
_BASE_REGISTRY = "ghcr.io/
|
|
377
|
+
_BASE_REGISTRY = "ghcr.io/flyteorg"
|
|
208
378
|
_DEFAULT_IMAGE_NAME = "flyte"
|
|
379
|
+
_DEFAULT_IMAGE_REF_NAME = "default"
|
|
209
380
|
|
|
210
381
|
|
|
211
382
|
def _detect_python_version() -> Tuple[int, int]:
|
|
@@ -238,77 +409,82 @@ class Image:
|
|
|
238
409
|
registry: Optional[str] = field(default=None)
|
|
239
410
|
name: Optional[str] = field(default=None)
|
|
240
411
|
platform: Tuple[Architecture, ...] = field(default=("linux/amd64",))
|
|
241
|
-
tag: Optional[str] = field(default=None)
|
|
242
412
|
python_version: Tuple[int, int] = field(default_factory=_detect_python_version)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
_identifier_override: Optional[str] = field(default=None, init=False)
|
|
246
|
-
# This is set on default images. These images are built from the base Dockerfile in this library and shouldn't be
|
|
247
|
-
# modified with additional layers.
|
|
248
|
-
is_final: bool = field(default=False)
|
|
413
|
+
# Refer to the image_refs (name:image-uri) set in CLI or config
|
|
414
|
+
_ref_name: Optional[str] = field(default=None)
|
|
249
415
|
|
|
250
416
|
# Layers to be added to the image. In init, because frozen, but users shouldn't access, so underscore.
|
|
251
417
|
_layers: Tuple[Layer, ...] = field(default_factory=tuple)
|
|
252
418
|
|
|
419
|
+
# Only settable internally.
|
|
420
|
+
_tag: Optional[str] = field(default=None, init=False)
|
|
421
|
+
|
|
253
422
|
_DEFAULT_IMAGE_PREFIXES: ClassVar = {
|
|
254
423
|
PYTHON_3_10: "py3.10-",
|
|
255
424
|
PYTHON_3_11: "py3.11-",
|
|
256
425
|
PYTHON_3_12: "py3.12-",
|
|
257
426
|
PYTHON_3_13: "py3.13-",
|
|
427
|
+
PYTHON_3_14: "py3.14-",
|
|
258
428
|
}
|
|
259
429
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
#
|
|
277
|
-
|
|
278
|
-
#
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
image_dict["layers"] = layers_str_repr
|
|
283
|
-
spec_bytes = image_dict.__str__().encode("utf-8")
|
|
284
|
-
return base64.urlsafe_b64encode(hashlib.md5(spec_bytes).digest()).decode("ascii").rstrip("=")
|
|
430
|
+
# class-level token not included in __init__
|
|
431
|
+
_token: ClassVar[object] = object()
|
|
432
|
+
|
|
433
|
+
# Underscore cuz we may rename in the future, don't expose for now,
|
|
434
|
+
_image_registry_secret: Optional[Secret] = None
|
|
435
|
+
|
|
436
|
+
# check for the guard that we put in place
|
|
437
|
+
def __post_init__(self):
|
|
438
|
+
if object.__getattribute__(self, "__dict__").pop("_guard", None) is not Image._token:
|
|
439
|
+
raise TypeError(
|
|
440
|
+
"Direct instantiation of Image not allowed, please use one of the various from_...() methods instead"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# Private constructor for internal use only
|
|
444
|
+
@classmethod
|
|
445
|
+
def _new(cls, **kwargs) -> Image:
|
|
446
|
+
# call the normal __init__, injecting a private keyword that users won't know
|
|
447
|
+
obj = cls.__new__(cls) # allocate
|
|
448
|
+
# set guard to prevent direct construction
|
|
449
|
+
object.__setattr__(obj, "_guard", cls._token)
|
|
450
|
+
cls.__init__(obj, **kwargs) # run dataclass generated __init__
|
|
451
|
+
return obj
|
|
285
452
|
|
|
286
453
|
def validate(self):
|
|
287
454
|
for layer in self._layers:
|
|
288
455
|
layer.validate()
|
|
289
456
|
|
|
290
457
|
@classmethod
|
|
291
|
-
def _get_default_image_for(
|
|
458
|
+
def _get_default_image_for(
|
|
459
|
+
cls,
|
|
460
|
+
python_version: Tuple[int, int],
|
|
461
|
+
flyte_version: Optional[str] = None,
|
|
462
|
+
install_flyte: bool = True,
|
|
463
|
+
platform: Optional[Tuple[Architecture, ...]] = None,
|
|
464
|
+
) -> Image:
|
|
292
465
|
# Would love a way to move this outside of this class (but still needs to be accessible via Image.auto())
|
|
293
466
|
# this default image definition may need to be updated once there is a released pypi version
|
|
294
467
|
from flyte._version import __version__
|
|
295
468
|
|
|
296
|
-
dev_mode =
|
|
297
|
-
if
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
469
|
+
dev_mode = (__version__ and "dev" in __version__) and not flyte_version and install_flyte
|
|
470
|
+
if install_flyte is False:
|
|
471
|
+
preset_tag = f"py{python_version[0]}.{python_version[1]}"
|
|
472
|
+
else:
|
|
473
|
+
if flyte_version is None:
|
|
474
|
+
flyte_version = __version__.replace("+", "-")
|
|
475
|
+
suffix = flyte_version if flyte_version.startswith("v") else f"v{flyte_version}"
|
|
476
|
+
preset_tag = f"py{python_version[0]}.{python_version[1]}-{suffix}"
|
|
477
|
+
image = Image._new(
|
|
302
478
|
base_image=f"python:{python_version[0]}.{python_version[1]}-slim-bookworm",
|
|
303
479
|
registry=_BASE_REGISTRY,
|
|
304
480
|
name=_DEFAULT_IMAGE_NAME,
|
|
305
|
-
|
|
306
|
-
platform=("linux/amd64", "linux/arm64"),
|
|
481
|
+
python_version=python_version,
|
|
482
|
+
platform=("linux/amd64", "linux/arm64") if platform is None else platform,
|
|
307
483
|
)
|
|
308
484
|
labels_and_user = _DockerLines(
|
|
309
485
|
(
|
|
310
|
-
"LABEL org.opencontainers.image.authors='Union.AI <
|
|
311
|
-
"LABEL org.opencontainers.image.source=https://github.com/
|
|
486
|
+
"LABEL org.opencontainers.image.authors='Union.AI <info@union.ai>'",
|
|
487
|
+
"LABEL org.opencontainers.image.source=https://github.com/flyteorg/flyte",
|
|
312
488
|
"RUN useradd --create-home --shell /bin/bash flytekit &&"
|
|
313
489
|
" chown -R flytekit /root && chown -R flytekit /home",
|
|
314
490
|
"WORKDIR /root",
|
|
@@ -323,99 +499,80 @@ class Image:
|
|
|
323
499
|
"UV_LINK_MODE": "copy",
|
|
324
500
|
}
|
|
325
501
|
)
|
|
326
|
-
image = image.with_apt_packages(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
502
|
+
image = image.with_apt_packages("build-essential", "ca-certificates")
|
|
503
|
+
|
|
504
|
+
if install_flyte:
|
|
505
|
+
if dev_mode:
|
|
506
|
+
if os.path.exists(DIST_FOLDER):
|
|
507
|
+
image = image.with_local_v2()
|
|
508
|
+
else:
|
|
509
|
+
flyte_version = typing.cast(str, flyte_version)
|
|
510
|
+
if Version(flyte_version).is_devrelease or Version(flyte_version).is_prerelease:
|
|
511
|
+
image = image.with_pip_packages(f"flyte=={flyte_version}", pre=True)
|
|
512
|
+
else:
|
|
513
|
+
image = image.with_pip_packages(f"flyte=={flyte_version}")
|
|
514
|
+
if not dev_mode:
|
|
515
|
+
object.__setattr__(image, "_tag", preset_tag)
|
|
337
516
|
|
|
338
517
|
return image
|
|
339
518
|
|
|
340
|
-
@staticmethod
|
|
341
|
-
def _is_editable_install():
|
|
342
|
-
"""Internal hacky function to see if the current install is editable or not."""
|
|
343
|
-
curr = Path(__file__)
|
|
344
|
-
pyproject = curr.parent.parent.parent / "pyproject.toml"
|
|
345
|
-
return pyproject.exists()
|
|
346
|
-
|
|
347
|
-
@classmethod
|
|
348
|
-
def from_uv_debian(
|
|
349
|
-
cls,
|
|
350
|
-
registry: str,
|
|
351
|
-
name: str,
|
|
352
|
-
tag: Optional[str] = None,
|
|
353
|
-
python_version: Optional[Tuple[int, int]] = None,
|
|
354
|
-
arch: Union[Architecture, Tuple[Architecture, ...]] = "linux/amd64",
|
|
355
|
-
) -> Image:
|
|
356
|
-
"""
|
|
357
|
-
This creates a new debian-based base image.
|
|
358
|
-
If using the Union or docker builders, image will have uv available and a virtualenv created at /opt/venv.
|
|
359
|
-
|
|
360
|
-
:param registry: Registry to use for the image
|
|
361
|
-
:param name: Name of the image
|
|
362
|
-
:param tag: Tag to use for the image
|
|
363
|
-
:param python_version: Python version to use for the image
|
|
364
|
-
:param arch: Architecture to use for the image, default is linux/amd64
|
|
365
|
-
:return: Image
|
|
366
|
-
"""
|
|
367
|
-
base_image = "debian:bookworm-slim"
|
|
368
|
-
plat = arch if isinstance(arch, tuple) else (arch,)
|
|
369
|
-
if python_version is None:
|
|
370
|
-
python_version = _detect_python_version()
|
|
371
|
-
img = cls(
|
|
372
|
-
base_image=base_image, name=name, registry=registry, tag=tag, platform=plat, python_version=python_version
|
|
373
|
-
)
|
|
374
|
-
return img
|
|
375
|
-
|
|
376
519
|
@classmethod
|
|
377
|
-
def
|
|
520
|
+
def from_debian_base(
|
|
378
521
|
cls,
|
|
379
522
|
python_version: Optional[Tuple[int, int]] = None,
|
|
380
523
|
flyte_version: Optional[str] = None,
|
|
524
|
+
install_flyte: bool = True,
|
|
381
525
|
registry: Optional[str] = None,
|
|
526
|
+
registry_secret: Optional[str | Secret] = None,
|
|
382
527
|
name: Optional[str] = None,
|
|
528
|
+
platform: Optional[Tuple[Architecture, ...]] = None,
|
|
383
529
|
) -> Image:
|
|
384
530
|
"""
|
|
385
531
|
Use this method to start using the default base image, built from this library's base Dockerfile
|
|
386
532
|
Default images are multi-arch amd/arm64
|
|
387
533
|
|
|
388
534
|
:param python_version: If not specified, will use the current Python version
|
|
389
|
-
:param flyte_version:
|
|
535
|
+
:param flyte_version: Flyte version to use
|
|
536
|
+
:param install_flyte: If True, will install the flyte library in the image
|
|
390
537
|
:param registry: Registry to use for the image
|
|
538
|
+
:param registry_secret: Secret to use to pull/push the private image.
|
|
391
539
|
:param name: Name of the image if you want to override the default name
|
|
540
|
+
:param platform: Platform to use for the image, default is linux/amd64, use tuple for multiple values
|
|
541
|
+
Example: ("linux/amd64", "linux/arm64")
|
|
392
542
|
|
|
393
543
|
:return: Image
|
|
394
544
|
"""
|
|
395
545
|
if python_version is None:
|
|
396
546
|
python_version = _detect_python_version()
|
|
397
547
|
|
|
398
|
-
base_image = cls._get_default_image_for(
|
|
399
|
-
|
|
400
|
-
|
|
548
|
+
base_image = cls._get_default_image_for(
|
|
549
|
+
python_version=python_version,
|
|
550
|
+
flyte_version=flyte_version,
|
|
551
|
+
install_flyte=install_flyte,
|
|
552
|
+
platform=platform,
|
|
553
|
+
)
|
|
401
554
|
|
|
402
|
-
if registry
|
|
403
|
-
return base_image.clone(registry=registry, name=name)
|
|
555
|
+
if registry or name:
|
|
556
|
+
return base_image.clone(registry=registry, name=name, registry_secret=registry_secret)
|
|
404
557
|
|
|
405
|
-
# Set this to auto for all auto images because the meaning of "auto" can change (based on logic inside
|
|
406
|
-
# _get_default_image_for, acts differently in a running task container) so let's make sure it stays auto.
|
|
407
|
-
object.__setattr__(base_image, "_identifier_override", "auto")
|
|
408
558
|
return base_image
|
|
409
559
|
|
|
410
560
|
@classmethod
|
|
411
|
-
def
|
|
561
|
+
def from_base(cls, image_uri: str) -> Image:
|
|
412
562
|
"""
|
|
413
563
|
Use this method to start with a pre-built base image. This image must already exist in the registry of course.
|
|
414
564
|
|
|
415
565
|
:param image_uri: The full URI of the image, in the format <registry>/<name>:<tag>
|
|
416
566
|
:return:
|
|
417
567
|
"""
|
|
418
|
-
img = cls(base_image=image_uri)
|
|
568
|
+
img = cls._new(base_image=image_uri)
|
|
569
|
+
return img
|
|
570
|
+
|
|
571
|
+
@classmethod
|
|
572
|
+
def from_ref_name(cls, name: str = _DEFAULT_IMAGE_REF_NAME) -> Image:
|
|
573
|
+
# NOTE: set image name as _ref_name to enable adding additional layers.
|
|
574
|
+
# See: https://github.com/flyteorg/flyte-sdk/blob/14de802701aab7b8615ffb99c650a36305ef01f7/src/flyte/_image.py#L642
|
|
575
|
+
img = cls._new(name=name, _ref_name=name)
|
|
419
576
|
return img
|
|
420
577
|
|
|
421
578
|
@classmethod
|
|
@@ -425,8 +582,14 @@ class Image:
|
|
|
425
582
|
*,
|
|
426
583
|
name: str,
|
|
427
584
|
registry: str | None = None,
|
|
585
|
+
registry_secret: Optional[str | Secret] = None,
|
|
428
586
|
python_version: Optional[Tuple[int, int]] = None,
|
|
429
|
-
|
|
587
|
+
index_url: Optional[str] = None,
|
|
588
|
+
extra_index_urls: Union[str, List[str], Tuple[str, ...], None] = None,
|
|
589
|
+
pre: bool = False,
|
|
590
|
+
extra_args: Optional[str] = None,
|
|
591
|
+
platform: Optional[Tuple[Architecture, ...]] = None,
|
|
592
|
+
secret_mounts: Optional[SecretRequest] = None,
|
|
430
593
|
) -> Image:
|
|
431
594
|
"""
|
|
432
595
|
Use this method to create a new image with the specified uv script.
|
|
@@ -444,78 +607,127 @@ class Image:
|
|
|
444
607
|
```
|
|
445
608
|
|
|
446
609
|
For more information on the uv script format, see the documentation:
|
|
447
|
-
|
|
448
|
-
UV: Declaring script dependencies</href>
|
|
610
|
+
[UV: Declaring script dependencies](https://docs.astral.sh/uv/guides/scripts/#declaring-script-dependencies)
|
|
449
611
|
|
|
450
612
|
:param name: name of the image
|
|
451
613
|
:param registry: registry to use for the image
|
|
614
|
+
:param registry_secret: Secret to use to pull/push the private image.
|
|
615
|
+
:param python_version: Python version to use for the image, if not specified, will use the current Python
|
|
616
|
+
version
|
|
452
617
|
:param script: path to the uv script
|
|
453
|
-
:param
|
|
618
|
+
:param platform: architecture to use for the image, default is linux/amd64, use tuple for multiple values
|
|
619
|
+
:param python_version: Python version for the image, if not specified, will use the current Python version
|
|
620
|
+
:param index_url: index url to use for pip install, default is None
|
|
621
|
+
:param extra_index_urls: extra index urls to use for pip install, default is True
|
|
622
|
+
:param pre: whether to allow pre-release versions, default is False
|
|
623
|
+
:param extra_args: extra arguments to pass to pip install, default is None
|
|
624
|
+
:param secret_mounts: Secret mounts to use for the image, default is None.
|
|
454
625
|
|
|
455
626
|
:return: Image
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
secret_mounts:
|
|
456
630
|
"""
|
|
457
|
-
|
|
631
|
+
ll = UVScript(
|
|
632
|
+
script=Path(script),
|
|
633
|
+
index_url=index_url,
|
|
634
|
+
extra_index_urls=_ensure_tuple(extra_index_urls) if extra_index_urls else None,
|
|
635
|
+
pre=pre,
|
|
636
|
+
extra_args=extra_args,
|
|
637
|
+
secret_mounts=_ensure_tuple(secret_mounts) if secret_mounts else None,
|
|
638
|
+
)
|
|
458
639
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
raise ValueError("registry must be specified")
|
|
470
|
-
img = cls.from_uv_debian(registry=registry, name=name, arch=arch, python_version=python_version)
|
|
471
|
-
if header.dependencies:
|
|
472
|
-
return img.with_pip_packages(header.dependencies)
|
|
473
|
-
# todo: override the _identifier_override to be the script name or a hash of the script contents
|
|
474
|
-
# This is needed because inside the image, the identifier will be computed to be something different.
|
|
475
|
-
return img
|
|
640
|
+
img = cls.from_debian_base(
|
|
641
|
+
registry=registry,
|
|
642
|
+
registry_secret=registry_secret,
|
|
643
|
+
install_flyte=False,
|
|
644
|
+
name=name,
|
|
645
|
+
python_version=python_version,
|
|
646
|
+
platform=platform,
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
return img.clone(addl_layer=ll)
|
|
476
650
|
|
|
477
651
|
def clone(
|
|
478
|
-
self,
|
|
652
|
+
self,
|
|
653
|
+
registry: Optional[str] = None,
|
|
654
|
+
registry_secret: Optional[str | Secret] = None,
|
|
655
|
+
name: Optional[str] = None,
|
|
656
|
+
base_image: Optional[str] = None,
|
|
657
|
+
python_version: Optional[Tuple[int, int]] = None,
|
|
658
|
+
addl_layer: Optional[Layer] = None,
|
|
479
659
|
) -> Image:
|
|
480
660
|
"""
|
|
481
661
|
Use this method to clone the current image and change the registry and name
|
|
482
662
|
|
|
483
663
|
:param registry: Registry to use for the image
|
|
664
|
+
:param registry_secret: Secret to use to pull/push the private image.
|
|
484
665
|
:param name: Name of the image
|
|
485
|
-
|
|
666
|
+
:param python_version: Python version for the image, if not specified, will use the current Python version
|
|
667
|
+
:param addl_layer: Additional layer to add to the image. This will be added to the end of the layers.
|
|
486
668
|
:return:
|
|
487
669
|
"""
|
|
670
|
+
from flyte import Secret
|
|
671
|
+
|
|
672
|
+
if addl_layer and self.dockerfile:
|
|
673
|
+
# We don't know how to inspect dockerfiles to know what kind it is (OS, python version, uv vs poetry, etc)
|
|
674
|
+
# so there's no guarantee any of the layering logic will work.
|
|
675
|
+
raise ValueError(
|
|
676
|
+
"Flyte current cannot add additional layers to a Dockerfile-based Image."
|
|
677
|
+
" Please amend the dockerfile directly."
|
|
678
|
+
)
|
|
488
679
|
registry = registry if registry else self.registry
|
|
489
680
|
name = name if name else self.name
|
|
681
|
+
registry_secret = registry_secret if registry_secret else self._image_registry_secret
|
|
682
|
+
base_image = base_image if base_image else self.base_image
|
|
683
|
+
if addl_layer and (not name):
|
|
684
|
+
raise ValueError(
|
|
685
|
+
f"Cannot add additional layer {addl_layer} to an image without name. Please first clone()."
|
|
686
|
+
)
|
|
490
687
|
new_layers = (*self._layers, addl_layer) if addl_layer else self._layers
|
|
491
|
-
img = Image(
|
|
492
|
-
base_image=
|
|
688
|
+
img = Image._new(
|
|
689
|
+
base_image=base_image,
|
|
493
690
|
dockerfile=self.dockerfile,
|
|
494
691
|
registry=registry,
|
|
495
692
|
name=name,
|
|
496
|
-
tag=self.tag,
|
|
497
693
|
platform=self.platform,
|
|
498
|
-
python_version=self.python_version,
|
|
499
|
-
is_final=self.is_final,
|
|
694
|
+
python_version=python_version or self.python_version,
|
|
500
695
|
_layers=new_layers,
|
|
696
|
+
_image_registry_secret=Secret(key=registry_secret) if isinstance(registry_secret, str) else registry_secret,
|
|
697
|
+
_ref_name=self._ref_name,
|
|
501
698
|
)
|
|
502
699
|
|
|
503
700
|
return img
|
|
504
701
|
|
|
505
702
|
@classmethod
|
|
506
|
-
def from_dockerfile(
|
|
703
|
+
def from_dockerfile(
|
|
704
|
+
cls, file: Path, registry: str, name: str, platform: Union[Architecture, Tuple[Architecture, ...], None] = None
|
|
705
|
+
) -> Image:
|
|
507
706
|
"""
|
|
508
|
-
Use this method to create a new image with the specified dockerfile
|
|
707
|
+
Use this method to create a new image with the specified dockerfile. Note you cannot use additional layers
|
|
708
|
+
after this, as the system doesn't attempt to parse/understand the Dockerfile, and what kind of setup it has
|
|
709
|
+
(python version, uv vs poetry, etc), so please put all logic into the dockerfile itself.
|
|
710
|
+
|
|
711
|
+
Also since Python sees paths as from the calling directory, please use Path objects with absolute paths. The
|
|
712
|
+
context for the builder will be the directory where the dockerfile is located.
|
|
509
713
|
|
|
510
714
|
:param file: path to the dockerfile
|
|
511
715
|
:param name: name of the image
|
|
512
716
|
:param registry: registry to use for the image
|
|
513
|
-
:param
|
|
717
|
+
:param platform: architecture to use for the image, default is linux/amd64, use tuple for multiple values
|
|
718
|
+
Example: ("linux/amd64", "linux/arm64")
|
|
514
719
|
|
|
515
720
|
:return:
|
|
516
721
|
"""
|
|
517
|
-
|
|
518
|
-
|
|
722
|
+
platform = _ensure_tuple(platform) if platform else None
|
|
723
|
+
kwargs = {
|
|
724
|
+
"dockerfile": file,
|
|
725
|
+
"registry": registry,
|
|
726
|
+
"name": name,
|
|
727
|
+
}
|
|
728
|
+
if platform:
|
|
729
|
+
kwargs["platform"] = platform
|
|
730
|
+
img = cls._new(**kwargs)
|
|
519
731
|
|
|
520
732
|
return img
|
|
521
733
|
|
|
@@ -528,6 +740,8 @@ class Image:
|
|
|
528
740
|
from ._utils import filehash_update
|
|
529
741
|
|
|
530
742
|
hasher = hashlib.md5()
|
|
743
|
+
if self.base_image:
|
|
744
|
+
hasher.update(self.base_image.encode("utf-8"))
|
|
531
745
|
if self.dockerfile:
|
|
532
746
|
# Note the location of the dockerfile shouldn't matter, only the contents
|
|
533
747
|
filehash_update(self.dockerfile, hasher)
|
|
@@ -538,7 +752,7 @@ class Image:
|
|
|
538
752
|
|
|
539
753
|
@property
|
|
540
754
|
def _final_tag(self) -> str:
|
|
541
|
-
t = self.
|
|
755
|
+
t = self._tag if self._tag else self._get_hash_digest()
|
|
542
756
|
return t or "latest"
|
|
543
757
|
|
|
544
758
|
@cached_property
|
|
@@ -549,6 +763,9 @@ class Image:
|
|
|
549
763
|
if self.registry and self.name:
|
|
550
764
|
tag = self._final_tag
|
|
551
765
|
return f"{self.registry}/{self.name}:{tag}"
|
|
766
|
+
elif self._ref_name and len(self._layers) == 0:
|
|
767
|
+
assert self.base_image is not None, f"Base image is not set for image ref name {self._ref_name}"
|
|
768
|
+
return self.base_image
|
|
552
769
|
elif self.name:
|
|
553
770
|
return f"{self.name}:{self._final_tag}"
|
|
554
771
|
elif self.base_image:
|
|
@@ -567,30 +784,36 @@ class Image:
|
|
|
567
784
|
new_image = self.clone(addl_layer=WorkDir(workdir=workdir))
|
|
568
785
|
return new_image
|
|
569
786
|
|
|
570
|
-
def with_requirements(
|
|
787
|
+
def with_requirements(
|
|
788
|
+
self,
|
|
789
|
+
file: str | Path,
|
|
790
|
+
secret_mounts: Optional[SecretRequest] = None,
|
|
791
|
+
) -> Image:
|
|
571
792
|
"""
|
|
572
793
|
Use this method to create a new image with the specified requirements file layered on top of the current image
|
|
573
794
|
Cannot be used in conjunction with conda
|
|
574
795
|
|
|
575
796
|
:param file: path to the requirements file, must be a .txt file
|
|
797
|
+
:param secret_mounts: list of secret to mount for the build process.
|
|
576
798
|
:return:
|
|
577
799
|
"""
|
|
578
|
-
if
|
|
579
|
-
|
|
580
|
-
if not file.is_file():
|
|
581
|
-
raise ValueError(f"Requirements file {file} is not a file")
|
|
800
|
+
if isinstance(file, str):
|
|
801
|
+
file = Path(file)
|
|
582
802
|
if file.suffix != ".txt":
|
|
583
803
|
raise ValueError(f"Requirements file {file} must have a .txt extension")
|
|
584
|
-
new_image = self.clone(
|
|
804
|
+
new_image = self.clone(
|
|
805
|
+
addl_layer=Requirements(file=file, secret_mounts=_ensure_tuple(secret_mounts) if secret_mounts else None)
|
|
806
|
+
)
|
|
585
807
|
return new_image
|
|
586
808
|
|
|
587
809
|
def with_pip_packages(
|
|
588
810
|
self,
|
|
589
|
-
packages:
|
|
811
|
+
*packages: str,
|
|
590
812
|
index_url: Optional[str] = None,
|
|
591
813
|
extra_index_urls: Union[str, List[str], Tuple[str, ...], None] = None,
|
|
592
814
|
pre: bool = False,
|
|
593
815
|
extra_args: Optional[str] = None,
|
|
816
|
+
secret_mounts: Optional[SecretRequest] = None,
|
|
594
817
|
) -> Image:
|
|
595
818
|
"""
|
|
596
819
|
Use this method to create a new image with the specified pip packages layered on top of the current image
|
|
@@ -598,9 +821,24 @@ class Image:
|
|
|
598
821
|
|
|
599
822
|
Example:
|
|
600
823
|
```python
|
|
601
|
-
@flyte.task(image=(flyte.Image
|
|
602
|
-
|
|
603
|
-
|
|
824
|
+
@flyte.task(image=(flyte.Image.from_debian_base().with_pip_packages("requests", "numpy")))
|
|
825
|
+
def my_task(x: int) -> int:
|
|
826
|
+
import numpy as np
|
|
827
|
+
return np.sum([x, 1])
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
To mount secrets during the build process to download private packages, you can use the `secret_mounts`.
|
|
831
|
+
In the below example, "GITHUB_PAT" will be mounted as env var "GITHUB_PAT",
|
|
832
|
+
and "apt-secret" will be mounted at /etc/apt/apt-secret.
|
|
833
|
+
Example:
|
|
834
|
+
```python
|
|
835
|
+
private_package = "git+https://$GITHUB_PAT@github.com/flyteorg/flytex.git@2e20a2acebfc3877d84af643fdd768edea41d533"
|
|
836
|
+
@flyte.task(
|
|
837
|
+
image=(
|
|
838
|
+
flyte.Image.from_debian_base()
|
|
839
|
+
.with_pip_packages("private_package", secret_mounts=[Secret(key="GITHUB_PAT")])
|
|
840
|
+
.with_apt_packages("git", secret_mounts=[Secret(key="apt-secret", mount="/etc/apt/apt-secret")])
|
|
841
|
+
)
|
|
604
842
|
def my_task(x: int) -> int:
|
|
605
843
|
import numpy as np
|
|
606
844
|
return np.sum([x, 1])
|
|
@@ -611,12 +849,10 @@ class Image:
|
|
|
611
849
|
:param extra_index_urls: extra index urls to use for pip install, default is None
|
|
612
850
|
:param pre: whether to allow pre-release versions, default is False
|
|
613
851
|
:param extra_args: extra arguments to pass to pip install, default is None
|
|
614
|
-
|
|
615
|
-
:param extra_args: extra arguments to pass to pip install, default is None
|
|
852
|
+
:param secret_mounts: list of secret to mount for the build process.
|
|
616
853
|
:return: Image
|
|
617
854
|
"""
|
|
618
|
-
|
|
619
|
-
new_packages: Optional[Tuple] = _ensure_tuple(packages) if packages else None
|
|
855
|
+
new_packages: Optional[Tuple] = packages or None
|
|
620
856
|
new_extra_index_urls: Optional[Tuple] = _ensure_tuple(extra_index_urls) if extra_index_urls else None
|
|
621
857
|
|
|
622
858
|
ll = PipPackages(
|
|
@@ -625,6 +861,7 @@ class Image:
|
|
|
625
861
|
extra_index_urls=new_extra_index_urls,
|
|
626
862
|
pre=pre,
|
|
627
863
|
extra_args=extra_args,
|
|
864
|
+
secret_mounts=_ensure_tuple(secret_mounts) if secret_mounts else None,
|
|
628
865
|
)
|
|
629
866
|
new_image = self.clone(addl_layer=ll)
|
|
630
867
|
return new_image
|
|
@@ -640,73 +877,161 @@ class Image:
|
|
|
640
877
|
new_image = self.clone(addl_layer=Env.from_dict(env_vars))
|
|
641
878
|
return new_image
|
|
642
879
|
|
|
643
|
-
def with_source_folder(self,
|
|
880
|
+
def with_source_folder(self, src: Path, dst: str = ".", copy_contents_only: bool = False) -> Image:
|
|
644
881
|
"""
|
|
645
882
|
Use this method to create a new image with the specified local directory layered on top of the current image.
|
|
646
883
|
If dest is not specified, it will be copied to the working directory of the image
|
|
647
884
|
|
|
648
|
-
:param
|
|
649
|
-
:param
|
|
885
|
+
:param src: root folder of the source code from the build context to be copied
|
|
886
|
+
:param dst: destination folder in the image
|
|
887
|
+
:param copy_contents_only: If True, will copy the contents of the source folder to the destination folder,
|
|
888
|
+
instead of the folder itself. Default is False.
|
|
650
889
|
:return: Image
|
|
651
890
|
"""
|
|
652
|
-
|
|
653
|
-
|
|
891
|
+
if not copy_contents_only:
|
|
892
|
+
dst = str("./" + src.name) if dst == "." else dst
|
|
893
|
+
new_image = self.clone(addl_layer=CopyConfig(path_type=1, src=src, dst=dst))
|
|
654
894
|
return new_image
|
|
655
895
|
|
|
656
|
-
def with_source_file(self,
|
|
896
|
+
def with_source_file(self, src: Path, dst: str = ".") -> Image:
|
|
657
897
|
"""
|
|
658
898
|
Use this method to create a new image with the specified local file layered on top of the current image.
|
|
659
899
|
If dest is not specified, it will be copied to the working directory of the image
|
|
660
900
|
|
|
661
|
-
:param
|
|
662
|
-
:param
|
|
901
|
+
:param src: root folder of the source code from the build context to be copied
|
|
902
|
+
:param dst: destination folder in the image
|
|
663
903
|
:return: Image
|
|
664
904
|
"""
|
|
665
|
-
|
|
666
|
-
new_image = self.clone(addl_layer=CopyConfig(path_type=0, context_source=context_source, image_dest=image_dest))
|
|
905
|
+
new_image = self.clone(addl_layer=CopyConfig(path_type=0, src=src, dst=dst))
|
|
667
906
|
return new_image
|
|
668
907
|
|
|
669
|
-
def
|
|
908
|
+
def with_dockerignore(self, path: Path) -> Image:
|
|
909
|
+
new_image = self.clone(addl_layer=DockerIgnore(path=str(path)))
|
|
910
|
+
return new_image
|
|
911
|
+
|
|
912
|
+
def with_uv_project(
|
|
913
|
+
self,
|
|
914
|
+
pyproject_file: str | Path,
|
|
915
|
+
uvlock: Path | None = None,
|
|
916
|
+
index_url: Optional[str] = None,
|
|
917
|
+
extra_index_urls: Union[List[str], Tuple[str, ...], None] = None,
|
|
918
|
+
pre: bool = False,
|
|
919
|
+
extra_args: Optional[str] = None,
|
|
920
|
+
secret_mounts: Optional[SecretRequest] = None,
|
|
921
|
+
project_install_mode: typing.Literal["dependencies_only", "install_project"] = "dependencies_only",
|
|
922
|
+
) -> Image:
|
|
670
923
|
"""
|
|
671
924
|
Use this method to create a new image with the specified uv.lock file layered on top of the current image
|
|
672
925
|
Must have a corresponding pyproject.toml file in the same directory
|
|
673
926
|
Cannot be used in conjunction with conda
|
|
674
|
-
|
|
927
|
+
|
|
928
|
+
By default, this method copies the pyproject.toml and uv.lock files into the image.
|
|
929
|
+
|
|
930
|
+
If `project_install_mode` is "install_project", it will also copy directory
|
|
931
|
+
where the pyproject.toml file is located into the image.
|
|
675
932
|
|
|
676
933
|
:param pyproject_file: path to the pyproject.toml file, needs to have a corresponding uv.lock file
|
|
677
|
-
:
|
|
934
|
+
:param uvlock: path to the uv.lock file, if not specified, will use the default uv.lock file in the same
|
|
935
|
+
directory as the pyproject.toml file. (pyproject.parent / uv.lock)
|
|
936
|
+
:param index_url: index url to use for pip install, default is None
|
|
937
|
+
:param extra_index_urls: extra index urls to use for pip install, default is None
|
|
938
|
+
:param pre: whether to allow pre-release versions, default is False
|
|
939
|
+
:param extra_args: extra arguments to pass to pip install, default is None
|
|
940
|
+
:param secret_mounts: list of secret mounts to use for the build process.
|
|
941
|
+
:param project_install_mode: whether to install the project as a package or
|
|
942
|
+
only dependencies, default is "dependencies_only"
|
|
943
|
+
:return: Image
|
|
678
944
|
"""
|
|
679
|
-
if
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
945
|
+
if isinstance(pyproject_file, str):
|
|
946
|
+
pyproject_file = Path(pyproject_file)
|
|
947
|
+
new_image = self.clone(
|
|
948
|
+
addl_layer=UVProject(
|
|
949
|
+
pyproject=pyproject_file,
|
|
950
|
+
uvlock=uvlock or (pyproject_file.parent / "uv.lock"),
|
|
951
|
+
index_url=index_url,
|
|
952
|
+
extra_index_urls=extra_index_urls,
|
|
953
|
+
pre=pre,
|
|
954
|
+
extra_args=extra_args,
|
|
955
|
+
secret_mounts=_ensure_tuple(secret_mounts) if secret_mounts else None,
|
|
956
|
+
project_install_mode=project_install_mode,
|
|
957
|
+
)
|
|
958
|
+
)
|
|
687
959
|
return new_image
|
|
688
960
|
|
|
689
|
-
def
|
|
961
|
+
def with_poetry_project(
|
|
962
|
+
self,
|
|
963
|
+
pyproject_file: str | Path,
|
|
964
|
+
poetry_lock: Path | None = None,
|
|
965
|
+
extra_args: Optional[str] = None,
|
|
966
|
+
secret_mounts: Optional[SecretRequest] = None,
|
|
967
|
+
project_install_mode: typing.Literal["dependencies_only", "install_project"] = "dependencies_only",
|
|
968
|
+
):
|
|
969
|
+
"""
|
|
970
|
+
Use this method to create a new image with the specified pyproject.toml layered on top of the current image.
|
|
971
|
+
Must have a corresponding pyproject.toml file in the same directory.
|
|
972
|
+
Cannot be used in conjunction with conda.
|
|
973
|
+
|
|
974
|
+
By default, this method copies the entire project into the image,
|
|
975
|
+
including files such as pyproject.toml, poetry.lock, and the src/ directory.
|
|
976
|
+
|
|
977
|
+
If you prefer not to install the current project, you can pass through `extra_args`
|
|
978
|
+
`--no-root`. In this case, the image builder will only copy pyproject.toml and poetry.lock
|
|
979
|
+
into the image.
|
|
980
|
+
|
|
981
|
+
:param pyproject_file: Path to the pyproject.toml file. A poetry.lock file must exist in the same directory
|
|
982
|
+
unless `poetry_lock` is explicitly provided.
|
|
983
|
+
:param poetry_lock: Path to the poetry.lock file. If not specified, the default is the file named
|
|
984
|
+
'poetry.lock' in the same directory as `pyproject_file` (pyproject.parent / "poetry.lock").
|
|
985
|
+
:param extra_args: Extra arguments to pass through to the package installer/resolver, default is None.
|
|
986
|
+
:param secret_mounts: Secrets to make available during dependency resolution/build (e.g., private indexes).
|
|
987
|
+
:param project_install_mode: whether to install the project as a package or
|
|
988
|
+
only dependencies, default is "dependencies_only"
|
|
989
|
+
:return: Image
|
|
990
|
+
"""
|
|
991
|
+
if isinstance(pyproject_file, str):
|
|
992
|
+
pyproject_file = Path(pyproject_file)
|
|
993
|
+
new_image = self.clone(
|
|
994
|
+
addl_layer=PoetryProject(
|
|
995
|
+
pyproject=pyproject_file,
|
|
996
|
+
poetry_lock=poetry_lock or (pyproject_file.parent / "poetry.lock"),
|
|
997
|
+
extra_args=extra_args,
|
|
998
|
+
secret_mounts=_ensure_tuple(secret_mounts) if secret_mounts else None,
|
|
999
|
+
project_install_mode=project_install_mode,
|
|
1000
|
+
)
|
|
1001
|
+
)
|
|
1002
|
+
return new_image
|
|
1003
|
+
|
|
1004
|
+
def with_apt_packages(self, *packages: str, secret_mounts: Optional[SecretRequest] = None) -> Image:
|
|
690
1005
|
"""
|
|
691
1006
|
Use this method to create a new image with the specified apt packages layered on top of the current image
|
|
692
1007
|
|
|
693
1008
|
:param packages: list of apt packages to install
|
|
1009
|
+
:param secret_mounts: list of secret mounts to use for the build process.
|
|
694
1010
|
:return: Image
|
|
695
1011
|
"""
|
|
696
|
-
|
|
697
|
-
|
|
1012
|
+
new_image = self.clone(
|
|
1013
|
+
addl_layer=AptPackages(
|
|
1014
|
+
packages=packages,
|
|
1015
|
+
secret_mounts=_ensure_tuple(secret_mounts) if secret_mounts else None,
|
|
1016
|
+
)
|
|
1017
|
+
)
|
|
698
1018
|
return new_image
|
|
699
1019
|
|
|
700
|
-
def with_commands(self, commands: List[str]) -> Image:
|
|
1020
|
+
def with_commands(self, commands: List[str], secret_mounts: Optional[SecretRequest] = None) -> Image:
|
|
701
1021
|
"""
|
|
702
1022
|
Use this method to create a new image with the specified commands layered on top of the current image
|
|
703
1023
|
Be sure not to use RUN in your command.
|
|
704
1024
|
|
|
705
1025
|
:param commands: list of commands to run
|
|
1026
|
+
:param secret_mounts: list of secret mounts to use for the build process.
|
|
706
1027
|
:return: Image
|
|
707
1028
|
"""
|
|
708
1029
|
new_commands: Tuple = _ensure_tuple(commands)
|
|
709
|
-
new_image = self.clone(
|
|
1030
|
+
new_image = self.clone(
|
|
1031
|
+
addl_layer=Commands(
|
|
1032
|
+
commands=new_commands, secret_mounts=_ensure_tuple(secret_mounts) if secret_mounts else None
|
|
1033
|
+
)
|
|
1034
|
+
)
|
|
710
1035
|
return new_image
|
|
711
1036
|
|
|
712
1037
|
def with_local_v2(self) -> Image:
|
|
@@ -716,23 +1041,11 @@ class Image:
|
|
|
716
1041
|
|
|
717
1042
|
:return: Image
|
|
718
1043
|
"""
|
|
719
|
-
|
|
720
|
-
# Manually declare the CopyConfig instead of using with_source_folder so we can set the hashing
|
|
1044
|
+
# Manually declare the PythonWheel so we can set the hashing
|
|
721
1045
|
# used to compute the identifier. Can remove if we ever decide to expose the lambda in with_ commands
|
|
722
|
-
with_dist = self.clone(
|
|
723
|
-
addl_layer=CopyConfig(
|
|
724
|
-
path_type=1, context_source=dist_folder, image_dest=".", _compute_identifier=lambda x: "/dist"
|
|
725
|
-
)
|
|
726
|
-
)
|
|
1046
|
+
with_dist = self.clone(addl_layer=PythonWheels(wheel_dir=DIST_FOLDER, package_name="flyte", pre=True))
|
|
727
1047
|
|
|
728
|
-
return with_dist
|
|
729
|
-
[
|
|
730
|
-
"--mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv"
|
|
731
|
-
" --mount=from=uv,source=/uv,target=/usr/bin/uv"
|
|
732
|
-
" --mount=source=dist,target=/dist,type=bind"
|
|
733
|
-
" uv pip install --python $VIRTUALENV $(ls /dist/*whl)"
|
|
734
|
-
]
|
|
735
|
-
)
|
|
1048
|
+
return with_dist
|
|
736
1049
|
|
|
737
1050
|
def __img_str__(self) -> str:
|
|
738
1051
|
"""
|