ai-computer-client 0.1.1__py3-none-any.whl → 0.3.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.
ai_computer/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- from .client import SandboxClient, SandboxResponse, StreamEvent
1
+ from .client import SandboxClient, SandboxResponse, StreamEvent, FileOperationResponse
2
2
 
3
- __version__ = "0.1.0"
4
- __all__ = ["SandboxClient", "SandboxResponse", "StreamEvent"]
3
+ __version__ = "0.3.0"
4
+ __all__ = ["SandboxClient", "SandboxResponse", "StreamEvent", "FileOperationResponse"]
ai_computer/client.py CHANGED
@@ -1,8 +1,11 @@
1
1
  import aiohttp
2
2
  import json
3
3
  import asyncio
4
- from typing import Optional, Dict, AsyncGenerator, Union
4
+ from typing import Optional, Dict, AsyncGenerator, Union, List, BinaryIO
5
5
  from dataclasses import dataclass
6
+ import os
7
+ import mimetypes
8
+ from pathlib import Path
6
9
 
7
10
  @dataclass
8
11
  class SandboxResponse:
@@ -28,6 +31,25 @@ class StreamEvent:
28
31
  type: str
29
32
  data: str
30
33
 
34
+ @dataclass
35
+ class FileOperationResponse:
36
+ """Response from file operations.
37
+
38
+ Attributes:
39
+ success: Whether the operation was successful
40
+ filename: Name of the file
41
+ size: Size of the file in bytes
42
+ path: Path where the file was saved
43
+ message: Optional status message
44
+ error: Optional error message if operation failed
45
+ """
46
+ success: bool
47
+ filename: Optional[str] = None
48
+ size: Optional[int] = None
49
+ path: Optional[str] = None
50
+ message: Optional[str] = None
51
+ error: Optional[str] = None
52
+
31
53
  class SandboxClient:
32
54
  """Client for interacting with the AI Sandbox service.
33
55
 
@@ -241,6 +263,59 @@ class SandboxClient:
241
263
  except Exception as e:
242
264
  yield StreamEvent(type="error", data=f"Connection error: {str(e)}")
243
265
 
266
+ async def execute_shell(
267
+ self,
268
+ command: str,
269
+ args: Optional[List[str]] = None,
270
+ timeout: int = 30
271
+ ) -> SandboxResponse:
272
+ """Execute a shell command in the sandbox.
273
+
274
+ Args:
275
+ command: The shell command to execute
276
+ args: Optional list of arguments for the command
277
+ timeout: Maximum execution time in seconds
278
+
279
+ Returns:
280
+ SandboxResponse containing execution results
281
+ """
282
+ if not self.token or not self.sandbox_id:
283
+ return SandboxResponse(success=False, error="Client not properly initialized. Call setup() first")
284
+
285
+ # Ensure sandbox is ready
286
+ ready = await self.wait_for_ready()
287
+ if not ready.success:
288
+ return ready
289
+
290
+ headers = {
291
+ "Authorization": f"Bearer {self.token}",
292
+ "Content-Type": "application/json"
293
+ }
294
+
295
+ data = {
296
+ "command": command,
297
+ "args": args or [],
298
+ "timeout": timeout
299
+ }
300
+
301
+ try:
302
+ async with aiohttp.ClientSession() as session:
303
+ async with session.post(
304
+ f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/execute/shell",
305
+ headers=headers,
306
+ json=data
307
+ ) as response:
308
+ if response.status != 200:
309
+ error_text = await response.text()
310
+ return SandboxResponse(success=False, error=error_text)
311
+
312
+ # Parse the response
313
+ result = await response.json()
314
+ return SandboxResponse(success=True, data=result)
315
+
316
+ except Exception as e:
317
+ return SandboxResponse(success=False, error=f"Connection error: {str(e)}")
318
+
244
319
  async def cleanup(self) -> SandboxResponse:
245
320
  """Delete the sandbox.
246
321
 
@@ -262,4 +337,417 @@ class SandboxClient:
262
337
  return SandboxResponse(success=True, data=data)
263
338
  else:
264
339
  text = await response.text()
265
- return SandboxResponse(success=False, error=text)
340
+ return SandboxResponse(success=False, error=text)
341
+
342
+ async def upload_file(
343
+ self,
344
+ file_path: Union[str, Path],
345
+ destination: str = "/workspace",
346
+ chunk_size: int = 1024 * 1024, # 1MB chunks
347
+ timeout: int = 300 # 5 minutes
348
+ ) -> FileOperationResponse:
349
+ """Upload a file to the sandbox environment.
350
+
351
+ Args:
352
+ file_path: Path to the file to upload
353
+ destination: Destination path in the sandbox (default: /workspace)
354
+ chunk_size: Size of chunks for reading large files
355
+ timeout: Maximum upload time in seconds
356
+
357
+ Returns:
358
+ FileOperationResponse containing upload results
359
+ """
360
+ if not self.token or not self.sandbox_id:
361
+ return FileOperationResponse(
362
+ success=False,
363
+ error="Client not properly initialized. Call setup() first"
364
+ )
365
+
366
+ # Ensure sandbox is ready
367
+ ready = await self.wait_for_ready()
368
+ if not ready.success:
369
+ return FileOperationResponse(
370
+ success=False,
371
+ error=ready.error or "Sandbox not ready"
372
+ )
373
+
374
+ # Convert to Path object and validate file
375
+ file_path = Path(file_path)
376
+ if not file_path.exists():
377
+ return FileOperationResponse(
378
+ success=False,
379
+ error=f"File not found: {file_path}"
380
+ )
381
+
382
+ if not file_path.is_file():
383
+ return FileOperationResponse(
384
+ success=False,
385
+ error=f"Not a file: {file_path}"
386
+ )
387
+
388
+ # Get file size and validate
389
+ file_size = file_path.stat().st_size
390
+ if file_size > 100 * 1024 * 1024: # 100MB limit
391
+ return FileOperationResponse(
392
+ success=False,
393
+ error="File too large. Maximum size is 100MB"
394
+ )
395
+
396
+ try:
397
+ # Prepare the upload
398
+ headers = {
399
+ "Authorization": f"Bearer {self.token}"
400
+ }
401
+
402
+ # Guess content type
403
+ content_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream'
404
+
405
+ # Prepare multipart form data
406
+ data = aiohttp.FormData()
407
+ data.add_field('file',
408
+ open(file_path, 'rb'),
409
+ filename=file_path.name,
410
+ content_type=content_type)
411
+ data.add_field('path', destination)
412
+
413
+ timeout_settings = aiohttp.ClientTimeout(
414
+ total=timeout,
415
+ connect=30,
416
+ sock_connect=30,
417
+ sock_read=timeout
418
+ )
419
+
420
+ async with aiohttp.ClientSession(timeout=timeout_settings) as session:
421
+ async with session.post(
422
+ f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/files/upload",
423
+ headers=headers,
424
+ data=data
425
+ ) as response:
426
+ if response.status != 200:
427
+ error_text = await response.text()
428
+ return FileOperationResponse(
429
+ success=False,
430
+ error=f"Upload failed: {error_text}"
431
+ )
432
+
433
+ result = await response.json()
434
+ return FileOperationResponse(
435
+ success=True,
436
+ filename=result.get("filename"),
437
+ size=result.get("size"),
438
+ path=result.get("path"),
439
+ message=result.get("message")
440
+ )
441
+
442
+ except asyncio.TimeoutError:
443
+ return FileOperationResponse(
444
+ success=False,
445
+ error=f"Upload timed out after {timeout} seconds"
446
+ )
447
+ except Exception as e:
448
+ return FileOperationResponse(
449
+ success=False,
450
+ error=f"Upload failed: {str(e)}"
451
+ )
452
+
453
+ async def download_file(
454
+ self,
455
+ sandbox_path: str,
456
+ local_path: Optional[Union[str, Path]] = None,
457
+ chunk_size: int = 8192, # 8KB chunks for download
458
+ timeout: int = 300 # 5 minutes
459
+ ) -> FileOperationResponse:
460
+ """Download a file from the sandbox environment.
461
+
462
+ Args:
463
+ sandbox_path: Path to the file in the sandbox
464
+ local_path: Local path to save the file (default: current directory with original filename)
465
+ chunk_size: Size of chunks for downloading large files
466
+ timeout: Maximum download time in seconds
467
+
468
+ Returns:
469
+ FileOperationResponse containing download results
470
+ """
471
+ if not self.token or not self.sandbox_id:
472
+ return FileOperationResponse(
473
+ success=False,
474
+ error="Client not properly initialized. Call setup() first"
475
+ )
476
+
477
+ # Ensure sandbox is ready
478
+ ready = await self.wait_for_ready()
479
+ if not ready.success:
480
+ return FileOperationResponse(
481
+ success=False,
482
+ error=ready.error or "Sandbox not ready"
483
+ )
484
+
485
+ # Normalize sandbox path
486
+ sandbox_path = sandbox_path.lstrip('/')
487
+
488
+ # Determine local path
489
+ if local_path is None:
490
+ local_path = Path(os.path.basename(sandbox_path))
491
+ else:
492
+ local_path = Path(local_path)
493
+
494
+ # Create parent directories if they don't exist
495
+ local_path.parent.mkdir(parents=True, exist_ok=True)
496
+
497
+ try:
498
+ timeout_settings = aiohttp.ClientTimeout(
499
+ total=timeout,
500
+ connect=30,
501
+ sock_connect=30,
502
+ sock_read=timeout
503
+ )
504
+
505
+ headers = {
506
+ "Authorization": f"Bearer {self.token}"
507
+ }
508
+
509
+ async with aiohttp.ClientSession(timeout=timeout_settings) as session:
510
+ async with session.get(
511
+ f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/files/download/{sandbox_path}",
512
+ headers=headers
513
+ ) as response:
514
+ if response.status != 200:
515
+ error_text = await response.text()
516
+ return FileOperationResponse(
517
+ success=False,
518
+ error=f"Download failed: {error_text}"
519
+ )
520
+
521
+ # Get content length if available
522
+ total_size = int(response.headers.get('content-length', 0))
523
+
524
+ # Download the file in chunks
525
+ downloaded_size = 0
526
+ try:
527
+ with open(local_path, 'wb') as f:
528
+ async for chunk in response.content.iter_chunked(chunk_size):
529
+ f.write(chunk)
530
+ downloaded_size += len(chunk)
531
+
532
+ return FileOperationResponse(
533
+ success=True,
534
+ filename=local_path.name,
535
+ size=downloaded_size or total_size,
536
+ path=str(local_path.absolute()),
537
+ message="File downloaded successfully"
538
+ )
539
+ except Exception as e:
540
+ # Clean up partial download
541
+ if local_path.exists():
542
+ local_path.unlink()
543
+ raise e
544
+
545
+ except asyncio.TimeoutError:
546
+ # Clean up partial download
547
+ if local_path.exists():
548
+ local_path.unlink()
549
+ return FileOperationResponse(
550
+ success=False,
551
+ error=f"Download timed out after {timeout} seconds"
552
+ )
553
+ except Exception as e:
554
+ # Clean up partial download
555
+ if local_path.exists():
556
+ local_path.unlink()
557
+ return FileOperationResponse(
558
+ success=False,
559
+ error=f"Download failed: {str(e)}"
560
+ )
561
+
562
+ async def upload_bytes(
563
+ self,
564
+ content: Union[bytes, BinaryIO],
565
+ filename: str,
566
+ destination: str = "/workspace",
567
+ content_type: Optional[str] = None,
568
+ timeout: int = 300 # 5 minutes
569
+ ) -> FileOperationResponse:
570
+ """Upload bytes or a file-like object to the sandbox environment.
571
+
572
+ Args:
573
+ content: Bytes or file-like object to upload
574
+ filename: Name to give the file in the sandbox
575
+ destination: Destination path in the sandbox (default: /workspace)
576
+ content_type: Optional MIME type (will be guessed from filename if not provided)
577
+ timeout: Maximum upload time in seconds
578
+
579
+ Returns:
580
+ FileOperationResponse containing upload results
581
+ """
582
+ if not self.token or not self.sandbox_id:
583
+ return FileOperationResponse(
584
+ success=False,
585
+ error="Client not properly initialized. Call setup() first"
586
+ )
587
+
588
+ # Ensure sandbox is ready
589
+ ready = await self.wait_for_ready()
590
+ if not ready.success:
591
+ return FileOperationResponse(
592
+ success=False,
593
+ error=ready.error or "Sandbox not ready"
594
+ )
595
+
596
+ try:
597
+ # Handle both bytes and file-like objects
598
+ if isinstance(content, bytes):
599
+ file_obj = content
600
+ content_length = len(content)
601
+ else:
602
+ # Ensure we're at the start of the file
603
+ if hasattr(content, 'seek'):
604
+ content.seek(0)
605
+ file_obj = content
606
+ # Try to get content length if possible
607
+ content_length = None
608
+ if hasattr(content, 'seek') and hasattr(content, 'tell'):
609
+ try:
610
+ current_pos = content.tell()
611
+ content.seek(0, os.SEEK_END)
612
+ content_length = content.tell()
613
+ content.seek(current_pos)
614
+ except (OSError, IOError):
615
+ pass
616
+
617
+ # Validate size if we can determine it
618
+ if content_length and content_length > 100 * 1024 * 1024: # 100MB limit
619
+ return FileOperationResponse(
620
+ success=False,
621
+ error="Content too large. Maximum size is 100MB"
622
+ )
623
+
624
+ # Prepare the upload
625
+ headers = {
626
+ "Authorization": f"Bearer {self.token}"
627
+ }
628
+
629
+ # Guess content type if not provided
630
+ if not content_type:
631
+ content_type = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
632
+
633
+ # Prepare multipart form data
634
+ data = aiohttp.FormData()
635
+ data.add_field('file',
636
+ file_obj,
637
+ filename=filename,
638
+ content_type=content_type)
639
+ data.add_field('path', destination)
640
+
641
+ timeout_settings = aiohttp.ClientTimeout(
642
+ total=timeout,
643
+ connect=30,
644
+ sock_connect=30,
645
+ sock_read=timeout
646
+ )
647
+
648
+ async with aiohttp.ClientSession(timeout=timeout_settings) as session:
649
+ async with session.post(
650
+ f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/files/upload",
651
+ headers=headers,
652
+ data=data
653
+ ) as response:
654
+ if response.status != 200:
655
+ error_text = await response.text()
656
+ return FileOperationResponse(
657
+ success=False,
658
+ error=f"Upload failed: {error_text}"
659
+ )
660
+
661
+ result = await response.json()
662
+ return FileOperationResponse(
663
+ success=True,
664
+ filename=result.get("filename"),
665
+ size=result.get("size"),
666
+ path=result.get("path"),
667
+ message=result.get("message")
668
+ )
669
+
670
+ except asyncio.TimeoutError:
671
+ return FileOperationResponse(
672
+ success=False,
673
+ error=f"Upload timed out after {timeout} seconds"
674
+ )
675
+ except Exception as e:
676
+ return FileOperationResponse(
677
+ success=False,
678
+ error=f"Upload failed: {str(e)}"
679
+ )
680
+
681
+ async def download_bytes(
682
+ self,
683
+ sandbox_path: str,
684
+ chunk_size: int = 8192, # 8KB chunks for download
685
+ timeout: int = 300 # 5 minutes
686
+ ) -> Union[bytes, FileOperationResponse]:
687
+ """Download a file from the sandbox environment into memory.
688
+
689
+ Args:
690
+ sandbox_path: Path to the file in the sandbox
691
+ chunk_size: Size of chunks for downloading large files
692
+ timeout: Maximum download time in seconds
693
+
694
+ Returns:
695
+ On success: The file contents as bytes
696
+ On failure: FileOperationResponse with error details
697
+ """
698
+ if not self.token or not self.sandbox_id:
699
+ return FileOperationResponse(
700
+ success=False,
701
+ error="Client not properly initialized. Call setup() first"
702
+ )
703
+
704
+ # Ensure sandbox is ready
705
+ ready = await self.wait_for_ready()
706
+ if not ready.success:
707
+ return FileOperationResponse(
708
+ success=False,
709
+ error=ready.error or "Sandbox not ready"
710
+ )
711
+
712
+ # Normalize sandbox path
713
+ sandbox_path = sandbox_path.lstrip('/')
714
+
715
+ try:
716
+ timeout_settings = aiohttp.ClientTimeout(
717
+ total=timeout,
718
+ connect=30,
719
+ sock_connect=30,
720
+ sock_read=timeout
721
+ )
722
+
723
+ headers = {
724
+ "Authorization": f"Bearer {self.token}"
725
+ }
726
+
727
+ async with aiohttp.ClientSession(timeout=timeout_settings) as session:
728
+ async with session.get(
729
+ f"{self.base_url}/api/v1/sandbox/{self.sandbox_id}/files/download/{sandbox_path}",
730
+ headers=headers
731
+ ) as response:
732
+ if response.status != 200:
733
+ error_text = await response.text()
734
+ return FileOperationResponse(
735
+ success=False,
736
+ error=f"Download failed: {error_text}"
737
+ )
738
+
739
+ # Read the entire response into memory
740
+ content = await response.read()
741
+
742
+ return content
743
+
744
+ except asyncio.TimeoutError:
745
+ return FileOperationResponse(
746
+ success=False,
747
+ error=f"Download timed out after {timeout} seconds"
748
+ )
749
+ except Exception as e:
750
+ return FileOperationResponse(
751
+ success=False,
752
+ error=f"Download failed: {str(e)}"
753
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-computer-client
3
- Version: 0.1.1
3
+ Version: 0.3.0
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
@@ -0,0 +1,6 @@
1
+ ai_computer/__init__.py,sha256=41ibex5hg-OZ28VsmhU2RqxzzV6YEUD6vLbuNSddRsk,197
2
+ ai_computer/client.py,sha256=--gmDUzABePoV7XWvo-zMH69Jg3JQR1vbotwEoAHtSg,28080
3
+ ai_computer_client-0.3.0.dist-info/METADATA,sha256=rhUG0es12ZwHZApMsT8CBna_BB_2VJSrpMVr9Kc3wcs,5654
4
+ ai_computer_client-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ ai_computer_client-0.3.0.dist-info/licenses/LICENSE,sha256=N_0S5G1Wik2LWVDViJMAM0Z-6vTBX1bvDjb8vouBA-c,1068
6
+ ai_computer_client-0.3.0.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- ai_computer/__init__.py,sha256=g2yDiDT6i24MV3Y2JNfk2ZFkYvxvrN6NXywjZPslXDY,149
2
- ai_computer/client.py,sha256=PaBRmSFzgOXNxxQAd3zSCaqzZ8rcvUOhsnFulu40uhc,10214
3
- ai_computer_client-0.1.1.dist-info/METADATA,sha256=DZpD89o215WIDWe213XGNFOhEMUQ61P5kUzzLSr0eaE,5654
4
- ai_computer_client-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
- ai_computer_client-0.1.1.dist-info/licenses/LICENSE,sha256=N_0S5G1Wik2LWVDViJMAM0Z-6vTBX1bvDjb8vouBA-c,1068
6
- ai_computer_client-0.1.1.dist-info/RECORD,,