Skip to content

Commit 0f7d920

Browse files
committed
Media type encoding support
1 parent 5a02484 commit 0f7d920

File tree

4 files changed

+144
-30
lines changed

4 files changed

+144
-30
lines changed
Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,113 @@
11
import warnings
22
from typing import Any
3+
from typing import Dict
34
from typing import Optional
45
from xml.etree.ElementTree import ParseError
56

67
from openapi_core.deserializing.media_types.datatypes import (
78
DeserializerCallable,
89
)
10+
from openapi_core.deserializing.media_types.datatypes import (
11+
MediaTypeDeserializersDict,
12+
)
913
from openapi_core.deserializing.media_types.exceptions import (
1014
MediaTypeDeserializeError,
1115
)
16+
from openapi_core.schema.encodings import get_encoding_default_content_type
17+
from openapi_core.spec import Spec
1218

1319

14-
class CallableMediaTypeDeserializer:
20+
class ContentTypesDeserializer:
1521
def __init__(
1622
self,
17-
mimetype: str,
18-
deserializer_callable: Optional[DeserializerCallable] = None,
23+
media_type_deserializers: Optional[MediaTypeDeserializersDict] = None,
24+
extra_media_type_deserializers: Optional[MediaTypeDeserializersDict] = None,
1925
):
20-
self.mimetype = mimetype
21-
self.deserializer_callable = deserializer_callable
26+
if media_type_deserializers is None:
27+
media_type_deserializers = {}
28+
self.media_type_deserializers = media_type_deserializers
29+
if extra_media_type_deserializers is None:
30+
extra_media_type_deserializers = {}
31+
self.extra_media_type_deserializers = extra_media_type_deserializers
2232

23-
def deserialize(self, value: Any) -> Any:
24-
if self.deserializer_callable is None:
25-
warnings.warn(f"Unsupported {self.mimetype} mimetype")
33+
def deserialize(self, mimetype: str, value: Any) -> Any:
34+
deserializer_callable = self.get_deserializer_callable(mimetype)
35+
if deserializer_callable is None:
36+
warnings.warn(f"Unsupported {mimetype} mimetype")
2637
return value
2738

2839
try:
29-
return self.deserializer_callable(value)
40+
return deserializer_callable(value)
3041
except (ParseError, ValueError, TypeError, AttributeError):
31-
raise MediaTypeDeserializeError(self.mimetype, value)
42+
raise MediaTypeDeserializeError(mimetype, value)
43+
44+
def get_deserializer_callable(
45+
self,
46+
mimetype: str,
47+
) -> Optional[DeserializerCallable]:
48+
if mimetype in self.extra_media_type_deserializers:
49+
return self.extra_media_type_deserializers[mimetype]
50+
return self.media_type_deserializers.get(mimetype)
51+
52+
53+
class MediaTypeDeserializer(ContentTypesDeserializer):
54+
def __init__(
55+
self,
56+
schema: Spec,
57+
mimetype: str,
58+
content_types_deserializers: ContentTypesDeserializer,
59+
encoding: Optional[Spec] = None,
60+
):
61+
self.schema = schema
62+
self.mimetype = mimetype
63+
self.content_types_deserializers = content_types_deserializers
64+
self.encoding = encoding
65+
66+
def deserialize(self, value: Any) -> Any:
67+
deserialized = self.content_types_deserializers.deserialize(self.mimetype, value)
68+
69+
if self.mimetype != "application/x-www-form-urlencoded" and not self.mimetype.startswith("multipart"):
70+
return deserialized
71+
72+
return self.decode(deserialized)
73+
74+
def evolve(self, schema: Spec, mimetype: str) -> "MediaTypeDeserializer":
75+
cls = self.__class__
76+
77+
return cls(
78+
schema,
79+
mimetype,
80+
self.content_types_deserializers,
81+
)
82+
83+
def decode(self, value: Dict[str, Any]) -> Dict[str, Any]:
84+
return {
85+
prop_name: self.decode_property(prop_name, prop_value)
86+
for prop_name, prop_value in value.items()
87+
}
88+
89+
def decode_property(self, prop_name: str, value: Any) -> Any:
90+
schema_props = self.schema.get("properties")
91+
prop_schema = None
92+
if schema_props is not None and prop_name in schema_props:
93+
prop_schema = self.schema.get(prop_name)
94+
prop_content_type = self.get_property_content_type(prop_name, prop_schema)
95+
prop_deserializer = self.evolve(
96+
schema=prop_schema,
97+
mimetype=prop_content_type,
98+
)
99+
return prop_deserializer.deserialize(value)
100+
101+
def get_property_content_type(self, prop_name: str, prop_schema: Optional[Spec] = None) -> str:
102+
if self.encoding is None:
103+
return get_encoding_default_content_type(prop_schema)
104+
105+
if prop_name not in self.encoding:
106+
return get_encoding_default_content_type(prop_schema)
107+
108+
prep_encoding = self.encoding.get(prop_name)
109+
prop_content_type = prep_encoding.getkey("contentType")
110+
if prop_content_type is None:
111+
return get_encoding_default_content_type(prop_schema)
112+
113+
return prop_content_type

openapi_core/deserializing/media_types/factories.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
MediaTypeDeserializersDict,
88
)
99
from openapi_core.deserializing.media_types.deserializers import (
10-
CallableMediaTypeDeserializer,
10+
ContentTypesDeserializer,
1111
)
12+
from openapi_core.deserializing.media_types.deserializers import (
13+
MediaTypeDeserializer,
14+
)
15+
from openapi_core.spec import Spec
1216

1317

1418
class MediaTypeDeserializersFactory:
@@ -22,25 +26,23 @@ def __init__(
2226

2327
def create(
2428
self,
29+
schema: Spec,
2530
mimetype: str,
31+
encoding: Optional[Spec] = None,
2632
extra_media_type_deserializers: Optional[
2733
MediaTypeDeserializersDict
2834
] = None,
29-
) -> CallableMediaTypeDeserializer:
35+
) -> MediaTypeDeserializer:
3036
if extra_media_type_deserializers is None:
3137
extra_media_type_deserializers = {}
32-
deserialize_callable = self.get_deserializer_callable(
33-
mimetype,
34-
extra_media_type_deserializers=extra_media_type_deserializers,
35-
)
3638

37-
return CallableMediaTypeDeserializer(mimetype, deserialize_callable)
39+
content_types_deserializer = ContentTypesDeserializer(
40+
self.media_type_deserializers,
41+
extra_media_type_deserializers,
42+
)
3843

39-
def get_deserializer_callable(
40-
self,
41-
mimetype: str,
42-
extra_media_type_deserializers: MediaTypeDeserializersDict,
43-
) -> Optional[DeserializerCallable]:
44-
if mimetype in extra_media_type_deserializers:
45-
return extra_media_type_deserializers[mimetype]
46-
return self.media_type_deserializers.get(mimetype)
44+
return MediaTypeDeserializer(
45+
schema, mimetype,
46+
content_types_deserializer,
47+
encoding=encoding,
48+
)

openapi_core/validation/validators.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,16 @@ def _get_media_type(self, content: Spec, mimetype: str) -> MediaType:
7878
finder = MediaTypeFinder(content)
7979
return finder.find(mimetype)
8080

81-
def _deserialise_data(self, mimetype: str, value: Any) -> Any:
81+
def _deserialise_data(self, media_type: Spec, mimetype: str, value: Any) -> Any:
82+
schema = media_type.get("schema")
83+
encoding = None
84+
if "encoding" in media_type:
85+
encoding = media_type.get("encoding")
86+
8287
deserializer = self.media_type_deserializers_factory.create(
88+
schema,
8389
mimetype,
90+
encoding=encoding,
8491
extra_media_type_deserializers=self.extra_media_type_deserializers,
8592
)
8693
return deserializer.deserialize(value)
@@ -152,7 +159,7 @@ def _get_param_or_header_value_and_schema(
152159
else:
153160
content = param_or_header / "content"
154161
mimetype, media_type = next(content.items())
155-
deserialised = self._deserialise_data(mimetype, raw_value)
162+
deserialised = self._deserialise_data(media_type, mimetype, raw_value)
156163
schema = media_type / "schema"
157164
casted = self._cast(schema, deserialised)
158165
return casted, schema
@@ -161,7 +168,7 @@ def _get_content_value_and_schema(
161168
self, raw: Any, mimetype: str, content: Spec
162169
) -> Tuple[Any, Optional[Spec]]:
163170
media_type, mimetype = self._get_media_type(content, mimetype)
164-
deserialised = self._deserialise_data(mimetype, raw)
171+
deserialised = self._deserialise_data(media_type, mimetype, raw)
165172
casted = self._cast(media_type, deserialised)
166173

167174
if "schema" not in media_type:

tests/unit/deserializing/test_media_types_deserializers.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,19 @@ class TestMediaTypeDeserializer:
1414
def deserializer_factory(self):
1515
def create_deserializer(
1616
media_type,
17+
schema=None,
18+
encoding=None,
1719
media_type_deserializers=media_type_deserializers,
1820
extra_media_type_deserializers=None,
1921
):
22+
if schema is None:
23+
schema or {}
2024
return MediaTypeDeserializersFactory(
2125
media_type_deserializers,
2226
).create(
27+
schema,
2328
media_type,
29+
encoding=encoding,
2430
extra_media_type_deserializers=extra_media_type_deserializers,
2531
)
2632

@@ -132,7 +138,15 @@ def test_urlencoded_form_empty(self, deserializer_factory):
132138

133139
def test_urlencoded_form_simple(self, deserializer_factory):
134140
mimetype = "application/x-www-form-urlencoded"
135-
deserializer = deserializer_factory(mimetype)
141+
schema = {
142+
"type": "object",
143+
"properties": {
144+
"param1": {
145+
"type": "string",
146+
},
147+
},
148+
}
149+
deserializer = deserializer_factory(mimetype, schema=schema)
136150
value = "param1=test"
137151

138152
result = deserializer.deserialize(value)
@@ -150,7 +164,16 @@ def test_data_form_empty(self, deserializer_factory, value):
150164

151165
def test_data_form_simple(self, deserializer_factory):
152166
mimetype = "multipart/form-data"
153-
deserializer = deserializer_factory(mimetype)
167+
schema = {
168+
"type": "object",
169+
"properties": {
170+
"param1": {
171+
"type": "string",
172+
"format": "binary",
173+
},
174+
},
175+
}
176+
deserializer = deserializer_factory(mimetype, schema=schema)
154177
value = (
155178
b'Content-Type: multipart/form-data; boundary="'
156179
b'===============2872712225071193122=="\n'

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy