sprntrl 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,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ venv/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .DS_Store
12
+ *.png
sprntrl-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Supernatural
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.
sprntrl-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: sprntrl
3
+ Version: 0.1.0
4
+ Summary: Supernatural Python SDK — stealth browser-as-a-service client.
5
+ Project-URL: Homepage, https://supernatural.sh
6
+ Project-URL: Repository, https://github.com/supernatural-browser/sprntrl-python
7
+ Project-URL: Documentation, https://app.supernatural.sh/docs
8
+ Project-URL: Bug Tracker, https://github.com/supernatural-browser/sprntrl-python/issues
9
+ Author: Supernatural
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: automation,browser,cdp,playwright,scraping,stealth
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.9
25
+ Requires-Dist: httpx>=0.25
26
+ Provides-Extra: dev
27
+ Requires-Dist: mypy>=1.8; extra == 'dev'
28
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
29
+ Requires-Dist: pytest>=7; extra == 'dev'
30
+ Requires-Dist: respx>=0.20; extra == 'dev'
31
+ Requires-Dist: ruff>=0.5; extra == 'dev'
32
+ Provides-Extra: playwright
33
+ Requires-Dist: playwright>=1.40; extra == 'playwright'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # Supernatural Python SDK
37
+
38
+ Official Python client for the [Supernatural](https://supernatural.sh) stealth browser-as-a-service API.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install sprntrl
44
+ # Optional: Playwright integration
45
+ pip install 'sprntrl[playwright]' && playwright install chromium
46
+ ```
47
+
48
+ ## Quick start
49
+
50
+ ```python
51
+ from sprntrl import Sprntrl
52
+
53
+ client = Sprntrl() # reads SPRNTRL_API_KEY from env
54
+
55
+ session = client.sessions.create(os="macos", location="us-east")
56
+
57
+ # browser_session is a context manager that waits for the session,
58
+ # connects Playwright, and closes the browser + Playwright on exit.
59
+ # auto_whitelist=True registers your IP (CDP access is IP-gated).
60
+ with client.sessions.browser_session(session["id"], auto_whitelist=True) as browser:
61
+ page = browser.contexts[0].new_page()
62
+ page.goto("https://bot.sannysoft.com")
63
+ page.screenshot(path="out.png")
64
+
65
+ client.sessions.stop(session["id"])
66
+ ```
67
+
68
+ ### Async
69
+
70
+ ```python
71
+ import asyncio
72
+ from sprntrl import AsyncSprntrl
73
+
74
+ async def main():
75
+ async with AsyncSprntrl() as client:
76
+ session = await client.sessions.create(os="macos", location="us-east")
77
+ async with client.sessions.browser_session(session["id"], auto_whitelist=True) as browser:
78
+ page = await browser.contexts[0].new_page()
79
+ await page.goto("https://example.com")
80
+ await client.sessions.stop(session["id"])
81
+
82
+ asyncio.run(main())
83
+ ```
84
+
85
+ ### Lower-level `connect()` and `cdp_url()`
86
+
87
+ If you want to manage the browser lifecycle yourself:
88
+
89
+ ```python
90
+ browser = client.sessions.connect(session_id, auto_whitelist=True)
91
+ # ... your code ...
92
+ browser.close()
93
+ ```
94
+
95
+ Or to hand the raw WebSocket URL to any CDP client (chrome-remote-interface-python, raw `websockets`, etc.):
96
+
97
+ ```python
98
+ url = client.sessions.cdp_url(session_id)
99
+ # url = "wss://api.supernatural.sh/api/v1/sessions/<id>/cdp"
100
+ ```
101
+
102
+ ## Configuration
103
+
104
+ | Env var | Default |
105
+ |--------------------|----------------------------|
106
+ | `SPRNTRL_API_KEY` | — |
107
+ | `SPRNTRL_BASE_URL` | `https://api.supernatural.sh` |
108
+
109
+ Or override per client:
110
+
111
+ ```python
112
+ client = Sprntrl(api_key="sk_...", base_url="https://api.supernatural.sh", timeout=30, max_retries=2)
113
+ ```
114
+
115
+ ## Resources
116
+
117
+ - `client.sessions` — create, list, list_active, list_history, list_resumable, list_locations, get, stop, resume, delete_persistent, wait_until_ready, connect, browser_session, cdp_url
118
+ - `client.sessions.files` — list, download, upload
119
+ - `client.profiles` — create, list, get, update, duplicate, delete
120
+ - `client.templates.list()`
121
+ - `client.ip_whitelist` — list, add, remove
122
+ - `client.usage` — current, history
123
+ - `client.user` — me, update, update_settings, change_password
124
+ - `client.api_keys` — list, create (full key returned ONCE), revoke
125
+
126
+ ## Error handling
127
+
128
+ ```python
129
+ from sprntrl import Sprntrl, APIError, RateLimitError, AuthenticationError
130
+
131
+ client = Sprntrl()
132
+ try:
133
+ client.sessions.create(os="macos", location="us-east")
134
+ except RateLimitError as e:
135
+ print("rate limited:", e.status, e.body)
136
+ except AuthenticationError:
137
+ print("bad API key")
138
+ except APIError as e:
139
+ print("api error:", e.status, e)
140
+ ```
141
+
142
+ Transient errors (5xx, 429, 408, connection errors) are retried automatically up to `max_retries` times with exponential backoff.
143
+
144
+ ## Gotchas
145
+
146
+ - **CDP access is IP-whitelist gated.** The WebSocket at `/api/v1/sessions/:id/cdp` does not accept bearer auth — instead, your public IP (as Cloudflare sees it) must be in your account's whitelist. Use `client.ip_whitelist.add("current")` or pass `auto_whitelist=True` to `sessions.connect`.
147
+ - **Sessions start async.** `sessions.create` returns immediately with `status: "creating"`. Call `sessions.wait_until_ready(id)` before connecting, or just use `sessions.connect()` which waits for you.
148
+ - **API key is shown only once.** `api_keys.create()` returns the full `key` field exactly once — store it immediately.
149
+
150
+ ## License
151
+
152
+ MIT
@@ -0,0 +1,117 @@
1
+ # Supernatural Python SDK
2
+
3
+ Official Python client for the [Supernatural](https://supernatural.sh) stealth browser-as-a-service API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install sprntrl
9
+ # Optional: Playwright integration
10
+ pip install 'sprntrl[playwright]' && playwright install chromium
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```python
16
+ from sprntrl import Sprntrl
17
+
18
+ client = Sprntrl() # reads SPRNTRL_API_KEY from env
19
+
20
+ session = client.sessions.create(os="macos", location="us-east")
21
+
22
+ # browser_session is a context manager that waits for the session,
23
+ # connects Playwright, and closes the browser + Playwright on exit.
24
+ # auto_whitelist=True registers your IP (CDP access is IP-gated).
25
+ with client.sessions.browser_session(session["id"], auto_whitelist=True) as browser:
26
+ page = browser.contexts[0].new_page()
27
+ page.goto("https://bot.sannysoft.com")
28
+ page.screenshot(path="out.png")
29
+
30
+ client.sessions.stop(session["id"])
31
+ ```
32
+
33
+ ### Async
34
+
35
+ ```python
36
+ import asyncio
37
+ from sprntrl import AsyncSprntrl
38
+
39
+ async def main():
40
+ async with AsyncSprntrl() as client:
41
+ session = await client.sessions.create(os="macos", location="us-east")
42
+ async with client.sessions.browser_session(session["id"], auto_whitelist=True) as browser:
43
+ page = await browser.contexts[0].new_page()
44
+ await page.goto("https://example.com")
45
+ await client.sessions.stop(session["id"])
46
+
47
+ asyncio.run(main())
48
+ ```
49
+
50
+ ### Lower-level `connect()` and `cdp_url()`
51
+
52
+ If you want to manage the browser lifecycle yourself:
53
+
54
+ ```python
55
+ browser = client.sessions.connect(session_id, auto_whitelist=True)
56
+ # ... your code ...
57
+ browser.close()
58
+ ```
59
+
60
+ Or to hand the raw WebSocket URL to any CDP client (chrome-remote-interface-python, raw `websockets`, etc.):
61
+
62
+ ```python
63
+ url = client.sessions.cdp_url(session_id)
64
+ # url = "wss://api.supernatural.sh/api/v1/sessions/<id>/cdp"
65
+ ```
66
+
67
+ ## Configuration
68
+
69
+ | Env var | Default |
70
+ |--------------------|----------------------------|
71
+ | `SPRNTRL_API_KEY` | — |
72
+ | `SPRNTRL_BASE_URL` | `https://api.supernatural.sh` |
73
+
74
+ Or override per client:
75
+
76
+ ```python
77
+ client = Sprntrl(api_key="sk_...", base_url="https://api.supernatural.sh", timeout=30, max_retries=2)
78
+ ```
79
+
80
+ ## Resources
81
+
82
+ - `client.sessions` — create, list, list_active, list_history, list_resumable, list_locations, get, stop, resume, delete_persistent, wait_until_ready, connect, browser_session, cdp_url
83
+ - `client.sessions.files` — list, download, upload
84
+ - `client.profiles` — create, list, get, update, duplicate, delete
85
+ - `client.templates.list()`
86
+ - `client.ip_whitelist` — list, add, remove
87
+ - `client.usage` — current, history
88
+ - `client.user` — me, update, update_settings, change_password
89
+ - `client.api_keys` — list, create (full key returned ONCE), revoke
90
+
91
+ ## Error handling
92
+
93
+ ```python
94
+ from sprntrl import Sprntrl, APIError, RateLimitError, AuthenticationError
95
+
96
+ client = Sprntrl()
97
+ try:
98
+ client.sessions.create(os="macos", location="us-east")
99
+ except RateLimitError as e:
100
+ print("rate limited:", e.status, e.body)
101
+ except AuthenticationError:
102
+ print("bad API key")
103
+ except APIError as e:
104
+ print("api error:", e.status, e)
105
+ ```
106
+
107
+ Transient errors (5xx, 429, 408, connection errors) are retried automatically up to `max_retries` times with exponential backoff.
108
+
109
+ ## Gotchas
110
+
111
+ - **CDP access is IP-whitelist gated.** The WebSocket at `/api/v1/sessions/:id/cdp` does not accept bearer auth — instead, your public IP (as Cloudflare sees it) must be in your account's whitelist. Use `client.ip_whitelist.add("current")` or pass `auto_whitelist=True` to `sessions.connect`.
112
+ - **Sessions start async.** `sessions.create` returns immediately with `status: "creating"`. Call `sessions.wait_until_ready(id)` before connecting, or just use `sessions.connect()` which waits for you.
113
+ - **API key is shown only once.** `api_keys.create()` returns the full `key` field exactly once — store it immediately.
114
+
115
+ ## License
116
+
117
+ MIT
@@ -0,0 +1,64 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sprntrl"
7
+ version = "0.1.0"
8
+ description = "Supernatural Python SDK — stealth browser-as-a-service client."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Supernatural" }]
13
+ keywords = ["browser", "automation", "playwright", "cdp", "stealth", "scraping"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Internet :: WWW/HTTP :: Browsers",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ ]
27
+ dependencies = [
28
+ "httpx>=0.25",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ playwright = ["playwright>=1.40"]
33
+ dev = [
34
+ "pytest>=7",
35
+ "pytest-asyncio>=0.23",
36
+ "respx>=0.20",
37
+ "ruff>=0.5",
38
+ "mypy>=1.8",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://supernatural.sh"
43
+ Repository = "https://github.com/supernatural-browser/sprntrl-python"
44
+ Documentation = "https://app.supernatural.sh/docs"
45
+ "Bug Tracker" = "https://github.com/supernatural-browser/sprntrl-python/issues"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["sprntrl"]
49
+ # sprntrl/py.typed (PEP 561) is under the package dir, so hatchling ships it
50
+ # in the wheel automatically alongside the .py sources.
51
+
52
+ [tool.hatch.build.targets.sdist]
53
+ # Default sdist is VCS-driven; list explicitly so LICENSE + py.typed ship even
54
+ # before they're committed and the sdist stays reproducible.
55
+ include = ["sprntrl", "README.md", "LICENSE", "pyproject.toml"]
56
+
57
+ [tool.ruff]
58
+ line-length = 100
59
+ target-version = "py39"
60
+
61
+ [tool.mypy]
62
+ python_version = "3.9"
63
+ strict = false
64
+ warn_unused_ignores = true
@@ -0,0 +1,37 @@
1
+ """Sprntrl Python SDK — stealth browser-as-a-service client."""
2
+
3
+ from ._client import Sprntrl, AsyncSprntrl
4
+ from ._errors import (
5
+ SprntrlError,
6
+ APIError,
7
+ BadRequestError,
8
+ AuthenticationError,
9
+ PermissionDeniedError,
10
+ NotFoundError,
11
+ ConflictError,
12
+ UnprocessableEntityError,
13
+ RateLimitError,
14
+ InternalServerError,
15
+ APIConnectionError,
16
+ APIConnectionTimeoutError,
17
+ )
18
+
19
+ __version__ = "0.1.0"
20
+
21
+ __all__ = [
22
+ "Sprntrl",
23
+ "AsyncSprntrl",
24
+ "SprntrlError",
25
+ "APIError",
26
+ "BadRequestError",
27
+ "AuthenticationError",
28
+ "PermissionDeniedError",
29
+ "NotFoundError",
30
+ "ConflictError",
31
+ "UnprocessableEntityError",
32
+ "RateLimitError",
33
+ "InternalServerError",
34
+ "APIConnectionError",
35
+ "APIConnectionTimeoutError",
36
+ "__version__",
37
+ ]
@@ -0,0 +1,232 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import time
5
+ import asyncio
6
+ import random
7
+ from typing import Any, Mapping, Union
8
+ from urllib.parse import urljoin
9
+
10
+ import httpx
11
+
12
+ from ._errors import (
13
+ APIConnectionError,
14
+ APIConnectionTimeoutError,
15
+ error_for_status,
16
+ SprntrlError,
17
+ )
18
+
19
+
20
+ DEFAULT_BASE_URL = "https://api.supernatural.sh"
21
+ DEFAULT_TIMEOUT = 60.0
22
+ DEFAULT_MAX_RETRIES = 2
23
+ _USER_AGENT = "sprntrl-python/0.1.0"
24
+
25
+ JSONLike = Union[Mapping[str, Any], list, str, int, float, bool, None]
26
+
27
+
28
+ class _BaseClient:
29
+ """Shared config and helpers for sync and async clients."""
30
+
31
+ def __init__(
32
+ self,
33
+ *,
34
+ api_key: str | None = None,
35
+ base_url: str | None = None,
36
+ timeout: float = DEFAULT_TIMEOUT,
37
+ max_retries: int = DEFAULT_MAX_RETRIES,
38
+ default_headers: Mapping[str, str] | None = None,
39
+ ) -> None:
40
+ api_key = api_key or os.environ.get("SPRNTRL_API_KEY")
41
+ if not api_key:
42
+ raise SprntrlError(
43
+ "No API key provided. Pass api_key= or set SPRNTRL_API_KEY."
44
+ )
45
+ base_url = base_url or os.environ.get("SPRNTRL_BASE_URL") or DEFAULT_BASE_URL
46
+ self.api_key = api_key
47
+ self.base_url = base_url.rstrip("/")
48
+ self.timeout = timeout
49
+ self.max_retries = max_retries
50
+ self._default_headers = dict(default_headers or {})
51
+
52
+ def _headers(self, extra: Mapping[str, str] | None = None) -> dict[str, str]:
53
+ h = {
54
+ "Authorization": f"ApiKey {self.api_key}",
55
+ "Accept": "application/json",
56
+ "User-Agent": _USER_AGENT,
57
+ **self._default_headers,
58
+ }
59
+ if extra:
60
+ h.update(extra)
61
+ return h
62
+
63
+ def _url(self, path: str) -> str:
64
+ if path.startswith("http://") or path.startswith("https://"):
65
+ return path
66
+ if not path.startswith("/"):
67
+ path = "/" + path
68
+ return self.base_url + path
69
+
70
+ @staticmethod
71
+ def _should_retry(exc: Exception | None, status: int | None) -> bool:
72
+ if exc is not None:
73
+ return True # connection-level errors
74
+ if status is None:
75
+ return False
76
+ return status == 408 or status == 409 or status == 429 or status >= 500
77
+
78
+ @staticmethod
79
+ def _backoff(attempt: int) -> float:
80
+ # 0.5s, 1s, 2s + jitter
81
+ base = 0.5 * (2 ** attempt)
82
+ return base + random.uniform(0, 0.25)
83
+
84
+ @staticmethod
85
+ def _parse_error(response: httpx.Response) -> tuple[str, Any]:
86
+ body: Any = None
87
+ try:
88
+ body = response.json()
89
+ except Exception:
90
+ body = response.text
91
+ msg = None
92
+ if isinstance(body, dict):
93
+ msg = body.get("error") or body.get("message") or body.get("detail")
94
+ if not msg:
95
+ msg = f"HTTP {response.status_code}"
96
+ return msg, body
97
+
98
+
99
+ class SyncClient(_BaseClient):
100
+ def __init__(self, **kwargs: Any) -> None:
101
+ super().__init__(**kwargs)
102
+ self._http = httpx.Client(timeout=self.timeout)
103
+
104
+ def close(self) -> None:
105
+ self._http.close()
106
+
107
+ def __enter__(self) -> "SyncClient":
108
+ return self
109
+
110
+ def __exit__(self, *exc: Any) -> None:
111
+ self.close()
112
+
113
+ def _request(
114
+ self,
115
+ method: str,
116
+ path: str,
117
+ *,
118
+ json: JSONLike = None,
119
+ params: Mapping[str, Any] | None = None,
120
+ headers: Mapping[str, str] | None = None,
121
+ files: Any = None,
122
+ data: Any = None,
123
+ stream: bool = False,
124
+ ) -> Any:
125
+ url = self._url(path)
126
+ h = self._headers(headers)
127
+ last_exc: Exception | None = None
128
+ for attempt in range(self.max_retries + 1):
129
+ try:
130
+ response = self._http.request(
131
+ method,
132
+ url,
133
+ json=json if files is None and data is None else None,
134
+ params=params,
135
+ headers=h,
136
+ files=files,
137
+ data=data,
138
+ )
139
+ except httpx.TimeoutException as exc:
140
+ last_exc = APIConnectionTimeoutError(str(exc))
141
+ except httpx.RequestError as exc:
142
+ last_exc = APIConnectionError(str(exc), cause=exc)
143
+ else:
144
+ status = response.status_code
145
+ if 200 <= status < 300:
146
+ if stream:
147
+ return response
148
+ if not response.content:
149
+ return None
150
+ ctype = response.headers.get("content-type", "")
151
+ if "application/json" in ctype:
152
+ return response.json()
153
+ return response.content
154
+ if self._should_retry(None, status) and attempt < self.max_retries:
155
+ time.sleep(self._backoff(attempt))
156
+ continue
157
+ msg, body = self._parse_error(response)
158
+ raise error_for_status(status, msg, body=body)
159
+ if attempt < self.max_retries:
160
+ time.sleep(self._backoff(attempt))
161
+ continue
162
+ raise last_exc
163
+ assert last_exc is not None
164
+ raise last_exc
165
+
166
+
167
+ class AsyncClient(_BaseClient):
168
+ def __init__(self, **kwargs: Any) -> None:
169
+ super().__init__(**kwargs)
170
+ self._http = httpx.AsyncClient(timeout=self.timeout)
171
+
172
+ async def close(self) -> None:
173
+ await self._http.aclose()
174
+
175
+ async def __aenter__(self) -> "AsyncClient":
176
+ return self
177
+
178
+ async def __aexit__(self, *exc: Any) -> None:
179
+ await self.close()
180
+
181
+ async def _request(
182
+ self,
183
+ method: str,
184
+ path: str,
185
+ *,
186
+ json: JSONLike = None,
187
+ params: Mapping[str, Any] | None = None,
188
+ headers: Mapping[str, str] | None = None,
189
+ files: Any = None,
190
+ data: Any = None,
191
+ stream: bool = False,
192
+ ) -> Any:
193
+ url = self._url(path)
194
+ h = self._headers(headers)
195
+ last_exc: Exception | None = None
196
+ for attempt in range(self.max_retries + 1):
197
+ try:
198
+ response = await self._http.request(
199
+ method,
200
+ url,
201
+ json=json if files is None and data is None else None,
202
+ params=params,
203
+ headers=h,
204
+ files=files,
205
+ data=data,
206
+ )
207
+ except httpx.TimeoutException as exc:
208
+ last_exc = APIConnectionTimeoutError(str(exc))
209
+ except httpx.RequestError as exc:
210
+ last_exc = APIConnectionError(str(exc), cause=exc)
211
+ else:
212
+ status = response.status_code
213
+ if 200 <= status < 300:
214
+ if stream:
215
+ return response
216
+ if not response.content:
217
+ return None
218
+ ctype = response.headers.get("content-type", "")
219
+ if "application/json" in ctype:
220
+ return response.json()
221
+ return response.content
222
+ if self._should_retry(None, status) and attempt < self.max_retries:
223
+ await asyncio.sleep(self._backoff(attempt))
224
+ continue
225
+ msg, body = self._parse_error(response)
226
+ raise error_for_status(status, msg, body=body)
227
+ if attempt < self.max_retries:
228
+ await asyncio.sleep(self._backoff(attempt))
229
+ continue
230
+ raise last_exc
231
+ assert last_exc is not None
232
+ raise last_exc
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from ._base_client import SyncClient, AsyncClient
4
+ from .resources import (
5
+ Sessions,
6
+ AsyncSessions,
7
+ Profiles,
8
+ AsyncProfiles,
9
+ Templates,
10
+ AsyncTemplates,
11
+ IPWhitelist,
12
+ AsyncIPWhitelist,
13
+ Usage,
14
+ AsyncUsage,
15
+ User,
16
+ AsyncUser,
17
+ APIKeys,
18
+ AsyncAPIKeys,
19
+ )
20
+
21
+
22
+ class Sprntrl(SyncClient):
23
+ """Sync Sprntrl SDK client.
24
+
25
+ Usage:
26
+ with Sprntrl() as client:
27
+ session = client.sessions.create(os="macos", location="us-east")
28
+ client.sessions.wait_until_ready(session["id"])
29
+ """
30
+
31
+ def __init__(self, **kwargs) -> None:
32
+ super().__init__(**kwargs)
33
+ self.sessions = Sessions(self)
34
+ self.profiles = Profiles(self)
35
+ self.templates = Templates(self)
36
+ self.ip_whitelist = IPWhitelist(self)
37
+ self.usage = Usage(self)
38
+ self.user = User(self)
39
+ self.api_keys = APIKeys(self)
40
+
41
+
42
+ class AsyncSprntrl(AsyncClient):
43
+ """Async Sprntrl SDK client.
44
+
45
+ Usage:
46
+ async with AsyncSprntrl() as client:
47
+ session = await client.sessions.create(os="macos", location="us-east")
48
+ await client.sessions.wait_until_ready(session["id"])
49
+ """
50
+
51
+ def __init__(self, **kwargs) -> None:
52
+ super().__init__(**kwargs)
53
+ self.sessions = AsyncSessions(self)
54
+ self.profiles = AsyncProfiles(self)
55
+ self.templates = AsyncTemplates(self)
56
+ self.ip_whitelist = AsyncIPWhitelist(self)
57
+ self.usage = AsyncUsage(self)
58
+ self.user = AsyncUser(self)
59
+ self.api_keys = AsyncAPIKeys(self)