cliV1 0.0.7__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.
Files changed (92) hide show
  1. cliv1-0.0.7.dist-info/METADATA +212 -0
  2. cliv1-0.0.7.dist-info/RECORD +92 -0
  3. cliv1-0.0.7.dist-info/WHEEL +4 -0
  4. cliv1-0.0.7.dist-info/licenses/LICENSE +21 -0
  5. swagger_petstore_open_api_3_1_0/__init__.py +15 -0
  6. swagger_petstore_open_api_3_1_0/apis/__init__.py +5 -0
  7. swagger_petstore_open_api_3_1_0/apis/pet_api.py +922 -0
  8. swagger_petstore_open_api_3_1_0/apis/store.py +432 -0
  9. swagger_petstore_open_api_3_1_0/apis/user_api.py +829 -0
  10. swagger_petstore_open_api_3_1_0/async_client.py +87 -0
  11. swagger_petstore_open_api_3_1_0/auth.py +17 -0
  12. swagger_petstore_open_api_3_1_0/base_client.py +27 -0
  13. swagger_petstore_open_api_3_1_0/client.py +85 -0
  14. swagger_petstore_open_api_3_1_0/core/__init__.py +395 -0
  15. swagger_petstore_open_api_3_1_0/core/_internal/__init__.py +9 -0
  16. swagger_petstore_open_api_3_1_0/core/_internal/content_disposition.py +154 -0
  17. swagger_petstore_open_api_3_1_0/core/_internal/flattening.py +104 -0
  18. swagger_petstore_open_api_3_1_0/core/_internal/headers.py +243 -0
  19. swagger_petstore_open_api_3_1_0/core/_internal/redaction.py +208 -0
  20. swagger_petstore_open_api_3_1_0/core/_internal/sse_parser.py +124 -0
  21. swagger_petstore_open_api_3_1_0/core/_internal/subscripts.py +50 -0
  22. swagger_petstore_open_api_3_1_0/core/_internal/urls.py +172 -0
  23. swagger_petstore_open_api_3_1_0/core/_internal/wire.py +201 -0
  24. swagger_petstore_open_api_3_1_0/core/adapters.py +177 -0
  25. swagger_petstore_open_api_3_1_0/core/auth/__init__.py +70 -0
  26. swagger_petstore_open_api_3_1_0/core/auth/composition.py +175 -0
  27. swagger_petstore_open_api_3_1_0/core/auth/models.py +216 -0
  28. swagger_petstore_open_api_3_1_0/core/auth/protocols.py +103 -0
  29. swagger_petstore_open_api_3_1_0/core/auth/schemes/__init__.py +94 -0
  30. swagger_petstore_open_api_3_1_0/core/auth/schemes/api_key_auth.py +53 -0
  31. swagger_petstore_open_api_3_1_0/core/auth/schemes/basic_auth.py +73 -0
  32. swagger_petstore_open_api_3_1_0/core/auth/schemes/bearer_auth.py +21 -0
  33. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_authorization_code.py +395 -0
  34. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_client_credentials.py +130 -0
  35. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_password.py +145 -0
  36. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_schemes.py +300 -0
  37. swagger_petstore_open_api_3_1_0/core/base_raw_response.py +49 -0
  38. swagger_petstore_open_api_3_1_0/core/bodies.py +590 -0
  39. swagger_petstore_open_api_3_1_0/core/converters/__init__.py +68 -0
  40. swagger_petstore_open_api_3_1_0/core/converters/date_time.py +237 -0
  41. swagger_petstore_open_api_3_1_0/core/converters/encoded_bytes.py +273 -0
  42. swagger_petstore_open_api_3_1_0/core/converters/open_enum.py +35 -0
  43. swagger_petstore_open_api_3_1_0/core/decoding.py +498 -0
  44. swagger_petstore_open_api_3_1_0/core/discriminators.py +129 -0
  45. swagger_petstore_open_api_3_1_0/core/event_streams.py +225 -0
  46. swagger_petstore_open_api_3_1_0/core/exceptions.py +78 -0
  47. swagger_petstore_open_api_3_1_0/core/file_responses.py +337 -0
  48. swagger_petstore_open_api_3_1_0/core/files.py +174 -0
  49. swagger_petstore_open_api_3_1_0/core/httpx2_transport.py +748 -0
  50. swagger_petstore_open_api_3_1_0/core/logging.py +472 -0
  51. swagger_petstore_open_api_3_1_0/core/models.py +143 -0
  52. swagger_petstore_open_api_3_1_0/core/optionality.py +144 -0
  53. swagger_petstore_open_api_3_1_0/core/pagination.py +452 -0
  54. swagger_petstore_open_api_3_1_0/core/paginator.py +55 -0
  55. swagger_petstore_open_api_3_1_0/core/params.py +157 -0
  56. swagger_petstore_open_api_3_1_0/core/raw_client.py +480 -0
  57. swagger_petstore_open_api_3_1_0/core/request_options.py +172 -0
  58. swagger_petstore_open_api_3_1_0/core/results.py +178 -0
  59. swagger_petstore_open_api_3_1_0/core/retries.py +237 -0
  60. swagger_petstore_open_api_3_1_0/core/runtime_env.py +43 -0
  61. swagger_petstore_open_api_3_1_0/core/servers.py +33 -0
  62. swagger_petstore_open_api_3_1_0/core/transport.py +218 -0
  63. swagger_petstore_open_api_3_1_0/errors/__init__.py +48 -0
  64. swagger_petstore_open_api_3_1_0/errors/add_pet_error.py +21 -0
  65. swagger_petstore_open_api_3_1_0/errors/delete_order_error.py +21 -0
  66. swagger_petstore_open_api_3_1_0/errors/delete_pet_error.py +21 -0
  67. swagger_petstore_open_api_3_1_0/errors/delete_user_error.py +21 -0
  68. swagger_petstore_open_api_3_1_0/errors/find_pets_by_status_error.py +21 -0
  69. swagger_petstore_open_api_3_1_0/errors/find_pets_by_tags_error.py +21 -0
  70. swagger_petstore_open_api_3_1_0/errors/get_order_by_id_error.py +21 -0
  71. swagger_petstore_open_api_3_1_0/errors/get_pet_by_id_error.py +21 -0
  72. swagger_petstore_open_api_3_1_0/errors/get_user_by_name_error.py +21 -0
  73. swagger_petstore_open_api_3_1_0/errors/login_user_error.py +21 -0
  74. swagger_petstore_open_api_3_1_0/errors/place_order_error.py +21 -0
  75. swagger_petstore_open_api_3_1_0/errors/update_pet_error.py +21 -0
  76. swagger_petstore_open_api_3_1_0/errors/update_pet_with_form_error.py +21 -0
  77. swagger_petstore_open_api_3_1_0/errors/update_user_error.py +21 -0
  78. swagger_petstore_open_api_3_1_0/errors/upload_file_error.py +21 -0
  79. swagger_petstore_open_api_3_1_0/models/__init__.py +23 -0
  80. swagger_petstore_open_api_3_1_0/models/api_response.py +18 -0
  81. swagger_petstore_open_api_3_1_0/models/category.py +15 -0
  82. swagger_petstore_open_api_3_1_0/models/enums/__init__.py +4 -0
  83. swagger_petstore_open_api_3_1_0/models/enums/order_status.py +17 -0
  84. swagger_petstore_open_api_3_1_0/models/enums/pet_status.py +17 -0
  85. swagger_petstore_open_api_3_1_0/models/order.py +27 -0
  86. swagger_petstore_open_api_3_1_0/models/pet.py +28 -0
  87. swagger_petstore_open_api_3_1_0/models/tag_model.py +15 -0
  88. swagger_petstore_open_api_3_1_0/models/user.py +29 -0
  89. swagger_petstore_open_api_3_1_0/py.typed +0 -0
  90. swagger_petstore_open_api_3_1_0/server/__init__.py +21 -0
  91. swagger_petstore_open_api_3_1_0/server/server.py +17 -0
  92. swagger_petstore_open_api_3_1_0/server/server_config.py +52 -0
@@ -0,0 +1,144 @@
1
+ """The unset sentinel and the states it enables: omitted vs ``null`` vs a value.
2
+
3
+ :data:`UNSET` marks a field the caller never touched and :func:`strip_unset` is where "omitted" is
4
+ recovered at dump time, by dropping any key whose live value is still the sentinel. Two annotations
5
+ admit it, one per spelling a spec can declare: :data:`Optional` for a property that may be omitted,
6
+ :data:`OptionalNullable` for one that may be omitted *or* explicitly ``null``. See
7
+ ``docs/designs/optional-nullable-fields.md``.
8
+
9
+ :data:`Optional` is deliberately **not** ``typing.Optional``. Here it means ``T | UnsetType`` --
10
+ present or absent, with no ``None`` arm -- where ``typing.Optional[T]`` means ``T | None``. They
11
+ differ by exactly the arm that matters: a property the spec does not declare nullable must not be
12
+ able to reach the wire as ``null``, so passing ``None`` to one of these fields is a type error
13
+ rather than a value that serializes. Nothing in a generated SDK imports ``typing.Optional`` -- unions
14
+ are spelled with PEP 604 ``|`` -- so the two never appear in the same module.
15
+
16
+ Both are plain generic aliases, not ``TypeAliasType``. pydantic substitutes a ``TypeAliasType``'s
17
+ parameter itself and folds ``Any | T`` into ``Any`` while doing so, which would take the sentinel arm
18
+ out of ``Optional[Any]`` -- the spelling of a property with no ``type``. ``typing`` substitutes a
19
+ plain alias and keeps the arm; mypy reads the two forms alike. See ADR-0071."""
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import Any, ClassVar, Final, Literal, TypeAlias, TypeVar, cast, final
24
+
25
+ from pydantic import BaseModel, GetCoreSchemaHandler
26
+ from pydantic_core import PydanticSerializationUnexpectedValue, core_schema
27
+
28
+
29
+ @final
30
+ class UnsetType:
31
+ """Marks a field the caller never touched, distinct from one set to ``None``.
32
+
33
+ Only ever seen as a field's default -- never constructed or passed by a caller. Dumps
34
+ as plain ``None`` through pydantic's normal fast path (see :func:`strip_unset` for
35
+ where the tri-state is actually recovered), and dumps nothing else: a value its union's
36
+ real arm failed to serialize is refused here, so it raises instead of going out as ``null``.
37
+ See ``docs/designs/optional-nullable-fields.md``.
38
+
39
+ A singleton, and identity-stable through every channel that could duplicate it: ``UnsetType()``
40
+ returns the existing instance, ``copy`` and ``deepcopy`` return it unchanged, and unpickling
41
+ reduces back through the constructor. Without that, ``model_copy(deep=True)`` would deep-copy
42
+ the sentinel into a *second* object, and ``field is UNSET`` -- the natural way to test for
43
+ it -- would start returning ``False`` on the copy. :func:`strip_unset` uses ``isinstance`` and
44
+ so is immune either way, but callers should not have to know that. ``final`` is the same
45
+ guarantee stated statically: a subclass would inherit ``_instance`` already set, so its
46
+ constructor would hand back a ``UnsetType`` rather than an instance of itself."""
47
+
48
+ _instance: ClassVar[UnsetType | None] = None
49
+
50
+ def __new__(cls) -> UnsetType:
51
+ if cls._instance is None:
52
+ cls._instance = super().__new__(cls)
53
+ return cls._instance
54
+
55
+ def __bool__(self) -> Literal[False]:
56
+ return False
57
+
58
+ def __repr__(self) -> str:
59
+ return "UNSET"
60
+
61
+ def __copy__(self) -> UnsetType:
62
+ return self
63
+
64
+ def __deepcopy__(self, memo: dict[int, Any]) -> UnsetType:
65
+ return self
66
+
67
+ def __reduce__(self) -> tuple[type[UnsetType], tuple[()]]:
68
+ # Protocols >= 2 reduce via ``cls.__new__(cls)`` and reach the guard above by luck of
69
+ # protocol; 0 and 1 reconstruct through ``copyreg._reconstructor``, which calls
70
+ # ``object.__new__`` and bypasses ``__new__`` entirely -- measured, ``restored is UNSET``
71
+ # was ``False`` at both. Reducing to a plain call makes every protocol uniform.
72
+ return (UnsetType, ())
73
+
74
+ @classmethod
75
+ def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
76
+ return core_schema.is_instance_schema(
77
+ cls, serialization=core_schema.plain_serializer_function_ser_schema(cls._dump)
78
+ )
79
+
80
+ @staticmethod
81
+ def _dump(value: object) -> None:
82
+ # pydantic offers a union's value to each arm until one serializes it. Answering None for
83
+ # anything made this arm the catch-all: a list its own arm failed on went out as null.
84
+ # PydanticSerializationUnexpectedValue is pydantic's "not this arm" signal.
85
+ if isinstance(value, UnsetType):
86
+ return None
87
+ raise PydanticSerializationUnexpectedValue(f"Expected UNSET, got {type(value).__name__}")
88
+
89
+
90
+ UNSET: Final = UnsetType()
91
+
92
+ T = TypeVar("T")
93
+ Optional: TypeAlias = T | UnsetType
94
+ OptionalNullable: TypeAlias = T | None | UnsetType
95
+
96
+ DumpedT = TypeVar("DumpedT")
97
+
98
+
99
+ def strip_unset(value: object, dumped: DumpedT) -> DumpedT:
100
+ """Recursively drop any dumped key whose live field value is :class:`UnsetType`.
101
+
102
+ Walks ``value`` (the validated model, list or dict, pre-dump) alongside ``dumped`` (its
103
+ ``mode="json"`` dump) so a field that dumped as ``None`` only because it defaulted to
104
+ ``UNSET`` is omitted, while one explicitly set to ``None`` stays ``null``.
105
+
106
+ The mapping branch pairs the two by *key* rather than by position, because ``mode="json"``
107
+ renders a non-``str`` key as text (``{1: "x"}`` dumps as ``{"1": "x"}``): an entry the dump
108
+ renamed keeps its dumped form instead of misaligning the walk. ``list`` and ``dict`` are the
109
+ whole container set on purpose -- an array and a map are what a spec can express, so they are
110
+ the only shapes an emitted declared type nests, and they are the same two ``adapters.py``
111
+ descends. A ``tuple`` or ``set`` passes through like any other value.
112
+
113
+ Shape-preserving: a dict in, a dict out; a list in, a list out. The three ``cast`` calls
114
+ are that invariant, which mypy cannot verify through the rebuild -- they keep the
115
+ imprecision here rather than returning ``Any`` to every caller.
116
+
117
+ Args:
118
+ value: The validated model, list or dict, before dumping -- the side that still
119
+ holds the sentinel.
120
+ dumped: That same value's ``mode="json"`` dump, walked alongside it.
121
+
122
+ Returns:
123
+ ``dumped`` with every never-touched key removed, in the shape it arrived in."""
124
+ if isinstance(value, BaseModel):
125
+ result = dict(cast(Any, dumped))
126
+ for name, field in type(value).model_fields.items():
127
+ # ``dumped`` may be keyed by wire alias (the default) or by Python name
128
+ # (``to_dict(by_alias=False)``); prefer whichever spelling is actually there.
129
+ key = field.alias if field.alias is not None and field.alias in result else name
130
+ if key not in result:
131
+ continue
132
+ attr = getattr(value, name)
133
+ if isinstance(attr, UnsetType):
134
+ del result[key]
135
+ else:
136
+ result[key] = strip_unset(attr, result[key])
137
+ return cast(DumpedT, result)
138
+ if isinstance(value, list) and isinstance(dumped, list):
139
+ stripped = [strip_unset(item, d) for item, d in zip(value, dumped, strict=True)]
140
+ return cast(DumpedT, stripped)
141
+ if isinstance(value, dict) and isinstance(dumped, dict):
142
+ stripped_values = {key: strip_unset(value[key], item) if key in value else item for key, item in dumped.items()}
143
+ return cast(DumpedT, stripped_values)
144
+ return dumped
@@ -0,0 +1,452 @@
1
+ """Pagination: the five paging states, their factories, and the sync and async loops that walk them.
2
+
3
+ A state holds the value that moves from page to page (an offset, a page number, a cursor, a key, or the next
4
+ page's URL and query) and the selectors the endpoint uses to read the next move off a response. After each
5
+ successful page the loop calls ``advance``, which returns the next state, or ``None`` to stop.
6
+
7
+ States are frozen. ``advance`` returns a copy made with ``dataclasses.replace`` that changes only the moving
8
+ value, so the selectors stay with the walk. The stop rules, and the order they are checked in, match the C#
9
+ SDK. A state never advances to the value it just sent, so a server that keeps returning the same page ends
10
+ the walk instead of looping forever. A selector over an ``OptionalNullable`` field may return ``UNSET``;
11
+ every stop rule treats that as no answer.
12
+
13
+ An endpoint never names a state class. It calls a factory with the page model as a subscript, such as
14
+ ``offset_state[Products](…)``. The subscript types the selectors, so a mistake inside one fails the build.
15
+ The factory returns the class itself, because Python 3.10 cannot construct a frozen, slotted dataclass
16
+ through a ``typing`` subscript. Calling a factory without its subscript fails the build.
17
+
18
+ ``paginate`` and ``async_paginate`` run the loop: fetch the page for the current state, yield the result,
19
+ stop after a ``Failure``, then ask ``advance`` for the next state. The result is yielded before it is
20
+ checked, so a ``Failure`` is the last page the caller sees rather than one that is dropped. Each page is a
21
+ full ``execute``, so auth, retries, the idempotency key and ``request_options`` apply to every page.
22
+ Building a paginator sends nothing; the first request goes out when iteration starts, which is why neither
23
+ function is a coroutine.
24
+
25
+ Each function has one ``@overload`` per state class. The overload keeps the state's own class inside the
26
+ endpoint's ``fetch_page`` lambda, so ``state.offset`` resolves, and ties the state's page type to the
27
+ decoder's, so a mismatch fails the build. The implementation takes the union of all five states and mypy
28
+ checks it against every overload. A new strategy needs a new state, factory, pair of overloads and
29
+ ``PagingState`` arm, and nothing else.
30
+
31
+ ``results.unwrap`` turns a paginator of results into a paginator of payloads; that is the parsed mode."""
32
+
33
+ from __future__ import annotations
34
+
35
+ from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Mapping, Sequence
36
+ from dataclasses import dataclass, replace
37
+ from typing import Any, Final, Generic, TypeAlias, TypeVar, overload
38
+ from urllib.parse import parse_qsl, urljoin, urlsplit
39
+
40
+ from typing_extensions import TypeForm
41
+
42
+ from ._internal.subscripts import SubscriptOnly
43
+ from ._internal.urls import build_url
44
+ from .optionality import OptionalNullable, UnsetType
45
+ from .paginator import AsyncPaginator, Paginator
46
+ from .params import Param, SerializationFormat, UrlTemplate, param
47
+ from .results import ApiResult, Success, unwrap
48
+
49
+ PayloadT = TypeVar("PayloadT")
50
+ E = TypeVar("E")
51
+ ItemT = TypeVar("ItemT")
52
+
53
+
54
+ @dataclass(frozen=True, kw_only=True, slots=True)
55
+ class OffsetState(Generic[PayloadT]):
56
+ """Moves by the number of items each page returned.
57
+
58
+ The next offset is the current offset (``None`` counts as ``0``) plus ``item_count``. The walk stops, in
59
+ this order: when the next offset reaches ``total_items``; when the page was empty; when ``has_more``
60
+ returns ``False``; or when there is no total and the page has fewer items than ``limit``. If ``has_more``
61
+ returns ``True``, the walk continues and the last rule is skipped."""
62
+
63
+ offset: int | None
64
+ limit: int | None
65
+ item_count: Callable[[PayloadT], int]
66
+ has_more: Callable[[PayloadT, Mapping[str, str]], OptionalNullable[bool]] | None = None
67
+ total_items: Callable[[PayloadT, Mapping[str, str]], OptionalNullable[int]] | None = None
68
+
69
+ def advance(self, *, payload: PayloadT, headers: Mapping[str, str]) -> OffsetState[PayloadT] | None:
70
+ """Return the next state, or ``None`` when a stop rule applies."""
71
+ data_count = self.item_count(payload)
72
+ current_offset = self.offset if self.offset is not None else 0
73
+ next_offset = current_offset + data_count
74
+ total_items = self.total_items(payload, headers) if self.total_items is not None else None
75
+ if isinstance(total_items, int) and next_offset >= total_items:
76
+ return None
77
+ if next_offset == current_offset:
78
+ return None
79
+ has_more = self.has_more(payload, headers) if self.has_more is not None else None
80
+ if isinstance(has_more, bool):
81
+ return replace(self, offset=next_offset) if has_more else None
82
+ if not isinstance(total_items, int) and self.limit is not None and data_count < self.limit:
83
+ return None
84
+ return replace(self, offset=next_offset)
85
+
86
+
87
+ @dataclass(frozen=True, kw_only=True, slots=True)
88
+ class PageState(Generic[PayloadT]):
89
+ """Moves by a 1-based page number.
90
+
91
+ The next page is the current page (``None`` counts as ``1``) plus one. The walk stops, in this order: when
92
+ the current page has reached ``total_pages``; when ``has_more`` returns ``False``; when the page was empty;
93
+ or when there is no total and the page has fewer items than ``limit``. If ``has_more`` returns ``True``,
94
+ the walk continues and the last two rules are skipped."""
95
+
96
+ page: int | None
97
+ limit: int | None
98
+ item_count: Callable[[PayloadT], int]
99
+ has_more: Callable[[PayloadT, Mapping[str, str]], OptionalNullable[bool]] | None = None
100
+ total_pages: Callable[[PayloadT, Mapping[str, str]], OptionalNullable[int]] | None = None
101
+
102
+ def advance(self, *, payload: PayloadT, headers: Mapping[str, str]) -> PageState[PayloadT] | None:
103
+ """Return the next state, or ``None`` when a stop rule applies."""
104
+ total_pages = self.total_pages(payload, headers) if self.total_pages is not None else None
105
+ current_page = self.page if self.page is not None else 1
106
+ if isinstance(total_pages, int) and current_page >= total_pages:
107
+ return None
108
+ has_more = self.has_more(payload, headers) if self.has_more is not None else None
109
+ if isinstance(has_more, bool):
110
+ return replace(self, page=current_page + 1) if has_more else None
111
+ count = self.item_count(payload)
112
+ if count == 0:
113
+ return None
114
+ if not isinstance(total_pages, int) and self.limit is not None and count < self.limit:
115
+ return None
116
+ return replace(self, page=current_page + 1)
117
+
118
+
119
+ @dataclass(frozen=True, kw_only=True, slots=True)
120
+ class CursorState(Generic[PayloadT]):
121
+ """Moves by the cursor each page returns.
122
+
123
+ The walk stops when the next cursor is missing, empty or ``UNSET``, when it equals the cursor just sent,
124
+ or when ``has_more`` returns ``False``. There is no ``count < limit`` rule on purpose: a short page that
125
+ still has a cursor does not mean the collection has ended."""
126
+
127
+ cursor: str | None
128
+ next_cursor: Callable[[PayloadT, Mapping[str, str]], OptionalNullable[str]]
129
+ has_more: Callable[[PayloadT, Mapping[str, str]], OptionalNullable[bool]] | None = None
130
+
131
+ def advance(self, *, payload: PayloadT, headers: Mapping[str, str]) -> CursorState[PayloadT] | None:
132
+ """Return the next state, or ``None`` when a stop rule applies."""
133
+ next_cursor = self.next_cursor(payload, headers)
134
+ if not next_cursor:
135
+ return None
136
+ if next_cursor == self.cursor:
137
+ return None
138
+ if self.has_more is not None and self.has_more(payload, headers) is False:
139
+ return None
140
+ return replace(self, cursor=next_cursor)
141
+
142
+
143
+ @dataclass(frozen=True, kw_only=True, slots=True)
144
+ class KeysetState(Generic[PayloadT]):
145
+ """Moves by a key read from the page just received, usually the last item's id.
146
+
147
+ The walk stops, in this order: when the page was empty; when ``has_more`` returns ``False``; when
148
+ ``has_more`` gives no answer and the page has fewer items than ``limit``; or when the next key is ``None``,
149
+ ``UNSET`` or equal to the key just sent. ``""`` is a valid key, not a missing one."""
150
+
151
+ key: str | None
152
+ limit: int | None
153
+ item_count: Callable[[PayloadT], int]
154
+ next_key: Callable[[PayloadT, Mapping[str, str]], OptionalNullable[str]]
155
+ has_more: Callable[[PayloadT, Mapping[str, str]], OptionalNullable[bool]] | None = None
156
+
157
+ def advance(self, *, payload: PayloadT, headers: Mapping[str, str]) -> KeysetState[PayloadT] | None:
158
+ """Return the next state, or ``None`` when a stop rule applies."""
159
+ count = self.item_count(payload)
160
+ if count == 0:
161
+ return None
162
+ has_more = self.has_more(payload, headers) if self.has_more is not None else None
163
+ if has_more is False:
164
+ return None
165
+ if not isinstance(has_more, bool) and self.limit is not None and count < self.limit:
166
+ return None
167
+ next_key = self.next_key(payload, headers)
168
+ if next_key is None or isinstance(next_key, UnsetType) or next_key == self.key:
169
+ return None
170
+ return replace(self, key=next_key)
171
+
172
+
173
+ @dataclass(frozen=True, kw_only=True, slots=True)
174
+ class LinkState(Generic[PayloadT]):
175
+ """Moves by following the next-page link a response gives, in its body or its ``Link`` header.
176
+
177
+ ``path_params`` fill the placeholders of the first ``url_template``. Following a link replaces
178
+ ``url_template`` (with the link's path), ``path_params`` (with none, since that path is literal) and
179
+ ``query_params`` (with its query). The link is resolved against the URL this state renders and must keep
180
+ its scheme, host and port; a link to another origin raises instead of being followed. The walk stops when
181
+ the next link is missing, empty or ``UNSET``, or when it renders to the same URL as this state."""
182
+
183
+ url_template: UrlTemplate
184
+ path_params: Sequence[Param[Any]] = ()
185
+ query_params: Sequence[Param[Any]]
186
+ next_link: Callable[[PayloadT, Mapping[str, str]], OptionalNullable[str]]
187
+
188
+ def advance(self, *, payload: PayloadT, headers: Mapping[str, str]) -> LinkState[PayloadT] | None:
189
+ """Return the state for the page's next link, or ``None`` when the walk stops.
190
+
191
+ Raises:
192
+ ValueError: If the link points to another origin, or its port is not a number."""
193
+ next_link = self.next_link(payload, headers)
194
+ if not next_link:
195
+ return None
196
+ current_url = build_url(self.url_template, path_params=self.path_params, query_params=self.query_params)
197
+ next_template, link_params = _resolve_next_link(current_url, next_link)
198
+ if build_url(next_template, query_params=link_params) == current_url:
199
+ return None
200
+ return replace(self, url_template=next_template, path_params=(), query_params=link_params)
201
+
202
+
203
+ def _resolve_next_link(current_url: str, next_link: str) -> tuple[UrlTemplate, tuple[Param[Any], ...]]:
204
+ """Resolve ``next_link`` against ``current_url`` and split it into a URL template and query parameters.
205
+
206
+ Scheme, host and port are compared as written; default ports are not filled in. Each query key becomes one
207
+ ``Param[list[str]]`` rendered ``PLAIN``, so a repeated key keeps one pair per value, in the link's order.
208
+ Blank values are kept. A link with no path gets ``/``.
209
+
210
+ Raises:
211
+ ValueError: If the resolved link's scheme, host or port differs from ``current_url``'s."""
212
+ current = urlsplit(current_url)
213
+ resolved = urlsplit(urljoin(current_url, next_link))
214
+ if resolved.scheme != current.scheme or resolved.hostname != current.hostname or resolved.port != current.port:
215
+ raise ValueError(
216
+ f"Cross-origin pagination link '{resolved.scheme}://{resolved.netloc}' does not match the origin of the "
217
+ f"URL just fetched, '{current.scheme}://{current.netloc}'."
218
+ )
219
+ grouped: dict[str, list[str]] = {}
220
+ for key, value in parse_qsl(resolved.query, keep_blank_values=True):
221
+ grouped.setdefault(key, []).append(value)
222
+ link_params = tuple(
223
+ param[list[str]](key, values, serialization_format=SerializationFormat.PLAIN) for key, values in grouped.items()
224
+ )
225
+ return UrlTemplate(base_url=f"{resolved.scheme}://{resolved.netloc}", path=resolved.path or "/"), link_params
226
+
227
+
228
+ class _OffsetStateFactory(SubscriptOnly):
229
+ """``offset_state[PayloadT](…)`` builds a :class:`OffsetState` whose selectors read ``PayloadT``."""
230
+
231
+ __slots__ = ()
232
+ factory_name = "offset_state"
233
+ spelling = "offset_state[PayloadT](…), e.g. offset_state[Products](offset=offset, limit=limit, item_count=…)"
234
+
235
+ def __getitem__(self, declared: TypeForm[PayloadT]) -> type[OffsetState[PayloadT]]:
236
+ return OffsetState
237
+
238
+
239
+ class _PageStateFactory(SubscriptOnly):
240
+ """``page_state[PayloadT](…)`` builds a :class:`PageState` whose selectors read ``PayloadT``."""
241
+
242
+ __slots__ = ()
243
+ factory_name = "page_state"
244
+ spelling = "page_state[PayloadT](…), e.g. page_state[Products](page=page, limit=limit, item_count=…)"
245
+
246
+ def __getitem__(self, declared: TypeForm[PayloadT]) -> type[PageState[PayloadT]]:
247
+ return PageState
248
+
249
+
250
+ class _CursorStateFactory(SubscriptOnly):
251
+ """``cursor_state[PayloadT](…)`` builds a :class:`CursorState` whose selectors read ``PayloadT``."""
252
+
253
+ __slots__ = ()
254
+ factory_name = "cursor_state"
255
+ spelling = "cursor_state[PayloadT](…), e.g. cursor_state[Orders](cursor=cursor, next_cursor=…)"
256
+
257
+ def __getitem__(self, declared: TypeForm[PayloadT]) -> type[CursorState[PayloadT]]:
258
+ return CursorState
259
+
260
+
261
+ class _KeysetStateFactory(SubscriptOnly):
262
+ """``keyset_state[PayloadT](…)`` builds a :class:`KeysetState` whose selectors read ``PayloadT``."""
263
+
264
+ __slots__ = ()
265
+ factory_name = "keyset_state"
266
+ spelling = "keyset_state[PayloadT](…), e.g. keyset_state[CardTransactions](key=since, …)"
267
+
268
+ def __getitem__(self, declared: TypeForm[PayloadT]) -> type[KeysetState[PayloadT]]:
269
+ return KeysetState
270
+
271
+
272
+ class _LinkStateFactory(SubscriptOnly):
273
+ """``link_state[PayloadT](…)`` builds a :class:`LinkState` whose selectors read ``PayloadT``."""
274
+
275
+ __slots__ = ()
276
+ factory_name = "link_state"
277
+ spelling = "link_state[PayloadT](…), e.g. link_state[Orders](url_template=…, query_params=…, next_link=…)"
278
+
279
+ def __getitem__(self, declared: TypeForm[PayloadT]) -> type[LinkState[PayloadT]]:
280
+ return LinkState
281
+
282
+
283
+ offset_state: Final = _OffsetStateFactory()
284
+ page_state: Final = _PageStateFactory()
285
+ cursor_state: Final = _CursorStateFactory()
286
+ keyset_state: Final = _KeysetStateFactory()
287
+ link_state: Final = _LinkStateFactory()
288
+
289
+ PagingState: TypeAlias = (
290
+ OffsetState[PayloadT] | PageState[PayloadT] | CursorState[PayloadT] | KeysetState[PayloadT] | LinkState[PayloadT]
291
+ )
292
+
293
+
294
+ @overload
295
+ def paginate(
296
+ *,
297
+ initial_state: OffsetState[PayloadT],
298
+ fetch_page: Callable[[OffsetState[PayloadT]], ApiResult[PayloadT, E]],
299
+ items_selector: Callable[[PayloadT], Sequence[ItemT]],
300
+ ) -> Paginator[ApiResult[PayloadT, E], ItemT]: ...
301
+
302
+
303
+ @overload
304
+ def paginate(
305
+ *,
306
+ initial_state: PageState[PayloadT],
307
+ fetch_page: Callable[[PageState[PayloadT]], ApiResult[PayloadT, E]],
308
+ items_selector: Callable[[PayloadT], Sequence[ItemT]],
309
+ ) -> Paginator[ApiResult[PayloadT, E], ItemT]: ...
310
+
311
+
312
+ @overload
313
+ def paginate(
314
+ *,
315
+ initial_state: CursorState[PayloadT],
316
+ fetch_page: Callable[[CursorState[PayloadT]], ApiResult[PayloadT, E]],
317
+ items_selector: Callable[[PayloadT], Sequence[ItemT]],
318
+ ) -> Paginator[ApiResult[PayloadT, E], ItemT]: ...
319
+
320
+
321
+ @overload
322
+ def paginate(
323
+ *,
324
+ initial_state: KeysetState[PayloadT],
325
+ fetch_page: Callable[[KeysetState[PayloadT]], ApiResult[PayloadT, E]],
326
+ items_selector: Callable[[PayloadT], Sequence[ItemT]],
327
+ ) -> Paginator[ApiResult[PayloadT, E], ItemT]: ...
328
+
329
+
330
+ @overload
331
+ def paginate(
332
+ *,
333
+ initial_state: LinkState[PayloadT],
334
+ fetch_page: Callable[[LinkState[PayloadT]], ApiResult[PayloadT, E]],
335
+ items_selector: Callable[[PayloadT], Sequence[ItemT]],
336
+ ) -> Paginator[ApiResult[PayloadT, E], ItemT]: ...
337
+
338
+
339
+ def paginate(
340
+ *,
341
+ initial_state: PagingState[PayloadT],
342
+ fetch_page: Callable[[Any], ApiResult[PayloadT, E]],
343
+ items_selector: Callable[[PayloadT], Sequence[ItemT]],
344
+ ) -> Paginator[ApiResult[PayloadT, E], ItemT]:
345
+ """Walk a paginated operation on the sync client, one ``execute`` per page.
346
+
347
+ Args:
348
+ initial_state: The first page's state, built with a state factory such as
349
+ ``offset_state[Products](…)``.
350
+ fetch_page: Sends the request for the state it is given and returns that page's ``ApiResult``.
351
+ items_selector: Returns one payload's items, in order. Iterating items unwraps each page first, so it
352
+ raises ``ApiError`` at a failed page.
353
+
354
+ Returns:
355
+ A lazy paginator that can be iterated more than once. Iterating it yields items. ``iter_pages()``
356
+ yields each ``ApiResult``, stops after a ``Failure`` and never raises."""
357
+
358
+ def fetch_pages() -> Iterator[ApiResult[PayloadT, E]]:
359
+ state = initial_state
360
+ while True:
361
+ result = fetch_page(state)
362
+ yield result
363
+ if not isinstance(result, Success):
364
+ return
365
+ next_state = state.advance(payload=result.payload, headers=result.headers)
366
+ if next_state is None:
367
+ return
368
+ state = next_state
369
+
370
+ return Paginator(fetch_pages, lambda result: items_selector(unwrap(result)))
371
+
372
+
373
+ @overload
374
+ def async_paginate(
375
+ *,
376
+ initial_state: OffsetState[PayloadT],
377
+ fetch_page: Callable[[OffsetState[PayloadT]], Awaitable[ApiResult[PayloadT, E]]],
378
+ items_selector: Callable[[PayloadT], Sequence[ItemT]],
379
+ ) -> AsyncPaginator[ApiResult[PayloadT, E], ItemT]: ...
380
+
381
+
382
+ @overload
383
+ def async_paginate(
384
+ *,
385
+ initial_state: PageState[PayloadT],
386
+ fetch_page: Callable[[PageState[PayloadT]], Awaitable[ApiResult[PayloadT, E]]],
387
+ items_selector: Callable[[PayloadT], Sequence[ItemT]],
388
+ ) -> AsyncPaginator[ApiResult[PayloadT, E], ItemT]: ...
389
+
390
+
391
+ @overload
392
+ def async_paginate(
393
+ *,
394
+ initial_state: CursorState[PayloadT],
395
+ fetch_page: Callable[[CursorState[PayloadT]], Awaitable[ApiResult[PayloadT, E]]],
396
+ items_selector: Callable[[PayloadT], Sequence[ItemT]],
397
+ ) -> AsyncPaginator[ApiResult[PayloadT, E], ItemT]: ...
398
+
399
+
400
+ @overload
401
+ def async_paginate(
402
+ *,
403
+ initial_state: KeysetState[PayloadT],
404
+ fetch_page: Callable[[KeysetState[PayloadT]], Awaitable[ApiResult[PayloadT, E]]],
405
+ items_selector: Callable[[PayloadT], Sequence[ItemT]],
406
+ ) -> AsyncPaginator[ApiResult[PayloadT, E], ItemT]: ...
407
+
408
+
409
+ @overload
410
+ def async_paginate(
411
+ *,
412
+ initial_state: LinkState[PayloadT],
413
+ fetch_page: Callable[[LinkState[PayloadT]], Awaitable[ApiResult[PayloadT, E]]],
414
+ items_selector: Callable[[PayloadT], Sequence[ItemT]],
415
+ ) -> AsyncPaginator[ApiResult[PayloadT, E], ItemT]: ...
416
+
417
+
418
+ def async_paginate(
419
+ *,
420
+ initial_state: PagingState[PayloadT],
421
+ fetch_page: Callable[[Any], Awaitable[ApiResult[PayloadT, E]]],
422
+ items_selector: Callable[[PayloadT], Sequence[ItemT]],
423
+ ) -> AsyncPaginator[ApiResult[PayloadT, E], ItemT]:
424
+ """Walk a paginated operation on the async client, one awaited ``execute`` per page.
425
+
426
+ Works like :func:`paginate`, except that ``fetch_page`` returns an awaitable.
427
+
428
+ Args:
429
+ initial_state: The first page's state, built with a state factory such as
430
+ ``offset_state[Products](…)``.
431
+ fetch_page: Sends the request for the state it is given and returns an awaitable of that page's
432
+ ``ApiResult``.
433
+ items_selector: Returns one payload's items, in order. Iterating items unwraps each page first, so it
434
+ raises ``ApiError`` at a failed page.
435
+
436
+ Returns:
437
+ A lazy async paginator that can be iterated more than once, with the same item and page views as the
438
+ sync one."""
439
+
440
+ async def fetch_pages() -> AsyncIterator[ApiResult[PayloadT, E]]:
441
+ state = initial_state
442
+ while True:
443
+ result = await fetch_page(state)
444
+ yield result
445
+ if not isinstance(result, Success):
446
+ return
447
+ next_state = state.advance(payload=result.payload, headers=result.headers)
448
+ if next_state is None:
449
+ return
450
+ state = next_state
451
+
452
+ return AsyncPaginator(fetch_pages, lambda result: items_selector(unwrap(result)))
@@ -0,0 +1,55 @@
1
+ """The paginator a paginated endpoint returns: a lazy view over a walk of pages that can be iterated again.
2
+
3
+ A paginator holds a function that starts a new page iterator, never an iterator itself. So building one sends
4
+ nothing, the first request goes out when iteration starts, and each new iteration starts again from the
5
+ first page. Iterating yields items; ``iter_pages()`` yields pages.
6
+
7
+ ``results`` imports this module, so this module must not import ``results``."""
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import AsyncIterator, Callable, Iterator, Sequence
12
+ from dataclasses import dataclass
13
+ from typing import Generic, TypeVar
14
+
15
+ PageT = TypeVar("PageT")
16
+ ItemT = TypeVar("ItemT")
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class Paginator(Generic[PageT, ItemT]):
21
+ """A walk over a paginated operation. Iterate it for items, or call ``iter_pages()`` for pages.
22
+
23
+ Nothing is sent until iteration starts. Each iteration starts again from the first page, and a ``break``
24
+ stops it with no further request."""
25
+
26
+ _fetch_pages: Callable[[], Iterator[PageT]]
27
+ _items_selector: Callable[[PageT], Sequence[ItemT]]
28
+
29
+ def __iter__(self) -> Iterator[ItemT]:
30
+ for page in self._fetch_pages():
31
+ yield from self._items_selector(page)
32
+
33
+ def iter_pages(self) -> Iterator[PageT]:
34
+ """Start a new walk and yield each page as it arrives."""
35
+ return self._fetch_pages()
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class AsyncPaginator(Generic[PageT, ItemT]):
40
+ """An async walk over a paginated operation. Use ``async for`` for items, or ``iter_pages()`` for pages.
41
+
42
+ Nothing is sent until iteration starts. Each iteration starts again from the first page, and a ``break``
43
+ stops it with no further request."""
44
+
45
+ _fetch_pages: Callable[[], AsyncIterator[PageT]]
46
+ _items_selector: Callable[[PageT], Sequence[ItemT]]
47
+
48
+ async def __aiter__(self) -> AsyncIterator[ItemT]:
49
+ async for page in self._fetch_pages():
50
+ for item in self._items_selector(page):
51
+ yield item
52
+
53
+ def iter_pages(self) -> AsyncIterator[PageT]:
54
+ """Start a new walk and yield each page as it arrives."""
55
+ return self._fetch_pages()