flyte 0.1.0__py3-none-any.whl → 0.2.0b1__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 (205) hide show
  1. flyte/__init__.py +62 -2
  2. flyte/_api_commons.py +3 -0
  3. flyte/_bin/__init__.py +0 -0
  4. flyte/_bin/runtime.py +126 -0
  5. flyte/_build.py +25 -0
  6. flyte/_cache/__init__.py +12 -0
  7. flyte/_cache/cache.py +146 -0
  8. flyte/_cache/defaults.py +9 -0
  9. flyte/_cache/policy_function_body.py +42 -0
  10. flyte/_cli/__init__.py +0 -0
  11. flyte/_cli/_common.py +299 -0
  12. flyte/_cli/_create.py +42 -0
  13. flyte/_cli/_delete.py +23 -0
  14. flyte/_cli/_deploy.py +140 -0
  15. flyte/_cli/_get.py +235 -0
  16. flyte/_cli/_params.py +538 -0
  17. flyte/_cli/_run.py +174 -0
  18. flyte/_cli/main.py +98 -0
  19. flyte/_code_bundle/__init__.py +8 -0
  20. flyte/_code_bundle/_ignore.py +113 -0
  21. flyte/_code_bundle/_packaging.py +187 -0
  22. flyte/_code_bundle/_utils.py +339 -0
  23. flyte/_code_bundle/bundle.py +178 -0
  24. flyte/_context.py +146 -0
  25. flyte/_datastructures.py +342 -0
  26. flyte/_deploy.py +202 -0
  27. flyte/_doc.py +29 -0
  28. flyte/_docstring.py +32 -0
  29. flyte/_environment.py +43 -0
  30. flyte/_group.py +31 -0
  31. flyte/_hash.py +23 -0
  32. flyte/_image.py +757 -0
  33. flyte/_initialize.py +643 -0
  34. flyte/_interface.py +84 -0
  35. flyte/_internal/__init__.py +3 -0
  36. flyte/_internal/controllers/__init__.py +115 -0
  37. flyte/_internal/controllers/_local_controller.py +118 -0
  38. flyte/_internal/controllers/_trace.py +40 -0
  39. flyte/_internal/controllers/pbhash.py +39 -0
  40. flyte/_internal/controllers/remote/__init__.py +40 -0
  41. flyte/_internal/controllers/remote/_action.py +141 -0
  42. flyte/_internal/controllers/remote/_client.py +43 -0
  43. flyte/_internal/controllers/remote/_controller.py +361 -0
  44. flyte/_internal/controllers/remote/_core.py +402 -0
  45. flyte/_internal/controllers/remote/_informer.py +361 -0
  46. flyte/_internal/controllers/remote/_service_protocol.py +50 -0
  47. flyte/_internal/imagebuild/__init__.py +11 -0
  48. flyte/_internal/imagebuild/docker_builder.py +416 -0
  49. flyte/_internal/imagebuild/image_builder.py +241 -0
  50. flyte/_internal/imagebuild/remote_builder.py +0 -0
  51. flyte/_internal/resolvers/__init__.py +0 -0
  52. flyte/_internal/resolvers/_task_module.py +54 -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 +205 -0
  57. flyte/_internal/runtime/entrypoints.py +135 -0
  58. flyte/_internal/runtime/io.py +136 -0
  59. flyte/_internal/runtime/resources_serde.py +138 -0
  60. flyte/_internal/runtime/task_serde.py +210 -0
  61. flyte/_internal/runtime/taskrunner.py +190 -0
  62. flyte/_internal/runtime/types_serde.py +54 -0
  63. flyte/_logging.py +124 -0
  64. flyte/_protos/__init__.py +0 -0
  65. flyte/_protos/common/authorization_pb2.py +66 -0
  66. flyte/_protos/common/authorization_pb2.pyi +108 -0
  67. flyte/_protos/common/authorization_pb2_grpc.py +4 -0
  68. flyte/_protos/common/identifier_pb2.py +71 -0
  69. flyte/_protos/common/identifier_pb2.pyi +82 -0
  70. flyte/_protos/common/identifier_pb2_grpc.py +4 -0
  71. flyte/_protos/common/identity_pb2.py +48 -0
  72. flyte/_protos/common/identity_pb2.pyi +72 -0
  73. flyte/_protos/common/identity_pb2_grpc.py +4 -0
  74. flyte/_protos/common/list_pb2.py +36 -0
  75. flyte/_protos/common/list_pb2.pyi +69 -0
  76. flyte/_protos/common/list_pb2_grpc.py +4 -0
  77. flyte/_protos/common/policy_pb2.py +37 -0
  78. flyte/_protos/common/policy_pb2.pyi +27 -0
  79. flyte/_protos/common/policy_pb2_grpc.py +4 -0
  80. flyte/_protos/common/role_pb2.py +37 -0
  81. flyte/_protos/common/role_pb2.pyi +53 -0
  82. flyte/_protos/common/role_pb2_grpc.py +4 -0
  83. flyte/_protos/common/runtime_version_pb2.py +28 -0
  84. flyte/_protos/common/runtime_version_pb2.pyi +24 -0
  85. flyte/_protos/common/runtime_version_pb2_grpc.py +4 -0
  86. flyte/_protos/logs/dataplane/payload_pb2.py +96 -0
  87. flyte/_protos/logs/dataplane/payload_pb2.pyi +168 -0
  88. flyte/_protos/logs/dataplane/payload_pb2_grpc.py +4 -0
  89. flyte/_protos/secret/definition_pb2.py +49 -0
  90. flyte/_protos/secret/definition_pb2.pyi +93 -0
  91. flyte/_protos/secret/definition_pb2_grpc.py +4 -0
  92. flyte/_protos/secret/payload_pb2.py +62 -0
  93. flyte/_protos/secret/payload_pb2.pyi +94 -0
  94. flyte/_protos/secret/payload_pb2_grpc.py +4 -0
  95. flyte/_protos/secret/secret_pb2.py +38 -0
  96. flyte/_protos/secret/secret_pb2.pyi +6 -0
  97. flyte/_protos/secret/secret_pb2_grpc.py +198 -0
  98. flyte/_protos/secret/secret_pb2_grpc_grpc.py +198 -0
  99. flyte/_protos/validate/validate/validate_pb2.py +76 -0
  100. flyte/_protos/workflow/node_execution_service_pb2.py +26 -0
  101. flyte/_protos/workflow/node_execution_service_pb2.pyi +4 -0
  102. flyte/_protos/workflow/node_execution_service_pb2_grpc.py +32 -0
  103. flyte/_protos/workflow/queue_service_pb2.py +106 -0
  104. flyte/_protos/workflow/queue_service_pb2.pyi +141 -0
  105. flyte/_protos/workflow/queue_service_pb2_grpc.py +172 -0
  106. flyte/_protos/workflow/run_definition_pb2.py +128 -0
  107. flyte/_protos/workflow/run_definition_pb2.pyi +310 -0
  108. flyte/_protos/workflow/run_definition_pb2_grpc.py +4 -0
  109. flyte/_protos/workflow/run_logs_service_pb2.py +41 -0
  110. flyte/_protos/workflow/run_logs_service_pb2.pyi +28 -0
  111. flyte/_protos/workflow/run_logs_service_pb2_grpc.py +69 -0
  112. flyte/_protos/workflow/run_service_pb2.py +133 -0
  113. flyte/_protos/workflow/run_service_pb2.pyi +175 -0
  114. flyte/_protos/workflow/run_service_pb2_grpc.py +412 -0
  115. flyte/_protos/workflow/state_service_pb2.py +58 -0
  116. flyte/_protos/workflow/state_service_pb2.pyi +71 -0
  117. flyte/_protos/workflow/state_service_pb2_grpc.py +138 -0
  118. flyte/_protos/workflow/task_definition_pb2.py +72 -0
  119. flyte/_protos/workflow/task_definition_pb2.pyi +65 -0
  120. flyte/_protos/workflow/task_definition_pb2_grpc.py +4 -0
  121. flyte/_protos/workflow/task_service_pb2.py +44 -0
  122. flyte/_protos/workflow/task_service_pb2.pyi +31 -0
  123. flyte/_protos/workflow/task_service_pb2_grpc.py +104 -0
  124. flyte/_resources.py +226 -0
  125. flyte/_retry.py +32 -0
  126. flyte/_reusable_environment.py +25 -0
  127. flyte/_run.py +410 -0
  128. flyte/_secret.py +61 -0
  129. flyte/_task.py +367 -0
  130. flyte/_task_environment.py +200 -0
  131. flyte/_timeout.py +47 -0
  132. flyte/_tools.py +27 -0
  133. flyte/_trace.py +128 -0
  134. flyte/_utils/__init__.py +20 -0
  135. flyte/_utils/asyn.py +119 -0
  136. flyte/_utils/coro_management.py +25 -0
  137. flyte/_utils/file_handling.py +72 -0
  138. flyte/_utils/helpers.py +108 -0
  139. flyte/_utils/lazy_module.py +54 -0
  140. flyte/_utils/uv_script_parser.py +49 -0
  141. flyte/_version.py +21 -0
  142. flyte/config/__init__.py +168 -0
  143. flyte/config/_config.py +196 -0
  144. flyte/config/_internal.py +64 -0
  145. flyte/connectors/__init__.py +0 -0
  146. flyte/errors.py +143 -0
  147. flyte/extras/__init__.py +5 -0
  148. flyte/extras/_container.py +273 -0
  149. flyte/io/__init__.py +11 -0
  150. flyte/io/_dataframe.py +0 -0
  151. flyte/io/_dir.py +448 -0
  152. flyte/io/_file.py +468 -0
  153. flyte/io/pickle/__init__.py +0 -0
  154. flyte/io/pickle/transformer.py +117 -0
  155. flyte/io/structured_dataset/__init__.py +129 -0
  156. flyte/io/structured_dataset/basic_dfs.py +219 -0
  157. flyte/io/structured_dataset/structured_dataset.py +1061 -0
  158. flyte/remote/__init__.py +25 -0
  159. flyte/remote/_client/__init__.py +0 -0
  160. flyte/remote/_client/_protocols.py +131 -0
  161. flyte/remote/_client/auth/__init__.py +12 -0
  162. flyte/remote/_client/auth/_authenticators/__init__.py +0 -0
  163. flyte/remote/_client/auth/_authenticators/base.py +397 -0
  164. flyte/remote/_client/auth/_authenticators/client_credentials.py +73 -0
  165. flyte/remote/_client/auth/_authenticators/device_code.py +118 -0
  166. flyte/remote/_client/auth/_authenticators/external_command.py +79 -0
  167. flyte/remote/_client/auth/_authenticators/factory.py +200 -0
  168. flyte/remote/_client/auth/_authenticators/pkce.py +516 -0
  169. flyte/remote/_client/auth/_channel.py +184 -0
  170. flyte/remote/_client/auth/_client_config.py +83 -0
  171. flyte/remote/_client/auth/_default_html.py +32 -0
  172. flyte/remote/_client/auth/_grpc_utils/__init__.py +0 -0
  173. flyte/remote/_client/auth/_grpc_utils/auth_interceptor.py +288 -0
  174. flyte/remote/_client/auth/_grpc_utils/default_metadata_interceptor.py +151 -0
  175. flyte/remote/_client/auth/_keyring.py +143 -0
  176. flyte/remote/_client/auth/_token_client.py +260 -0
  177. flyte/remote/_client/auth/errors.py +16 -0
  178. flyte/remote/_client/controlplane.py +95 -0
  179. flyte/remote/_console.py +18 -0
  180. flyte/remote/_data.py +155 -0
  181. flyte/remote/_logs.py +116 -0
  182. flyte/remote/_project.py +86 -0
  183. flyte/remote/_run.py +873 -0
  184. flyte/remote/_secret.py +132 -0
  185. flyte/remote/_task.py +227 -0
  186. flyte/report/__init__.py +3 -0
  187. flyte/report/_report.py +178 -0
  188. flyte/report/_template.html +124 -0
  189. flyte/storage/__init__.py +24 -0
  190. flyte/storage/_remote_fs.py +34 -0
  191. flyte/storage/_storage.py +251 -0
  192. flyte/storage/_utils.py +5 -0
  193. flyte/types/__init__.py +13 -0
  194. flyte/types/_interface.py +25 -0
  195. flyte/types/_renderer.py +162 -0
  196. flyte/types/_string_literals.py +120 -0
  197. flyte/types/_type_engine.py +2211 -0
  198. flyte/types/_utils.py +80 -0
  199. flyte-0.2.0b1.dist-info/METADATA +179 -0
  200. flyte-0.2.0b1.dist-info/RECORD +204 -0
  201. {flyte-0.1.0.dist-info → flyte-0.2.0b1.dist-info}/WHEEL +2 -1
  202. flyte-0.2.0b1.dist-info/entry_points.txt +3 -0
  203. flyte-0.2.0b1.dist-info/top_level.txt +1 -0
  204. flyte-0.1.0.dist-info/METADATA +0 -6
  205. flyte-0.1.0.dist-info/RECORD +0 -5
@@ -0,0 +1,342 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import os
5
+ import pathlib
6
+ import tempfile
7
+ from dataclasses import dataclass, field, replace
8
+ from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Type
9
+
10
+ from flyte._docstring import Docstring
11
+ from flyte._interface import extract_return_annotation
12
+ from flyte._logging import logger
13
+ from flyte._utils.helpers import base36_encode
14
+
15
+ if TYPE_CHECKING:
16
+ from flyte._internal.imagebuild.image_builder import ImageCache
17
+ from flyte.report import Report
18
+
19
+
20
+ def generate_random_name() -> str:
21
+ """
22
+ Generate a random name for the task. This is used to create unique names for tasks.
23
+ TODO we can use unique-namer in the future, for now its just guids
24
+ """
25
+ from uuid import uuid4
26
+
27
+ return str(uuid4()) # Placeholder for actual random name generation logic
28
+
29
+
30
+ @dataclass(frozen=True, kw_only=True)
31
+ class ActionID:
32
+ """
33
+ A class representing the ID of an Action, nested within a Run. This is used to identify a specific action on a task.
34
+ """
35
+
36
+ name: str
37
+ run_name: str | None = None
38
+ project: str | None = None
39
+ domain: str | None = None
40
+ org: str | None = None
41
+
42
+ def __post_init__(self):
43
+ if self.run_name is None:
44
+ object.__setattr__(self, "run_name", self.name)
45
+
46
+ @classmethod
47
+ def create_random(cls):
48
+ name = generate_random_name()
49
+ return cls(name=name, run_name=name)
50
+
51
+ def new_sub_action(self, name: str | None = None) -> ActionID:
52
+ """
53
+ Create a new sub-run with the given name. If name is None, a random name will be generated.
54
+ """
55
+ if name is None:
56
+ name = generate_random_name()
57
+ return replace(self, name=name)
58
+
59
+ def new_sub_action_from(self, task_name: str, input_hash: str, group: str | None) -> ActionID:
60
+ """Make a deterministic name"""
61
+ import hashlib
62
+
63
+ components = f"{self.run_name}-{self.name}-{input_hash}-{task_name}" + (f"-{group}" if group else "")
64
+ # has the components into something deterministic
65
+ bytes_digest = hashlib.md5(components.encode()).digest()
66
+ new_name = base36_encode(bytes_digest)
67
+ return self.new_sub_action(new_name)
68
+
69
+
70
+ @dataclass(frozen=True, kw_only=True)
71
+ class RawDataPath:
72
+ """
73
+ A class representing the raw data path for a task. This is used to store the raw data for the task execution and
74
+ also get mutations on the path.
75
+ """
76
+
77
+ path: str
78
+
79
+ @classmethod
80
+ def from_local_folder(cls, local_folder: str | pathlib.Path | None = None) -> RawDataPath:
81
+ """
82
+ Create a new context attribute object, with local path given. Will be created if it doesn't exist.
83
+ :return: Path to the temporary directory
84
+ """
85
+ match local_folder:
86
+ case pathlib.Path():
87
+ local_folder.mkdir(parents=True, exist_ok=True)
88
+ return RawDataPath(path=str(local_folder))
89
+ case None:
90
+ # Create a temporary directory for data storage
91
+ p = tempfile.mkdtemp()
92
+ logger.debug(f"Creating temporary directory for data storage: {p}")
93
+ return RawDataPath(path=p)
94
+ case str():
95
+ return RawDataPath(path=local_folder)
96
+ case _:
97
+ raise ValueError(f"Invalid local path {local_folder}")
98
+
99
+ def get_random_remote_path(self, file_name: Optional[str] = None) -> str:
100
+ """
101
+ Returns a random path for uploading a file/directory to.
102
+
103
+ :param file_name: If given, will be joined after a randomly generated portion.
104
+ :return:
105
+ """
106
+ import random
107
+ from uuid import UUID
108
+
109
+ import fsspec
110
+ from fsspec.utils import get_protocol
111
+
112
+ random_string = UUID(int=random.getrandbits(128)).hex
113
+ file_prefix = self.path
114
+
115
+ protocol = get_protocol(file_prefix)
116
+ if "file" in protocol:
117
+ local_path = pathlib.Path(file_prefix) / random_string
118
+ if file_name:
119
+ # Only if file name is given do we create the parent, because it may be needed as a folder otherwise
120
+ local_path = local_path / file_name
121
+ if not local_path.exists():
122
+ local_path.parent.mkdir(exist_ok=True, parents=True)
123
+ local_path.touch()
124
+ return str(local_path.absolute())
125
+
126
+ fs = fsspec.filesystem(protocol)
127
+ if file_prefix.endswith(fs.sep):
128
+ file_prefix = file_prefix[:-1]
129
+ remote_path = fs.sep.join([file_prefix, random_string])
130
+ if file_name:
131
+ remote_path = fs.sep.join([remote_path, file_name])
132
+ return remote_path
133
+
134
+
135
+ @dataclass(frozen=True)
136
+ class GroupData:
137
+ name: str
138
+
139
+
140
+ @dataclass(frozen=True, kw_only=True)
141
+ class TaskContext:
142
+ """
143
+ A context class to hold the current task executions context.
144
+ This can be used to access various contextual parameters in the task execution by the user.
145
+
146
+ :param action: The action ID of the current execution. This is always set, within a run.
147
+ :param version: The version of the executed task. This is set when the task is executed by an action and will be
148
+ set on all sub-actions.
149
+ """
150
+
151
+ action: ActionID
152
+ version: str
153
+ raw_data_path: RawDataPath
154
+ output_path: str
155
+ run_base_dir: str
156
+ report: Report
157
+ group_data: GroupData | None = None
158
+ checkpoints: Checkpoints | None = None
159
+ code_bundle: CodeBundle | None = None
160
+ compiled_image_cache: ImageCache | None = None
161
+ data: Dict[str, Any] = field(default_factory=dict)
162
+
163
+ def replace(self, **kwargs) -> TaskContext:
164
+ if "data" in kwargs:
165
+ rec_data = kwargs.pop("data")
166
+ if rec_data is None:
167
+ return replace(self, **kwargs)
168
+ data = {}
169
+ if self.data is not None:
170
+ data = self.data.copy()
171
+ data.update(rec_data)
172
+ kwargs.update({"data": data})
173
+ return replace(self, **kwargs)
174
+
175
+ def __getitem__(self, key: str) -> Optional[Any]:
176
+ return self.data.get(key)
177
+
178
+
179
+ @dataclass(frozen=True, kw_only=True)
180
+ class CodeBundle:
181
+ """
182
+ A class representing a code bundle for a task. This is used to package the code and the inflation path.
183
+ The code bundle computes the version of the code using the hash of the code.
184
+
185
+ :param computed_version: The version of the code bundle. This is the hash of the code.
186
+ :param destination: The destination path for the code bundle to be inflated to.
187
+ :param tgz: Optional path to the tgz file.
188
+ :param pkl: Optional path to the pkl file.
189
+ :param downloaded_path: The path to the downloaded code bundle. This is only available during runtime, when
190
+ the code bundle has been downloaded and inflated.
191
+ """
192
+
193
+ computed_version: str
194
+ destination: str = "."
195
+ tgz: str | None = None
196
+ pkl: str | None = None
197
+ downloaded_path: pathlib.Path | None = None
198
+
199
+ # runtime_dependencies: Tuple[str, ...] = field(default_factory=tuple) In the future if we want we could add this
200
+ # but this messes up actors, spark etc
201
+
202
+ def __post_init__(self):
203
+ if self.tgz is None and self.pkl is None:
204
+ raise ValueError("Either tgz or pkl must be provided")
205
+
206
+ def with_downloaded_path(self, path: pathlib.Path) -> CodeBundle:
207
+ """
208
+ Create a new CodeBundle with the given downloaded path.
209
+ """
210
+ return replace(self, downloaded_path=path)
211
+
212
+
213
+ @dataclass(frozen=True)
214
+ class Checkpoints:
215
+ """
216
+ A class representing the checkpoints for a task. This is used to store the checkpoints for the task execution.
217
+ """
218
+
219
+ prev_checkpoint_path: str | None
220
+ checkpoint_path: str | None
221
+
222
+
223
+ @dataclass(frozen=True)
224
+ class NativeInterface:
225
+ """
226
+ A class representing the native interface for a task. This is used to interact with the task and its execution
227
+ context.
228
+ """
229
+
230
+ inputs: Dict[str, Tuple[Type, Any]]
231
+ outputs: Dict[str, Type]
232
+ docstring: Optional[Docstring] = field(default=None)
233
+
234
+ def has_outputs(self) -> bool:
235
+ """
236
+ Check if the task has outputs. This is used to determine if the task has outputs or not.
237
+ """
238
+ return self.outputs is not None and len(self.outputs) > 0
239
+
240
+ @classmethod
241
+ def from_types(cls, inputs: Dict[str, Type], outputs: Dict[str, Type]) -> NativeInterface:
242
+ """
243
+ Create a new NativeInterface from the given types. This is used to create a native interface for the task.
244
+ """
245
+ return cls(inputs={k: (v, inspect.Parameter.empty) for k, v in inputs.items()}, outputs=outputs)
246
+
247
+ @classmethod
248
+ def from_callable(cls, func: Callable) -> NativeInterface:
249
+ """
250
+ Extract the native interface from the given function. This is used to create a native interface for the task.
251
+ """
252
+ sig = inspect.signature(func)
253
+
254
+ # Extract parameter details (name, type, default value)
255
+ param_info = {name: (param.annotation, param.default) for name, param in sig.parameters.items()}
256
+
257
+ # Get return type
258
+ outputs = extract_return_annotation(sig.return_annotation)
259
+ return cls(inputs=param_info, outputs=outputs)
260
+
261
+ def convert_to_kwargs(self, *args, **kwargs) -> Dict[str, Any]:
262
+ """
263
+ Convert the given arguments to keyword arguments based on the native interface. This is used to convert the
264
+ arguments to the correct types for the task execution.
265
+ """
266
+ # Convert positional arguments to keyword arguments
267
+ if len(args) > len(self.inputs):
268
+ raise ValueError(f"Too many positional arguments provided, inputs {self.inputs.keys()}, args {len(args)}")
269
+ for arg, input_name in zip(args, self.inputs.keys()):
270
+ kwargs[input_name] = arg
271
+ return kwargs
272
+
273
+ def get_input_types(self) -> Dict[str, Type]:
274
+ """
275
+ Get the input types for the task. This is used to get the types of the inputs for the task execution.
276
+ """
277
+ return {k: v[0] for k, v in self.inputs.items()}
278
+
279
+ def __repr__(self):
280
+ """
281
+ Returns a string representation of the task interface.
282
+ """
283
+ i = "("
284
+ if self.inputs:
285
+ initial = True
286
+ for key, tpe in self.inputs.items():
287
+ if not initial:
288
+ i += ", "
289
+ initial = False
290
+ tp = tpe[0] if isinstance(tpe[0], str) else tpe[0].__name__
291
+ i += f"{key}: {tp}"
292
+ if tpe[1] is not inspect.Parameter.empty:
293
+ i += f" = {tpe[1]}"
294
+ i += ")"
295
+ if self.outputs:
296
+ initial = True
297
+ multi = len(self.outputs) > 1
298
+ i += " -> "
299
+ if multi:
300
+ i += "("
301
+ for key, tpe in self.outputs.items():
302
+ if not initial:
303
+ i += ", "
304
+ initial = False
305
+ tp = tpe.__name__ if isinstance(tpe, type) else tpe
306
+ i += f"{key}: {tp}"
307
+ if multi:
308
+ i += ")"
309
+ return i + ":"
310
+
311
+
312
+ @dataclass
313
+ class SerializationContext:
314
+ """
315
+ This object holds serialization time contextual information, that can be used when serializing the task and
316
+ various parameters of a tasktemplate. This is only available when the task is being serialized and can be
317
+ during a deployment or runtime.
318
+
319
+ :param version: The version of the task
320
+ :param code_bundle: The code bundle for the task. This is used to package the code and the inflation path.
321
+ :param input_path: The path to the inputs for the task. This is used to determine where the inputs will be located
322
+ :param output_path: The path to the outputs for the task. This is used to determine where the outputs will be
323
+ located
324
+ """
325
+
326
+ version: str
327
+ project: str | None = None
328
+ domain: str | None = None
329
+ org: str | None = None
330
+ code_bundle: Optional[CodeBundle] = None
331
+ input_path: str = "{{.input}}"
332
+ output_path: str = "{{.outputPrefix}}"
333
+ _entrypoint_path: str = field(default="_bin/runtime.py", init=False)
334
+ image_cache: ImageCache | None = None
335
+ root_dir: Optional[pathlib.Path] = None
336
+
337
+ def get_entrypoint_path(self, interpreter_path: str) -> str:
338
+ """
339
+ Get the entrypoint path for the task. This is used to determine the entrypoint for the task execution.
340
+ :param interpreter_path: The path to the interpreter (python)
341
+ """
342
+ return os.path.join(os.path.dirname(os.path.dirname(interpreter_path)), self._entrypoint_path)
flyte/_deploy.py ADDED
@@ -0,0 +1,202 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
6
+
7
+ import rich.repr
8
+
9
+ from ._api_commons import syncer
10
+ from ._datastructures import SerializationContext
11
+ from ._environment import Environment
12
+ from ._image import Image
13
+ from ._initialize import get_client, get_common_config, requires_client, requires_initialization
14
+ from ._logging import logger
15
+ from ._task import TaskTemplate
16
+ from ._task_environment import TaskEnvironment
17
+
18
+ if TYPE_CHECKING:
19
+ from flyte._protos.workflow import task_definition_pb2
20
+
21
+ from ._code_bundle import CopyFiles
22
+ from ._internal.imagebuild.image_builder import ImageCache
23
+
24
+
25
+ @rich.repr.auto
26
+ @dataclass
27
+ class DeploymentPlan:
28
+ envs: Dict[str, Environment]
29
+ version: Optional[str] = None
30
+
31
+
32
+ @rich.repr.auto
33
+ @dataclass
34
+ class Deployment:
35
+ envs: Dict[str, Environment]
36
+ deployed_tasks: List[task_definition_pb2.TaskSpec] | None = None
37
+
38
+ def summary_repr(self) -> str:
39
+ """
40
+ Returns a summary representation of the deployment.
41
+ """
42
+ env_names = ", ".join(self.envs.keys())
43
+ task_names_versions = ", ".join(
44
+ f"{task.task_template.id.name} (v{task.task_template.id.version})" for task in self.deployed_tasks or []
45
+ )
46
+ return f"Deployment(envs=[{env_names}], tasks=[{task_names_versions}])"
47
+
48
+
49
+ @requires_client
50
+ async def _deploy_task(
51
+ task: TaskTemplate, serialization_context: SerializationContext, dryrun: bool = False
52
+ ) -> task_definition_pb2.TaskSpec:
53
+ """
54
+ Deploy the given task.
55
+ """
56
+ from ._internal.runtime.task_serde import translate_task_to_wire
57
+ from ._protos.workflow import task_definition_pb2, task_service_pb2
58
+
59
+ image_uri = task.image.uri if isinstance(task.image, Image) else task.image
60
+
61
+ spec = translate_task_to_wire(task, serialization_context)
62
+ if dryrun:
63
+ return spec
64
+ msg = f"Deploying task {task.name}, with image {image_uri} version {serialization_context.version}"
65
+ if spec.task_template.HasField("container") and spec.task_template.container.args:
66
+ msg += f" from {spec.task_template.container.args[-3]}.{spec.task_template.container.args[-1]}"
67
+ logger.info(msg)
68
+ task_id = task_definition_pb2.TaskIdentifier(
69
+ org=spec.task_template.id.org,
70
+ project=spec.task_template.id.project,
71
+ domain=spec.task_template.id.domain,
72
+ version=spec.task_template.id.version,
73
+ name=spec.task_template.id.name,
74
+ )
75
+
76
+ await get_client().task_service.DeployTask(task_service_pb2.DeployTaskRequest(task_id=task_id, spec=spec))
77
+ logger.info(f"Deployed task {task.name} with version {task_id.version}")
78
+ return spec
79
+
80
+
81
+ async def _build_image_bg(env_name: str, image: Image) -> Tuple[str, str]:
82
+ """
83
+ Build the image in the background and return the environment name and the built image.
84
+ """
85
+ from ._build import build
86
+
87
+ logger.info(f"Building image {image.name} for environment {env_name}")
88
+ return env_name, await build.aio(image)
89
+
90
+
91
+ async def build_images(deployment: DeploymentPlan) -> ImageCache:
92
+ """
93
+ Build the images for the given deployment plan and update the environment with the built image.
94
+ """
95
+ from ._internal.imagebuild.image_builder import ImageCache
96
+
97
+ images = []
98
+ image_identifier_map = {}
99
+ for env_name, env in deployment.envs.items():
100
+ if not isinstance(env.image, str):
101
+ logger.info(f"Building Image for environment {env_name}, image: {env.image}")
102
+ images.append(_build_image_bg(env_name, env.image))
103
+
104
+ elif env.image == "auto" and "auto" not in image_identifier_map:
105
+ auto_image = Image.auto()
106
+ image_identifier_map["auto"] = auto_image.uri
107
+ final_images = await asyncio.gather(*images)
108
+
109
+ for env_name, image_uri in final_images:
110
+ logger.info(f"Built Image for environment {env_name}, image: {image_uri}")
111
+ env = deployment.envs[env_name]
112
+ if isinstance(env.image, Image):
113
+ image_identifier_map[env.image.identifier] = env.image.uri
114
+
115
+ return ImageCache(image_lookup=image_identifier_map)
116
+
117
+
118
+ @requires_initialization
119
+ async def apply(deployment: DeploymentPlan, copy_style: CopyFiles, dryrun: bool = False) -> Deployment:
120
+ from ._code_bundle import build_code_bundle
121
+
122
+ cfg = get_common_config()
123
+ image_cache = await build_images(deployment)
124
+ if copy_style == "none":
125
+ code_bundle = None
126
+ assert deployment.version is not None, "Version must be set when copy_style is none"
127
+ else:
128
+ code_bundle = await build_code_bundle(from_dir=cfg.root_dir, dryrun=dryrun, copy_style=copy_style)
129
+ deployment.version = code_bundle.computed_version
130
+
131
+ sc = SerializationContext(
132
+ project=cfg.project,
133
+ domain=cfg.domain,
134
+ org=cfg.org,
135
+ code_bundle=code_bundle,
136
+ version=deployment.version,
137
+ image_cache=image_cache,
138
+ root_dir=cfg.root_dir,
139
+ )
140
+
141
+ tasks = []
142
+ for env_name, env in deployment.envs.items():
143
+ logger.info(f"Deploying environment {env_name}")
144
+ if isinstance(env, TaskEnvironment):
145
+ for task in env.tasks.values():
146
+ tasks.append(_deploy_task(task, dryrun=dryrun, serialization_context=sc))
147
+ return Deployment(envs=deployment.envs, deployed_tasks=await asyncio.gather(*tasks))
148
+
149
+
150
+ def _recursive_discover(
151
+ planned_envs: Dict[str, Environment], envs: Environment | List[Environment]
152
+ ) -> Dict[str, Environment]:
153
+ """
154
+ Recursively deploy the environment and its dependencies, if not already deployed (present in env_tasks) and
155
+ return the updated env_tasks.
156
+ """
157
+ if isinstance(envs, Environment):
158
+ envs = [envs]
159
+ for env in envs:
160
+ # Skip if the environment is already planned
161
+ if env.name in planned_envs:
162
+ continue
163
+ # Recursively discover dependent environments
164
+ for dependent_env in env.env_dep_hints:
165
+ _recursive_discover(planned_envs, dependent_env)
166
+ # Add the environment to the existing envs
167
+ planned_envs[env.name] = env
168
+ return planned_envs
169
+
170
+
171
+ def plan_deploy(*envs: Environment, version: Optional[str] = None) -> DeploymentPlan:
172
+ if envs is None:
173
+ return DeploymentPlan({})
174
+ planned_envs = _recursive_discover({}, *envs)
175
+ return DeploymentPlan(planned_envs, version=version)
176
+
177
+
178
+ @syncer.wrap
179
+ async def deploy(
180
+ *envs: Environment,
181
+ dryrun: bool = False,
182
+ version: str | None = None,
183
+ interactive_mode: bool | None = None,
184
+ copy_style: CopyFiles = "loaded_modules",
185
+ ) -> Deployment:
186
+ """
187
+ Deploy the given environment or list of environments.
188
+ :param envs: Environment or list of environments to deploy.
189
+ :param dryrun: dryrun mode, if True, the deployment will not be applied to the control plane.
190
+ :param version: version of the deployment, if None, the version will be computed from the code bundle.
191
+ TODO: Support for interactive_mode
192
+ :param interactive_mode: Optional, can be forced to True or False.
193
+ If not provided, it will be set based on the current environment. For example Jupyter notebooks are considered
194
+ interactive mode, while scripts are not. This is used to determine how the code bundle is created.
195
+ :param copy_style: Copy style to use when running the task
196
+
197
+ :return: Deployment object containing the deployed environments and tasks.
198
+ """
199
+ if interactive_mode:
200
+ raise NotImplementedError("Interactive mode not yet implemented for deployment")
201
+ deployment = plan_deploy(*envs, version=version)
202
+ return await apply(deployment, copy_style=copy_style, dryrun=dryrun)
flyte/_doc.py ADDED
@@ -0,0 +1,29 @@
1
+ import inspect
2
+ from dataclasses import dataclass
3
+ from typing import Callable, Optional
4
+
5
+
6
+ @dataclass
7
+ class Documentation:
8
+ """
9
+ This class is used to store the documentation of a task.
10
+
11
+ It can be set explicitly or extracted from the docstring of the task.
12
+ """
13
+
14
+ description: str
15
+
16
+ def __help__str__(self):
17
+ return self.description
18
+
19
+
20
+ def extract_docstring(func: Optional[Callable]) -> Documentation:
21
+ """
22
+ Extracts the description from a docstring.
23
+ """
24
+ if func is None:
25
+ return Documentation(description="")
26
+ docstring = inspect.getdoc(func)
27
+ if not docstring:
28
+ return Documentation(description="")
29
+ return Documentation(description=docstring)
flyte/_docstring.py ADDED
@@ -0,0 +1,32 @@
1
+ from typing import TYPE_CHECKING, Callable, Dict, Optional
2
+
3
+ if TYPE_CHECKING:
4
+ pass
5
+
6
+
7
+ class Docstring(object):
8
+ def __init__(self, docstring: Optional[str] = None, callable_: Optional[Callable] = None):
9
+ import docstring_parser
10
+
11
+ self._parsed_docstring: docstring_parser.Docstring
12
+
13
+ if docstring is not None:
14
+ self._parsed_docstring = docstring_parser.parse(docstring)
15
+ elif callable_.__doc__ is not None:
16
+ self._parsed_docstring = docstring_parser.parse(callable_.__doc__)
17
+
18
+ @property
19
+ def input_descriptions(self) -> Dict[str, Optional[str]]:
20
+ return {p.arg_name: p.description for p in self._parsed_docstring.params}
21
+
22
+ @property
23
+ def output_descriptions(self) -> Dict[str, Optional[str]]:
24
+ return {p.return_name: p.description for p in self._parsed_docstring.many_returns if p.return_name is not None}
25
+
26
+ @property
27
+ def short_description(self) -> Optional[str]:
28
+ return self._parsed_docstring.short_description
29
+
30
+ @property
31
+ def long_description(self) -> Optional[str]:
32
+ return self._parsed_docstring.long_description
flyte/_environment.py ADDED
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Union
5
+
6
+ import rich.repr
7
+
8
+ from flyte._secret import SecretRequest
9
+
10
+ from ._image import Image
11
+ from ._resources import Resources
12
+
13
+ if TYPE_CHECKING:
14
+ from kubernetes.client import V1PodTemplate
15
+
16
+
17
+ @rich.repr.auto
18
+ @dataclass(init=True, repr=True)
19
+ class Environment:
20
+ """
21
+ :param name: Name of the environment
22
+ :param image: Docker image to use for the environment. If set to "auto", will use the default image.
23
+ :param resources: Resources to allocate for the environment.
24
+ :param env: Environment variables to set for the environment.
25
+ :param secrets: Secrets to inject into the environment.
26
+ :param env_dep_hints: Environment dependencies to hint, so when you deploy the environment, the dependencies are
27
+ also deployed. This is useful when you have a set of environments that depend on each other.
28
+ """
29
+
30
+ name: str
31
+ env_dep_hints: List[Environment] = field(default_factory=list)
32
+ pod_template: Optional[Union[str, "V1PodTemplate"]] = None
33
+ description: Optional[str] = None
34
+ secrets: Optional[SecretRequest] = None
35
+ env: Optional[Dict[str, str]] = None
36
+ resources: Optional[Resources] = None
37
+ image: Union[str, Image, Literal["auto"]] = "auto"
38
+
39
+ def add_dependency(self, *env: Environment):
40
+ """
41
+ Add a dependency to the environment.
42
+ """
43
+ self.env_dep_hints.extend(env)
flyte/_group.py ADDED
@@ -0,0 +1,31 @@
1
+ from contextlib import contextmanager
2
+
3
+ from ._context import internal_ctx
4
+ from ._datastructures import GroupData
5
+
6
+
7
+ @contextmanager
8
+ def group(name: str):
9
+ """
10
+ Create a new group with the given name. The method is intended to be used as a context manager.
11
+
12
+ Example:
13
+ ```python
14
+ @task
15
+ async def my_task():
16
+ ...
17
+ with group("my_group"):
18
+ t1(x,y) # tasks in this block will be grouped under "my_group"
19
+ ...
20
+ ```
21
+
22
+ :param name: The name of the group
23
+ """
24
+ ctx = internal_ctx()
25
+ if ctx.data.task_context is None:
26
+ yield
27
+ return
28
+ tctx = ctx.data.task_context
29
+ new_tctx = tctx.replace(group_data=GroupData(name))
30
+ with ctx.replace_task_context(new_tctx):
31
+ yield