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.
Files changed (266) hide show
  1. flyte/__init__.py +83 -30
  2. flyte/_bin/connect.py +61 -0
  3. flyte/_bin/debug.py +38 -0
  4. flyte/_bin/runtime.py +87 -19
  5. flyte/_bin/serve.py +351 -0
  6. flyte/_build.py +3 -2
  7. flyte/_cache/cache.py +6 -5
  8. flyte/_cache/local_cache.py +216 -0
  9. flyte/_code_bundle/_ignore.py +31 -5
  10. flyte/_code_bundle/_packaging.py +42 -11
  11. flyte/_code_bundle/_utils.py +57 -34
  12. flyte/_code_bundle/bundle.py +130 -27
  13. flyte/_constants.py +1 -0
  14. flyte/_context.py +21 -5
  15. flyte/_custom_context.py +73 -0
  16. flyte/_debug/constants.py +37 -0
  17. flyte/_debug/utils.py +17 -0
  18. flyte/_debug/vscode.py +315 -0
  19. flyte/_deploy.py +396 -75
  20. flyte/_deployer.py +109 -0
  21. flyte/_environment.py +94 -11
  22. flyte/_excepthook.py +37 -0
  23. flyte/_group.py +2 -1
  24. flyte/_hash.py +1 -16
  25. flyte/_image.py +544 -231
  26. flyte/_initialize.py +456 -316
  27. flyte/_interface.py +40 -5
  28. flyte/_internal/controllers/__init__.py +22 -8
  29. flyte/_internal/controllers/_local_controller.py +159 -35
  30. flyte/_internal/controllers/_trace.py +18 -10
  31. flyte/_internal/controllers/remote/__init__.py +38 -9
  32. flyte/_internal/controllers/remote/_action.py +82 -12
  33. flyte/_internal/controllers/remote/_client.py +6 -2
  34. flyte/_internal/controllers/remote/_controller.py +290 -64
  35. flyte/_internal/controllers/remote/_core.py +155 -95
  36. flyte/_internal/controllers/remote/_informer.py +40 -20
  37. flyte/_internal/controllers/remote/_service_protocol.py +2 -2
  38. flyte/_internal/imagebuild/__init__.py +2 -10
  39. flyte/_internal/imagebuild/docker_builder.py +391 -84
  40. flyte/_internal/imagebuild/image_builder.py +111 -55
  41. flyte/_internal/imagebuild/remote_builder.py +409 -0
  42. flyte/_internal/imagebuild/utils.py +79 -0
  43. flyte/_internal/resolvers/_app_env_module.py +92 -0
  44. flyte/_internal/resolvers/_task_module.py +5 -38
  45. flyte/_internal/resolvers/app_env.py +26 -0
  46. flyte/_internal/resolvers/common.py +8 -1
  47. flyte/_internal/resolvers/default.py +2 -2
  48. flyte/_internal/runtime/convert.py +319 -36
  49. flyte/_internal/runtime/entrypoints.py +106 -18
  50. flyte/_internal/runtime/io.py +71 -23
  51. flyte/_internal/runtime/resources_serde.py +21 -7
  52. flyte/_internal/runtime/reuse.py +125 -0
  53. flyte/_internal/runtime/rusty.py +196 -0
  54. flyte/_internal/runtime/task_serde.py +239 -66
  55. flyte/_internal/runtime/taskrunner.py +48 -8
  56. flyte/_internal/runtime/trigger_serde.py +162 -0
  57. flyte/_internal/runtime/types_serde.py +7 -16
  58. flyte/_keyring/file.py +115 -0
  59. flyte/_link.py +30 -0
  60. flyte/_logging.py +241 -42
  61. flyte/_map.py +312 -0
  62. flyte/_metrics.py +59 -0
  63. flyte/_module.py +74 -0
  64. flyte/_pod.py +30 -0
  65. flyte/_resources.py +296 -33
  66. flyte/_retry.py +1 -7
  67. flyte/_reusable_environment.py +72 -7
  68. flyte/_run.py +462 -132
  69. flyte/_secret.py +47 -11
  70. flyte/_serve.py +333 -0
  71. flyte/_task.py +245 -56
  72. flyte/_task_environment.py +219 -97
  73. flyte/_task_plugins.py +47 -0
  74. flyte/_tools.py +8 -8
  75. flyte/_trace.py +15 -24
  76. flyte/_trigger.py +1027 -0
  77. flyte/_utils/__init__.py +12 -1
  78. flyte/_utils/asyn.py +3 -1
  79. flyte/_utils/async_cache.py +139 -0
  80. flyte/_utils/coro_management.py +5 -4
  81. flyte/_utils/description_parser.py +19 -0
  82. flyte/_utils/docker_credentials.py +173 -0
  83. flyte/_utils/helpers.py +45 -19
  84. flyte/_utils/module_loader.py +123 -0
  85. flyte/_utils/org_discovery.py +57 -0
  86. flyte/_utils/uv_script_parser.py +8 -1
  87. flyte/_version.py +16 -3
  88. flyte/app/__init__.py +27 -0
  89. flyte/app/_app_environment.py +362 -0
  90. flyte/app/_connector_environment.py +40 -0
  91. flyte/app/_deploy.py +130 -0
  92. flyte/app/_parameter.py +343 -0
  93. flyte/app/_runtime/__init__.py +3 -0
  94. flyte/app/_runtime/app_serde.py +383 -0
  95. flyte/app/_types.py +113 -0
  96. flyte/app/extras/__init__.py +9 -0
  97. flyte/app/extras/_auth_middleware.py +217 -0
  98. flyte/app/extras/_fastapi.py +93 -0
  99. flyte/app/extras/_model_loader/__init__.py +3 -0
  100. flyte/app/extras/_model_loader/config.py +7 -0
  101. flyte/app/extras/_model_loader/loader.py +288 -0
  102. flyte/cli/__init__.py +12 -0
  103. flyte/cli/_abort.py +28 -0
  104. flyte/cli/_build.py +114 -0
  105. flyte/cli/_common.py +493 -0
  106. flyte/cli/_create.py +371 -0
  107. flyte/cli/_delete.py +45 -0
  108. flyte/cli/_deploy.py +401 -0
  109. flyte/cli/_gen.py +316 -0
  110. flyte/cli/_get.py +446 -0
  111. flyte/cli/_option.py +33 -0
  112. flyte/{_cli → cli}/_params.py +57 -17
  113. flyte/cli/_plugins.py +209 -0
  114. flyte/cli/_prefetch.py +292 -0
  115. flyte/cli/_run.py +690 -0
  116. flyte/cli/_serve.py +338 -0
  117. flyte/cli/_update.py +86 -0
  118. flyte/cli/_user.py +20 -0
  119. flyte/cli/main.py +246 -0
  120. flyte/config/__init__.py +2 -167
  121. flyte/config/_config.py +215 -163
  122. flyte/config/_internal.py +10 -1
  123. flyte/config/_reader.py +225 -0
  124. flyte/connectors/__init__.py +11 -0
  125. flyte/connectors/_connector.py +330 -0
  126. flyte/connectors/_server.py +194 -0
  127. flyte/connectors/utils.py +159 -0
  128. flyte/errors.py +134 -2
  129. flyte/extend.py +24 -0
  130. flyte/extras/_container.py +69 -56
  131. flyte/git/__init__.py +3 -0
  132. flyte/git/_config.py +279 -0
  133. flyte/io/__init__.py +8 -1
  134. flyte/io/{structured_dataset → _dataframe}/__init__.py +32 -30
  135. flyte/io/{structured_dataset → _dataframe}/basic_dfs.py +75 -68
  136. flyte/io/{structured_dataset/structured_dataset.py → _dataframe/dataframe.py} +207 -242
  137. flyte/io/_dir.py +575 -113
  138. flyte/io/_file.py +587 -141
  139. flyte/io/_hashing_io.py +342 -0
  140. flyte/io/extend.py +7 -0
  141. flyte/models.py +635 -0
  142. flyte/prefetch/__init__.py +22 -0
  143. flyte/prefetch/_hf_model.py +563 -0
  144. flyte/remote/__init__.py +14 -3
  145. flyte/remote/_action.py +879 -0
  146. flyte/remote/_app.py +346 -0
  147. flyte/remote/_auth_metadata.py +42 -0
  148. flyte/remote/_client/_protocols.py +62 -4
  149. flyte/remote/_client/auth/_auth_utils.py +19 -0
  150. flyte/remote/_client/auth/_authenticators/base.py +8 -2
  151. flyte/remote/_client/auth/_authenticators/device_code.py +4 -5
  152. flyte/remote/_client/auth/_authenticators/factory.py +4 -0
  153. flyte/remote/_client/auth/_authenticators/passthrough.py +79 -0
  154. flyte/remote/_client/auth/_authenticators/pkce.py +17 -18
  155. flyte/remote/_client/auth/_channel.py +47 -18
  156. flyte/remote/_client/auth/_client_config.py +5 -3
  157. flyte/remote/_client/auth/_keyring.py +15 -2
  158. flyte/remote/_client/auth/_token_client.py +3 -3
  159. flyte/remote/_client/controlplane.py +206 -18
  160. flyte/remote/_common.py +66 -0
  161. flyte/remote/_data.py +107 -22
  162. flyte/remote/_logs.py +116 -33
  163. flyte/remote/_project.py +21 -19
  164. flyte/remote/_run.py +164 -631
  165. flyte/remote/_secret.py +72 -29
  166. flyte/remote/_task.py +387 -46
  167. flyte/remote/_trigger.py +368 -0
  168. flyte/remote/_user.py +43 -0
  169. flyte/report/_report.py +10 -6
  170. flyte/storage/__init__.py +13 -1
  171. flyte/storage/_config.py +237 -0
  172. flyte/storage/_parallel_reader.py +289 -0
  173. flyte/storage/_storage.py +268 -59
  174. flyte/syncify/__init__.py +56 -0
  175. flyte/syncify/_api.py +414 -0
  176. flyte/types/__init__.py +39 -0
  177. flyte/types/_interface.py +22 -7
  178. flyte/{io/pickle/transformer.py → types/_pickle.py} +37 -9
  179. flyte/types/_string_literals.py +8 -9
  180. flyte/types/_type_engine.py +226 -126
  181. flyte/types/_utils.py +1 -1
  182. flyte-2.0.0b46.data/scripts/debug.py +38 -0
  183. flyte-2.0.0b46.data/scripts/runtime.py +194 -0
  184. flyte-2.0.0b46.dist-info/METADATA +352 -0
  185. flyte-2.0.0b46.dist-info/RECORD +221 -0
  186. flyte-2.0.0b46.dist-info/entry_points.txt +8 -0
  187. flyte-2.0.0b46.dist-info/licenses/LICENSE +201 -0
  188. flyte/_api_commons.py +0 -3
  189. flyte/_cli/_common.py +0 -299
  190. flyte/_cli/_create.py +0 -42
  191. flyte/_cli/_delete.py +0 -23
  192. flyte/_cli/_deploy.py +0 -140
  193. flyte/_cli/_get.py +0 -235
  194. flyte/_cli/_run.py +0 -174
  195. flyte/_cli/main.py +0 -98
  196. flyte/_datastructures.py +0 -342
  197. flyte/_internal/controllers/pbhash.py +0 -39
  198. flyte/_protos/common/authorization_pb2.py +0 -66
  199. flyte/_protos/common/authorization_pb2.pyi +0 -108
  200. flyte/_protos/common/authorization_pb2_grpc.py +0 -4
  201. flyte/_protos/common/identifier_pb2.py +0 -71
  202. flyte/_protos/common/identifier_pb2.pyi +0 -82
  203. flyte/_protos/common/identifier_pb2_grpc.py +0 -4
  204. flyte/_protos/common/identity_pb2.py +0 -48
  205. flyte/_protos/common/identity_pb2.pyi +0 -72
  206. flyte/_protos/common/identity_pb2_grpc.py +0 -4
  207. flyte/_protos/common/list_pb2.py +0 -36
  208. flyte/_protos/common/list_pb2.pyi +0 -69
  209. flyte/_protos/common/list_pb2_grpc.py +0 -4
  210. flyte/_protos/common/policy_pb2.py +0 -37
  211. flyte/_protos/common/policy_pb2.pyi +0 -27
  212. flyte/_protos/common/policy_pb2_grpc.py +0 -4
  213. flyte/_protos/common/role_pb2.py +0 -37
  214. flyte/_protos/common/role_pb2.pyi +0 -53
  215. flyte/_protos/common/role_pb2_grpc.py +0 -4
  216. flyte/_protos/common/runtime_version_pb2.py +0 -28
  217. flyte/_protos/common/runtime_version_pb2.pyi +0 -24
  218. flyte/_protos/common/runtime_version_pb2_grpc.py +0 -4
  219. flyte/_protos/logs/dataplane/payload_pb2.py +0 -96
  220. flyte/_protos/logs/dataplane/payload_pb2.pyi +0 -168
  221. flyte/_protos/logs/dataplane/payload_pb2_grpc.py +0 -4
  222. flyte/_protos/secret/definition_pb2.py +0 -49
  223. flyte/_protos/secret/definition_pb2.pyi +0 -93
  224. flyte/_protos/secret/definition_pb2_grpc.py +0 -4
  225. flyte/_protos/secret/payload_pb2.py +0 -62
  226. flyte/_protos/secret/payload_pb2.pyi +0 -94
  227. flyte/_protos/secret/payload_pb2_grpc.py +0 -4
  228. flyte/_protos/secret/secret_pb2.py +0 -38
  229. flyte/_protos/secret/secret_pb2.pyi +0 -6
  230. flyte/_protos/secret/secret_pb2_grpc.py +0 -198
  231. flyte/_protos/secret/secret_pb2_grpc_grpc.py +0 -198
  232. flyte/_protos/validate/validate/validate_pb2.py +0 -76
  233. flyte/_protos/workflow/node_execution_service_pb2.py +0 -26
  234. flyte/_protos/workflow/node_execution_service_pb2.pyi +0 -4
  235. flyte/_protos/workflow/node_execution_service_pb2_grpc.py +0 -32
  236. flyte/_protos/workflow/queue_service_pb2.py +0 -106
  237. flyte/_protos/workflow/queue_service_pb2.pyi +0 -141
  238. flyte/_protos/workflow/queue_service_pb2_grpc.py +0 -172
  239. flyte/_protos/workflow/run_definition_pb2.py +0 -128
  240. flyte/_protos/workflow/run_definition_pb2.pyi +0 -310
  241. flyte/_protos/workflow/run_definition_pb2_grpc.py +0 -4
  242. flyte/_protos/workflow/run_logs_service_pb2.py +0 -41
  243. flyte/_protos/workflow/run_logs_service_pb2.pyi +0 -28
  244. flyte/_protos/workflow/run_logs_service_pb2_grpc.py +0 -69
  245. flyte/_protos/workflow/run_service_pb2.py +0 -133
  246. flyte/_protos/workflow/run_service_pb2.pyi +0 -175
  247. flyte/_protos/workflow/run_service_pb2_grpc.py +0 -412
  248. flyte/_protos/workflow/state_service_pb2.py +0 -58
  249. flyte/_protos/workflow/state_service_pb2.pyi +0 -71
  250. flyte/_protos/workflow/state_service_pb2_grpc.py +0 -138
  251. flyte/_protos/workflow/task_definition_pb2.py +0 -72
  252. flyte/_protos/workflow/task_definition_pb2.pyi +0 -65
  253. flyte/_protos/workflow/task_definition_pb2_grpc.py +0 -4
  254. flyte/_protos/workflow/task_service_pb2.py +0 -44
  255. flyte/_protos/workflow/task_service_pb2.pyi +0 -31
  256. flyte/_protos/workflow/task_service_pb2_grpc.py +0 -104
  257. flyte/io/_dataframe.py +0 -0
  258. flyte/io/pickle/__init__.py +0 -0
  259. flyte/remote/_console.py +0 -18
  260. flyte-0.2.0b1.dist-info/METADATA +0 -179
  261. flyte-0.2.0b1.dist-info/RECORD +0 -204
  262. flyte-0.2.0b1.dist-info/entry_points.txt +0 -3
  263. /flyte/{_cli → _debug}/__init__.py +0 -0
  264. /flyte/{_protos → _keyring}/__init__.py +0 -0
  265. {flyte-0.2.0b1.dist-info → flyte-2.0.0b46.dist-info}/WHEEL +0 -0
  266. {flyte-0.2.0b1.dist-info → flyte-2.0.0b46.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,409 @@
1
+ import gzip
2
+ import os
3
+ import shutil
4
+ import tarfile
5
+ import tempfile
6
+ import typing
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Optional, Tuple, cast
10
+ from uuid import uuid4
11
+
12
+ import aiofiles
13
+ from flyteidl2.common import phase_pb2
14
+
15
+ import flyte
16
+ import flyte.errors
17
+ from flyte import Image, remote
18
+ from flyte._code_bundle._utils import tar_strip_file_attributes
19
+ from flyte._image import (
20
+ _BASE_REGISTRY,
21
+ AptPackages,
22
+ Architecture,
23
+ Commands,
24
+ CopyConfig,
25
+ DockerIgnore,
26
+ Env,
27
+ PipOption,
28
+ PipPackages,
29
+ PoetryProject,
30
+ PythonWheels,
31
+ Requirements,
32
+ UVProject,
33
+ UVScript,
34
+ WorkDir,
35
+ )
36
+ from flyte._internal.imagebuild.image_builder import ImageBuilder, ImageChecker
37
+ from flyte._internal.imagebuild.utils import copy_files_to_context, get_and_list_dockerignore
38
+ from flyte._internal.runtime.task_serde import get_security_context
39
+ from flyte._logging import logger
40
+ from flyte._secret import Secret
41
+ from flyte.remote import ActionOutputs, Run
42
+
43
+ if TYPE_CHECKING:
44
+ from flyteidl2.imagebuilder import definition_pb2 as image_definition_pb2
45
+
46
+ IMAGE_TASK_NAME = "build-image"
47
+ IMAGE_TASK_PROJECT = "system"
48
+ IMAGE_TASK_DOMAIN = "production"
49
+
50
+
51
+ class RemoteImageChecker(ImageChecker):
52
+ _images_client = None
53
+
54
+ @classmethod
55
+ async def image_exists(
56
+ cls, repository: str, tag: str, arch: Tuple[Architecture, ...] = ("linux/amd64",)
57
+ ) -> Optional[str]:
58
+ try:
59
+ import flyte.remote as remote
60
+
61
+ remote.Task.get(
62
+ name=IMAGE_TASK_NAME,
63
+ project=IMAGE_TASK_PROJECT,
64
+ domain=IMAGE_TASK_DOMAIN,
65
+ auto_version="latest",
66
+ )
67
+ except Exception as e:
68
+ msg = "remote image builder is not enabled. Please contact Union support to enable it."
69
+ raise flyte.errors.ImageBuildError(msg) from e
70
+
71
+ image_name = f"{repository.split('/')[-1]}:{tag}"
72
+
73
+ try:
74
+ from flyteidl2.common.identifier_pb2 import ProjectIdentifier
75
+ from flyteidl2.imagebuilder import definition_pb2 as image_definition__pb2
76
+ from flyteidl2.imagebuilder import payload_pb2 as image_payload__pb2
77
+ from flyteidl2.imagebuilder import service_pb2_grpc as image_service_pb2_grpc
78
+
79
+ from flyte._initialize import _get_init_config
80
+
81
+ cfg = _get_init_config()
82
+ if cfg is None:
83
+ raise ValueError("Init config should not be None")
84
+ image_id = image_definition__pb2.ImageIdentifier(name=image_name)
85
+ req = image_payload__pb2.GetImageRequest(
86
+ id=image_id,
87
+ organization=cfg.org,
88
+ project_id=ProjectIdentifier(organization=cfg.org, domain=cfg.domain, name=cfg.project),
89
+ )
90
+ if cls._images_client is None:
91
+ if cfg.client is None:
92
+ raise ValueError("remote client should not be None")
93
+ cls._images_client = image_service_pb2_grpc.ImageServiceStub(cfg.client._channel)
94
+ resp = await cls._images_client.GetImage(req)
95
+ logger.warning(f"[blue]Image {resp.image.fqin} found. Skip building.[/blue]")
96
+ return resp.image.fqin
97
+ except Exception:
98
+ logger.warning(f"[blue]Image {image_name} was not found or has expired.[/blue]", extra={"highlight": False})
99
+ return None
100
+
101
+
102
+ class RemoteImageBuilder(ImageBuilder):
103
+ def get_checkers(self) -> Optional[typing.List[typing.Type[ImageChecker]]]:
104
+ """Return the image checker."""
105
+ return [RemoteImageChecker]
106
+
107
+ async def build_image(self, image: Image, dry_run: bool = False) -> str:
108
+ image_name = f"{image.name}:{image._final_tag}"
109
+ spec, context = await _validate_configuration(image)
110
+
111
+ start = datetime.now(timezone.utc)
112
+ try:
113
+ entity = await remote.Task.get(
114
+ name=IMAGE_TASK_NAME,
115
+ project=IMAGE_TASK_PROJECT,
116
+ domain=IMAGE_TASK_DOMAIN,
117
+ auto_version="latest",
118
+ ).override.aio(secrets=_get_build_secrets_from_image(image))
119
+ except flyte.errors.ReferenceTaskError:
120
+ raise flyte.errors.ImageBuildError(
121
+ "remote image builder is not enabled. Please contact Union support to enable it."
122
+ )
123
+
124
+ logger.warning("[bold blue]🐳 Submitting a new build...[/bold blue]")
125
+ if image.registry and image.registry != _BASE_REGISTRY:
126
+ target_image = f"{image.registry}/{image_name}"
127
+ else:
128
+ # Use the default system registry in the backend.
129
+ target_image = image_name
130
+
131
+ from flyte._initialize import get_init_config
132
+
133
+ cfg = get_init_config()
134
+ run = cast(
135
+ Run,
136
+ await flyte.with_runcontext(
137
+ project=cfg.project, domain=cfg.domain, cache_lookup_scope="project-domain"
138
+ ).run.aio(entity, spec=spec, context=context, target_image=target_image),
139
+ )
140
+ logger.warning(f"⏳ Waiting for build to finish at: [bold cyan link={run.url}]{run.url}[/bold cyan link]")
141
+
142
+ await run.wait.aio(quiet=True)
143
+ run_details = await run.details.aio()
144
+
145
+ elapsed = str(datetime.now(timezone.utc) - start).split(".")[0]
146
+
147
+ if run_details.action_details.raw_phase == phase_pb2.ACTION_PHASE_SUCCEEDED:
148
+ logger.warning(f"[bold green]✅ Build completed in {elapsed}![/bold green]")
149
+ else:
150
+ raise flyte.errors.ImageBuildError(f"❌ Build failed in {elapsed} at {run.url}")
151
+
152
+ outputs = await run_details.outputs()
153
+ return _get_fully_qualified_image_name(outputs)
154
+
155
+
156
+ async def _validate_configuration(image: Image) -> Tuple[str, Optional[str]]:
157
+ """Validate the configuration and prepare the spec and context files.""" # Prepare the spec file
158
+ tmp_path = Path(tempfile.gettempdir()) / str(uuid4())
159
+ os.makedirs(tmp_path, exist_ok=True)
160
+
161
+ context_path = tmp_path / "build.uc-image-builder"
162
+ context_path.mkdir(exist_ok=True)
163
+
164
+ image_idl = _get_layers_proto(image, context_path)
165
+
166
+ spec_path = tmp_path / "spec.pb"
167
+ with spec_path.open("wb") as f:
168
+ f.write(image_idl.SerializeToString())
169
+
170
+ _, spec_url = await remote.upload_file.aio(spec_path)
171
+
172
+ if any(context_path.iterdir()):
173
+ # If there are files in the context directory, upload it
174
+ tar_path = tmp_path / "context.tar"
175
+ with tarfile.open(tar_path, "w", dereference=False) as tar:
176
+ files: typing.List[str] = os.listdir(context_path)
177
+ for ws_file in files:
178
+ tar.add(
179
+ os.path.join(context_path, ws_file),
180
+ recursive=True,
181
+ arcname=ws_file,
182
+ filter=tar_strip_file_attributes,
183
+ )
184
+ context_dst = Path(f"{tar_path!s}.gz")
185
+ with gzip.GzipFile(filename=context_dst, mode="wb", mtime=0) as gzipped:
186
+ async with aiofiles.open(tar_path, "rb") as tar_file:
187
+ content = await tar_file.read()
188
+ gzipped.write(content)
189
+
190
+ context_size = tar_path.stat().st_size
191
+ if context_size > 5 * 1024 * 1024:
192
+ logger.warning(
193
+ f"[yellow]Context size is {context_size / (1024 * 1024):.2f} MB, which is larger than 5 MB. "
194
+ "Upload and build speed will be impacted.[/yellow]",
195
+ )
196
+ _, context_url = await remote.upload_file.aio(context_dst)
197
+ else:
198
+ context_url = ""
199
+
200
+ return spec_url, context_url
201
+
202
+
203
+ def _get_layers_proto(image: Image, context_path: Path) -> "image_definition_pb2.ImageSpec":
204
+ from flyteidl2.imagebuilder import definition_pb2 as image_definition_pb2
205
+
206
+ if image.dockerfile is not None:
207
+ raise flyte.errors.ImageBuildError(
208
+ "Custom Dockerfile is not supported with remote image builder.You can use local image builder instead."
209
+ )
210
+
211
+ layers = []
212
+ for layer in image._layers:
213
+ secret_mounts = None
214
+ pip_options = image_definition_pb2.PipOptions()
215
+
216
+ if isinstance(layer, PipOption):
217
+ pip_options = image_definition_pb2.PipOptions(
218
+ index_url=layer.index_url,
219
+ extra_index_urls=layer.extra_index_urls,
220
+ pre=layer.pre,
221
+ extra_args=layer.extra_args,
222
+ )
223
+
224
+ if hasattr(layer, "secret_mounts"):
225
+ sc = get_security_context(layer.secret_mounts)
226
+ secret_mounts = sc.secrets if sc else None
227
+
228
+ if isinstance(layer, AptPackages):
229
+ apt_layer = image_definition_pb2.Layer(
230
+ apt_packages=image_definition_pb2.AptPackages(
231
+ packages=layer.packages,
232
+ secret_mounts=secret_mounts,
233
+ ),
234
+ )
235
+ layers.append(apt_layer)
236
+ elif isinstance(layer, PythonWheels):
237
+ dst_path = copy_files_to_context(layer.wheel_dir, context_path)
238
+ wheel_layer = image_definition_pb2.Layer(
239
+ python_wheels=image_definition_pb2.PythonWheels(
240
+ dir=str(dst_path.relative_to(context_path)),
241
+ options=pip_options,
242
+ secret_mounts=secret_mounts,
243
+ )
244
+ )
245
+ layers.append(wheel_layer)
246
+
247
+ elif isinstance(layer, Requirements):
248
+ dst_path = copy_files_to_context(layer.file, context_path)
249
+ requirements_layer = image_definition_pb2.Layer(
250
+ requirements=image_definition_pb2.Requirements(
251
+ file=str(dst_path.relative_to(context_path)),
252
+ options=pip_options,
253
+ secret_mounts=secret_mounts,
254
+ )
255
+ )
256
+ layers.append(requirements_layer)
257
+ elif isinstance(layer, PipPackages) or isinstance(layer, UVScript):
258
+ if isinstance(layer, UVScript):
259
+ from flyte._utils import parse_uv_script_file
260
+
261
+ header = parse_uv_script_file(layer.script)
262
+ if not header.dependencies:
263
+ continue
264
+ packages: typing.Iterable[str] = header.dependencies
265
+ if header.pyprojects:
266
+ layers.append(
267
+ image_definition_pb2.Layer(
268
+ apt_packages=image_definition_pb2.AptPackages(
269
+ packages=["git"], # To get the version of the project.
270
+ ),
271
+ )
272
+ )
273
+ docker_ignore_patterns = get_and_list_dockerignore(image)
274
+
275
+ for pyproject in header.pyprojects:
276
+ pyproject_dst = copy_files_to_context(Path(pyproject), context_path, docker_ignore_patterns)
277
+ uv_project_layer = image_definition_pb2.Layer(
278
+ uv_project=image_definition_pb2.UVProject(
279
+ pyproject=str(pyproject_dst.relative_to(context_path)),
280
+ uvlock=str(
281
+ copy_files_to_context(Path(pyproject) / "uv.lock", context_path).relative_to(
282
+ context_path
283
+ )
284
+ ),
285
+ options=pip_options,
286
+ secret_mounts=secret_mounts,
287
+ )
288
+ )
289
+ layers.append(uv_project_layer)
290
+
291
+ else:
292
+ packages = layer.packages or []
293
+ pip_layer = image_definition_pb2.Layer(
294
+ pip_packages=image_definition_pb2.PipPackages(
295
+ packages=packages,
296
+ options=pip_options,
297
+ secret_mounts=secret_mounts,
298
+ )
299
+ )
300
+ layers.append(pip_layer)
301
+ elif isinstance(layer, UVProject):
302
+ if layer.project_install_mode == "dependencies_only":
303
+ # Copy pyproject itself
304
+ pyproject_dst = copy_files_to_context(layer.pyproject, context_path)
305
+ if pip_options.extra_args:
306
+ if "--no-install-project" not in pip_options.extra_args:
307
+ pip_options.extra_args += " --no-install-project"
308
+ else:
309
+ pip_options.extra_args = " --no-install-project"
310
+ if "--no-sources" not in pip_options.extra_args:
311
+ pip_options.extra_args += " --no-sources"
312
+ else:
313
+ # Copy the entire project
314
+ docker_ignore_patterns = get_and_list_dockerignore(image)
315
+ pyproject_dst = copy_files_to_context(layer.pyproject.parent, context_path, docker_ignore_patterns)
316
+
317
+ uv_layer = image_definition_pb2.Layer(
318
+ uv_project=image_definition_pb2.UVProject(
319
+ pyproject=str(pyproject_dst.relative_to(context_path)),
320
+ uvlock=str(copy_files_to_context(layer.uvlock, context_path).relative_to(context_path)),
321
+ options=pip_options,
322
+ secret_mounts=secret_mounts,
323
+ )
324
+ )
325
+ layers.append(uv_layer)
326
+ elif isinstance(layer, PoetryProject):
327
+ extra_args = layer.extra_args or ""
328
+ if layer.project_install_mode == "dependencies_only":
329
+ # Copy pyproject itself
330
+ if "--no-root" not in extra_args:
331
+ extra_args += " --no-root"
332
+ pyproject_dst = copy_files_to_context(layer.pyproject, context_path)
333
+ else:
334
+ # Copy the entire project
335
+ pyproject_dst = copy_files_to_context(layer.pyproject.parent, context_path)
336
+
337
+ poetry_layer = image_definition_pb2.Layer(
338
+ poetry_project=image_definition_pb2.PoetryProject(
339
+ pyproject=str(pyproject_dst.relative_to(context_path)),
340
+ poetry_lock=str(copy_files_to_context(layer.poetry_lock, context_path).relative_to(context_path)),
341
+ extra_args=extra_args,
342
+ secret_mounts=secret_mounts,
343
+ )
344
+ )
345
+ layers.append(poetry_layer)
346
+ elif isinstance(layer, Commands):
347
+ commands_layer = image_definition_pb2.Layer(
348
+ commands=image_definition_pb2.Commands(
349
+ cmd=list(layer.commands),
350
+ secret_mounts=secret_mounts,
351
+ )
352
+ )
353
+ layers.append(commands_layer)
354
+ elif isinstance(layer, DockerIgnore):
355
+ shutil.copy(layer.path, context_path)
356
+ elif isinstance(layer, CopyConfig):
357
+ dst_path = copy_files_to_context(layer.src, context_path)
358
+
359
+ copy_layer = image_definition_pb2.Layer(
360
+ copy_config=image_definition_pb2.CopyConfig(
361
+ src=str(dst_path.relative_to(context_path)),
362
+ dst=str(layer.dst),
363
+ )
364
+ )
365
+ layers.append(copy_layer)
366
+ elif isinstance(layer, Env):
367
+ env_layer = image_definition_pb2.Layer(
368
+ env=image_definition_pb2.Env(
369
+ env_variables=dict(layer.env_vars),
370
+ )
371
+ )
372
+ layers.append(env_layer)
373
+ elif isinstance(layer, WorkDir):
374
+ workdir_layer = image_definition_pb2.Layer(
375
+ workdir=image_definition_pb2.WorkDir(workdir=layer.workdir),
376
+ )
377
+ layers.append(workdir_layer)
378
+
379
+ return image_definition_pb2.ImageSpec(
380
+ base_image=image.base_image,
381
+ python_version=f"{image.python_version[0]}.{image.python_version[1]}",
382
+ layers=layers,
383
+ )
384
+
385
+
386
+ def _get_fully_qualified_image_name(outputs: ActionOutputs) -> str:
387
+ return outputs.pb2.literals[0].value.scalar.primitive.string_value
388
+
389
+
390
+ def _get_build_secrets_from_image(image: Image) -> Optional[typing.List[Secret]]:
391
+ secrets = []
392
+ DEFAULT_SECRET_DIR = Path("/etc/flyte/secrets")
393
+ for layer in image._layers:
394
+ if isinstance(layer, (PipOption, Commands, AptPackages)) and layer.secret_mounts is not None:
395
+ for secret_mount in layer.secret_mounts:
396
+ # Mount all the image secrets to a default directory that will be passed to the BuildKit server.
397
+ if isinstance(secret_mount, Secret):
398
+ secrets.append(Secret(key=secret_mount.key, group=secret_mount.group, mount=DEFAULT_SECRET_DIR))
399
+ elif isinstance(secret_mount, str):
400
+ secrets.append(Secret(key=secret_mount, mount=DEFAULT_SECRET_DIR))
401
+ else:
402
+ raise ValueError(f"Unsupported secret_mount type: {type(secret_mount)}")
403
+
404
+ image_registry_secret = image._image_registry_secret
405
+ if image_registry_secret:
406
+ secrets.append(
407
+ Secret(key=image_registry_secret.key, group=image_registry_secret.group, mount=DEFAULT_SECRET_DIR)
408
+ )
409
+ return secrets
@@ -0,0 +1,79 @@
1
+ import shutil
2
+ from pathlib import Path, PurePath
3
+ from typing import List, Optional
4
+
5
+ from flyte._image import DockerIgnore, Image
6
+ from flyte._logging import logger
7
+
8
+
9
+ def copy_files_to_context(src: Path, context_path: Path, ignore_patterns: list[str] = []) -> Path:
10
+ """
11
+ This helper function ensures that absolute paths that users specify are converted correctly to a path in the
12
+ context directory. Doing this prevents collisions while ensuring files are available in the context.
13
+
14
+ For example, if a user has
15
+ img.with_requirements(Path("/Users/username/requirements.txt"))
16
+ .with_requirements(Path("requirements.txt"))
17
+ .with_requirements(Path("../requirements.txt"))
18
+
19
+ copying with this function ensures that the Docker context folder has all three files.
20
+
21
+ :param src: The source path to copy
22
+ :param context_path: The context path where the files should be copied to
23
+ """
24
+ if src.is_absolute() or ".." in str(src):
25
+ rel_path = PurePath(*src.parts[1:])
26
+ dst_path = context_path / "_flyte_abs_context" / rel_path
27
+ else:
28
+ dst_path = context_path / src
29
+ dst_path.parent.mkdir(parents=True, exist_ok=True)
30
+ if src.is_dir():
31
+ default_ignore_patterns = [".idea", ".venv"]
32
+ ignore_patterns = list(set(ignore_patterns + default_ignore_patterns))
33
+ shutil.copytree(src, dst_path, dirs_exist_ok=True, ignore=shutil.ignore_patterns(*ignore_patterns))
34
+ else:
35
+ shutil.copy(src, dst_path)
36
+ return dst_path
37
+
38
+
39
+ def get_and_list_dockerignore(image: Image) -> List[str]:
40
+ """
41
+ Get and parse dockerignore patterns from .dockerignore file.
42
+
43
+ This function first looks for a DockerIgnore layer in the image's layers. If found, it uses
44
+ the path specified in that layer. If no DockerIgnore layer is found, it falls back to looking
45
+ for a .dockerignore file in the root_path directory.
46
+
47
+ :param image: The Image object
48
+ """
49
+ from flyte._initialize import _get_init_config
50
+
51
+ # Look for DockerIgnore layer in the image layers
52
+ dockerignore_path: Optional[Path] = None
53
+ patterns: List[str] = []
54
+
55
+ for layer in image._layers:
56
+ if isinstance(layer, DockerIgnore) and layer.path.strip():
57
+ dockerignore_path = Path(layer.path)
58
+ # If DockerIgnore layer not specified, set dockerignore_path under root_path
59
+ init_config = _get_init_config()
60
+ root_path = init_config.root_dir if init_config else None
61
+ if not dockerignore_path and root_path:
62
+ dockerignore_path = Path(root_path) / ".dockerignore"
63
+ # Return empty list if no .dockerignore file found
64
+ if not dockerignore_path or not dockerignore_path.exists() or not dockerignore_path.is_file():
65
+ logger.info(f".dockerignore file not found at path: {dockerignore_path}")
66
+ return patterns
67
+
68
+ try:
69
+ with open(dockerignore_path, "r", encoding="utf-8") as f:
70
+ for line in f:
71
+ stripped_line = line.strip()
72
+ # Skip empty lines, whitespace-only lines, and comments
73
+ if not stripped_line or stripped_line.startswith("#"):
74
+ continue
75
+ patterns.append(stripped_line)
76
+ except Exception as e:
77
+ logger.error(f"Failed to read .dockerignore file at {dockerignore_path}: {e}")
78
+ return []
79
+ return patterns
@@ -0,0 +1,92 @@
1
+ import pathlib
2
+ import sys
3
+
4
+ from flyte._logging import logger
5
+ from flyte._module import extract_obj_module
6
+ from flyte.app._app_environment import AppEnvironment
7
+
8
+
9
+ def extract_app_env_module(app_env: AppEnvironment, /, source_dir: pathlib.Path) -> tuple[str, str]:
10
+ """
11
+ Extract the module name and variable name for a AppEnvironment instance.
12
+
13
+ Args:
14
+ app_env: The AppEnvironment instance to locate.
15
+ caller_frame: Frame information from where AppEnvironment was instantiated.
16
+ If None, falls back to extract_obj_module (which may not work correctly).
17
+ serialization_context: Context containing the root directory for calculating
18
+ relative module paths.
19
+
20
+ Returns:
21
+ A tuple of (module_name, variable_name) where:
22
+ - module_name: Dotted module path (e.g., "examples.apps.single_script_fastapi")
23
+ - variable_name: The name of the variable holding the AppEnvironment (e.g., "env")
24
+
25
+ Raises:
26
+ RuntimeError: If the module cannot be loaded or the app variable cannot be found.
27
+
28
+ Example:
29
+ >>> frame = inspect.getframeinfo(inspect.currentframe().f_back)
30
+ >>> module_name, var_name = _extract_app_env_module_and_var(
31
+ ... app_env, frame, serialization_context
32
+ ... )
33
+ >>> # Returns: ("examples.apps.my_app", "env")
34
+ >>> # Can be used as: fserve examples.apps.my_app:env
35
+ """
36
+ if app_env._caller_frame is None:
37
+ raise RuntimeError("Caller frame cannot be None")
38
+
39
+ # Get the file path where the app was defined
40
+ file_path = pathlib.Path(app_env._caller_frame.filename)
41
+
42
+ # Calculate module name relative to source_dir
43
+ try:
44
+ relative_path = file_path.relative_to(source_dir or pathlib.Path("."))
45
+ logger.info(f"Relative path: {relative_path}, {source_dir} {pathlib.Path('.')}")
46
+ module_name = pathlib.Path(relative_path).with_suffix("").as_posix().replace("/", ".")
47
+ except ValueError:
48
+ # File is not relative to source_dir, use the stem
49
+ module_name = file_path.stem
50
+
51
+ # Instead of reloading the module, inspect the caller frame's local variables
52
+ # The app variable should be in the frame's globals
53
+ caller_globals = None
54
+
55
+ # Try to get globals from the main module if it matches our file
56
+ if hasattr(sys.modules.get("__main__"), "__file__"):
57
+ main_file = pathlib.Path(sys.modules["__main__"].__file__ or ".").resolve()
58
+ if main_file == file_path.resolve():
59
+ caller_globals = sys.modules["__main__"].__dict__
60
+
61
+ if caller_globals is None:
62
+ # Load the module to inspect it by importing it by name
63
+ # Note: we can't use extract_obj_module here because it uses inspect.getmodule()
64
+ # which returns the module where the CLASS is defined, not where the INSTANCE is created
65
+ import importlib
66
+
67
+ try:
68
+ entity_module = importlib.import_module(module_name)
69
+ caller_globals = entity_module.__dict__
70
+ except (ModuleNotFoundError, ImportError):
71
+ # Fallback for test scenarios where module might be <string> or not importable
72
+ # In this case, use extract_obj_module as a last resort
73
+ _, entity_module = extract_obj_module(app_env, source_dir)
74
+ caller_globals = entity_module.__dict__
75
+
76
+ # Extract variable name from module - look for AppEnvironment instances
77
+ app_var_name = None
78
+ for var_name, obj in caller_globals.items():
79
+ if isinstance(obj, AppEnvironment):
80
+ # Found a AppEnvironment - this is likely the one we want
81
+ # Store the first one we find
82
+ if app_var_name is None:
83
+ app_var_name = var_name
84
+ # If the objects match by identity, use this one
85
+ if obj is app_env:
86
+ app_var_name = var_name
87
+ break
88
+
89
+ if app_var_name is None:
90
+ raise RuntimeError("Could not find variable name for FastAPI app in module")
91
+
92
+ return app_var_name, module_name
@@ -1,13 +1,11 @@
1
- import inspect
2
- import os
3
1
  import pathlib
4
- import sys
5
2
  from typing import Tuple
6
3
 
4
+ from flyte._module import extract_obj_module
7
5
  from flyte._task import AsyncFunctionTaskTemplate, TaskTemplate
8
6
 
9
7
 
10
- def extract_task_module(task: TaskTemplate, /, source_dir: pathlib.Path | None = None) -> Tuple[str, str]:
8
+ def extract_task_module(task: TaskTemplate, /, source_dir: pathlib.Path) -> Tuple[str, str]:
11
9
  """
12
10
  Extract the task module from the task template.
13
11
 
@@ -15,40 +13,9 @@ def extract_task_module(task: TaskTemplate, /, source_dir: pathlib.Path | None =
15
13
  :param source_dir: The source directory to use for relative paths.
16
14
  :return: A tuple containing the entity name, module
17
15
  """
18
- entity_name = task.name
19
16
  if isinstance(task, AsyncFunctionTaskTemplate):
20
- entity_module = inspect.getmodule(task.func)
21
- if entity_module is None:
22
- raise ValueError(f"Task {entity_name} has no module.")
23
-
24
- fp = entity_module.__file__
25
- if fp is None:
26
- raise ValueError(f"Task {entity_name} has no module.")
27
-
28
- file_path = pathlib.Path(fp)
29
- # Get the relative path to the current directory
30
- # Will raise ValueError if the file is not in the source directory
31
- relative_path = file_path.relative_to(str(source_dir))
32
-
33
- if relative_path == pathlib.Path("."):
34
- entity_module_name = entity_module.__name__
35
- else:
36
- # Replace file separators with dots and remove the '.py' extension
37
- dotted_path = os.path.splitext(str(relative_path))[0].replace(os.sep, ".")
38
- entity_module_name = dotted_path
39
-
40
17
  entity_name = task.func.__name__
18
+ entity_module_name, _ = extract_obj_module(task.func, source_dir)
19
+ return entity_name, entity_module_name
41
20
  else:
42
- raise NotImplementedError(f"Task module {entity_name} not implemented.")
43
-
44
- if entity_module_name == "__main__":
45
- """
46
- This case is for the case in which the task is run from the main module.
47
- """
48
- fp = sys.modules["__main__"].__file__
49
- if fp is None:
50
- raise ValueError(f"Task {entity_name} has no module.")
51
- main_path = pathlib.Path(fp)
52
- entity_module_name = main_path.stem
53
-
54
- return entity_name, entity_module_name
21
+ raise NotImplementedError(f"Task module {task.name} not implemented.")
@@ -0,0 +1,26 @@
1
+ import importlib
2
+ from pathlib import Path
3
+
4
+ from flyte._internal.resolvers._app_env_module import extract_app_env_module
5
+ from flyte._internal.resolvers.common import Resolver
6
+ from flyte.app._app_environment import AppEnvironment
7
+
8
+
9
+ class AppEnvResolver(Resolver):
10
+ """
11
+ Please see the notes in the TaskResolverMixin as it describes this default behavior.
12
+ """
13
+
14
+ @property
15
+ def import_path(self) -> str:
16
+ return "flyte._internal.resolvers.app_env.AppEnvResolver"
17
+
18
+ def load_app_env(self, loader_args: str) -> AppEnvironment:
19
+ module_name, app_var_name = loader_args.split(":")
20
+ app_env_module = importlib.import_module(name=module_name) # type: ignore
21
+ app_env_def = getattr(app_env_module, app_var_name)
22
+ return app_env_def
23
+
24
+ def loader_args(self, app_env: AppEnvironment, root_dir: Path) -> str: # type:ignore
25
+ app_var_name, module_name = extract_app_env_module(app_env, root_dir)
26
+ return f"{module_name}:{app_var_name}"
@@ -3,6 +3,7 @@ from pathlib import Path
3
3
  from typing import List, Optional
4
4
 
5
5
  from flyte._task import TaskTemplate
6
+ from flyte.app._app_environment import AppEnvironment
6
7
 
7
8
 
8
9
  class Resolver(Protocol):
@@ -23,7 +24,13 @@ class Resolver(Protocol):
23
24
  """
24
25
  raise NotImplementedError
25
26
 
26
- def loader_args(self, t: TaskTemplate, root_dir: Optional[Path]) -> List[str]:
27
+ def load_app_env(self, loader_args: str) -> AppEnvironment:
28
+ """
29
+ Given the set of identifier keys, should return one AppEnvironment or raise an error if not found
30
+ """
31
+ raise NotImplementedError
32
+
33
+ def loader_args(self, t: TaskTemplate, root_dir: Optional[Path]) -> List[str] | str:
27
34
  """
28
35
  Return a list of strings that can help identify the parameter TaskTemplate. Each string should not have
29
36
  spaces or special characters. This is used to identify the task in the resolver.