moru 0.1.0__py3-none-any.whl → 0.2.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 (72) hide show
  1. moru/__init__.py +8 -0
  2. moru/api/__init__.py +4 -0
  3. moru/api/client/__init__.py +1 -1
  4. moru/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +4 -0
  5. moru/api/client/api/sandboxes/get_sandboxes.py +4 -0
  6. moru/api/client/api/sandboxes/get_sandboxes_metrics.py +5 -1
  7. moru/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +4 -0
  8. moru/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +67 -23
  9. moru/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +5 -0
  10. moru/api/client/api/sandboxes/get_v2_sandbox_runs.py +218 -0
  11. moru/api/client/api/sandboxes/get_v2_sandboxes.py +5 -2
  12. moru/api/client/api/sandboxes/post_sandboxes.py +4 -0
  13. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +6 -0
  14. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +5 -0
  15. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +3 -0
  16. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +5 -0
  17. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +4 -0
  18. moru/api/client/api/templates/delete_templates_template_id.py +3 -0
  19. moru/api/client/api/templates/get_templates.py +3 -0
  20. moru/api/client/api/templates/get_templates_template_id.py +3 -0
  21. moru/api/client/api/templates/get_templates_template_id_builds_build_id_logs.py +276 -0
  22. moru/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +23 -4
  23. moru/api/client/api/templates/get_templates_template_id_files_hash.py +5 -0
  24. moru/api/client/api/templates/patch_templates_template_id.py +4 -0
  25. moru/api/client/api/templates/post_templates.py +4 -0
  26. moru/api/client/api/templates/post_templates_template_id.py +3 -0
  27. moru/api/client/api/templates/post_templates_template_id_builds_build_id.py +3 -0
  28. moru/api/client/api/templates/post_v2_templates.py +4 -0
  29. moru/api/client/api/templates/post_v3_templates.py +4 -0
  30. moru/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +3 -0
  31. moru/api/client/models/__init__.py +30 -0
  32. moru/api/client/models/admin_sandbox_kill_result.py +67 -0
  33. moru/api/client/models/build_log_entry.py +1 -1
  34. moru/api/client/models/create_volume_request.py +59 -0
  35. moru/api/client/models/file_info.py +105 -0
  36. moru/api/client/models/file_info_type.py +9 -0
  37. moru/api/client/models/file_list_response.py +84 -0
  38. moru/api/client/models/logs_direction.py +9 -0
  39. moru/api/client/models/logs_source.py +9 -0
  40. moru/api/client/models/machine_info.py +83 -0
  41. moru/api/client/models/new_sandbox.py +19 -0
  42. moru/api/client/models/node.py +10 -0
  43. moru/api/client/models/node_detail.py +10 -0
  44. moru/api/client/models/sandbox_log_entry.py +9 -9
  45. moru/api/client/models/sandbox_log_event_type.py +11 -0
  46. moru/api/client/models/sandbox_run.py +130 -0
  47. moru/api/client/models/sandbox_run_end_reason.py +11 -0
  48. moru/api/client/models/sandbox_run_status.py +10 -0
  49. moru/api/client/models/template_build_logs_response.py +73 -0
  50. moru/api/client/models/upload_response.py +67 -0
  51. moru/api/client/models/volume.py +105 -0
  52. moru/sandbox/mcp.py +835 -6
  53. moru/sandbox_async/commands/command.py +5 -1
  54. moru/sandbox_async/filesystem/filesystem.py +5 -1
  55. moru/sandbox_async/main.py +21 -0
  56. moru/sandbox_async/sandbox_api.py +17 -11
  57. moru/sandbox_sync/filesystem/filesystem.py +5 -1
  58. moru/sandbox_sync/main.py +21 -0
  59. moru/sandbox_sync/sandbox_api.py +17 -11
  60. moru/volume/__init__.py +11 -0
  61. moru/volume/types.py +83 -0
  62. moru/volume/volume_api.py +330 -0
  63. moru/volume_async/__init__.py +5 -0
  64. moru/volume_async/main.py +327 -0
  65. moru/volume_async/volume_api.py +290 -0
  66. moru/volume_sync/__init__.py +5 -0
  67. moru/volume_sync/main.py +325 -0
  68. moru-0.2.0.dist-info/METADATA +122 -0
  69. {moru-0.1.0.dist-info → moru-0.2.0.dist-info}/RECORD +71 -46
  70. {moru-0.1.0.dist-info → moru-0.2.0.dist-info}/WHEEL +1 -1
  71. moru-0.1.0.dist-info/METADATA +0 -63
  72. {moru-0.1.0.dist-info/licenses → moru-0.2.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,327 @@
1
+ """Asynchronous Volume class for persistent storage operations."""
2
+
3
+ from typing import BinaryIO, List, Optional, Union
4
+
5
+ from typing_extensions import Self, Unpack
6
+
7
+ from moru.connection_config import ApiParams, ConnectionConfig
8
+ from moru.volume.types import FileInfo, VolumeInfo
9
+ from moru.volume_async.volume_api import AsyncVolumeApi
10
+
11
+
12
+ class AsyncVolume:
13
+ """
14
+ Moru async volume is persistent storage for sandboxes.
15
+
16
+ Volumes provide crash-durable file storage that persists across sandbox
17
+ lifecycle. Data is accessible even when no sandbox is running.
18
+
19
+ Use `await AsyncVolume.create()` to create a new volume (idempotent).
20
+
21
+ Example:
22
+ ```python
23
+ from moru import AsyncVolume, AsyncSandbox
24
+
25
+ # Create a volume (idempotent - returns existing if name matches)
26
+ vol = await AsyncVolume.create(name="my-workspace")
27
+
28
+ # Attach to sandbox
29
+ sbx = await AsyncSandbox.create(
30
+ template="base",
31
+ volume_id=vol.volume_id,
32
+ volume_mount_path="/workspace",
33
+ )
34
+
35
+ # Work with files
36
+ files = await vol.list_files("/")
37
+ await vol.upload("/data/input.csv", b"col1,col2\\n1,2\\n")
38
+ content = await vol.download("/output/result.csv")
39
+
40
+ # Delete volume
41
+ await vol.delete()
42
+ ```
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ volume_id: str,
48
+ name: str,
49
+ total_size_bytes: int = 0,
50
+ total_file_count: int = 0,
51
+ connection_config: Optional[ConnectionConfig] = None,
52
+ **opts: Unpack[ApiParams],
53
+ ):
54
+ """
55
+ Initialize an AsyncVolume instance.
56
+
57
+ :param volume_id: Unique volume identifier
58
+ :param name: Volume name
59
+ :param total_size_bytes: Total size of files in volume
60
+ :param total_file_count: Total number of files in volume
61
+ :param connection_config: Connection configuration
62
+ """
63
+ self._volume_id = volume_id
64
+ self._name = name
65
+ self._total_size_bytes = total_size_bytes
66
+ self._total_file_count = total_file_count
67
+ self._connection_config = connection_config or ConnectionConfig(**opts)
68
+
69
+ @property
70
+ def volume_id(self) -> str:
71
+ """Unique volume identifier."""
72
+ return self._volume_id
73
+
74
+ @property
75
+ def name(self) -> str:
76
+ """Volume name."""
77
+ return self._name
78
+
79
+ @property
80
+ def total_size_bytes(self) -> int:
81
+ """Total size of files in volume (bytes)."""
82
+ return self._total_size_bytes
83
+
84
+ @property
85
+ def total_file_count(self) -> int:
86
+ """Total number of files in volume."""
87
+ return self._total_file_count
88
+
89
+ @classmethod
90
+ async def create(
91
+ cls,
92
+ name: str,
93
+ **opts: Unpack[ApiParams],
94
+ ) -> Self:
95
+ """
96
+ Create a new volume (idempotent).
97
+
98
+ If a volume with the same name already exists for this team,
99
+ the existing volume is returned.
100
+
101
+ :param name: Volume name (unique per team, slug format: lowercase, hyphens)
102
+
103
+ :return: AsyncVolume instance
104
+
105
+ Example:
106
+ ```python
107
+ vol = await AsyncVolume.create(name="my-workspace")
108
+ print(f"Volume: {vol.volume_id}")
109
+ ```
110
+ """
111
+ info = await AsyncVolumeApi._create_volume(name=name, **opts)
112
+
113
+ return cls(
114
+ volume_id=info.volume_id,
115
+ name=info.name,
116
+ total_size_bytes=info.total_size_bytes,
117
+ total_file_count=info.total_file_count,
118
+ **opts,
119
+ )
120
+
121
+ @classmethod
122
+ async def get(
123
+ cls,
124
+ volume_id_or_name: str,
125
+ **opts: Unpack[ApiParams],
126
+ ) -> Self:
127
+ """
128
+ Get a volume by ID or name.
129
+
130
+ :param volume_id_or_name: Volume ID (vol_xxx) or name
131
+
132
+ :return: AsyncVolume instance
133
+
134
+ Example:
135
+ ```python
136
+ vol = await AsyncVolume.get("vol_abc123")
137
+ # or
138
+ vol = await AsyncVolume.get("my-workspace")
139
+ ```
140
+ """
141
+ info = await AsyncVolumeApi._get_volume(
142
+ volume_id_or_name=volume_id_or_name, **opts
143
+ )
144
+
145
+ return cls(
146
+ volume_id=info.volume_id,
147
+ name=info.name,
148
+ total_size_bytes=info.total_size_bytes,
149
+ total_file_count=info.total_file_count,
150
+ **opts,
151
+ )
152
+
153
+ @staticmethod
154
+ async def list(**opts: Unpack[ApiParams]) -> List[VolumeInfo]:
155
+ """
156
+ List all volumes.
157
+
158
+ :return: List of volume info objects
159
+
160
+ Example:
161
+ ```python
162
+ volumes = await AsyncVolume.list()
163
+ for vol in volumes:
164
+ print(f"{vol.name}: {vol.total_size_bytes} bytes")
165
+ ```
166
+ """
167
+ volumes, _ = await AsyncVolumeApi._list_volumes(**opts)
168
+ return volumes
169
+
170
+ async def delete(self, **opts: Unpack[ApiParams]) -> bool:
171
+ """
172
+ Delete the volume.
173
+
174
+ :return: True if deleted, False if not found
175
+
176
+ Example:
177
+ ```python
178
+ vol = await AsyncVolume.create(name="temp-workspace")
179
+ await vol.delete()
180
+ ```
181
+ """
182
+ return await AsyncVolumeApi._delete_volume(
183
+ volume_id_or_name=self._volume_id,
184
+ **self._connection_config.get_api_params(**opts),
185
+ )
186
+
187
+ async def get_info(self, **opts: Unpack[ApiParams]) -> VolumeInfo:
188
+ """
189
+ Get updated volume information.
190
+
191
+ :return: Volume info with current size and file count
192
+
193
+ Example:
194
+ ```python
195
+ vol = await AsyncVolume.get("my-workspace")
196
+ info = await vol.get_info()
197
+ print(f"Size: {info.total_size_bytes} bytes, Files: {info.total_file_count}")
198
+ ```
199
+ """
200
+ info = await AsyncVolumeApi._get_volume(
201
+ volume_id_or_name=self._volume_id,
202
+ **self._connection_config.get_api_params(**opts),
203
+ )
204
+
205
+ # Update cached values
206
+ self._total_size_bytes = info.total_size_bytes
207
+ self._total_file_count = info.total_file_count
208
+
209
+ return info
210
+
211
+ async def list_files(
212
+ self,
213
+ path: str = "/",
214
+ **opts: Unpack[ApiParams],
215
+ ) -> List[FileInfo]:
216
+ """
217
+ List files and directories at a path.
218
+
219
+ Works even while volume is attached to a sandbox.
220
+
221
+ :param path: Directory path to list (default: "/")
222
+
223
+ :return: List of file info objects
224
+
225
+ Example:
226
+ ```python
227
+ files = await vol.list_files("/src")
228
+ for f in files:
229
+ print(f"{f.name} ({f.type})")
230
+ ```
231
+ """
232
+ files, _ = await AsyncVolumeApi._list_files(
233
+ volume_id=self._volume_id,
234
+ path=path,
235
+ **self._connection_config.get_api_params(**opts),
236
+ )
237
+ return files
238
+
239
+ async def upload(
240
+ self,
241
+ path: str,
242
+ content: Union[bytes, BinaryIO],
243
+ **opts: Unpack[ApiParams],
244
+ ) -> None:
245
+ """
246
+ Upload file content to the volume.
247
+
248
+ Creates parent directories as needed. Works even while volume
249
+ is attached to a sandbox - changes are visible immediately.
250
+
251
+ :param path: Destination path in volume
252
+ :param content: File content as bytes or file-like object
253
+
254
+ Example:
255
+ ```python
256
+ await vol.upload("/data/input.csv", b"col1,col2\\n1,2\\n")
257
+
258
+ # Or with a file
259
+ with open("local_file.txt", "rb") as f:
260
+ await vol.upload("/remote/file.txt", f.read())
261
+ ```
262
+ """
263
+ if hasattr(content, "read"):
264
+ content = content.read()
265
+
266
+ await AsyncVolumeApi._upload_file(
267
+ volume_id=self._volume_id,
268
+ path=path,
269
+ content=content,
270
+ **self._connection_config.get_api_params(**opts),
271
+ )
272
+
273
+ async def download(
274
+ self,
275
+ path: str,
276
+ **opts: Unpack[ApiParams],
277
+ ) -> bytes:
278
+ """
279
+ Download file content from the volume.
280
+
281
+ Works even while volume is attached to a sandbox.
282
+
283
+ :param path: File path in volume
284
+
285
+ :return: File content as bytes
286
+
287
+ Example:
288
+ ```python
289
+ content = await vol.download("/output/result.csv")
290
+ print(content.decode("utf-8"))
291
+ ```
292
+ """
293
+ return await AsyncVolumeApi._download_file(
294
+ volume_id=self._volume_id,
295
+ path=path,
296
+ **self._connection_config.get_api_params(**opts),
297
+ )
298
+
299
+ async def delete_file(
300
+ self,
301
+ path: str,
302
+ recursive: bool = False,
303
+ **opts: Unpack[ApiParams],
304
+ ) -> bool:
305
+ """
306
+ Delete file or directory from the volume.
307
+
308
+ :param path: Path to delete
309
+ :param recursive: Delete directory recursively
310
+
311
+ :return: True if deleted
312
+
313
+ Example:
314
+ ```python
315
+ await vol.delete_file("/temp/cache.txt")
316
+ await vol.delete_file("/temp/", recursive=True)
317
+ ```
318
+ """
319
+ return await AsyncVolumeApi._delete_file(
320
+ volume_id=self._volume_id,
321
+ path=path,
322
+ recursive=recursive,
323
+ **self._connection_config.get_api_params(**opts),
324
+ )
325
+
326
+ def __repr__(self) -> str:
327
+ return f"AsyncVolume(volume_id={self._volume_id!r}, name={self._name!r})"
@@ -0,0 +1,290 @@
1
+ """Async VolumeApi class with static methods for volume operations."""
2
+
3
+ from typing import List, Optional, Tuple
4
+
5
+ from typing_extensions import Unpack
6
+
7
+ from moru.api import handle_api_exception
8
+ from moru.api.client_async import get_api_client
9
+ from moru.connection_config import ApiParams, ConnectionConfig
10
+ from moru.exceptions import NotFoundException
11
+ from moru.volume.types import FileInfo, VolumeInfo
12
+
13
+
14
+ class AsyncVolumeApi:
15
+ """Base class for async volume API operations."""
16
+
17
+ @staticmethod
18
+ async def _create_volume(
19
+ name: str,
20
+ **opts: Unpack[ApiParams],
21
+ ) -> VolumeInfo:
22
+ """
23
+ Create a new volume (idempotent).
24
+
25
+ :param name: Volume name
26
+ :return: Volume info
27
+ """
28
+ config = ConnectionConfig(**opts)
29
+ api_client = get_api_client(config)
30
+ client = api_client.get_async_httpx_client()
31
+
32
+ response = await client.post(
33
+ f"{config.api_url}/volumes",
34
+ json={"name": name},
35
+ timeout=config.request_timeout,
36
+ )
37
+
38
+ err = handle_api_exception(response)
39
+ if err:
40
+ raise err
41
+
42
+ return VolumeInfo._from_api_response(response.json())
43
+
44
+ @staticmethod
45
+ async def _get_volume(
46
+ volume_id_or_name: str,
47
+ **opts: Unpack[ApiParams],
48
+ ) -> VolumeInfo:
49
+ """
50
+ Get volume by ID or name.
51
+
52
+ :param volume_id_or_name: Volume ID (vol_xxx) or name
53
+ :return: Volume info
54
+ """
55
+ config = ConnectionConfig(**opts)
56
+ api_client = get_api_client(config)
57
+ client = api_client.get_async_httpx_client()
58
+
59
+ response = await client.get(
60
+ f"{config.api_url}/volumes/{volume_id_or_name}",
61
+ timeout=config.request_timeout,
62
+ )
63
+
64
+ if response.status_code == 404:
65
+ raise NotFoundException(f"Volume '{volume_id_or_name}' not found")
66
+
67
+ err = handle_api_exception(response)
68
+ if err:
69
+ raise err
70
+
71
+ return VolumeInfo._from_api_response(response.json())
72
+
73
+ @staticmethod
74
+ async def _list_volumes(
75
+ limit: Optional[int] = None,
76
+ next_token: Optional[str] = None,
77
+ **opts: Unpack[ApiParams],
78
+ ) -> Tuple[List[VolumeInfo], Optional[str]]:
79
+ """
80
+ List all volumes.
81
+
82
+ :param limit: Maximum number of volumes to return
83
+ :param next_token: Pagination token
84
+ :return: Tuple of (volumes, next_token)
85
+ """
86
+ config = ConnectionConfig(**opts)
87
+ api_client = get_api_client(config)
88
+ client = api_client.get_async_httpx_client()
89
+
90
+ params = {}
91
+ if limit is not None:
92
+ params["limit"] = limit
93
+ if next_token is not None:
94
+ params["nextToken"] = next_token
95
+
96
+ response = await client.get(
97
+ f"{config.api_url}/volumes",
98
+ params=params,
99
+ timeout=config.request_timeout,
100
+ )
101
+
102
+ err = handle_api_exception(response)
103
+ if err:
104
+ raise err
105
+
106
+ volumes = [VolumeInfo._from_api_response(v) for v in response.json()]
107
+ result_next_token = response.headers.get("x-next-token")
108
+
109
+ return volumes, result_next_token
110
+
111
+ @staticmethod
112
+ async def _delete_volume(
113
+ volume_id_or_name: str,
114
+ **opts: Unpack[ApiParams],
115
+ ) -> bool:
116
+ """
117
+ Delete a volume.
118
+
119
+ :param volume_id_or_name: Volume ID (vol_xxx) or name
120
+ :return: True if deleted, False if not found
121
+ """
122
+ config = ConnectionConfig(**opts)
123
+ api_client = get_api_client(config)
124
+ client = api_client.get_async_httpx_client()
125
+
126
+ response = await client.delete(
127
+ f"{config.api_url}/volumes/{volume_id_or_name}",
128
+ timeout=config.request_timeout,
129
+ )
130
+
131
+ if response.status_code == 404:
132
+ return False
133
+
134
+ err = handle_api_exception(response)
135
+ if err:
136
+ raise err
137
+
138
+ return True
139
+
140
+ @staticmethod
141
+ async def _list_files(
142
+ volume_id: str,
143
+ path: str = "/",
144
+ limit: Optional[int] = None,
145
+ next_token: Optional[str] = None,
146
+ **opts: Unpack[ApiParams],
147
+ ) -> Tuple[List[FileInfo], Optional[str]]:
148
+ """
149
+ List files in a volume.
150
+
151
+ :param volume_id: Volume ID (vol_xxx)
152
+ :param path: Directory path to list
153
+ :param limit: Maximum number of files to return
154
+ :param next_token: Pagination token
155
+ :return: Tuple of (files, next_token)
156
+ """
157
+ config = ConnectionConfig(**opts)
158
+ api_client = get_api_client(config)
159
+ client = api_client.get_async_httpx_client()
160
+
161
+ params = {"path": path}
162
+ if limit is not None:
163
+ params["limit"] = limit
164
+ if next_token is not None:
165
+ params["nextToken"] = next_token
166
+
167
+ response = await client.get(
168
+ f"{config.api_url}/volumes/{volume_id}/files",
169
+ params=params,
170
+ timeout=config.request_timeout,
171
+ )
172
+
173
+ if response.status_code == 404:
174
+ raise NotFoundException(f"Volume '{volume_id}' not found")
175
+
176
+ err = handle_api_exception(response)
177
+ if err:
178
+ raise err
179
+
180
+ data = response.json()
181
+ files = [FileInfo._from_api_response(f) for f in data.get("files", [])]
182
+ result_next_token = data.get("nextToken")
183
+
184
+ return files, result_next_token
185
+
186
+ @staticmethod
187
+ async def _upload_file(
188
+ volume_id: str,
189
+ path: str,
190
+ content: bytes,
191
+ **opts: Unpack[ApiParams],
192
+ ) -> int:
193
+ """
194
+ Upload file content to volume.
195
+
196
+ :param volume_id: Volume ID (vol_xxx)
197
+ :param path: Destination path in volume
198
+ :param content: File content as bytes
199
+ :return: Size of uploaded file
200
+ """
201
+ config = ConnectionConfig(**opts)
202
+ api_client = get_api_client(config)
203
+ client = api_client.get_async_httpx_client()
204
+
205
+ response = await client.put(
206
+ f"{config.api_url}/volumes/{volume_id}/files/upload",
207
+ params={"path": path},
208
+ content=content,
209
+ headers={"Content-Type": "application/octet-stream"},
210
+ timeout=config.request_timeout,
211
+ )
212
+
213
+ if response.status_code == 404:
214
+ raise NotFoundException(f"Volume '{volume_id}' not found")
215
+
216
+ err = handle_api_exception(response)
217
+ if err:
218
+ raise err
219
+
220
+ return response.json().get("size", len(content))
221
+
222
+ @staticmethod
223
+ async def _download_file(
224
+ volume_id: str,
225
+ path: str,
226
+ **opts: Unpack[ApiParams],
227
+ ) -> bytes:
228
+ """
229
+ Download file content from volume.
230
+
231
+ :param volume_id: Volume ID (vol_xxx)
232
+ :param path: File path in volume
233
+ :return: File content as bytes
234
+ """
235
+ config = ConnectionConfig(**opts)
236
+ api_client = get_api_client(config)
237
+ client = api_client.get_async_httpx_client()
238
+
239
+ response = await client.get(
240
+ f"{config.api_url}/volumes/{volume_id}/files/download",
241
+ params={"path": path},
242
+ timeout=config.request_timeout,
243
+ )
244
+
245
+ if response.status_code == 404:
246
+ raise NotFoundException(f"Volume '{volume_id}' or file '{path}' not found")
247
+
248
+ err = handle_api_exception(response)
249
+ if err:
250
+ raise err
251
+
252
+ return response.content
253
+
254
+ @staticmethod
255
+ async def _delete_file(
256
+ volume_id: str,
257
+ path: str,
258
+ recursive: bool = False,
259
+ **opts: Unpack[ApiParams],
260
+ ) -> bool:
261
+ """
262
+ Delete file or directory from volume.
263
+
264
+ :param volume_id: Volume ID (vol_xxx)
265
+ :param path: Path to delete
266
+ :param recursive: Delete directory recursively
267
+ :return: True if deleted
268
+ """
269
+ config = ConnectionConfig(**opts)
270
+ api_client = get_api_client(config)
271
+ client = api_client.get_async_httpx_client()
272
+
273
+ params: dict = {"path": path}
274
+ if recursive:
275
+ params["recursive"] = "true"
276
+
277
+ response = await client.delete(
278
+ f"{config.api_url}/volumes/{volume_id}/files",
279
+ params=params,
280
+ timeout=config.request_timeout,
281
+ )
282
+
283
+ if response.status_code == 404:
284
+ raise NotFoundException(f"Volume '{volume_id}' or path '{path}' not found")
285
+
286
+ err = handle_api_exception(response)
287
+ if err:
288
+ raise err
289
+
290
+ return True
@@ -0,0 +1,5 @@
1
+ """Synchronous Volume module."""
2
+
3
+ from .main import Volume
4
+
5
+ __all__ = ["Volume"]