flyte 0.1.0__py3-none-any.whl → 0.2.0a0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (219) hide show
  1. flyte/__init__.py +78 -2
  2. flyte/_bin/__init__.py +0 -0
  3. flyte/_bin/runtime.py +152 -0
  4. flyte/_build.py +26 -0
  5. flyte/_cache/__init__.py +12 -0
  6. flyte/_cache/cache.py +145 -0
  7. flyte/_cache/defaults.py +9 -0
  8. flyte/_cache/policy_function_body.py +42 -0
  9. flyte/_code_bundle/__init__.py +8 -0
  10. flyte/_code_bundle/_ignore.py +113 -0
  11. flyte/_code_bundle/_packaging.py +187 -0
  12. flyte/_code_bundle/_utils.py +323 -0
  13. flyte/_code_bundle/bundle.py +209 -0
  14. flyte/_context.py +152 -0
  15. flyte/_deploy.py +243 -0
  16. flyte/_doc.py +29 -0
  17. flyte/_docstring.py +32 -0
  18. flyte/_environment.py +84 -0
  19. flyte/_excepthook.py +37 -0
  20. flyte/_group.py +32 -0
  21. flyte/_hash.py +23 -0
  22. flyte/_image.py +762 -0
  23. flyte/_initialize.py +492 -0
  24. flyte/_interface.py +84 -0
  25. flyte/_internal/__init__.py +3 -0
  26. flyte/_internal/controllers/__init__.py +128 -0
  27. flyte/_internal/controllers/_local_controller.py +193 -0
  28. flyte/_internal/controllers/_trace.py +41 -0
  29. flyte/_internal/controllers/remote/__init__.py +60 -0
  30. flyte/_internal/controllers/remote/_action.py +146 -0
  31. flyte/_internal/controllers/remote/_client.py +47 -0
  32. flyte/_internal/controllers/remote/_controller.py +494 -0
  33. flyte/_internal/controllers/remote/_core.py +410 -0
  34. flyte/_internal/controllers/remote/_informer.py +361 -0
  35. flyte/_internal/controllers/remote/_service_protocol.py +50 -0
  36. flyte/_internal/imagebuild/__init__.py +11 -0
  37. flyte/_internal/imagebuild/docker_builder.py +427 -0
  38. flyte/_internal/imagebuild/image_builder.py +246 -0
  39. flyte/_internal/imagebuild/remote_builder.py +0 -0
  40. flyte/_internal/resolvers/__init__.py +0 -0
  41. flyte/_internal/resolvers/_task_module.py +54 -0
  42. flyte/_internal/resolvers/common.py +31 -0
  43. flyte/_internal/resolvers/default.py +28 -0
  44. flyte/_internal/runtime/__init__.py +0 -0
  45. flyte/_internal/runtime/convert.py +342 -0
  46. flyte/_internal/runtime/entrypoints.py +135 -0
  47. flyte/_internal/runtime/io.py +136 -0
  48. flyte/_internal/runtime/resources_serde.py +138 -0
  49. flyte/_internal/runtime/task_serde.py +330 -0
  50. flyte/_internal/runtime/taskrunner.py +191 -0
  51. flyte/_internal/runtime/types_serde.py +54 -0
  52. flyte/_logging.py +135 -0
  53. flyte/_map.py +215 -0
  54. flyte/_pod.py +19 -0
  55. flyte/_protos/__init__.py +0 -0
  56. flyte/_protos/common/authorization_pb2.py +66 -0
  57. flyte/_protos/common/authorization_pb2.pyi +108 -0
  58. flyte/_protos/common/authorization_pb2_grpc.py +4 -0
  59. flyte/_protos/common/identifier_pb2.py +71 -0
  60. flyte/_protos/common/identifier_pb2.pyi +82 -0
  61. flyte/_protos/common/identifier_pb2_grpc.py +4 -0
  62. flyte/_protos/common/identity_pb2.py +48 -0
  63. flyte/_protos/common/identity_pb2.pyi +72 -0
  64. flyte/_protos/common/identity_pb2_grpc.py +4 -0
  65. flyte/_protos/common/list_pb2.py +36 -0
  66. flyte/_protos/common/list_pb2.pyi +71 -0
  67. flyte/_protos/common/list_pb2_grpc.py +4 -0
  68. flyte/_protos/common/policy_pb2.py +37 -0
  69. flyte/_protos/common/policy_pb2.pyi +27 -0
  70. flyte/_protos/common/policy_pb2_grpc.py +4 -0
  71. flyte/_protos/common/role_pb2.py +37 -0
  72. flyte/_protos/common/role_pb2.pyi +53 -0
  73. flyte/_protos/common/role_pb2_grpc.py +4 -0
  74. flyte/_protos/common/runtime_version_pb2.py +28 -0
  75. flyte/_protos/common/runtime_version_pb2.pyi +24 -0
  76. flyte/_protos/common/runtime_version_pb2_grpc.py +4 -0
  77. flyte/_protos/logs/dataplane/payload_pb2.py +100 -0
  78. flyte/_protos/logs/dataplane/payload_pb2.pyi +177 -0
  79. flyte/_protos/logs/dataplane/payload_pb2_grpc.py +4 -0
  80. flyte/_protos/secret/definition_pb2.py +49 -0
  81. flyte/_protos/secret/definition_pb2.pyi +93 -0
  82. flyte/_protos/secret/definition_pb2_grpc.py +4 -0
  83. flyte/_protos/secret/payload_pb2.py +62 -0
  84. flyte/_protos/secret/payload_pb2.pyi +94 -0
  85. flyte/_protos/secret/payload_pb2_grpc.py +4 -0
  86. flyte/_protos/secret/secret_pb2.py +38 -0
  87. flyte/_protos/secret/secret_pb2.pyi +6 -0
  88. flyte/_protos/secret/secret_pb2_grpc.py +198 -0
  89. flyte/_protos/secret/secret_pb2_grpc_grpc.py +198 -0
  90. flyte/_protos/validate/validate/validate_pb2.py +76 -0
  91. flyte/_protos/workflow/common_pb2.py +27 -0
  92. flyte/_protos/workflow/common_pb2.pyi +14 -0
  93. flyte/_protos/workflow/common_pb2_grpc.py +4 -0
  94. flyte/_protos/workflow/environment_pb2.py +29 -0
  95. flyte/_protos/workflow/environment_pb2.pyi +12 -0
  96. flyte/_protos/workflow/environment_pb2_grpc.py +4 -0
  97. flyte/_protos/workflow/node_execution_service_pb2.py +26 -0
  98. flyte/_protos/workflow/node_execution_service_pb2.pyi +4 -0
  99. flyte/_protos/workflow/node_execution_service_pb2_grpc.py +32 -0
  100. flyte/_protos/workflow/queue_service_pb2.py +105 -0
  101. flyte/_protos/workflow/queue_service_pb2.pyi +146 -0
  102. flyte/_protos/workflow/queue_service_pb2_grpc.py +172 -0
  103. flyte/_protos/workflow/run_definition_pb2.py +128 -0
  104. flyte/_protos/workflow/run_definition_pb2.pyi +314 -0
  105. flyte/_protos/workflow/run_definition_pb2_grpc.py +4 -0
  106. flyte/_protos/workflow/run_logs_service_pb2.py +41 -0
  107. flyte/_protos/workflow/run_logs_service_pb2.pyi +28 -0
  108. flyte/_protos/workflow/run_logs_service_pb2_grpc.py +69 -0
  109. flyte/_protos/workflow/run_service_pb2.py +129 -0
  110. flyte/_protos/workflow/run_service_pb2.pyi +171 -0
  111. flyte/_protos/workflow/run_service_pb2_grpc.py +412 -0
  112. flyte/_protos/workflow/state_service_pb2.py +66 -0
  113. flyte/_protos/workflow/state_service_pb2.pyi +75 -0
  114. flyte/_protos/workflow/state_service_pb2_grpc.py +138 -0
  115. flyte/_protos/workflow/task_definition_pb2.py +79 -0
  116. flyte/_protos/workflow/task_definition_pb2.pyi +81 -0
  117. flyte/_protos/workflow/task_definition_pb2_grpc.py +4 -0
  118. flyte/_protos/workflow/task_service_pb2.py +60 -0
  119. flyte/_protos/workflow/task_service_pb2.pyi +59 -0
  120. flyte/_protos/workflow/task_service_pb2_grpc.py +138 -0
  121. flyte/_resources.py +226 -0
  122. flyte/_retry.py +32 -0
  123. flyte/_reusable_environment.py +25 -0
  124. flyte/_run.py +482 -0
  125. flyte/_secret.py +61 -0
  126. flyte/_task.py +449 -0
  127. flyte/_task_environment.py +183 -0
  128. flyte/_timeout.py +47 -0
  129. flyte/_tools.py +27 -0
  130. flyte/_trace.py +120 -0
  131. flyte/_utils/__init__.py +26 -0
  132. flyte/_utils/asyn.py +119 -0
  133. flyte/_utils/async_cache.py +139 -0
  134. flyte/_utils/coro_management.py +23 -0
  135. flyte/_utils/file_handling.py +72 -0
  136. flyte/_utils/helpers.py +134 -0
  137. flyte/_utils/lazy_module.py +54 -0
  138. flyte/_utils/org_discovery.py +57 -0
  139. flyte/_utils/uv_script_parser.py +49 -0
  140. flyte/_version.py +21 -0
  141. flyte/cli/__init__.py +3 -0
  142. flyte/cli/_abort.py +28 -0
  143. flyte/cli/_common.py +337 -0
  144. flyte/cli/_create.py +145 -0
  145. flyte/cli/_delete.py +23 -0
  146. flyte/cli/_deploy.py +152 -0
  147. flyte/cli/_gen.py +163 -0
  148. flyte/cli/_get.py +310 -0
  149. flyte/cli/_params.py +538 -0
  150. flyte/cli/_run.py +231 -0
  151. flyte/cli/main.py +166 -0
  152. flyte/config/__init__.py +3 -0
  153. flyte/config/_config.py +216 -0
  154. flyte/config/_internal.py +64 -0
  155. flyte/config/_reader.py +207 -0
  156. flyte/connectors/__init__.py +0 -0
  157. flyte/errors.py +172 -0
  158. flyte/extras/__init__.py +5 -0
  159. flyte/extras/_container.py +263 -0
  160. flyte/io/__init__.py +27 -0
  161. flyte/io/_dir.py +448 -0
  162. flyte/io/_file.py +467 -0
  163. flyte/io/_structured_dataset/__init__.py +129 -0
  164. flyte/io/_structured_dataset/basic_dfs.py +219 -0
  165. flyte/io/_structured_dataset/structured_dataset.py +1061 -0
  166. flyte/models.py +391 -0
  167. flyte/remote/__init__.py +26 -0
  168. flyte/remote/_client/__init__.py +0 -0
  169. flyte/remote/_client/_protocols.py +133 -0
  170. flyte/remote/_client/auth/__init__.py +12 -0
  171. flyte/remote/_client/auth/_auth_utils.py +14 -0
  172. flyte/remote/_client/auth/_authenticators/__init__.py +0 -0
  173. flyte/remote/_client/auth/_authenticators/base.py +397 -0
  174. flyte/remote/_client/auth/_authenticators/client_credentials.py +73 -0
  175. flyte/remote/_client/auth/_authenticators/device_code.py +118 -0
  176. flyte/remote/_client/auth/_authenticators/external_command.py +79 -0
  177. flyte/remote/_client/auth/_authenticators/factory.py +200 -0
  178. flyte/remote/_client/auth/_authenticators/pkce.py +516 -0
  179. flyte/remote/_client/auth/_channel.py +215 -0
  180. flyte/remote/_client/auth/_client_config.py +83 -0
  181. flyte/remote/_client/auth/_default_html.py +32 -0
  182. flyte/remote/_client/auth/_grpc_utils/__init__.py +0 -0
  183. flyte/remote/_client/auth/_grpc_utils/auth_interceptor.py +288 -0
  184. flyte/remote/_client/auth/_grpc_utils/default_metadata_interceptor.py +151 -0
  185. flyte/remote/_client/auth/_keyring.py +143 -0
  186. flyte/remote/_client/auth/_token_client.py +260 -0
  187. flyte/remote/_client/auth/errors.py +16 -0
  188. flyte/remote/_client/controlplane.py +95 -0
  189. flyte/remote/_console.py +18 -0
  190. flyte/remote/_data.py +159 -0
  191. flyte/remote/_logs.py +176 -0
  192. flyte/remote/_project.py +85 -0
  193. flyte/remote/_run.py +970 -0
  194. flyte/remote/_secret.py +132 -0
  195. flyte/remote/_task.py +391 -0
  196. flyte/report/__init__.py +3 -0
  197. flyte/report/_report.py +178 -0
  198. flyte/report/_template.html +124 -0
  199. flyte/storage/__init__.py +29 -0
  200. flyte/storage/_config.py +233 -0
  201. flyte/storage/_remote_fs.py +34 -0
  202. flyte/storage/_storage.py +271 -0
  203. flyte/storage/_utils.py +5 -0
  204. flyte/syncify/__init__.py +56 -0
  205. flyte/syncify/_api.py +371 -0
  206. flyte/types/__init__.py +36 -0
  207. flyte/types/_interface.py +40 -0
  208. flyte/types/_pickle.py +118 -0
  209. flyte/types/_renderer.py +162 -0
  210. flyte/types/_string_literals.py +120 -0
  211. flyte/types/_type_engine.py +2287 -0
  212. flyte/types/_utils.py +80 -0
  213. flyte-0.2.0a0.dist-info/METADATA +249 -0
  214. flyte-0.2.0a0.dist-info/RECORD +218 -0
  215. {flyte-0.1.0.dist-info → flyte-0.2.0a0.dist-info}/WHEEL +2 -1
  216. flyte-0.2.0a0.dist-info/entry_points.txt +3 -0
  217. flyte-0.2.0a0.dist-info/top_level.txt +1 -0
  218. flyte-0.1.0.dist-info/METADATA +0 -6
  219. flyte-0.1.0.dist-info/RECORD +0 -5
@@ -0,0 +1,54 @@
1
+ import importlib.util
2
+ import sys
3
+ import types
4
+
5
+
6
+ class _LazyModule(types.ModuleType):
7
+ """
8
+ `lazy_module` returns an instance of this class if the module is not found in the python environment.
9
+ """
10
+
11
+ def __init__(self, module_name: str):
12
+ super().__init__(module_name)
13
+ self._module_name = module_name
14
+
15
+ def __getattribute__(self, attr):
16
+ raise ImportError(f"Module {object.__getattribute__(self, '_module_name')} is not yet installed.")
17
+
18
+
19
+ def is_imported(module_name):
20
+ """
21
+ This function is used to check if a module has been imported by the regular import.
22
+ Return false if module is lazy imported and not used yet.
23
+ """
24
+ return (
25
+ module_name in sys.modules
26
+ and object.__getattribute__(lazy_module(module_name), "__class__").__name__ != "_LazyModule"
27
+ )
28
+
29
+
30
+ def lazy_module(fullname):
31
+ """
32
+ This function is used to lazily import modules. It is used in the following way:
33
+ .. code-block:: python
34
+ from flytekit.lazy_import import lazy_module
35
+ sklearn = lazy_module("sklearn")
36
+ sklearn.svm.SVC()
37
+ :param Text fullname: The full name of the module to import
38
+ """
39
+ if fullname in sys.modules:
40
+ return sys.modules[fullname]
41
+ # https://docs.python.org/3/library/importlib.html#implementing-lazy-imports
42
+ spec = importlib.util.find_spec(fullname)
43
+ if spec is None or spec.loader is None:
44
+ # Return a lazy module if the module is not found in the python environment,
45
+ # so that we can raise a proper error when the user tries to access an attribute in the module.
46
+ # The reason to do this is because importlib.util.LazyLoader still requires
47
+ # the module to be installed even if you don't use it.
48
+ return _LazyModule(fullname)
49
+ loader = importlib.util.LazyLoader(spec.loader)
50
+ spec.loader = loader
51
+ module = importlib.util.module_from_spec(spec)
52
+ sys.modules[fullname] = module
53
+ loader.exec_module(module)
54
+ return module
@@ -0,0 +1,57 @@
1
+ def hostname_from_url(url: str) -> str:
2
+ """Parse a URL and return the hostname part."""
3
+
4
+ # Handle dns:/// format specifically (gRPC convention)
5
+ if url.startswith("dns:///"):
6
+ return url[7:] # Skip the "dns:///" prefix
7
+
8
+ # Handle standard URL formats
9
+ import urllib.parse
10
+
11
+ parsed = urllib.parse.urlparse(url)
12
+ return parsed.netloc or parsed.path.lstrip("/").rsplit("/")[0]
13
+
14
+
15
+ def org_from_endpoint(endpoint: str | None) -> str | None:
16
+ """
17
+ Extracts the organization from the endpoint URL. The organization is assumed to be the first part of the domain.
18
+ This is temporary until we have a proper organization discovery mechanism through APIs.
19
+
20
+ :param endpoint: The endpoint URL
21
+ :return: The organization name or None if not found
22
+ """
23
+ if not endpoint:
24
+ return None
25
+
26
+ hostname = hostname_from_url(endpoint)
27
+ domain_parts = hostname.split(".")
28
+ if len(domain_parts) > 2:
29
+ # Assuming the organization is the first part of the domain
30
+ return domain_parts[0]
31
+ return None
32
+
33
+
34
+ def sanitize_endpoint(endpoint: str | None) -> str | None:
35
+ """
36
+ Sanitize the endpoint URL by ensuring it has a valid scheme.
37
+ :param endpoint: The endpoint URL to sanitize
38
+ :return: Sanitized endpoint URL or None if the input was None
39
+ """
40
+ if not endpoint:
41
+ return None
42
+ if "://" not in endpoint:
43
+ endpoint = f"dns:///{endpoint}"
44
+ else:
45
+ if endpoint.startswith("https://"):
46
+ # If the endpoint starts with dns:///, we assume it's a gRPC endpoint
47
+ endpoint = f"dns:///{endpoint[8:]}"
48
+ elif endpoint.startswith("http://"):
49
+ # If the endpoint starts with http://, we assume it's a REST endpoint
50
+ endpoint = f"dns:///{endpoint[7:]}"
51
+ elif not endpoint.startswith("dns:///"):
52
+ raise RuntimeError(
53
+ f"Invalid endpoint {endpoint}, expected format is "
54
+ f"dns:///<hostname> or https://<hostname> or http://<hostname>"
55
+ )
56
+ endpoint = endpoint.removesuffix("/")
57
+ return endpoint
@@ -0,0 +1,49 @@
1
+ import pathlib
2
+ import re
3
+ from dataclasses import dataclass, field
4
+ from typing import Dict, List, Optional
5
+
6
+ import toml
7
+
8
+
9
+ @dataclass
10
+ class ToolUVConfig:
11
+ exclude_newer: Optional[str] = None
12
+
13
+
14
+ @dataclass
15
+ class UVScriptMetadata:
16
+ requires_python: Optional[str] = None
17
+ dependencies: List[str] = field(default_factory=list)
18
+ tool: Optional[Dict[str, ToolUVConfig]] = None
19
+
20
+
21
+ def _extract_uv_metadata_block(text: str) -> str | None:
22
+ pattern = re.compile(r"# /// script\s*(.*?)# ///", re.DOTALL)
23
+ match = pattern.search(text)
24
+ if not match:
25
+ return None
26
+ lines = [line.lstrip("# ").rstrip() for line in match.group(1).splitlines()]
27
+ return "\n".join(lines)
28
+
29
+
30
+ def parse_uv_script_file(path: pathlib.Path) -> UVScriptMetadata:
31
+ if not path.exists() or not path.is_file():
32
+ raise FileNotFoundError(f"File not found: {path}")
33
+
34
+ text = path.read_text(encoding="utf-8")
35
+ raw_header = _extract_uv_metadata_block(text)
36
+ if raw_header is None:
37
+ raise ValueError("No uv metadata block found")
38
+
39
+ try:
40
+ data = toml.loads(raw_header)
41
+ except toml.TomlDecodeError as e:
42
+ raise ValueError(f"Invalid TOML in metadata block: {e}")
43
+
44
+ tool_data = data.get("tool", {}).get("uv", {})
45
+ return UVScriptMetadata(
46
+ requires_python=data.get("requires-python"),
47
+ dependencies=data.get("dependencies", []),
48
+ tool={"uv": ToolUVConfig(exclude_newer=tool_data.get("exclude-newer"))} if tool_data else None,
49
+ )
flyte/_version.py ADDED
@@ -0,0 +1,21 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '0.2.0a0'
21
+ __version_tuple__ = version_tuple = (0, 2, 0, 'a0')
flyte/cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from flyte.cli.main import main
2
+
3
+ __all__ = ["main"]
flyte/cli/_abort.py ADDED
@@ -0,0 +1,28 @@
1
+ import rich_click as click
2
+ from rich.console import Console
3
+
4
+ from flyte.cli import _common as common
5
+
6
+
7
+ @click.group(name="abort")
8
+ def abort():
9
+ """
10
+ Abort an ongoing process.
11
+ """
12
+
13
+
14
+ @abort.command(cls=common.CommandBase)
15
+ @click.argument("run-name", type=str, required=True)
16
+ @click.pass_obj
17
+ def run(cfg: common.CLIConfig, run_name: str, project: str | None = None, domain: str | None = None):
18
+ """
19
+ Abort a run.
20
+ """
21
+ from flyte.remote import Run
22
+
23
+ cfg.init(project=project, domain=domain)
24
+ r = Run.get(name=run_name)
25
+ if r:
26
+ console = Console()
27
+ r.abort()
28
+ console.print(f"Run '{run_name}' has been aborted.")
flyte/cli/_common.py ADDED
@@ -0,0 +1,337 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import logging
5
+ import os
6
+ import sys
7
+ from abc import abstractmethod
8
+ from dataclasses import dataclass, replace
9
+ from pathlib import Path
10
+ from types import MappingProxyType, ModuleType
11
+ from typing import Any, Dict, Iterable, List, Optional
12
+
13
+ import rich.box
14
+ import rich.repr
15
+ import rich_click as click
16
+ from rich.console import Console
17
+ from rich.panel import Panel
18
+ from rich.table import Table
19
+ from rich.traceback import Traceback
20
+
21
+ import flyte.errors
22
+ from flyte._logging import logger
23
+ from flyte.config import Config
24
+
25
+ PREFERRED_BORDER_COLOR = "dim cyan"
26
+ PREFERRED_ACCENT_COLOR = "bold #FFD700"
27
+ HEADER_STYLE = f"{PREFERRED_ACCENT_COLOR} on black"
28
+ PANELS = False
29
+
30
+ PROJECT_OPTION = click.Option(
31
+ param_decls=["-p", "--project"],
32
+ required=False,
33
+ type=str,
34
+ default=None,
35
+ help="Project to which this command applies.",
36
+ show_default=True,
37
+ )
38
+
39
+ DOMAIN_OPTION = click.Option(
40
+ param_decls=["-d", "--domain"],
41
+ required=False,
42
+ type=str,
43
+ default=None,
44
+ help="Domain to which this command applies.",
45
+ show_default=True,
46
+ )
47
+
48
+ DRY_RUN_OPTION = click.Option(
49
+ param_decls=["--dry-run", "--dryrun"],
50
+ required=False,
51
+ type=bool,
52
+ is_flag=True,
53
+ default=False,
54
+ help="Dry run. Do not actually call the backend service.",
55
+ show_default=True,
56
+ )
57
+
58
+
59
+ def _common_options() -> List[click.Option]:
60
+ """
61
+ Common options that will be added to all commands and groups that inherit from CommandBase or GroupBase.
62
+ """
63
+ return [PROJECT_OPTION, DOMAIN_OPTION]
64
+
65
+
66
+ # This is global state for the CLI, it is manipulated by the main command
67
+
68
+
69
+ @rich.repr.auto
70
+ @dataclass(frozen=True)
71
+ class CLIConfig:
72
+ """
73
+ This is the global state for the CLI. It is manipulated by the main command.
74
+ """
75
+
76
+ config: Config
77
+ ctx: click.Context
78
+ log_level: int | None = logging.ERROR
79
+ endpoint: str | None = None
80
+ insecure: bool = False
81
+ org: str | None = None
82
+
83
+ def replace(self, **kwargs) -> CLIConfig:
84
+ """
85
+ Replace the global state with a new one.
86
+ """
87
+ return replace(self, **kwargs)
88
+
89
+ def init(self, project: str | None = None, domain: str | None = None):
90
+ from flyte.config._config import TaskConfig
91
+
92
+ task_cfg = TaskConfig(
93
+ org=self.org or self.config.task.org,
94
+ project=project or self.config.task.project,
95
+ domain=domain or self.config.task.domain,
96
+ )
97
+
98
+ kwargs: Dict[str, Any] = {}
99
+ if self.endpoint:
100
+ kwargs["endpoint"] = self.endpoint
101
+ if self.insecure is not None:
102
+ kwargs["insecure"] = self.insecure
103
+ platform_cfg = self.config.platform.replace(**kwargs)
104
+
105
+ updated_config = self.config.with_params(platform_cfg, task_cfg)
106
+
107
+ flyte.init_from_config(updated_config, log_level=self.log_level)
108
+
109
+
110
+ class InvokeBaseMixin:
111
+ """
112
+ Mixin to catch grpc.RpcError, flyte.RpcError, other errors and other exceptions
113
+ and raise them as gclick.ClickException.
114
+ """
115
+
116
+ def invoke(self, ctx):
117
+ import grpc
118
+
119
+ try:
120
+ return super().invoke(ctx) # type: ignore
121
+ except grpc.aio.AioRpcError as e:
122
+ if e.code() == grpc.StatusCode.UNAUTHENTICATED:
123
+ raise click.ClickException(f"Authentication failed. Please check your credentials. {e.details()}")
124
+ if e.code() == grpc.StatusCode.NOT_FOUND:
125
+ raise click.ClickException(f"Requested object NOT FOUND. Please check your input. Error: {e.details()}")
126
+ if e.code() == grpc.StatusCode.ALREADY_EXISTS:
127
+ raise click.ClickException("Resource already exists.")
128
+ if e.code() == grpc.StatusCode.INTERNAL:
129
+ raise click.ClickException(f"Internal server error: {e.details()}")
130
+ if e.code() == grpc.StatusCode.UNAVAILABLE:
131
+ raise click.ClickException(
132
+ f"Service is currently unavailable. Please try again later. Error: {e.details()}"
133
+ )
134
+ if e.code() == grpc.StatusCode.PERMISSION_DENIED:
135
+ raise click.ClickException(f"Permission denied. Please check your access rights. Error: {e.details()}")
136
+ if e.code() == grpc.StatusCode.INVALID_ARGUMENT:
137
+ raise click.ClickException(f"Invalid argument provided. Please check your input. Error: {e.details()}")
138
+ raise click.ClickException(f"RPC error invoking command: {e!s}") from e
139
+ except flyte.errors.InitializationError as e:
140
+ raise click.ClickException(f"Initialization failed. Pass remote config for CLI. (Reason: {e})")
141
+ except flyte.errors.BaseRuntimeError as e:
142
+ raise click.ClickException(f"{e.kind} failure, {e.code}. {e}") from e
143
+ except click.exceptions.Exit as e:
144
+ # This is a normal exit, do nothing
145
+ raise e
146
+ except click.exceptions.NoArgsIsHelpError:
147
+ # Do not raise an error if no arguments are passed, just show the help message.
148
+ # https://github.com/pallets/click/pull/1489
149
+ return None
150
+ except Exception as e:
151
+ if ctx.obj and ctx.obj.log_level and ctx.obj.log_level <= logging.DEBUG:
152
+ # If the user has requested verbose output, print the full traceback
153
+ console = Console()
154
+ console.print(Traceback.from_exception(type(e), e, e.__traceback__))
155
+ raise click.ClickException(f"Error invoking command: {e}") from e
156
+
157
+
158
+ class CommandBase(InvokeBaseMixin, click.RichCommand):
159
+ """
160
+ Base class for all commands, that adds common options to all commands if enabled.
161
+ """
162
+
163
+ common_options_enabled = True
164
+
165
+ def __init__(self, *args, **kwargs):
166
+ if "params" not in kwargs:
167
+ kwargs["params"] = []
168
+ if self.common_options_enabled:
169
+ kwargs["params"].extend(_common_options())
170
+ super().__init__(*args, **kwargs)
171
+
172
+
173
+ class GroupBase(InvokeBaseMixin, click.RichGroup):
174
+ """
175
+ Base class for all commands, that adds common options to all commands if enabled.
176
+ """
177
+
178
+ common_options_enabled = True
179
+
180
+ def __init__(self, *args, **kwargs):
181
+ if "params" not in kwargs:
182
+ kwargs["params"] = []
183
+ if self.common_options_enabled:
184
+ kwargs["params"].extend(_common_options())
185
+ super().__init__(*args, **kwargs)
186
+
187
+
188
+ class GroupBaseNoOptions(GroupBase):
189
+ common_options_enabled = False
190
+
191
+
192
+ def get_option_from_metadata(metadata: MappingProxyType) -> click.Option:
193
+ return metadata["click.option"]
194
+
195
+
196
+ def key_value_callback(_: Any, param: str, values: List[str]) -> Optional[Dict[str, str]]:
197
+ """
198
+ Callback for click to parse key-value pairs.
199
+ """
200
+ if not values:
201
+ return None
202
+ result = {}
203
+ for v in values:
204
+ if "=" not in v:
205
+ raise click.BadParameter(f"Expected key-value pair of the form key=value, got {v}")
206
+ k, v_ = v.split("=", 1)
207
+ result[k.strip()] = v_.strip()
208
+ return result
209
+
210
+
211
+ class ObjectsPerFileGroup(GroupBase):
212
+ """
213
+ Group that creates a command for each object in a python file.
214
+ """
215
+
216
+ def __init__(self, filename: Path | None = None, *args, **kwargs):
217
+ super().__init__(*args, **kwargs)
218
+ if filename is None:
219
+ raise ValueError("filename must be provided")
220
+ if not filename.exists():
221
+ raise click.ClickException(f"{filename} does not exists")
222
+ self.filename = filename
223
+ self._objs: Dict[str, Any] | None = None
224
+
225
+ @abstractmethod
226
+ def _filter_objects(self, module: ModuleType) -> Dict[str, Any]:
227
+ """
228
+ Filter the objects in the module to only include the ones we want to expose.
229
+ """
230
+ raise NotImplementedError
231
+
232
+ @property
233
+ def objs(self) -> Dict[str, Any]:
234
+ if self._objs is not None:
235
+ return self._objs
236
+
237
+ module_name = os.path.splitext(os.path.basename(self.filename))[0]
238
+ module_path = os.path.dirname(os.path.abspath(self.filename))
239
+
240
+ spec = importlib.util.spec_from_file_location(module_name, self.filename)
241
+ if spec is None or spec.loader is None:
242
+ raise click.ClickException(f"Could not load module {module_name} from {self.filename}")
243
+
244
+ module = importlib.util.module_from_spec(spec)
245
+ sys.modules[module_name] = module
246
+
247
+ sys.path.append(module_path)
248
+ spec.loader.exec_module(module)
249
+
250
+ self._objs = self._filter_objects(module)
251
+ if not self._objs:
252
+ raise click.ClickException(f"No objects found in {self.filename}")
253
+ return self._objs
254
+
255
+ def list_commands(self, ctx):
256
+ m = list(self.objs.keys())
257
+ return sorted(m)
258
+
259
+ @abstractmethod
260
+ def _get_command_for_obj(self, ctx: click.Context, obj_name: str, obj: Any) -> click.Command: ...
261
+
262
+ def get_command(self, ctx, obj_name):
263
+ obj = self.objs[obj_name]
264
+ return self._get_command_for_obj(ctx, obj_name, obj)
265
+
266
+
267
+ class FileGroup(GroupBase):
268
+ """
269
+ Group that creates a command for each file in the current directory that is not __init__.py.
270
+ """
271
+
272
+ common_options_enabled = False
273
+
274
+ def __init__(
275
+ self,
276
+ *args,
277
+ directory: Path | None = None,
278
+ **kwargs,
279
+ ):
280
+ if "params" not in kwargs:
281
+ kwargs["params"] = []
282
+ super().__init__(*args, **kwargs)
283
+ self._files = None
284
+ self._dir = directory
285
+
286
+ @property
287
+ def files(self):
288
+ if self._files is None:
289
+ directory = self._dir or Path(".").absolute()
290
+ self._files = [os.fspath(p) for p in directory.glob("*.py") if p.name != "__init__.py"]
291
+ return self._files
292
+
293
+ def list_commands(self, ctx):
294
+ return self.files
295
+
296
+ def get_command(self, ctx, filename):
297
+ raise NotImplementedError
298
+
299
+
300
+ def get_table(title: str, vals: Iterable[Any]) -> Table:
301
+ """
302
+ Get a table from a list of values.
303
+ """
304
+ table = Table(
305
+ title=title,
306
+ box=rich.box.SQUARE_DOUBLE_HEAD,
307
+ header_style=HEADER_STYLE,
308
+ show_header=True,
309
+ border_style=PREFERRED_BORDER_COLOR,
310
+ )
311
+ headers = None
312
+ has_rich_repr = False
313
+ for p in vals:
314
+ if hasattr(p, "__rich_repr__"):
315
+ has_rich_repr = True
316
+ elif not isinstance(p, (list, tuple)):
317
+ raise ValueError("Expected a list or tuple of values, or an object with __rich_repr__ method.")
318
+ o = list(p.__rich_repr__()) if has_rich_repr else p
319
+ if headers is None:
320
+ headers = [k for k, _ in o]
321
+ for h in headers:
322
+ table.add_column(h.capitalize())
323
+ table.add_row(*[str(v) for _, v in o])
324
+ return table
325
+
326
+
327
+ def get_panel(title: str, renderable: Any) -> Panel:
328
+ """
329
+ Get a panel from a list of values.
330
+ """
331
+ if not PANELS:
332
+ return renderable
333
+ return Panel.fit(
334
+ renderable,
335
+ title=f"[{PREFERRED_ACCENT_COLOR}]{title}[/{PREFERRED_ACCENT_COLOR}]",
336
+ border_style=PREFERRED_BORDER_COLOR,
337
+ )
flyte/cli/_create.py ADDED
@@ -0,0 +1,145 @@
1
+ from pathlib import Path
2
+ from typing import Any, Dict, get_args
3
+
4
+ import rich_click as click
5
+
6
+ import flyte.cli._common as common
7
+ from flyte.remote import SecretTypes
8
+
9
+
10
+ @click.group(name="create")
11
+ def create():
12
+ """
13
+ Create resources in a Flyte deployment.
14
+ """
15
+
16
+
17
+ @create.command(cls=common.CommandBase)
18
+ @click.argument("name", type=str, required=True)
19
+ @click.argument("value", type=str, required=False)
20
+ @click.option("--from-file", type=click.Path(exists=True), help="Path to the file with the binary secret.")
21
+ @click.option(
22
+ "--type", type=click.Choice(get_args(SecretTypes)), default="regular", help="Type of the secret.", show_default=True
23
+ )
24
+ @click.pass_obj
25
+ def secret(
26
+ cfg: common.CLIConfig,
27
+ name: str,
28
+ value: str | bytes | None = None,
29
+ from_file: str | None = None,
30
+ type: SecretTypes = "regular",
31
+ project: str | None = None,
32
+ domain: str | None = None,
33
+ ):
34
+ """
35
+ Create a new secret. The name of the secret is required. For example:
36
+
37
+ ```bash
38
+ $ flyte create secret my_secret --value my_value
39
+ ```
40
+
41
+ If `--from-file` is specified, the value will be read from the file instead of being provided directly:
42
+
43
+ ```bash
44
+ $ flyte create secret my_secret --from-file /path/to/secret_file
45
+ ```
46
+
47
+ The `--type` option can be used to create specific types of secrets.
48
+ Either `regular` or `image_pull` can be specified.
49
+ Secrets intended to access container images should be specified as `image_pull`.
50
+ Other secrets should be specified as `regular`.
51
+ If no type is specified, `regular` is assumed.
52
+
53
+ ```bash
54
+ $ flyte create secret my_secret --type image_pull
55
+ ```
56
+ """
57
+ from flyte.remote import Secret
58
+
59
+ cfg.init(project, domain)
60
+ if from_file:
61
+ with open(from_file, "rb") as f:
62
+ value = f.read()
63
+ Secret.create(name=name, value=value, type=type)
64
+
65
+
66
+ @create.command(cls=common.CommandBase)
67
+ @click.option("--endpoint", type=str, help="Endpoint of the Flyte backend.")
68
+ @click.option("--insecure", is_flag=True, help="Use an insecure connection to the Flyte backend.")
69
+ @click.option(
70
+ "--org",
71
+ type=str,
72
+ required=False,
73
+ help="Organization to use. This will override the organization in the configuration file.",
74
+ )
75
+ @click.option(
76
+ "-o",
77
+ "--output",
78
+ type=click.Path(exists=False, writable=True),
79
+ default=Path.cwd() / "config.yaml",
80
+ help="Path to the output directory where the configuration will be saved. Defaults to current directory.",
81
+ show_default=True,
82
+ )
83
+ @click.option(
84
+ "--force",
85
+ is_flag=True,
86
+ default=False,
87
+ help="Force overwrite of the configuration file if it already exists.",
88
+ show_default=True,
89
+ )
90
+ def config(
91
+ output: str,
92
+ endpoint: str | None = None,
93
+ insecure: bool = False,
94
+ org: str | None = None,
95
+ project: str | None = None,
96
+ domain: str | None = None,
97
+ force: bool = False,
98
+ ):
99
+ """
100
+ Creates a configuration file for Flyte CLI.
101
+ If the `--output` option is not specified, it will create a file named `config.yaml` in the current directory.
102
+ If the file already exists, it will raise an error unless the `--force` option is used.
103
+ """
104
+ import yaml
105
+
106
+ from flyte._utils import org_from_endpoint, sanitize_endpoint
107
+
108
+ output_path = Path(output)
109
+
110
+ if output_path.exists() and not force:
111
+ force = click.confirm(f"Overwrite [{output_path}]?", default=False)
112
+ if not force:
113
+ click.echo(f"Will not overwrite the existing config file at {output_path}")
114
+ return
115
+
116
+ admin: Dict[str, Any] = {}
117
+ if endpoint:
118
+ endpoint = sanitize_endpoint(endpoint)
119
+ admin["endpoint"] = endpoint
120
+ if insecure:
121
+ admin["insecure"] = insecure
122
+
123
+ if not org and endpoint:
124
+ org = org_from_endpoint(endpoint)
125
+
126
+ task: Dict[str, str] = {}
127
+ if org:
128
+ task["org"] = org
129
+ if project:
130
+ task["project"] = project
131
+ if domain:
132
+ task["domain"] = domain
133
+
134
+ if not admin and not task:
135
+ raise click.BadParameter("At least one of --endpoint or --org must be provided.")
136
+
137
+ with open(output_path, "w") as f:
138
+ d: Dict[str, Any] = {}
139
+ if admin:
140
+ d["admin"] = admin
141
+ if task:
142
+ d["task"] = task
143
+ yaml.dump(d, f)
144
+
145
+ click.echo(f"Config file written to {output_path}")