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_connect/client.py
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import gzip
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import struct
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
from httpcore import (
|
|
9
|
+
ConnectionPool,
|
|
10
|
+
AsyncConnectionPool,
|
|
11
|
+
RemoteProtocolError,
|
|
12
|
+
Response,
|
|
13
|
+
)
|
|
14
|
+
from enum import Flag, Enum
|
|
15
|
+
from typing import Callable, Optional, Dict, Any, Generator, Tuple
|
|
16
|
+
from google.protobuf import json_format
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EnvelopeFlags(Flag):
|
|
20
|
+
compressed = 0b00000001
|
|
21
|
+
end_stream = 0b00000010
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Code(Enum):
|
|
25
|
+
canceled = "canceled"
|
|
26
|
+
unknown = "unknown"
|
|
27
|
+
invalid_argument = "invalid_argument"
|
|
28
|
+
deadline_exceeded = "deadline_exceeded"
|
|
29
|
+
not_found = "not_found"
|
|
30
|
+
already_exists = "already_exists"
|
|
31
|
+
permission_denied = "permission_denied"
|
|
32
|
+
resource_exhausted = "resource_exhausted"
|
|
33
|
+
failed_precondition = "failed_precondition"
|
|
34
|
+
aborted = "aborted"
|
|
35
|
+
out_of_range = "out_of_range"
|
|
36
|
+
unimplemented = "unimplemented"
|
|
37
|
+
internal = "internal"
|
|
38
|
+
unavailable = "unavailable"
|
|
39
|
+
data_loss = "data_loss"
|
|
40
|
+
unauthenticated = "unauthenticated"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def make_error_from_http_code(http_code: int):
|
|
44
|
+
error_code_map = {
|
|
45
|
+
400: Code.invalid_argument,
|
|
46
|
+
401: Code.unauthenticated,
|
|
47
|
+
403: Code.permission_denied,
|
|
48
|
+
404: Code.not_found,
|
|
49
|
+
409: Code.already_exists,
|
|
50
|
+
413: Code.resource_exhausted,
|
|
51
|
+
429: Code.resource_exhausted,
|
|
52
|
+
499: Code.canceled,
|
|
53
|
+
500: Code.internal,
|
|
54
|
+
501: Code.unimplemented,
|
|
55
|
+
502: Code.unavailable,
|
|
56
|
+
503: Code.unavailable,
|
|
57
|
+
504: Code.deadline_exceeded,
|
|
58
|
+
505: Code.unimplemented,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return error_code_map.get(http_code, Code.unknown)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ConnectException(Exception):
|
|
65
|
+
def __init__(self, status: Code, message: str):
|
|
66
|
+
self.status = status
|
|
67
|
+
self.message = message
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
envelope_header_length = 5
|
|
71
|
+
envelope_header_pack = ">BI"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def encode_envelope(*, flags: EnvelopeFlags, data):
|
|
75
|
+
return encode_envelope_header(flags=flags.value, data=data) + data
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def encode_envelope_header(*, flags, data):
|
|
79
|
+
return struct.pack(envelope_header_pack, flags, len(data))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def decode_envelope_header(header):
|
|
83
|
+
flags, data_len = struct.unpack(envelope_header_pack, header)
|
|
84
|
+
return EnvelopeFlags(flags), data_len
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def error_for_response(http_resp: Response):
|
|
88
|
+
try:
|
|
89
|
+
error = json.loads(http_resp.content)
|
|
90
|
+
return make_error(error)
|
|
91
|
+
except (json.decoder.JSONDecodeError, KeyError):
|
|
92
|
+
error = {"code": http_resp.status, "message": http_resp.content.decode("utf-8")}
|
|
93
|
+
return make_error(error)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def make_error(error):
|
|
97
|
+
status = None
|
|
98
|
+
try:
|
|
99
|
+
code_value = error.get("code")
|
|
100
|
+
# return error code from http status code
|
|
101
|
+
if isinstance(code_value, int):
|
|
102
|
+
status = make_error_from_http_code(code_value)
|
|
103
|
+
else:
|
|
104
|
+
status = Code(code_value)
|
|
105
|
+
except (KeyError, ValueError):
|
|
106
|
+
status = Code.unknown
|
|
107
|
+
|
|
108
|
+
return ConnectException(status, error.get("message", ""))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _sync_retry(func, exc, retries):
|
|
112
|
+
def retry(*args, **kwargs):
|
|
113
|
+
for _ in range(retries):
|
|
114
|
+
try:
|
|
115
|
+
return func(*args, **kwargs)
|
|
116
|
+
except exc:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
return func(*args, **kwargs)
|
|
120
|
+
|
|
121
|
+
return retry
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _async_retry(func, exc, retries):
|
|
125
|
+
async def retry(*args, **kwargs):
|
|
126
|
+
for _ in range(retries):
|
|
127
|
+
try:
|
|
128
|
+
return await func(*args, **kwargs)
|
|
129
|
+
except exc:
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
return await func(*args, **kwargs)
|
|
133
|
+
|
|
134
|
+
return retry
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _retry(exc: typing.Type[Exception], retries: int):
|
|
138
|
+
def decorator(func):
|
|
139
|
+
if inspect.iscoroutinefunction(func):
|
|
140
|
+
return _async_retry(func, exc, retries)
|
|
141
|
+
|
|
142
|
+
return _sync_retry(func, exc, retries)
|
|
143
|
+
|
|
144
|
+
return decorator
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class GzipCompressor:
|
|
148
|
+
name = "gzip"
|
|
149
|
+
decompress = gzip.decompress
|
|
150
|
+
compress = gzip.compress
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class JSONCodec:
|
|
154
|
+
content_type = "json"
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def encode(msg):
|
|
158
|
+
return json_format.MessageToJson(msg).encode("utf8")
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def decode(data, *, msg_type):
|
|
162
|
+
msg = msg_type()
|
|
163
|
+
json_format.Parse(data.decode("utf8"), msg, ignore_unknown_fields=True)
|
|
164
|
+
return msg
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class ProtobufCodec:
|
|
168
|
+
content_type = "proto"
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def encode(msg):
|
|
172
|
+
return msg.SerializeToString()
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def decode(data, *, msg_type):
|
|
176
|
+
msg = msg_type()
|
|
177
|
+
msg.ParseFromString(data)
|
|
178
|
+
return msg
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class Client:
|
|
182
|
+
def __init__(
|
|
183
|
+
self,
|
|
184
|
+
*,
|
|
185
|
+
pool: Optional[ConnectionPool] = None,
|
|
186
|
+
async_pool: Optional[AsyncConnectionPool] = None,
|
|
187
|
+
url: str,
|
|
188
|
+
response_type,
|
|
189
|
+
compressor=None,
|
|
190
|
+
json: Optional[bool] = False,
|
|
191
|
+
headers: Optional[Dict[str, str]] = None,
|
|
192
|
+
logger: Optional[logging.Logger] = None,
|
|
193
|
+
):
|
|
194
|
+
if headers is None:
|
|
195
|
+
headers = {}
|
|
196
|
+
|
|
197
|
+
self.pool = pool
|
|
198
|
+
self.async_pool = async_pool
|
|
199
|
+
self.url = url
|
|
200
|
+
self._codec = JSONCodec if json else ProtobufCodec
|
|
201
|
+
self._response_type = response_type
|
|
202
|
+
self._compressor = compressor
|
|
203
|
+
self._headers = headers
|
|
204
|
+
self._connection_retries = 3
|
|
205
|
+
self._logger = logger
|
|
206
|
+
|
|
207
|
+
def _log_request(self) -> None:
|
|
208
|
+
if self._logger is not None:
|
|
209
|
+
self._logger.info(f"Request: POST {self.url}")
|
|
210
|
+
|
|
211
|
+
def _log_response(self, status: int) -> None:
|
|
212
|
+
if self._logger is None:
|
|
213
|
+
return
|
|
214
|
+
if status >= 400:
|
|
215
|
+
self._logger.error(f"Response: {status} {self.url}")
|
|
216
|
+
else:
|
|
217
|
+
self._logger.info(f"Response: {status} {self.url}")
|
|
218
|
+
|
|
219
|
+
def _log_stream_message(self) -> None:
|
|
220
|
+
if self._logger is not None:
|
|
221
|
+
self._logger.debug(f"Response stream: {self.url}")
|
|
222
|
+
|
|
223
|
+
def _prepare_unary_request(
|
|
224
|
+
self,
|
|
225
|
+
req,
|
|
226
|
+
request_timeout=None,
|
|
227
|
+
headers: Optional[dict] = None,
|
|
228
|
+
**opts,
|
|
229
|
+
) -> dict:
|
|
230
|
+
data = self._codec.encode(req)
|
|
231
|
+
|
|
232
|
+
if self._compressor is not None:
|
|
233
|
+
data = self._compressor.compress(data)
|
|
234
|
+
|
|
235
|
+
if headers is None:
|
|
236
|
+
headers = {}
|
|
237
|
+
|
|
238
|
+
extensions = (
|
|
239
|
+
None
|
|
240
|
+
if request_timeout is None
|
|
241
|
+
else {
|
|
242
|
+
"timeout": {
|
|
243
|
+
"connect": request_timeout,
|
|
244
|
+
"pool": request_timeout,
|
|
245
|
+
"read": request_timeout,
|
|
246
|
+
"write": request_timeout,
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
"method": "POST",
|
|
253
|
+
"url": self.url,
|
|
254
|
+
"content": data,
|
|
255
|
+
"extensions": extensions,
|
|
256
|
+
"headers": {
|
|
257
|
+
**self._headers,
|
|
258
|
+
**headers,
|
|
259
|
+
**opts.get("headers", {}),
|
|
260
|
+
"connect-protocol-version": "1",
|
|
261
|
+
"content-encoding": (
|
|
262
|
+
"identity" if self._compressor is None else self._compressor.name
|
|
263
|
+
),
|
|
264
|
+
"content-type": f"application/{self._codec.content_type}",
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
def _process_unary_response(
|
|
269
|
+
self,
|
|
270
|
+
http_resp: Response,
|
|
271
|
+
):
|
|
272
|
+
self._log_response(http_resp.status)
|
|
273
|
+
|
|
274
|
+
if http_resp.status != 200:
|
|
275
|
+
raise error_for_response(http_resp)
|
|
276
|
+
|
|
277
|
+
content = http_resp.content
|
|
278
|
+
|
|
279
|
+
if self._compressor is not None:
|
|
280
|
+
content = self._compressor.decompress(content)
|
|
281
|
+
|
|
282
|
+
return self._codec.decode(
|
|
283
|
+
content,
|
|
284
|
+
msg_type=self._response_type,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
@_retry(RemoteProtocolError, 3)
|
|
288
|
+
async def acall_unary(
|
|
289
|
+
self,
|
|
290
|
+
req,
|
|
291
|
+
request_timeout=None,
|
|
292
|
+
headers: Optional[dict] = None,
|
|
293
|
+
**opts,
|
|
294
|
+
):
|
|
295
|
+
if self.async_pool is None:
|
|
296
|
+
raise ValueError("async_pool is required")
|
|
297
|
+
|
|
298
|
+
req_data = self._prepare_unary_request(
|
|
299
|
+
req,
|
|
300
|
+
request_timeout,
|
|
301
|
+
headers,
|
|
302
|
+
**opts,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
self._log_request()
|
|
306
|
+
res = await self.async_pool.request(**req_data)
|
|
307
|
+
return self._process_unary_response(res)
|
|
308
|
+
|
|
309
|
+
@_retry(RemoteProtocolError, 3)
|
|
310
|
+
def call_unary(
|
|
311
|
+
self,
|
|
312
|
+
req,
|
|
313
|
+
request_timeout=None,
|
|
314
|
+
headers: Optional[dict] = None,
|
|
315
|
+
**opts,
|
|
316
|
+
):
|
|
317
|
+
if self.pool is None:
|
|
318
|
+
raise ValueError("pool is required")
|
|
319
|
+
|
|
320
|
+
req_data = self._prepare_unary_request(
|
|
321
|
+
req,
|
|
322
|
+
request_timeout,
|
|
323
|
+
headers,
|
|
324
|
+
**opts,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
self._log_request()
|
|
328
|
+
res = self.pool.request(**req_data)
|
|
329
|
+
return self._process_unary_response(res)
|
|
330
|
+
|
|
331
|
+
def _create_stream_timeout(self, timeout: Optional[float]):
|
|
332
|
+
if timeout:
|
|
333
|
+
return {"connect-timeout-ms": str(int(timeout * 1000))}
|
|
334
|
+
return {}
|
|
335
|
+
|
|
336
|
+
def _prepare_server_stream_request(
|
|
337
|
+
self,
|
|
338
|
+
req,
|
|
339
|
+
request_timeout=None,
|
|
340
|
+
timeout=None,
|
|
341
|
+
headers: Optional[dict] = None,
|
|
342
|
+
**opts,
|
|
343
|
+
) -> dict:
|
|
344
|
+
headers = headers or {}
|
|
345
|
+
data = self._codec.encode(req)
|
|
346
|
+
flags = EnvelopeFlags(0)
|
|
347
|
+
|
|
348
|
+
# `request_timeout` bounds connection setup and request sending, but NOT the
|
|
349
|
+
# stream read: a stream can stay open for the whole command `timeout` (minutes
|
|
350
|
+
# or, when disabled, indefinitely), so we deliberately leave `read` unset.
|
|
351
|
+
# The command `timeout` is enforced server-side via the `connect-timeout-ms`
|
|
352
|
+
# header (see `_create_stream_timeout`), which returns a clean `deadline_exceeded`.
|
|
353
|
+
# This mirrors the JS SDK, which has no per-chunk read timeout either — setting
|
|
354
|
+
# `read` to the command `timeout` would race that server response and surface a
|
|
355
|
+
# raw transport `ReadTimeout` instead.
|
|
356
|
+
timeout_ext = {}
|
|
357
|
+
if request_timeout is not None:
|
|
358
|
+
timeout_ext["connect"] = request_timeout
|
|
359
|
+
timeout_ext["pool"] = request_timeout
|
|
360
|
+
timeout_ext["write"] = request_timeout
|
|
361
|
+
extensions = {"timeout": timeout_ext} if timeout_ext else None
|
|
362
|
+
|
|
363
|
+
if self._compressor is not None:
|
|
364
|
+
data = self._compressor.compress(data)
|
|
365
|
+
flags |= EnvelopeFlags.compressed
|
|
366
|
+
|
|
367
|
+
stream_timeout = self._create_stream_timeout(timeout)
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
"method": "POST",
|
|
371
|
+
"url": self.url,
|
|
372
|
+
"content": encode_envelope(
|
|
373
|
+
flags=flags,
|
|
374
|
+
data=data,
|
|
375
|
+
),
|
|
376
|
+
"extensions": extensions,
|
|
377
|
+
"headers": {
|
|
378
|
+
**self._headers,
|
|
379
|
+
**headers,
|
|
380
|
+
**opts.get("headers", {}),
|
|
381
|
+
**stream_timeout,
|
|
382
|
+
"connect-protocol-version": "1",
|
|
383
|
+
"connect-content-encoding": (
|
|
384
|
+
"identity" if self._compressor is None else self._compressor.name
|
|
385
|
+
),
|
|
386
|
+
"content-type": f"application/connect+{self._codec.content_type}",
|
|
387
|
+
},
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
# Note: no retry here — generator functions don't execute until iterated, so a
|
|
391
|
+
# call-level retry never fires, and retrying mid-stream would replay delivered events.
|
|
392
|
+
async def acall_server_stream(
|
|
393
|
+
self,
|
|
394
|
+
req,
|
|
395
|
+
request_timeout=None,
|
|
396
|
+
timeout=None,
|
|
397
|
+
headers: Optional[dict] = None,
|
|
398
|
+
**opts,
|
|
399
|
+
):
|
|
400
|
+
if self.async_pool is None:
|
|
401
|
+
raise ValueError("async_pool is required")
|
|
402
|
+
|
|
403
|
+
req_data = self._prepare_server_stream_request(
|
|
404
|
+
req,
|
|
405
|
+
request_timeout,
|
|
406
|
+
timeout,
|
|
407
|
+
headers,
|
|
408
|
+
**opts,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
parser = ServerStreamParser(
|
|
412
|
+
decode=self._codec.decode,
|
|
413
|
+
response_type=self._response_type,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
self._log_request()
|
|
417
|
+
async with self.async_pool.stream(**req_data) as http_resp:
|
|
418
|
+
if http_resp.status != 200:
|
|
419
|
+
self._log_response(http_resp.status)
|
|
420
|
+
await http_resp.aread()
|
|
421
|
+
raise error_for_response(http_resp)
|
|
422
|
+
|
|
423
|
+
async for chunk in http_resp.aiter_stream():
|
|
424
|
+
for parsed in parser.parse(chunk):
|
|
425
|
+
self._log_stream_message()
|
|
426
|
+
yield parsed
|
|
427
|
+
|
|
428
|
+
def call_server_stream(
|
|
429
|
+
self,
|
|
430
|
+
req,
|
|
431
|
+
request_timeout=None,
|
|
432
|
+
timeout=None,
|
|
433
|
+
headers: Optional[dict] = None,
|
|
434
|
+
**opts,
|
|
435
|
+
):
|
|
436
|
+
if self.pool is None:
|
|
437
|
+
raise ValueError("pool is required")
|
|
438
|
+
|
|
439
|
+
req_data = self._prepare_server_stream_request(
|
|
440
|
+
req,
|
|
441
|
+
request_timeout,
|
|
442
|
+
timeout,
|
|
443
|
+
headers,
|
|
444
|
+
**opts,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
parser = ServerStreamParser(
|
|
448
|
+
decode=self._codec.decode,
|
|
449
|
+
response_type=self._response_type,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
self._log_request()
|
|
453
|
+
with self.pool.stream(**req_data) as http_resp:
|
|
454
|
+
if http_resp.status != 200:
|
|
455
|
+
self._log_response(http_resp.status)
|
|
456
|
+
http_resp.read()
|
|
457
|
+
raise error_for_response(http_resp)
|
|
458
|
+
|
|
459
|
+
for chunk in http_resp.iter_stream():
|
|
460
|
+
for parsed in parser.parse(chunk):
|
|
461
|
+
self._log_stream_message()
|
|
462
|
+
yield parsed
|
|
463
|
+
|
|
464
|
+
def call_client_stream(self, req, **opts):
|
|
465
|
+
raise NotImplementedError("client stream not supported")
|
|
466
|
+
|
|
467
|
+
def acall_client_stream(self, req, **opts):
|
|
468
|
+
raise NotImplementedError("client stream not supported")
|
|
469
|
+
|
|
470
|
+
def call_bidi_stream(self, req, **opts):
|
|
471
|
+
raise NotImplementedError("bidi stream not supported")
|
|
472
|
+
|
|
473
|
+
def acall_bidi_stream(self, req, **opts):
|
|
474
|
+
raise NotImplementedError("bidi stream not supported")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
DataLen = int
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class ServerStreamParser:
|
|
481
|
+
def __init__(
|
|
482
|
+
self,
|
|
483
|
+
decode: Callable,
|
|
484
|
+
response_type: Any,
|
|
485
|
+
):
|
|
486
|
+
self.decode = decode
|
|
487
|
+
self.response_type = response_type
|
|
488
|
+
|
|
489
|
+
self.buffer: bytes = b""
|
|
490
|
+
self._header: Optional[tuple[EnvelopeFlags, DataLen]] = None
|
|
491
|
+
|
|
492
|
+
def shift_buffer(self, size: int):
|
|
493
|
+
buffer = self.buffer[:size]
|
|
494
|
+
self.buffer = self.buffer[size:]
|
|
495
|
+
return buffer
|
|
496
|
+
|
|
497
|
+
@property
|
|
498
|
+
def header(self) -> Tuple[EnvelopeFlags, DataLen]:
|
|
499
|
+
if self._header:
|
|
500
|
+
return self._header
|
|
501
|
+
|
|
502
|
+
header_data = self.shift_buffer(envelope_header_length)
|
|
503
|
+
self._header = decode_envelope_header(header_data)
|
|
504
|
+
|
|
505
|
+
return self._header
|
|
506
|
+
|
|
507
|
+
@header.deleter
|
|
508
|
+
def header(self):
|
|
509
|
+
self._header = None
|
|
510
|
+
|
|
511
|
+
def parse(self, chunk: bytes) -> Generator[Any, None, None]:
|
|
512
|
+
self.buffer += chunk
|
|
513
|
+
|
|
514
|
+
# Once the header is consumed, the remaining payload can be shorter
|
|
515
|
+
# than the header length, so only require a full header when we still
|
|
516
|
+
# need to read one.
|
|
517
|
+
while self._header is not None or len(self.buffer) >= envelope_header_length:
|
|
518
|
+
flags, data_len = self.header
|
|
519
|
+
|
|
520
|
+
if data_len > len(self.buffer):
|
|
521
|
+
break
|
|
522
|
+
|
|
523
|
+
data = self.shift_buffer(data_len)
|
|
524
|
+
|
|
525
|
+
if EnvelopeFlags.end_stream in flags:
|
|
526
|
+
data = json.loads(data)
|
|
527
|
+
|
|
528
|
+
if "error" in data:
|
|
529
|
+
raise make_error(data["error"])
|
|
530
|
+
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
yield self.decode(data, msg_type=self.response_type)
|
|
534
|
+
del self.header
|
loopix_connect/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loopix-sdk
|
|
3
|
+
Version: 2.30.0
|
|
4
|
+
Summary: Loopix SDK that give agents cloud environments
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: loopix
|
|
8
|
+
Author-email: hello@vm.betmandu.net
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Dist: attrs (>=23.2.0)
|
|
18
|
+
Requires-Dist: dockerfile-parse (>=2.0.1,<3.0.0)
|
|
19
|
+
Requires-Dist: h2 (>=4,<5)
|
|
20
|
+
Requires-Dist: httpcore (>=1.0.5,<2.0.0)
|
|
21
|
+
Requires-Dist: httpx (>=0.27.0,<1.0.0)
|
|
22
|
+
Requires-Dist: packaging (>=24.1)
|
|
23
|
+
Requires-Dist: protobuf (>=4.21.0)
|
|
24
|
+
Requires-Dist: python-dateutil (>=2.8.2)
|
|
25
|
+
Requires-Dist: rich (>=14.0.0)
|
|
26
|
+
Requires-Dist: typing-extensions (>=4.1.0)
|
|
27
|
+
Requires-Dist: wcmatch (>=10.1,<11.0)
|
|
28
|
+
Project-URL: Bug Tracker, https://github.com/loopix-dev/loopix/issues
|
|
29
|
+
Project-URL: Homepage, https://vm.betmandu.net/
|
|
30
|
+
Project-URL: Repository, https://github.com/loopix-dev/loopix/tree/main/packages/python-sdk
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
<picture>
|
|
35
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/loopix-dev/loopix/refs/heads/main/readme-assets/logo-white.png">
|
|
36
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/loopix-dev/loopix/refs/heads/main/readme-assets/logo-black.png">
|
|
37
|
+
<img alt="Loopix Logo" src="https://raw.githubusercontent.com/loopix-dev/loopix/refs/heads/main/readme-assets/logo-black.png" width="200">
|
|
38
|
+
</picture>
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
<h4 align="center">
|
|
42
|
+
<a href="https://pypi.org/project/loopix/">
|
|
43
|
+
<img alt="Last 1 month downloads for the Python SDK" loading="lazy" decoding="async" style="color:transparent;width:170px;height:18px" src="https://static.pepy.tech/personalized-badge/loopix?period=monthly&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=PyPi%20Monthly%20Downloads">
|
|
44
|
+
</a>
|
|
45
|
+
</h4>
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## What is Loopix?
|
|
49
|
+
[Loopix](https://www.loopix.dev/) is an open-source infrastructure that allows you to run AI-generated code in secure isolated sandboxes in the cloud. To start and control sandboxes, use our [JavaScript SDK](https://www.npmjs.com/package/loopix) or [Python SDK](https://pypi.org/project/loopix).
|
|
50
|
+
|
|
51
|
+
## Run your first Sandbox
|
|
52
|
+
|
|
53
|
+
### 1. Install SDK
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
pip install loopix
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. Get your Loopix API key
|
|
60
|
+
1. Sign up to Loopix [here](https://vm.betmandu.net).
|
|
61
|
+
2. Get your API key [here](https://vm.betmandu.net/dashboard?tab=keys).
|
|
62
|
+
3. Set environment variable with your API key
|
|
63
|
+
```
|
|
64
|
+
LOOPIX_API_KEY=lpx_***
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 3. Start a sandbox and run commands
|
|
68
|
+
|
|
69
|
+
```py
|
|
70
|
+
from loopix import Sandbox
|
|
71
|
+
|
|
72
|
+
with Sandbox.create() as sandbox:
|
|
73
|
+
result = sandbox.commands.run('echo "Hello from Loopix!"')
|
|
74
|
+
print(result.stdout) # Hello from Loopix!
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 4. Code execution with Code Interpreter
|
|
78
|
+
|
|
79
|
+
If you need [`run_code()`](https://vm.betmandu.net/docs/code-interpreting), install the [Code Interpreter SDK](https://github.com/loopix-dev/code-interpreter):
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
pip install loopix-code-interpreter
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```py
|
|
86
|
+
from loopix_code_interpreter import Sandbox
|
|
87
|
+
|
|
88
|
+
with Sandbox.create() as sandbox:
|
|
89
|
+
execution = sandbox.run_code("x = 1; x += 1; x")
|
|
90
|
+
print(execution.text) # outputs 2
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 5. Check docs
|
|
94
|
+
Visit [Loopix documentation](https://vm.betmandu.net/docs).
|
|
95
|
+
|
|
96
|
+
### 6. Loopix cookbook
|
|
97
|
+
Visit our [Cookbook](https://github.com/loopix-dev/loopix-cookbook/tree/main) to get inspired by examples with different LLMs and AI frameworks.
|
|
98
|
+
|