isle-sdk 0.1.0__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.
@@ -0,0 +1,56 @@
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # generated test and benchmark output
17
+ /artifacts/
18
+
19
+ # python
20
+ __pycache__/
21
+ /sdk/python/dist/
22
+ /sdk/python/.venv/
23
+ *.egg-info/
24
+
25
+ # next.js
26
+ /.next/
27
+ /out/
28
+
29
+ # production
30
+ /build
31
+
32
+ # misc
33
+ .DS_Store
34
+ *.pem
35
+
36
+ # debug
37
+ npm-debug.log*
38
+ yarn-debug.log*
39
+ yarn-error.log*
40
+ .pnpm-debug.log*
41
+
42
+ # env files
43
+ .env
44
+ .env.local
45
+ .env*.local
46
+
47
+ # vercel
48
+ .vercel
49
+
50
+ # typescript
51
+ *.tsbuildinfo
52
+ next-env.d.ts
53
+
54
+ # supabase
55
+ supabase/.temp/
56
+ .env*
isle_sdk-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Isle contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.5
2
+ Name: isle-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Isle — application environments for AI agents
5
+ Project-URL: Homepage, https://www.tryisle.com
6
+ Project-URL: Documentation, https://www.tryisle.com/docs
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Requires-Python: >=3.9
13
+ Requires-Dist: httpx>=0.25.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Isle Python SDK
17
+
18
+ Application environments for AI agents.
19
+
20
+ ## Install
21
+
22
+ Requires Python 3.9 or later.
23
+
24
+ ```bash
25
+ python -m pip install isle-sdk
26
+ ```
27
+
28
+ The package is named `isle-sdk` on PyPI and imported as `isle`. Get an API key
29
+ from the [Isle dashboard](https://www.tryisle.com/api-keys).
30
+
31
+ The SDK automatically generates an idempotency key and reuses it for one
32
+ transport retry. Pass `idempotency_key=` when the same create operation may be
33
+ retried by a different process.
34
+
35
+ ## Quickstart
36
+
37
+ ```python
38
+ from isle import Client, IsleAPIError
39
+
40
+ isle = Client(api_key="isle_...")
41
+
42
+ # Create a KiCad sandbox
43
+ sandbox = isle.sandboxes.create(
44
+ "kicad",
45
+ name="Power supply board",
46
+ idempotency_key="power-supply-board-v1",
47
+ )
48
+ sandbox.wait_until_ready()
49
+
50
+ # Upload a file
51
+ sandbox.files.upload("schematic.kicad_sch")
52
+
53
+ # Take a screenshot
54
+ image = sandbox.screen.screenshot()
55
+
56
+ # Control the mouse and keyboard
57
+ sandbox.mouse.click(500, 300)
58
+ sandbox.keyboard.type("100nF")
59
+ sandbox.keyboard.keypress(["CTRL", "S"])
60
+
61
+ # Download results
62
+ sandbox.files.download("/home/user/work/board.kicad_pcb")
63
+
64
+ # Stop (archives the sandbox, can be resumed later)
65
+ sandbox.stop()
66
+ sandbox.wait_until_stopped()
67
+
68
+ # Resume a stopped sandbox
69
+ sandbox.resume()
70
+
71
+ # Lifecycle failures are returned by the API and retained on the environment
72
+ sandbox.refresh()
73
+ if sandbox.last_error:
74
+ print(sandbox.last_error, sandbox.last_error_at)
75
+
76
+ # Retrieve timestamped error history (newest first)
77
+ for error in sandbox.errors():
78
+ print(error["source"], error["message"], error["created_at"])
79
+
80
+ # Recording metadata distinguishes a complete recording from the bounded
81
+ # 512 MiB / six-hour / low-disk partial recording policy.
82
+ recording = sandbox.recording_info()
83
+ print(recording["url"], recording["truncated"], recording["truncation_reason"])
84
+
85
+ # Permanently delete a stopped sandbox and its retained data
86
+ sandbox.stop()
87
+ sandbox.wait_until_stopped()
88
+ sandbox.destroy(timeout=120)
89
+ ```
90
+
91
+ API failures raise `IsleAPIError`. Its `status_code`, `error`, and `metadata`
92
+ attributes can be used to handle failures without parsing an exception string.
93
+ For example, a provisioning failure includes its recoverable
94
+ `metadata["environment_id"]` when the API created a record before the remote
95
+ environment failed to start.
96
+
97
+ ## Environments
98
+
99
+ - `kicad` - EDA environment with KiCad
100
+ - `freecad` - CAD environment with FreeCAD
101
+
102
+ ## Development
103
+
104
+ From an authorized Isle repository checkout:
105
+
106
+ ```bash
107
+ python -m pip install -e ./sdk/python
108
+ python -m unittest discover -s sdk/python/tests
109
+ ```
@@ -0,0 +1,94 @@
1
+ # Isle Python SDK
2
+
3
+ Application environments for AI agents.
4
+
5
+ ## Install
6
+
7
+ Requires Python 3.9 or later.
8
+
9
+ ```bash
10
+ python -m pip install isle-sdk
11
+ ```
12
+
13
+ The package is named `isle-sdk` on PyPI and imported as `isle`. Get an API key
14
+ from the [Isle dashboard](https://www.tryisle.com/api-keys).
15
+
16
+ The SDK automatically generates an idempotency key and reuses it for one
17
+ transport retry. Pass `idempotency_key=` when the same create operation may be
18
+ retried by a different process.
19
+
20
+ ## Quickstart
21
+
22
+ ```python
23
+ from isle import Client, IsleAPIError
24
+
25
+ isle = Client(api_key="isle_...")
26
+
27
+ # Create a KiCad sandbox
28
+ sandbox = isle.sandboxes.create(
29
+ "kicad",
30
+ name="Power supply board",
31
+ idempotency_key="power-supply-board-v1",
32
+ )
33
+ sandbox.wait_until_ready()
34
+
35
+ # Upload a file
36
+ sandbox.files.upload("schematic.kicad_sch")
37
+
38
+ # Take a screenshot
39
+ image = sandbox.screen.screenshot()
40
+
41
+ # Control the mouse and keyboard
42
+ sandbox.mouse.click(500, 300)
43
+ sandbox.keyboard.type("100nF")
44
+ sandbox.keyboard.keypress(["CTRL", "S"])
45
+
46
+ # Download results
47
+ sandbox.files.download("/home/user/work/board.kicad_pcb")
48
+
49
+ # Stop (archives the sandbox, can be resumed later)
50
+ sandbox.stop()
51
+ sandbox.wait_until_stopped()
52
+
53
+ # Resume a stopped sandbox
54
+ sandbox.resume()
55
+
56
+ # Lifecycle failures are returned by the API and retained on the environment
57
+ sandbox.refresh()
58
+ if sandbox.last_error:
59
+ print(sandbox.last_error, sandbox.last_error_at)
60
+
61
+ # Retrieve timestamped error history (newest first)
62
+ for error in sandbox.errors():
63
+ print(error["source"], error["message"], error["created_at"])
64
+
65
+ # Recording metadata distinguishes a complete recording from the bounded
66
+ # 512 MiB / six-hour / low-disk partial recording policy.
67
+ recording = sandbox.recording_info()
68
+ print(recording["url"], recording["truncated"], recording["truncation_reason"])
69
+
70
+ # Permanently delete a stopped sandbox and its retained data
71
+ sandbox.stop()
72
+ sandbox.wait_until_stopped()
73
+ sandbox.destroy(timeout=120)
74
+ ```
75
+
76
+ API failures raise `IsleAPIError`. Its `status_code`, `error`, and `metadata`
77
+ attributes can be used to handle failures without parsing an exception string.
78
+ For example, a provisioning failure includes its recoverable
79
+ `metadata["environment_id"]` when the API created a record before the remote
80
+ environment failed to start.
81
+
82
+ ## Environments
83
+
84
+ - `kicad` - EDA environment with KiCad
85
+ - `freecad` - CAD environment with FreeCAD
86
+
87
+ ## Development
88
+
89
+ From an authorized Isle repository checkout:
90
+
91
+ ```bash
92
+ python -m pip install -e ./sdk/python
93
+ python -m unittest discover -s sdk/python/tests
94
+ ```
@@ -0,0 +1,5 @@
1
+ from isle.client import Client, IsleAPIError
2
+ from isle.sandbox import Environment, Sandbox
3
+
4
+ __all__ = ["Client", "Environment", "IsleAPIError", "Sandbox"]
5
+ __version__ = "0.1.0"
@@ -0,0 +1,215 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ import time
5
+ import uuid
6
+
7
+ import httpx
8
+
9
+ from isle.sandbox import Environment, Sandbox, validate_environment
10
+
11
+
12
+ class IsleAPIError(Exception):
13
+ """An error response returned by the Isle API."""
14
+
15
+ def __init__(self, response: httpx.Response) -> None:
16
+ self.status_code = response.status_code
17
+ self.response = response
18
+ self.method = response.request.method
19
+ self.url = str(response.request.url)
20
+
21
+ try:
22
+ payload: Any = response.json()
23
+ except ValueError:
24
+ payload = None
25
+
26
+ self.payload = payload
27
+ self.metadata = payload if isinstance(payload, dict) else {}
28
+ self.error = self.metadata.get("error")
29
+
30
+ if self.error is not None:
31
+ detail = str(self.error)
32
+ elif payload is not None:
33
+ detail = str(payload)
34
+ else:
35
+ detail = response.text or response.reason_phrase
36
+
37
+ super().__init__(f"Isle API error {self.status_code}: {detail}")
38
+
39
+
40
+ class _SandboxManager:
41
+ def __init__(self, client: Client) -> None:
42
+ self._client = client
43
+
44
+ def create(
45
+ self,
46
+ environment: Environment,
47
+ *,
48
+ name: str | None = None,
49
+ retain: bool = True,
50
+ timeout: float = 360.0,
51
+ idempotency_key: str | None = None,
52
+ ) -> Sandbox:
53
+ environment = validate_environment(environment)
54
+ body: dict = {"environment": environment, "retain": retain}
55
+ if name is not None:
56
+ body["name"] = name
57
+ resolved_idempotency_key = (
58
+ idempotency_key or f"isle-sdk-{uuid.uuid4().hex}"
59
+ )
60
+ headers = {"Idempotency-Key": resolved_idempotency_key}
61
+ deadline = time.monotonic() + timeout
62
+ for attempt in range(2):
63
+ try:
64
+ resp = self._client._request(
65
+ "POST",
66
+ "/api/sandboxes",
67
+ json=body,
68
+ headers=headers,
69
+ timeout=(
70
+ timeout
71
+ if attempt == 0
72
+ else max(deadline - time.monotonic(), 0.001)
73
+ ),
74
+ )
75
+ break
76
+ except httpx.TransportError:
77
+ if attempt == 1 or time.monotonic() >= deadline:
78
+ raise
79
+ return Sandbox(self._client, resp)
80
+
81
+ def list(self) -> list[Sandbox]:
82
+ resp = self._client._request("GET", "/api/sandboxes")
83
+ return [Sandbox(self._client, sandbox) for sandbox in resp]
84
+
85
+ def get(self, sandbox_id: str) -> Sandbox:
86
+ resp = self._client._request("GET", f"/api/sandboxes/{sandbox_id}")
87
+ return Sandbox(self._client, resp)
88
+
89
+
90
+ class _ProviderDeletionManager:
91
+ def __init__(self, client: Client) -> None:
92
+ self._client = client
93
+
94
+ def get(self, tracking_id: str) -> dict:
95
+ return self._client._request(
96
+ "GET", f"/api/provider-deletions/{tracking_id}"
97
+ )
98
+
99
+ def wait(
100
+ self,
101
+ tracking_id: str,
102
+ *,
103
+ timeout: float = 120.0,
104
+ poll: float = 1.0,
105
+ ) -> dict:
106
+ if timeout < 0:
107
+ raise ValueError("timeout must be non-negative")
108
+ if poll <= 0:
109
+ raise ValueError("poll must be greater than zero")
110
+
111
+ deadline = time.monotonic() + timeout
112
+ last: dict | None = None
113
+ while True:
114
+ remaining = deadline - time.monotonic()
115
+ if remaining <= 0:
116
+ break
117
+ last = self._client._request(
118
+ "GET",
119
+ f"/api/provider-deletions/{tracking_id}",
120
+ timeout=remaining,
121
+ )
122
+ if (
123
+ last.get("provider_deletion_complete") is True
124
+ or last.get("requires_manual_verification") is True
125
+ or last.get("provider_operation_status") == "failed"
126
+ ):
127
+ return last
128
+ remaining = deadline - time.monotonic()
129
+ if remaining <= 0:
130
+ break
131
+ time.sleep(min(poll, remaining))
132
+
133
+ status = (
134
+ last.get("provider_operation_status", "unknown")
135
+ if last is not None
136
+ else "unknown"
137
+ )
138
+ raise TimeoutError(
139
+ f"Provider deletion {tracking_id} did not finish in {timeout}s "
140
+ f"(last status: {status})"
141
+ )
142
+
143
+ class Client:
144
+ def __init__(
145
+ self,
146
+ api_key: str | None = None,
147
+ *,
148
+ base_url: str = "https://www.tryisle.com",
149
+ ) -> None:
150
+ import os
151
+
152
+ self.api_key = api_key or os.environ.get("ISLE_API_KEY", "")
153
+ if not self.api_key:
154
+ raise ValueError(
155
+ "api_key is required. Pass it directly or set ISLE_API_KEY."
156
+ )
157
+
158
+ self.base_url = base_url.rstrip("/")
159
+ self._http = httpx.Client(
160
+ base_url=self.base_url,
161
+ headers={"Authorization": f"Bearer {self.api_key}"},
162
+ timeout=30.0,
163
+ # Follow same-host routing redirects. The default uses the
164
+ # canonical host directly because HTTP clients intentionally strip
165
+ # Authorization when a redirect changes hosts.
166
+ follow_redirects=True,
167
+ )
168
+ self.sandboxes = _SandboxManager(self)
169
+ self.provider_deletions = _ProviderDeletionManager(self)
170
+
171
+ def _request(
172
+ self,
173
+ method: str,
174
+ path: str,
175
+ *,
176
+ json: dict | None = None,
177
+ params: dict | None = None,
178
+ headers: dict[str, str] | None = None,
179
+ timeout: float | None = None,
180
+ ) -> dict | list:
181
+ request_options: dict[str, Any] = {
182
+ "json": json,
183
+ "params": params,
184
+ "headers": headers,
185
+ }
186
+ if timeout is not None:
187
+ request_options["timeout"] = timeout
188
+ resp = self._http.request(method, path, **request_options)
189
+ if resp.status_code >= 400:
190
+ raise IsleAPIError(resp)
191
+ return resp.json()
192
+
193
+ def _request_raw(
194
+ self,
195
+ method: str,
196
+ path: str,
197
+ *,
198
+ timeout: float | None = None,
199
+ **kwargs,
200
+ ) -> httpx.Response:
201
+ if timeout is not None:
202
+ kwargs["timeout"] = timeout
203
+ resp = self._http.request(method, path, **kwargs)
204
+ if resp.status_code >= 400:
205
+ raise IsleAPIError(resp)
206
+ return resp
207
+
208
+ def close(self) -> None:
209
+ self._http.close()
210
+
211
+ def __enter__(self) -> Client:
212
+ return self
213
+
214
+ def __exit__(self, *args) -> None:
215
+ self.close()
File without changes