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
flyte/app/_input.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import typing
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from functools import cache, cached_property
|
|
7
|
+
from typing import List, Literal, Optional
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
if typing.TYPE_CHECKING:
|
|
12
|
+
import flyte.io
|
|
13
|
+
|
|
14
|
+
RUNTIME_INPUTS_FILE = "flyte-inputs.json"
|
|
15
|
+
|
|
16
|
+
InputType = Literal["file", "directory", "string"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Input:
|
|
21
|
+
"""
|
|
22
|
+
Input for application.
|
|
23
|
+
|
|
24
|
+
:param name: Name of input.
|
|
25
|
+
:param value: Value for input.
|
|
26
|
+
:param env_var: Environment name to set the value in the serving environment.
|
|
27
|
+
:param download: When True, the input will be automatically downloaded. This
|
|
28
|
+
only works if the value refers to an item in a object store. i.e. `s3://...`
|
|
29
|
+
:param mount: If `value` is a directory, then the directory will be available
|
|
30
|
+
at `mount`. If `value` is a file, then the file will be downloaded into the
|
|
31
|
+
`mount` directory.
|
|
32
|
+
:param ignore_patterns: If `value` is a directory, then this is a list of glob
|
|
33
|
+
patterns to ignore.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
value: str | flyte.io.File | flyte.io.Dir
|
|
37
|
+
name: Optional[str] = None
|
|
38
|
+
env_var: Optional[str] = None
|
|
39
|
+
download: bool = False
|
|
40
|
+
mount: Optional[str] = None
|
|
41
|
+
ignore_patterns: list[str] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
def __post_init__(self):
|
|
44
|
+
import flyte.io
|
|
45
|
+
|
|
46
|
+
env_name_re = re.compile("^[_a-zA-Z][_a-zA-Z0-9]*$")
|
|
47
|
+
|
|
48
|
+
if self.env_var is not None and env_name_re.match(self.env_var) is None:
|
|
49
|
+
raise ValueError(f"env_var ({self.env_var}) is not a valid environment name for shells")
|
|
50
|
+
|
|
51
|
+
if not isinstance(self.value, (str, flyte.io.File, flyte.io.Dir)):
|
|
52
|
+
raise TypeError(f"Expected value to be of type str, file or dir, got {type(self.value)}")
|
|
53
|
+
|
|
54
|
+
if self.name is None:
|
|
55
|
+
self.name = "i0"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_SerializedInputType = Literal["file", "directory", "string"]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SerializableInput(BaseModel):
|
|
62
|
+
"""
|
|
63
|
+
Serializable version of Input.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
name: str
|
|
67
|
+
value: str
|
|
68
|
+
download: bool
|
|
69
|
+
type: _SerializedInputType = "string"
|
|
70
|
+
env_var: Optional[str] = None
|
|
71
|
+
dest: Optional[str] = None
|
|
72
|
+
ignore_patterns: List[str] = field(default_factory=list)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_input(cls, inp: Input) -> "SerializableInput":
|
|
76
|
+
import flyte.io
|
|
77
|
+
|
|
78
|
+
# inp.name is guaranteed to be set by Input.__post_init__
|
|
79
|
+
assert inp.name is not None, "Input name should be set by __post_init__"
|
|
80
|
+
|
|
81
|
+
tpe: _SerializedInputType = "string"
|
|
82
|
+
if isinstance(inp.value, flyte.io.File):
|
|
83
|
+
value = inp.value.path
|
|
84
|
+
tpe = "file"
|
|
85
|
+
download = True if inp.mount is not None else inp.download
|
|
86
|
+
elif isinstance(inp.value, flyte.io.Dir):
|
|
87
|
+
value = inp.value.path
|
|
88
|
+
tpe = "directory"
|
|
89
|
+
download = True if inp.mount is not None else inp.download
|
|
90
|
+
else:
|
|
91
|
+
value = inp.value
|
|
92
|
+
download = False
|
|
93
|
+
|
|
94
|
+
return cls(
|
|
95
|
+
name=inp.name,
|
|
96
|
+
value=value,
|
|
97
|
+
type=tpe,
|
|
98
|
+
download=download,
|
|
99
|
+
env_var=inp.env_var,
|
|
100
|
+
dest=inp.mount,
|
|
101
|
+
ignore_patterns=inp.ignore_patterns,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class SerializableInputCollection(BaseModel):
|
|
106
|
+
"""
|
|
107
|
+
Collection of inputs for application.
|
|
108
|
+
|
|
109
|
+
:param inputs: List of inputs.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
inputs: List[SerializableInput] = field(default_factory=list)
|
|
113
|
+
|
|
114
|
+
@cached_property
|
|
115
|
+
def to_transport(self) -> str:
|
|
116
|
+
import base64
|
|
117
|
+
import gzip
|
|
118
|
+
from io import BytesIO
|
|
119
|
+
|
|
120
|
+
json_str = self.model_dump_json()
|
|
121
|
+
buf = BytesIO()
|
|
122
|
+
with gzip.GzipFile(mode="wb", fileobj=buf, mtime=0) as f:
|
|
123
|
+
f.write(json_str.encode("utf-8"))
|
|
124
|
+
return base64.b64encode(buf.getvalue()).decode("utf-8")
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def from_transport(cls, s: str) -> SerializableInputCollection:
|
|
128
|
+
import base64
|
|
129
|
+
import gzip
|
|
130
|
+
|
|
131
|
+
compressed_val = base64.b64decode(s.encode("utf-8"))
|
|
132
|
+
json_str = gzip.decompress(compressed_val).decode("utf-8")
|
|
133
|
+
return cls.model_validate_json(json_str)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def from_inputs(cls, inputs: List[Input]) -> SerializableInputCollection:
|
|
137
|
+
return cls(inputs=[SerializableInput.from_input(inp) for inp in inputs])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@cache
|
|
141
|
+
def _load_inputs() -> dict[str, str]:
|
|
142
|
+
"""Load inputs for application or endpoint."""
|
|
143
|
+
import json
|
|
144
|
+
import os
|
|
145
|
+
|
|
146
|
+
config_file = os.getenv(RUNTIME_INPUTS_FILE)
|
|
147
|
+
|
|
148
|
+
if config_file is None:
|
|
149
|
+
raise ValueError("Inputs are not mounted")
|
|
150
|
+
|
|
151
|
+
with open(config_file, "r") as f:
|
|
152
|
+
inputs = json.load(f)
|
|
153
|
+
|
|
154
|
+
return inputs
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_input(name: str) -> str:
|
|
158
|
+
"""Get inputs for application or endpoint."""
|
|
159
|
+
inputs = _load_inputs()
|
|
160
|
+
return inputs[name]
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Serialization module for AppEnvironment to AppIDL conversion.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to serialize an AppEnvironment object into
|
|
5
|
+
the AppIDL protobuf format, using SerializationContext for configuration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from copy import deepcopy
|
|
11
|
+
from typing import List, Optional, Union
|
|
12
|
+
|
|
13
|
+
from flyteidl2.app import app_definition_pb2
|
|
14
|
+
from flyteidl2.common import runtime_version_pb2
|
|
15
|
+
from flyteidl2.core import literals_pb2, tasks_pb2
|
|
16
|
+
from google.protobuf.duration_pb2 import Duration
|
|
17
|
+
|
|
18
|
+
import flyte
|
|
19
|
+
import flyte.errors
|
|
20
|
+
import flyte.io
|
|
21
|
+
from flyte._internal.runtime.resources_serde import get_proto_extended_resources, get_proto_resources
|
|
22
|
+
from flyte._internal.runtime.task_serde import get_security_context, lookup_image_in_cache
|
|
23
|
+
from flyte.app import AppEnvironment, Input, Scaling
|
|
24
|
+
from flyte.models import SerializationContext
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_proto_container(
|
|
28
|
+
app_env: AppEnvironment,
|
|
29
|
+
serialization_context: SerializationContext,
|
|
30
|
+
) -> tasks_pb2.Container:
|
|
31
|
+
"""
|
|
32
|
+
Construct the container specification.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
app_env: The app environment
|
|
36
|
+
serialization_context: Serialization context
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Container protobuf message
|
|
40
|
+
"""
|
|
41
|
+
from flyte import Image
|
|
42
|
+
|
|
43
|
+
env = [literals_pb2.KeyValuePair(key=k, value=v) for k, v in app_env.env_vars.items()] if app_env.env_vars else None
|
|
44
|
+
resources = get_proto_resources(app_env.resources)
|
|
45
|
+
|
|
46
|
+
if app_env.image == "auto":
|
|
47
|
+
img = Image.from_debian_base()
|
|
48
|
+
elif isinstance(app_env.image, str):
|
|
49
|
+
img = Image.from_base(app_env.image)
|
|
50
|
+
else:
|
|
51
|
+
img = app_env.image
|
|
52
|
+
|
|
53
|
+
env_name = app_env.name
|
|
54
|
+
img_uri = lookup_image_in_cache(serialization_context, env_name, img)
|
|
55
|
+
|
|
56
|
+
p = app_env.get_port()
|
|
57
|
+
container_ports = [tasks_pb2.ContainerPort(container_port=p.port, name=p.name)]
|
|
58
|
+
|
|
59
|
+
return tasks_pb2.Container(
|
|
60
|
+
image=img_uri,
|
|
61
|
+
command=app_env.container_cmd(serialization_context),
|
|
62
|
+
args=app_env.container_args(serialization_context),
|
|
63
|
+
resources=resources,
|
|
64
|
+
ports=container_ports,
|
|
65
|
+
env=env,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _sanitize_resource_name(resource: tasks_pb2.Resources.ResourceEntry) -> str:
|
|
70
|
+
"""
|
|
71
|
+
Sanitize resource name for Kubernetes compatibility.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
resource: Resource entry
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Sanitized resource name
|
|
78
|
+
"""
|
|
79
|
+
return tasks_pb2.Resources.ResourceName.Name(resource.name).lower().replace("_", "-")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _serialized_pod_spec(
|
|
83
|
+
app_env: AppEnvironment,
|
|
84
|
+
pod_template: flyte.PodTemplate,
|
|
85
|
+
serialization_context: SerializationContext,
|
|
86
|
+
) -> dict:
|
|
87
|
+
"""
|
|
88
|
+
Convert pod spec into a dict for serialization.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
app_env: The app environment
|
|
92
|
+
pod_template: Pod template specification
|
|
93
|
+
serialization_context: Serialization context
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dictionary representation of the pod spec
|
|
97
|
+
"""
|
|
98
|
+
from kubernetes.client import ApiClient
|
|
99
|
+
from kubernetes.client.models import V1Container, V1ContainerPort, V1EnvVar, V1ResourceRequirements
|
|
100
|
+
|
|
101
|
+
pod_template = deepcopy(pod_template)
|
|
102
|
+
|
|
103
|
+
if pod_template.pod_spec is None:
|
|
104
|
+
return {}
|
|
105
|
+
|
|
106
|
+
if pod_template.primary_container_name != "app":
|
|
107
|
+
msg = "Primary container name must be 'app'"
|
|
108
|
+
raise ValueError(msg)
|
|
109
|
+
|
|
110
|
+
containers: list[V1Container] = pod_template.pod_spec.containers
|
|
111
|
+
primary_exists = any(container.name == pod_template.primary_container_name for container in containers)
|
|
112
|
+
|
|
113
|
+
if not primary_exists:
|
|
114
|
+
msg = "Primary container does not exist with name 'app'"
|
|
115
|
+
raise ValueError(msg)
|
|
116
|
+
|
|
117
|
+
final_containers = []
|
|
118
|
+
|
|
119
|
+
# Process containers
|
|
120
|
+
for container in containers:
|
|
121
|
+
img = container.image
|
|
122
|
+
if isinstance(img, flyte.Image):
|
|
123
|
+
img = lookup_image_in_cache(serialization_context, container.name, img)
|
|
124
|
+
container.image = img
|
|
125
|
+
|
|
126
|
+
if container.name == pod_template.primary_container_name:
|
|
127
|
+
container.args = app_env.container_args(serialization_context)
|
|
128
|
+
container.command = app_env.container_cmd(serialization_context)
|
|
129
|
+
|
|
130
|
+
limits, requests = {}, {}
|
|
131
|
+
resources = get_proto_resources(app_env.resources)
|
|
132
|
+
if resources:
|
|
133
|
+
for resource in resources.limits:
|
|
134
|
+
limits[_sanitize_resource_name(resource)] = resource.value
|
|
135
|
+
for resource in resources.requests:
|
|
136
|
+
requests[_sanitize_resource_name(resource)] = resource.value
|
|
137
|
+
|
|
138
|
+
resource_requirements = V1ResourceRequirements(limits=limits, requests=requests)
|
|
139
|
+
|
|
140
|
+
if limits or requests:
|
|
141
|
+
container.resources = resource_requirements
|
|
142
|
+
|
|
143
|
+
if app_env.env_vars:
|
|
144
|
+
container.env = [V1EnvVar(name=k, value=v) for k, v in app_env.env_vars.items()] + (container.env or [])
|
|
145
|
+
|
|
146
|
+
_port = app_env.get_port()
|
|
147
|
+
container.ports = [V1ContainerPort(container_port=_port.port, name=_port.name)] + (container.ports or [])
|
|
148
|
+
|
|
149
|
+
final_containers.append(container)
|
|
150
|
+
|
|
151
|
+
pod_template.pod_spec.containers = final_containers
|
|
152
|
+
return ApiClient().sanitize_for_serialization(pod_template.pod_spec)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _get_k8s_pod(
|
|
156
|
+
app_env: AppEnvironment,
|
|
157
|
+
pod_template: flyte.PodTemplate,
|
|
158
|
+
serialization_context: SerializationContext,
|
|
159
|
+
) -> tasks_pb2.K8sPod:
|
|
160
|
+
"""
|
|
161
|
+
Convert pod_template into a K8sPod IDL.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
app_env: The app environment
|
|
165
|
+
pod_template: Pod template specification
|
|
166
|
+
serialization_context: Serialization context
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
K8sPod protobuf message
|
|
170
|
+
"""
|
|
171
|
+
import json
|
|
172
|
+
|
|
173
|
+
from google.protobuf.json_format import Parse
|
|
174
|
+
from google.protobuf.struct_pb2 import Struct
|
|
175
|
+
|
|
176
|
+
pod_spec_dict = _serialized_pod_spec(app_env, pod_template, serialization_context)
|
|
177
|
+
pod_spec_idl = Parse(json.dumps(pod_spec_dict), Struct())
|
|
178
|
+
|
|
179
|
+
metadata = tasks_pb2.K8sObjectMetadata(
|
|
180
|
+
labels=pod_template.labels,
|
|
181
|
+
annotations=pod_template.annotations,
|
|
182
|
+
)
|
|
183
|
+
return tasks_pb2.K8sPod(pod_spec=pod_spec_idl, metadata=metadata)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _get_scaling_metric(
|
|
187
|
+
metric: Optional[Union[Scaling.Concurrency, Scaling.RequestRate]],
|
|
188
|
+
) -> Optional[app_definition_pb2.ScalingMetric]:
|
|
189
|
+
"""
|
|
190
|
+
Convert scaling metric to protobuf format.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
metric: Scaling metric (Concurrency or RequestRate)
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
ScalingMetric protobuf message or None
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
if metric is None:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
if isinstance(metric, Scaling.Concurrency):
|
|
203
|
+
return app_definition_pb2.ScalingMetric(concurrency=app_definition_pb2.Concurrency(val=metric.val))
|
|
204
|
+
elif isinstance(metric, Scaling.RequestRate):
|
|
205
|
+
return app_definition_pb2.ScalingMetric(request_rate=app_definition_pb2.RequestRate(val=metric.val))
|
|
206
|
+
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def translate_inputs(inputs: List[Input]) -> app_definition_pb2.InputList:
|
|
211
|
+
"""
|
|
212
|
+
Placeholder for translating inputs to protobuf format.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
InputList protobuf message
|
|
216
|
+
"""
|
|
217
|
+
if not inputs:
|
|
218
|
+
return app_definition_pb2.InputList()
|
|
219
|
+
|
|
220
|
+
inputs_list = []
|
|
221
|
+
for input in inputs:
|
|
222
|
+
if isinstance(input.value, str):
|
|
223
|
+
inputs_list.append(app_definition_pb2.Input(name=input.name, string_value=input.value))
|
|
224
|
+
elif isinstance(input.value, flyte.io.File):
|
|
225
|
+
inputs_list.append(app_definition_pb2.Input(name=input.name, string_value=str(input.value.path)))
|
|
226
|
+
elif isinstance(input.value, flyte.io.Dir):
|
|
227
|
+
inputs_list.append(app_definition_pb2.Input(name=input.name, string_value=str(input.value.path)))
|
|
228
|
+
else:
|
|
229
|
+
raise ValueError(f"Unsupported input value type: {type(input.value)}")
|
|
230
|
+
return app_definition_pb2.InputList(items=inputs_list)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def translate_app_env_to_idl(
|
|
234
|
+
app_env: AppEnvironment,
|
|
235
|
+
serialization_context: SerializationContext,
|
|
236
|
+
desired_state: app_definition_pb2.Spec.DesiredState = app_definition_pb2.Spec.DesiredState.DESIRED_STATE_ACTIVE,
|
|
237
|
+
) -> app_definition_pb2.App:
|
|
238
|
+
"""
|
|
239
|
+
Translate an AppEnvironment to AppIDL protobuf format.
|
|
240
|
+
|
|
241
|
+
This is the main entry point for serializing an AppEnvironment object into
|
|
242
|
+
the AppIDL protobuf format.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
app_env: The app environment to serialize
|
|
246
|
+
serialization_context: Serialization context containing org, project, domain, version, etc.
|
|
247
|
+
desired_state: Desired state of the app (ACTIVE, INACTIVE, etc.)
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
AppIDL protobuf message
|
|
251
|
+
"""
|
|
252
|
+
# Build security context
|
|
253
|
+
task_sec_ctx = get_security_context(app_env.secrets)
|
|
254
|
+
allow_anonymous = False
|
|
255
|
+
if not app_env.requires_auth:
|
|
256
|
+
allow_anonymous = True
|
|
257
|
+
|
|
258
|
+
security_context = None
|
|
259
|
+
if task_sec_ctx or allow_anonymous:
|
|
260
|
+
security_context = app_definition_pb2.SecurityContext(
|
|
261
|
+
run_as=task_sec_ctx.run_as if task_sec_ctx else None,
|
|
262
|
+
secrets=task_sec_ctx.secrets if task_sec_ctx else [],
|
|
263
|
+
allow_anonymous=allow_anonymous,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Build autoscaling config
|
|
267
|
+
scaling_metric = _get_scaling_metric(app_env.scaling.metric)
|
|
268
|
+
|
|
269
|
+
dur = None
|
|
270
|
+
if app_env.scaling.scaledown_after:
|
|
271
|
+
dur = Duration()
|
|
272
|
+
dur.FromTimedelta(app_env.scaling.scaledown_after)
|
|
273
|
+
|
|
274
|
+
min_replicas, max_replicas = app_env.scaling.get_replicas()
|
|
275
|
+
autoscaling = app_definition_pb2.AutoscalingConfig(
|
|
276
|
+
replicas=app_definition_pb2.Replicas(min=min_replicas, max=max_replicas),
|
|
277
|
+
scaledown_period=dur,
|
|
278
|
+
scaling_metric=scaling_metric,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Build spec based on image type
|
|
282
|
+
container = None
|
|
283
|
+
pod = None
|
|
284
|
+
if app_env.pod_template:
|
|
285
|
+
if isinstance(app_env.pod_template, str):
|
|
286
|
+
raise NotImplementedError("PodTemplate as str is not supported yet")
|
|
287
|
+
pod = _get_k8s_pod(
|
|
288
|
+
app_env,
|
|
289
|
+
app_env.pod_template,
|
|
290
|
+
serialization_context,
|
|
291
|
+
)
|
|
292
|
+
elif app_env.image:
|
|
293
|
+
container = get_proto_container(
|
|
294
|
+
app_env,
|
|
295
|
+
serialization_context,
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
msg = "image must be a str, Image, or PodTemplate"
|
|
299
|
+
raise ValueError(msg)
|
|
300
|
+
|
|
301
|
+
ingress = app_definition_pb2.IngressConfig(
|
|
302
|
+
private=False,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Build links
|
|
306
|
+
links = None
|
|
307
|
+
if app_env.links:
|
|
308
|
+
links = [
|
|
309
|
+
app_definition_pb2.Link(path=link.path, title=link.title, is_relative=link.is_relative)
|
|
310
|
+
for link in app_env.links
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
# Build profile
|
|
314
|
+
profile = app_definition_pb2.Profile(
|
|
315
|
+
type=app_env.type,
|
|
316
|
+
short_description=app_env.description,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Build the full App IDL
|
|
320
|
+
return app_definition_pb2.App(
|
|
321
|
+
metadata=app_definition_pb2.Meta(
|
|
322
|
+
id=app_definition_pb2.Identifier(
|
|
323
|
+
org=serialization_context.org,
|
|
324
|
+
project=serialization_context.project,
|
|
325
|
+
domain=serialization_context.domain,
|
|
326
|
+
name=app_env.name,
|
|
327
|
+
),
|
|
328
|
+
),
|
|
329
|
+
spec=app_definition_pb2.Spec(
|
|
330
|
+
desired_state=desired_state,
|
|
331
|
+
ingress=ingress,
|
|
332
|
+
autoscaling=autoscaling,
|
|
333
|
+
security_context=security_context,
|
|
334
|
+
cluster_pool=app_env.cluster_pool,
|
|
335
|
+
extended_resources=get_proto_extended_resources(app_env.resources),
|
|
336
|
+
runtime_metadata=runtime_version_pb2.RuntimeMetadata(
|
|
337
|
+
type=runtime_version_pb2.RuntimeMetadata.RuntimeType.FLYTE_SDK,
|
|
338
|
+
version=flyte.version(),
|
|
339
|
+
flavor="python",
|
|
340
|
+
),
|
|
341
|
+
profile=profile,
|
|
342
|
+
links=links,
|
|
343
|
+
container=container,
|
|
344
|
+
pod=pod,
|
|
345
|
+
inputs=translate_inputs(app_env.inputs),
|
|
346
|
+
),
|
|
347
|
+
)
|
flyte/app/_types.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
from typing import Optional, Tuple, Union
|
|
4
|
+
|
|
5
|
+
import rich.repr
|
|
6
|
+
|
|
7
|
+
INVALID_APP_PORTS = [8012, 8022, 8112, 9090, 9091]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@rich.repr.auto
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class Port:
|
|
13
|
+
port: int
|
|
14
|
+
name: Optional[str] = None
|
|
15
|
+
|
|
16
|
+
def __post_init__(self):
|
|
17
|
+
if self.port in INVALID_APP_PORTS:
|
|
18
|
+
invalid_ports = ", ".join(str(p) for p in INVALID_APP_PORTS)
|
|
19
|
+
msg = f"port {self.port} is not allowed. Please do not use ports: {invalid_ports}"
|
|
20
|
+
raise ValueError(msg)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@rich.repr.auto
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class Link:
|
|
26
|
+
path: str
|
|
27
|
+
title: str
|
|
28
|
+
is_relative: bool = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@rich.repr.auto
|
|
32
|
+
@dataclass
|
|
33
|
+
class Scaling:
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class Concurrency:
|
|
36
|
+
"""
|
|
37
|
+
Use this to specify the concurrency metric for autoscaling, i.e. the number of concurrent requests at a replica
|
|
38
|
+
at which to scale up.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
val: int
|
|
42
|
+
|
|
43
|
+
def __post_init__(self):
|
|
44
|
+
if self.val < 1:
|
|
45
|
+
raise ValueError("Concurrency must be greater than or equal to 1")
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class RequestRate:
|
|
49
|
+
"""
|
|
50
|
+
Use this to specify the request rate metric for autoscaling, i.e. the number of requests per second at a replica
|
|
51
|
+
at which to scale up.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
val: int
|
|
55
|
+
|
|
56
|
+
def __post_init__(self):
|
|
57
|
+
if self.val < 1:
|
|
58
|
+
raise ValueError("Request rate must be greater than or equal to 1")
|
|
59
|
+
|
|
60
|
+
replicas: Union[int, Tuple[int, int]] = (1, 1)
|
|
61
|
+
metric: Optional[Union[Concurrency, RequestRate]] = None
|
|
62
|
+
scaledown_after: int | timedelta | None = None
|
|
63
|
+
|
|
64
|
+
def __post_init__(self):
|
|
65
|
+
if isinstance(self.replicas, int):
|
|
66
|
+
if self.replicas < 0:
|
|
67
|
+
raise ValueError("replicas must be greater than or equal to 0")
|
|
68
|
+
self.replicas = (self.replicas, self.replicas)
|
|
69
|
+
elif isinstance(self.replicas, tuple):
|
|
70
|
+
if len(self.replicas) != 2:
|
|
71
|
+
raise ValueError("replicas tuple must be of length 2")
|
|
72
|
+
min_replicas, max_replicas = self.replicas
|
|
73
|
+
if min_replicas < 0:
|
|
74
|
+
raise ValueError("min_replicas must be greater than or equal to 0")
|
|
75
|
+
if max_replicas < 1 or max_replicas < min_replicas:
|
|
76
|
+
raise ValueError("max_replicas must be greater than or equal to 1 and min_replicas")
|
|
77
|
+
else:
|
|
78
|
+
raise TypeError("replicas must be an int or a tuple of two ints")
|
|
79
|
+
|
|
80
|
+
if self.metric:
|
|
81
|
+
if not isinstance(self.metric, (Scaling.Concurrency, Scaling.RequestRate)):
|
|
82
|
+
raise TypeError("metric must be an instance of Scaling.Concurrency or Scaling.RequestRate")
|
|
83
|
+
|
|
84
|
+
if self.scaledown_after:
|
|
85
|
+
if isinstance(self.scaledown_after, int):
|
|
86
|
+
self.scaledown_after = timedelta(seconds=self.scaledown_after)
|
|
87
|
+
elif not isinstance(self.scaledown_after, timedelta):
|
|
88
|
+
raise TypeError("scaledown_after must be an int or a timedelta")
|
|
89
|
+
|
|
90
|
+
def get_replicas(self) -> Tuple[int, int]:
|
|
91
|
+
if isinstance(self.replicas, int):
|
|
92
|
+
return self.replicas, self.replicas
|
|
93
|
+
return self.replicas
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@rich.repr.auto
|
|
97
|
+
@dataclass
|
|
98
|
+
class Domain:
|
|
99
|
+
# SubDomain config
|
|
100
|
+
subdomain: Optional[str] = None
|
|
101
|
+
custom_domain: Optional[str] = None
|