flyte 2.0.0b22__py3-none-any.whl → 2.0.0b30__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.
Files changed (197) hide show
  1. flyte/__init__.py +18 -2
  2. flyte/_bin/runtime.py +43 -5
  3. flyte/_cache/cache.py +4 -2
  4. flyte/_cache/local_cache.py +216 -0
  5. flyte/_code_bundle/_ignore.py +1 -1
  6. flyte/_code_bundle/_packaging.py +4 -4
  7. flyte/_code_bundle/_utils.py +14 -8
  8. flyte/_code_bundle/bundle.py +13 -5
  9. flyte/_constants.py +1 -0
  10. flyte/_context.py +4 -1
  11. flyte/_custom_context.py +73 -0
  12. flyte/_debug/constants.py +0 -1
  13. flyte/_debug/vscode.py +6 -1
  14. flyte/_deploy.py +223 -59
  15. flyte/_environment.py +5 -0
  16. flyte/_excepthook.py +1 -1
  17. flyte/_image.py +144 -82
  18. flyte/_initialize.py +95 -12
  19. flyte/_interface.py +2 -0
  20. flyte/_internal/controllers/_local_controller.py +65 -24
  21. flyte/_internal/controllers/_trace.py +1 -1
  22. flyte/_internal/controllers/remote/_action.py +13 -11
  23. flyte/_internal/controllers/remote/_client.py +1 -1
  24. flyte/_internal/controllers/remote/_controller.py +9 -4
  25. flyte/_internal/controllers/remote/_core.py +16 -16
  26. flyte/_internal/controllers/remote/_informer.py +4 -4
  27. flyte/_internal/controllers/remote/_service_protocol.py +7 -7
  28. flyte/_internal/imagebuild/docker_builder.py +139 -84
  29. flyte/_internal/imagebuild/image_builder.py +7 -13
  30. flyte/_internal/imagebuild/remote_builder.py +65 -13
  31. flyte/_internal/imagebuild/utils.py +51 -3
  32. flyte/_internal/resolvers/_task_module.py +5 -38
  33. flyte/_internal/resolvers/default.py +2 -2
  34. flyte/_internal/runtime/convert.py +42 -20
  35. flyte/_internal/runtime/entrypoints.py +24 -1
  36. flyte/_internal/runtime/io.py +21 -8
  37. flyte/_internal/runtime/resources_serde.py +20 -6
  38. flyte/_internal/runtime/reuse.py +1 -1
  39. flyte/_internal/runtime/rusty.py +20 -5
  40. flyte/_internal/runtime/task_serde.py +33 -27
  41. flyte/_internal/runtime/taskrunner.py +10 -1
  42. flyte/_internal/runtime/trigger_serde.py +160 -0
  43. flyte/_internal/runtime/types_serde.py +1 -1
  44. flyte/_keyring/file.py +39 -9
  45. flyte/_logging.py +79 -12
  46. flyte/_map.py +31 -12
  47. flyte/_module.py +70 -0
  48. flyte/_pod.py +2 -2
  49. flyte/_resources.py +213 -31
  50. flyte/_run.py +107 -41
  51. flyte/_task.py +66 -10
  52. flyte/_task_environment.py +96 -24
  53. flyte/_task_plugins.py +4 -2
  54. flyte/_trigger.py +1000 -0
  55. flyte/_utils/__init__.py +2 -1
  56. flyte/_utils/asyn.py +3 -1
  57. flyte/_utils/docker_credentials.py +173 -0
  58. flyte/_utils/module_loader.py +17 -2
  59. flyte/_version.py +3 -3
  60. flyte/cli/_abort.py +3 -3
  61. flyte/cli/_build.py +1 -3
  62. flyte/cli/_common.py +78 -7
  63. flyte/cli/_create.py +178 -3
  64. flyte/cli/_delete.py +23 -1
  65. flyte/cli/_deploy.py +49 -11
  66. flyte/cli/_get.py +79 -34
  67. flyte/cli/_params.py +8 -6
  68. flyte/cli/_plugins.py +209 -0
  69. flyte/cli/_run.py +127 -11
  70. flyte/cli/_serve.py +64 -0
  71. flyte/cli/_update.py +37 -0
  72. flyte/cli/_user.py +17 -0
  73. flyte/cli/main.py +30 -4
  74. flyte/config/_config.py +2 -0
  75. flyte/config/_internal.py +1 -0
  76. flyte/config/_reader.py +3 -3
  77. flyte/connectors/__init__.py +11 -0
  78. flyte/connectors/_connector.py +270 -0
  79. flyte/connectors/_server.py +197 -0
  80. flyte/connectors/utils.py +135 -0
  81. flyte/errors.py +10 -1
  82. flyte/extend.py +8 -1
  83. flyte/extras/_container.py +6 -1
  84. flyte/git/_config.py +11 -9
  85. flyte/io/__init__.py +2 -0
  86. flyte/io/_dataframe/__init__.py +2 -0
  87. flyte/io/_dataframe/basic_dfs.py +1 -1
  88. flyte/io/_dataframe/dataframe.py +12 -8
  89. flyte/io/_dir.py +551 -120
  90. flyte/io/_file.py +538 -141
  91. flyte/models.py +57 -12
  92. flyte/remote/__init__.py +6 -1
  93. flyte/remote/_action.py +18 -16
  94. flyte/remote/_client/_protocols.py +39 -4
  95. flyte/remote/_client/auth/_channel.py +10 -6
  96. flyte/remote/_client/controlplane.py +17 -5
  97. flyte/remote/_console.py +3 -2
  98. flyte/remote/_data.py +4 -3
  99. flyte/remote/_logs.py +3 -3
  100. flyte/remote/_run.py +47 -7
  101. flyte/remote/_secret.py +26 -17
  102. flyte/remote/_task.py +21 -9
  103. flyte/remote/_trigger.py +306 -0
  104. flyte/remote/_user.py +33 -0
  105. flyte/storage/__init__.py +6 -1
  106. flyte/storage/_parallel_reader.py +274 -0
  107. flyte/storage/_storage.py +185 -103
  108. flyte/types/__init__.py +16 -0
  109. flyte/types/_interface.py +2 -2
  110. flyte/types/_pickle.py +17 -4
  111. flyte/types/_string_literals.py +8 -9
  112. flyte/types/_type_engine.py +26 -19
  113. flyte/types/_utils.py +1 -1
  114. {flyte-2.0.0b22.data → flyte-2.0.0b30.data}/scripts/runtime.py +43 -5
  115. {flyte-2.0.0b22.dist-info → flyte-2.0.0b30.dist-info}/METADATA +8 -1
  116. flyte-2.0.0b30.dist-info/RECORD +192 -0
  117. flyte/_protos/__init__.py +0 -0
  118. flyte/_protos/common/authorization_pb2.py +0 -66
  119. flyte/_protos/common/authorization_pb2.pyi +0 -108
  120. flyte/_protos/common/authorization_pb2_grpc.py +0 -4
  121. flyte/_protos/common/identifier_pb2.py +0 -99
  122. flyte/_protos/common/identifier_pb2.pyi +0 -120
  123. flyte/_protos/common/identifier_pb2_grpc.py +0 -4
  124. flyte/_protos/common/identity_pb2.py +0 -48
  125. flyte/_protos/common/identity_pb2.pyi +0 -72
  126. flyte/_protos/common/identity_pb2_grpc.py +0 -4
  127. flyte/_protos/common/list_pb2.py +0 -36
  128. flyte/_protos/common/list_pb2.pyi +0 -71
  129. flyte/_protos/common/list_pb2_grpc.py +0 -4
  130. flyte/_protos/common/policy_pb2.py +0 -37
  131. flyte/_protos/common/policy_pb2.pyi +0 -27
  132. flyte/_protos/common/policy_pb2_grpc.py +0 -4
  133. flyte/_protos/common/role_pb2.py +0 -37
  134. flyte/_protos/common/role_pb2.pyi +0 -53
  135. flyte/_protos/common/role_pb2_grpc.py +0 -4
  136. flyte/_protos/common/runtime_version_pb2.py +0 -28
  137. flyte/_protos/common/runtime_version_pb2.pyi +0 -24
  138. flyte/_protos/common/runtime_version_pb2_grpc.py +0 -4
  139. flyte/_protos/imagebuilder/definition_pb2.py +0 -60
  140. flyte/_protos/imagebuilder/definition_pb2.pyi +0 -153
  141. flyte/_protos/imagebuilder/definition_pb2_grpc.py +0 -4
  142. flyte/_protos/imagebuilder/payload_pb2.py +0 -32
  143. flyte/_protos/imagebuilder/payload_pb2.pyi +0 -21
  144. flyte/_protos/imagebuilder/payload_pb2_grpc.py +0 -4
  145. flyte/_protos/imagebuilder/service_pb2.py +0 -29
  146. flyte/_protos/imagebuilder/service_pb2.pyi +0 -5
  147. flyte/_protos/imagebuilder/service_pb2_grpc.py +0 -66
  148. flyte/_protos/logs/dataplane/payload_pb2.py +0 -100
  149. flyte/_protos/logs/dataplane/payload_pb2.pyi +0 -177
  150. flyte/_protos/logs/dataplane/payload_pb2_grpc.py +0 -4
  151. flyte/_protos/secret/definition_pb2.py +0 -49
  152. flyte/_protos/secret/definition_pb2.pyi +0 -93
  153. flyte/_protos/secret/definition_pb2_grpc.py +0 -4
  154. flyte/_protos/secret/payload_pb2.py +0 -62
  155. flyte/_protos/secret/payload_pb2.pyi +0 -94
  156. flyte/_protos/secret/payload_pb2_grpc.py +0 -4
  157. flyte/_protos/secret/secret_pb2.py +0 -38
  158. flyte/_protos/secret/secret_pb2.pyi +0 -6
  159. flyte/_protos/secret/secret_pb2_grpc.py +0 -198
  160. flyte/_protos/secret/secret_pb2_grpc_grpc.py +0 -198
  161. flyte/_protos/validate/validate/validate_pb2.py +0 -76
  162. flyte/_protos/workflow/common_pb2.py +0 -27
  163. flyte/_protos/workflow/common_pb2.pyi +0 -14
  164. flyte/_protos/workflow/common_pb2_grpc.py +0 -4
  165. flyte/_protos/workflow/environment_pb2.py +0 -29
  166. flyte/_protos/workflow/environment_pb2.pyi +0 -12
  167. flyte/_protos/workflow/environment_pb2_grpc.py +0 -4
  168. flyte/_protos/workflow/node_execution_service_pb2.py +0 -26
  169. flyte/_protos/workflow/node_execution_service_pb2.pyi +0 -4
  170. flyte/_protos/workflow/node_execution_service_pb2_grpc.py +0 -32
  171. flyte/_protos/workflow/queue_service_pb2.py +0 -111
  172. flyte/_protos/workflow/queue_service_pb2.pyi +0 -168
  173. flyte/_protos/workflow/queue_service_pb2_grpc.py +0 -172
  174. flyte/_protos/workflow/run_definition_pb2.py +0 -123
  175. flyte/_protos/workflow/run_definition_pb2.pyi +0 -352
  176. flyte/_protos/workflow/run_definition_pb2_grpc.py +0 -4
  177. flyte/_protos/workflow/run_logs_service_pb2.py +0 -41
  178. flyte/_protos/workflow/run_logs_service_pb2.pyi +0 -28
  179. flyte/_protos/workflow/run_logs_service_pb2_grpc.py +0 -69
  180. flyte/_protos/workflow/run_service_pb2.py +0 -137
  181. flyte/_protos/workflow/run_service_pb2.pyi +0 -185
  182. flyte/_protos/workflow/run_service_pb2_grpc.py +0 -446
  183. flyte/_protos/workflow/state_service_pb2.py +0 -67
  184. flyte/_protos/workflow/state_service_pb2.pyi +0 -76
  185. flyte/_protos/workflow/state_service_pb2_grpc.py +0 -138
  186. flyte/_protos/workflow/task_definition_pb2.py +0 -82
  187. flyte/_protos/workflow/task_definition_pb2.pyi +0 -88
  188. flyte/_protos/workflow/task_definition_pb2_grpc.py +0 -4
  189. flyte/_protos/workflow/task_service_pb2.py +0 -60
  190. flyte/_protos/workflow/task_service_pb2.pyi +0 -59
  191. flyte/_protos/workflow/task_service_pb2_grpc.py +0 -138
  192. flyte-2.0.0b22.dist-info/RECORD +0 -250
  193. {flyte-2.0.0b22.data → flyte-2.0.0b30.data}/scripts/debug.py +0 -0
  194. {flyte-2.0.0b22.dist-info → flyte-2.0.0b30.dist-info}/WHEEL +0 -0
  195. {flyte-2.0.0b22.dist-info → flyte-2.0.0b30.dist-info}/entry_points.txt +0 -0
  196. {flyte-2.0.0b22.dist-info → flyte-2.0.0b30.dist-info}/licenses/LICENSE +0 -0
  197. {flyte-2.0.0b22.dist-info → flyte-2.0.0b30.dist-info}/top_level.txt +0 -0
flyte/_utils/__init__.py CHANGED
@@ -9,12 +9,13 @@ from .coro_management import run_coros
9
9
  from .file_handling import filehash_update, update_hasher_for_source
10
10
  from .helpers import get_cwd_editable_install
11
11
  from .lazy_module import lazy_module
12
- from .module_loader import load_python_modules
12
+ from .module_loader import adjust_sys_path, load_python_modules
13
13
  from .org_discovery import hostname_from_url, org_from_endpoint, sanitize_endpoint
14
14
  from .uv_script_parser import parse_uv_script_file
15
15
 
16
16
  __all__ = [
17
17
  "AsyncLRUCache",
18
+ "adjust_sys_path",
18
19
  "filehash_update",
19
20
  "get_cwd_editable_install",
20
21
  "hostname_from_url",
flyte/_utils/asyn.py CHANGED
@@ -9,6 +9,8 @@ async def async_add(a: int, b: int) -> int:
9
9
  result = run_sync(async_add, a=10, b=12)
10
10
  """
11
11
 
12
+ from __future__ import annotations
13
+
12
14
  import asyncio
13
15
  import atexit
14
16
  import functools
@@ -88,7 +90,7 @@ class _TaskRunner:
88
90
 
89
91
 
90
92
  class _AsyncLoopManager:
91
- def __init__(self):
93
+ def __init__(self: _AsyncLoopManager):
92
94
  self._runner_map: dict[str, _TaskRunner] = {}
93
95
 
94
96
  def run_sync(self, coro_func: Callable[..., Awaitable[T]], *args, **kwargs) -> T:
@@ -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)
@@ -5,9 +5,9 @@ import sys
5
5
  from pathlib import Path
6
6
  from typing import List, Tuple
7
7
 
8
- from rich.progress import BarColumn, Progress, TextColumn, TimeElapsedColumn, TimeRemainingColumn
9
-
10
8
  import flyte.errors
9
+ from flyte._constants import FLYTE_SYS_PATH
10
+ from flyte._logging import logger
11
11
 
12
12
 
13
13
  def load_python_modules(path: Path, recursive: bool = False) -> Tuple[List[str], List[Tuple[Path, str]]]:
@@ -18,6 +18,8 @@ def load_python_modules(path: Path, recursive: bool = False) -> Tuple[List[str],
18
18
  :param recursive: If True, load modules recursively from subdirectories
19
19
  :return: List of loaded module names, and list of file paths that failed to load
20
20
  """
21
+ from rich.progress import BarColumn, Progress, TextColumn, TimeElapsedColumn, TimeRemainingColumn
22
+
21
23
  loaded_modules = []
22
24
  failed_paths = []
23
25
 
@@ -87,3 +89,16 @@ def _load_module_from_file(file_path: Path) -> str | None:
87
89
 
88
90
  except Exception as e:
89
91
  raise flyte.errors.ModuleLoadError(f"Failed to load module from {file_path}: {e}") from e
92
+
93
+
94
+ def adjust_sys_path():
95
+ """
96
+ Adjust sys.path to include local sys.path entries under the root directory.
97
+ """
98
+ if "." not in sys.path or os.getcwd() not in sys.path:
99
+ sys.path.insert(0, ".")
100
+ logger.info(f"Added {os.getcwd()} to sys.path")
101
+ for p in os.environ.get(FLYTE_SYS_PATH, "").split(":"):
102
+ if p and p not in sys.path:
103
+ sys.path.insert(0, p)
104
+ logger.info(f"Added {p} to sys.path")
flyte/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '2.0.0b22'
32
- __version_tuple__ = version_tuple = (2, 0, 0, 'b22')
31
+ __version__ = version = '2.0.0b30'
32
+ __version_tuple__ = version_tuple = (2, 0, 0, 'b30')
33
33
 
34
- __commit_id__ = commit_id = 'gce9a6bede'
34
+ __commit_id__ = commit_id = 'gf1b5e2b2b'
flyte/cli/_abort.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import rich_click as click
2
- from rich.console import Console
3
2
 
4
3
  from flyte.cli import _common as common
5
4
 
@@ -23,6 +22,7 @@ def run(cfg: common.CLIConfig, run_name: str, project: str | None = None, domain
23
22
  cfg.init(project=project, domain=domain)
24
23
  r = Run.get(name=run_name)
25
24
  if r:
26
- console = Console()
27
- r.abort()
25
+ console = common.get_console()
26
+ with console.status(f"Aborting run '{run_name}'...", spinner="dots"):
27
+ r.abort()
28
28
  console.print(f"Run '{run_name}' has been aborted.")
flyte/cli/_build.py CHANGED
@@ -44,9 +44,7 @@ class BuildEnvCommand(click.Command):
44
44
  super().__init__(*args, **kwargs)
45
45
 
46
46
  def invoke(self, ctx: click.Context):
47
- from rich.console import Console
48
-
49
- console = Console()
47
+ console = common.get_console()
50
48
  console.print(f"Building Environment: {self.obj_name}")
51
49
  obj: CLIConfig = ctx.obj
52
50
  obj.init()
flyte/cli/_common.py CHANGED
@@ -1,11 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import importlib.util
4
+ import json
4
5
  import logging
5
6
  import os
7
+ import pathlib
6
8
  import sys
7
9
  from abc import abstractmethod
8
10
  from dataclasses import dataclass, replace
11
+ from functools import lru_cache
9
12
  from pathlib import Path
10
13
  from types import MappingProxyType, ModuleType
11
14
  from typing import Any, Dict, Iterable, List, Literal, Optional
@@ -19,10 +22,12 @@ from rich.pretty import pretty_repr
19
22
  from rich.table import Table
20
23
  from rich.traceback import Traceback
21
24
 
25
+ import flyte.config
22
26
  import flyte.errors
27
+ from flyte._logging import LogFormat
23
28
  from flyte.config import Config
24
29
 
25
- OutputFormat = Literal["table", "json", "table-simple"]
30
+ OutputFormat = Literal["table", "json", "table-simple", "json-raw"]
26
31
 
27
32
  PREFERRED_BORDER_COLOR = "dim cyan"
28
33
  PREFERRED_ACCENT_COLOR = "bold #FFD700"
@@ -99,6 +104,7 @@ class CLIConfig:
99
104
  config: Config
100
105
  ctx: click.Context
101
106
  log_level: int | None = logging.ERROR
107
+ log_format: LogFormat = "console"
102
108
  endpoint: str | None = None
103
109
  insecure: bool = False
104
110
  org: str | None = None
@@ -111,13 +117,20 @@ class CLIConfig:
111
117
  """
112
118
  return replace(self, **kwargs)
113
119
 
114
- def init(self, project: str | None = None, domain: str | None = None, root_dir: str | None = None):
120
+ def init(
121
+ self,
122
+ project: str | None = None,
123
+ domain: str | None = None,
124
+ root_dir: str | None = None,
125
+ images: tuple[str, ...] | None = None,
126
+ sync_local_sys_paths: bool = True,
127
+ ):
115
128
  from flyte.config._config import TaskConfig
116
129
 
117
130
  task_cfg = TaskConfig(
118
131
  org=self.org or self.config.task.org,
119
- project=project or self.config.task.project,
120
- domain=domain or self.config.task.domain,
132
+ project=project if project is not None else self.config.task.project,
133
+ domain=domain if domain is not None else self.config.task.domain,
121
134
  )
122
135
 
123
136
  kwargs: Dict[str, Any] = {}
@@ -131,7 +144,14 @@ class CLIConfig:
131
144
 
132
145
  updated_config = self.config.with_params(platform_cfg, task_cfg)
133
146
 
134
- flyte.init_from_config(updated_config, log_level=self.log_level, root_dir=root_dir)
147
+ flyte.init_from_config(
148
+ updated_config,
149
+ log_level=self.log_level,
150
+ log_format=self.log_format,
151
+ root_dir=pathlib.Path(root_dir) if root_dir else None,
152
+ images=images,
153
+ sync_local_sys_paths=sync_local_sys_paths,
154
+ )
135
155
 
136
156
 
137
157
  class InvokeBaseMixin:
@@ -177,7 +197,7 @@ class InvokeBaseMixin:
177
197
  except Exception as e:
178
198
  if ctx.obj and ctx.obj.log_level and ctx.obj.log_level <= logging.DEBUG:
179
199
  # If the user has requested verbose output, print the full traceback
180
- console = Console()
200
+ console = get_console()
181
201
  console.print(Traceback.from_exception(type(e), e, e.__traceback__))
182
202
  exit(1)
183
203
  else:
@@ -354,7 +374,7 @@ def _table_format(table: Table, vals: Iterable[Any]) -> Table:
354
374
  if headers is None:
355
375
  headers = [k for k, _ in o]
356
376
  for h in headers:
357
- table.add_column(h.capitalize())
377
+ table.add_column(h.capitalize(), no_wrap=True if "name" in h.casefold() else False)
358
378
  table.add_row(*[str(v) for _, v in o])
359
379
  return table
360
380
 
@@ -374,6 +394,7 @@ def format(title: str, vals: Iterable[Any], of: OutputFormat = "table") -> Table
374
394
  header_style=HEADER_STYLE,
375
395
  show_header=True,
376
396
  border_style=PREFERRED_BORDER_COLOR,
397
+ expand=True,
377
398
  ),
378
399
  vals,
379
400
  )
@@ -381,6 +402,11 @@ def format(title: str, vals: Iterable[Any], of: OutputFormat = "table") -> Table
381
402
  if not vals:
382
403
  return pretty_repr([])
383
404
  return pretty_repr([v.to_dict() for v in vals])
405
+ case "json-raw":
406
+ if not vals:
407
+ return []
408
+ return json.dumps([v.to_dict() for v in vals])
409
+
384
410
  raise click.ClickException("Unknown output format. Supported formats are: table, table-simple, json.")
385
411
 
386
412
 
@@ -395,3 +421,48 @@ def get_panel(title: str, renderable: Any, of: OutputFormat = "table") -> Panel:
395
421
  title=f"[{PREFERRED_ACCENT_COLOR}]{title}[/{PREFERRED_ACCENT_COLOR}]",
396
422
  border_style=PREFERRED_BORDER_COLOR,
397
423
  )
424
+
425
+
426
+ def get_console() -> Console:
427
+ """
428
+ Get a console that is configured to use colors if the terminal supports it.
429
+ """
430
+ return Console(color_system="auto", force_terminal=True, width=120)
431
+
432
+
433
+ def parse_images(cfg: Config, values: tuple[str, ...] | None) -> None:
434
+ """
435
+ Parse image values and update the config.
436
+
437
+ Args:
438
+ cfg: The Config object to write images to
439
+ values: List of image strings in format "imagename=imageuri" or just "imageuri"
440
+ """
441
+ if values is None:
442
+ return
443
+ for value in values:
444
+ if "=" in value:
445
+ image_name, image_uri = value.split("=", 1)
446
+ cfg.image.image_refs[image_name] = image_uri
447
+ else:
448
+ # If no name specified, use "default" as the name
449
+ cfg.image.image_refs["default"] = value
450
+
451
+
452
+ @lru_cache()
453
+ def initialize_config(
454
+ ctx: click.Context,
455
+ project: str,
456
+ domain: str,
457
+ root_dir: str | None = None,
458
+ images: tuple[str, ...] | None = None,
459
+ sync_local_sys_paths: bool = True,
460
+ ):
461
+ obj: CLIConfig | None = ctx.obj
462
+ if obj is None:
463
+ import flyte.config
464
+
465
+ obj = CLIConfig(flyte.config.auto(), ctx)
466
+
467
+ obj.init(project, domain, root_dir, images, sync_local_sys_paths)
468
+ return obj
flyte/cli/_create.py CHANGED
@@ -3,6 +3,7 @@ from typing import Any, Dict, get_args
3
3
 
4
4
  import rich_click as click
5
5
 
6
+ import flyte
6
7
  import flyte.cli._common as common
7
8
  from flyte.cli._option import MutuallyExclusiveOption
8
9
  from flyte.remote import SecretTypes
@@ -23,18 +24,49 @@ def create():
23
24
  prompt="Enter secret value",
24
25
  hide_input=True,
25
26
  cls=MutuallyExclusiveOption,
26
- mutually_exclusive=["from_file"],
27
+ mutually_exclusive=["from_file", "from_docker_config", "registry"],
27
28
  )
28
29
  @click.option(
29
30
  "--from-file",
30
31
  type=click.Path(exists=True),
31
32
  help="Path to the file with the binary secret.",
32
33
  cls=MutuallyExclusiveOption,
33
- mutually_exclusive=["value"],
34
+ mutually_exclusive=["value", "from_docker_config", "registry"],
34
35
  )
35
36
  @click.option(
36
37
  "--type", type=click.Choice(get_args(SecretTypes)), default="regular", help="Type of the secret.", show_default=True
37
38
  )
39
+ @click.option(
40
+ "--from-docker-config",
41
+ is_flag=True,
42
+ help="Create image pull secret from Docker config file (only for --type image_pull).",
43
+ cls=MutuallyExclusiveOption,
44
+ mutually_exclusive=["value", "from_file", "registry", "username", "password"],
45
+ )
46
+ @click.option(
47
+ "--docker-config-path",
48
+ type=click.Path(exists=True),
49
+ help="Path to Docker config file (defaults to ~/.docker/config.json or $DOCKER_CONFIG).",
50
+ )
51
+ @click.option(
52
+ "--registries",
53
+ help="Comma-separated list of registries to include (only with --from-docker-config).",
54
+ )
55
+ @click.option(
56
+ "--registry",
57
+ help="Registry hostname (e.g., ghcr.io, docker.io) for explicit credentials (only for --type image_pull).",
58
+ cls=MutuallyExclusiveOption,
59
+ mutually_exclusive=["value", "from_file", "from_docker_config"],
60
+ )
61
+ @click.option(
62
+ "--username",
63
+ help="Username for the registry (only with --registry).",
64
+ )
65
+ @click.option(
66
+ "--password",
67
+ help="Password for the registry (only with --registry). If not provided, will prompt.",
68
+ hide_input=True,
69
+ )
38
70
  @click.pass_obj
39
71
  def secret(
40
72
  cfg: common.CLIConfig,
@@ -42,6 +74,12 @@ def secret(
42
74
  value: str | bytes | None = None,
43
75
  from_file: str | None = None,
44
76
  type: SecretTypes = "regular",
77
+ from_docker_config: bool = False,
78
+ docker_config_path: str | None = None,
79
+ registries: str | None = None,
80
+ registry: str | None = None,
81
+ username: str | None = None,
82
+ password: str | None = None,
45
83
  project: str | None = None,
46
84
  domain: str | None = None,
47
85
  ):
@@ -72,16 +110,80 @@ def secret(
72
110
  Other secrets should be specified as `regular`.
73
111
  If no type is specified, `regular` is assumed.
74
112
 
113
+ For image pull secrets, you have several options:
114
+
115
+ 1. Interactive mode (prompts for registry, username, password):
75
116
  ```bash
76
117
  $ flyte create secret my_secret --type image_pull
77
118
  ```
119
+
120
+ 2. With explicit credentials:
121
+ ```bash
122
+ $ flyte create secret my_secret --type image_pull --registry ghcr.io --username myuser
123
+ ```
124
+
125
+ 3. Lastly, you can create a secret from your existing Docker installation (i.e., you've run `docker login` in
126
+ the past) and you just want to pull from those credentials. Since you may have logged in to multiple registries,
127
+ you can specify which registries to include. If no registries are specified, all registries are added.
128
+ ```bash
129
+ $ flyte create secret my_secret --type image_pull --from-docker-config --registries ghcr.io,docker.io
130
+ ```
78
131
  """
79
132
  from flyte.remote import Secret
80
133
 
134
+ # todo: remove this hack when secrets creation more easily distinguishes between org and project/domain level
135
+ # (and domain level) secrets
136
+ project = "" if project is None else project
137
+ domain = "" if domain is None else domain
81
138
  cfg.init(project, domain)
82
- if from_file:
139
+
140
+ # Handle image pull secret creation
141
+ if type == "image_pull":
142
+ if project != "" or domain != "":
143
+ raise click.ClickException("Project and domain must not be set when creating an image pull secret.")
144
+
145
+ if from_docker_config:
146
+ # Mode 3: From Docker config
147
+ from flyte._utils.docker_credentials import create_dockerconfigjson_from_config
148
+
149
+ registry_list = [r.strip() for r in registries.split(",")] if registries else None
150
+ try:
151
+ value = create_dockerconfigjson_from_config(
152
+ registries=registry_list,
153
+ docker_config_path=docker_config_path,
154
+ )
155
+ except Exception as e:
156
+ raise click.ClickException(f"Failed to create dockerconfigjson from Docker config: {e}") from e
157
+
158
+ elif registry:
159
+ # Mode 2: Explicit credentials
160
+ from flyte._utils.docker_credentials import create_dockerconfigjson_from_credentials
161
+
162
+ if not username:
163
+ username = click.prompt("Username")
164
+ if not password:
165
+ password = click.prompt("Password", hide_input=True)
166
+
167
+ value = create_dockerconfigjson_from_credentials(registry, username, password)
168
+
169
+ else:
170
+ # Mode 1: Interactive prompts
171
+ from flyte._utils.docker_credentials import create_dockerconfigjson_from_credentials
172
+
173
+ registry = click.prompt("Registry (e.g., ghcr.io, docker.io)")
174
+ username = click.prompt("Username")
175
+ password = click.prompt("Password", hide_input=True)
176
+
177
+ value = create_dockerconfigjson_from_credentials(registry, username, password)
178
+
179
+ elif from_file:
83
180
  with open(from_file, "rb") as f:
84
181
  value = f.read()
182
+
183
+ # Encode string values to bytes
184
+ if isinstance(value, str):
185
+ value = value.encode("utf-8")
186
+
85
187
  Secret.create(name=name, value=value, type=type)
86
188
 
87
189
 
@@ -194,3 +296,76 @@ def config(
194
296
  yaml.dump(d, f)
195
297
 
196
298
  click.echo(f"Config file written to {output_path}")
299
+
300
+
301
+ @create.command(cls=common.CommandBase)
302
+ @click.argument("task_name", type=str, required=True)
303
+ @click.argument("name", type=str, required=True)
304
+ @click.option(
305
+ "--schedule",
306
+ type=str,
307
+ required=True,
308
+ help="Cron schedule for the trigger. Defaults to every minute.",
309
+ show_default=True,
310
+ )
311
+ @click.option(
312
+ "--description",
313
+ type=str,
314
+ default="",
315
+ help="Description of the trigger.",
316
+ show_default=True,
317
+ )
318
+ @click.option(
319
+ "--auto-activate",
320
+ is_flag=True,
321
+ default=True,
322
+ help="Whether the trigger should not be automatically activated. Defaults to True.",
323
+ show_default=True,
324
+ )
325
+ @click.option(
326
+ "--trigger-time-var",
327
+ type=str,
328
+ default="trigger_time",
329
+ help="Variable name for the trigger time in the task inputs. Defaults to 'trigger_time'.",
330
+ show_default=True,
331
+ )
332
+ @click.pass_obj
333
+ def trigger(
334
+ cfg: common.CLIConfig,
335
+ task_name: str,
336
+ name: str,
337
+ schedule: str,
338
+ trigger_time_var: str = "trigger_time",
339
+ auto_activate: bool = True,
340
+ description: str = "",
341
+ project: str | None = None,
342
+ domain: str | None = None,
343
+ ):
344
+ """
345
+ Create a new trigger for a task. The task name and trigger name are required.
346
+
347
+ Example:
348
+
349
+ ```bash
350
+ $ flyte create trigger my_task my_trigger --schedule "0 0 * * *"
351
+ ```
352
+
353
+ This will create a trigger that runs every day at midnight.
354
+ """
355
+ from flyte.remote import Trigger
356
+
357
+ cfg.init(project, domain)
358
+ console = common.get_console()
359
+
360
+ trigger = flyte.Trigger(
361
+ name=name,
362
+ automation=flyte.Cron(schedule),
363
+ description=description,
364
+ auto_activate=auto_activate,
365
+ inputs={trigger_time_var: flyte.TriggerTime}, # Use the trigger time variable in inputs
366
+ env_vars=None,
367
+ interruptible=None,
368
+ )
369
+ with console.status("Creating trigger..."):
370
+ v = Trigger.create(trigger, task_name=task_name)
371
+ console.print(f"[bold green]Trigger {v.name} created successfully![/bold green]")