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
@@ -0,0 +1,30 @@
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, str2bool
11
+ from .lazy_module import lazy_module
12
+ from .module_loader import adjust_sys_path, load_python_modules
13
+ from .org_discovery import hostname_from_url, org_from_endpoint, sanitize_endpoint
14
+ from .uv_script_parser import parse_uv_script_file
15
+
16
+ __all__ = [
17
+ "AsyncLRUCache",
18
+ "adjust_sys_path",
19
+ "filehash_update",
20
+ "get_cwd_editable_install",
21
+ "hostname_from_url",
22
+ "lazy_module",
23
+ "load_python_modules",
24
+ "org_from_endpoint",
25
+ "parse_uv_script_file",
26
+ "run_coros",
27
+ "sanitize_endpoint",
28
+ "str2bool",
29
+ "update_hasher_for_source",
30
+ ]
flyte/_utils/asyn.py ADDED
@@ -0,0 +1,121 @@
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
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import atexit
16
+ import functools
17
+ import os
18
+ import threading
19
+ from contextlib import contextmanager
20
+ from typing import Any, Awaitable, Callable, TypeVar
21
+
22
+ from typing_extensions import ParamSpec
23
+
24
+ from flyte._logging import logger
25
+
26
+ T = TypeVar("T")
27
+
28
+ P = ParamSpec("P")
29
+
30
+
31
+ @contextmanager
32
+ def _selector_policy():
33
+ original_policy = asyncio.get_event_loop_policy()
34
+ try:
35
+ if os.name == "nt" and hasattr(asyncio, "WindowsSelectorEventLoopPolicy"):
36
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
37
+
38
+ yield
39
+ finally:
40
+ asyncio.set_event_loop_policy(original_policy)
41
+
42
+
43
+ class _TaskRunner:
44
+ """A task runner that runs an asyncio event loop on a background thread."""
45
+
46
+ def __init__(self) -> None:
47
+ self.__loop: asyncio.AbstractEventLoop | None = None
48
+ self.__runner_thread: threading.Thread | None = None
49
+ self.__lock = threading.Lock()
50
+ atexit.register(self._close)
51
+
52
+ def _close(self) -> None:
53
+ if self.__loop:
54
+ self.__loop.stop()
55
+
56
+ def _execute(self) -> None:
57
+ loop = self.__loop
58
+ assert loop is not None
59
+ try:
60
+ loop.run_forever()
61
+ finally:
62
+ loop.close()
63
+
64
+ def get_exc_handler(self):
65
+ def exc_handler(loop, context):
66
+ logger.error(
67
+ f"Taskrunner for {self.__runner_thread.name if self.__runner_thread else 'no thread'} caught"
68
+ f" exception in {loop}: {context}"
69
+ )
70
+
71
+ return exc_handler
72
+
73
+ def run(self, coro: Any) -> Any:
74
+ """Synchronously run a coroutine on a background thread."""
75
+ name = f"{threading.current_thread().name} : loop-runner"
76
+ with self.__lock:
77
+ if self.__loop is None:
78
+ with _selector_policy():
79
+ self.__loop = asyncio.new_event_loop()
80
+
81
+ exc_handler = self.get_exc_handler()
82
+ self.__loop.set_exception_handler(exc_handler)
83
+ self.__runner_thread = threading.Thread(target=self._execute, daemon=True, name=name)
84
+ self.__runner_thread.start()
85
+ fut = asyncio.run_coroutine_threadsafe(coro, self.__loop)
86
+
87
+ res = fut.result(None)
88
+
89
+ return res
90
+
91
+
92
+ class _AsyncLoopManager:
93
+ def __init__(self: _AsyncLoopManager):
94
+ self._runner_map: dict[str, _TaskRunner] = {}
95
+
96
+ def run_sync(self, coro_func: Callable[..., Awaitable[T]], *args, **kwargs) -> T:
97
+ """
98
+ This should be called from synchronous functions to run an async function.
99
+ """
100
+ name = threading.current_thread().name + f"PID:{os.getpid()}"
101
+ coro = coro_func(*args, **kwargs)
102
+ if name not in self._runner_map:
103
+ if len(self._runner_map) > 500:
104
+ logger.warning(
105
+ "More than 500 event loop runners created!!! This could be a case of runaway recursion..."
106
+ )
107
+ self._runner_map[name] = _TaskRunner()
108
+ return self._runner_map[name].run(coro)
109
+
110
+ def synced(self, coro_func: Callable[P, Awaitable[T]]) -> Callable[P, T]:
111
+ """Make loop run coroutine until it returns. Runs in other thread"""
112
+
113
+ @functools.wraps(coro_func)
114
+ def wrapped(*args: Any, **kwargs: Any) -> T:
115
+ return self.run_sync(coro_func, *args, **kwargs)
116
+
117
+ return wrapped
118
+
119
+
120
+ loop_manager = _AsyncLoopManager()
121
+ 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,27 @@
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] # Python 3.11+
15
+ tasks: typing.List[asyncio.Task] = [asyncio.create_task(c) for c in coros]
16
+ done, pending = await asyncio.wait(tasks, return_when=return_when)
17
+ # TODO we might want to handle asyncio.CancelledError here, for cases when the `action` is cancelled
18
+ # and we want to propagate it to all tasks. Though the backend will handle it anyway,
19
+ # so this is not strictly necessary.
20
+
21
+ for t in pending: # type: asyncio.Task
22
+ t.cancel() # Cancel all tasks that didn't finish first
23
+
24
+ for t in done:
25
+ err = t.exception()
26
+ if err:
27
+ raise err
@@ -0,0 +1,173 @@
1
+ """Helper functions for creating Docker registry credentials for image pull secrets."""
2
+
3
+ import base64
4
+ import json
5
+ import logging
6
+ import os
7
+ import subprocess
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ _CONFIG_JSON = "config.json"
14
+ _DEFAULT_CONFIG_PATH = f"~/.docker/{_CONFIG_JSON}"
15
+ _CRED_HELPERS = "credHelpers"
16
+ _CREDS_STORE = "credsStore"
17
+
18
+
19
+ def _load_docker_config(config_path: str | Path | None = None) -> dict[str, Any]:
20
+ """
21
+ Load Docker config from specified path.
22
+
23
+ Args:
24
+ config_path: Path to Docker config file. If None, uses DOCKER_CONFIG env var
25
+ or defaults to ~/.docker/config.json
26
+
27
+ Returns:
28
+ Dictionary containing Docker config
29
+
30
+ Raises:
31
+ FileNotFoundError: If the config file does not exist
32
+ json.JSONDecodeError: If the config file is not valid JSON
33
+ """
34
+ if not config_path:
35
+ docker_config_env = os.environ.get("DOCKER_CONFIG")
36
+ if docker_config_env:
37
+ config_path = Path(docker_config_env) / _CONFIG_JSON
38
+ else:
39
+ config_path = Path(_DEFAULT_CONFIG_PATH).expanduser()
40
+ else:
41
+ config_path = Path(config_path).expanduser()
42
+
43
+ with open(config_path) as f:
44
+ return json.load(f)
45
+
46
+
47
+ def _get_credential_helper(config: dict[str, Any], registry: str | None = None) -> str | None:
48
+ """Get credential helper for registry or global default."""
49
+ if registry and _CRED_HELPERS in config and registry in config[_CRED_HELPERS]:
50
+ return config[_CRED_HELPERS].get(registry)
51
+ return config.get(_CREDS_STORE)
52
+
53
+
54
+ def _get_credentials_from_helper(helper: str, registry: str) -> tuple[str, str] | None:
55
+ """
56
+ Get credentials from system credential helper.
57
+
58
+ Args:
59
+ helper: Name of the credential helper (e.g., "osxkeychain", "wincred")
60
+ registry: Registry hostname to get credentials for
61
+
62
+ Returns:
63
+ Tuple of (username, password) or None if credentials cannot be retrieved
64
+ """
65
+ helper_cmd = f"docker-credential-{helper}"
66
+
67
+ try:
68
+ process = subprocess.Popen(
69
+ [helper_cmd, "get"],
70
+ stdin=subprocess.PIPE,
71
+ stdout=subprocess.PIPE,
72
+ stderr=subprocess.PIPE,
73
+ text=True,
74
+ )
75
+ output, error = process.communicate(input=registry)
76
+
77
+ if process.returncode != 0:
78
+ logger.error(f"Credential helper error: {error}")
79
+ return None
80
+
81
+ creds = json.loads(output)
82
+ return creds.get("Username"), creds.get("Secret")
83
+ except FileNotFoundError:
84
+ logger.error(f"Credential helper {helper_cmd} not found in PATH")
85
+ return None
86
+ except Exception as e:
87
+ logger.error(f"Error getting credentials: {e!s}")
88
+ return None
89
+
90
+
91
+ def create_dockerconfigjson_from_config(
92
+ registries: list[str] | None = None,
93
+ docker_config_path: str | Path | None = None,
94
+ ) -> str:
95
+ """
96
+ Create a dockerconfigjson string from existing Docker config.
97
+
98
+ This function extracts Docker registry credentials from the user's Docker config file
99
+ and creates a JSON string containing only the credentials for the specified registries.
100
+ It handles credentials stored directly in the config file as well as those managed by
101
+ credential helpers.
102
+
103
+ Args:
104
+ registries: List of registries to extract credentials for. If None, all registries
105
+ from the config will be used.
106
+ docker_config_path: Path to the Docker config file. If None, the function will look
107
+ for the config file in the standard locations.
108
+
109
+ Returns:
110
+ JSON string in dockerconfigjson format: {"auths": {"registry": {"auth": "..."}}}
111
+
112
+ Raises:
113
+ FileNotFoundError: If Docker config file cannot be found
114
+ ValueError: If no credentials can be extracted
115
+ """
116
+ config = _load_docker_config(docker_config_path)
117
+
118
+ # Create new config structure with empty auths
119
+ new_config: dict[str, Any] = {"auths": {}}
120
+
121
+ # Use specified registries or all from config
122
+ target_registries = registries or list(config.get("auths", {}).keys())
123
+
124
+ if not target_registries:
125
+ raise ValueError("No registries found in Docker config and none specified")
126
+
127
+ for registry in target_registries:
128
+ registry_config = config.get("auths", {}).get(registry, {})
129
+ if registry_config.get("auth"):
130
+ # Direct auth token exists
131
+ new_config["auths"][registry] = {"auth": registry_config["auth"]}
132
+ else:
133
+ # Try to get credentials from helper
134
+ helper = _get_credential_helper(config, registry)
135
+ if helper:
136
+ creds = _get_credentials_from_helper(helper, registry)
137
+ if creds:
138
+ username, password = creds
139
+ auth_string = f"{username}:{password}"
140
+ new_config["auths"][registry] = {"auth": base64.b64encode(auth_string.encode()).decode()}
141
+ else:
142
+ logger.warning(f"Could not retrieve credentials for {registry} from credential helper")
143
+ else:
144
+ logger.warning(f"No credentials found for {registry}")
145
+
146
+ if not new_config["auths"]:
147
+ raise ValueError(f"No credentials could be extracted for registries: {', '.join(target_registries)}")
148
+
149
+ return json.dumps(new_config)
150
+
151
+
152
+ def create_dockerconfigjson_from_credentials(
153
+ registry: str,
154
+ username: str,
155
+ password: str,
156
+ ) -> str:
157
+ """
158
+ Create a dockerconfigjson string from explicit credentials.
159
+
160
+ Args:
161
+ registry: Registry hostname (e.g., "ghcr.io", "docker.io")
162
+ username: Username or token name for the registry
163
+ password: Password or access token for the registry
164
+
165
+ Returns:
166
+ JSON string in dockerconfigjson format: {"auths": {"registry": {"auth": "..."}}}
167
+ """
168
+ auth_string = f"{username}:{password}"
169
+ auth_token = base64.b64encode(auth_string.encode()).decode()
170
+
171
+ config = {"auths": {registry: {"auth": auth_token}}}
172
+
173
+ return json.dumps(config)
@@ -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)