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.
- cliv1-0.0.6.dist-info/METADATA +212 -0
- cliv1-0.0.6.dist-info/RECORD +92 -0
- cliv1-0.0.6.dist-info/WHEEL +4 -0
- cliv1-0.0.6.dist-info/licenses/LICENSE +21 -0
- swagger_petstore_open_api_3_1_0/__init__.py +15 -0
- swagger_petstore_open_api_3_1_0/apis/__init__.py +5 -0
- swagger_petstore_open_api_3_1_0/apis/pet_api.py +922 -0
- swagger_petstore_open_api_3_1_0/apis/store.py +432 -0
- swagger_petstore_open_api_3_1_0/apis/user_api.py +829 -0
- swagger_petstore_open_api_3_1_0/async_client.py +87 -0
- swagger_petstore_open_api_3_1_0/auth.py +17 -0
- swagger_petstore_open_api_3_1_0/base_client.py +27 -0
- swagger_petstore_open_api_3_1_0/client.py +85 -0
- swagger_petstore_open_api_3_1_0/core/__init__.py +395 -0
- swagger_petstore_open_api_3_1_0/core/_internal/__init__.py +9 -0
- swagger_petstore_open_api_3_1_0/core/_internal/content_disposition.py +154 -0
- swagger_petstore_open_api_3_1_0/core/_internal/flattening.py +104 -0
- swagger_petstore_open_api_3_1_0/core/_internal/headers.py +243 -0
- swagger_petstore_open_api_3_1_0/core/_internal/redaction.py +208 -0
- swagger_petstore_open_api_3_1_0/core/_internal/sse_parser.py +124 -0
- swagger_petstore_open_api_3_1_0/core/_internal/subscripts.py +50 -0
- swagger_petstore_open_api_3_1_0/core/_internal/urls.py +172 -0
- swagger_petstore_open_api_3_1_0/core/_internal/wire.py +201 -0
- swagger_petstore_open_api_3_1_0/core/adapters.py +177 -0
- swagger_petstore_open_api_3_1_0/core/auth/__init__.py +70 -0
- swagger_petstore_open_api_3_1_0/core/auth/composition.py +175 -0
- swagger_petstore_open_api_3_1_0/core/auth/models.py +216 -0
- swagger_petstore_open_api_3_1_0/core/auth/protocols.py +103 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/__init__.py +94 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/api_key_auth.py +53 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/basic_auth.py +73 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/bearer_auth.py +21 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_authorization_code.py +395 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_client_credentials.py +130 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_password.py +145 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_schemes.py +300 -0
- swagger_petstore_open_api_3_1_0/core/base_raw_response.py +49 -0
- swagger_petstore_open_api_3_1_0/core/bodies.py +590 -0
- swagger_petstore_open_api_3_1_0/core/converters/__init__.py +68 -0
- swagger_petstore_open_api_3_1_0/core/converters/date_time.py +237 -0
- swagger_petstore_open_api_3_1_0/core/converters/encoded_bytes.py +273 -0
- swagger_petstore_open_api_3_1_0/core/converters/open_enum.py +35 -0
- swagger_petstore_open_api_3_1_0/core/decoding.py +498 -0
- swagger_petstore_open_api_3_1_0/core/discriminators.py +129 -0
- swagger_petstore_open_api_3_1_0/core/event_streams.py +225 -0
- swagger_petstore_open_api_3_1_0/core/exceptions.py +78 -0
- swagger_petstore_open_api_3_1_0/core/file_responses.py +337 -0
- swagger_petstore_open_api_3_1_0/core/files.py +174 -0
- swagger_petstore_open_api_3_1_0/core/httpx2_transport.py +748 -0
- swagger_petstore_open_api_3_1_0/core/logging.py +472 -0
- swagger_petstore_open_api_3_1_0/core/models.py +143 -0
- swagger_petstore_open_api_3_1_0/core/optionality.py +144 -0
- swagger_petstore_open_api_3_1_0/core/pagination.py +452 -0
- swagger_petstore_open_api_3_1_0/core/paginator.py +55 -0
- swagger_petstore_open_api_3_1_0/core/params.py +157 -0
- swagger_petstore_open_api_3_1_0/core/raw_client.py +480 -0
- swagger_petstore_open_api_3_1_0/core/request_options.py +172 -0
- swagger_petstore_open_api_3_1_0/core/results.py +178 -0
- swagger_petstore_open_api_3_1_0/core/retries.py +237 -0
- swagger_petstore_open_api_3_1_0/core/runtime_env.py +43 -0
- swagger_petstore_open_api_3_1_0/core/servers.py +33 -0
- swagger_petstore_open_api_3_1_0/core/transport.py +218 -0
- swagger_petstore_open_api_3_1_0/errors/__init__.py +48 -0
- swagger_petstore_open_api_3_1_0/errors/add_pet_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/delete_order_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/delete_pet_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/delete_user_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/find_pets_by_status_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/find_pets_by_tags_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/get_order_by_id_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/get_pet_by_id_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/get_user_by_name_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/login_user_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/place_order_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/update_pet_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/update_pet_with_form_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/update_user_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/upload_file_error.py +21 -0
- swagger_petstore_open_api_3_1_0/models/__init__.py +23 -0
- swagger_petstore_open_api_3_1_0/models/api_response.py +18 -0
- swagger_petstore_open_api_3_1_0/models/category.py +15 -0
- swagger_petstore_open_api_3_1_0/models/enums/__init__.py +4 -0
- swagger_petstore_open_api_3_1_0/models/enums/order_status.py +17 -0
- swagger_petstore_open_api_3_1_0/models/enums/pet_status.py +17 -0
- swagger_petstore_open_api_3_1_0/models/order.py +27 -0
- swagger_petstore_open_api_3_1_0/models/pet.py +28 -0
- swagger_petstore_open_api_3_1_0/models/tag_model.py +15 -0
- swagger_petstore_open_api_3_1_0/models/user.py +29 -0
- swagger_petstore_open_api_3_1_0/py.typed +0 -0
- swagger_petstore_open_api_3_1_0/server/__init__.py +21 -0
- swagger_petstore_open_api_3_1_0/server/server.py +17 -0
- 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
|