flyte 2.0.0b32__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flyte might be problematic. Click here for more details.

Files changed (204) hide show
  1. flyte/__init__.py +108 -0
  2. flyte/_bin/__init__.py +0 -0
  3. flyte/_bin/debug.py +38 -0
  4. flyte/_bin/runtime.py +195 -0
  5. flyte/_bin/serve.py +178 -0
  6. flyte/_build.py +26 -0
  7. flyte/_cache/__init__.py +12 -0
  8. flyte/_cache/cache.py +147 -0
  9. flyte/_cache/defaults.py +9 -0
  10. flyte/_cache/local_cache.py +216 -0
  11. flyte/_cache/policy_function_body.py +42 -0
  12. flyte/_code_bundle/__init__.py +8 -0
  13. flyte/_code_bundle/_ignore.py +121 -0
  14. flyte/_code_bundle/_packaging.py +218 -0
  15. flyte/_code_bundle/_utils.py +347 -0
  16. flyte/_code_bundle/bundle.py +266 -0
  17. flyte/_constants.py +1 -0
  18. flyte/_context.py +155 -0
  19. flyte/_custom_context.py +73 -0
  20. flyte/_debug/__init__.py +0 -0
  21. flyte/_debug/constants.py +38 -0
  22. flyte/_debug/utils.py +17 -0
  23. flyte/_debug/vscode.py +307 -0
  24. flyte/_deploy.py +408 -0
  25. flyte/_deployer.py +109 -0
  26. flyte/_doc.py +29 -0
  27. flyte/_docstring.py +32 -0
  28. flyte/_environment.py +122 -0
  29. flyte/_excepthook.py +37 -0
  30. flyte/_group.py +32 -0
  31. flyte/_hash.py +8 -0
  32. flyte/_image.py +1055 -0
  33. flyte/_initialize.py +628 -0
  34. flyte/_interface.py +119 -0
  35. flyte/_internal/__init__.py +3 -0
  36. flyte/_internal/controllers/__init__.py +129 -0
  37. flyte/_internal/controllers/_local_controller.py +239 -0
  38. flyte/_internal/controllers/_trace.py +48 -0
  39. flyte/_internal/controllers/remote/__init__.py +58 -0
  40. flyte/_internal/controllers/remote/_action.py +211 -0
  41. flyte/_internal/controllers/remote/_client.py +47 -0
  42. flyte/_internal/controllers/remote/_controller.py +583 -0
  43. flyte/_internal/controllers/remote/_core.py +465 -0
  44. flyte/_internal/controllers/remote/_informer.py +381 -0
  45. flyte/_internal/controllers/remote/_service_protocol.py +50 -0
  46. flyte/_internal/imagebuild/__init__.py +3 -0
  47. flyte/_internal/imagebuild/docker_builder.py +706 -0
  48. flyte/_internal/imagebuild/image_builder.py +277 -0
  49. flyte/_internal/imagebuild/remote_builder.py +386 -0
  50. flyte/_internal/imagebuild/utils.py +78 -0
  51. flyte/_internal/resolvers/__init__.py +0 -0
  52. flyte/_internal/resolvers/_task_module.py +21 -0
  53. flyte/_internal/resolvers/common.py +31 -0
  54. flyte/_internal/resolvers/default.py +28 -0
  55. flyte/_internal/runtime/__init__.py +0 -0
  56. flyte/_internal/runtime/convert.py +486 -0
  57. flyte/_internal/runtime/entrypoints.py +204 -0
  58. flyte/_internal/runtime/io.py +188 -0
  59. flyte/_internal/runtime/resources_serde.py +152 -0
  60. flyte/_internal/runtime/reuse.py +125 -0
  61. flyte/_internal/runtime/rusty.py +193 -0
  62. flyte/_internal/runtime/task_serde.py +362 -0
  63. flyte/_internal/runtime/taskrunner.py +209 -0
  64. flyte/_internal/runtime/trigger_serde.py +160 -0
  65. flyte/_internal/runtime/types_serde.py +54 -0
  66. flyte/_keyring/__init__.py +0 -0
  67. flyte/_keyring/file.py +115 -0
  68. flyte/_logging.py +300 -0
  69. flyte/_map.py +312 -0
  70. flyte/_module.py +72 -0
  71. flyte/_pod.py +30 -0
  72. flyte/_resources.py +473 -0
  73. flyte/_retry.py +32 -0
  74. flyte/_reusable_environment.py +102 -0
  75. flyte/_run.py +724 -0
  76. flyte/_secret.py +96 -0
  77. flyte/_task.py +550 -0
  78. flyte/_task_environment.py +316 -0
  79. flyte/_task_plugins.py +47 -0
  80. flyte/_timeout.py +47 -0
  81. flyte/_tools.py +27 -0
  82. flyte/_trace.py +119 -0
  83. flyte/_trigger.py +1000 -0
  84. flyte/_utils/__init__.py +30 -0
  85. flyte/_utils/asyn.py +121 -0
  86. flyte/_utils/async_cache.py +139 -0
  87. flyte/_utils/coro_management.py +27 -0
  88. flyte/_utils/docker_credentials.py +173 -0
  89. flyte/_utils/file_handling.py +72 -0
  90. flyte/_utils/helpers.py +134 -0
  91. flyte/_utils/lazy_module.py +54 -0
  92. flyte/_utils/module_loader.py +104 -0
  93. flyte/_utils/org_discovery.py +57 -0
  94. flyte/_utils/uv_script_parser.py +49 -0
  95. flyte/_version.py +34 -0
  96. flyte/app/__init__.py +22 -0
  97. flyte/app/_app_environment.py +157 -0
  98. flyte/app/_deploy.py +125 -0
  99. flyte/app/_input.py +160 -0
  100. flyte/app/_runtime/__init__.py +3 -0
  101. flyte/app/_runtime/app_serde.py +347 -0
  102. flyte/app/_types.py +101 -0
  103. flyte/app/extras/__init__.py +3 -0
  104. flyte/app/extras/_fastapi.py +151 -0
  105. flyte/cli/__init__.py +12 -0
  106. flyte/cli/_abort.py +28 -0
  107. flyte/cli/_build.py +114 -0
  108. flyte/cli/_common.py +468 -0
  109. flyte/cli/_create.py +371 -0
  110. flyte/cli/_delete.py +45 -0
  111. flyte/cli/_deploy.py +293 -0
  112. flyte/cli/_gen.py +176 -0
  113. flyte/cli/_get.py +370 -0
  114. flyte/cli/_option.py +33 -0
  115. flyte/cli/_params.py +554 -0
  116. flyte/cli/_plugins.py +209 -0
  117. flyte/cli/_run.py +597 -0
  118. flyte/cli/_serve.py +64 -0
  119. flyte/cli/_update.py +37 -0
  120. flyte/cli/_user.py +17 -0
  121. flyte/cli/main.py +221 -0
  122. flyte/config/__init__.py +3 -0
  123. flyte/config/_config.py +248 -0
  124. flyte/config/_internal.py +73 -0
  125. flyte/config/_reader.py +225 -0
  126. flyte/connectors/__init__.py +11 -0
  127. flyte/connectors/_connector.py +270 -0
  128. flyte/connectors/_server.py +197 -0
  129. flyte/connectors/utils.py +135 -0
  130. flyte/errors.py +243 -0
  131. flyte/extend.py +19 -0
  132. flyte/extras/__init__.py +5 -0
  133. flyte/extras/_container.py +286 -0
  134. flyte/git/__init__.py +3 -0
  135. flyte/git/_config.py +21 -0
  136. flyte/io/__init__.py +29 -0
  137. flyte/io/_dataframe/__init__.py +131 -0
  138. flyte/io/_dataframe/basic_dfs.py +223 -0
  139. flyte/io/_dataframe/dataframe.py +1026 -0
  140. flyte/io/_dir.py +910 -0
  141. flyte/io/_file.py +914 -0
  142. flyte/io/_hashing_io.py +342 -0
  143. flyte/models.py +479 -0
  144. flyte/py.typed +0 -0
  145. flyte/remote/__init__.py +35 -0
  146. flyte/remote/_action.py +738 -0
  147. flyte/remote/_app.py +57 -0
  148. flyte/remote/_client/__init__.py +0 -0
  149. flyte/remote/_client/_protocols.py +189 -0
  150. flyte/remote/_client/auth/__init__.py +12 -0
  151. flyte/remote/_client/auth/_auth_utils.py +14 -0
  152. flyte/remote/_client/auth/_authenticators/__init__.py +0 -0
  153. flyte/remote/_client/auth/_authenticators/base.py +403 -0
  154. flyte/remote/_client/auth/_authenticators/client_credentials.py +73 -0
  155. flyte/remote/_client/auth/_authenticators/device_code.py +117 -0
  156. flyte/remote/_client/auth/_authenticators/external_command.py +79 -0
  157. flyte/remote/_client/auth/_authenticators/factory.py +200 -0
  158. flyte/remote/_client/auth/_authenticators/pkce.py +516 -0
  159. flyte/remote/_client/auth/_channel.py +213 -0
  160. flyte/remote/_client/auth/_client_config.py +85 -0
  161. flyte/remote/_client/auth/_default_html.py +32 -0
  162. flyte/remote/_client/auth/_grpc_utils/__init__.py +0 -0
  163. flyte/remote/_client/auth/_grpc_utils/auth_interceptor.py +288 -0
  164. flyte/remote/_client/auth/_grpc_utils/default_metadata_interceptor.py +151 -0
  165. flyte/remote/_client/auth/_keyring.py +152 -0
  166. flyte/remote/_client/auth/_token_client.py +260 -0
  167. flyte/remote/_client/auth/errors.py +16 -0
  168. flyte/remote/_client/controlplane.py +128 -0
  169. flyte/remote/_common.py +30 -0
  170. flyte/remote/_console.py +19 -0
  171. flyte/remote/_data.py +161 -0
  172. flyte/remote/_logs.py +185 -0
  173. flyte/remote/_project.py +88 -0
  174. flyte/remote/_run.py +386 -0
  175. flyte/remote/_secret.py +142 -0
  176. flyte/remote/_task.py +527 -0
  177. flyte/remote/_trigger.py +306 -0
  178. flyte/remote/_user.py +33 -0
  179. flyte/report/__init__.py +3 -0
  180. flyte/report/_report.py +182 -0
  181. flyte/report/_template.html +124 -0
  182. flyte/storage/__init__.py +36 -0
  183. flyte/storage/_config.py +237 -0
  184. flyte/storage/_parallel_reader.py +274 -0
  185. flyte/storage/_remote_fs.py +34 -0
  186. flyte/storage/_storage.py +456 -0
  187. flyte/storage/_utils.py +5 -0
  188. flyte/syncify/__init__.py +56 -0
  189. flyte/syncify/_api.py +375 -0
  190. flyte/types/__init__.py +52 -0
  191. flyte/types/_interface.py +40 -0
  192. flyte/types/_pickle.py +145 -0
  193. flyte/types/_renderer.py +162 -0
  194. flyte/types/_string_literals.py +119 -0
  195. flyte/types/_type_engine.py +2254 -0
  196. flyte/types/_utils.py +80 -0
  197. flyte-2.0.0b32.data/scripts/debug.py +38 -0
  198. flyte-2.0.0b32.data/scripts/runtime.py +195 -0
  199. flyte-2.0.0b32.dist-info/METADATA +351 -0
  200. flyte-2.0.0b32.dist-info/RECORD +204 -0
  201. flyte-2.0.0b32.dist-info/WHEEL +5 -0
  202. flyte-2.0.0b32.dist-info/entry_points.txt +7 -0
  203. flyte-2.0.0b32.dist-info/licenses/LICENSE +201 -0
  204. flyte-2.0.0b32.dist-info/top_level.txt +1 -0
flyte/syncify/_api.py ADDED
@@ -0,0 +1,375 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import atexit
5
+ import concurrent.futures
6
+ import functools
7
+ import inspect
8
+ import logging
9
+ import threading
10
+ from typing import (
11
+ Any,
12
+ AsyncIterator,
13
+ Awaitable,
14
+ Callable,
15
+ Coroutine,
16
+ Iterator,
17
+ ParamSpec,
18
+ Protocol,
19
+ TypeVar,
20
+ Union,
21
+ cast,
22
+ overload,
23
+ )
24
+
25
+ from flyte._logging import logger
26
+
27
+ P = ParamSpec("P")
28
+ R_co = TypeVar("R_co", covariant=True)
29
+ T = TypeVar("T")
30
+
31
+
32
+ class SyncFunction(Protocol[P, R_co]):
33
+ """
34
+ A protocol that defines the interface for synchronous functions or methods that can be converted from asynchronous
35
+ ones.
36
+ """
37
+
38
+ def __call__(self, *args: Any, **kwargs: Any) -> R_co: ...
39
+
40
+ def aio(self, *args: Any, **kwargs: Any) -> Awaitable[R_co]: ...
41
+
42
+
43
+ class SyncGenFunction(Protocol[P, R_co]):
44
+ """
45
+ A protocol that defines the interface for synchronous functions or methods that can be converted from asynchronous
46
+ ones.
47
+ """
48
+
49
+ def __call__(self, *args: Any, **kwargs: Any) -> Iterator[R_co]: ...
50
+
51
+ def aio(self, *args: Any, **kwargs: Any) -> AsyncIterator[R_co]: ...
52
+
53
+
54
+ class _BackgroundLoop:
55
+ """
56
+ A background event loop that runs in a separate thread and used the `Syncify` decorator to run asynchronous
57
+ functions or methods synchronously.
58
+ """
59
+
60
+ def __init__(self, name: str):
61
+ self.loop = asyncio.new_event_loop()
62
+ self.thread = threading.Thread(name=name, target=self._run, daemon=True)
63
+ self.thread.start()
64
+ atexit.register(self.stop)
65
+
66
+ def _run(self):
67
+ import flyte.errors
68
+
69
+ # Set the exception handler to silence specific gRPC polling errors
70
+ self.loop.set_exception_handler(flyte.errors.silence_grpc_polling_error)
71
+ asyncio.set_event_loop(self.loop)
72
+ self.loop.run_forever()
73
+
74
+ def stop(self):
75
+ # stop the loop and wait briefly for thread to exit
76
+ self.loop.call_soon_threadsafe(self.loop.stop)
77
+ self.thread.join(timeout=1)
78
+
79
+ def is_in_loop(self) -> bool:
80
+ """
81
+ Check if the current thread is the background loop thread.
82
+ """
83
+ # If the current thread is not the background loop thread, return False
84
+ if threading.current_thread() != self.thread:
85
+ return False
86
+
87
+ if not self.thread.is_alive():
88
+ # If the thread is not alive, we cannot be in the loop
89
+ return False
90
+
91
+ # Lets get the current event loop and check if it matches the background loop
92
+ loop = None
93
+ try:
94
+ loop = asyncio.get_running_loop()
95
+ except RuntimeError:
96
+ pass
97
+
98
+ return loop == self.loop
99
+
100
+ def iterate_in_loop_sync(self, async_gen: AsyncIterator[R_co]) -> Iterator[R_co]:
101
+ # Create an iterator that pulls items from the async generator
102
+ assert self.thread.name != threading.current_thread().name, (
103
+ f"Cannot run coroutine in the same thread {self.thread.name}"
104
+ )
105
+ while True:
106
+ try:
107
+ # use __anext__() and cast to Coroutine so mypy is happy
108
+ future: concurrent.futures.Future[R_co] = asyncio.run_coroutine_threadsafe(
109
+ cast(Coroutine[Any, Any, R_co], async_gen.__anext__()),
110
+ self.loop,
111
+ )
112
+ yield future.result()
113
+ except (StopAsyncIteration, StopIteration):
114
+ break
115
+ except Exception as e:
116
+ if logger.getEffectiveLevel() > logging.DEBUG:
117
+ # If the log level is not DEBUG, we will remove the extra stack frames to avoid confusion for the
118
+ # user
119
+ # This is because the stack trace will include the Syncify wrapper and the background loop thread
120
+ tb = e.__traceback__
121
+ while tb and tb.tb_next:
122
+ if tb.tb_frame.f_code.co_name == "":
123
+ break
124
+ tb = tb.tb_next
125
+ raise e.with_traceback(tb)
126
+ # If the log level is DEBUG, we will keep the extra stack frames to help with debugging
127
+ raise e
128
+
129
+ def call_in_loop_sync(self, coro: Coroutine[Any, Any, R_co]) -> R_co | Iterator[R_co]:
130
+ """
131
+ Run the given coroutine in the background loop and return its result.
132
+ """
133
+ future: concurrent.futures.Future[R_co | AsyncIterator[R_co]] = asyncio.run_coroutine_threadsafe(
134
+ coro, self.loop
135
+ )
136
+ result = future.result()
137
+ if result is not None and hasattr(result, "__aiter__"):
138
+ # If the result is an async iterator, we need to convert it to a sync iterator
139
+ return cast(Iterator[R_co], self.iterate_in_loop_sync(cast(AsyncIterator[R_co], result)))
140
+ # Otherwise, just return the result
141
+ return result
142
+
143
+ async def iterate_in_loop(self, async_gen: AsyncIterator[R_co]) -> AsyncIterator[R_co]:
144
+ """
145
+ Run the given async iterator in the background loop and yield its results.
146
+ """
147
+ if self.is_in_loop():
148
+ # If we are already in the background loop, just return the async iterator
149
+ async for r in async_gen:
150
+ yield r
151
+ return
152
+
153
+ while True:
154
+ try:
155
+ # same replacement here for the async path
156
+ future: concurrent.futures.Future[R_co] = asyncio.run_coroutine_threadsafe(
157
+ cast(Coroutine[Any, Any, R_co], async_gen.__anext__()),
158
+ self.loop,
159
+ )
160
+ # Wrap the future in an asyncio Future to yield it in an async context
161
+ aio_future: asyncio.Future[R_co] = asyncio.wrap_future(future)
162
+ # await for the future to complete and yield its result
163
+ v = await aio_future
164
+ yield v
165
+ except StopAsyncIteration:
166
+ break
167
+ except Exception as e:
168
+ if logger.getEffectiveLevel() > logging.DEBUG:
169
+ # If the log level is not DEBUG, we will remove the extra stack frames to avoid confusion for the
170
+ # user.
171
+ # This is because the stack trace will include the Syncify wrapper and the background loop thread
172
+ tb = e.__traceback__
173
+ while tb and tb.tb_next:
174
+ if tb.tb_frame.f_code.co_name == "":
175
+ break
176
+ tb = tb.tb_next
177
+ raise e.with_traceback(tb)
178
+ # If the log level is DEBUG, we will keep the extra stack frames to help with debugging
179
+ raise e
180
+
181
+ async def aio(self, coro: Coroutine[Any, Any, R_co]) -> R_co:
182
+ """
183
+ Run the given coroutine in the background loop and return its result.
184
+ """
185
+ if self.is_in_loop():
186
+ # If we are already in the background loop, just run the coroutine
187
+ return await coro
188
+ try:
189
+ # Otherwise, run it in the background loop and wait for the result
190
+ future: concurrent.futures.Future[R_co] = asyncio.run_coroutine_threadsafe(coro, self.loop)
191
+ # Wrap the future in an asyncio Future to await it in an async context
192
+ aio_future: asyncio.Future[R_co] = asyncio.wrap_future(future)
193
+ # await for the future to complete and return its result
194
+ return await aio_future
195
+ except Exception as e:
196
+ if logger.getEffectiveLevel() > logging.DEBUG:
197
+ # If the log level is not DEBUG, we will remove the extra stack frames to avoid confusion for the user
198
+ # This is because the stack trace will include the Syncify wrapper and the background loop thread
199
+ tb = e.__traceback__
200
+ while tb and tb.tb_next:
201
+ if tb.tb_frame.f_code.co_name == "":
202
+ break
203
+ tb = tb.tb_next
204
+ raise e.with_traceback(tb)
205
+ # If the log level is DEBUG, we will keep the extra stack frames to help with debugging
206
+ raise e
207
+
208
+
209
+ class _SyncWrapper:
210
+ """
211
+ A wrapper class that the Syncify decorator uses to convert asynchronous functions or methods into synchronous ones.
212
+ """
213
+
214
+ def __init__(
215
+ self,
216
+ fn: Any,
217
+ bg_loop: _BackgroundLoop,
218
+ underlying_obj: Any = None,
219
+ ):
220
+ self.fn = fn
221
+ self._bg_loop = bg_loop
222
+ self._underlying_obj = underlying_obj
223
+
224
+ def __get__(self, instance: Any, owner: Any) -> Any:
225
+ """
226
+ This method is called when the wrapper is accessed as a method of a class instance.
227
+ :param instance:
228
+ :param owner:
229
+ :return:
230
+ """
231
+ fn: Any = self.fn
232
+ if instance is not None:
233
+ # If we have an instance, we need to bind the method to the instance (for instance methods)
234
+ fn = self.fn.__get__(instance, owner)
235
+
236
+ if instance is None and owner is not None and self._underlying_obj is not None:
237
+ # If we have an owner, we need to bind the method to the owner (for classmethods or staticmethods)
238
+ fn = self._underlying_obj.__get__(None, owner)
239
+
240
+ wrapper = _SyncWrapper(fn, bg_loop=self._bg_loop, underlying_obj=self._underlying_obj)
241
+ functools.update_wrapper(wrapper, self.fn)
242
+ return wrapper
243
+
244
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
245
+ if threading.current_thread().name == self._bg_loop.thread.name:
246
+ # If we are already in the background loop thread, we can call the function directly
247
+ raise AssertionError(
248
+ f"Deadlock detected: blocking call used in syncify thread {self._bg_loop.thread.name} "
249
+ f"when calling function {self.fn}, use .aio() if in an async call."
250
+ )
251
+ try:
252
+ # bind method if needed
253
+ coro_fn = self.fn
254
+
255
+ if inspect.isasyncgenfunction(coro_fn):
256
+ # Handle async iterator by converting to sync iterator
257
+ async_gen = coro_fn(*args, **kwargs)
258
+ return self._bg_loop.iterate_in_loop_sync(async_gen)
259
+ else:
260
+ return self._bg_loop.call_in_loop_sync(coro_fn(*args, **kwargs))
261
+ except Exception as e:
262
+ if logger.getEffectiveLevel() > logging.DEBUG:
263
+ # If the log level is not DEBUG, we will remove the extra stack frames to avoid confusion for the user
264
+ # This is because the stack trace will include the Syncify wrapper and the background loop thread
265
+ tb = e.__traceback__
266
+ while tb and tb.tb_next:
267
+ if tb.tb_frame.f_code.co_name == self.fn.__name__:
268
+ break
269
+ tb = tb.tb_next
270
+ raise e.with_traceback(tb)
271
+ # If the log level is DEBUG, we will keep the extra stack frames to help with debugging
272
+ raise e
273
+
274
+ def aio(self, *args: Any, **kwargs: Any) -> Any:
275
+ fn = self.fn
276
+
277
+ try:
278
+ if inspect.isasyncgenfunction(fn):
279
+ # If the function is an async generator, we need to handle it differently
280
+ async_iter = fn(*args, **kwargs)
281
+ return self._bg_loop.iterate_in_loop(async_iter)
282
+ else:
283
+ # If we are already in the background loop, just return the coroutine
284
+ coro = fn(*args, **kwargs)
285
+ if hasattr(coro, "__aiter__"):
286
+ # If the coroutine is an async iterator, we need to handle it differently
287
+ return self._bg_loop.iterate_in_loop(coro)
288
+ return self._bg_loop.aio(coro)
289
+ except Exception as e:
290
+ if logger.getEffectiveLevel() > logging.DEBUG:
291
+ # If the log level is not DEBUG, we will remove the extra stack frames to avoid confusion for the user
292
+ # This is because the stack trace will include the Syncify wrapper and the background loop thread
293
+ tb = e.__traceback__
294
+ while tb and tb.tb_next:
295
+ if tb.tb_frame.f_code.co_name == self.fn.__name__:
296
+ break
297
+ tb = tb.tb_next
298
+ raise e.with_traceback(tb)
299
+ # If the log level is DEBUG, we will keep the extra stack frames to help with debugging
300
+ raise e
301
+
302
+
303
+ class Syncify:
304
+ """
305
+ A decorator to convert asynchronous functions or methods into synchronous ones.
306
+
307
+ This is useful for integrating async code into synchronous contexts.
308
+
309
+ Example::
310
+
311
+ ```python
312
+ syncer = Syncify()
313
+
314
+ @syncer
315
+ async def async_function(x: str) -> str:
316
+ return f"Hello, Async World {x}!"
317
+
318
+
319
+ # now you can call it synchronously
320
+ result = async_function("Async World")
321
+ print(result)
322
+ # Output: Hello, Async World Async World!
323
+
324
+ # or call it asynchronously
325
+ async def main():
326
+ result = await async_function.aio("World")
327
+ print(result)
328
+ ```
329
+
330
+ """
331
+
332
+ def __init__(self, name: str = "flyte_syncify"):
333
+ self._bg_loop = _BackgroundLoop(name=name)
334
+
335
+ @overload
336
+ def __call__(self, func: Callable[P, Awaitable[R_co]]) -> Any: ...
337
+
338
+ # def __call__(self, func: Callable[P, Awaitable[R_co]]) -> SyncFunction[P, R_co]: ...
339
+
340
+ @overload
341
+ def __call__(self, func: Callable[P, Iterator[R_co] | AsyncIterator[R_co]]) -> SyncGenFunction[P, R_co]: ...
342
+
343
+ # def __call__(self, func: Callable[[Type[T], *P.args, *P.kwargs], Awaitable[R_co]])
344
+ # -> SyncFunction[[Type[T], *P.args, *P.kwargs], R_co]: ...
345
+ @overload
346
+ def __call__(self, func: classmethod) -> Union[SyncFunction[P, R_co], SyncGenFunction[P, R_co]]: ...
347
+
348
+ @overload
349
+ def __call__(self, func: staticmethod) -> staticmethod: ...
350
+
351
+ def __call__(self, obj):
352
+ if isinstance(obj, classmethod):
353
+ wrapper = _SyncWrapper(obj.__func__, bg_loop=self._bg_loop, underlying_obj=obj)
354
+ functools.update_wrapper(wrapper, obj.__func__)
355
+ return wrapper
356
+
357
+ if isinstance(obj, staticmethod):
358
+ fn = obj.__func__
359
+ wrapper = _SyncWrapper(fn, bg_loop=self._bg_loop)
360
+ functools.update_wrapper(wrapper, fn)
361
+ return staticmethod(wrapper)
362
+
363
+ if inspect.isasyncgenfunction(obj):
364
+ wrapper = _SyncWrapper(obj, bg_loop=self._bg_loop)
365
+ functools.update_wrapper(wrapper, obj)
366
+ return cast(Callable[P, Iterator[R_co]], wrapper)
367
+
368
+ if inspect.iscoroutinefunction(obj):
369
+ wrapper = _SyncWrapper(obj, bg_loop=self._bg_loop)
370
+ functools.update_wrapper(wrapper, obj)
371
+ return wrapper
372
+
373
+ raise TypeError(
374
+ "Syncify can only be applied to async functions, async generators, async classmethods or staticmethods."
375
+ )
@@ -0,0 +1,52 @@
1
+ """
2
+ # Flyte Type System
3
+
4
+ The Flyte type system provides a way to define, transform, and manipulate types in Flyte workflows.
5
+ Since the data flowing through Flyte has to often cross process, container and langauge boundaries, the type system
6
+ is designed to be serializable to a universal format that can be understood across different environments. This
7
+ universal format is based on Protocol Buffers. The types are called LiteralTypes and the runtime
8
+ representation of data is called Literals.
9
+
10
+ The type system includes:
11
+ - **TypeEngine**: The core engine that manages type transformations and serialization. This is the main entry point for
12
+ for all the internal type transformations and serialization logic.
13
+ - **TypeTransformer**: A class that defines how to transform one type to another. This is extensible
14
+ allowing users to define custom types and transformations.
15
+ - **Renderable**: An interface for types that can be rendered as HTML, that can be outputted to a flyte.report.
16
+
17
+ It is always possible to bypass the type system and use the `FlytePickle` type to serialize any python object
18
+ into a pickle format. The pickle format is not human-readable, but can be passed between flyte tasks that are
19
+ written in python. The Pickled objects cannot be represented in the UI, and may be in-efficient for large datasets.
20
+ """
21
+
22
+ from importlib.metadata import entry_points
23
+
24
+ from flyte._logging import logger
25
+
26
+ from ._interface import guess_interface
27
+ from ._pickle import FlytePickle
28
+ from ._renderer import Renderable
29
+ from ._string_literals import literal_string_repr
30
+ from ._type_engine import TypeEngine, TypeTransformer, TypeTransformerFailedError
31
+
32
+ __all__ = [
33
+ "FlytePickle",
34
+ "Renderable",
35
+ "TypeEngine",
36
+ "TypeTransformer",
37
+ "TypeTransformerFailedError",
38
+ "guess_interface",
39
+ "literal_string_repr",
40
+ ]
41
+
42
+
43
+ def _load_custom_type_transformers():
44
+ plugins = entry_points(group="flyte.plugins.types")
45
+ for ep in plugins:
46
+ try:
47
+ logger.info(f"Loading type transformer: {ep.name}")
48
+ loaded = ep.load()
49
+ if callable(loaded):
50
+ loaded()
51
+ except Exception as e:
52
+ logger.warning(f"Failed to load type transformer {ep.name} with error: {e}")
@@ -0,0 +1,40 @@
1
+ import inspect
2
+ from typing import Any, Dict, Iterable, Tuple, Type, cast
3
+
4
+ from flyteidl2.core import interface_pb2, literals_pb2
5
+ from flyteidl2.task import common_pb2
6
+
7
+ from flyte.models import NativeInterface
8
+
9
+
10
+ def guess_interface(
11
+ interface: interface_pb2.TypedInterface, default_inputs: Iterable[common_pb2.NamedParameter] | None = None
12
+ ) -> NativeInterface:
13
+ """
14
+ Returns the interface of the task with guessed types, as types may not be present in current env.
15
+ """
16
+ import flyte.types
17
+
18
+ if interface is None:
19
+ return NativeInterface({}, {})
20
+
21
+ default_input_literals: Dict[str, literals_pb2.Literal] = {}
22
+ if default_inputs is not None:
23
+ for param in default_inputs:
24
+ if param.parameter.HasField("default"):
25
+ default_input_literals[param.name] = param.parameter.default
26
+
27
+ guessed_inputs: Dict[str, Tuple[Type[Any], Any] | Any] = {}
28
+ if interface.inputs is not None and len(interface.inputs.variables) > 0:
29
+ input_types = flyte.types.TypeEngine.guess_python_types(cast(dict, interface.inputs.variables))
30
+ for name, t in input_types.items():
31
+ if name not in default_input_literals:
32
+ guessed_inputs[name] = (t, inspect.Parameter.empty)
33
+ else:
34
+ guessed_inputs[name] = (t, NativeInterface.has_default)
35
+
36
+ guessed_outputs: Dict[str, Type[Any]] = {}
37
+ if interface.outputs is not None and len(interface.outputs.variables) > 0:
38
+ guessed_outputs = flyte.types.TypeEngine.guess_python_types(cast(dict, interface.outputs.variables))
39
+
40
+ return NativeInterface.from_types(guessed_inputs, guessed_outputs, default_input_literals)
flyte/types/_pickle.py ADDED
@@ -0,0 +1,145 @@
1
+ import hashlib
2
+ import os
3
+ import sys
4
+ import typing
5
+ from typing import Type
6
+
7
+ import aiofiles
8
+ import cloudpickle
9
+ from flyteidl2.core import literals_pb2, types_pb2
10
+
11
+ import flyte.storage as storage
12
+
13
+ from ._type_engine import TypeEngine, TypeTransformer
14
+
15
+ T = typing.TypeVar("T")
16
+ DEFAULT_PICKLE_BYTES_LIMIT = 2**10 * 10 # 10KB
17
+
18
+
19
+ class FlytePickle(typing.Generic[T]):
20
+ """
21
+ This type is only used by flytekit internally. User should not use this type.
22
+ Any type that flyte can't recognize will become FlytePickle
23
+ """
24
+
25
+ @classmethod
26
+ def python_type(cls) -> typing.Type:
27
+ return type(None)
28
+
29
+ @classmethod
30
+ def __class_getitem__(cls, python_type: typing.Type) -> typing.Type:
31
+ if python_type is None:
32
+ return cls
33
+
34
+ class _SpecificFormatClass(FlytePickle):
35
+ # Get the type engine to see this as kind of a generic
36
+ __origin__ = FlytePickle
37
+
38
+ @classmethod
39
+ def python_type(cls) -> typing.Type:
40
+ return python_type
41
+
42
+ return _SpecificFormatClass
43
+
44
+ @classmethod
45
+ async def to_pickle(cls, python_val: typing.Any) -> str:
46
+ h = hashlib.md5()
47
+ str_bytes = cloudpickle.dumps(python_val)
48
+ h.update(str_bytes)
49
+
50
+ uri = storage.get_random_local_path(file_path_or_file_name=h.hexdigest())
51
+ os.makedirs(os.path.dirname(uri), exist_ok=True)
52
+ async with aiofiles.open(uri, "w+b") as outfile:
53
+ await outfile.write(str_bytes)
54
+
55
+ return await storage.put(str(uri))
56
+
57
+ @classmethod
58
+ async def from_pickle(cls, uri: str) -> typing.Any:
59
+ # Deserialize the pickle, and return data in the pickle,
60
+ # and download pickle file to local first if file is not in the local file systems.
61
+ if storage.is_remote(uri):
62
+ local_path = storage.get_random_local_path()
63
+ await storage.get(uri, str(local_path), False)
64
+ uri = str(local_path)
65
+ async with aiofiles.open(uri, "rb") as infile:
66
+ data = cloudpickle.loads(await infile.read())
67
+ return data
68
+
69
+
70
+ class FlytePickleTransformer(TypeTransformer[FlytePickle]):
71
+ PYTHON_PICKLE_FORMAT = "PythonPickle"
72
+
73
+ def __init__(self):
74
+ super().__init__(name="FlytePickle", t=FlytePickle)
75
+
76
+ def assert_type(self, t: Type[T], v: T):
77
+ # Every type can serialize to pickle, so we don't need to check the type here.
78
+ ...
79
+
80
+ async def to_python_value(self, lv: literals_pb2.Literal, expected_python_type: Type[T]) -> T:
81
+ if lv.scalar.blob and lv.scalar.blob.uri:
82
+ uri = lv.scalar.blob.uri
83
+ return await FlytePickle.from_pickle(uri)
84
+ elif lv.scalar.binary and lv.scalar.binary.value:
85
+ return cloudpickle.loads(lv.scalar.binary.value)
86
+ else:
87
+ raise ValueError(f"Cannot convert {lv} to {expected_python_type}")
88
+
89
+ async def to_literal(
90
+ self,
91
+ python_val: T,
92
+ python_type: Type[T],
93
+ expected: types_pb2.LiteralType,
94
+ ) -> literals_pb2.Literal:
95
+ if python_val is None:
96
+ raise AssertionError("Cannot pickle None Value.")
97
+ meta = literals_pb2.BlobMetadata(
98
+ type=types_pb2.BlobType(
99
+ format=self.PYTHON_PICKLE_FORMAT, dimensionality=types_pb2.BlobType.BlobDimensionality.SINGLE
100
+ )
101
+ )
102
+ if sys.getsizeof(python_val) > DEFAULT_PICKLE_BYTES_LIMIT:
103
+ remote_path = await FlytePickle.to_pickle(python_val)
104
+ return literals_pb2.Literal(
105
+ scalar=literals_pb2.Scalar(blob=literals_pb2.Blob(metadata=meta, uri=remote_path))
106
+ )
107
+ else:
108
+ return literals_pb2.Literal(
109
+ scalar=literals_pb2.Scalar(binary=literals_pb2.Binary(value=cloudpickle.dumps(python_val)))
110
+ )
111
+
112
+ def guess_python_type(self, literal_type: types_pb2.LiteralType) -> typing.Type[FlytePickle[typing.Any]]:
113
+ if (
114
+ literal_type.blob is not None
115
+ and literal_type.blob.dimensionality == types_pb2.BlobType.BlobDimensionality.SINGLE
116
+ and literal_type.blob.format == FlytePickleTransformer.PYTHON_PICKLE_FORMAT
117
+ ):
118
+ return FlytePickle
119
+ if literal_type.simple == types_pb2.SimpleType.BINARY:
120
+ return FlytePickle
121
+
122
+ raise ValueError(f"Transformer {self} cannot reverse {literal_type}")
123
+
124
+ def get_literal_type(self, t: Type[T]) -> types_pb2.LiteralType:
125
+ lt = types_pb2.LiteralType(
126
+ union_type=types_pb2.UnionType(
127
+ variants=[
128
+ types_pb2.LiteralType(
129
+ blob=types_pb2.BlobType(
130
+ format=self.PYTHON_PICKLE_FORMAT,
131
+ dimensionality=types_pb2.BlobType.BlobDimensionality.SINGLE,
132
+ ),
133
+ structure=types_pb2.TypeStructure(tag=self.name),
134
+ ),
135
+ types_pb2.LiteralType(
136
+ simple=types_pb2.SimpleType.BINARY, structure=types_pb2.TypeStructure(tag=self.name)
137
+ ),
138
+ ]
139
+ ),
140
+ )
141
+ lt.metadata = {"python_class_name": str(t)}
142
+ return lt
143
+
144
+
145
+ TypeEngine.register(FlytePickleTransformer())