Skip to content

Commit 9f700ff

Browse files
committed
Media type encoding support
1 parent 330cb71 commit 9f700ff

File tree

11 files changed

+532
-103
lines changed

11 files changed

+532
-103
lines changed
Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,37 @@
1-
from json import loads as json_loads
2-
from xml.etree.ElementTree import fromstring as xml_loads
1+
from collections import defaultdict
32

43
from openapi_core.deserializing.media_types.datatypes import (
54
MediaTypeDeserializersDict,
65
)
76
from openapi_core.deserializing.media_types.factories import (
87
MediaTypeDeserializersFactory,
98
)
9+
from openapi_core.deserializing.media_types.util import binary_loads
1010
from openapi_core.deserializing.media_types.util import data_form_loads
11+
from openapi_core.deserializing.media_types.util import json_loads
1112
from openapi_core.deserializing.media_types.util import plain_loads
1213
from openapi_core.deserializing.media_types.util import urlencoded_form_loads
14+
from openapi_core.deserializing.media_types.util import xml_loads
15+
from openapi_core.deserializing.styles import style_deserializers_factory
1316

1417
__all__ = ["media_type_deserializers_factory"]
1518

16-
media_type_deserializers: MediaTypeDeserializersDict = {
17-
"text/html": plain_loads,
18-
"text/plain": plain_loads,
19-
"application/json": json_loads,
20-
"application/vnd.api+json": json_loads,
21-
"application/xml": xml_loads,
22-
"application/xhtml+xml": xml_loads,
23-
"application/x-www-form-urlencoded": urlencoded_form_loads,
24-
"multipart/form-data": data_form_loads,
25-
}
19+
media_type_deserializers: MediaTypeDeserializersDict = defaultdict(
20+
lambda: binary_loads,
21+
**{
22+
"text/html": plain_loads,
23+
"text/plain": plain_loads,
24+
"application/octet-stream": binary_loads,
25+
"application/json": json_loads,
26+
"application/vnd.api+json": json_loads,
27+
"application/xml": xml_loads,
28+
"application/xhtml+xml": xml_loads,
29+
"application/x-www-form-urlencoded": urlencoded_form_loads,
30+
"multipart/form-data": data_form_loads,
31+
}
32+
)
2633

2734
media_type_deserializers_factory = MediaTypeDeserializersFactory(
35+
style_deserializers_factory,
2836
media_type_deserializers=media_type_deserializers,
2937
)
Lines changed: 159 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,182 @@
11
import warnings
22
from typing import Any
3+
from typing import Mapping
34
from typing import Optional
5+
from typing import cast
46
from xml.etree.ElementTree import ParseError
57

8+
from jsonschema_path import SchemaPath
9+
610
from openapi_core.deserializing.media_types.datatypes import (
711
DeserializerCallable,
812
)
13+
from openapi_core.deserializing.media_types.datatypes import (
14+
MediaTypeDeserializersDict,
15+
)
916
from openapi_core.deserializing.media_types.exceptions import (
1017
MediaTypeDeserializeError,
1118
)
19+
from openapi_core.deserializing.styles.factories import (
20+
StyleDeserializersFactory,
21+
)
22+
from openapi_core.schema.encodings import get_content_type
23+
from openapi_core.schema.parameters import get_style_and_explode
24+
from openapi_core.schema.protocols import SuportsGetAll
25+
from openapi_core.schema.protocols import SuportsGetList
26+
from openapi_core.schema.schemas import get_properties
27+
28+
29+
class MediaTypesDeserializer:
30+
def __init__(
31+
self,
32+
media_type_deserializers: Optional[MediaTypeDeserializersDict] = None,
33+
extra_media_type_deserializers: Optional[
34+
MediaTypeDeserializersDict
35+
] = None,
36+
):
37+
if media_type_deserializers is None:
38+
media_type_deserializers = {}
39+
self.media_type_deserializers = media_type_deserializers
40+
if extra_media_type_deserializers is None:
41+
extra_media_type_deserializers = {}
42+
self.extra_media_type_deserializers = extra_media_type_deserializers
43+
44+
def deserialize(self, mimetype: str, value: Any, **parameters: str) -> Any:
45+
deserializer_callable = self.get_deserializer_callable(mimetype)
46+
47+
try:
48+
return deserializer_callable(value, **parameters)
49+
except (ParseError, ValueError, TypeError, AttributeError):
50+
raise MediaTypeDeserializeError(mimetype, value)
51+
52+
def get_deserializer_callable(
53+
self,
54+
mimetype: str,
55+
) -> DeserializerCallable:
56+
if mimetype in self.extra_media_type_deserializers:
57+
return self.extra_media_type_deserializers[mimetype]
58+
return self.media_type_deserializers[mimetype]
1259

1360

14-
class CallableMediaTypeDeserializer:
61+
class MediaTypeDeserializer:
1562
def __init__(
1663
self,
64+
style_deserializers_factory: StyleDeserializersFactory,
65+
media_types_deserializer: MediaTypesDeserializer,
1766
mimetype: str,
18-
deserializer_callable: Optional[DeserializerCallable] = None,
67+
schema: Optional[SchemaPath] = None,
68+
encoding: Optional[SchemaPath] = None,
1969
**parameters: str,
2070
):
71+
self.style_deserializers_factory = style_deserializers_factory
72+
self.media_types_deserializer = media_types_deserializer
2173
self.mimetype = mimetype
22-
self.deserializer_callable = deserializer_callable
74+
self.schema = schema
75+
self.encoding = encoding
2376
self.parameters = parameters
2477

2578
def deserialize(self, value: Any) -> Any:
26-
if self.deserializer_callable is None:
27-
warnings.warn(f"Unsupported {self.mimetype} mimetype")
28-
return value
79+
deserialized = self.media_types_deserializer.deserialize(
80+
self.mimetype, value, **self.parameters
81+
)
2982

30-
try:
31-
return self.deserializer_callable(value, **self.parameters)
32-
except (ParseError, ValueError, TypeError, AttributeError):
33-
raise MediaTypeDeserializeError(self.mimetype, value)
83+
if (
84+
self.mimetype != "application/x-www-form-urlencoded"
85+
and not self.mimetype.startswith("multipart")
86+
):
87+
return deserialized
88+
89+
# decode multipart request bodies
90+
return self.decode(deserialized)
91+
92+
def evolve(
93+
self, mimetype: str, schema: Optional[SchemaPath]
94+
) -> "MediaTypeDeserializer":
95+
cls = self.__class__
96+
97+
return cls(
98+
self.style_deserializers_factory,
99+
self.media_types_deserializer,
100+
mimetype,
101+
schema=schema,
102+
)
103+
104+
def decode(self, location: Mapping[str, Any]) -> Mapping[str, Any]:
105+
# schema is required for multipart
106+
assert self.schema is not None
107+
schema_props = self.schema.get("properties")
108+
properties = {}
109+
for prop_name, prop_schema in get_properties(self.schema).items():
110+
try:
111+
properties[prop_name] = self.decode_property(
112+
prop_name, prop_schema, location
113+
)
114+
except KeyError:
115+
if "default" not in prop_schema:
116+
continue
117+
properties[prop_name] = prop_schema["default"]
118+
119+
return properties
120+
121+
def decode_property(
122+
self,
123+
prop_name: str,
124+
prop_schema: SchemaPath,
125+
location: Mapping[str, Any],
126+
) -> Any:
127+
if self.encoding is None or prop_name not in self.encoding:
128+
return self.decode_property_content_type(
129+
prop_name, prop_schema, location
130+
)
131+
132+
prep_encoding = self.encoding / prop_name
133+
if (
134+
"style" not in prep_encoding
135+
and "explode" not in prep_encoding
136+
and "allowReserved" not in prep_encoding
137+
):
138+
return self.decode_property_content_type(
139+
prop_name, prop_schema, location, prep_encoding
140+
)
141+
142+
return self.decode_property_style(
143+
prop_name, prop_schema, location, prep_encoding
144+
)
145+
146+
def decode_property_style(
147+
self,
148+
prop_name: str,
149+
prop_schema: SchemaPath,
150+
location: Mapping[str, Any],
151+
prep_encoding: SchemaPath,
152+
) -> Any:
153+
prop_style, prop_explode = get_style_and_explode(
154+
prep_encoding, default_location="query"
155+
)
156+
prop_deserializer = self.style_deserializers_factory.create(
157+
prop_style, prop_explode, prop_schema, name=prop_name
158+
)
159+
return prop_deserializer.deserialize(location)
160+
161+
def decode_property_content_type(
162+
self,
163+
prop_name: str,
164+
prop_schema: SchemaPath,
165+
location: Mapping[str, Any],
166+
prep_encoding: Optional[SchemaPath] = None,
167+
) -> Any:
168+
prop_content_type = get_content_type(prop_schema, prep_encoding)
169+
prop_deserializer = self.evolve(
170+
prop_content_type,
171+
prop_schema,
172+
)
173+
prop_schema_type = prop_schema.getkey("type", "")
174+
if prop_schema_type == "array":
175+
if isinstance(location, SuportsGetAll):
176+
value = location.getall(prop_name)
177+
if isinstance(location, SuportsGetList):
178+
value = location.getlist(prop_name)
179+
return list(map(prop_deserializer.deserialize, value))
180+
else:
181+
value = location[prop_name]
182+
return prop_deserializer.deserialize(value)

openapi_core/deserializing/media_types/factories.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,60 @@
11
from typing import Mapping
22
from typing import Optional
33

4+
from jsonschema_path import SchemaPath
5+
46
from openapi_core.deserializing.media_types.datatypes import (
57
DeserializerCallable,
68
)
79
from openapi_core.deserializing.media_types.datatypes import (
810
MediaTypeDeserializersDict,
911
)
1012
from openapi_core.deserializing.media_types.deserializers import (
11-
CallableMediaTypeDeserializer,
13+
MediaTypeDeserializer,
14+
)
15+
from openapi_core.deserializing.media_types.deserializers import (
16+
MediaTypesDeserializer,
17+
)
18+
from openapi_core.deserializing.styles.factories import (
19+
StyleDeserializersFactory,
1220
)
1321

1422

1523
class MediaTypeDeserializersFactory:
1624
def __init__(
1725
self,
26+
style_deserializers_factory: StyleDeserializersFactory,
1827
media_type_deserializers: Optional[MediaTypeDeserializersDict] = None,
1928
):
29+
self.style_deserializers_factory = style_deserializers_factory
2030
if media_type_deserializers is None:
2131
media_type_deserializers = {}
2232
self.media_type_deserializers = media_type_deserializers
2333

2434
def create(
2535
self,
2636
mimetype: str,
37+
schema: Optional[SchemaPath] = None,
2738
parameters: Optional[Mapping[str, str]] = None,
39+
encoding: Optional[SchemaPath] = None,
2840
extra_media_type_deserializers: Optional[
2941
MediaTypeDeserializersDict
3042
] = None,
31-
) -> CallableMediaTypeDeserializer:
43+
) -> MediaTypeDeserializer:
3244
if parameters is None:
3345
parameters = {}
3446
if extra_media_type_deserializers is None:
3547
extra_media_type_deserializers = {}
36-
deserialize_callable = self.get_deserializer_callable(
37-
mimetype,
38-
extra_media_type_deserializers=extra_media_type_deserializers,
48+
media_types_deserializer = MediaTypesDeserializer(
49+
self.media_type_deserializers,
50+
extra_media_type_deserializers,
3951
)
4052

41-
return CallableMediaTypeDeserializer(
42-
mimetype, deserialize_callable, **parameters
53+
return MediaTypeDeserializer(
54+
self.style_deserializers_factory,
55+
media_types_deserializer,
56+
mimetype,
57+
schema=schema,
58+
encoding=encoding,
59+
**parameters,
4360
)
44-
45-
def get_deserializer_callable(
46-
self,
47-
mimetype: str,
48-
extra_media_type_deserializers: MediaTypeDeserializersDict,
49-
) -> Optional[DeserializerCallable]:
50-
if mimetype in extra_media_type_deserializers:
51-
return extra_media_type_deserializers[mimetype]
52-
return self.media_type_deserializers.get(mimetype)
Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
11
from email.parser import Parser
2+
from json import loads
23
from typing import Any
3-
from typing import Dict
4+
from typing import Mapping
45
from typing import Union
56
from urllib.parse import parse_qsl
7+
from xml.etree.ElementTree import Element
8+
from xml.etree.ElementTree import fromstring
9+
10+
from werkzeug.datastructures import ImmutableMultiDict
11+
12+
13+
def binary_loads(value: Union[str, bytes], **parameters: str) -> bytes:
14+
charset = "utf-8"
15+
if "charset" in parameters:
16+
charset = parameters["charset"]
17+
if isinstance(value, str):
18+
return value.encode(charset)
19+
return value
620

721

822
def plain_loads(value: Union[str, bytes], **parameters: str) -> str:
@@ -18,20 +32,37 @@ def plain_loads(value: Union[str, bytes], **parameters: str) -> str:
1832
return value
1933

2034

21-
def urlencoded_form_loads(value: Any, **parameters: str) -> Dict[str, Any]:
35+
def json_loads(value: Union[str, bytes], **parameters: str) -> Any:
36+
return loads(value)
37+
38+
39+
def xml_loads(value: Union[str, bytes], **parameters: str) -> Element:
40+
return fromstring(value)
41+
42+
43+
def urlencoded_form_loads(value: Any, **parameters: str) -> Mapping[str, Any]:
2244
return dict(parse_qsl(value))
2345

2446

2547
def data_form_loads(
2648
value: Union[str, bytes], **parameters: str
27-
) -> Dict[str, Any]:
49+
) -> Mapping[str, Any]:
2850
if isinstance(value, bytes):
2951
value = value.decode("ASCII", errors="surrogateescape")
52+
boundary = ""
53+
if "boundary" in parameters:
54+
boundary = parameters["boundary"]
3055
parser = Parser()
31-
parts = parser.parsestr(value, headersonly=False)
32-
return {
33-
part.get_param("name", header="content-disposition"): part.get_payload(
34-
decode=True
35-
)
36-
for part in parts.get_payload()
37-
}
56+
mimetype = "multipart/form-data"
57+
header = f'Content-Type: {mimetype}; boundary="{boundary}"'
58+
text = "\n\n".join([header, value])
59+
parts = parser.parsestr(text, headersonly=False)
60+
return ImmutableMultiDict(
61+
[
62+
(
63+
part.get_param("name", header="content-disposition"),
64+
part.get_payload(decode=True),
65+
)
66+
for part in parts.get_payload()
67+
]
68+
)

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