Skip to content

Commit 898a8d0

Browse files
authored
Merge pull request #427 from p1c2u/feature/starlette-support
Starlette support
2 parents 1f07fa2 + db3e222 commit 898a8d0

File tree

13 files changed

+452
-45
lines changed

13 files changed

+452
-45
lines changed

docs/index.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ Table of contents
3131

3232
installation
3333
usage
34-
extensions
35-
customizations
3634
integrations
35+
customizations
36+
extensions
3737

3838

3939
Related projects

docs/integrations.rst

+43-14
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The integration supports Django from version 3.0 and above.
1616
Middleware
1717
~~~~~~~~~~
1818

19-
Django can be integrated by middleware. Add `DjangoOpenAPIMiddleware` to your `MIDDLEWARE` list and define `OPENAPI_SPEC`.
19+
Django can be integrated by middleware. Add ``DjangoOpenAPIMiddleware`` to your ``MIDDLEWARE`` list and define ``OPENAPI_SPEC``.
2020

2121
.. code-block:: python
2222
@@ -52,7 +52,7 @@ After that you have access to validation result object with all validated reques
5252
Low level
5353
~~~~~~~~~
5454

55-
You can use `DjangoOpenAPIRequest` as a Django request factory:
55+
You can use ``DjangoOpenAPIRequest`` as a Django request factory:
5656

5757
.. code-block:: python
5858
@@ -62,7 +62,7 @@ You can use `DjangoOpenAPIRequest` as a Django request factory:
6262
openapi_request = DjangoOpenAPIRequest(django_request)
6363
result = openapi_request_validator.validate(spec, openapi_request)
6464
65-
You can use `DjangoOpenAPIResponse` as a Django response factory:
65+
You can use ``DjangoOpenAPIResponse`` as a Django response factory:
6666

6767
.. code-block:: python
6868
@@ -82,7 +82,7 @@ The integration supports Falcon from version 3.0 and above.
8282
Middleware
8383
~~~~~~~~~~
8484

85-
The Falcon API can be integrated by `FalconOpenAPIMiddleware` middleware.
85+
The Falcon API can be integrated by ``FalconOpenAPIMiddleware`` middleware.
8686

8787
.. code-block:: python
8888
@@ -111,7 +111,7 @@ After that you will have access to validation result object with all validated r
111111
Low level
112112
~~~~~~~~~
113113

114-
You can use `FalconOpenAPIRequest` as a Falcon request factory:
114+
You can use ``FalconOpenAPIRequest`` as a Falcon request factory:
115115

116116
.. code-block:: python
117117
@@ -121,7 +121,7 @@ You can use `FalconOpenAPIRequest` as a Falcon request factory:
121121
openapi_request = FalconOpenAPIRequest(falcon_request)
122122
result = openapi_request_validator.validate(spec, openapi_request)
123123
124-
You can use `FalconOpenAPIResponse` as a Falcon response factory:
124+
You can use ``FalconOpenAPIResponse`` as a Falcon response factory:
125125

126126
.. code-block:: python
127127
@@ -140,7 +140,7 @@ This section describes integration with `Flask <https://flask.palletsprojects.co
140140
Decorator
141141
~~~~~~~~~
142142

143-
Flask views can be integrated by `FlaskOpenAPIViewDecorator` decorator.
143+
Flask views can be integrated by ``FlaskOpenAPIViewDecorator`` decorator.
144144

145145
.. code-block:: python
146146
@@ -163,7 +163,7 @@ If you want to decorate class based view you can use the decorators attribute:
163163
View
164164
~~~~
165165

166-
As an alternative to the decorator-based integration, a Flask method based views can be integrated by inheritance from `FlaskOpenAPIView` class.
166+
As an alternative to the decorator-based integration, a Flask method based views can be integrated by inheritance from ``FlaskOpenAPIView`` class.
167167

168168
.. code-block:: python
169169
@@ -177,7 +177,7 @@ As an alternative to the decorator-based integration, a Flask method based views
177177
Request parameters
178178
~~~~~~~~~~~~~~~~~~
179179

180-
In Flask, all unmarshalled request data are provided as Flask request object's `openapi.parameters` attribute
180+
In Flask, all unmarshalled request data are provided as Flask request object's ``openapi.parameters`` attribute
181181

182182
.. code-block:: python
183183
@@ -192,7 +192,7 @@ In Flask, all unmarshalled request data are provided as Flask request object's `
192192
Low level
193193
~~~~~~~~~
194194

195-
You can use `FlaskOpenAPIRequest` as a Flask request factory:
195+
You can use ``FlaskOpenAPIRequest`` as a Flask request factory:
196196

197197
.. code-block:: python
198198
@@ -219,7 +219,7 @@ This section describes integration with `Requests <https://requests.readthedocs.
219219
Low level
220220
~~~~~~~~~
221221

222-
You can use `RequestsOpenAPIRequest` as a Requests request factory:
222+
You can use ``RequestsOpenAPIRequest`` as a Requests request factory:
223223

224224
.. code-block:: python
225225
@@ -229,7 +229,7 @@ You can use `RequestsOpenAPIRequest` as a Requests request factory:
229229
openapi_request = RequestsOpenAPIRequest(requests_request)
230230
result = openapi_request_validator.validate(spec, openapi_request)
231231
232-
You can use `RequestsOpenAPIResponse` as a Requests response factory:
232+
You can use ``RequestsOpenAPIResponse`` as a Requests response factory:
233233

234234
.. code-block:: python
235235
@@ -240,6 +240,35 @@ You can use `RequestsOpenAPIResponse` as a Requests response factory:
240240
result = openapi_respose_validator.validate(spec, openapi_request, openapi_response)
241241
242242
243+
Starlette
244+
---------
245+
246+
This section describes integration with `Starlette <https://www.starlette.io>`__ ASGI framework.
247+
248+
Low level
249+
~~~~~~~~~
250+
251+
You can use ``StarletteOpenAPIRequest`` as a Starlette request factory:
252+
253+
.. code-block:: python
254+
255+
from openapi_core.validation.request import openapi_request_validator
256+
from openapi_core.contrib.starlette import StarletteOpenAPIRequest
257+
258+
openapi_request = StarletteOpenAPIRequest(starlette_request)
259+
result = openapi_request_validator.validate(spec, openapi_request)
260+
261+
You can use ``StarletteOpenAPIResponse`` as a Starlette response factory:
262+
263+
.. code-block:: python
264+
265+
from openapi_core.validation.response import openapi_respose_validator
266+
from openapi_core.contrib.starlette import StarletteOpenAPIResponse
267+
268+
openapi_response = StarletteOpenAPIResponse(starlette_response)
269+
result = openapi_respose_validator.validate(spec, openapi_request, openapi_response)
270+
271+
243272
Tornado
244273
-------
245274

@@ -254,7 +283,7 @@ This section describes integration with `Werkzeug <https://werkzeug.palletsproje
254283
Low level
255284
~~~~~~~~~
256285

257-
You can use `WerkzeugOpenAPIRequest` as a Werkzeug request factory:
286+
You can use ``WerkzeugOpenAPIRequest`` as a Werkzeug request factory:
258287

259288
.. code-block:: python
260289
@@ -264,7 +293,7 @@ You can use `WerkzeugOpenAPIRequest` as a Werkzeug request factory:
264293
openapi_request = WerkzeugOpenAPIRequest(werkzeug_request)
265294
result = openapi_request_validator.validate(spec, openapi_request)
266295
267-
You can use `WerkzeugOpenAPIResponse` as a Werkzeug response factory:
296+
You can use ``WerkzeugOpenAPIResponse`` as a Werkzeug response factory:
268297

269298
.. code-block:: python
270299
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from openapi_core.contrib.starlette.requests import StarletteOpenAPIRequest
2+
from openapi_core.contrib.starlette.responses import StarletteOpenAPIResponse
3+
4+
__all__ = [
5+
"StarletteOpenAPIRequest",
6+
"StarletteOpenAPIResponse",
7+
]
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""OpenAPI core contrib starlette requests module"""
2+
from typing import Optional
3+
4+
from asgiref.sync import AsyncToSync
5+
from starlette.requests import Request
6+
7+
from openapi_core.validation.request.datatypes import RequestParameters
8+
9+
10+
class StarletteOpenAPIRequest:
11+
def __init__(self, request: Request):
12+
self.request = request
13+
14+
self.parameters = RequestParameters(
15+
query=self.request.query_params,
16+
header=self.request.headers,
17+
cookie=self.request.cookies,
18+
)
19+
20+
self._get_body = AsyncToSync(self.request.body, force_new_loop=True) # type: ignore
21+
22+
@property
23+
def host_url(self) -> str:
24+
return self.request.base_url._url
25+
26+
@property
27+
def path(self) -> str:
28+
return self.request.url.path
29+
30+
@property
31+
def method(self) -> str:
32+
return self.request.method.lower()
33+
34+
@property
35+
def body(self) -> Optional[str]:
36+
body = self._get_body()
37+
if body is None:
38+
return None
39+
if isinstance(body, bytes):
40+
return body.decode("utf-8")
41+
assert isinstance(body, str)
42+
return body
43+
44+
@property
45+
def mimetype(self) -> str:
46+
content_type = self.request.headers["Content-Type"]
47+
if content_type:
48+
return content_type.partition(";")[0]
49+
50+
return ""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""OpenAPI core contrib starlette responses module"""
2+
from starlette.datastructures import Headers
3+
from starlette.responses import Response
4+
5+
6+
class StarletteOpenAPIResponse:
7+
def __init__(self, response: Response):
8+
self.response = response
9+
10+
@property
11+
def data(self) -> str:
12+
if isinstance(self.response.body, bytes):
13+
return self.response.body.decode("utf-8")
14+
assert isinstance(self.response.body, str)
15+
return self.response.body
16+
17+
@property
18+
def status_code(self) -> int:
19+
return self.response.status_code
20+
21+
@property
22+
def mimetype(self) -> str:
23+
return self.response.media_type or ""
24+
25+
@property
26+
def headers(self) -> Headers:
27+
return self.response.headers

openapi_core/schema/parameters.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import re
22
from typing import Any
33
from typing import Dict
4+
from typing import Mapping
45
from typing import Optional
5-
from typing import Union
6-
7-
from werkzeug.datastructures import Headers
86

97
from openapi_core.schema.protocols import SuportsGetAll
108
from openapi_core.schema.protocols import SuportsGetList
@@ -49,7 +47,7 @@ def get_explode(param_or_header: Spec) -> bool:
4947

5048
def get_value(
5149
param_or_header: Spec,
52-
location: Union[Headers, Dict[str, Any]],
50+
location: Mapping[str, Any],
5351
name: Optional[str] = None,
5452
) -> Any:
5553
"""Returns parameter/header value from specific location"""
@@ -80,7 +78,7 @@ def get_value(
8078

8179

8280
def get_deep_object_value(
83-
location: Union[Headers, Dict[str, Any]],
81+
location: Mapping[str, Any],
8482
name: Optional[str] = None,
8583
) -> Dict[str, Any]:
8684
values = {}

openapi_core/validation/request/datatypes.py

+9-14
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
from dataclasses import dataclass
55
from dataclasses import field
66
from typing import Any
7-
from typing import Dict
8-
from typing import Optional
7+
from typing import Mapping
98

109
from werkzeug.datastructures import Headers
1110
from werkzeug.datastructures import ImmutableMultiDict
@@ -28,25 +27,21 @@ class RequestParameters:
2827
Path parameters as dict. Gets resolved against spec if empty.
2928
"""
3029

31-
query: ImmutableMultiDict[str, Any] = field(
32-
default_factory=ImmutableMultiDict
33-
)
34-
header: Headers = field(default_factory=Headers)
35-
cookie: ImmutableMultiDict[str, Any] = field(
36-
default_factory=ImmutableMultiDict
37-
)
38-
path: dict[str, Any] = field(default_factory=dict)
30+
query: Mapping[str, Any] = field(default_factory=ImmutableMultiDict)
31+
header: Mapping[str, Any] = field(default_factory=Headers)
32+
cookie: Mapping[str, Any] = field(default_factory=ImmutableMultiDict)
33+
path: Mapping[str, Any] = field(default_factory=dict)
3934

4035
def __getitem__(self, location: str) -> Any:
4136
return getattr(self, location)
4237

4338

4439
@dataclass
4540
class Parameters:
46-
query: dict[str, Any] = field(default_factory=dict)
47-
header: dict[str, Any] = field(default_factory=dict)
48-
cookie: dict[str, Any] = field(default_factory=dict)
49-
path: dict[str, Any] = field(default_factory=dict)
41+
query: Mapping[str, Any] = field(default_factory=dict)
42+
header: Mapping[str, Any] = field(default_factory=dict)
43+
cookie: Mapping[str, Any] = field(default_factory=dict)
44+
path: Mapping[str, Any] = field(default_factory=dict)
5045

5146

5247
@dataclass

openapi_core/validation/response/protocols.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""OpenAPI core validation response protocols module"""
22
from typing import TYPE_CHECKING
3+
from typing import Any
4+
from typing import Mapping
35
from typing import Optional
46

57
if TYPE_CHECKING:
@@ -13,8 +15,6 @@
1315
from typing_extensions import Protocol
1416
from typing_extensions import runtime_checkable
1517

16-
from werkzeug.datastructures import Headers
17-
1818
from openapi_core.spec import Spec
1919
from openapi_core.validation.request.protocols import Request
2020
from openapi_core.validation.response.datatypes import ResponseValidationResult
@@ -48,7 +48,7 @@ def mimetype(self) -> str:
4848
...
4949

5050
@property
51-
def headers(self) -> Headers:
51+
def headers(self) -> Mapping[str, Any]:
5252
...
5353

5454

openapi_core/validation/validators.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
"""OpenAPI core validation validators module"""
22
from typing import Any
3-
from typing import Dict
3+
from typing import Mapping
44
from typing import Optional
5-
from typing import Union
6-
from urllib.parse import urljoin
7-
8-
from werkzeug.datastructures import Headers
95

106
from openapi_core.casting.schemas import schema_casters_factory
117
from openapi_core.casting.schemas.factories import SchemaCastersFactory
@@ -82,7 +78,7 @@ def _unmarshal(self, schema: Spec, value: Any) -> Any:
8278
def _get_param_or_header_value(
8379
self,
8480
param_or_header: Spec,
85-
location: Union[Headers, Dict[str, Any]],
81+
location: Mapping[str, Any],
8682
name: Optional[str] = None,
8783
) -> Any:
8884
try:

0 commit comments

Comments
 (0)