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.
Files changed (238) hide show
  1. loopix/__init__.py +260 -0
  2. loopix/api/__init__.py +287 -0
  3. loopix/api/client/__init__.py +8 -0
  4. loopix/api/client/api/__init__.py +1 -0
  5. loopix/api/client/api/sandboxes/__init__.py +1 -0
  6. loopix/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
  7. loopix/api/client/api/sandboxes/get_sandboxes.py +176 -0
  8. loopix/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
  9. loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
  10. loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
  11. loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +212 -0
  12. loopix/api/client/api/sandboxes/get_v2_sandboxes.py +230 -0
  13. loopix/api/client/api/sandboxes/get_v_2_sandboxes_sandbox_id_logs.py +254 -0
  14. loopix/api/client/api/sandboxes/post_sandboxes.py +172 -0
  15. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
  16. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +187 -0
  17. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +181 -0
  18. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +189 -0
  19. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py +195 -0
  20. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +193 -0
  21. loopix/api/client/api/sandboxes/put_sandboxes_sandbox_id_network.py +199 -0
  22. loopix/api/client/api/snapshots/__init__.py +1 -0
  23. loopix/api/client/api/snapshots/get_snapshots.py +202 -0
  24. loopix/api/client/api/tags/__init__.py +1 -0
  25. loopix/api/client/api/tags/delete_templates_tags.py +174 -0
  26. loopix/api/client/api/tags/get_templates_template_id_tags.py +172 -0
  27. loopix/api/client/api/tags/post_templates_tags.py +176 -0
  28. loopix/api/client/api/templates/__init__.py +1 -0
  29. loopix/api/client/api/templates/delete_templates_template_id.py +157 -0
  30. loopix/api/client/api/templates/get_templates.py +172 -0
  31. loopix/api/client/api/templates/get_templates_aliases_alias.py +167 -0
  32. loopix/api/client/api/templates/get_templates_template_id.py +195 -0
  33. loopix/api/client/api/templates/get_templates_template_id_builds_build_id_logs.py +272 -0
  34. loopix/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +232 -0
  35. loopix/api/client/api/templates/get_templates_template_id_files_hash.py +180 -0
  36. loopix/api/client/api/templates/patch_templates_template_id.py +183 -0
  37. loopix/api/client/api/templates/patch_v_2_templates_template_id.py +185 -0
  38. loopix/api/client/api/templates/post_templates.py +172 -0
  39. loopix/api/client/api/templates/post_templates_template_id.py +181 -0
  40. loopix/api/client/api/templates/post_templates_template_id_builds_build_id.py +170 -0
  41. loopix/api/client/api/templates/post_v2_templates.py +172 -0
  42. loopix/api/client/api/templates/post_v3_templates.py +176 -0
  43. loopix/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +192 -0
  44. loopix/api/client/api/volumes/__init__.py +1 -0
  45. loopix/api/client/api/volumes/delete_volumes_volume_id.py +161 -0
  46. loopix/api/client/api/volumes/get_volumes.py +140 -0
  47. loopix/api/client/api/volumes/get_volumes_volume_id.py +163 -0
  48. loopix/api/client/api/volumes/post_volumes.py +172 -0
  49. loopix/api/client/client.py +286 -0
  50. loopix/api/client/errors.py +16 -0
  51. loopix/api/client/models/__init__.py +185 -0
  52. loopix/api/client/models/admin_build_cancel_result.py +67 -0
  53. loopix/api/client/models/admin_sandbox_kill_result.py +67 -0
  54. loopix/api/client/models/assign_template_tags_request.py +67 -0
  55. loopix/api/client/models/assigned_template_tags.py +68 -0
  56. loopix/api/client/models/aws_registry.py +85 -0
  57. loopix/api/client/models/aws_registry_type.py +8 -0
  58. loopix/api/client/models/build_log_entry.py +89 -0
  59. loopix/api/client/models/build_status_reason.py +95 -0
  60. loopix/api/client/models/connect_sandbox.py +59 -0
  61. loopix/api/client/models/created_access_token.py +100 -0
  62. loopix/api/client/models/created_team_api_key.py +166 -0
  63. loopix/api/client/models/delete_template_tags_request.py +67 -0
  64. loopix/api/client/models/disk_metrics.py +91 -0
  65. loopix/api/client/models/error.py +67 -0
  66. loopix/api/client/models/gcp_registry.py +69 -0
  67. loopix/api/client/models/gcp_registry_type.py +8 -0
  68. loopix/api/client/models/general_registry.py +77 -0
  69. loopix/api/client/models/general_registry_type.py +8 -0
  70. loopix/api/client/models/identifier_masking_details.py +83 -0
  71. loopix/api/client/models/listed_sandbox.py +179 -0
  72. loopix/api/client/models/log_level.py +11 -0
  73. loopix/api/client/models/logs_direction.py +9 -0
  74. loopix/api/client/models/logs_source.py +9 -0
  75. loopix/api/client/models/machine_info.py +83 -0
  76. loopix/api/client/models/max_team_metric.py +78 -0
  77. loopix/api/client/models/mcp_type_0.py +44 -0
  78. loopix/api/client/models/new_access_token.py +59 -0
  79. loopix/api/client/models/new_sandbox.py +224 -0
  80. loopix/api/client/models/new_team_api_key.py +59 -0
  81. loopix/api/client/models/new_volume.py +59 -0
  82. loopix/api/client/models/node.py +160 -0
  83. loopix/api/client/models/node_detail.py +160 -0
  84. loopix/api/client/models/node_metrics.py +122 -0
  85. loopix/api/client/models/node_status.py +12 -0
  86. loopix/api/client/models/node_status_change.py +82 -0
  87. loopix/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
  88. loopix/api/client/models/post_sandboxes_sandbox_id_snapshots_body.py +60 -0
  89. loopix/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
  90. loopix/api/client/models/resumed_sandbox.py +68 -0
  91. loopix/api/client/models/sandbox.py +145 -0
  92. loopix/api/client/models/sandbox_auto_resume_config.py +60 -0
  93. loopix/api/client/models/sandbox_detail.py +267 -0
  94. loopix/api/client/models/sandbox_lifecycle.py +70 -0
  95. loopix/api/client/models/sandbox_log.py +70 -0
  96. loopix/api/client/models/sandbox_log_entry.py +93 -0
  97. loopix/api/client/models/sandbox_log_entry_fields.py +44 -0
  98. loopix/api/client/models/sandbox_logs.py +91 -0
  99. loopix/api/client/models/sandbox_logs_v2_response.py +73 -0
  100. loopix/api/client/models/sandbox_metric.py +126 -0
  101. loopix/api/client/models/sandbox_network_config.py +118 -0
  102. loopix/api/client/models/sandbox_network_config_rules.py +72 -0
  103. loopix/api/client/models/sandbox_network_rule.py +74 -0
  104. loopix/api/client/models/sandbox_network_transform.py +79 -0
  105. loopix/api/client/models/sandbox_network_transform_headers.py +47 -0
  106. loopix/api/client/models/sandbox_network_update_config.py +114 -0
  107. loopix/api/client/models/sandbox_network_update_config_rules.py +71 -0
  108. loopix/api/client/models/sandbox_on_timeout.py +9 -0
  109. loopix/api/client/models/sandbox_pause_request.py +62 -0
  110. loopix/api/client/models/sandbox_state.py +9 -0
  111. loopix/api/client/models/sandbox_volume_mount.py +67 -0
  112. loopix/api/client/models/sandboxes_with_metrics.py +59 -0
  113. loopix/api/client/models/snapshot_info.py +70 -0
  114. loopix/api/client/models/team.py +83 -0
  115. loopix/api/client/models/team_api_key.py +158 -0
  116. loopix/api/client/models/team_metric.py +86 -0
  117. loopix/api/client/models/team_user.py +75 -0
  118. loopix/api/client/models/template.py +225 -0
  119. loopix/api/client/models/template_alias_response.py +67 -0
  120. loopix/api/client/models/template_build.py +139 -0
  121. loopix/api/client/models/template_build_file_upload.py +70 -0
  122. loopix/api/client/models/template_build_info.py +126 -0
  123. loopix/api/client/models/template_build_logs_response.py +73 -0
  124. loopix/api/client/models/template_build_request.py +115 -0
  125. loopix/api/client/models/template_build_request_v2.py +88 -0
  126. loopix/api/client/models/template_build_request_v3.py +107 -0
  127. loopix/api/client/models/template_build_start_v2.py +184 -0
  128. loopix/api/client/models/template_build_status.py +11 -0
  129. loopix/api/client/models/template_legacy.py +207 -0
  130. loopix/api/client/models/template_request_response_v3.py +99 -0
  131. loopix/api/client/models/template_step.py +91 -0
  132. loopix/api/client/models/template_tag.py +78 -0
  133. loopix/api/client/models/template_update_request.py +59 -0
  134. loopix/api/client/models/template_update_response.py +59 -0
  135. loopix/api/client/models/template_with_builds.py +156 -0
  136. loopix/api/client/models/update_team_api_key.py +59 -0
  137. loopix/api/client/models/volume.py +67 -0
  138. loopix/api/client/models/volume_and_token.py +75 -0
  139. loopix/api/client/models/volume_token.py +59 -0
  140. loopix/api/client/py.typed +1 -0
  141. loopix/api/client/types.py +54 -0
  142. loopix/api/client_async/__init__.py +74 -0
  143. loopix/api/client_sync/__init__.py +73 -0
  144. loopix/api/metadata.py +14 -0
  145. loopix/connection_config.py +309 -0
  146. loopix/envd/api.py +170 -0
  147. loopix/envd/filesystem/filesystem_connect.py +193 -0
  148. loopix/envd/filesystem/filesystem_pb2.py +80 -0
  149. loopix/envd/filesystem/filesystem_pb2.pyi +272 -0
  150. loopix/envd/process/process_connect.py +174 -0
  151. loopix/envd/process/process_pb2.py +96 -0
  152. loopix/envd/process/process_pb2.pyi +316 -0
  153. loopix/envd/rpc.py +139 -0
  154. loopix/envd/versions.py +11 -0
  155. loopix/exceptions.py +133 -0
  156. loopix/io_utils.py +57 -0
  157. loopix/paginator.py +52 -0
  158. loopix/py.typed +0 -0
  159. loopix/sandbox/_git/__init__.py +85 -0
  160. loopix/sandbox/_git/args.py +363 -0
  161. loopix/sandbox/_git/auth.py +132 -0
  162. loopix/sandbox/_git/config.py +32 -0
  163. loopix/sandbox/_git/parse.py +222 -0
  164. loopix/sandbox/_git/types.py +149 -0
  165. loopix/sandbox/commands/command_handle.py +69 -0
  166. loopix/sandbox/commands/main.py +39 -0
  167. loopix/sandbox/filesystem/filesystem.py +337 -0
  168. loopix/sandbox/filesystem/watch_handle.py +70 -0
  169. loopix/sandbox/main.py +227 -0
  170. loopix/sandbox/mcp.py +1949 -0
  171. loopix/sandbox/network.py +8 -0
  172. loopix/sandbox/sandbox_api.py +624 -0
  173. loopix/sandbox/signature.py +47 -0
  174. loopix/sandbox/utils.py +34 -0
  175. loopix/sandbox_async/commands/command.py +396 -0
  176. loopix/sandbox_async/commands/command_handle.py +298 -0
  177. loopix/sandbox_async/commands/pty.py +257 -0
  178. loopix/sandbox_async/filesystem/filesystem.py +720 -0
  179. loopix/sandbox_async/filesystem/watch_handle.py +97 -0
  180. loopix/sandbox_async/git.py +1100 -0
  181. loopix/sandbox_async/main.py +987 -0
  182. loopix/sandbox_async/paginator.py +140 -0
  183. loopix/sandbox_async/sandbox_api.py +504 -0
  184. loopix/sandbox_async/utils.py +7 -0
  185. loopix/sandbox_domains.py +5 -0
  186. loopix/sandbox_sync/commands/command.py +420 -0
  187. loopix/sandbox_sync/commands/command_handle.py +239 -0
  188. loopix/sandbox_sync/commands/pty.py +279 -0
  189. loopix/sandbox_sync/filesystem/filesystem.py +710 -0
  190. loopix/sandbox_sync/filesystem/watch_handle.py +102 -0
  191. loopix/sandbox_sync/git.py +1077 -0
  192. loopix/sandbox_sync/main.py +975 -0
  193. loopix/sandbox_sync/paginator.py +140 -0
  194. loopix/sandbox_sync/sandbox_api.py +491 -0
  195. loopix/template/consts.py +45 -0
  196. loopix/template/dockerfile_parser.py +286 -0
  197. loopix/template/logger.py +232 -0
  198. loopix/template/main.py +1368 -0
  199. loopix/template/readycmd.py +144 -0
  200. loopix/template/types.py +194 -0
  201. loopix/template/utils.py +426 -0
  202. loopix/template_async/build_api.py +419 -0
  203. loopix/template_async/main.py +528 -0
  204. loopix/template_sync/build_api.py +409 -0
  205. loopix/template_sync/main.py +529 -0
  206. loopix/volume/client/__init__.py +8 -0
  207. loopix/volume/client/api/__init__.py +1 -0
  208. loopix/volume/client/api/volumes/__init__.py +1 -0
  209. loopix/volume/client/api/volumes/delete_volumecontent_volume_id_path.py +174 -0
  210. loopix/volume/client/api/volumes/get_volumecontent_volume_id_dir.py +204 -0
  211. loopix/volume/client/api/volumes/get_volumecontent_volume_id_file.py +179 -0
  212. loopix/volume/client/api/volumes/get_volumecontent_volume_id_path.py +176 -0
  213. loopix/volume/client/api/volumes/patch_volumecontent_volume_id_path.py +203 -0
  214. loopix/volume/client/api/volumes/post_volumecontent_volume_id_dir.py +239 -0
  215. loopix/volume/client/api/volumes/put_volumecontent_volume_id_file.py +259 -0
  216. loopix/volume/client/client.py +286 -0
  217. loopix/volume/client/errors.py +16 -0
  218. loopix/volume/client/models/__init__.py +13 -0
  219. loopix/volume/client/models/error.py +67 -0
  220. loopix/volume/client/models/patch_volumecontent_volume_id_path_body.py +77 -0
  221. loopix/volume/client/models/volume_entry_stat.py +145 -0
  222. loopix/volume/client/models/volume_entry_stat_type.py +11 -0
  223. loopix/volume/client/py.typed +1 -0
  224. loopix/volume/client/types.py +54 -0
  225. loopix/volume/client_async/__init__.py +88 -0
  226. loopix/volume/client_sync/__init__.py +80 -0
  227. loopix/volume/connection_config.py +145 -0
  228. loopix/volume/types.py +62 -0
  229. loopix/volume/utils.py +52 -0
  230. loopix/volume/volume_async.py +639 -0
  231. loopix/volume/volume_sync.py +639 -0
  232. loopix_connect/__init__.py +1 -0
  233. loopix_connect/client.py +534 -0
  234. loopix_connect/py.typed +0 -0
  235. loopix_sdk-2.30.0.dist-info/METADATA +98 -0
  236. loopix_sdk-2.30.0.dist-info/RECORD +238 -0
  237. loopix_sdk-2.30.0.dist-info/WHEEL +4 -0
  238. loopix_sdk-2.30.0.dist-info/licenses/LICENSE +9 -0
@@ -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
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
+