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.
- flyte/__init__.py +78 -2
- flyte/_bin/__init__.py +0 -0
- flyte/_bin/runtime.py +152 -0
- flyte/_build.py +26 -0
- flyte/_cache/__init__.py +12 -0
- flyte/_cache/cache.py +145 -0
- flyte/_cache/defaults.py +9 -0
- flyte/_cache/policy_function_body.py +42 -0
- flyte/_code_bundle/__init__.py +8 -0
- flyte/_code_bundle/_ignore.py +113 -0
- flyte/_code_bundle/_packaging.py +187 -0
- flyte/_code_bundle/_utils.py +323 -0
- flyte/_code_bundle/bundle.py +209 -0
- flyte/_context.py +152 -0
- flyte/_deploy.py +243 -0
- flyte/_doc.py +29 -0
- flyte/_docstring.py +32 -0
- flyte/_environment.py +84 -0
- flyte/_excepthook.py +37 -0
- flyte/_group.py +32 -0
- flyte/_hash.py +23 -0
- flyte/_image.py +762 -0
- flyte/_initialize.py +492 -0
- flyte/_interface.py +84 -0
- flyte/_internal/__init__.py +3 -0
- flyte/_internal/controllers/__init__.py +128 -0
- flyte/_internal/controllers/_local_controller.py +193 -0
- flyte/_internal/controllers/_trace.py +41 -0
- flyte/_internal/controllers/remote/__init__.py +60 -0
- flyte/_internal/controllers/remote/_action.py +146 -0
- flyte/_internal/controllers/remote/_client.py +47 -0
- flyte/_internal/controllers/remote/_controller.py +494 -0
- flyte/_internal/controllers/remote/_core.py +410 -0
- flyte/_internal/controllers/remote/_informer.py +361 -0
- flyte/_internal/controllers/remote/_service_protocol.py +50 -0
- flyte/_internal/imagebuild/__init__.py +11 -0
- flyte/_internal/imagebuild/docker_builder.py +427 -0
- flyte/_internal/imagebuild/image_builder.py +246 -0
- flyte/_internal/imagebuild/remote_builder.py +0 -0
- flyte/_internal/resolvers/__init__.py +0 -0
- flyte/_internal/resolvers/_task_module.py +54 -0
- flyte/_internal/resolvers/common.py +31 -0
- flyte/_internal/resolvers/default.py +28 -0
- flyte/_internal/runtime/__init__.py +0 -0
- flyte/_internal/runtime/convert.py +342 -0
- flyte/_internal/runtime/entrypoints.py +135 -0
- flyte/_internal/runtime/io.py +136 -0
- flyte/_internal/runtime/resources_serde.py +138 -0
- flyte/_internal/runtime/task_serde.py +330 -0
- flyte/_internal/runtime/taskrunner.py +191 -0
- flyte/_internal/runtime/types_serde.py +54 -0
- flyte/_logging.py +135 -0
- flyte/_map.py +215 -0
- flyte/_pod.py +19 -0
- flyte/_protos/__init__.py +0 -0
- flyte/_protos/common/authorization_pb2.py +66 -0
- flyte/_protos/common/authorization_pb2.pyi +108 -0
- flyte/_protos/common/authorization_pb2_grpc.py +4 -0
- flyte/_protos/common/identifier_pb2.py +71 -0
- flyte/_protos/common/identifier_pb2.pyi +82 -0
- flyte/_protos/common/identifier_pb2_grpc.py +4 -0
- flyte/_protos/common/identity_pb2.py +48 -0
- flyte/_protos/common/identity_pb2.pyi +72 -0
- flyte/_protos/common/identity_pb2_grpc.py +4 -0
- flyte/_protos/common/list_pb2.py +36 -0
- flyte/_protos/common/list_pb2.pyi +71 -0
- flyte/_protos/common/list_pb2_grpc.py +4 -0
- flyte/_protos/common/policy_pb2.py +37 -0
- flyte/_protos/common/policy_pb2.pyi +27 -0
- flyte/_protos/common/policy_pb2_grpc.py +4 -0
- flyte/_protos/common/role_pb2.py +37 -0
- flyte/_protos/common/role_pb2.pyi +53 -0
- flyte/_protos/common/role_pb2_grpc.py +4 -0
- flyte/_protos/common/runtime_version_pb2.py +28 -0
- flyte/_protos/common/runtime_version_pb2.pyi +24 -0
- flyte/_protos/common/runtime_version_pb2_grpc.py +4 -0
- flyte/_protos/logs/dataplane/payload_pb2.py +100 -0
- flyte/_protos/logs/dataplane/payload_pb2.pyi +177 -0
- flyte/_protos/logs/dataplane/payload_pb2_grpc.py +4 -0
- flyte/_protos/secret/definition_pb2.py +49 -0
- flyte/_protos/secret/definition_pb2.pyi +93 -0
- flyte/_protos/secret/definition_pb2_grpc.py +4 -0
- flyte/_protos/secret/payload_pb2.py +62 -0
- flyte/_protos/secret/payload_pb2.pyi +94 -0
- flyte/_protos/secret/payload_pb2_grpc.py +4 -0
- flyte/_protos/secret/secret_pb2.py +38 -0
- flyte/_protos/secret/secret_pb2.pyi +6 -0
- flyte/_protos/secret/secret_pb2_grpc.py +198 -0
- flyte/_protos/secret/secret_pb2_grpc_grpc.py +198 -0
- flyte/_protos/validate/validate/validate_pb2.py +76 -0
- flyte/_protos/workflow/common_pb2.py +27 -0
- flyte/_protos/workflow/common_pb2.pyi +14 -0
- flyte/_protos/workflow/common_pb2_grpc.py +4 -0
- flyte/_protos/workflow/environment_pb2.py +29 -0
- flyte/_protos/workflow/environment_pb2.pyi +12 -0
- flyte/_protos/workflow/environment_pb2_grpc.py +4 -0
- flyte/_protos/workflow/node_execution_service_pb2.py +26 -0
- flyte/_protos/workflow/node_execution_service_pb2.pyi +4 -0
- flyte/_protos/workflow/node_execution_service_pb2_grpc.py +32 -0
- flyte/_protos/workflow/queue_service_pb2.py +105 -0
- flyte/_protos/workflow/queue_service_pb2.pyi +146 -0
- flyte/_protos/workflow/queue_service_pb2_grpc.py +172 -0
- flyte/_protos/workflow/run_definition_pb2.py +128 -0
- flyte/_protos/workflow/run_definition_pb2.pyi +314 -0
- flyte/_protos/workflow/run_definition_pb2_grpc.py +4 -0
- flyte/_protos/workflow/run_logs_service_pb2.py +41 -0
- flyte/_protos/workflow/run_logs_service_pb2.pyi +28 -0
- flyte/_protos/workflow/run_logs_service_pb2_grpc.py +69 -0
- flyte/_protos/workflow/run_service_pb2.py +129 -0
- flyte/_protos/workflow/run_service_pb2.pyi +171 -0
- flyte/_protos/workflow/run_service_pb2_grpc.py +412 -0
- flyte/_protos/workflow/state_service_pb2.py +66 -0
- flyte/_protos/workflow/state_service_pb2.pyi +75 -0
- flyte/_protos/workflow/state_service_pb2_grpc.py +138 -0
- flyte/_protos/workflow/task_definition_pb2.py +79 -0
- flyte/_protos/workflow/task_definition_pb2.pyi +81 -0
- flyte/_protos/workflow/task_definition_pb2_grpc.py +4 -0
- flyte/_protos/workflow/task_service_pb2.py +60 -0
- flyte/_protos/workflow/task_service_pb2.pyi +59 -0
- flyte/_protos/workflow/task_service_pb2_grpc.py +138 -0
- flyte/_resources.py +226 -0
- flyte/_retry.py +32 -0
- flyte/_reusable_environment.py +25 -0
- flyte/_run.py +482 -0
- flyte/_secret.py +61 -0
- flyte/_task.py +449 -0
- flyte/_task_environment.py +183 -0
- flyte/_timeout.py +47 -0
- flyte/_tools.py +27 -0
- flyte/_trace.py +120 -0
- flyte/_utils/__init__.py +26 -0
- flyte/_utils/asyn.py +119 -0
- flyte/_utils/async_cache.py +139 -0
- flyte/_utils/coro_management.py +23 -0
- flyte/_utils/file_handling.py +72 -0
- flyte/_utils/helpers.py +134 -0
- flyte/_utils/lazy_module.py +54 -0
- flyte/_utils/org_discovery.py +57 -0
- flyte/_utils/uv_script_parser.py +49 -0
- flyte/_version.py +21 -0
- flyte/cli/__init__.py +3 -0
- flyte/cli/_abort.py +28 -0
- flyte/cli/_common.py +337 -0
- flyte/cli/_create.py +145 -0
- flyte/cli/_delete.py +23 -0
- flyte/cli/_deploy.py +152 -0
- flyte/cli/_gen.py +163 -0
- flyte/cli/_get.py +310 -0
- flyte/cli/_params.py +538 -0
- flyte/cli/_run.py +231 -0
- flyte/cli/main.py +166 -0
- flyte/config/__init__.py +3 -0
- flyte/config/_config.py +216 -0
- flyte/config/_internal.py +64 -0
- flyte/config/_reader.py +207 -0
- flyte/connectors/__init__.py +0 -0
- flyte/errors.py +172 -0
- flyte/extras/__init__.py +5 -0
- flyte/extras/_container.py +263 -0
- flyte/io/__init__.py +27 -0
- flyte/io/_dir.py +448 -0
- flyte/io/_file.py +467 -0
- flyte/io/_structured_dataset/__init__.py +129 -0
- flyte/io/_structured_dataset/basic_dfs.py +219 -0
- flyte/io/_structured_dataset/structured_dataset.py +1061 -0
- flyte/models.py +391 -0
- flyte/remote/__init__.py +26 -0
- flyte/remote/_client/__init__.py +0 -0
- flyte/remote/_client/_protocols.py +133 -0
- flyte/remote/_client/auth/__init__.py +12 -0
- flyte/remote/_client/auth/_auth_utils.py +14 -0
- flyte/remote/_client/auth/_authenticators/__init__.py +0 -0
- flyte/remote/_client/auth/_authenticators/base.py +397 -0
- flyte/remote/_client/auth/_authenticators/client_credentials.py +73 -0
- flyte/remote/_client/auth/_authenticators/device_code.py +118 -0
- flyte/remote/_client/auth/_authenticators/external_command.py +79 -0
- flyte/remote/_client/auth/_authenticators/factory.py +200 -0
- flyte/remote/_client/auth/_authenticators/pkce.py +516 -0
- flyte/remote/_client/auth/_channel.py +215 -0
- flyte/remote/_client/auth/_client_config.py +83 -0
- flyte/remote/_client/auth/_default_html.py +32 -0
- flyte/remote/_client/auth/_grpc_utils/__init__.py +0 -0
- flyte/remote/_client/auth/_grpc_utils/auth_interceptor.py +288 -0
- flyte/remote/_client/auth/_grpc_utils/default_metadata_interceptor.py +151 -0
- flyte/remote/_client/auth/_keyring.py +143 -0
- flyte/remote/_client/auth/_token_client.py +260 -0
- flyte/remote/_client/auth/errors.py +16 -0
- flyte/remote/_client/controlplane.py +95 -0
- flyte/remote/_console.py +18 -0
- flyte/remote/_data.py +159 -0
- flyte/remote/_logs.py +176 -0
- flyte/remote/_project.py +85 -0
- flyte/remote/_run.py +970 -0
- flyte/remote/_secret.py +132 -0
- flyte/remote/_task.py +391 -0
- flyte/report/__init__.py +3 -0
- flyte/report/_report.py +178 -0
- flyte/report/_template.html +124 -0
- flyte/storage/__init__.py +29 -0
- flyte/storage/_config.py +233 -0
- flyte/storage/_remote_fs.py +34 -0
- flyte/storage/_storage.py +271 -0
- flyte/storage/_utils.py +5 -0
- flyte/syncify/__init__.py +56 -0
- flyte/syncify/_api.py +371 -0
- flyte/types/__init__.py +36 -0
- flyte/types/_interface.py +40 -0
- flyte/types/_pickle.py +118 -0
- flyte/types/_renderer.py +162 -0
- flyte/types/_string_literals.py +120 -0
- flyte/types/_type_engine.py +2287 -0
- flyte/types/_utils.py +80 -0
- flyte-0.2.0a0.dist-info/METADATA +249 -0
- flyte-0.2.0a0.dist-info/RECORD +218 -0
- {flyte-0.1.0.dist-info → flyte-0.2.0a0.dist-info}/WHEEL +2 -1
- flyte-0.2.0a0.dist-info/entry_points.txt +3 -0
- flyte-0.2.0a0.dist-info/top_level.txt +1 -0
- flyte-0.1.0.dist-info/METADATA +0 -6
- 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)
|
flyte/_utils/__init__.py
ADDED
|
@@ -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)
|
flyte/_utils/helpers.py
ADDED
|
@@ -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)
|