flyte 0.1.0__py3-none-any.whl → 0.2.0a0__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 (219) hide show
  1. flyte/__init__.py +78 -2
  2. flyte/_bin/__init__.py +0 -0
  3. flyte/_bin/runtime.py +152 -0
  4. flyte/_build.py +26 -0
  5. flyte/_cache/__init__.py +12 -0
  6. flyte/_cache/cache.py +145 -0
  7. flyte/_cache/defaults.py +9 -0
  8. flyte/_cache/policy_function_body.py +42 -0
  9. flyte/_code_bundle/__init__.py +8 -0
  10. flyte/_code_bundle/_ignore.py +113 -0
  11. flyte/_code_bundle/_packaging.py +187 -0
  12. flyte/_code_bundle/_utils.py +323 -0
  13. flyte/_code_bundle/bundle.py +209 -0
  14. flyte/_context.py +152 -0
  15. flyte/_deploy.py +243 -0
  16. flyte/_doc.py +29 -0
  17. flyte/_docstring.py +32 -0
  18. flyte/_environment.py +84 -0
  19. flyte/_excepthook.py +37 -0
  20. flyte/_group.py +32 -0
  21. flyte/_hash.py +23 -0
  22. flyte/_image.py +762 -0
  23. flyte/_initialize.py +492 -0
  24. flyte/_interface.py +84 -0
  25. flyte/_internal/__init__.py +3 -0
  26. flyte/_internal/controllers/__init__.py +128 -0
  27. flyte/_internal/controllers/_local_controller.py +193 -0
  28. flyte/_internal/controllers/_trace.py +41 -0
  29. flyte/_internal/controllers/remote/__init__.py +60 -0
  30. flyte/_internal/controllers/remote/_action.py +146 -0
  31. flyte/_internal/controllers/remote/_client.py +47 -0
  32. flyte/_internal/controllers/remote/_controller.py +494 -0
  33. flyte/_internal/controllers/remote/_core.py +410 -0
  34. flyte/_internal/controllers/remote/_informer.py +361 -0
  35. flyte/_internal/controllers/remote/_service_protocol.py +50 -0
  36. flyte/_internal/imagebuild/__init__.py +11 -0
  37. flyte/_internal/imagebuild/docker_builder.py +427 -0
  38. flyte/_internal/imagebuild/image_builder.py +246 -0
  39. flyte/_internal/imagebuild/remote_builder.py +0 -0
  40. flyte/_internal/resolvers/__init__.py +0 -0
  41. flyte/_internal/resolvers/_task_module.py +54 -0
  42. flyte/_internal/resolvers/common.py +31 -0
  43. flyte/_internal/resolvers/default.py +28 -0
  44. flyte/_internal/runtime/__init__.py +0 -0
  45. flyte/_internal/runtime/convert.py +342 -0
  46. flyte/_internal/runtime/entrypoints.py +135 -0
  47. flyte/_internal/runtime/io.py +136 -0
  48. flyte/_internal/runtime/resources_serde.py +138 -0
  49. flyte/_internal/runtime/task_serde.py +330 -0
  50. flyte/_internal/runtime/taskrunner.py +191 -0
  51. flyte/_internal/runtime/types_serde.py +54 -0
  52. flyte/_logging.py +135 -0
  53. flyte/_map.py +215 -0
  54. flyte/_pod.py +19 -0
  55. flyte/_protos/__init__.py +0 -0
  56. flyte/_protos/common/authorization_pb2.py +66 -0
  57. flyte/_protos/common/authorization_pb2.pyi +108 -0
  58. flyte/_protos/common/authorization_pb2_grpc.py +4 -0
  59. flyte/_protos/common/identifier_pb2.py +71 -0
  60. flyte/_protos/common/identifier_pb2.pyi +82 -0
  61. flyte/_protos/common/identifier_pb2_grpc.py +4 -0
  62. flyte/_protos/common/identity_pb2.py +48 -0
  63. flyte/_protos/common/identity_pb2.pyi +72 -0
  64. flyte/_protos/common/identity_pb2_grpc.py +4 -0
  65. flyte/_protos/common/list_pb2.py +36 -0
  66. flyte/_protos/common/list_pb2.pyi +71 -0
  67. flyte/_protos/common/list_pb2_grpc.py +4 -0
  68. flyte/_protos/common/policy_pb2.py +37 -0
  69. flyte/_protos/common/policy_pb2.pyi +27 -0
  70. flyte/_protos/common/policy_pb2_grpc.py +4 -0
  71. flyte/_protos/common/role_pb2.py +37 -0
  72. flyte/_protos/common/role_pb2.pyi +53 -0
  73. flyte/_protos/common/role_pb2_grpc.py +4 -0
  74. flyte/_protos/common/runtime_version_pb2.py +28 -0
  75. flyte/_protos/common/runtime_version_pb2.pyi +24 -0
  76. flyte/_protos/common/runtime_version_pb2_grpc.py +4 -0
  77. flyte/_protos/logs/dataplane/payload_pb2.py +100 -0
  78. flyte/_protos/logs/dataplane/payload_pb2.pyi +177 -0
  79. flyte/_protos/logs/dataplane/payload_pb2_grpc.py +4 -0
  80. flyte/_protos/secret/definition_pb2.py +49 -0
  81. flyte/_protos/secret/definition_pb2.pyi +93 -0
  82. flyte/_protos/secret/definition_pb2_grpc.py +4 -0
  83. flyte/_protos/secret/payload_pb2.py +62 -0
  84. flyte/_protos/secret/payload_pb2.pyi +94 -0
  85. flyte/_protos/secret/payload_pb2_grpc.py +4 -0
  86. flyte/_protos/secret/secret_pb2.py +38 -0
  87. flyte/_protos/secret/secret_pb2.pyi +6 -0
  88. flyte/_protos/secret/secret_pb2_grpc.py +198 -0
  89. flyte/_protos/secret/secret_pb2_grpc_grpc.py +198 -0
  90. flyte/_protos/validate/validate/validate_pb2.py +76 -0
  91. flyte/_protos/workflow/common_pb2.py +27 -0
  92. flyte/_protos/workflow/common_pb2.pyi +14 -0
  93. flyte/_protos/workflow/common_pb2_grpc.py +4 -0
  94. flyte/_protos/workflow/environment_pb2.py +29 -0
  95. flyte/_protos/workflow/environment_pb2.pyi +12 -0
  96. flyte/_protos/workflow/environment_pb2_grpc.py +4 -0
  97. flyte/_protos/workflow/node_execution_service_pb2.py +26 -0
  98. flyte/_protos/workflow/node_execution_service_pb2.pyi +4 -0
  99. flyte/_protos/workflow/node_execution_service_pb2_grpc.py +32 -0
  100. flyte/_protos/workflow/queue_service_pb2.py +105 -0
  101. flyte/_protos/workflow/queue_service_pb2.pyi +146 -0
  102. flyte/_protos/workflow/queue_service_pb2_grpc.py +172 -0
  103. flyte/_protos/workflow/run_definition_pb2.py +128 -0
  104. flyte/_protos/workflow/run_definition_pb2.pyi +314 -0
  105. flyte/_protos/workflow/run_definition_pb2_grpc.py +4 -0
  106. flyte/_protos/workflow/run_logs_service_pb2.py +41 -0
  107. flyte/_protos/workflow/run_logs_service_pb2.pyi +28 -0
  108. flyte/_protos/workflow/run_logs_service_pb2_grpc.py +69 -0
  109. flyte/_protos/workflow/run_service_pb2.py +129 -0
  110. flyte/_protos/workflow/run_service_pb2.pyi +171 -0
  111. flyte/_protos/workflow/run_service_pb2_grpc.py +412 -0
  112. flyte/_protos/workflow/state_service_pb2.py +66 -0
  113. flyte/_protos/workflow/state_service_pb2.pyi +75 -0
  114. flyte/_protos/workflow/state_service_pb2_grpc.py +138 -0
  115. flyte/_protos/workflow/task_definition_pb2.py +79 -0
  116. flyte/_protos/workflow/task_definition_pb2.pyi +81 -0
  117. flyte/_protos/workflow/task_definition_pb2_grpc.py +4 -0
  118. flyte/_protos/workflow/task_service_pb2.py +60 -0
  119. flyte/_protos/workflow/task_service_pb2.pyi +59 -0
  120. flyte/_protos/workflow/task_service_pb2_grpc.py +138 -0
  121. flyte/_resources.py +226 -0
  122. flyte/_retry.py +32 -0
  123. flyte/_reusable_environment.py +25 -0
  124. flyte/_run.py +482 -0
  125. flyte/_secret.py +61 -0
  126. flyte/_task.py +449 -0
  127. flyte/_task_environment.py +183 -0
  128. flyte/_timeout.py +47 -0
  129. flyte/_tools.py +27 -0
  130. flyte/_trace.py +120 -0
  131. flyte/_utils/__init__.py +26 -0
  132. flyte/_utils/asyn.py +119 -0
  133. flyte/_utils/async_cache.py +139 -0
  134. flyte/_utils/coro_management.py +23 -0
  135. flyte/_utils/file_handling.py +72 -0
  136. flyte/_utils/helpers.py +134 -0
  137. flyte/_utils/lazy_module.py +54 -0
  138. flyte/_utils/org_discovery.py +57 -0
  139. flyte/_utils/uv_script_parser.py +49 -0
  140. flyte/_version.py +21 -0
  141. flyte/cli/__init__.py +3 -0
  142. flyte/cli/_abort.py +28 -0
  143. flyte/cli/_common.py +337 -0
  144. flyte/cli/_create.py +145 -0
  145. flyte/cli/_delete.py +23 -0
  146. flyte/cli/_deploy.py +152 -0
  147. flyte/cli/_gen.py +163 -0
  148. flyte/cli/_get.py +310 -0
  149. flyte/cli/_params.py +538 -0
  150. flyte/cli/_run.py +231 -0
  151. flyte/cli/main.py +166 -0
  152. flyte/config/__init__.py +3 -0
  153. flyte/config/_config.py +216 -0
  154. flyte/config/_internal.py +64 -0
  155. flyte/config/_reader.py +207 -0
  156. flyte/connectors/__init__.py +0 -0
  157. flyte/errors.py +172 -0
  158. flyte/extras/__init__.py +5 -0
  159. flyte/extras/_container.py +263 -0
  160. flyte/io/__init__.py +27 -0
  161. flyte/io/_dir.py +448 -0
  162. flyte/io/_file.py +467 -0
  163. flyte/io/_structured_dataset/__init__.py +129 -0
  164. flyte/io/_structured_dataset/basic_dfs.py +219 -0
  165. flyte/io/_structured_dataset/structured_dataset.py +1061 -0
  166. flyte/models.py +391 -0
  167. flyte/remote/__init__.py +26 -0
  168. flyte/remote/_client/__init__.py +0 -0
  169. flyte/remote/_client/_protocols.py +133 -0
  170. flyte/remote/_client/auth/__init__.py +12 -0
  171. flyte/remote/_client/auth/_auth_utils.py +14 -0
  172. flyte/remote/_client/auth/_authenticators/__init__.py +0 -0
  173. flyte/remote/_client/auth/_authenticators/base.py +397 -0
  174. flyte/remote/_client/auth/_authenticators/client_credentials.py +73 -0
  175. flyte/remote/_client/auth/_authenticators/device_code.py +118 -0
  176. flyte/remote/_client/auth/_authenticators/external_command.py +79 -0
  177. flyte/remote/_client/auth/_authenticators/factory.py +200 -0
  178. flyte/remote/_client/auth/_authenticators/pkce.py +516 -0
  179. flyte/remote/_client/auth/_channel.py +215 -0
  180. flyte/remote/_client/auth/_client_config.py +83 -0
  181. flyte/remote/_client/auth/_default_html.py +32 -0
  182. flyte/remote/_client/auth/_grpc_utils/__init__.py +0 -0
  183. flyte/remote/_client/auth/_grpc_utils/auth_interceptor.py +288 -0
  184. flyte/remote/_client/auth/_grpc_utils/default_metadata_interceptor.py +151 -0
  185. flyte/remote/_client/auth/_keyring.py +143 -0
  186. flyte/remote/_client/auth/_token_client.py +260 -0
  187. flyte/remote/_client/auth/errors.py +16 -0
  188. flyte/remote/_client/controlplane.py +95 -0
  189. flyte/remote/_console.py +18 -0
  190. flyte/remote/_data.py +159 -0
  191. flyte/remote/_logs.py +176 -0
  192. flyte/remote/_project.py +85 -0
  193. flyte/remote/_run.py +970 -0
  194. flyte/remote/_secret.py +132 -0
  195. flyte/remote/_task.py +391 -0
  196. flyte/report/__init__.py +3 -0
  197. flyte/report/_report.py +178 -0
  198. flyte/report/_template.html +124 -0
  199. flyte/storage/__init__.py +29 -0
  200. flyte/storage/_config.py +233 -0
  201. flyte/storage/_remote_fs.py +34 -0
  202. flyte/storage/_storage.py +271 -0
  203. flyte/storage/_utils.py +5 -0
  204. flyte/syncify/__init__.py +56 -0
  205. flyte/syncify/_api.py +371 -0
  206. flyte/types/__init__.py +36 -0
  207. flyte/types/_interface.py +40 -0
  208. flyte/types/_pickle.py +118 -0
  209. flyte/types/_renderer.py +162 -0
  210. flyte/types/_string_literals.py +120 -0
  211. flyte/types/_type_engine.py +2287 -0
  212. flyte/types/_utils.py +80 -0
  213. flyte-0.2.0a0.dist-info/METADATA +249 -0
  214. flyte-0.2.0a0.dist-info/RECORD +218 -0
  215. {flyte-0.1.0.dist-info → flyte-0.2.0a0.dist-info}/WHEEL +2 -1
  216. flyte-0.2.0a0.dist-info/entry_points.txt +3 -0
  217. flyte-0.2.0a0.dist-info/top_level.txt +1 -0
  218. flyte-0.1.0.dist-info/METADATA +0 -6
  219. flyte-0.1.0.dist-info/RECORD +0 -5
flyte/_tools.py ADDED
@@ -0,0 +1,27 @@
1
+ import os
2
+
3
+
4
+ def ipython_check() -> bool:
5
+ """
6
+ Check if interface is launching from iPython (not colab)
7
+ :return is_ipython (bool): True or False
8
+ """
9
+ is_ipython = False
10
+ try: # Check if running interactively using ipython.
11
+ from IPython import get_ipython
12
+
13
+ if get_ipython() is not None:
14
+ is_ipython = True
15
+ except (ImportError, NameError):
16
+ pass
17
+ return is_ipython
18
+
19
+
20
+ def is_in_cluster() -> bool:
21
+ """
22
+ Check if the task is running in a cluster
23
+ :return is_in_cluster (bool): True or False
24
+ """
25
+ if os.getenv("_UN_CLS"):
26
+ return True
27
+ return False
flyte/_trace.py ADDED
@@ -0,0 +1,120 @@
1
+ import functools
2
+ import inspect
3
+ import time
4
+ from datetime import timedelta
5
+ from typing import Any, AsyncGenerator, AsyncIterator, Awaitable, Callable, TypeGuard, TypeVar, Union, cast
6
+
7
+ from flyte.models import NativeInterface
8
+
9
+ T = TypeVar("T")
10
+
11
+
12
+ def trace(func: Callable[..., T]) -> Callable[..., T]:
13
+ """
14
+ A decorator that traces function execution with timing information.
15
+ Works with regular functions, async functions, and async generators/iterators.
16
+ """
17
+
18
+ @functools.wraps(func)
19
+ def wrapper_sync(*args: Any, **kwargs: Any) -> Any:
20
+ raise NotImplementedError
21
+
22
+ @functools.wraps(func)
23
+ async def wrapper_async(*args: Any, **kwargs: Any) -> Any:
24
+ from flyte._context import internal_ctx
25
+
26
+ ctx = internal_ctx()
27
+ if ctx.is_task_context():
28
+ # If we are in a task context, that implies we are executing a Run.
29
+ # In this scenario, we should submit the task to the controller.
30
+ # We will also check if we are not initialized, It is not expected to be not initialized
31
+ from ._internal.controllers import get_controller
32
+
33
+ controller = get_controller()
34
+ iface = NativeInterface.from_callable(func)
35
+ info, ok = await controller.get_action_outputs(iface, func, *args, **kwargs)
36
+ if ok:
37
+ if info.output:
38
+ return info.output
39
+ elif info.error:
40
+ raise info.error
41
+ start_time = time.time()
42
+ try:
43
+ # Cast to Awaitable to satisfy mypy
44
+ coroutine_result = cast(Awaitable[Any], func(*args, **kwargs))
45
+ results = await coroutine_result
46
+ duration = time.time() - start_time
47
+ info.add_outputs(results, timedelta(seconds=duration))
48
+ await controller.record_trace(info)
49
+ return results
50
+ except Exception as e:
51
+ # If there is an error, we need to record it
52
+ duration = time.time() - start_time
53
+ info.add_error(e, timedelta(seconds=duration))
54
+ await controller.record_trace(info)
55
+ raise e
56
+ else:
57
+ # If we are not in a task context, we can just call the function normally
58
+ # Cast to Awaitable to satisfy mypy
59
+ coroutine_result = cast(Awaitable[Any], func(*args, **kwargs))
60
+ return await coroutine_result
61
+
62
+ def is_async_iterable(obj: Any) -> TypeGuard[Union[AsyncGenerator, AsyncIterator]]:
63
+ return hasattr(obj, "__aiter__")
64
+
65
+ @functools.wraps(func)
66
+ async def wrapper_async_iterator(*args: Any, **kwargs: Any) -> AsyncIterator[Any]:
67
+ from flyte._context import internal_ctx
68
+
69
+ ctx = internal_ctx()
70
+ if ctx.is_task_context():
71
+ # If we are in a task context, that implies we are executing a Run.
72
+ # In this scenario, we should submit the task to the controller.
73
+ # We will also check if we are not initialized, It is not expected to be not initialized
74
+ from ._internal.controllers import get_controller
75
+
76
+ controller = get_controller()
77
+ iface = NativeInterface.from_callable(func)
78
+ info, ok = await controller.get_action_outputs(iface, func, *args, **kwargs)
79
+ if ok:
80
+ if info.output:
81
+ for item in info.output:
82
+ yield item
83
+ elif info.error:
84
+ raise info.error
85
+ start_time = time.time()
86
+ try:
87
+ items = []
88
+ result = func(*args, **kwargs)
89
+ # TODO ideally we should use streaming into the type-engine so that it stream uploads large blocks
90
+ if inspect.isasyncgen(result) or is_async_iterable(result):
91
+ # If it's directly an async generator
92
+ async_iter = result
93
+ async for item in async_iter:
94
+ items.append(item)
95
+ yield item
96
+ duration = time.time() - start_time
97
+ info.add_outputs(items, timedelta(seconds=duration))
98
+ await controller.record_trace(info)
99
+ return
100
+ except Exception as e:
101
+ end_time = time.time()
102
+ duration = end_time - start_time
103
+ info.add_error(e, timedelta(seconds=duration))
104
+ await controller.record_trace(info)
105
+ raise e
106
+ else:
107
+ result = func(*args, **kwargs)
108
+ if is_async_iterable(result):
109
+ async for item in result:
110
+ yield item
111
+
112
+ # Choose the appropriate wrapper based on the function type
113
+ if inspect.iscoroutinefunction(func):
114
+ # This handles async functions that return normal values
115
+ return cast(Callable[..., T], wrapper_async)
116
+ elif inspect.isasyncgenfunction(func):
117
+ return cast(Callable[..., T], wrapper_async_iterator)
118
+ else:
119
+ # For regular sync functions
120
+ return cast(Callable[..., T], wrapper_sync)
@@ -0,0 +1,26 @@
1
+ """
2
+ Internal utility functions.
3
+
4
+ Except for logging, modules in this package should not depend on any other part of the repo.
5
+ """
6
+
7
+ from .async_cache import AsyncLRUCache
8
+ from .coro_management import run_coros
9
+ from .file_handling import filehash_update, update_hasher_for_source
10
+ from .helpers import get_cwd_editable_install
11
+ from .lazy_module import lazy_module
12
+ from .org_discovery import hostname_from_url, org_from_endpoint, sanitize_endpoint
13
+ from .uv_script_parser import parse_uv_script_file
14
+
15
+ __all__ = [
16
+ "AsyncLRUCache",
17
+ "filehash_update",
18
+ "get_cwd_editable_install",
19
+ "hostname_from_url",
20
+ "lazy_module",
21
+ "org_from_endpoint",
22
+ "parse_uv_script_file",
23
+ "run_coros",
24
+ "sanitize_endpoint",
25
+ "update_hasher_for_source",
26
+ ]
flyte/_utils/asyn.py ADDED
@@ -0,0 +1,119 @@
1
+ """Manages an async event loop on another thread. Developers should only require to call
2
+ sync to use the managed loop:
3
+
4
+ from flytekit.tools.asyn import run_sync
5
+
6
+ async def async_add(a: int, b: int) -> int:
7
+ return a + b
8
+
9
+ result = run_sync(async_add, a=10, b=12)
10
+ """
11
+
12
+ import asyncio
13
+ import atexit
14
+ import functools
15
+ import os
16
+ import threading
17
+ from contextlib import contextmanager
18
+ from typing import Any, Awaitable, Callable, TypeVar
19
+
20
+ from typing_extensions import ParamSpec
21
+
22
+ from flyte._logging import logger
23
+
24
+ T = TypeVar("T")
25
+
26
+ P = ParamSpec("P")
27
+
28
+
29
+ @contextmanager
30
+ def _selector_policy():
31
+ original_policy = asyncio.get_event_loop_policy()
32
+ try:
33
+ if os.name == "nt" and hasattr(asyncio, "WindowsSelectorEventLoopPolicy"):
34
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
35
+
36
+ yield
37
+ finally:
38
+ asyncio.set_event_loop_policy(original_policy)
39
+
40
+
41
+ class _TaskRunner:
42
+ """A task runner that runs an asyncio event loop on a background thread."""
43
+
44
+ def __init__(self) -> None:
45
+ self.__loop: asyncio.AbstractEventLoop | None = None
46
+ self.__runner_thread: threading.Thread | None = None
47
+ self.__lock = threading.Lock()
48
+ atexit.register(self._close)
49
+
50
+ def _close(self) -> None:
51
+ if self.__loop:
52
+ self.__loop.stop()
53
+
54
+ def _execute(self) -> None:
55
+ loop = self.__loop
56
+ assert loop is not None
57
+ try:
58
+ loop.run_forever()
59
+ finally:
60
+ loop.close()
61
+
62
+ def get_exc_handler(self):
63
+ def exc_handler(loop, context):
64
+ logger.error(
65
+ f"Taskrunner for {self.__runner_thread.name if self.__runner_thread else 'no thread'} caught"
66
+ f" exception in {loop}: {context}"
67
+ )
68
+
69
+ return exc_handler
70
+
71
+ def run(self, coro: Any) -> Any:
72
+ """Synchronously run a coroutine on a background thread."""
73
+ name = f"{threading.current_thread().name} : loop-runner"
74
+ with self.__lock:
75
+ if self.__loop is None:
76
+ with _selector_policy():
77
+ self.__loop = asyncio.new_event_loop()
78
+
79
+ exc_handler = self.get_exc_handler()
80
+ self.__loop.set_exception_handler(exc_handler)
81
+ self.__runner_thread = threading.Thread(target=self._execute, daemon=True, name=name)
82
+ self.__runner_thread.start()
83
+ fut = asyncio.run_coroutine_threadsafe(coro, self.__loop)
84
+
85
+ res = fut.result(None)
86
+
87
+ return res
88
+
89
+
90
+ class _AsyncLoopManager:
91
+ def __init__(self):
92
+ self._runner_map: dict[str, _TaskRunner] = {}
93
+
94
+ def run_sync(self, coro_func: Callable[..., Awaitable[T]], *args, **kwargs) -> T:
95
+ """
96
+ This should be called from synchronous functions to run an async function.
97
+ """
98
+ name = threading.current_thread().name + f"PID:{os.getpid()}"
99
+ coro = coro_func(*args, **kwargs)
100
+ if name not in self._runner_map:
101
+ if len(self._runner_map) > 500:
102
+ logger.warning(
103
+ "More than 500 event loop runners created!!! This could be a case of runaway recursion..."
104
+ )
105
+ self._runner_map[name] = _TaskRunner()
106
+ return self._runner_map[name].run(coro)
107
+
108
+ def synced(self, coro_func: Callable[P, Awaitable[T]]) -> Callable[P, T]:
109
+ """Make loop run coroutine until it returns. Runs in other thread"""
110
+
111
+ @functools.wraps(coro_func)
112
+ def wrapped(*args: Any, **kwargs: Any) -> T:
113
+ return self.run_sync(coro_func, *args, **kwargs)
114
+
115
+ return wrapped
116
+
117
+
118
+ loop_manager = _AsyncLoopManager()
119
+ run_sync = loop_manager.run_sync
@@ -0,0 +1,139 @@
1
+ import asyncio
2
+ import time
3
+ from collections import OrderedDict
4
+ from typing import Awaitable, Callable, Dict, Generic, Optional, TypeVar
5
+
6
+ K = TypeVar("K")
7
+ V = TypeVar("V")
8
+
9
+
10
+ class AsyncLRUCache(Generic[K, V]):
11
+ """
12
+ A high-performance async-compatible LRU cache.
13
+
14
+ Examples:
15
+ ```python
16
+ # Create a cache instance
17
+ cache = AsyncLRUCache[str, dict](maxsize=100)
18
+
19
+ async def fetch_data(user_id: str) -> dict:
20
+ # Define the expensive operation as a local function
21
+ async def get_user_data():
22
+ await asyncio.sleep(1) # Simulating network/DB delay
23
+ return {"id": user_id, "name": f"User {user_id}"}
24
+
25
+ # Use the cache
26
+ return await cache.get(f"user:{user_id}", get_user_data)
27
+ ```
28
+ This cache can be used from async coroutines and handles concurrent access safely.
29
+ """
30
+
31
+ def __init__(self, maxsize: int = 128, ttl: Optional[float] = None):
32
+ """
33
+ Initialize the async LRU cache.
34
+
35
+ Args:
36
+ maxsize: Maximum number of items to keep in the cache
37
+ ttl: Time-to-live for cache entries in seconds, or None for no expiration
38
+ """
39
+ self._cache: OrderedDict[K, tuple[V, float]] = OrderedDict()
40
+ self._maxsize = maxsize
41
+ self._ttl = ttl
42
+ self._locks: Dict[K, asyncio.Lock] = {}
43
+ self._access_lock = asyncio.Lock()
44
+
45
+ async def get(self, key: K, value_func: Callable[[], V | Awaitable[V]]) -> V:
46
+ """
47
+ Get a value from the cache, computing it if necessary.
48
+
49
+ Args:
50
+ key: The cache key
51
+ value_func: Function or coroutine to compute the value if not cached
52
+
53
+ Returns:
54
+ The cached or computed value
55
+ """
56
+ # Fast path: check if key exists and is not expired
57
+ if key in self._cache:
58
+ value, timestamp = self._cache[key]
59
+ if self._ttl is None or time.time() - timestamp < self._ttl:
60
+ # Move the accessed item to the end (most recently used)
61
+ async with self._access_lock:
62
+ self._cache.move_to_end(key)
63
+ return value
64
+
65
+ # Slow path: compute the value
66
+ # Get or create a lock for this key to prevent redundant computation
67
+ async with self._access_lock:
68
+ lock = self._locks.get(key)
69
+ if lock is None:
70
+ lock = asyncio.Lock()
71
+ self._locks[key] = lock
72
+
73
+ async with lock:
74
+ # Check again in case another coroutine computed the value while we waited
75
+ if key in self._cache:
76
+ value, timestamp = self._cache[key]
77
+ if self._ttl is None or time.time() - timestamp < self._ttl:
78
+ async with self._access_lock:
79
+ self._cache.move_to_end(key)
80
+ return value
81
+
82
+ # Compute the value
83
+ if asyncio.iscoroutinefunction(value_func):
84
+ value = await value_func()
85
+ else:
86
+ value = value_func() # type: ignore
87
+
88
+ # Store in cache
89
+ async with self._access_lock:
90
+ self._cache[key] = (value, time.time())
91
+ # Evict least recently used items if needed
92
+ while len(self._cache) > self._maxsize:
93
+ self._cache.popitem(last=False)
94
+ # Clean up the lock
95
+ self._locks.pop(key, None)
96
+
97
+ return value
98
+
99
+ async def set(self, key: K, value: V) -> None:
100
+ """
101
+ Explicitly set a value in the cache.
102
+
103
+ Args:
104
+ key: The cache key
105
+ value: The value to cache
106
+ """
107
+ async with self._access_lock:
108
+ self._cache[key] = (value, time.time())
109
+ # Evict least recently used items if needed
110
+ while len(self._cache) > self._maxsize:
111
+ self._cache.popitem(last=False)
112
+
113
+ async def invalidate(self, key: K) -> None:
114
+ """Remove a specific key from the cache."""
115
+ async with self._access_lock:
116
+ self._cache.pop(key, None)
117
+
118
+ async def clear(self) -> None:
119
+ """Clear the entire cache."""
120
+ async with self._access_lock:
121
+ self._cache.clear()
122
+ self._locks.clear()
123
+
124
+ async def contains(self, key: K) -> bool:
125
+ """Check if a key exists in the cache and is not expired."""
126
+ if key not in self._cache:
127
+ return False
128
+
129
+ if self._ttl is None:
130
+ return True
131
+
132
+ _, timestamp = self._cache[key]
133
+ return time.time() - timestamp < self._ttl
134
+
135
+
136
+ # Example usage:
137
+ """
138
+
139
+ """
@@ -0,0 +1,23 @@
1
+ import asyncio
2
+ import typing
3
+
4
+
5
+ async def run_coros(*coros: typing.Coroutine, return_when: str = asyncio.FIRST_COMPLETED):
6
+ """
7
+ Run a list of coroutines concurrently and wait for the first one to finish or exit.
8
+ When the first one finishes, cancel all other tasks.
9
+
10
+ :param coros:
11
+ :param return_when:
12
+ :return:
13
+ """
14
+ tasks: typing.List[asyncio.Task[typing.Never]] = [asyncio.create_task(c) for c in coros]
15
+ done, pending = await asyncio.wait(tasks, return_when=return_when)
16
+
17
+ for t in pending: # type: asyncio.Task
18
+ t.cancel() # Cancel all tasks that didn't finish first
19
+
20
+ for t in done:
21
+ err = t.exception()
22
+ if err:
23
+ raise err
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import os
5
+ import pathlib
6
+ import stat
7
+ import typing
8
+ from pathlib import Path
9
+ from typing import List, Optional, Union
10
+
11
+ from flyte._logging import logger
12
+
13
+
14
+ def filehash_update(path: pathlib.Path, hasher: hashlib._Hash) -> None:
15
+ blocksize = 65536
16
+ with open(path, "rb") as f:
17
+ bytes = f.read(blocksize)
18
+ while bytes:
19
+ hasher.update(bytes)
20
+ bytes = f.read(blocksize)
21
+
22
+
23
+ def _pathhash_update(path: Union[os.PathLike, str], hasher: hashlib._Hash) -> None:
24
+ path_list = str(path).split(os.sep)
25
+ hasher.update("".join(path_list).encode("utf-8"))
26
+
27
+
28
+ def update_hasher_for_source(
29
+ source: Union[os.PathLike, List[os.PathLike]], hasher: hashlib._Hash, filter: Optional[typing.Callable] = None
30
+ ):
31
+ """
32
+ Walks the entirety of the source dir to compute a deterministic md5 hex digest of the dir contents.
33
+ :param os.PathLike source:
34
+ :param callable filter:
35
+ :return Text:
36
+ """
37
+
38
+ def compute_digest_for_file(path: os.PathLike, rel_path: os.PathLike) -> None:
39
+ # Only consider files that exist (e.g. disregard symlinks that point to non-existent files)
40
+ if not os.path.exists(path):
41
+ logger.info(f"Skipping non-existent file {path}")
42
+ return
43
+
44
+ # Skip socket files
45
+ if stat.S_ISSOCK(os.stat(path).st_mode):
46
+ logger.info(f"Skip socket file {path}")
47
+ return
48
+
49
+ if filter:
50
+ if filter(rel_path):
51
+ return
52
+
53
+ filehash_update(Path(path), hasher)
54
+ _pathhash_update(rel_path, hasher)
55
+
56
+ def compute_digest_for_dir(source: os.PathLike):
57
+ for root, _, files in os.walk(str(source), topdown=True):
58
+ files.sort()
59
+
60
+ for fname in files:
61
+ abspath = os.path.join(root, fname)
62
+ relpath = os.path.relpath(abspath, source)
63
+ compute_digest_for_file(Path(abspath), Path(relpath))
64
+
65
+ if isinstance(source, list):
66
+ for src in source:
67
+ if os.path.isdir(src):
68
+ compute_digest_for_dir(src)
69
+ else:
70
+ compute_digest_for_file(src, os.path.basename(src))
71
+ else:
72
+ compute_digest_for_dir(source)
@@ -0,0 +1,134 @@
1
+ import os
2
+ import string
3
+ import typing
4
+ from contextlib import contextmanager
5
+ from pathlib import Path
6
+
7
+
8
+ def load_proto_from_file(pb2_type, path):
9
+ with open(path, "rb") as reader:
10
+ out = pb2_type()
11
+ out.ParseFromString(reader.read())
12
+ return out
13
+
14
+
15
+ def write_proto_to_file(proto, path):
16
+ Path(os.path.dirname(path)).mkdir(parents=True, exist_ok=True)
17
+ with open(path, "wb") as writer:
18
+ writer.write(proto.SerializeToString())
19
+
20
+
21
+ def str2bool(value: typing.Optional[str]) -> bool:
22
+ """
23
+ Convert a string to a boolean. This is useful for parsing environment variables.
24
+ :param value: The string to convert to a boolean
25
+ :return: the boolean value
26
+ """
27
+ if value is None:
28
+ return False
29
+ return value.lower() in ("true", "t", "1")
30
+
31
+
32
+ BASE36_ALPHABET = string.digits + string.ascii_lowercase # 0-9 + a-z (36 characters)
33
+
34
+
35
+ def base36_encode(byte_data: bytes) -> str:
36
+ """
37
+ This function expects to encode bytes coming from an hd5 hash function into a base36 encoded string.
38
+ md5 shas are limited to 128 bits, so the maximum byte value should easily fit into a 30 character long string.
39
+ If the input is too large howeer
40
+ """
41
+ # Convert bytes to a big integer
42
+ num = int.from_bytes(byte_data, byteorder="big")
43
+
44
+ # Convert integer to base36 string
45
+ if num == 0:
46
+ return BASE36_ALPHABET[0]
47
+
48
+ base36 = []
49
+ while num:
50
+ num, rem = divmod(num, 36)
51
+ base36.append(BASE36_ALPHABET[rem])
52
+ return "".join(reversed(base36))
53
+
54
+
55
+ def _iter_editable():
56
+ """
57
+ Yield (project_name, source_path) for every editable distribution
58
+ visible to the current interpreter
59
+ """
60
+ import json
61
+ import pathlib
62
+ from importlib.metadata import distributions
63
+
64
+ for dist in distributions():
65
+ # PEP-610 / PEP-660 (preferred, wheel-style editables)
66
+ direct = dist.read_text("direct_url.json")
67
+ if direct:
68
+ data = json.loads(direct)
69
+ if data.get("dir_info", {}).get("editable"): # spec key
70
+ # todo: will need testing on windows
71
+ yield dist.metadata["Name"], pathlib.Path(data["url"][7:]) # strip file://
72
+ continue
73
+
74
+ # Legacy setuptools-develop / pip-e (egg-link)
75
+ for file in dist.files or (): # importlib.metadata 3.8+
76
+ if file.suffix == ".egg-link":
77
+ with open(dist.locate_file(file), "r") as f:
78
+ line = f.readline()
79
+ yield dist.metadata["Name"], pathlib.Path(line.strip())
80
+
81
+
82
+ def get_cwd_editable_install() -> typing.Optional[Path]:
83
+ """
84
+ This helper function is incomplete since it hasn't been tested with all the package managers out there,
85
+ but the intention is that it returns the source folder for an editable install if the current working directory
86
+ is inside the editable install project - if the code is inside an src/ folder, and the cwd is a level above,
87
+ it should still work, returning the src/ folder. If cwd is the src/ folder, this should return the same.
88
+
89
+ The idea is that the return path will be used to determine the relative path for imported modules when building
90
+ the code bundle.
91
+
92
+ :return:
93
+ """
94
+
95
+ from flyte._logging import logger
96
+
97
+ editable_installs = []
98
+ for name, path in _iter_editable():
99
+ logger.debug(f"Detected editable install: {name} at {path}")
100
+ editable_installs.append(path)
101
+
102
+ # check to see if the current working directory is in any of the editable installs
103
+ # including if the current folder is the root folder, one level up from the src and contains
104
+ # the pyproject.toml file.
105
+ # Two scenarios to consider
106
+ # - if cwd is nested inside the editable install folder.
107
+ # - if the cwd is exactly one level above the editable install folder.
108
+ cwd = Path.cwd()
109
+ for install in editable_installs:
110
+ # child.is_relative_to(parent) is True if child is inside parent
111
+ if cwd.is_relative_to(install):
112
+ return install
113
+ else:
114
+ # check if the cwd is one level above the install folder
115
+ if install.parent == cwd:
116
+ # check if the install folder contains a pyproject.toml file
117
+ if (cwd / "pyproject.toml").exists() or (cwd / "setup.py").exists():
118
+ return install # note we want the install folder, not the parent
119
+
120
+ return None
121
+
122
+
123
+ @contextmanager
124
+ def _selector_policy():
125
+ import asyncio
126
+
127
+ original_policy = asyncio.get_event_loop_policy()
128
+ try:
129
+ if os.name == "nt" and hasattr(asyncio, "WindowsSelectorEventLoopPolicy"):
130
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
131
+
132
+ yield
133
+ finally:
134
+ asyncio.set_event_loop_policy(original_policy)