starmallow 0.6.2__tar.gz → 0.6.4__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.
- {starmallow-0.6.2 → starmallow-0.6.4}/PKG-INFO +1 -1
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/__init__.py +1 -1
- starmallow-0.6.4/starmallow/background.py +29 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/endpoint.py +1 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/request_resolver.py +7 -6
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/routing.py +6 -1
- starmallow-0.6.4/tests/test_annotated.py +527 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_input.py +1 -0
- starmallow-0.6.2/tests/test_annotated.py +0 -244
- {starmallow-0.6.2 → starmallow-0.6.4}/.editorconfig +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/.gitignore +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/.pre-commit-config.yaml +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/Dockerfile +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/LICENSE.md +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/README.md +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/docker-compose.yml +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/docs/design_ideas.md +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/examples/__init__.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/examples/cache_server.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/examples/flask_server.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/examples/goals.ipynb +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/examples/gunicorn.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/examples/recommended_server.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/examples/sample_server.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/pyproject.toml +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/applications.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/concurrency.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/constants.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/dataclasses.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/datastructures.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/decorators.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/delimited_field.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/docs.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/endpoints.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/exception_handlers.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/exceptions.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/ext/__init__.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/ext/marshmallow/__init__.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/ext/marshmallow/openapi.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/fields.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/middleware/__init__.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/middleware/asyncexitstack.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/params.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/requests.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/responses.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/schema_generator.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/security/__init__.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/security/api_key.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/security/base.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/security/http.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/security/oauth2.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/security/open_id_connect_url.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/security/utils.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/serializers.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/types.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/utils.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/websockets.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/__init__.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/basic_api.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/__init__.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/__init__.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_cookie.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_cookie_description.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_cookie_optional.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_header.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_header_description.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_header_optional.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_query.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_query_description.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_query_optional.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/__init__.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_base.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_base_description.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_base_optional.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_basic.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_basic_realm.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_basic_realm_description.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_bearer.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_bearer_description.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_bearer_optional.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_digest.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_digest_description.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_digest_optional.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/oauth2/__init__.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/oauth2/test_oauth2.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/oauth2/test_oauth2_authorization_code_bearer.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/oauth2/test_oauth2_authorization_code_bearer_description.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/oauth2/test_oauth2_optional.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/oauth2/test_oauth2_optional_description.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/oauth2/test_oauth2_password_bearer_optional.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/oauth2/test_oauth2_password_bearer_optional_description.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/openid_connect/__init__.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/openid_connect/test_openid_connect.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/openid_connect/test_openid_connect_description.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/openid_connect/test_openid_connect_optional.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_additional_properties.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_additional_response_extra.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_additional_responses_bad.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_additional_responses_custom_model_in_callback.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_additional_responses_custom_validationerror.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_additional_responses_default_validationerror.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_additional_responses_response_class.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_additional_responses_router.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_basic_api.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_dataclass_fields.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_delimited_params.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_http_endpoints.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_middleware.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_requests_orjson.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_requests_ujson.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_resolved_param_contextmanagers.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_resolved_params.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_responses.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_responses_orjson.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_responses_ujson.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_ws_router.py +0 -0
- {starmallow-0.6.2 → starmallow-0.6.4}/tests/utils.py +0 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
import typing
|
2
|
+
from logging import getLogger
|
3
|
+
|
4
|
+
from starlette.background import BackgroundTask as StarletteBackgroundTask
|
5
|
+
from starlette.background import BackgroundTasks as StarletteBackgroundTasks
|
6
|
+
from starlette.background import P
|
7
|
+
from starlette.concurrency import run_in_threadpool
|
8
|
+
|
9
|
+
logger = getLogger(__name__)
|
10
|
+
|
11
|
+
|
12
|
+
class BackgroundTask(StarletteBackgroundTask):
|
13
|
+
|
14
|
+
async def __call__(self) -> None:
|
15
|
+
try:
|
16
|
+
if self.is_async:
|
17
|
+
await self.func(*self.args, **self.kwargs)
|
18
|
+
else:
|
19
|
+
await run_in_threadpool(self.func, *self.args, **self.kwargs)
|
20
|
+
except BaseException as e:
|
21
|
+
logger.exception(f'Background Task {self.func} failed: {e}')
|
22
|
+
|
23
|
+
|
24
|
+
class BackgroundTasks(StarletteBackgroundTasks):
|
25
|
+
def add_task(
|
26
|
+
self, func: typing.Callable[P, typing.Any], *args: P.args, **kwargs: P.kwargs
|
27
|
+
) -> None:
|
28
|
+
task = BackgroundTask(func, *args, **kwargs)
|
29
|
+
self.tasks.append(task)
|
@@ -211,6 +211,7 @@ class EndpointMixin:
|
|
211
211
|
kwargs.update({
|
212
212
|
'load_default': None,
|
213
213
|
'required': False,
|
214
|
+
'allow_none': True, # Even if a default is provided, we should also allow None
|
214
215
|
})
|
215
216
|
# This does not support Union[A,B,C,None]. Only Union[A,None] and Optional[A]
|
216
217
|
model = next((a for a in get_args(type_annotation) if a is not None), None)
|
@@ -8,13 +8,14 @@ import marshmallow as ma
|
|
8
8
|
import marshmallow.fields as mf
|
9
9
|
from marshmallow.error_store import ErrorStore
|
10
10
|
from marshmallow.utils import missing as missing_
|
11
|
-
from starlette.background import BackgroundTasks
|
11
|
+
from starlette.background import BackgroundTasks as StarletteBackgroundTasks
|
12
12
|
from starlette.datastructures import FormData, Headers, QueryParams
|
13
13
|
from starlette.exceptions import HTTPException
|
14
14
|
from starlette.requests import HTTPConnection, Request
|
15
15
|
from starlette.responses import Response
|
16
16
|
from starlette.websockets import WebSocket
|
17
17
|
|
18
|
+
from starmallow.background import BackgroundTasks
|
18
19
|
from starmallow.params import Param, ParamType, ResolvedParam
|
19
20
|
from starmallow.utils import (
|
20
21
|
is_async_gen_callable,
|
@@ -113,7 +114,7 @@ def request_params_to_args(
|
|
113
114
|
async def resolve_basic_args(
|
114
115
|
request: Request | WebSocket,
|
115
116
|
response: Response,
|
116
|
-
background_tasks:
|
117
|
+
background_tasks: StarletteBackgroundTasks,
|
117
118
|
params: Dict[ParamType, Dict[str, Param]],
|
118
119
|
):
|
119
120
|
path_values, path_errors = request_params_to_args(
|
@@ -183,7 +184,7 @@ async def resolve_basic_args(
|
|
183
184
|
values[param_name] = request
|
184
185
|
elif lenient_issubclass(param_type, Response):
|
185
186
|
values[param_name] = response
|
186
|
-
elif lenient_issubclass(param_type,
|
187
|
+
elif lenient_issubclass(param_type, StarletteBackgroundTasks):
|
187
188
|
values[param_name] = background_tasks
|
188
189
|
|
189
190
|
return values, errors
|
@@ -217,7 +218,7 @@ async def call_resolver(
|
|
217
218
|
async def resolve_subparams(
|
218
219
|
request: Request | WebSocket,
|
219
220
|
response: Response,
|
220
|
-
background_tasks:
|
221
|
+
background_tasks: StarletteBackgroundTasks,
|
221
222
|
params: Dict[str, ResolvedParam],
|
222
223
|
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]],
|
223
224
|
) -> Dict[str, Any]:
|
@@ -250,10 +251,10 @@ async def resolve_subparams(
|
|
250
251
|
async def resolve_params(
|
251
252
|
request: Request | WebSocket,
|
252
253
|
params: Dict[ParamType, Dict[str, Param]],
|
253
|
-
background_tasks: Optional[
|
254
|
+
background_tasks: Optional[StarletteBackgroundTasks] = None,
|
254
255
|
response: Optional[Response] = None,
|
255
256
|
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
|
256
|
-
) -> Tuple[Dict[str, Any], Dict[str, Union[Any, List, Dict]],
|
257
|
+
) -> Tuple[Dict[str, Any], Dict[str, Union[Any, List, Dict]], StarletteBackgroundTasks, Response]:
|
257
258
|
dependency_cache = dependency_cache or {}
|
258
259
|
|
259
260
|
if response is None:
|
@@ -96,7 +96,12 @@ def request_response(
|
|
96
96
|
response = await run_in_threadpool(func, request)
|
97
97
|
await response(scope, receive, send)
|
98
98
|
|
99
|
-
|
99
|
+
try:
|
100
|
+
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
|
101
|
+
except RuntimeError as e:
|
102
|
+
# This likely means that the exception was thrown by a background task
|
103
|
+
# after the response has been successfully send to the client
|
104
|
+
logger.exception(f'Runtime error occurred: {e}')
|
100
105
|
|
101
106
|
return app
|
102
107
|
|
@@ -0,0 +1,527 @@
|
|
1
|
+
'''Test Resolved Params'''
|
2
|
+
import datetime as dt
|
3
|
+
from typing import Annotated, Literal, Optional, Union
|
4
|
+
|
5
|
+
import marshmallow as ma
|
6
|
+
import marshmallow.fields as mf
|
7
|
+
import pytest
|
8
|
+
from marshmallow_dataclass import dataclass as ma_dataclass
|
9
|
+
from starlette.testclient import TestClient
|
10
|
+
|
11
|
+
from starmallow import Body, Header, Path, Query, ResolvedParam, StarMallow
|
12
|
+
|
13
|
+
from .utils import assert_json
|
14
|
+
|
15
|
+
app = StarMallow()
|
16
|
+
|
17
|
+
|
18
|
+
############################################################
|
19
|
+
# Models - classes and schemas
|
20
|
+
############################################################
|
21
|
+
# region - VS Code folding marker - https://code.visualstudio.com/docs/editor/codebasics#_folding
|
22
|
+
def paging_parameters(
|
23
|
+
offset: Annotated[int, Query(0)],
|
24
|
+
limit: Annotated[int, Query()] = 1000,
|
25
|
+
):
|
26
|
+
return {"offset": offset, "limit": limit}
|
27
|
+
|
28
|
+
|
29
|
+
def search_parameters(q: Annotated[str, Path()]):
|
30
|
+
return {"q": q}
|
31
|
+
|
32
|
+
|
33
|
+
# To test nested resolved params
|
34
|
+
def searchable_page_parameters(
|
35
|
+
paging_params: Annotated[dict[str, int], ResolvedParam(paging_parameters)],
|
36
|
+
search_params: Annotated[dict[str, str], ResolvedParam(search_parameters)],
|
37
|
+
):
|
38
|
+
return {
|
39
|
+
"offset": paging_params["offset"],
|
40
|
+
"limit": paging_params["limit"],
|
41
|
+
"q": search_params["q"],
|
42
|
+
}
|
43
|
+
|
44
|
+
|
45
|
+
@ma_dataclass
|
46
|
+
class PathParams:
|
47
|
+
item_id: int
|
48
|
+
|
49
|
+
|
50
|
+
# For testing with native marshmallow schemas
|
51
|
+
class PathParamsSchema(ma.Schema):
|
52
|
+
item_id = mf.Integer()
|
53
|
+
|
54
|
+
|
55
|
+
@ma_dataclass
|
56
|
+
class MultiPathParams:
|
57
|
+
item_id: int
|
58
|
+
sub_item_id: int
|
59
|
+
|
60
|
+
|
61
|
+
@ma_dataclass
|
62
|
+
class MultiQueryParams:
|
63
|
+
name: str
|
64
|
+
|
65
|
+
|
66
|
+
@ma_dataclass
|
67
|
+
class MultiBodyParams:
|
68
|
+
weight: float
|
69
|
+
|
70
|
+
|
71
|
+
@ma_dataclass
|
72
|
+
class MultiHeaderParams:
|
73
|
+
color: str = 'blue'
|
74
|
+
# endregion
|
75
|
+
# endregion
|
76
|
+
|
77
|
+
|
78
|
+
############################################################
|
79
|
+
# Test API
|
80
|
+
############################################################
|
81
|
+
# region
|
82
|
+
@app.get("/paging")
|
83
|
+
def get_paging(paging_params: Annotated[dict[str, int], ResolvedParam(paging_parameters)]):
|
84
|
+
return paging_params
|
85
|
+
|
86
|
+
|
87
|
+
@app.get("/filtered_paging_1/{q}")
|
88
|
+
def get_filtered_paging_1(search_params: Annotated[dict[str, str], ResolvedParam(search_parameters)]):
|
89
|
+
return search_params
|
90
|
+
|
91
|
+
|
92
|
+
@app.get("/filtered_paging_2/{q}")
|
93
|
+
def get_filtered_paging_2(filtered_paging_params: Annotated[dict[str, int | str], ResolvedParam(searchable_page_parameters)]):
|
94
|
+
return filtered_paging_params
|
95
|
+
|
96
|
+
|
97
|
+
@app.post("/optional_with_default")
|
98
|
+
def post_optional_with_default(
|
99
|
+
optional_body: Annotated[dt.datetime | None, Body()] = dt.datetime.max,
|
100
|
+
):
|
101
|
+
return {'optional_body': optional_body}
|
102
|
+
|
103
|
+
|
104
|
+
@app.post('/multi_combo_optional/{item_id}/{sub_item_id}')
|
105
|
+
def post_multi_combo_optional(
|
106
|
+
item_id: int,
|
107
|
+
path_params: Annotated[MultiPathParams, Path()],
|
108
|
+
query_params: Annotated[MultiQueryParams | None, Query()],
|
109
|
+
body_params: Annotated[MultiBodyParams | None, Body()],
|
110
|
+
weight_unit: Annotated[Optional[Literal['lbs', 'kg']], Query(title='Weight')],
|
111
|
+
color: Annotated[Union[str, None], Header('blue')],
|
112
|
+
# Tests convert_underscores
|
113
|
+
user_agent: Annotated[Optional[str], Header(None)],
|
114
|
+
# Tests aliasing
|
115
|
+
aliased_header: Annotated[Optional[str], Header(None, alias="myalias")],
|
116
|
+
):
|
117
|
+
return {
|
118
|
+
'item_id': item_id,
|
119
|
+
'param_item_id': path_params.item_id,
|
120
|
+
'sub_item': path_params.sub_item_id,
|
121
|
+
# Special None response to signify that the entire object is None
|
122
|
+
'name': query_params.name if query_params is not None else '__NONE__',
|
123
|
+
'weight': body_params.weight if body_params is not None else '__NONE__',
|
124
|
+
'weight_unit': weight_unit,
|
125
|
+
'color': color,
|
126
|
+
'user_agent': user_agent,
|
127
|
+
'aliased_header': aliased_header,
|
128
|
+
}
|
129
|
+
# endregion
|
130
|
+
|
131
|
+
|
132
|
+
############################################################
|
133
|
+
# Tests
|
134
|
+
############################################################
|
135
|
+
# region
|
136
|
+
client = TestClient(app)
|
137
|
+
|
138
|
+
openapi_schema = {
|
139
|
+
"openapi": "3.0.2",
|
140
|
+
"info": {"title": "StarMallow", "version": "0.1.0"},
|
141
|
+
"paths": {
|
142
|
+
"/paging": {
|
143
|
+
"get": {
|
144
|
+
"responses": {
|
145
|
+
"200": {
|
146
|
+
"description": "Successful Response",
|
147
|
+
"content": {"application/json": {"schema": {}}},
|
148
|
+
},
|
149
|
+
"422": {
|
150
|
+
"description": "Validation Error",
|
151
|
+
"content": {
|
152
|
+
"application/json": {
|
153
|
+
"schema": {
|
154
|
+
"$ref": "#/components/schemas/HTTPValidationError",
|
155
|
+
},
|
156
|
+
},
|
157
|
+
},
|
158
|
+
},
|
159
|
+
},
|
160
|
+
"summary": "Get Paging",
|
161
|
+
"operationId": "get_paging_paging_get",
|
162
|
+
"parameters": [
|
163
|
+
{
|
164
|
+
"required": False,
|
165
|
+
"schema": {
|
166
|
+
"default": 0,
|
167
|
+
"type": "integer",
|
168
|
+
"title": "Offset",
|
169
|
+
},
|
170
|
+
"name": "offset",
|
171
|
+
"in": "query",
|
172
|
+
},
|
173
|
+
{
|
174
|
+
"required": False,
|
175
|
+
"schema": {
|
176
|
+
"default": 1000,
|
177
|
+
"type": "integer",
|
178
|
+
"title": "Limit",
|
179
|
+
},
|
180
|
+
"name": "limit",
|
181
|
+
"in": "query",
|
182
|
+
},
|
183
|
+
],
|
184
|
+
},
|
185
|
+
},
|
186
|
+
"/filtered_paging_1/{q}": {
|
187
|
+
"get": {
|
188
|
+
"responses": {
|
189
|
+
"200": {
|
190
|
+
"description": "Successful Response",
|
191
|
+
"content": {"application/json": {"schema": {}}},
|
192
|
+
},
|
193
|
+
"422": {
|
194
|
+
"description": "Validation Error",
|
195
|
+
"content": {
|
196
|
+
"application/json": {
|
197
|
+
"schema": {
|
198
|
+
"$ref": "#/components/schemas/HTTPValidationError",
|
199
|
+
},
|
200
|
+
},
|
201
|
+
},
|
202
|
+
},
|
203
|
+
},
|
204
|
+
"summary": "Get Filtered Paging 1",
|
205
|
+
"operationId": "get_filtered_paging_1_filtered_paging_1__q__get",
|
206
|
+
"parameters": [
|
207
|
+
{
|
208
|
+
"required": True,
|
209
|
+
"schema": {
|
210
|
+
"type": "string",
|
211
|
+
"title": "Q",
|
212
|
+
},
|
213
|
+
"name": "q",
|
214
|
+
"in": "path",
|
215
|
+
},
|
216
|
+
],
|
217
|
+
},
|
218
|
+
},
|
219
|
+
"/filtered_paging_2/{q}": {
|
220
|
+
"get": {
|
221
|
+
"responses": {
|
222
|
+
"200": {
|
223
|
+
"description": "Successful Response",
|
224
|
+
"content": {"application/json": {"schema": {}}},
|
225
|
+
},
|
226
|
+
"422": {
|
227
|
+
"description": "Validation Error",
|
228
|
+
"content": {
|
229
|
+
"application/json": {
|
230
|
+
"schema": {
|
231
|
+
"$ref": "#/components/schemas/HTTPValidationError",
|
232
|
+
},
|
233
|
+
},
|
234
|
+
},
|
235
|
+
},
|
236
|
+
},
|
237
|
+
"summary": "Get Filtered Paging 2",
|
238
|
+
"operationId": "get_filtered_paging_2_filtered_paging_2__q__get",
|
239
|
+
"parameters": [
|
240
|
+
{
|
241
|
+
"required": False,
|
242
|
+
"schema": {
|
243
|
+
"default": 0,
|
244
|
+
"type": "integer",
|
245
|
+
"title": "Offset",
|
246
|
+
},
|
247
|
+
"name": "offset",
|
248
|
+
"in": "query",
|
249
|
+
},
|
250
|
+
{
|
251
|
+
"required": False,
|
252
|
+
"schema": {
|
253
|
+
"default": 1000,
|
254
|
+
"type": "integer",
|
255
|
+
"title": "Limit",
|
256
|
+
},
|
257
|
+
"name": "limit",
|
258
|
+
"in": "query",
|
259
|
+
},
|
260
|
+
{
|
261
|
+
"required": True,
|
262
|
+
"schema": {
|
263
|
+
"type": "string",
|
264
|
+
"title": "Q",
|
265
|
+
},
|
266
|
+
"name": "q",
|
267
|
+
"in": "path",
|
268
|
+
},
|
269
|
+
],
|
270
|
+
},
|
271
|
+
},
|
272
|
+
"/optional_with_default": {
|
273
|
+
"post": {
|
274
|
+
"responses": {
|
275
|
+
"200": {
|
276
|
+
"description": "Successful Response",
|
277
|
+
"content": {"application/json": {"schema": {}}},
|
278
|
+
},
|
279
|
+
"422": {
|
280
|
+
"description": "Validation Error",
|
281
|
+
"content": {
|
282
|
+
"application/json": {
|
283
|
+
"schema": {
|
284
|
+
"$ref": "#/components/schemas/HTTPValidationError",
|
285
|
+
},
|
286
|
+
},
|
287
|
+
},
|
288
|
+
},
|
289
|
+
},
|
290
|
+
"summary": "Post Optional With Default",
|
291
|
+
"operationId": "post_optional_with_default_optional_with_default_post",
|
292
|
+
"requestBody": {
|
293
|
+
"content": {
|
294
|
+
"application/json": {
|
295
|
+
"schema": {
|
296
|
+
"$ref": "#/components/schemas/Body_post_optional_with_default_optional_with_default_post",
|
297
|
+
},
|
298
|
+
},
|
299
|
+
},
|
300
|
+
"required": True,
|
301
|
+
},
|
302
|
+
},
|
303
|
+
},
|
304
|
+
"/multi_combo_optional/{item_id}/{sub_item_id}": {
|
305
|
+
"post": {
|
306
|
+
"responses": {
|
307
|
+
"200": {
|
308
|
+
"description": "Successful Response",
|
309
|
+
"content": {"application/json": {"schema": {}}},
|
310
|
+
},
|
311
|
+
"422": {
|
312
|
+
"description": "Validation Error",
|
313
|
+
"content": {
|
314
|
+
"application/json": {
|
315
|
+
"schema": {
|
316
|
+
"$ref": "#/components/schemas/HTTPValidationError",
|
317
|
+
},
|
318
|
+
},
|
319
|
+
},
|
320
|
+
},
|
321
|
+
},
|
322
|
+
"summary": "Post Multi Combo Optional",
|
323
|
+
"operationId": "post_multi_combo_optional_multi_combo_optional__item_id___sub_item_id__post",
|
324
|
+
"parameters": [
|
325
|
+
{
|
326
|
+
'in': 'query',
|
327
|
+
'name': 'name',
|
328
|
+
'required': True,
|
329
|
+
'schema': {
|
330
|
+
'title': 'Name',
|
331
|
+
'type': 'string',
|
332
|
+
},
|
333
|
+
},
|
334
|
+
{
|
335
|
+
'in': 'query',
|
336
|
+
'name': 'weight_unit',
|
337
|
+
'required': False,
|
338
|
+
'schema': {
|
339
|
+
"default": None,
|
340
|
+
'enum': ['lbs', 'kg'],
|
341
|
+
"nullable": True,
|
342
|
+
'title': 'Weight',
|
343
|
+
},
|
344
|
+
},
|
345
|
+
{
|
346
|
+
'in': 'path',
|
347
|
+
'name': 'item_id',
|
348
|
+
'required': True,
|
349
|
+
'schema': {
|
350
|
+
'title': 'Item Id',
|
351
|
+
'type': 'integer',
|
352
|
+
},
|
353
|
+
},
|
354
|
+
{
|
355
|
+
'in': 'path',
|
356
|
+
'name': 'sub_item_id',
|
357
|
+
'required': True,
|
358
|
+
'schema': {
|
359
|
+
'title': 'Sub Item Id',
|
360
|
+
'type': 'integer',
|
361
|
+
},
|
362
|
+
},
|
363
|
+
{
|
364
|
+
'in': 'header',
|
365
|
+
'name': 'color',
|
366
|
+
'required': False,
|
367
|
+
'schema': {
|
368
|
+
'default': 'blue',
|
369
|
+
"nullable": True,
|
370
|
+
'title': 'Color',
|
371
|
+
'type': 'string',
|
372
|
+
},
|
373
|
+
},
|
374
|
+
{
|
375
|
+
'in': 'header',
|
376
|
+
'name': 'user_agent',
|
377
|
+
'required': False,
|
378
|
+
'schema': {
|
379
|
+
"default": None,
|
380
|
+
"nullable": True,
|
381
|
+
'title': 'User Agent',
|
382
|
+
'type': 'string',
|
383
|
+
},
|
384
|
+
},
|
385
|
+
{
|
386
|
+
'in': 'header',
|
387
|
+
'name': 'aliased_header',
|
388
|
+
'required': False,
|
389
|
+
'schema': {
|
390
|
+
"default": None,
|
391
|
+
"nullable": True,
|
392
|
+
'title': 'Myalias',
|
393
|
+
'type': 'string',
|
394
|
+
},
|
395
|
+
},
|
396
|
+
],
|
397
|
+
'requestBody': {
|
398
|
+
'content': {
|
399
|
+
'application/json': {
|
400
|
+
'schema': {'$ref': '#/components/schemas/MultiBodyParams'},
|
401
|
+
},
|
402
|
+
},
|
403
|
+
'required': False,
|
404
|
+
},
|
405
|
+
},
|
406
|
+
},
|
407
|
+
},
|
408
|
+
"components": {
|
409
|
+
"schemas": {
|
410
|
+
"Body_post_optional_with_default_optional_with_default_post": {
|
411
|
+
"properties": {
|
412
|
+
"optional_body": {
|
413
|
+
"default": "9999-12-31T23:59:59.999999",
|
414
|
+
"format": "date-time",
|
415
|
+
"nullable": True,
|
416
|
+
"title": "Optional Body",
|
417
|
+
"type": "string",
|
418
|
+
},
|
419
|
+
},
|
420
|
+
"required": [],
|
421
|
+
"title": "Body_post_optional_with_default_optional_with_default_post",
|
422
|
+
"type": "object",
|
423
|
+
},
|
424
|
+
"HTTPValidationError": {
|
425
|
+
'properties': {
|
426
|
+
'detail': {
|
427
|
+
'description': 'Error detail',
|
428
|
+
'title': 'Detail',
|
429
|
+
},
|
430
|
+
'errors': {
|
431
|
+
'description': 'Exception or error type',
|
432
|
+
'title': 'Errors',
|
433
|
+
},
|
434
|
+
'status_code': {
|
435
|
+
'description': 'HTTP status code',
|
436
|
+
'title': 'Status Code',
|
437
|
+
'type': 'integer',
|
438
|
+
},
|
439
|
+
},
|
440
|
+
'required': ['detail', 'status_code'],
|
441
|
+
'title': 'HTTPValidationError',
|
442
|
+
'type': 'object',
|
443
|
+
},
|
444
|
+
"MultiBodyParams": {
|
445
|
+
"type": "object",
|
446
|
+
"properties": {
|
447
|
+
"weight": {
|
448
|
+
"type": "number",
|
449
|
+
"title": "Weight",
|
450
|
+
},
|
451
|
+
},
|
452
|
+
"required": [
|
453
|
+
"weight",
|
454
|
+
],
|
455
|
+
"title": "Body Params",
|
456
|
+
},
|
457
|
+
},
|
458
|
+
},
|
459
|
+
}
|
460
|
+
|
461
|
+
|
462
|
+
@pytest.mark.parametrize(
|
463
|
+
"path,expected_status,expected_response",
|
464
|
+
[
|
465
|
+
("/paging?limit=50", 200, {"offset": 0, "limit": 50}),
|
466
|
+
("/filtered_paging_1/name=foobar", 200, {"q": "name=foobar"}),
|
467
|
+
(
|
468
|
+
"/filtered_paging_2/name=foobar?limit=50", 200,
|
469
|
+
{"offset": 0, "limit": 50, "q": "name=foobar"},
|
470
|
+
),
|
471
|
+
("/openapi.json", 200, openapi_schema),
|
472
|
+
],
|
473
|
+
)
|
474
|
+
def test_get_path(path, expected_status, expected_response):
|
475
|
+
response = client.get(path)
|
476
|
+
assert response.status_code == expected_status
|
477
|
+
assert_json(response.json(), expected_response)
|
478
|
+
|
479
|
+
|
480
|
+
@pytest.mark.parametrize(
|
481
|
+
"path,headers,body,expected_status,expected_response",
|
482
|
+
[
|
483
|
+
(
|
484
|
+
"/optional_with_default",
|
485
|
+
{},
|
486
|
+
{},
|
487
|
+
200,
|
488
|
+
{
|
489
|
+
'optional_body': '9999-12-31T23:59:59.999999',
|
490
|
+
},
|
491
|
+
),
|
492
|
+
(
|
493
|
+
"/optional_with_default",
|
494
|
+
{},
|
495
|
+
{
|
496
|
+
'optional_body': None,
|
497
|
+
},
|
498
|
+
200,
|
499
|
+
{
|
500
|
+
'optional_body': None,
|
501
|
+
},
|
502
|
+
),
|
503
|
+
(
|
504
|
+
"/multi_combo_optional/5/3",
|
505
|
+
{},
|
506
|
+
None,
|
507
|
+
200,
|
508
|
+
{
|
509
|
+
'item_id': 5,
|
510
|
+
'param_item_id': 5,
|
511
|
+
'sub_item': 3,
|
512
|
+
'name': '__NONE__',
|
513
|
+
'weight': '__NONE__',
|
514
|
+
'weight_unit': None,
|
515
|
+
'color': 'blue',
|
516
|
+
"user_agent": "testclient",
|
517
|
+
"aliased_header": None,
|
518
|
+
},
|
519
|
+
),
|
520
|
+
],
|
521
|
+
)
|
522
|
+
def test_post_path(path, headers, body, expected_status, expected_response):
|
523
|
+
response = client.post(path, headers=headers, json=body)
|
524
|
+
assert response.status_code == expected_status
|
525
|
+
assert_json(response.json(), expected_response)
|
526
|
+
|
527
|
+
# endregion
|