cliV1 0.1.1__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 (83) hide show
  1. cliv1-0.1.1.dist-info/METADATA +156 -0
  2. cliv1-0.1.1.dist-info/RECORD +83 -0
  3. cliv1-0.1.1.dist-info/WHEEL +4 -0
  4. cliv1-0.1.1.dist-info/licenses/LICENSE +21 -0
  5. swagger_petstore_open_api_3_0/__init__.py +15 -0
  6. swagger_petstore_open_api_3_0/apis/__init__.py +5 -0
  7. swagger_petstore_open_api_3_0/apis/pet_api.py +881 -0
  8. swagger_petstore_open_api_3_0/apis/store.py +411 -0
  9. swagger_petstore_open_api_3_0/apis/user_api.py +792 -0
  10. swagger_petstore_open_api_3_0/async_client.py +79 -0
  11. swagger_petstore_open_api_3_0/auth.py +17 -0
  12. swagger_petstore_open_api_3_0/base_client.py +18 -0
  13. swagger_petstore_open_api_3_0/client.py +77 -0
  14. swagger_petstore_open_api_3_0/core/__init__.py +342 -0
  15. swagger_petstore_open_api_3_0/core/_internal/__init__.py +7 -0
  16. swagger_petstore_open_api_3_0/core/_internal/content_disposition.py +154 -0
  17. swagger_petstore_open_api_3_0/core/_internal/flattening.py +139 -0
  18. swagger_petstore_open_api_3_0/core/_internal/headers.py +144 -0
  19. swagger_petstore_open_api_3_0/core/_internal/urls.py +166 -0
  20. swagger_petstore_open_api_3_0/core/_internal/wire.py +173 -0
  21. swagger_petstore_open_api_3_0/core/adapters.py +177 -0
  22. swagger_petstore_open_api_3_0/core/auth/__init__.py +70 -0
  23. swagger_petstore_open_api_3_0/core/auth/composition.py +175 -0
  24. swagger_petstore_open_api_3_0/core/auth/models.py +217 -0
  25. swagger_petstore_open_api_3_0/core/auth/protocols.py +103 -0
  26. swagger_petstore_open_api_3_0/core/auth/schemes/__init__.py +94 -0
  27. swagger_petstore_open_api_3_0/core/auth/schemes/api_key_auth.py +53 -0
  28. swagger_petstore_open_api_3_0/core/auth/schemes/basic_auth.py +73 -0
  29. swagger_petstore_open_api_3_0/core/auth/schemes/bearer_auth.py +21 -0
  30. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_authorization_code.py +393 -0
  31. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_client_credentials.py +127 -0
  32. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_password.py +142 -0
  33. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_schemes.py +300 -0
  34. swagger_petstore_open_api_3_0/core/base_raw_response.py +49 -0
  35. swagger_petstore_open_api_3_0/core/bodies.py +563 -0
  36. swagger_petstore_open_api_3_0/core/converters/__init__.py +68 -0
  37. swagger_petstore_open_api_3_0/core/converters/date_time.py +237 -0
  38. swagger_petstore_open_api_3_0/core/converters/encoded_bytes.py +271 -0
  39. swagger_petstore_open_api_3_0/core/converters/open_enum.py +35 -0
  40. swagger_petstore_open_api_3_0/core/decoding.py +263 -0
  41. swagger_petstore_open_api_3_0/core/exceptions.py +41 -0
  42. swagger_petstore_open_api_3_0/core/file_responses.py +324 -0
  43. swagger_petstore_open_api_3_0/core/files.py +173 -0
  44. swagger_petstore_open_api_3_0/core/httpx_transport.py +766 -0
  45. swagger_petstore_open_api_3_0/core/models.py +143 -0
  46. swagger_petstore_open_api_3_0/core/optionality.py +130 -0
  47. swagger_petstore_open_api_3_0/core/params.py +161 -0
  48. swagger_petstore_open_api_3_0/core/raw_client.py +388 -0
  49. swagger_petstore_open_api_3_0/core/request_options.py +94 -0
  50. swagger_petstore_open_api_3_0/core/results.py +110 -0
  51. swagger_petstore_open_api_3_0/core/runtime_env.py +43 -0
  52. swagger_petstore_open_api_3_0/core/servers.py +33 -0
  53. swagger_petstore_open_api_3_0/core/transport.py +248 -0
  54. swagger_petstore_open_api_3_0/errors/__init__.py +48 -0
  55. swagger_petstore_open_api_3_0/errors/add_pet_error.py +21 -0
  56. swagger_petstore_open_api_3_0/errors/delete_order_error.py +21 -0
  57. swagger_petstore_open_api_3_0/errors/delete_pet_error.py +21 -0
  58. swagger_petstore_open_api_3_0/errors/delete_user_error.py +21 -0
  59. swagger_petstore_open_api_3_0/errors/find_pets_by_status_error.py +21 -0
  60. swagger_petstore_open_api_3_0/errors/find_pets_by_tags_error.py +21 -0
  61. swagger_petstore_open_api_3_0/errors/get_order_by_id_error.py +21 -0
  62. swagger_petstore_open_api_3_0/errors/get_pet_by_id_error.py +21 -0
  63. swagger_petstore_open_api_3_0/errors/get_user_by_name_error.py +21 -0
  64. swagger_petstore_open_api_3_0/errors/login_user_error.py +21 -0
  65. swagger_petstore_open_api_3_0/errors/place_order_error.py +21 -0
  66. swagger_petstore_open_api_3_0/errors/update_pet_error.py +21 -0
  67. swagger_petstore_open_api_3_0/errors/update_pet_with_form_error.py +21 -0
  68. swagger_petstore_open_api_3_0/errors/update_user_error.py +21 -0
  69. swagger_petstore_open_api_3_0/errors/upload_file_error.py +21 -0
  70. swagger_petstore_open_api_3_0/models/__init__.py +23 -0
  71. swagger_petstore_open_api_3_0/models/api_response.py +18 -0
  72. swagger_petstore_open_api_3_0/models/category.py +15 -0
  73. swagger_petstore_open_api_3_0/models/enums/__init__.py +4 -0
  74. swagger_petstore_open_api_3_0/models/enums/order_status.py +17 -0
  75. swagger_petstore_open_api_3_0/models/enums/pet_status.py +17 -0
  76. swagger_petstore_open_api_3_0/models/order.py +27 -0
  77. swagger_petstore_open_api_3_0/models/pet.py +28 -0
  78. swagger_petstore_open_api_3_0/models/tag.py +15 -0
  79. swagger_petstore_open_api_3_0/models/user.py +29 -0
  80. swagger_petstore_open_api_3_0/py.typed +0 -0
  81. swagger_petstore_open_api_3_0/server/__init__.py +21 -0
  82. swagger_petstore_open_api_3_0/server/server.py +17 -0
  83. swagger_petstore_open_api_3_0/server/server_config.py +52 -0
@@ -0,0 +1,139 @@
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, for both destinations that need it: query strings
6
+ (``urls.encode_query`` encodes the pairs) and form bodies
7
+ (:func:`to_fields` collects them into a mapping).
8
+
9
+ Each parameter arrives carrying the adapter for its declared type, so the wire *format* is decided
10
+ there rather than here: :func:`wire_value` dumps through that adapter -- the same step the path and
11
+ header renderers use -- and this module is then concerned only with how the result explodes into
12
+ keys.
13
+
14
+ Stateless module functions rather than a class: there is nothing to configure, so there was nothing
15
+ for an instance to hold."""
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Mapping, Sequence
20
+ from typing import Any, Final, TypeAlias
21
+
22
+ from ..params import Param, SerializationFormat
23
+ from .wire import is_collection, to_text, wire_value
24
+
25
+ Pairs: TypeAlias = list[tuple[str, str]]
26
+
27
+ _SEPARATORS: Final = {
28
+ SerializationFormat.CSV: ",",
29
+ SerializationFormat.TSV: "\t",
30
+ SerializationFormat.PSV: "|",
31
+ }
32
+
33
+
34
+ def flatten(params: Sequence[Param[Any]] | None) -> Pairs:
35
+ """Flatten every parameter into ``(key, value)`` pairs, in order.
36
+
37
+ Args:
38
+ params: The parameters to flatten; ``None`` is treated as none at all.
39
+
40
+ Returns:
41
+ ``(key, text)`` pairs, a repeated key appearing once per value.
42
+
43
+ Raises:
44
+ ValueError: If a parameter's value does not match its declared type."""
45
+ pairs: Pairs = []
46
+ for param in params or ():
47
+ pairs.extend(_flatten_value(key=param.key, value=wire_value(param), fmt=param.serialization_format))
48
+ return pairs
49
+
50
+
51
+ def to_fields(params: Sequence[Param[Any]]) -> dict[str, str | list[str]]:
52
+ """Flatten ``params`` into a mapping, collecting repeated keys into a list.
53
+
54
+ This is the shape a form body takes: unlike a query string, which can repeat a key, a form
55
+ mapping needs the repeats gathered under one entry.
56
+
57
+ Every value is text or a list of text, never anything richer: :func:`flatten` has already
58
+ reduced each parameter to ``(key, value)`` pairs of ``str``, so the only structure left to
59
+ add here is the list a repeated key collects into.
60
+
61
+ Args:
62
+ params: The parameters to flatten into fields.
63
+
64
+ Returns:
65
+ Each key mapped to its text, or to the list of texts it collected.
66
+
67
+ Raises:
68
+ ValueError: If a parameter's value does not match its declared type."""
69
+ fields: dict[str, str | list[str]] = {}
70
+
71
+ for key, value in flatten(params):
72
+ if key not in fields:
73
+ fields[key] = value
74
+ continue
75
+
76
+ existing = fields[key]
77
+ if isinstance(existing, list):
78
+ existing.append(value)
79
+ else:
80
+ fields[key] = [existing, value]
81
+
82
+ return fields
83
+
84
+
85
+ def _flatten_value(*, key: str, value: object, fmt: SerializationFormat) -> Pairs:
86
+ if value is None:
87
+ return []
88
+
89
+ if isinstance(value, Mapping):
90
+ return [
91
+ pair
92
+ for child_key, child_value in value.items()
93
+ for pair in _flatten_value(key=f"{key}[{child_key}]", value=child_value, fmt=fmt)
94
+ ]
95
+
96
+ if is_collection(value):
97
+ values = [v for v in value if v is not None]
98
+ if not values:
99
+ return []
100
+
101
+ joined = _try_join(values, fmt)
102
+ if joined is not None:
103
+ return [(key, joined)]
104
+
105
+ if fmt is SerializationFormat.INDEXED:
106
+ return [
107
+ pair
108
+ for index, item in enumerate(values)
109
+ for pair in _flatten_value(key=f"{key}[{index}]", value=item, fmt=fmt)
110
+ ]
111
+
112
+ if fmt is SerializationFormat.UNINDEXED:
113
+ return [pair for item in values for pair in _flatten_value(key=f"{key}[]", value=item, fmt=fmt)]
114
+
115
+ return [pair for item in values for pair in _flatten_value(key=key, value=item, fmt=fmt)]
116
+
117
+ return [(key, to_text(value))]
118
+
119
+
120
+ def _try_join(values: Sequence[object], fmt: SerializationFormat) -> str | None:
121
+ """Join ``values`` with ``fmt``'s separator, or ``None`` if ``fmt`` does not join.
122
+
123
+ Args:
124
+ values: The already-dumped members to join.
125
+ fmt: The parameter's serialization format.
126
+
127
+ Returns:
128
+ The joined text, or ``None`` when ``fmt`` does not join or a member is itself
129
+ structured -- both cases leave the caller to explode the members instead."""
130
+ separator = _SEPARATORS.get(fmt)
131
+ if separator is None:
132
+ return None
133
+
134
+ # A nested collection or mapping cannot be represented inside a joined scalar, so the
135
+ # caller falls back to exploding the members under the repeated bare key.
136
+ if any(isinstance(v, Mapping) or is_collection(v) for v in values):
137
+ return None
138
+
139
+ return separator.join(to_text(v) for v in values)
@@ -0,0 +1,144 @@
1
+ """Rendering, normalizing and resolving request 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
24
+
25
+ from ..params import Param
26
+ from .wire import text_values
27
+
28
+ COOKIE_HEADER: Final = "cookie"
29
+ """The one field name this module knows, lowercased as every rendered name is.
30
+
31
+ RFC 6265 §5.4 permits a user agent at most one ``Cookie`` field, so two contributors of a cookie are
32
+ two entries in one jar rather than two claims on one slot -- the only field where a later layer folds
33
+ into an earlier one instead of replacing it."""
34
+
35
+
36
+ def fold_cookie_jars(*jars: str | None) -> str:
37
+ """Join what each layer contributed to the cookie jar, skipping those that contributed nothing.
38
+
39
+ Args:
40
+ jars: Each layer's ``Cookie`` field value, in precedence order.
41
+
42
+ Returns:
43
+ One RFC 6265 §5.4 field value, empty when no layer contributed a cookie."""
44
+ return "; ".join(jar for jar in jars if jar)
45
+
46
+
47
+ def render_headers(headers: Sequence[Param[Any]] | None) -> dict[str, str]:
48
+ """Render each header parameter into a lowercased name and one field value.
49
+
50
+ Rendering and lowercasing in one pass is what keeps :func:`resolve_headers` an ordinary dict
51
+ merge; within one layer the later of two same-named headers wins, which falls out of the dict.
52
+ ``Cookie`` is the exception: its contributors fold into one jar, so an endpoint's own cookie and
53
+ the credential a scheme appended after it survive each other.
54
+
55
+ A header contributing nothing -- a ``None`` value, or a collection with no members -- is omitted
56
+ rather than sent empty, so a lower layer's value survives. Several values fold into one
57
+ comma-separated field value: RFC 9110's own rule for a repeated header, and OpenAPI's default
58
+ ``style: simple`` for a header array, with no space after the comma. Folding here is also what
59
+ keeps ``HttpRequest.headers`` a plain ``Mapping[str, str]``, so no transport has to learn that a
60
+ header can be multi-valued.
61
+
62
+ Args:
63
+ headers: The header parameters; ``None`` is treated as none at all.
64
+
65
+ Returns:
66
+ Lowercased header names mapped to one field value each; a header contributing nothing is
67
+ absent.
68
+
69
+ Raises:
70
+ ValueError: If a header's value does not match its declared type, or a flat collection
71
+ holds a ``None``."""
72
+ rendered: dict[str, str] = {}
73
+ for header in headers or ():
74
+ values = text_values(header)
75
+ if values:
76
+ name = header.key.lower()
77
+ field_value = ",".join(values)
78
+ if name == COOKIE_HEADER:
79
+ field_value = fold_cookie_jars(rendered.get(name), field_value)
80
+ rendered[name] = field_value
81
+ return rendered
82
+
83
+
84
+ def normalize_headers(headers: Mapping[str, str] | None) -> dict[str, str]:
85
+ """Return already-string headers as a plain dict with lowercased names.
86
+
87
+ One layer reaches :func:`resolve_headers` this way and only one: the caller's ``extra_headers``.
88
+ Every spec-derived layer arrives as a :class:`Param` and goes through :func:`render_headers`.
89
+
90
+ Args:
91
+ headers: Already-string headers, typically a caller's ``extra_headers``.
92
+
93
+ Returns:
94
+ The same mapping with every name lowercased."""
95
+ return {key.lower(): value for key, value in (headers or {}).items()}
96
+
97
+
98
+ def resolve_headers(
99
+ global_headers: Sequence[Param[Any]],
100
+ local_headers: Sequence[Param[Any]] | None,
101
+ *,
102
+ extra_headers: Mapping[str, str] | None = None,
103
+ ) -> dict[str, str]:
104
+ """Return the headers for one call: the API's, then the endpoint's, then the caller's.
105
+
106
+ Every layer is normalized before merging, so a global ``X-Tenant`` is overridden by an
107
+ endpoint's ``x-tenant`` rather than reaching the wire alongside it -- and a caller's
108
+ ``extra_headers`` wins over both. That it can override a header the endpoint set is the point of
109
+ passing it, not an oversight.
110
+
111
+ ``Cookie`` is the one field a later layer does not replace. RFC 6265 §5.4 permits at most one of
112
+ them, so the layers' cookies are several entries in a single jar rather than competing claims on
113
+ one slot: a caller adding a cookie adds to the credential a scheme contributed instead of
114
+ silently discarding it.
115
+
116
+ ``extra_headers`` is keyword-only. It no longer shares a type with the two layers below it, so
117
+ an inversion would now be caught -- but it stays keyword-only regardless: the two spec-derived
118
+ layers are still same-typed and positional, and a caller layer reading as a third peer would
119
+ misrepresent it as something the API prescribes.
120
+
121
+ Args:
122
+ global_headers: What the API prescribes for every call.
123
+ local_headers: What this one endpoint declares, plus whatever its scheme contributed; wins
124
+ over the API's.
125
+ extra_headers: What the caller passed for this one request; wins over both.
126
+
127
+ Returns:
128
+ The effective headers, names lowercased so the merge is case-insensitive.
129
+
130
+ Raises:
131
+ ValueError: If a header's value does not match its declared type."""
132
+ normalized_global_headers = render_headers(global_headers)
133
+ normalized_local_headers = render_headers(local_headers)
134
+ normalized_extra_headers = normalize_headers(extra_headers)
135
+
136
+ resolved = normalized_global_headers | normalized_local_headers | normalized_extra_headers
137
+ jar = fold_cookie_jars(
138
+ normalized_global_headers.get(COOKIE_HEADER),
139
+ normalized_local_headers.get(COOKIE_HEADER),
140
+ normalized_extra_headers.get(COOKIE_HEADER),
141
+ )
142
+ if jar:
143
+ resolved[COOKIE_HEADER] = jar
144
+ return resolved
@@ -0,0 +1,166 @@
1
+ """Turning a :class:`UrlTemplate` plus parameters into a request URL.
2
+
3
+ Five pure functions in three 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 five 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 encode_query(params: Sequence[Param[Any]] | None) -> str:
96
+ """Flatten ``params`` and percent-encode them into a query string (no leading ``?``).
97
+
98
+ Args:
99
+ params: The query parameters; ``None`` is treated as none at all.
100
+
101
+ Returns:
102
+ The encoded query string, empty when nothing was contributed.
103
+
104
+ Raises:
105
+ ValueError: If a parameter's value does not match its declared type."""
106
+ return urlencode(flatten(params))
107
+
108
+
109
+ def resolve_params(
110
+ global_params: Sequence[Param[Any]], call_params: Sequence[Param[Any]] | None
111
+ ) -> tuple[Param[Any], ...]:
112
+ """Return the parameters for one call, the call's winning per key.
113
+
114
+ One function for both kinds that need it: a path parameter and a query parameter are the same
115
+ type, so they are the same rule -- a call's parameter replaces a global of the same key but
116
+ keeps the global's position, so what an endpoint produces does not depend on which side supplied
117
+ a parameter. Keys are unique within either side -- a collection is carried by a single
118
+ :class:`Param` and expanded by its destination, never by repeating the key -- so replacing
119
+ rather than appending is unambiguous.
120
+
121
+ Both arguments share a type, so the precedence lives in their names rather than in the function
122
+ name: ``global_params`` is what the API prescribes, ``call_params`` what this one call declares.
123
+
124
+ Headers resolve separately, since they merge case-insensitively -- which ``render_headers`` gets
125
+ by lowercasing as it renders. Placeholders in a *base URL* are server variables, carried on the
126
+ :class:`UrlTemplate` itself and rendered by :func:`build_url`.
127
+
128
+ Args:
129
+ global_params: What the API prescribes for every call.
130
+ call_params: What this one call declares; wins per key.
131
+
132
+ Returns:
133
+ The effective parameters, each key once, in the order the global set established."""
134
+ by_key = {param.key: param for param in (*global_params, *(call_params or ()))}
135
+ return tuple(by_key.values())
136
+
137
+
138
+ def build_url(
139
+ url_template: UrlTemplate,
140
+ *,
141
+ path_params: Sequence[Param[Any]] | None = None,
142
+ query_params: Sequence[Param[Any]] | None = None,
143
+ ) -> str:
144
+ """Resolve ``url_template`` into a complete request URL.
145
+
146
+ Args:
147
+ url_template: The unresolved URL, carrying its base URL, path and server variables.
148
+ path_params: The call's path parameters.
149
+ query_params: The call's query parameters.
150
+
151
+ Returns:
152
+ The absolute URL, base and path joined on exactly one ``/``, query appended when non-empty.
153
+
154
+ Raises:
155
+ ValueError: If a placeholder is unfilled or a parameter's value does not match its
156
+ declared type."""
157
+ base = expand_template(url_template.base_url, render_segments(url_template.variables))
158
+ path = expand_template(url_template.path, render_segments(path_params))
159
+
160
+ # Join on exactly one separator. Plain urljoin would be wrong here: it treats the base as a
161
+ # document reference and discards its last path segment, so a base of ".../v2" with path
162
+ # "users" would resolve to ".../users", silently dropping the version.
163
+ url = f"{base.rstrip('/')}/{path.lstrip('/')}"
164
+
165
+ query = encode_query(query_params)
166
+ return f"{url}?{query}" if query else url
@@ -0,0 +1,173 @@
1
+ """One declared value, one wire-ready form.
2
+
3
+ Every request parameter -- query, form, path, header, or a base URL's server variable -- reaches
4
+ the wire the same way: validated against the type the endpoint declared, dumped through that
5
+ type's adapter, and turned into text. Only the *arrangement* differs, and that stays with each
6
+ destination: :mod:`.form` explodes values into ``key[sub]`` pairs, :mod:`.urls` joins them into
7
+ ``/``-separated segments, :mod:`.headers` folds them into one field value.
8
+
9
+ That shared prefix lives here, which is why a ``datetime`` declared ``RFC1123DateTime`` renders as
10
+ its GMT string at every destination without any of them knowing what a datetime is -- and why a
11
+ fifth destination would need no serialization code of its own."""
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from collections.abc import Iterable, Mapping
17
+ from typing import Any, TypeGuard
18
+
19
+ from pydantic import ValidationError
20
+
21
+ from ..optionality import strip_unset
22
+ from ..params import Param
23
+
24
+
25
+ def wire_value(param: Param[Any]) -> object:
26
+ """``param``'s value in wire form: validated against its declared type, then dumped.
27
+
28
+ The same two steps, in the same order, as :func:`json_body` -- and the reason a parameter
29
+ holds its resolved adapter rather than the type expression it came from. Validation coerces
30
+ dict-shaped input (``Model | FooDict`` companions) through the model's own rules; the dump
31
+ applies the type's serializer, which is what renders an ``RFC1123DateTime`` as its GMT string,
32
+ an enum as its value and a model under its wire aliases.
33
+
34
+ A validation failure is re-raised naming the parameter: pydantic titles the error by the
35
+ *type* (``1 validation error for int``), which on an endpoint with five parameters leaves the
36
+ caller guessing which one failed. ``ValidationError`` subclasses ``ValueError``, so a caller's
37
+ ``except ValueError`` covers both spellings, and the full per-field detail rides ``__cause__``.
38
+
39
+ ``strip_unset`` is what drops a field the caller never touched, and the only thing that can:
40
+ ``UnsetType`` is not ``None``, so the dump keeps the key and it serializes as ``null``, which a
41
+ JSON-encoded path segment or header value would carry verbatim. An *explicit* ``None`` is
42
+ kept -- see docs/designs/optional-nullable-fields.md. Which of those nulls survives is the
43
+ destination's own decision, taken in :func:`text_values` and ``flattening.flatten``: compact
44
+ JSON can spell one, a query pair or a flat path segment cannot.
45
+
46
+ The result is still structured -- a collection stays a list, a model stays a mapping -- because
47
+ the query and form destination needs that shape to explode into ``key[sub]`` keys.
48
+
49
+ Args:
50
+ param: The parameter to render, carrying its declared type's adapter.
51
+
52
+ Returns:
53
+ The dumped value, still structured; a top-level set is sorted so one call yields one URL.
54
+
55
+ Raises:
56
+ ValueError: If the value does not match its declared type. The message names the
57
+ parameter; pydantic's per-field detail rides ``__cause__``."""
58
+ try:
59
+ validated = param.adapter.validate_python(param.value)
60
+ except ValidationError as error:
61
+ raise ValueError(f"Parameter value does not match its declared type: {param.key}") from error
62
+
63
+ dumped: object = strip_unset(validated, param.adapter.dump_python(validated, mode="json"))
64
+
65
+ # JSON has no set, so the dump has already flattened one into a list in hash-iteration order.
66
+ # Sorting is what keeps one call producing one URL across runs. A *top-level* set is the whole
67
+ # reach on purpose: a set nested inside a container is not an emittable declared type --
68
+ # ``adapters.py``'s ``_CONTAINERS`` and ``strip_unset`` close at ``list``/``dict`` for the same
69
+ # reason -- so walking deeper would be speculative surface for a shape no spec yields.
70
+ if isinstance(validated, (set, frozenset)) and isinstance(dumped, list):
71
+ return sorted(dumped, key=str)
72
+
73
+ return dumped
74
+
75
+
76
+ def text_values(param: Param[Any]) -> tuple[str, ...]:
77
+ """``param`` as the flat texts it contributes -- one per path segment or header list member.
78
+
79
+ Empty means it contributes nothing: its value was ``None``, or its collection was. A scalar is
80
+ one text, a flat collection is one text per member, and anything structured -- a mapping, a
81
+ model, or a collection holding either -- is one text of compact JSON, since neither a path
82
+ segment nor a header value can carry a nested shape any other way.
83
+
84
+ The arrangement is settled *before* the ``None`` rule, because the two arrangements can
85
+ represent different things: compact JSON spells ``null``, so a structured collection carries a
86
+ ``None`` member as one, while a flat collection has no such spelling and refuses it.
87
+
88
+ Args:
89
+ param: The parameter to render.
90
+
91
+ Returns:
92
+ One text per segment or header member; empty when the parameter contributes nothing.
93
+
94
+ Raises:
95
+ ValueError: If the value does not match its declared type, or if a *flat* collection holds
96
+ a ``None`` -- neither a path segment nor a header member can spell one."""
97
+ value = wire_value(param)
98
+ if value is None:
99
+ return ()
100
+
101
+ if isinstance(value, Mapping):
102
+ return (compact_json(value),)
103
+
104
+ if not is_collection(value):
105
+ return (to_text(value),)
106
+
107
+ members = list(value)
108
+ if any(isinstance(member, Mapping) or is_collection(member) for member in members):
109
+ return (compact_json(members),)
110
+
111
+ if any(member is None for member in members):
112
+ # Query and form encoding can drop a member, because it can also repeat or omit a key.
113
+ # Neither of these destinations can: a path would address a different route and a header
114
+ # would carry an empty list slot, both silently.
115
+ raise ValueError(f"Parameter value contains None: {param.key}")
116
+
117
+ return tuple(to_text(member) for member in members)
118
+
119
+
120
+ def to_text(value: object) -> str:
121
+ """One dumped scalar as the text a URL or a header carries.
122
+
123
+ The adapter has already applied wire aliases, enum values and temporal formats, so only
124
+ booleans are left: every destination here is text and expects JSON's ``true``/``false``,
125
+ whereas ``str(True)`` would emit ``True``. Called only where a value actually becomes text,
126
+ which is what lets the structured branches above keep real booleans for :func:`compact_json`.
127
+
128
+ Args:
129
+ value: One already-dumped scalar.
130
+
131
+ Returns:
132
+ Its text form, with booleans lowercased to JSON's spelling."""
133
+ if isinstance(value, bool):
134
+ return "true" if value else "false"
135
+
136
+ return str(value)
137
+
138
+
139
+ def compact_json(value: object) -> str:
140
+ """JSON-encode a structured value with no insignificant whitespace.
141
+
142
+ ``ensure_ascii`` stays at its default: a header field value is ISO-8859-1 by RFC 9110 and the
143
+ transport rejects anything outside it, so escaping non-ASCII is the only encoding that
144
+ survives. ``allow_nan`` is turned off: at its default a non-finite float encodes as
145
+ ``Infinity`` or ``NaN`` -- a Python dialect no RFC 8259 parser accepts -- so the call would
146
+ fail server-side, far from the line that produced the value.
147
+
148
+ Args:
149
+ value: The structured value to encode.
150
+
151
+ Returns:
152
+ Its JSON text, non-ASCII escaped and no insignificant whitespace.
153
+
154
+ Raises:
155
+ ValueError: If ``value`` holds a non-finite float, which has no RFC 8259 spelling."""
156
+ return json.dumps(value, separators=(",", ":"), allow_nan=False)
157
+
158
+
159
+ def is_collection(value: object) -> TypeGuard[Iterable[object]]:
160
+ """True for an iterable that should expand into several wire values.
161
+
162
+ ``TypeGuard``, deliberately not the newer ``TypeIs``: ``TypeIs`` also narrows the *negative*
163
+ branch, which would be unsound here -- ``str`` is ``Iterable`` yet this returns ``False`` for
164
+ it, so a checker told "not an ``Iterable``" would wrongly rule out strings. ``TypeGuard``
165
+ narrows only where this returns ``True``, which is exactly what the caller needs to iterate.
166
+
167
+ Args:
168
+ value: The dumped value to classify.
169
+
170
+ Returns:
171
+ ``True`` for an iterable that expands into several wire values -- a ``str``, ``bytes`` or
172
+ ``Mapping`` is one value, not several."""
173
+ return isinstance(value, Iterable) and not isinstance(value, (str, bytes, Mapping))