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 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