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