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
pierre/__init__.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""The Pierre SDK: `Pierre` for blocking code and `AsyncPierre` for `asyncio` code.
|
|
2
|
+
|
|
3
|
+
The local utilities are in their modules: `pierre.auth`, `pierre.git`, and `pierre.webhooks`. The
|
|
4
|
+
types of the API are in `pierre.types`.
|
|
5
|
+
|
|
6
|
+
Docs: the MCP server at https://code.storage/docs/mcp, or the Markdown pages from
|
|
7
|
+
https://code.storage/docs/llms.txt.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pierre._client import AsyncPierre, ClientOptions, Pierre
|
|
11
|
+
from pierre._errors import (
|
|
12
|
+
APIConnectionError,
|
|
13
|
+
APIError,
|
|
14
|
+
APITimeoutError,
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
BadRequestError,
|
|
17
|
+
ConflictError,
|
|
18
|
+
InternalServerError,
|
|
19
|
+
MergeConflictError,
|
|
20
|
+
NotFoundError,
|
|
21
|
+
PermissionDeniedError,
|
|
22
|
+
PierreError,
|
|
23
|
+
RateLimitError,
|
|
24
|
+
RefGuardError,
|
|
25
|
+
RefUpdateError,
|
|
26
|
+
WebhookVerificationError,
|
|
27
|
+
)
|
|
28
|
+
from pierre._page import AsyncPage, AsyncPaginator, Page
|
|
29
|
+
from pierre._response import RawResponse
|
|
30
|
+
from pierre._types import NOT_GIVEN, NotGiven
|
|
31
|
+
from pierre._version import __version__
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"__version__",
|
|
35
|
+
"NOT_GIVEN",
|
|
36
|
+
"APIConnectionError",
|
|
37
|
+
"APIError",
|
|
38
|
+
"APITimeoutError",
|
|
39
|
+
"AsyncPage",
|
|
40
|
+
"AsyncPaginator",
|
|
41
|
+
"AsyncPierre",
|
|
42
|
+
"AuthenticationError",
|
|
43
|
+
"BadRequestError",
|
|
44
|
+
"ClientOptions",
|
|
45
|
+
"ConflictError",
|
|
46
|
+
"InternalServerError",
|
|
47
|
+
"MergeConflictError",
|
|
48
|
+
"NotFoundError",
|
|
49
|
+
"NotGiven",
|
|
50
|
+
"Page",
|
|
51
|
+
"PermissionDeniedError",
|
|
52
|
+
"Pierre",
|
|
53
|
+
"PierreError",
|
|
54
|
+
"RateLimitError",
|
|
55
|
+
"RawResponse",
|
|
56
|
+
"RefGuardError",
|
|
57
|
+
"RefUpdateError",
|
|
58
|
+
"WebhookVerificationError",
|
|
59
|
+
]
|
pierre/_client.py
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
"""The clients: options, environment variables, credentials, and the transport. `Pierre` serves
|
|
2
|
+
blocking code, and `AsyncPierre` serves `asyncio` code."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, replace
|
|
7
|
+
from types import TracebackType
|
|
8
|
+
from typing import Callable, Dict, Mapping, Optional, Sequence, Tuple, Type, TypeVar, Union
|
|
9
|
+
from urllib.parse import urlsplit
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from pierre._compat import Self
|
|
14
|
+
from pierre._credentials import (
|
|
15
|
+
Credentials,
|
|
16
|
+
GetToken,
|
|
17
|
+
Rules,
|
|
18
|
+
Signer,
|
|
19
|
+
check_ref_policies,
|
|
20
|
+
policies_of,
|
|
21
|
+
token_issuer,
|
|
22
|
+
)
|
|
23
|
+
from pierre._errors import PierreError
|
|
24
|
+
from pierre._generated.client import (
|
|
25
|
+
AsyncPierreNamespaces,
|
|
26
|
+
AsyncPierreWithRawResponse,
|
|
27
|
+
PierreNamespaces,
|
|
28
|
+
PierreWithRawResponse,
|
|
29
|
+
)
|
|
30
|
+
from pierre._runtime import (
|
|
31
|
+
AsyncRuntime,
|
|
32
|
+
HttpConfig,
|
|
33
|
+
SyncRuntime,
|
|
34
|
+
check_max_retries,
|
|
35
|
+
check_timeout,
|
|
36
|
+
)
|
|
37
|
+
from pierre._types import RefPolicy, TokenRequest
|
|
38
|
+
from pierre._version import __version__
|
|
39
|
+
|
|
40
|
+
_DEFAULT_TIMEOUT = 60.0
|
|
41
|
+
_DEFAULT_MAX_RETRIES = 2
|
|
42
|
+
|
|
43
|
+
# A DNS label: the form of an organization name in its default hosts.
|
|
44
|
+
_HOST_LABEL = re.compile(r"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?", re.IGNORECASE | re.ASCII)
|
|
45
|
+
|
|
46
|
+
_VARIABLES = (
|
|
47
|
+
"PIERRE_ORG",
|
|
48
|
+
"PIERRE_PRIVATE_KEY",
|
|
49
|
+
"PIERRE_PRIVATE_KEY_FILE",
|
|
50
|
+
"PIERRE_TOKEN",
|
|
51
|
+
"PIERRE_API_BASE_URL",
|
|
52
|
+
"PIERRE_GIT_HOST",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class ClientOptions:
|
|
58
|
+
"""The settings of a client, on `pierre.options`. It holds no credential."""
|
|
59
|
+
|
|
60
|
+
# The organization name, when the client knows it.
|
|
61
|
+
org: Optional[str]
|
|
62
|
+
base_url: str
|
|
63
|
+
# The Git host, when the client knows the organization or `git_host` is set.
|
|
64
|
+
git_host: Optional[str]
|
|
65
|
+
# The timeout of each attempt, in seconds.
|
|
66
|
+
timeout: float
|
|
67
|
+
max_retries: int
|
|
68
|
+
# The ref policies of each request token, when the client has them.
|
|
69
|
+
ref_policies: Optional[Tuple[RefPolicy, ...]]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class _Credential:
|
|
74
|
+
kind: str
|
|
75
|
+
value: object
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class _Settings:
|
|
80
|
+
"""The settings after the options and the environment, before the defaults."""
|
|
81
|
+
|
|
82
|
+
org: Optional[str]
|
|
83
|
+
base_url: Optional[str]
|
|
84
|
+
git_host: Optional[str]
|
|
85
|
+
timeout: float
|
|
86
|
+
max_retries: int
|
|
87
|
+
default_headers: Mapping[str, str]
|
|
88
|
+
http_client: Optional[object]
|
|
89
|
+
rules: Optional[Rules]
|
|
90
|
+
credential: _Credential
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _environment() -> Dict[str, str]:
|
|
94
|
+
"""The Pierre variables of the process. An empty value counts as not set."""
|
|
95
|
+
return {name: os.environ[name] for name in _VARIABLES if os.environ.get(name)}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _credential_of(
|
|
99
|
+
private_key: Optional[str], token: Optional[str], get_token: Optional[GetToken]
|
|
100
|
+
) -> Optional[_Credential]:
|
|
101
|
+
"""The credential in the options, or `None`. More than one is an error."""
|
|
102
|
+
given = [value for value in (private_key, token, get_token) if value is not None]
|
|
103
|
+
if len(given) > 1:
|
|
104
|
+
raise PierreError("Give only one of `private_key`, `token`, and `get_token`.")
|
|
105
|
+
if private_key is not None:
|
|
106
|
+
key = private_key
|
|
107
|
+
return _Credential("private_key", Signer(lambda: key))
|
|
108
|
+
if token is not None:
|
|
109
|
+
if not isinstance(token, str) or token == "":
|
|
110
|
+
raise PierreError("`token` must not be empty.")
|
|
111
|
+
return _Credential("token", token)
|
|
112
|
+
if get_token is not None:
|
|
113
|
+
if not callable(get_token):
|
|
114
|
+
raise PierreError("`get_token` must be a function.")
|
|
115
|
+
return _Credential("get_token", get_token)
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _read_key_file(path: str) -> str:
|
|
120
|
+
try:
|
|
121
|
+
with open(path, encoding="utf-8") as file:
|
|
122
|
+
return file.read()
|
|
123
|
+
except OSError as error:
|
|
124
|
+
raise PierreError(f"Cannot read PIERRE_PRIVATE_KEY_FILE ({path}).") from error
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _environment_credential(env: Mapping[str, str]) -> _Credential:
|
|
128
|
+
token = env.get("PIERRE_TOKEN")
|
|
129
|
+
private_key = env.get("PIERRE_PRIVATE_KEY")
|
|
130
|
+
key_file = env.get("PIERRE_PRIVATE_KEY_FILE")
|
|
131
|
+
if token is not None:
|
|
132
|
+
return _Credential("token", token)
|
|
133
|
+
if private_key is not None:
|
|
134
|
+
return _Credential("private_key", Signer(lambda: private_key))
|
|
135
|
+
if key_file is not None:
|
|
136
|
+
return _Credential("private_key", Signer(lambda: _read_key_file(key_file)))
|
|
137
|
+
raise PierreError(
|
|
138
|
+
"Give `private_key`, `token`, or `get_token`, or set PIERRE_PRIVATE_KEY, "
|
|
139
|
+
"PIERRE_PRIVATE_KEY_FILE, or PIERRE_TOKEN."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass(frozen=True)
|
|
144
|
+
class _Given:
|
|
145
|
+
"""The keyword arguments of a constructor or of `with_options`. `None` is not given."""
|
|
146
|
+
|
|
147
|
+
org: Optional[str] = None
|
|
148
|
+
private_key: Optional[str] = None
|
|
149
|
+
token: Optional[str] = None
|
|
150
|
+
get_token: Optional[GetToken] = None
|
|
151
|
+
base_url: Optional[str] = None
|
|
152
|
+
git_host: Optional[str] = None
|
|
153
|
+
timeout: Optional[float] = None
|
|
154
|
+
max_retries: Optional[int] = None
|
|
155
|
+
default_headers: Optional[Mapping[str, str]] = None
|
|
156
|
+
http_client: Optional[object] = None
|
|
157
|
+
ref_policies: Optional[Sequence[RefPolicy]] = None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _from_environment(given: _Given) -> _Settings:
|
|
161
|
+
env = _environment()
|
|
162
|
+
return _Settings(
|
|
163
|
+
org=given.org if given.org is not None else env.get("PIERRE_ORG"),
|
|
164
|
+
base_url=given.base_url if given.base_url is not None else env.get("PIERRE_API_BASE_URL"),
|
|
165
|
+
git_host=given.git_host if given.git_host is not None else env.get("PIERRE_GIT_HOST"),
|
|
166
|
+
timeout=check_timeout(given.timeout if given.timeout is not None else _DEFAULT_TIMEOUT),
|
|
167
|
+
max_retries=check_max_retries(
|
|
168
|
+
given.max_retries if given.max_retries is not None else _DEFAULT_MAX_RETRIES
|
|
169
|
+
),
|
|
170
|
+
default_headers=dict(given.default_headers or {}),
|
|
171
|
+
http_client=given.http_client,
|
|
172
|
+
rules=check_ref_policies(given.ref_policies, "ref_policies"),
|
|
173
|
+
# A credential in the options replaces every credential in the environment.
|
|
174
|
+
credential=_credential_of(given.private_key, given.token, given.get_token)
|
|
175
|
+
or _environment_credential(env),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _with_changes(parent: _Settings, changes: _Given) -> _Settings:
|
|
180
|
+
rules = check_ref_policies(changes.ref_policies, "ref_policies")
|
|
181
|
+
return _Settings(
|
|
182
|
+
org=changes.org if changes.org is not None else parent.org,
|
|
183
|
+
base_url=changes.base_url if changes.base_url is not None else parent.base_url,
|
|
184
|
+
git_host=changes.git_host if changes.git_host is not None else parent.git_host,
|
|
185
|
+
timeout=check_timeout(changes.timeout if changes.timeout is not None else parent.timeout),
|
|
186
|
+
max_retries=check_max_retries(
|
|
187
|
+
changes.max_retries if changes.max_retries is not None else parent.max_retries
|
|
188
|
+
),
|
|
189
|
+
default_headers=dict(changes.default_headers)
|
|
190
|
+
if changes.default_headers is not None
|
|
191
|
+
else parent.default_headers,
|
|
192
|
+
http_client=changes.http_client if changes.http_client is not None else parent.http_client,
|
|
193
|
+
rules=rules if rules is not None else parent.rules,
|
|
194
|
+
credential=_credential_of(changes.private_key, changes.token, changes.get_token)
|
|
195
|
+
or parent.credential,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _check_base_url(base_url: str) -> str:
|
|
200
|
+
"""Checks that the base URL is an absolute `https` or `http` URL."""
|
|
201
|
+
parts = urlsplit(base_url) if isinstance(base_url, str) else None
|
|
202
|
+
if parts is None or parts.scheme not in ("https", "http") or parts.netloc == "":
|
|
203
|
+
raise PierreError(
|
|
204
|
+
"`base_url` must be an absolute https URL, for example https://api.acme.code.storage."
|
|
205
|
+
)
|
|
206
|
+
return base_url
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@dataclass(frozen=True)
|
|
210
|
+
class _Resolved:
|
|
211
|
+
"""The settings with the defaults, which a client keeps."""
|
|
212
|
+
|
|
213
|
+
settings: _Settings
|
|
214
|
+
options: ClientOptions
|
|
215
|
+
credentials: Credentials
|
|
216
|
+
config: HttpConfig
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _resolve(settings: _Settings) -> _Resolved:
|
|
220
|
+
credential = settings.credential
|
|
221
|
+
org = settings.org
|
|
222
|
+
if org is None and credential.kind == "token":
|
|
223
|
+
org = token_issuer(str(credential.value))
|
|
224
|
+
# The SDK does not verify a token, so its `iss` can name any host. A default host uses only a
|
|
225
|
+
# host label, and so stays under `code.storage`.
|
|
226
|
+
label = org if org is not None and _HOST_LABEL.fullmatch(org) else None
|
|
227
|
+
if settings.base_url is None and label is None:
|
|
228
|
+
raise PierreError(
|
|
229
|
+
"Set `org` or PIERRE_ORG, or give `base_url`."
|
|
230
|
+
if org is None
|
|
231
|
+
else "The organization name is not a host label, so give `base_url`."
|
|
232
|
+
)
|
|
233
|
+
rules = settings.rules
|
|
234
|
+
# The SDK cannot change the claims of a fixed token, so the limit would not apply.
|
|
235
|
+
if credential.kind == "token" and rules:
|
|
236
|
+
raise PierreError(
|
|
237
|
+
"A client with a fixed `token` cannot have `ref_policies`. "
|
|
238
|
+
"Give `private_key` or `get_token`."
|
|
239
|
+
)
|
|
240
|
+
base_url = _check_base_url(
|
|
241
|
+
f"https://api.{label}.code.storage" if settings.base_url is None else settings.base_url
|
|
242
|
+
)
|
|
243
|
+
git_host = (
|
|
244
|
+
(None if label is None else f"{label}.code.storage")
|
|
245
|
+
if settings.git_host is None
|
|
246
|
+
else settings.git_host
|
|
247
|
+
)
|
|
248
|
+
if credential.kind == "private_key":
|
|
249
|
+
# The gateway finds the public keys of the organization by the `iss` claim.
|
|
250
|
+
if org is None:
|
|
251
|
+
raise PierreError("A private key needs `org` or PIERRE_ORG.")
|
|
252
|
+
credentials = Credentials("private_key", org=org, signer=credential.value) # type: ignore[arg-type]
|
|
253
|
+
elif credential.kind == "token":
|
|
254
|
+
credentials = Credentials("token", token=str(credential.value))
|
|
255
|
+
else:
|
|
256
|
+
credentials = Credentials("get_token", get_token=credential.value) # type: ignore[arg-type]
|
|
257
|
+
return _Resolved(
|
|
258
|
+
# A copy keeps the organization of `options`, also when it came from the token.
|
|
259
|
+
settings=replace(settings, org=org),
|
|
260
|
+
options=ClientOptions(
|
|
261
|
+
org=org,
|
|
262
|
+
base_url=base_url,
|
|
263
|
+
git_host=git_host,
|
|
264
|
+
timeout=settings.timeout,
|
|
265
|
+
max_retries=settings.max_retries,
|
|
266
|
+
ref_policies=None if rules is None else tuple(policies_of(rules)),
|
|
267
|
+
),
|
|
268
|
+
credentials=credentials,
|
|
269
|
+
config=HttpConfig(
|
|
270
|
+
base_url=base_url,
|
|
271
|
+
credentials=credentials,
|
|
272
|
+
client=f"pierre-sdk-python/{__version__}",
|
|
273
|
+
default_headers=settings.default_headers,
|
|
274
|
+
timeout=settings.timeout,
|
|
275
|
+
max_retries=settings.max_retries,
|
|
276
|
+
rules=rules,
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
Client = TypeVar("Client", bound="_ClientCore")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class _ClientCore:
|
|
285
|
+
"""The settings of a client. Every top-level name other than `options`, `with_options`, and
|
|
286
|
+
`with_raw_response` is free for an API namespace."""
|
|
287
|
+
|
|
288
|
+
_resolved: _Resolved
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def options(self) -> ClientOptions:
|
|
292
|
+
"""The settings of the client. It holds no credential."""
|
|
293
|
+
return self._resolved.options
|
|
294
|
+
|
|
295
|
+
def _copy(self: Client, changes: _Given) -> Client:
|
|
296
|
+
"""A copy of the client with changed options. It does not read the environment again."""
|
|
297
|
+
copy = type(self).__new__(type(self))
|
|
298
|
+
copy._setup(_with_changes(self._resolved.settings, changes))
|
|
299
|
+
return copy
|
|
300
|
+
|
|
301
|
+
def _setup(self, settings: _Settings) -> None:
|
|
302
|
+
raise NotImplementedError
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class Pierre(_ClientCore, PierreNamespaces):
|
|
306
|
+
"""The Pierre client for blocking code. `Pierre()` reads `PIERRE_ORG` and
|
|
307
|
+
`PIERRE_PRIVATE_KEY`, or the other variables in the docs. Each namespace path is a catalog ID
|
|
308
|
+
in snake_case, for example `pierre.repos.branches.create()`.
|
|
309
|
+
|
|
310
|
+
Docs: the MCP server at https://code.storage/docs/mcp, or the Markdown pages from
|
|
311
|
+
https://code.storage/docs/llms.txt.
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
def __init__(
|
|
315
|
+
self,
|
|
316
|
+
*,
|
|
317
|
+
org: Optional[str] = None,
|
|
318
|
+
private_key: Optional[str] = None,
|
|
319
|
+
token: Optional[str] = None,
|
|
320
|
+
get_token: Optional[Callable[[TokenRequest], str]] = None,
|
|
321
|
+
base_url: Optional[str] = None,
|
|
322
|
+
git_host: Optional[str] = None,
|
|
323
|
+
timeout: Optional[float] = None,
|
|
324
|
+
max_retries: Optional[int] = None,
|
|
325
|
+
default_headers: Optional[Mapping[str, str]] = None,
|
|
326
|
+
http_client: Optional[httpx.Client] = None,
|
|
327
|
+
ref_policies: Optional[Sequence[RefPolicy]] = None,
|
|
328
|
+
) -> None:
|
|
329
|
+
"""Makes a client. An option that is `None` takes its value from the environment, or its
|
|
330
|
+
default.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
org: The organization name. Default: `PIERRE_ORG`, or the `iss` claim of `token`.
|
|
334
|
+
private_key: A PKCS #8 PEM private key. Default: `PIERRE_PRIVATE_KEY`, or the file at
|
|
335
|
+
`PIERRE_PRIVATE_KEY_FILE`.
|
|
336
|
+
token: A fixed token. Default: `PIERRE_TOKEN`.
|
|
337
|
+
get_token: A function that returns a token for each request, for a client with no
|
|
338
|
+
private key. It receives a `TokenRequest`.
|
|
339
|
+
base_url: The API base URL. Default: `PIERRE_API_BASE_URL`, or
|
|
340
|
+
`https://api.{org}.code.storage`.
|
|
341
|
+
git_host: The Git host. Default: `PIERRE_GIT_HOST`, or `{org}.code.storage`.
|
|
342
|
+
timeout: The timeout of each attempt, in seconds. Default: 60.
|
|
343
|
+
max_retries: The number of retries of a transient failure. Default: 2.
|
|
344
|
+
default_headers: Headers for every request.
|
|
345
|
+
http_client: The `httpx.Client` that sends each request.
|
|
346
|
+
ref_policies: The `refs` claim of each request token, which limits the refs that a
|
|
347
|
+
write can change. A request option replaces it. A client with a fixed `token`
|
|
348
|
+
cannot have it.
|
|
349
|
+
"""
|
|
350
|
+
self._setup(
|
|
351
|
+
_from_environment(
|
|
352
|
+
_Given(
|
|
353
|
+
org=org,
|
|
354
|
+
private_key=private_key,
|
|
355
|
+
token=token,
|
|
356
|
+
get_token=get_token,
|
|
357
|
+
base_url=base_url,
|
|
358
|
+
git_host=git_host,
|
|
359
|
+
timeout=timeout,
|
|
360
|
+
max_retries=max_retries,
|
|
361
|
+
default_headers=default_headers,
|
|
362
|
+
http_client=http_client,
|
|
363
|
+
ref_policies=ref_policies,
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
def _setup(self, settings: _Settings) -> None:
|
|
369
|
+
# The httpx client that this client made, which `__exit__` closes. A copy shares it, and
|
|
370
|
+
# does not close it.
|
|
371
|
+
self._made_client: Optional[httpx.Client] = None
|
|
372
|
+
if settings.http_client is None:
|
|
373
|
+
self._made_client = httpx.Client()
|
|
374
|
+
settings = replace(settings, http_client=self._made_client)
|
|
375
|
+
elif not isinstance(settings.http_client, httpx.Client):
|
|
376
|
+
raise PierreError("`http_client` of `Pierre` must be an `httpx.Client`.")
|
|
377
|
+
self._resolved = _resolve(settings)
|
|
378
|
+
self._runtime = SyncRuntime(self._resolved.config, settings.http_client) # type: ignore[arg-type]
|
|
379
|
+
self._attach_namespaces(self._runtime)
|
|
380
|
+
|
|
381
|
+
def __enter__(self) -> Self:
|
|
382
|
+
return self
|
|
383
|
+
|
|
384
|
+
def __exit__(
|
|
385
|
+
self,
|
|
386
|
+
exc_type: Optional[Type[BaseException]],
|
|
387
|
+
exc: Optional[BaseException],
|
|
388
|
+
traceback: Optional[TracebackType],
|
|
389
|
+
) -> None:
|
|
390
|
+
"""Closes the httpx client that `Pierre()` made. An `http_client` of the caller stays
|
|
391
|
+
open."""
|
|
392
|
+
if self._made_client is not None:
|
|
393
|
+
self._made_client.close()
|
|
394
|
+
|
|
395
|
+
def with_options(
|
|
396
|
+
self,
|
|
397
|
+
*,
|
|
398
|
+
org: Optional[str] = None,
|
|
399
|
+
private_key: Optional[str] = None,
|
|
400
|
+
token: Optional[str] = None,
|
|
401
|
+
get_token: Optional[Callable[[TokenRequest], str]] = None,
|
|
402
|
+
base_url: Optional[str] = None,
|
|
403
|
+
git_host: Optional[str] = None,
|
|
404
|
+
timeout: Optional[float] = None,
|
|
405
|
+
max_retries: Optional[int] = None,
|
|
406
|
+
default_headers: Optional[Mapping[str, str]] = None,
|
|
407
|
+
http_client: Optional[httpx.Client] = None,
|
|
408
|
+
ref_policies: Optional[Sequence[RefPolicy]] = None,
|
|
409
|
+
) -> "Pierre":
|
|
410
|
+
"""Returns a copy of the client with changed options. The copy keeps every option that the
|
|
411
|
+
call does not set, including `org`, and does not read the environment again. It keeps the
|
|
412
|
+
credential, unless the call gives one. When `org` changes, the default hosts follow it."""
|
|
413
|
+
return self._copy(
|
|
414
|
+
_Given(
|
|
415
|
+
org=org,
|
|
416
|
+
private_key=private_key,
|
|
417
|
+
token=token,
|
|
418
|
+
get_token=get_token,
|
|
419
|
+
base_url=base_url,
|
|
420
|
+
git_host=git_host,
|
|
421
|
+
timeout=timeout,
|
|
422
|
+
max_retries=max_retries,
|
|
423
|
+
default_headers=default_headers,
|
|
424
|
+
http_client=http_client,
|
|
425
|
+
ref_policies=ref_policies,
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
@property
|
|
430
|
+
def with_raw_response(self) -> PierreWithRawResponse:
|
|
431
|
+
"""The namespaces of the client, whose methods return a `RawResponse`: the result, the
|
|
432
|
+
`httpx.Response`, the declared response headers, and the request ID."""
|
|
433
|
+
return PierreWithRawResponse(self)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class AsyncPierre(_ClientCore, AsyncPierreNamespaces):
|
|
437
|
+
"""The Pierre client for `asyncio` code. It has the namespaces and the options of `Pierre`,
|
|
438
|
+
and each method is async.
|
|
439
|
+
|
|
440
|
+
Docs: the MCP server at https://code.storage/docs/mcp, or the Markdown pages from
|
|
441
|
+
https://code.storage/docs/llms.txt.
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
def __init__(
|
|
445
|
+
self,
|
|
446
|
+
*,
|
|
447
|
+
org: Optional[str] = None,
|
|
448
|
+
private_key: Optional[str] = None,
|
|
449
|
+
token: Optional[str] = None,
|
|
450
|
+
get_token: Optional[GetToken] = None,
|
|
451
|
+
base_url: Optional[str] = None,
|
|
452
|
+
git_host: Optional[str] = None,
|
|
453
|
+
timeout: Optional[float] = None,
|
|
454
|
+
max_retries: Optional[int] = None,
|
|
455
|
+
default_headers: Optional[Mapping[str, str]] = None,
|
|
456
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
457
|
+
ref_policies: Optional[Sequence[RefPolicy]] = None,
|
|
458
|
+
) -> None:
|
|
459
|
+
"""Makes a client. The arguments are the arguments of `Pierre()`, except two:
|
|
460
|
+
`get_token` can be async, and `http_client` is an `httpx.AsyncClient`."""
|
|
461
|
+
self._setup(
|
|
462
|
+
_from_environment(
|
|
463
|
+
_Given(
|
|
464
|
+
org=org,
|
|
465
|
+
private_key=private_key,
|
|
466
|
+
token=token,
|
|
467
|
+
get_token=get_token,
|
|
468
|
+
base_url=base_url,
|
|
469
|
+
git_host=git_host,
|
|
470
|
+
timeout=timeout,
|
|
471
|
+
max_retries=max_retries,
|
|
472
|
+
default_headers=default_headers,
|
|
473
|
+
http_client=http_client,
|
|
474
|
+
ref_policies=ref_policies,
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
def _setup(self, settings: _Settings) -> None:
|
|
480
|
+
# The httpx client that this client made, which `__aexit__` closes. A copy shares it, and
|
|
481
|
+
# does not close it.
|
|
482
|
+
self._made_client: Optional[httpx.AsyncClient] = None
|
|
483
|
+
if settings.http_client is None:
|
|
484
|
+
self._made_client = httpx.AsyncClient()
|
|
485
|
+
settings = replace(settings, http_client=self._made_client)
|
|
486
|
+
elif not isinstance(settings.http_client, httpx.AsyncClient):
|
|
487
|
+
raise PierreError("`http_client` of `AsyncPierre` must be an `httpx.AsyncClient`.")
|
|
488
|
+
self._resolved = _resolve(settings)
|
|
489
|
+
self._runtime = AsyncRuntime(self._resolved.config, settings.http_client) # type: ignore[arg-type]
|
|
490
|
+
self._attach_namespaces(self._runtime)
|
|
491
|
+
|
|
492
|
+
async def __aenter__(self) -> Self:
|
|
493
|
+
return self
|
|
494
|
+
|
|
495
|
+
async def __aexit__(
|
|
496
|
+
self,
|
|
497
|
+
exc_type: Optional[Type[BaseException]],
|
|
498
|
+
exc: Optional[BaseException],
|
|
499
|
+
traceback: Optional[TracebackType],
|
|
500
|
+
) -> None:
|
|
501
|
+
"""Closes the httpx client that `AsyncPierre()` made. An `http_client` of the caller
|
|
502
|
+
stays open."""
|
|
503
|
+
if self._made_client is not None:
|
|
504
|
+
await self._made_client.aclose()
|
|
505
|
+
|
|
506
|
+
def with_options(
|
|
507
|
+
self,
|
|
508
|
+
*,
|
|
509
|
+
org: Optional[str] = None,
|
|
510
|
+
private_key: Optional[str] = None,
|
|
511
|
+
token: Optional[str] = None,
|
|
512
|
+
get_token: Optional[GetToken] = None,
|
|
513
|
+
base_url: Optional[str] = None,
|
|
514
|
+
git_host: Optional[str] = None,
|
|
515
|
+
timeout: Optional[float] = None,
|
|
516
|
+
max_retries: Optional[int] = None,
|
|
517
|
+
default_headers: Optional[Mapping[str, str]] = None,
|
|
518
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
519
|
+
ref_policies: Optional[Sequence[RefPolicy]] = None,
|
|
520
|
+
) -> "AsyncPierre":
|
|
521
|
+
"""Returns a copy of the client with changed options, as `Pierre.with_options` does."""
|
|
522
|
+
return self._copy(
|
|
523
|
+
_Given(
|
|
524
|
+
org=org,
|
|
525
|
+
private_key=private_key,
|
|
526
|
+
token=token,
|
|
527
|
+
get_token=get_token,
|
|
528
|
+
base_url=base_url,
|
|
529
|
+
git_host=git_host,
|
|
530
|
+
timeout=timeout,
|
|
531
|
+
max_retries=max_retries,
|
|
532
|
+
default_headers=default_headers,
|
|
533
|
+
http_client=http_client,
|
|
534
|
+
ref_policies=ref_policies,
|
|
535
|
+
)
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
@property
|
|
539
|
+
def with_raw_response(self) -> AsyncPierreWithRawResponse:
|
|
540
|
+
"""The namespaces of the client, whose methods return a `RawResponse`."""
|
|
541
|
+
return AsyncPierreWithRawResponse(self)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
AnyPierre = Union[Pierre, AsyncPierre]
|
pierre/_compat.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Typing names that `typing` has only in newer Python versions."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
if sys.version_info >= (3, 11):
|
|
6
|
+
from typing import NotRequired as NotRequired
|
|
7
|
+
from typing import ParamSpec as ParamSpec
|
|
8
|
+
from typing import Self as Self
|
|
9
|
+
from typing import TypedDict as TypedDict
|
|
10
|
+
else:
|
|
11
|
+
from typing_extensions import NotRequired as NotRequired
|
|
12
|
+
from typing_extensions import ParamSpec as ParamSpec
|
|
13
|
+
from typing_extensions import Self as Self
|
|
14
|
+
from typing_extensions import TypedDict as TypedDict
|