nohead 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.
- nohead/__init__.py +77 -0
- nohead/_async/__init__.py +0 -0
- nohead/_async/_client.py +184 -0
- nohead/_async/_nohead.py +147 -0
- nohead/_async/resources/__init__.py +0 -0
- nohead/_async/resources/_resource.py +8 -0
- nohead/_async/resources/assets.py +128 -0
- nohead/_async/resources/other.py +47 -0
- nohead/_async/resources/records.py +343 -0
- nohead/_async/resources/schema.py +330 -0
- nohead/_async/resources/webhooks.py +128 -0
- nohead/_base.py +275 -0
- nohead/_errors.py +182 -0
- nohead/_generated/__init__.py +0 -0
- nohead/_generated/operations.py +70 -0
- nohead/_pagination.py +129 -0
- nohead/_sync/__init__.py +0 -0
- nohead/_sync/_client.py +185 -0
- nohead/_sync/_nohead.py +148 -0
- nohead/_sync/resources/__init__.py +0 -0
- nohead/_sync/resources/_resource.py +9 -0
- nohead/_sync/resources/assets.py +129 -0
- nohead/_sync/resources/other.py +48 -0
- nohead/_sync/resources/records.py +344 -0
- nohead/_sync/resources/schema.py +329 -0
- nohead/_sync/resources/webhooks.py +125 -0
- nohead/_uploads.py +109 -0
- nohead/_version.py +1 -0
- nohead/models.py +1073 -0
- nohead/params.py +129 -0
- nohead/py.typed +0 -0
- nohead/types.py +34 -0
- nohead/webhooks.py +123 -0
- nohead-0.1.0.dist-info/METADATA +364 -0
- nohead-0.1.0.dist-info/RECORD +37 -0
- nohead-0.1.0.dist-info/WHEEL +4 -0
- nohead-0.1.0.dist-info/licenses/LICENSE +21 -0
nohead/__init__.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""The official Python SDK for the Nohead API.
|
|
2
|
+
|
|
3
|
+
from nohead import Nohead
|
|
4
|
+
|
|
5
|
+
nohead = Nohead() # NOHEAD_API_KEY
|
|
6
|
+
for post in nohead.records.list("posts", filter={"status": "published"}):
|
|
7
|
+
print(post.data["title"])
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from . import models, params, types, webhooks
|
|
11
|
+
from ._async._nohead import AsyncNohead
|
|
12
|
+
from ._base import DEFAULT_BASE_URL, NoheadWarning
|
|
13
|
+
from ._errors import (
|
|
14
|
+
APIConnectionError,
|
|
15
|
+
APIError,
|
|
16
|
+
APITimeoutError,
|
|
17
|
+
AuthenticationError,
|
|
18
|
+
AuthorizationError,
|
|
19
|
+
ConflictError,
|
|
20
|
+
InternalServerError,
|
|
21
|
+
InvalidRequestError,
|
|
22
|
+
NoheadError,
|
|
23
|
+
NotFoundError,
|
|
24
|
+
PlanLimitExceededError,
|
|
25
|
+
PreconditionFailedError,
|
|
26
|
+
RateLimitError,
|
|
27
|
+
ServiceUnavailableError,
|
|
28
|
+
UploadError,
|
|
29
|
+
ValidationError,
|
|
30
|
+
WebhookVerificationError,
|
|
31
|
+
)
|
|
32
|
+
from ._pagination import AsyncPage, AsyncPaginator, Page
|
|
33
|
+
from ._sync._nohead import Nohead
|
|
34
|
+
from ._uploads import Uploadable
|
|
35
|
+
from ._version import __version__
|
|
36
|
+
from .models import Asset, Collection, Field, Record, Schema, Webhook
|
|
37
|
+
from .webhooks import WebhookEvent
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"DEFAULT_BASE_URL",
|
|
41
|
+
"APIConnectionError",
|
|
42
|
+
"APIError",
|
|
43
|
+
"APITimeoutError",
|
|
44
|
+
"Asset",
|
|
45
|
+
"AsyncNohead",
|
|
46
|
+
"AsyncPage",
|
|
47
|
+
"AsyncPaginator",
|
|
48
|
+
"AuthenticationError",
|
|
49
|
+
"AuthorizationError",
|
|
50
|
+
"Collection",
|
|
51
|
+
"ConflictError",
|
|
52
|
+
"Field",
|
|
53
|
+
"InternalServerError",
|
|
54
|
+
"InvalidRequestError",
|
|
55
|
+
"Nohead",
|
|
56
|
+
"NoheadError",
|
|
57
|
+
"NoheadWarning",
|
|
58
|
+
"NotFoundError",
|
|
59
|
+
"Page",
|
|
60
|
+
"PlanLimitExceededError",
|
|
61
|
+
"PreconditionFailedError",
|
|
62
|
+
"RateLimitError",
|
|
63
|
+
"Record",
|
|
64
|
+
"Schema",
|
|
65
|
+
"ServiceUnavailableError",
|
|
66
|
+
"UploadError",
|
|
67
|
+
"Uploadable",
|
|
68
|
+
"ValidationError",
|
|
69
|
+
"Webhook",
|
|
70
|
+
"WebhookEvent",
|
|
71
|
+
"WebhookVerificationError",
|
|
72
|
+
"__version__",
|
|
73
|
+
"models",
|
|
74
|
+
"params",
|
|
75
|
+
"types",
|
|
76
|
+
"webhooks",
|
|
77
|
+
]
|
|
File without changes
|
nohead/_async/_client.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# pyright: reportPrivateUsage=false
|
|
2
|
+
"""The HTTP core of the async client: requests, retries, errors and pages.
|
|
3
|
+
|
|
4
|
+
src/nohead/_sync is generated from this package by scripts/unasync.py: edit
|
|
5
|
+
the async code and run `uv run python scripts/unasync.py`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from typing import Any, TypeVar
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from .. import _base
|
|
17
|
+
from .._errors import APIConnectionError, APITimeoutError, NoheadError, api_error
|
|
18
|
+
from .._generated.operations import OPERATIONS
|
|
19
|
+
from .._pagination import AsyncPage, AsyncPaginator, start_async_pages
|
|
20
|
+
from .._uploads import UploadSource, aiter_file
|
|
21
|
+
from ..models import ListMeta, Principal
|
|
22
|
+
from ..types import IfMatch
|
|
23
|
+
|
|
24
|
+
T = TypeVar("T")
|
|
25
|
+
M = TypeVar("M")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AsyncAPIClient:
|
|
29
|
+
def __init__(self, settings: _base.Settings, http: httpx.AsyncClient | None) -> None:
|
|
30
|
+
self.settings = settings
|
|
31
|
+
self.http = http or httpx.AsyncClient()
|
|
32
|
+
self.owns_http = http is None
|
|
33
|
+
self._project_id = settings.project_id
|
|
34
|
+
|
|
35
|
+
def copy(self, settings: _base.Settings) -> AsyncAPIClient:
|
|
36
|
+
"""The same connection pool and project, other settings."""
|
|
37
|
+
client = AsyncAPIClient(settings, self.http)
|
|
38
|
+
client.owns_http = False
|
|
39
|
+
client._project_id = settings.project_id or self._project_id
|
|
40
|
+
return client
|
|
41
|
+
|
|
42
|
+
async def close(self) -> None:
|
|
43
|
+
if self.owns_http:
|
|
44
|
+
await self.http.aclose()
|
|
45
|
+
|
|
46
|
+
async def project_id(self) -> str:
|
|
47
|
+
"""The API key's project, from `GET /v1/me` the first time."""
|
|
48
|
+
if self._project_id is None:
|
|
49
|
+
me = await self.request("me_get", cast=Principal)
|
|
50
|
+
if me.api_key is None:
|
|
51
|
+
raise NoheadError("The credentials are not a project API key")
|
|
52
|
+
self._project_id = me.api_key.project_id
|
|
53
|
+
return self._project_id
|
|
54
|
+
|
|
55
|
+
async def request(
|
|
56
|
+
self,
|
|
57
|
+
operation: str,
|
|
58
|
+
*,
|
|
59
|
+
cast: type[T] | Any,
|
|
60
|
+
path: Mapping[str, str | int] | None = None,
|
|
61
|
+
query: Mapping[str, Any] | None = None,
|
|
62
|
+
body: Any = None,
|
|
63
|
+
idempotency_key: str | None = None,
|
|
64
|
+
change_note: str | None = None,
|
|
65
|
+
if_match: IfMatch | None = None,
|
|
66
|
+
) -> T:
|
|
67
|
+
data = await self.send(
|
|
68
|
+
operation,
|
|
69
|
+
path=path,
|
|
70
|
+
query=query,
|
|
71
|
+
body=body,
|
|
72
|
+
idempotency_key=idempotency_key,
|
|
73
|
+
change_note=change_note,
|
|
74
|
+
if_match=if_match,
|
|
75
|
+
)
|
|
76
|
+
return _base.validate(cast, data)
|
|
77
|
+
|
|
78
|
+
def paginate(
|
|
79
|
+
self,
|
|
80
|
+
operation: str,
|
|
81
|
+
*,
|
|
82
|
+
item: type[T],
|
|
83
|
+
meta: type[M] = ListMeta,
|
|
84
|
+
path: Mapping[str, str | int] | None = None,
|
|
85
|
+
query: Mapping[str, Any] | None = None,
|
|
86
|
+
) -> AsyncPaginator[T, Any]:
|
|
87
|
+
"""A list operation as pages of `item`."""
|
|
88
|
+
params = dict(query or {})
|
|
89
|
+
|
|
90
|
+
async def fetch(cursor: str | None) -> AsyncPage[T, Any]:
|
|
91
|
+
data = await self.send(operation, path=path, query={**params, "cursor": cursor})
|
|
92
|
+
items: list[T] = [_base.validate(item, raw) for raw in data["data"]]
|
|
93
|
+
return AsyncPage(items, _base.validate(meta, data["meta"]), fetch)
|
|
94
|
+
|
|
95
|
+
return start_async_pages(fetch, params.pop("cursor", None))
|
|
96
|
+
|
|
97
|
+
async def send(
|
|
98
|
+
self,
|
|
99
|
+
operation: str,
|
|
100
|
+
*,
|
|
101
|
+
path: Mapping[str, str | int] | None = None,
|
|
102
|
+
query: Mapping[str, Any] | None = None,
|
|
103
|
+
body: Any = None,
|
|
104
|
+
idempotency_key: str | None = None,
|
|
105
|
+
change_note: str | None = None,
|
|
106
|
+
if_match: IfMatch | None = None,
|
|
107
|
+
) -> Any:
|
|
108
|
+
"""Sends one operation, retrying as the README describes; returns the JSON."""
|
|
109
|
+
method, template = OPERATIONS[operation]
|
|
110
|
+
values = dict(path or {})
|
|
111
|
+
if "{project_id}" in template and "project_id" not in values:
|
|
112
|
+
values["project_id"] = await self.project_id()
|
|
113
|
+
settings = self.settings
|
|
114
|
+
url = _base.url(settings, template, values)
|
|
115
|
+
headers = _base.headers(
|
|
116
|
+
settings,
|
|
117
|
+
method,
|
|
118
|
+
has_body=body is not None,
|
|
119
|
+
idempotency_key=idempotency_key,
|
|
120
|
+
change_note=change_note,
|
|
121
|
+
if_match=if_match,
|
|
122
|
+
)
|
|
123
|
+
content = None if body is None else _base.encode(body)
|
|
124
|
+
params = _base.query(query)
|
|
125
|
+
|
|
126
|
+
attempt = 0
|
|
127
|
+
while True:
|
|
128
|
+
try:
|
|
129
|
+
response = await self.http.request(
|
|
130
|
+
method,
|
|
131
|
+
url,
|
|
132
|
+
params=params,
|
|
133
|
+
headers=headers,
|
|
134
|
+
content=content,
|
|
135
|
+
timeout=settings.timeout,
|
|
136
|
+
)
|
|
137
|
+
except httpx.TransportError as error:
|
|
138
|
+
failure = _connection_error(error, settings.timeout, url)
|
|
139
|
+
if attempt >= settings.max_retries:
|
|
140
|
+
raise failure from error
|
|
141
|
+
await asyncio.sleep(_base.backoff(attempt))
|
|
142
|
+
attempt += 1
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
_base.warn(settings, operation, response)
|
|
146
|
+
if response.is_success:
|
|
147
|
+
return _base.parse(response)
|
|
148
|
+
error = api_error(response.status_code, _base.parse(response), response.headers)
|
|
149
|
+
delay = _base.retry_delay(error, attempt, settings.max_retries)
|
|
150
|
+
if delay is None:
|
|
151
|
+
raise error
|
|
152
|
+
await asyncio.sleep(delay)
|
|
153
|
+
attempt += 1
|
|
154
|
+
|
|
155
|
+
async def put_upload(
|
|
156
|
+
self, url: str, method: str, headers: Mapping[str, str], source: UploadSource
|
|
157
|
+
) -> httpx.Response:
|
|
158
|
+
"""Sends a file to a presigned storage URL (no API credentials)."""
|
|
159
|
+
settings = self.settings
|
|
160
|
+
retries = settings.max_retries if source.replayable else 0
|
|
161
|
+
attempt = 0
|
|
162
|
+
while True:
|
|
163
|
+
try:
|
|
164
|
+
response = await self.http.request(
|
|
165
|
+
method,
|
|
166
|
+
url,
|
|
167
|
+
headers={**headers, "Content-Length": str(source.byte_size)},
|
|
168
|
+
content=aiter_file(source),
|
|
169
|
+
timeout=settings.timeout,
|
|
170
|
+
)
|
|
171
|
+
if response.status_code not in _base.RETRYABLE_STATUSES or attempt >= retries:
|
|
172
|
+
return response
|
|
173
|
+
except httpx.TransportError as error:
|
|
174
|
+
if attempt >= retries:
|
|
175
|
+
raise _connection_error(error, settings.timeout, url) from error
|
|
176
|
+
await asyncio.sleep(_base.backoff(attempt))
|
|
177
|
+
attempt += 1
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _connection_error(error: httpx.TransportError, timeout: float, url: str) -> NoheadError:
|
|
181
|
+
host = httpx.URL(url).host
|
|
182
|
+
if isinstance(error, httpx.TimeoutException):
|
|
183
|
+
return APITimeoutError(f"The request to {host} timed out after {timeout} s")
|
|
184
|
+
return APIConnectionError(f"Could not reach {host}: {error}")
|
nohead/_async/_nohead.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# pyright: reportPrivateUsage=false
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import Literal, Self
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .. import _base
|
|
11
|
+
from .._pagination import AsyncPaginator
|
|
12
|
+
from ..models import Record, SearchResultsMeta
|
|
13
|
+
from ._client import AsyncAPIClient
|
|
14
|
+
from .resources.assets import AsyncAssets
|
|
15
|
+
from .resources.other import AsyncAuditEvents, AsyncFeatureFlags, AsyncHealth, AsyncMe
|
|
16
|
+
from .resources.records import AsyncRecords
|
|
17
|
+
from .resources.schema import AsyncCollections, AsyncFields, AsyncMigrations
|
|
18
|
+
from .resources.webhooks import AsyncWebhooks
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AsyncNohead:
|
|
22
|
+
"""An async client for the Nohead API, authenticated with a project API key.
|
|
23
|
+
|
|
24
|
+
async with AsyncNohead() as nohead: # NOHEAD_API_KEY
|
|
25
|
+
async for post in nohead.records.list("posts"):
|
|
26
|
+
print(post.data["title"])
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
records: AsyncRecords
|
|
30
|
+
collections: AsyncCollections
|
|
31
|
+
fields: AsyncFields
|
|
32
|
+
migrations: AsyncMigrations
|
|
33
|
+
assets: AsyncAssets
|
|
34
|
+
webhooks: AsyncWebhooks
|
|
35
|
+
audit_events: AsyncAuditEvents
|
|
36
|
+
feature_flags: AsyncFeatureFlags
|
|
37
|
+
me: AsyncMe
|
|
38
|
+
health: AsyncHealth
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
*,
|
|
43
|
+
api_key: str | None = None,
|
|
44
|
+
base_url: str | None = None,
|
|
45
|
+
project_id: str | None = None,
|
|
46
|
+
max_retries: int = 2,
|
|
47
|
+
timeout: float = 60.0,
|
|
48
|
+
headers: Mapping[str, str] | None = None,
|
|
49
|
+
warnings: bool = True,
|
|
50
|
+
http_client: httpx.AsyncClient | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Args:
|
|
54
|
+
api_key: A project API key (`sk_live_...`). Defaults to NOHEAD_API_KEY.
|
|
55
|
+
base_url: Defaults to NOHEAD_API_URL, else https://api.nohead.io.
|
|
56
|
+
project_id: The key's project. Looked up once with GET /v1/me when omitted.
|
|
57
|
+
max_retries: Retries of failed requests (see the README, "Retries").
|
|
58
|
+
timeout: Seconds per attempt.
|
|
59
|
+
headers: Headers added to every request.
|
|
60
|
+
warnings: Warn about deprecated operations and plan usage (once each).
|
|
61
|
+
http_client: An httpx client to send requests with (proxies, transports).
|
|
62
|
+
"""
|
|
63
|
+
settings = _base.settings(
|
|
64
|
+
api_key=api_key,
|
|
65
|
+
base_url=base_url,
|
|
66
|
+
project_id=project_id,
|
|
67
|
+
max_retries=max_retries,
|
|
68
|
+
timeout=timeout,
|
|
69
|
+
headers=headers,
|
|
70
|
+
warnings=warnings,
|
|
71
|
+
)
|
|
72
|
+
self._setup(AsyncAPIClient(settings, http_client))
|
|
73
|
+
|
|
74
|
+
def _setup(self, client: AsyncAPIClient) -> None:
|
|
75
|
+
self._client = client
|
|
76
|
+
self.records = AsyncRecords(client)
|
|
77
|
+
self.collections = AsyncCollections(client)
|
|
78
|
+
self.fields = AsyncFields(client)
|
|
79
|
+
self.migrations = AsyncMigrations(client)
|
|
80
|
+
self.assets = AsyncAssets(client)
|
|
81
|
+
self.webhooks = AsyncWebhooks(client)
|
|
82
|
+
self.audit_events = AsyncAuditEvents(client)
|
|
83
|
+
self.feature_flags = AsyncFeatureFlags(client)
|
|
84
|
+
self.me = AsyncMe(client)
|
|
85
|
+
self.health = AsyncHealth(client)
|
|
86
|
+
|
|
87
|
+
def with_options(
|
|
88
|
+
self,
|
|
89
|
+
*,
|
|
90
|
+
timeout: float | None = None,
|
|
91
|
+
max_retries: int | None = None,
|
|
92
|
+
headers: Mapping[str, str] | None = None,
|
|
93
|
+
) -> Self:
|
|
94
|
+
"""A client with other settings that shares this one's connections:
|
|
95
|
+
`nohead.with_options(timeout=5).records.get(id)`."""
|
|
96
|
+
current = self._client.settings
|
|
97
|
+
settings = _base.Settings(
|
|
98
|
+
api_key=current.api_key,
|
|
99
|
+
base_url=current.base_url,
|
|
100
|
+
project_id=current.project_id,
|
|
101
|
+
max_retries=current.max_retries if max_retries is None else max_retries,
|
|
102
|
+
timeout=current.timeout if timeout is None else timeout,
|
|
103
|
+
headers={**current.headers, **(headers or {})},
|
|
104
|
+
warnings=current.warnings,
|
|
105
|
+
)
|
|
106
|
+
copy = object.__new__(type(self))
|
|
107
|
+
copy._setup(self._client.copy(settings))
|
|
108
|
+
return copy
|
|
109
|
+
|
|
110
|
+
def search(
|
|
111
|
+
self,
|
|
112
|
+
query: str,
|
|
113
|
+
*,
|
|
114
|
+
collections: Sequence[str] | None = None,
|
|
115
|
+
status: Literal["draft", "published"] | None = None,
|
|
116
|
+
limit: int | None = None,
|
|
117
|
+
cursor: str | None = None,
|
|
118
|
+
) -> AsyncPaginator[Record, SearchResultsMeta]:
|
|
119
|
+
"""Full-text search across the key's project (or some of its collections),
|
|
120
|
+
most relevant first, through the first 1,000 hits."""
|
|
121
|
+
return self._client.paginate(
|
|
122
|
+
"projects_search",
|
|
123
|
+
item=Record,
|
|
124
|
+
meta=SearchResultsMeta,
|
|
125
|
+
query={
|
|
126
|
+
"q": query,
|
|
127
|
+
"collections": collections,
|
|
128
|
+
"filter": {"status": status},
|
|
129
|
+
"limit": limit,
|
|
130
|
+
"cursor": cursor,
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
async def close(self) -> None:
|
|
135
|
+
"""Closes the connections (unless an `http_client` was passed in)."""
|
|
136
|
+
await self._client.close()
|
|
137
|
+
|
|
138
|
+
async def __aenter__(self) -> Self:
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
async def __aexit__(
|
|
142
|
+
self,
|
|
143
|
+
exc_type: type[BaseException] | None,
|
|
144
|
+
exc: BaseException | None,
|
|
145
|
+
tb: TracebackType | None,
|
|
146
|
+
) -> None:
|
|
147
|
+
await self.close()
|
|
File without changes
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from typing_extensions import Unpack
|
|
6
|
+
|
|
7
|
+
from ..._errors import UploadError
|
|
8
|
+
from ..._pagination import AsyncPaginator
|
|
9
|
+
from ..._uploads import Uploadable, open_upload
|
|
10
|
+
from ...models import Asset, AssetUpload, DownloadUrl, ImageUrl
|
|
11
|
+
from ...params import AssetUploadCreate
|
|
12
|
+
from ._resource import AsyncResource
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AsyncAssets(AsyncResource):
|
|
16
|
+
"""Files of the key's project. `asset` is an asset ID (`ast_...`)."""
|
|
17
|
+
|
|
18
|
+
async def upload(
|
|
19
|
+
self,
|
|
20
|
+
file: Uploadable,
|
|
21
|
+
*,
|
|
22
|
+
filename: str | None = None,
|
|
23
|
+
content_type: str | None = None,
|
|
24
|
+
byte_size: int | None = None,
|
|
25
|
+
idempotency_key: str | None = None,
|
|
26
|
+
) -> Asset:
|
|
27
|
+
"""Uploads a file and returns the `ready` asset: creates the upload, sends the
|
|
28
|
+
bytes straight to storage, then completes it (which checks the file).
|
|
29
|
+
|
|
30
|
+
`file` is bytes, a path, or a file opened in binary mode. Raises UploadError if
|
|
31
|
+
storage refuses the bytes, and ValidationError if the file fails the checks.
|
|
32
|
+
"""
|
|
33
|
+
source = open_upload(file, filename, content_type, byte_size)
|
|
34
|
+
try:
|
|
35
|
+
created = await self.create_upload(
|
|
36
|
+
filename=source.filename,
|
|
37
|
+
content_type=source.content_type,
|
|
38
|
+
byte_size=source.byte_size,
|
|
39
|
+
idempotency_key=idempotency_key,
|
|
40
|
+
)
|
|
41
|
+
response = await self._client.put_upload(
|
|
42
|
+
created.upload.url, created.upload.method, created.upload.headers, source
|
|
43
|
+
)
|
|
44
|
+
finally:
|
|
45
|
+
source.close()
|
|
46
|
+
if not response.is_success:
|
|
47
|
+
raise UploadError(
|
|
48
|
+
f"Storage refused the upload of {source.filename} ({response.status_code})",
|
|
49
|
+
response.status_code,
|
|
50
|
+
)
|
|
51
|
+
return await self.complete(created.asset.id)
|
|
52
|
+
|
|
53
|
+
async def create_upload(
|
|
54
|
+
self, *, idempotency_key: str | None = None, **params: Unpack[AssetUploadCreate]
|
|
55
|
+
) -> AssetUpload:
|
|
56
|
+
"""The first step of an upload: a `pending` asset and a presigned URL to PUT the
|
|
57
|
+
bytes to (for example from a browser). Then `complete` it."""
|
|
58
|
+
return await self._client.request(
|
|
59
|
+
"assets_upload", cast=AssetUpload, body=params, idempotency_key=idempotency_key
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
async def complete(self, asset: str, *, idempotency_key: str | None = None) -> Asset:
|
|
63
|
+
"""Checks an uploaded file and marks the asset `ready`."""
|
|
64
|
+
return await self._client.request(
|
|
65
|
+
"assets_complete",
|
|
66
|
+
cast=Asset,
|
|
67
|
+
path={"asset_id": asset},
|
|
68
|
+
idempotency_key=idempotency_key,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def list(
|
|
72
|
+
self, *, deleted: bool | None = None, limit: int | None = None, cursor: str | None = None
|
|
73
|
+
) -> AsyncPaginator[Asset]:
|
|
74
|
+
return self._client.paginate(
|
|
75
|
+
"assets_list",
|
|
76
|
+
item=Asset,
|
|
77
|
+
query={"deleted": deleted, "limit": limit, "cursor": cursor},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
async def get(self, asset: str) -> Asset:
|
|
81
|
+
return await self._client.request("assets_get", cast=Asset, path={"asset_id": asset})
|
|
82
|
+
|
|
83
|
+
async def delete(self, asset: str, *, idempotency_key: str | None = None) -> Asset:
|
|
84
|
+
"""Soft-deletes the asset; the file is purged after 30 days."""
|
|
85
|
+
return await self._client.request(
|
|
86
|
+
"assets_delete",
|
|
87
|
+
cast=Asset,
|
|
88
|
+
path={"asset_id": asset},
|
|
89
|
+
idempotency_key=idempotency_key,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
async def restore(self, asset: str, *, idempotency_key: str | None = None) -> Asset:
|
|
93
|
+
return await self._client.request(
|
|
94
|
+
"assets_restore",
|
|
95
|
+
cast=Asset,
|
|
96
|
+
path={"asset_id": asset},
|
|
97
|
+
idempotency_key=idempotency_key,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
async def image_url(
|
|
101
|
+
self,
|
|
102
|
+
asset: str,
|
|
103
|
+
*,
|
|
104
|
+
width: int | None = None,
|
|
105
|
+
height: int | None = None,
|
|
106
|
+
fit: Literal["contain", "cover", "fill"] | None = None,
|
|
107
|
+
format: Literal["original", "webp", "avif", "jpeg", "png"] | None = None,
|
|
108
|
+
quality: int | None = None,
|
|
109
|
+
) -> ImageUrl:
|
|
110
|
+
"""A signed, cacheable URL of an image rendition (`url`)."""
|
|
111
|
+
return await self._client.request(
|
|
112
|
+
"assets_image_url",
|
|
113
|
+
cast=ImageUrl,
|
|
114
|
+
path={"asset_id": asset},
|
|
115
|
+
query={
|
|
116
|
+
"width": width,
|
|
117
|
+
"height": height,
|
|
118
|
+
"fit": fit,
|
|
119
|
+
"format": format,
|
|
120
|
+
"quality": quality,
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
async def download_url(self, asset: str) -> DownloadUrl:
|
|
125
|
+
"""A 15-minute link to download the original file (`url`)."""
|
|
126
|
+
return await self._client.request(
|
|
127
|
+
"assets_download_url", cast=DownloadUrl, path={"asset_id": asset}
|
|
128
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from ..._pagination import AsyncPaginator
|
|
6
|
+
from ...models import AuditEvent, Principal
|
|
7
|
+
from ...models import FeatureFlags as FeatureFlagValues
|
|
8
|
+
from ...models import Health as HealthStatus
|
|
9
|
+
from ...types import RecordFilter
|
|
10
|
+
from ._resource import AsyncResource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AsyncAuditEvents(AsyncResource):
|
|
14
|
+
"""The project's activity log, newest first. Needs the `audit:read` scope."""
|
|
15
|
+
|
|
16
|
+
def list(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
filter: RecordFilter | None = None,
|
|
20
|
+
sort: Literal["id", "-id"] | None = None,
|
|
21
|
+
limit: int | None = None,
|
|
22
|
+
cursor: str | None = None,
|
|
23
|
+
) -> AsyncPaginator[AuditEvent]:
|
|
24
|
+
"""`filter` takes `action` (exact, or a `record.*` prefix), `resource_type`,
|
|
25
|
+
`resource_id`, `actor_type`, `actor_id`, `since` and `until`."""
|
|
26
|
+
return self._client.paginate(
|
|
27
|
+
"audit_events_list_for_project",
|
|
28
|
+
item=AuditEvent,
|
|
29
|
+
query={"filter": filter, "sort": sort, "limit": limit, "cursor": cursor},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AsyncFeatureFlags(AsyncResource):
|
|
34
|
+
async def list(self) -> FeatureFlagValues:
|
|
35
|
+
"""Flags evaluated for the key's project."""
|
|
36
|
+
return await self._client.request("feature_flags_list", cast=FeatureFlagValues)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AsyncMe(AsyncResource):
|
|
40
|
+
async def get(self) -> Principal:
|
|
41
|
+
"""The API key: its project, scopes, `published_only` and expiry."""
|
|
42
|
+
return await self._client.request("me_get", cast=Principal)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AsyncHealth(AsyncResource):
|
|
46
|
+
async def check(self) -> HealthStatus:
|
|
47
|
+
return await self._client.request("health_check", cast=HealthStatus)
|