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.
Files changed (36) hide show
  1. vertracloud_api-0.2.2/.release-please-manifest.json +1 -0
  2. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/CHANGELOG.md +7 -0
  3. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/PKG-INFO +18 -1
  4. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/README.md +17 -0
  5. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/pyproject.toml +1 -1
  6. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/__init__.py +6 -0
  7. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/client.py +1 -1
  8. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/errors.py +19 -0
  9. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/resources/apps.py +30 -1
  10. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/types.py +11 -1
  11. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/test_multipart_and_binary.py +36 -0
  12. vertracloud_api-0.2.1/.release-please-manifest.json +0 -1
  13. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/.github/workflows/ci.yml +0 -0
  14. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/.github/workflows/release.yml +0 -0
  15. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/.gitignore +0 -0
  16. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/LICENSE +0 -0
  17. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/release-please-config.json +0 -0
  18. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/py.typed +0 -0
  19. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/resources/__init__.py +0 -0
  20. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/resources/account.py +0 -0
  21. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/resources/billing.py +0 -0
  22. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/resources/blob.py +0 -0
  23. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/resources/databases.py +0 -0
  24. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/resources/snapshots.py +0 -0
  25. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/resources/workspaces.py +0 -0
  26. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/sse.py +0 -0
  27. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/src/vertracloud/transport.py +0 -0
  28. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/__init__.py +0 -0
  29. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/fake_transport.py +0 -0
  30. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/routes_matrix.py +0 -0
  31. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/test_blob.py +0 -0
  32. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/test_client.py +0 -0
  33. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/test_contract.py +0 -0
  34. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/test_routes_matrix.py +0 -0
  35. {vertracloud_api-0.2.1 → vertracloud_api-0.2.2}/tests/test_sse.py +0 -0
  36. {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.1
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.1"
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.1" # x-release-please-version
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