flyte 2.0.0b32__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (204) hide show
  1. flyte/__init__.py +108 -0
  2. flyte/_bin/__init__.py +0 -0
  3. flyte/_bin/debug.py +38 -0
  4. flyte/_bin/runtime.py +195 -0
  5. flyte/_bin/serve.py +178 -0
  6. flyte/_build.py +26 -0
  7. flyte/_cache/__init__.py +12 -0
  8. flyte/_cache/cache.py +147 -0
  9. flyte/_cache/defaults.py +9 -0
  10. flyte/_cache/local_cache.py +216 -0
  11. flyte/_cache/policy_function_body.py +42 -0
  12. flyte/_code_bundle/__init__.py +8 -0
  13. flyte/_code_bundle/_ignore.py +121 -0
  14. flyte/_code_bundle/_packaging.py +218 -0
  15. flyte/_code_bundle/_utils.py +347 -0
  16. flyte/_code_bundle/bundle.py +266 -0
  17. flyte/_constants.py +1 -0
  18. flyte/_context.py +155 -0
  19. flyte/_custom_context.py +73 -0
  20. flyte/_debug/__init__.py +0 -0
  21. flyte/_debug/constants.py +38 -0
  22. flyte/_debug/utils.py +17 -0
  23. flyte/_debug/vscode.py +307 -0
  24. flyte/_deploy.py +408 -0
  25. flyte/_deployer.py +109 -0
  26. flyte/_doc.py +29 -0
  27. flyte/_docstring.py +32 -0
  28. flyte/_environment.py +122 -0
  29. flyte/_excepthook.py +37 -0
  30. flyte/_group.py +32 -0
  31. flyte/_hash.py +8 -0
  32. flyte/_image.py +1055 -0
  33. flyte/_initialize.py +628 -0
  34. flyte/_interface.py +119 -0
  35. flyte/_internal/__init__.py +3 -0
  36. flyte/_internal/controllers/__init__.py +129 -0
  37. flyte/_internal/controllers/_local_controller.py +239 -0
  38. flyte/_internal/controllers/_trace.py +48 -0
  39. flyte/_internal/controllers/remote/__init__.py +58 -0
  40. flyte/_internal/controllers/remote/_action.py +211 -0
  41. flyte/_internal/controllers/remote/_client.py +47 -0
  42. flyte/_internal/controllers/remote/_controller.py +583 -0
  43. flyte/_internal/controllers/remote/_core.py +465 -0
  44. flyte/_internal/controllers/remote/_informer.py +381 -0
  45. flyte/_internal/controllers/remote/_service_protocol.py +50 -0
  46. flyte/_internal/imagebuild/__init__.py +3 -0
  47. flyte/_internal/imagebuild/docker_builder.py +706 -0
  48. flyte/_internal/imagebuild/image_builder.py +277 -0
  49. flyte/_internal/imagebuild/remote_builder.py +386 -0
  50. flyte/_internal/imagebuild/utils.py +78 -0
  51. flyte/_internal/resolvers/__init__.py +0 -0
  52. flyte/_internal/resolvers/_task_module.py +21 -0
  53. flyte/_internal/resolvers/common.py +31 -0
  54. flyte/_internal/resolvers/default.py +28 -0
  55. flyte/_internal/runtime/__init__.py +0 -0
  56. flyte/_internal/runtime/convert.py +486 -0
  57. flyte/_internal/runtime/entrypoints.py +204 -0
  58. flyte/_internal/runtime/io.py +188 -0
  59. flyte/_internal/runtime/resources_serde.py +152 -0
  60. flyte/_internal/runtime/reuse.py +125 -0
  61. flyte/_internal/runtime/rusty.py +193 -0
  62. flyte/_internal/runtime/task_serde.py +362 -0
  63. flyte/_internal/runtime/taskrunner.py +209 -0
  64. flyte/_internal/runtime/trigger_serde.py +160 -0
  65. flyte/_internal/runtime/types_serde.py +54 -0
  66. flyte/_keyring/__init__.py +0 -0
  67. flyte/_keyring/file.py +115 -0
  68. flyte/_logging.py +300 -0
  69. flyte/_map.py +312 -0
  70. flyte/_module.py +72 -0
  71. flyte/_pod.py +30 -0
  72. flyte/_resources.py +473 -0
  73. flyte/_retry.py +32 -0
  74. flyte/_reusable_environment.py +102 -0
  75. flyte/_run.py +724 -0
  76. flyte/_secret.py +96 -0
  77. flyte/_task.py +550 -0
  78. flyte/_task_environment.py +316 -0
  79. flyte/_task_plugins.py +47 -0
  80. flyte/_timeout.py +47 -0
  81. flyte/_tools.py +27 -0
  82. flyte/_trace.py +119 -0
  83. flyte/_trigger.py +1000 -0
  84. flyte/_utils/__init__.py +30 -0
  85. flyte/_utils/asyn.py +121 -0
  86. flyte/_utils/async_cache.py +139 -0
  87. flyte/_utils/coro_management.py +27 -0
  88. flyte/_utils/docker_credentials.py +173 -0
  89. flyte/_utils/file_handling.py +72 -0
  90. flyte/_utils/helpers.py +134 -0
  91. flyte/_utils/lazy_module.py +54 -0
  92. flyte/_utils/module_loader.py +104 -0
  93. flyte/_utils/org_discovery.py +57 -0
  94. flyte/_utils/uv_script_parser.py +49 -0
  95. flyte/_version.py +34 -0
  96. flyte/app/__init__.py +22 -0
  97. flyte/app/_app_environment.py +157 -0
  98. flyte/app/_deploy.py +125 -0
  99. flyte/app/_input.py +160 -0
  100. flyte/app/_runtime/__init__.py +3 -0
  101. flyte/app/_runtime/app_serde.py +347 -0
  102. flyte/app/_types.py +101 -0
  103. flyte/app/extras/__init__.py +3 -0
  104. flyte/app/extras/_fastapi.py +151 -0
  105. flyte/cli/__init__.py +12 -0
  106. flyte/cli/_abort.py +28 -0
  107. flyte/cli/_build.py +114 -0
  108. flyte/cli/_common.py +468 -0
  109. flyte/cli/_create.py +371 -0
  110. flyte/cli/_delete.py +45 -0
  111. flyte/cli/_deploy.py +293 -0
  112. flyte/cli/_gen.py +176 -0
  113. flyte/cli/_get.py +370 -0
  114. flyte/cli/_option.py +33 -0
  115. flyte/cli/_params.py +554 -0
  116. flyte/cli/_plugins.py +209 -0
  117. flyte/cli/_run.py +597 -0
  118. flyte/cli/_serve.py +64 -0
  119. flyte/cli/_update.py +37 -0
  120. flyte/cli/_user.py +17 -0
  121. flyte/cli/main.py +221 -0
  122. flyte/config/__init__.py +3 -0
  123. flyte/config/_config.py +248 -0
  124. flyte/config/_internal.py +73 -0
  125. flyte/config/_reader.py +225 -0
  126. flyte/connectors/__init__.py +11 -0
  127. flyte/connectors/_connector.py +270 -0
  128. flyte/connectors/_server.py +197 -0
  129. flyte/connectors/utils.py +135 -0
  130. flyte/errors.py +243 -0
  131. flyte/extend.py +19 -0
  132. flyte/extras/__init__.py +5 -0
  133. flyte/extras/_container.py +286 -0
  134. flyte/git/__init__.py +3 -0
  135. flyte/git/_config.py +21 -0
  136. flyte/io/__init__.py +29 -0
  137. flyte/io/_dataframe/__init__.py +131 -0
  138. flyte/io/_dataframe/basic_dfs.py +223 -0
  139. flyte/io/_dataframe/dataframe.py +1026 -0
  140. flyte/io/_dir.py +910 -0
  141. flyte/io/_file.py +914 -0
  142. flyte/io/_hashing_io.py +342 -0
  143. flyte/models.py +479 -0
  144. flyte/py.typed +0 -0
  145. flyte/remote/__init__.py +35 -0
  146. flyte/remote/_action.py +738 -0
  147. flyte/remote/_app.py +57 -0
  148. flyte/remote/_client/__init__.py +0 -0
  149. flyte/remote/_client/_protocols.py +189 -0
  150. flyte/remote/_client/auth/__init__.py +12 -0
  151. flyte/remote/_client/auth/_auth_utils.py +14 -0
  152. flyte/remote/_client/auth/_authenticators/__init__.py +0 -0
  153. flyte/remote/_client/auth/_authenticators/base.py +403 -0
  154. flyte/remote/_client/auth/_authenticators/client_credentials.py +73 -0
  155. flyte/remote/_client/auth/_authenticators/device_code.py +117 -0
  156. flyte/remote/_client/auth/_authenticators/external_command.py +79 -0
  157. flyte/remote/_client/auth/_authenticators/factory.py +200 -0
  158. flyte/remote/_client/auth/_authenticators/pkce.py +516 -0
  159. flyte/remote/_client/auth/_channel.py +213 -0
  160. flyte/remote/_client/auth/_client_config.py +85 -0
  161. flyte/remote/_client/auth/_default_html.py +32 -0
  162. flyte/remote/_client/auth/_grpc_utils/__init__.py +0 -0
  163. flyte/remote/_client/auth/_grpc_utils/auth_interceptor.py +288 -0
  164. flyte/remote/_client/auth/_grpc_utils/default_metadata_interceptor.py +151 -0
  165. flyte/remote/_client/auth/_keyring.py +152 -0
  166. flyte/remote/_client/auth/_token_client.py +260 -0
  167. flyte/remote/_client/auth/errors.py +16 -0
  168. flyte/remote/_client/controlplane.py +128 -0
  169. flyte/remote/_common.py +30 -0
  170. flyte/remote/_console.py +19 -0
  171. flyte/remote/_data.py +161 -0
  172. flyte/remote/_logs.py +185 -0
  173. flyte/remote/_project.py +88 -0
  174. flyte/remote/_run.py +386 -0
  175. flyte/remote/_secret.py +142 -0
  176. flyte/remote/_task.py +527 -0
  177. flyte/remote/_trigger.py +306 -0
  178. flyte/remote/_user.py +33 -0
  179. flyte/report/__init__.py +3 -0
  180. flyte/report/_report.py +182 -0
  181. flyte/report/_template.html +124 -0
  182. flyte/storage/__init__.py +36 -0
  183. flyte/storage/_config.py +237 -0
  184. flyte/storage/_parallel_reader.py +274 -0
  185. flyte/storage/_remote_fs.py +34 -0
  186. flyte/storage/_storage.py +456 -0
  187. flyte/storage/_utils.py +5 -0
  188. flyte/syncify/__init__.py +56 -0
  189. flyte/syncify/_api.py +375 -0
  190. flyte/types/__init__.py +52 -0
  191. flyte/types/_interface.py +40 -0
  192. flyte/types/_pickle.py +145 -0
  193. flyte/types/_renderer.py +162 -0
  194. flyte/types/_string_literals.py +119 -0
  195. flyte/types/_type_engine.py +2254 -0
  196. flyte/types/_utils.py +80 -0
  197. flyte-2.0.0b32.data/scripts/debug.py +38 -0
  198. flyte-2.0.0b32.data/scripts/runtime.py +195 -0
  199. flyte-2.0.0b32.dist-info/METADATA +351 -0
  200. flyte-2.0.0b32.dist-info/RECORD +204 -0
  201. flyte-2.0.0b32.dist-info/WHEEL +5 -0
  202. flyte-2.0.0b32.dist-info/entry_points.txt +7 -0
  203. flyte-2.0.0b32.dist-info/licenses/LICENSE +201 -0
  204. flyte-2.0.0b32.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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)