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,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}")