flyte 0.2.0b1__py3-none-any.whl → 2.0.0b46__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.
- flyte/__init__.py +83 -30
- flyte/_bin/connect.py +61 -0
- flyte/_bin/debug.py +38 -0
- flyte/_bin/runtime.py +87 -19
- flyte/_bin/serve.py +351 -0
- flyte/_build.py +3 -2
- flyte/_cache/cache.py +6 -5
- flyte/_cache/local_cache.py +216 -0
- flyte/_code_bundle/_ignore.py +31 -5
- flyte/_code_bundle/_packaging.py +42 -11
- flyte/_code_bundle/_utils.py +57 -34
- flyte/_code_bundle/bundle.py +130 -27
- flyte/_constants.py +1 -0
- flyte/_context.py +21 -5
- flyte/_custom_context.py +73 -0
- flyte/_debug/constants.py +37 -0
- flyte/_debug/utils.py +17 -0
- flyte/_debug/vscode.py +315 -0
- flyte/_deploy.py +396 -75
- flyte/_deployer.py +109 -0
- flyte/_environment.py +94 -11
- flyte/_excepthook.py +37 -0
- flyte/_group.py +2 -1
- flyte/_hash.py +1 -16
- flyte/_image.py +544 -231
- flyte/_initialize.py +456 -316
- flyte/_interface.py +40 -5
- flyte/_internal/controllers/__init__.py +22 -8
- flyte/_internal/controllers/_local_controller.py +159 -35
- flyte/_internal/controllers/_trace.py +18 -10
- flyte/_internal/controllers/remote/__init__.py +38 -9
- flyte/_internal/controllers/remote/_action.py +82 -12
- flyte/_internal/controllers/remote/_client.py +6 -2
- flyte/_internal/controllers/remote/_controller.py +290 -64
- flyte/_internal/controllers/remote/_core.py +155 -95
- flyte/_internal/controllers/remote/_informer.py +40 -20
- flyte/_internal/controllers/remote/_service_protocol.py +2 -2
- flyte/_internal/imagebuild/__init__.py +2 -10
- flyte/_internal/imagebuild/docker_builder.py +391 -84
- flyte/_internal/imagebuild/image_builder.py +111 -55
- flyte/_internal/imagebuild/remote_builder.py +409 -0
- flyte/_internal/imagebuild/utils.py +79 -0
- flyte/_internal/resolvers/_app_env_module.py +92 -0
- flyte/_internal/resolvers/_task_module.py +5 -38
- flyte/_internal/resolvers/app_env.py +26 -0
- flyte/_internal/resolvers/common.py +8 -1
- flyte/_internal/resolvers/default.py +2 -2
- flyte/_internal/runtime/convert.py +319 -36
- flyte/_internal/runtime/entrypoints.py +106 -18
- flyte/_internal/runtime/io.py +71 -23
- flyte/_internal/runtime/resources_serde.py +21 -7
- flyte/_internal/runtime/reuse.py +125 -0
- flyte/_internal/runtime/rusty.py +196 -0
- flyte/_internal/runtime/task_serde.py +239 -66
- flyte/_internal/runtime/taskrunner.py +48 -8
- flyte/_internal/runtime/trigger_serde.py +162 -0
- flyte/_internal/runtime/types_serde.py +7 -16
- flyte/_keyring/file.py +115 -0
- flyte/_link.py +30 -0
- flyte/_logging.py +241 -42
- flyte/_map.py +312 -0
- flyte/_metrics.py +59 -0
- flyte/_module.py +74 -0
- flyte/_pod.py +30 -0
- flyte/_resources.py +296 -33
- flyte/_retry.py +1 -7
- flyte/_reusable_environment.py +72 -7
- flyte/_run.py +462 -132
- flyte/_secret.py +47 -11
- flyte/_serve.py +333 -0
- flyte/_task.py +245 -56
- flyte/_task_environment.py +219 -97
- flyte/_task_plugins.py +47 -0
- flyte/_tools.py +8 -8
- flyte/_trace.py +15 -24
- flyte/_trigger.py +1027 -0
- flyte/_utils/__init__.py +12 -1
- flyte/_utils/asyn.py +3 -1
- flyte/_utils/async_cache.py +139 -0
- flyte/_utils/coro_management.py +5 -4
- flyte/_utils/description_parser.py +19 -0
- flyte/_utils/docker_credentials.py +173 -0
- flyte/_utils/helpers.py +45 -19
- flyte/_utils/module_loader.py +123 -0
- flyte/_utils/org_discovery.py +57 -0
- flyte/_utils/uv_script_parser.py +8 -1
- flyte/_version.py +16 -3
- flyte/app/__init__.py +27 -0
- flyte/app/_app_environment.py +362 -0
- flyte/app/_connector_environment.py +40 -0
- flyte/app/_deploy.py +130 -0
- flyte/app/_parameter.py +343 -0
- flyte/app/_runtime/__init__.py +3 -0
- flyte/app/_runtime/app_serde.py +383 -0
- flyte/app/_types.py +113 -0
- flyte/app/extras/__init__.py +9 -0
- flyte/app/extras/_auth_middleware.py +217 -0
- flyte/app/extras/_fastapi.py +93 -0
- flyte/app/extras/_model_loader/__init__.py +3 -0
- flyte/app/extras/_model_loader/config.py +7 -0
- flyte/app/extras/_model_loader/loader.py +288 -0
- flyte/cli/__init__.py +12 -0
- flyte/cli/_abort.py +28 -0
- flyte/cli/_build.py +114 -0
- flyte/cli/_common.py +493 -0
- flyte/cli/_create.py +371 -0
- flyte/cli/_delete.py +45 -0
- flyte/cli/_deploy.py +401 -0
- flyte/cli/_gen.py +316 -0
- flyte/cli/_get.py +446 -0
- flyte/cli/_option.py +33 -0
- flyte/{_cli → cli}/_params.py +57 -17
- flyte/cli/_plugins.py +209 -0
- flyte/cli/_prefetch.py +292 -0
- flyte/cli/_run.py +690 -0
- flyte/cli/_serve.py +338 -0
- flyte/cli/_update.py +86 -0
- flyte/cli/_user.py +20 -0
- flyte/cli/main.py +246 -0
- flyte/config/__init__.py +2 -167
- flyte/config/_config.py +215 -163
- flyte/config/_internal.py +10 -1
- flyte/config/_reader.py +225 -0
- flyte/connectors/__init__.py +11 -0
- flyte/connectors/_connector.py +330 -0
- flyte/connectors/_server.py +194 -0
- flyte/connectors/utils.py +159 -0
- flyte/errors.py +134 -2
- flyte/extend.py +24 -0
- flyte/extras/_container.py +69 -56
- flyte/git/__init__.py +3 -0
- flyte/git/_config.py +279 -0
- flyte/io/__init__.py +8 -1
- flyte/io/{structured_dataset → _dataframe}/__init__.py +32 -30
- flyte/io/{structured_dataset → _dataframe}/basic_dfs.py +75 -68
- flyte/io/{structured_dataset/structured_dataset.py → _dataframe/dataframe.py} +207 -242
- flyte/io/_dir.py +575 -113
- flyte/io/_file.py +587 -141
- flyte/io/_hashing_io.py +342 -0
- flyte/io/extend.py +7 -0
- flyte/models.py +635 -0
- flyte/prefetch/__init__.py +22 -0
- flyte/prefetch/_hf_model.py +563 -0
- flyte/remote/__init__.py +14 -3
- flyte/remote/_action.py +879 -0
- flyte/remote/_app.py +346 -0
- flyte/remote/_auth_metadata.py +42 -0
- flyte/remote/_client/_protocols.py +62 -4
- flyte/remote/_client/auth/_auth_utils.py +19 -0
- flyte/remote/_client/auth/_authenticators/base.py +8 -2
- flyte/remote/_client/auth/_authenticators/device_code.py +4 -5
- flyte/remote/_client/auth/_authenticators/factory.py +4 -0
- flyte/remote/_client/auth/_authenticators/passthrough.py +79 -0
- flyte/remote/_client/auth/_authenticators/pkce.py +17 -18
- flyte/remote/_client/auth/_channel.py +47 -18
- flyte/remote/_client/auth/_client_config.py +5 -3
- flyte/remote/_client/auth/_keyring.py +15 -2
- flyte/remote/_client/auth/_token_client.py +3 -3
- flyte/remote/_client/controlplane.py +206 -18
- flyte/remote/_common.py +66 -0
- flyte/remote/_data.py +107 -22
- flyte/remote/_logs.py +116 -33
- flyte/remote/_project.py +21 -19
- flyte/remote/_run.py +164 -631
- flyte/remote/_secret.py +72 -29
- flyte/remote/_task.py +387 -46
- flyte/remote/_trigger.py +368 -0
- flyte/remote/_user.py +43 -0
- flyte/report/_report.py +10 -6
- flyte/storage/__init__.py +13 -1
- flyte/storage/_config.py +237 -0
- flyte/storage/_parallel_reader.py +289 -0
- flyte/storage/_storage.py +268 -59
- flyte/syncify/__init__.py +56 -0
- flyte/syncify/_api.py +414 -0
- flyte/types/__init__.py +39 -0
- flyte/types/_interface.py +22 -7
- flyte/{io/pickle/transformer.py → types/_pickle.py} +37 -9
- flyte/types/_string_literals.py +8 -9
- flyte/types/_type_engine.py +226 -126
- flyte/types/_utils.py +1 -1
- flyte-2.0.0b46.data/scripts/debug.py +38 -0
- flyte-2.0.0b46.data/scripts/runtime.py +194 -0
- flyte-2.0.0b46.dist-info/METADATA +352 -0
- flyte-2.0.0b46.dist-info/RECORD +221 -0
- flyte-2.0.0b46.dist-info/entry_points.txt +8 -0
- flyte-2.0.0b46.dist-info/licenses/LICENSE +201 -0
- flyte/_api_commons.py +0 -3
- flyte/_cli/_common.py +0 -299
- flyte/_cli/_create.py +0 -42
- flyte/_cli/_delete.py +0 -23
- flyte/_cli/_deploy.py +0 -140
- flyte/_cli/_get.py +0 -235
- flyte/_cli/_run.py +0 -174
- flyte/_cli/main.py +0 -98
- flyte/_datastructures.py +0 -342
- flyte/_internal/controllers/pbhash.py +0 -39
- flyte/_protos/common/authorization_pb2.py +0 -66
- flyte/_protos/common/authorization_pb2.pyi +0 -108
- flyte/_protos/common/authorization_pb2_grpc.py +0 -4
- flyte/_protos/common/identifier_pb2.py +0 -71
- flyte/_protos/common/identifier_pb2.pyi +0 -82
- flyte/_protos/common/identifier_pb2_grpc.py +0 -4
- flyte/_protos/common/identity_pb2.py +0 -48
- flyte/_protos/common/identity_pb2.pyi +0 -72
- flyte/_protos/common/identity_pb2_grpc.py +0 -4
- flyte/_protos/common/list_pb2.py +0 -36
- flyte/_protos/common/list_pb2.pyi +0 -69
- flyte/_protos/common/list_pb2_grpc.py +0 -4
- flyte/_protos/common/policy_pb2.py +0 -37
- flyte/_protos/common/policy_pb2.pyi +0 -27
- flyte/_protos/common/policy_pb2_grpc.py +0 -4
- flyte/_protos/common/role_pb2.py +0 -37
- flyte/_protos/common/role_pb2.pyi +0 -53
- flyte/_protos/common/role_pb2_grpc.py +0 -4
- flyte/_protos/common/runtime_version_pb2.py +0 -28
- flyte/_protos/common/runtime_version_pb2.pyi +0 -24
- flyte/_protos/common/runtime_version_pb2_grpc.py +0 -4
- flyte/_protos/logs/dataplane/payload_pb2.py +0 -96
- flyte/_protos/logs/dataplane/payload_pb2.pyi +0 -168
- flyte/_protos/logs/dataplane/payload_pb2_grpc.py +0 -4
- flyte/_protos/secret/definition_pb2.py +0 -49
- flyte/_protos/secret/definition_pb2.pyi +0 -93
- flyte/_protos/secret/definition_pb2_grpc.py +0 -4
- flyte/_protos/secret/payload_pb2.py +0 -62
- flyte/_protos/secret/payload_pb2.pyi +0 -94
- flyte/_protos/secret/payload_pb2_grpc.py +0 -4
- flyte/_protos/secret/secret_pb2.py +0 -38
- flyte/_protos/secret/secret_pb2.pyi +0 -6
- flyte/_protos/secret/secret_pb2_grpc.py +0 -198
- flyte/_protos/secret/secret_pb2_grpc_grpc.py +0 -198
- flyte/_protos/validate/validate/validate_pb2.py +0 -76
- flyte/_protos/workflow/node_execution_service_pb2.py +0 -26
- flyte/_protos/workflow/node_execution_service_pb2.pyi +0 -4
- flyte/_protos/workflow/node_execution_service_pb2_grpc.py +0 -32
- flyte/_protos/workflow/queue_service_pb2.py +0 -106
- flyte/_protos/workflow/queue_service_pb2.pyi +0 -141
- flyte/_protos/workflow/queue_service_pb2_grpc.py +0 -172
- flyte/_protos/workflow/run_definition_pb2.py +0 -128
- flyte/_protos/workflow/run_definition_pb2.pyi +0 -310
- flyte/_protos/workflow/run_definition_pb2_grpc.py +0 -4
- flyte/_protos/workflow/run_logs_service_pb2.py +0 -41
- flyte/_protos/workflow/run_logs_service_pb2.pyi +0 -28
- flyte/_protos/workflow/run_logs_service_pb2_grpc.py +0 -69
- flyte/_protos/workflow/run_service_pb2.py +0 -133
- flyte/_protos/workflow/run_service_pb2.pyi +0 -175
- flyte/_protos/workflow/run_service_pb2_grpc.py +0 -412
- flyte/_protos/workflow/state_service_pb2.py +0 -58
- flyte/_protos/workflow/state_service_pb2.pyi +0 -71
- flyte/_protos/workflow/state_service_pb2_grpc.py +0 -138
- flyte/_protos/workflow/task_definition_pb2.py +0 -72
- flyte/_protos/workflow/task_definition_pb2.pyi +0 -65
- flyte/_protos/workflow/task_definition_pb2_grpc.py +0 -4
- flyte/_protos/workflow/task_service_pb2.py +0 -44
- flyte/_protos/workflow/task_service_pb2.pyi +0 -31
- flyte/_protos/workflow/task_service_pb2_grpc.py +0 -104
- flyte/io/_dataframe.py +0 -0
- flyte/io/pickle/__init__.py +0 -0
- flyte/remote/_console.py +0 -18
- flyte-0.2.0b1.dist-info/METADATA +0 -179
- flyte-0.2.0b1.dist-info/RECORD +0 -204
- flyte-0.2.0b1.dist-info/entry_points.txt +0 -3
- /flyte/{_cli → _debug}/__init__.py +0 -0
- /flyte/{_protos → _keyring}/__init__.py +0 -0
- {flyte-0.2.0b1.dist-info → flyte-2.0.0b46.dist-info}/WHEEL +0 -0
- {flyte-0.2.0b1.dist-info → flyte-2.0.0b46.dist-info}/top_level.txt +0 -0
flyte/remote/_data.py
CHANGED
|
@@ -15,8 +15,9 @@ import httpx
|
|
|
15
15
|
from flyteidl.service import dataproxy_pb2
|
|
16
16
|
from google.protobuf import duration_pb2
|
|
17
17
|
|
|
18
|
-
from flyte._initialize import CommonInit, get_client,
|
|
19
|
-
from flyte.errors import RuntimeSystemError
|
|
18
|
+
from flyte._initialize import CommonInit, ensure_client, get_client, get_init_config, require_project_and_domain
|
|
19
|
+
from flyte.errors import InitializationError, RuntimeSystemError
|
|
20
|
+
from flyte.syncify import syncify
|
|
20
21
|
|
|
21
22
|
_UPLOAD_EXPIRES_IN = timedelta(seconds=60)
|
|
22
23
|
|
|
@@ -53,9 +54,89 @@ def hash_file(file_path: typing.Union[os.PathLike, str]) -> Tuple[bytes, str, in
|
|
|
53
54
|
return h.digest(), h.hexdigest(), size
|
|
54
55
|
|
|
55
56
|
|
|
57
|
+
async def _upload_with_retry(
|
|
58
|
+
fp: Path,
|
|
59
|
+
signed_url: str,
|
|
60
|
+
extra_headers: dict,
|
|
61
|
+
verify: bool,
|
|
62
|
+
max_retries: int = 3,
|
|
63
|
+
min_backoff_sec: float = 0.5,
|
|
64
|
+
max_backoff_sec: float = 10.0,
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Upload file to signed URL with exponential backoff retry.
|
|
68
|
+
|
|
69
|
+
Retries on transient network errors and 5xx/429/408 HTTP errors.
|
|
70
|
+
Does not retry on 4xx client errors (except 408/429).
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
fp: Path to file to upload
|
|
74
|
+
signed_url: Pre-signed URL for upload
|
|
75
|
+
extra_headers: Headers including Content-MD5, Content-Length
|
|
76
|
+
verify: Whether to verify SSL certificates
|
|
77
|
+
max_retries: Maximum retry attempts (default: 3)
|
|
78
|
+
min_backoff_sec: Initial backoff delay (default: 0.5)
|
|
79
|
+
max_backoff_sec: Maximum backoff delay (default: 10.0)
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
RuntimeSystemError: If upload fails after all retries
|
|
83
|
+
"""
|
|
84
|
+
from flyte._logging import logger
|
|
85
|
+
|
|
86
|
+
retry_attempt = 0
|
|
87
|
+
last_error = None
|
|
88
|
+
|
|
89
|
+
while retry_attempt <= max_retries:
|
|
90
|
+
async with aiofiles.open(str(fp), "rb") as file:
|
|
91
|
+
async with httpx.AsyncClient(verify=verify) as aclient:
|
|
92
|
+
put_resp = await aclient.put(signed_url, headers=extra_headers, content=file)
|
|
93
|
+
|
|
94
|
+
# Success
|
|
95
|
+
if put_resp.status_code in [200, 201, 204]:
|
|
96
|
+
if retry_attempt > 0:
|
|
97
|
+
logger.info(f"Upload succeeded after {retry_attempt} retries for {fp.name}")
|
|
98
|
+
return put_resp
|
|
99
|
+
|
|
100
|
+
# Check if retryable status code
|
|
101
|
+
if put_resp.status_code in [408, 429, 500, 502, 503, 504]:
|
|
102
|
+
if retry_attempt >= max_retries:
|
|
103
|
+
raise RuntimeSystemError(
|
|
104
|
+
"UploadFailed",
|
|
105
|
+
f"Failed to upload {fp} after {max_retries} retries: {put_resp.text}",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Backoff and retry
|
|
109
|
+
retry_attempt += 1
|
|
110
|
+
if retry_attempt <= max_retries:
|
|
111
|
+
backoff_delay = min(min_backoff_sec * (2 ** (retry_attempt - 1)), max_backoff_sec)
|
|
112
|
+
logger.warning(
|
|
113
|
+
f"Upload failed for {fp.name}, backing off for {backoff_delay:.2f}s "
|
|
114
|
+
f"[retry {retry_attempt}/{max_retries}]: {last_error}"
|
|
115
|
+
)
|
|
116
|
+
await asyncio.sleep(backoff_delay)
|
|
117
|
+
else:
|
|
118
|
+
# Non-retryable HTTP error
|
|
119
|
+
raise RuntimeSystemError(
|
|
120
|
+
"UploadFailed",
|
|
121
|
+
f"Failed to upload {fp} to {signed_url}, status code: {put_resp.status_code}, "
|
|
122
|
+
f"response: {put_resp.text}",
|
|
123
|
+
)
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@require_project_and_domain
|
|
56
128
|
async def _upload_single_file(
|
|
57
129
|
cfg: CommonInit, fp: Path, verify: bool = True, basedir: str | None = None
|
|
58
130
|
) -> Tuple[str, str]:
|
|
131
|
+
"""
|
|
132
|
+
Upload a single file to remote storage using a signed URL.
|
|
133
|
+
|
|
134
|
+
:param cfg: Configuration containing project and domain information.
|
|
135
|
+
:param fp: Path to the file to upload.
|
|
136
|
+
:param verify: Whether to verify SSL certificates.
|
|
137
|
+
:param basedir: Optional base directory prefix for the remote path.
|
|
138
|
+
:return: Tuple of (MD5 digest hex string, remote native URL).
|
|
139
|
+
"""
|
|
59
140
|
md5_bytes, str_digest, _ = hash_file(fp)
|
|
60
141
|
from flyte._logging import logger
|
|
61
142
|
|
|
@@ -77,39 +158,42 @@ async def _upload_single_file(
|
|
|
77
158
|
except grpc.aio.AioRpcError as e:
|
|
78
159
|
if e.code() == grpc.StatusCode.NOT_FOUND:
|
|
79
160
|
raise RuntimeSystemError(
|
|
80
|
-
"NotFound", f"Failed to get signed url for {fp}, please check your project and domain."
|
|
161
|
+
"NotFound", f"Failed to get signed url for {fp}, please check your project and domain: {e.details()}"
|
|
81
162
|
)
|
|
82
163
|
elif e.code() == grpc.StatusCode.PERMISSION_DENIED:
|
|
83
164
|
raise RuntimeSystemError(
|
|
84
|
-
"PermissionDenied", f"Failed to get signed url for {fp}, please check your permissions."
|
|
165
|
+
"PermissionDenied", f"Failed to get signed url for {fp}, please check your permissions: {e.details()}"
|
|
85
166
|
)
|
|
167
|
+
elif e.code() == grpc.StatusCode.UNAVAILABLE:
|
|
168
|
+
raise InitializationError("EndpointUnavailable", "user", "Service is unavailable.")
|
|
86
169
|
else:
|
|
87
|
-
raise RuntimeSystemError(e.code().value, f"Failed to get signed url for {fp}.")
|
|
170
|
+
raise RuntimeSystemError(e.code().value, f"Failed to get signed url for {fp}: {e.details()}")
|
|
88
171
|
except Exception as e:
|
|
89
172
|
raise RuntimeSystemError(type(e).__name__, f"Failed to get signed url for {fp}.") from e
|
|
90
|
-
logger.debug(f"Uploading to
|
|
173
|
+
logger.debug(f"Uploading to [link={resp.signed_url}]signed url[/link] for [link=file://{fp}]{fp}[/link]")
|
|
91
174
|
extra_headers = get_extra_headers_for_protocol(resp.native_url)
|
|
92
175
|
extra_headers.update(resp.headers)
|
|
93
176
|
encoded_md5 = b64encode(md5_bytes)
|
|
94
177
|
content_length = fp.stat().st_size
|
|
95
178
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
179
|
+
# Update headers with MD5 and content length
|
|
180
|
+
extra_headers.update({"Content-Length": str(content_length), "Content-MD5": encoded_md5.decode("utf-8")})
|
|
181
|
+
|
|
182
|
+
await _upload_with_retry(
|
|
183
|
+
fp=fp,
|
|
184
|
+
signed_url=resp.signed_url,
|
|
185
|
+
extra_headers=extra_headers,
|
|
186
|
+
verify=verify,
|
|
187
|
+
max_retries=3,
|
|
188
|
+
min_backoff_sec=0.5,
|
|
189
|
+
max_backoff_sec=10.0,
|
|
190
|
+
)
|
|
191
|
+
|
|
108
192
|
logger.debug(f"Uploaded with digest {str_digest}, blob location is {resp.native_url}")
|
|
109
193
|
return str_digest, resp.native_url
|
|
110
194
|
|
|
111
195
|
|
|
112
|
-
@
|
|
196
|
+
@syncify
|
|
113
197
|
async def upload_file(fp: Path, verify: bool = True) -> Tuple[str, str]:
|
|
114
198
|
"""
|
|
115
199
|
Uploads a file to a remote location and returns the remote URI.
|
|
@@ -119,13 +203,13 @@ async def upload_file(fp: Path, verify: bool = True) -> Tuple[str, str]:
|
|
|
119
203
|
:return: A tuple containing the MD5 digest and the remote URI.
|
|
120
204
|
"""
|
|
121
205
|
# This is a placeholder implementation. Replace with actual upload logic.
|
|
122
|
-
|
|
206
|
+
ensure_client()
|
|
207
|
+
cfg = get_init_config()
|
|
123
208
|
if not fp.is_file():
|
|
124
209
|
raise ValueError(f"{fp} is not a single file, upload arg must be a single file.")
|
|
125
210
|
return await _upload_single_file(cfg, fp, verify=verify)
|
|
126
211
|
|
|
127
212
|
|
|
128
|
-
@requires_client
|
|
129
213
|
async def upload_dir(dir_path: Path, verify: bool = True) -> str:
|
|
130
214
|
"""
|
|
131
215
|
Uploads a directory to a remote location and returns the remote URI.
|
|
@@ -135,7 +219,8 @@ async def upload_dir(dir_path: Path, verify: bool = True) -> str:
|
|
|
135
219
|
:return: The remote URI of the uploaded directory.
|
|
136
220
|
"""
|
|
137
221
|
# This is a placeholder implementation. Replace with actual upload logic.
|
|
138
|
-
|
|
222
|
+
ensure_client()
|
|
223
|
+
cfg = get_init_config()
|
|
139
224
|
if not dir_path.is_dir():
|
|
140
225
|
raise ValueError(f"{dir_path} is not a directory, upload arg must be a directory.")
|
|
141
226
|
|
flyte/remote/_logs.py
CHANGED
|
@@ -3,25 +3,44 @@ from collections import deque
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from typing import AsyncGenerator, AsyncIterator
|
|
5
5
|
|
|
6
|
+
import grpc
|
|
7
|
+
from flyteidl2.common import identifier_pb2
|
|
8
|
+
from flyteidl2.logs.dataplane import payload_pb2
|
|
9
|
+
from flyteidl2.workflow import run_logs_service_pb2
|
|
6
10
|
from rich.console import Console
|
|
7
11
|
from rich.live import Live
|
|
8
12
|
from rich.panel import Panel
|
|
9
13
|
from rich.text import Text
|
|
10
14
|
|
|
11
|
-
from flyte.
|
|
12
|
-
from flyte.
|
|
13
|
-
from flyte.
|
|
14
|
-
from flyte.
|
|
15
|
+
from flyte._initialize import ensure_client, get_client
|
|
16
|
+
from flyte._logging import logger
|
|
17
|
+
from flyte._tools import ipython_check, ipywidgets_check
|
|
18
|
+
from flyte.errors import LogsNotYetAvailableError
|
|
19
|
+
from flyte.syncify import syncify
|
|
15
20
|
|
|
21
|
+
style_map = {
|
|
22
|
+
payload_pb2.LogLineOriginator.SYSTEM: "bold magenta",
|
|
23
|
+
payload_pb2.LogLineOriginator.USER: "cyan",
|
|
24
|
+
payload_pb2.LogLineOriginator.UNKNOWN: "light red",
|
|
25
|
+
}
|
|
16
26
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
|
|
28
|
+
def _format_line(logline: payload_pb2.LogLine, show_ts: bool, filter_system: bool) -> Text | None:
|
|
29
|
+
"""
|
|
30
|
+
Format a log line for display with optional timestamp and system filtering.
|
|
31
|
+
|
|
32
|
+
:param logline: The log line protobuf to format.
|
|
33
|
+
:param show_ts: Whether to include timestamps.
|
|
34
|
+
:param filter_system: Whether to filter out system log lines.
|
|
35
|
+
:return: A formatted Text object or None if the line should be filtered out.
|
|
36
|
+
"""
|
|
37
|
+
if filter_system:
|
|
38
|
+
if logline.originator == payload_pb2.LogLineOriginator.SYSTEM:
|
|
39
|
+
return None
|
|
23
40
|
style = style_map.get(logline.originator, "")
|
|
24
|
-
if "flyte" in logline.message and "flyte.errors" not in logline.message:
|
|
41
|
+
if "[flyte]" in logline.message and "flyte.errors" not in logline.message:
|
|
42
|
+
if filter_system:
|
|
43
|
+
return None
|
|
25
44
|
style = "dim"
|
|
26
45
|
ts = ""
|
|
27
46
|
if show_ts:
|
|
@@ -34,7 +53,15 @@ class AsyncLogViewer:
|
|
|
34
53
|
A class to view logs asynchronously in the console or terminal or jupyter notebook.
|
|
35
54
|
"""
|
|
36
55
|
|
|
37
|
-
def __init__(
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
log_source: AsyncIterator,
|
|
59
|
+
max_lines: int = 30,
|
|
60
|
+
name: str = "Logs",
|
|
61
|
+
show_ts: bool = False,
|
|
62
|
+
filter_system: bool = False,
|
|
63
|
+
panel: bool = False,
|
|
64
|
+
):
|
|
38
65
|
self.console = Console()
|
|
39
66
|
self.log_source = log_source
|
|
40
67
|
self.max_lines = max_lines
|
|
@@ -42,56 +69,98 @@ class AsyncLogViewer:
|
|
|
42
69
|
self.name = name
|
|
43
70
|
self.show_ts = show_ts
|
|
44
71
|
self.total_lines = 0
|
|
72
|
+
self.filter_flyte = filter_system
|
|
73
|
+
self.panel = panel
|
|
45
74
|
|
|
46
|
-
def _render(self):
|
|
75
|
+
def _render(self) -> Panel | Text:
|
|
76
|
+
"""
|
|
77
|
+
Render the current log lines as a Panel or Text object for display.
|
|
78
|
+
"""
|
|
47
79
|
log_text = Text()
|
|
48
80
|
for line in self.lines:
|
|
49
81
|
log_text.append(line)
|
|
50
|
-
|
|
82
|
+
if self.panel:
|
|
83
|
+
return Panel(log_text, title=self.name, border_style="yellow")
|
|
84
|
+
return log_text
|
|
51
85
|
|
|
52
86
|
async def run(self):
|
|
53
|
-
|
|
87
|
+
"""
|
|
88
|
+
Run the log viewer, streaming and displaying log lines until completion.
|
|
89
|
+
"""
|
|
90
|
+
with Live(self._render(), refresh_per_second=20, console=self.console) as live:
|
|
54
91
|
try:
|
|
55
92
|
async for logline in self.log_source:
|
|
56
|
-
formatted = _format_line(logline, show_ts=self.show_ts)
|
|
57
|
-
|
|
93
|
+
formatted = _format_line(logline, show_ts=self.show_ts, filter_system=self.filter_flyte)
|
|
94
|
+
if formatted:
|
|
95
|
+
self.lines.append(formatted)
|
|
58
96
|
self.total_lines += 1
|
|
59
97
|
live.update(self._render())
|
|
60
98
|
except asyncio.CancelledError:
|
|
61
99
|
pass
|
|
100
|
+
except KeyboardInterrupt:
|
|
101
|
+
pass
|
|
102
|
+
except StopAsyncIteration:
|
|
103
|
+
self.console.print("[dim]Log stream ended.[/dim]")
|
|
104
|
+
except LogsNotYetAvailableError as e:
|
|
105
|
+
self.console.print(f"[red]Error:[/red] {e}")
|
|
106
|
+
live.update("")
|
|
62
107
|
self.console.print(f"Scrolled {self.total_lines} lines of logs.")
|
|
63
108
|
|
|
64
109
|
|
|
65
110
|
@dataclass
|
|
66
111
|
class Logs:
|
|
112
|
+
@syncify
|
|
67
113
|
@classmethod
|
|
68
|
-
@requires_client
|
|
69
|
-
@syncer.wrap
|
|
70
114
|
async def tail(
|
|
71
|
-
cls,
|
|
115
|
+
cls,
|
|
116
|
+
action_id: identifier_pb2.ActionIdentifier,
|
|
117
|
+
attempt: int = 1,
|
|
118
|
+
retry: int = 5,
|
|
72
119
|
) -> AsyncGenerator[payload_pb2.LogLine, None]:
|
|
73
120
|
"""
|
|
74
121
|
Tail the logs for a given action ID and attempt.
|
|
75
122
|
:param action_id: The action ID to tail logs for.
|
|
76
123
|
:param attempt: The attempt number (default is 0).
|
|
77
124
|
"""
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
125
|
+
ensure_client()
|
|
126
|
+
retries = 0
|
|
127
|
+
while True:
|
|
128
|
+
try:
|
|
129
|
+
resp = get_client().logs_service.TailLogs(
|
|
130
|
+
run_logs_service_pb2.TailLogsRequest(action_id=action_id, attempt=attempt)
|
|
131
|
+
)
|
|
132
|
+
async for log_set in resp:
|
|
133
|
+
if log_set.logs:
|
|
134
|
+
for log in log_set.logs:
|
|
135
|
+
for line in log.lines:
|
|
136
|
+
yield line
|
|
137
|
+
return
|
|
138
|
+
except asyncio.CancelledError:
|
|
139
|
+
return
|
|
140
|
+
except KeyboardInterrupt:
|
|
141
|
+
return
|
|
142
|
+
except StopAsyncIteration:
|
|
143
|
+
return
|
|
144
|
+
except grpc.aio.AioRpcError as e:
|
|
145
|
+
retries += 1
|
|
146
|
+
if retries >= retry:
|
|
147
|
+
if e.code() == grpc.StatusCode.NOT_FOUND:
|
|
148
|
+
raise LogsNotYetAvailableError(
|
|
149
|
+
f"Log stream not available for action {action_id.name} in run {action_id.run.name}."
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
await asyncio.sleep(2)
|
|
86
153
|
|
|
87
154
|
@classmethod
|
|
88
155
|
async def create_viewer(
|
|
89
156
|
cls,
|
|
90
|
-
action_id:
|
|
157
|
+
action_id: identifier_pb2.ActionIdentifier,
|
|
91
158
|
attempt: int = 1,
|
|
92
159
|
max_lines: int = 30,
|
|
93
160
|
show_ts: bool = False,
|
|
94
161
|
raw: bool = False,
|
|
162
|
+
filter_system: bool = False,
|
|
163
|
+
panel: bool = False,
|
|
95
164
|
):
|
|
96
165
|
"""
|
|
97
166
|
Create a log viewer for a given action ID and attempt.
|
|
@@ -101,16 +170,30 @@ class Logs:
|
|
|
101
170
|
and keep only max_lines in view.
|
|
102
171
|
:param show_ts: Whether to show timestamps in the logs.
|
|
103
172
|
:param raw: if True, return the raw log lines instead of a viewer.
|
|
173
|
+
:param filter_system: Whether to filter log lines based on system logs.
|
|
174
|
+
:param panel: Whether to use a panel for the log viewer. only applicable if raw is False.
|
|
104
175
|
"""
|
|
176
|
+
if attempt < 1:
|
|
177
|
+
raise ValueError("Attempt number must be greater than 0.")
|
|
178
|
+
|
|
179
|
+
if ipython_check():
|
|
180
|
+
if not ipywidgets_check():
|
|
181
|
+
logger.warning("IPython widgets is not available, defaulting to console output.")
|
|
182
|
+
raw = True
|
|
183
|
+
|
|
105
184
|
if raw:
|
|
106
185
|
console = Console()
|
|
107
|
-
async for line in cls.tail.aio(
|
|
108
|
-
|
|
186
|
+
async for line in cls.tail.aio(action_id=action_id, attempt=attempt):
|
|
187
|
+
line_text = _format_line(line, show_ts=show_ts, filter_system=filter_system)
|
|
188
|
+
if line_text:
|
|
189
|
+
console.print(line_text, end="")
|
|
109
190
|
return
|
|
110
191
|
viewer = AsyncLogViewer(
|
|
111
|
-
log_source=cls.tail.aio(
|
|
192
|
+
log_source=cls.tail.aio(action_id=action_id, attempt=attempt),
|
|
112
193
|
max_lines=max_lines,
|
|
113
194
|
show_ts=show_ts,
|
|
114
|
-
name=f"{action_id.run.name}:{action_id.name}",
|
|
195
|
+
name=f"{action_id.run.name}:{action_id.name} ({attempt})",
|
|
196
|
+
filter_system=filter_system,
|
|
197
|
+
panel=panel,
|
|
115
198
|
)
|
|
116
199
|
await viewer.run()
|
flyte/remote/_project.py
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import typing
|
|
4
3
|
from dataclasses import dataclass
|
|
5
|
-
from typing import
|
|
4
|
+
from typing import AsyncIterator, Iterator, Literal, Tuple, Union
|
|
6
5
|
|
|
7
6
|
import rich.repr
|
|
8
7
|
from flyteidl.admin import common_pb2, project_pb2
|
|
9
8
|
|
|
10
|
-
from flyte.
|
|
11
|
-
from flyte.
|
|
9
|
+
from flyte._initialize import ensure_client, get_client
|
|
10
|
+
from flyte.syncify import syncify
|
|
12
11
|
|
|
12
|
+
from ._common import ToJSONMixin
|
|
13
13
|
|
|
14
|
+
|
|
15
|
+
# TODO Add support for orgs again
|
|
14
16
|
@dataclass
|
|
15
|
-
class Project:
|
|
17
|
+
class Project(ToJSONMixin):
|
|
16
18
|
"""
|
|
17
19
|
A class representing a project in the Union API.
|
|
18
20
|
"""
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
pb2: project_pb2.Project
|
|
21
23
|
|
|
24
|
+
@syncify
|
|
22
25
|
@classmethod
|
|
23
|
-
@requires_client
|
|
24
|
-
@syncer.wrap
|
|
25
26
|
async def get(cls, name: str, org: str | None = None) -> Project:
|
|
26
27
|
"""
|
|
27
28
|
Get a run by its ID or name. If both are provided, the ID will take precedence.
|
|
@@ -29,23 +30,23 @@ class Project:
|
|
|
29
30
|
:param name: The name of the project.
|
|
30
31
|
:param org: The organization of the project (if applicable).
|
|
31
32
|
"""
|
|
33
|
+
ensure_client()
|
|
32
34
|
service = get_client().project_domain_service # type: ignore
|
|
33
35
|
resp = await service.GetProject(
|
|
34
36
|
project_pb2.ProjectGetRequest(
|
|
35
37
|
id=name,
|
|
36
|
-
org=org,
|
|
38
|
+
# org=org,
|
|
37
39
|
)
|
|
38
40
|
)
|
|
39
41
|
return cls(resp)
|
|
40
42
|
|
|
43
|
+
@syncify
|
|
41
44
|
@classmethod
|
|
42
|
-
@requires_client
|
|
43
|
-
@syncer.wrap
|
|
44
45
|
async def listall(
|
|
45
46
|
cls,
|
|
46
47
|
filters: str | None = None,
|
|
47
48
|
sort_by: Tuple[str, Literal["asc", "desc"]] | None = None,
|
|
48
|
-
) ->
|
|
49
|
+
) -> Union[AsyncIterator[Project], Iterator[Project]]:
|
|
49
50
|
"""
|
|
50
51
|
Get a run by its ID or name. If both are provided, the ID will take precedence.
|
|
51
52
|
|
|
@@ -53,12 +54,13 @@ class Project:
|
|
|
53
54
|
:param sort_by: The sorting criteria for the project list, in the format (field, order).
|
|
54
55
|
:return: An iterator of projects.
|
|
55
56
|
"""
|
|
57
|
+
ensure_client()
|
|
56
58
|
token = None
|
|
57
59
|
sort_by = sort_by or ("created_at", "asc")
|
|
58
60
|
sort_pb2 = common_pb2.Sort(
|
|
59
61
|
key=sort_by[0], direction=common_pb2.Sort.ASCENDING if sort_by[1] == "asc" else common_pb2.Sort.DESCENDING
|
|
60
62
|
)
|
|
61
|
-
org = get_common_config().org
|
|
63
|
+
# org = get_common_config().org
|
|
62
64
|
while True:
|
|
63
65
|
resp = await get_client().project_domain_service.ListProjects( # type: ignore
|
|
64
66
|
project_pb2.ProjectListRequest(
|
|
@@ -66,7 +68,7 @@ class Project:
|
|
|
66
68
|
token=token,
|
|
67
69
|
filters=filters,
|
|
68
70
|
sort_by=sort_pb2,
|
|
69
|
-
org=org,
|
|
71
|
+
# org=org,
|
|
70
72
|
)
|
|
71
73
|
)
|
|
72
74
|
token = resp.token
|
|
@@ -76,11 +78,11 @@ class Project:
|
|
|
76
78
|
break
|
|
77
79
|
|
|
78
80
|
def __rich_repr__(self) -> rich.repr.Result:
|
|
79
|
-
yield "name", self.
|
|
80
|
-
yield "id", self.
|
|
81
|
-
yield "description", self.
|
|
82
|
-
yield "state", project_pb2.Project.ProjectState.Name(self.
|
|
81
|
+
yield "name", self.pb2.name
|
|
82
|
+
yield "id", self.pb2.id
|
|
83
|
+
yield "description", self.pb2.description
|
|
84
|
+
yield "state", project_pb2.Project.ProjectState.Name(self.pb2.state)
|
|
83
85
|
yield (
|
|
84
86
|
"labels",
|
|
85
|
-
", ".join([f"{k}: {v}" for k, v in self.
|
|
87
|
+
", ".join([f"{k}: {v}" for k, v in self.pb2.labels.values.items()]) if self.pb2.labels else None,
|
|
86
88
|
)
|