vertracloud-api 0.3.0__tar.gz → 0.3.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 (22) hide show
  1. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/CHANGELOG.md +15 -0
  2. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/PKG-INFO +66 -5
  3. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/README.md +65 -4
  4. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/pyproject.toml +1 -1
  5. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/src/vertracloud/client.py +3 -1
  6. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/src/vertracloud/errors.py +13 -0
  7. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/src/vertracloud/resources/apps.py +21 -6
  8. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/src/vertracloud/resources/databases.py +11 -3
  9. vertracloud_api-0.3.2/src/vertracloud/resources/images.py +239 -0
  10. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/src/vertracloud/resources/snapshots.py +17 -3
  11. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/src/vertracloud/types.py +242 -10
  12. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/.gitignore +0 -0
  13. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/LICENSE +0 -0
  14. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/src/vertracloud/__init__.py +0 -0
  15. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/src/vertracloud/py.typed +0 -0
  16. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/src/vertracloud/resources/__init__.py +0 -0
  17. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/src/vertracloud/resources/account.py +0 -0
  18. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/src/vertracloud/resources/billing.py +0 -0
  19. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/src/vertracloud/resources/blob.py +0 -0
  20. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/src/vertracloud/resources/workspaces.py +0 -0
  21. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/src/vertracloud/sse.py +0 -0
  22. {vertracloud_api-0.3.0 → vertracloud_api-0.3.2}/src/vertracloud/transport.py +0 -0
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.2](https://github.com/vertracloud/sdk-api-py/compare/v0.3.1...v0.3.2) (2026-10-25)
4
+
5
+
6
+ ### Features
7
+
8
+ * add images resource and database engine versions ([2136b86](https://github.com/vertracloud/sdk-api-py/commit/2136b86739608096c9e4590589b3aca2f42d1777))
9
+
10
+ ## [0.3.1](https://github.com/vertracloud/sdk-api-py/compare/v0.3.0...v0.3.1) (2026-10-22)
11
+
12
+
13
+ ### Features
14
+
15
+ * **apps:** Deno, .NET and Elixir and language on create ([c7a7bb3](https://github.com/vertracloud/sdk-api-py/commit/c7a7bb3e4141d8f0c45853807acfd49719841100))
16
+ * **apps:** remove cleanup_old_runtime_language from the restart body ([e8aa75d](https://github.com/vertracloud/sdk-api-py/commit/e8aa75daf69752af8ea16048e84e85857f613ff4))
17
+
3
18
  ## [0.3.0](https://github.com/vertracloud/sdk-api-py/compare/v0.2.2...v0.3.0) (2026-10-22)
4
19
 
5
20
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vertracloud-api
3
- Version: 0.3.0
3
+ Version: 0.3.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
@@ -76,7 +76,7 @@ client = VertraClient(
76
76
 
77
77
  ### Per-call options
78
78
 
79
- Methods on apps and databases take a keyword-only `workspace_id`; downloads, uploads and streams also take `timeout`:
79
+ Methods on apps, databases and images 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
@@ -177,7 +177,7 @@ except vertracloud.WaitTimeoutError: # still building after `timeout` seconds
177
177
  ...
178
178
  ```
179
179
 
180
- `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).
180
+ While the build runs, calls that need a stopped or running app can fail with `APP_BUILDING` (409).
181
181
 
182
182
  ### Realtime logs (SSE)
183
183
 
@@ -214,6 +214,66 @@ client.blob.delete_many(prefix="tmp/")
214
214
 
215
215
  `data` can be `bytes`, a file path, or a seekable binary file. Up to 5 MB it is one request; larger files go straight to storage in parts, with a retry per part. If the process dies halfway, `client.blob.resume(upload_id, data)` sends only the missing parts.
216
216
 
217
+ ### Custom Docker images
218
+
219
+ An image is a build context (a ZIP of up to 100 MB, or a GitHub repository) with a `Dockerfile` at its root. Create and rebuild reply right away with `status: "building"`; the build then ends as `success` or `failed`, and `error` says why. Whether your plan can create images is decided by the API (`PLAN_DOES_NOT_SUPPORT_CUSTOM_IMAGES`).
220
+
221
+ ```python
222
+ # From a ZIP: the file is passed as bytes, like apps.create
223
+ with open("projeto.zip", "rb") as f:
224
+ image = client.images.create({"name": "meu-projeto"}, file=f.read())
225
+
226
+ # Or from GitHub: `github_branch` is optional (default: the repository's default branch). A repository in your
227
+ # GitHub App installations is linked and each push to the branch rebuilds the image; a public repository outside
228
+ # them is built once, with no link.
229
+ site = client.images.create({"name": "site", "github_repository": "acme/site", "github_branch": "main"})
230
+
231
+ # Check the Dockerfile before building; `refusals` block the build, `warnings` do not
232
+ report = client.images.check({"dockerfile": dockerfile_text, "files": ["Dockerfile", "app.py"]})
233
+
234
+ # Follow the build as lines of text; the stream repeats the history, then goes live
235
+ for line in client.images.logs(image["id"]):
236
+ print(line)
237
+
238
+ image = client.images.get(image["id"]) # then status is "success" or "failed"
239
+
240
+ app = client.apps.create_from_image(
241
+ {"image_id": image["id"], "name": "meu-app", "memory": 512, "web": True, "subdomain": "random"}
242
+ )
243
+ ```
244
+
245
+ Images can belong to a workspace, like apps. `list` and every method that acts on an existing image take a keyword-only `workspace_id`; `create` and `apps.create_from_image` take it in the body (`{"name": "site", "workspace_id": workspace_id, ...}`). `check` only reads the `Dockerfile` you send, so it has no workspace.
246
+
247
+ ```python
248
+ images = client.images.list(workspace_id=workspace_id)
249
+ client.images.rebuild(image_id, file=zip_bytes, workspace_id=workspace_id)
250
+ ```
251
+
252
+ `create` takes an optional `description` (up to 128 characters). `update` renames the image and/or changes its description without building anything; the apps keep running. Send only what changes, and `None` removes the description:
253
+
254
+ ```python
255
+ client.images.update(image["id"], {"name": "meu-projeto-base", "description": "Base Node 22"})
256
+ client.images.update(image["id"], {"description": None})
257
+ ```
258
+
259
+ `images.logs` returns an iterator with `close()`, like `apps.realtime`. `rebuild(image_id, file=...)` replaces the whole build context; it never restarts the apps that run the image. `link_github` and `unlink_github` change the GitHub link, and `versions` lists the successful builds, newest first.
260
+
261
+ `delete` removes the image and **every app created from it**. The name must be typed back as `confirm_name`:
262
+
263
+ ```python
264
+ client.images.delete(image["id"], confirm_name="meu-projeto") # {"deleted_apps": [...]}
265
+ ```
266
+
267
+ ### Database versions
268
+
269
+ ```python
270
+ catalog = client.databases.versions() # keyed by engine type, as a string
271
+ db = client.databases.create({"name": "meu-db", "type": 1, "ram": 512, "version": catalog["1"]["recommended"]})
272
+ print(db["version"]) # major line, e.g. "18"
273
+ ```
274
+
275
+ Omit `version` to get the recommended one; with `snapshot_id`, omit it to keep the snapshot's line (an older one fails with `SNAPSHOT_VERSION_NEWER`, an unknown one with `INVALID_DATABASE_VERSION`). A database's version cannot be changed after creation; security updates are applied when it is stopped and started.
276
+
217
277
  ### Errors
218
278
 
219
279
  Every non-2xx response raises a `VertraAPIError` carrying the HTTP `status`, the API `code` (e.g. `APP_NOT_FOUND`) and `details`; `str(err)` includes the message. The API key is never part of the error, even when it is printed:
@@ -245,8 +305,9 @@ client = VertraClient(api_key="test", transport=my_fake_transport)
245
305
 
246
306
  | Domain | Resource | Routes |
247
307
  |---|---|---|
248
- | Apps | `client.apps` (+ `.deploys`, `.releases`, `.domains`, `.network`, `.envs`, `.files`) | 42 |
249
- | Databases | `client.databases` (+ `.credentials`) | 13 |
308
+ | Apps | `client.apps` (+ `.deploys`, `.releases`, `.domains`, `.network`, `.envs`, `.files`) | 43 |
309
+ | Databases | `client.databases` (+ `.credentials`) | 14 |
310
+ | Custom images | `client.images` | 12 |
250
311
  | Snapshots | `client.snapshots` | 5 |
251
312
  | Account | `client.account` (+ `.sessions`, `.folders`, `.favorites`) | 10 |
252
313
  | Workspaces | `client.workspaces` (+ `.members`, `.roles`, `.invites`, `.action_requests`, `.apps`, `.databases`, `.folders`, `.favorites`) | 30 |
@@ -57,7 +57,7 @@ client = VertraClient(
57
57
 
58
58
  ### Per-call options
59
59
 
60
- Methods on apps and databases take a keyword-only `workspace_id`; downloads, uploads and streams also take `timeout`:
60
+ Methods on apps, databases and images 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
@@ -158,7 +158,7 @@ except vertracloud.WaitTimeoutError: # still building after `timeout` seconds
158
158
  ...
159
159
  ```
160
160
 
161
- `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
+ While the build runs, calls that need a stopped or running app can fail with `APP_BUILDING` (409).
162
162
 
163
163
  ### Realtime logs (SSE)
164
164
 
@@ -195,6 +195,66 @@ client.blob.delete_many(prefix="tmp/")
195
195
 
196
196
  `data` can be `bytes`, a file path, or a seekable binary file. Up to 5 MB it is one request; larger files go straight to storage in parts, with a retry per part. If the process dies halfway, `client.blob.resume(upload_id, data)` sends only the missing parts.
197
197
 
198
+ ### Custom Docker images
199
+
200
+ An image is a build context (a ZIP of up to 100 MB, or a GitHub repository) with a `Dockerfile` at its root. Create and rebuild reply right away with `status: "building"`; the build then ends as `success` or `failed`, and `error` says why. Whether your plan can create images is decided by the API (`PLAN_DOES_NOT_SUPPORT_CUSTOM_IMAGES`).
201
+
202
+ ```python
203
+ # From a ZIP: the file is passed as bytes, like apps.create
204
+ with open("projeto.zip", "rb") as f:
205
+ image = client.images.create({"name": "meu-projeto"}, file=f.read())
206
+
207
+ # Or from GitHub: `github_branch` is optional (default: the repository's default branch). A repository in your
208
+ # GitHub App installations is linked and each push to the branch rebuilds the image; a public repository outside
209
+ # them is built once, with no link.
210
+ site = client.images.create({"name": "site", "github_repository": "acme/site", "github_branch": "main"})
211
+
212
+ # Check the Dockerfile before building; `refusals` block the build, `warnings` do not
213
+ report = client.images.check({"dockerfile": dockerfile_text, "files": ["Dockerfile", "app.py"]})
214
+
215
+ # Follow the build as lines of text; the stream repeats the history, then goes live
216
+ for line in client.images.logs(image["id"]):
217
+ print(line)
218
+
219
+ image = client.images.get(image["id"]) # then status is "success" or "failed"
220
+
221
+ app = client.apps.create_from_image(
222
+ {"image_id": image["id"], "name": "meu-app", "memory": 512, "web": True, "subdomain": "random"}
223
+ )
224
+ ```
225
+
226
+ Images can belong to a workspace, like apps. `list` and every method that acts on an existing image take a keyword-only `workspace_id`; `create` and `apps.create_from_image` take it in the body (`{"name": "site", "workspace_id": workspace_id, ...}`). `check` only reads the `Dockerfile` you send, so it has no workspace.
227
+
228
+ ```python
229
+ images = client.images.list(workspace_id=workspace_id)
230
+ client.images.rebuild(image_id, file=zip_bytes, workspace_id=workspace_id)
231
+ ```
232
+
233
+ `create` takes an optional `description` (up to 128 characters). `update` renames the image and/or changes its description without building anything; the apps keep running. Send only what changes, and `None` removes the description:
234
+
235
+ ```python
236
+ client.images.update(image["id"], {"name": "meu-projeto-base", "description": "Base Node 22"})
237
+ client.images.update(image["id"], {"description": None})
238
+ ```
239
+
240
+ `images.logs` returns an iterator with `close()`, like `apps.realtime`. `rebuild(image_id, file=...)` replaces the whole build context; it never restarts the apps that run the image. `link_github` and `unlink_github` change the GitHub link, and `versions` lists the successful builds, newest first.
241
+
242
+ `delete` removes the image and **every app created from it**. The name must be typed back as `confirm_name`:
243
+
244
+ ```python
245
+ client.images.delete(image["id"], confirm_name="meu-projeto") # {"deleted_apps": [...]}
246
+ ```
247
+
248
+ ### Database versions
249
+
250
+ ```python
251
+ catalog = client.databases.versions() # keyed by engine type, as a string
252
+ db = client.databases.create({"name": "meu-db", "type": 1, "ram": 512, "version": catalog["1"]["recommended"]})
253
+ print(db["version"]) # major line, e.g. "18"
254
+ ```
255
+
256
+ Omit `version` to get the recommended one; with `snapshot_id`, omit it to keep the snapshot's line (an older one fails with `SNAPSHOT_VERSION_NEWER`, an unknown one with `INVALID_DATABASE_VERSION`). A database's version cannot be changed after creation; security updates are applied when it is stopped and started.
257
+
198
258
  ### Errors
199
259
 
200
260
  Every non-2xx response raises a `VertraAPIError` carrying the HTTP `status`, the API `code` (e.g. `APP_NOT_FOUND`) and `details`; `str(err)` includes the message. The API key is never part of the error, even when it is printed:
@@ -226,8 +286,9 @@ client = VertraClient(api_key="test", transport=my_fake_transport)
226
286
 
227
287
  | Domain | Resource | Routes |
228
288
  |---|---|---|
229
- | Apps | `client.apps` (+ `.deploys`, `.releases`, `.domains`, `.network`, `.envs`, `.files`) | 42 |
230
- | Databases | `client.databases` (+ `.credentials`) | 13 |
289
+ | Apps | `client.apps` (+ `.deploys`, `.releases`, `.domains`, `.network`, `.envs`, `.files`) | 43 |
290
+ | Databases | `client.databases` (+ `.credentials`) | 14 |
291
+ | Custom images | `client.images` | 12 |
231
292
  | Snapshots | `client.snapshots` | 5 |
232
293
  | Account | `client.account` (+ `.sessions`, `.folders`, `.favorites`) | 10 |
233
294
  | Workspaces | `client.workspaces` (+ `.members`, `.roles`, `.invites`, `.action_requests`, `.apps`, `.databases`, `.folders`, `.favorites`) | 30 |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vertracloud-api"
7
- version = "0.3.0"
7
+ version = "0.3.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"
@@ -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.0" # x-release-please-version
19
+ __version__ = "0.3.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"
@@ -136,11 +136,13 @@ class VertraClient:
136
136
  from .resources.billing import BillingResource
137
137
  from .resources.blob import BlobResource
138
138
  from .resources.databases import DatabasesResource
139
+ from .resources.images import ImagesResource
139
140
  from .resources.snapshots import SnapshotsResource
140
141
  from .resources.workspaces import WorkspacesResource
141
142
 
142
143
  self.apps = AppsResource(self)
143
144
  self.databases = DatabasesResource(self)
145
+ self.images = ImagesResource(self)
144
146
  self.snapshots = SnapshotsResource(self)
145
147
  self.account = AccountResource(self)
146
148
  self.workspaces = WorkspacesResource(self)
@@ -32,6 +32,19 @@ class VertraAPIError(Exception):
32
32
  def __repr__(self) -> str:
33
33
  return f"{self.__class__.__name__}(status={self.status!r}, code={self.code!r})"
34
34
 
35
+ @property
36
+ def issues(self) -> list[dict[str, Any]]:
37
+ """Findings of a `DOCKERFILE_REFUSED` error: the objects in `details.issues`, as the API sent them
38
+ (`{code, line}`). Tolerant on purpose: any other error, or a `details` of another shape, gives `[]`
39
+ and never raises; entries that are not objects are skipped."""
40
+ details = self.details
41
+ if not isinstance(details, dict):
42
+ return []
43
+ raw = details.get("issues")
44
+ if not isinstance(raw, list):
45
+ return []
46
+ return [item for item in raw if isinstance(item, dict)]
47
+
35
48
 
36
49
  class AuthenticationError(VertraAPIError):
37
50
  """401 — invalid key or user not found (`API_KEY_INVALID`, `USER_NOT_FOUND`)."""
@@ -1,4 +1,4 @@
1
- """`apps` domain — 42 routes (scopes `apps:read/write/delete/envs/files`).
1
+ """`apps` domain — 43 routes (scopes `apps:read/write/delete/envs/files`).
2
2
 
3
3
  `workspace_id` is accepted as an optional query parameter on almost every method.
4
4
  """
@@ -15,6 +15,7 @@ from ..types import (
15
15
  AddDomainBody,
16
16
  APIAppFileUploadResponse,
17
17
  APIApplication,
18
+ APIApplicationCreated,
18
19
  APIApplicationDeployment,
19
20
  APIApplicationDomain,
20
21
  APIApplicationDomainConnect,
@@ -29,11 +30,13 @@ from ..types import (
29
30
  APIApplicationRollbackPreview,
30
31
  APIApplicationStatus,
31
32
  APIApplicationStatusShort,
33
+ APIApplicationWebhook,
32
34
  APIApplicationWebPublish,
33
35
  APIRuntimesResponse,
34
36
  APISubdomainResponse,
35
37
  APIWebhookUrl,
36
38
  CreateAppBody,
39
+ CreateAppFromImageBody,
37
40
  CreateWebhookBody,
38
41
  EnvironmentSetBody,
39
42
  JsonValue,
@@ -61,7 +64,7 @@ class AppDeploysWebhookResource:
61
64
  def __init__(self, client: VertraClient) -> None:
62
65
  self._c = client
63
66
 
64
- def get(self, app_id: str, *, workspace_id: str | None = None) -> APIWebhookUrl:
67
+ def get(self, app_id: str, *, workspace_id: str | None = None) -> APIApplicationWebhook:
65
68
  """GET /v1/apps/:id/deploys/webhook — scope `apps:read`.
66
69
  Doc: https://docs.vertracloud.app/api-reference/endpoint/apps/deploys/webhookurl
67
70
  """
@@ -255,7 +258,9 @@ class AppNetworkResource:
255
258
  self, app_id: str, body: PublishAppBody | None = None, *, workspace_id: str | None = None
256
259
  ) -> APIApplicationWebPublish:
257
260
  """POST /v1/apps/:id/network/publish — scope `apps:write`. The Pro plan gets a random
258
- subdomain, Scale picks its own; an empty `{}` body is valid."""
261
+ subdomain, Scale picks its own; an empty `{}` body is valid. `port` (only for an app on a
262
+ custom image) is the port the image listens on; it is required when the app has no web
263
+ port yet."""
259
264
  return self._c.request_json(
260
265
  "POST",
261
266
  f"/v1/apps/{encode_path_param(app_id)}/network/publish",
@@ -467,9 +472,10 @@ class AppsResource:
467
472
  file: bytes | None = None,
468
473
  filename: str = "app.zip",
469
474
  timeout: float | None = None,
470
- ) -> APIApplication:
475
+ ) -> APIApplicationCreated:
471
476
  """POST /v1/apps — scope `apps:write`. Multipart: field `file` (zip) OR `snapshot_id`
472
- in `body`, other fields as text parts. Rate limit 15/min, plus the plan's
477
+ in `body`, other fields as text parts. A snapshot of an app on a custom image can come with
478
+ `warning` (`IMAGE_VERSION_NOT_FOUND`). Rate limit 15/min, plus the plan's
473
479
  deploys-per-hour cap. Replies 202 with `status: "building"`: the build continues in the
474
480
  background — use `wait_until_ready`, `logs` or `realtime` to follow it.
475
481
  Doc: https://docs.vertracloud.app/api-reference/endpoint/apps/create
@@ -492,6 +498,15 @@ class AppsResource:
492
498
  "POST", "/v1/apps", fields=fields, timeout=timeout if timeout is not None else LARGE_PAYLOAD_TIMEOUT
493
499
  )
494
500
 
501
+ def create_from_image(self, body: CreateAppFromImageBody) -> APIApplication:
502
+ """POST /v1/apps/image — scope `apps:write`. JSON. Creates an app that runs from a custom
503
+ image, which must be ready (`images.get(...)["status"] == "success"`). Replies 202 with
504
+ `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`.
506
+ Doc: https://docs.vertracloud.app/api-reference/endpoint/apps/create-from-image
507
+ """
508
+ return self._c.request_json("POST", "/v1/apps/image", json_body=body)
509
+
495
510
  def wait_until_ready(
496
511
  self,
497
512
  app_id: str,
@@ -534,7 +549,7 @@ class AppsResource:
534
549
  self, app_id: str, body: RestartAppBody | None = None, *, workspace_id: str | None = None
535
550
  ) -> APIApplicationOperationResponse:
536
551
  """POST /v1/apps/:id/restart — scope `apps:write`. Flags: `reinstall_dependencies`,
537
- `force_build`, `cleanup_old_runtime_language`."""
552
+ `force_build`."""
538
553
  return self._c.request_json(
539
554
  "POST",
540
555
  f"/v1/apps/{encode_path_param(app_id)}/restart",
@@ -1,4 +1,4 @@
1
- """`databases` domain — 13 routes (scopes `databases:read/write/delete/credentials`)."""
1
+ """`databases` domain — 14 routes (scopes `databases:read/write/delete/credentials`)."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -11,6 +11,7 @@ from ..types import (
11
11
  APIDatabasePasswordReset,
12
12
  APIDatabaseStatus,
13
13
  APIDatabaseStatusShort,
14
+ APIDatabaseVersions,
14
15
  APIOperationResponse,
15
16
  CreateDatabaseBody,
16
17
  UpdateDatabaseBody,
@@ -63,7 +64,7 @@ class DatabaseCredentialsResource:
63
64
 
64
65
 
65
66
  class DatabasesResource:
66
- """`databases.*` — statusAll/get/status/metrics/create/update/start/stop/reset/delete +
67
+ """`databases.*` — status_all/versions/get/status/metrics/create/update/start/stop/reset/delete +
67
68
  `credentials`."""
68
69
 
69
70
  def __init__(self, client: VertraClient) -> None:
@@ -74,6 +75,12 @@ class DatabasesResource:
74
75
  """GET /v1/databases/status — scope `databases:read`."""
75
76
  return self._c.request_json("GET", "/v1/databases/status", query={"workspace_id": workspace_id})
76
77
 
78
+ def versions(self) -> APIDatabaseVersions:
79
+ """GET /v1/databases/versions — scope `databases:read`. Catalog keyed by engine `type`:
80
+ `recommended` and `available` lines for new databases. A database's version cannot be
81
+ changed after creation; security updates are applied when it is stopped and started."""
82
+ return self._c.request_json("GET", "/v1/databases/versions")
83
+
77
84
  def get(self, db_id: str, *, workspace_id: str | None = None) -> APIDatabase:
78
85
  """GET /v1/databases/:id — scope `databases:read`."""
79
86
  return self._c.request_json(
@@ -93,7 +100,8 @@ class DatabasesResource:
93
100
  )
94
101
 
95
102
  def create(self, body: CreateDatabaseBody) -> APIDatabase:
96
- """POST /v1/databases — scope `databases:write`."""
103
+ """POST /v1/databases — scope `databases:write`.
104
+ Errors: `INVALID_DATABASE_VERSION`, `SNAPSHOT_VERSION_NEWER`."""
97
105
  return self._c.request_json("POST", "/v1/databases", json_body=body)
98
106
 
99
107
  def update(self, db_id: str, body: UpdateDatabaseBody, *, workspace_id: str | None = None) -> APIDatabase:
@@ -0,0 +1,239 @@
1
+ """`images` domain — custom Docker images (scopes `images:read`, `images:write` and `images:delete`).
2
+
3
+ A custom image is a build context with a `Dockerfile`, built by the platform in isolation.
4
+ `apps.create_from_image` (scope `apps:write`) runs an app from a ready image. Create and rebuild
5
+ reply 202 with `status: "building"`: follow the build with `logs`, or poll `get` until `status`
6
+ is `success` or `failed`.
7
+
8
+ `workspace_id` is a keyword-only query parameter on the routes that act on an existing image and
9
+ on `list`. Without it the call acts on the caller's own images, as the apps routes do.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Iterator
15
+ from typing import TYPE_CHECKING
16
+
17
+ from ..client import LARGE_PAYLOAD_TIMEOUT, MultipartField, encode_path_param
18
+ from ..sse import SseStream
19
+ from ..types import (
20
+ APICustomImage,
21
+ APICustomImageContext,
22
+ APICustomImageDeleteResult,
23
+ APICustomImageVersion,
24
+ APIDockerfileCheck,
25
+ CheckDockerfileBody,
26
+ CreateCustomImageBody,
27
+ CustomImageGithubBody,
28
+ UpdateCustomImageBody,
29
+ )
30
+
31
+ if TYPE_CHECKING:
32
+ from ..client import VertraClient
33
+
34
+ # Aliases so `list[...]` doesn't resolve to the `list` method inside the class itself.
35
+ ImageList = list[APICustomImage]
36
+ ImageVersionList = list[APICustomImageVersion]
37
+
38
+
39
+ class ImageLogStream:
40
+ """Build log of an image, as lines. Iterate it for the text of each `log` event; `close()`
41
+ stops following a live build. Other events (`system`, `heartbeat`) are skipped."""
42
+
43
+ def __init__(self, stream: SseStream) -> None:
44
+ self._stream = stream
45
+
46
+ def __iter__(self) -> Iterator[str]:
47
+ for event in self._stream:
48
+ if event.event == "log":
49
+ yield event.data
50
+
51
+ def close(self) -> None:
52
+ self._stream.close()
53
+
54
+
55
+ class ImagesResource:
56
+ """`images.*` — list/get/create/update/rebuild/delete/versions/logs/context/link_github/unlink_github/check."""
57
+
58
+ def __init__(self, client: VertraClient) -> None:
59
+ self._c = client
60
+
61
+ def list(self, *, workspace_id: str | None = None) -> ImageList:
62
+ """GET /v1/images — scope `images:read`. With `workspace_id`, lists the images of that workspace.
63
+ Doc: https://docs.vertracloud.app/api-reference/endpoint/images/list
64
+ """
65
+ return self._c.request_json("GET", "/v1/images", query={"workspace_id": workspace_id})
66
+
67
+ def get(self, image_id: str, *, workspace_id: str | None = None) -> APICustomImage:
68
+ """GET /v1/images/:image_id — scope `images:read`. `status` is the state of the latest build;
69
+ `current_version_id` and `digest` are `None` until a build succeeds.
70
+ Doc: https://docs.vertracloud.app/api-reference/endpoint/images/get
71
+ """
72
+ return self._c.request_json(
73
+ "GET", f"/v1/images/{encode_path_param(image_id)}", query={"workspace_id": workspace_id}
74
+ )
75
+
76
+ def create(
77
+ self,
78
+ body: CreateCustomImageBody,
79
+ *,
80
+ file: bytes | None = None,
81
+ filename: str = "image.zip",
82
+ timeout: float | None = None,
83
+ ) -> APICustomImage:
84
+ """POST /v1/images — scope `images:write`. Multipart. The source is either `file` (a ZIP
85
+ of up to 100 MB, with a `Dockerfile` at its root) or `body["github_repository"]`;
86
+ `body["github_branch"]` is optional (default: the repository's default branch). A repository
87
+ in your GitHub App installations is linked and each push to the branch rebuilds the image; a
88
+ public repository outside them is built once, with no link (`github` is `None`). Raises
89
+ `ValueError` before any request when the source is missing, doubled, or a branch comes without
90
+ a repository. Replies 202 with `status: "building"`.
91
+ Errors: `PLAN_DOES_NOT_SUPPORT_CUSTOM_IMAGES`, `MAX_IMAGES_REACHED`, `DOCKERFILE_NOT_FOUND`,
92
+ `DOCKERFILE_REFUSED` (its `issues` attribute lists the findings), `IMAGE_CONTEXT_TOO_LARGE`,
93
+ `IMAGE_BUILDER_UNAVAILABLE`, `INSTALLATION_NOT_FOUND` (a private or missing repository outside
94
+ your installations).
95
+ Doc: https://docs.vertracloud.app/api-reference/endpoint/images/create
96
+ """
97
+ repository = body.get("github_repository")
98
+ branch = body.get("github_branch")
99
+ if file is not None and (repository is not None or branch is not None):
100
+ raise ValueError("pass either file or github_repository (with github_branch), not both")
101
+ if file is None and repository is None:
102
+ raise ValueError("pass file, or github_repository (with an optional github_branch)")
103
+
104
+ fields = [MultipartField("name", body["name"])]
105
+ description = body.get("description")
106
+ if description is not None:
107
+ fields.append(MultipartField("description", description))
108
+ if file is not None:
109
+ fields.append(MultipartField("file", file, filename=filename))
110
+ if repository is not None:
111
+ fields.append(MultipartField("github_repository", repository))
112
+ if branch is not None:
113
+ fields.append(MultipartField("github_branch", branch))
114
+ workspace_id = body.get("workspace_id")
115
+ if workspace_id is not None:
116
+ fields.append(MultipartField("workspace_id", workspace_id))
117
+ return self._c.request_multipart(
118
+ "POST", "/v1/images", fields=fields, timeout=timeout if timeout is not None else LARGE_PAYLOAD_TIMEOUT
119
+ )
120
+
121
+ def update(self, image_id: str, body: UpdateCustomImageBody, *, workspace_id: str | None = None) -> APICustomImage:
122
+ """PATCH /v1/images/:image_id — scope `images:write`. Renames the image and/or changes its
123
+ description; send only what changes (`description: None` or an empty string removes it). Builds
124
+ nothing: the apps keep running and nothing restarts. Errors: `VALIDATION_ERROR` (empty body, name
125
+ out of range or already used by another of your images with `details.path == "name"`,
126
+ description too long), `IMAGE_NOT_FOUND`, `ACCESS_DENIED` (system images cannot be changed).
127
+ Doc: https://docs.vertracloud.app/api-reference/endpoint/images/update
128
+ """
129
+ return self._c.request_json(
130
+ "PATCH",
131
+ f"/v1/images/{encode_path_param(image_id)}",
132
+ json_body=body,
133
+ query={"workspace_id": workspace_id},
134
+ )
135
+
136
+ def rebuild(
137
+ self,
138
+ image_id: str,
139
+ *,
140
+ file: bytes,
141
+ filename: str = "image.zip",
142
+ workspace_id: str | None = None,
143
+ timeout: float | None = None,
144
+ ) -> APICustomImage:
145
+ """PUT /v1/images/:image_id — scope `images:write`. Multipart `file` replaces the whole build
146
+ context. Replies 202 with `status: "building"`. Refused with `IMAGE_BUILD_IN_PROGRESS` while a
147
+ build runs. A rebuild never restarts the apps that run the image.
148
+ Doc: https://docs.vertracloud.app/api-reference/endpoint/images/rebuild
149
+ """
150
+ return self._c.request_multipart(
151
+ "PUT",
152
+ f"/v1/images/{encode_path_param(image_id)}",
153
+ fields=[MultipartField("file", file, filename=filename)],
154
+ query={"workspace_id": workspace_id},
155
+ timeout=timeout if timeout is not None else LARGE_PAYLOAD_TIMEOUT,
156
+ )
157
+
158
+ def delete(
159
+ self, image_id: str, *, confirm_name: str, workspace_id: str | None = None
160
+ ) -> APICustomImageDeleteResult:
161
+ """DELETE /v1/images/:image_id — scope `images:delete`, and also `apps:delete` when apps run the
162
+ image. Body `{confirm_name}` must equal the image name (`IMAGE_NAME_MISMATCH` otherwise).
163
+ Deletes the versions, the context and every app created from the image; `deleted_apps` lists
164
+ their ids. Refused with `IMAGE_BUILD_IN_PROGRESS` while a build runs.
165
+ Doc: https://docs.vertracloud.app/api-reference/endpoint/images/delete
166
+ """
167
+ return self._c.request_json(
168
+ "DELETE",
169
+ f"/v1/images/{encode_path_param(image_id)}",
170
+ json_body={"confirm_name": confirm_name},
171
+ query={"workspace_id": workspace_id},
172
+ )
173
+
174
+ def versions(self, image_id: str, *, workspace_id: str | None = None) -> ImageVersionList:
175
+ """GET /v1/images/:image_id/versions — scope `images:read`. Newest first.
176
+ Doc: https://docs.vertracloud.app/api-reference/endpoint/images/versions
177
+ """
178
+ return self._c.request_json(
179
+ "GET", f"/v1/images/{encode_path_param(image_id)}/versions", query={"workspace_id": workspace_id}
180
+ )
181
+
182
+ def logs(
183
+ self,
184
+ image_id: str,
185
+ *,
186
+ version_id: str | None = None,
187
+ workspace_id: str | None = None,
188
+ timeout: float | None = None,
189
+ ) -> ImageLogStream:
190
+ """GET /v1/images/:image_id/logs — scope `images:read`. A Server-Sent Events stream: it repeats
191
+ the history of the build and then follows it live. `version_id` picks the build of one version;
192
+ by default it is the latest build. Iterate it for lines; `close()` or `break` ends it.
193
+ Doc: https://docs.vertracloud.app/api-reference/endpoint/images/logs
194
+ """
195
+ stream = self._c.request_sse(
196
+ f"/v1/images/{encode_path_param(image_id)}/logs",
197
+ query={"version_id": version_id, "workspace_id": workspace_id},
198
+ timeout=timeout,
199
+ )
200
+ return ImageLogStream(stream)
201
+
202
+ def context(self, image_id: str, *, workspace_id: str | None = None) -> APICustomImageContext:
203
+ """GET /v1/images/:image_id/context — scope `images:read`. `url` is a temporary download link
204
+ of the current build context.
205
+ Doc: https://docs.vertracloud.app/api-reference/endpoint/images/context
206
+ """
207
+ return self._c.request_json(
208
+ "GET", f"/v1/images/{encode_path_param(image_id)}/context", query={"workspace_id": workspace_id}
209
+ )
210
+
211
+ def link_github(
212
+ self, image_id: str, body: CustomImageGithubBody, *, workspace_id: str | None = None
213
+ ) -> APICustomImage:
214
+ """PUT /v1/images/:image_id/github — scope `images:write`. Links a repository (`owner/name`) in
215
+ your GitHub App installations: each push to the branch rebuilds the image. `body["branch"]` is
216
+ optional (default: the repository's default branch).
217
+ Doc: https://docs.vertracloud.app/api-reference/endpoint/images/github-link
218
+ """
219
+ return self._c.request_json(
220
+ "PUT",
221
+ f"/v1/images/{encode_path_param(image_id)}/github",
222
+ json_body=body,
223
+ query={"workspace_id": workspace_id},
224
+ )
225
+
226
+ def unlink_github(self, image_id: str, *, workspace_id: str | None = None) -> APICustomImage:
227
+ """DELETE /v1/images/:image_id/github — scope `images:write`. Unlinks the repository.
228
+ Doc: https://docs.vertracloud.app/api-reference/endpoint/images/github-unlink
229
+ """
230
+ return self._c.request_json(
231
+ "DELETE", f"/v1/images/{encode_path_param(image_id)}/github", query={"workspace_id": workspace_id}
232
+ )
233
+
234
+ def check(self, body: CheckDockerfileBody) -> APIDockerfileCheck:
235
+ """POST /v1/images/check — scope `images:read`. Checks a `Dockerfile` and the paths of its
236
+ context before building; creates nothing. `refusals` would block the build, `warnings` would not.
237
+ Doc: https://docs.vertracloud.app/api-reference/endpoint/images/check
238
+ """
239
+ return self._c.request_json("POST", "/v1/images/check", json_body=body)
@@ -56,11 +56,25 @@ class SnapshotsResource:
56
56
  query={"scope": scope},
57
57
  )
58
58
 
59
- def restore(self, resource_id: str, snapshot_id: str, scope: SnapshotScope) -> APISnapshotRestoreResponse:
60
- """POST /v1/users/:id/snapshots/:snapshot_id/restore — scope `snapshots:write`."""
59
+ def restore(
60
+ self,
61
+ resource_id: str,
62
+ snapshot_id: str,
63
+ scope: SnapshotScope,
64
+ *,
65
+ restore_image_version: bool | None = None,
66
+ ) -> APISnapshotRestoreResponse:
67
+ """POST /v1/users/:id/snapshots/:snapshot_id/restore — scope `snapshots:write`.
68
+ For database snapshots it can fail with `SNAPSHOT_VERSION_NEWER`.
69
+ `restore_image_version` applies to an app on a custom image: `True` also goes back to the image
70
+ version the app ran when the snapshot was taken, if it still exists (the response then carries
71
+ `warning` when it does not). Omitted, the API restores only the data."""
72
+ body: dict[str, bool] = (
73
+ {} if restore_image_version is None else {"restore_image_version": restore_image_version}
74
+ )
61
75
  return self._c.request_json(
62
76
  "POST",
63
77
  f"/v1/users/{encode_path_param(resource_id)}/snapshots/{encode_path_param(snapshot_id)}/restore",
64
- json_body={},
78
+ json_body=body,
65
79
  query={"scope": scope},
66
80
  )
@@ -66,6 +66,7 @@ class CreateAppBody(TypedDict, total=False):
66
66
  main: str
67
67
  subdomain: str
68
68
  version: str
69
+ language: ApplicationLanguage
69
70
  name: str
70
71
  description: str | None
71
72
  workspace_id: str
@@ -103,7 +104,6 @@ class RollbackAppBody(TypedDict, total=False):
103
104
 
104
105
 
105
106
  class RestartAppBody(TypedDict, total=False):
106
- cleanup_old_runtime_language: str
107
107
  reinstall_dependencies: bool
108
108
  force_build: bool
109
109
 
@@ -145,6 +145,9 @@ class SetSubdomainBody(TypedDict):
145
145
 
146
146
  class PublishAppBody(TypedDict, total=False):
147
147
  subdomain: str
148
+ # Only for an app on a custom image: the port the image listens on (1-65535).
149
+ # Required when the app was created without web; ignored for other apps.
150
+ port: int
148
151
 
149
152
 
150
153
  class PurgeCacheBody(TypedDict, total=False):
@@ -170,15 +173,23 @@ class MoveFileBody(TypedDict, total=False):
170
173
  workspace_id: str
171
174
 
172
175
 
173
- class CreateWebhookBody(TypedDict):
174
- """Body for `apps.deploys.webhook.create` — all 4 fields are required."""
175
-
176
+ class _CreateWebhookBodyRequired(TypedDict):
176
177
  owner: str
177
178
  repo_name: str
178
179
  repo_id: str
179
180
  account_id: str
180
181
 
181
182
 
183
+ class CreateWebhookBody(_CreateWebhookBodyRequired, total=False):
184
+ """Body for `apps.deploys.webhook.create` — the first 4 fields are required.
185
+
186
+ `branch` is the only branch the auto-deploy reacts to. Without it, a push on `main` or `master`
187
+ deploys, as it always did. Linking again with another `branch` changes it; linking again
188
+ without one goes back to `main` or `master`."""
189
+
190
+ branch: str
191
+
192
+
182
193
  class _CreateDatabaseBodyRequired(TypedDict):
183
194
  name: str
184
195
  ram: int
@@ -192,6 +203,10 @@ class CreateDatabaseBody(_CreateDatabaseBodyRequired, total=False):
192
203
  type: int
193
204
  workspace_id: str
194
205
  snapshot_id: str
206
+ version: str
207
+ """Engine major line (see `databases.versions()`). Omitted, the recommended one is used; with
208
+ `snapshot_id`, omit it to keep the snapshot's line (an older one fails with `SNAPSHOT_VERSION_NEWER`,
209
+ an unknown one with `INVALID_DATABASE_VERSION`)."""
195
210
 
196
211
 
197
212
  class UpdateDatabaseBody(TypedDict, total=False):
@@ -295,7 +310,19 @@ class PutBillingDetailsBody(_PutBillingDetailsRequired, total=False):
295
310
  # -- responses -------------------------------------------------------------
296
311
 
297
312
  ApplicationLanguage = Literal[
298
- "javascript", "typescript", "bun", "python", "static", "php", "go", "ruby", "java", "rust"
313
+ "javascript",
314
+ "typescript",
315
+ "bun",
316
+ "python",
317
+ "static",
318
+ "php",
319
+ "go",
320
+ "ruby",
321
+ "java",
322
+ "rust",
323
+ "deno",
324
+ "dotnet",
325
+ "elixir",
299
326
  ]
300
327
  ApplicationStatus = Literal["up", "down", "building"]
301
328
  ApplicationBuildError = Literal[
@@ -381,12 +408,20 @@ class _APIApplicationRequired(TypedDict):
381
408
  shield_cooldown: APIApplicationShieldCooldown | None
382
409
  deploy: APIApplicationDeploySettings
383
410
  last_build_error: ApplicationBuildError | None
411
+ # Custom Docker image the app runs from; `None` for a regular app.
412
+ custom_image: APIApplicationCustomImage | None
384
413
 
385
414
 
386
415
  class APIApplication(_APIApplicationRequired, total=False):
387
416
  description: str
388
417
  github: APIApplicationGithub | None
389
- missing_dependencies: list[str]
418
+
419
+
420
+ class APIApplicationCreated(APIApplication, total=False):
421
+ """Response of `apps.create`. `warning` is set when the app was created from the snapshot of an app
422
+ on a custom image and the recorded image version no longer exists: the app uses the current version."""
423
+
424
+ warning: SnapshotImageWarning
390
425
 
391
426
 
392
427
  class APIApplicationNetwork(TypedDict):
@@ -427,8 +462,11 @@ class _APIAppFileUploadResponseRequired(TypedDict):
427
462
 
428
463
 
429
464
  class APIAppFileUploadResponse(_APIAppFileUploadResponseRequired, total=False):
430
- missing_dependencies: list[str]
431
465
  removed_directories: list[str]
466
+ # Only for a commit to an app on a custom image: the image build the commit started. `None` otherwise.
467
+ image_build: APIApplicationImageBuild | None
468
+ # Readable summary for the CLI, e.g. that the image is rebuilding.
469
+ message: str
432
470
 
433
471
 
434
472
  class APIApplicationMetric(TypedDict):
@@ -520,6 +558,16 @@ class APIWebhookUrl(TypedDict):
520
558
  url: str
521
559
 
522
560
 
561
+ class APIApplicationWebhook(TypedDict):
562
+ """Auto-deploy webhook of an application and the repository it is linked to.
563
+ `branch` is `None` for a link created without one (reacts to `main` or `master`)."""
564
+
565
+ webhook_url: str
566
+ repo_owner: str
567
+ repo_name: str
568
+ branch: str | None
569
+
570
+
523
571
  class APISubdomainResponse(TypedDict):
524
572
  subdomain: str
525
573
 
@@ -554,12 +602,19 @@ class APIApplicationFileContent(TypedDict):
554
602
  last_modified: str
555
603
 
556
604
 
557
- class APIApplicationScanSuggestions(TypedDict, total=False):
605
+ class _APIApplicationScanSuggestionsRequired(TypedDict):
606
+ language: ApplicationLanguage | None
607
+ main: str | None
608
+ version: str | None
609
+ has_manifest: bool
610
+ candidates: list[ApplicationLanguage]
611
+
612
+
613
+ class APIApplicationScanSuggestions(_APIApplicationScanSuggestionsRequired, total=False):
558
614
  suggested_start_command: str
559
615
  suggested_build_command: str
560
616
  suggested_memory_mb: int
561
617
  suggested_public_web: bool
562
- missing_dependencies: list[str]
563
618
 
564
619
 
565
620
  class APIRuntimeEntry(TypedDict):
@@ -587,6 +642,18 @@ class APIDatabase(TypedDict):
587
642
  updated_at: str
588
643
  last_snapshot: str | None
589
644
  offline_since: str | None
645
+ version: str
646
+ """Engine major line, e.g. `"18"` or `"8.0"`; the patch is never exposed. Not limited to the
647
+ catalog: older databases may report a line that is no longer offered for new ones."""
648
+
649
+
650
+ class APIDatabaseVersionCatalog(TypedDict):
651
+ recommended: str
652
+ available: list[str]
653
+
654
+
655
+ APIDatabaseVersions = dict[str, APIDatabaseVersionCatalog]
656
+ """Catalog keyed by the engine `type` as a string (`"1"`, `"2"`, ...)."""
590
657
 
591
658
 
592
659
  class APIDatabaseNetwork(TypedDict):
@@ -649,10 +716,15 @@ class APIGroupedResourceSnapshots(TypedDict):
649
716
  snapshots: list[APIResourceSnapshot]
650
717
 
651
718
 
652
- class APISnapshotRestoreResponse(TypedDict):
719
+ class _APISnapshotRestoreResponseRequired(TypedDict):
653
720
  message: str
654
721
 
655
722
 
723
+ class APISnapshotRestoreResponse(_APISnapshotRestoreResponseRequired, total=False):
724
+ # Set when `restore_image_version` was asked but the recorded image version is gone: only data was restored.
725
+ warning: SnapshotImageWarning
726
+
727
+
656
728
  class APIUser(TypedDict):
657
729
  id: str
658
730
  name: str
@@ -1116,3 +1188,163 @@ class _APIBlobCustomDomainRequired(TypedDict):
1116
1188
  class APIBlobCustomDomain(_APIBlobCustomDomainRequired, total=False):
1117
1189
  # Only while the domain is not active: the TXT record that proves ownership.
1118
1190
  verification: APIBlobCustomDomainVerification | None
1191
+
1192
+
1193
+ # -- Custom Docker images: `images.*` and `apps.create_from_image` ---------------------------
1194
+
1195
+ CustomImageStatus = Literal["building", "success", "failed"]
1196
+ CustomImageBuildSource = Literal["upload", "commit", "github"]
1197
+ CustomImageOwner = Literal["account", "system"]
1198
+ DockerfileIssueCode = Literal[
1199
+ "missing_dockerfile",
1200
+ "syntax_frontend",
1201
+ "run_security_insecure",
1202
+ "run_network_host",
1203
+ "mount_ssh",
1204
+ "mount_secret",
1205
+ "workdir_app",
1206
+ "env_file_in_context",
1207
+ "copy_env_file",
1208
+ "volume_outside_app",
1209
+ "volume_refused",
1210
+ ]
1211
+
1212
+
1213
+ class DockerfileIssue(TypedDict):
1214
+ """Refusals block the build; warnings do not. `line` is `None` when no line applies."""
1215
+
1216
+ code: DockerfileIssueCode
1217
+ line: int | None
1218
+
1219
+
1220
+ class APIDockerfileCheck(TypedDict):
1221
+ refusals: list[DockerfileIssue]
1222
+ warnings: list[DockerfileIssue]
1223
+ suggested_port: int
1224
+
1225
+
1226
+ class APICustomImageApp(TypedDict):
1227
+ id: str
1228
+ name: str
1229
+ version_id: str | None
1230
+ outdated: bool
1231
+
1232
+
1233
+ class APIApplicationCustomImage(APICustomImageApp):
1234
+ """`APIApplication.custom_image`: the image the app runs from and the port it listens on."""
1235
+
1236
+ image_id: str
1237
+ port: int | None
1238
+
1239
+
1240
+ class APICustomImageGithub(TypedDict):
1241
+ repository: str
1242
+ branch: str
1243
+
1244
+
1245
+ class APICustomImage(TypedDict):
1246
+ id: str
1247
+ name: str
1248
+ # Free-text description, up to 128 characters; None when the image has none.
1249
+ description: str | None
1250
+ owner: CustomImageOwner
1251
+ status: CustomImageStatus
1252
+ error: str | None
1253
+ digest: str | None
1254
+ uncompressed_bytes: int | None
1255
+ layer_count: int | None
1256
+ # Port suggested for a new app on this image (first EXPOSE, or 80); None for images created before the field.
1257
+ suggested_port: int | None
1258
+ apps: list[APICustomImageApp]
1259
+ github: APICustomImageGithub | None
1260
+ current_version_id: str | None
1261
+ created_at: str
1262
+ updated_at: str
1263
+
1264
+
1265
+ class APICustomImageVersion(TypedDict):
1266
+ id: str
1267
+ digest: str
1268
+ source: CustomImageBuildSource
1269
+ message: str | None
1270
+ created_at: str
1271
+ in_use: bool
1272
+
1273
+
1274
+ class APICustomImageContext(TypedDict):
1275
+ url: str
1276
+
1277
+
1278
+ class APICustomImageDeleteResult(TypedDict):
1279
+ deleted_apps: list[str]
1280
+
1281
+
1282
+ class _CreateCustomImageRequired(TypedDict):
1283
+ name: str
1284
+
1285
+
1286
+ class CreateCustomImageBody(_CreateCustomImageRequired, total=False):
1287
+ """`POST /v1/images` fields. Exactly one source: a `file` passed to `images.create`, or
1288
+ `github_repository` (`owner/name`). `github_branch` is optional; without it the repository's default
1289
+ branch is used. A repository in your GitHub App installations is linked (each push to the branch
1290
+ rebuilds the image); a public repository outside them is built once, with no link.
1291
+ `description` is optional free text, up to 128 characters (an empty value means none)."""
1292
+
1293
+ description: str
1294
+ github_repository: str
1295
+ github_branch: str
1296
+ workspace_id: str
1297
+
1298
+
1299
+ class _CustomImageGithubRequired(TypedDict):
1300
+ repository: str
1301
+
1302
+
1303
+ class UpdateCustomImageBody(TypedDict, total=False):
1304
+ """`PATCH /v1/images/:image_id` fields; send only what changes (at least one). `name` is 1 to 50
1305
+ characters, unique among your images. `description` is up to 128 characters; `None` or an empty
1306
+ string removes it."""
1307
+
1308
+ name: str
1309
+ description: str | None
1310
+
1311
+
1312
+ class CustomImageGithubBody(_CustomImageGithubRequired, total=False):
1313
+ """`PUT /v1/images/:image_id/github` fields. `branch` is optional: without it the repository's
1314
+ default branch is used. The repository must be in one of your GitHub App installations."""
1315
+
1316
+ branch: str
1317
+
1318
+
1319
+ class CheckDockerfileBody(TypedDict):
1320
+ dockerfile: str
1321
+ files: list[str]
1322
+
1323
+
1324
+ class APIApplicationImageBuild(TypedDict):
1325
+ """The image build started by a commit to an app on a custom image (`apps.files.upload`)."""
1326
+
1327
+ image_id: str
1328
+ build_id: str
1329
+
1330
+
1331
+ # Warning of a snapshot restore or of a create from a snapshot: the image version the snapshot recorded was
1332
+ # removed by retention, so the app stays on the image's current version.
1333
+ SnapshotImageWarning = Literal["IMAGE_VERSION_NOT_FOUND"]
1334
+
1335
+
1336
+ class _CreateAppFromImageRequired(TypedDict):
1337
+ image_id: str
1338
+ name: str
1339
+ memory: int
1340
+ web: bool
1341
+
1342
+
1343
+ class CreateAppFromImageBody(_CreateAppFromImageRequired, total=False):
1344
+ """`POST /v1/apps/image`. `subdomain` and `port` apply only with `web: True`."""
1345
+
1346
+ description: str
1347
+ subdomain: str
1348
+ port: int
1349
+ envs: list[EnvEntry]
1350
+ workspace_id: str
File without changes