ai-computer-client 0.3.3__py3-none-any.whl → 0.3.4__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.
- ai_computer/client.py +24 -8
- ai_computer/submodules/code.py +3 -3
- ai_computer/submodules/filesystem.py +95 -30
- {ai_computer_client-0.3.3.dist-info → ai_computer_client-0.3.4.dist-info}/METADATA +28 -5
- ai_computer_client-0.3.4.dist-info/RECORD +12 -0
- ai_computer_client-0.3.3.dist-info/RECORD +0 -12
- {ai_computer_client-0.3.3.dist-info → ai_computer_client-0.3.4.dist-info}/WHEEL +0 -0
- {ai_computer_client-0.3.3.dist-info → ai_computer_client-0.3.4.dist-info}/licenses/LICENSE +0 -0
ai_computer/client.py
CHANGED
@@ -6,10 +6,14 @@ from dataclasses import dataclass
|
|
6
6
|
import os
|
7
7
|
import mimetypes
|
8
8
|
from pathlib import Path
|
9
|
+
import logging
|
9
10
|
|
10
11
|
from .models import SandboxResponse, StreamEvent, FileOperationResponse
|
11
12
|
from .submodules import FileSystemModule, ShellModule, CodeModule
|
12
13
|
|
14
|
+
# Set up logging
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
13
17
|
@dataclass
|
14
18
|
class SandboxResponse:
|
15
19
|
"""Response from sandbox operations.
|
@@ -137,19 +141,23 @@ class SandboxClient:
|
|
137
141
|
"""Wait for the sandbox to be ready.
|
138
142
|
|
139
143
|
Args:
|
140
|
-
max_attempts: Maximum number of attempts to check
|
144
|
+
max_attempts: Maximum number of attempts to check status
|
141
145
|
delay: Delay between attempts in seconds
|
142
146
|
|
143
147
|
Returns:
|
144
|
-
SandboxResponse
|
148
|
+
SandboxResponse with success=True if sandbox is ready
|
145
149
|
"""
|
146
|
-
if not self.
|
147
|
-
return SandboxResponse(
|
150
|
+
if not self.sandbox_id:
|
151
|
+
return SandboxResponse(
|
152
|
+
success=False,
|
153
|
+
error="Sandbox ID not set. Call setup() first."
|
154
|
+
)
|
148
155
|
|
149
156
|
headers = {"Authorization": f"Bearer {self.token}"}
|
150
157
|
|
151
158
|
for attempt in range(max_attempts):
|
152
159
|
try:
|
160
|
+
logger.debug(f"Checking sandbox status (attempt {attempt + 1}/{max_attempts})...")
|
153
161
|
async with aiohttp.ClientSession() as session:
|
154
162
|
async with session.get(
|
155
163
|
f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/status",
|
@@ -157,13 +165,16 @@ class SandboxClient:
|
|
157
165
|
) as response:
|
158
166
|
if response.status != 200:
|
159
167
|
# If we get an error, wait and try again
|
168
|
+
logger.debug(f"Waiting {delay}s before next attempt...")
|
160
169
|
await asyncio.sleep(delay)
|
161
170
|
continue
|
162
171
|
|
163
172
|
data = await response.json()
|
164
|
-
status = data.get("status")
|
173
|
+
status = data.get("status", "").lower()
|
174
|
+
logger.debug(f"Current sandbox status: {status}")
|
165
175
|
|
166
|
-
|
176
|
+
# Check for both 'ready' and 'running' status as indicators that the sandbox is ready
|
177
|
+
if status == "ready" or status == "running":
|
167
178
|
return SandboxResponse(success=True, data=data)
|
168
179
|
elif status == "error":
|
169
180
|
return SandboxResponse(
|
@@ -172,13 +183,18 @@ class SandboxClient:
|
|
172
183
|
)
|
173
184
|
|
174
185
|
# If not ready yet, wait and try again
|
186
|
+
logger.debug(f"Waiting {delay}s before next attempt...")
|
175
187
|
await asyncio.sleep(delay)
|
176
188
|
|
177
189
|
except Exception as e:
|
178
190
|
# If we get an exception, wait and try again
|
191
|
+
logger.error(f"Error checking sandbox status: {str(e)}")
|
179
192
|
await asyncio.sleep(delay)
|
180
193
|
|
181
|
-
return SandboxResponse(
|
194
|
+
return SandboxResponse(
|
195
|
+
success=False,
|
196
|
+
error=f"Sandbox not ready after {max_attempts} attempts"
|
197
|
+
)
|
182
198
|
|
183
199
|
async def cleanup(self) -> SandboxResponse:
|
184
200
|
"""Delete the sandbox.
|
@@ -367,7 +383,7 @@ class SandboxClient:
|
|
367
383
|
await self.wait_for_ready()
|
368
384
|
|
369
385
|
try:
|
370
|
-
response = await self.fs.
|
386
|
+
response = await self.fs.download_bytes(remote_path, timeout=timeout or 300)
|
371
387
|
if response.success:
|
372
388
|
return response.data.get('content')
|
373
389
|
else:
|
ai_computer/submodules/code.py
CHANGED
@@ -44,7 +44,7 @@ class CodeModule(BaseSubmodule):
|
|
44
44
|
try:
|
45
45
|
async with aiohttp.ClientSession() as session:
|
46
46
|
async with session.post(
|
47
|
-
f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/execute
|
47
|
+
f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/execute",
|
48
48
|
headers=headers,
|
49
49
|
json=data
|
50
50
|
) as response:
|
@@ -87,7 +87,7 @@ class CodeModule(BaseSubmodule):
|
|
87
87
|
try:
|
88
88
|
async with aiohttp.ClientSession() as session:
|
89
89
|
async with session.post(
|
90
|
-
f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/execute/
|
90
|
+
f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/execute/stream",
|
91
91
|
headers=headers,
|
92
92
|
json=data
|
93
93
|
) as response:
|
@@ -186,7 +186,7 @@ result
|
|
186
186
|
try:
|
187
187
|
async with aiohttp.ClientSession() as session:
|
188
188
|
async with session.post(
|
189
|
-
f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/execute
|
189
|
+
f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/execute",
|
190
190
|
headers=headers,
|
191
191
|
json=data
|
192
192
|
) as response:
|
@@ -4,6 +4,7 @@ import os
|
|
4
4
|
import mimetypes
|
5
5
|
from pathlib import Path
|
6
6
|
from typing import Optional, Union, BinaryIO, Dict, Any
|
7
|
+
from urllib.parse import quote
|
7
8
|
|
8
9
|
from .base import BaseSubmodule
|
9
10
|
from ..models import FileOperationResponse, SandboxResponse
|
@@ -122,14 +123,14 @@ class FileSystemModule(BaseSubmodule):
|
|
122
123
|
async def download_file(
|
123
124
|
self,
|
124
125
|
remote_path: str,
|
125
|
-
local_path: Optional[
|
126
|
-
timeout: int = 300
|
126
|
+
local_path: Optional[str] = None,
|
127
|
+
timeout: int = 300
|
127
128
|
) -> FileOperationResponse:
|
128
|
-
"""Download a file from the sandbox.
|
129
|
+
"""Download a file from the sandbox to the local filesystem.
|
129
130
|
|
130
131
|
Args:
|
131
132
|
remote_path: Path to the file in the sandbox
|
132
|
-
local_path: Local path to save the file (
|
133
|
+
local_path: Local path to save the file (defaults to basename of remote_path)
|
133
134
|
timeout: Maximum download time in seconds
|
134
135
|
|
135
136
|
Returns:
|
@@ -139,15 +140,9 @@ class FileSystemModule(BaseSubmodule):
|
|
139
140
|
if not ready.success:
|
140
141
|
return FileOperationResponse(
|
141
142
|
success=False,
|
142
|
-
error=ready.error
|
143
|
+
error=ready.error
|
143
144
|
)
|
144
145
|
|
145
|
-
# Ensure path is absolute and normalize any double slashes
|
146
|
-
if not remote_path.startswith('/'):
|
147
|
-
remote_path = f"/{remote_path}"
|
148
|
-
clean_path = '/'.join(part for part in remote_path.split('/') if part)
|
149
|
-
clean_path = f"/{clean_path}"
|
150
|
-
|
151
146
|
# Determine local path if not provided
|
152
147
|
if local_path is None:
|
153
148
|
local_path = os.path.basename(remote_path)
|
@@ -160,9 +155,12 @@ class FileSystemModule(BaseSubmodule):
|
|
160
155
|
"Authorization": f"Bearer {self.token}"
|
161
156
|
}
|
162
157
|
|
163
|
-
|
164
|
-
|
165
|
-
|
158
|
+
# Store original path for error messages
|
159
|
+
original_path = remote_path
|
160
|
+
|
161
|
+
# Ensure path is absolute
|
162
|
+
if not remote_path.startswith('/'):
|
163
|
+
remote_path = f"/{remote_path}"
|
166
164
|
|
167
165
|
timeout_settings = aiohttp.ClientTimeout(
|
168
166
|
total=timeout,
|
@@ -173,8 +171,12 @@ class FileSystemModule(BaseSubmodule):
|
|
173
171
|
|
174
172
|
try:
|
175
173
|
async with aiohttp.ClientSession(timeout=timeout_settings) as session:
|
174
|
+
# Use the new API endpoint with query parameters
|
175
|
+
url = f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/files"
|
176
|
+
params = {"path": quote(remote_path)}
|
177
|
+
|
176
178
|
async with session.get(
|
177
|
-
|
179
|
+
url,
|
178
180
|
headers=headers,
|
179
181
|
params=params
|
180
182
|
) as response:
|
@@ -182,7 +184,7 @@ class FileSystemModule(BaseSubmodule):
|
|
182
184
|
error_text = await response.text()
|
183
185
|
return FileOperationResponse(
|
184
186
|
success=False,
|
185
|
-
error=f"
|
187
|
+
error=f"Failed to download file '{original_path}': {error_text}"
|
186
188
|
)
|
187
189
|
|
188
190
|
# Get content disposition header to extract filename
|
@@ -225,38 +227,36 @@ class FileSystemModule(BaseSubmodule):
|
|
225
227
|
error=f"Download failed: {str(e)}"
|
226
228
|
)
|
227
229
|
|
228
|
-
async def read_file(self, path: str, encoding: str = 'utf-8') -> SandboxResponse:
|
229
|
-
"""Read a file from the sandbox
|
230
|
+
async def read_file(self, path: str, encoding: Optional[str] = 'utf-8') -> SandboxResponse:
|
231
|
+
"""Read a file from the sandbox.
|
230
232
|
|
231
233
|
Args:
|
232
234
|
path: Path to the file in the sandbox
|
233
|
-
encoding: Text encoding to use (
|
235
|
+
encoding: Text encoding to use (None for binary)
|
234
236
|
|
235
237
|
Returns:
|
236
|
-
SandboxResponse with the file
|
238
|
+
SandboxResponse with the file content
|
237
239
|
"""
|
238
240
|
ready = await self._ensure_ready()
|
239
241
|
if not ready.success:
|
240
242
|
return ready
|
241
|
-
|
242
|
-
# Ensure path is absolute
|
243
|
+
|
244
|
+
# Ensure path is absolute
|
243
245
|
if not path.startswith('/'):
|
244
246
|
path = f"/{path}"
|
245
|
-
|
246
|
-
clean_path = f"/{clean_path}"
|
247
|
-
|
247
|
+
|
248
248
|
headers = {
|
249
249
|
"Authorization": f"Bearer {self.token}"
|
250
250
|
}
|
251
251
|
|
252
|
-
params = {
|
253
|
-
"path": clean_path
|
254
|
-
}
|
255
|
-
|
256
252
|
try:
|
257
253
|
async with aiohttp.ClientSession() as session:
|
254
|
+
# Use the new API endpoint with query parameters
|
255
|
+
url = f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/files"
|
256
|
+
params = {"path": quote(path)}
|
257
|
+
|
258
258
|
async with session.get(
|
259
|
-
|
259
|
+
url,
|
260
260
|
headers=headers,
|
261
261
|
params=params
|
262
262
|
) as response:
|
@@ -370,4 +370,69 @@ class FileSystemModule(BaseSubmodule):
|
|
370
370
|
# Clean up the temporary file
|
371
371
|
if os.path.exists(temp_file.name):
|
372
372
|
os.unlink(temp_file.name)
|
373
|
+
|
374
|
+
async def download_bytes(self, path: str, timeout: int = 300) -> SandboxResponse:
|
375
|
+
"""Download a file from the sandbox into memory.
|
376
|
+
|
377
|
+
Args:
|
378
|
+
path: Path to the file in the sandbox
|
379
|
+
timeout: Maximum download time in seconds
|
380
|
+
|
381
|
+
Returns:
|
382
|
+
SandboxResponse with the file content as bytes in the data field
|
383
|
+
"""
|
384
|
+
ready = await self._ensure_ready()
|
385
|
+
if not ready.success:
|
386
|
+
return ready
|
387
|
+
|
388
|
+
# Ensure path is absolute
|
389
|
+
if not path.startswith('/'):
|
390
|
+
path = f"/{path}"
|
391
|
+
|
392
|
+
headers = {
|
393
|
+
"Authorization": f"Bearer {self.token}"
|
394
|
+
}
|
395
|
+
|
396
|
+
timeout_settings = aiohttp.ClientTimeout(
|
397
|
+
total=timeout,
|
398
|
+
connect=30,
|
399
|
+
sock_connect=30,
|
400
|
+
sock_read=timeout
|
401
|
+
)
|
402
|
+
|
403
|
+
try:
|
404
|
+
async with aiohttp.ClientSession(timeout=timeout_settings) as session:
|
405
|
+
# Use the new API endpoint with query parameters
|
406
|
+
url = f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/files"
|
407
|
+
params = {"path": quote(path)}
|
408
|
+
|
409
|
+
async with session.get(
|
410
|
+
url,
|
411
|
+
headers=headers,
|
412
|
+
params=params
|
413
|
+
) as response:
|
414
|
+
if response.status != 200:
|
415
|
+
error_text = await response.text()
|
416
|
+
return SandboxResponse(
|
417
|
+
success=False,
|
418
|
+
error=f"Download failed: {error_text}"
|
419
|
+
)
|
420
|
+
|
421
|
+
# Read the content
|
422
|
+
content = await response.read()
|
423
|
+
size = len(content)
|
424
|
+
|
425
|
+
return SandboxResponse(
|
426
|
+
success=True,
|
427
|
+
data={
|
428
|
+
'content': content,
|
429
|
+
'size': size
|
430
|
+
}
|
431
|
+
)
|
432
|
+
|
433
|
+
except Exception as e:
|
434
|
+
return SandboxResponse(
|
435
|
+
success=False,
|
436
|
+
error=f"Failed to download file: {str(e)}"
|
437
|
+
)
|
373
438
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ai-computer-client
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.4
|
4
4
|
Summary: Python client for interacting with the AI Computer service
|
5
5
|
Project-URL: Homepage, https://github.com/ColeMurray/ai-computer-client-python
|
6
6
|
Project-URL: Documentation, https://github.com/ColeMurray/ai-computer-client-python#readme
|
@@ -23,6 +23,9 @@ Provides-Extra: dev
|
|
23
23
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
24
24
|
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
25
25
|
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
26
|
+
Provides-Extra: integration
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'integration'
|
28
|
+
Requires-Dist: pytest>=7.0.0; extra == 'integration'
|
26
29
|
Description-Content-Type: text/markdown
|
27
30
|
|
28
31
|
# AI Computer Python Client
|
@@ -201,14 +204,34 @@ class StreamEvent:
|
|
201
204
|
|
202
205
|
### Running Tests
|
203
206
|
|
204
|
-
|
205
|
-
# Install development dependencies
|
206
|
-
pip install -e ".[dev]"
|
207
|
+
To run the unit tests:
|
207
208
|
|
208
|
-
|
209
|
+
```bash
|
209
210
|
pytest
|
210
211
|
```
|
211
212
|
|
213
|
+
### Running Integration Tests
|
214
|
+
|
215
|
+
We have a comprehensive suite of integration tests that validate the client against the live API. These tests are automatically run as part of our CI/CD pipeline before each release.
|
216
|
+
|
217
|
+
To run the integration tests locally:
|
218
|
+
|
219
|
+
1. Set the required environment variables:
|
220
|
+
|
221
|
+
```bash
|
222
|
+
export AI_COMPUTER_API_KEY="your_api_key_here"
|
223
|
+
# Optional: Use a specific sandbox ID (if not provided, a new one will be created)
|
224
|
+
export AI_COMPUTER_SANDBOX_ID="optional_sandbox_id"
|
225
|
+
```
|
226
|
+
|
227
|
+
2. Run the tests:
|
228
|
+
|
229
|
+
```bash
|
230
|
+
python -m integration_tests.test_integration
|
231
|
+
```
|
232
|
+
|
233
|
+
For more details, see the [Integration Tests README](integration_tests/README.md).
|
234
|
+
|
212
235
|
### Contributing
|
213
236
|
|
214
237
|
1. Fork the repository
|
@@ -0,0 +1,12 @@
|
|
1
|
+
ai_computer/__init__.py,sha256=0L4QSM3q4zWzYqNwisOqF_8ObcMdacmlXHbn55RX9YU,364
|
2
|
+
ai_computer/client.py,sha256=U-xm04Koy429TbiMwJTURLeRH3NWy3lJFX5i7rvvacs,15090
|
3
|
+
ai_computer/models.py,sha256=JG3gTKqCVrKlKsr4REAq5tb-WmZttn78LzxlrNiOJsQ,1214
|
4
|
+
ai_computer/submodules/__init__.py,sha256=kz4NTuF9r3i_VwTLWsyRHaHKereyq0kFe1HrfKQLtB4,162
|
5
|
+
ai_computer/submodules/base.py,sha256=3WoURENRnho26PkdghKM8S9s3-GYr11CwZidXhs-fFM,2530
|
6
|
+
ai_computer/submodules/code.py,sha256=8GNpgL-6aVpD1UNtq-nvtvnMQy72dWKe247YA4cLsOs,11201
|
7
|
+
ai_computer/submodules/filesystem.py,sha256=7x3g6Cr8PDKIuc9ogETuVjcOAlFKoHXhUcuntgRJv3s,15831
|
8
|
+
ai_computer/submodules/shell.py,sha256=lcy4CpgoJWN6tsGi-IUb6PQpywLSNOD3_lt_yvSU6Y8,1632
|
9
|
+
ai_computer_client-0.3.4.dist-info/METADATA,sha256=vVUG7UlP2FxGwugFbRm4gdL_JzLW1vt-S_kOH1W6Ugk,6407
|
10
|
+
ai_computer_client-0.3.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
11
|
+
ai_computer_client-0.3.4.dist-info/licenses/LICENSE,sha256=N_0S5G1Wik2LWVDViJMAM0Z-6vTBX1bvDjb8vouBA-c,1068
|
12
|
+
ai_computer_client-0.3.4.dist-info/RECORD,,
|
@@ -1,12 +0,0 @@
|
|
1
|
-
ai_computer/__init__.py,sha256=0L4QSM3q4zWzYqNwisOqF_8ObcMdacmlXHbn55RX9YU,364
|
2
|
-
ai_computer/client.py,sha256=ozdVAp_I8l7lGI5zI8cQUN00RfJ_V8XhrkjspwHV5yI,14402
|
3
|
-
ai_computer/models.py,sha256=JG3gTKqCVrKlKsr4REAq5tb-WmZttn78LzxlrNiOJsQ,1214
|
4
|
-
ai_computer/submodules/__init__.py,sha256=kz4NTuF9r3i_VwTLWsyRHaHKereyq0kFe1HrfKQLtB4,162
|
5
|
-
ai_computer/submodules/base.py,sha256=3WoURENRnho26PkdghKM8S9s3-GYr11CwZidXhs-fFM,2530
|
6
|
-
ai_computer/submodules/code.py,sha256=U0cLcB7iu90QS3BbFP31oNPJYgpgvYSWzxakhxMa3-A,11216
|
7
|
-
ai_computer/submodules/filesystem.py,sha256=79gBefBIMj8qwFwTdVrpktOTqfvAfeZTf_jdTSxtcHs,13610
|
8
|
-
ai_computer/submodules/shell.py,sha256=lcy4CpgoJWN6tsGi-IUb6PQpywLSNOD3_lt_yvSU6Y8,1632
|
9
|
-
ai_computer_client-0.3.3.dist-info/METADATA,sha256=5kn2XaOh0LeQR7hexOsd1C1pOPrbr-pfWPLhDPUIaWU,5658
|
10
|
-
ai_computer_client-0.3.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
11
|
-
ai_computer_client-0.3.3.dist-info/licenses/LICENSE,sha256=N_0S5G1Wik2LWVDViJMAM0Z-6vTBX1bvDjb8vouBA-c,1068
|
12
|
-
ai_computer_client-0.3.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|