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/__init__.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Secure sandboxed cloud environments made for AI agents and AI apps.
|
|
3
|
+
|
|
4
|
+
Check docs [here](https://vm.betmandu.net/docs).
|
|
5
|
+
|
|
6
|
+
Loopix Sandbox is a secure cloud sandbox environment made for AI agents and AI
|
|
7
|
+
apps.
|
|
8
|
+
Sandboxes allow AI agents and apps to have long running cloud secure environments.
|
|
9
|
+
In these environments, large language models can use the same tools as humans do.
|
|
10
|
+
|
|
11
|
+
Loopix Python SDK supports both sync and async API:
|
|
12
|
+
|
|
13
|
+
```py
|
|
14
|
+
from loopix import Sandbox
|
|
15
|
+
|
|
16
|
+
# Create sandbox
|
|
17
|
+
sandbox = Sandbox.create()
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```py
|
|
21
|
+
from loopix import AsyncSandbox
|
|
22
|
+
|
|
23
|
+
# Create sandbox
|
|
24
|
+
sandbox = await AsyncSandbox.create()
|
|
25
|
+
```
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from .api import (
|
|
29
|
+
ApiClient,
|
|
30
|
+
client,
|
|
31
|
+
)
|
|
32
|
+
from .connection_config import (
|
|
33
|
+
ApiParams,
|
|
34
|
+
ConnectionConfig,
|
|
35
|
+
ProxyTypes,
|
|
36
|
+
Username,
|
|
37
|
+
)
|
|
38
|
+
from .volume.connection_config import VolumeApiParams, VolumeConnectionConfig
|
|
39
|
+
from .exceptions import (
|
|
40
|
+
AuthenticationException,
|
|
41
|
+
FileNotFoundException,
|
|
42
|
+
GitAuthException,
|
|
43
|
+
GitUpstreamException,
|
|
44
|
+
BuildException,
|
|
45
|
+
FileUploadException,
|
|
46
|
+
InvalidArgumentException,
|
|
47
|
+
NotEnoughSpaceException,
|
|
48
|
+
NotFoundException,
|
|
49
|
+
RateLimitException,
|
|
50
|
+
SandboxException,
|
|
51
|
+
SandboxNotFoundException,
|
|
52
|
+
TemplateException,
|
|
53
|
+
TimeoutException,
|
|
54
|
+
VolumeException,
|
|
55
|
+
)
|
|
56
|
+
from .sandbox.commands.command_handle import (
|
|
57
|
+
CommandExitException,
|
|
58
|
+
CommandResult,
|
|
59
|
+
PtyOutput,
|
|
60
|
+
PtySize,
|
|
61
|
+
Stderr,
|
|
62
|
+
Stdout,
|
|
63
|
+
)
|
|
64
|
+
from .sandbox.commands.main import ProcessInfo
|
|
65
|
+
from .sandbox.filesystem.filesystem import EntryInfo, FileType, WriteInfo
|
|
66
|
+
from .sandbox.filesystem.watch_handle import (
|
|
67
|
+
FilesystemEvent,
|
|
68
|
+
FilesystemEventType,
|
|
69
|
+
)
|
|
70
|
+
from .sandbox._git import GitBranches, GitFileStatus, GitResetMode, GitStatus
|
|
71
|
+
from .sandbox_sync.git import Git
|
|
72
|
+
from .sandbox.network import ALL_TRAFFIC
|
|
73
|
+
from .sandbox.signature import get_signature
|
|
74
|
+
from .sandbox.sandbox_api import (
|
|
75
|
+
GitHubMcpServer,
|
|
76
|
+
GitHubMcpServerConfig,
|
|
77
|
+
McpServer,
|
|
78
|
+
SandboxInfo,
|
|
79
|
+
SandboxInfoLifecycle,
|
|
80
|
+
SandboxMetrics,
|
|
81
|
+
SandboxLifecycle,
|
|
82
|
+
SandboxOnTimeout,
|
|
83
|
+
SandboxNetworkInfo,
|
|
84
|
+
SandboxNetworkOpts,
|
|
85
|
+
SandboxNetworkRule,
|
|
86
|
+
SandboxNetworkRuleInfo,
|
|
87
|
+
SandboxNetworkRules,
|
|
88
|
+
SandboxNetworkSelector,
|
|
89
|
+
SandboxNetworkSelectorContext,
|
|
90
|
+
SandboxNetworkTransform,
|
|
91
|
+
SandboxNetworkUpdate,
|
|
92
|
+
SandboxQuery,
|
|
93
|
+
SandboxState,
|
|
94
|
+
SnapshotInfo,
|
|
95
|
+
)
|
|
96
|
+
from .sandbox_async.commands.command_handle import AsyncCommandHandle
|
|
97
|
+
from .sandbox_async.filesystem.watch_handle import AsyncWatchHandle
|
|
98
|
+
from .sandbox_async.main import AsyncSandbox
|
|
99
|
+
from .sandbox_async.paginator import AsyncSandboxPaginator, AsyncSnapshotPaginator
|
|
100
|
+
from .sandbox_async.utils import OutputHandler
|
|
101
|
+
from .sandbox_sync.commands.command_handle import CommandHandle
|
|
102
|
+
from .sandbox_sync.filesystem.watch_handle import WatchHandle
|
|
103
|
+
from .sandbox_sync.main import Sandbox
|
|
104
|
+
from .sandbox_sync.paginator import SandboxPaginator, SnapshotPaginator
|
|
105
|
+
from .template.logger import (
|
|
106
|
+
LogEntry,
|
|
107
|
+
LogEntryEnd,
|
|
108
|
+
LogEntryLevel,
|
|
109
|
+
LogEntryStart,
|
|
110
|
+
default_build_logger,
|
|
111
|
+
)
|
|
112
|
+
from .template.main import TemplateBase, TemplateClass
|
|
113
|
+
from .template.readycmd import (
|
|
114
|
+
ReadyCmd,
|
|
115
|
+
wait_for_file,
|
|
116
|
+
wait_for_port,
|
|
117
|
+
wait_for_process,
|
|
118
|
+
wait_for_timeout,
|
|
119
|
+
wait_for_url,
|
|
120
|
+
)
|
|
121
|
+
from .template.types import (
|
|
122
|
+
BuildInfo,
|
|
123
|
+
BuildStatusReason,
|
|
124
|
+
CopyItem,
|
|
125
|
+
TemplateBuildStatus,
|
|
126
|
+
TemplateBuildStatusResponse,
|
|
127
|
+
TemplateTag,
|
|
128
|
+
TemplateTagInfo,
|
|
129
|
+
)
|
|
130
|
+
from .template_async.main import AsyncTemplate
|
|
131
|
+
from .template_sync.main import Template
|
|
132
|
+
|
|
133
|
+
from .volume.volume_sync import Volume
|
|
134
|
+
from .volume.volume_async import AsyncVolume
|
|
135
|
+
from .volume.types import (
|
|
136
|
+
VolumeInfo,
|
|
137
|
+
VolumeAndToken,
|
|
138
|
+
VolumeEntryStat,
|
|
139
|
+
VolumeFileType,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
__all__ = [
|
|
143
|
+
# API
|
|
144
|
+
"ApiClient",
|
|
145
|
+
"client",
|
|
146
|
+
# Connection config
|
|
147
|
+
"ConnectionConfig",
|
|
148
|
+
"VolumeConnectionConfig",
|
|
149
|
+
"ProxyTypes",
|
|
150
|
+
"ApiParams",
|
|
151
|
+
"VolumeApiParams",
|
|
152
|
+
"Username",
|
|
153
|
+
# Exceptions
|
|
154
|
+
"SandboxException",
|
|
155
|
+
"TimeoutException",
|
|
156
|
+
"NotFoundException",
|
|
157
|
+
"FileNotFoundException",
|
|
158
|
+
"SandboxNotFoundException",
|
|
159
|
+
"AuthenticationException",
|
|
160
|
+
"GitAuthException",
|
|
161
|
+
"GitUpstreamException",
|
|
162
|
+
"InvalidArgumentException",
|
|
163
|
+
"NotEnoughSpaceException",
|
|
164
|
+
"TemplateException",
|
|
165
|
+
"BuildException",
|
|
166
|
+
"FileUploadException",
|
|
167
|
+
"RateLimitException",
|
|
168
|
+
"VolumeException",
|
|
169
|
+
# Sandbox API
|
|
170
|
+
"SandboxInfo",
|
|
171
|
+
"SandboxInfoLifecycle",
|
|
172
|
+
"SandboxMetrics",
|
|
173
|
+
"ProcessInfo",
|
|
174
|
+
"SandboxQuery",
|
|
175
|
+
"SandboxState",
|
|
176
|
+
"SandboxMetrics",
|
|
177
|
+
"GitStatus",
|
|
178
|
+
"GitBranches",
|
|
179
|
+
"GitFileStatus",
|
|
180
|
+
"GitResetMode",
|
|
181
|
+
# Command handle
|
|
182
|
+
"CommandResult",
|
|
183
|
+
"Stderr",
|
|
184
|
+
"Stdout",
|
|
185
|
+
"CommandExitException",
|
|
186
|
+
"PtyOutput",
|
|
187
|
+
"PtySize",
|
|
188
|
+
# Filesystem
|
|
189
|
+
"FilesystemEvent",
|
|
190
|
+
"FilesystemEventType",
|
|
191
|
+
"EntryInfo",
|
|
192
|
+
"WriteInfo",
|
|
193
|
+
"FileType",
|
|
194
|
+
# Network
|
|
195
|
+
"SandboxNetworkOpts",
|
|
196
|
+
"SandboxNetworkInfo",
|
|
197
|
+
"SandboxNetworkSelector",
|
|
198
|
+
"SandboxNetworkSelectorContext",
|
|
199
|
+
"SandboxNetworkRule",
|
|
200
|
+
"SandboxNetworkRuleInfo",
|
|
201
|
+
"SandboxNetworkRules",
|
|
202
|
+
"SandboxNetworkTransform",
|
|
203
|
+
"SandboxNetworkUpdate",
|
|
204
|
+
"SandboxLifecycle",
|
|
205
|
+
"SandboxOnTimeout",
|
|
206
|
+
"ALL_TRAFFIC",
|
|
207
|
+
# Snapshot
|
|
208
|
+
"SnapshotInfo",
|
|
209
|
+
"SnapshotPaginator",
|
|
210
|
+
"AsyncSnapshotPaginator",
|
|
211
|
+
# Signature
|
|
212
|
+
"get_signature",
|
|
213
|
+
# Sync sandbox
|
|
214
|
+
"Sandbox",
|
|
215
|
+
"SandboxPaginator",
|
|
216
|
+
"WatchHandle",
|
|
217
|
+
"CommandHandle",
|
|
218
|
+
# Async sandbox
|
|
219
|
+
"OutputHandler",
|
|
220
|
+
"AsyncSandboxPaginator",
|
|
221
|
+
"AsyncSandbox",
|
|
222
|
+
"AsyncWatchHandle",
|
|
223
|
+
"AsyncCommandHandle",
|
|
224
|
+
# Template
|
|
225
|
+
"Template",
|
|
226
|
+
"AsyncTemplate",
|
|
227
|
+
"TemplateBase",
|
|
228
|
+
"TemplateClass",
|
|
229
|
+
"CopyItem",
|
|
230
|
+
"BuildInfo",
|
|
231
|
+
"BuildStatusReason",
|
|
232
|
+
"TemplateBuildStatus",
|
|
233
|
+
"TemplateBuildStatusResponse",
|
|
234
|
+
"TemplateTag",
|
|
235
|
+
"TemplateTagInfo",
|
|
236
|
+
"ReadyCmd",
|
|
237
|
+
"wait_for_file",
|
|
238
|
+
"wait_for_url",
|
|
239
|
+
"wait_for_port",
|
|
240
|
+
"wait_for_process",
|
|
241
|
+
"wait_for_timeout",
|
|
242
|
+
"LogEntry",
|
|
243
|
+
"LogEntryStart",
|
|
244
|
+
"LogEntryEnd",
|
|
245
|
+
"LogEntryLevel",
|
|
246
|
+
"default_build_logger",
|
|
247
|
+
# MCP
|
|
248
|
+
"McpServer",
|
|
249
|
+
"GitHubMcpServer",
|
|
250
|
+
"GitHubMcpServerConfig",
|
|
251
|
+
# Git
|
|
252
|
+
"Git",
|
|
253
|
+
# Volume
|
|
254
|
+
"Volume",
|
|
255
|
+
"AsyncVolume",
|
|
256
|
+
"VolumeInfo",
|
|
257
|
+
"VolumeAndToken",
|
|
258
|
+
"VolumeEntryStat",
|
|
259
|
+
"VolumeFileType",
|
|
260
|
+
]
|
loopix/api/__init__.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import threading
|
|
7
|
+
import weakref
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from types import TracebackType
|
|
10
|
+
from typing import Callable, Optional, Protocol, Union
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from httpx import AsyncBaseTransport, BaseTransport, Limits, Timeout
|
|
14
|
+
|
|
15
|
+
from loopix.api.client.client import AuthenticatedClient
|
|
16
|
+
from loopix.api.client.types import Response
|
|
17
|
+
from loopix.api.metadata import default_headers
|
|
18
|
+
from loopix.connection_config import ConnectionConfig
|
|
19
|
+
from loopix.exceptions import (
|
|
20
|
+
AuthenticationException,
|
|
21
|
+
RateLimitException,
|
|
22
|
+
SandboxException,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def make_logging_event_hooks(log: Optional[logging.Logger]) -> dict:
|
|
27
|
+
"""Build synchronous httpx ``event_hooks`` that log requests and responses
|
|
28
|
+
to the given ``logging.Logger``. Requests log at ``INFO``, successful
|
|
29
|
+
responses at ``INFO`` and responses with status >= 400 at ``ERROR``.
|
|
30
|
+
|
|
31
|
+
Returns no hooks when ``log`` is ``None`` so that nothing is logged unless a
|
|
32
|
+
logger was explicitly supplied."""
|
|
33
|
+
if log is None:
|
|
34
|
+
return {}
|
|
35
|
+
|
|
36
|
+
def on_request(request) -> None:
|
|
37
|
+
log.info(f"Request {request.method} {request.url}")
|
|
38
|
+
|
|
39
|
+
def on_response(response: Response) -> None:
|
|
40
|
+
if response.status_code >= 400:
|
|
41
|
+
log.error(f"Response {response.status_code}")
|
|
42
|
+
else:
|
|
43
|
+
log.info(f"Response {response.status_code}")
|
|
44
|
+
|
|
45
|
+
return {"request": [on_request], "response": [on_response]}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def make_async_logging_event_hooks(log: Optional[logging.Logger]) -> dict:
|
|
49
|
+
"""Asynchronous counterpart of :func:`make_logging_event_hooks`."""
|
|
50
|
+
if log is None:
|
|
51
|
+
return {}
|
|
52
|
+
|
|
53
|
+
async def on_request(request) -> None:
|
|
54
|
+
log.info(f"Request {request.method} {request.url}")
|
|
55
|
+
|
|
56
|
+
async def on_response(response: Response) -> None:
|
|
57
|
+
if response.status_code >= 400:
|
|
58
|
+
log.error(f"Response {response.status_code}")
|
|
59
|
+
else:
|
|
60
|
+
log.info(f"Response {response.status_code}")
|
|
61
|
+
|
|
62
|
+
return {"request": [on_request], "response": [on_response]}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
limits = Limits(
|
|
66
|
+
max_keepalive_connections=int(os.getenv("LOOPIX_MAX_KEEPALIVE_CONNECTIONS", "20")),
|
|
67
|
+
max_connections=int(os.getenv("LOOPIX_MAX_CONNECTIONS", "2000")),
|
|
68
|
+
keepalive_expiry=int(os.getenv("LOOPIX_KEEPALIVE_EXPIRY", "300")),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
connection_retries = int(os.getenv("LOOPIX_CONNECTION_RETRIES", "3"))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class SandboxCreateResponse:
|
|
76
|
+
sandbox_id: str
|
|
77
|
+
sandbox_domain: Optional[str]
|
|
78
|
+
envd_version: str
|
|
79
|
+
envd_access_token: Optional[str]
|
|
80
|
+
traffic_access_token: Optional[str]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def handle_api_exception(
|
|
84
|
+
e: "SupportsApiErrorResponse",
|
|
85
|
+
default_exception_class: type[Exception] = SandboxException,
|
|
86
|
+
stack_trace: Optional[TracebackType] = None,
|
|
87
|
+
):
|
|
88
|
+
try:
|
|
89
|
+
body = json.loads(e.content) if e.content else {}
|
|
90
|
+
except json.JSONDecodeError:
|
|
91
|
+
body = {}
|
|
92
|
+
|
|
93
|
+
if e.status_code == 401:
|
|
94
|
+
message = f"{e.status_code}: Unauthorized, please check your credentials."
|
|
95
|
+
if body.get("message"):
|
|
96
|
+
message += f" - {body['message']}"
|
|
97
|
+
return AuthenticationException(message)
|
|
98
|
+
|
|
99
|
+
if e.status_code == 429:
|
|
100
|
+
message = f"{e.status_code}: Rate limit exceeded, please try again later."
|
|
101
|
+
if body.get("message"):
|
|
102
|
+
message += f" - {body['message']}"
|
|
103
|
+
return RateLimitException(message)
|
|
104
|
+
|
|
105
|
+
if "message" in body:
|
|
106
|
+
return default_exception_class(
|
|
107
|
+
f"{e.status_code}: {body['message']}"
|
|
108
|
+
).with_traceback(stack_trace)
|
|
109
|
+
return default_exception_class(f"{e.status_code}: {e.content}").with_traceback(
|
|
110
|
+
stack_trace
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class SupportsApiErrorResponse(Protocol):
|
|
115
|
+
@property
|
|
116
|
+
def status_code(self) -> int: ...
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def content(self) -> Union[str, bytes]: ...
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
_API_KEY_PATTERN = re.compile(r"\Alpx_[0-9a-f]+\Z")
|
|
123
|
+
_API_KEY_EXAMPLE = "lpx_" + "0" * 40
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def validate_api_key(api_key: str) -> None:
|
|
127
|
+
"""Validate that an Loopix API key has the expected ``lpx_`` prefix
|
|
128
|
+
followed by hex characters. Raises ``AuthenticationException`` otherwise.
|
|
129
|
+
"""
|
|
130
|
+
if not _API_KEY_PATTERN.match(api_key):
|
|
131
|
+
raise AuthenticationException(
|
|
132
|
+
'Invalid API key format: expected "lpx_" followed by hex '
|
|
133
|
+
f'characters (e.g. "{_API_KEY_EXAMPLE}"). '
|
|
134
|
+
"Visit the API Keys tab at https://vm.betmandu.net/dashboard?tab=keys to get your API key."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ApiClient(AuthenticatedClient):
|
|
139
|
+
"""
|
|
140
|
+
The client for interacting with the Loopix API.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def __init__(
|
|
144
|
+
self,
|
|
145
|
+
config: ConnectionConfig,
|
|
146
|
+
transport: Optional[Union[BaseTransport, AsyncBaseTransport]] = None,
|
|
147
|
+
transport_factory: Optional[Callable[[], BaseTransport]] = None,
|
|
148
|
+
async_transport_factory: Optional[Callable[[], AsyncBaseTransport]] = None,
|
|
149
|
+
*args,
|
|
150
|
+
**kwargs,
|
|
151
|
+
):
|
|
152
|
+
if transport is not None and (
|
|
153
|
+
transport_factory is not None or async_transport_factory is not None
|
|
154
|
+
):
|
|
155
|
+
raise ValueError("Use either transport or transport_factory, not both")
|
|
156
|
+
|
|
157
|
+
self._transport_factory = transport_factory
|
|
158
|
+
self._async_transport_factory = async_transport_factory
|
|
159
|
+
self._thread_local = threading.local()
|
|
160
|
+
# Keyed weakly by the event loop object itself, not id(loop) —
|
|
161
|
+
# CPython reuses object ids, so a new loop could otherwise inherit
|
|
162
|
+
# a client bound to a previous, closed loop.
|
|
163
|
+
self._async_clients: weakref.WeakKeyDictionary[
|
|
164
|
+
asyncio.AbstractEventLoop, httpx.AsyncClient
|
|
165
|
+
] = weakref.WeakKeyDictionary()
|
|
166
|
+
self._proxy = config.proxy
|
|
167
|
+
|
|
168
|
+
if config.api_key is None:
|
|
169
|
+
raise AuthenticationException(
|
|
170
|
+
"API key is required, please visit the API Keys tab at https://vm.betmandu.net/dashboard?tab=keys to get your API key. "
|
|
171
|
+
"You can either set the environment variable `LOOPIX_API_KEY` "
|
|
172
|
+
'or you can pass it directly to the method like api_key="lpx_..."',
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if config.api_key is not None and config.validate_api_key:
|
|
176
|
+
validate_api_key(config.api_key)
|
|
177
|
+
|
|
178
|
+
token = config.api_key
|
|
179
|
+
auth_header_name = "X-API-KEY"
|
|
180
|
+
prefix = ""
|
|
181
|
+
|
|
182
|
+
self._logger = config.logger
|
|
183
|
+
|
|
184
|
+
headers = {
|
|
185
|
+
**default_headers,
|
|
186
|
+
# Deprecated: send the access token alongside the API key when one
|
|
187
|
+
# is available, mirroring the JS SDK. Prefer `api_headers` instead.
|
|
188
|
+
# Spread before `config.headers` so a custom `Authorization` in
|
|
189
|
+
# `api_headers` wins over the deprecated access token, matching JS.
|
|
190
|
+
**(
|
|
191
|
+
{"Authorization": f"Bearer {config.access_token}"}
|
|
192
|
+
if config.access_token is not None
|
|
193
|
+
else {}
|
|
194
|
+
),
|
|
195
|
+
**(config.headers or {}),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# Prevent passing these parameters twice
|
|
199
|
+
more_headers: Optional[dict] = kwargs.pop("headers", None)
|
|
200
|
+
if more_headers:
|
|
201
|
+
headers.update(more_headers)
|
|
202
|
+
kwargs.pop("token", None)
|
|
203
|
+
kwargs.pop("auth_header_name", None)
|
|
204
|
+
kwargs.pop("prefix", None)
|
|
205
|
+
|
|
206
|
+
httpx_args = {
|
|
207
|
+
"event_hooks": self._logging_event_hooks(),
|
|
208
|
+
}
|
|
209
|
+
if transport is not None:
|
|
210
|
+
httpx_args["transport"] = transport
|
|
211
|
+
if (
|
|
212
|
+
transport is None
|
|
213
|
+
and transport_factory is None
|
|
214
|
+
and async_transport_factory is None
|
|
215
|
+
):
|
|
216
|
+
httpx_args["proxy"] = config.proxy
|
|
217
|
+
|
|
218
|
+
# config.request_timeout is None when the timeout is explicitly
|
|
219
|
+
# disabled (request_timeout=0), which httpx.Timeout(None) preserves.
|
|
220
|
+
kwargs.setdefault("timeout", Timeout(config.request_timeout))
|
|
221
|
+
|
|
222
|
+
super().__init__(
|
|
223
|
+
base_url=config.api_url,
|
|
224
|
+
httpx_args=httpx_args,
|
|
225
|
+
headers=headers,
|
|
226
|
+
token=token or "",
|
|
227
|
+
auth_header_name=auth_header_name,
|
|
228
|
+
prefix=prefix,
|
|
229
|
+
*args,
|
|
230
|
+
**kwargs,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def _logging_event_hooks(self) -> dict:
|
|
234
|
+
return make_logging_event_hooks(self._logger)
|
|
235
|
+
|
|
236
|
+
def _headers_with_auth(self) -> dict:
|
|
237
|
+
return {
|
|
238
|
+
**self._headers,
|
|
239
|
+
self.auth_header_name: (
|
|
240
|
+
f"{self.prefix} {self.token}" if self.prefix else self.token
|
|
241
|
+
),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
def get_httpx_client(self) -> httpx.Client:
|
|
245
|
+
if self._client is not None or self._transport_factory is None:
|
|
246
|
+
return super().get_httpx_client()
|
|
247
|
+
|
|
248
|
+
client = getattr(self._thread_local, "client", None)
|
|
249
|
+
if client is None:
|
|
250
|
+
client = httpx.Client(
|
|
251
|
+
base_url=self._base_url,
|
|
252
|
+
cookies=self._cookies,
|
|
253
|
+
headers=self._headers_with_auth(),
|
|
254
|
+
timeout=self._timeout,
|
|
255
|
+
verify=self._verify_ssl,
|
|
256
|
+
follow_redirects=self._follow_redirects,
|
|
257
|
+
event_hooks=self._httpx_args.get("event_hooks"),
|
|
258
|
+
transport=self._transport_factory(),
|
|
259
|
+
)
|
|
260
|
+
self._thread_local.client = client
|
|
261
|
+
return client
|
|
262
|
+
|
|
263
|
+
def get_async_httpx_client(self) -> httpx.AsyncClient:
|
|
264
|
+
if self._async_client is not None or self._async_transport_factory is None:
|
|
265
|
+
return super().get_async_httpx_client()
|
|
266
|
+
|
|
267
|
+
loop = asyncio.get_running_loop()
|
|
268
|
+
client = self._async_clients.get(loop)
|
|
269
|
+
if client is None:
|
|
270
|
+
client = httpx.AsyncClient(
|
|
271
|
+
base_url=self._base_url,
|
|
272
|
+
cookies=self._cookies,
|
|
273
|
+
headers=self._headers_with_auth(),
|
|
274
|
+
timeout=self._timeout,
|
|
275
|
+
verify=self._verify_ssl,
|
|
276
|
+
follow_redirects=self._follow_redirects,
|
|
277
|
+
event_hooks=self._httpx_args.get("event_hooks"),
|
|
278
|
+
transport=self._async_transport_factory(),
|
|
279
|
+
)
|
|
280
|
+
self._async_clients[loop] = client
|
|
281
|
+
return client
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# We need to override the logging hooks for the async usage
|
|
285
|
+
class AsyncApiClient(ApiClient):
|
|
286
|
+
def _logging_event_hooks(self) -> dict:
|
|
287
|
+
return make_async_logging_event_hooks(self._logger)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Contains methods for accessing the API"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Contains endpoint functions for accessing the API"""
|