cliV1 0.0.6__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.6.dist-info/METADATA +212 -0
  2. cliv1-0.0.6.dist-info/RECORD +92 -0
  3. cliv1-0.0.6.dist-info/WHEEL +4 -0
  4. cliv1-0.0.6.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,208 @@
1
+ """Masking, for what the runtime prints: header values, URLs and form fields.
2
+
3
+ The rule is vocabulary knowability: a value is shown only when its name is known-safe, so a credential
4
+ under a name nobody predicted is hidden by default. Every denylist in the research leaked
5
+ in a real run; the allowlist is the one rule that held. A form body is the one denylist: it prints only on
6
+ the caller's opt-in, and no vocabulary of safe form keys exists, so there only the names ``mask_keys`` lists
7
+ are hidden. Stdlib-only on purpose: ``transport.py`` imports this module for ``HttpRequest.__repr__``, and
8
+ ``logging.py`` imports ``transport.py``."""
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Mapping
13
+ from collections.abc import Set as AbstractSet
14
+ from typing import Final
15
+ from urllib.parse import unquote_plus, urlsplit, urlunsplit
16
+
17
+ DEFAULT_PLACEHOLDER: Final = "REDACTED"
18
+ """What a hidden value prints as unless ``LoggingOptions.redaction_placeholder`` says otherwise -- the OpenTelemetry
19
+ semantic conventions' word for one (ADR-0070, rule 4). A repr and an abandonment warning, which read no options,
20
+ always print this one."""
21
+
22
+ SAFE_HEADERS: Final = frozenset(
23
+ {
24
+ "accept",
25
+ "accept-charset",
26
+ "accept-encoding",
27
+ "accept-language",
28
+ "accept-ranges",
29
+ "age",
30
+ "allow",
31
+ "cache-control",
32
+ "connection",
33
+ "content-disposition",
34
+ "content-encoding",
35
+ "content-language",
36
+ "content-length",
37
+ "content-md5",
38
+ "content-range",
39
+ "content-type",
40
+ "date",
41
+ "etag",
42
+ "expect",
43
+ "expires",
44
+ "host",
45
+ "idempotency-key",
46
+ "if-match",
47
+ "if-modified-since",
48
+ "if-none-match",
49
+ "if-range",
50
+ "if-unmodified-since",
51
+ "keep-alive",
52
+ "last-modified",
53
+ "max-forwards",
54
+ "origin",
55
+ "pragma",
56
+ "range",
57
+ "ratelimit-limit",
58
+ "ratelimit-policy",
59
+ "ratelimit-remaining",
60
+ "ratelimit-reset",
61
+ "request-id",
62
+ "retry-after",
63
+ "server",
64
+ "te",
65
+ "trailer",
66
+ "transfer-encoding",
67
+ "upgrade",
68
+ "user-agent",
69
+ "vary",
70
+ "via",
71
+ "warning",
72
+ "www-authenticate",
73
+ # W3C trace context
74
+ "traceparent",
75
+ "tracestate",
76
+ # correlation and rate-limit headers outside the standards
77
+ "x-correlation-id",
78
+ "x-ratelimit-limit",
79
+ "x-ratelimit-remaining",
80
+ "x-ratelimit-reset",
81
+ "x-request-id",
82
+ # the runtime's own identification headers (ADR-0045)
83
+ "x-apimatic-lang",
84
+ "x-apimatic-package-version",
85
+ "x-apimatic-gen-version",
86
+ "x-apimatic-os",
87
+ "x-apimatic-runtime",
88
+ }
89
+ )
90
+ """The 61 header names whose values may be printed: the standard request and response headers that carry no
91
+ credential, lowercased."""
92
+
93
+ SAFE_QUERY_KEYS: Final = frozenset(
94
+ {"cursor", "dates", "datetime", "limit", "offset", "page", "since", "size", "strings"}
95
+ )
96
+ """The nine query keys whose values may be printed: a fixed vocabulary of paging and filter names, as ``SAFE_HEADERS``
97
+ is for headers, the same in every SDK this runtime is vendored into and never derived from a spec. A key an endpoint
98
+ declares but the vocabulary lacks is masked like any other -- the safe direction."""
99
+
100
+
101
+ def redact_headers(
102
+ headers: Mapping[str, str], *, unmask: AbstractSet[str], mask: AbstractSet[str], placeholder: str
103
+ ) -> dict[str, str]:
104
+ """``headers`` with every value replaced by ``placeholder`` unless its name may be shown.
105
+
106
+ Precedence: ``unmask`` wins, then ``mask``, then the safe list, then the placeholder. Names
107
+ are compared as given: the runtime lowercases every header name as it resolves it, and the option
108
+ sets are lowercased once at the client, so a name that arrives otherwise is masked -- the safe
109
+ direction.
110
+
111
+ Args:
112
+ headers: The headers to describe, names lowercased by the layer that resolved them.
113
+ unmask: Names to show regardless of the safe list.
114
+ mask: Names to hide regardless of the safe list.
115
+ placeholder: What a hidden value prints as -- the client's ``redaction_placeholder``.
116
+
117
+ Returns:
118
+ The same names in the same order, each value shown or ``placeholder``."""
119
+ return {
120
+ name: value if name in unmask or (name not in mask and name in SAFE_HEADERS) else placeholder
121
+ for name, value in headers.items()
122
+ }
123
+
124
+
125
+ def redact_query(query: str, *, mask_keys: AbstractSet[str], placeholder: str) -> str:
126
+ """``key=value&...`` with every value replaced by ``placeholder`` unless its key may be shown.
127
+
128
+ A value is shown only when its key is in ``SAFE_QUERY_KEYS`` and not in ``mask_keys``: unknown means
129
+ hidden, the header rule again, so a credential's key -- never a paging name -- is masked without a
130
+ list naming it. A key is matched by its :func:`base_name`, once unquoted, so the keys a collection
131
+ explodes into (``strings[0]``, ``strings[]``, ``dates[0]``) answer to the ``strings`` the vocabulary
132
+ holds, and ``PAGE`` answers to ``page``. Keys are never rewritten; a pair without ``=`` and an empty
133
+ pair are left as they are.
134
+
135
+ Args:
136
+ query: The encoded pairs, without the leading ``?``.
137
+ mask_keys: Lowercased names whose values are hidden even when known-safe.
138
+ placeholder: What a hidden value prints as.
139
+
140
+ Returns:
141
+ The pairs in their order, each value shown or ``placeholder``."""
142
+ redacted_pairs: list[str] = []
143
+ for pair in query.split("&"):
144
+ key, separator, _ = pair.partition("=")
145
+ name = base_name(unquote_plus(key))
146
+ redacted_pairs.append(
147
+ f"{key}={placeholder}" if separator and (name in mask_keys or name not in SAFE_QUERY_KEYS) else pair
148
+ )
149
+ return "&".join(redacted_pairs)
150
+
151
+
152
+ def redact_form(form: bytes, *, mask_keys: AbstractSet[str], placeholder: str) -> bytes:
153
+ """A form body's bytes with the value of every key ``mask_keys`` names replaced by ``placeholder``.
154
+
155
+ The form-body rule: the walk :func:`redact_query` makes, over the bytes the body carries and with the
156
+ opposite list -- every value is shown unless its key is named. A key is matched by its :func:`base_name`,
157
+ once unquoted, as there, so the ``password[0]`` this runtime's INDEXED format writes answers to
158
+ ``password``, and a key nested in a map answers to its outer name. Keys are never rewritten; a pair
159
+ without ``=`` and an empty pair are left as they are. Only a key is decoded, to read its name; the body
160
+ itself is never decoded here.
161
+
162
+ Args:
163
+ form: The encoded pairs of an ``application/x-www-form-urlencoded`` body.
164
+ mask_keys: Lowercased names whose values are hidden.
165
+ placeholder: What a hidden value prints as; it is written as UTF-8.
166
+
167
+ Returns:
168
+ The pairs in their order, each value kept or replaced by ``placeholder``."""
169
+ placeholder_bytes = placeholder.encode("utf-8")
170
+ redacted_pairs: list[bytes] = []
171
+ for pair in form.split(b"&"):
172
+ key, separator, _ = pair.partition(b"=")
173
+ name = base_name(unquote_plus(key.decode("utf-8", errors="replace")))
174
+ redacted_pairs.append(key + b"=" + placeholder_bytes if separator and name in mask_keys else pair)
175
+ return b"&".join(redacted_pairs)
176
+
177
+
178
+ def redact_url(url: str, *, mask_keys: AbstractSet[str] = frozenset(), placeholder: str = DEFAULT_PLACEHOLDER) -> str:
179
+ """The form of ``url`` that may be printed: userinfo dropped, query values masked, keys kept.
180
+
181
+ The one printable-URL rule, for a log line, a repr and the abandonment warnings alike. The query
182
+ goes through :func:`redact_query`; the defaults are the form a repr or a warning uses, having no
183
+ options to read, and print what a client with default options logs, since no known-safe key is
184
+ among the default ``mask_keys``. Total: a URL the parser refuses is described rather than raised
185
+ on, since this runs inside a log call and a finalizer.
186
+
187
+ Args:
188
+ url: The URL to describe.
189
+ mask_keys: Query keys whose values are hidden even when known-safe.
190
+ placeholder: What a hidden value prints as.
191
+
192
+ Returns:
193
+ ``scheme://host[:port]/path[?masked query][#fragment]``, or ``<unparseable url>``."""
194
+ try:
195
+ parts = urlsplit(url)
196
+ except ValueError:
197
+ return "<unparseable url>"
198
+ query = redact_query(parts.query, mask_keys=mask_keys, placeholder=placeholder) if parts.query else ""
199
+ return urlunsplit(parts._replace(netloc=parts.netloc.rpartition("@")[2], query=query))
200
+
201
+
202
+ def base_name(key: str) -> str:
203
+ """The name ``mask_keys`` and ``SAFE_QUERY_KEYS`` match ``key`` by: cut before the first ``[``, lowercased.
204
+
205
+ So the keys a collection explodes into (``strings[0]``, ``strings[]``) and a key nested in a map
206
+ (``filter[name]``) answer to their outer name, and ``PAGE`` answers to ``page``. A query or form key is
207
+ unquoted before it comes here; a multipart field name is not, since it is never percent-encoded."""
208
+ return key.partition("[")[0].lower()
@@ -0,0 +1,124 @@
1
+ """Parsing the ``text/event-stream`` grammar: bytes in, event data out.
2
+
3
+ The WHATWG algorithm ("Server-sent events", HTML Standard) written as a push parser, so one
4
+ implementation serves both flavours: a consumer hands over each chunk the transport delivers and takes
5
+ back the events those bytes completed, and nothing here touches the wire. Per the standard: one leading
6
+ UTF-8 BOM is dropped; a line ends at CRLF, LF or CR; a line starting with ``:`` is a comment; ``data``
7
+ accumulates and its values join with LF; a blank line dispatches, but only when a ``data`` line was seen,
8
+ otherwise it resets; ``event``, ``id`` and ``retry`` are recognised so they are ignored correctly, and so
9
+ is any other field -- only ``data`` reaches the consumer.
10
+
11
+ Lines are found with ``bytes.splitlines``, which knows exactly the standard's three terminators and no
12
+ others -- unlike ``str.splitlines``, which also cuts on ``\\x0b``, ``\\x0c``, ``\\x1c`` to ``\\x1e``,
13
+ ``\\x85`` and U+2028/9 and would split a payload holding one of them. It is one C pass per chunk,
14
+ measured at a twelfth of a regex loop's cost, and only the new chunk is ever scanned: the unfinished tail
15
+ of the previous chunk is carried and joined onto the first line the next chunk completes, so parsing
16
+ stays linear in the bytes received however small the chunks.
17
+
18
+ Two things the standard leaves to the client. A CR at the very end of a chunk is a terminator on its own,
19
+ and the LF that may follow it in the next chunk is then skipped, so a CRLF split across two reads is one
20
+ terminator, never two -- and the event dispatches the moment its CR arrives rather than waiting for the
21
+ next chunk. And an event still pending at EOF is dropped, as the standard's processing model says and as
22
+ the .NET parser does: a truncated final event is not delivered as if it were whole.
23
+
24
+ Each completed line is decoded as UTF-8 on its own, strictly. That is exact rather than a shortcut: both
25
+ terminator bytes are ASCII, and in UTF-8 an ASCII byte is never part of a multi-byte sequence, so a line
26
+ boundary cannot fall inside a character and no incremental decoder is needed. A line that is not UTF-8
27
+ raises the stdlib's ``UnicodeDecodeError``, unwrapped: it already names the offending byte and position.
28
+ The one bound is on bytes held for an event that has not dispatched -- the carried tail plus the ``data``
29
+ already collected -- so a server that never sends a blank line is refused at 1 MiB with a ``ValueError``
30
+ instead of growing the buffer without limit."""
31
+
32
+ from __future__ import annotations
33
+
34
+ from typing import Final
35
+
36
+ MAX_EVENT_BYTES: Final = 1 << 20
37
+ """Bytes held for one undispatched event before the stream is refused (1 MiB)."""
38
+
39
+ _BOM: Final = b"\xef\xbb\xbf"
40
+ _LINE_ENDS: Final = (b"\n", b"\r")
41
+
42
+
43
+ class SseParser:
44
+ """The push parser: ``parse_chunk`` each chunk as the transport delivers it, take back the events it completed."""
45
+
46
+ __slots__ = ("_at_start", "_data", "_pending", "_skip_lf", "_tail")
47
+
48
+ def __init__(self) -> None:
49
+ self._tail = bytearray() # the unfinished line of the last chunk; never holds a terminator
50
+ self._at_start = True
51
+ self._skip_lf = False
52
+ self._data: list[str] = []
53
+ self._pending = 0
54
+
55
+ def parse_chunk(self, chunk: bytes) -> list[str]:
56
+ """Consume ``chunk`` and return the data of every event it completed, in order.
57
+
58
+ Args:
59
+ chunk: The next bytes off the wire, of any length including zero.
60
+
61
+ Returns:
62
+ Each dispatched event's ``data``, its lines already joined with LF.
63
+
64
+ Raises:
65
+ ValueError: If the pending event outgrows :data:`MAX_EVENT_BYTES`, or -- as a
66
+ ``UnicodeDecodeError`` -- if a line is not valid UTF-8."""
67
+ if chunk and self._skip_lf:
68
+ # The previous chunk ended on a CR that was treated as a terminator; a leading LF now is the
69
+ # other half of that CRLF, not a blank line. An empty chunk decides nothing, so the flag waits.
70
+ self._skip_lf = False
71
+ if chunk[:1] == b"\n":
72
+ chunk = chunk[1:]
73
+ if self._at_start:
74
+ self._tail += chunk
75
+ if len(self._tail) < len(_BOM) and _BOM.startswith(self._tail):
76
+ return []
77
+ if self._tail.startswith(_BOM):
78
+ del self._tail[: len(_BOM)]
79
+ self._at_start = False
80
+ chunk = bytes(self._tail)
81
+ self._tail.clear()
82
+
83
+ # bytes.splitlines knows exactly SSE's three terminators; keepends tells the last piece apart --
84
+ # terminated, or an unfinished tail to carry into the next chunk.
85
+ pieces = chunk.splitlines(keepends=True)
86
+ unfinished = pieces.pop() if pieces and not pieces[-1].endswith(_LINE_ENDS) else b""
87
+ if pieces:
88
+ pieces[0] = bytes(self._tail) + pieces[0]
89
+ self._tail.clear()
90
+ self._skip_lf = not unfinished and pieces[-1].endswith(b"\r")
91
+ self._tail += unfinished
92
+
93
+ events: list[str] = []
94
+ for piece in pieces:
95
+ data = self._line(piece)
96
+ if data is not None:
97
+ events.append(data)
98
+ if len(self._tail) + self._pending > MAX_EVENT_BYTES:
99
+ raise ValueError(
100
+ f"an event exceeded {MAX_EVENT_BYTES} bytes without dispatching -- refusing to buffer further"
101
+ )
102
+ return events
103
+
104
+ def _line(self, raw: bytes) -> str | None:
105
+ # ``raw`` is one terminated line, so the strip removes exactly its CRLF, LF or CR. Strict decode:
106
+ # bad bytes raise UnicodeDecodeError straight out of parse_chunk.
107
+ line = raw.rstrip(b"\r\n").decode("utf-8")
108
+ if not line:
109
+ return self._dispatch()
110
+ if line.startswith(":"):
111
+ return None
112
+ name, colon, value = line.partition(":")
113
+ if name == "data":
114
+ self._data.append(value[1:] if colon and value.startswith(" ") else value)
115
+ self._pending += len(raw)
116
+ return None
117
+
118
+ def _dispatch(self) -> str | None:
119
+ if not self._data:
120
+ return None
121
+ data = "\n".join(self._data)
122
+ self._data = []
123
+ self._pending = 0
124
+ return data
@@ -0,0 +1,50 @@
1
+ """What every subscripted factory shares: refusing to be called.
2
+
3
+ The fence itself is static and lives in each factory's *absence* of a ``__call__``; this module
4
+ carries only the message a caller sees when they trip it at runtime."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+
11
+ class SubscriptOnly:
12
+ """A factory whose declared type is structurally mandatory.
13
+
14
+ Fourteen factories across ``params``, ``bodies`` and ``decoding`` name their type in a subscript
15
+ (ADR-0013). Thirteen of the fourteen declare no ``__call__`` a type checker can see, so omitting
16
+ the subscript is an ``[operator]`` error at the call site -- that is the fence, and it is entirely
17
+ static; ``decode_text`` is the exception, and the last paragraph says why. What this
18
+ class carries is the *courtesy*: a runtime ``__call__``, invisible to the checker, turning
19
+ Python's bare "object is not callable" into a message that names the right spelling.
20
+
21
+ Subclasses supply the two facts the message is built from, as class attributes or as fields of
22
+ their own. What a base may **not** share is a ``__getitem__``'s *return shape*: it depends on the
23
+ subscripted type, and Python has no higher-kinded types, so no base can parametrise over it --
24
+ the precision ADR-0013 exists for, the reason ``json_decoder[int]`` is rejected where
25
+ ``ApiResult[str, ...]`` is declared. Two factories over the *same* shape may share a class and
26
+ tell themselves apart by a field, because what they then differ in -- a body's format, say, or
27
+ which adapter entry point reads an event's ``data`` -- carries no type at all. Only a shape
28
+ does.
29
+
30
+ ``decode_text`` is the one member of the family that does **not** inherit this: its bare call is
31
+ the ``str`` default, so its ``__call__`` is real, typed, and meant to be reached."""
32
+
33
+ # Empty rather than naming the two attributes below: a subclass supplies them either as class
34
+ # attributes or as its own dataclass fields, and claiming them here would collide with the
35
+ # latter. Without this line the ``slots=True`` the dataclass subclasses declare buys nothing --
36
+ # a slotted class over an unslotted base still gets a ``__dict__`` per instance.
37
+ __slots__ = ()
38
+
39
+ factory_name: str
40
+ """How the caller spells the factory -- ``json_decoder``, ``param``."""
41
+
42
+ spelling: str
43
+ """The correct form, with an example: ``param[T](key, value), e.g. param[bool]("array", True)``."""
44
+
45
+ if not TYPE_CHECKING:
46
+
47
+ def __call__(self, *args, **kwargs):
48
+ raise TypeError(
49
+ f"{self.factory_name} is not called -- name the declared type in its subscript: {self.spelling}"
50
+ )
@@ -0,0 +1,172 @@
1
+ """Turning a :class:`UrlTemplate` plus parameters into a request URL.
2
+
3
+ Five pure functions in four groups. :func:`resolve_params` combines the parameters an API applies
4
+ to every request with the ones a single call declares; :func:`render_segments` turns each of those
5
+ into the URL segments it contributes, reading its declared type; :func:`build_url` then renders the
6
+ result, with server variables filling the base URL's placeholders, path parameters filling the
7
+ path's, and query parameters flattened and encoded onto the end. Resolution stays separate from
8
+ rendering so each can be tested on its own, and none of the six holds state -- there is nothing to
9
+ configure, so there is nothing to construct."""
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from collections.abc import Mapping, Sequence
15
+ from typing import Any, Final
16
+ from urllib.parse import quote, urlencode
17
+
18
+ from ..params import Param, UrlTemplate
19
+ from .flattening import flatten
20
+ from .wire import text_values
21
+
22
+ _PLACEHOLDER: Final = re.compile(r"\{([^{}]+)\}")
23
+
24
+
25
+ def expand_template(template: str, segments: Mapping[str, tuple[str, ...]]) -> str:
26
+ """Substitute every ``{placeholder}`` in ``template`` from ``segments``.
27
+
28
+ Each value is the sequence of *unencoded* members its placeholder expands from. Members are
29
+ percent-encoded with no safe characters and joined with ``,``, so a collection fills one path
30
+ segment rather than several -- OpenAPI's ``style: simple`` with ``explode: false``, the default
31
+ for a path parameter, and the same rule ``headers.py`` applies to a header array.
32
+
33
+ Encoding happens per member, *before* the join, which is what keeps the two commas apart: the
34
+ separator is written here and stays literal (a sub-delimiter under RFC 3986), while a comma
35
+ carried by a value is encoded to ``%2C`` along with everything else non-unreserved. A value
36
+ still cannot inject a path separator, for the same reason.
37
+
38
+ Any ``{name}`` is a placeholder -- the braces delimit it, so the name is not restricted to word
39
+ characters and a kebab-case or dotted path parameter resolves like any other. A name with no
40
+ segment raises rather than passing through: unencoded braces are not valid in a URL anyway.
41
+
42
+ Deciding what a value's members *are* belongs to :func:`render_segments`, which is the only
43
+ thing holding a declared type. This function therefore has one rule rather than four, and both
44
+ the join and the encoding live in exactly one place.
45
+
46
+ Args:
47
+ template: The URL or path holding the ``{placeholder}`` tokens.
48
+ segments: Each placeholder name mapped to its unencoded members.
49
+
50
+ Returns:
51
+ ``template`` with every placeholder substituted, each member percent-encoded.
52
+
53
+ Raises:
54
+ ValueError: If ``template`` is empty, or names a placeholder ``segments`` has no entry
55
+ for."""
56
+ if not template:
57
+ raise ValueError("URL template must not be empty")
58
+
59
+ def replace(match: re.Match[str]) -> str:
60
+ key = match.group(1)
61
+ if key not in segments:
62
+ raise ValueError(f"Missing template parameter: {key}")
63
+ return ",".join(quote(segment, safe="") for segment in segments[key])
64
+
65
+ return _PLACEHOLDER.sub(replace, template)
66
+
67
+
68
+ def render_segments(params: Sequence[Param[Any]] | None) -> dict[str, tuple[str, ...]]:
69
+ """Render each parameter into the URL segments its ``{placeholder}`` expands to.
70
+
71
+ Serves a path parameter and a base URL's server variable alike -- both fill a placeholder and
72
+ both name the type they were declared as, so there is one rule rather than two.
73
+
74
+ Members, not finished text: :func:`expand_template` owns the ``,`` join and the
75
+ percent-encoding, so neither is written twice and no value can inject a path separator.
76
+
77
+ Args:
78
+ params: The path parameters or server variables; ``None`` is treated as none at all.
79
+
80
+ Returns:
81
+ Each parameter's key mapped to the unencoded members its placeholder expands to.
82
+
83
+ Raises:
84
+ ValueError: If a parameter contributes no value at all -- a placeholder cannot be left
85
+ unfilled -- or if its value does not match its declared type."""
86
+ rendered: dict[str, tuple[str, ...]] = {}
87
+ for param in params or ():
88
+ segments = text_values(param)
89
+ if not segments:
90
+ raise ValueError(f"URL parameter has no value: {param.key}")
91
+ rendered[param.key] = segments
92
+ return rendered
93
+
94
+
95
+ def urlencode_params(params: Sequence[Param[Any]] | None) -> str:
96
+ """Flatten ``params`` and percent-encode them as ``application/x-www-form-urlencoded`` pairs.
97
+
98
+ One encoder for both destinations that carry pairs -- a query string (:func:`build_url`, which
99
+ adds the ``?``) and a form body (``form_body``) -- so one parameter renders to the same bytes
100
+ wherever it goes. Pair order is the parameters' order, a repeated key once per value. Non-ASCII
101
+ text is UTF-8 encoded and then percent-encoded (``urlencode``'s default, and the WHATWG rule),
102
+ so the result is ASCII.
103
+
104
+ Args:
105
+ params: The parameters to render; ``None`` is treated as none at all.
106
+
107
+ Returns:
108
+ The encoded pairs, empty when nothing was contributed.
109
+
110
+ Raises:
111
+ ValueError: If a parameter's value does not match its declared type."""
112
+ return urlencode(flatten(params))
113
+
114
+
115
+ def resolve_params(
116
+ global_params: Sequence[Param[Any]], call_params: Sequence[Param[Any]] | None
117
+ ) -> tuple[Param[Any], ...]:
118
+ """Return the parameters for one call, the call's winning per key.
119
+
120
+ One function for both kinds that need it: a path parameter and a query parameter are the same
121
+ type, so they are the same rule -- a call's parameter replaces a global of the same key but
122
+ keeps the global's position, so what an endpoint produces does not depend on which side supplied
123
+ a parameter. Keys are unique within either side -- a collection is carried by a single
124
+ :class:`Param` and expanded by its destination, never by repeating the key -- so replacing
125
+ rather than appending is unambiguous.
126
+
127
+ Both arguments share a type, so the precedence lives in their names rather than in the function
128
+ name: ``global_params`` is what the API prescribes, ``call_params`` what this one call declares.
129
+
130
+ Headers resolve separately, since they merge case-insensitively -- which ``render_headers`` gets
131
+ by lowercasing as it renders. Placeholders in a *base URL* are server variables, carried on the
132
+ :class:`UrlTemplate` itself and rendered by :func:`build_url`.
133
+
134
+ Args:
135
+ global_params: What the API prescribes for every call.
136
+ call_params: What this one call declares; wins per key.
137
+
138
+ Returns:
139
+ The effective parameters, each key once, in the order the global set established."""
140
+ by_key = {param.key: param for param in (*global_params, *(call_params or ()))}
141
+ return tuple(by_key.values())
142
+
143
+
144
+ def build_url(
145
+ url_template: UrlTemplate,
146
+ *,
147
+ path_params: Sequence[Param[Any]] | None = None,
148
+ query_params: Sequence[Param[Any]] | None = None,
149
+ ) -> str:
150
+ """Resolve ``url_template`` into a complete request URL.
151
+
152
+ Args:
153
+ url_template: The unresolved URL, carrying its base URL, path and server variables.
154
+ path_params: The call's path parameters.
155
+ query_params: The call's query parameters.
156
+
157
+ Returns:
158
+ The absolute URL, base and path joined on exactly one ``/``, query appended when non-empty.
159
+
160
+ Raises:
161
+ ValueError: If a placeholder is unfilled or a parameter's value does not match its
162
+ declared type."""
163
+ base = expand_template(url_template.base_url, render_segments(url_template.variables))
164
+ path = expand_template(url_template.path, render_segments(path_params))
165
+
166
+ # Join on exactly one separator. Plain urljoin would be wrong here: it treats the base as a
167
+ # document reference and discards its last path segment, so a base of ".../v2" with path
168
+ # "users" would resolve to ".../users", silently dropping the version.
169
+ url = f"{base.rstrip('/')}/{path.lstrip('/')}"
170
+
171
+ query = urlencode_params(query_params)
172
+ return f"{url}?{query}" if query else url