vertracloud-api 0.2.0__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.0 → vertracloud_api-0.2.2}/CHANGELOG.md +14 -0
  3. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/PKG-INFO +18 -1
  4. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/README.md +17 -0
  5. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/pyproject.toml +1 -1
  6. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/src/vertracloud/__init__.py +6 -0
  7. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/src/vertracloud/client.py +1 -1
  8. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/src/vertracloud/errors.py +19 -0
  9. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/src/vertracloud/resources/apps.py +30 -1
  10. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/src/vertracloud/resources/blob.py +8 -1
  11. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/src/vertracloud/types.py +11 -1
  12. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/tests/test_blob.py +10 -0
  13. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/tests/test_multipart_and_binary.py +36 -0
  14. vertracloud_api-0.2.0/.release-please-manifest.json +0 -1
  15. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/.github/workflows/ci.yml +0 -0
  16. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/.github/workflows/release.yml +0 -0
  17. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/.gitignore +0 -0
  18. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/LICENSE +0 -0
  19. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/release-please-config.json +0 -0
  20. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/src/vertracloud/py.typed +0 -0
  21. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/src/vertracloud/resources/__init__.py +0 -0
  22. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/src/vertracloud/resources/account.py +0 -0
  23. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/src/vertracloud/resources/billing.py +0 -0
  24. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/src/vertracloud/resources/databases.py +0 -0
  25. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/src/vertracloud/resources/snapshots.py +0 -0
  26. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/src/vertracloud/resources/workspaces.py +0 -0
  27. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/src/vertracloud/sse.py +0 -0
  28. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/src/vertracloud/transport.py +0 -0
  29. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/tests/__init__.py +0 -0
  30. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/tests/fake_transport.py +0 -0
  31. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/tests/routes_matrix.py +0 -0
  32. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/tests/test_client.py +0 -0
  33. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/tests/test_contract.py +0 -0
  34. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/tests/test_routes_matrix.py +0 -0
  35. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/tests/test_sse.py +0 -0
  36. {vertracloud_api-0.2.0 → vertracloud_api-0.2.2}/uv.lock +0 -0
@@ -0,0 +1 @@
1
+ {".":"0.2.2"}
@@ -1,5 +1,19 @@
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
+
10
+ ## [0.2.1](https://github.com/vertracloud/sdk-api-py/compare/v0.2.0...v0.2.1) (2026-10-15)
11
+
12
+
13
+ ### Features
14
+
15
+ * **blob:** add rename_on_conflict to put and copy ([6773c4c](https://github.com/vertracloud/sdk-api-py/commit/6773c4cfa63032875dfe1b3f92967fe3128b4e7c))
16
+
3
17
  ## [0.2.0](https://github.com/vertracloud/sdk-api-py/compare/v0.1.5...v0.2.0) (2026-10-15)
4
18
 
5
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vertracloud-api
3
- Version: 0.2.0
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.0"
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.0" # 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(
@@ -240,6 +240,7 @@ class BlobResource:
240
240
  visibility: BlobVisibility | None = None,
241
241
  overwrite: bool | None = None,
242
242
  random_suffix: bool | None = None,
243
+ rename_on_conflict: bool | None = None,
243
244
  cache_control: str | None = None,
244
245
  content_disposition: str | None = None,
245
246
  filename: str | None = None,
@@ -253,11 +254,14 @@ class BlobResource:
253
254
  Up to 5 MB: `POST /objects` (multipart). Larger: `POST /uploads`, then a presigned
254
255
  PUT (single) or parts (multipart) and `/complete`. `on_progress(loaded, total)` is
255
256
  called as bytes are sent. If a large upload is interrupted, call `resume`.
257
+ `rename_on_conflict=True` saves a taken path as `name-1.ext`, `name-2.ext`, … (not with `overwrite`);
258
+ the returned object has the final path.
256
259
  """
257
260
  opts: dict[str, Any] = {
258
261
  "visibility": visibility,
259
262
  "overwrite": overwrite,
260
263
  "random_suffix": random_suffix,
264
+ "rename_on_conflict": rename_on_conflict,
261
265
  "cache_control": cache_control,
262
266
  "content_disposition": content_disposition,
263
267
  "filename": filename,
@@ -428,7 +432,10 @@ class BlobResource:
428
432
  return results
429
433
 
430
434
  def copy(self, id: str, path: str, *, workspace_id: str | None = None, **opts: Any) -> APIBlobObject:
431
- """POST /objects/copy — `overwrite`, `random_suffix`, `visibility`, `move` as options."""
435
+ """POST /objects/copy — `overwrite`, `random_suffix`, `visibility`, `move` as options.
436
+
437
+ `rename_on_conflict=True` (copy only) saves a taken path, even the source's own, as `name-1.ext`.
438
+ """
432
439
  return self._json(
433
440
  "POST", "/objects/copy", json_body={"source": id, "path": path, **opts}, query=_ws(workspace_id)
434
441
  )
@@ -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):
@@ -74,6 +74,16 @@ def test_put_small_is_multipart_on_objects() -> None:
74
74
  assert len(t.calls) == 1
75
75
 
76
76
 
77
+ def test_put_and_copy_send_rename_on_conflict() -> None:
78
+ client, t = _setup()
79
+ t.queue_json({"response": OBJ})
80
+ t.queue_json({"response": OBJ})
81
+ client.blob.put("a/b.txt", b"hi", rename_on_conflict=True)
82
+ assert _q(t.calls[0])["rename_on_conflict"] == ["true"]
83
+ client.blob.copy("o1", "a/b.txt", rename_on_conflict=True)
84
+ assert json.loads(t.calls[1].body or b"{}") == {"source": "o1", "path": "a/b.txt", "rename_on_conflict": True}
85
+
86
+
77
87
  def test_put_from_path(tmp_path: Path) -> None:
78
88
  f = tmp_path / "x.txt"
79
89
  f.write_bytes(b"from-disk")
@@ -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.0"}
File without changes
File without changes