vertracloud-api 0.3.2__tar.gz → 0.3.3__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 (25) hide show
  1. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/CHANGELOG.md +8 -0
  2. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/PKG-INFO +54 -4
  3. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/README.md +53 -3
  4. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/pyproject.toml +1 -1
  5. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/src/vertracloud/__init__.py +4 -0
  6. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/src/vertracloud/client.py +5 -1
  7. vertracloud_api-0.3.3/src/vertracloud/errcodes.py +65 -0
  8. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/src/vertracloud/errors.py +35 -0
  9. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/src/vertracloud/resources/apps.py +34 -9
  10. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/src/vertracloud/resources/databases.py +8 -3
  11. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/src/vertracloud/resources/images.py +4 -0
  12. vertracloud_api-0.3.3/src/vertracloud/resources/stacks.py +324 -0
  13. vertracloud_api-0.3.3/src/vertracloud/resources/templates.py +542 -0
  14. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/src/vertracloud/resources/workspaces.py +53 -2
  15. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/src/vertracloud/types.py +1135 -8
  16. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/.gitignore +0 -0
  17. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/LICENSE +0 -0
  18. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/src/vertracloud/py.typed +0 -0
  19. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/src/vertracloud/resources/__init__.py +0 -0
  20. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/src/vertracloud/resources/account.py +0 -0
  21. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/src/vertracloud/resources/billing.py +0 -0
  22. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/src/vertracloud/resources/blob.py +0 -0
  23. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/src/vertracloud/resources/snapshots.py +0 -0
  24. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/src/vertracloud/sse.py +0 -0
  25. {vertracloud_api-0.3.2 → vertracloud_api-0.3.3}/src/vertracloud/transport.py +0 -0
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.3](https://github.com/vertracloud/sdk-api-py/compare/v0.3.2...v0.3.3) (2026-10-30)
4
+
5
+
6
+ ### Features
7
+
8
+ * add locked_deploy_settings to applications ([6294bc1](https://github.com/vertracloud/sdk-api-py/commit/6294bc11ae5e1471ef68c997dbf875c8efdd4a69))
9
+ * add templates and stacks resources ([f638dde](https://github.com/vertracloud/sdk-api-py/commit/f638dde5c4df10070f5a0485fa5e3f1fafae02d1))
10
+
3
11
  ## [0.3.2](https://github.com/vertracloud/sdk-api-py/compare/v0.3.1...v0.3.2) (2026-10-25)
4
12
 
5
13
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vertracloud-api
3
- Version: 0.3.2
3
+ Version: 0.3.3
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
@@ -22,7 +22,7 @@ Description-Content-Type: text/markdown
22
22
  [![PyPI](https://img.shields.io/pypi/v/vertracloud-api.svg)](https://pypi.org/project/vertracloud-api/)
23
23
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
24
24
 
25
- Official Python SDK for the [Vertra Cloud](https://vertracloud.app) public API: apps, databases, snapshots, account, workspaces and billing — one typed method per route.
25
+ Official Python SDK for the [Vertra Cloud](https://vertracloud.app) public API: apps, databases, templates, stacks, snapshots, account, workspaces and billing — one typed method per route.
26
26
 
27
27
  - Python 3.10+, fully typed (`py.typed`), with `TypedDict` payloads.
28
28
  - Zero dependencies outside the standard library.
@@ -76,7 +76,7 @@ client = VertraClient(
76
76
 
77
77
  ### Per-call options
78
78
 
79
- Methods on apps, databases and images take a keyword-only `workspace_id`; downloads, uploads and streams also take `timeout`:
79
+ Methods on apps, databases, images, templates and stacks take a keyword-only `workspace_id`; downloads, uploads and streams also take `timeout`:
80
80
 
81
81
  ```python
82
82
  # Act on a resource that belongs to a workspace
@@ -264,6 +264,52 @@ client.images.update(image["id"], {"description": None})
264
264
  client.images.delete(image["id"], confirm_name="meu-projeto") # {"deleted_apps": [...]}
265
265
  ```
266
266
 
267
+ ### Templates
268
+
269
+ A template describes what to create: one application, or a stack with applications, databases and a Blob folder. `deploy` returns a job right away; follow it with `deploys.events` (Server-Sent Events) or read `deploys.get`. A job that fails is undone as a whole.
270
+
271
+ ```python
272
+ page = client.templates.list(category="bots", sort="popular")
273
+
274
+ job = client.templates.deploy(
275
+ "n8n",
276
+ {"name": "my-n8n", "inputs": {"admin_email": "me@example.com"}},
277
+ idempotency_key="deploy-my-n8n-1", # repeating the call returns the same job
278
+ )
279
+
280
+ events = client.templates.deploys.events(job["id"])
281
+ for event in events: # heartbeats are skipped; the loop ends when the API sends `end`
282
+ print(event["status"], event["current_step"])
283
+ job = client.templates.deploys.get(job["id"]) # outputs are read here, never on the stream
284
+ ```
285
+
286
+ `idempotency_key` is sent as the `Idempotency-Key` header and never goes in the body: the same key with the same request returns the same job, the same key with another request fails with `IDEMPOTENCY_KEY_CONFLICT`. Outputs marked `once` come back on the first read of a finished job and are empty afterwards.
287
+
288
+ Private templates: `create` and `new_version` take the manifest and one ZIP per service that runs code (`sources={"web": zip_bytes}`); `from_app` and `from_stack` save what you already run (read `app_draft` / `stack_draft` first to review the variables). `share_links` creates, lists and revokes links; `share` previews, uses, copies or reports what a link gives. `updates` reads and runs the update of an app or stack that came from a template, and detaches it. `validate` checks a manifest without creating anything; the findings of a refused one are in `err.manifest_issues`.
289
+
290
+ An application that came from an open template stops receiving versions when its code is changed. The calls that change code (`apps.files.write`, `move`, `delete`, `upload` and `apps.deploys.webhook.create`) fail with `TEMPLATE_DETACH_CONFIRMATION_REQUIRED` until you pass `confirm_detach=True` (in the body of `move` and `webhook.create`); `err.detach_action` tells which action asked for it.
291
+
292
+ ### Stacks
293
+
294
+ A stack groups applications, databases and Blob folders, and keeps the connection variables of each application up to date. Changes that alter variables or certificates return `restart_required` and `apps_to_restart`: an application reads them only when it starts, so the restart is yours to ask for. The same notice comes back from `databases.credentials.certificate.reset` and `password.reset`.
295
+
296
+ ```python
297
+ change = client.stacks.create(
298
+ {
299
+ "name": "shop",
300
+ "members": [
301
+ {"resource_type": "application", "resource_id": app_id},
302
+ {"resource_type": "database", "resource_id": db_id},
303
+ ],
304
+ "bindings": [{"app_id": app_id, "database_id": db_id}],
305
+ }
306
+ )
307
+ if change["restart_required"]:
308
+ print("restart:", change["apps_to_restart"])
309
+ ```
310
+
311
+ `delete` needs `confirm_name` equal to the stack name; mode `dissolve` removes only the stack, `delete_all` deletes the members too. `members`, `bindings` and `snapshots` group the routes of members, connections and whole-stack snapshots; `connection` returns code to connect an app to what it is linked to. Move a stack or a template to a workspace with `workspaces.stacks` and `workspaces.templates`. The error codes of both areas are constants in `vertracloud.errcodes`.
312
+
267
313
  ### Database versions
268
314
 
269
315
  ```python
@@ -291,6 +337,8 @@ except VertraAPIError as err:
291
337
  print(err.status, err.code, err)
292
338
  ```
293
339
 
340
+ A few errors carry a structured `details`; tolerant properties read them and give an empty value for any other error: `err.issues` (`DOCKERFILE_REFUSED`), `err.manifest_issues` (`TEMPLATE_MANIFEST_INVALID`), `err.secret_files` (`TEMPLATE_SECRET_IN_SOURCE`), `err.update_blocked` (`TEMPLATE_UPDATE_BLOCKED`) and `err.detach_action` (`TEMPLATE_DETACH_CONFIRMATION_REQUIRED`).
341
+
294
342
  Other subclasses: `AuthenticationError` (401), `ScopeDeniedError` (403 — including a missing key scope) and `ValidationError` (400/422). The error codes are listed in the [API reference](https://docs.vertracloud.app/api-reference/introduction).
295
343
 
296
344
  ### Testing your code
@@ -308,9 +356,11 @@ client = VertraClient(api_key="test", transport=my_fake_transport)
308
356
  | Apps | `client.apps` (+ `.deploys`, `.releases`, `.domains`, `.network`, `.envs`, `.files`) | 43 |
309
357
  | Databases | `client.databases` (+ `.credentials`) | 14 |
310
358
  | Custom images | `client.images` | 12 |
359
+ | Templates | `client.templates` (+ `.deploys`, `.share_links`, `.share`, `.updates`) | 32 |
360
+ | Stacks | `client.stacks` (+ `.members`, `.bindings`, `.snapshots`) | 20 |
311
361
  | Snapshots | `client.snapshots` | 5 |
312
362
  | Account | `client.account` (+ `.sessions`, `.folders`, `.favorites`) | 10 |
313
- | Workspaces | `client.workspaces` (+ `.members`, `.roles`, `.invites`, `.action_requests`, `.apps`, `.databases`, `.folders`, `.favorites`) | 30 |
363
+ | Workspaces | `client.workspaces` (+ `.members`, `.roles`, `.invites`, `.action_requests`, `.apps`, `.databases`, `.templates`, `.stacks`, `.folders`, `.favorites`) | 34 |
314
364
  | Billing | `client.billing` (+ `.orders`) | 7 |
315
365
  | Blob Storage | `client.blob` (+ `.shares`, `.rules`, `.domains`) | own host |
316
366
 
@@ -3,7 +3,7 @@
3
3
  [![PyPI](https://img.shields.io/pypi/v/vertracloud-api.svg)](https://pypi.org/project/vertracloud-api/)
4
4
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
5
 
6
- Official Python SDK for the [Vertra Cloud](https://vertracloud.app) public API: apps, databases, snapshots, account, workspaces and billing — one typed method per route.
6
+ Official Python SDK for the [Vertra Cloud](https://vertracloud.app) public API: apps, databases, templates, stacks, snapshots, account, workspaces and billing — one typed method per route.
7
7
 
8
8
  - Python 3.10+, fully typed (`py.typed`), with `TypedDict` payloads.
9
9
  - Zero dependencies outside the standard library.
@@ -57,7 +57,7 @@ client = VertraClient(
57
57
 
58
58
  ### Per-call options
59
59
 
60
- Methods on apps, databases and images take a keyword-only `workspace_id`; downloads, uploads and streams also take `timeout`:
60
+ Methods on apps, databases, images, templates and stacks take a keyword-only `workspace_id`; downloads, uploads and streams also take `timeout`:
61
61
 
62
62
  ```python
63
63
  # Act on a resource that belongs to a workspace
@@ -245,6 +245,52 @@ client.images.update(image["id"], {"description": None})
245
245
  client.images.delete(image["id"], confirm_name="meu-projeto") # {"deleted_apps": [...]}
246
246
  ```
247
247
 
248
+ ### Templates
249
+
250
+ A template describes what to create: one application, or a stack with applications, databases and a Blob folder. `deploy` returns a job right away; follow it with `deploys.events` (Server-Sent Events) or read `deploys.get`. A job that fails is undone as a whole.
251
+
252
+ ```python
253
+ page = client.templates.list(category="bots", sort="popular")
254
+
255
+ job = client.templates.deploy(
256
+ "n8n",
257
+ {"name": "my-n8n", "inputs": {"admin_email": "me@example.com"}},
258
+ idempotency_key="deploy-my-n8n-1", # repeating the call returns the same job
259
+ )
260
+
261
+ events = client.templates.deploys.events(job["id"])
262
+ for event in events: # heartbeats are skipped; the loop ends when the API sends `end`
263
+ print(event["status"], event["current_step"])
264
+ job = client.templates.deploys.get(job["id"]) # outputs are read here, never on the stream
265
+ ```
266
+
267
+ `idempotency_key` is sent as the `Idempotency-Key` header and never goes in the body: the same key with the same request returns the same job, the same key with another request fails with `IDEMPOTENCY_KEY_CONFLICT`. Outputs marked `once` come back on the first read of a finished job and are empty afterwards.
268
+
269
+ Private templates: `create` and `new_version` take the manifest and one ZIP per service that runs code (`sources={"web": zip_bytes}`); `from_app` and `from_stack` save what you already run (read `app_draft` / `stack_draft` first to review the variables). `share_links` creates, lists and revokes links; `share` previews, uses, copies or reports what a link gives. `updates` reads and runs the update of an app or stack that came from a template, and detaches it. `validate` checks a manifest without creating anything; the findings of a refused one are in `err.manifest_issues`.
270
+
271
+ An application that came from an open template stops receiving versions when its code is changed. The calls that change code (`apps.files.write`, `move`, `delete`, `upload` and `apps.deploys.webhook.create`) fail with `TEMPLATE_DETACH_CONFIRMATION_REQUIRED` until you pass `confirm_detach=True` (in the body of `move` and `webhook.create`); `err.detach_action` tells which action asked for it.
272
+
273
+ ### Stacks
274
+
275
+ A stack groups applications, databases and Blob folders, and keeps the connection variables of each application up to date. Changes that alter variables or certificates return `restart_required` and `apps_to_restart`: an application reads them only when it starts, so the restart is yours to ask for. The same notice comes back from `databases.credentials.certificate.reset` and `password.reset`.
276
+
277
+ ```python
278
+ change = client.stacks.create(
279
+ {
280
+ "name": "shop",
281
+ "members": [
282
+ {"resource_type": "application", "resource_id": app_id},
283
+ {"resource_type": "database", "resource_id": db_id},
284
+ ],
285
+ "bindings": [{"app_id": app_id, "database_id": db_id}],
286
+ }
287
+ )
288
+ if change["restart_required"]:
289
+ print("restart:", change["apps_to_restart"])
290
+ ```
291
+
292
+ `delete` needs `confirm_name` equal to the stack name; mode `dissolve` removes only the stack, `delete_all` deletes the members too. `members`, `bindings` and `snapshots` group the routes of members, connections and whole-stack snapshots; `connection` returns code to connect an app to what it is linked to. Move a stack or a template to a workspace with `workspaces.stacks` and `workspaces.templates`. The error codes of both areas are constants in `vertracloud.errcodes`.
293
+
248
294
  ### Database versions
249
295
 
250
296
  ```python
@@ -272,6 +318,8 @@ except VertraAPIError as err:
272
318
  print(err.status, err.code, err)
273
319
  ```
274
320
 
321
+ A few errors carry a structured `details`; tolerant properties read them and give an empty value for any other error: `err.issues` (`DOCKERFILE_REFUSED`), `err.manifest_issues` (`TEMPLATE_MANIFEST_INVALID`), `err.secret_files` (`TEMPLATE_SECRET_IN_SOURCE`), `err.update_blocked` (`TEMPLATE_UPDATE_BLOCKED`) and `err.detach_action` (`TEMPLATE_DETACH_CONFIRMATION_REQUIRED`).
322
+
275
323
  Other subclasses: `AuthenticationError` (401), `ScopeDeniedError` (403 — including a missing key scope) and `ValidationError` (400/422). The error codes are listed in the [API reference](https://docs.vertracloud.app/api-reference/introduction).
276
324
 
277
325
  ### Testing your code
@@ -289,9 +337,11 @@ client = VertraClient(api_key="test", transport=my_fake_transport)
289
337
  | Apps | `client.apps` (+ `.deploys`, `.releases`, `.domains`, `.network`, `.envs`, `.files`) | 43 |
290
338
  | Databases | `client.databases` (+ `.credentials`) | 14 |
291
339
  | Custom images | `client.images` | 12 |
340
+ | Templates | `client.templates` (+ `.deploys`, `.share_links`, `.share`, `.updates`) | 32 |
341
+ | Stacks | `client.stacks` (+ `.members`, `.bindings`, `.snapshots`) | 20 |
292
342
  | Snapshots | `client.snapshots` | 5 |
293
343
  | Account | `client.account` (+ `.sessions`, `.folders`, `.favorites`) | 10 |
294
- | Workspaces | `client.workspaces` (+ `.members`, `.roles`, `.invites`, `.action_requests`, `.apps`, `.databases`, `.folders`, `.favorites`) | 30 |
344
+ | Workspaces | `client.workspaces` (+ `.members`, `.roles`, `.invites`, `.action_requests`, `.apps`, `.databases`, `.templates`, `.stacks`, `.folders`, `.favorites`) | 34 |
295
345
  | Billing | `client.billing` (+ `.orders`) | 7 |
296
346
  | Blob Storage | `client.blob` (+ `.shares`, `.rules`, `.domains`) | own host |
297
347
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vertracloud-api"
7
- version = "0.3.2"
7
+ version = "0.3.3"
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"
@@ -1,5 +1,6 @@
1
1
  """Typed Python client for the Vertra Cloud public API (`import vertracloud`)."""
2
2
 
3
+ from . import errcodes
3
4
  from .client import VertraClient, __version__
4
5
  from .errors import (
5
6
  AppBuildFailedError,
@@ -12,6 +13,7 @@ from .errors import (
12
13
  VertraAPIError,
13
14
  WaitTimeoutError,
14
15
  )
16
+ from .resources.templates import TemplateDeployEventStream
15
17
  from .sse import SseEvent, SseStream
16
18
  from .transport import Response, Transport, UrllibTransport
17
19
 
@@ -29,6 +31,8 @@ __all__ = [
29
31
  "RateLimitError",
30
32
  "SseEvent",
31
33
  "SseStream",
34
+ "TemplateDeployEventStream",
35
+ "errcodes",
32
36
  "Transport",
33
37
  "UrllibTransport",
34
38
  "Response",
@@ -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.3.2" # x-release-please-version
19
+ __version__ = "0.3.3" # 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"
@@ -138,11 +138,15 @@ class VertraClient:
138
138
  from .resources.databases import DatabasesResource
139
139
  from .resources.images import ImagesResource
140
140
  from .resources.snapshots import SnapshotsResource
141
+ from .resources.stacks import StacksResource
142
+ from .resources.templates import TemplatesResource
141
143
  from .resources.workspaces import WorkspacesResource
142
144
 
143
145
  self.apps = AppsResource(self)
144
146
  self.databases = DatabasesResource(self)
145
147
  self.images = ImagesResource(self)
148
+ self.templates = TemplatesResource(self)
149
+ self.stacks = StacksResource(self)
146
150
  self.snapshots = SnapshotsResource(self)
147
151
  self.account = AccountResource(self)
148
152
  self.workspaces = WorkspacesResource(self)
@@ -0,0 +1,65 @@
1
+ """Error codes of the template and stack routes, as found in `VertraAPIError.code`.
2
+
3
+ Compare with the constants instead of retyping the strings::
4
+
5
+ from vertracloud import errcodes
6
+
7
+ try:
8
+ client.templates.deploy("n8n", {"name": "my-n8n"})
9
+ except VertraAPIError as err:
10
+ if err.code == errcodes.TEMPLATE_RAM_INSUFFICIENT:
11
+ ...
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ # -- templates
17
+ TEMPLATE_NOT_FOUND = "TEMPLATE_NOT_FOUND"
18
+ TEMPLATE_VERSION_NOT_FOUND = "TEMPLATE_VERSION_NOT_FOUND"
19
+ TEMPLATE_SLUG_TAKEN = "TEMPLATE_SLUG_TAKEN"
20
+ TEMPLATE_MANIFEST_INVALID = "TEMPLATE_MANIFEST_INVALID" # `VertraAPIError.manifest_issues`
21
+ TEMPLATE_INPUT_INVALID = "TEMPLATE_INPUT_INVALID" # `details.issues` lists the questions
22
+ TEMPLATE_SOURCE_REQUIRED = "TEMPLATE_SOURCE_REQUIRED"
23
+ TEMPLATE_SECRET_IN_SOURCE = "TEMPLATE_SECRET_IN_SOURCE" # `VertraAPIError.secret_files`
24
+ TEMPLATE_GITHUB_MANIFEST_NOT_FOUND = "TEMPLATE_GITHUB_MANIFEST_NOT_FOUND"
25
+ TEMPLATE_PLAN_REQUIRED = "TEMPLATE_PLAN_REQUIRED"
26
+ TEMPLATE_RAM_INSUFFICIENT = "TEMPLATE_RAM_INSUFFICIENT"
27
+ TEMPLATE_IMAGE_SLOTS_EXHAUSTED = "TEMPLATE_IMAGE_SLOTS_EXHAUSTED"
28
+ TEMPLATE_CLOSED = "TEMPLATE_CLOSED"
29
+ TEMPLATE_SETTING_LOCKED = "TEMPLATE_SETTING_LOCKED"
30
+ TEMPLATE_PRIVATE_LIMIT_REACHED = "TEMPLATE_PRIVATE_LIMIT_REACHED"
31
+ TEMPLATE_DEPLOY_IN_PROGRESS = "TEMPLATE_DEPLOY_IN_PROGRESS"
32
+ TEMPLATE_DEPLOY_FAILED = "TEMPLATE_DEPLOY_FAILED" # the deploy was undone; `details.step` is the step that failed
33
+ TEMPLATE_DEPLOY_NOT_FOUND = "TEMPLATE_DEPLOY_NOT_FOUND"
34
+ IDEMPOTENCY_KEY_CONFLICT = "IDEMPOTENCY_KEY_CONFLICT"
35
+ IMAGE_TEMPLATE_ONLY = "IMAGE_TEMPLATE_ONLY"
36
+ IMAGE_IN_USE = "IMAGE_IN_USE" # `details.templates` lists the ids of the templates that use the image
37
+ TEMPLATE_SAVE_NOT_ALLOWED = "TEMPLATE_SAVE_NOT_ALLOWED"
38
+ TEMPLATE_UPDATE_BLOCKED = "TEMPLATE_UPDATE_BLOCKED" # `VertraAPIError.update_blocked`
39
+ TEMPLATE_DEPRECATED = "TEMPLATE_DEPRECATED"
40
+ TEMPLATE_VERSION_NOT_SELECTABLE = "TEMPLATE_VERSION_NOT_SELECTABLE"
41
+ TEMPLATE_DETACH_CONFIRMATION_REQUIRED = "TEMPLATE_DETACH_CONFIRMATION_REQUIRED" # `VertraAPIError.detach_action`
42
+ TEMPLATE_ROLLBACK_REQUIRES_DATA = "TEMPLATE_ROLLBACK_REQUIRES_DATA"
43
+ TEMPLATE_STORAGE_UNAVAILABLE = "TEMPLATE_STORAGE_UNAVAILABLE"
44
+ MANAGED_ENV_READONLY = "MANAGED_ENV_READONLY"
45
+
46
+ # -- share links
47
+ SHARE_LINK_NOT_FOUND = "SHARE_LINK_NOT_FOUND"
48
+ SHARE_LINK_REVOKED = "SHARE_LINK_REVOKED"
49
+ SHARE_LINK_EXPIRED = "SHARE_LINK_EXPIRED"
50
+ SHARE_LINK_EXHAUSTED = "SHARE_LINK_EXHAUSTED"
51
+
52
+ # -- stacks
53
+ STACK_NOT_FOUND = "STACK_NOT_FOUND"
54
+ STACK_MEMBER_CONFLICT = "STACK_MEMBER_CONFLICT"
55
+ STACK_MEMBER_NOT_FOUND = "STACK_MEMBER_NOT_FOUND"
56
+ STACK_BINDING_NOT_FOUND = "STACK_BINDING_NOT_FOUND"
57
+ STACK_NETWORK_MODE_UNAVAILABLE = "STACK_NETWORK_MODE_UNAVAILABLE"
58
+ STACK_SERVICE_LOCKED = "STACK_SERVICE_LOCKED"
59
+ STACK_NAME_MISMATCH = "STACK_NAME_MISMATCH"
60
+ STACK_TRANSFORM_EMPTY = "STACK_TRANSFORM_EMPTY"
61
+ STACK_BLOB_PREFIX_TAKEN = "STACK_BLOB_PREFIX_TAKEN"
62
+
63
+ # -- API keys restricted to a Blob folder, or managed by the platform
64
+ BLOB_PREFIX_DENIED = "BLOB_PREFIX_DENIED"
65
+ API_KEY_MANAGED = "API_KEY_MANAGED"
@@ -45,6 +45,41 @@ class VertraAPIError(Exception):
45
45
  return []
46
46
  return [item for item in raw if isinstance(item, dict)]
47
47
 
48
+ def _details_for(self, code: str) -> dict[str, Any] | None:
49
+ if self.code != code or not isinstance(self.details, dict):
50
+ return None
51
+ return self.details
52
+
53
+ @property
54
+ def manifest_issues(self) -> list[dict[str, Any]]:
55
+ """Findings of a `TEMPLATE_MANIFEST_INVALID` error: the objects in `details.issues` (`{path, code, message}`).
56
+ Tolerant like `issues`: any other error gives `[]`."""
57
+ details = self._details_for("TEMPLATE_MANIFEST_INVALID")
58
+ raw = details.get("issues") if details else None
59
+ return [item for item in raw if isinstance(item, dict)] if isinstance(raw, list) else []
60
+
61
+ @property
62
+ def secret_files(self) -> list[str]:
63
+ """Paths of a `TEMPLATE_SECRET_IN_SOURCE` error: the files of the code sent to become a template that hold
64
+ a secret. Any other error gives `[]`."""
65
+ details = self._details_for("TEMPLATE_SECRET_IN_SOURCE")
66
+ raw = details.get("files") if details else None
67
+ return [item for item in raw if isinstance(item, str)] if isinstance(raw, list) else []
68
+
69
+ @property
70
+ def update_blocked(self) -> dict[str, Any] | None:
71
+ """Reason of a `TEMPLATE_UPDATE_BLOCKED` error (`{reason, details?}`); `None` for any other error."""
72
+ return self._details_for("TEMPLATE_UPDATE_BLOCKED")
73
+
74
+ @property
75
+ def detach_action(self) -> str | None:
76
+ """Action of a `TEMPLATE_DETACH_CONFIRMATION_REQUIRED` error (`files.write`, `deploy.code`,
77
+ `deploy.github` or `commit`): repeat the call with `confirm_detach=True` to detach the app from its template.
78
+ `None` for any other error."""
79
+ details = self._details_for("TEMPLATE_DETACH_CONFIRMATION_REQUIRED")
80
+ action = details.get("action") if details else None
81
+ return action if isinstance(action, str) else None
82
+
48
83
 
49
84
  class AuthenticationError(VertraAPIError):
50
85
  """401 — invalid key or user not found (`API_KEY_INVALID`, `USER_NOT_FOUND`)."""
@@ -73,7 +73,8 @@ class AppDeploysWebhookResource:
73
73
  )
74
74
 
75
75
  def create(self, app_id: str, body: CreateWebhookBody, *, workspace_id: str | None = None) -> APIWebhookUrl:
76
- """POST /v1/apps/:id/deploys/webhook — scope `apps:write`."""
76
+ """POST /v1/apps/:id/deploys/webhook — scope `apps:write`. `body["confirm_detach"]` confirms that linking
77
+ GitHub detaches an app of an open template from it (`TEMPLATE_DETACH_CONFIRMATION_REQUIRED` otherwise)."""
77
78
  return self._c.request_json(
78
79
  "POST",
79
80
  f"/v1/apps/{encode_path_param(app_id)}/deploys/webhook",
@@ -335,9 +336,22 @@ class AppFilesResource:
335
336
  content: str | None = None,
336
337
  last_modified: str | None = None,
337
338
  workspace_id: str | None = None,
339
+ confirm_detach: bool = False,
338
340
  ) -> None:
339
- """PUT /v1/apps/:id/files — scope `apps:files`. Response is void."""
340
- body = {"path": path, "content": content, "last_modified": last_modified, "workspace_id": workspace_id}
341
+ """PUT /v1/apps/:id/files — scope `apps:files`. Response is void.
342
+
343
+ An app that came from an open template stops receiving versions of it when its code is changed: without
344
+ `confirm_detach=True` the call fails with `TEMPLATE_DETACH_CONFIRMATION_REQUIRED` and changes nothing
345
+ (`VertraAPIError.detach_action` says which action); with it the change is made and the app is detached.
346
+ Ignored for an app that does not come from a template. The same applies to `move`, `delete`, `upload`
347
+ and `deploys.webhook.create`."""
348
+ body = {
349
+ "path": path,
350
+ "content": content,
351
+ "last_modified": last_modified,
352
+ "workspace_id": workspace_id,
353
+ "confirm_detach": True if confirm_detach else None,
354
+ }
341
355
  return self._c.request_json(
342
356
  "PUT",
343
357
  f"/v1/apps/{encode_path_param(app_id)}/files",
@@ -345,7 +359,8 @@ class AppFilesResource:
345
359
  )
346
360
 
347
361
  def move(self, app_id: str, body: MoveFileBody, *, workspace_id: str | None = None) -> None:
348
- """PATCH /v1/apps/:id/files — scope `apps:files`. Body: `{path, to, workspace_id?}`."""
362
+ """PATCH /v1/apps/:id/files — scope `apps:files`. Body: `{path, to, workspace_id?, confirm_detach?}`
363
+ (see `write` for `confirm_detach`)."""
349
364
  merged = {**body, "workspace_id": workspace_id or body.get("workspace_id")}
350
365
  return self._c.request_json(
351
366
  "PATCH",
@@ -353,9 +368,10 @@ class AppFilesResource:
353
368
  json_body={k: v for k, v in merged.items() if v is not None},
354
369
  )
355
370
 
356
- def delete(self, app_id: str, *, path: str, workspace_id: str | None = None) -> None:
357
- """DELETE /v1/apps/:id/files — scope `apps:files`. Body: `{path, workspace_id?}`."""
358
- body = {"path": path, "workspace_id": workspace_id}
371
+ def delete(self, app_id: str, *, path: str, workspace_id: str | None = None, confirm_detach: bool = False) -> None:
372
+ """DELETE /v1/apps/:id/files — scope `apps:files`. Body: `{path, workspace_id?, confirm_detach?}`
373
+ (see `write` for `confirm_detach`)."""
374
+ body = {"path": path, "workspace_id": workspace_id, "confirm_detach": True if confirm_detach else None}
359
375
  return self._c.request_json(
360
376
  "DELETE",
361
377
  f"/v1/apps/{encode_path_param(app_id)}/files",
@@ -372,6 +388,7 @@ class AppFilesResource:
372
388
  path: str | None = None,
373
389
  message: str | None = None,
374
390
  workspace_id: str | None = None,
391
+ confirm_detach: bool = False,
375
392
  timeout: float | None = None,
376
393
  ) -> APIAppFileUploadResponse:
377
394
  """POST /v1/apps/:id/files/upload — scope `apps:files`. Multipart, field `file`;
@@ -379,6 +396,7 @@ class AppFilesResource:
379
396
  `path` is the destination folder inside the app (omitted or empty means the root);
380
397
  a ZIP is extracted into it, any other file is written to `path/<filename>`.
381
398
  `message` (up to 500 characters) is the commit message shown in the release history.
399
+ `confirm_detach=True` is sent as the query `confirm_detach=true` (see `write`).
382
400
  Returns `app_id` and `updated_at`; ZIP uploads also include dependency metadata."""
383
401
  fields = [MultipartField("file", file, filename=filename)]
384
402
  if message is not None:
@@ -387,7 +405,12 @@ class AppFilesResource:
387
405
  "POST",
388
406
  f"/v1/apps/{encode_path_param(app_id)}/files/upload",
389
407
  fields=fields,
390
- query={"restart": restart, "path": path, "workspace_id": workspace_id},
408
+ query={
409
+ "restart": restart,
410
+ "path": path,
411
+ "workspace_id": workspace_id,
412
+ "confirm_detach": True if confirm_detach else None,
413
+ },
391
414
  timeout=timeout if timeout is not None else LARGE_PAYLOAD_TIMEOUT,
392
415
  )
393
416
 
@@ -502,7 +525,9 @@ class AppsResource:
502
525
  """POST /v1/apps/image — scope `apps:write`. JSON. Creates an app that runs from a custom
503
526
  image, which must be ready (`images.get(...)["status"] == "success"`). Replies 202 with
504
527
  `status: "building"` and `custom_image` filled in; follow the build as with `create`.
505
- `web: False` makes an app without traffic; `subdomain` and `port` need `web: True`.
528
+ `web: False` makes an app without traffic; `subdomain` and `port` need `web: True`. An image that
529
+ belongs to a template (a platform image or a `template_copy`) fails with `IMAGE_TEMPLATE_ONLY`: use the
530
+ template instead.
506
531
  Doc: https://docs.vertracloud.app/api-reference/endpoint/apps/create-from-image
507
532
  """
508
533
  return self._c.request_json("POST", "/v1/apps/image", json_body=body)
@@ -8,6 +8,7 @@ from ..client import encode_path_param
8
8
  from ..types import (
9
9
  APIDatabase,
10
10
  APIDatabaseCertificate,
11
+ APIDatabaseCertificateReset,
11
12
  APIDatabasePasswordReset,
12
13
  APIDatabaseStatus,
13
14
  APIDatabaseStatusShort,
@@ -36,8 +37,10 @@ class DatabaseCertificateResource:
36
37
  timeout=timeout,
37
38
  )
38
39
 
39
- def reset(self, db_id: str, *, workspace_id: str | None = None) -> None:
40
- """POST /v1/databases/:id/credentials/certificate/reset — scope `databases:credentials`."""
40
+ def reset(self, db_id: str, *, workspace_id: str | None = None) -> APIDatabaseCertificateReset:
41
+ """POST /v1/databases/:id/credentials/certificate/reset — scope `databases:credentials`. Besides the new
42
+ certificate, the answer lists the apps of stacks connected to the database that must restart to read it
43
+ (`restart_required`, `apps_to_restart`)."""
41
44
  return self._c.request_json(
42
45
  "POST",
43
46
  f"/v1/databases/{encode_path_param(db_id)}/credentials/certificate/reset",
@@ -50,7 +53,9 @@ class DatabasePasswordResource:
50
53
  self._c = client
51
54
 
52
55
  def reset(self, db_id: str, *, workspace_id: str | None = None) -> APIDatabasePasswordReset:
53
- """POST /v1/databases/:id/credentials/reset — scope `databases:credentials`."""
56
+ """POST /v1/databases/:id/credentials/reset — scope `databases:credentials`. The answer also lists the apps
57
+ of stacks connected to the database that must restart to read the new password (`restart_required`,
58
+ `apps_to_restart`)."""
54
59
  return self._c.request_json(
55
60
  "POST", f"/v1/databases/{encode_path_param(db_id)}/credentials/reset", query={"workspace_id": workspace_id}
56
61
  )
@@ -7,6 +7,10 @@ is `success` or `failed`.
7
7
 
8
8
  `workspace_id` is a keyword-only query parameter on the routes that act on an existing image and
9
9
  on `list`. Without it the call acts on the caller's own images, as the apps routes do.
10
+
11
+ An image that came from a template (`kind == "template_copy"`, or a platform image) cannot be renamed, rebuilt,
12
+ linked to GitHub or committed to: those calls fail with `IMAGE_TEMPLATE_ONLY`. Deleting an image that is the
13
+ source of one of your private templates fails with `IMAGE_IN_USE`.
10
14
  """
11
15
 
12
16
  from __future__ import annotations