treinta-previews 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.
Files changed (37) hide show
  1. treinta_previews-0.1.0/.gitignore +9 -0
  2. treinta_previews-0.1.0/CHANGELOG.md +23 -0
  3. treinta_previews-0.1.0/LICENSE +21 -0
  4. treinta_previews-0.1.0/PKG-INFO +241 -0
  5. treinta_previews-0.1.0/README.md +204 -0
  6. treinta_previews-0.1.0/pyproject.toml +80 -0
  7. treinta_previews-0.1.0/scripts/smoke.py +79 -0
  8. treinta_previews-0.1.0/src/previews/__init__.py +88 -0
  9. treinta_previews-0.1.0/src/previews/_case.py +48 -0
  10. treinta_previews-0.1.0/src/previews/_config.py +45 -0
  11. treinta_previews-0.1.0/src/previews/_http.py +62 -0
  12. treinta_previews-0.1.0/src/previews/_version.py +3 -0
  13. treinta_previews-0.1.0/src/previews/archive.py +271 -0
  14. treinta_previews-0.1.0/src/previews/async_client.py +122 -0
  15. treinta_previews-0.1.0/src/previews/client.py +136 -0
  16. treinta_previews-0.1.0/src/previews/errors.py +36 -0
  17. treinta_previews-0.1.0/src/previews/models.py +351 -0
  18. treinta_previews-0.1.0/src/previews/proxy/__init__.py +27 -0
  19. treinta_previews-0.1.0/src/previews/proxy/core.py +122 -0
  20. treinta_previews-0.1.0/src/previews/proxy/fastapi.py +139 -0
  21. treinta_previews-0.1.0/src/previews/proxy/flask.py +129 -0
  22. treinta_previews-0.1.0/src/previews/py.typed +0 -0
  23. treinta_previews-0.1.0/src/previews/resources/__init__.py +70 -0
  24. treinta_previews-0.1.0/src/previews/resources/accounts.py +29 -0
  25. treinta_previews-0.1.0/src/previews/resources/detect.py +64 -0
  26. treinta_previews-0.1.0/src/previews/resources/drives.py +64 -0
  27. treinta_previews-0.1.0/src/previews/resources/integrations.py +58 -0
  28. treinta_previews-0.1.0/src/previews/resources/snapshots.py +86 -0
  29. treinta_previews-0.1.0/src/previews/resources/vms.py +335 -0
  30. treinta_previews-0.1.0/src/previews/sse.py +205 -0
  31. treinta_previews-0.1.0/tests/conftest.py +43 -0
  32. treinta_previews-0.1.0/tests/test_archive.py +116 -0
  33. treinta_previews-0.1.0/tests/test_case.py +67 -0
  34. treinta_previews-0.1.0/tests/test_live.py +35 -0
  35. treinta_previews-0.1.0/tests/test_models.py +254 -0
  36. treinta_previews-0.1.0/tests/test_proxy.py +147 -0
  37. treinta_previews-0.1.0/tests/test_sse.py +115 -0
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ __pycache__/
6
+ *.pyc
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to `treinta-previews` are documented here. This project
4
+ adheres to [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [0.1.0] - 2026-07-01
7
+
8
+ Initial release. 1:1 parity with the Node SDK (`cli/client.ts` + `web/src/lib/api.ts`).
9
+
10
+ ### Added
11
+ - Synchronous `PreviewsClient` and asynchronous `AsyncPreviewsClient`.
12
+ - Resources: `vms`, `snapshots`, `drives`, `integrations`, `detect`, `accounts`, `system`.
13
+ - VM lifecycle: `create`, `list`, `get`, `destroy`, `restart`, `redeploy`,
14
+ `run`, `logs` (SSE), `get_env`, `set_env`, `bandwidth`, `persist`,
15
+ `unpersist`, `rename_slug`, `slug_available`, `upload_files`,
16
+ `mint_widget_token`, `wait_until_running`, `create_from_folder`.
17
+ - Folder → zip archiving (`archive.zip_directory`) faithful to `mcp/src/archive.ts`:
18
+ honors `.gitignore` via `git ls-files`, strips secrets, force-includes
19
+ `.env.example`, 50 MiB compressed cap, uncompressed tripwire, fingerprint cache.
20
+ - SSE parsing for named-event logs and unnamed detect streams.
21
+ - Frozen-dataclass models with `from_dict` (camelCase → snake_case, tolerant of
22
+ unknown keys) and a shipped `py.typed` marker.
23
+ - Backend widget proxy (`previews.proxy`) with FastAPI and Flask adapters.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Treinta Previews
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,241 @@
1
+ Metadata-Version: 2.4
2
+ Name: treinta-previews
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the Propie / Previews VM-as-a-Service platform (Firecracker microVMs).
5
+ Project-URL: Homepage, https://previews.amapola.treinta.ai
6
+ Project-URL: Repository, https://github.com/justfedec/previews
7
+ Author-email: Treinta Previews <carrizofg@gmail.com>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: firecracker,microvm,previews,propie,sdk,vm
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Requires-Dist: httpx<1,>=0.27
24
+ Provides-Extra: dev
25
+ Requires-Dist: anyio; extra == 'dev'
26
+ Requires-Dist: build; extra == 'dev'
27
+ Requires-Dist: mypy; extra == 'dev'
28
+ Requires-Dist: pytest-asyncio; extra == 'dev'
29
+ Requires-Dist: pytest>=8; extra == 'dev'
30
+ Requires-Dist: ruff; extra == 'dev'
31
+ Requires-Dist: twine; extra == 'dev'
32
+ Provides-Extra: fastapi
33
+ Requires-Dist: fastapi>=0.100; extra == 'fastapi'
34
+ Provides-Extra: flask
35
+ Requires-Dist: flask>=2.2; extra == 'flask'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # treinta-previews (Python SDK)
39
+
40
+ Python SDK for the **Propie / Previews** VM-as-a-Service platform — launch
41
+ Firecracker microVMs from a git repo or a local folder, run commands, stream
42
+ logs, attach drives and databases, take snapshots, promote to permanent
43
+ (scale-to-zero) previews, and embed a browser widget safely.
44
+
45
+ It mirrors the Node SDK (`cli/client.ts` + `web/src/lib/api.ts`) 1:1 in surface
46
+ and naming, translated to Pythonic snake_case.
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install treinta-previews
52
+ ```
53
+
54
+ - **Distribution name:** `treinta-previews`
55
+ - **Import name:** `previews`
56
+
57
+ ```python
58
+ from previews import PreviewsClient
59
+ ```
60
+
61
+ > If the top-level name `previews` ever collides with another package in your
62
+ > environment, the intended fallback import name is `treinta_previews`. This
63
+ > release publishes the package as `previews`; use a virtualenv to avoid
64
+ > collisions.
65
+
66
+ Optional extras:
67
+
68
+ ```bash
69
+ pip install "treinta-previews[fastapi]" # widget proxy FastAPI adapter
70
+ pip install "treinta-previews[flask]" # widget proxy Flask adapter
71
+ ```
72
+
73
+ ## Authentication
74
+
75
+ The client resolves credentials from arguments or the environment:
76
+
77
+ | Setting | Argument | Env vars (in precedence order) | Default |
78
+ | -------- | ---------- | -------------------------------------------------------------------- | ------- |
79
+ | API key | `api_key` | `TREINTA_PREVIEWS_API_KEY`, `PROPIE_API_KEY`, `PREVIEWS_API_KEY` | — |
80
+ | Base URL | `base_url` | `TREINTA_PREVIEWS_API_URL`, `PROPIE_API_URL` | `https://previews.amapola.treinta.ai/api` |
81
+
82
+ Keys look like `pvk_<prefix>_<secret>` and are sent as `Authorization: Bearer pvk_...`.
83
+
84
+ ## Quick start
85
+
86
+ ```python
87
+ from previews import PreviewsClient
88
+
89
+ with PreviewsClient() as client: # api key from env
90
+ vm = client.vms.create(
91
+ repo_url="https://github.com/owner/repo",
92
+ stack="node20",
93
+ exposed_port=3000,
94
+ environment_variables={"NODE_ENV": "production"},
95
+ )
96
+ vm = client.vms.wait_until_running(vm.id)
97
+ print(vm.url)
98
+
99
+ result = client.vms.run(vm.id, "npm test")
100
+ print(result.exit_code, result.stdout)
101
+
102
+ for event in client.vms.logs(vm.id, follow=False):
103
+ print(event.message)
104
+
105
+ client.vms.destroy(vm.id)
106
+ ```
107
+
108
+ Deploy a local folder (zipped locally, honoring `.gitignore`, stripping secrets):
109
+
110
+ ```python
111
+ det = client.detect.folder("/path/to/app") # SSE stack detection
112
+ vm = client.vms.create_from_folder(
113
+ "/path/to/app",
114
+ stack=det.stack,
115
+ start_command=det.start_command,
116
+ exposed_port=det.exposed_port,
117
+ )
118
+ ```
119
+
120
+ ### Async
121
+
122
+ ```python
123
+ from previews import AsyncPreviewsClient
124
+
125
+ async with AsyncPreviewsClient() as client:
126
+ vms = await client.vms.list()
127
+ async for event in client.vms.logs(vms[0].id, follow=False):
128
+ print(event.message)
129
+ ```
130
+
131
+ ## API surface
132
+
133
+ `PreviewsClient(api_key=None, *, base_url=None, timeout=30.0, http_client=None)`
134
+ (and the identical `AsyncPreviewsClient` with `await`/async generators/`aclose()`).
135
+ Both are context managers and expose `request(method, path, *, json=None, ...)`
136
+ plus the resources below.
137
+
138
+ ### `client.vms`
139
+
140
+ | Method | REST |
141
+ | --- | --- |
142
+ | `create(**fields)` | `POST /vms` (json) |
143
+ | `create_from_folder(path, **meta)` / `create_from_zip(zip_bytes, **meta)` | `POST /vms` (multipart) |
144
+ | `list()` | `GET /vms` |
145
+ | `get(id)` | `GET /vms/:id` |
146
+ | `destroy(id)` | `DELETE /vms/:id` |
147
+ | `restart(id)` | `POST /vms/:id/restart` |
148
+ | `redeploy(id)` | `POST /vms/:id/redeploy` |
149
+ | `run(id, command, *, timeout=300.0)` | `POST /vms/:id/run` (buffered) |
150
+ | `logs(id, *, follow=True)` | `GET /vms/:id/logs` (SSE) → `Iterator[LogEvent]` |
151
+ | `get_env(id)` / `set_env(id, env)` | `GET`/`PUT /vms/:id/env` |
152
+ | `bandwidth(id)` | `GET /vms/:id/bandwidth` |
153
+ | `persist(id, slug)` / `unpersist(id)` / `rename_slug(id, slug)` | `/vms/:id/persist`, `/vms/:id/slug` |
154
+ | `slug_available(slug)` → `(bool, reason?)` | `GET /vms/slug-available` |
155
+ | `upload_files(id, files, *, base_dir="/app")` | `POST /vms/:id/files` |
156
+ | `mint_widget_token(id, *, capabilities=None, ttl_seconds=None)` | `POST /vms/:id/widget-token` |
157
+ | `wait_until_running(id, *, timeout=300.0, interval=2.0, on_status=None)` | polls `GET /vms/:id` |
158
+
159
+ `create` / `create_from_*` accept snake_case fields: `repo_url`, `stack`,
160
+ `branch`, `subdirectory`, `exposed_port`, `install_command`, `start_command`,
161
+ `environment_variables`, `drive_id`, `drive_mount_path`, `drive_read_only`,
162
+ `database_integration_id`, `vcpus`, `memory_mib`, `persistent`, `slug`.
163
+
164
+ ### `client.snapshots` / `.drives` / `.integrations` / `.detect` / `.accounts`
165
+
166
+ - `snapshots.list() / create(vm_id, name=None) / clone(snapshot_id, *, name=None, environment_variables=None) / delete(snapshot_id)`
167
+ - `drives.list() / create(name, size_gib, *, mount_path=None) / delete(id)`
168
+ - `integrations.list() / create(**fields) / delete(id)`
169
+ - `detect.public(repo_url, *, on_progress=None) / zip(zip_bytes, ...) / folder(path, ...)`
170
+ - `accounts.current()` → `CurrentPrincipal(account, project, auth_type, api_key)`
171
+
172
+ ### System status
173
+
174
+ Not a dedicated resource; use the generic request helper:
175
+
176
+ ```python
177
+ from previews import SystemStatus
178
+ status = SystemStatus.from_dict(client.request("GET", "/system/status"))
179
+ print(status.caches["npm"].size_bytes)
180
+ ```
181
+
182
+ ## Errors
183
+
184
+ Non-2xx responses raise `PreviewsApiError(status, code, message)` from the
185
+ `{ error: { code, message } }` envelope (a non-JSON body yields code `UNKNOWN`).
186
+ `wait_until_running` raises `PreviewsTimeoutError` on deadline. Missing
187
+ credentials raise `PreviewsConfigError`.
188
+
189
+ ## Models
190
+
191
+ Responses are frozen dataclasses with a tolerant `from_dict` (camelCase →
192
+ snake_case, unknown keys ignored): `Preview` (alias `VM`), `RunCommandResult`,
193
+ `VMSnapshot`, `DetectionResult`/`EnvVarHint`, `BandwidthSample`/`BandwidthResponse`,
194
+ `Account`/`Project`/`ApiKeyRef`/`CurrentPrincipal`, `DatabaseIntegration`,
195
+ `Drive`, `SystemStatus`/`CacheStat`, `WidgetToken`, `UploadResult`, `LogEvent`.
196
+ The package ships `py.typed`.
197
+
198
+ ## Widget backend proxy
199
+
200
+ The React widget UI is built separately. This SDK provides the backend piece.
201
+
202
+ **Scoped token (recommended):** mint a short-lived `pwt_` token server-side and
203
+ hand it to the browser, which then calls the platform directly.
204
+
205
+ ```python
206
+ token = client.vms.mint_widget_token(vm_id, capabilities=["preview:read", "vm:run"])
207
+ ```
208
+
209
+ **Backend proxy:** the browser calls *your* server; the proxy forwards only a
210
+ whitelisted subset of operations to the platform using the `pvk_` key it holds —
211
+ the key never reaches the browser.
212
+
213
+ ```python
214
+ from previews import PreviewsClient
215
+ from previews.proxy import PreviewsProxy, ProxyConfig, make_router # or make_blueprint
216
+
217
+ client = PreviewsClient()
218
+ proxy = PreviewsProxy(ProxyConfig(
219
+ client=client,
220
+ allowed_origins=["https://app.example.com"],
221
+ allowed_vm_ids={"<vm-uuid>"}, # None = any VM in the project
222
+ # allow_ops defaults to WIDGET_SAFE_OPS = {"status","run","files","logs","mint_token"}
223
+ ))
224
+
225
+ # FastAPI
226
+ from fastapi import FastAPI
227
+ app = FastAPI()
228
+ app.include_router(make_router(proxy, prefix="/previews"))
229
+
230
+ # Flask
231
+ # from flask import Flask
232
+ # app = Flask(__name__)
233
+ # app.register_blueprint(make_blueprint(proxy, url_prefix="/previews"))
234
+ ```
235
+
236
+ The proxy enforces the origin allowlist, the op whitelist (hard-capped to the
237
+ widget-safe set), and the per-VM restriction; it never echoes the `pvk_` key.
238
+
239
+ ## License
240
+
241
+ MIT
@@ -0,0 +1,204 @@
1
+ # treinta-previews (Python SDK)
2
+
3
+ Python SDK for the **Propie / Previews** VM-as-a-Service platform — launch
4
+ Firecracker microVMs from a git repo or a local folder, run commands, stream
5
+ logs, attach drives and databases, take snapshots, promote to permanent
6
+ (scale-to-zero) previews, and embed a browser widget safely.
7
+
8
+ It mirrors the Node SDK (`cli/client.ts` + `web/src/lib/api.ts`) 1:1 in surface
9
+ and naming, translated to Pythonic snake_case.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install treinta-previews
15
+ ```
16
+
17
+ - **Distribution name:** `treinta-previews`
18
+ - **Import name:** `previews`
19
+
20
+ ```python
21
+ from previews import PreviewsClient
22
+ ```
23
+
24
+ > If the top-level name `previews` ever collides with another package in your
25
+ > environment, the intended fallback import name is `treinta_previews`. This
26
+ > release publishes the package as `previews`; use a virtualenv to avoid
27
+ > collisions.
28
+
29
+ Optional extras:
30
+
31
+ ```bash
32
+ pip install "treinta-previews[fastapi]" # widget proxy FastAPI adapter
33
+ pip install "treinta-previews[flask]" # widget proxy Flask adapter
34
+ ```
35
+
36
+ ## Authentication
37
+
38
+ The client resolves credentials from arguments or the environment:
39
+
40
+ | Setting | Argument | Env vars (in precedence order) | Default |
41
+ | -------- | ---------- | -------------------------------------------------------------------- | ------- |
42
+ | API key | `api_key` | `TREINTA_PREVIEWS_API_KEY`, `PROPIE_API_KEY`, `PREVIEWS_API_KEY` | — |
43
+ | Base URL | `base_url` | `TREINTA_PREVIEWS_API_URL`, `PROPIE_API_URL` | `https://previews.amapola.treinta.ai/api` |
44
+
45
+ Keys look like `pvk_<prefix>_<secret>` and are sent as `Authorization: Bearer pvk_...`.
46
+
47
+ ## Quick start
48
+
49
+ ```python
50
+ from previews import PreviewsClient
51
+
52
+ with PreviewsClient() as client: # api key from env
53
+ vm = client.vms.create(
54
+ repo_url="https://github.com/owner/repo",
55
+ stack="node20",
56
+ exposed_port=3000,
57
+ environment_variables={"NODE_ENV": "production"},
58
+ )
59
+ vm = client.vms.wait_until_running(vm.id)
60
+ print(vm.url)
61
+
62
+ result = client.vms.run(vm.id, "npm test")
63
+ print(result.exit_code, result.stdout)
64
+
65
+ for event in client.vms.logs(vm.id, follow=False):
66
+ print(event.message)
67
+
68
+ client.vms.destroy(vm.id)
69
+ ```
70
+
71
+ Deploy a local folder (zipped locally, honoring `.gitignore`, stripping secrets):
72
+
73
+ ```python
74
+ det = client.detect.folder("/path/to/app") # SSE stack detection
75
+ vm = client.vms.create_from_folder(
76
+ "/path/to/app",
77
+ stack=det.stack,
78
+ start_command=det.start_command,
79
+ exposed_port=det.exposed_port,
80
+ )
81
+ ```
82
+
83
+ ### Async
84
+
85
+ ```python
86
+ from previews import AsyncPreviewsClient
87
+
88
+ async with AsyncPreviewsClient() as client:
89
+ vms = await client.vms.list()
90
+ async for event in client.vms.logs(vms[0].id, follow=False):
91
+ print(event.message)
92
+ ```
93
+
94
+ ## API surface
95
+
96
+ `PreviewsClient(api_key=None, *, base_url=None, timeout=30.0, http_client=None)`
97
+ (and the identical `AsyncPreviewsClient` with `await`/async generators/`aclose()`).
98
+ Both are context managers and expose `request(method, path, *, json=None, ...)`
99
+ plus the resources below.
100
+
101
+ ### `client.vms`
102
+
103
+ | Method | REST |
104
+ | --- | --- |
105
+ | `create(**fields)` | `POST /vms` (json) |
106
+ | `create_from_folder(path, **meta)` / `create_from_zip(zip_bytes, **meta)` | `POST /vms` (multipart) |
107
+ | `list()` | `GET /vms` |
108
+ | `get(id)` | `GET /vms/:id` |
109
+ | `destroy(id)` | `DELETE /vms/:id` |
110
+ | `restart(id)` | `POST /vms/:id/restart` |
111
+ | `redeploy(id)` | `POST /vms/:id/redeploy` |
112
+ | `run(id, command, *, timeout=300.0)` | `POST /vms/:id/run` (buffered) |
113
+ | `logs(id, *, follow=True)` | `GET /vms/:id/logs` (SSE) → `Iterator[LogEvent]` |
114
+ | `get_env(id)` / `set_env(id, env)` | `GET`/`PUT /vms/:id/env` |
115
+ | `bandwidth(id)` | `GET /vms/:id/bandwidth` |
116
+ | `persist(id, slug)` / `unpersist(id)` / `rename_slug(id, slug)` | `/vms/:id/persist`, `/vms/:id/slug` |
117
+ | `slug_available(slug)` → `(bool, reason?)` | `GET /vms/slug-available` |
118
+ | `upload_files(id, files, *, base_dir="/app")` | `POST /vms/:id/files` |
119
+ | `mint_widget_token(id, *, capabilities=None, ttl_seconds=None)` | `POST /vms/:id/widget-token` |
120
+ | `wait_until_running(id, *, timeout=300.0, interval=2.0, on_status=None)` | polls `GET /vms/:id` |
121
+
122
+ `create` / `create_from_*` accept snake_case fields: `repo_url`, `stack`,
123
+ `branch`, `subdirectory`, `exposed_port`, `install_command`, `start_command`,
124
+ `environment_variables`, `drive_id`, `drive_mount_path`, `drive_read_only`,
125
+ `database_integration_id`, `vcpus`, `memory_mib`, `persistent`, `slug`.
126
+
127
+ ### `client.snapshots` / `.drives` / `.integrations` / `.detect` / `.accounts`
128
+
129
+ - `snapshots.list() / create(vm_id, name=None) / clone(snapshot_id, *, name=None, environment_variables=None) / delete(snapshot_id)`
130
+ - `drives.list() / create(name, size_gib, *, mount_path=None) / delete(id)`
131
+ - `integrations.list() / create(**fields) / delete(id)`
132
+ - `detect.public(repo_url, *, on_progress=None) / zip(zip_bytes, ...) / folder(path, ...)`
133
+ - `accounts.current()` → `CurrentPrincipal(account, project, auth_type, api_key)`
134
+
135
+ ### System status
136
+
137
+ Not a dedicated resource; use the generic request helper:
138
+
139
+ ```python
140
+ from previews import SystemStatus
141
+ status = SystemStatus.from_dict(client.request("GET", "/system/status"))
142
+ print(status.caches["npm"].size_bytes)
143
+ ```
144
+
145
+ ## Errors
146
+
147
+ Non-2xx responses raise `PreviewsApiError(status, code, message)` from the
148
+ `{ error: { code, message } }` envelope (a non-JSON body yields code `UNKNOWN`).
149
+ `wait_until_running` raises `PreviewsTimeoutError` on deadline. Missing
150
+ credentials raise `PreviewsConfigError`.
151
+
152
+ ## Models
153
+
154
+ Responses are frozen dataclasses with a tolerant `from_dict` (camelCase →
155
+ snake_case, unknown keys ignored): `Preview` (alias `VM`), `RunCommandResult`,
156
+ `VMSnapshot`, `DetectionResult`/`EnvVarHint`, `BandwidthSample`/`BandwidthResponse`,
157
+ `Account`/`Project`/`ApiKeyRef`/`CurrentPrincipal`, `DatabaseIntegration`,
158
+ `Drive`, `SystemStatus`/`CacheStat`, `WidgetToken`, `UploadResult`, `LogEvent`.
159
+ The package ships `py.typed`.
160
+
161
+ ## Widget backend proxy
162
+
163
+ The React widget UI is built separately. This SDK provides the backend piece.
164
+
165
+ **Scoped token (recommended):** mint a short-lived `pwt_` token server-side and
166
+ hand it to the browser, which then calls the platform directly.
167
+
168
+ ```python
169
+ token = client.vms.mint_widget_token(vm_id, capabilities=["preview:read", "vm:run"])
170
+ ```
171
+
172
+ **Backend proxy:** the browser calls *your* server; the proxy forwards only a
173
+ whitelisted subset of operations to the platform using the `pvk_` key it holds —
174
+ the key never reaches the browser.
175
+
176
+ ```python
177
+ from previews import PreviewsClient
178
+ from previews.proxy import PreviewsProxy, ProxyConfig, make_router # or make_blueprint
179
+
180
+ client = PreviewsClient()
181
+ proxy = PreviewsProxy(ProxyConfig(
182
+ client=client,
183
+ allowed_origins=["https://app.example.com"],
184
+ allowed_vm_ids={"<vm-uuid>"}, # None = any VM in the project
185
+ # allow_ops defaults to WIDGET_SAFE_OPS = {"status","run","files","logs","mint_token"}
186
+ ))
187
+
188
+ # FastAPI
189
+ from fastapi import FastAPI
190
+ app = FastAPI()
191
+ app.include_router(make_router(proxy, prefix="/previews"))
192
+
193
+ # Flask
194
+ # from flask import Flask
195
+ # app = Flask(__name__)
196
+ # app.register_blueprint(make_blueprint(proxy, url_prefix="/previews"))
197
+ ```
198
+
199
+ The proxy enforces the origin allowlist, the op whitelist (hard-capped to the
200
+ widget-safe set), and the per-VM restriction; it never echoes the `pvk_` key.
201
+
202
+ ## License
203
+
204
+ MIT
@@ -0,0 +1,80 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "treinta-previews"
7
+ dynamic = ["version"]
8
+ description = "Python SDK for the Propie / Previews VM-as-a-Service platform (Firecracker microVMs)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Treinta Previews", email = "carrizofg@gmail.com" }]
13
+ keywords = ["previews", "propie", "firecracker", "vm", "microvm", "sdk"]
14
+ dependencies = ["httpx>=0.27,<1"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Typing :: Typed",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://previews.amapola.treinta.ai"
31
+ Repository = "https://github.com/justfedec/previews"
32
+
33
+ [project.optional-dependencies]
34
+ fastapi = ["fastapi>=0.100"]
35
+ flask = ["flask>=2.2"]
36
+ dev = [
37
+ "pytest>=8",
38
+ "pytest-asyncio",
39
+ "anyio",
40
+ "mypy",
41
+ "ruff",
42
+ "build",
43
+ "twine",
44
+ ]
45
+
46
+ [tool.hatch.version]
47
+ path = "src/previews/_version.py"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["src/previews"]
51
+
52
+ [tool.hatch.build.targets.sdist]
53
+ include = [
54
+ "src",
55
+ "tests",
56
+ "scripts",
57
+ "README.md",
58
+ "LICENSE",
59
+ "CHANGELOG.md",
60
+ "pyproject.toml",
61
+ ]
62
+
63
+ [tool.pytest.ini_options]
64
+ asyncio_mode = "auto"
65
+ testpaths = ["tests"]
66
+
67
+ [tool.ruff]
68
+ line-length = 100
69
+ target-version = "py39"
70
+
71
+ # UP (pyupgrade) is intentionally not selected: the SDK targets Python 3.9, so
72
+ # it uses typing.List/Dict/Optional rather than PEP 585/604 builtins.
73
+ [tool.ruff.lint]
74
+ select = ["E", "F", "I", "B"]
75
+ ignore = ["E501"]
76
+
77
+ [tool.mypy]
78
+ python_version = "3.10"
79
+ warn_unused_ignores = true
80
+ ignore_missing_imports = true
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+ """End-to-end smoke test against a live Previews API.
3
+
4
+ Exercises the full happy path:
5
+
6
+ accounts.current -> detect -> create -> wait_until_running -> run
7
+ -> logs -> upload_files -> mint_widget_token -> destroy
8
+
9
+ Run it manually (it is NOT part of the offline pytest suite)::
10
+
11
+ export TREINTA_PREVIEWS_API_KEY=pvk_...
12
+ python scripts/smoke.py https://github.com/vercel/next.js
13
+
14
+ This module must import and parse cleanly even without an API key.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import sys
20
+
21
+ from previews import PreviewsClient
22
+
23
+
24
+ def main(repo_url: str) -> int:
25
+ with PreviewsClient() as client:
26
+ principal = client.accounts.current()
27
+ acct = principal.account.name if principal.account else "?"
28
+ print(f"[accounts] authenticated as account={acct} project={principal.project.slug if principal.project else '?'}")
29
+
30
+ print(f"[detect] {repo_url} ...")
31
+ detection = client.detect.public(repo_url, on_progress=lambda m: print(f" .. {m}"))
32
+ print(f"[detect] stack={detection.stack} start={detection.start_command!r} port={detection.exposed_port}")
33
+
34
+ print("[create] launching VM ...")
35
+ vm = client.vms.create(
36
+ repo_url=detection.repo_url or repo_url,
37
+ stack=detection.stack,
38
+ branch=detection.branch or "main",
39
+ exposed_port=detection.exposed_port or 3000,
40
+ install_command=detection.install_command,
41
+ start_command=detection.start_command,
42
+ )
43
+ print(f"[create] id={vm.id} status={vm.status}")
44
+
45
+ try:
46
+ print("[wait] polling until running ...")
47
+ vm = client.vms.wait_until_running(
48
+ vm.id, on_status=lambda v: print(f" .. status={v.status}")
49
+ )
50
+ print(f"[wait] running at {vm.url}")
51
+
52
+ result = client.vms.run(vm.id, "echo hello && uname -a")
53
+ print(f"[run] exit={result.exit_code} out={result.stdout.strip()!r}")
54
+
55
+ print("[logs] tailing (first buffered burst) ...")
56
+ for i, event in enumerate(client.vms.logs(vm.id, follow=False)):
57
+ print(f" [{event.event}] {event.message}")
58
+ if i > 20:
59
+ break
60
+
61
+ upload = client.vms.upload_files(
62
+ vm.id, {"smoke.txt": "written by scripts/smoke.py\n"}, base_dir="/app"
63
+ )
64
+ print(f"[files] wrote {upload.written} file(s): {upload.paths}")
65
+
66
+ token = client.vms.mint_widget_token(vm.id, capabilities=["preview:read"], ttl_seconds=300)
67
+ print(f"[widget] token={token.token[:12]}... expires={token.expires_at}")
68
+ finally:
69
+ destroyed = client.vms.destroy(vm.id)
70
+ print(f"[destroy] id={destroyed.id} status={destroyed.status}")
71
+
72
+ return 0
73
+
74
+
75
+ if __name__ == "__main__":
76
+ if len(sys.argv) < 2:
77
+ print("usage: python scripts/smoke.py <public-github-repo-url>", file=sys.stderr)
78
+ raise SystemExit(2)
79
+ raise SystemExit(main(sys.argv[1]))