loopix-sdk 2.30.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- loopix/__init__.py +260 -0
- loopix/api/__init__.py +287 -0
- loopix/api/client/__init__.py +8 -0
- loopix/api/client/api/__init__.py +1 -0
- loopix/api/client/api/sandboxes/__init__.py +1 -0
- loopix/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
- loopix/api/client/api/sandboxes/get_sandboxes.py +176 -0
- loopix/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +212 -0
- loopix/api/client/api/sandboxes/get_v2_sandboxes.py +230 -0
- loopix/api/client/api/sandboxes/get_v_2_sandboxes_sandbox_id_logs.py +254 -0
- loopix/api/client/api/sandboxes/post_sandboxes.py +172 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +187 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +181 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +189 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py +195 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +193 -0
- loopix/api/client/api/sandboxes/put_sandboxes_sandbox_id_network.py +199 -0
- loopix/api/client/api/snapshots/__init__.py +1 -0
- loopix/api/client/api/snapshots/get_snapshots.py +202 -0
- loopix/api/client/api/tags/__init__.py +1 -0
- loopix/api/client/api/tags/delete_templates_tags.py +174 -0
- loopix/api/client/api/tags/get_templates_template_id_tags.py +172 -0
- loopix/api/client/api/tags/post_templates_tags.py +176 -0
- loopix/api/client/api/templates/__init__.py +1 -0
- loopix/api/client/api/templates/delete_templates_template_id.py +157 -0
- loopix/api/client/api/templates/get_templates.py +172 -0
- loopix/api/client/api/templates/get_templates_aliases_alias.py +167 -0
- loopix/api/client/api/templates/get_templates_template_id.py +195 -0
- loopix/api/client/api/templates/get_templates_template_id_builds_build_id_logs.py +272 -0
- loopix/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +232 -0
- loopix/api/client/api/templates/get_templates_template_id_files_hash.py +180 -0
- loopix/api/client/api/templates/patch_templates_template_id.py +183 -0
- loopix/api/client/api/templates/patch_v_2_templates_template_id.py +185 -0
- loopix/api/client/api/templates/post_templates.py +172 -0
- loopix/api/client/api/templates/post_templates_template_id.py +181 -0
- loopix/api/client/api/templates/post_templates_template_id_builds_build_id.py +170 -0
- loopix/api/client/api/templates/post_v2_templates.py +172 -0
- loopix/api/client/api/templates/post_v3_templates.py +176 -0
- loopix/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +192 -0
- loopix/api/client/api/volumes/__init__.py +1 -0
- loopix/api/client/api/volumes/delete_volumes_volume_id.py +161 -0
- loopix/api/client/api/volumes/get_volumes.py +140 -0
- loopix/api/client/api/volumes/get_volumes_volume_id.py +163 -0
- loopix/api/client/api/volumes/post_volumes.py +172 -0
- loopix/api/client/client.py +286 -0
- loopix/api/client/errors.py +16 -0
- loopix/api/client/models/__init__.py +185 -0
- loopix/api/client/models/admin_build_cancel_result.py +67 -0
- loopix/api/client/models/admin_sandbox_kill_result.py +67 -0
- loopix/api/client/models/assign_template_tags_request.py +67 -0
- loopix/api/client/models/assigned_template_tags.py +68 -0
- loopix/api/client/models/aws_registry.py +85 -0
- loopix/api/client/models/aws_registry_type.py +8 -0
- loopix/api/client/models/build_log_entry.py +89 -0
- loopix/api/client/models/build_status_reason.py +95 -0
- loopix/api/client/models/connect_sandbox.py +59 -0
- loopix/api/client/models/created_access_token.py +100 -0
- loopix/api/client/models/created_team_api_key.py +166 -0
- loopix/api/client/models/delete_template_tags_request.py +67 -0
- loopix/api/client/models/disk_metrics.py +91 -0
- loopix/api/client/models/error.py +67 -0
- loopix/api/client/models/gcp_registry.py +69 -0
- loopix/api/client/models/gcp_registry_type.py +8 -0
- loopix/api/client/models/general_registry.py +77 -0
- loopix/api/client/models/general_registry_type.py +8 -0
- loopix/api/client/models/identifier_masking_details.py +83 -0
- loopix/api/client/models/listed_sandbox.py +179 -0
- loopix/api/client/models/log_level.py +11 -0
- loopix/api/client/models/logs_direction.py +9 -0
- loopix/api/client/models/logs_source.py +9 -0
- loopix/api/client/models/machine_info.py +83 -0
- loopix/api/client/models/max_team_metric.py +78 -0
- loopix/api/client/models/mcp_type_0.py +44 -0
- loopix/api/client/models/new_access_token.py +59 -0
- loopix/api/client/models/new_sandbox.py +224 -0
- loopix/api/client/models/new_team_api_key.py +59 -0
- loopix/api/client/models/new_volume.py +59 -0
- loopix/api/client/models/node.py +160 -0
- loopix/api/client/models/node_detail.py +160 -0
- loopix/api/client/models/node_metrics.py +122 -0
- loopix/api/client/models/node_status.py +12 -0
- loopix/api/client/models/node_status_change.py +82 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_snapshots_body.py +60 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
- loopix/api/client/models/resumed_sandbox.py +68 -0
- loopix/api/client/models/sandbox.py +145 -0
- loopix/api/client/models/sandbox_auto_resume_config.py +60 -0
- loopix/api/client/models/sandbox_detail.py +267 -0
- loopix/api/client/models/sandbox_lifecycle.py +70 -0
- loopix/api/client/models/sandbox_log.py +70 -0
- loopix/api/client/models/sandbox_log_entry.py +93 -0
- loopix/api/client/models/sandbox_log_entry_fields.py +44 -0
- loopix/api/client/models/sandbox_logs.py +91 -0
- loopix/api/client/models/sandbox_logs_v2_response.py +73 -0
- loopix/api/client/models/sandbox_metric.py +126 -0
- loopix/api/client/models/sandbox_network_config.py +118 -0
- loopix/api/client/models/sandbox_network_config_rules.py +72 -0
- loopix/api/client/models/sandbox_network_rule.py +74 -0
- loopix/api/client/models/sandbox_network_transform.py +79 -0
- loopix/api/client/models/sandbox_network_transform_headers.py +47 -0
- loopix/api/client/models/sandbox_network_update_config.py +114 -0
- loopix/api/client/models/sandbox_network_update_config_rules.py +71 -0
- loopix/api/client/models/sandbox_on_timeout.py +9 -0
- loopix/api/client/models/sandbox_pause_request.py +62 -0
- loopix/api/client/models/sandbox_state.py +9 -0
- loopix/api/client/models/sandbox_volume_mount.py +67 -0
- loopix/api/client/models/sandboxes_with_metrics.py +59 -0
- loopix/api/client/models/snapshot_info.py +70 -0
- loopix/api/client/models/team.py +83 -0
- loopix/api/client/models/team_api_key.py +158 -0
- loopix/api/client/models/team_metric.py +86 -0
- loopix/api/client/models/team_user.py +75 -0
- loopix/api/client/models/template.py +225 -0
- loopix/api/client/models/template_alias_response.py +67 -0
- loopix/api/client/models/template_build.py +139 -0
- loopix/api/client/models/template_build_file_upload.py +70 -0
- loopix/api/client/models/template_build_info.py +126 -0
- loopix/api/client/models/template_build_logs_response.py +73 -0
- loopix/api/client/models/template_build_request.py +115 -0
- loopix/api/client/models/template_build_request_v2.py +88 -0
- loopix/api/client/models/template_build_request_v3.py +107 -0
- loopix/api/client/models/template_build_start_v2.py +184 -0
- loopix/api/client/models/template_build_status.py +11 -0
- loopix/api/client/models/template_legacy.py +207 -0
- loopix/api/client/models/template_request_response_v3.py +99 -0
- loopix/api/client/models/template_step.py +91 -0
- loopix/api/client/models/template_tag.py +78 -0
- loopix/api/client/models/template_update_request.py +59 -0
- loopix/api/client/models/template_update_response.py +59 -0
- loopix/api/client/models/template_with_builds.py +156 -0
- loopix/api/client/models/update_team_api_key.py +59 -0
- loopix/api/client/models/volume.py +67 -0
- loopix/api/client/models/volume_and_token.py +75 -0
- loopix/api/client/models/volume_token.py +59 -0
- loopix/api/client/py.typed +1 -0
- loopix/api/client/types.py +54 -0
- loopix/api/client_async/__init__.py +74 -0
- loopix/api/client_sync/__init__.py +73 -0
- loopix/api/metadata.py +14 -0
- loopix/connection_config.py +309 -0
- loopix/envd/api.py +170 -0
- loopix/envd/filesystem/filesystem_connect.py +193 -0
- loopix/envd/filesystem/filesystem_pb2.py +80 -0
- loopix/envd/filesystem/filesystem_pb2.pyi +272 -0
- loopix/envd/process/process_connect.py +174 -0
- loopix/envd/process/process_pb2.py +96 -0
- loopix/envd/process/process_pb2.pyi +316 -0
- loopix/envd/rpc.py +139 -0
- loopix/envd/versions.py +11 -0
- loopix/exceptions.py +133 -0
- loopix/io_utils.py +57 -0
- loopix/paginator.py +52 -0
- loopix/py.typed +0 -0
- loopix/sandbox/_git/__init__.py +85 -0
- loopix/sandbox/_git/args.py +363 -0
- loopix/sandbox/_git/auth.py +132 -0
- loopix/sandbox/_git/config.py +32 -0
- loopix/sandbox/_git/parse.py +222 -0
- loopix/sandbox/_git/types.py +149 -0
- loopix/sandbox/commands/command_handle.py +69 -0
- loopix/sandbox/commands/main.py +39 -0
- loopix/sandbox/filesystem/filesystem.py +337 -0
- loopix/sandbox/filesystem/watch_handle.py +70 -0
- loopix/sandbox/main.py +227 -0
- loopix/sandbox/mcp.py +1949 -0
- loopix/sandbox/network.py +8 -0
- loopix/sandbox/sandbox_api.py +624 -0
- loopix/sandbox/signature.py +47 -0
- loopix/sandbox/utils.py +34 -0
- loopix/sandbox_async/commands/command.py +396 -0
- loopix/sandbox_async/commands/command_handle.py +298 -0
- loopix/sandbox_async/commands/pty.py +257 -0
- loopix/sandbox_async/filesystem/filesystem.py +720 -0
- loopix/sandbox_async/filesystem/watch_handle.py +97 -0
- loopix/sandbox_async/git.py +1100 -0
- loopix/sandbox_async/main.py +987 -0
- loopix/sandbox_async/paginator.py +140 -0
- loopix/sandbox_async/sandbox_api.py +504 -0
- loopix/sandbox_async/utils.py +7 -0
- loopix/sandbox_domains.py +5 -0
- loopix/sandbox_sync/commands/command.py +420 -0
- loopix/sandbox_sync/commands/command_handle.py +239 -0
- loopix/sandbox_sync/commands/pty.py +279 -0
- loopix/sandbox_sync/filesystem/filesystem.py +710 -0
- loopix/sandbox_sync/filesystem/watch_handle.py +102 -0
- loopix/sandbox_sync/git.py +1077 -0
- loopix/sandbox_sync/main.py +975 -0
- loopix/sandbox_sync/paginator.py +140 -0
- loopix/sandbox_sync/sandbox_api.py +491 -0
- loopix/template/consts.py +45 -0
- loopix/template/dockerfile_parser.py +286 -0
- loopix/template/logger.py +232 -0
- loopix/template/main.py +1368 -0
- loopix/template/readycmd.py +144 -0
- loopix/template/types.py +194 -0
- loopix/template/utils.py +426 -0
- loopix/template_async/build_api.py +419 -0
- loopix/template_async/main.py +528 -0
- loopix/template_sync/build_api.py +409 -0
- loopix/template_sync/main.py +529 -0
- loopix/volume/client/__init__.py +8 -0
- loopix/volume/client/api/__init__.py +1 -0
- loopix/volume/client/api/volumes/__init__.py +1 -0
- loopix/volume/client/api/volumes/delete_volumecontent_volume_id_path.py +174 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_dir.py +204 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_file.py +179 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_path.py +176 -0
- loopix/volume/client/api/volumes/patch_volumecontent_volume_id_path.py +203 -0
- loopix/volume/client/api/volumes/post_volumecontent_volume_id_dir.py +239 -0
- loopix/volume/client/api/volumes/put_volumecontent_volume_id_file.py +259 -0
- loopix/volume/client/client.py +286 -0
- loopix/volume/client/errors.py +16 -0
- loopix/volume/client/models/__init__.py +13 -0
- loopix/volume/client/models/error.py +67 -0
- loopix/volume/client/models/patch_volumecontent_volume_id_path_body.py +77 -0
- loopix/volume/client/models/volume_entry_stat.py +145 -0
- loopix/volume/client/models/volume_entry_stat_type.py +11 -0
- loopix/volume/client/py.typed +1 -0
- loopix/volume/client/types.py +54 -0
- loopix/volume/client_async/__init__.py +88 -0
- loopix/volume/client_sync/__init__.py +80 -0
- loopix/volume/connection_config.py +145 -0
- loopix/volume/types.py +62 -0
- loopix/volume/utils.py +52 -0
- loopix/volume/volume_async.py +639 -0
- loopix/volume/volume_sync.py +639 -0
- loopix_connect/__init__.py +1 -0
- loopix_connect/client.py +534 -0
- loopix_connect/py.typed +0 -0
- loopix_sdk-2.30.0.dist-info/METADATA +98 -0
- loopix_sdk-2.30.0.dist-info/RECORD +238 -0
- loopix_sdk-2.30.0.dist-info/WHEEL +4 -0
- loopix_sdk-2.30.0.dist-info/licenses/LICENSE +9 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import (
|
|
4
|
+
Any,
|
|
5
|
+
Callable,
|
|
6
|
+
Dict,
|
|
7
|
+
List,
|
|
8
|
+
Literal,
|
|
9
|
+
Mapping,
|
|
10
|
+
Optional,
|
|
11
|
+
TypedDict,
|
|
12
|
+
Union,
|
|
13
|
+
cast,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from typing_extensions import NotRequired, Unpack
|
|
17
|
+
|
|
18
|
+
from loopix.api.client.models import (
|
|
19
|
+
ListedSandbox,
|
|
20
|
+
SandboxDetail,
|
|
21
|
+
SandboxState,
|
|
22
|
+
)
|
|
23
|
+
from loopix.api.client.models import (
|
|
24
|
+
SandboxLifecycle as ClientSandboxLifecycle,
|
|
25
|
+
)
|
|
26
|
+
from loopix.api.client.models import (
|
|
27
|
+
SandboxNetworkConfig as ClientSandboxNetworkConfig,
|
|
28
|
+
)
|
|
29
|
+
from loopix.api.client.models import (
|
|
30
|
+
SandboxNetworkConfigRules,
|
|
31
|
+
)
|
|
32
|
+
from loopix.api.client.models import (
|
|
33
|
+
SandboxNetworkRule as ClientSandboxNetworkRule,
|
|
34
|
+
)
|
|
35
|
+
from loopix.api.client.models import (
|
|
36
|
+
SandboxNetworkTransform as ClientSandboxNetworkTransform,
|
|
37
|
+
)
|
|
38
|
+
from loopix.api.client.models import (
|
|
39
|
+
SandboxNetworkTransformHeaders as ClientSandboxNetworkTransformHeaders,
|
|
40
|
+
)
|
|
41
|
+
from loopix.api.client.models import (
|
|
42
|
+
SandboxNetworkUpdateConfig,
|
|
43
|
+
)
|
|
44
|
+
from loopix.api.client.models import (
|
|
45
|
+
SandboxNetworkUpdateConfigRules,
|
|
46
|
+
)
|
|
47
|
+
from loopix.api.client.types import Unset
|
|
48
|
+
from loopix.connection_config import ApiParams
|
|
49
|
+
from loopix.sandbox.mcp import McpServer as BaseMcpServer
|
|
50
|
+
from loopix.sandbox.network import ALL_TRAFFIC
|
|
51
|
+
from loopix.paginator import PaginatorBase
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class GitHubMcpServerConfig(TypedDict):
|
|
55
|
+
"""
|
|
56
|
+
Configuration for a GitHub-based MCP server.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
run_cmd: str
|
|
60
|
+
"""
|
|
61
|
+
Command to run the MCP server. Must start a stdio-compatible server.
|
|
62
|
+
"""
|
|
63
|
+
install_cmd: NotRequired[str]
|
|
64
|
+
"""
|
|
65
|
+
Command to install dependencies for the MCP server. Working directory is the root of the github repository.
|
|
66
|
+
"""
|
|
67
|
+
envs: NotRequired[Dict[str, str]]
|
|
68
|
+
"""
|
|
69
|
+
Environment variables to set in the MCP process.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Extended MCP server configuration that includes base servers
|
|
74
|
+
# and allows dynamic GitHub-based MCP servers with custom run and install commands.
|
|
75
|
+
# For GitHub servers, use keys in the format "github/owner/repo"
|
|
76
|
+
GitHubMcpServer = Dict[str, Union[GitHubMcpServerConfig, Any]]
|
|
77
|
+
|
|
78
|
+
# Union type that combines base MCP servers with GitHub-based servers
|
|
79
|
+
McpServer = Union[BaseMcpServer, GitHubMcpServer]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SandboxNetworkTransform(TypedDict):
|
|
83
|
+
"""
|
|
84
|
+
Transform applied to egress requests matching a :class:`SandboxNetworkRule`.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
headers: NotRequired[Dict[str, str]]
|
|
88
|
+
"""
|
|
89
|
+
Headers to inject into the outbound request. Values override any headers
|
|
90
|
+
already present on the request.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SandboxNetworkRule(TypedDict):
|
|
95
|
+
"""
|
|
96
|
+
Per-domain rule applied to egress requests.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
transform: NotRequired[SandboxNetworkTransform]
|
|
100
|
+
"""
|
|
101
|
+
Transform applied to requests matching this rule.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
SandboxNetworkRules = Dict[str, List[SandboxNetworkRule]]
|
|
106
|
+
"""
|
|
107
|
+
Map of host (or CIDR / IP) to ordered list of rules applied to outbound
|
|
108
|
+
requests for that host. Registering a host here does not allow egress on its
|
|
109
|
+
own — the host must also appear in ``SandboxNetworkOpts.allow_out``.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class SandboxNetworkRuleInfo(TypedDict):
|
|
114
|
+
"""
|
|
115
|
+
Per-domain rule as returned by the sandbox info endpoint. Mirrors
|
|
116
|
+
:class:`SandboxNetworkRule` but with ``transform`` always materialized to
|
|
117
|
+
the static :class:`SandboxNetworkTransform` shape — no callable variant.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
transform: NotRequired[SandboxNetworkTransform]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class SandboxNetworkSelectorContext:
|
|
125
|
+
"""
|
|
126
|
+
Context passed to ``allow_out``/``deny_out`` callables.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
all_traffic: str
|
|
130
|
+
"""All traffic sentinel — equivalent to ``"0.0.0.0/0"``."""
|
|
131
|
+
|
|
132
|
+
rules: Mapping[str, List[SandboxNetworkRule]]
|
|
133
|
+
"""Rules registered in :attr:`SandboxNetworkOpts.rules`."""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
SandboxNetworkSelector = Union[
|
|
137
|
+
List[str],
|
|
138
|
+
Callable[[SandboxNetworkSelectorContext], List[str]],
|
|
139
|
+
]
|
|
140
|
+
"""
|
|
141
|
+
Egress rule list, either a static list of CIDR blocks / IP addresses /
|
|
142
|
+
hostnames, or a callable that receives a :class:`SandboxNetworkSelectorContext`
|
|
143
|
+
and returns the same.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class SandboxNetworkOpts(TypedDict):
|
|
148
|
+
"""
|
|
149
|
+
Sandbox network configuration options.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
allow_out: NotRequired[SandboxNetworkSelector]
|
|
153
|
+
"""
|
|
154
|
+
Allow outbound traffic from the sandbox to the specified addresses.
|
|
155
|
+
If ``allow_out`` is not specified, all outbound traffic is allowed.
|
|
156
|
+
|
|
157
|
+
Accepts either a static list of CIDR blocks / IP addresses / hostnames, or
|
|
158
|
+
a callable that receives a :class:`SandboxNetworkSelectorContext` and
|
|
159
|
+
returns the same. ``ctx.all_traffic`` is ``"0.0.0.0/0"``; ``ctx.rules`` is
|
|
160
|
+
a read-only view of :attr:`rules`.
|
|
161
|
+
|
|
162
|
+
Examples:
|
|
163
|
+
- Static list: ``["1.1.1.1", "8.8.8.0/24"]``
|
|
164
|
+
- Allow only rule-registered hosts:
|
|
165
|
+
``lambda ctx: list(ctx.rules.keys())``
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
deny_out: NotRequired[SandboxNetworkSelector]
|
|
169
|
+
"""
|
|
170
|
+
Deny outbound traffic from the sandbox to the specified addresses.
|
|
171
|
+
|
|
172
|
+
Accepts the same shapes as ``allow_out``.
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
- Static list: ``["1.1.1.1", "8.8.8.0/24"]``
|
|
176
|
+
- Block all egress: ``lambda ctx: [ctx.all_traffic]``
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
rules: NotRequired[SandboxNetworkRules]
|
|
180
|
+
"""
|
|
181
|
+
Per-domain transform rules applied to matching egress HTTP/HTTPS
|
|
182
|
+
requests. Keys are domains (e.g. ``"api.example.com"``); values are
|
|
183
|
+
ordered lists of :class:`SandboxNetworkRule`.
|
|
184
|
+
|
|
185
|
+
Registering a host here does not allow egress on its own — the host must
|
|
186
|
+
also appear in ``allow_out``. Hosts registered here are exposed to the
|
|
187
|
+
``allow_out``/``deny_out`` callables via ``ctx.rules``.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
allow_public_traffic: NotRequired[bool]
|
|
191
|
+
"""
|
|
192
|
+
Controls whether sandbox URLs should be publicly accessible or require authentication.
|
|
193
|
+
Defaults to True.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
mask_request_host: NotRequired[str]
|
|
197
|
+
"""
|
|
198
|
+
Allows specifying a custom host mask for all sandbox requests.
|
|
199
|
+
Supports ${PORT} variable. Defaults to "vm.betmandu.net/sandbox/sandboxid/${PORT}".
|
|
200
|
+
|
|
201
|
+
Examples:
|
|
202
|
+
- Custom subdomain: `"${PORT}-myapp.example.com"`
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class SandboxNetworkUpdate(TypedDict, total=False):
|
|
207
|
+
"""
|
|
208
|
+
Subset of :class:`SandboxNetworkOpts` accepted by ``Sandbox.update_network``.
|
|
209
|
+
The update endpoint replaces all egress rules atomically — fields that are
|
|
210
|
+
omitted are cleared on the server.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
allow_out: SandboxNetworkSelector
|
|
214
|
+
"""See :attr:`SandboxNetworkOpts.allow_out`."""
|
|
215
|
+
|
|
216
|
+
deny_out: SandboxNetworkSelector
|
|
217
|
+
"""See :attr:`SandboxNetworkOpts.deny_out`."""
|
|
218
|
+
|
|
219
|
+
rules: SandboxNetworkRules
|
|
220
|
+
"""See :attr:`SandboxNetworkOpts.rules`."""
|
|
221
|
+
|
|
222
|
+
allow_internet_access: bool
|
|
223
|
+
"""
|
|
224
|
+
Allow sandbox to access the internet. When set to ``False``, it behaves the
|
|
225
|
+
same as specifying ``deny_out=["0.0.0.0/0"]`` in the network config.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class SandboxNetworkInfo(TypedDict, total=False):
|
|
230
|
+
"""
|
|
231
|
+
Network configuration as returned by the sandbox info endpoint.
|
|
232
|
+
Mirrors :class:`SandboxNetworkOpts` but with ``allow_out``/``deny_out``
|
|
233
|
+
always materialized to plain string lists.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
allow_out: List[str]
|
|
237
|
+
deny_out: List[str]
|
|
238
|
+
rules: Dict[str, List[SandboxNetworkRuleInfo]]
|
|
239
|
+
allow_public_traffic: bool
|
|
240
|
+
mask_request_host: str
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class SandboxOnTimeoutPause(TypedDict):
|
|
244
|
+
"""
|
|
245
|
+
Object form of `on_timeout` that auto-pauses the sandbox when the timeout is
|
|
246
|
+
reached, optionally controlling the pause snapshot kind via `keep_memory`.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
action: Literal["pause"]
|
|
250
|
+
"""Auto-pause the sandbox when the timeout is reached."""
|
|
251
|
+
|
|
252
|
+
keep_memory: NotRequired[bool]
|
|
253
|
+
"""
|
|
254
|
+
Whether the timeout auto-pause keeps a full memory snapshot. Defaults to `True`.
|
|
255
|
+
When `False`, the auto-pause drops the in-memory state and persists only the
|
|
256
|
+
filesystem (a filesystem-only snapshot); resuming such a sandbox cold-boots
|
|
257
|
+
(reboots) it from disk, losing running processes and open connections.
|
|
258
|
+
|
|
259
|
+
Cannot be combined with `auto_resume`: auto-resume wakes a paused sandbox on
|
|
260
|
+
inbound traffic by restoring its memory snapshot in place, so the request that
|
|
261
|
+
woke it hits an already-running process. A filesystem-only snapshot has no
|
|
262
|
+
memory to restore — resuming cold-boots it — so it can't be woken transparently
|
|
263
|
+
by traffic and must be resumed explicitly via `connect()`.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class SandboxOnTimeoutKill(TypedDict):
|
|
268
|
+
"""
|
|
269
|
+
Object form of `on_timeout` that kills the sandbox when the timeout is reached.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
action: Literal["kill"]
|
|
273
|
+
"""Kill the sandbox when the timeout is reached."""
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
SandboxOnTimeout = Union[
|
|
277
|
+
Literal["pause", "kill"], SandboxOnTimeoutPause, SandboxOnTimeoutKill
|
|
278
|
+
]
|
|
279
|
+
"""
|
|
280
|
+
What should happen to the sandbox when the timeout is reached. Either the bare
|
|
281
|
+
action (`"pause"` / `"kill"`) or the object form. The object form is a
|
|
282
|
+
discriminated union on `action`: `keep_memory` is only accepted alongside
|
|
283
|
+
`action: "pause"`. Passing `keep_memory` with `action: "kill"` is a static type
|
|
284
|
+
error.
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class SandboxLifecycle(TypedDict):
|
|
289
|
+
"""
|
|
290
|
+
Sandbox lifecycle configuration; defines post-timeout behavior and auto-resume settings.
|
|
291
|
+
Defaults to `on_timeout="kill"` and `auto_resume=False`.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
on_timeout: SandboxOnTimeout
|
|
295
|
+
"""
|
|
296
|
+
What should happen to the sandbox when timeout is reached. `"kill"` terminates
|
|
297
|
+
the sandbox; `"pause"` pauses it for later resume. Accepts either the bare
|
|
298
|
+
action or an object `{"action": "pause", "keep_memory": ...}` /
|
|
299
|
+
`{"action": "kill"}` to also control the pause snapshot kind. Defaults to
|
|
300
|
+
`"kill"`.
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
auto_resume: NotRequired[bool]
|
|
304
|
+
"""
|
|
305
|
+
Whether activity should cause the sandbox to resume when paused. Defaults to `False`.
|
|
306
|
+
Can be `True` only when `on_timeout` is `pause`. Not supported when
|
|
307
|
+
`keep_memory` is `False` (a filesystem-only snapshot must be resumed
|
|
308
|
+
explicitly via `connect()`).
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class SandboxInfoLifecycle(TypedDict):
|
|
313
|
+
"""
|
|
314
|
+
Sandbox lifecycle configuration returned by sandbox info.
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
on_timeout: Literal["pause", "kill"]
|
|
318
|
+
"""
|
|
319
|
+
What should happen to the sandbox when timeout is reached.
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
auto_resume: bool
|
|
323
|
+
"""
|
|
324
|
+
Whether activity should cause the sandbox to resume when paused.
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _resolve_network_selector(
|
|
329
|
+
selector: Optional[SandboxNetworkSelector],
|
|
330
|
+
rules: Mapping[str, List[SandboxNetworkRule]],
|
|
331
|
+
) -> Optional[List[str]]:
|
|
332
|
+
if selector is None:
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
if callable(selector):
|
|
336
|
+
ctx = SandboxNetworkSelectorContext(all_traffic=ALL_TRAFFIC, rules=rules)
|
|
337
|
+
return list(selector(ctx))
|
|
338
|
+
|
|
339
|
+
return list(selector)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _build_client_rules(rules: SandboxNetworkRules) -> SandboxNetworkConfigRules:
|
|
343
|
+
client_rules = SandboxNetworkConfigRules()
|
|
344
|
+
for host, host_rules in rules.items():
|
|
345
|
+
converted: List[ClientSandboxNetworkRule] = []
|
|
346
|
+
for rule in host_rules:
|
|
347
|
+
transform = rule.get("transform")
|
|
348
|
+
if transform is None:
|
|
349
|
+
converted.append(ClientSandboxNetworkRule())
|
|
350
|
+
continue
|
|
351
|
+
|
|
352
|
+
client_transform = ClientSandboxNetworkTransform()
|
|
353
|
+
headers = transform.get("headers")
|
|
354
|
+
if headers:
|
|
355
|
+
client_headers = ClientSandboxNetworkTransformHeaders()
|
|
356
|
+
client_headers.additional_properties = dict(headers)
|
|
357
|
+
client_transform.headers = client_headers
|
|
358
|
+
|
|
359
|
+
converted.append(ClientSandboxNetworkRule(transform=client_transform))
|
|
360
|
+
client_rules.additional_properties[host] = converted
|
|
361
|
+
|
|
362
|
+
return client_rules
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _build_network_egress(
|
|
366
|
+
network: Mapping[str, Any],
|
|
367
|
+
) -> Dict[str, Any]:
|
|
368
|
+
"""
|
|
369
|
+
Resolve the shared egress fields (``allow_out`` / ``deny_out`` / per-host
|
|
370
|
+
``rules``) used by both the create and update endpoints. ``rules`` in the
|
|
371
|
+
returned dict is the inner ``Dict[host, List[ClientSandboxNetworkRule]]``
|
|
372
|
+
— callers wrap it in their endpoint-specific rules attrs class.
|
|
373
|
+
"""
|
|
374
|
+
rules = network.get("rules") or {}
|
|
375
|
+
allow_out = _resolve_network_selector(network.get("allow_out"), rules)
|
|
376
|
+
deny_out = _resolve_network_selector(network.get("deny_out"), rules)
|
|
377
|
+
|
|
378
|
+
body: Dict[str, Any] = {}
|
|
379
|
+
if allow_out is not None:
|
|
380
|
+
body["allow_out"] = allow_out
|
|
381
|
+
if deny_out is not None:
|
|
382
|
+
body["deny_out"] = deny_out
|
|
383
|
+
if "rules" in network and network["rules"] is not None:
|
|
384
|
+
body["rules"] = _build_client_rules(network["rules"]).additional_properties
|
|
385
|
+
|
|
386
|
+
return body
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def build_network_config(
|
|
390
|
+
network: Optional[SandboxNetworkOpts],
|
|
391
|
+
) -> Optional[Dict[str, Any]]:
|
|
392
|
+
"""Resolve a :class:`SandboxNetworkOpts` into the dict the API expects."""
|
|
393
|
+
if network is None:
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
body = _build_network_egress(network)
|
|
397
|
+
if "rules" in body:
|
|
398
|
+
client_rules = SandboxNetworkConfigRules()
|
|
399
|
+
client_rules.additional_properties = body["rules"]
|
|
400
|
+
body["rules"] = client_rules
|
|
401
|
+
if "allow_public_traffic" in network:
|
|
402
|
+
body["allow_public_traffic"] = network["allow_public_traffic"]
|
|
403
|
+
if "mask_request_host" in network:
|
|
404
|
+
body["mask_request_host"] = network["mask_request_host"]
|
|
405
|
+
|
|
406
|
+
return body
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def build_network_update_body(
|
|
410
|
+
network: SandboxNetworkUpdate,
|
|
411
|
+
) -> SandboxNetworkUpdateConfig:
|
|
412
|
+
"""Resolve a :class:`SandboxNetworkUpdate` into the API client body."""
|
|
413
|
+
egress = _build_network_egress(network)
|
|
414
|
+
|
|
415
|
+
body = SandboxNetworkUpdateConfig()
|
|
416
|
+
if "allow_out" in egress:
|
|
417
|
+
body.allow_out = egress["allow_out"]
|
|
418
|
+
if "deny_out" in egress:
|
|
419
|
+
body.deny_out = egress["deny_out"]
|
|
420
|
+
if "rules" in egress:
|
|
421
|
+
rules = SandboxNetworkUpdateConfigRules()
|
|
422
|
+
rules.additional_properties = egress["rules"]
|
|
423
|
+
body.rules = rules
|
|
424
|
+
if "allow_internet_access" in network:
|
|
425
|
+
body.allow_internet_access = network["allow_internet_access"]
|
|
426
|
+
|
|
427
|
+
return body
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def from_client_network_config(
|
|
431
|
+
network: Union[Unset, ClientSandboxNetworkConfig],
|
|
432
|
+
) -> Optional[SandboxNetworkInfo]:
|
|
433
|
+
if isinstance(network, Unset):
|
|
434
|
+
return None
|
|
435
|
+
|
|
436
|
+
result: SandboxNetworkInfo = {}
|
|
437
|
+
|
|
438
|
+
if not isinstance(network.allow_out, Unset):
|
|
439
|
+
result["allow_out"] = list(network.allow_out)
|
|
440
|
+
if not isinstance(network.deny_out, Unset):
|
|
441
|
+
result["deny_out"] = list(network.deny_out)
|
|
442
|
+
if not isinstance(network.rules, Unset):
|
|
443
|
+
result["rules"] = cast(
|
|
444
|
+
Dict[str, List[SandboxNetworkRuleInfo]], network.rules.to_dict()
|
|
445
|
+
)
|
|
446
|
+
if not isinstance(network.allow_public_traffic, Unset):
|
|
447
|
+
result["allow_public_traffic"] = network.allow_public_traffic
|
|
448
|
+
if not isinstance(network.mask_request_host, Unset):
|
|
449
|
+
result["mask_request_host"] = network.mask_request_host
|
|
450
|
+
|
|
451
|
+
return result
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def from_client_lifecycle(
|
|
455
|
+
lifecycle: Union[Unset, ClientSandboxLifecycle],
|
|
456
|
+
) -> Optional[SandboxInfoLifecycle]:
|
|
457
|
+
if isinstance(lifecycle, Unset):
|
|
458
|
+
return None
|
|
459
|
+
|
|
460
|
+
result: SandboxInfoLifecycle = {
|
|
461
|
+
"on_timeout": cast(Literal["pause", "kill"], lifecycle.on_timeout),
|
|
462
|
+
"auto_resume": lifecycle.auto_resume,
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return result
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@dataclass
|
|
469
|
+
class SandboxInfo:
|
|
470
|
+
"""Information about a sandbox."""
|
|
471
|
+
|
|
472
|
+
sandbox_id: str
|
|
473
|
+
"""Sandbox ID."""
|
|
474
|
+
sandbox_domain: Optional[str]
|
|
475
|
+
"""Domain where the sandbox is hosted."""
|
|
476
|
+
template_id: str
|
|
477
|
+
"""Template ID."""
|
|
478
|
+
name: Optional[str]
|
|
479
|
+
"""Template name."""
|
|
480
|
+
metadata: Dict[str, str]
|
|
481
|
+
"""Saved sandbox metadata."""
|
|
482
|
+
started_at: datetime
|
|
483
|
+
"""Sandbox start time."""
|
|
484
|
+
end_at: datetime
|
|
485
|
+
"""Sandbox expiration date."""
|
|
486
|
+
state: SandboxState
|
|
487
|
+
"""Sandbox state."""
|
|
488
|
+
cpu_count: int
|
|
489
|
+
"""Sandbox CPU count."""
|
|
490
|
+
memory_mb: int
|
|
491
|
+
"""Sandbox Memory size in MiB."""
|
|
492
|
+
envd_version: str
|
|
493
|
+
"""Envd version."""
|
|
494
|
+
allow_internet_access: Optional[bool] = None
|
|
495
|
+
"""Whether internet access was explicitly enabled or disabled for the sandbox."""
|
|
496
|
+
network: Optional[SandboxNetworkInfo] = None
|
|
497
|
+
"""Sandbox network configuration."""
|
|
498
|
+
lifecycle: Optional[SandboxInfoLifecycle] = None
|
|
499
|
+
"""Sandbox lifecycle configuration."""
|
|
500
|
+
volume_mounts: List[Dict[str, str]] = field(default_factory=list)
|
|
501
|
+
"""Volume mounts for the sandbox."""
|
|
502
|
+
|
|
503
|
+
@classmethod
|
|
504
|
+
def _from_sandbox_data(
|
|
505
|
+
cls,
|
|
506
|
+
sandbox: Union[ListedSandbox, SandboxDetail],
|
|
507
|
+
sandbox_domain: Optional[str] = None,
|
|
508
|
+
allow_internet_access: Optional[bool] = None,
|
|
509
|
+
network: Optional[SandboxNetworkInfo] = None,
|
|
510
|
+
lifecycle: Optional[SandboxInfoLifecycle] = None,
|
|
511
|
+
):
|
|
512
|
+
return cls(
|
|
513
|
+
sandbox_domain=sandbox_domain,
|
|
514
|
+
sandbox_id=sandbox.sandbox_id,
|
|
515
|
+
template_id=sandbox.template_id,
|
|
516
|
+
name=(sandbox.alias if isinstance(sandbox.alias, str) else None),
|
|
517
|
+
metadata=cast(
|
|
518
|
+
Dict[str, str],
|
|
519
|
+
sandbox.metadata if isinstance(sandbox.metadata, dict) else {},
|
|
520
|
+
),
|
|
521
|
+
started_at=sandbox.started_at,
|
|
522
|
+
end_at=sandbox.end_at,
|
|
523
|
+
state=sandbox.state,
|
|
524
|
+
cpu_count=sandbox.cpu_count,
|
|
525
|
+
memory_mb=sandbox.memory_mb,
|
|
526
|
+
envd_version=sandbox.envd_version,
|
|
527
|
+
volume_mounts=[
|
|
528
|
+
{"name": vm.name, "path": vm.path} for vm in sandbox.volume_mounts
|
|
529
|
+
]
|
|
530
|
+
if not isinstance(sandbox.volume_mounts, Unset)
|
|
531
|
+
else [],
|
|
532
|
+
allow_internet_access=allow_internet_access,
|
|
533
|
+
network=network,
|
|
534
|
+
lifecycle=lifecycle,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
@classmethod
|
|
538
|
+
def _from_listed_sandbox(cls, listed_sandbox: ListedSandbox):
|
|
539
|
+
return cls._from_sandbox_data(listed_sandbox)
|
|
540
|
+
|
|
541
|
+
@classmethod
|
|
542
|
+
def _from_sandbox_detail(cls, sandbox_detail: SandboxDetail):
|
|
543
|
+
return cls._from_sandbox_data(
|
|
544
|
+
sandbox_detail,
|
|
545
|
+
sandbox_domain=(
|
|
546
|
+
sandbox_detail.domain
|
|
547
|
+
if isinstance(sandbox_detail.domain, str)
|
|
548
|
+
else None
|
|
549
|
+
),
|
|
550
|
+
allow_internet_access=(
|
|
551
|
+
sandbox_detail.allow_internet_access
|
|
552
|
+
if isinstance(sandbox_detail.allow_internet_access, bool)
|
|
553
|
+
else None
|
|
554
|
+
),
|
|
555
|
+
network=from_client_network_config(sandbox_detail.network),
|
|
556
|
+
lifecycle=from_client_lifecycle(sandbox_detail.lifecycle),
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
@dataclass
|
|
561
|
+
class SandboxQuery:
|
|
562
|
+
"""Query parameters for listing sandboxes."""
|
|
563
|
+
|
|
564
|
+
metadata: Optional[dict[str, str]] = None
|
|
565
|
+
"""Filter sandboxes by metadata."""
|
|
566
|
+
|
|
567
|
+
state: Optional[list[SandboxState]] = None
|
|
568
|
+
"""Filter sandboxes by state."""
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
@dataclass
|
|
572
|
+
class SandboxMetrics:
|
|
573
|
+
"""Sandbox metrics."""
|
|
574
|
+
|
|
575
|
+
cpu_count: int
|
|
576
|
+
"""Number of CPUs."""
|
|
577
|
+
cpu_used_pct: float
|
|
578
|
+
"""CPU usage percentage."""
|
|
579
|
+
disk_total: int
|
|
580
|
+
"""Total disk space in bytes."""
|
|
581
|
+
disk_used: int
|
|
582
|
+
"""Disk used in bytes."""
|
|
583
|
+
mem_total: int
|
|
584
|
+
"""Total memory in bytes."""
|
|
585
|
+
mem_used: int
|
|
586
|
+
"""Memory used in bytes."""
|
|
587
|
+
mem_cache: int
|
|
588
|
+
"""Cached memory (page cache) in bytes."""
|
|
589
|
+
timestamp: datetime
|
|
590
|
+
"""Timestamp of the metric entry."""
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
@dataclass
|
|
594
|
+
class SnapshotInfo:
|
|
595
|
+
"""Information about a snapshot."""
|
|
596
|
+
|
|
597
|
+
snapshot_id: str
|
|
598
|
+
"""Snapshot identifier — template ID with tag, or namespaced name with tag (e.g. my-snapshot:latest). Can be used with Sandbox.create() to create a new sandbox from this snapshot."""
|
|
599
|
+
names: List[str] = field(default_factory=list)
|
|
600
|
+
"""Full names of the snapshot template including team namespace and tag (e.g. team-slug/my-snapshot:v2)."""
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
class SnapshotPaginatorBase(PaginatorBase[SnapshotInfo, ApiParams]):
|
|
604
|
+
def __init__(
|
|
605
|
+
self,
|
|
606
|
+
sandbox_id: Optional[str] = None,
|
|
607
|
+
limit: Optional[int] = None,
|
|
608
|
+
next_token: Optional[str] = None,
|
|
609
|
+
**opts: Unpack[ApiParams],
|
|
610
|
+
):
|
|
611
|
+
super().__init__(limit=limit, next_token=next_token, **opts)
|
|
612
|
+
self.sandbox_id = sandbox_id
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class SandboxPaginatorBase(PaginatorBase[SandboxInfo, ApiParams]):
|
|
616
|
+
def __init__(
|
|
617
|
+
self,
|
|
618
|
+
query: Optional[SandboxQuery] = None,
|
|
619
|
+
limit: Optional[int] = None,
|
|
620
|
+
next_token: Optional[str] = None,
|
|
621
|
+
**opts: Unpack[ApiParams],
|
|
622
|
+
):
|
|
623
|
+
super().__init__(limit=limit, next_token=next_token, **opts)
|
|
624
|
+
self.query = query
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from typing import Optional, TypedDict, Literal
|
|
6
|
+
|
|
7
|
+
Operation = Literal["read", "write"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Signature(TypedDict):
|
|
11
|
+
signature: str
|
|
12
|
+
expiration: Optional[int] # Unix timestamp or None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_signature(
|
|
16
|
+
path: str,
|
|
17
|
+
operation: Operation,
|
|
18
|
+
user: Optional[str],
|
|
19
|
+
envd_access_token: Optional[str],
|
|
20
|
+
expiration_in_seconds: Optional[int] = None,
|
|
21
|
+
) -> Signature:
|
|
22
|
+
"""
|
|
23
|
+
Generate a v1 signature for sandbox file URLs.
|
|
24
|
+
"""
|
|
25
|
+
if not envd_access_token:
|
|
26
|
+
raise ValueError("Access token is not set and signature cannot be generated!")
|
|
27
|
+
|
|
28
|
+
expiration = (
|
|
29
|
+
int(time.time()) + expiration_in_seconds
|
|
30
|
+
if expiration_in_seconds is not None
|
|
31
|
+
else None
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# if user is None, set it to empty string to handle default user
|
|
35
|
+
if user is None:
|
|
36
|
+
user = ""
|
|
37
|
+
|
|
38
|
+
raw = (
|
|
39
|
+
f"{path}:{operation}:{user}:{envd_access_token}"
|
|
40
|
+
if expiration is None
|
|
41
|
+
else f"{path}:{operation}:{user}:{envd_access_token}:{expiration}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
digest = hashlib.sha256(raw.encode("utf-8")).digest()
|
|
45
|
+
encoded = base64.b64encode(digest).rstrip(b"=").decode("ascii")
|
|
46
|
+
|
|
47
|
+
return {"signature": f"v1_{encoded}", "expiration": expiration}
|