flyte 2.0.0b32__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flyte might be problematic. Click here for more details.
- flyte/__init__.py +108 -0
- flyte/_bin/__init__.py +0 -0
- flyte/_bin/debug.py +38 -0
- flyte/_bin/runtime.py +195 -0
- flyte/_bin/serve.py +178 -0
- flyte/_build.py +26 -0
- flyte/_cache/__init__.py +12 -0
- flyte/_cache/cache.py +147 -0
- flyte/_cache/defaults.py +9 -0
- flyte/_cache/local_cache.py +216 -0
- flyte/_cache/policy_function_body.py +42 -0
- flyte/_code_bundle/__init__.py +8 -0
- flyte/_code_bundle/_ignore.py +121 -0
- flyte/_code_bundle/_packaging.py +218 -0
- flyte/_code_bundle/_utils.py +347 -0
- flyte/_code_bundle/bundle.py +266 -0
- flyte/_constants.py +1 -0
- flyte/_context.py +155 -0
- flyte/_custom_context.py +73 -0
- flyte/_debug/__init__.py +0 -0
- flyte/_debug/constants.py +38 -0
- flyte/_debug/utils.py +17 -0
- flyte/_debug/vscode.py +307 -0
- flyte/_deploy.py +408 -0
- flyte/_deployer.py +109 -0
- flyte/_doc.py +29 -0
- flyte/_docstring.py +32 -0
- flyte/_environment.py +122 -0
- flyte/_excepthook.py +37 -0
- flyte/_group.py +32 -0
- flyte/_hash.py +8 -0
- flyte/_image.py +1055 -0
- flyte/_initialize.py +628 -0
- flyte/_interface.py +119 -0
- flyte/_internal/__init__.py +3 -0
- flyte/_internal/controllers/__init__.py +129 -0
- flyte/_internal/controllers/_local_controller.py +239 -0
- flyte/_internal/controllers/_trace.py +48 -0
- flyte/_internal/controllers/remote/__init__.py +58 -0
- flyte/_internal/controllers/remote/_action.py +211 -0
- flyte/_internal/controllers/remote/_client.py +47 -0
- flyte/_internal/controllers/remote/_controller.py +583 -0
- flyte/_internal/controllers/remote/_core.py +465 -0
- flyte/_internal/controllers/remote/_informer.py +381 -0
- flyte/_internal/controllers/remote/_service_protocol.py +50 -0
- flyte/_internal/imagebuild/__init__.py +3 -0
- flyte/_internal/imagebuild/docker_builder.py +706 -0
- flyte/_internal/imagebuild/image_builder.py +277 -0
- flyte/_internal/imagebuild/remote_builder.py +386 -0
- flyte/_internal/imagebuild/utils.py +78 -0
- flyte/_internal/resolvers/__init__.py +0 -0
- flyte/_internal/resolvers/_task_module.py +21 -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 +486 -0
- flyte/_internal/runtime/entrypoints.py +204 -0
- flyte/_internal/runtime/io.py +188 -0
- flyte/_internal/runtime/resources_serde.py +152 -0
- flyte/_internal/runtime/reuse.py +125 -0
- flyte/_internal/runtime/rusty.py +193 -0
- flyte/_internal/runtime/task_serde.py +362 -0
- flyte/_internal/runtime/taskrunner.py +209 -0
- flyte/_internal/runtime/trigger_serde.py +160 -0
- flyte/_internal/runtime/types_serde.py +54 -0
- flyte/_keyring/__init__.py +0 -0
- flyte/_keyring/file.py +115 -0
- flyte/_logging.py +300 -0
- flyte/_map.py +312 -0
- flyte/_module.py +72 -0
- flyte/_pod.py +30 -0
- flyte/_resources.py +473 -0
- flyte/_retry.py +32 -0
- flyte/_reusable_environment.py +102 -0
- flyte/_run.py +724 -0
- flyte/_secret.py +96 -0
- flyte/_task.py +550 -0
- flyte/_task_environment.py +316 -0
- flyte/_task_plugins.py +47 -0
- flyte/_timeout.py +47 -0
- flyte/_tools.py +27 -0
- flyte/_trace.py +119 -0
- flyte/_trigger.py +1000 -0
- flyte/_utils/__init__.py +30 -0
- flyte/_utils/asyn.py +121 -0
- flyte/_utils/async_cache.py +139 -0
- flyte/_utils/coro_management.py +27 -0
- flyte/_utils/docker_credentials.py +173 -0
- flyte/_utils/file_handling.py +72 -0
- flyte/_utils/helpers.py +134 -0
- flyte/_utils/lazy_module.py +54 -0
- flyte/_utils/module_loader.py +104 -0
- flyte/_utils/org_discovery.py +57 -0
- flyte/_utils/uv_script_parser.py +49 -0
- flyte/_version.py +34 -0
- flyte/app/__init__.py +22 -0
- flyte/app/_app_environment.py +157 -0
- flyte/app/_deploy.py +125 -0
- flyte/app/_input.py +160 -0
- flyte/app/_runtime/__init__.py +3 -0
- flyte/app/_runtime/app_serde.py +347 -0
- flyte/app/_types.py +101 -0
- flyte/app/extras/__init__.py +3 -0
- flyte/app/extras/_fastapi.py +151 -0
- flyte/cli/__init__.py +12 -0
- flyte/cli/_abort.py +28 -0
- flyte/cli/_build.py +114 -0
- flyte/cli/_common.py +468 -0
- flyte/cli/_create.py +371 -0
- flyte/cli/_delete.py +45 -0
- flyte/cli/_deploy.py +293 -0
- flyte/cli/_gen.py +176 -0
- flyte/cli/_get.py +370 -0
- flyte/cli/_option.py +33 -0
- flyte/cli/_params.py +554 -0
- flyte/cli/_plugins.py +209 -0
- flyte/cli/_run.py +597 -0
- flyte/cli/_serve.py +64 -0
- flyte/cli/_update.py +37 -0
- flyte/cli/_user.py +17 -0
- flyte/cli/main.py +221 -0
- flyte/config/__init__.py +3 -0
- flyte/config/_config.py +248 -0
- flyte/config/_internal.py +73 -0
- flyte/config/_reader.py +225 -0
- flyte/connectors/__init__.py +11 -0
- flyte/connectors/_connector.py +270 -0
- flyte/connectors/_server.py +197 -0
- flyte/connectors/utils.py +135 -0
- flyte/errors.py +243 -0
- flyte/extend.py +19 -0
- flyte/extras/__init__.py +5 -0
- flyte/extras/_container.py +286 -0
- flyte/git/__init__.py +3 -0
- flyte/git/_config.py +21 -0
- flyte/io/__init__.py +29 -0
- flyte/io/_dataframe/__init__.py +131 -0
- flyte/io/_dataframe/basic_dfs.py +223 -0
- flyte/io/_dataframe/dataframe.py +1026 -0
- flyte/io/_dir.py +910 -0
- flyte/io/_file.py +914 -0
- flyte/io/_hashing_io.py +342 -0
- flyte/models.py +479 -0
- flyte/py.typed +0 -0
- flyte/remote/__init__.py +35 -0
- flyte/remote/_action.py +738 -0
- flyte/remote/_app.py +57 -0
- flyte/remote/_client/__init__.py +0 -0
- flyte/remote/_client/_protocols.py +189 -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 +403 -0
- flyte/remote/_client/auth/_authenticators/client_credentials.py +73 -0
- flyte/remote/_client/auth/_authenticators/device_code.py +117 -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 +213 -0
- flyte/remote/_client/auth/_client_config.py +85 -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 +152 -0
- flyte/remote/_client/auth/_token_client.py +260 -0
- flyte/remote/_client/auth/errors.py +16 -0
- flyte/remote/_client/controlplane.py +128 -0
- flyte/remote/_common.py +30 -0
- flyte/remote/_console.py +19 -0
- flyte/remote/_data.py +161 -0
- flyte/remote/_logs.py +185 -0
- flyte/remote/_project.py +88 -0
- flyte/remote/_run.py +386 -0
- flyte/remote/_secret.py +142 -0
- flyte/remote/_task.py +527 -0
- flyte/remote/_trigger.py +306 -0
- flyte/remote/_user.py +33 -0
- flyte/report/__init__.py +3 -0
- flyte/report/_report.py +182 -0
- flyte/report/_template.html +124 -0
- flyte/storage/__init__.py +36 -0
- flyte/storage/_config.py +237 -0
- flyte/storage/_parallel_reader.py +274 -0
- flyte/storage/_remote_fs.py +34 -0
- flyte/storage/_storage.py +456 -0
- flyte/storage/_utils.py +5 -0
- flyte/syncify/__init__.py +56 -0
- flyte/syncify/_api.py +375 -0
- flyte/types/__init__.py +52 -0
- flyte/types/_interface.py +40 -0
- flyte/types/_pickle.py +145 -0
- flyte/types/_renderer.py +162 -0
- flyte/types/_string_literals.py +119 -0
- flyte/types/_type_engine.py +2254 -0
- flyte/types/_utils.py +80 -0
- flyte-2.0.0b32.data/scripts/debug.py +38 -0
- flyte-2.0.0b32.data/scripts/runtime.py +195 -0
- flyte-2.0.0b32.dist-info/METADATA +351 -0
- flyte-2.0.0b32.dist-info/RECORD +204 -0
- flyte-2.0.0b32.dist-info/WHEEL +5 -0
- flyte-2.0.0b32.dist-info/entry_points.txt +7 -0
- flyte-2.0.0b32.dist-info/licenses/LICENSE +201 -0
- flyte-2.0.0b32.dist-info/top_level.txt +1 -0
|
@@ -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,104 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import importlib.util
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Tuple
|
|
7
|
+
|
|
8
|
+
import flyte.errors
|
|
9
|
+
from flyte._constants import FLYTE_SYS_PATH
|
|
10
|
+
from flyte._logging import logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_python_modules(path: Path, recursive: bool = False) -> Tuple[List[str], List[Tuple[Path, str]]]:
|
|
14
|
+
"""
|
|
15
|
+
Load all Python modules from a path and return list of loaded module names.
|
|
16
|
+
|
|
17
|
+
:param path: File or directory path
|
|
18
|
+
:param recursive: If True, load modules recursively from subdirectories
|
|
19
|
+
:return: List of loaded module names, and list of file paths that failed to load
|
|
20
|
+
"""
|
|
21
|
+
from rich.progress import BarColumn, Progress, TextColumn, TimeElapsedColumn, TimeRemainingColumn
|
|
22
|
+
|
|
23
|
+
loaded_modules = []
|
|
24
|
+
failed_paths = []
|
|
25
|
+
|
|
26
|
+
if path.is_file() and path.suffix == ".py":
|
|
27
|
+
# Single file case
|
|
28
|
+
module_name = _load_module_from_file(path)
|
|
29
|
+
if module_name:
|
|
30
|
+
loaded_modules.append(module_name)
|
|
31
|
+
|
|
32
|
+
elif path.is_dir():
|
|
33
|
+
# Directory case
|
|
34
|
+
pattern = "**/*.py" if recursive else "*.py"
|
|
35
|
+
python_files = glob.glob(str(path / pattern), recursive=recursive)
|
|
36
|
+
|
|
37
|
+
with Progress(
|
|
38
|
+
TextColumn("[progress.description]{task.description}"),
|
|
39
|
+
BarColumn(),
|
|
40
|
+
"[progress.percentage]{task.percentage:>3.0f}%",
|
|
41
|
+
TimeElapsedColumn(),
|
|
42
|
+
TimeRemainingColumn(),
|
|
43
|
+
TextColumn("• {task.fields[current_file]}"),
|
|
44
|
+
) as progress:
|
|
45
|
+
task = progress.add_task(f"Loading {len(python_files)} files", total=len(python_files), current_file="")
|
|
46
|
+
for file_path in python_files:
|
|
47
|
+
p = Path(file_path)
|
|
48
|
+
progress.update(task, advance=1, current_file=p.name)
|
|
49
|
+
# Skip __init__.py files
|
|
50
|
+
if p.name == "__init__.py":
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
module_name = _load_module_from_file(p)
|
|
55
|
+
if module_name:
|
|
56
|
+
loaded_modules.append(module_name)
|
|
57
|
+
except flyte.errors.ModuleLoadError as e:
|
|
58
|
+
failed_paths.append((p, str(e)))
|
|
59
|
+
|
|
60
|
+
progress.update(task, advance=1, current_file="[green]Done[/green]")
|
|
61
|
+
|
|
62
|
+
return loaded_modules, failed_paths
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _load_module_from_file(file_path: Path) -> str | None:
|
|
66
|
+
"""
|
|
67
|
+
Load a Python module from a file path.
|
|
68
|
+
|
|
69
|
+
:param file_path: Path to the Python file
|
|
70
|
+
:return: Module name if successfully loaded, None otherwise
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
# Use the file stem as module name
|
|
74
|
+
module_name = file_path.stem
|
|
75
|
+
|
|
76
|
+
# Load the module specification
|
|
77
|
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
|
78
|
+
if spec is None or spec.loader is None:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
# Create and execute the module
|
|
82
|
+
module = importlib.util.module_from_spec(spec)
|
|
83
|
+
sys.modules[module_name] = module
|
|
84
|
+
module_path = os.path.dirname(os.path.abspath(file_path))
|
|
85
|
+
sys.path.append(module_path)
|
|
86
|
+
spec.loader.exec_module(module)
|
|
87
|
+
|
|
88
|
+
return module_name
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
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")
|
|
@@ -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,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '2.0.0b32'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 0, 0, 'b32')
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = 'gc7251f82e'
|
flyte/app/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from flyte.app._app_environment import AppEnvironment
|
|
2
|
+
from flyte.app._input import Input
|
|
3
|
+
from flyte.app._types import Domain, Link, Port, Scaling
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"AppEnvironment",
|
|
7
|
+
"Domain",
|
|
8
|
+
"Input",
|
|
9
|
+
"Link",
|
|
10
|
+
"Port",
|
|
11
|
+
"Scaling",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_app_deployer():
|
|
16
|
+
from flyte import _deployer as deployer
|
|
17
|
+
from flyte.app._deploy import _deploy_app_env
|
|
18
|
+
|
|
19
|
+
deployer.register_deployer(AppEnvironment, _deploy_app_env)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
register_app_deployer()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import shlex
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import List, Optional, Union
|
|
7
|
+
|
|
8
|
+
import rich.repr
|
|
9
|
+
|
|
10
|
+
from flyte import Environment
|
|
11
|
+
from flyte.app._input import Input
|
|
12
|
+
from flyte.app._types import Domain, Link, Port, Scaling
|
|
13
|
+
from flyte.models import SerializationContext
|
|
14
|
+
|
|
15
|
+
APP_NAME_RE = re.compile(r"[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*")
|
|
16
|
+
INVALID_APP_PORTS = [8012, 8022, 8112, 9090, 9091]
|
|
17
|
+
INTERNAL_APP_ENDPOINT_PATTERN_ENV_VAR = "INTERNAL_APP_ENDPOINT_PATTERN"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@rich.repr.auto
|
|
21
|
+
@dataclass(init=True, repr=True)
|
|
22
|
+
class AppEnvironment(Environment):
|
|
23
|
+
"""
|
|
24
|
+
:param name: Name of the environment
|
|
25
|
+
:param image: Docker image to use for the environment. If set to "auto", will use the default image.
|
|
26
|
+
:param resources: Resources to allocate for the environment.
|
|
27
|
+
:param env_vars: Environment variables to set for the environment.
|
|
28
|
+
:param secrets: Secrets to inject into the environment.
|
|
29
|
+
:param depends_on: Environment dependencies to hint, so when you deploy the environment, the dependencies are
|
|
30
|
+
also deployed. This is useful when you have a set of environments that depend on each other.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
type: Optional[str] = None
|
|
34
|
+
port: int | Port = 8080
|
|
35
|
+
args: Optional[Union[List[str], str]] = None
|
|
36
|
+
command: Optional[Union[List[str], str]] = None
|
|
37
|
+
requires_auth: bool = True
|
|
38
|
+
scaling: Scaling = field(default_factory=Scaling)
|
|
39
|
+
domain: Domain | None = field(default_factory=Domain)
|
|
40
|
+
# Integration
|
|
41
|
+
links: List[Link] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
# Code
|
|
44
|
+
include: List[str] = field(default_factory=list)
|
|
45
|
+
inputs: List[Input] = field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
# queue / cluster_pool
|
|
48
|
+
cluster_pool: str = "default"
|
|
49
|
+
|
|
50
|
+
# config: Optional[AppConfigProtocol] = None
|
|
51
|
+
|
|
52
|
+
def _validate_name(self):
|
|
53
|
+
if not APP_NAME_RE.fullmatch(self.name):
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"App name '{self.name}' must consist of lower case alphanumeric characters or '-', "
|
|
56
|
+
"and must start and end with an alphanumeric character."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def __post_init__(self):
|
|
60
|
+
super().__post_init__()
|
|
61
|
+
if self.args is not None and not isinstance(self.args, (list, str)):
|
|
62
|
+
raise TypeError(f"Expected args to be of type List[str] or str, got {type(self.args)}")
|
|
63
|
+
if isinstance(self.port, int):
|
|
64
|
+
self.port = Port(port=self.port) # Name should be blank can be h2c / http1
|
|
65
|
+
if self.port.port in INVALID_APP_PORTS:
|
|
66
|
+
raise ValueError(f"Port {self.port.port} is reserved and cannot be used for AppEnvironment")
|
|
67
|
+
if self.command is not None and not isinstance(self.command, (list, str)):
|
|
68
|
+
raise TypeError(f"Expected command to be of type List[str] or str, got {type(self.command)}")
|
|
69
|
+
if not isinstance(self.scaling, Scaling):
|
|
70
|
+
raise TypeError(f"Expected scaling to be of type Scaling, got {type(self.scaling)}")
|
|
71
|
+
if not isinstance(self.domain, (Domain, type(None))):
|
|
72
|
+
raise TypeError(f"Expected domain to be of type Domain or None, got {type(self.domain)}")
|
|
73
|
+
for link in self.links:
|
|
74
|
+
if not isinstance(link, Link):
|
|
75
|
+
raise TypeError(f"Expected links to be of type List[Link], got {type(link)}")
|
|
76
|
+
|
|
77
|
+
self._validate_name()
|
|
78
|
+
|
|
79
|
+
# get instantiated file to keep track of app root directory
|
|
80
|
+
frame = inspect.currentframe().f_back.f_back # two frames up to get the app filename
|
|
81
|
+
self._app_filename = frame.f_code.co_filename
|
|
82
|
+
|
|
83
|
+
def container_args(self, serialize_context: SerializationContext) -> List[str]:
|
|
84
|
+
if self.args is None:
|
|
85
|
+
return []
|
|
86
|
+
elif isinstance(self.args, str):
|
|
87
|
+
return shlex.split(self.args)
|
|
88
|
+
else:
|
|
89
|
+
# args is a list
|
|
90
|
+
return self.args
|
|
91
|
+
|
|
92
|
+
def _serialize_inputs(self) -> str:
|
|
93
|
+
if not self.inputs:
|
|
94
|
+
return ""
|
|
95
|
+
from ._input import SerializableInputCollection
|
|
96
|
+
|
|
97
|
+
serialized_inputs = SerializableInputCollection.from_inputs(self.inputs)
|
|
98
|
+
return serialized_inputs.to_transport
|
|
99
|
+
|
|
100
|
+
def container_cmd(self, serialize_context: SerializationContext) -> List[str]:
|
|
101
|
+
if self.command is None:
|
|
102
|
+
# Default command
|
|
103
|
+
version = serialize_context.version
|
|
104
|
+
if version is None and serialize_context.code_bundle is not None:
|
|
105
|
+
version = serialize_context.code_bundle.computed_version
|
|
106
|
+
|
|
107
|
+
cmd: list[str] = [
|
|
108
|
+
"fserve",
|
|
109
|
+
"--version",
|
|
110
|
+
version or "",
|
|
111
|
+
"--project",
|
|
112
|
+
serialize_context.project or "",
|
|
113
|
+
"--domain",
|
|
114
|
+
serialize_context.domain or "",
|
|
115
|
+
"--org",
|
|
116
|
+
serialize_context.org or "",
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
if serialize_context.image_cache and serialize_context.image_cache.serialized_form:
|
|
120
|
+
cmd = [*cmd, "--image-cache", serialize_context.image_cache.serialized_form]
|
|
121
|
+
else:
|
|
122
|
+
if serialize_context.image_cache:
|
|
123
|
+
cmd = [*cmd, "--image-cache", serialize_context.image_cache.to_transport]
|
|
124
|
+
|
|
125
|
+
if serialize_context.code_bundle:
|
|
126
|
+
if serialize_context.code_bundle.tgz:
|
|
127
|
+
cmd = [*cmd, *["--tgz", f"{serialize_context.code_bundle.tgz}"]]
|
|
128
|
+
elif serialize_context.code_bundle.pkl:
|
|
129
|
+
cmd = [*cmd, *["--pkl", f"{serialize_context.code_bundle.pkl}"]]
|
|
130
|
+
cmd = [*cmd, *["--dest", f"{serialize_context.code_bundle.destination or '.'}"]]
|
|
131
|
+
|
|
132
|
+
if self.inputs:
|
|
133
|
+
cmd.append("--inputs")
|
|
134
|
+
cmd.append(self._serialize_inputs())
|
|
135
|
+
|
|
136
|
+
return [*cmd, "--"]
|
|
137
|
+
elif isinstance(self.command, str):
|
|
138
|
+
return shlex.split(self.command)
|
|
139
|
+
else:
|
|
140
|
+
# command is a list
|
|
141
|
+
return self.command
|
|
142
|
+
|
|
143
|
+
def get_port(self) -> Port:
|
|
144
|
+
if isinstance(self.port, int):
|
|
145
|
+
self.port = Port(port=self.port)
|
|
146
|
+
return self.port
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def endpoint(self) -> str:
|
|
150
|
+
endpoint_pattern = os.getenv(INTERNAL_APP_ENDPOINT_PATTERN_ENV_VAR)
|
|
151
|
+
if endpoint_pattern is not None:
|
|
152
|
+
return endpoint_pattern.replace("{app_fqdn}", self.name)
|
|
153
|
+
|
|
154
|
+
import flyte.remote
|
|
155
|
+
|
|
156
|
+
app = flyte.remote.App.get(name=self.name)
|
|
157
|
+
return app.endpoint
|
flyte/app/_deploy.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import flyte._deployer as deployer
|
|
8
|
+
from flyte import Image
|
|
9
|
+
from flyte._code_bundle.bundle import build_code_bundle_from_relative_paths
|
|
10
|
+
from flyte._initialize import ensure_client, get_client
|
|
11
|
+
from flyte._logging import logger
|
|
12
|
+
from flyte.models import SerializationContext
|
|
13
|
+
|
|
14
|
+
from ._app_environment import AppEnvironment
|
|
15
|
+
|
|
16
|
+
if typing.TYPE_CHECKING:
|
|
17
|
+
from flyteidl2.app import app_definition_pb2
|
|
18
|
+
|
|
19
|
+
from flyte._deployer import DeployedEnvironment
|
|
20
|
+
|
|
21
|
+
FILES_TAR_FILE_NAME = "code_bundle.tgz"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class DeployedAppEnvironment:
|
|
26
|
+
env: AppEnvironment
|
|
27
|
+
deployed_app: app_definition_pb2.App
|
|
28
|
+
|
|
29
|
+
def get_name(self) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Returns the name of the deployed environment.
|
|
32
|
+
"""
|
|
33
|
+
return self.env.name
|
|
34
|
+
|
|
35
|
+
def env_repr(self) -> typing.List[typing.Tuple[str, ...]]:
|
|
36
|
+
return [
|
|
37
|
+
("environment", self.env.name),
|
|
38
|
+
("image", self.env.image.uri if isinstance(self.env.image, Image) else self.env.image or ""),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
def table_repr(self) -> typing.List[typing.List[typing.Tuple[str, ...]]]:
|
|
42
|
+
from flyteidl2.app import app_definition_pb2
|
|
43
|
+
|
|
44
|
+
return [
|
|
45
|
+
[
|
|
46
|
+
("type", "App"),
|
|
47
|
+
("name", self.deployed_app.metadata.id.name),
|
|
48
|
+
("version", self.deployed_app.spec.runtime_metadata.version),
|
|
49
|
+
(
|
|
50
|
+
"state",
|
|
51
|
+
app_definition_pb2.Spec.DesiredState.Name(self.deployed_app.spec.desired_state),
|
|
52
|
+
),
|
|
53
|
+
(
|
|
54
|
+
"public_url",
|
|
55
|
+
self.deployed_app.status.ingress.public_url,
|
|
56
|
+
),
|
|
57
|
+
],
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
def summary_repr(self) -> str:
|
|
61
|
+
return f"Deployed App[{self.deployed_app.metadata.id.name}] in environment {self.env.name}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def _deploy_app(
|
|
65
|
+
app: AppEnvironment, serialization_context: SerializationContext, dryrun: bool = False
|
|
66
|
+
) -> app_definition_pb2.App:
|
|
67
|
+
"""
|
|
68
|
+
Deploy the given app.
|
|
69
|
+
"""
|
|
70
|
+
import grpc.aio
|
|
71
|
+
from flyteidl2.app import app_definition_pb2, app_payload_pb2
|
|
72
|
+
|
|
73
|
+
import flyte.errors
|
|
74
|
+
from flyte.app._runtime import translate_app_env_to_idl
|
|
75
|
+
|
|
76
|
+
if app.include:
|
|
77
|
+
app_file = Path(app._app_filename)
|
|
78
|
+
app_root_dir = app_file.parent
|
|
79
|
+
files = (app_file.name, *app.include)
|
|
80
|
+
code_bundle = await build_code_bundle_from_relative_paths(files, from_dir=app_root_dir)
|
|
81
|
+
serialization_context.code_bundle = code_bundle
|
|
82
|
+
|
|
83
|
+
image_uri = app.image.uri if isinstance(app.image, Image) else app.image
|
|
84
|
+
try:
|
|
85
|
+
app_idl = translate_app_env_to_idl(app, serialization_context)
|
|
86
|
+
if dryrun:
|
|
87
|
+
return app_idl
|
|
88
|
+
ensure_client()
|
|
89
|
+
msg = f"Deploying app {app.name}, with image {image_uri} version {serialization_context.version}"
|
|
90
|
+
if app_idl.spec.HasField("container") and app_idl.spec.container.args:
|
|
91
|
+
msg += f" with args {app_idl.spec.container.args}"
|
|
92
|
+
logger.info(msg)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
await get_client().app_service.Create(app_payload_pb2.CreateRequest(app=app_idl))
|
|
96
|
+
logger.info(f"Deployed app {app.name} with version {app_idl.spec.runtime_metadata.version}")
|
|
97
|
+
except grpc.aio.AioRpcError as e:
|
|
98
|
+
if e.code() == grpc.StatusCode.ALREADY_EXISTS:
|
|
99
|
+
logger.warning(f"App {app.name} with image {image_uri} already exists, updating...")
|
|
100
|
+
resp = await get_client().app_service.Get(app_payload_pb2.GetRequest(app_id=app_idl.metadata.id))
|
|
101
|
+
# Update the revision to match the existing app
|
|
102
|
+
updated_app = app_definition_pb2.App(
|
|
103
|
+
metadata=resp.app.metadata, spec=app_idl.spec, status=resp.app.status
|
|
104
|
+
)
|
|
105
|
+
logger.info(f"Updating app {app.name} to revision {updated_app.metadata.revision}")
|
|
106
|
+
update_resp = await get_client().app_service.Update(app_payload_pb2.UpdateRequest(app=updated_app))
|
|
107
|
+
return update_resp.app
|
|
108
|
+
raise
|
|
109
|
+
|
|
110
|
+
return app_idl
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
logger.error(f"Failed to deploy app {app.name} with image {image_uri}: {exc}")
|
|
113
|
+
raise flyte.errors.DeploymentError(
|
|
114
|
+
f"Failed to deploy app {app.name} with image {image_uri}, Error: {exc!s}"
|
|
115
|
+
) from exc
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def _deploy_app_env(context: deployer.DeploymentContext) -> DeployedEnvironment:
|
|
119
|
+
if not isinstance(context.environment, AppEnvironment):
|
|
120
|
+
raise TypeError(f"Expected AppEnvironment, got {type(context.environment)}")
|
|
121
|
+
|
|
122
|
+
app_env = context.environment
|
|
123
|
+
deployed_app = await _deploy_app(app_env, context.serialization_context, dryrun=context.dryrun)
|
|
124
|
+
|
|
125
|
+
return DeployedAppEnvironment(env=app_env, deployed_app=deployed_app)
|