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,154 @@
1
+ """``Content-Disposition``, both directions: the filename a response suggests, and the one a
2
+ request carries.
3
+
4
+ One header, one module. The RFC 6266/8187 grammar is the shared asset, so reading it and writing
5
+ it stay together rather than putting one table in two files. ``headers.py`` remains the
6
+ *parameter*-layer module, resolving the outbound layers a caller and the spec contribute; what
7
+ this module renders is a value a body carries, exactly as its media type is.
8
+
9
+ The standard library gets the reading wrong twice, both reproduced during planning
10
+ (docs/plans/file-handling.md, verified facts). ``Message.get_filename()`` returns the plain
11
+ ``filename`` when both forms are present -- RFC 6266 §4.3 requires the extended ``filename*`` to
12
+ win -- and the RFC 2231 machinery percent-decodes the extended value as latin-1 whatever charset
13
+ it declares, so the text must be re-encoded through latin-1 and decoded with the declared charset
14
+ to come back intact.
15
+
16
+ The two readers treat their input as attacker-controlled. The parser is **total**: any malformed
17
+ header yields ``None`` rather than raising, because it runs while a streamed connection is held
18
+ and a raise there would leak it. The sanitiser is the paranoid half: a server-suggested name is
19
+ reduced to a basename, stripped of everything Windows refuses, and refused outright when what
20
+ survives is empty, a reserved device name, or too long to create.
21
+
22
+ The writer needs no such paranoia, and by construction rather than by trust: every byte outside
23
+ RFC 8187's ``attr-char`` is percent-encoded, so no name can splice a header and there is nothing
24
+ to refuse."""
25
+
26
+ from __future__ import annotations
27
+
28
+ import re
29
+ from email.message import Message
30
+ from typing import Final
31
+ from urllib.parse import quote
32
+
33
+ _SEPARATORS: Final = re.compile(r"[\\/]")
34
+ _WINDOWS_ILLEGAL: Final = frozenset('<>:"|?*')
35
+ _WINDOWS_RESERVED: Final = frozenset(
36
+ {"CON", "PRN", "AUX", "NUL"} | {f"COM{d}" for d in "123456789"} | {f"LPT{d}" for d in "123456789"}
37
+ )
38
+
39
+
40
+ def content_disposition_filename(header: str | None) -> str | None:
41
+ """The filename a ``Content-Disposition`` suggests, or ``None``.
42
+
43
+ Prefers the RFC 8187 extended ``filename*`` (RFC 6266 §4.3) and recovers its text from the
44
+ standard library's latin-1 percent-decode; RFC 8187 §3.2.1 permits only UTF-8, so any other
45
+ declared charset falls back to the plain ``filename`` rather than guessing. Continuation
46
+ segments (``filename*0*``, ``filename*1*``) are collapsed by the same machinery.
47
+
48
+ Total by design: a malformed header returns ``None``, never raises -- this runs between a
49
+ streamed response's head arriving and its owner returning, where a raise would leak the
50
+ connection. The result is *not* trusted as a path; see :func:`safe_filename`.
51
+
52
+ Args:
53
+ header: The header's value, or ``None`` when the response carried none.
54
+
55
+ Returns:
56
+ The suggested filename, or ``None`` when there is no usable one."""
57
+ if header is None:
58
+ return None
59
+ try:
60
+ message = Message()
61
+ message["content-disposition"] = header
62
+ params = message.get_params(header="content-disposition")
63
+ if params is None:
64
+ return None
65
+
66
+ plain: str | None = None
67
+ extended: str | None = None
68
+ for name, raw in params:
69
+ if name != "filename":
70
+ continue
71
+ # The parser hands back a plain parameter as ``str`` and an RFC 2231 extended one as a
72
+ # ``(charset, language, text)`` tuple. A duplicated plain form keeps its first
73
+ # occurrence; duplicated extended forms never reach here -- the stdlib merges them as
74
+ # continuations before ``get_params`` returns.
75
+ value: object = raw
76
+ if isinstance(value, str) and plain is None:
77
+ plain = value
78
+ elif isinstance(value, tuple) and extended is None:
79
+ extended = _decode_extended(value)
80
+ return (extended or plain) or None
81
+ except Exception:
82
+ # Total by design -- the docstring says why. Bad wire data degrades, never raises.
83
+ return None
84
+
85
+
86
+ def _decode_extended(value: tuple[object, ...]) -> str | None:
87
+ # (charset, language, text) -- text percent-decoded as latin-1 by the parser regardless of the
88
+ # declared charset, which is the mojibake the round trip below undoes.
89
+ if len(value) != 3:
90
+ return None
91
+ charset, _language, text = value
92
+ if not isinstance(charset, str) or not isinstance(text, str):
93
+ return None
94
+ if charset.lower() not in ("utf-8", "utf8"):
95
+ return None
96
+ try:
97
+ return text.encode("latin-1").decode("utf-8")
98
+ except (UnicodeEncodeError, UnicodeDecodeError):
99
+ return None
100
+
101
+
102
+ def render_content_disposition(filename: str) -> str:
103
+ """The ``Content-Disposition`` value naming a raw request body.
104
+
105
+ RFC 6266's dual form: a quoted ``filename`` holding what a quoted-string can, and, whenever
106
+ that reduction lost something, the RFC 8187 ``filename*`` that did not. A conformant receiver
107
+ prefers the extended parameter (§4.3) and recovers the name exactly; one reading only the plain
108
+ parameter still gets a usable name.
109
+
110
+ Percent-encoding is what makes this safe rather than a character check: every byte outside
111
+ RFC 8187's ``attr-char`` becomes ``%XX``, so a CR, an LF or a ``"`` cannot reach the header
112
+ however the caller spelled the name. There is nothing to refuse.
113
+
114
+ Args:
115
+ filename: The name the caller gave the file, as ``NamedFile.filename`` holds it.
116
+
117
+ Returns:
118
+ The header value -- ``attachment``, and whichever parameters can carry the name."""
119
+ plain = "".join(c for c in filename if c.isascii() and c.isprintable() and c not in '"\\')
120
+ parameters = [f'filename="{plain}"'] if plain else []
121
+ if plain != filename:
122
+ parameters.append(f"filename*=UTF-8''{quote(filename, safe='', encoding='utf-8')}")
123
+ return "; ".join(["attachment", *parameters])
124
+
125
+
126
+ def safe_filename(filename: str) -> str:
127
+ """Reduce a server-supplied filename to a name safe to create in a directory.
128
+
129
+ Basename first, under both separator flavours -- ``Content-Disposition`` is
130
+ attacker-controlled and ``filename="../../etc/passwd"`` reaches the parser unchanged -- then
131
+ strip what Windows refuses: ``<>:"|?*``, unprintable characters, and trailing dots or spaces.
132
+ Dropping the colon also neutralises an NTFS alternate-data-stream name. What remains is
133
+ refused rather than mangled when it cannot safely name a file: a reserved device-name stem
134
+ (``CON``, ``NUL``, ``COM1``... -- Windows 10 resolves even ``CON.pdf`` to the console device,
135
+ and a bare ``NUL`` open "succeeds" while the bytes go to the null device) or a name past 255
136
+ characters, the per-component ceiling on NTFS and most POSIX filesystems.
137
+
138
+ Args:
139
+ filename: The server's suggestion, as :func:`content_disposition_filename` returned it.
140
+
141
+ Returns:
142
+ The reduced name, never empty.
143
+
144
+ Raises:
145
+ ValueError: If nothing safe remains -- an empty reduction, ``.`` or ``..``, a reserved
146
+ device name, or a name too long to create."""
147
+ basename = _SEPARATORS.split(filename)[-1]
148
+ cleaned = "".join(ch for ch in basename if ch not in _WINDOWS_ILLEGAL and ch.isprintable())
149
+ cleaned = cleaned.rstrip(". ")
150
+ # A reserved stem is matched as Windows does: up to the first dot, trailing spaces ignored.
151
+ stem = cleaned.split(".", 1)[0].rstrip(" ")
152
+ if not cleaned or stem.upper() in _WINDOWS_RESERVED or len(cleaned) > 255:
153
+ raise ValueError(f"{filename!r} reduces to nothing safe to create -- pass an explicit file path instead")
154
+ return cleaned
@@ -0,0 +1,104 @@
1
+ """Flattening parameters into wire pairs.
2
+
3
+ A parameter's value can be a scalar, a model, a mapping, or a collection of any of those, and how a
4
+ *collection* explodes into keys depends on the parameter's :class:`SerializationFormat`. This module
5
+ does that flattening once; both destinations that carry pairs -- a query string and a form body --
6
+ then reach the wire through ``urls.urlencode_params``.
7
+
8
+ Each parameter arrives carrying the adapter for its declared type, so the wire *format* is decided
9
+ there rather than here: :func:`wire_value` dumps through that adapter -- the same step the path and
10
+ header renderers use -- and this module is then concerned only with how the result explodes into
11
+ keys.
12
+
13
+ Stateless module functions rather than a class: there is nothing to configure, so there was nothing
14
+ for an instance to hold."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Mapping, Sequence
19
+ from typing import Any, Final, TypeAlias
20
+
21
+ from ..params import Param, SerializationFormat
22
+ from .wire import is_collection, to_text, wire_value
23
+
24
+ Pairs: TypeAlias = list[tuple[str, str]]
25
+
26
+ _SEPARATORS: Final = {
27
+ SerializationFormat.CSV: ",",
28
+ SerializationFormat.TSV: "\t",
29
+ SerializationFormat.PSV: "|",
30
+ }
31
+
32
+
33
+ def flatten(params: Sequence[Param[Any]] | None) -> Pairs:
34
+ """Flatten every parameter into ``(key, value)`` pairs, in order.
35
+
36
+ Args:
37
+ params: The parameters to flatten; ``None`` is treated as none at all.
38
+
39
+ Returns:
40
+ ``(key, text)`` pairs, a repeated key appearing once per value.
41
+
42
+ Raises:
43
+ ValueError: If a parameter's value does not match its declared type."""
44
+ pairs: Pairs = []
45
+ for param in params or ():
46
+ pairs.extend(_flatten_value(key=param.key, value=wire_value(param), fmt=param.serialization_format))
47
+ return pairs
48
+
49
+
50
+ def _flatten_value(*, key: str, value: object, fmt: SerializationFormat) -> Pairs:
51
+ if value is None:
52
+ return []
53
+
54
+ if isinstance(value, Mapping):
55
+ return [
56
+ pair
57
+ for child_key, child_value in value.items()
58
+ for pair in _flatten_value(key=f"{key}[{child_key}]", value=child_value, fmt=fmt)
59
+ ]
60
+
61
+ if is_collection(value):
62
+ values = [v for v in value if v is not None]
63
+ if not values:
64
+ return []
65
+
66
+ joined = _try_join(values, fmt)
67
+ if joined is not None:
68
+ return [(key, joined)]
69
+
70
+ if fmt is SerializationFormat.INDEXED:
71
+ return [
72
+ pair
73
+ for index, item in enumerate(values)
74
+ for pair in _flatten_value(key=f"{key}[{index}]", value=item, fmt=fmt)
75
+ ]
76
+
77
+ if fmt is SerializationFormat.UNINDEXED:
78
+ return [pair for item in values for pair in _flatten_value(key=f"{key}[]", value=item, fmt=fmt)]
79
+
80
+ return [pair for item in values for pair in _flatten_value(key=key, value=item, fmt=fmt)]
81
+
82
+ return [(key, to_text(value))]
83
+
84
+
85
+ def _try_join(values: Sequence[object], fmt: SerializationFormat) -> str | None:
86
+ """Join ``values`` with ``fmt``'s separator, or ``None`` if ``fmt`` does not join.
87
+
88
+ Args:
89
+ values: The already-dumped members to join.
90
+ fmt: The parameter's serialization format.
91
+
92
+ Returns:
93
+ The joined text, or ``None`` when ``fmt`` does not join or a member is itself
94
+ structured -- both cases leave the caller to explode the members instead."""
95
+ separator = _SEPARATORS.get(fmt)
96
+ if separator is None:
97
+ return None
98
+
99
+ # A nested collection or mapping cannot be represented inside a joined scalar, so the
100
+ # caller falls back to exploding the members under the repeated bare key.
101
+ if any(isinstance(v, Mapping) or is_collection(v) for v in values):
102
+ return None
103
+
104
+ return separator.join(to_text(v) for v in values)
@@ -0,0 +1,243 @@
1
+ """Rendering, normalizing and resolving request headers, plus reading response headers.
2
+
3
+ One place decides how header names reach the wire, so the choice is made once rather than at each
4
+ endpoint. Names are lowercased: HTTP/1.1 treats them case-insensitively and HTTP/2 requires
5
+ lowercase, so lowercasing is correct for both and makes the produced mapping comparable.
6
+
7
+ Lowercasing is also what makes :func:`resolve_headers` case-insensitive by construction: once every
8
+ layer is normalized, an ordinary dict merge cannot let a differently-cased duplicate through.
9
+
10
+ The layers are not all the same kind, and that asymmetry is deliberate. The API's and the endpoint's
11
+ headers are :class:`Param`s -- spec-derived, so each names the type it was declared as and is
12
+ rendered through it by :func:`render_headers`. A caller's ``extra_headers`` arrives as a plain
13
+ mapping of strings, because a caller-supplied value carries no declared type for an adapter to act
14
+ on.
15
+
16
+ ``Cookie`` is the one field this module knows by name. Every other field takes the later layer and
17
+ discards the earlier; RFC 6265 §5.4 permits at most one ``Cookie``, so its contributors fold into a
18
+ single jar instead."""
19
+
20
+ from __future__ import annotations
21
+
22
+ from collections.abc import Mapping, Sequence
23
+ from typing import Any, Final, TypeVar
24
+
25
+ from pydantic import ValidationError
26
+ from typing_extensions import TypeForm
27
+
28
+ from ..adapters import adapter_for, validation_target
29
+ from ..params import Param
30
+ from .wire import text_values
31
+
32
+ T = TypeVar("T")
33
+
34
+ COOKIE_HEADER: Final = "cookie"
35
+ """The one field name this module knows, lowercased as every rendered name is.
36
+
37
+ RFC 6265 §5.4 permits a user agent at most one ``Cookie`` field, so two contributors of a cookie are
38
+ two entries in one jar rather than two claims on one slot -- the only field where a later layer folds
39
+ into an earlier one instead of replacing it."""
40
+
41
+
42
+ def fold_cookie_jars(*jars: str | None) -> str:
43
+ """Join what each layer contributed to the cookie jar, skipping those that contributed nothing.
44
+
45
+ Args:
46
+ jars: Each layer's ``Cookie`` field value, in precedence order.
47
+
48
+ Returns:
49
+ One RFC 6265 §5.4 field value, empty when no layer contributed a cookie."""
50
+ return "; ".join(jar for jar in jars if jar)
51
+
52
+
53
+ def render_headers(headers: Sequence[Param[Any]] | None) -> dict[str, str]:
54
+ """Render each header parameter into a lowercased name and one field value.
55
+
56
+ Rendering and lowercasing in one pass is what keeps :func:`resolve_headers` an ordinary dict
57
+ merge; within one layer the later of two same-named headers wins, which falls out of the dict.
58
+ ``Cookie`` is the exception: its contributors fold into one jar, so an endpoint's own cookie and
59
+ the credential a scheme appended after it survive each other.
60
+
61
+ A header contributing nothing -- a ``None`` value, or a collection with no members -- is omitted
62
+ rather than sent empty, so a lower layer's value survives. Several values fold into one
63
+ comma-separated field value: RFC 9110's own rule for a repeated header, and OpenAPI's default
64
+ ``style: simple`` for a header array, with no space after the comma. Folding here is also what
65
+ keeps ``HttpRequest.headers`` a plain ``Mapping[str, str]``, so no transport has to learn that a
66
+ header can be multi-valued.
67
+
68
+ Args:
69
+ headers: The header parameters; ``None`` is treated as none at all.
70
+
71
+ Returns:
72
+ Lowercased header names mapped to one field value each; a header contributing nothing is
73
+ absent.
74
+
75
+ Raises:
76
+ ValueError: If a header's value does not match its declared type, or a flat collection
77
+ holds a ``None``."""
78
+ rendered: dict[str, str] = {}
79
+ for header in headers or ():
80
+ values = text_values(header)
81
+ if values:
82
+ name = header.key.lower()
83
+ field_value = ",".join(values)
84
+ if name == COOKIE_HEADER:
85
+ field_value = fold_cookie_jars(rendered.get(name), field_value)
86
+ rendered[name] = field_value
87
+ return rendered
88
+
89
+
90
+ def normalize_headers(headers: Mapping[str, str] | None) -> dict[str, str]:
91
+ """Return already-string headers as a plain dict with lowercased names.
92
+
93
+ One layer reaches :func:`resolve_headers` this way and only one: the caller's ``extra_headers``.
94
+ Every spec-derived layer arrives as a :class:`Param` and goes through :func:`render_headers`.
95
+
96
+ Args:
97
+ headers: Already-string headers, typically a caller's ``extra_headers``.
98
+
99
+ Returns:
100
+ The same mapping with every name lowercased."""
101
+ return {key.lower(): value for key, value in (headers or {}).items()}
102
+
103
+
104
+ def resolve_headers(
105
+ global_headers: Sequence[Param[Any]],
106
+ local_headers: Sequence[Param[Any]] | None,
107
+ *,
108
+ extra_headers: Mapping[str, str] | None = None,
109
+ ) -> dict[str, str]:
110
+ """Return the headers for one call: the API's, then the endpoint's, then the caller's.
111
+
112
+ Every layer is normalized before merging, so a global ``X-Tenant`` is overridden by an
113
+ endpoint's ``x-tenant`` rather than reaching the wire alongside it -- and a caller's
114
+ ``extra_headers`` wins over both. That it can override a header the endpoint set is the point of
115
+ passing it, not an oversight.
116
+
117
+ ``Cookie`` is the one field a later layer does not replace. RFC 6265 §5.4 permits at most one of
118
+ them, so the layers' cookies are several entries in a single jar rather than competing claims on
119
+ one slot: a caller adding a cookie adds to the credential a scheme contributed instead of
120
+ silently discarding it.
121
+
122
+ ``extra_headers`` is keyword-only. It no longer shares a type with the two layers below it, so
123
+ an inversion would now be caught -- but it stays keyword-only regardless: the two spec-derived
124
+ layers are still same-typed and positional, and a caller layer reading as a third peer would
125
+ misrepresent it as something the API prescribes.
126
+
127
+ Args:
128
+ global_headers: What the API prescribes for every call.
129
+ local_headers: What this one endpoint declares, plus whatever its scheme contributed; wins
130
+ over the API's.
131
+ extra_headers: What the caller passed for this one request; wins over both.
132
+
133
+ Returns:
134
+ The effective headers, names lowercased so the merge is case-insensitive.
135
+
136
+ Raises:
137
+ ValueError: If a header's value does not match its declared type."""
138
+ normalized_global_headers = render_headers(global_headers)
139
+ normalized_local_headers = render_headers(local_headers)
140
+ normalized_extra_headers = normalize_headers(extra_headers)
141
+
142
+ resolved = normalized_global_headers | normalized_local_headers | normalized_extra_headers
143
+ jar = fold_cookie_jars(
144
+ normalized_global_headers.get(COOKIE_HEADER),
145
+ normalized_local_headers.get(COOKIE_HEADER),
146
+ normalized_extra_headers.get(COOKIE_HEADER),
147
+ )
148
+ if jar:
149
+ resolved[COOKIE_HEADER] = jar
150
+ return resolved
151
+
152
+
153
+ def read_header(headers: Mapping[str, str], name: str, declared: TypeForm[T]) -> T | None:
154
+ """Read one response header as its declared type. Paging selectors that read a header use this.
155
+
156
+ The value is validated in lax mode, so ``read_header(headers, name, bool)`` accepts ``1``, ``0``,
157
+ ``true`` and ``false`` in any case.
158
+
159
+ Args:
160
+ headers: The response headers. Names may be in any case.
161
+ name: The header to read, matched case-insensitively.
162
+ declared: The type to validate the trimmed value into.
163
+
164
+ Returns:
165
+ The validated value. ``None`` if the header is missing, blank or fails validation, so a bad header
166
+ from the server never raises in the middle of a walk."""
167
+ text = find_header(headers, name)
168
+ if text is None:
169
+ return None
170
+ try:
171
+ return adapter_for(validation_target(declared)).validate_python(text)
172
+ except ValidationError:
173
+ return None
174
+
175
+
176
+ def find_link_target(headers: Mapping[str, str], *, header_name: str, rel: str) -> str | None:
177
+ """Return the target of the first entry of an RFC 8288 link header whose ``rel`` includes ``rel``.
178
+
179
+ The header is the one the operation's pointer names, ``Link`` when the API follows RFC 8288 to the letter.
180
+ Entries are split on commas outside quotes, and parameters on semicolons outside quotes. ``rel`` is
181
+ matched case-insensitively against each entry's space-separated ``rel`` list. Backslash escapes inside
182
+ quotes are not unescaped. ``header_name`` and ``rel`` are keyword-only: both are strings, so a swap would
183
+ otherwise type-check.
184
+
185
+ Args:
186
+ headers: The response headers. Names may be in any case.
187
+ header_name: The header to read, matched case-insensitively.
188
+ rel: The relation to find, such as ``"next"``.
189
+
190
+ Returns:
191
+ The target as written. ``None`` if the header is missing or blank, no entry has that ``rel``, or a
192
+ ``<`` has no closing ``>``."""
193
+ value = find_header(headers, header_name)
194
+ if value is None:
195
+ return None
196
+ index = 0
197
+ while (index := value.find("<", index)) >= 0:
198
+ target_end = value.find(">", index + 1)
199
+ if target_end < 0:
200
+ return None
201
+ target = value[index + 1 : target_end]
202
+ parameters_end = _find_unquoted(value, ",", target_end + 1)
203
+ if _rel_matches(value[target_end + 1 : parameters_end], rel):
204
+ return target
205
+ index = parameters_end
206
+ return None
207
+
208
+
209
+ def find_header(headers: Mapping[str, str], name: str) -> str | None:
210
+ """Return the trimmed value of the first header named ``name``, matched case-insensitively.
211
+
212
+ A blank value counts as missing and returns ``None``. Used by :func:`read_header`,
213
+ :func:`find_link_target` and ``retries.retry_after_seconds``."""
214
+ value = next(
215
+ (field_value for field_name, field_value in headers.items() if field_name.lower() == name.lower()), None
216
+ )
217
+ if value is None:
218
+ return None
219
+ text = value.strip()
220
+ return text if text else None
221
+
222
+
223
+ def _find_unquoted(text: str, delimiter: str, start: int) -> int:
224
+ in_quotes = False
225
+ for i in range(start, len(text)):
226
+ if text[i] == '"':
227
+ in_quotes = not in_quotes
228
+ elif text[i] == delimiter and not in_quotes:
229
+ return i
230
+ return len(text)
231
+
232
+
233
+ def _rel_matches(parameters: str, rel: str) -> bool:
234
+ start = 0
235
+ while start < len(parameters):
236
+ end = _find_unquoted(parameters, ";", start)
237
+ parameter = parameters[start:end]
238
+ start = end + 1
239
+ name, separator, value = parameter.partition("=")
240
+ if not separator or name.strip().lower() != "rel":
241
+ continue
242
+ return any(rel_type.lower() == rel.lower() for rel_type in value.strip().strip('"').split(" "))
243
+ return False