Skip to content

Commit a323ca9

Browse files
committed
Support Falcon 4
1 parent 56ae5ef commit a323ca9

File tree

9 files changed

+94
-21
lines changed

9 files changed

+94
-21
lines changed

docs/integrations/falcon.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
# Falcon
22

3-
This section describes integration with [Falcon](https://falconframework.org) web framework.
4-
The integration supports Falcon from version 3.0 and above.
3+
This section describes the integration with the [Falcon](https://falconframework.org) web framework.
4+
The integration supports Falcon version 3.0 and above.
5+
6+
!!! warning
7+
8+
This integration does not support multipart form body requests.
9+
510

611
## Middleware
712

8-
The Falcon API can be integrated by `FalconOpenAPIMiddleware` middleware.
13+
The Falcon API can be integrated using the `FalconOpenAPIMiddleware`.
914

1015
``` python hl_lines="1 3 7"
1116
from openapi_core.contrib.falcon.middlewares import FalconOpenAPIMiddleware
@@ -34,7 +39,7 @@ app = falcon.App(
3439
)
3540
```
3641

37-
You can skip response validation process: by setting `response_cls` to `None`
42+
You can skip the response validation process by setting `response_cls` to `None`.
3843

3944
``` python hl_lines="5"
4045
from openapi_core.contrib.falcon.middlewares import FalconOpenAPIMiddleware
@@ -50,20 +55,20 @@ app = falcon.App(
5055
)
5156
```
5257

53-
After that you will have access to validation result object with all validated request data from Falcon view through request context.
58+
After this, you will have access to the validation result object, which contains all validated request data, from the Falcon view through the request context.
5459

5560
``` python
5661
class ThingsResource:
5762
def on_get(self, req, resp):
58-
# get parameters object with path, query, cookies and headers parameters
63+
# Get the parameters object with path, query, cookies, and headers parameters
5964
validated_params = req.context.openapi.parameters
60-
# or specific location parameters
65+
# Or specific location parameters
6166
validated_path_params = req.context.openapi.parameters.path
6267

63-
# get body
68+
# Get the body
6469
validated_body = req.context.openapi.body
6570

66-
# get security data
71+
# Get security data
6772
validated_security = req.context.openapi.security
6873
```
6974

openapi_core/contrib/falcon/requests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def body(self) -> Optional[bytes]:
6767
)
6868
try:
6969
body = handler.serialize(
70-
media, content_type=self.request.content_type
70+
media, content_type=self.content_type
7171
)
7272
# multipart form serialization is not supported
7373
except NotImplementedError:

openapi_core/contrib/falcon/responses.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""OpenAPI core contrib falcon responses module"""
22

3+
from io import BytesIO
34
from itertools import tee
5+
from typing import Iterable
46

57
from falcon.response import Response
68
from werkzeug.datastructures import Headers
@@ -17,16 +19,22 @@ def data(self) -> bytes:
1719
if self.response.text is None:
1820
if self.response.stream is None:
1921
return b""
20-
resp_iter1, resp_iter2 = tee(self.response.stream)
21-
self.response.stream = resp_iter1
22-
content = b"".join(resp_iter2)
23-
return content
22+
if isinstance(self.response.stream, Iterable):
23+
resp_iter1, resp_iter2 = tee(self.response.stream)
24+
self.response.stream = resp_iter1
25+
content = b"".join(resp_iter2)
26+
return content
27+
# checks ReadableIO protocol
28+
if hasattr(self.response.stream, "read"):
29+
data = self.response.stream.read()
30+
self.response.stream = BytesIO(data)
31+
return data
2432
assert isinstance(self.response.text, str)
2533
return self.response.text.encode("utf-8")
2634

2735
@property
2836
def status_code(self) -> int:
29-
return int(self.response.status[:3])
37+
return self.response.status_code
3038

3139
@property
3240
def content_type(self) -> str:

openapi_core/contrib/falcon/util.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from typing import Any
2-
from typing import Dict
32
from typing import Generator
3+
from typing import Mapping
44
from typing import Tuple
55

66

77
def unpack_params(
8-
params: Dict[str, Any]
8+
params: Mapping[str, Any]
99
) -> Generator[Tuple[str, Any], None, None]:
1010
for k, v in params.items():
1111
if isinstance(v, list):

tests/integration/contrib/django/test_django_project.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ def test_post_media_type_invalid(self, client):
184184
"title": (
185185
"Content for the following mimetype not found: "
186186
"text/html. "
187-
"Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'text/plain']"
187+
"Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain']"
188188
),
189189
}
190190
]

tests/integration/contrib/falcon/test_falcon_project.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from base64 import b64encode
22
from json import dumps
3+
from urllib3 import encode_multipart_formdata
34

45
import pytest
56

@@ -204,7 +205,7 @@ def test_post_media_type_invalid(self, client):
204205
"title": (
205206
"Content for the following mimetype not found: "
206207
f"{content_type}. "
207-
"Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'text/plain']"
208+
"Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain']"
208209
),
209210
}
210211
]
@@ -292,6 +293,43 @@ def test_post_valid(self, client, data_json):
292293
assert response.status_code == 201
293294
assert not response.content
294295

296+
@pytest.mark.xfail(
297+
reason="falcon multipart form serialization unsupported",
298+
strict=True,
299+
)
300+
def test_post_multipart_valid(self, client, data_gif):
301+
cookies = {"user": 1}
302+
auth = "authuser"
303+
fields = {
304+
"name": "Cat",
305+
"address": (
306+
"aaddress.json",
307+
dumps(dict(city="Warsaw")),
308+
"application/json",
309+
),
310+
"photo": (
311+
"photo.jpg",
312+
data_gif,
313+
"image/jpeg",
314+
),
315+
}
316+
body, content_type_header = encode_multipart_formdata(fields)
317+
headers = {
318+
"Authorization": f"Basic {auth}",
319+
"Content-Type": content_type_header,
320+
}
321+
322+
response = client.simulate_post(
323+
"/v1/pets",
324+
host="staging.gigantic-server.com",
325+
headers=headers,
326+
body=body,
327+
cookies=cookies,
328+
protocol="https",
329+
)
330+
331+
assert response.status_code == 200
332+
295333

296334
class TestPetDetailResource:
297335
def test_get_server_invalid(self, client):

tests/integration/contrib/fastapi/test_fastapi_project.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def test_post_media_type_invalid(self, client):
183183
"title": (
184184
"Content for the following mimetype not found: "
185185
"text/html. "
186-
"Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'text/plain']"
186+
"Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain']"
187187
),
188188
}
189189
]

tests/integration/contrib/starlette/test_starlette_project.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def test_post_media_type_invalid(self, client):
183183
"title": (
184184
"Content for the following mimetype not found: "
185185
"text/html. "
186-
"Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'text/plain']"
186+
"Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain']"
187187
),
188188
}
189189
]

tests/integration/data/v3.0/petstore.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ paths:
150150
application/x-www-form-urlencoded:
151151
schema:
152152
$ref: '#/components/schemas/PetCreate'
153+
multipart/form-data:
154+
schema:
155+
$ref: '#/components/schemas/PetWithPhotoCreate'
153156
text/plain: {}
154157
responses:
155158
'201':
@@ -375,6 +378,16 @@ components:
375378
oneOf:
376379
- $ref: "#/components/schemas/Cat"
377380
- $ref: "#/components/schemas/Bird"
381+
PetWithPhotoCreate:
382+
type: object
383+
x-model: PetWithPhotoCreate
384+
allOf:
385+
- $ref: "#/components/schemas/PetCreatePartOne"
386+
- $ref: "#/components/schemas/PetCreatePartTwo"
387+
- $ref: "#/components/schemas/PetCreatePartPhoto"
388+
oneOf:
389+
- $ref: "#/components/schemas/Cat"
390+
- $ref: "#/components/schemas/Bird"
378391
PetCreatePartOne:
379392
type: object
380393
x-model: PetCreatePartOne
@@ -395,6 +408,15 @@ components:
395408
$ref: "#/components/schemas/Position"
396409
healthy:
397410
type: boolean
411+
PetCreatePartPhoto:
412+
type: object
413+
x-model: PetCreatePartPhoto
414+
properties:
415+
photo:
416+
$ref: "#/components/schemas/PetPhoto"
417+
PetPhoto:
418+
type: string
419+
format: binary
398420
Bird:
399421
type: object
400422
x-model: Bird

0 commit comments

Comments
 (0)