reflectapi-runtime 0.19.0__tar.gz → 0.20.1__tar.gz
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.
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/PKG-INFO +17 -10
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/README.md +9 -6
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/pyproject.toml +8 -4
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/__init__.py +9 -2
- reflectapi_runtime-0.20.1/src/reflectapi_runtime/serde.py +141 -0
- reflectapi_runtime-0.20.1/tests/test_enum_forward_compatibility.py +129 -0
- reflectapi_runtime-0.19.0/src/reflectapi_runtime/serde.py +0 -48
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/.gitignore +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/auth.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/batch.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/client.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/duration.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/exceptions.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/hypothesis_strategies.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/middleware.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/partial.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/response.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/sse.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/streaming.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/testing.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/transport.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/types.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/__init__.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/test_auth.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/test_auth_negative_cases.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/test_batch.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/test_client.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/test_codegen_regressions.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/test_edge_cases.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/test_enhanced_features.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/test_exceptions.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/test_middleware.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/test_partial.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/test_pydantic_serialization.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/test_response.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/test_sse.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/test_streaming.py +0 -0
- {reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/test_testing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: reflectapi-runtime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.1
|
|
4
4
|
Summary: Runtime library for ReflectAPI Python clients
|
|
5
5
|
Project-URL: Homepage, https://github.com/thepartly/reflectapi
|
|
6
6
|
Project-URL: Repository, https://github.com/thepartly/reflectapi
|
|
@@ -19,9 +19,13 @@ Classifier: Topic :: Internet :: WWW/HTTP
|
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
20
|
Classifier: Typing :: Typed
|
|
21
21
|
Requires-Python: >=3.12
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist:
|
|
24
|
-
Requires-Dist:
|
|
22
|
+
Requires-Dist: anyio>=4.14.2
|
|
23
|
+
Requires-Dist: httpcore>=1.0.9
|
|
24
|
+
Requires-Dist: httpx>=0.25.2
|
|
25
|
+
Requires-Dist: idna>=3.15
|
|
26
|
+
Requires-Dist: pydantic>=2.10.0; python_version < '3.14'
|
|
27
|
+
Requires-Dist: pydantic>=2.12.0; python_version >= '3.14'
|
|
28
|
+
Requires-Dist: typing-extensions>=4.5.0
|
|
25
29
|
Provides-Extra: dev
|
|
26
30
|
Requires-Dist: hypothesis>=6.0.0; extra == 'dev'
|
|
27
31
|
Requires-Dist: mypy>=1.5.0; extra == 'dev'
|
|
@@ -81,12 +85,15 @@ against the declared item model. The wire format is Server-Sent Events
|
|
|
81
85
|
do not enter the iterator. Breaking out of the loop or calling
|
|
82
86
|
`stream.aclose()` releases the underlying HTTP connection.
|
|
83
87
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
Events are validated against the generated model. Closed response enums reject
|
|
89
|
+
unfamiliar variants; enums declared with `#[reflectapi(open)]` retain them as
|
|
90
|
+
an unknown case with the tag and parsed JSON, and `#[serde(other)]` uses the
|
|
91
|
+
declared fallback. A malformed known variant raises `ValidationError`
|
|
92
|
+
mid-stream and terminates the iterator. Earlier items remain delivered.
|
|
88
93
|
|
|
89
94
|
## Compatibility
|
|
90
95
|
|
|
91
|
-
Python 3.12+.
|
|
92
|
-
|
|
96
|
+
Python 3.12+. SDKs generated with ReflectAPI 0.20 require the 0.20 runtime.
|
|
97
|
+
Pydantic 2.10 or newer supports the generated generic models; Python 3.14
|
|
98
|
+
requires Pydantic 2.12 or newer. Installing the runtime resolves these requirements
|
|
99
|
+
and the supported HTTP dependencies automatically.
|
|
@@ -44,12 +44,15 @@ against the declared item model. The wire format is Server-Sent Events
|
|
|
44
44
|
do not enter the iterator. Breaking out of the loop or calling
|
|
45
45
|
`stream.aclose()` releases the underlying HTTP connection.
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
Events are validated against the generated model. Closed response enums reject
|
|
48
|
+
unfamiliar variants; enums declared with `#[reflectapi(open)]` retain them as
|
|
49
|
+
an unknown case with the tag and parsed JSON, and `#[serde(other)]` uses the
|
|
50
|
+
declared fallback. A malformed known variant raises `ValidationError`
|
|
51
|
+
mid-stream and terminates the iterator. Earlier items remain delivered.
|
|
51
52
|
|
|
52
53
|
## Compatibility
|
|
53
54
|
|
|
54
|
-
Python 3.12+.
|
|
55
|
-
|
|
55
|
+
Python 3.12+. SDKs generated with ReflectAPI 0.20 require the 0.20 runtime.
|
|
56
|
+
Pydantic 2.10 or newer supports the generated generic models; Python 3.14
|
|
57
|
+
requires Pydantic 2.12 or newer. Installing the runtime resolves these requirements
|
|
58
|
+
and the supported HTTP dependencies automatically.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "reflectapi-runtime"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.20.1"
|
|
8
8
|
description = "Runtime library for ReflectAPI Python clients"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -26,9 +26,13 @@ classifiers = [
|
|
|
26
26
|
"Typing :: Typed",
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
|
-
"pydantic>=2.0.
|
|
30
|
-
"
|
|
31
|
-
"
|
|
29
|
+
"pydantic>=2.10.0; python_version < '3.14'",
|
|
30
|
+
"pydantic>=2.12.0; python_version >= '3.14'",
|
|
31
|
+
"httpx>=0.25.2",
|
|
32
|
+
"httpcore>=1.0.9",
|
|
33
|
+
"anyio>=4.14.2",
|
|
34
|
+
"idna>=3.15",
|
|
35
|
+
"typing-extensions>=4.5.0",
|
|
32
36
|
]
|
|
33
37
|
|
|
34
38
|
[project.optional-dependencies]
|
|
@@ -39,7 +39,12 @@ from .hypothesis_strategies import (
|
|
|
39
39
|
from .middleware import AsyncMiddleware
|
|
40
40
|
from .partial import ReflectapiPartialModel
|
|
41
41
|
from .response import ApiResponse, ApiStream, AsyncApiStream, TransportMetadata
|
|
42
|
-
from .serde import
|
|
42
|
+
from .serde import (
|
|
43
|
+
UnknownEnumValue,
|
|
44
|
+
classify_tagged_enum,
|
|
45
|
+
parse_externally_tagged,
|
|
46
|
+
serialize_externally_tagged,
|
|
47
|
+
)
|
|
43
48
|
from .streaming import AsyncStreamingClient, StreamingResponse
|
|
44
49
|
from .testing import (
|
|
45
50
|
AsyncCassetteMiddleware,
|
|
@@ -50,7 +55,7 @@ from .testing import (
|
|
|
50
55
|
)
|
|
51
56
|
from .types import BatchResult, ReflectapiEmpty, ReflectapiInfallible
|
|
52
57
|
|
|
53
|
-
__version__ = "0.
|
|
58
|
+
__version__ = "0.20.1"
|
|
54
59
|
|
|
55
60
|
__all__ = [
|
|
56
61
|
# Authentication
|
|
@@ -93,6 +98,8 @@ __all__ = [
|
|
|
93
98
|
"ReflectapiPartialModel",
|
|
94
99
|
"parse_externally_tagged",
|
|
95
100
|
"serialize_externally_tagged",
|
|
101
|
+
"UnknownEnumValue",
|
|
102
|
+
"classify_tagged_enum",
|
|
96
103
|
"StreamingResponse",
|
|
97
104
|
"ApiStream",
|
|
98
105
|
"AsyncApiStream",
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Serialization helpers used by generated ReflectAPI clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, model_serializer, model_validator
|
|
9
|
+
|
|
10
|
+
VariantHandler = Callable[[Any], Any] | str
|
|
11
|
+
VariantSerializers = dict[str, tuple[Callable[[Any], bool], Callable[[Any], Any]]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UnknownEnumValue(BaseModel):
|
|
15
|
+
"""An output enum case whose wire tag was not in the generated schema.
|
|
16
|
+
|
|
17
|
+
Generated clients construct this after checking the wire discriminator. A
|
|
18
|
+
raw object cannot be validated directly as this case, which keeps malformed
|
|
19
|
+
known cases from silently falling through a Pydantic union.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
tag: str
|
|
23
|
+
raw: Any
|
|
24
|
+
|
|
25
|
+
@model_validator(mode="before")
|
|
26
|
+
@classmethod
|
|
27
|
+
def _instance_only(cls, value: Any) -> Any:
|
|
28
|
+
if not isinstance(value, cls):
|
|
29
|
+
raise ValueError("Unknown enum cases are created from unfamiliar wire tags")
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
@model_serializer(mode="plain")
|
|
33
|
+
def _serialize_raw(self) -> Any:
|
|
34
|
+
return self.raw
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def classify_tagged_enum(
|
|
38
|
+
data: Any,
|
|
39
|
+
known_tags: frozenset[str],
|
|
40
|
+
*,
|
|
41
|
+
representation: str,
|
|
42
|
+
tag_field: str = "",
|
|
43
|
+
content_field: str = "",
|
|
44
|
+
fallback_tag: str | None = None,
|
|
45
|
+
unknown_type: type[UnknownEnumValue] | None = None,
|
|
46
|
+
unit_tags: frozenset[str] = frozenset(),
|
|
47
|
+
) -> Any:
|
|
48
|
+
"""Classify an unfamiliar valid tag without masking other validation errors."""
|
|
49
|
+
if isinstance(data, UnknownEnumValue):
|
|
50
|
+
return data
|
|
51
|
+
if representation == "external":
|
|
52
|
+
if isinstance(data, str):
|
|
53
|
+
tag = data
|
|
54
|
+
elif isinstance(data, dict) and len(data) == 1:
|
|
55
|
+
tag = next(iter(data))
|
|
56
|
+
else:
|
|
57
|
+
return data
|
|
58
|
+
elif isinstance(data, dict):
|
|
59
|
+
if tag_field not in data or not isinstance(data[tag_field], str):
|
|
60
|
+
raise ValueError("Tagged enum requires a string discriminator")
|
|
61
|
+
tag = data[tag_field]
|
|
62
|
+
else:
|
|
63
|
+
return data
|
|
64
|
+
if isinstance(tag, str) and tag in unit_tags and isinstance(data, dict):
|
|
65
|
+
if representation == "adjacent" and data.get(content_field) is not None:
|
|
66
|
+
raise ValueError("Unit variant cannot carry non-null content")
|
|
67
|
+
if representation == "external":
|
|
68
|
+
if data[tag] is not None:
|
|
69
|
+
raise ValueError("Unit variant cannot carry non-null content")
|
|
70
|
+
return tag
|
|
71
|
+
if not isinstance(tag, str) or tag in known_tags:
|
|
72
|
+
return data
|
|
73
|
+
if (
|
|
74
|
+
representation == "adjacent"
|
|
75
|
+
and unknown_type is None
|
|
76
|
+
and fallback_tag is not None
|
|
77
|
+
and data.get(content_field) is not None
|
|
78
|
+
):
|
|
79
|
+
raise ValueError("Fallback unit variant cannot carry non-null content")
|
|
80
|
+
if unknown_type is not None:
|
|
81
|
+
return unknown_type.model_construct(tag=tag, raw=data)
|
|
82
|
+
if fallback_tag is not None:
|
|
83
|
+
if representation == "external":
|
|
84
|
+
return fallback_tag
|
|
85
|
+
return {tag_field: fallback_tag}
|
|
86
|
+
return data
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def parse_externally_tagged(
|
|
90
|
+
data: Any,
|
|
91
|
+
variants: dict[str, VariantHandler],
|
|
92
|
+
types: tuple[type[Any], ...],
|
|
93
|
+
enum_name: str,
|
|
94
|
+
*,
|
|
95
|
+
fallback_tag: str | None = None,
|
|
96
|
+
unknown_type: type[UnknownEnumValue] | None = None,
|
|
97
|
+
strict_unit: bool = False,
|
|
98
|
+
) -> Any:
|
|
99
|
+
"""Parse an externally tagged enum from ``{"Variant": value}`` format."""
|
|
100
|
+
if types and isinstance(data, types):
|
|
101
|
+
return data
|
|
102
|
+
data = classify_tagged_enum(
|
|
103
|
+
data,
|
|
104
|
+
frozenset(variants),
|
|
105
|
+
representation="external",
|
|
106
|
+
fallback_tag=fallback_tag,
|
|
107
|
+
unknown_type=unknown_type,
|
|
108
|
+
)
|
|
109
|
+
if isinstance(data, UnknownEnumValue):
|
|
110
|
+
return data
|
|
111
|
+
if isinstance(data, str) and data in variants:
|
|
112
|
+
handler = variants[data]
|
|
113
|
+
if handler == "_unit":
|
|
114
|
+
return data
|
|
115
|
+
if isinstance(data, dict):
|
|
116
|
+
if len(data) != 1:
|
|
117
|
+
raise ValueError("Externally tagged enum must have exactly one key")
|
|
118
|
+
key, value = next(iter(data.items()))
|
|
119
|
+
if key in variants:
|
|
120
|
+
handler = variants[key]
|
|
121
|
+
if handler == "_unit":
|
|
122
|
+
if strict_unit and value is not None:
|
|
123
|
+
raise ValueError(f"Unit variant {key} cannot carry a payload")
|
|
124
|
+
return key
|
|
125
|
+
if callable(handler):
|
|
126
|
+
return handler(value)
|
|
127
|
+
raise ValueError(f"Unknown variant for {enum_name}: {data}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def serialize_externally_tagged(
|
|
131
|
+
root: Any,
|
|
132
|
+
serializers: VariantSerializers,
|
|
133
|
+
enum_name: str,
|
|
134
|
+
) -> Any:
|
|
135
|
+
"""Serialize an externally tagged enum to ``{"Variant": value}`` format."""
|
|
136
|
+
if isinstance(root, UnknownEnumValue):
|
|
137
|
+
return root.raw
|
|
138
|
+
for _variant_name, (check, serialize) in serializers.items():
|
|
139
|
+
if check(root):
|
|
140
|
+
return serialize(root)
|
|
141
|
+
raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""The runtime must preserve future response cases without hiding bad known cases."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from pydantic import BaseModel, RootModel, ValidationError, model_validator
|
|
9
|
+
|
|
10
|
+
from reflectapi_runtime import (
|
|
11
|
+
UnknownEnumValue,
|
|
12
|
+
classify_tagged_enum,
|
|
13
|
+
parse_externally_tagged,
|
|
14
|
+
serialize_externally_tagged,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Known(BaseModel):
|
|
19
|
+
kind: Literal["Known"]
|
|
20
|
+
value: int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FutureUnknown(UnknownEnumValue):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OpenResult(RootModel[Known | FutureUnknown]):
|
|
28
|
+
root: Known | FutureUnknown
|
|
29
|
+
|
|
30
|
+
@model_validator(mode="before")
|
|
31
|
+
@classmethod
|
|
32
|
+
def _classify(cls, data):
|
|
33
|
+
return classify_tagged_enum(
|
|
34
|
+
data,
|
|
35
|
+
frozenset(("Known",)),
|
|
36
|
+
representation="tagged",
|
|
37
|
+
tag_field="kind",
|
|
38
|
+
unknown_type=FutureUnknown,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Envelope(BaseModel):
|
|
43
|
+
results: list[OpenResult]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_open_case_round_trips_inside_nested_model():
|
|
47
|
+
wire = {"results": [{"kind": "Known", "value": 2}, {"kind": "Future", "x": [1]}]}
|
|
48
|
+
parsed = Envelope.model_validate(wire)
|
|
49
|
+
assert isinstance(parsed.results[0].root, Known)
|
|
50
|
+
unknown = parsed.results[1].root
|
|
51
|
+
assert isinstance(unknown, FutureUnknown)
|
|
52
|
+
assert unknown.tag == "Future"
|
|
53
|
+
assert unknown.raw == wire["results"][1]
|
|
54
|
+
assert parsed.model_dump() == wire
|
|
55
|
+
assert Envelope.model_validate_json(parsed.model_dump_json()).model_dump() == wire
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.parametrize(
|
|
59
|
+
"value",
|
|
60
|
+
[
|
|
61
|
+
{"kind": "Known", "value": "not an integer"},
|
|
62
|
+
{"value": 2},
|
|
63
|
+
{"kind": 42, "value": 2},
|
|
64
|
+
{"kind": None, "value": 2},
|
|
65
|
+
],
|
|
66
|
+
)
|
|
67
|
+
def test_open_case_rejects_malformed_known_or_missing_tag(value):
|
|
68
|
+
with pytest.raises(ValidationError):
|
|
69
|
+
OpenResult.model_validate(value)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_other_fallback_discards_unfamiliar_payload():
|
|
73
|
+
assert classify_tagged_enum(
|
|
74
|
+
{"kind": "Future", "payload": {"x": 1}},
|
|
75
|
+
frozenset(("Known", "Other")),
|
|
76
|
+
representation="tagged",
|
|
77
|
+
tag_field="kind",
|
|
78
|
+
fallback_tag="Other",
|
|
79
|
+
) == {"kind": "Other"}
|
|
80
|
+
assert classify_tagged_enum(
|
|
81
|
+
{"kind": "Known", "value": "bad"},
|
|
82
|
+
frozenset(("Known", "Other")),
|
|
83
|
+
representation="tagged",
|
|
84
|
+
tag_field="kind",
|
|
85
|
+
fallback_tag="Other",
|
|
86
|
+
) == {"kind": "Known", "value": "bad"}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_external_unknown_string_and_single_key_object():
|
|
90
|
+
for wire in ("Future", {"Future": {"x": 1}}):
|
|
91
|
+
unknown = parse_externally_tagged(
|
|
92
|
+
wire,
|
|
93
|
+
{"Known": "_unit"},
|
|
94
|
+
(),
|
|
95
|
+
"Result",
|
|
96
|
+
unknown_type=FutureUnknown,
|
|
97
|
+
)
|
|
98
|
+
assert isinstance(unknown, FutureUnknown)
|
|
99
|
+
assert unknown.tag == "Future"
|
|
100
|
+
assert serialize_externally_tagged(unknown, {}, "Result") == wire
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_external_unit_object_normalizes_to_known_literal():
|
|
104
|
+
assert classify_tagged_enum(
|
|
105
|
+
{"Known": None},
|
|
106
|
+
frozenset(("Known",)),
|
|
107
|
+
representation="external",
|
|
108
|
+
unit_tags=frozenset(("Known",)),
|
|
109
|
+
) == "Known"
|
|
110
|
+
for payload in (1, {"bad": True}):
|
|
111
|
+
with pytest.raises(ValueError):
|
|
112
|
+
classify_tagged_enum(
|
|
113
|
+
{"Known": payload},
|
|
114
|
+
frozenset(("Known",)),
|
|
115
|
+
representation="external",
|
|
116
|
+
unit_tags=frozenset(("Known",)),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.parametrize("wire", [{"A": 1, "B": 2}, {42: "value"}, None, 42])
|
|
121
|
+
def test_external_malformed_tags_do_not_become_unknown(wire):
|
|
122
|
+
with pytest.raises(ValueError):
|
|
123
|
+
parse_externally_tagged(
|
|
124
|
+
wire,
|
|
125
|
+
{"Known": "_unit"},
|
|
126
|
+
(),
|
|
127
|
+
"Result",
|
|
128
|
+
unknown_type=FutureUnknown,
|
|
129
|
+
)
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
"""Serialization helpers used by generated ReflectAPI clients."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from collections.abc import Callable
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
VariantHandler = Callable[[Any], Any] | str
|
|
10
|
-
VariantSerializers = dict[str, tuple[Callable[[Any], bool], Callable[[Any], Any]]]
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def parse_externally_tagged(
|
|
14
|
-
data: Any,
|
|
15
|
-
variants: dict[str, VariantHandler],
|
|
16
|
-
types: tuple[type[Any], ...],
|
|
17
|
-
enum_name: str,
|
|
18
|
-
) -> Any:
|
|
19
|
-
"""Parse an externally tagged enum from ``{"Variant": value}`` format."""
|
|
20
|
-
if types and isinstance(data, types):
|
|
21
|
-
return data
|
|
22
|
-
if isinstance(data, str) and data in variants:
|
|
23
|
-
handler = variants[data]
|
|
24
|
-
if handler == "_unit":
|
|
25
|
-
return data
|
|
26
|
-
if isinstance(data, dict):
|
|
27
|
-
if len(data) != 1:
|
|
28
|
-
raise ValueError("Externally tagged enum must have exactly one key")
|
|
29
|
-
key, value = next(iter(data.items()))
|
|
30
|
-
if key in variants:
|
|
31
|
-
handler = variants[key]
|
|
32
|
-
if handler == "_unit":
|
|
33
|
-
return key
|
|
34
|
-
if callable(handler):
|
|
35
|
-
return handler(value)
|
|
36
|
-
raise ValueError(f"Unknown variant for {enum_name}: {data}")
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def serialize_externally_tagged(
|
|
40
|
-
root: Any,
|
|
41
|
-
serializers: VariantSerializers,
|
|
42
|
-
enum_name: str,
|
|
43
|
-
) -> Any:
|
|
44
|
-
"""Serialize an externally tagged enum to ``{"Variant": value}`` format."""
|
|
45
|
-
for _variant_name, (check, serialize) in serializers.items():
|
|
46
|
-
if check(root):
|
|
47
|
-
return serialize(root)
|
|
48
|
-
raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
{reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/src/reflectapi_runtime/middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reflectapi_runtime-0.19.0 → reflectapi_runtime-0.20.1}/tests/test_pydantic_serialization.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|