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