cua-computer 0.4.0__tar.gz → 0.4.2__tar.gz

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 (38) hide show
  1. {cua_computer-0.4.0 → cua_computer-0.4.2}/PKG-INFO +1 -1
  2. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/computer.py +20 -1
  3. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/providers/base.py +1 -0
  4. cua_computer-0.4.2/computer/providers/docker/__init__.py +13 -0
  5. cua_computer-0.4.2/computer/providers/docker/provider.py +502 -0
  6. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/providers/factory.py +24 -0
  7. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/ui/gradio/app.py +1 -1
  8. {cua_computer-0.4.0 → cua_computer-0.4.2}/pyproject.toml +3 -3
  9. {cua_computer-0.4.0 → cua_computer-0.4.2}/README.md +0 -0
  10. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/__init__.py +0 -0
  11. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/diorama_computer.py +0 -0
  12. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/helpers.py +0 -0
  13. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/interface/__init__.py +0 -0
  14. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/interface/base.py +0 -0
  15. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/interface/factory.py +0 -0
  16. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/interface/generic.py +0 -0
  17. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/interface/linux.py +0 -0
  18. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/interface/macos.py +0 -0
  19. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/interface/models.py +0 -0
  20. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/interface/windows.py +0 -0
  21. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/logger.py +0 -0
  22. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/models.py +0 -0
  23. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/providers/__init__.py +0 -0
  24. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/providers/cloud/__init__.py +0 -0
  25. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/providers/cloud/provider.py +0 -0
  26. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/providers/lume/__init__.py +0 -0
  27. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/providers/lume/provider.py +0 -0
  28. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/providers/lume_api.py +0 -0
  29. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/providers/lumier/__init__.py +0 -0
  30. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/providers/lumier/provider.py +0 -0
  31. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/providers/winsandbox/__init__.py +0 -0
  32. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/providers/winsandbox/provider.py +0 -0
  33. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/providers/winsandbox/setup_script.ps1 +0 -0
  34. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/telemetry.py +0 -0
  35. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/ui/__init__.py +0 -0
  36. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/ui/__main__.py +0 -0
  37. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/ui/gradio/__init__.py +0 -0
  38. {cua_computer-0.4.0 → cua_computer-0.4.2}/computer/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cua-computer
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Computer-Use Interface (CUI) framework powering Cua
5
5
  Author-Email: TryCua <gh@trycua.com>
6
6
  Requires-Python: >=3.11
@@ -43,7 +43,7 @@ class Computer:
43
43
  cpu: str = "4",
44
44
  os_type: OSType = "macos",
45
45
  name: str = "",
46
- image: str = "macos-sequoia-cua:latest",
46
+ image: Optional[str] = None,
47
47
  shared_directories: Optional[List[str]] = None,
48
48
  use_host_computer_server: bool = False,
49
49
  verbosity: Union[int, LogLevel] = logging.INFO,
@@ -88,6 +88,12 @@ class Computer:
88
88
  self.logger = Logger("computer", verbosity)
89
89
  self.logger.info("Initializing Computer...")
90
90
 
91
+ if os_type == "macos":
92
+ image = "macos-sequoia-cua:latest"
93
+ elif os_type == "linux":
94
+ image = "trycua/cua-ubuntu:latest"
95
+ image = str(image)
96
+
91
97
  # Store original parameters
92
98
  self.image = image
93
99
  self.port = port
@@ -301,6 +307,19 @@ class Computer:
301
307
  storage=storage,
302
308
  verbose=verbose,
303
309
  ephemeral=ephemeral,
310
+ noVNC_port=noVNC_port,
311
+ )
312
+ elif self.provider_type == VMProviderType.DOCKER:
313
+ self.config.vm_provider = VMProviderFactory.create_provider(
314
+ self.provider_type,
315
+ port=port,
316
+ host=host,
317
+ storage=storage,
318
+ shared_path=shared_path,
319
+ image=image or "trycua/cua-ubuntu:latest",
320
+ verbose=verbose,
321
+ ephemeral=ephemeral,
322
+ noVNC_port=noVNC_port,
304
323
  )
305
324
  else:
306
325
  raise ValueError(f"Unsupported provider type: {self.provider_type}")
@@ -11,6 +11,7 @@ class VMProviderType(StrEnum):
11
11
  LUMIER = "lumier"
12
12
  CLOUD = "cloud"
13
13
  WINSANDBOX = "winsandbox"
14
+ DOCKER = "docker"
14
15
  UNKNOWN = "unknown"
15
16
 
16
17
 
@@ -0,0 +1,13 @@
1
+ """Docker provider for running containers with computer-server."""
2
+
3
+ from .provider import DockerProvider
4
+
5
+ # Check if Docker is available
6
+ try:
7
+ import subprocess
8
+ subprocess.run(["docker", "--version"], capture_output=True, check=True)
9
+ HAS_DOCKER = True
10
+ except (subprocess.SubprocessError, FileNotFoundError):
11
+ HAS_DOCKER = False
12
+
13
+ __all__ = ["DockerProvider", "HAS_DOCKER"]
@@ -0,0 +1,502 @@
1
+ """
2
+ Docker VM provider implementation.
3
+
4
+ This provider uses Docker containers running the CUA Ubuntu image to create
5
+ Linux VMs with computer-server. It handles VM lifecycle operations through Docker
6
+ commands and container management.
7
+ """
8
+
9
+ import logging
10
+ import json
11
+ import asyncio
12
+ from typing import Dict, List, Optional, Any
13
+ import subprocess
14
+ import time
15
+ import re
16
+
17
+ from ..base import BaseVMProvider, VMProviderType
18
+
19
+ # Setup logging
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Check if Docker is available
23
+ try:
24
+ subprocess.run(["docker", "--version"], capture_output=True, check=True)
25
+ HAS_DOCKER = True
26
+ except (subprocess.SubprocessError, FileNotFoundError):
27
+ HAS_DOCKER = False
28
+
29
+
30
+ class DockerProvider(BaseVMProvider):
31
+ """
32
+ Docker VM Provider implementation using Docker containers.
33
+
34
+ This provider uses Docker to run containers with the CUA Ubuntu image
35
+ that includes computer-server for remote computer use.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ port: Optional[int] = 8000,
41
+ host: str = "localhost",
42
+ storage: Optional[str] = None,
43
+ shared_path: Optional[str] = None,
44
+ image: str = "trycua/cua-ubuntu:latest",
45
+ verbose: bool = False,
46
+ ephemeral: bool = False,
47
+ vnc_port: Optional[int] = 6901,
48
+ ):
49
+ """Initialize the Docker VM Provider.
50
+
51
+ Args:
52
+ port: Currently unused (VM provider port)
53
+ host: Hostname for the API server (default: localhost)
54
+ storage: Path for persistent VM storage
55
+ shared_path: Path for shared folder between host and container
56
+ image: Docker image to use (default: "trycua/cua-ubuntu:latest")
57
+ verbose: Enable verbose logging
58
+ ephemeral: Use ephemeral (temporary) storage
59
+ vnc_port: Port for VNC interface (default: 6901)
60
+ """
61
+ self.host = host
62
+ self.api_port = 8000
63
+ self.vnc_port = vnc_port
64
+ self.ephemeral = ephemeral
65
+
66
+ # Handle ephemeral storage (temporary directory)
67
+ if ephemeral:
68
+ self.storage = "ephemeral"
69
+ else:
70
+ self.storage = storage
71
+
72
+ self.shared_path = shared_path
73
+ self.image = image
74
+ self.verbose = verbose
75
+ self._container_id = None
76
+ self._running_containers = {} # Track running containers by name
77
+
78
+ @property
79
+ def provider_type(self) -> VMProviderType:
80
+ """Return the provider type."""
81
+ return VMProviderType.DOCKER
82
+
83
+ def _parse_memory(self, memory_str: str) -> str:
84
+ """Parse memory string to Docker format.
85
+
86
+ Examples:
87
+ "8GB" -> "8g"
88
+ "1024MB" -> "1024m"
89
+ "512" -> "512m"
90
+ """
91
+ if isinstance(memory_str, int):
92
+ return f"{memory_str}m"
93
+
94
+ if isinstance(memory_str, str):
95
+ # Extract number and unit
96
+ match = re.match(r"(\d+)([A-Za-z]*)", memory_str)
97
+ if match:
98
+ value, unit = match.groups()
99
+ unit = unit.upper()
100
+
101
+ if unit == "GB" or unit == "G":
102
+ return f"{value}g"
103
+ elif unit == "MB" or unit == "M" or unit == "":
104
+ return f"{value}m"
105
+
106
+ # Default fallback
107
+ logger.warning(f"Could not parse memory string '{memory_str}', using 4g default")
108
+ return "4g" # Default to 4GB
109
+
110
+ async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
111
+ """Get VM information by name.
112
+
113
+ Args:
114
+ name: Name of the VM to get information for
115
+ storage: Optional storage path override. If provided, this will be used
116
+ instead of the provider's default storage path.
117
+
118
+ Returns:
119
+ Dictionary with VM information including status, IP address, etc.
120
+ """
121
+ try:
122
+ # Check if container exists and get its status
123
+ cmd = ["docker", "inspect", name]
124
+ result = subprocess.run(cmd, capture_output=True, text=True)
125
+
126
+ if result.returncode != 0:
127
+ # Container doesn't exist
128
+ return {
129
+ "name": name,
130
+ "status": "not_found",
131
+ "ip_address": None,
132
+ "ports": {},
133
+ "image": self.image,
134
+ "provider": "docker"
135
+ }
136
+
137
+ # Parse container info
138
+ container_info = json.loads(result.stdout)[0]
139
+ state = container_info["State"]
140
+ network_settings = container_info["NetworkSettings"]
141
+
142
+ # Determine status
143
+ if state["Running"]:
144
+ status = "running"
145
+ elif state["Paused"]:
146
+ status = "paused"
147
+ else:
148
+ status = "stopped"
149
+
150
+ # Get IP address
151
+ ip_address = network_settings.get("IPAddress", "")
152
+ if not ip_address and "Networks" in network_settings:
153
+ # Try to get IP from bridge network
154
+ for network_name, network_info in network_settings["Networks"].items():
155
+ if network_info.get("IPAddress"):
156
+ ip_address = network_info["IPAddress"]
157
+ break
158
+
159
+ # Get port mappings
160
+ ports = {}
161
+ if "Ports" in network_settings and network_settings["Ports"]:
162
+ # network_settings["Ports"] is a dict like:
163
+ # {'6901/tcp': [{'HostIp': '0.0.0.0', 'HostPort': '6901'}, ...], ...}
164
+ for container_port, port_mappings in network_settings["Ports"].items():
165
+ if port_mappings: # Check if there are any port mappings
166
+ # Take the first mapping (usually the IPv4 one)
167
+ for mapping in port_mappings:
168
+ if mapping.get("HostPort"):
169
+ ports[container_port] = mapping["HostPort"]
170
+ break # Use the first valid mapping
171
+
172
+ return {
173
+ "name": name,
174
+ "status": status,
175
+ "ip_address": ip_address or "127.0.0.1", # Use localhost if no IP
176
+ "ports": ports,
177
+ "image": container_info["Config"]["Image"],
178
+ "provider": "docker",
179
+ "container_id": container_info["Id"][:12], # Short ID
180
+ "created": container_info["Created"],
181
+ "started": state.get("StartedAt", ""),
182
+ }
183
+
184
+ except Exception as e:
185
+ logger.error(f"Error getting VM info for {name}: {e}")
186
+ import traceback
187
+ traceback.print_exc()
188
+ return {
189
+ "name": name,
190
+ "status": "error",
191
+ "error": str(e),
192
+ "provider": "docker"
193
+ }
194
+
195
+ async def list_vms(self) -> List[Dict[str, Any]]:
196
+ """List all Docker containers managed by this provider."""
197
+ try:
198
+ # List all containers (running and stopped) with the CUA image
199
+ cmd = ["docker", "ps", "-a", "--filter", f"ancestor={self.image}", "--format", "json"]
200
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
201
+
202
+ containers = []
203
+ if result.stdout.strip():
204
+ for line in result.stdout.strip().split('\n'):
205
+ if line.strip():
206
+ container_data = json.loads(line)
207
+ vm_info = await self.get_vm(container_data["Names"])
208
+ containers.append(vm_info)
209
+
210
+ return containers
211
+
212
+ except subprocess.CalledProcessError as e:
213
+ logger.error(f"Error listing containers: {e.stderr}")
214
+ return []
215
+ except Exception as e:
216
+ logger.error(f"Error listing VMs: {e}")
217
+ import traceback
218
+ traceback.print_exc()
219
+ return []
220
+
221
+ async def run_vm(self, image: str, name: str, run_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
222
+ """Run a VM with the given options.
223
+
224
+ Args:
225
+ image: Name/tag of the Docker image to use
226
+ name: Name of the container to run
227
+ run_opts: Options for running the VM, including:
228
+ - memory: Memory limit (e.g., "4GB", "2048MB")
229
+ - cpu: CPU limit (e.g., 2 for 2 cores)
230
+ - vnc_port: Specific port for VNC interface
231
+ - api_port: Specific port for computer-server API
232
+
233
+ Returns:
234
+ Dictionary with VM status information
235
+ """
236
+ try:
237
+ # Check if container already exists
238
+ existing_vm = await self.get_vm(name, storage)
239
+ if existing_vm["status"] == "running":
240
+ logger.info(f"Container {name} is already running")
241
+ return existing_vm
242
+ elif existing_vm["status"] in ["stopped", "paused"]:
243
+ # Start existing container
244
+ logger.info(f"Starting existing container {name}")
245
+ start_cmd = ["docker", "start", name]
246
+ result = subprocess.run(start_cmd, capture_output=True, text=True, check=True)
247
+
248
+ # Wait for container to be ready
249
+ await self._wait_for_container_ready(name)
250
+ return await self.get_vm(name, storage)
251
+
252
+ # Use provided image or default
253
+ docker_image = image if image != "default" else self.image
254
+
255
+ # Build docker run command
256
+ cmd = ["docker", "run", "-d", "--name", name]
257
+
258
+ # Add memory limit if specified
259
+ if "memory" in run_opts:
260
+ memory_limit = self._parse_memory(run_opts["memory"])
261
+ cmd.extend(["--memory", memory_limit])
262
+
263
+ # Add CPU limit if specified
264
+ if "cpu" in run_opts:
265
+ cpu_count = str(run_opts["cpu"])
266
+ cmd.extend(["--cpus", cpu_count])
267
+
268
+ # Add port mappings
269
+ vnc_port = run_opts.get("vnc_port", self.vnc_port)
270
+ api_port = run_opts.get("api_port", self.api_port)
271
+
272
+ if vnc_port:
273
+ cmd.extend(["-p", f"{vnc_port}:6901"]) # VNC port
274
+ if api_port:
275
+ cmd.extend(["-p", f"{api_port}:8000"]) # computer-server API port
276
+
277
+ # Add volume mounts if storage is specified
278
+ storage_path = storage or self.storage
279
+ if storage_path and storage_path != "ephemeral":
280
+ # Mount storage directory
281
+ cmd.extend(["-v", f"{storage_path}:/home/kasm-user/storage"])
282
+
283
+ # Add shared path if specified
284
+ if self.shared_path:
285
+ cmd.extend(["-v", f"{self.shared_path}:/home/kasm-user/shared"])
286
+
287
+ # Add environment variables
288
+ cmd.extend(["-e", "VNC_PW=password"]) # Set VNC password
289
+ cmd.extend(["-e", "VNCOPTIONS=-disableBasicAuth"]) # Disable VNC basic auth
290
+
291
+ # Add the image
292
+ cmd.append(docker_image)
293
+
294
+ logger.info(f"Running Docker container with command: {' '.join(cmd)}")
295
+
296
+ # Run the container
297
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
298
+ container_id = result.stdout.strip()
299
+
300
+ logger.info(f"Container {name} started with ID: {container_id[:12]}")
301
+
302
+ # Store container info
303
+ self._container_id = container_id
304
+ self._running_containers[name] = container_id
305
+
306
+ # Wait for container to be ready
307
+ await self._wait_for_container_ready(name)
308
+
309
+ # Return VM info
310
+ vm_info = await self.get_vm(name, storage)
311
+ vm_info["container_id"] = container_id[:12]
312
+
313
+ return vm_info
314
+
315
+ except subprocess.CalledProcessError as e:
316
+ error_msg = f"Failed to run container {name}: {e.stderr}"
317
+ logger.error(error_msg)
318
+ return {
319
+ "name": name,
320
+ "status": "error",
321
+ "error": error_msg,
322
+ "provider": "docker"
323
+ }
324
+ except Exception as e:
325
+ error_msg = f"Error running VM {name}: {e}"
326
+ logger.error(error_msg)
327
+ return {
328
+ "name": name,
329
+ "status": "error",
330
+ "error": error_msg,
331
+ "provider": "docker"
332
+ }
333
+
334
+ async def _wait_for_container_ready(self, container_name: str, timeout: int = 60) -> bool:
335
+ """Wait for the Docker container to be fully ready.
336
+
337
+ Args:
338
+ container_name: Name of the Docker container to check
339
+ timeout: Maximum time to wait in seconds (default: 60 seconds)
340
+
341
+ Returns:
342
+ True if the container is running and ready
343
+ """
344
+ logger.info(f"Waiting for container {container_name} to be ready...")
345
+
346
+ start_time = time.time()
347
+ while time.time() - start_time < timeout:
348
+ try:
349
+ # Check if container is running
350
+ vm_info = await self.get_vm(container_name)
351
+ if vm_info["status"] == "running":
352
+ logger.info(f"Container {container_name} is running")
353
+
354
+ # Additional check: try to connect to computer-server API
355
+ # This is optional - we'll just wait a bit more for services to start
356
+ await asyncio.sleep(5)
357
+ return True
358
+
359
+ except Exception as e:
360
+ logger.debug(f"Container {container_name} not ready yet: {e}")
361
+
362
+ await asyncio.sleep(2)
363
+
364
+ logger.warning(f"Container {container_name} did not become ready within {timeout} seconds")
365
+ return False
366
+
367
+ async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
368
+ """Stop a running VM by stopping the Docker container."""
369
+ try:
370
+ logger.info(f"Stopping container {name}")
371
+
372
+ # Stop the container
373
+ cmd = ["docker", "stop", name]
374
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
375
+
376
+ # Remove from running containers tracking
377
+ if name in self._running_containers:
378
+ del self._running_containers[name]
379
+
380
+ logger.info(f"Container {name} stopped successfully")
381
+
382
+ return {
383
+ "name": name,
384
+ "status": "stopped",
385
+ "message": "Container stopped successfully",
386
+ "provider": "docker"
387
+ }
388
+
389
+ except subprocess.CalledProcessError as e:
390
+ error_msg = f"Failed to stop container {name}: {e.stderr}"
391
+ logger.error(error_msg)
392
+ return {
393
+ "name": name,
394
+ "status": "error",
395
+ "error": error_msg,
396
+ "provider": "docker"
397
+ }
398
+ except Exception as e:
399
+ error_msg = f"Error stopping VM {name}: {e}"
400
+ logger.error(error_msg)
401
+ return {
402
+ "name": name,
403
+ "status": "error",
404
+ "error": error_msg,
405
+ "provider": "docker"
406
+ }
407
+
408
+ async def update_vm(self, name: str, update_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
409
+ """Update VM configuration.
410
+
411
+ Note: Docker containers cannot be updated while running.
412
+ This method will return an error suggesting to recreate the container.
413
+ """
414
+ return {
415
+ "name": name,
416
+ "status": "error",
417
+ "error": "Docker containers cannot be updated while running. Please stop and recreate the container with new options.",
418
+ "provider": "docker"
419
+ }
420
+
421
+ async def get_ip(self, name: str, storage: Optional[str] = None, retry_delay: int = 2) -> str:
422
+ """Get the IP address of a VM, waiting indefinitely until it's available.
423
+
424
+ Args:
425
+ name: Name of the VM to get the IP for
426
+ storage: Optional storage path override
427
+ retry_delay: Delay between retries in seconds (default: 2)
428
+
429
+ Returns:
430
+ IP address of the VM when it becomes available
431
+ """
432
+ logger.info(f"Getting IP address for container {name}")
433
+
434
+ total_attempts = 0
435
+ while True:
436
+ total_attempts += 1
437
+
438
+ try:
439
+ vm_info = await self.get_vm(name, storage)
440
+
441
+ if vm_info["status"] == "error":
442
+ raise Exception(f"VM is in error state: {vm_info.get('error', 'Unknown error')}")
443
+
444
+ # TODO: for now, return localhost
445
+ # it seems the docker container is not accessible from the host
446
+ # on WSL2, unless you port forward? not sure
447
+ if True:
448
+ logger.warning("Overriding container IP with localhost")
449
+ return "localhost"
450
+
451
+ # Check if we got a valid IP
452
+ ip = vm_info.get("ip_address", None)
453
+ if ip and ip != "unknown" and not ip.startswith("0.0.0.0"):
454
+ logger.info(f"Got valid container IP address: {ip}")
455
+ return ip
456
+
457
+ # For Docker containers, we can also use localhost if ports are mapped
458
+ if vm_info["status"] == "running" and vm_info.get("ports"):
459
+ logger.info(f"Container is running with port mappings, using localhost")
460
+ return "127.0.0.1"
461
+
462
+ # Check the container status
463
+ status = vm_info.get("status", "unknown")
464
+
465
+ if status == "stopped":
466
+ logger.info(f"Container status is {status}, but still waiting for it to start")
467
+ elif status != "running":
468
+ logger.info(f"Container is not running yet (status: {status}). Waiting...")
469
+ else:
470
+ logger.info("Container is running but no valid IP address yet. Waiting...")
471
+
472
+ except Exception as e:
473
+ logger.warning(f"Error getting container {name} IP: {e}, continuing to wait...")
474
+
475
+ # Wait before next retry
476
+ await asyncio.sleep(retry_delay)
477
+
478
+ # Add progress log every 10 attempts
479
+ if total_attempts % 10 == 0:
480
+ logger.info(f"Still waiting for container {name} IP after {total_attempts} attempts...")
481
+
482
+ async def __aenter__(self):
483
+ """Async context manager entry."""
484
+ logger.debug("Entering DockerProvider context")
485
+ return self
486
+
487
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
488
+ """Async context manager exit.
489
+
490
+ This method handles cleanup of running containers if needed.
491
+ """
492
+ logger.debug(f"Exiting DockerProvider context, handling exceptions: {exc_type}")
493
+ try:
494
+ # Optionally stop running containers on context exit
495
+ # For now, we'll leave containers running as they might be needed
496
+ # Users can manually stop them if needed
497
+ pass
498
+ except Exception as e:
499
+ logger.error(f"Error during DockerProvider cleanup: {e}")
500
+ if exc_type is None:
501
+ raise
502
+ return False
@@ -134,5 +134,29 @@ class VMProviderFactory:
134
134
  "pywinsandbox is required for WinSandboxProvider. "
135
135
  "Please install it with 'pip install -U git+https://github.com/karkason/pywinsandbox.git'"
136
136
  ) from e
137
+ elif provider_type == VMProviderType.DOCKER:
138
+ try:
139
+ from .docker import DockerProvider, HAS_DOCKER
140
+ if not HAS_DOCKER:
141
+ raise ImportError(
142
+ "Docker is required for DockerProvider. "
143
+ "Please install Docker and ensure it is running."
144
+ )
145
+ return DockerProvider(
146
+ port=port,
147
+ host=host,
148
+ storage=storage,
149
+ shared_path=shared_path,
150
+ image=image or "trycua/cua-ubuntu:latest",
151
+ verbose=verbose,
152
+ ephemeral=ephemeral,
153
+ vnc_port=noVNC_port
154
+ )
155
+ except ImportError as e:
156
+ logger.error(f"Failed to import DockerProvider: {e}")
157
+ raise ImportError(
158
+ "Docker is required for DockerProvider. "
159
+ "Please install Docker and ensure it is running."
160
+ ) from e
137
161
  else:
138
162
  raise ValueError(f"Unsupported provider type: {provider_type}")
@@ -302,7 +302,7 @@ def upload_to_huggingface(dataset_name, visibility, filter_tags=None):
302
302
  )
303
303
  card = DatasetCard.from_template(
304
304
  card_data=card_data,
305
- template_str="---\n{{ card_data }}\n---\n\n# Uploaded computer interface trajectories\n\nThese trajectories were generated and uploaded using [c/ua](https://github.com/trycua/cua)"
305
+ template_str="---\n{{ card_data }}\n---\n\n# Uploaded computer interface trajectories\n\nThese trajectories were generated and uploaded using [cua](https://github.com/trycua/cua)"
306
306
  )
307
307
  card.push_to_hub(
308
308
  dataset_name,
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
6
6
 
7
7
  [project]
8
8
  name = "cua-computer"
9
- version = "0.4.0"
9
+ version = "0.4.2"
10
10
  description = "Computer-Use Interface (CUI) framework powering Cua"
11
11
  readme = "README.md"
12
12
  authors = [
@@ -57,7 +57,7 @@ target-version = [
57
57
 
58
58
  [tool.ruff]
59
59
  line-length = 100
60
- target-version = "0.4.0"
60
+ target-version = "0.4.2"
61
61
  select = [
62
62
  "E",
63
63
  "F",
@@ -71,7 +71,7 @@ docstring-code-format = true
71
71
 
72
72
  [tool.mypy]
73
73
  strict = true
74
- python_version = "0.4.0"
74
+ python_version = "0.4.2"
75
75
  ignore_missing_imports = true
76
76
  disallow_untyped_defs = true
77
77
  check_untyped_defs = true
File without changes