vertracloud-api 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vertracloud_api-0.2.2/.release-please-manifest.json +1 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/CHANGELOG.md +7 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/PKG-INFO +18 -1
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/README.md +17 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/pyproject.toml +1 -1
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/__init__.py +6 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/client.py +1 -1
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/errors.py +19 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/resources/apps.py +30 -1
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/types.py +11 -1
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/test_multipart_and_binary.py +36 -0
- vertracloud_api-0.2.1/.release-please-manifest.json +0 -1
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/.github/workflows/ci.yml +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/.github/workflows/release.yml +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/.gitignore +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/LICENSE +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/release-please-config.json +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/py.typed +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/resources/__init__.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/resources/account.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/resources/billing.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/resources/blob.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/resources/databases.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/resources/snapshots.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/resources/workspaces.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/sse.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/transport.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/__init__.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/fake_transport.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/routes_matrix.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/test_blob.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/test_client.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/test_contract.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/test_routes_matrix.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/test_sse.py +0 -0
- {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/uv.lock +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{".":"0.2.2"}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.2](https://github.com/vertracloud/sdk-api-py/compare/v0.2.1...v0.2.2) (2026-10-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* support async app creation (building status, wait_until_ready) ([044a73f](https://github.com/vertracloud/sdk-api-py/commit/044a73f981f540e1e070d52c0243469448243e46))
|
|
9
|
+
|
|
3
10
|
## [0.2.1](https://github.com/vertracloud/sdk-api-py/compare/v0.2.0...v0.2.1) (2026-10-15)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vertracloud-api
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Official Python SDK for the public API of Vertra Cloud, a Brazilian application hosting platform (PaaS).
|
|
5
5
|
Project-URL: Homepage, https://vertracloud.app
|
|
6
6
|
Project-URL: Documentation, https://docs.vertracloud.app/sdks
|
|
@@ -142,6 +142,23 @@ with open("backup.zip", "wb") as f:
|
|
|
142
142
|
|
|
143
143
|
`client.snapshots.download` works the same way.
|
|
144
144
|
|
|
145
|
+
### Waiting for the first build
|
|
146
|
+
|
|
147
|
+
`apps.create` replies right away with `status: "building"`. Wait for the build to end:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
app = client.apps.create({"name": "my-app", "memory": 512}, file=data)
|
|
151
|
+
try:
|
|
152
|
+
app = client.apps.wait_until_ready(app["id"], timeout=600, interval=2) # status is "up"
|
|
153
|
+
# ended "down": e.code is app["last_build_error"] (e.g. "INSTALL_OUT_OF_MEMORY"), "APP_DOWN" if none
|
|
154
|
+
except vertracloud.AppBuildFailedError as e:
|
|
155
|
+
print(client.apps.logs(app["id"]))
|
|
156
|
+
except vertracloud.WaitTimeoutError: # still building after `timeout` seconds
|
|
157
|
+
...
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`missing_dependencies` on the app is filled only after the build ends. Before it ends, calls that need a stopped or running app can fail with `APP_BUILDING` (409).
|
|
161
|
+
|
|
145
162
|
### Realtime logs (SSE)
|
|
146
163
|
|
|
147
164
|
```python
|
|
@@ -123,6 +123,23 @@ with open("backup.zip", "wb") as f:
|
|
|
123
123
|
|
|
124
124
|
`client.snapshots.download` works the same way.
|
|
125
125
|
|
|
126
|
+
### Waiting for the first build
|
|
127
|
+
|
|
128
|
+
`apps.create` replies right away with `status: "building"`. Wait for the build to end:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
app = client.apps.create({"name": "my-app", "memory": 512}, file=data)
|
|
132
|
+
try:
|
|
133
|
+
app = client.apps.wait_until_ready(app["id"], timeout=600, interval=2) # status is "up"
|
|
134
|
+
# ended "down": e.code is app["last_build_error"] (e.g. "INSTALL_OUT_OF_MEMORY"), "APP_DOWN" if none
|
|
135
|
+
except vertracloud.AppBuildFailedError as e:
|
|
136
|
+
print(client.apps.logs(app["id"]))
|
|
137
|
+
except vertracloud.WaitTimeoutError: # still building after `timeout` seconds
|
|
138
|
+
...
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`missing_dependencies` on the app is filled only after the build ends. Before it ends, calls that need a stopped or running app can fail with `APP_BUILDING` (409).
|
|
142
|
+
|
|
126
143
|
### Realtime logs (SSE)
|
|
127
144
|
|
|
128
145
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vertracloud-api"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Official Python SDK for the public API of Vertra Cloud, a Brazilian application hosting platform (PaaS)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from .client import VertraClient, __version__
|
|
4
4
|
from .errors import (
|
|
5
|
+
AppBuildFailedError,
|
|
6
|
+
AppNotReadyError,
|
|
5
7
|
AuthenticationError,
|
|
6
8
|
NotFoundError,
|
|
7
9
|
RateLimitError,
|
|
8
10
|
ScopeDeniedError,
|
|
9
11
|
ValidationError,
|
|
10
12
|
VertraAPIError,
|
|
13
|
+
WaitTimeoutError,
|
|
11
14
|
)
|
|
12
15
|
from .sse import SseEvent, SseStream
|
|
13
16
|
from .transport import Response, Transport, UrllibTransport
|
|
@@ -16,6 +19,9 @@ __all__ = [
|
|
|
16
19
|
"VertraClient",
|
|
17
20
|
"__version__",
|
|
18
21
|
"VertraAPIError",
|
|
22
|
+
"AppNotReadyError",
|
|
23
|
+
"AppBuildFailedError",
|
|
24
|
+
"WaitTimeoutError",
|
|
19
25
|
"AuthenticationError",
|
|
20
26
|
"ScopeDeniedError",
|
|
21
27
|
"NotFoundError",
|
|
@@ -16,7 +16,7 @@ from .sse import SseStream
|
|
|
16
16
|
from .transport import Response, Transport, UrllibTransport
|
|
17
17
|
from .types import JsonValue
|
|
18
18
|
|
|
19
|
-
__version__ = "0.2.
|
|
19
|
+
__version__ = "0.2.2" # x-release-please-version
|
|
20
20
|
|
|
21
21
|
DEFAULT_BASE_URL = "https://api.vertracloud.app"
|
|
22
22
|
DEFAULT_BLOB_BASE_URL = "https://blob.vertracloud.app/v1"
|
|
@@ -70,6 +70,25 @@ class RateLimitError(VertraAPIError):
|
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
class AppNotReadyError(Exception):
|
|
74
|
+
"""`apps.wait_until_ready` ended without a running app. `app` is the last payload seen
|
|
75
|
+
(`None` on timeout before any reply). The cause of a `down` build is in `apps.logs`."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, message: str, *, code: str, app: Any = None) -> None:
|
|
78
|
+
self.code = code
|
|
79
|
+
self.app = app
|
|
80
|
+
super().__init__(f"{code}: {message}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AppBuildFailedError(AppNotReadyError):
|
|
84
|
+
"""The build ended with the app `down`. `code` is the app's `last_build_error` (e.g.
|
|
85
|
+
`INSTALL_OUT_OF_MEMORY`), or `APP_DOWN` when the API sent none."""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class WaitTimeoutError(AppNotReadyError):
|
|
89
|
+
"""`wait_until_ready` timed out while the app was still `building` (`code` is `WAIT_TIMEOUT`)."""
|
|
90
|
+
|
|
91
|
+
|
|
73
92
|
def error_from_response(status: int, body: dict[str, Any]) -> VertraAPIError:
|
|
74
93
|
"""Picks the class from the HTTP status; `code` comes from the error response body."""
|
|
75
94
|
code = str(body.get("code") or "UNKNOWN_ERROR")
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import time
|
|
8
9
|
from typing import TYPE_CHECKING, Any
|
|
9
10
|
|
|
10
11
|
from ..client import LARGE_PAYLOAD_TIMEOUT, MultipartField, encode_path_param
|
|
12
|
+
from ..errors import AppBuildFailedError, WaitTimeoutError
|
|
11
13
|
from ..sse import SseStream
|
|
12
14
|
from ..types import (
|
|
13
15
|
APIAppFileUploadResponse,
|
|
@@ -442,7 +444,8 @@ class AppsResource:
|
|
|
442
444
|
) -> APIApplication:
|
|
443
445
|
"""POST /v1/apps — scope `apps:write`. Multipart: field `file` (zip) OR `snapshot_id`
|
|
444
446
|
in `body`, other fields as text parts. Rate limit 15/min, plus the plan's
|
|
445
|
-
deploys-per-hour cap.
|
|
447
|
+
deploys-per-hour cap. Replies 202 with `status: "building"`: the build continues in the
|
|
448
|
+
background — use `wait_until_ready`, `logs` or `realtime` to follow it.
|
|
446
449
|
Doc: https://docs.vertracloud.app/api-reference/endpoint/apps/create
|
|
447
450
|
"""
|
|
448
451
|
fields: list[MultipartField] = []
|
|
@@ -463,6 +466,32 @@ class AppsResource:
|
|
|
463
466
|
"POST", "/v1/apps", fields=fields, timeout=timeout if timeout is not None else LARGE_PAYLOAD_TIMEOUT
|
|
464
467
|
)
|
|
465
468
|
|
|
469
|
+
def wait_until_ready(
|
|
470
|
+
self,
|
|
471
|
+
app_id: str,
|
|
472
|
+
*,
|
|
473
|
+
timeout: float = 600.0,
|
|
474
|
+
interval: float = 2.0,
|
|
475
|
+
workspace_id: str | None = None,
|
|
476
|
+
) -> APIApplication:
|
|
477
|
+
"""Polls `GET /v1/apps/:id` while `status` is `building`. Returns the app when it is `up`;
|
|
478
|
+
raises `AppBuildFailedError` when it ends `down` and `WaitTimeoutError` after `timeout`
|
|
479
|
+
seconds. API errors from the polling propagate unchanged."""
|
|
480
|
+
deadline = time.monotonic() + timeout
|
|
481
|
+
while True:
|
|
482
|
+
app = self.get(app_id, workspace_id=workspace_id)
|
|
483
|
+
if app["status"] == "up":
|
|
484
|
+
return app
|
|
485
|
+
if app["status"] == "down":
|
|
486
|
+
raise AppBuildFailedError(
|
|
487
|
+
"the build ended and the app is down; read apps.logs for the cause",
|
|
488
|
+
code=app.get("last_build_error") or "APP_DOWN",
|
|
489
|
+
app=app,
|
|
490
|
+
)
|
|
491
|
+
if time.monotonic() + interval > deadline:
|
|
492
|
+
raise WaitTimeoutError(f"still building after {timeout:g}s", code="WAIT_TIMEOUT", app=app)
|
|
493
|
+
time.sleep(interval)
|
|
494
|
+
|
|
466
495
|
def start(self, app_id: str, *, workspace_id: str | None = None) -> APIApplicationOperationResponse:
|
|
467
496
|
"""POST /v1/apps/:id/start — scope `apps:write`."""
|
|
468
497
|
return self._c.request_json(
|
|
@@ -249,7 +249,16 @@ class PutBillingDetailsBody(_PutBillingDetailsRequired, total=False):
|
|
|
249
249
|
ApplicationLanguage = Literal[
|
|
250
250
|
"javascript", "typescript", "bun", "python", "static", "php", "go", "ruby", "java", "rust"
|
|
251
251
|
]
|
|
252
|
-
ApplicationStatus = Literal["up", "down"]
|
|
252
|
+
ApplicationStatus = Literal["up", "down", "building"]
|
|
253
|
+
ApplicationBuildError = Literal[
|
|
254
|
+
"BUILD_FAILED",
|
|
255
|
+
"BUILD_TIMEOUT",
|
|
256
|
+
"BUILD_INTERRUPTED",
|
|
257
|
+
"INSTALL_FAILED",
|
|
258
|
+
"INSTALL_OUT_OF_MEMORY",
|
|
259
|
+
"INSTALL_TIMEOUT",
|
|
260
|
+
"INSTALL_SHIELD_BLOCKED",
|
|
261
|
+
]
|
|
253
262
|
ApplicationFileType = Literal["file", "directory"]
|
|
254
263
|
DatabaseStatus = Literal["up", "down"]
|
|
255
264
|
ResourceType = Literal[1, 2]
|
|
@@ -298,6 +307,7 @@ class _APIApplicationRequired(TypedDict):
|
|
|
298
307
|
offline_since: str | None
|
|
299
308
|
shield_cooldown: APIApplicationShieldCooldown | None
|
|
300
309
|
deploy: APIApplicationDeploySettings
|
|
310
|
+
last_build_error: ApplicationBuildError | None
|
|
301
311
|
|
|
302
312
|
|
|
303
313
|
class APIApplication(_APIApplicationRequired, total=False):
|
|
@@ -118,3 +118,39 @@ def test_snapshot_download_returns_bytes() -> None:
|
|
|
118
118
|
transport.queue_binary(b"zip-content")
|
|
119
119
|
client = VertraClient(api_key="k", transport=transport)
|
|
120
120
|
assert client.snapshots.download("user-1", "snap-1", "applications") == b"zip-content"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _app(status: str) -> dict:
|
|
124
|
+
return {"response": {"id": "app-1", "status": status}}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_wait_until_ready_returns_app_when_up() -> None:
|
|
128
|
+
from vertracloud import VertraClient
|
|
129
|
+
|
|
130
|
+
transport = FakeTransport()
|
|
131
|
+
transport.queue_json(_app("building"))
|
|
132
|
+
transport.queue_json(_app("up"))
|
|
133
|
+
client = VertraClient(api_key="k", transport=transport)
|
|
134
|
+
assert client.apps.wait_until_ready("app-1", interval=0)["status"] == "up"
|
|
135
|
+
assert len(transport.calls) == 2
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_wait_until_ready_raises_on_down_and_timeout() -> None:
|
|
139
|
+
from vertracloud import AppBuildFailedError, VertraClient, WaitTimeoutError
|
|
140
|
+
|
|
141
|
+
transport = FakeTransport()
|
|
142
|
+
transport.queue_json(_app("down"))
|
|
143
|
+
client = VertraClient(api_key="k", transport=transport)
|
|
144
|
+
with pytest.raises(AppBuildFailedError) as down:
|
|
145
|
+
client.apps.wait_until_ready("app-1", interval=0)
|
|
146
|
+
assert down.value.code == "APP_DOWN"
|
|
147
|
+
|
|
148
|
+
transport.queue_json({"response": {"id": "app-1", "status": "down", "last_build_error": "INSTALL_OUT_OF_MEMORY"}})
|
|
149
|
+
with pytest.raises(AppBuildFailedError) as oom:
|
|
150
|
+
client.apps.wait_until_ready("app-1", interval=0)
|
|
151
|
+
assert oom.value.code == "INSTALL_OUT_OF_MEMORY"
|
|
152
|
+
|
|
153
|
+
transport.default_json = _app("building")
|
|
154
|
+
with pytest.raises(WaitTimeoutError) as slow:
|
|
155
|
+
client.apps.wait_until_ready("app-1", timeout=0, interval=0)
|
|
156
|
+
assert slow.value.code == "WAIT_TIMEOUT"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{".":"0.2.1"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|