pierre-sdk 0.1.0.dev4__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.
- pierre/__init__.py +59 -0
- pierre/_client.py +544 -0
- pierre/_compat.py +14 -0
- pierre/_content.py +168 -0
- pierre/_credentials.py +321 -0
- pierre/_errors.py +205 -0
- pierre/_generated/__init__.py +2 -0
- pierre/_generated/client.py +54 -0
- pierre/_generated/headers.py +69 -0
- pierre/_generated/namespaces/__init__.py +2 -0
- pierre/_generated/namespaces/meta.py +125 -0
- pierre/_generated/namespaces/repos.py +5658 -0
- pierre/_generated/schemas.py +1437 -0
- pierre/_generated/scopes.py +12 -0
- pierre/_generated/webhooks.py +30 -0
- pierre/_helpers/__init__.py +1 -0
- pierre/_helpers/commits.py +220 -0
- pierre/_helpers/repos.py +58 -0
- pierre/_operation.py +66 -0
- pierre/_page.py +154 -0
- pierre/_response.py +92 -0
- pierre/_retry.py +65 -0
- pierre/_runtime.py +763 -0
- pierre/_types.py +73 -0
- pierre/_version.py +3 -0
- pierre/auth.py +84 -0
- pierre/git.py +161 -0
- pierre/py.typed +0 -0
- pierre/types.py +14 -0
- pierre/webhooks.py +145 -0
- pierre_sdk-0.1.0.dev4.dist-info/METADATA +153 -0
- pierre_sdk-0.1.0.dev4.dist-info/RECORD +34 -0
- pierre_sdk-0.1.0.dev4.dist-info/WHEEL +4 -0
- pierre_sdk-0.1.0.dev4.dist-info/licenses/LICENSE +189 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Generated by sdkgen. Do not edit this file.
|
|
2
|
+
"""The scopes of the operations of the API."""
|
|
3
|
+
|
|
4
|
+
from typing import Literal, Tuple
|
|
5
|
+
|
|
6
|
+
__all__ = ["SCOPES", "Scope"]
|
|
7
|
+
|
|
8
|
+
# A scope that an operation of this SDK version needs.
|
|
9
|
+
Scope = Literal["git:read", "git:write", "org:read", "repo:write"]
|
|
10
|
+
|
|
11
|
+
# Each scope that an operation of this SDK version needs, in name order.
|
|
12
|
+
SCOPES: Tuple[Scope, ...] = ("git:read", "git:write", "org:read", "repo:write")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Generated by sdkgen. Do not edit this file.
|
|
2
|
+
"""The webhook events of the API, each with its type."""
|
|
3
|
+
|
|
4
|
+
from typing import Literal, Tuple, Union
|
|
5
|
+
|
|
6
|
+
from pierre._generated import schemas
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"KnownWebhookEvent",
|
|
10
|
+
"WebhookEventType",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
# An event of a type that this SDK version knows: the payload schema of its type.
|
|
14
|
+
KnownWebhookEvent = Union[
|
|
15
|
+
schemas.GitPushEvent,
|
|
16
|
+
schemas.GitSyncFailedEvent,
|
|
17
|
+
schemas.GitSyncStartedEvent,
|
|
18
|
+
schemas.GitSyncSucceededEvent,
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
# The type of an event that this SDK version knows.
|
|
22
|
+
WebhookEventType = Literal["push", "repo.sync.failed", "repo.sync.started", "repo.sync.succeeded"]
|
|
23
|
+
|
|
24
|
+
# Each event type that `verify_webhook` types, in name order.
|
|
25
|
+
WEBHOOK_EVENT_TYPES: Tuple[WebhookEventType, ...] = (
|
|
26
|
+
"push",
|
|
27
|
+
"repo.sync.failed",
|
|
28
|
+
"repo.sync.started",
|
|
29
|
+
"repo.sync.succeeded",
|
|
30
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Hand-written helpers over the runtime."""
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""The public form of the streaming commit writes. The wire form sends a
|
|
2
|
+
metadata line, then the content of each file as base64 chunk lines. These helpers take the
|
|
3
|
+
content itself and write the chunks."""
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import inspect
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import (
|
|
9
|
+
Any,
|
|
10
|
+
AsyncIterable,
|
|
11
|
+
AsyncIterator,
|
|
12
|
+
Awaitable,
|
|
13
|
+
Callable,
|
|
14
|
+
Dict,
|
|
15
|
+
Iterable,
|
|
16
|
+
Iterator,
|
|
17
|
+
List,
|
|
18
|
+
Literal,
|
|
19
|
+
Mapping,
|
|
20
|
+
Tuple,
|
|
21
|
+
Union,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from pierre._compat import NotRequired, TypedDict
|
|
25
|
+
from pierre._content import (
|
|
26
|
+
Content,
|
|
27
|
+
async_chunks,
|
|
28
|
+
async_close,
|
|
29
|
+
chunks,
|
|
30
|
+
close,
|
|
31
|
+
)
|
|
32
|
+
from pierre._errors import PierreError
|
|
33
|
+
from pierre._operation import OperationSpec
|
|
34
|
+
from pierre._runtime import AsyncRuntime, SyncRuntime
|
|
35
|
+
from pierre._types import RequestOptions
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"CommitFile",
|
|
39
|
+
"Content",
|
|
40
|
+
"FileContent",
|
|
41
|
+
"async_create_commit",
|
|
42
|
+
"async_create_commit_from_diff",
|
|
43
|
+
"create_commit",
|
|
44
|
+
"create_commit_from_diff",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# File content, or a function that returns it when the SDK sends the file. In `AsyncPierre`, the
|
|
48
|
+
# function can be async.
|
|
49
|
+
FileContent = Union[Content, Callable[[], Union[Content, Awaitable[Content]]]]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CommitFile(TypedDict):
|
|
53
|
+
"""One file of a commit."""
|
|
54
|
+
|
|
55
|
+
# The repository path of the file.
|
|
56
|
+
path: str
|
|
57
|
+
# `upsert` by default.
|
|
58
|
+
operation: NotRequired[Literal["upsert", "delete"]]
|
|
59
|
+
# The Git file mode. `100644` by default for an upsert.
|
|
60
|
+
mode: NotRequired[str]
|
|
61
|
+
# The file content. An upsert needs it, and a delete must not have it.
|
|
62
|
+
#
|
|
63
|
+
# The SDK reads all of `files` before it sends any content, because the first NDJSON line
|
|
64
|
+
# names every file. To open only one file at a time, give a function, for example
|
|
65
|
+
# `lambda: open(path, "rb")`. The SDK calls the function when it sends that file, and closes
|
|
66
|
+
# the file that the function returns. The SDK copies a `bytearray` or a `memoryview` when it
|
|
67
|
+
# reads the entry, so a caller can reuse a buffer.
|
|
68
|
+
content: NotRequired[FileContent]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# One checked file: the file fields, its content, and its content ID.
|
|
72
|
+
_Entry = Tuple[Dict[str, Any], object, str]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _entry(file: object) -> _Entry:
|
|
76
|
+
if not isinstance(file, Mapping) or not isinstance(file.get("path"), str):
|
|
77
|
+
raise PierreError("Each file needs `path`.")
|
|
78
|
+
path = file["path"]
|
|
79
|
+
operation = file.get("operation", "upsert")
|
|
80
|
+
content = file.get("content")
|
|
81
|
+
if operation == "upsert" and content is None:
|
|
82
|
+
raise PierreError(f"The file {path} needs `content`.")
|
|
83
|
+
if operation == "delete" and content is not None:
|
|
84
|
+
raise PierreError(f"The delete of {path} must not have `content`.")
|
|
85
|
+
if isinstance(content, (bytearray, memoryview)):
|
|
86
|
+
content = bytes(content)
|
|
87
|
+
wire: Dict[str, Any] = {"path": path, "content_id": str(uuid.uuid4()), "operation": operation}
|
|
88
|
+
if operation == "upsert":
|
|
89
|
+
wire["mode"] = file.get("mode", "100644")
|
|
90
|
+
return wire, content, wire["content_id"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _is_function(content: object) -> bool:
|
|
94
|
+
return (
|
|
95
|
+
callable(content)
|
|
96
|
+
and not callable(getattr(content, "read", None))
|
|
97
|
+
and not isinstance(content, (Iterable, AsyncIterable))
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _blob_chunk(content_id: str, data: bytes, eof: bool) -> Dict[str, Any]:
|
|
102
|
+
return {
|
|
103
|
+
"blob_chunk": {
|
|
104
|
+
"content_id": content_id,
|
|
105
|
+
"data": base64.b64encode(data).decode("ascii"),
|
|
106
|
+
"eof": eof,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _sync_files(files: object) -> Iterable[object]:
|
|
112
|
+
if hasattr(files, "__aiter__") or not isinstance(files, Iterable):
|
|
113
|
+
raise PierreError(
|
|
114
|
+
"`Pierre` takes a list or an iterable of files. Use `AsyncPierre` for an async iterable."
|
|
115
|
+
)
|
|
116
|
+
return files
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def create_commit(
|
|
120
|
+
runtime: SyncRuntime,
|
|
121
|
+
operation: OperationSpec[Any],
|
|
122
|
+
params: Mapping[str, Any],
|
|
123
|
+
options: RequestOptions,
|
|
124
|
+
) -> Any:
|
|
125
|
+
"""`pierre.repos.commits.create`. The SDK reads all of `files` before the request."""
|
|
126
|
+
metadata = {name: value for name, value in params.items() if name != "files"}
|
|
127
|
+
entries = [_entry(file) for file in _sync_files(params["files"])]
|
|
128
|
+
|
|
129
|
+
def blob_chunks() -> Iterator[Dict[str, Any]]:
|
|
130
|
+
for _, content, content_id in entries:
|
|
131
|
+
if content is None:
|
|
132
|
+
continue
|
|
133
|
+
source = content() if _is_function(content) else content # type: ignore[operator]
|
|
134
|
+
if inspect.isawaitable(source):
|
|
135
|
+
if inspect.iscoroutine(source):
|
|
136
|
+
source.close()
|
|
137
|
+
raise PierreError("`Pierre` takes a content function that is not async.")
|
|
138
|
+
try:
|
|
139
|
+
for data, eof in chunks(source):
|
|
140
|
+
yield _blob_chunk(content_id, data, eof)
|
|
141
|
+
finally:
|
|
142
|
+
if source is not content:
|
|
143
|
+
close(source)
|
|
144
|
+
|
|
145
|
+
wire = {**metadata, "files": [entry[0] for entry in entries], "chunks": blob_chunks()}
|
|
146
|
+
return runtime.request(operation, wire, options)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def async_create_commit(
|
|
150
|
+
runtime: AsyncRuntime,
|
|
151
|
+
operation: OperationSpec[Any],
|
|
152
|
+
params: Mapping[str, Any],
|
|
153
|
+
options: RequestOptions,
|
|
154
|
+
) -> Any:
|
|
155
|
+
"""`pierre.repos.commits.create` of `AsyncPierre`, which also takes async files and content."""
|
|
156
|
+
metadata = {name: value for name, value in params.items() if name != "files"}
|
|
157
|
+
files = params["files"]
|
|
158
|
+
entries: List[_Entry] = []
|
|
159
|
+
if isinstance(files, AsyncIterable):
|
|
160
|
+
async for file in files:
|
|
161
|
+
entries.append(_entry(file))
|
|
162
|
+
elif isinstance(files, Iterable):
|
|
163
|
+
entries = [_entry(file) for file in files]
|
|
164
|
+
else:
|
|
165
|
+
raise PierreError("`files` must be a list, an iterable, or an async iterable of files.")
|
|
166
|
+
|
|
167
|
+
async def blob_chunks() -> AsyncIterator[Dict[str, Any]]:
|
|
168
|
+
for _, content, content_id in entries:
|
|
169
|
+
if content is None:
|
|
170
|
+
continue
|
|
171
|
+
source = content() if _is_function(content) else content # type: ignore[operator]
|
|
172
|
+
if inspect.isawaitable(source):
|
|
173
|
+
source = await source
|
|
174
|
+
try:
|
|
175
|
+
async for data, eof in async_chunks(source):
|
|
176
|
+
yield _blob_chunk(content_id, data, eof)
|
|
177
|
+
finally:
|
|
178
|
+
if source is not content:
|
|
179
|
+
await async_close(source)
|
|
180
|
+
|
|
181
|
+
wire = {**metadata, "files": [entry[0] for entry in entries], "chunks": blob_chunks()}
|
|
182
|
+
return await runtime.request(operation, wire, options)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _diff_chunk(data: bytes, eof: bool) -> Dict[str, Any]:
|
|
186
|
+
return {"diff_chunk": {"data": base64.b64encode(data).decode("ascii"), "eof": eof}}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def create_commit_from_diff(
|
|
190
|
+
runtime: SyncRuntime,
|
|
191
|
+
operation: OperationSpec[Any],
|
|
192
|
+
params: Mapping[str, Any],
|
|
193
|
+
options: RequestOptions,
|
|
194
|
+
) -> Any:
|
|
195
|
+
"""`pierre.repos.commits.create_from_diff`: the diff as `diff_chunk` lines."""
|
|
196
|
+
metadata = {name: value for name, value in params.items() if name != "diff"}
|
|
197
|
+
diff = params["diff"]
|
|
198
|
+
|
|
199
|
+
def diff_chunks() -> Iterator[Dict[str, Any]]:
|
|
200
|
+
for data, eof in chunks(diff):
|
|
201
|
+
yield _diff_chunk(data, eof)
|
|
202
|
+
|
|
203
|
+
return runtime.request(operation, {**metadata, "chunks": diff_chunks()}, options)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
async def async_create_commit_from_diff(
|
|
207
|
+
runtime: AsyncRuntime,
|
|
208
|
+
operation: OperationSpec[Any],
|
|
209
|
+
params: Mapping[str, Any],
|
|
210
|
+
options: RequestOptions,
|
|
211
|
+
) -> Any:
|
|
212
|
+
"""`pierre.repos.commits.create_from_diff` of `AsyncPierre`."""
|
|
213
|
+
metadata = {name: value for name, value in params.items() if name != "diff"}
|
|
214
|
+
diff = params["diff"]
|
|
215
|
+
|
|
216
|
+
async def diff_chunks() -> AsyncIterator[Dict[str, Any]]:
|
|
217
|
+
async for data, eof in async_chunks(diff):
|
|
218
|
+
yield _diff_chunk(data, eof)
|
|
219
|
+
|
|
220
|
+
return await runtime.request(operation, {**metadata, "chunks": diff_chunks()}, options)
|
pierre/_helpers/repos.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""The repository create. A copy reads its source with a token, and the SDK adds
|
|
2
|
+
that token when the caller gives none."""
|
|
3
|
+
|
|
4
|
+
from typing import Any, Mapping, Optional
|
|
5
|
+
|
|
6
|
+
from pierre._operation import OperationSpec
|
|
7
|
+
from pierre._runtime import AsyncRuntime, SyncRuntime
|
|
8
|
+
from pierre._types import RequestOptions
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _source(params: Mapping[str, Any]) -> Optional[Mapping[str, Any]]:
|
|
12
|
+
"""The source of a copy that needs a token, or `None`."""
|
|
13
|
+
source = params.get("base_repo")
|
|
14
|
+
if not isinstance(source, Mapping):
|
|
15
|
+
return None
|
|
16
|
+
auth = source.get("auth")
|
|
17
|
+
# The deprecated sync form, with `provider`, reads no Code Storage repository.
|
|
18
|
+
if source.get("provider") is not None or (isinstance(auth, Mapping) and "token" in auth):
|
|
19
|
+
return None
|
|
20
|
+
# The server rejects a copy with no source name.
|
|
21
|
+
return source if isinstance(source.get("name"), str) else None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _with_token(params: Mapping[str, Any], source: Mapping[str, Any], token: str) -> Any:
|
|
25
|
+
auth = source.get("auth")
|
|
26
|
+
return {
|
|
27
|
+
**params,
|
|
28
|
+
"base_repo": {
|
|
29
|
+
**source,
|
|
30
|
+
"auth": {**(auth if isinstance(auth, Mapping) else {}), "token": token},
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_repo(
|
|
36
|
+
runtime: SyncRuntime,
|
|
37
|
+
operation: OperationSpec[Any],
|
|
38
|
+
params: Mapping[str, Any],
|
|
39
|
+
options: RequestOptions,
|
|
40
|
+
) -> Any:
|
|
41
|
+
"""`pierre.repos.create`, with a `git:read` token for the source repository of a copy."""
|
|
42
|
+
source = _source(params)
|
|
43
|
+
if source is not None:
|
|
44
|
+
params = _with_token(params, source, runtime.read_token(source["name"], options))
|
|
45
|
+
return runtime.request(operation, params, options)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def async_create_repo(
|
|
49
|
+
runtime: AsyncRuntime,
|
|
50
|
+
operation: OperationSpec[Any],
|
|
51
|
+
params: Mapping[str, Any],
|
|
52
|
+
options: RequestOptions,
|
|
53
|
+
) -> Any:
|
|
54
|
+
"""`pierre.repos.create` of `AsyncPierre`."""
|
|
55
|
+
source = _source(params)
|
|
56
|
+
if source is not None:
|
|
57
|
+
params = _with_token(params, source, await runtime.read_token(source["name"], options))
|
|
58
|
+
return await runtime.request(operation, params, options)
|
pierre/_operation.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""The facts of one operation, from the SDK model. The generated layer defines one spec for each
|
|
2
|
+
operation, and the runtime reads it to send the request."""
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Generic, Literal, Optional, Tuple, TypeVar, Union
|
|
6
|
+
|
|
7
|
+
Headers = TypeVar("Headers")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class ParameterSpec:
|
|
12
|
+
"""A path, query, or header parameter."""
|
|
13
|
+
|
|
14
|
+
location: Literal["path", "query", "header"]
|
|
15
|
+
# The wire name.
|
|
16
|
+
name: str
|
|
17
|
+
# The keyword argument.
|
|
18
|
+
field: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class JsonBodySpec:
|
|
23
|
+
"""A JSON body: the runtime sends these arguments. When the body is optional and the call
|
|
24
|
+
gives none of them, the runtime sends no body."""
|
|
25
|
+
|
|
26
|
+
required: bool
|
|
27
|
+
fields: Tuple[str, ...]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class NdjsonBodySpec:
|
|
32
|
+
"""An NDJSON body: the first line is `{first: <these arguments>}`, and the argument `chunks`
|
|
33
|
+
gives each later line when `rest` is true."""
|
|
34
|
+
|
|
35
|
+
first: str
|
|
36
|
+
fields: Tuple[str, ...]
|
|
37
|
+
rest: bool
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class HeaderSpec:
|
|
42
|
+
"""A declared response header, by its wire name and its field."""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
field: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class OperationSpec(Generic[Headers]):
|
|
50
|
+
"""One operation. `Headers` is the type of its declared response headers."""
|
|
51
|
+
|
|
52
|
+
id: str
|
|
53
|
+
method: Literal["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"]
|
|
54
|
+
path: str
|
|
55
|
+
parameters: Tuple[ParameterSpec, ...]
|
|
56
|
+
body: Union[JsonBodySpec, NdjsonBodySpec, None]
|
|
57
|
+
response_encoding: Literal["json", "ndjson", "bytes", "none"]
|
|
58
|
+
response_headers: Tuple[HeaderSpec, ...]
|
|
59
|
+
# Whether a 304 response is a result, not an error.
|
|
60
|
+
not_modified: bool
|
|
61
|
+
scopes: Tuple[str, ...]
|
|
62
|
+
# Whether the token needs a `repo` claim.
|
|
63
|
+
repo_claim: bool
|
|
64
|
+
# The response field that holds the items of a list, or `None` for another operation.
|
|
65
|
+
item_field: Optional[str]
|
|
66
|
+
idempotency_key: bool
|
pierre/_page.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""List pages."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
AsyncIterator,
|
|
7
|
+
Awaitable,
|
|
8
|
+
Callable,
|
|
9
|
+
Generator,
|
|
10
|
+
Generic,
|
|
11
|
+
Iterator,
|
|
12
|
+
List,
|
|
13
|
+
Optional,
|
|
14
|
+
TypeVar,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from pierre._errors import PierreError
|
|
20
|
+
|
|
21
|
+
Item = TypeVar("Item")
|
|
22
|
+
Body = TypeVar("Body")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _next_cursor(has_more: bool, next_cursor: Optional[str]) -> Optional[str]:
|
|
26
|
+
return next_cursor if has_more and next_cursor else None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_NO_NEXT_PAGE = "No next page. Call `has_next_page()` before `get_next_page()`."
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Page(Generic[Item, Body]):
|
|
33
|
+
"""One page of a list in `Pierre`. `body` is the full response body, with the fields beside
|
|
34
|
+
the items. Iteration gives the items of this page and of each next page. To serialize a page,
|
|
35
|
+
serialize `body`, the response that the API sent: `json.dumps(page.body)`."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
data: List[Item],
|
|
41
|
+
has_more: bool,
|
|
42
|
+
next_cursor: Optional[str],
|
|
43
|
+
body: Body,
|
|
44
|
+
response: httpx.Response,
|
|
45
|
+
fetch: Callable[[str], "Page[Item, Body]"],
|
|
46
|
+
) -> None:
|
|
47
|
+
self.data = data
|
|
48
|
+
self.has_more = has_more
|
|
49
|
+
self.next_cursor = next_cursor
|
|
50
|
+
self.body = body
|
|
51
|
+
# The `httpx.Response` of this page. The SDK read its body.
|
|
52
|
+
self.response = response
|
|
53
|
+
self._fetch = fetch
|
|
54
|
+
|
|
55
|
+
def has_next_page(self) -> bool:
|
|
56
|
+
"""Whether a next page exists: `has_more` is true, and `next_cursor` is not empty."""
|
|
57
|
+
return _next_cursor(self.has_more, self.next_cursor) is not None
|
|
58
|
+
|
|
59
|
+
def get_next_page(self) -> "Page[Item, Body]":
|
|
60
|
+
"""Gets the next page, with the arguments of the first call. Call `has_next_page()` first:
|
|
61
|
+
with no next page, it raises `PierreError`."""
|
|
62
|
+
cursor = _next_cursor(self.has_more, self.next_cursor)
|
|
63
|
+
if cursor is None:
|
|
64
|
+
raise PierreError(_NO_NEXT_PAGE)
|
|
65
|
+
return self._fetch(cursor)
|
|
66
|
+
|
|
67
|
+
def iter_pages(self) -> Iterator["Page[Item, Body]"]:
|
|
68
|
+
"""This page, then each next page."""
|
|
69
|
+
page = self
|
|
70
|
+
while True:
|
|
71
|
+
yield page
|
|
72
|
+
if not page.has_next_page():
|
|
73
|
+
return
|
|
74
|
+
page = page.get_next_page()
|
|
75
|
+
|
|
76
|
+
def __iter__(self) -> Iterator[Item]:
|
|
77
|
+
for page in self.iter_pages():
|
|
78
|
+
yield from page.data
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AsyncPage(Generic[Item, Body]):
|
|
82
|
+
"""One page of a list in `AsyncPierre`. `async for` gives the items of this page and of each
|
|
83
|
+
next page. To serialize a page, serialize `body`, the response that the API sent:
|
|
84
|
+
`json.dumps(page.body)`."""
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
*,
|
|
89
|
+
data: List[Item],
|
|
90
|
+
has_more: bool,
|
|
91
|
+
next_cursor: Optional[str],
|
|
92
|
+
body: Body,
|
|
93
|
+
response: httpx.Response,
|
|
94
|
+
fetch: Callable[[str], Awaitable["AsyncPage[Item, Body]"]],
|
|
95
|
+
) -> None:
|
|
96
|
+
self.data = data
|
|
97
|
+
self.has_more = has_more
|
|
98
|
+
self.next_cursor = next_cursor
|
|
99
|
+
self.body = body
|
|
100
|
+
# The `httpx.Response` of this page. The SDK read its body.
|
|
101
|
+
self.response = response
|
|
102
|
+
self._fetch = fetch
|
|
103
|
+
|
|
104
|
+
def has_next_page(self) -> bool:
|
|
105
|
+
"""Whether a next page exists: `has_more` is true, and `next_cursor` is not empty."""
|
|
106
|
+
return _next_cursor(self.has_more, self.next_cursor) is not None
|
|
107
|
+
|
|
108
|
+
async def get_next_page(self) -> "AsyncPage[Item, Body]":
|
|
109
|
+
"""Gets the next page, with the arguments of the first call. Call `has_next_page()` first:
|
|
110
|
+
with no next page, it raises `PierreError`."""
|
|
111
|
+
cursor = _next_cursor(self.has_more, self.next_cursor)
|
|
112
|
+
if cursor is None:
|
|
113
|
+
raise PierreError(_NO_NEXT_PAGE)
|
|
114
|
+
return await self._fetch(cursor)
|
|
115
|
+
|
|
116
|
+
async def iter_pages(self) -> AsyncIterator["AsyncPage[Item, Body]"]:
|
|
117
|
+
"""This page, then each next page."""
|
|
118
|
+
page = self
|
|
119
|
+
while True:
|
|
120
|
+
yield page
|
|
121
|
+
if not page.has_next_page():
|
|
122
|
+
return
|
|
123
|
+
page = await page.get_next_page()
|
|
124
|
+
|
|
125
|
+
async def __aiter__(self) -> AsyncIterator[Item]:
|
|
126
|
+
async for page in self.iter_pages():
|
|
127
|
+
for item in page.data:
|
|
128
|
+
yield item
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class AsyncPaginator(Generic[Item, Body]):
|
|
132
|
+
"""The result of a list method of `AsyncPierre`. `await` gives the first page, and
|
|
133
|
+
`async for` gives the items of every page. The request starts at the first use, and every use
|
|
134
|
+
reads that one request."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, start: Callable[[], Awaitable[AsyncPage[Item, Body]]]) -> None:
|
|
137
|
+
self._start = start
|
|
138
|
+
self._first: "Optional[asyncio.Future[AsyncPage[Item, Body]]]" = None
|
|
139
|
+
|
|
140
|
+
async def _page(self) -> AsyncPage[Item, Body]:
|
|
141
|
+
# One task holds the first request, so that uses at the same time share its result or
|
|
142
|
+
# its error. A cancel stops only the use that the caller cancels, so a later use still
|
|
143
|
+
# reads the request.
|
|
144
|
+
if self._first is None:
|
|
145
|
+
self._first = asyncio.ensure_future(self._start())
|
|
146
|
+
return await asyncio.shield(self._first)
|
|
147
|
+
|
|
148
|
+
def __await__(self) -> Generator[Any, None, AsyncPage[Item, Body]]:
|
|
149
|
+
return self._page().__await__()
|
|
150
|
+
|
|
151
|
+
async def __aiter__(self) -> AsyncIterator[Item]:
|
|
152
|
+
page = await self._page()
|
|
153
|
+
async for item in page:
|
|
154
|
+
yield item
|
pierre/_response.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""`pierre.with_raw_response`: each method gives its result with the response.
|
|
2
|
+
The `with_raw_response` namespaces wrap the methods of the client namespaces, so they take the
|
|
3
|
+
same arguments."""
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Awaitable, Callable, Generic, Optional, TypeVar, cast
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from pierre._compat import ParamSpec
|
|
13
|
+
from pierre._errors import PierreError
|
|
14
|
+
from pierre._operation import OperationSpec
|
|
15
|
+
|
|
16
|
+
Data = TypeVar("Data")
|
|
17
|
+
Headers = TypeVar("Headers")
|
|
18
|
+
Params = ParamSpec("Params")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class RawResponse(Generic[Data, Headers]):
|
|
23
|
+
"""The result of a method of `pierre.with_raw_response`."""
|
|
24
|
+
|
|
25
|
+
# What the method of the client returns.
|
|
26
|
+
data: Data
|
|
27
|
+
# The `httpx.Response` of the last attempt. The SDK read its body, except for a byte stream,
|
|
28
|
+
# whose `data` is this response.
|
|
29
|
+
response: httpx.Response
|
|
30
|
+
# The response headers that the document declares, by the wire form of each name.
|
|
31
|
+
headers: Headers
|
|
32
|
+
# The `X-Request-Id` header, or `None` when the server does not send it.
|
|
33
|
+
request_id: Optional[str]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _Claim:
|
|
37
|
+
"""Marks the first request of a `with_raw_response` call. A request inside the call, such as
|
|
38
|
+
one from a content function, gives its plain result."""
|
|
39
|
+
|
|
40
|
+
claimed = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_RAW: "ContextVar[Optional[_Claim]]" = ContextVar("pierre_raw_response", default=None)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def claim_raw() -> bool:
|
|
47
|
+
"""Whether this request is the first request of a `with_raw_response` call."""
|
|
48
|
+
claim = _RAW.get()
|
|
49
|
+
if claim is None or claim.claimed:
|
|
50
|
+
return False
|
|
51
|
+
claim.claimed = True
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _checked(result: object) -> object:
|
|
56
|
+
if not isinstance(result, RawResponse):
|
|
57
|
+
raise PierreError("The method sent no request, so it has no response.")
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def raw_method(
|
|
62
|
+
method: Callable[Params, Data], operation: OperationSpec[Headers]
|
|
63
|
+
) -> Callable[Params, RawResponse[Data, Headers]]:
|
|
64
|
+
"""The method of `Pierre` that returns a `RawResponse`. `operation` gives the headers type."""
|
|
65
|
+
|
|
66
|
+
@functools.wraps(method)
|
|
67
|
+
def call(*args: Params.args, **kwargs: Params.kwargs) -> RawResponse[Data, Headers]:
|
|
68
|
+
token = _RAW.set(_Claim())
|
|
69
|
+
try:
|
|
70
|
+
result: object = method(*args, **kwargs)
|
|
71
|
+
finally:
|
|
72
|
+
_RAW.reset(token)
|
|
73
|
+
return cast(RawResponse[Data, Headers], _checked(result))
|
|
74
|
+
|
|
75
|
+
return call
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def async_raw_method(
|
|
79
|
+
method: Callable[Params, Awaitable[Data]], operation: OperationSpec[Headers]
|
|
80
|
+
) -> Callable[Params, Awaitable[RawResponse[Data, Headers]]]:
|
|
81
|
+
"""The method of `AsyncPierre` that returns a `RawResponse`."""
|
|
82
|
+
|
|
83
|
+
@functools.wraps(method)
|
|
84
|
+
async def call(*args: Params.args, **kwargs: Params.kwargs) -> RawResponse[Data, Headers]:
|
|
85
|
+
token = _RAW.set(_Claim())
|
|
86
|
+
try:
|
|
87
|
+
result: object = await method(*args, **kwargs)
|
|
88
|
+
finally:
|
|
89
|
+
_RAW.reset(token)
|
|
90
|
+
return cast(RawResponse[Data, Headers], _checked(result))
|
|
91
|
+
|
|
92
|
+
return call
|