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,639 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from typing import IO, Iterator, List, Literal, Optional, Union, cast, overload
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from httpx._types import ProxyTypes
|
|
8
|
+
from typing_extensions import Unpack
|
|
9
|
+
|
|
10
|
+
from loopix.api import handle_api_exception
|
|
11
|
+
from loopix.api.client.api.volumes import (
|
|
12
|
+
post_volumes,
|
|
13
|
+
get_volumes,
|
|
14
|
+
get_volumes_volume_id,
|
|
15
|
+
delete_volumes_volume_id,
|
|
16
|
+
)
|
|
17
|
+
from loopix.api.client.models import (
|
|
18
|
+
NewVolume as NewVolumeModel,
|
|
19
|
+
Error,
|
|
20
|
+
)
|
|
21
|
+
from loopix.api.client.types import Response
|
|
22
|
+
from loopix.api.client_sync import get_api_client as get_core_api_client
|
|
23
|
+
from loopix.connection_config import ApiParams, ConnectionConfig
|
|
24
|
+
from loopix.exceptions import NotFoundException, VolumeException
|
|
25
|
+
from loopix.volume.client.api.volumes import (
|
|
26
|
+
get_volumecontent_volume_id_path as get_path,
|
|
27
|
+
get_volumecontent_volume_id_dir as get_dir,
|
|
28
|
+
post_volumecontent_volume_id_dir as post_dir,
|
|
29
|
+
delete_volumecontent_volume_id_path as delete_path,
|
|
30
|
+
patch_volumecontent_volume_id_path as patch_path,
|
|
31
|
+
put_volumecontent_volume_id_file as put_file,
|
|
32
|
+
)
|
|
33
|
+
from loopix.volume.client.models import (
|
|
34
|
+
Error as VolumeError,
|
|
35
|
+
PatchVolumecontentVolumeIDPathBody as PatchPathBody,
|
|
36
|
+
VolumeEntryStat as VolumeEntryStatApi,
|
|
37
|
+
)
|
|
38
|
+
from loopix.volume.client.types import File as FilePayload, UNSET
|
|
39
|
+
from loopix.volume.client_sync import get_api_client as get_volume_api_client
|
|
40
|
+
from loopix.volume.connection_config import (
|
|
41
|
+
VolumeApiParams,
|
|
42
|
+
VolumeConnectionConfig,
|
|
43
|
+
FILE_TIMEOUT,
|
|
44
|
+
)
|
|
45
|
+
from loopix.volume.types import (
|
|
46
|
+
VolumeAndToken,
|
|
47
|
+
VolumeInfo,
|
|
48
|
+
VolumeEntryStat,
|
|
49
|
+
)
|
|
50
|
+
from loopix.io_utils import iter_io_chunks
|
|
51
|
+
from loopix.volume.utils import DualMethod, convert_volume_entry_stat
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Volume:
|
|
55
|
+
"""Loopix Volume for persistent storage that can be mounted to sandboxes."""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
volume_id: str,
|
|
60
|
+
name: str,
|
|
61
|
+
token: Optional[str] = None,
|
|
62
|
+
domain: Optional[str] = None,
|
|
63
|
+
debug: Optional[bool] = None,
|
|
64
|
+
proxy: Optional[ProxyTypes] = None,
|
|
65
|
+
):
|
|
66
|
+
self._volume_id = volume_id
|
|
67
|
+
self._name = name
|
|
68
|
+
self._token = token
|
|
69
|
+
self._domain = domain
|
|
70
|
+
self._debug = debug
|
|
71
|
+
self._proxy = proxy
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def volume_id(self) -> str:
|
|
75
|
+
return self._volume_id
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def name(self) -> str:
|
|
79
|
+
return self._name
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def token(self) -> Optional[str]:
|
|
83
|
+
return self._token
|
|
84
|
+
|
|
85
|
+
def _get_volume_config(
|
|
86
|
+
self, **opts: Unpack[VolumeApiParams]
|
|
87
|
+
) -> VolumeConnectionConfig:
|
|
88
|
+
return VolumeConnectionConfig(
|
|
89
|
+
domain=opts.get("domain") or self._domain,
|
|
90
|
+
debug=opts.get("debug") if opts.get("debug") is not None else self._debug,
|
|
91
|
+
token=opts.get("token") or self._token,
|
|
92
|
+
api_url=opts.get("api_url"),
|
|
93
|
+
request_timeout=opts.get("request_timeout"),
|
|
94
|
+
headers=opts.get("headers"),
|
|
95
|
+
logger=opts.get("logger"),
|
|
96
|
+
proxy=opts.get("proxy") if opts.get("proxy") is not None else self._proxy,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def create(cls, name: str, **opts: Unpack[ApiParams]) -> "Volume":
|
|
101
|
+
"""
|
|
102
|
+
Create a new volume.
|
|
103
|
+
|
|
104
|
+
:param name: Name of the volume
|
|
105
|
+
|
|
106
|
+
:return: A Volume instance for the new volume
|
|
107
|
+
"""
|
|
108
|
+
config = ConnectionConfig(**opts)
|
|
109
|
+
|
|
110
|
+
api_client = get_core_api_client(config)
|
|
111
|
+
res = post_volumes.sync_detailed(
|
|
112
|
+
body=NewVolumeModel(name=name),
|
|
113
|
+
client=api_client,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if res.status_code >= 300:
|
|
117
|
+
raise handle_api_exception(res, VolumeException)
|
|
118
|
+
|
|
119
|
+
if res.parsed is None:
|
|
120
|
+
raise Exception("Body of the request is None")
|
|
121
|
+
|
|
122
|
+
if isinstance(res.parsed, Error):
|
|
123
|
+
raise Exception(f"{res.parsed.message}: Request failed")
|
|
124
|
+
|
|
125
|
+
vol = cls(
|
|
126
|
+
volume_id=res.parsed.volume_id,
|
|
127
|
+
name=res.parsed.name,
|
|
128
|
+
token=res.parsed.token,
|
|
129
|
+
domain=config.domain,
|
|
130
|
+
debug=config.debug,
|
|
131
|
+
proxy=config.proxy,
|
|
132
|
+
)
|
|
133
|
+
return vol
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def connect(cls, volume_id: str, **opts: Unpack[ApiParams]) -> "Volume":
|
|
137
|
+
"""
|
|
138
|
+
Connect to an existing volume by ID.
|
|
139
|
+
|
|
140
|
+
:param volume_id: Volume ID
|
|
141
|
+
|
|
142
|
+
:return: A Volume instance for the existing volume
|
|
143
|
+
"""
|
|
144
|
+
info = cls.get_info(volume_id, **opts)
|
|
145
|
+
config = ConnectionConfig(**opts)
|
|
146
|
+
return cls(
|
|
147
|
+
volume_id=volume_id,
|
|
148
|
+
name=info.name,
|
|
149
|
+
token=info.token,
|
|
150
|
+
domain=config.domain,
|
|
151
|
+
debug=config.debug,
|
|
152
|
+
proxy=config.proxy,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def _class_get_info(volume_id: str, **opts: Unpack[ApiParams]) -> VolumeAndToken:
|
|
157
|
+
"""
|
|
158
|
+
Get information about a volume.
|
|
159
|
+
|
|
160
|
+
:param volume_id: Volume ID
|
|
161
|
+
|
|
162
|
+
:return: Volume info
|
|
163
|
+
"""
|
|
164
|
+
config = ConnectionConfig(**opts)
|
|
165
|
+
|
|
166
|
+
api_client = get_core_api_client(config)
|
|
167
|
+
res = get_volumes_volume_id.sync_detailed(
|
|
168
|
+
volume_id,
|
|
169
|
+
client=api_client,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if res.status_code == 404:
|
|
173
|
+
raise NotFoundException(f"Volume {volume_id} not found")
|
|
174
|
+
|
|
175
|
+
if res.status_code >= 300:
|
|
176
|
+
raise handle_api_exception(res, VolumeException)
|
|
177
|
+
|
|
178
|
+
if res.parsed is None:
|
|
179
|
+
raise Exception("Body of the request is None")
|
|
180
|
+
|
|
181
|
+
if isinstance(res.parsed, Error):
|
|
182
|
+
raise Exception(f"{res.parsed.message}: Request failed")
|
|
183
|
+
|
|
184
|
+
return VolumeAndToken(
|
|
185
|
+
volume_id=res.parsed.volume_id,
|
|
186
|
+
name=res.parsed.name,
|
|
187
|
+
token=res.parsed.token,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _class_list(**opts: Unpack[ApiParams]) -> List[VolumeInfo]:
|
|
192
|
+
"""
|
|
193
|
+
List all volumes.
|
|
194
|
+
|
|
195
|
+
:return: List of volumes
|
|
196
|
+
"""
|
|
197
|
+
config = ConnectionConfig(**opts)
|
|
198
|
+
|
|
199
|
+
api_client = get_core_api_client(config)
|
|
200
|
+
res = get_volumes.sync_detailed(
|
|
201
|
+
client=api_client,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if res.status_code >= 300:
|
|
205
|
+
raise handle_api_exception(res, VolumeException)
|
|
206
|
+
|
|
207
|
+
if res.parsed is None:
|
|
208
|
+
return []
|
|
209
|
+
|
|
210
|
+
if isinstance(res.parsed, Error):
|
|
211
|
+
raise Exception(f"{res.parsed.message}: Request failed")
|
|
212
|
+
|
|
213
|
+
return [VolumeInfo(volume_id=v.volume_id, name=v.name) for v in res.parsed]
|
|
214
|
+
|
|
215
|
+
@staticmethod
|
|
216
|
+
def destroy(volume_id: str, **opts: Unpack[ApiParams]) -> bool:
|
|
217
|
+
"""
|
|
218
|
+
Destroy a volume.
|
|
219
|
+
|
|
220
|
+
:param volume_id: Volume ID
|
|
221
|
+
"""
|
|
222
|
+
config = ConnectionConfig(**opts)
|
|
223
|
+
|
|
224
|
+
api_client = get_core_api_client(config)
|
|
225
|
+
res = delete_volumes_volume_id.sync_detailed(
|
|
226
|
+
volume_id,
|
|
227
|
+
client=api_client,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if res.status_code == 404:
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
if res.status_code >= 300:
|
|
234
|
+
raise handle_api_exception(res, VolumeException)
|
|
235
|
+
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
def _instance_list(
|
|
239
|
+
self, path: str, depth: Optional[int] = None, **opts: Unpack[VolumeApiParams]
|
|
240
|
+
) -> List[VolumeEntryStat]:
|
|
241
|
+
"""
|
|
242
|
+
List directory contents.
|
|
243
|
+
|
|
244
|
+
:param path: Path to the directory
|
|
245
|
+
:param depth: Number of layers deep to recurse into the directory
|
|
246
|
+
:param opts: Connection options
|
|
247
|
+
|
|
248
|
+
:return: List of items (files and directories) in the directory
|
|
249
|
+
"""
|
|
250
|
+
config = self._get_volume_config(**opts)
|
|
251
|
+
api_client = get_volume_api_client(config)
|
|
252
|
+
|
|
253
|
+
res = get_dir.sync_detailed(
|
|
254
|
+
self._volume_id,
|
|
255
|
+
path=path,
|
|
256
|
+
depth=depth if depth is not None else UNSET,
|
|
257
|
+
client=api_client,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if res.status_code == 404:
|
|
261
|
+
raise NotFoundException(f"Path {path} not found")
|
|
262
|
+
|
|
263
|
+
if res.status_code >= 300:
|
|
264
|
+
raise handle_api_exception(res, VolumeException)
|
|
265
|
+
|
|
266
|
+
if res.parsed is None:
|
|
267
|
+
return []
|
|
268
|
+
|
|
269
|
+
if isinstance(res.parsed, VolumeError):
|
|
270
|
+
raise Exception(f"{res.parsed.message}: Request failed")
|
|
271
|
+
|
|
272
|
+
# VolumeDirectoryListing is a list according to the spec
|
|
273
|
+
if isinstance(res.parsed, list):
|
|
274
|
+
parsed_entries = cast(List[VolumeEntryStatApi], res.parsed)
|
|
275
|
+
return [convert_volume_entry_stat(entry) for entry in parsed_entries]
|
|
276
|
+
return []
|
|
277
|
+
|
|
278
|
+
def make_dir(
|
|
279
|
+
self,
|
|
280
|
+
path: str,
|
|
281
|
+
uid: Optional[int] = None,
|
|
282
|
+
gid: Optional[int] = None,
|
|
283
|
+
mode: Optional[int] = None,
|
|
284
|
+
force: Optional[bool] = None,
|
|
285
|
+
**opts: Unpack[VolumeApiParams],
|
|
286
|
+
) -> VolumeEntryStat:
|
|
287
|
+
"""
|
|
288
|
+
Create a directory.
|
|
289
|
+
|
|
290
|
+
:param path: Path to the directory to create
|
|
291
|
+
:param uid: User ID of the created directory
|
|
292
|
+
:param gid: Group ID of the created directory
|
|
293
|
+
:param mode: Mode of the created directory
|
|
294
|
+
:param force: Create parent directories if they don't exist
|
|
295
|
+
:param opts: Connection options
|
|
296
|
+
|
|
297
|
+
:return: Information about the created directory
|
|
298
|
+
"""
|
|
299
|
+
config = self._get_volume_config(**opts)
|
|
300
|
+
api_client = get_volume_api_client(config)
|
|
301
|
+
|
|
302
|
+
res = post_dir.sync_detailed(
|
|
303
|
+
self._volume_id,
|
|
304
|
+
path=path,
|
|
305
|
+
uid=uid if uid is not None else UNSET,
|
|
306
|
+
gid=gid if gid is not None else UNSET,
|
|
307
|
+
mode=mode if mode is not None else UNSET,
|
|
308
|
+
force=force if force is not None else UNSET,
|
|
309
|
+
client=api_client,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if res.status_code == 404:
|
|
313
|
+
raise NotFoundException(f"Path {path} not found")
|
|
314
|
+
|
|
315
|
+
if res.status_code >= 300:
|
|
316
|
+
raise handle_api_exception(res, VolumeException)
|
|
317
|
+
|
|
318
|
+
if res.parsed is None:
|
|
319
|
+
raise Exception("Body of the request is None")
|
|
320
|
+
|
|
321
|
+
if isinstance(res.parsed, VolumeError):
|
|
322
|
+
raise Exception(f"{res.parsed.message}: Request failed")
|
|
323
|
+
|
|
324
|
+
return convert_volume_entry_stat(res.parsed)
|
|
325
|
+
|
|
326
|
+
def exists(self, path: str, **opts: Unpack[VolumeApiParams]) -> bool:
|
|
327
|
+
"""
|
|
328
|
+
Check whether a file or directory exists.
|
|
329
|
+
|
|
330
|
+
Uses get_info under the hood. Returns True if the path exists,
|
|
331
|
+
False if it does not (404). Other errors are re-raised.
|
|
332
|
+
|
|
333
|
+
:param path: Path to the file or directory
|
|
334
|
+
:param opts: Connection options
|
|
335
|
+
|
|
336
|
+
:return: True if the path exists, False otherwise
|
|
337
|
+
"""
|
|
338
|
+
try:
|
|
339
|
+
self.get_info(path, **opts)
|
|
340
|
+
return True
|
|
341
|
+
except NotFoundException:
|
|
342
|
+
return False
|
|
343
|
+
|
|
344
|
+
def _instance_get_info(
|
|
345
|
+
self, path: str, **opts: Unpack[VolumeApiParams]
|
|
346
|
+
) -> VolumeEntryStat:
|
|
347
|
+
"""
|
|
348
|
+
Get information about a file or directory.
|
|
349
|
+
|
|
350
|
+
:param path: Path to the file or directory
|
|
351
|
+
:param opts: Connection options
|
|
352
|
+
|
|
353
|
+
:return: Information about the entry
|
|
354
|
+
"""
|
|
355
|
+
config = self._get_volume_config(**opts)
|
|
356
|
+
api_client = get_volume_api_client(config)
|
|
357
|
+
|
|
358
|
+
res = get_path.sync_detailed(
|
|
359
|
+
self._volume_id,
|
|
360
|
+
path=path,
|
|
361
|
+
client=api_client,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if res.status_code == 404:
|
|
365
|
+
raise NotFoundException(f"Path {path} not found")
|
|
366
|
+
|
|
367
|
+
if res.status_code >= 300:
|
|
368
|
+
raise handle_api_exception(res, VolumeException)
|
|
369
|
+
|
|
370
|
+
if res.parsed is None:
|
|
371
|
+
raise Exception("Body of the request is None")
|
|
372
|
+
|
|
373
|
+
if isinstance(res.parsed, VolumeError):
|
|
374
|
+
raise Exception(f"{res.parsed.message}: Request failed")
|
|
375
|
+
|
|
376
|
+
return convert_volume_entry_stat(cast(VolumeEntryStatApi, res.parsed))
|
|
377
|
+
|
|
378
|
+
get_info = DualMethod(_class_get_info.__func__, _instance_get_info)
|
|
379
|
+
list = DualMethod(_class_list.__func__, _instance_list)
|
|
380
|
+
|
|
381
|
+
def update_metadata(
|
|
382
|
+
self,
|
|
383
|
+
path: str,
|
|
384
|
+
uid: Optional[int] = None,
|
|
385
|
+
gid: Optional[int] = None,
|
|
386
|
+
mode: Optional[int] = None,
|
|
387
|
+
**opts: Unpack[VolumeApiParams],
|
|
388
|
+
) -> VolumeEntryStat:
|
|
389
|
+
"""
|
|
390
|
+
Update file or directory metadata.
|
|
391
|
+
|
|
392
|
+
:param path: Path to the file or directory
|
|
393
|
+
:param uid: User ID of the file or directory
|
|
394
|
+
:param gid: Group ID of the file or directory
|
|
395
|
+
:param mode: Mode of the file or directory
|
|
396
|
+
:param opts: Connection options
|
|
397
|
+
|
|
398
|
+
:return: Updated entry information
|
|
399
|
+
"""
|
|
400
|
+
config = self._get_volume_config(**opts)
|
|
401
|
+
api_client = get_volume_api_client(config)
|
|
402
|
+
|
|
403
|
+
body = PatchPathBody(
|
|
404
|
+
uid=uid if uid is not None else UNSET,
|
|
405
|
+
gid=gid if gid is not None else UNSET,
|
|
406
|
+
mode=mode if mode is not None else UNSET,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
res = patch_path.sync_detailed(
|
|
410
|
+
self._volume_id,
|
|
411
|
+
path=path,
|
|
412
|
+
body=body,
|
|
413
|
+
client=api_client,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
if res.status_code == 404:
|
|
417
|
+
raise NotFoundException(f"Path {path} not found")
|
|
418
|
+
|
|
419
|
+
if res.status_code >= 300:
|
|
420
|
+
raise handle_api_exception(res, VolumeException)
|
|
421
|
+
|
|
422
|
+
if res.parsed is None:
|
|
423
|
+
raise Exception("Body of the request is None")
|
|
424
|
+
|
|
425
|
+
return convert_volume_entry_stat(cast(VolumeEntryStatApi, res.parsed))
|
|
426
|
+
|
|
427
|
+
@overload
|
|
428
|
+
def read_file(
|
|
429
|
+
self,
|
|
430
|
+
path: str,
|
|
431
|
+
format: Literal["text"] = "text",
|
|
432
|
+
**opts: Unpack[VolumeApiParams],
|
|
433
|
+
) -> str: ...
|
|
434
|
+
|
|
435
|
+
@overload
|
|
436
|
+
def read_file(
|
|
437
|
+
self,
|
|
438
|
+
path: str,
|
|
439
|
+
format: Literal["bytes"],
|
|
440
|
+
**opts: Unpack[VolumeApiParams],
|
|
441
|
+
) -> bytes: ...
|
|
442
|
+
|
|
443
|
+
@overload
|
|
444
|
+
def read_file(
|
|
445
|
+
self,
|
|
446
|
+
path: str,
|
|
447
|
+
format: Literal["stream"],
|
|
448
|
+
stream_idle_timeout: Optional[float] = None,
|
|
449
|
+
**opts: Unpack[VolumeApiParams],
|
|
450
|
+
) -> Iterator[bytes]: ...
|
|
451
|
+
|
|
452
|
+
def read_file(
|
|
453
|
+
self,
|
|
454
|
+
path: str,
|
|
455
|
+
format: Literal["text", "bytes", "stream"] = "text",
|
|
456
|
+
stream_idle_timeout: Optional[float] = None,
|
|
457
|
+
**opts: Unpack[VolumeApiParams],
|
|
458
|
+
) -> Union[str, bytes, Iterator[bytes]]:
|
|
459
|
+
"""
|
|
460
|
+
Read file content.
|
|
461
|
+
|
|
462
|
+
You can pass `text`, `bytes`, or `stream` to `format` to change the return type.
|
|
463
|
+
|
|
464
|
+
:param path: Path to the file
|
|
465
|
+
:param format: Format of the file content—`text` by default
|
|
466
|
+
:param stream_idle_timeout: Idle timeout in **seconds** for a streamed
|
|
467
|
+
read (`format="stream"`)—abort if no chunk arrives within this
|
|
468
|
+
window while reading. Resets on every chunk, so it bounds a stalled
|
|
469
|
+
stream without limiting total transfer time. Defaults to the request
|
|
470
|
+
timeout; pass `0` to disable.
|
|
471
|
+
:param opts: Connection options
|
|
472
|
+
|
|
473
|
+
:return: File content as string, bytes, or iterator of bytes
|
|
474
|
+
"""
|
|
475
|
+
config = self._get_volume_config(**opts)
|
|
476
|
+
api_client = get_volume_api_client(config)
|
|
477
|
+
|
|
478
|
+
params = {"path": path}
|
|
479
|
+
timeout = VolumeConnectionConfig._get_request_timeout(
|
|
480
|
+
FILE_TIMEOUT, opts.get("request_timeout")
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
if format == "stream":
|
|
484
|
+
# The request timeout bounds connection setup, not total transfer;
|
|
485
|
+
# consuming the body must not be killed by it. httpx's per-chunk
|
|
486
|
+
# `read` timeout becomes the idle-read timeout for the body
|
|
487
|
+
# (defaults to the request timeout), bounding a stalled stream
|
|
488
|
+
# without limiting total transfer time. Pass `0` to disable.
|
|
489
|
+
# Mirrors the sandbox files stream path.
|
|
490
|
+
idle_timeout = (
|
|
491
|
+
timeout if stream_idle_timeout is None else stream_idle_timeout
|
|
492
|
+
)
|
|
493
|
+
stream_timeout = httpx.Timeout(timeout, read=idle_timeout or None)
|
|
494
|
+
|
|
495
|
+
def stream_file() -> Iterator[bytes]:
|
|
496
|
+
with api_client.get_httpx_client().stream(
|
|
497
|
+
method="GET",
|
|
498
|
+
url=f"/volumecontent/{self._volume_id}/file",
|
|
499
|
+
params=params,
|
|
500
|
+
timeout=stream_timeout,
|
|
501
|
+
) as response:
|
|
502
|
+
if response.status_code == 404:
|
|
503
|
+
raise NotFoundException(f"Path {path} not found")
|
|
504
|
+
|
|
505
|
+
if response.status_code >= 300:
|
|
506
|
+
api_response = Response(
|
|
507
|
+
status_code=HTTPStatus(response.status_code),
|
|
508
|
+
content=response.read(),
|
|
509
|
+
headers=response.headers,
|
|
510
|
+
parsed=None,
|
|
511
|
+
)
|
|
512
|
+
raise handle_api_exception(api_response, VolumeException)
|
|
513
|
+
|
|
514
|
+
yield from response.iter_bytes()
|
|
515
|
+
|
|
516
|
+
return stream_file()
|
|
517
|
+
|
|
518
|
+
response = api_client.get_httpx_client().request(
|
|
519
|
+
method="GET",
|
|
520
|
+
url=f"/volumecontent/{self._volume_id}/file",
|
|
521
|
+
params=params,
|
|
522
|
+
timeout=timeout,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
if response.status_code == 404:
|
|
526
|
+
raise NotFoundException(f"Path {path} not found")
|
|
527
|
+
|
|
528
|
+
if response.status_code >= 300:
|
|
529
|
+
api_response = Response(
|
|
530
|
+
status_code=HTTPStatus(response.status_code),
|
|
531
|
+
content=response.content,
|
|
532
|
+
headers=response.headers,
|
|
533
|
+
parsed=None,
|
|
534
|
+
)
|
|
535
|
+
raise handle_api_exception(api_response, VolumeException)
|
|
536
|
+
|
|
537
|
+
if format == "bytes":
|
|
538
|
+
return response.content
|
|
539
|
+
else:
|
|
540
|
+
return response.text
|
|
541
|
+
|
|
542
|
+
def write_file(
|
|
543
|
+
self,
|
|
544
|
+
path: str,
|
|
545
|
+
data: Union[str, bytes, IO],
|
|
546
|
+
uid: Optional[int] = None,
|
|
547
|
+
gid: Optional[int] = None,
|
|
548
|
+
mode: Optional[int] = None,
|
|
549
|
+
force: Optional[bool] = None,
|
|
550
|
+
**opts: Unpack[VolumeApiParams],
|
|
551
|
+
) -> VolumeEntryStat:
|
|
552
|
+
"""
|
|
553
|
+
Write content to a file.
|
|
554
|
+
|
|
555
|
+
Writing to a file that doesn't exist creates the file.
|
|
556
|
+
Writing to a file that already exists overwrites the file.
|
|
557
|
+
|
|
558
|
+
:param path: Path to the file
|
|
559
|
+
:param data: Data to write to the file. Data can be a string, bytes, or IO. File-like objects are streamed in chunks instead of being buffered in memory.
|
|
560
|
+
:param uid: User ID of the created file
|
|
561
|
+
:param gid: Group ID of the created file
|
|
562
|
+
:param mode: Mode of the created file
|
|
563
|
+
:param force: Force overwrite of an existing file
|
|
564
|
+
:param opts: Connection options
|
|
565
|
+
|
|
566
|
+
:return: Information about the written file
|
|
567
|
+
"""
|
|
568
|
+
config = self._get_volume_config(**opts)
|
|
569
|
+
upload_timeout = VolumeConnectionConfig._get_request_timeout(
|
|
570
|
+
FILE_TIMEOUT, opts.get("request_timeout")
|
|
571
|
+
)
|
|
572
|
+
api_client = get_volume_api_client(config)
|
|
573
|
+
if upload_timeout is not None:
|
|
574
|
+
api_client = api_client.with_timeout(httpx.Timeout(upload_timeout))
|
|
575
|
+
|
|
576
|
+
content: Union[bytes, IO[bytes], Iterator[bytes]]
|
|
577
|
+
if isinstance(data, str):
|
|
578
|
+
content = data.encode("utf-8")
|
|
579
|
+
elif isinstance(data, bytes):
|
|
580
|
+
content = data
|
|
581
|
+
elif isinstance(data, io.TextIOBase):
|
|
582
|
+
# Text-mode IO yields str chunks—encode them while streaming.
|
|
583
|
+
content = iter_io_chunks(data)
|
|
584
|
+
elif hasattr(data, "read"):
|
|
585
|
+
# httpx streams file-like objects in chunks without buffering.
|
|
586
|
+
content = data
|
|
587
|
+
else:
|
|
588
|
+
raise ValueError(f"Unsupported data type: {type(data)}")
|
|
589
|
+
|
|
590
|
+
res = put_file.sync_detailed(
|
|
591
|
+
self._volume_id,
|
|
592
|
+
body=FilePayload(payload=content), # type: ignore[arg-type] # httpx accepts bytes and streamable content directly
|
|
593
|
+
path=path,
|
|
594
|
+
uid=uid if uid is not None else UNSET,
|
|
595
|
+
gid=gid if gid is not None else UNSET,
|
|
596
|
+
mode=mode if mode is not None else UNSET,
|
|
597
|
+
force=force if force is not None else UNSET,
|
|
598
|
+
client=api_client,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
if res.status_code == 404:
|
|
602
|
+
raise NotFoundException(f"Path {path} not found")
|
|
603
|
+
|
|
604
|
+
if res.status_code >= 300:
|
|
605
|
+
raise handle_api_exception(res, VolumeException)
|
|
606
|
+
|
|
607
|
+
if res.parsed is None:
|
|
608
|
+
raise Exception("Body of the request is None")
|
|
609
|
+
|
|
610
|
+
if isinstance(res.parsed, VolumeError):
|
|
611
|
+
raise Exception(f"{res.parsed.message}: Request failed")
|
|
612
|
+
|
|
613
|
+
return convert_volume_entry_stat(cast(VolumeEntryStatApi, res.parsed))
|
|
614
|
+
|
|
615
|
+
def remove(
|
|
616
|
+
self,
|
|
617
|
+
path: str,
|
|
618
|
+
**opts: Unpack[VolumeApiParams],
|
|
619
|
+
) -> None:
|
|
620
|
+
"""
|
|
621
|
+
Remove a file or directory.
|
|
622
|
+
|
|
623
|
+
:param path: Path to the file or directory to remove
|
|
624
|
+
:param opts: Connection options
|
|
625
|
+
"""
|
|
626
|
+
config = self._get_volume_config(**opts)
|
|
627
|
+
api_client = get_volume_api_client(config)
|
|
628
|
+
|
|
629
|
+
res = delete_path.sync_detailed(
|
|
630
|
+
self._volume_id,
|
|
631
|
+
path=path,
|
|
632
|
+
client=api_client,
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
if res.status_code == 404:
|
|
636
|
+
raise NotFoundException(f"Path {path} not found")
|
|
637
|
+
|
|
638
|
+
if res.status_code >= 300:
|
|
639
|
+
raise handle_api_exception(res, VolumeException)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .client import Client, GzipCompressor, ConnectException, Code # noqa: F401
|