hopx-ai 0.1.10__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.
Potentially problematic release.
This version of hopx-ai might be problematic. Click here for more details.
- hopx_ai/__init__.py +114 -0
- hopx_ai/_agent_client.py +373 -0
- hopx_ai/_async_client.py +230 -0
- hopx_ai/_client.py +230 -0
- hopx_ai/_generated/__init__.py +22 -0
- hopx_ai/_generated/models.py +502 -0
- hopx_ai/_utils.py +9 -0
- hopx_ai/_ws_client.py +141 -0
- hopx_ai/async_sandbox.py +427 -0
- hopx_ai/cache.py +97 -0
- hopx_ai/commands.py +174 -0
- hopx_ai/desktop.py +1227 -0
- hopx_ai/env_vars.py +242 -0
- hopx_ai/errors.py +249 -0
- hopx_ai/files.py +489 -0
- hopx_ai/models.py +274 -0
- hopx_ai/models_updated.py +270 -0
- hopx_ai/sandbox.py +1439 -0
- hopx_ai/template/__init__.py +47 -0
- hopx_ai/template/build_flow.py +540 -0
- hopx_ai/template/builder.py +300 -0
- hopx_ai/template/file_hasher.py +81 -0
- hopx_ai/template/ready_checks.py +106 -0
- hopx_ai/template/tar_creator.py +122 -0
- hopx_ai/template/types.py +199 -0
- hopx_ai/terminal.py +164 -0
- hopx_ai-0.1.10.dist-info/METADATA +460 -0
- hopx_ai-0.1.10.dist-info/RECORD +29 -0
- hopx_ai-0.1.10.dist-info/WHEEL +4 -0
hopx_ai/files.py
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"""File operations resource for Bunnyshell Sandboxes."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional, Union, AsyncIterator, Dict, Any
|
|
4
|
+
import logging
|
|
5
|
+
from .models import FileInfo
|
|
6
|
+
from ._agent_client import AgentHTTPClient
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Files:
|
|
12
|
+
"""
|
|
13
|
+
File operations resource.
|
|
14
|
+
|
|
15
|
+
Provides methods for reading, writing, uploading, downloading, and managing files
|
|
16
|
+
inside the sandbox.
|
|
17
|
+
|
|
18
|
+
Features:
|
|
19
|
+
- Text and binary file support
|
|
20
|
+
- Automatic retry with exponential backoff
|
|
21
|
+
- Connection pooling for efficiency
|
|
22
|
+
- Proper error handling
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> sandbox = Sandbox.create(template="code-interpreter")
|
|
26
|
+
>>>
|
|
27
|
+
>>> # Text files
|
|
28
|
+
>>> sandbox.files.write('/workspace/hello.py', 'print("Hello, World!")')
|
|
29
|
+
>>> content = sandbox.files.read('/workspace/hello.py')
|
|
30
|
+
>>>
|
|
31
|
+
>>> # Binary files
|
|
32
|
+
>>> sandbox.files.write_bytes('/workspace/image.png', image_bytes)
|
|
33
|
+
>>> data = sandbox.files.read_bytes('/workspace/image.png')
|
|
34
|
+
>>>
|
|
35
|
+
>>> # List files
|
|
36
|
+
>>> files = sandbox.files.list('/workspace')
|
|
37
|
+
>>> for f in files:
|
|
38
|
+
... print(f"{f.name}: {f.size_kb:.2f} KB")
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, client: AgentHTTPClient, sandbox: Optional[Any] = None):
|
|
42
|
+
"""
|
|
43
|
+
Initialize Files resource.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
client: Shared agent HTTP client
|
|
47
|
+
sandbox: Parent sandbox instance for lazy WebSocket init
|
|
48
|
+
"""
|
|
49
|
+
self._client = client
|
|
50
|
+
self._sandbox = sandbox
|
|
51
|
+
logger.debug("Files resource initialized")
|
|
52
|
+
|
|
53
|
+
def read(self, path: str, *, timeout: Optional[int] = None) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Read text file contents.
|
|
56
|
+
|
|
57
|
+
For binary files, use read_bytes() instead.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
path: File path (e.g., '/workspace/data.txt')
|
|
61
|
+
timeout: Request timeout in seconds (overrides default)
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
File contents as string
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
FileNotFoundError: If file doesn't exist
|
|
68
|
+
FileOperationError: If read fails
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
>>> content = sandbox.files.read('/workspace/data.txt')
|
|
72
|
+
>>> print(content)
|
|
73
|
+
"""
|
|
74
|
+
logger.debug(f"Reading text file: {path}")
|
|
75
|
+
|
|
76
|
+
response = self._client.get(
|
|
77
|
+
"/files/read",
|
|
78
|
+
params={"path": path},
|
|
79
|
+
operation="read file",
|
|
80
|
+
context={"path": path},
|
|
81
|
+
timeout=timeout
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
data = response.json()
|
|
85
|
+
return data.get("content", "")
|
|
86
|
+
|
|
87
|
+
def read_bytes(self, path: str, *, timeout: Optional[int] = None) -> bytes:
|
|
88
|
+
"""
|
|
89
|
+
Read binary file contents.
|
|
90
|
+
|
|
91
|
+
Use this for images, PDFs, or any binary data.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
path: File path (e.g., '/workspace/plot.png')
|
|
95
|
+
timeout: Request timeout in seconds (overrides default)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
File contents as bytes
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
FileNotFoundError: If file doesn't exist
|
|
102
|
+
FileOperationError: If read fails
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
>>> # Read matplotlib plot
|
|
106
|
+
>>> plot_data = sandbox.files.read_bytes('/workspace/plot.png')
|
|
107
|
+
>>> with open('local_plot.png', 'wb') as f:
|
|
108
|
+
... f.write(plot_data)
|
|
109
|
+
"""
|
|
110
|
+
logger.debug(f"Reading binary file: {path}")
|
|
111
|
+
|
|
112
|
+
response = self._client.get(
|
|
113
|
+
"/files/download",
|
|
114
|
+
params={"path": path},
|
|
115
|
+
operation="read binary file",
|
|
116
|
+
context={"path": path},
|
|
117
|
+
timeout=timeout
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return response.content
|
|
121
|
+
|
|
122
|
+
def write(
|
|
123
|
+
self,
|
|
124
|
+
path: str,
|
|
125
|
+
content: str,
|
|
126
|
+
mode: str = "0644",
|
|
127
|
+
*,
|
|
128
|
+
timeout: Optional[int] = None
|
|
129
|
+
) -> None:
|
|
130
|
+
"""
|
|
131
|
+
Write text file contents.
|
|
132
|
+
|
|
133
|
+
For binary files, use write_bytes() instead.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
path: File path (e.g., '/workspace/output.txt')
|
|
137
|
+
content: File contents to write (string)
|
|
138
|
+
mode: File permissions (default: '0644')
|
|
139
|
+
timeout: Request timeout in seconds (overrides default)
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
FileOperationError: If write fails
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
>>> sandbox.files.write('/workspace/hello.py', 'print("Hello!")')
|
|
146
|
+
>>>
|
|
147
|
+
>>> # With custom permissions
|
|
148
|
+
>>> sandbox.files.write('/workspace/script.sh', '#!/bin/bash\\necho hi', mode='0755')
|
|
149
|
+
"""
|
|
150
|
+
logger.debug(f"Writing text file: {path} ({len(content)} chars)")
|
|
151
|
+
|
|
152
|
+
self._client.post(
|
|
153
|
+
"/files/write",
|
|
154
|
+
json={
|
|
155
|
+
"path": path,
|
|
156
|
+
"content": content,
|
|
157
|
+
"mode": mode
|
|
158
|
+
},
|
|
159
|
+
operation="write file",
|
|
160
|
+
context={"path": path},
|
|
161
|
+
timeout=timeout
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def write_bytes(
|
|
165
|
+
self,
|
|
166
|
+
path: str,
|
|
167
|
+
content: bytes,
|
|
168
|
+
mode: str = "0644",
|
|
169
|
+
*,
|
|
170
|
+
timeout: Optional[int] = None
|
|
171
|
+
) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Write binary file contents.
|
|
174
|
+
|
|
175
|
+
Use this for images, PDFs, or any binary data.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
path: File path (e.g., '/workspace/image.png')
|
|
179
|
+
content: File contents to write (bytes)
|
|
180
|
+
mode: File permissions (default: '0644')
|
|
181
|
+
timeout: Request timeout in seconds (overrides default)
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
FileOperationError: If write fails
|
|
185
|
+
|
|
186
|
+
Example:
|
|
187
|
+
>>> # Save image
|
|
188
|
+
>>> with open('image.png', 'rb') as f:
|
|
189
|
+
... image_data = f.read()
|
|
190
|
+
>>> sandbox.files.write_bytes('/workspace/image.png', image_data)
|
|
191
|
+
"""
|
|
192
|
+
logger.debug(f"Writing binary file: {path} ({len(content)} bytes)")
|
|
193
|
+
|
|
194
|
+
# Encode bytes to base64 for JSON transport
|
|
195
|
+
import base64
|
|
196
|
+
content_b64 = base64.b64encode(content).decode('ascii')
|
|
197
|
+
|
|
198
|
+
self._client.post(
|
|
199
|
+
"/files/write",
|
|
200
|
+
json={
|
|
201
|
+
"path": path,
|
|
202
|
+
"content": content_b64,
|
|
203
|
+
"mode": mode,
|
|
204
|
+
"encoding": "base64"
|
|
205
|
+
},
|
|
206
|
+
operation="write binary file",
|
|
207
|
+
context={"path": path},
|
|
208
|
+
timeout=timeout
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def list(self, path: str = "/workspace", *, timeout: Optional[int] = None) -> List[FileInfo]:
|
|
212
|
+
"""
|
|
213
|
+
List directory contents.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
path: Directory path (default: '/workspace')
|
|
217
|
+
timeout: Request timeout in seconds (overrides default)
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
List of FileInfo objects
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
FileNotFoundError: If directory doesn't exist
|
|
224
|
+
FileOperationError: If list fails
|
|
225
|
+
|
|
226
|
+
Example:
|
|
227
|
+
>>> files = sandbox.files.list('/workspace')
|
|
228
|
+
>>> for f in files:
|
|
229
|
+
... if f.is_file:
|
|
230
|
+
... print(f"📄 {f.name}: {f.size_kb:.2f} KB")
|
|
231
|
+
... else:
|
|
232
|
+
... print(f"📁 {f.name}/")
|
|
233
|
+
"""
|
|
234
|
+
logger.debug(f"Listing directory: {path}")
|
|
235
|
+
|
|
236
|
+
response = self._client.get(
|
|
237
|
+
"/files/list",
|
|
238
|
+
params={"path": path},
|
|
239
|
+
operation="list directory",
|
|
240
|
+
context={"path": path},
|
|
241
|
+
timeout=timeout
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
data = response.json()
|
|
245
|
+
|
|
246
|
+
files = []
|
|
247
|
+
for item in data.get("files", []):
|
|
248
|
+
files.append(FileInfo(
|
|
249
|
+
name=item.get("name", ""),
|
|
250
|
+
path=item.get("path", ""),
|
|
251
|
+
size=item.get("size", 0),
|
|
252
|
+
is_directory=item.get("is_directory", item.get("is_dir", False)), # Support both
|
|
253
|
+
permissions=item.get("permissions", item.get("mode", "")), # Support both
|
|
254
|
+
modified_time=item.get("modified_time", item.get("modified")) # Support both
|
|
255
|
+
))
|
|
256
|
+
|
|
257
|
+
return files
|
|
258
|
+
|
|
259
|
+
def upload(
|
|
260
|
+
self,
|
|
261
|
+
local_path: str,
|
|
262
|
+
remote_path: str,
|
|
263
|
+
*,
|
|
264
|
+
timeout: Optional[int] = None
|
|
265
|
+
) -> None:
|
|
266
|
+
"""
|
|
267
|
+
Upload file from local filesystem to sandbox.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
local_path: Path to local file
|
|
271
|
+
remote_path: Destination path in sandbox
|
|
272
|
+
timeout: Request timeout in seconds (overrides default, recommended: 60+)
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
FileNotFoundError: If local file doesn't exist
|
|
276
|
+
FileOperationError: If upload fails
|
|
277
|
+
|
|
278
|
+
Example:
|
|
279
|
+
>>> # Upload local file to sandbox
|
|
280
|
+
>>> sandbox.files.upload('./data.csv', '/workspace/data.csv')
|
|
281
|
+
>>>
|
|
282
|
+
>>> # Upload with custom timeout for large file
|
|
283
|
+
>>> sandbox.files.upload('./large.zip', '/workspace/large.zip', timeout=120)
|
|
284
|
+
"""
|
|
285
|
+
logger.debug(f"Uploading file: {local_path} -> {remote_path}")
|
|
286
|
+
|
|
287
|
+
with open(local_path, 'rb') as f:
|
|
288
|
+
self._client.post(
|
|
289
|
+
"/files/upload",
|
|
290
|
+
files={"file": f},
|
|
291
|
+
data={"path": remote_path},
|
|
292
|
+
operation="upload file",
|
|
293
|
+
context={"path": remote_path},
|
|
294
|
+
timeout=timeout or 60 # Default 60s for uploads
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def download(
|
|
298
|
+
self,
|
|
299
|
+
remote_path: str,
|
|
300
|
+
local_path: str,
|
|
301
|
+
*,
|
|
302
|
+
timeout: Optional[int] = None
|
|
303
|
+
) -> None:
|
|
304
|
+
"""
|
|
305
|
+
Download file from sandbox to local filesystem.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
remote_path: Path in sandbox
|
|
309
|
+
local_path: Destination path on local filesystem
|
|
310
|
+
timeout: Request timeout in seconds (overrides default, recommended: 60+)
|
|
311
|
+
|
|
312
|
+
Raises:
|
|
313
|
+
FileNotFoundError: If file doesn't exist in sandbox
|
|
314
|
+
FileOperationError: If download fails
|
|
315
|
+
|
|
316
|
+
Example:
|
|
317
|
+
>>> # Download file from sandbox
|
|
318
|
+
>>> sandbox.files.download('/workspace/result.csv', './result.csv')
|
|
319
|
+
>>>
|
|
320
|
+
>>> # Download plot
|
|
321
|
+
>>> sandbox.files.download('/workspace/plot.png', './plot.png')
|
|
322
|
+
"""
|
|
323
|
+
logger.debug(f"Downloading file: {remote_path} -> {local_path}")
|
|
324
|
+
|
|
325
|
+
response = self._client.get(
|
|
326
|
+
"/files/download",
|
|
327
|
+
params={"path": remote_path},
|
|
328
|
+
operation="download file",
|
|
329
|
+
context={"path": remote_path},
|
|
330
|
+
timeout=timeout or 60 # Default 60s for downloads
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
with open(local_path, 'wb') as f:
|
|
334
|
+
f.write(response.content)
|
|
335
|
+
|
|
336
|
+
def exists(self, path: str, *, timeout: Optional[int] = None) -> bool:
|
|
337
|
+
"""
|
|
338
|
+
Check if file or directory exists.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
path: File or directory path
|
|
342
|
+
timeout: Request timeout in seconds (overrides default)
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
True if exists, False otherwise
|
|
346
|
+
|
|
347
|
+
Example:
|
|
348
|
+
>>> if sandbox.files.exists('/workspace/data.csv'):
|
|
349
|
+
... print("File exists!")
|
|
350
|
+
... else:
|
|
351
|
+
... print("File not found")
|
|
352
|
+
"""
|
|
353
|
+
logger.debug(f"Checking if exists: {path}")
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
response = self._client.get(
|
|
357
|
+
"/files/exists",
|
|
358
|
+
params={"path": path},
|
|
359
|
+
operation="check file exists",
|
|
360
|
+
context={"path": path},
|
|
361
|
+
timeout=timeout or 10
|
|
362
|
+
)
|
|
363
|
+
data = response.json()
|
|
364
|
+
return data.get("exists", False)
|
|
365
|
+
except Exception:
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
def remove(self, path: str, *, timeout: Optional[int] = None) -> None:
|
|
369
|
+
"""
|
|
370
|
+
Delete file or directory.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
path: Path to file or directory to delete
|
|
374
|
+
timeout: Request timeout in seconds (overrides default)
|
|
375
|
+
|
|
376
|
+
Raises:
|
|
377
|
+
FileNotFoundError: If file doesn't exist
|
|
378
|
+
FileOperationError: If delete fails
|
|
379
|
+
|
|
380
|
+
Example:
|
|
381
|
+
>>> # Remove file
|
|
382
|
+
>>> sandbox.files.remove('/workspace/temp.txt')
|
|
383
|
+
>>>
|
|
384
|
+
>>> # Remove directory (recursive)
|
|
385
|
+
>>> sandbox.files.remove('/workspace/old_data')
|
|
386
|
+
"""
|
|
387
|
+
logger.debug(f"Removing: {path}")
|
|
388
|
+
|
|
389
|
+
self._client.delete(
|
|
390
|
+
"/files/remove",
|
|
391
|
+
params={"path": path},
|
|
392
|
+
operation="remove file",
|
|
393
|
+
context={"path": path},
|
|
394
|
+
timeout=timeout
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
def mkdir(self, path: str, *, timeout: Optional[int] = None) -> None:
|
|
398
|
+
"""
|
|
399
|
+
Create directory.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
path: Directory path to create
|
|
403
|
+
timeout: Request timeout in seconds (overrides default)
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
FileOperationError: If mkdir fails
|
|
407
|
+
|
|
408
|
+
Example:
|
|
409
|
+
>>> # Create directory
|
|
410
|
+
>>> sandbox.files.mkdir('/workspace/data')
|
|
411
|
+
>>>
|
|
412
|
+
>>> # Create nested directories
|
|
413
|
+
>>> sandbox.files.mkdir('/workspace/project/src')
|
|
414
|
+
"""
|
|
415
|
+
logger.debug(f"Creating directory: {path}")
|
|
416
|
+
|
|
417
|
+
self._client.post(
|
|
418
|
+
"/files/mkdir",
|
|
419
|
+
json={"path": path},
|
|
420
|
+
operation="create directory",
|
|
421
|
+
context={"path": path},
|
|
422
|
+
timeout=timeout
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
async def watch(
|
|
426
|
+
self,
|
|
427
|
+
path: str = "/workspace",
|
|
428
|
+
*,
|
|
429
|
+
timeout: Optional[int] = None
|
|
430
|
+
) -> AsyncIterator[Dict[str, Any]]:
|
|
431
|
+
"""
|
|
432
|
+
Watch filesystem for changes via WebSocket.
|
|
433
|
+
|
|
434
|
+
Stream file system events (create, modify, delete, rename) in real-time.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
path: Path to watch (default: /workspace)
|
|
438
|
+
timeout: Connection timeout in seconds
|
|
439
|
+
|
|
440
|
+
Yields:
|
|
441
|
+
Change event dictionaries:
|
|
442
|
+
- {"type": "change", "path": "...", "event": "created", "timestamp": "..."}
|
|
443
|
+
- {"type": "change", "path": "...", "event": "modified", "timestamp": "..."}
|
|
444
|
+
- {"type": "change", "path": "...", "event": "deleted", "timestamp": "..."}
|
|
445
|
+
- {"type": "change", "path": "...", "event": "renamed", "timestamp": "..."}
|
|
446
|
+
|
|
447
|
+
Note:
|
|
448
|
+
Requires websockets library: pip install websockets
|
|
449
|
+
|
|
450
|
+
Example:
|
|
451
|
+
>>> import asyncio
|
|
452
|
+
>>>
|
|
453
|
+
>>> async def watch_files():
|
|
454
|
+
... sandbox = Sandbox.create(template="code-interpreter")
|
|
455
|
+
...
|
|
456
|
+
... # Start watching
|
|
457
|
+
... async for event in sandbox.files.watch("/workspace"):
|
|
458
|
+
... print(f"{event['event']}: {event['path']}")
|
|
459
|
+
...
|
|
460
|
+
... # Stop after 10 events
|
|
461
|
+
... if event_count >= 10:
|
|
462
|
+
... break
|
|
463
|
+
>>>
|
|
464
|
+
>>> asyncio.run(watch_files())
|
|
465
|
+
"""
|
|
466
|
+
# Lazy-load WebSocket client from sandbox if needed
|
|
467
|
+
if self._sandbox is not None:
|
|
468
|
+
self._sandbox._ensure_ws_client()
|
|
469
|
+
ws_client = self._sandbox._ws_client
|
|
470
|
+
else:
|
|
471
|
+
raise RuntimeError(
|
|
472
|
+
"WebSocket client not available. "
|
|
473
|
+
"File watching requires websockets library: pip install websockets"
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Connect to file watcher endpoint
|
|
477
|
+
async with await ws_client.connect("/files/watch", timeout=timeout) as ws:
|
|
478
|
+
# Send watch request
|
|
479
|
+
await ws_client.send_message(ws, {
|
|
480
|
+
"action": "watch",
|
|
481
|
+
"path": path
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
# Stream change events
|
|
485
|
+
async for message in ws_client.iter_messages(ws):
|
|
486
|
+
yield message
|
|
487
|
+
|
|
488
|
+
def __repr__(self) -> str:
|
|
489
|
+
return f"<Files client={self._client}>"
|