vertracloud-api 0.2.2__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/CHANGELOG.md +26 -0
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/PKG-INFO +24 -4
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/README.md +23 -3
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/pyproject.toml +4 -1
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/src/vertracloud/client.py +36 -6
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/src/vertracloud/resources/apps.py +51 -25
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/src/vertracloud/transport.py +36 -1
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/src/vertracloud/types.py +106 -29
- vertracloud_api-0.2.2/.github/workflows/ci.yml +0 -24
- vertracloud_api-0.2.2/.github/workflows/release.yml +0 -37
- vertracloud_api-0.2.2/.release-please-manifest.json +0 -1
- vertracloud_api-0.2.2/release-please-config.json +0 -50
- vertracloud_api-0.2.2/tests/__init__.py +0 -0
- vertracloud_api-0.2.2/tests/fake_transport.py +0 -80
- vertracloud_api-0.2.2/tests/routes_matrix.py +0 -138
- vertracloud_api-0.2.2/tests/test_blob.py +0 -336
- vertracloud_api-0.2.2/tests/test_client.py +0 -159
- vertracloud_api-0.2.2/tests/test_contract.py +0 -355
- vertracloud_api-0.2.2/tests/test_multipart_and_binary.py +0 -156
- vertracloud_api-0.2.2/tests/test_routes_matrix.py +0 -183
- vertracloud_api-0.2.2/tests/test_sse.py +0 -90
- vertracloud_api-0.2.2/uv.lock +0 -517
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/.gitignore +0 -0
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/LICENSE +0 -0
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/src/vertracloud/__init__.py +0 -0
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/src/vertracloud/errors.py +0 -0
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/src/vertracloud/py.typed +0 -0
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/src/vertracloud/resources/__init__.py +0 -0
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/src/vertracloud/resources/account.py +0 -0
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/src/vertracloud/resources/billing.py +0 -0
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/src/vertracloud/resources/blob.py +0 -0
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/src/vertracloud/resources/databases.py +0 -0
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/src/vertracloud/resources/snapshots.py +0 -0
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/src/vertracloud/resources/workspaces.py +0 -0
- {vertracloud_api-0.2.2 → vertracloud_api-0.3.1}/src/vertracloud/sse.py +0 -0
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.1](https://github.com/vertracloud/sdk-api-py/compare/v0.3.0...v0.3.1) (2026-10-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **apps:** Deno, .NET and Elixir and language on create ([c7a7bb3](https://github.com/vertracloud/sdk-api-py/commit/c7a7bb3e4141d8f0c45853807acfd49719841100))
|
|
9
|
+
* **apps:** remove cleanup_old_runtime_language from the restart body ([e8aa75d](https://github.com/vertracloud/sdk-api-py/commit/e8aa75daf69752af8ea16048e84e85857f613ff4))
|
|
10
|
+
|
|
11
|
+
## [0.3.0](https://github.com/vertracloud/sdk-api-py/compare/v0.2.2...v0.3.0) (2026-10-22)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### ⚠ BREAKING CHANGES
|
|
15
|
+
|
|
16
|
+
* plan names and multiple custom domains per app
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* plan names and multiple custom domains per app ([1df9eab](https://github.com/vertracloud/sdk-api-py/commit/1df9eab6d548a6136409652419afd26dbd6c8b41))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* drop the Authorization header when a redirect changes scheme, host or port ([70f5451](https://github.com/vertracloud/sdk-api-py/commit/70f5451d8ee02579344ac2fde46a16ed1b9952eb))
|
|
26
|
+
* reject empty and dot path segments before sending a request ([2964db9](https://github.com/vertracloud/sdk-api-py/commit/2964db9c6da64ce0427ee4b4c2975b012edbad8e))
|
|
27
|
+
* **upload:** escape quotes and reject CR/LF in multipart Content-Disposition ([e56ebb0](https://github.com/vertracloud/sdk-api-py/commit/e56ebb0ef9717cd45d766d94905225ede8c4466e))
|
|
28
|
+
|
|
3
29
|
## [0.2.2](https://github.com/vertracloud/sdk-api-py/compare/v0.2.1...v0.2.2) (2026-10-18)
|
|
4
30
|
|
|
5
31
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vertracloud-api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -48,7 +48,8 @@ from vertracloud import VertraClient
|
|
|
48
48
|
client = VertraClient(api_key=os.environ["VERTRA_API_KEY"])
|
|
49
49
|
|
|
50
50
|
me = client.account.get()
|
|
51
|
-
|
|
51
|
+
plan = me["plan"]
|
|
52
|
+
print(f"Hi {me['name']}! Plan: {plan['name'] if plan else 'none'}")
|
|
52
53
|
|
|
53
54
|
for app in me["applications"]:
|
|
54
55
|
print(app["id"], app["name"], app["status"])
|
|
@@ -99,6 +100,25 @@ metrics = client.apps.metrics(app_id, range="24h")
|
|
|
99
100
|
logs = client.apps.logs(app_id)
|
|
100
101
|
```
|
|
101
102
|
|
|
103
|
+
### Custom domains
|
|
104
|
+
|
|
105
|
+
An app can have several domains, each with its own port, path prefix or redirect. `add` returns the created
|
|
106
|
+
domain and its automatic `www` (or root) companion; the limit depends on your plan.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
domains = client.apps.domains.add(app_id, {"hostname": "example.com", "port": 8080})
|
|
110
|
+
main = domains[0]
|
|
111
|
+
|
|
112
|
+
for record in client.apps.domains.dns(app_id, main["id"]):
|
|
113
|
+
print(record["type"], record["name"], record["value"], record["status"])
|
|
114
|
+
|
|
115
|
+
client.apps.domains.update(app_id, domains[1]["id"], {"redirect_to": main["id"], "redirect_status": 301})
|
|
116
|
+
client.apps.domains.remove(app_id, main["id"])
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`client.apps.domains.connect(app_id, domain_id)` returns a link that configures the DNS records at providers that
|
|
120
|
+
support it, or raises a 404 `DOMAIN_CONNECT_UNAVAILABLE`.
|
|
121
|
+
|
|
102
122
|
### Releases and rollback
|
|
103
123
|
|
|
104
124
|
```python
|
|
@@ -157,7 +177,7 @@ except vertracloud.WaitTimeoutError: # still building after `timeout` seconds
|
|
|
157
177
|
...
|
|
158
178
|
```
|
|
159
179
|
|
|
160
|
-
|
|
180
|
+
While the build runs, calls that need a stopped or running app can fail with `APP_BUILDING` (409).
|
|
161
181
|
|
|
162
182
|
### Realtime logs (SSE)
|
|
163
183
|
|
|
@@ -225,7 +245,7 @@ client = VertraClient(api_key="test", transport=my_fake_transport)
|
|
|
225
245
|
|
|
226
246
|
| Domain | Resource | Routes |
|
|
227
247
|
|---|---|---|
|
|
228
|
-
| Apps | `client.apps` (+ `.deploys`, `.releases`, `.network`, `.envs`, `.files`) |
|
|
248
|
+
| Apps | `client.apps` (+ `.deploys`, `.releases`, `.domains`, `.network`, `.envs`, `.files`) | 42 |
|
|
229
249
|
| Databases | `client.databases` (+ `.credentials`) | 13 |
|
|
230
250
|
| Snapshots | `client.snapshots` | 5 |
|
|
231
251
|
| Account | `client.account` (+ `.sessions`, `.folders`, `.favorites`) | 10 |
|
|
@@ -29,7 +29,8 @@ from vertracloud import VertraClient
|
|
|
29
29
|
client = VertraClient(api_key=os.environ["VERTRA_API_KEY"])
|
|
30
30
|
|
|
31
31
|
me = client.account.get()
|
|
32
|
-
|
|
32
|
+
plan = me["plan"]
|
|
33
|
+
print(f"Hi {me['name']}! Plan: {plan['name'] if plan else 'none'}")
|
|
33
34
|
|
|
34
35
|
for app in me["applications"]:
|
|
35
36
|
print(app["id"], app["name"], app["status"])
|
|
@@ -80,6 +81,25 @@ metrics = client.apps.metrics(app_id, range="24h")
|
|
|
80
81
|
logs = client.apps.logs(app_id)
|
|
81
82
|
```
|
|
82
83
|
|
|
84
|
+
### Custom domains
|
|
85
|
+
|
|
86
|
+
An app can have several domains, each with its own port, path prefix or redirect. `add` returns the created
|
|
87
|
+
domain and its automatic `www` (or root) companion; the limit depends on your plan.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
domains = client.apps.domains.add(app_id, {"hostname": "example.com", "port": 8080})
|
|
91
|
+
main = domains[0]
|
|
92
|
+
|
|
93
|
+
for record in client.apps.domains.dns(app_id, main["id"]):
|
|
94
|
+
print(record["type"], record["name"], record["value"], record["status"])
|
|
95
|
+
|
|
96
|
+
client.apps.domains.update(app_id, domains[1]["id"], {"redirect_to": main["id"], "redirect_status": 301})
|
|
97
|
+
client.apps.domains.remove(app_id, main["id"])
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`client.apps.domains.connect(app_id, domain_id)` returns a link that configures the DNS records at providers that
|
|
101
|
+
support it, or raises a 404 `DOMAIN_CONNECT_UNAVAILABLE`.
|
|
102
|
+
|
|
83
103
|
### Releases and rollback
|
|
84
104
|
|
|
85
105
|
```python
|
|
@@ -138,7 +158,7 @@ except vertracloud.WaitTimeoutError: # still building after `timeout` seconds
|
|
|
138
158
|
...
|
|
139
159
|
```
|
|
140
160
|
|
|
141
|
-
|
|
161
|
+
While the build runs, calls that need a stopped or running app can fail with `APP_BUILDING` (409).
|
|
142
162
|
|
|
143
163
|
### Realtime logs (SSE)
|
|
144
164
|
|
|
@@ -206,7 +226,7 @@ client = VertraClient(api_key="test", transport=my_fake_transport)
|
|
|
206
226
|
|
|
207
227
|
| Domain | Resource | Routes |
|
|
208
228
|
|---|---|---|
|
|
209
|
-
| Apps | `client.apps` (+ `.deploys`, `.releases`, `.network`, `.envs`, `.files`) |
|
|
229
|
+
| Apps | `client.apps` (+ `.deploys`, `.releases`, `.domains`, `.network`, `.envs`, `.files`) | 42 |
|
|
210
230
|
| Databases | `client.databases` (+ `.credentials`) | 13 |
|
|
211
231
|
| Snapshots | `client.snapshots` | 5 |
|
|
212
232
|
| Account | `client.account` (+ `.sessions`, `.folders`, `.favorites`) | 10 |
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vertracloud-api"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
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"
|
|
@@ -21,6 +21,9 @@ Issues = "https://github.com/vertracloud/sdk-api-py/issues"
|
|
|
21
21
|
[project.optional-dependencies]
|
|
22
22
|
dev = ["pytest>=8", "ruff>=0.6", "mypy>=1.11", "build>=1.2"]
|
|
23
23
|
|
|
24
|
+
[tool.hatch.build.targets.sdist]
|
|
25
|
+
include = ["src", "README.md", "LICENSE", "CHANGELOG.md", "pyproject.toml"]
|
|
26
|
+
|
|
24
27
|
[tool.hatch.build.targets.wheel]
|
|
25
28
|
packages = ["src/vertracloud"]
|
|
26
29
|
|
|
@@ -9,14 +9,14 @@ import mimetypes
|
|
|
9
9
|
import uuid
|
|
10
10
|
from collections.abc import Iterable
|
|
11
11
|
from typing import Any
|
|
12
|
-
from urllib.parse import quote, urlencode
|
|
12
|
+
from urllib.parse import quote, unquote, urlencode
|
|
13
13
|
|
|
14
14
|
from .errors import error_from_response
|
|
15
15
|
from .sse import SseStream
|
|
16
16
|
from .transport import Response, Transport, UrllibTransport
|
|
17
17
|
from .types import JsonValue
|
|
18
18
|
|
|
19
|
-
__version__ = "0.
|
|
19
|
+
__version__ = "0.3.1" # 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"
|
|
@@ -25,9 +25,28 @@ DEFAULT_TIMEOUT = 30.0
|
|
|
25
25
|
LARGE_PAYLOAD_TIMEOUT = 120.0
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def _is_dot_segment(value: str) -> bool:
|
|
29
|
+
current = value
|
|
30
|
+
for _ in range(4):
|
|
31
|
+
if current in ("", ".", ".."):
|
|
32
|
+
return True
|
|
33
|
+
decoded = unquote(current)
|
|
34
|
+
if decoded == current:
|
|
35
|
+
return False
|
|
36
|
+
current = decoded
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
28
40
|
def encode_path_param(value: str) -> str:
|
|
29
|
-
"""`quote(safe="")` — a path param never lets `/`, `?`, etc. leak into the URL structure.
|
|
30
|
-
|
|
41
|
+
"""`quote(safe="")` — a path param never lets `/`, `?`, etc. leak into the URL structure.
|
|
42
|
+
|
|
43
|
+
Raises `ValueError` (before any request) for an empty value, `.` or `..`, also when percent-encoded:
|
|
44
|
+
servers and URL parsers collapse dot-segments, so the call would reach a different route.
|
|
45
|
+
"""
|
|
46
|
+
text = str(value)
|
|
47
|
+
if _is_dot_segment(text):
|
|
48
|
+
raise ValueError(f"Invalid path segment: {text!r}")
|
|
49
|
+
return quote(text, safe="")
|
|
31
50
|
|
|
32
51
|
|
|
33
52
|
class MultipartField:
|
|
@@ -49,13 +68,24 @@ class MultipartField:
|
|
|
49
68
|
self.content_type = content_type
|
|
50
69
|
|
|
51
70
|
|
|
71
|
+
def _quote_param(value: str, what: str) -> str:
|
|
72
|
+
"""Quote a Content-Disposition parameter like Go's `mime/multipart` (`\\` and `"` escaped).
|
|
73
|
+
|
|
74
|
+
CR/LF are rejected instead of rewritten: silently changing a file name would surprise the
|
|
75
|
+
caller, and a header line break is never a legitimate name.
|
|
76
|
+
"""
|
|
77
|
+
if "\r" in value or "\n" in value:
|
|
78
|
+
raise ValueError(f"multipart {what} must not contain CR/LF")
|
|
79
|
+
return value.replace("\\", "\\\\").replace('"', '\\"')
|
|
80
|
+
|
|
81
|
+
|
|
52
82
|
def _build_multipart(fields: Iterable[MultipartField]) -> tuple[bytes, str]:
|
|
53
83
|
boundary = f"----vertracloud-{uuid.uuid4().hex}"
|
|
54
84
|
parts: list[bytes] = []
|
|
55
85
|
for field in fields:
|
|
56
|
-
disposition = f'form-data; name="{field.name}"'
|
|
86
|
+
disposition = f'form-data; name="{_quote_param(field.name, "field name")}"'
|
|
57
87
|
if field.filename:
|
|
58
|
-
disposition += f'; filename="{field.filename}"'
|
|
88
|
+
disposition += f'; filename="{_quote_param(field.filename, "filename")}"'
|
|
59
89
|
header = f"--{boundary}\r\nContent-Disposition: {disposition}\r\n"
|
|
60
90
|
content_type = field.content_type
|
|
61
91
|
if content_type is None and field.filename:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""`apps` domain —
|
|
1
|
+
"""`apps` domain — 42 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
|
"""
|
|
@@ -12,9 +12,13 @@ from ..client import LARGE_PAYLOAD_TIMEOUT, MultipartField, encode_path_param
|
|
|
12
12
|
from ..errors import AppBuildFailedError, WaitTimeoutError
|
|
13
13
|
from ..sse import SseStream
|
|
14
14
|
from ..types import (
|
|
15
|
+
AddDomainBody,
|
|
15
16
|
APIAppFileUploadResponse,
|
|
16
17
|
APIApplication,
|
|
17
18
|
APIApplicationDeployment,
|
|
19
|
+
APIApplicationDomain,
|
|
20
|
+
APIApplicationDomainConnect,
|
|
21
|
+
APIApplicationDomainDnsRecord,
|
|
18
22
|
APIApplicationEnvironment,
|
|
19
23
|
APIApplicationFileContent,
|
|
20
24
|
APIApplicationFileTree,
|
|
@@ -26,14 +30,11 @@ from ..types import (
|
|
|
26
30
|
APIApplicationStatus,
|
|
27
31
|
APIApplicationStatusShort,
|
|
28
32
|
APIApplicationWebPublish,
|
|
29
|
-
APICustomDomainResponse,
|
|
30
|
-
APIDnsRecord,
|
|
31
33
|
APIRuntimesResponse,
|
|
32
34
|
APISubdomainResponse,
|
|
33
35
|
APIWebhookUrl,
|
|
34
36
|
CreateAppBody,
|
|
35
37
|
CreateWebhookBody,
|
|
36
|
-
CustomDomainBody,
|
|
37
38
|
EnvironmentSetBody,
|
|
38
39
|
JsonValue,
|
|
39
40
|
MoveFileBody,
|
|
@@ -43,6 +44,7 @@ from ..types import (
|
|
|
43
44
|
RollbackAppBody,
|
|
44
45
|
SetSubdomainBody,
|
|
45
46
|
UpdateAppConfigBody,
|
|
47
|
+
UpdateDomainBody,
|
|
46
48
|
)
|
|
47
49
|
|
|
48
50
|
if TYPE_CHECKING:
|
|
@@ -51,6 +53,8 @@ if TYPE_CHECKING:
|
|
|
51
53
|
# Aliases so `list[...]` doesn't resolve to the `list` method inside the class itself.
|
|
52
54
|
EnvList = list[APIApplicationEnvironment]
|
|
53
55
|
FileTreeList = list[APIApplicationFileTree]
|
|
56
|
+
DomainList = list[APIApplicationDomain]
|
|
57
|
+
DomainDnsList = list[APIApplicationDomainDnsRecord]
|
|
54
58
|
|
|
55
59
|
|
|
56
60
|
class AppDeploysWebhookResource:
|
|
@@ -167,45 +171,66 @@ class AppReleasesResource:
|
|
|
167
171
|
)
|
|
168
172
|
|
|
169
173
|
|
|
170
|
-
class
|
|
174
|
+
class AppDomainsResource:
|
|
171
175
|
def __init__(self, client: VertraClient) -> None:
|
|
172
176
|
self._c = client
|
|
173
177
|
|
|
174
|
-
def
|
|
175
|
-
"""GET /v1/apps/:id/
|
|
178
|
+
def list(self, app_id: str, *, workspace_id: str | None = None) -> DomainList:
|
|
179
|
+
"""GET /v1/apps/:id/domains — scope `apps:read`. Automatic companions included."""
|
|
176
180
|
return self._c.request_json(
|
|
177
|
-
"GET", f"/v1/apps/{encode_path_param(app_id)}/
|
|
181
|
+
"GET", f"/v1/apps/{encode_path_param(app_id)}/domains", query={"workspace_id": workspace_id}
|
|
178
182
|
)
|
|
179
183
|
|
|
180
|
-
def
|
|
181
|
-
"""POST /v1/apps/:id/
|
|
182
|
-
Doc: https://docs.vertracloud.app/api-reference/endpoint/apps/network/customdomain
|
|
183
|
-
"""
|
|
184
|
+
def add(self, app_id: str, body: AddDomainBody, *, workspace_id: str | None = None) -> DomainList:
|
|
185
|
+
"""POST /v1/apps/:id/domains — scope `apps:write`. Returns the created domain and its companion."""
|
|
184
186
|
return self._c.request_json(
|
|
185
187
|
"POST",
|
|
186
|
-
f"/v1/apps/{encode_path_param(app_id)}/
|
|
188
|
+
f"/v1/apps/{encode_path_param(app_id)}/domains",
|
|
187
189
|
json_body=body,
|
|
188
190
|
query={"workspace_id": workspace_id},
|
|
189
191
|
)
|
|
190
192
|
|
|
191
|
-
def
|
|
192
|
-
|
|
193
|
+
def update(
|
|
194
|
+
self, app_id: str, domain_id: str, body: UpdateDomainBody, *, workspace_id: str | None = None
|
|
195
|
+
) -> DomainList:
|
|
196
|
+
"""PATCH /v1/apps/:id/domains/:domain_id — scope `apps:write`."""
|
|
193
197
|
return self._c.request_json(
|
|
194
|
-
"
|
|
198
|
+
"PATCH",
|
|
199
|
+
f"/v1/apps/{encode_path_param(app_id)}/domains/{encode_path_param(domain_id)}",
|
|
200
|
+
json_body=body,
|
|
201
|
+
query={"workspace_id": workspace_id},
|
|
195
202
|
)
|
|
196
203
|
|
|
204
|
+
def remove(self, app_id: str, domain_id: str, *, workspace_id: str | None = None) -> None:
|
|
205
|
+
"""DELETE /v1/apps/:id/domains/:domain_id — scope `apps:write`. Also removes the automatic companion."""
|
|
206
|
+
return self._c.request_json(
|
|
207
|
+
"DELETE",
|
|
208
|
+
f"/v1/apps/{encode_path_param(app_id)}/domains/{encode_path_param(domain_id)}",
|
|
209
|
+
query={"workspace_id": workspace_id},
|
|
210
|
+
)
|
|
197
211
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
self._c
|
|
201
|
-
|
|
212
|
+
def dns(self, app_id: str, domain_id: str, *, workspace_id: str | None = None) -> DomainDnsList:
|
|
213
|
+
"""GET /v1/apps/:id/domains/:domain_id/dns — scope `apps:read`."""
|
|
214
|
+
return self._c.request_json(
|
|
215
|
+
"GET",
|
|
216
|
+
f"/v1/apps/{encode_path_param(app_id)}/domains/{encode_path_param(domain_id)}/dns",
|
|
217
|
+
query={"workspace_id": workspace_id},
|
|
218
|
+
)
|
|
202
219
|
|
|
203
|
-
def
|
|
204
|
-
"""
|
|
220
|
+
def connect(self, app_id: str, domain_id: str, *, workspace_id: str | None = None) -> APIApplicationDomainConnect:
|
|
221
|
+
"""POST /v1/apps/:id/domains/:domain_id/connect — scope `apps:write`. Raises a 404
|
|
222
|
+
`DOMAIN_CONNECT_UNAVAILABLE` when the DNS provider does not support it."""
|
|
205
223
|
return self._c.request_json(
|
|
206
|
-
"
|
|
224
|
+
"POST",
|
|
225
|
+
f"/v1/apps/{encode_path_param(app_id)}/domains/{encode_path_param(domain_id)}/connect",
|
|
226
|
+
query={"workspace_id": workspace_id},
|
|
207
227
|
)
|
|
208
228
|
|
|
229
|
+
|
|
230
|
+
class AppNetworkResource:
|
|
231
|
+
def __init__(self, client: VertraClient) -> None:
|
|
232
|
+
self._c = client
|
|
233
|
+
|
|
209
234
|
def purge_cache(self, app_id: str, body: PurgeCacheBody | None = None, *, workspace_id: str | None = None) -> None:
|
|
210
235
|
"""POST /v1/apps/:id/network/purge-cache — scope `apps:write`. An empty `{}` body is valid."""
|
|
211
236
|
return self._c.request_json(
|
|
@@ -364,12 +389,13 @@ class AppFilesResource:
|
|
|
364
389
|
|
|
365
390
|
class AppsResource:
|
|
366
391
|
"""`apps.*` — get/status/statusAll/runtimes/realtime/metrics/logs/download/create/start/
|
|
367
|
-
stop/restart/updateConfig/delete + `deploys`, `releases`, `network`, `envs`, `files`."""
|
|
392
|
+
stop/restart/updateConfig/delete + `deploys`, `releases`, `domains`, `network`, `envs`, `files`."""
|
|
368
393
|
|
|
369
394
|
def __init__(self, client: VertraClient) -> None:
|
|
370
395
|
self._c = client
|
|
371
396
|
self.deploys = AppDeploysResource(client)
|
|
372
397
|
self.releases = AppReleasesResource(client)
|
|
398
|
+
self.domains = AppDomainsResource(client)
|
|
373
399
|
self.network = AppNetworkResource(client)
|
|
374
400
|
self.envs = AppEnvsResource(client)
|
|
375
401
|
self.files = AppFilesResource(client)
|
|
@@ -508,7 +534,7 @@ class AppsResource:
|
|
|
508
534
|
self, app_id: str, body: RestartAppBody | None = None, *, workspace_id: str | None = None
|
|
509
535
|
) -> APIApplicationOperationResponse:
|
|
510
536
|
"""POST /v1/apps/:id/restart — scope `apps:write`. Flags: `reinstall_dependencies`,
|
|
511
|
-
`force_build
|
|
537
|
+
`force_build`."""
|
|
512
538
|
return self._c.request_json(
|
|
513
539
|
"POST",
|
|
514
540
|
f"/v1/apps/{encode_path_param(app_id)}/restart",
|
|
@@ -9,7 +9,9 @@ import urllib.error
|
|
|
9
9
|
import urllib.request
|
|
10
10
|
from collections.abc import Iterator
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
|
+
from email.message import Message
|
|
12
13
|
from typing import Protocol
|
|
14
|
+
from urllib.parse import urlsplit
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
@dataclass
|
|
@@ -35,6 +37,39 @@ class Transport(Protocol):
|
|
|
35
37
|
) -> Response: ...
|
|
36
38
|
|
|
37
39
|
|
|
40
|
+
class _AuthStrippingRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
41
|
+
"""Follows redirects but never forwards `Authorization` to another origin (scheme, host or port)."""
|
|
42
|
+
|
|
43
|
+
def redirect_request(
|
|
44
|
+
self,
|
|
45
|
+
req: urllib.request.Request,
|
|
46
|
+
fp: object,
|
|
47
|
+
code: int,
|
|
48
|
+
msg: str,
|
|
49
|
+
headers: Message | dict[str, str],
|
|
50
|
+
newurl: str,
|
|
51
|
+
) -> urllib.request.Request | None:
|
|
52
|
+
new = super().redirect_request(req, fp, code, msg, headers, newurl) # type: ignore[arg-type]
|
|
53
|
+
if new is not None and _origin(req.full_url) != _origin(new.full_url):
|
|
54
|
+
for name in [h for h in new.headers if h.lower() == "authorization"]:
|
|
55
|
+
del new.headers[name]
|
|
56
|
+
for name in [h for h in new.unredirected_hdrs if h.lower() == "authorization"]:
|
|
57
|
+
del new.unredirected_hdrs[name]
|
|
58
|
+
return new
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _origin(url: str) -> tuple[str, str, int | None]:
|
|
62
|
+
parts = urlsplit(url)
|
|
63
|
+
return (
|
|
64
|
+
parts.scheme.lower(),
|
|
65
|
+
(parts.hostname or "").lower(),
|
|
66
|
+
parts.port or {"http": 80, "https": 443}.get(parts.scheme.lower()),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_opener = urllib.request.build_opener(_AuthStrippingRedirectHandler)
|
|
71
|
+
|
|
72
|
+
|
|
38
73
|
@dataclass
|
|
39
74
|
class UrllibTransport:
|
|
40
75
|
"""Default implementation, pure stdlib (`urllib`) — zero runtime dependencies."""
|
|
@@ -53,7 +88,7 @@ class UrllibTransport:
|
|
|
53
88
|
) -> Response:
|
|
54
89
|
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
55
90
|
try:
|
|
56
|
-
resp =
|
|
91
|
+
resp = _opener.open(req, timeout=timeout) # noqa: S310 - URL comes from the configured base_url
|
|
57
92
|
except urllib.error.HTTPError as exc:
|
|
58
93
|
data = exc.read()
|
|
59
94
|
resp_headers = {k.lower(): v for k, v in exc.headers.items()} if exc.headers else {}
|