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.

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