loopix-sdk 2.30.0__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.
- loopix/__init__.py +260 -0
- loopix/api/__init__.py +287 -0
- loopix/api/client/__init__.py +8 -0
- loopix/api/client/api/__init__.py +1 -0
- loopix/api/client/api/sandboxes/__init__.py +1 -0
- loopix/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
- loopix/api/client/api/sandboxes/get_sandboxes.py +176 -0
- loopix/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +212 -0
- loopix/api/client/api/sandboxes/get_v2_sandboxes.py +230 -0
- loopix/api/client/api/sandboxes/get_v_2_sandboxes_sandbox_id_logs.py +254 -0
- loopix/api/client/api/sandboxes/post_sandboxes.py +172 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +187 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +181 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +189 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py +195 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +193 -0
- loopix/api/client/api/sandboxes/put_sandboxes_sandbox_id_network.py +199 -0
- loopix/api/client/api/snapshots/__init__.py +1 -0
- loopix/api/client/api/snapshots/get_snapshots.py +202 -0
- loopix/api/client/api/tags/__init__.py +1 -0
- loopix/api/client/api/tags/delete_templates_tags.py +174 -0
- loopix/api/client/api/tags/get_templates_template_id_tags.py +172 -0
- loopix/api/client/api/tags/post_templates_tags.py +176 -0
- loopix/api/client/api/templates/__init__.py +1 -0
- loopix/api/client/api/templates/delete_templates_template_id.py +157 -0
- loopix/api/client/api/templates/get_templates.py +172 -0
- loopix/api/client/api/templates/get_templates_aliases_alias.py +167 -0
- loopix/api/client/api/templates/get_templates_template_id.py +195 -0
- loopix/api/client/api/templates/get_templates_template_id_builds_build_id_logs.py +272 -0
- loopix/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +232 -0
- loopix/api/client/api/templates/get_templates_template_id_files_hash.py +180 -0
- loopix/api/client/api/templates/patch_templates_template_id.py +183 -0
- loopix/api/client/api/templates/patch_v_2_templates_template_id.py +185 -0
- loopix/api/client/api/templates/post_templates.py +172 -0
- loopix/api/client/api/templates/post_templates_template_id.py +181 -0
- loopix/api/client/api/templates/post_templates_template_id_builds_build_id.py +170 -0
- loopix/api/client/api/templates/post_v2_templates.py +172 -0
- loopix/api/client/api/templates/post_v3_templates.py +176 -0
- loopix/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +192 -0
- loopix/api/client/api/volumes/__init__.py +1 -0
- loopix/api/client/api/volumes/delete_volumes_volume_id.py +161 -0
- loopix/api/client/api/volumes/get_volumes.py +140 -0
- loopix/api/client/api/volumes/get_volumes_volume_id.py +163 -0
- loopix/api/client/api/volumes/post_volumes.py +172 -0
- loopix/api/client/client.py +286 -0
- loopix/api/client/errors.py +16 -0
- loopix/api/client/models/__init__.py +185 -0
- loopix/api/client/models/admin_build_cancel_result.py +67 -0
- loopix/api/client/models/admin_sandbox_kill_result.py +67 -0
- loopix/api/client/models/assign_template_tags_request.py +67 -0
- loopix/api/client/models/assigned_template_tags.py +68 -0
- loopix/api/client/models/aws_registry.py +85 -0
- loopix/api/client/models/aws_registry_type.py +8 -0
- loopix/api/client/models/build_log_entry.py +89 -0
- loopix/api/client/models/build_status_reason.py +95 -0
- loopix/api/client/models/connect_sandbox.py +59 -0
- loopix/api/client/models/created_access_token.py +100 -0
- loopix/api/client/models/created_team_api_key.py +166 -0
- loopix/api/client/models/delete_template_tags_request.py +67 -0
- loopix/api/client/models/disk_metrics.py +91 -0
- loopix/api/client/models/error.py +67 -0
- loopix/api/client/models/gcp_registry.py +69 -0
- loopix/api/client/models/gcp_registry_type.py +8 -0
- loopix/api/client/models/general_registry.py +77 -0
- loopix/api/client/models/general_registry_type.py +8 -0
- loopix/api/client/models/identifier_masking_details.py +83 -0
- loopix/api/client/models/listed_sandbox.py +179 -0
- loopix/api/client/models/log_level.py +11 -0
- loopix/api/client/models/logs_direction.py +9 -0
- loopix/api/client/models/logs_source.py +9 -0
- loopix/api/client/models/machine_info.py +83 -0
- loopix/api/client/models/max_team_metric.py +78 -0
- loopix/api/client/models/mcp_type_0.py +44 -0
- loopix/api/client/models/new_access_token.py +59 -0
- loopix/api/client/models/new_sandbox.py +224 -0
- loopix/api/client/models/new_team_api_key.py +59 -0
- loopix/api/client/models/new_volume.py +59 -0
- loopix/api/client/models/node.py +160 -0
- loopix/api/client/models/node_detail.py +160 -0
- loopix/api/client/models/node_metrics.py +122 -0
- loopix/api/client/models/node_status.py +12 -0
- loopix/api/client/models/node_status_change.py +82 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_snapshots_body.py +60 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
- loopix/api/client/models/resumed_sandbox.py +68 -0
- loopix/api/client/models/sandbox.py +145 -0
- loopix/api/client/models/sandbox_auto_resume_config.py +60 -0
- loopix/api/client/models/sandbox_detail.py +267 -0
- loopix/api/client/models/sandbox_lifecycle.py +70 -0
- loopix/api/client/models/sandbox_log.py +70 -0
- loopix/api/client/models/sandbox_log_entry.py +93 -0
- loopix/api/client/models/sandbox_log_entry_fields.py +44 -0
- loopix/api/client/models/sandbox_logs.py +91 -0
- loopix/api/client/models/sandbox_logs_v2_response.py +73 -0
- loopix/api/client/models/sandbox_metric.py +126 -0
- loopix/api/client/models/sandbox_network_config.py +118 -0
- loopix/api/client/models/sandbox_network_config_rules.py +72 -0
- loopix/api/client/models/sandbox_network_rule.py +74 -0
- loopix/api/client/models/sandbox_network_transform.py +79 -0
- loopix/api/client/models/sandbox_network_transform_headers.py +47 -0
- loopix/api/client/models/sandbox_network_update_config.py +114 -0
- loopix/api/client/models/sandbox_network_update_config_rules.py +71 -0
- loopix/api/client/models/sandbox_on_timeout.py +9 -0
- loopix/api/client/models/sandbox_pause_request.py +62 -0
- loopix/api/client/models/sandbox_state.py +9 -0
- loopix/api/client/models/sandbox_volume_mount.py +67 -0
- loopix/api/client/models/sandboxes_with_metrics.py +59 -0
- loopix/api/client/models/snapshot_info.py +70 -0
- loopix/api/client/models/team.py +83 -0
- loopix/api/client/models/team_api_key.py +158 -0
- loopix/api/client/models/team_metric.py +86 -0
- loopix/api/client/models/team_user.py +75 -0
- loopix/api/client/models/template.py +225 -0
- loopix/api/client/models/template_alias_response.py +67 -0
- loopix/api/client/models/template_build.py +139 -0
- loopix/api/client/models/template_build_file_upload.py +70 -0
- loopix/api/client/models/template_build_info.py +126 -0
- loopix/api/client/models/template_build_logs_response.py +73 -0
- loopix/api/client/models/template_build_request.py +115 -0
- loopix/api/client/models/template_build_request_v2.py +88 -0
- loopix/api/client/models/template_build_request_v3.py +107 -0
- loopix/api/client/models/template_build_start_v2.py +184 -0
- loopix/api/client/models/template_build_status.py +11 -0
- loopix/api/client/models/template_legacy.py +207 -0
- loopix/api/client/models/template_request_response_v3.py +99 -0
- loopix/api/client/models/template_step.py +91 -0
- loopix/api/client/models/template_tag.py +78 -0
- loopix/api/client/models/template_update_request.py +59 -0
- loopix/api/client/models/template_update_response.py +59 -0
- loopix/api/client/models/template_with_builds.py +156 -0
- loopix/api/client/models/update_team_api_key.py +59 -0
- loopix/api/client/models/volume.py +67 -0
- loopix/api/client/models/volume_and_token.py +75 -0
- loopix/api/client/models/volume_token.py +59 -0
- loopix/api/client/py.typed +1 -0
- loopix/api/client/types.py +54 -0
- loopix/api/client_async/__init__.py +74 -0
- loopix/api/client_sync/__init__.py +73 -0
- loopix/api/metadata.py +14 -0
- loopix/connection_config.py +309 -0
- loopix/envd/api.py +170 -0
- loopix/envd/filesystem/filesystem_connect.py +193 -0
- loopix/envd/filesystem/filesystem_pb2.py +80 -0
- loopix/envd/filesystem/filesystem_pb2.pyi +272 -0
- loopix/envd/process/process_connect.py +174 -0
- loopix/envd/process/process_pb2.py +96 -0
- loopix/envd/process/process_pb2.pyi +316 -0
- loopix/envd/rpc.py +139 -0
- loopix/envd/versions.py +11 -0
- loopix/exceptions.py +133 -0
- loopix/io_utils.py +57 -0
- loopix/paginator.py +52 -0
- loopix/py.typed +0 -0
- loopix/sandbox/_git/__init__.py +85 -0
- loopix/sandbox/_git/args.py +363 -0
- loopix/sandbox/_git/auth.py +132 -0
- loopix/sandbox/_git/config.py +32 -0
- loopix/sandbox/_git/parse.py +222 -0
- loopix/sandbox/_git/types.py +149 -0
- loopix/sandbox/commands/command_handle.py +69 -0
- loopix/sandbox/commands/main.py +39 -0
- loopix/sandbox/filesystem/filesystem.py +337 -0
- loopix/sandbox/filesystem/watch_handle.py +70 -0
- loopix/sandbox/main.py +227 -0
- loopix/sandbox/mcp.py +1949 -0
- loopix/sandbox/network.py +8 -0
- loopix/sandbox/sandbox_api.py +624 -0
- loopix/sandbox/signature.py +47 -0
- loopix/sandbox/utils.py +34 -0
- loopix/sandbox_async/commands/command.py +396 -0
- loopix/sandbox_async/commands/command_handle.py +298 -0
- loopix/sandbox_async/commands/pty.py +257 -0
- loopix/sandbox_async/filesystem/filesystem.py +720 -0
- loopix/sandbox_async/filesystem/watch_handle.py +97 -0
- loopix/sandbox_async/git.py +1100 -0
- loopix/sandbox_async/main.py +987 -0
- loopix/sandbox_async/paginator.py +140 -0
- loopix/sandbox_async/sandbox_api.py +504 -0
- loopix/sandbox_async/utils.py +7 -0
- loopix/sandbox_domains.py +5 -0
- loopix/sandbox_sync/commands/command.py +420 -0
- loopix/sandbox_sync/commands/command_handle.py +239 -0
- loopix/sandbox_sync/commands/pty.py +279 -0
- loopix/sandbox_sync/filesystem/filesystem.py +710 -0
- loopix/sandbox_sync/filesystem/watch_handle.py +102 -0
- loopix/sandbox_sync/git.py +1077 -0
- loopix/sandbox_sync/main.py +975 -0
- loopix/sandbox_sync/paginator.py +140 -0
- loopix/sandbox_sync/sandbox_api.py +491 -0
- loopix/template/consts.py +45 -0
- loopix/template/dockerfile_parser.py +286 -0
- loopix/template/logger.py +232 -0
- loopix/template/main.py +1368 -0
- loopix/template/readycmd.py +144 -0
- loopix/template/types.py +194 -0
- loopix/template/utils.py +426 -0
- loopix/template_async/build_api.py +419 -0
- loopix/template_async/main.py +528 -0
- loopix/template_sync/build_api.py +409 -0
- loopix/template_sync/main.py +529 -0
- loopix/volume/client/__init__.py +8 -0
- loopix/volume/client/api/__init__.py +1 -0
- loopix/volume/client/api/volumes/__init__.py +1 -0
- loopix/volume/client/api/volumes/delete_volumecontent_volume_id_path.py +174 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_dir.py +204 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_file.py +179 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_path.py +176 -0
- loopix/volume/client/api/volumes/patch_volumecontent_volume_id_path.py +203 -0
- loopix/volume/client/api/volumes/post_volumecontent_volume_id_dir.py +239 -0
- loopix/volume/client/api/volumes/put_volumecontent_volume_id_file.py +259 -0
- loopix/volume/client/client.py +286 -0
- loopix/volume/client/errors.py +16 -0
- loopix/volume/client/models/__init__.py +13 -0
- loopix/volume/client/models/error.py +67 -0
- loopix/volume/client/models/patch_volumecontent_volume_id_path_body.py +77 -0
- loopix/volume/client/models/volume_entry_stat.py +145 -0
- loopix/volume/client/models/volume_entry_stat_type.py +11 -0
- loopix/volume/client/py.typed +1 -0
- loopix/volume/client/types.py +54 -0
- loopix/volume/client_async/__init__.py +88 -0
- loopix/volume/client_sync/__init__.py +80 -0
- loopix/volume/connection_config.py +145 -0
- loopix/volume/types.py +62 -0
- loopix/volume/utils.py +52 -0
- loopix/volume/volume_async.py +639 -0
- loopix/volume/volume_sync.py +639 -0
- loopix_connect/__init__.py +1 -0
- loopix_connect/client.py +534 -0
- loopix_connect/py.typed +0 -0
- loopix_sdk-2.30.0.dist-info/METADATA +98 -0
- loopix_sdk-2.30.0.dist-info/RECORD +238 -0
- loopix_sdk-2.30.0.dist-info/WHEEL +4 -0
- loopix_sdk-2.30.0.dist-info/licenses/LICENSE +9 -0
loopix/envd/rpc.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
|
|
3
|
+
import httpcore
|
|
4
|
+
from typing import Awaitable, Callable, Optional
|
|
5
|
+
from packaging.version import Version
|
|
6
|
+
from loopix_connect.client import Code, ConnectException
|
|
7
|
+
|
|
8
|
+
from loopix.exceptions import (
|
|
9
|
+
SandboxException,
|
|
10
|
+
InvalidArgumentException,
|
|
11
|
+
NotFoundException,
|
|
12
|
+
TimeoutException,
|
|
13
|
+
format_sandbox_timeout_exception,
|
|
14
|
+
AuthenticationException,
|
|
15
|
+
RateLimitException,
|
|
16
|
+
)
|
|
17
|
+
from loopix.connection_config import Username, default_username
|
|
18
|
+
from loopix.envd.versions import ENVD_DEFAULT_USER
|
|
19
|
+
|
|
20
|
+
_DEFAULT_RPC_ERROR_MAP: dict[Code, Callable[[str], Exception]] = {
|
|
21
|
+
Code.invalid_argument: InvalidArgumentException,
|
|
22
|
+
Code.unauthenticated: AuthenticationException,
|
|
23
|
+
Code.not_found: NotFoundException,
|
|
24
|
+
Code.unavailable: format_sandbox_timeout_exception,
|
|
25
|
+
Code.resource_exhausted: lambda message: RateLimitException(
|
|
26
|
+
f"{message}: Rate limit exceeded, please try again later."
|
|
27
|
+
),
|
|
28
|
+
Code.canceled: lambda message: TimeoutException(
|
|
29
|
+
f"{message}: This error is likely due to exceeding 'request_timeout'. You can pass the request timeout value as an option when making the request."
|
|
30
|
+
),
|
|
31
|
+
Code.deadline_exceeded: lambda message: TimeoutException(
|
|
32
|
+
f"{message}: This error is likely due to exceeding 'timeout' — the total time a long running request (like process or directory watch) can be active. It can be modified by passing 'timeout' when making the request. Use '0' to disable the timeout."
|
|
33
|
+
),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def format_terminated_exception(
|
|
38
|
+
e: Exception,
|
|
39
|
+
sandbox_running: Optional[bool],
|
|
40
|
+
) -> Exception:
|
|
41
|
+
"""Handle an exception for a connection to the sandbox dropped mid-request: when a
|
|
42
|
+
sandbox health probe confirmed the sandbox is gone (``sandbox_running is False``),
|
|
43
|
+
return a ``TimeoutException``; otherwise return the original error unchanged."""
|
|
44
|
+
if sandbox_running is False:
|
|
45
|
+
return TimeoutException(
|
|
46
|
+
f"{e}: The sandbox was killed or reached its end of life while the request was in flight."
|
|
47
|
+
)
|
|
48
|
+
return e
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def handle_rpc_exception(
|
|
52
|
+
e: Exception,
|
|
53
|
+
error_map: Optional[dict[Code, Callable[[str], Exception]]] = None,
|
|
54
|
+
sandbox_running: Optional[bool] = None,
|
|
55
|
+
):
|
|
56
|
+
"""Handle errors from envd RPC calls by mapping gRPC status codes to specific exception types.
|
|
57
|
+
|
|
58
|
+
:param e: The caught exception, expected to be a ``ConnectException`` or a transport-level ``httpcore`` error.
|
|
59
|
+
:param error_map: Optional map of gRPC codes to exception factories that override the defaults.
|
|
60
|
+
:param sandbox_running: Result of a sandbox health probe (``None`` when unknown), used to disambiguate a connection dropped mid-request.
|
|
61
|
+
:return: The corresponding exception. A connection dropped mid-request with the sandbox confirmed gone becomes a ``TimeoutException``; non-``ConnectException`` errors are otherwise returned as-is.
|
|
62
|
+
"""
|
|
63
|
+
if isinstance(e, ConnectException):
|
|
64
|
+
if error_map and e.status in error_map:
|
|
65
|
+
return error_map[e.status](e.message)
|
|
66
|
+
|
|
67
|
+
if e.status in _DEFAULT_RPC_ERROR_MAP:
|
|
68
|
+
return _DEFAULT_RPC_ERROR_MAP[e.status](e.message)
|
|
69
|
+
|
|
70
|
+
return SandboxException(f"{e.status}: {e.message}")
|
|
71
|
+
|
|
72
|
+
# A remote protocol error (e.g. an HTTP/2 stream reset) means the connection to the
|
|
73
|
+
# sandbox was dropped mid-request — either the sandbox died or the network failed
|
|
74
|
+
if isinstance(e, httpcore.RemoteProtocolError):
|
|
75
|
+
return format_terminated_exception(e, sandbox_running)
|
|
76
|
+
|
|
77
|
+
# A transport-level timeout from httpcore means a configured timeout was exceeded
|
|
78
|
+
# before the server responded: `request_timeout` on a unary call's read phase, or
|
|
79
|
+
# `connect`/`pool`/`write` on a stream's setup/send phase. Streams have no read
|
|
80
|
+
# timeout — the command `timeout` is enforced server-side and surfaces as a
|
|
81
|
+
# `deadline_exceeded` ConnectException instead. Unlike the JS SDK, where the
|
|
82
|
+
# request timeout is an `AbortSignal` that connect normalizes into a `Code.canceled`
|
|
83
|
+
# ConnectError, httpcore raises this raw transport error outside the ConnectException
|
|
84
|
+
# path, so we map it here to a `TimeoutException` for a consistent timeout error.
|
|
85
|
+
if isinstance(e, httpcore.TimeoutException):
|
|
86
|
+
return TimeoutException(
|
|
87
|
+
f"{e}: This error is likely due to exceeding 'timeout' — the total time a long running request (like process or directory watch) can be active — or 'request_timeout'. You can modify these by passing 'timeout' or 'request_timeout' when making the request. Use '0' to disable the timeout."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return e
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def handle_rpc_exception_with_health(
|
|
94
|
+
e: Exception,
|
|
95
|
+
check_health: Optional[Callable[[], Optional[bool]]] = None,
|
|
96
|
+
error_map: Optional[dict[Code, Callable[[str], Exception]]] = None,
|
|
97
|
+
):
|
|
98
|
+
"""Like :func:`handle_rpc_exception`, but when the connection to the sandbox was
|
|
99
|
+
dropped mid-request it probes the sandbox health to tell apart the sandbox being
|
|
100
|
+
killed from a transient network failure (e.g. a load balancer dropping the connection).
|
|
101
|
+
"""
|
|
102
|
+
sandbox_running = None
|
|
103
|
+
if check_health is not None and isinstance(e, httpcore.RemoteProtocolError):
|
|
104
|
+
try:
|
|
105
|
+
sandbox_running = check_health()
|
|
106
|
+
except Exception:
|
|
107
|
+
sandbox_running = None
|
|
108
|
+
return handle_rpc_exception(e, error_map, sandbox_running)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def ahandle_rpc_exception_with_health(
|
|
112
|
+
e: Exception,
|
|
113
|
+
check_health: Optional[Callable[[], Awaitable[Optional[bool]]]] = None,
|
|
114
|
+
error_map: Optional[dict[Code, Callable[[str], Exception]]] = None,
|
|
115
|
+
):
|
|
116
|
+
"""Async version of :func:`handle_rpc_exception_with_health`."""
|
|
117
|
+
sandbox_running = None
|
|
118
|
+
if check_health is not None and isinstance(e, httpcore.RemoteProtocolError):
|
|
119
|
+
try:
|
|
120
|
+
sandbox_running = await check_health()
|
|
121
|
+
except Exception:
|
|
122
|
+
sandbox_running = None
|
|
123
|
+
return handle_rpc_exception(e, error_map, sandbox_running)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def authentication_header(
|
|
127
|
+
envd_version: Version, user: Optional[Username] = None
|
|
128
|
+
) -> dict[str, str]:
|
|
129
|
+
if user is None and envd_version < ENVD_DEFAULT_USER:
|
|
130
|
+
user = default_username
|
|
131
|
+
|
|
132
|
+
if not user:
|
|
133
|
+
return {}
|
|
134
|
+
|
|
135
|
+
value = f"{user}:"
|
|
136
|
+
|
|
137
|
+
encoded = base64.b64encode(value.encode("utf-8")).decode("utf-8")
|
|
138
|
+
|
|
139
|
+
return {"Authorization": f"Basic {encoded}"}
|
loopix/envd/versions.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from packaging.version import Version
|
|
2
|
+
|
|
3
|
+
ENVD_VERSION_RECURSIVE_WATCH = Version("0.1.4")
|
|
4
|
+
ENVD_DEBUG_FALLBACK = Version("99.99.99")
|
|
5
|
+
ENVD_COMMANDS_STDIN = Version("0.3.0")
|
|
6
|
+
ENVD_DEFAULT_USER = Version("0.4.0")
|
|
7
|
+
ENVD_ENVD_CLOSE = Version("0.5.2")
|
|
8
|
+
ENVD_OCTET_STREAM_UPLOAD = Version("0.5.7")
|
|
9
|
+
ENVD_FILE_METADATA = Version("0.6.2")
|
|
10
|
+
ENVD_VERSION_FS_EVENT_ENTRY_INFO = Version("0.6.3")
|
|
11
|
+
ENVD_VERSION_WATCH_NETWORK_MOUNTS = Version("0.6.4")
|
loopix/exceptions.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
def format_sandbox_timeout_exception(message: str):
|
|
2
|
+
return TimeoutException(
|
|
3
|
+
f"{message}: This error is likely due to sandbox timeout. You can modify the sandbox timeout by passing 'timeout' when starting the sandbox or calling '.set_timeout' on the sandbox with the desired timeout."
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def format_request_timeout_error() -> Exception:
|
|
8
|
+
return TimeoutException(
|
|
9
|
+
"Request timed out — the 'request_timeout' option can be used to increase this timeout",
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SandboxException(Exception):
|
|
14
|
+
"""
|
|
15
|
+
Base class for all sandbox errors.
|
|
16
|
+
|
|
17
|
+
Raised when a general sandbox exception occurs.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TimeoutException(SandboxException):
|
|
24
|
+
"""
|
|
25
|
+
Raised when a timeout occurs.
|
|
26
|
+
|
|
27
|
+
The `unavailable` exception type is caused by sandbox timeout.\n
|
|
28
|
+
The `canceled` exception type is caused by exceeding request timeout.\n
|
|
29
|
+
The `deadline_exceeded` exception type is caused by exceeding the timeout for process, watch, etc.\n
|
|
30
|
+
The `unknown` exception type is sometimes caused by the sandbox timeout when the request is not processed correctly.\n
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class InvalidArgumentException(SandboxException):
|
|
37
|
+
"""
|
|
38
|
+
Raised when an invalid argument is provided.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NotEnoughSpaceException(SandboxException):
|
|
45
|
+
"""
|
|
46
|
+
Raised when there is not enough disk space.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class NotFoundException(SandboxException):
|
|
53
|
+
"""
|
|
54
|
+
Raised when a resource is not found.
|
|
55
|
+
|
|
56
|
+
.. deprecated::
|
|
57
|
+
Use :class:`FileNotFoundException` or :class:`SandboxNotFoundException` instead.
|
|
58
|
+
This class will be removed in the next major version.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class FileNotFoundException(NotFoundException):
|
|
65
|
+
"""
|
|
66
|
+
Raised when a file or directory is not found inside a sandbox.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class SandboxNotFoundException(NotFoundException):
|
|
73
|
+
"""
|
|
74
|
+
Raised when a sandbox is not found (e.g. it doesn't exist or is no longer running).
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AuthenticationException(Exception):
|
|
81
|
+
"""
|
|
82
|
+
Raised when authentication fails.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class GitAuthException(AuthenticationException):
|
|
89
|
+
"""
|
|
90
|
+
Raised when git authentication fails.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class GitUpstreamException(SandboxException):
|
|
97
|
+
"""
|
|
98
|
+
Raised when git upstream tracking is missing.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TemplateException(SandboxException):
|
|
105
|
+
"""
|
|
106
|
+
Exception raised when the template uses old envd version. It isn't compatible with the new SDK.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class RateLimitException(SandboxException):
|
|
111
|
+
"""
|
|
112
|
+
Raised when the API rate limit is exceeded.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class BuildException(Exception):
|
|
117
|
+
"""
|
|
118
|
+
Raised when the build fails.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class FileUploadException(BuildException):
|
|
123
|
+
"""
|
|
124
|
+
Raised when the file upload fails.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class VolumeException(Exception):
|
|
129
|
+
"""
|
|
130
|
+
Base class for all volume errors.
|
|
131
|
+
|
|
132
|
+
Raised when general volume errors occur.
|
|
133
|
+
"""
|
loopix/io_utils.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import zlib
|
|
3
|
+
from typing import IO, AsyncIterable, AsyncIterator, Iterable, Iterator
|
|
4
|
+
|
|
5
|
+
IO_CHUNK_SIZE = 65_536
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def iter_io_chunks(data: IO) -> Iterator[bytes]:
|
|
9
|
+
"""Read a file-like object in chunks, encoding text chunks to UTF-8."""
|
|
10
|
+
while True:
|
|
11
|
+
chunk = data.read(IO_CHUNK_SIZE)
|
|
12
|
+
if not chunk:
|
|
13
|
+
break
|
|
14
|
+
yield chunk if isinstance(chunk, bytes) else chunk.encode("utf-8")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def aiter_io_chunks(data: IO) -> AsyncIterator[bytes]:
|
|
18
|
+
"""Read a file-like object in chunks, encoding text chunks to UTF-8.
|
|
19
|
+
|
|
20
|
+
`data.read` is a synchronous (potentially disk-blocking) call, so it runs in
|
|
21
|
+
a worker thread to avoid stalling the event loop during large uploads.
|
|
22
|
+
"""
|
|
23
|
+
while True:
|
|
24
|
+
chunk = await asyncio.to_thread(data.read, IO_CHUNK_SIZE)
|
|
25
|
+
if not chunk:
|
|
26
|
+
break
|
|
27
|
+
yield chunk if isinstance(chunk, bytes) else chunk.encode("utf-8")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _gzip_compressor():
|
|
31
|
+
# wbits > 16 makes zlib produce a gzip-formatted stream.
|
|
32
|
+
return zlib.compressobj(wbits=zlib.MAX_WBITS | 16)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def gzip_iter(chunks: Iterable[bytes]) -> Iterator[bytes]:
|
|
36
|
+
"""Gzip-compress a byte stream chunk by chunk."""
|
|
37
|
+
compressor = _gzip_compressor()
|
|
38
|
+
for chunk in chunks:
|
|
39
|
+
compressed = compressor.compress(chunk)
|
|
40
|
+
if compressed:
|
|
41
|
+
yield compressed
|
|
42
|
+
yield compressor.flush()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def agzip_iter(chunks: AsyncIterable[bytes]) -> AsyncIterator[bytes]:
|
|
46
|
+
"""Gzip-compress a byte stream chunk by chunk.
|
|
47
|
+
|
|
48
|
+
Compression is CPU-bound, so it runs in a worker thread to avoid stalling
|
|
49
|
+
the event loop during large uploads (zlib releases the GIL while
|
|
50
|
+
compressing, so the offload genuinely overlaps with the loop).
|
|
51
|
+
"""
|
|
52
|
+
compressor = _gzip_compressor()
|
|
53
|
+
async for chunk in chunks:
|
|
54
|
+
compressed = await asyncio.to_thread(compressor.compress, chunk)
|
|
55
|
+
if compressed:
|
|
56
|
+
yield compressed
|
|
57
|
+
yield await asyncio.to_thread(compressor.flush)
|
loopix/paginator.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Generic, Mapping, Optional, TypeVar
|
|
2
|
+
|
|
3
|
+
from typing_extensions import Unpack
|
|
4
|
+
|
|
5
|
+
from loopix.connection_config import ApiParams
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
OptsT = TypeVar("OptsT", bound=ApiParams)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PaginatorBase(Generic[T, OptsT]):
|
|
12
|
+
"""
|
|
13
|
+
Shared pagination state for cursor-based list endpoints.
|
|
14
|
+
|
|
15
|
+
Owns the `has_next` / `next_token` state and the reading of the
|
|
16
|
+
`x-next-token` response header (via `_update_pagination`). Each concrete
|
|
17
|
+
paginator implements `next_items` to do the actual fetching for its
|
|
18
|
+
endpoint, so any model can expose pagination by subclassing this without
|
|
19
|
+
reimplementing the bookkeeping.
|
|
20
|
+
|
|
21
|
+
`T` is the item type returned by `next_items`; `OptsT` is the connection
|
|
22
|
+
options type accepted by the paginator (an `ApiParams`-compatible TypedDict).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
limit: Optional[int] = None,
|
|
28
|
+
next_token: Optional[str] = None,
|
|
29
|
+
**opts: Unpack[OptsT],
|
|
30
|
+
):
|
|
31
|
+
self._opts: OptsT = opts
|
|
32
|
+
self.limit = limit
|
|
33
|
+
self._has_next = True
|
|
34
|
+
self._next_token = next_token
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def has_next(self) -> bool:
|
|
38
|
+
"""
|
|
39
|
+
Returns True if there are more items to fetch.
|
|
40
|
+
"""
|
|
41
|
+
return self._has_next
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def next_token(self) -> Optional[str]:
|
|
45
|
+
"""
|
|
46
|
+
Returns the next token to use for pagination.
|
|
47
|
+
"""
|
|
48
|
+
return self._next_token
|
|
49
|
+
|
|
50
|
+
def _update_pagination(self, headers: Mapping[str, str]) -> None:
|
|
51
|
+
self._next_token = headers.get("x-next-token")
|
|
52
|
+
self._has_next = bool(self._next_token)
|
loopix/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from loopix.sandbox._git.args import (
|
|
2
|
+
build_add_args,
|
|
3
|
+
build_branches_args,
|
|
4
|
+
build_checkout_branch_args,
|
|
5
|
+
build_clone_plan,
|
|
6
|
+
build_commit_args,
|
|
7
|
+
build_credential_approve_command,
|
|
8
|
+
build_create_branch_args,
|
|
9
|
+
build_delete_branch_args,
|
|
10
|
+
build_git_command,
|
|
11
|
+
build_has_upstream_args,
|
|
12
|
+
build_pull_args,
|
|
13
|
+
build_push_args,
|
|
14
|
+
build_remote_add_args,
|
|
15
|
+
build_remote_add_shell_command,
|
|
16
|
+
build_remote_get_command,
|
|
17
|
+
build_remote_get_url_args,
|
|
18
|
+
build_remote_set_url_args,
|
|
19
|
+
build_reset_args,
|
|
20
|
+
build_restore_args,
|
|
21
|
+
build_status_args,
|
|
22
|
+
shell_escape,
|
|
23
|
+
)
|
|
24
|
+
from loopix.sandbox._git.auth import (
|
|
25
|
+
build_auth_error_message,
|
|
26
|
+
build_upstream_error_message,
|
|
27
|
+
is_auth_failure,
|
|
28
|
+
is_missing_upstream,
|
|
29
|
+
strip_credentials,
|
|
30
|
+
with_credentials,
|
|
31
|
+
)
|
|
32
|
+
from loopix.sandbox._git.config import resolve_config_scope
|
|
33
|
+
from loopix.sandbox._git.parse import (
|
|
34
|
+
derive_repo_dir_from_url,
|
|
35
|
+
parse_git_branches,
|
|
36
|
+
parse_git_status,
|
|
37
|
+
parse_remote_url,
|
|
38
|
+
)
|
|
39
|
+
from loopix.sandbox._git.types import (
|
|
40
|
+
ClonePlan,
|
|
41
|
+
GitBranches,
|
|
42
|
+
GitFileStatus,
|
|
43
|
+
GitResetMode,
|
|
44
|
+
GitStatus,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"build_add_args",
|
|
49
|
+
"build_auth_error_message",
|
|
50
|
+
"build_branches_args",
|
|
51
|
+
"build_checkout_branch_args",
|
|
52
|
+
"build_clone_plan",
|
|
53
|
+
"build_commit_args",
|
|
54
|
+
"build_credential_approve_command",
|
|
55
|
+
"build_create_branch_args",
|
|
56
|
+
"build_delete_branch_args",
|
|
57
|
+
"build_git_command",
|
|
58
|
+
"build_has_upstream_args",
|
|
59
|
+
"build_pull_args",
|
|
60
|
+
"build_push_args",
|
|
61
|
+
"build_remote_add_args",
|
|
62
|
+
"build_remote_add_shell_command",
|
|
63
|
+
"build_remote_get_command",
|
|
64
|
+
"build_remote_get_url_args",
|
|
65
|
+
"build_remote_set_url_args",
|
|
66
|
+
"build_reset_args",
|
|
67
|
+
"build_restore_args",
|
|
68
|
+
"build_status_args",
|
|
69
|
+
"build_upstream_error_message",
|
|
70
|
+
"derive_repo_dir_from_url",
|
|
71
|
+
"is_auth_failure",
|
|
72
|
+
"is_missing_upstream",
|
|
73
|
+
"parse_git_branches",
|
|
74
|
+
"parse_git_status",
|
|
75
|
+
"parse_remote_url",
|
|
76
|
+
"resolve_config_scope",
|
|
77
|
+
"shell_escape",
|
|
78
|
+
"strip_credentials",
|
|
79
|
+
"with_credentials",
|
|
80
|
+
"ClonePlan",
|
|
81
|
+
"GitBranches",
|
|
82
|
+
"GitFileStatus",
|
|
83
|
+
"GitResetMode",
|
|
84
|
+
"GitStatus",
|
|
85
|
+
]
|