daytona-use-computer 0.0.48__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.
- daytona_use_computer-0.0.48/.env.example +6 -0
- daytona_use_computer-0.0.48/.gitignore +9 -0
- daytona_use_computer-0.0.48/.pre-commit-config.yaml +15 -0
- daytona_use_computer-0.0.48/PKG-INFO +197 -0
- daytona_use_computer-0.0.48/README.md +167 -0
- daytona_use_computer-0.0.48/pyproject.toml +82 -0
- daytona_use_computer-0.0.48/scripts/smoke_wheel.py +193 -0
- daytona_use_computer-0.0.48/use_computer/__init__.py +58 -0
- daytona_use_computer-0.0.48/use_computer/agents/__init__.py +52 -0
- daytona_use_computer-0.0.48/use_computer/agents/action_manifest.json +40 -0
- daytona_use_computer-0.0.48/use_computer/agents/base/__init__.py +31 -0
- daytona_use_computer-0.0.48/use_computer/agents/base/actions.py +282 -0
- daytona_use_computer-0.0.48/use_computer/agents/base/agent.py +275 -0
- daytona_use_computer-0.0.48/use_computer/agents/base/artifacts.py +90 -0
- daytona_use_computer-0.0.48/use_computer/agents/base/desktop.py +147 -0
- daytona_use_computer-0.0.48/use_computer/agents/base/final_state.py +136 -0
- daytona_use_computer-0.0.48/use_computer/agents/base/prompts.py +44 -0
- daytona_use_computer-0.0.48/use_computer/agents/base/recordings.py +162 -0
- daytona_use_computer-0.0.48/use_computer/agents/base/sandbox.py +7 -0
- daytona_use_computer-0.0.48/use_computer/agents/base/screenshots.py +52 -0
- daytona_use_computer-0.0.48/use_computer/agents/base/task_setup.py +273 -0
- daytona_use_computer-0.0.48/use_computer/agents/base/trajectory.py +34 -0
- daytona_use_computer-0.0.48/use_computer/agents/core.py +65 -0
- daytona_use_computer-0.0.48/use_computer/agents/debug/README.md +10 -0
- daytona_use_computer-0.0.48/use_computer/agents/debug/__init__.py +5 -0
- daytona_use_computer-0.0.48/use_computer/agents/debug/actions.py +89 -0
- daytona_use_computer-0.0.48/use_computer/agents/debug/agent.py +205 -0
- daytona_use_computer-0.0.48/use_computer/agents/debug/models.py +88 -0
- daytona_use_computer-0.0.48/use_computer/agents/prompts/anthropic.txt +7 -0
- daytona_use_computer-0.0.48/use_computer/agents/prompts/gemini.txt +6 -0
- daytona_use_computer-0.0.48/use_computer/agents/prompts/openai.txt +7 -0
- daytona_use_computer-0.0.48/use_computer/agents/prompts/osworld_pyautogui.txt +18 -0
- daytona_use_computer-0.0.48/use_computer/agents/prompts/pyautogui.txt +50 -0
- daytona_use_computer-0.0.48/use_computer/agents/providers/__init__.py +1 -0
- daytona_use_computer-0.0.48/use_computer/agents/providers/anthropic.py +316 -0
- daytona_use_computer-0.0.48/use_computer/agents/providers/gemini.py +413 -0
- daytona_use_computer-0.0.48/use_computer/agents/providers/generic.py +620 -0
- daytona_use_computer-0.0.48/use_computer/agents/providers/openai.py +363 -0
- daytona_use_computer-0.0.48/use_computer/agents/providers/osworld.py +229 -0
- daytona_use_computer-0.0.48/use_computer/agents/providers/tinker_backend.py +163 -0
- daytona_use_computer-0.0.48/use_computer/agents/types.py +204 -0
- daytona_use_computer-0.0.48/use_computer/automation/__init__.py +28 -0
- daytona_use_computer-0.0.48/use_computer/automation/ax_transpile.py +504 -0
- daytona_use_computer-0.0.48/use_computer/automation/osworld.py +466 -0
- daytona_use_computer-0.0.48/use_computer/automation/parsers.py +307 -0
- daytona_use_computer-0.0.48/use_computer/core/__init__.py +51 -0
- daytona_use_computer-0.0.48/use_computer/core/client.py +421 -0
- daytona_use_computer-0.0.48/use_computer/core/errors.py +112 -0
- daytona_use_computer-0.0.48/use_computer/core/models.py +132 -0
- daytona_use_computer-0.0.48/use_computer/core/retry.py +140 -0
- daytona_use_computer-0.0.48/use_computer/core/sandbox.py +514 -0
- daytona_use_computer-0.0.48/use_computer/macos/__init__.py +4 -0
- daytona_use_computer-0.0.48/use_computer/macos/keyboard.py +73 -0
- daytona_use_computer-0.0.48/use_computer/macos/mouse.py +207 -0
- daytona_use_computer-0.0.48/use_computer/py.typed +1 -0
- daytona_use_computer-0.0.48/use_computer/resources/__init__.py +20 -0
- daytona_use_computer-0.0.48/use_computer/resources/accessibility.py +39 -0
- daytona_use_computer-0.0.48/use_computer/resources/display.py +39 -0
- daytona_use_computer-0.0.48/use_computer/resources/permissions.py +59 -0
- daytona_use_computer-0.0.48/use_computer/resources/recording.py +98 -0
- daytona_use_computer-0.0.48/use_computer/resources/screenshot.py +63 -0
- daytona_use_computer-0.0.48/use_computer/setup_files.py +55 -0
- daytona_use_computer-0.0.48/uv.lock +6345 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.15.12
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff-format
|
|
6
|
+
- id: ruff-check
|
|
7
|
+
args: [--fix]
|
|
8
|
+
|
|
9
|
+
- repo: local
|
|
10
|
+
hooks:
|
|
11
|
+
- id: ty-check
|
|
12
|
+
name: ty check
|
|
13
|
+
entry: uv run --group dev ty check .
|
|
14
|
+
language: system
|
|
15
|
+
pass_filenames: false
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: daytona-use-computer
|
|
3
|
+
Version: 0.0.48
|
|
4
|
+
Summary: Python SDK for use.computer macOS sandboxes
|
|
5
|
+
Project-URL: Homepage, https://use.computer
|
|
6
|
+
Project-URL: Documentation, https://api.use.computer/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/daytona/use-computer-sdk
|
|
8
|
+
Author: use.computer
|
|
9
|
+
Keywords: automation,computer-use,macos,sandbox,vnc
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx>=0.27
|
|
21
|
+
Provides-Extra: agents
|
|
22
|
+
Requires-Dist: anthropic>=0.86; extra == 'agents'
|
|
23
|
+
Requires-Dist: google-genai>=1; extra == 'agents'
|
|
24
|
+
Requires-Dist: litellm>=1; extra == 'agents'
|
|
25
|
+
Requires-Dist: openai>=1; extra == 'agents'
|
|
26
|
+
Requires-Dist: pillow>=10; extra == 'agents'
|
|
27
|
+
Requires-Dist: tinker-cookbook>=0.1.0; (python_full_version >= '3.11') and extra == 'agents'
|
|
28
|
+
Requires-Dist: tinker>=0.14.0; (python_full_version >= '3.11') and extra == 'agents'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# use-computer Python SDK (`daytona-use-computer`)
|
|
32
|
+
|
|
33
|
+
use.computer gives you macOS sandboxes: VMs on dedicated Apple M4 Mac minis that you reserve for 24 hours or more, up to 2 VMs at a time per Mac.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install daytona-use-computer
|
|
37
|
+
export USE_COMPUTER_API_KEY=uc_live_...
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Optional agent integrations are installed explicitly:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install "daytona-use-computer[agents]" # computer-use agents and provider SDKs
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Base installs only the SDK client and `httpx`. The `agents` extra installs the agent runtime plus model-provider dependencies (Anthropic, OpenAI, Gemini, LiteLLM).
|
|
47
|
+
|
|
48
|
+
## Quickstart
|
|
49
|
+
|
|
50
|
+
Flow: sign up → $100 starter credit → reserve a Mac mini (dashboard, or `client.reserve(hours=24)` in the SDK) → `create()` a macOS sandbox → drive it (mouse, keyboard, screenshot, exec, files, recording, UI tree, VNC) → `delete()`. Reservations cost $1.91/hour per Mac; the starter credit pays for them.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from use_computer import Computer
|
|
54
|
+
|
|
55
|
+
client = Computer()
|
|
56
|
+
|
|
57
|
+
# 1. Reserve one M4 Mac Mini for 24 hours
|
|
58
|
+
reservation = client.reserve(hours=24, mac_model="m4")
|
|
59
|
+
|
|
60
|
+
# 2. Launch a macOS sandbox on the reserved Mac
|
|
61
|
+
with client.create(reservation_id=reservation.id) as mac:
|
|
62
|
+
# 3. Drive the macOS sandbox
|
|
63
|
+
mac.exec("open -a Safari")
|
|
64
|
+
mac.keyboard.type("hello from use.computer")
|
|
65
|
+
mac.mouse.click(500, 500)
|
|
66
|
+
png = mac.screenshot.take_full_screen()
|
|
67
|
+
print("Sandbox:", mac.sandbox_id)
|
|
68
|
+
# Open this sandbox's viewer from the use.computer dashboard.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Never print or share `vnc_url`: it contains your account API key. Open the viewer from the dashboard instead.
|
|
72
|
+
|
|
73
|
+
The optional `mac_model` picks the Mac: `"m4"` (the default; 10 vCPU and 16 GiB RAM) or `"m4-pro"` (12 or more vCPU and 24 GiB RAM). `reservation.mac_model` reports it.
|
|
74
|
+
|
|
75
|
+
Each `create()` can set `cpu`, `memory_gib`, and `disk_gib`. Omitted fields use the Mac's warm VM size, which is 5 vCPU and 8 GiB RAM today and starts right away. Any other size boots a new VM, which can take up to 5 minutes, so `create()` waits up to 10 minutes when you set any of them. A Mac runs at most 2 sandboxes at a time, and their sizes must fit in the Mac. The sandbox reports `cpu`, `memory_gib`, `disk_gib`, and `boot` (`"warm"` or `"cold"`):
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
with client.create(reservation_id=reservation.id, cpu=8, memory_gib=12) as mac:
|
|
79
|
+
print(mac.cpu, mac.memory_gib, mac.boot)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
If the size does not fit, `create()` raises `SandboxResourcesError`. Its `code` is `invalid_resources`, `resources_exceed_mac`, `sandbox_limit_reached`, or `insufficient_capacity`, and its `message` explains the refusal. `AsyncComputer` accepts the same options. Check `Computer().platforms(reservation_id=...)["macos"]["capacity"]` for the selected reservation's `max` and `used` counts.
|
|
83
|
+
|
|
84
|
+
The older `vm_layout` reservation option still works but is deprecated; the SDK sends it only when you pass it. `"split"` (the server default) allows two sandboxes, and `"whole"` allows one sandbox at a time.
|
|
85
|
+
|
|
86
|
+
When `ephemeral` is omitted, the server default is the reservation lifetime. Explicit `ephemeral=True` opts into destruction after 2 idle minutes; call `sandbox.start_keepalive(interval=30)` during long model-think periods. Explicit `ephemeral=False` disables idle destruction. Context managers still delete their sandbox on exit.
|
|
87
|
+
|
|
88
|
+
This default applies to the source release documented here. Published Python 0.0.46
|
|
89
|
+
still defaults to `ephemeral=True`; pass `ephemeral=False` explicitly on that
|
|
90
|
+
version. The next tagged release includes the omitted-field default above.
|
|
91
|
+
|
|
92
|
+
## Computer actions
|
|
93
|
+
|
|
94
|
+
Coordinates use screenshot pixels. `mouse.click(x, y, button="middle")` selects the
|
|
95
|
+
middle button; `click_count=3` sends one native triple-click sequence.
|
|
96
|
+
`mouse.down()` / `mouse.up()` hold and release the left button at the current
|
|
97
|
+
cursor, with optional `button`, `x`, and `y`. `keyboard.hold("shift", 0.5)` holds
|
|
98
|
+
keys for seconds. `mouse.drag(..., path=[{"x": 10, "y": 20}, ...])` preserves all
|
|
99
|
+
path points. `mouse.scroll(x, y, scroll_x=40, scroll_y=-80)` sends both pixel
|
|
100
|
+
deltas (positive right/down); the direction/amount form remains available.
|
|
101
|
+
The async methods have the same arguments.
|
|
102
|
+
|
|
103
|
+
Provider mappings are recorded in
|
|
104
|
+
[`action_manifest.json`](use_computer/agents/action_manifest.json).
|
|
105
|
+
Anthropic tool pixels are scaled to sandbox pixels; Gemini uses normalized
|
|
106
|
+
0..999 coordinates. Control and Command remain distinct. Invalid actions and
|
|
107
|
+
gateway errors are not reported as executed.
|
|
108
|
+
|
|
109
|
+
Agent logs separate action execution from `settle+screenshot`: the latter
|
|
110
|
+
includes a deliberate two-second settle, screenshot requests, and any recovery.
|
|
111
|
+
These are not model inference timings or raw screenshot latency.
|
|
112
|
+
|
|
113
|
+
`exec(command, timeout=120)` uses the gateway's non-login `/bin/zsh -c`;
|
|
114
|
+
`exec_ax` uses `/bin/sh` through the desktop server. The timeout is seconds
|
|
115
|
+
and is sent to the gateway and used as the HTTP timeout. For a 200-second
|
|
116
|
+
command, use `timeout=300`. Results retain separate stdout, stderr, and
|
|
117
|
+
`return_code`; a nonzero process exit is not an HTTP transport failure.
|
|
118
|
+
|
|
119
|
+
## Application automation permissions
|
|
120
|
+
|
|
121
|
+
macOS asks for consent when a process sends Apple events to another application
|
|
122
|
+
through AppleScript or `osascript`. That consent dialog cannot be answered inside
|
|
123
|
+
the sandbox. The gateway approves every installed application when creating a
|
|
124
|
+
sandbox or restoring a snapshot. After installing another app, call
|
|
125
|
+
`sandbox.permissions.allow_automation()` to approve it before scripting it:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
# Inside an async macOS sandbox session:
|
|
129
|
+
result = await sandbox.exec("brew install --cask firefox")
|
|
130
|
+
if result.return_code != 0:
|
|
131
|
+
raise RuntimeError(result.stderr)
|
|
132
|
+
approval = await sandbox.permissions.allow_automation(
|
|
133
|
+
bundle_ids=["org.mozilla.firefox"]
|
|
134
|
+
)
|
|
135
|
+
print(approval.success, approval.targets, approval.changed)
|
|
136
|
+
|
|
137
|
+
# Alternatively, approve all currently installed applications:
|
|
138
|
+
approval = await sandbox.permissions.allow_automation(installed=True)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The synchronous `MacOSSandbox` exposes the same method without `await`.
|
|
142
|
+
Approvals cover Apple events sent through `osascript`, SSH sessions, `exec`, and
|
|
143
|
+
Terminal.
|
|
144
|
+
|
|
145
|
+
`allow_automation(bundle_ids=None, installed=False)` sends
|
|
146
|
+
`POST /v1/sandboxes/{sandboxID}/permissions/automation`. Supply specific
|
|
147
|
+
`bundle_ids`, `installed=True` for all installed applications, or both in one
|
|
148
|
+
call. Omitted bundle IDs and the default `installed=False` are not sent.
|
|
149
|
+
The 200 response is decoded as the exported `AutomationApproval` dataclass:
|
|
150
|
+
`success: bool`, `targets: int` (applications considered), and `changed: int`
|
|
151
|
+
(approvals added or flipped from denied).
|
|
152
|
+
|
|
153
|
+
The gateway validates bundle IDs against
|
|
154
|
+
`^[A-Za-z0-9][A-Za-z0-9._-]{0,254}$`. Malformed IDs or an empty request receive
|
|
155
|
+
HTTP 400 with `{"error": ...}`. The SDK does not validate them locally; it raises
|
|
156
|
+
the standard `httpx.HTTPStatusError`, whose `response` retains the status and
|
|
157
|
+
error body.
|
|
158
|
+
|
|
159
|
+
## Runtime Snapshots
|
|
160
|
+
|
|
161
|
+
macOS snapshots let you seed a VM once, snapshot it, then create new sandboxes from that snapshot version. macOS snapshots preserve disk state, so installed apps, accounts, and seeded files are available immediately:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from use_computer import Computer
|
|
165
|
+
|
|
166
|
+
client = Computer()
|
|
167
|
+
|
|
168
|
+
# Configure the macOS desktop by hand over VNC, then snapshot it.
|
|
169
|
+
with client.create(type="macos") as mac:
|
|
170
|
+
print("Configure sandbox in the dashboard viewer:", mac.sandbox_id)
|
|
171
|
+
input("Press Enter once the desktop is ready to snapshot...")
|
|
172
|
+
snapshot = mac.snapshot("chrome-seeded-macos")
|
|
173
|
+
|
|
174
|
+
# New sandboxes boot from that saved disk state, no setup needed.
|
|
175
|
+
with client.create(type="macos", snapshot=snapshot.version) as seeded:
|
|
176
|
+
print("Seeded sandbox ready:", seeded.sandbox_id)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Use `client.snapshots()` to list saved snapshot versions.
|
|
180
|
+
|
|
181
|
+
## Examples
|
|
182
|
+
|
|
183
|
+
| File | What it shows |
|
|
184
|
+
| --- | --- |
|
|
185
|
+
| [`examples/_1_hello_macos.py`](../examples/python/_1_hello_macos.py) | reserve → create → exec → keyboard → screenshot |
|
|
186
|
+
| [`examples/_2_recording.py`](../examples/python/_2_recording.py) | start / stop / download a screen recording |
|
|
187
|
+
| [`examples/_3_file_transfer.py`](../examples/python/_3_file_transfer.py) | upload bytes, download a file back |
|
|
188
|
+
| [`examples/_4_keepalive.py`](../examples/python/_4_keepalive.py) | heartbeat for ephemeral sessions idle > 2 min |
|
|
189
|
+
| [`examples/_5_snapshots.py`](../examples/python/_5_snapshots.py) | snapshot seeded macOS state |
|
|
190
|
+
|
|
191
|
+
## HTTP API
|
|
192
|
+
|
|
193
|
+
Every SDK method wraps `https://api.use.computer/v1/...` with `Authorization: Bearer uc_live_...`. Swagger: [api.use.computer/docs](https://api.use.computer/docs). OpenAPI spec: [api.use.computer/openapi.yaml](https://api.use.computer/openapi.yaml).
|
|
194
|
+
|
|
195
|
+
Gateway URLs, whether passed explicitly or via `USE_COMPUTER_BASE_URL`, must use HTTPS except for loopback HTTP. The SDK does not load `.env` files by default; load one explicitly in your application if needed. Authenticated requests, including keepalive and agent reference screenshots, do not follow redirects.
|
|
196
|
+
|
|
197
|
+
Lifecycle calls (`reserve`, `create`, and `snapshot`, including restores via `create(snapshot=...)`) send a fresh UUID v4 `Idempotency-Key` per call. Idempotent HTTP methods and keyed lifecycle mutations retry transient failures using the same key and body; unkeyed mutations are not retried. A new lifecycle method invocation is a new intent with a new key.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# use-computer Python SDK (`daytona-use-computer`)
|
|
2
|
+
|
|
3
|
+
use.computer gives you macOS sandboxes: VMs on dedicated Apple M4 Mac minis that you reserve for 24 hours or more, up to 2 VMs at a time per Mac.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install daytona-use-computer
|
|
7
|
+
export USE_COMPUTER_API_KEY=uc_live_...
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Optional agent integrations are installed explicitly:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install "daytona-use-computer[agents]" # computer-use agents and provider SDKs
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Base installs only the SDK client and `httpx`. The `agents` extra installs the agent runtime plus model-provider dependencies (Anthropic, OpenAI, Gemini, LiteLLM).
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
Flow: sign up → $100 starter credit → reserve a Mac mini (dashboard, or `client.reserve(hours=24)` in the SDK) → `create()` a macOS sandbox → drive it (mouse, keyboard, screenshot, exec, files, recording, UI tree, VNC) → `delete()`. Reservations cost $1.91/hour per Mac; the starter credit pays for them.
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from use_computer import Computer
|
|
24
|
+
|
|
25
|
+
client = Computer()
|
|
26
|
+
|
|
27
|
+
# 1. Reserve one M4 Mac Mini for 24 hours
|
|
28
|
+
reservation = client.reserve(hours=24, mac_model="m4")
|
|
29
|
+
|
|
30
|
+
# 2. Launch a macOS sandbox on the reserved Mac
|
|
31
|
+
with client.create(reservation_id=reservation.id) as mac:
|
|
32
|
+
# 3. Drive the macOS sandbox
|
|
33
|
+
mac.exec("open -a Safari")
|
|
34
|
+
mac.keyboard.type("hello from use.computer")
|
|
35
|
+
mac.mouse.click(500, 500)
|
|
36
|
+
png = mac.screenshot.take_full_screen()
|
|
37
|
+
print("Sandbox:", mac.sandbox_id)
|
|
38
|
+
# Open this sandbox's viewer from the use.computer dashboard.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Never print or share `vnc_url`: it contains your account API key. Open the viewer from the dashboard instead.
|
|
42
|
+
|
|
43
|
+
The optional `mac_model` picks the Mac: `"m4"` (the default; 10 vCPU and 16 GiB RAM) or `"m4-pro"` (12 or more vCPU and 24 GiB RAM). `reservation.mac_model` reports it.
|
|
44
|
+
|
|
45
|
+
Each `create()` can set `cpu`, `memory_gib`, and `disk_gib`. Omitted fields use the Mac's warm VM size, which is 5 vCPU and 8 GiB RAM today and starts right away. Any other size boots a new VM, which can take up to 5 minutes, so `create()` waits up to 10 minutes when you set any of them. A Mac runs at most 2 sandboxes at a time, and their sizes must fit in the Mac. The sandbox reports `cpu`, `memory_gib`, `disk_gib`, and `boot` (`"warm"` or `"cold"`):
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
with client.create(reservation_id=reservation.id, cpu=8, memory_gib=12) as mac:
|
|
49
|
+
print(mac.cpu, mac.memory_gib, mac.boot)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
If the size does not fit, `create()` raises `SandboxResourcesError`. Its `code` is `invalid_resources`, `resources_exceed_mac`, `sandbox_limit_reached`, or `insufficient_capacity`, and its `message` explains the refusal. `AsyncComputer` accepts the same options. Check `Computer().platforms(reservation_id=...)["macos"]["capacity"]` for the selected reservation's `max` and `used` counts.
|
|
53
|
+
|
|
54
|
+
The older `vm_layout` reservation option still works but is deprecated; the SDK sends it only when you pass it. `"split"` (the server default) allows two sandboxes, and `"whole"` allows one sandbox at a time.
|
|
55
|
+
|
|
56
|
+
When `ephemeral` is omitted, the server default is the reservation lifetime. Explicit `ephemeral=True` opts into destruction after 2 idle minutes; call `sandbox.start_keepalive(interval=30)` during long model-think periods. Explicit `ephemeral=False` disables idle destruction. Context managers still delete their sandbox on exit.
|
|
57
|
+
|
|
58
|
+
This default applies to the source release documented here. Published Python 0.0.46
|
|
59
|
+
still defaults to `ephemeral=True`; pass `ephemeral=False` explicitly on that
|
|
60
|
+
version. The next tagged release includes the omitted-field default above.
|
|
61
|
+
|
|
62
|
+
## Computer actions
|
|
63
|
+
|
|
64
|
+
Coordinates use screenshot pixels. `mouse.click(x, y, button="middle")` selects the
|
|
65
|
+
middle button; `click_count=3` sends one native triple-click sequence.
|
|
66
|
+
`mouse.down()` / `mouse.up()` hold and release the left button at the current
|
|
67
|
+
cursor, with optional `button`, `x`, and `y`. `keyboard.hold("shift", 0.5)` holds
|
|
68
|
+
keys for seconds. `mouse.drag(..., path=[{"x": 10, "y": 20}, ...])` preserves all
|
|
69
|
+
path points. `mouse.scroll(x, y, scroll_x=40, scroll_y=-80)` sends both pixel
|
|
70
|
+
deltas (positive right/down); the direction/amount form remains available.
|
|
71
|
+
The async methods have the same arguments.
|
|
72
|
+
|
|
73
|
+
Provider mappings are recorded in
|
|
74
|
+
[`action_manifest.json`](use_computer/agents/action_manifest.json).
|
|
75
|
+
Anthropic tool pixels are scaled to sandbox pixels; Gemini uses normalized
|
|
76
|
+
0..999 coordinates. Control and Command remain distinct. Invalid actions and
|
|
77
|
+
gateway errors are not reported as executed.
|
|
78
|
+
|
|
79
|
+
Agent logs separate action execution from `settle+screenshot`: the latter
|
|
80
|
+
includes a deliberate two-second settle, screenshot requests, and any recovery.
|
|
81
|
+
These are not model inference timings or raw screenshot latency.
|
|
82
|
+
|
|
83
|
+
`exec(command, timeout=120)` uses the gateway's non-login `/bin/zsh -c`;
|
|
84
|
+
`exec_ax` uses `/bin/sh` through the desktop server. The timeout is seconds
|
|
85
|
+
and is sent to the gateway and used as the HTTP timeout. For a 200-second
|
|
86
|
+
command, use `timeout=300`. Results retain separate stdout, stderr, and
|
|
87
|
+
`return_code`; a nonzero process exit is not an HTTP transport failure.
|
|
88
|
+
|
|
89
|
+
## Application automation permissions
|
|
90
|
+
|
|
91
|
+
macOS asks for consent when a process sends Apple events to another application
|
|
92
|
+
through AppleScript or `osascript`. That consent dialog cannot be answered inside
|
|
93
|
+
the sandbox. The gateway approves every installed application when creating a
|
|
94
|
+
sandbox or restoring a snapshot. After installing another app, call
|
|
95
|
+
`sandbox.permissions.allow_automation()` to approve it before scripting it:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
# Inside an async macOS sandbox session:
|
|
99
|
+
result = await sandbox.exec("brew install --cask firefox")
|
|
100
|
+
if result.return_code != 0:
|
|
101
|
+
raise RuntimeError(result.stderr)
|
|
102
|
+
approval = await sandbox.permissions.allow_automation(
|
|
103
|
+
bundle_ids=["org.mozilla.firefox"]
|
|
104
|
+
)
|
|
105
|
+
print(approval.success, approval.targets, approval.changed)
|
|
106
|
+
|
|
107
|
+
# Alternatively, approve all currently installed applications:
|
|
108
|
+
approval = await sandbox.permissions.allow_automation(installed=True)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The synchronous `MacOSSandbox` exposes the same method without `await`.
|
|
112
|
+
Approvals cover Apple events sent through `osascript`, SSH sessions, `exec`, and
|
|
113
|
+
Terminal.
|
|
114
|
+
|
|
115
|
+
`allow_automation(bundle_ids=None, installed=False)` sends
|
|
116
|
+
`POST /v1/sandboxes/{sandboxID}/permissions/automation`. Supply specific
|
|
117
|
+
`bundle_ids`, `installed=True` for all installed applications, or both in one
|
|
118
|
+
call. Omitted bundle IDs and the default `installed=False` are not sent.
|
|
119
|
+
The 200 response is decoded as the exported `AutomationApproval` dataclass:
|
|
120
|
+
`success: bool`, `targets: int` (applications considered), and `changed: int`
|
|
121
|
+
(approvals added or flipped from denied).
|
|
122
|
+
|
|
123
|
+
The gateway validates bundle IDs against
|
|
124
|
+
`^[A-Za-z0-9][A-Za-z0-9._-]{0,254}$`. Malformed IDs or an empty request receive
|
|
125
|
+
HTTP 400 with `{"error": ...}`. The SDK does not validate them locally; it raises
|
|
126
|
+
the standard `httpx.HTTPStatusError`, whose `response` retains the status and
|
|
127
|
+
error body.
|
|
128
|
+
|
|
129
|
+
## Runtime Snapshots
|
|
130
|
+
|
|
131
|
+
macOS snapshots let you seed a VM once, snapshot it, then create new sandboxes from that snapshot version. macOS snapshots preserve disk state, so installed apps, accounts, and seeded files are available immediately:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from use_computer import Computer
|
|
135
|
+
|
|
136
|
+
client = Computer()
|
|
137
|
+
|
|
138
|
+
# Configure the macOS desktop by hand over VNC, then snapshot it.
|
|
139
|
+
with client.create(type="macos") as mac:
|
|
140
|
+
print("Configure sandbox in the dashboard viewer:", mac.sandbox_id)
|
|
141
|
+
input("Press Enter once the desktop is ready to snapshot...")
|
|
142
|
+
snapshot = mac.snapshot("chrome-seeded-macos")
|
|
143
|
+
|
|
144
|
+
# New sandboxes boot from that saved disk state, no setup needed.
|
|
145
|
+
with client.create(type="macos", snapshot=snapshot.version) as seeded:
|
|
146
|
+
print("Seeded sandbox ready:", seeded.sandbox_id)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Use `client.snapshots()` to list saved snapshot versions.
|
|
150
|
+
|
|
151
|
+
## Examples
|
|
152
|
+
|
|
153
|
+
| File | What it shows |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| [`examples/_1_hello_macos.py`](../examples/python/_1_hello_macos.py) | reserve → create → exec → keyboard → screenshot |
|
|
156
|
+
| [`examples/_2_recording.py`](../examples/python/_2_recording.py) | start / stop / download a screen recording |
|
|
157
|
+
| [`examples/_3_file_transfer.py`](../examples/python/_3_file_transfer.py) | upload bytes, download a file back |
|
|
158
|
+
| [`examples/_4_keepalive.py`](../examples/python/_4_keepalive.py) | heartbeat for ephemeral sessions idle > 2 min |
|
|
159
|
+
| [`examples/_5_snapshots.py`](../examples/python/_5_snapshots.py) | snapshot seeded macOS state |
|
|
160
|
+
|
|
161
|
+
## HTTP API
|
|
162
|
+
|
|
163
|
+
Every SDK method wraps `https://api.use.computer/v1/...` with `Authorization: Bearer uc_live_...`. Swagger: [api.use.computer/docs](https://api.use.computer/docs). OpenAPI spec: [api.use.computer/openapi.yaml](https://api.use.computer/openapi.yaml).
|
|
164
|
+
|
|
165
|
+
Gateway URLs, whether passed explicitly or via `USE_COMPUTER_BASE_URL`, must use HTTPS except for loopback HTTP. The SDK does not load `.env` files by default; load one explicitly in your application if needed. Authenticated requests, including keepalive and agent reference screenshots, do not follow redirects.
|
|
166
|
+
|
|
167
|
+
Lifecycle calls (`reserve`, `create`, and `snapshot`, including restores via `create(snapshot=...)`) send a fresh UUID v4 `Idempotency-Key` per call. Idempotent HTTP methods and keyed lifecycle mutations retry transient failures using the same key and body; unkeyed mutations are not retried. A new lifecycle method invocation is a new intent with a new key.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "daytona-use-computer"
|
|
7
|
+
version = "0.0.48"
|
|
8
|
+
description = "Python SDK for use.computer macOS sandboxes"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{ name = "use.computer" }]
|
|
12
|
+
dependencies = ["httpx>=0.27"]
|
|
13
|
+
keywords = ["computer-use", "macos", "automation", "sandbox", "vnc"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
agents = [
|
|
28
|
+
"Pillow>=10",
|
|
29
|
+
"openai>=1",
|
|
30
|
+
"anthropic>=0.86",
|
|
31
|
+
"litellm>=1",
|
|
32
|
+
"google-genai>=1",
|
|
33
|
+
"tinker>=0.14.0 ; python_full_version >= '3.11'",
|
|
34
|
+
"tinker-cookbook>=0.1.0 ; python_full_version >= '3.11'",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = [
|
|
40
|
+
"anthropic>=0.86",
|
|
41
|
+
"google-genai>=1",
|
|
42
|
+
"harbor @ git+https://github.com/josancamon19/harbor.git@b0203845795be95f86b149dbc73ab0fd56cf9192 ; python_version >= '3.12'",
|
|
43
|
+
"litellm>=1",
|
|
44
|
+
"openai>=1",
|
|
45
|
+
"pre-commit>=4",
|
|
46
|
+
"pytest>=9",
|
|
47
|
+
"ruff>=0.15",
|
|
48
|
+
"Pillow>=10",
|
|
49
|
+
"PyYAML>=6",
|
|
50
|
+
"tomli>=2",
|
|
51
|
+
"twine>=6",
|
|
52
|
+
"ty>=0.0.34",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[project.urls]
|
|
56
|
+
Homepage = "https://use.computer"
|
|
57
|
+
Documentation = "https://api.use.computer/docs"
|
|
58
|
+
Repository = "https://github.com/daytona/use-computer-sdk"
|
|
59
|
+
|
|
60
|
+
[tool.hatch.build.targets.wheel]
|
|
61
|
+
packages = ["use_computer"]
|
|
62
|
+
|
|
63
|
+
[tool.hatch.build]
|
|
64
|
+
exclude = [
|
|
65
|
+
"/.github",
|
|
66
|
+
"/.pytest_cache",
|
|
67
|
+
"/.ruff_cache",
|
|
68
|
+
"/.venv",
|
|
69
|
+
"/dist",
|
|
70
|
+
"/results",
|
|
71
|
+
"/tests",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[tool.pytest.ini_options]
|
|
75
|
+
testpaths = ["tests"]
|
|
76
|
+
|
|
77
|
+
[tool.ruff]
|
|
78
|
+
target-version = "py310"
|
|
79
|
+
line-length = 100
|
|
80
|
+
|
|
81
|
+
[tool.ruff.lint]
|
|
82
|
+
select = ["E", "F", "I", "UP"]
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Smoke-test the built wheel in clean environments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import zipfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
BASE_SMOKE = r"""
|
|
13
|
+
import importlib.util
|
|
14
|
+
import use_computer
|
|
15
|
+
|
|
16
|
+
def has(name):
|
|
17
|
+
try:
|
|
18
|
+
return importlib.util.find_spec(name) is not None
|
|
19
|
+
except ModuleNotFoundError:
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
assert "site-packages" in (use_computer.__file__ or ""), use_computer.__file__
|
|
23
|
+
for name in ["PIL", "openai", "anthropic", "google.genai", "litellm"]:
|
|
24
|
+
assert not has(name), name
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
AGENTS_SMOKE = r"""
|
|
28
|
+
import asyncio
|
|
29
|
+
import importlib
|
|
30
|
+
import importlib.util
|
|
31
|
+
import tempfile
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
assert importlib.util.find_spec("PIL") is not None
|
|
35
|
+
for name in ["openai", "anthropic", "google.genai", "litellm"]:
|
|
36
|
+
assert importlib.util.find_spec(name) is not None, name
|
|
37
|
+
for path in ["use_computer.agents", "use_computer.agents.debug"]:
|
|
38
|
+
importlib.import_module(path)
|
|
39
|
+
|
|
40
|
+
from use_computer.agents import (
|
|
41
|
+
AnthropicComputerAgent,
|
|
42
|
+
GeminiComputerAgent,
|
|
43
|
+
GenericComputerAgent,
|
|
44
|
+
|
|
45
|
+
OpenAIComputerAgent,
|
|
46
|
+
)
|
|
47
|
+
from use_computer.agents.debug import DebugComputerAgent
|
|
48
|
+
from use_computer.core.models import ExecResult
|
|
49
|
+
|
|
50
|
+
PNG_BYTES = (
|
|
51
|
+
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
|
|
52
|
+
b"\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDAT\x08\xd7c\xf8\xff"
|
|
53
|
+
b"\xff?\x00\x05\xfe\x02\xfeA\xe2`\\\x00\x00\x00\x00IEND\xaeB`\x82"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
class Recording:
|
|
57
|
+
async def start(self, name="trial"):
|
|
58
|
+
raise RuntimeError("recording disabled")
|
|
59
|
+
|
|
60
|
+
class Screenshot:
|
|
61
|
+
async def take_full_screen(self):
|
|
62
|
+
return PNG_BYTES
|
|
63
|
+
|
|
64
|
+
class Display:
|
|
65
|
+
async def get_windows(self):
|
|
66
|
+
return [{"role": "window", "title": "Fake"}]
|
|
67
|
+
|
|
68
|
+
class Mouse:
|
|
69
|
+
def __init__(self):
|
|
70
|
+
self.clicks = []
|
|
71
|
+
|
|
72
|
+
async def click(self, x, y, button="left", double=False):
|
|
73
|
+
self.clicks.append((x, y, button, double))
|
|
74
|
+
|
|
75
|
+
class Sandbox:
|
|
76
|
+
def __init__(self):
|
|
77
|
+
self.sandbox_id = "fake"
|
|
78
|
+
self.screenshot = Screenshot()
|
|
79
|
+
self.recording = Recording()
|
|
80
|
+
self.display = Display()
|
|
81
|
+
self.mouse = Mouse()
|
|
82
|
+
self.commands = []
|
|
83
|
+
|
|
84
|
+
async def exec_ssh(self, command, timeout=120):
|
|
85
|
+
self.commands.append(command)
|
|
86
|
+
return ExecResult(return_code=0, stdout="ok", stderr="")
|
|
87
|
+
|
|
88
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
89
|
+
root = Path(tmp)
|
|
90
|
+
sandbox = Sandbox()
|
|
91
|
+
result = asyncio.run(
|
|
92
|
+
DebugComputerAgent(max_steps=1).run(
|
|
93
|
+
"smoke",
|
|
94
|
+
sandbox=sandbox,
|
|
95
|
+
logs_dir=root / "agent",
|
|
96
|
+
task_dir=root / "task",
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
assert sandbox.mouse.clicks == [(960, 540, "left", False)]
|
|
100
|
+
assert sandbox.commands == ["echo hello world"]
|
|
101
|
+
assert result.final_message == "smoke"
|
|
102
|
+
|
|
103
|
+
for agent_cls, model_name in [
|
|
104
|
+
(AnthropicComputerAgent, "anthropic/claude-sonnet-4-6"),
|
|
105
|
+
(OpenAIComputerAgent, "openai/gpt-5.5"),
|
|
106
|
+
(GeminiComputerAgent, "gemini/gemini-2.5-computer-use-preview-10-2025"),
|
|
107
|
+
(GenericComputerAgent, "openai/gpt-4o"),
|
|
108
|
+
|
|
109
|
+
]:
|
|
110
|
+
assert agent_cls(model_name=model_name, max_steps=1).max_steps == 1
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main() -> None:
|
|
115
|
+
parser = argparse.ArgumentParser()
|
|
116
|
+
parser.add_argument("wheel", type=Path)
|
|
117
|
+
args = parser.parse_args()
|
|
118
|
+
|
|
119
|
+
wheel = args.wheel.resolve()
|
|
120
|
+
if not wheel.name.endswith(".whl"):
|
|
121
|
+
raise SystemExit(f"expected a wheel path, got {wheel}")
|
|
122
|
+
|
|
123
|
+
_check_wheel_contents(wheel)
|
|
124
|
+
_check_metadata(wheel)
|
|
125
|
+
_smoke_install(wheel, None, BASE_SMOKE)
|
|
126
|
+
_smoke_install(wheel, "agents", AGENTS_SMOKE)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _check_wheel_contents(wheel: Path) -> None:
|
|
130
|
+
with zipfile.ZipFile(wheel) as archive:
|
|
131
|
+
names = set(archive.namelist())
|
|
132
|
+
|
|
133
|
+
required = {
|
|
134
|
+
"use_computer/py.typed",
|
|
135
|
+
"use_computer/agents/prompts/anthropic.txt",
|
|
136
|
+
"use_computer/agents/prompts/gemini.txt",
|
|
137
|
+
"use_computer/agents/prompts/openai.txt",
|
|
138
|
+
"use_computer/agents/prompts/pyautogui.txt",
|
|
139
|
+
}
|
|
140
|
+
missing = sorted(required - names)
|
|
141
|
+
if missing:
|
|
142
|
+
raise SystemExit(f"wheel is missing files: {missing}")
|
|
143
|
+
|
|
144
|
+
forbidden = [name for name in names if "__pycache__" in name or name.startswith("tests/")]
|
|
145
|
+
if forbidden:
|
|
146
|
+
raise SystemExit(f"wheel contains generated/test files: {sorted(forbidden)[:10]}")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _check_metadata(wheel: Path) -> None:
|
|
150
|
+
with zipfile.ZipFile(wheel) as archive:
|
|
151
|
+
metadata_name = next(
|
|
152
|
+
name for name in archive.namelist() if name.endswith(".dist-info/METADATA")
|
|
153
|
+
)
|
|
154
|
+
metadata = archive.read(metadata_name).decode()
|
|
155
|
+
|
|
156
|
+
required = [
|
|
157
|
+
"Provides-Extra: agents",
|
|
158
|
+
"Requires-Dist: pillow>=10; extra == 'agents'",
|
|
159
|
+
"Requires-Dist: openai>=1; extra == 'agents'",
|
|
160
|
+
"Requires-Dist: anthropic>=0.86; extra == 'agents'",
|
|
161
|
+
"Requires-Dist: litellm>=1; extra == 'agents'",
|
|
162
|
+
"Requires-Dist: google-genai>=1; extra == 'agents'",
|
|
163
|
+
]
|
|
164
|
+
missing = [line for line in required if line not in metadata]
|
|
165
|
+
if missing:
|
|
166
|
+
raise SystemExit(f"wheel metadata is missing: {missing}")
|
|
167
|
+
|
|
168
|
+
forbidden = [
|
|
169
|
+
f"Provides-Extra: agents-{name}"
|
|
170
|
+
for name in ("openai", "anthropic", "litellm", "gemini", "all")
|
|
171
|
+
]
|
|
172
|
+
present = [line for line in forbidden if line in metadata]
|
|
173
|
+
if present:
|
|
174
|
+
raise SystemExit(f"wheel metadata still has provider-specific extras: {present}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _smoke_install(wheel: Path, extra: str | None, code: str) -> None:
|
|
178
|
+
with tempfile.TemporaryDirectory(prefix="use-computer-wheel-") as tmp:
|
|
179
|
+
tmp_path = Path(tmp)
|
|
180
|
+
venv = tmp_path / "venv"
|
|
181
|
+
_run(["uv", "venv", "--python", sys.executable, str(venv)], cwd=tmp_path)
|
|
182
|
+
python = venv / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python")
|
|
183
|
+
requirement = f"{wheel}[{extra}]" if extra else str(wheel)
|
|
184
|
+
_run(["uv", "pip", "install", "--python", str(python), requirement], cwd=tmp_path)
|
|
185
|
+
_run([str(python), "-c", code], cwd=tmp_path)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _run(cmd: list[str], *, cwd: Path) -> None:
|
|
189
|
+
subprocess.run(cmd, cwd=cwd, check=True)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
main()
|