loopix-sdk 2.30.0__py3-none-any.whl

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