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
@@ -0,0 +1,706 @@
1
+ import asyncio
2
+ import os
3
+ import shutil
4
+ import subprocess
5
+ import tempfile
6
+ import typing
7
+ from pathlib import Path
8
+ from string import Template
9
+ from typing import ClassVar, Optional, Protocol, cast
10
+
11
+ import aiofiles
12
+ import click
13
+
14
+ from flyte import Secret
15
+ from flyte._image import (
16
+ AptPackages,
17
+ Commands,
18
+ CopyConfig,
19
+ DockerIgnore,
20
+ Env,
21
+ Image,
22
+ Layer,
23
+ PipOption,
24
+ PipPackages,
25
+ PoetryProject,
26
+ PythonWheels,
27
+ Requirements,
28
+ UVProject,
29
+ UVScript,
30
+ WorkDir,
31
+ _DockerLines,
32
+ _ensure_tuple,
33
+ )
34
+ from flyte._internal.imagebuild.image_builder import (
35
+ DockerAPIImageChecker,
36
+ ImageBuilder,
37
+ ImageChecker,
38
+ LocalDockerCommandImageChecker,
39
+ LocalPodmanCommandImageChecker,
40
+ )
41
+ from flyte._internal.imagebuild.utils import copy_files_to_context, get_and_list_dockerignore
42
+ from flyte._logging import logger
43
+
44
+ _F_IMG_ID = "_F_IMG_ID"
45
+ FLYTE_DOCKER_BUILDER_CACHE_FROM = "FLYTE_DOCKER_BUILDER_CACHE_FROM"
46
+ FLYTE_DOCKER_BUILDER_CACHE_TO = "FLYTE_DOCKER_BUILDER_CACHE_TO"
47
+
48
+ UV_LOCK_WITHOUT_PROJECT_INSTALL_TEMPLATE = Template("""\
49
+ RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
50
+ --mount=type=bind,target=uv.lock,src=$UV_LOCK_PATH,rw \
51
+ --mount=type=bind,target=pyproject.toml,src=$PYPROJECT_PATH \
52
+ $SECRET_MOUNT \
53
+ uv sync --active --inexact $PIP_INSTALL_ARGS
54
+ """)
55
+
56
+ UV_LOCK_INSTALL_TEMPLATE = Template("""\
57
+ RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
58
+ --mount=type=bind,target=/root/.flyte/$PYPROJECT_PATH,src=$PYPROJECT_PATH,rw \
59
+ $SECRET_MOUNT \
60
+ uv sync --active --inexact --no-editable $PIP_INSTALL_ARGS --project /root/.flyte/$PYPROJECT_PATH
61
+ """)
62
+
63
+ POETRY_LOCK_WITHOUT_PROJECT_INSTALL_TEMPLATE = Template("""\
64
+ RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
65
+ uv pip install poetry
66
+
67
+ ENV POETRY_CACHE_DIR=/tmp/poetry_cache \
68
+ POETRY_VIRTUALENVS_IN_PROJECT=true
69
+
70
+ RUN --mount=type=cache,sharing=locked,mode=0777,target=/tmp/poetry_cache,id=poetry \
71
+ --mount=type=bind,target=poetry.lock,src=$POETRY_LOCK_PATH \
72
+ --mount=type=bind,target=pyproject.toml,src=$PYPROJECT_PATH \
73
+ $SECRET_MOUNT \
74
+ poetry install $POETRY_INSTALL_ARGS
75
+ """)
76
+
77
+ POETRY_LOCK_INSTALL_TEMPLATE = Template("""\
78
+ RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
79
+ uv pip install poetry
80
+
81
+ ENV POETRY_CACHE_DIR=/tmp/poetry_cache \
82
+ POETRY_VIRTUALENVS_IN_PROJECT=true
83
+
84
+ RUN --mount=type=cache,sharing=locked,mode=0777,target=/tmp/poetry_cache,id=poetry \
85
+ --mount=type=bind,target=/root/.flyte/$PYPROJECT_PATH,src=$PYPROJECT_PATH,rw \
86
+ $SECRET_MOUNT \
87
+ poetry install $POETRY_INSTALL_ARGS -C /root/.flyte/$PYPROJECT_PATH
88
+ """)
89
+
90
+ UV_PACKAGE_INSTALL_COMMAND_TEMPLATE = Template("""\
91
+ RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
92
+ $REQUIREMENTS_MOUNT \
93
+ $SECRET_MOUNT \
94
+ uv pip install --python $$UV_PYTHON $PIP_INSTALL_ARGS
95
+ """)
96
+
97
+ UV_WHEEL_INSTALL_COMMAND_TEMPLATE = Template("""\
98
+ RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=wheel \
99
+ --mount=source=/dist,target=/dist,type=bind \
100
+ $SECRET_MOUNT \
101
+ uv pip install --python $$UV_PYTHON $PIP_INSTALL_ARGS
102
+ """)
103
+
104
+ APT_INSTALL_COMMAND_TEMPLATE = Template("""\
105
+ RUN --mount=type=cache,sharing=locked,mode=0777,target=/var/cache/apt,id=apt \
106
+ $SECRET_MOUNT \
107
+ apt-get update && apt-get install -y --no-install-recommends \
108
+ $APT_PACKAGES
109
+ """)
110
+
111
+ UV_PYTHON_INSTALL_COMMAND = Template("""\
112
+ RUN --mount=type=cache,sharing=locked,mode=0777,target=/root/.cache/uv,id=uv \
113
+ $SECRET_MOUNT \
114
+ uv pip install $PIP_INSTALL_ARGS
115
+ """)
116
+
117
+ # uv pip install --python /root/env/bin/python
118
+ # new template
119
+ DOCKER_FILE_UV_BASE_TEMPLATE = Template("""\
120
+ # syntax=docker/dockerfile:1.10
121
+ FROM ghcr.io/astral-sh/uv:0.8.13 AS uv
122
+ FROM $BASE_IMAGE
123
+
124
+
125
+ USER root
126
+
127
+
128
+ # Copy in uv so that later commands don't have to mount it in
129
+ COPY --from=uv /uv /usr/bin/uv
130
+
131
+
132
+ # Configure default envs
133
+ ENV UV_COMPILE_BYTECODE=1 \
134
+ UV_LINK_MODE=copy \
135
+ VIRTUALENV=/opt/venv \
136
+ UV_PYTHON=/opt/venv/bin/python \
137
+ PATH="/opt/venv/bin:$$PATH"
138
+
139
+
140
+ # Create a virtualenv with the user specified python version
141
+ RUN uv venv $$VIRTUALENV --python=$PYTHON_VERSION
142
+
143
+
144
+ # Adds nvidia just in case it exists
145
+ ENV PATH="$$PATH:/usr/local/nvidia/bin:/usr/local/cuda/bin" \
146
+ LD_LIBRARY_PATH="/usr/local/nvidia/lib64"
147
+ """)
148
+
149
+ # This gets added on to the end of the dockerfile
150
+ DOCKER_FILE_BASE_FOOTER = Template("""\
151
+ ENV _F_IMG_ID=$F_IMG_ID
152
+ WORKDIR /root
153
+ SHELL ["/bin/bash", "-c"]
154
+ """)
155
+
156
+
157
+ class Handler(Protocol):
158
+ @staticmethod
159
+ async def handle(layer: Layer, context_path: Path, dockerfile: str) -> str: ...
160
+
161
+
162
+ class PipAndRequirementsHandler:
163
+ @staticmethod
164
+ async def handle(layer: PipPackages, context_path: Path, dockerfile: str) -> str:
165
+ secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
166
+
167
+ # Set pip_install_args based on the layer type - either a requirements file or a list of packages
168
+ if isinstance(layer, Requirements):
169
+ if not layer.file.exists():
170
+ raise FileNotFoundError(f"Requirements file {layer.file} does not exist")
171
+ if not layer.file.is_file():
172
+ raise ValueError(f"Requirements file {layer.file} is not a file")
173
+
174
+ # Copy the requirements file to the context path
175
+ requirements_path = copy_files_to_context(layer.file, context_path)
176
+ rel_path = str(requirements_path.relative_to(context_path))
177
+ pip_install_args = layer.get_pip_install_args()
178
+ pip_install_args.extend(["--requirement", "requirements.txt"])
179
+ mount = f"--mount=type=bind,target=requirements.txt,src={rel_path}"
180
+ else:
181
+ mount = ""
182
+ requirements = list(layer.packages) if layer.packages else []
183
+ reqs = " ".join(requirements)
184
+ pip_install_args = layer.get_pip_install_args()
185
+ pip_install_args.append(reqs)
186
+
187
+ delta = UV_PACKAGE_INSTALL_COMMAND_TEMPLATE.substitute(
188
+ SECRET_MOUNT=secret_mounts,
189
+ REQUIREMENTS_MOUNT=mount,
190
+ PIP_INSTALL_ARGS=" ".join(pip_install_args),
191
+ )
192
+
193
+ dockerfile += delta
194
+
195
+ return dockerfile
196
+
197
+
198
+ class PythonWheelHandler:
199
+ @staticmethod
200
+ async def handle(layer: PythonWheels, context_path: Path, dockerfile: str) -> str:
201
+ shutil.copytree(layer.wheel_dir, context_path / "dist", dirs_exist_ok=True)
202
+ pip_install_args = layer.get_pip_install_args()
203
+ secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
204
+
205
+ # First install: Install the wheel without dependencies using --no-deps
206
+ pip_install_args_no_deps = [
207
+ *pip_install_args,
208
+ *[
209
+ "--find-links",
210
+ "/dist",
211
+ "--no-deps",
212
+ "--no-index",
213
+ "--reinstall",
214
+ layer.package_name,
215
+ ],
216
+ ]
217
+
218
+ delta1 = UV_WHEEL_INSTALL_COMMAND_TEMPLATE.substitute(
219
+ PIP_INSTALL_ARGS=" ".join(pip_install_args_no_deps), SECRET_MOUNT=secret_mounts
220
+ )
221
+ dockerfile += delta1
222
+
223
+ # Second install: Install dependencies from PyPI
224
+ pip_install_args_deps = [*pip_install_args, layer.package_name]
225
+ delta2 = UV_WHEEL_INSTALL_COMMAND_TEMPLATE.substitute(
226
+ PIP_INSTALL_ARGS=" ".join(pip_install_args_deps), SECRET_MOUNT=secret_mounts
227
+ )
228
+ dockerfile += delta2
229
+
230
+ return dockerfile
231
+
232
+
233
+ class _DockerLinesHandler:
234
+ @staticmethod
235
+ async def handle(layer: _DockerLines, context_path: Path, dockerfile: str) -> str:
236
+ # Add the lines to the dockerfile
237
+ for line in layer.lines:
238
+ dockerfile += f"\n{line}\n"
239
+
240
+ return dockerfile
241
+
242
+
243
+ class EnvHandler:
244
+ @staticmethod
245
+ async def handle(layer: Env, context_path: Path, dockerfile: str) -> str:
246
+ # Add the env vars to the dockerfile
247
+ for key, value in layer.env_vars:
248
+ dockerfile += f"\nENV {key}={value}\n"
249
+
250
+ return dockerfile
251
+
252
+
253
+ class AptPackagesHandler:
254
+ @staticmethod
255
+ async def handle(layer: AptPackages, _: Path, dockerfile: str) -> str:
256
+ packages = layer.packages
257
+ secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
258
+ delta = APT_INSTALL_COMMAND_TEMPLATE.substitute(APT_PACKAGES=" ".join(packages), SECRET_MOUNT=secret_mounts)
259
+ dockerfile += delta
260
+
261
+ return dockerfile
262
+
263
+
264
+ class UVProjectHandler:
265
+ @staticmethod
266
+ async def handle(
267
+ layer: UVProject, context_path: Path, dockerfile: str, docker_ignore_patterns: list[str] = []
268
+ ) -> str:
269
+ secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
270
+ if layer.project_install_mode == "dependencies_only":
271
+ pip_install_args = " ".join(layer.get_pip_install_args())
272
+ if "--no-install-project" not in pip_install_args:
273
+ pip_install_args += " --no-install-project"
274
+ if "--no-sources" not in pip_install_args:
275
+ pip_install_args += " --no-sources"
276
+ # Only Copy pyproject.yaml and uv.lock.
277
+ pyproject_dst = copy_files_to_context(layer.pyproject, context_path)
278
+ uvlock_dst = copy_files_to_context(layer.uvlock, context_path)
279
+ delta = UV_LOCK_WITHOUT_PROJECT_INSTALL_TEMPLATE.substitute(
280
+ UV_LOCK_PATH=uvlock_dst.relative_to(context_path),
281
+ PYPROJECT_PATH=pyproject_dst.relative_to(context_path),
282
+ PIP_INSTALL_ARGS=pip_install_args,
283
+ SECRET_MOUNT=secret_mounts,
284
+ )
285
+ else:
286
+ # Copy the entire project.
287
+ pyproject_dst = copy_files_to_context(layer.pyproject.parent, context_path, docker_ignore_patterns)
288
+
289
+ # Make sure pyproject.toml and uv.lock files are not removed by docker ignore
290
+ uv_lock_context_path = pyproject_dst / "uv.lock"
291
+ pyproject_context_path = pyproject_dst / "pyproject.toml"
292
+ if not uv_lock_context_path.exists():
293
+ shutil.copy(layer.uvlock, pyproject_dst)
294
+ if not pyproject_context_path.exists():
295
+ shutil.copy(layer.pyproject, pyproject_dst)
296
+
297
+ delta = UV_LOCK_INSTALL_TEMPLATE.substitute(
298
+ PYPROJECT_PATH=pyproject_dst.relative_to(context_path),
299
+ PIP_INSTALL_ARGS=" ".join(layer.get_pip_install_args()),
300
+ SECRET_MOUNT=secret_mounts,
301
+ )
302
+
303
+ dockerfile += delta
304
+ return dockerfile
305
+
306
+
307
+ class PoetryProjectHandler:
308
+ @staticmethod
309
+ async def handel(
310
+ layer: PoetryProject, context_path: Path, dockerfile: str, docker_ignore_patterns: list[str] = []
311
+ ) -> str:
312
+ secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
313
+ extra_args = layer.extra_args or ""
314
+ if layer.project_install_mode == "dependencies_only":
315
+ # Only Copy pyproject.yaml and poetry.lock.
316
+ pyproject_dst = copy_files_to_context(layer.pyproject, context_path)
317
+ poetry_lock_dst = copy_files_to_context(layer.poetry_lock, context_path)
318
+ if "--no-root" not in extra_args:
319
+ extra_args += " --no-root"
320
+ delta = POETRY_LOCK_WITHOUT_PROJECT_INSTALL_TEMPLATE.substitute(
321
+ POETRY_LOCK_PATH=poetry_lock_dst.relative_to(context_path),
322
+ PYPROJECT_PATH=pyproject_dst.relative_to(context_path),
323
+ POETRY_INSTALL_ARGS=extra_args,
324
+ SECRET_MOUNT=secret_mounts,
325
+ )
326
+ else:
327
+ # Copy the entire project.
328
+ pyproject_dst = copy_files_to_context(layer.pyproject.parent, context_path, docker_ignore_patterns)
329
+
330
+ # Make sure pyproject.toml and poetry.lock files are not removed by docker ignore
331
+ poetry_lock_context_path = pyproject_dst / "poetry.lock"
332
+ pyproject_context_path = pyproject_dst / "pyproject.toml"
333
+ if not poetry_lock_context_path.exists():
334
+ shutil.copy(layer.poetry_lock, pyproject_dst)
335
+ if not pyproject_context_path.exists():
336
+ shutil.copy(layer.pyproject, pyproject_dst)
337
+
338
+ delta = POETRY_LOCK_INSTALL_TEMPLATE.substitute(
339
+ PYPROJECT_PATH=pyproject_dst.relative_to(context_path),
340
+ POETRY_INSTALL_ARGS=extra_args,
341
+ SECRET_MOUNT=secret_mounts,
342
+ )
343
+ dockerfile += delta
344
+ return dockerfile
345
+
346
+
347
+ class DockerIgnoreHandler:
348
+ @staticmethod
349
+ async def handle(layer: DockerIgnore, context_path: Path, _: str):
350
+ shutil.copy(layer.path, context_path)
351
+
352
+
353
+ class CopyConfigHandler:
354
+ @staticmethod
355
+ async def handle(
356
+ layer: CopyConfig, context_path: Path, dockerfile: str, docker_ignore_patterns: list[str] = []
357
+ ) -> str:
358
+ # Copy the source config file or directory to the context path
359
+ if layer.src.is_absolute() or ".." in str(layer.src):
360
+ dst_path = context_path / str(layer.src.absolute()).replace("/", "./_flyte_abs_context/", 1)
361
+ else:
362
+ dst_path = context_path / layer.src
363
+
364
+ dst_path.parent.mkdir(parents=True, exist_ok=True)
365
+ abs_path = layer.src.absolute()
366
+
367
+ if layer.src.is_file():
368
+ # Copy the file
369
+ shutil.copy(abs_path, dst_path)
370
+ elif layer.src.is_dir():
371
+ # Copy the entire directory
372
+ shutil.copytree(
373
+ abs_path, dst_path, dirs_exist_ok=True, ignore=shutil.ignore_patterns(*docker_ignore_patterns)
374
+ )
375
+ else:
376
+ logger.error(f"Source path not exists: {layer.src}")
377
+ return dockerfile
378
+
379
+ # Add a copy command to the dockerfile
380
+ dockerfile += f"\nCOPY {dst_path.relative_to(context_path)} {layer.dst}\n"
381
+ return dockerfile
382
+
383
+
384
+ class CommandsHandler:
385
+ @staticmethod
386
+ async def handle(layer: Commands, _: Path, dockerfile: str) -> str:
387
+ # Append raw commands to the dockerfile
388
+ secret_mounts = _get_secret_mounts_layer(layer.secret_mounts)
389
+ for command in layer.commands:
390
+ dockerfile += f"\nRUN {secret_mounts} {command}\n"
391
+
392
+ return dockerfile
393
+
394
+
395
+ class WorkDirHandler:
396
+ @staticmethod
397
+ async def handle(layer: WorkDir, _: Path, dockerfile: str) -> str:
398
+ # cd to the workdir
399
+ dockerfile += f"\nWORKDIR {layer.workdir}\n"
400
+
401
+ return dockerfile
402
+
403
+
404
+ def _get_secret_commands(layers: typing.Tuple[Layer, ...]) -> typing.List[str]:
405
+ commands = []
406
+
407
+ def _get_secret_command(secret: str | Secret) -> typing.List[str]:
408
+ if isinstance(secret, str):
409
+ secret = Secret(key=secret)
410
+ secret_id = hash(secret)
411
+ secret_env_key = "_".join([k.upper() for k in filter(None, (secret.group, secret.key))])
412
+ if os.getenv(secret_env_key):
413
+ return ["--secret", f"id={secret_id},env={secret_env_key}"]
414
+ secret_file_name = "_".join(list(filter(None, (secret.group, secret.key))))
415
+ secret_file_path = f"/etc/secrets/{secret_file_name}"
416
+ if not os.path.exists(secret_file_path):
417
+ raise FileNotFoundError(f"Secret not found in Env Var {secret_env_key} or file {secret_file_path}")
418
+ return ["--secret", f"id={secret_id},src={secret_file_path}"]
419
+
420
+ for layer in layers:
421
+ if isinstance(layer, (PipOption, AptPackages, Commands)):
422
+ if layer.secret_mounts:
423
+ for secret_mount in layer.secret_mounts:
424
+ commands.extend(_get_secret_command(secret_mount))
425
+ return commands
426
+
427
+
428
+ def _get_secret_mounts_layer(secrets: typing.Tuple[str | Secret, ...] | None) -> str:
429
+ if secrets is None:
430
+ return ""
431
+ secret_mounts_layer = ""
432
+ for s in secrets:
433
+ secret = Secret(key=s) if isinstance(s, str) else s
434
+ secret_id = hash(secret)
435
+ if secret.mount:
436
+ secret_mounts_layer += f"--mount=type=secret,id={secret_id},target={secret.mount}"
437
+ elif secret.as_env_var:
438
+ secret_mounts_layer += f"--mount=type=secret,id={secret_id},env={secret.as_env_var}"
439
+ else:
440
+ secret_default_env_key = "_".join(list(filter(None, (secret.group, secret.key))))
441
+ secret_mounts_layer += f"--mount=type=secret,id={secret_id},env={secret_default_env_key}"
442
+
443
+ return secret_mounts_layer
444
+
445
+
446
+ async def _process_layer(
447
+ layer: Layer, context_path: Path, dockerfile: str, docker_ignore_patterns: list[str] = []
448
+ ) -> str:
449
+ match layer:
450
+ case PythonWheels():
451
+ # Handle Python wheels
452
+ dockerfile = await PythonWheelHandler.handle(layer, context_path, dockerfile)
453
+
454
+ case UVScript():
455
+ # Handle UV script
456
+ from flyte._utils import parse_uv_script_file
457
+
458
+ header = parse_uv_script_file(layer.script)
459
+ if header.dependencies:
460
+ pip = PipPackages(
461
+ packages=_ensure_tuple(header.dependencies) if header.dependencies else None,
462
+ secret_mounts=layer.secret_mounts,
463
+ index_url=layer.index_url,
464
+ extra_args=layer.extra_args,
465
+ pre=layer.pre,
466
+ extra_index_urls=layer.extra_index_urls,
467
+ )
468
+ dockerfile = await PipAndRequirementsHandler.handle(pip, context_path, dockerfile)
469
+
470
+ case Requirements() | PipPackages():
471
+ # Handle pip packages and requirements
472
+ dockerfile = await PipAndRequirementsHandler.handle(layer, context_path, dockerfile)
473
+
474
+ case AptPackages():
475
+ # Handle apt packages
476
+ dockerfile = await AptPackagesHandler.handle(layer, context_path, dockerfile)
477
+
478
+ case UVProject():
479
+ # Handle UV project
480
+ dockerfile = await UVProjectHandler.handle(layer, context_path, dockerfile, docker_ignore_patterns)
481
+
482
+ case PoetryProject():
483
+ # Handle Poetry project
484
+ dockerfile = await PoetryProjectHandler.handel(layer, context_path, dockerfile, docker_ignore_patterns)
485
+
486
+ case PoetryProject():
487
+ # Handle Poetry project
488
+ dockerfile = await PoetryProjectHandler.handel(layer, context_path, dockerfile, docker_ignore_patterns)
489
+
490
+ case CopyConfig():
491
+ # Handle local files and folders
492
+ dockerfile = await CopyConfigHandler.handle(layer, context_path, dockerfile, docker_ignore_patterns)
493
+
494
+ case Commands():
495
+ # Handle commands
496
+ dockerfile = await CommandsHandler.handle(layer, context_path, dockerfile)
497
+
498
+ case DockerIgnore():
499
+ # Handle dockerignore
500
+ await DockerIgnoreHandler.handle(layer, context_path, dockerfile)
501
+
502
+ case WorkDir():
503
+ # Handle workdir
504
+ dockerfile = await WorkDirHandler.handle(layer, context_path, dockerfile)
505
+
506
+ case Env():
507
+ # Handle environment variables
508
+ dockerfile = await EnvHandler.handle(layer, context_path, dockerfile)
509
+
510
+ case _DockerLines():
511
+ # Only for internal use
512
+ dockerfile = await _DockerLinesHandler.handle(layer, context_path, dockerfile)
513
+
514
+ case _:
515
+ raise NotImplementedError(f"Layer type {type(layer)} not supported")
516
+
517
+ return dockerfile
518
+
519
+
520
+ class DockerImageBuilder(ImageBuilder):
521
+ """Image builder using Docker and buildkit."""
522
+
523
+ builder_type: ClassVar = "docker"
524
+ _builder_name: ClassVar = "flytex"
525
+
526
+ def get_checkers(self) -> Optional[typing.List[typing.Type[ImageChecker]]]:
527
+ # Can get a public token for docker.io but ghcr requires a pat, so harder to get the manifest anonymously
528
+ return [LocalDockerCommandImageChecker, LocalPodmanCommandImageChecker, DockerAPIImageChecker]
529
+
530
+ async def build_image(self, image: Image, dry_run: bool = False) -> str:
531
+ if image.dockerfile:
532
+ # If a dockerfile is provided, use it directly
533
+ return await self._build_from_dockerfile(image, push=True)
534
+
535
+ if len(image._layers) == 0:
536
+ logger.warning("No layers to build, returning the image URI as is.")
537
+ return image.uri
538
+
539
+ return await self._build_image(
540
+ image,
541
+ push=True,
542
+ dry_run=dry_run,
543
+ )
544
+
545
+ async def _build_from_dockerfile(self, image: Image, push: bool) -> str:
546
+ """
547
+ Build the image from a provided Dockerfile.
548
+ """
549
+ assert image.dockerfile # for mypy
550
+ await DockerImageBuilder._ensure_buildx_builder()
551
+
552
+ command = [
553
+ "docker",
554
+ "buildx",
555
+ "build",
556
+ "--builder",
557
+ DockerImageBuilder._builder_name,
558
+ "-f",
559
+ str(image.dockerfile),
560
+ "--tag",
561
+ f"{image.uri}",
562
+ "--platform",
563
+ ",".join(image.platform),
564
+ str(image.dockerfile.parent.absolute()), # Use the parent directory of the Dockerfile as the context
565
+ ]
566
+
567
+ if image.registry and push:
568
+ command.append("--push")
569
+ else:
570
+ command.append("--load")
571
+
572
+ command.extend(_get_secret_commands(layers=image._layers))
573
+
574
+ concat_command = " ".join(command)
575
+ logger.debug(f"Build command: {concat_command}")
576
+ click.secho(f"Run command: {concat_command} ", fg="blue")
577
+
578
+ await asyncio.to_thread(subprocess.run, command, cwd=str(cast(Path, image.dockerfile).cwd()), check=True)
579
+
580
+ return image.uri
581
+
582
+ @staticmethod
583
+ async def _ensure_buildx_builder():
584
+ """Ensure there is a docker buildx builder called flyte"""
585
+ # Check if buildx is available
586
+ try:
587
+ await asyncio.to_thread(
588
+ subprocess.run, ["docker", "buildx", "version"], check=True, stdout=subprocess.DEVNULL
589
+ )
590
+ except subprocess.CalledProcessError:
591
+ raise RuntimeError("Docker buildx is not available. Make sure BuildKit is installed and enabled.")
592
+
593
+ # List builders
594
+ result = await asyncio.to_thread(
595
+ subprocess.run, ["docker", "buildx", "ls"], capture_output=True, text=True, check=True
596
+ )
597
+ builders = result.stdout
598
+
599
+ # Check if there's any usable builder
600
+ if DockerImageBuilder._builder_name not in builders:
601
+ # No default builder found, create one
602
+ logger.info("No buildx builder found, creating one...")
603
+ await asyncio.to_thread(
604
+ subprocess.run,
605
+ [
606
+ "docker",
607
+ "buildx",
608
+ "create",
609
+ "--name",
610
+ DockerImageBuilder._builder_name,
611
+ "--platform",
612
+ "linux/amd64,linux/arm64",
613
+ ],
614
+ check=True,
615
+ )
616
+ else:
617
+ logger.info("Buildx builder already exists.")
618
+
619
+ async def _build_image(self, image: Image, *, push: bool = True, dry_run: bool = False) -> str:
620
+ """
621
+ if default image (only base image and locked), raise an error, don't have a dockerfile
622
+ if dockerfile, just build
623
+ in the main case, get the default Dockerfile template
624
+ - start from the base image
625
+ - use python to create a default venv and export variables
626
+
627
+
628
+ Then for the layers
629
+ - for each layer
630
+ - find the appropriate layer handler
631
+ - call layer handler with the context dir and the dockerfile
632
+ - handler can choose to do something (copy files from local) to the context and update the dockerfile
633
+ contents, returning the new string
634
+ """
635
+ # For testing, set `push=False` to just build the image locally and not push to
636
+ # registry.
637
+
638
+ await DockerImageBuilder._ensure_buildx_builder()
639
+
640
+ with tempfile.TemporaryDirectory() as tmp_dir:
641
+ logger.warning(f"Temporary directory: {tmp_dir}")
642
+ tmp_path = Path(tmp_dir)
643
+
644
+ dockerfile = DOCKER_FILE_UV_BASE_TEMPLATE.substitute(
645
+ BASE_IMAGE=image.base_image,
646
+ PYTHON_VERSION=f"{image.python_version[0]}.{image.python_version[1]}",
647
+ )
648
+
649
+ # Get .dockerignore file patterns first
650
+ docker_ignore_patterns = get_and_list_dockerignore(image)
651
+
652
+ for layer in image._layers:
653
+ dockerfile = await _process_layer(layer, tmp_path, dockerfile, docker_ignore_patterns)
654
+
655
+ dockerfile += DOCKER_FILE_BASE_FOOTER.substitute(F_IMG_ID=image.uri)
656
+
657
+ dockerfile_path = tmp_path / "Dockerfile"
658
+ async with aiofiles.open(dockerfile_path, mode="w") as f:
659
+ await f.write(dockerfile)
660
+
661
+ command = [
662
+ "docker",
663
+ "buildx",
664
+ "build",
665
+ "--builder",
666
+ DockerImageBuilder._builder_name,
667
+ "--tag",
668
+ f"{image.uri}",
669
+ "--platform",
670
+ ",".join(image.platform),
671
+ ]
672
+
673
+ cache_from = os.getenv(FLYTE_DOCKER_BUILDER_CACHE_FROM)
674
+ cache_to = os.getenv(FLYTE_DOCKER_BUILDER_CACHE_TO)
675
+ if cache_from and cache_to:
676
+ command[3:3] = [
677
+ f"--cache-from={cache_from}",
678
+ f"--cache-to={cache_to}",
679
+ ]
680
+
681
+ if image.registry and push:
682
+ command.append("--push")
683
+ else:
684
+ command.append("--load")
685
+
686
+ command.extend(_get_secret_commands(layers=image._layers))
687
+ command.append(tmp_dir)
688
+
689
+ concat_command = " ".join(command)
690
+ logger.debug(f"Build command: {concat_command}")
691
+ if dry_run:
692
+ click.secho("Dry run for docker builder...")
693
+ click.secho(f"Context path: {tmp_path}")
694
+ click.secho(f"Dockerfile: {dockerfile}")
695
+ click.secho(f"Command: {concat_command}")
696
+ return image.uri
697
+ else:
698
+ click.secho(f"Run command: {concat_command} ", fg="blue")
699
+
700
+ try:
701
+ await asyncio.to_thread(subprocess.run, command, check=True)
702
+ except subprocess.CalledProcessError as e:
703
+ logger.error(f"Failed to build image: {e}")
704
+ raise RuntimeError(f"Failed to build image: {e}")
705
+
706
+ return image.uri