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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import weakref
|
|
3
|
+
from typing import Dict, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from httpx._types import ProxyTypes
|
|
8
|
+
|
|
9
|
+
from loopix.api import AsyncApiClient, connection_retries, limits
|
|
10
|
+
from loopix.connection_config import ConnectionConfig
|
|
11
|
+
|
|
12
|
+
TransportKey = Tuple[bool, Optional[ProxyTypes]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_api_client(config: ConnectionConfig, **kwargs) -> AsyncApiClient:
|
|
16
|
+
return AsyncApiClient(
|
|
17
|
+
config,
|
|
18
|
+
async_transport_factory=lambda: get_transport(config),
|
|
19
|
+
**kwargs,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AsyncTransportWithLogger(httpx.AsyncHTTPTransport):
|
|
24
|
+
# Keyed weakly by the event loop object itself, not id(loop) — CPython
|
|
25
|
+
# reuses object ids, so a new loop could otherwise inherit a transport
|
|
26
|
+
# bound to a previous, closed loop.
|
|
27
|
+
_instances: weakref.WeakKeyDictionary[
|
|
28
|
+
asyncio.AbstractEventLoop,
|
|
29
|
+
Dict[TransportKey, "AsyncTransportWithLogger"],
|
|
30
|
+
] = weakref.WeakKeyDictionary()
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def pool(self):
|
|
34
|
+
return self._pool
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_cached_transport(cls, config: ConnectionConfig, http2: bool):
|
|
38
|
+
loop = asyncio.get_running_loop()
|
|
39
|
+
loop_instances = cls._instances.get(loop)
|
|
40
|
+
if loop_instances is None:
|
|
41
|
+
loop_instances = {}
|
|
42
|
+
cls._instances[loop] = loop_instances
|
|
43
|
+
|
|
44
|
+
key: TransportKey = (http2, config.proxy)
|
|
45
|
+
transport = loop_instances.get(key)
|
|
46
|
+
if transport is None:
|
|
47
|
+
transport = cls(
|
|
48
|
+
limits=limits,
|
|
49
|
+
proxy=config.proxy,
|
|
50
|
+
http2=http2,
|
|
51
|
+
retries=connection_retries,
|
|
52
|
+
)
|
|
53
|
+
loop_instances[key] = transport
|
|
54
|
+
|
|
55
|
+
return transport
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_transport(
|
|
59
|
+
config: ConnectionConfig, http2: bool = True
|
|
60
|
+
) -> AsyncTransportWithLogger:
|
|
61
|
+
return _get_cached_transport(AsyncTransportWithLogger, config, http2)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AsyncEnvdTransportWithLogger(AsyncTransportWithLogger):
|
|
65
|
+
_instances: weakref.WeakKeyDictionary[
|
|
66
|
+
asyncio.AbstractEventLoop,
|
|
67
|
+
Dict[TransportKey, "AsyncEnvdTransportWithLogger"],
|
|
68
|
+
] = weakref.WeakKeyDictionary()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_envd_transport(
|
|
72
|
+
config: ConnectionConfig, http2: bool = True
|
|
73
|
+
) -> AsyncEnvdTransportWithLogger:
|
|
74
|
+
return _get_cached_transport(AsyncEnvdTransportWithLogger, config, http2)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import Dict, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
from httpx._types import ProxyTypes
|
|
7
|
+
|
|
8
|
+
from loopix.api import ApiClient, connection_retries, limits
|
|
9
|
+
from loopix.connection_config import ConnectionConfig
|
|
10
|
+
|
|
11
|
+
TransportKey = Tuple[bool, Optional[ProxyTypes]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_api_client(config: ConnectionConfig, **kwargs) -> ApiClient:
|
|
15
|
+
return ApiClient(
|
|
16
|
+
config,
|
|
17
|
+
transport_factory=lambda: get_transport(config),
|
|
18
|
+
**kwargs,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TransportWithLogger(httpx.HTTPTransport):
|
|
23
|
+
_thread_local = threading.local()
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def pool(self):
|
|
27
|
+
return self._pool
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_transport(config: ConnectionConfig, http2: bool = True) -> TransportWithLogger:
|
|
31
|
+
instances: Dict[TransportKey, TransportWithLogger] = getattr(
|
|
32
|
+
TransportWithLogger._thread_local, "instances", {}
|
|
33
|
+
)
|
|
34
|
+
key: TransportKey = (http2, config.proxy)
|
|
35
|
+
cached = instances.get(key)
|
|
36
|
+
if cached is not None:
|
|
37
|
+
return cached
|
|
38
|
+
|
|
39
|
+
transport = TransportWithLogger(
|
|
40
|
+
limits=limits,
|
|
41
|
+
proxy=config.proxy,
|
|
42
|
+
http2=http2,
|
|
43
|
+
retries=connection_retries,
|
|
44
|
+
)
|
|
45
|
+
instances[key] = transport
|
|
46
|
+
TransportWithLogger._thread_local.instances = instances
|
|
47
|
+
return transport
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class EnvdTransportWithLogger(TransportWithLogger):
|
|
51
|
+
_thread_local = threading.local()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_envd_transport(
|
|
55
|
+
config: ConnectionConfig, http2: bool = True
|
|
56
|
+
) -> EnvdTransportWithLogger:
|
|
57
|
+
instances: Dict[TransportKey, EnvdTransportWithLogger] = getattr(
|
|
58
|
+
EnvdTransportWithLogger._thread_local, "instances", {}
|
|
59
|
+
)
|
|
60
|
+
key: TransportKey = (http2, config.proxy)
|
|
61
|
+
cached = instances.get(key)
|
|
62
|
+
if cached is not None:
|
|
63
|
+
return cached
|
|
64
|
+
|
|
65
|
+
transport = EnvdTransportWithLogger(
|
|
66
|
+
limits=limits,
|
|
67
|
+
proxy=config.proxy,
|
|
68
|
+
http2=http2,
|
|
69
|
+
retries=connection_retries,
|
|
70
|
+
)
|
|
71
|
+
instances[key] = transport
|
|
72
|
+
EnvdTransportWithLogger._thread_local.instances = instances
|
|
73
|
+
return transport
|
loopix/api/metadata.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
|
|
3
|
+
from importlib import metadata
|
|
4
|
+
|
|
5
|
+
package_version = metadata.version("loopix")
|
|
6
|
+
|
|
7
|
+
default_headers = {
|
|
8
|
+
"lang": "python",
|
|
9
|
+
"lang_version": platform.python_version(),
|
|
10
|
+
"package_version": metadata.version("loopix"),
|
|
11
|
+
"publisher": "loopix",
|
|
12
|
+
"sdk_runtime": "python",
|
|
13
|
+
"system": platform.system(),
|
|
14
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from typing import cast, Optional, Dict, TypedDict
|
|
5
|
+
|
|
6
|
+
from httpx._types import ProxyTypes
|
|
7
|
+
from typing_extensions import Unpack
|
|
8
|
+
|
|
9
|
+
from loopix.api.metadata import package_version
|
|
10
|
+
|
|
11
|
+
REQUEST_TIMEOUT: float = 60.0 # 60 seconds
|
|
12
|
+
|
|
13
|
+
KEEPALIVE_PING_INTERVAL_SEC = 50 # 50 seconds
|
|
14
|
+
KEEPALIVE_PING_HEADER = "Keepalive-Ping-Interval"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ApiParams(TypedDict, total=False):
|
|
18
|
+
"""
|
|
19
|
+
Parameters for a request.
|
|
20
|
+
|
|
21
|
+
In the case of a sandbox, it applies to all **requests made to the returned sandbox**.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
request_timeout: Optional[float]
|
|
25
|
+
"""Timeout for the request in **seconds**, defaults to 60 seconds."""
|
|
26
|
+
|
|
27
|
+
headers: Optional[Dict[str, str]]
|
|
28
|
+
"""Additional headers to send with the request. Deprecated, use api_headers instead."""
|
|
29
|
+
|
|
30
|
+
api_headers: Optional[Dict[str, str]]
|
|
31
|
+
"""Additional headers to send with Loopix API requests."""
|
|
32
|
+
|
|
33
|
+
api_key: Optional[str]
|
|
34
|
+
"""Loopix API Key to use for authentication, defaults to `LOOPIX_API_KEY` environment variable."""
|
|
35
|
+
|
|
36
|
+
validate_api_key: Optional[bool]
|
|
37
|
+
"""Whether to validate the format of the Loopix API key on the client side.
|
|
38
|
+
Disable this when your deployment issues API keys that don't match the
|
|
39
|
+
default `lpx_` format. Defaults to `LOOPIX_VALIDATE_API_KEY` environment
|
|
40
|
+
variable or `True`."""
|
|
41
|
+
|
|
42
|
+
domain: Optional[str]
|
|
43
|
+
"""Loopix domain to use for authentication, defaults to `LOOPIX_DOMAIN` environment variable."""
|
|
44
|
+
|
|
45
|
+
api_url: Optional[str]
|
|
46
|
+
"""URL to use for the API, defaults to `https://api.<domain>`. For internal use only."""
|
|
47
|
+
|
|
48
|
+
debug: Optional[bool]
|
|
49
|
+
"""Whether to use debug mode, defaults to `LOOPIX_DEBUG` environment variable."""
|
|
50
|
+
|
|
51
|
+
proxy: Optional[ProxyTypes]
|
|
52
|
+
"""Proxy to use for the request. In case of a sandbox it applies to all **requests made to the returned sandbox**."""
|
|
53
|
+
|
|
54
|
+
sandbox_url: Optional[str]
|
|
55
|
+
"""URL to connect to sandbox, defaults to `LOOPIX_SANDBOX_URL` environment variable."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ApiParamsWithLogger(ApiParams, total=False):
|
|
59
|
+
""":class:`ApiParams` plus the construction-time ``logger``.
|
|
60
|
+
|
|
61
|
+
Internal type returned by :meth:`ConnectionConfig.get_api_params` so that the
|
|
62
|
+
logger a sandbox was created/connected with keeps propagating to the
|
|
63
|
+
throwaway ``ConnectionConfig`` that instance control-plane methods rebuild.
|
|
64
|
+
Unlike :class:`ApiParams`, ``logger`` is not a public per-request option.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
logger: Optional[logging.Logger]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ConnectionConfig:
|
|
71
|
+
"""
|
|
72
|
+
Configuration for the connection to the API.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
envd_port = 49983
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def _domain():
|
|
79
|
+
return os.getenv("LOOPIX_DOMAIN") or "vm.betmandu.net"
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _debug():
|
|
83
|
+
return os.getenv("LOOPIX_DEBUG", "false").lower() == "true"
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def _api_key():
|
|
87
|
+
return os.getenv("LOOPIX_API_KEY")
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _validate_api_key():
|
|
91
|
+
return os.getenv("LOOPIX_VALIDATE_API_KEY", "true").lower() != "false"
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _api_url():
|
|
95
|
+
return os.getenv("LOOPIX_API_URL")
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def _sandbox_url():
|
|
99
|
+
return os.getenv("LOOPIX_SANDBOX_URL")
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _access_token():
|
|
103
|
+
return os.getenv("LOOPIX_ACCESS_TOKEN")
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def _build_user_agent(
|
|
107
|
+
integration: Optional[str] = None,
|
|
108
|
+
) -> str:
|
|
109
|
+
user_agent_parts = [f"loopix-python-sdk/{package_version}"]
|
|
110
|
+
|
|
111
|
+
if integration:
|
|
112
|
+
user_agent_parts.append(integration)
|
|
113
|
+
|
|
114
|
+
return " ".join(user_agent_parts)
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
domain: Optional[str] = None,
|
|
119
|
+
debug: Optional[bool] = None,
|
|
120
|
+
api_key: Optional[str] = None,
|
|
121
|
+
validate_api_key: Optional[bool] = None,
|
|
122
|
+
api_url: Optional[str] = None,
|
|
123
|
+
sandbox_url: Optional[str] = None,
|
|
124
|
+
access_token: Optional[str] = None,
|
|
125
|
+
request_timeout: Optional[float] = None,
|
|
126
|
+
headers: Optional[Dict[str, str]] = None,
|
|
127
|
+
api_headers: Optional[Dict[str, str]] = None,
|
|
128
|
+
integration: Optional[str] = None,
|
|
129
|
+
extra_sandbox_headers: Optional[Dict[str, str]] = None,
|
|
130
|
+
proxy: Optional[ProxyTypes] = None,
|
|
131
|
+
logger: Optional[logging.Logger] = None,
|
|
132
|
+
):
|
|
133
|
+
self.logger = logger
|
|
134
|
+
self.domain = domain or ConnectionConfig._domain()
|
|
135
|
+
self.debug = debug if debug is not None else ConnectionConfig._debug()
|
|
136
|
+
self.api_key = api_key or ConnectionConfig._api_key()
|
|
137
|
+
self.validate_api_key = (
|
|
138
|
+
validate_api_key
|
|
139
|
+
if validate_api_key is not None
|
|
140
|
+
else ConnectionConfig._validate_api_key()
|
|
141
|
+
)
|
|
142
|
+
# Deprecated: pass the token through `api_headers` instead, e.g.
|
|
143
|
+
# api_headers={"Authorization": f"Bearer {token}"}.
|
|
144
|
+
self.access_token = access_token or ConnectionConfig._access_token()
|
|
145
|
+
self.integration = integration
|
|
146
|
+
self.headers = {**(headers or {}), **(api_headers or {})}
|
|
147
|
+
if self.integration is not None or "User-Agent" not in self.headers:
|
|
148
|
+
self.headers["User-Agent"] = self._build_user_agent(
|
|
149
|
+
self.integration,
|
|
150
|
+
)
|
|
151
|
+
self.__extra_sandbox_headers = extra_sandbox_headers or {}
|
|
152
|
+
|
|
153
|
+
self.proxy = proxy
|
|
154
|
+
|
|
155
|
+
self.request_timeout = ConnectionConfig._get_request_timeout(
|
|
156
|
+
REQUEST_TIMEOUT,
|
|
157
|
+
request_timeout,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
self.api_url = (
|
|
161
|
+
api_url
|
|
162
|
+
or ConnectionConfig._api_url()
|
|
163
|
+
or ("http://localhost:3000" if self.debug else f"https://{self.domain}/api")
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
self._sandbox_url: Optional[str] = (
|
|
167
|
+
sandbox_url or ConnectionConfig._sandbox_url()
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def _get_request_timeout(
|
|
172
|
+
default_timeout: Optional[float],
|
|
173
|
+
request_timeout: Optional[float],
|
|
174
|
+
):
|
|
175
|
+
if request_timeout == 0:
|
|
176
|
+
return None
|
|
177
|
+
elif request_timeout is not None:
|
|
178
|
+
return request_timeout
|
|
179
|
+
else:
|
|
180
|
+
return default_timeout
|
|
181
|
+
|
|
182
|
+
def get_request_timeout(self, request_timeout: Optional[float] = None):
|
|
183
|
+
return self._get_request_timeout(self.request_timeout, request_timeout)
|
|
184
|
+
|
|
185
|
+
def get_sandbox_url(self, sandbox_id: str, sandbox_domain: str) -> str:
|
|
186
|
+
if self._sandbox_url:
|
|
187
|
+
return self._sandbox_url # type: ignore[return-value]
|
|
188
|
+
|
|
189
|
+
sandbox_domain = sandbox_domain or self.domain
|
|
190
|
+
|
|
191
|
+
if self.debug:
|
|
192
|
+
return f"http://{sandbox_domain}/sandbox/{sandbox_id}"
|
|
193
|
+
|
|
194
|
+
return f"https://{sandbox_domain}/sandbox/{sandbox_id}"
|
|
195
|
+
|
|
196
|
+
def get_sandbox_direct_url(self, sandbox_id: str, sandbox_domain: str) -> str:
|
|
197
|
+
if self._sandbox_url:
|
|
198
|
+
return self._sandbox_url # type: ignore[return-value]
|
|
199
|
+
|
|
200
|
+
sandbox_domain = sandbox_domain or self.domain
|
|
201
|
+
|
|
202
|
+
if self.debug:
|
|
203
|
+
return f"http://{sandbox_domain}/sandbox/{sandbox_id}/{self.envd_port}"
|
|
204
|
+
|
|
205
|
+
return f"https://{sandbox_domain}/sandbox/{sandbox_id}/{self.envd_port}"
|
|
206
|
+
|
|
207
|
+
def get_host(self, sandbox_id: str, sandbox_domain: str, port: int) -> str:
|
|
208
|
+
"""
|
|
209
|
+
Get the host address to connect to the sandbox.
|
|
210
|
+
You can then use this address to connect to the sandbox port from outside the sandbox via HTTP or WebSocket.
|
|
211
|
+
|
|
212
|
+
:param port: Port to connect to
|
|
213
|
+
:param sandbox_domain: Domain to connect to
|
|
214
|
+
:param sandbox_id: Sandbox to connect to
|
|
215
|
+
|
|
216
|
+
:return: Host address to connect to
|
|
217
|
+
"""
|
|
218
|
+
domain = sandbox_domain or self.domain
|
|
219
|
+
|
|
220
|
+
if self.debug:
|
|
221
|
+
return f"localhost:{port}"
|
|
222
|
+
|
|
223
|
+
return f"{domain}/sandbox/{sandbox_id}/{port}"
|
|
224
|
+
|
|
225
|
+
def get_api_params(
|
|
226
|
+
self,
|
|
227
|
+
**opts: Unpack[ApiParams],
|
|
228
|
+
) -> dict:
|
|
229
|
+
"""
|
|
230
|
+
Get the parameters for the API call.
|
|
231
|
+
|
|
232
|
+
This is used to avoid passing the following attributes to the API call:
|
|
233
|
+
- access_token
|
|
234
|
+
- api_url
|
|
235
|
+
|
|
236
|
+
It also returns a copy, so the original object is not modified.
|
|
237
|
+
|
|
238
|
+
:return: Dictionary of parameters for the API call
|
|
239
|
+
"""
|
|
240
|
+
headers = opts.get("headers")
|
|
241
|
+
api_headers = opts.get("api_headers")
|
|
242
|
+
request_timeout = opts.get("request_timeout")
|
|
243
|
+
api_key = opts.get("api_key")
|
|
244
|
+
validate_api_key = opts.get("validate_api_key")
|
|
245
|
+
api_url = opts.get("api_url")
|
|
246
|
+
domain = opts.get("domain")
|
|
247
|
+
debug = opts.get("debug")
|
|
248
|
+
proxy = opts.get("proxy")
|
|
249
|
+
sandbox_url = opts.get("sandbox_url")
|
|
250
|
+
|
|
251
|
+
req_headers = self.headers.copy()
|
|
252
|
+
if headers is not None:
|
|
253
|
+
req_headers.update(headers)
|
|
254
|
+
if api_headers is not None:
|
|
255
|
+
req_headers.update(api_headers)
|
|
256
|
+
if self.integration is not None:
|
|
257
|
+
req_headers["User-Agent"] = self._build_user_agent(
|
|
258
|
+
self.integration,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# `logger` is a construction-time option rather than a per-request
|
|
262
|
+
# ApiParams field, but it must propagate to the throwaway
|
|
263
|
+
# ConnectionConfig that instance control-plane methods (kill, pause,
|
|
264
|
+
# set_timeout, get_info, connect, ...) rebuild from these params, so
|
|
265
|
+
# those requests keep logging with the logger the sandbox was created
|
|
266
|
+
# or connected with.
|
|
267
|
+
return dict(
|
|
268
|
+
ApiParamsWithLogger(
|
|
269
|
+
api_key=api_key if api_key is not None else self.api_key,
|
|
270
|
+
validate_api_key=(
|
|
271
|
+
validate_api_key
|
|
272
|
+
if validate_api_key is not None
|
|
273
|
+
else self.validate_api_key
|
|
274
|
+
),
|
|
275
|
+
api_url=api_url if api_url is not None else self.api_url,
|
|
276
|
+
domain=domain if domain is not None else self.domain,
|
|
277
|
+
debug=debug if debug is not None else self.debug,
|
|
278
|
+
request_timeout=self.get_request_timeout(request_timeout),
|
|
279
|
+
headers=req_headers,
|
|
280
|
+
proxy=proxy if proxy is not None else self.proxy,
|
|
281
|
+
sandbox_url=(
|
|
282
|
+
sandbox_url
|
|
283
|
+
if sandbox_url is not None
|
|
284
|
+
else cast(Optional[str], self._sandbox_url)
|
|
285
|
+
),
|
|
286
|
+
logger=self.logger,
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def sandbox_headers(self):
|
|
292
|
+
"""
|
|
293
|
+
We need this separate as we use the same header for Loopix access token to API and envd access token to sandbox.
|
|
294
|
+
"""
|
|
295
|
+
return {
|
|
296
|
+
"User-Agent": self.headers["User-Agent"],
|
|
297
|
+
**self.__extra_sandbox_headers,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
Username = str
|
|
302
|
+
"""
|
|
303
|
+
User used for the operation in the sandbox.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
default_username: Username = "user"
|
|
307
|
+
"""
|
|
308
|
+
Default user used for the operation in the sandbox.
|
|
309
|
+
"""
|
loopix/envd/api.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from typing import Callable, Optional
|
|
5
|
+
|
|
6
|
+
from loopix.envd.rpc import format_terminated_exception
|
|
7
|
+
from loopix.exceptions import (
|
|
8
|
+
SandboxException,
|
|
9
|
+
NotFoundException,
|
|
10
|
+
AuthenticationException,
|
|
11
|
+
InvalidArgumentException,
|
|
12
|
+
NotEnoughSpaceException,
|
|
13
|
+
RateLimitException,
|
|
14
|
+
format_sandbox_timeout_exception,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ENVD_API_FILES_ROUTE = "/files"
|
|
19
|
+
ENVD_API_HEALTH_ROUTE = "/health"
|
|
20
|
+
|
|
21
|
+
_DEFAULT_API_ERROR_MAP: dict[int, Callable[[str], Exception]] = {
|
|
22
|
+
400: InvalidArgumentException,
|
|
23
|
+
401: AuthenticationException,
|
|
24
|
+
404: NotFoundException,
|
|
25
|
+
429: lambda message: RateLimitException(
|
|
26
|
+
f"{message}: The requests are being rate limited."
|
|
27
|
+
),
|
|
28
|
+
502: format_sandbox_timeout_exception,
|
|
29
|
+
507: NotEnoughSpaceException,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
HEALTH_CHECK_TIMEOUT = 5 # seconds
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_sandbox_health(envd_api: httpx.Client) -> Optional[bool]:
|
|
37
|
+
"""Probe the sandbox's envd health endpoint.
|
|
38
|
+
|
|
39
|
+
:return: ``True`` if the sandbox is running, ``False`` if it is not, ``None`` if its state could not be determined.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
r = envd_api.get(ENVD_API_HEALTH_ROUTE, timeout=HEALTH_CHECK_TIMEOUT)
|
|
43
|
+
if r.status_code == 502:
|
|
44
|
+
return False
|
|
45
|
+
if r.is_success:
|
|
46
|
+
return True
|
|
47
|
+
return None
|
|
48
|
+
except Exception:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def acheck_sandbox_health(envd_api: httpx.AsyncClient) -> Optional[bool]:
|
|
53
|
+
"""Async version of :func:`check_sandbox_health`."""
|
|
54
|
+
try:
|
|
55
|
+
r = await envd_api.get(ENVD_API_HEALTH_ROUTE, timeout=HEALTH_CHECK_TIMEOUT)
|
|
56
|
+
if r.status_code == 502:
|
|
57
|
+
return False
|
|
58
|
+
if r.is_success:
|
|
59
|
+
return True
|
|
60
|
+
return None
|
|
61
|
+
except Exception:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def handle_envd_api_transport_exception(
|
|
66
|
+
e: Exception,
|
|
67
|
+
sandbox_running: Optional[bool] = None,
|
|
68
|
+
) -> Exception:
|
|
69
|
+
"""Handle transport-level errors from envd API requests.
|
|
70
|
+
|
|
71
|
+
:param e: The caught exception, expected to be a transport-level ``httpx`` error.
|
|
72
|
+
:param sandbox_running: Result of a sandbox health probe (``None`` when unknown), used to disambiguate a connection dropped mid-request.
|
|
73
|
+
:return: A ``TimeoutException`` when the connection dropped mid-request and the sandbox is confirmed gone, or the original exception unchanged otherwise.
|
|
74
|
+
"""
|
|
75
|
+
# A remote protocol error (e.g. an HTTP/2 stream reset) means the connection to the
|
|
76
|
+
# sandbox was dropped mid-request — either the sandbox died or the network failed
|
|
77
|
+
if isinstance(e, httpx.RemoteProtocolError):
|
|
78
|
+
return format_terminated_exception(e, sandbox_running)
|
|
79
|
+
|
|
80
|
+
return e
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def handle_envd_api_transport_exception_with_health(
|
|
84
|
+
e: Exception,
|
|
85
|
+
envd_api: httpx.Client,
|
|
86
|
+
) -> Exception:
|
|
87
|
+
"""Like :func:`handle_envd_api_transport_exception`, but when the connection to the
|
|
88
|
+
sandbox was dropped mid-request it probes the sandbox health to tell apart the sandbox
|
|
89
|
+
being killed from a transient network failure (e.g. a load balancer dropping the connection).
|
|
90
|
+
"""
|
|
91
|
+
sandbox_running = (
|
|
92
|
+
check_sandbox_health(envd_api)
|
|
93
|
+
if isinstance(e, httpx.RemoteProtocolError)
|
|
94
|
+
else None
|
|
95
|
+
)
|
|
96
|
+
return handle_envd_api_transport_exception(e, sandbox_running)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def ahandle_envd_api_transport_exception_with_health(
|
|
100
|
+
e: Exception,
|
|
101
|
+
envd_api: httpx.AsyncClient,
|
|
102
|
+
) -> Exception:
|
|
103
|
+
"""Async version of :func:`handle_envd_api_transport_exception_with_health`."""
|
|
104
|
+
sandbox_running = (
|
|
105
|
+
await acheck_sandbox_health(envd_api)
|
|
106
|
+
if isinstance(e, httpx.RemoteProtocolError)
|
|
107
|
+
else None
|
|
108
|
+
)
|
|
109
|
+
return handle_envd_api_transport_exception(e, sandbox_running)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_message(e: httpx.Response) -> str:
|
|
113
|
+
try:
|
|
114
|
+
message = e.json().get("message", e.text)
|
|
115
|
+
except json.JSONDecodeError:
|
|
116
|
+
message = e.text
|
|
117
|
+
|
|
118
|
+
return message
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def handle_envd_api_exception(
|
|
122
|
+
res: httpx.Response,
|
|
123
|
+
error_map: Optional[dict[int, Callable[[str], Exception]]] = None,
|
|
124
|
+
):
|
|
125
|
+
"""Handle errors from envd API responses by mapping HTTP status codes to specific exception types.
|
|
126
|
+
|
|
127
|
+
:param res: The HTTP response.
|
|
128
|
+
:param error_map: Optional map of HTTP status codes to exception factories that override the defaults.
|
|
129
|
+
:return: The corresponding exception, or ``None`` if the response is successful.
|
|
130
|
+
"""
|
|
131
|
+
if res.is_success:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
res.read()
|
|
135
|
+
|
|
136
|
+
return format_envd_api_exception(res.status_code, get_message(res), error_map)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def ahandle_envd_api_exception(
|
|
140
|
+
res: httpx.Response,
|
|
141
|
+
error_map: Optional[dict[int, Callable[[str], Exception]]] = None,
|
|
142
|
+
):
|
|
143
|
+
"""Async version of :func:`handle_envd_api_exception`."""
|
|
144
|
+
if res.is_success:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
await res.aread()
|
|
148
|
+
|
|
149
|
+
return format_envd_api_exception(res.status_code, get_message(res), error_map)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def format_envd_api_exception(
|
|
153
|
+
status_code: int,
|
|
154
|
+
message: str,
|
|
155
|
+
error_map: Optional[dict[int, Callable[[str], Exception]]] = None,
|
|
156
|
+
):
|
|
157
|
+
"""Map an HTTP status code and message to the appropriate exception.
|
|
158
|
+
|
|
159
|
+
:param status_code: The HTTP status code.
|
|
160
|
+
:param message: The error message from the response body.
|
|
161
|
+
:param error_map: Optional map of HTTP status codes to exception factories that override the defaults.
|
|
162
|
+
:return: The corresponding exception.
|
|
163
|
+
"""
|
|
164
|
+
if error_map and status_code in error_map:
|
|
165
|
+
return error_map[status_code](message)
|
|
166
|
+
|
|
167
|
+
if status_code in _DEFAULT_API_ERROR_MAP:
|
|
168
|
+
return _DEFAULT_API_ERROR_MAP[status_code](message)
|
|
169
|
+
|
|
170
|
+
return SandboxException(f"{status_code}: {message}")
|