vertracloud-api 0.1.0__py3-none-any.whl
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/__init__.py +29 -0
- vertracloud/client.py +228 -0
- vertracloud/errors.py +93 -0
- vertracloud/py.typed +0 -0
- vertracloud/resources/__init__.py +0 -0
- vertracloud/resources/account.py +112 -0
- vertracloud/resources/apps.py +418 -0
- vertracloud/resources/billing.py +51 -0
- vertracloud/resources/databases.py +127 -0
- vertracloud/resources/snapshots.py +66 -0
- vertracloud/resources/workspaces.py +274 -0
- vertracloud/sse.py +65 -0
- vertracloud/transport.py +81 -0
- vertracloud/types.py +742 -0
- vertracloud_api-0.1.0.dist-info/METADATA +184 -0
- vertracloud_api-0.1.0.dist-info/RECORD +18 -0
- vertracloud_api-0.1.0.dist-info/WHEEL +4 -0
- vertracloud_api-0.1.0.dist-info/licenses/LICENSE +21 -0
vertracloud/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Typed Python client for the Vertra Cloud public API (`import vertracloud`)."""
|
|
2
|
+
|
|
3
|
+
from .client import VertraClient, __version__
|
|
4
|
+
from .errors import (
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
RateLimitError,
|
|
8
|
+
ScopeDeniedError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
VertraAPIError,
|
|
11
|
+
)
|
|
12
|
+
from .sse import SseEvent, SseStream
|
|
13
|
+
from .transport import Response, Transport, UrllibTransport
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"VertraClient",
|
|
17
|
+
"__version__",
|
|
18
|
+
"VertraAPIError",
|
|
19
|
+
"AuthenticationError",
|
|
20
|
+
"ScopeDeniedError",
|
|
21
|
+
"NotFoundError",
|
|
22
|
+
"ValidationError",
|
|
23
|
+
"RateLimitError",
|
|
24
|
+
"SseEvent",
|
|
25
|
+
"SseStream",
|
|
26
|
+
"Transport",
|
|
27
|
+
"UrllibTransport",
|
|
28
|
+
"Response",
|
|
29
|
+
]
|
vertracloud/client.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Central HTTP client. Every `resources/*.py` calls into `VertraClient` — no
|
|
2
|
+
resource method builds a URL, sets a header, or handles errors on its own.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import mimetypes
|
|
9
|
+
import uuid
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
from typing import Any
|
|
12
|
+
from urllib.parse import quote, urlencode
|
|
13
|
+
|
|
14
|
+
from .errors import error_from_response
|
|
15
|
+
from .sse import SseStream
|
|
16
|
+
from .transport import Response, Transport, UrllibTransport
|
|
17
|
+
from .types import JsonValue
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0" # x-release-please-version
|
|
20
|
+
|
|
21
|
+
DEFAULT_BASE_URL = "https://api.vertracloud.app"
|
|
22
|
+
DEFAULT_TIMEOUT = 30.0
|
|
23
|
+
# Download/upload/create for an app may involve a large zip file, so the timeout is longer.
|
|
24
|
+
LARGE_PAYLOAD_TIMEOUT = 120.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def encode_path_param(value: str) -> str:
|
|
28
|
+
"""`quote(safe="")` — a path param never lets `/`, `?`, etc. leak into the URL structure."""
|
|
29
|
+
return quote(str(value), safe="")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MultipartField:
|
|
33
|
+
"""One part of a multipart body: plain text, or a file (with filename/content-type)."""
|
|
34
|
+
|
|
35
|
+
__slots__ = ("name", "value", "filename", "content_type")
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
name: str,
|
|
40
|
+
value: str | bytes,
|
|
41
|
+
*,
|
|
42
|
+
filename: str | None = None,
|
|
43
|
+
content_type: str | None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
self.name = name
|
|
46
|
+
self.value = value
|
|
47
|
+
self.filename = filename
|
|
48
|
+
self.content_type = content_type
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _build_multipart(fields: Iterable[MultipartField]) -> tuple[bytes, str]:
|
|
52
|
+
boundary = f"----vertracloud-{uuid.uuid4().hex}"
|
|
53
|
+
parts: list[bytes] = []
|
|
54
|
+
for field in fields:
|
|
55
|
+
disposition = f'form-data; name="{field.name}"'
|
|
56
|
+
if field.filename:
|
|
57
|
+
disposition += f'; filename="{field.filename}"'
|
|
58
|
+
header = f"--{boundary}\r\nContent-Disposition: {disposition}\r\n"
|
|
59
|
+
content_type = field.content_type
|
|
60
|
+
if content_type is None and field.filename:
|
|
61
|
+
content_type = mimetypes.guess_type(field.filename)[0] or "application/octet-stream"
|
|
62
|
+
if content_type:
|
|
63
|
+
header += f"Content-Type: {content_type}\r\n"
|
|
64
|
+
header += "\r\n"
|
|
65
|
+
body = field.value if isinstance(field.value, bytes) else field.value.encode("utf-8")
|
|
66
|
+
parts.append(header.encode("utf-8") + body + b"\r\n")
|
|
67
|
+
parts.append(f"--{boundary}--\r\n".encode())
|
|
68
|
+
return b"".join(parts), boundary
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class VertraClient:
|
|
72
|
+
"""Client for the Vertra Cloud public API.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
api_key: the user's API key (`Authorization: Bearer <key>`).
|
|
76
|
+
base_url: defaults to `https://api.vertracloud.app`.
|
|
77
|
+
timeout: seconds, defaults to 30. Can be overridden per call (download/upload/create).
|
|
78
|
+
user_agent: defaults to `vertracloud-sdk-py/<version>`.
|
|
79
|
+
transport: injectable `Transport` implementation (tests use a fake one).
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
*,
|
|
85
|
+
api_key: str,
|
|
86
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
87
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
88
|
+
user_agent: str | None = None,
|
|
89
|
+
transport: Transport | None = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
if not api_key:
|
|
92
|
+
raise ValueError("api_key is required")
|
|
93
|
+
self._api_key = api_key
|
|
94
|
+
self.base_url = base_url.rstrip("/")
|
|
95
|
+
self.timeout = timeout
|
|
96
|
+
self.user_agent = user_agent or f"vertracloud-sdk-py/{__version__}"
|
|
97
|
+
self.transport: Transport = transport or UrllibTransport()
|
|
98
|
+
|
|
99
|
+
# Resources are imported here to avoid an import cycle at module top level.
|
|
100
|
+
from .resources.account import AccountResource
|
|
101
|
+
from .resources.apps import AppsResource
|
|
102
|
+
from .resources.billing import BillingResource
|
|
103
|
+
from .resources.databases import DatabasesResource
|
|
104
|
+
from .resources.snapshots import SnapshotsResource
|
|
105
|
+
from .resources.workspaces import WorkspacesResource
|
|
106
|
+
|
|
107
|
+
self.apps = AppsResource(self)
|
|
108
|
+
self.databases = DatabasesResource(self)
|
|
109
|
+
self.snapshots = SnapshotsResource(self)
|
|
110
|
+
self.account = AccountResource(self)
|
|
111
|
+
self.workspaces = WorkspacesResource(self)
|
|
112
|
+
self.billing = BillingResource(self)
|
|
113
|
+
|
|
114
|
+
def __repr__(self) -> str:
|
|
115
|
+
return f"VertraClient(base_url={self.base_url!r})"
|
|
116
|
+
|
|
117
|
+
# -- HTTP core -------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
def _headers(self, extra: dict[str, str] | None = None) -> dict[str, str]:
|
|
120
|
+
headers = {
|
|
121
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
122
|
+
"User-Agent": self.user_agent,
|
|
123
|
+
"Accept": "application/json",
|
|
124
|
+
}
|
|
125
|
+
if extra:
|
|
126
|
+
headers.update(extra)
|
|
127
|
+
return headers
|
|
128
|
+
|
|
129
|
+
def _url(self, path: str, query: dict[str, Any] | None = None) -> str:
|
|
130
|
+
url = f"{self.base_url}{path}"
|
|
131
|
+
if query:
|
|
132
|
+
clean = {
|
|
133
|
+
k: ("true" if v is True else "false" if v is False else v) for k, v in query.items() if v is not None
|
|
134
|
+
}
|
|
135
|
+
if clean:
|
|
136
|
+
url += f"?{urlencode(clean)}"
|
|
137
|
+
return url
|
|
138
|
+
|
|
139
|
+
def request_json(
|
|
140
|
+
self,
|
|
141
|
+
method: str,
|
|
142
|
+
path: str,
|
|
143
|
+
*,
|
|
144
|
+
query: dict[str, Any] | None = None,
|
|
145
|
+
json_body: Any = None,
|
|
146
|
+
timeout: float | None = None,
|
|
147
|
+
) -> JsonValue:
|
|
148
|
+
"""Standard JSON call: sends `json_body` (if given), unwraps `{response}`."""
|
|
149
|
+
headers = self._headers({"Content-Type": "application/json"} if json_body is not None else None)
|
|
150
|
+
body = json.dumps(json_body).encode("utf-8") if json_body is not None else None
|
|
151
|
+
resp = self.transport.request(method, self._url(path, query), headers, body, timeout or self.timeout)
|
|
152
|
+
return self._unwrap(resp)
|
|
153
|
+
|
|
154
|
+
def request_multipart(
|
|
155
|
+
self,
|
|
156
|
+
method: str,
|
|
157
|
+
path: str,
|
|
158
|
+
*,
|
|
159
|
+
fields: list[MultipartField],
|
|
160
|
+
query: dict[str, Any] | None = None,
|
|
161
|
+
timeout: float | None = None,
|
|
162
|
+
) -> JsonValue:
|
|
163
|
+
body, boundary = _build_multipart(fields)
|
|
164
|
+
headers = self._headers({"Content-Type": f"multipart/form-data; boundary={boundary}"})
|
|
165
|
+
resp = self.transport.request(method, self._url(path, query), headers, body, timeout or self.timeout)
|
|
166
|
+
return self._unwrap(resp)
|
|
167
|
+
|
|
168
|
+
def request_binary(
|
|
169
|
+
self,
|
|
170
|
+
method: str,
|
|
171
|
+
path: str,
|
|
172
|
+
*,
|
|
173
|
+
query: dict[str, Any] | None = None,
|
|
174
|
+
timeout: float | None = None,
|
|
175
|
+
) -> bytes:
|
|
176
|
+
"""Binary downloads (app/snapshot zip, certificate). No JSON envelope."""
|
|
177
|
+
headers = self._headers({"Accept": "*/*"})
|
|
178
|
+
resp = self.transport.request(method, self._url(path, query), headers, None, timeout or self.timeout)
|
|
179
|
+
if resp.status >= 400:
|
|
180
|
+
self._raise_for_error(resp)
|
|
181
|
+
return resp.body
|
|
182
|
+
|
|
183
|
+
def request_sse(
|
|
184
|
+
self,
|
|
185
|
+
path: str,
|
|
186
|
+
*,
|
|
187
|
+
query: dict[str, Any] | None = None,
|
|
188
|
+
timeout: float | None = None,
|
|
189
|
+
) -> SseStream:
|
|
190
|
+
"""`GET /v1/apps/:id/realtime` — the only SSE route in the catalog."""
|
|
191
|
+
headers = self._headers({"Accept": "text/event-stream"})
|
|
192
|
+
resp = self.transport.request(
|
|
193
|
+
"GET", self._url(path, query), headers, None, timeout or self.timeout, stream=True
|
|
194
|
+
)
|
|
195
|
+
if resp.status >= 400:
|
|
196
|
+
self._raise_for_error(resp)
|
|
197
|
+
if resp.stream is None:
|
|
198
|
+
raise VertraClientError("Transport did not return a stream for an SSE request")
|
|
199
|
+
return SseStream(resp.stream)
|
|
200
|
+
|
|
201
|
+
# -- envelope --------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
def _unwrap(self, resp: Response) -> JsonValue:
|
|
204
|
+
if resp.status >= 400:
|
|
205
|
+
self._raise_for_error(resp)
|
|
206
|
+
if not resp.body:
|
|
207
|
+
return {}
|
|
208
|
+
payload: Any = json.loads(resp.body)
|
|
209
|
+
if isinstance(payload, dict) and "response" in payload:
|
|
210
|
+
return payload["response"]
|
|
211
|
+
return payload
|
|
212
|
+
|
|
213
|
+
def _raise_for_error(self, resp: Response) -> None:
|
|
214
|
+
try:
|
|
215
|
+
payload = json.loads(resp.body) if resp.body else {}
|
|
216
|
+
except json.JSONDecodeError:
|
|
217
|
+
payload = {}
|
|
218
|
+
if not isinstance(payload, dict):
|
|
219
|
+
payload = {}
|
|
220
|
+
if "retry_after" not in payload:
|
|
221
|
+
retry_after_header = resp.headers.get("retry-after")
|
|
222
|
+
if retry_after_header is not None:
|
|
223
|
+
payload = {**payload, "retry_after": retry_after_header}
|
|
224
|
+
raise error_from_response(resp.status, payload)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class VertraClientError(Exception):
|
|
228
|
+
"""Client usage error (not from the API), such as a misbehaving transport."""
|
vertracloud/errors.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Typed SDK exceptions, derived from the API's error envelope.
|
|
2
|
+
|
|
3
|
+
Error envelope: `{code, message?, details?, retry_after?}`. The class is picked from the
|
|
4
|
+
HTTP status; `code` is always accessible on the exception. Never print the API key
|
|
5
|
+
here — `__repr__`/`__str__` only show status/code/message.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VertraAPIError(Exception):
|
|
14
|
+
"""Generic API error. Base class for every typed SDK exception."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
message: str | None,
|
|
19
|
+
*,
|
|
20
|
+
status: int,
|
|
21
|
+
code: str,
|
|
22
|
+
details: Any = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.status = status
|
|
25
|
+
self.code = code
|
|
26
|
+
self.details = details
|
|
27
|
+
super().__init__(message or code)
|
|
28
|
+
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
return f"{self.code} ({self.status}): {super().__str__()}"
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str:
|
|
33
|
+
return f"{self.__class__.__name__}(status={self.status!r}, code={self.code!r})"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AuthenticationError(VertraAPIError):
|
|
37
|
+
"""401 — invalid key or user not found (`API_KEY_INVALID`, `USER_NOT_FOUND`)."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ScopeDeniedError(VertraAPIError):
|
|
41
|
+
"""403 — key missing the required scope or IP, a ban, or a WEBSITE_ONLY route."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NotFoundError(VertraAPIError):
|
|
45
|
+
"""404 — resource does not exist (`APP_NOT_FOUND`, `DATABASE_NOT_FOUND`, ...)."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ValidationError(VertraAPIError):
|
|
49
|
+
"""400/422 — invalid body, query, or path."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RateLimitError(VertraAPIError):
|
|
53
|
+
"""429 — per-minute limit or daily quota exceeded."""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
message: str | None,
|
|
58
|
+
*,
|
|
59
|
+
status: int,
|
|
60
|
+
code: str,
|
|
61
|
+
details: Any = None,
|
|
62
|
+
retry_after: float | None = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
super().__init__(message, status=status, code=code, details=details)
|
|
65
|
+
self.retry_after = retry_after
|
|
66
|
+
|
|
67
|
+
def __repr__(self) -> str:
|
|
68
|
+
return (
|
|
69
|
+
f"{self.__class__.__name__}(status={self.status!r}, code={self.code!r}, retry_after={self.retry_after!r})"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def error_from_response(status: int, body: dict[str, Any]) -> VertraAPIError:
|
|
74
|
+
"""Picks the class from the HTTP status; `code` comes from the error response body."""
|
|
75
|
+
code = str(body.get("code") or "UNKNOWN_ERROR")
|
|
76
|
+
message = body.get("message")
|
|
77
|
+
details = body.get("details")
|
|
78
|
+
if status == 401:
|
|
79
|
+
return AuthenticationError(message, status=status, code=code, details=details)
|
|
80
|
+
if status == 403:
|
|
81
|
+
return ScopeDeniedError(message, status=status, code=code, details=details)
|
|
82
|
+
if status == 404:
|
|
83
|
+
return NotFoundError(message, status=status, code=code, details=details)
|
|
84
|
+
if status in (400, 422):
|
|
85
|
+
return ValidationError(message, status=status, code=code, details=details)
|
|
86
|
+
if status == 429:
|
|
87
|
+
retry_after = body.get("retry_after")
|
|
88
|
+
try:
|
|
89
|
+
retry_after = float(retry_after) if retry_after is not None else None
|
|
90
|
+
except (TypeError, ValueError):
|
|
91
|
+
retry_after = None
|
|
92
|
+
return RateLimitError(message, status=status, code=code, details=details, retry_after=retry_after)
|
|
93
|
+
return VertraAPIError(message, status=status, code=code, details=details)
|
vertracloud/py.typed
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""`account` domain — 10 routes (scopes `account:read/write`).
|
|
2
|
+
|
|
3
|
+
The user's activity log is website-only — there is no corresponding method here.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from ..client import encode_path_param
|
|
11
|
+
from ..types import (
|
|
12
|
+
APIUserInfoResponse,
|
|
13
|
+
APIUserSession,
|
|
14
|
+
APIWorkspaceResourceFolder,
|
|
15
|
+
CreateFolderBody,
|
|
16
|
+
UpdateAccountBody,
|
|
17
|
+
UpdateFolderBody,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from ..client import VertraClient
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AccountSessionsResource:
|
|
25
|
+
def __init__(self, client: VertraClient) -> None:
|
|
26
|
+
self._c = client
|
|
27
|
+
|
|
28
|
+
def list(self) -> list[APIUserSession]:
|
|
29
|
+
"""GET /v1/users/me/sessions — scope `account:read`."""
|
|
30
|
+
return self._c.request_json("GET", "/v1/users/me/sessions")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AccountFoldersResource:
|
|
34
|
+
def __init__(self, client: VertraClient) -> None:
|
|
35
|
+
self._c = client
|
|
36
|
+
|
|
37
|
+
def create(self, body: CreateFolderBody) -> APIWorkspaceResourceFolder:
|
|
38
|
+
"""POST /v1/users/me/resource-organization/folders — scope `account:write`."""
|
|
39
|
+
return self._c.request_json("POST", "/v1/users/me/resource-organization/folders", json_body=body)
|
|
40
|
+
|
|
41
|
+
def update(self, folder_id: str, body: UpdateFolderBody) -> APIWorkspaceResourceFolder:
|
|
42
|
+
"""PATCH /v1/users/me/resource-organization/folders/:folder_id — scope `account:write`."""
|
|
43
|
+
return self._c.request_json(
|
|
44
|
+
"PATCH", f"/v1/users/me/resource-organization/folders/{encode_path_param(folder_id)}", json_body=body
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def delete(self, folder_id: str) -> None:
|
|
48
|
+
"""DELETE /v1/users/me/resource-organization/folders/:folder_id — scope `account:write`."""
|
|
49
|
+
return self._c.request_json(
|
|
50
|
+
"DELETE", f"/v1/users/me/resource-organization/folders/{encode_path_param(folder_id)}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def add_resource(
|
|
54
|
+
self, folder_id: str, resource_type: str, resource_id: str, *, position: int | None = None
|
|
55
|
+
) -> APIWorkspaceResourceFolder:
|
|
56
|
+
"""PUT .../folders/:folder_id/resources/:resource_type/:resource_id — scope `account:write`."""
|
|
57
|
+
path = (
|
|
58
|
+
f"/v1/users/me/resource-organization/folders/{encode_path_param(folder_id)}"
|
|
59
|
+
f"/resources/{encode_path_param(resource_type)}/{encode_path_param(resource_id)}"
|
|
60
|
+
)
|
|
61
|
+
body = {"position": position} if position is not None else {}
|
|
62
|
+
return self._c.request_json("PUT", path, json_body=body)
|
|
63
|
+
|
|
64
|
+
def remove_resource(self, folder_id: str, resource_type: str, resource_id: str) -> None:
|
|
65
|
+
"""DELETE .../folders/:folder_id/resources/:resource_type/:resource_id — scope `account:write`."""
|
|
66
|
+
path = (
|
|
67
|
+
f"/v1/users/me/resource-organization/folders/{encode_path_param(folder_id)}"
|
|
68
|
+
f"/resources/{encode_path_param(resource_type)}/{encode_path_param(resource_id)}"
|
|
69
|
+
)
|
|
70
|
+
return self._c.request_json("DELETE", path)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class AccountFavoritesResource:
|
|
74
|
+
def __init__(self, client: VertraClient) -> None:
|
|
75
|
+
self._c = client
|
|
76
|
+
|
|
77
|
+
def add(self, resource_type: str, resource_id: str, *, position: int | None = None) -> None:
|
|
78
|
+
"""PUT /v1/users/me/resource-organization/favorites/:resource_type/:resource_id — `account:write`."""
|
|
79
|
+
path = (
|
|
80
|
+
f"/v1/users/me/resource-organization/favorites/{encode_path_param(resource_type)}"
|
|
81
|
+
f"/{encode_path_param(resource_id)}"
|
|
82
|
+
)
|
|
83
|
+
body = {"position": position} if position is not None else {}
|
|
84
|
+
return self._c.request_json("PUT", path, json_body=body)
|
|
85
|
+
|
|
86
|
+
def remove(self, resource_type: str, resource_id: str) -> None:
|
|
87
|
+
"""DELETE .../favorites/:resource_type/:resource_id — scope `account:write`."""
|
|
88
|
+
path = (
|
|
89
|
+
f"/v1/users/me/resource-organization/favorites/{encode_path_param(resource_type)}"
|
|
90
|
+
f"/{encode_path_param(resource_id)}"
|
|
91
|
+
)
|
|
92
|
+
return self._c.request_json("DELETE", path)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AccountResource:
|
|
96
|
+
"""`account.*` — get/update, `sessions`, `folders`, `favorites`."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, client: VertraClient) -> None:
|
|
99
|
+
self._c = client
|
|
100
|
+
self.sessions = AccountSessionsResource(client)
|
|
101
|
+
self.folders = AccountFoldersResource(client)
|
|
102
|
+
self.favorites = AccountFavoritesResource(client)
|
|
103
|
+
|
|
104
|
+
def get(self) -> APIUserInfoResponse:
|
|
105
|
+
"""GET /v1/users/me — scope `account:read`. There is no `GET /v1/apps` or `/v1/databases`:
|
|
106
|
+
the user's list of apps/databases comes from here (`applications[]`, `databases[]`) or
|
|
107
|
+
from `apps.status_all()`/`databases.status_all()`."""
|
|
108
|
+
return self._c.request_json("GET", "/v1/users/me")
|
|
109
|
+
|
|
110
|
+
def update(self, body: UpdateAccountBody) -> APIUserInfoResponse:
|
|
111
|
+
"""PATCH /v1/users/me — scope `account:write`."""
|
|
112
|
+
return self._c.request_json("PATCH", "/v1/users/me", json_body=body)
|