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.
Files changed (117) hide show
  1. {starmallow-0.6.2 → starmallow-0.6.4}/PKG-INFO +1 -1
  2. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/__init__.py +1 -1
  3. starmallow-0.6.4/starmallow/background.py +29 -0
  4. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/endpoint.py +1 -0
  5. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/request_resolver.py +7 -6
  6. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/routing.py +6 -1
  7. starmallow-0.6.4/tests/test_annotated.py +527 -0
  8. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_input.py +1 -0
  9. starmallow-0.6.2/tests/test_annotated.py +0 -244
  10. {starmallow-0.6.2 → starmallow-0.6.4}/.editorconfig +0 -0
  11. {starmallow-0.6.2 → starmallow-0.6.4}/.gitignore +0 -0
  12. {starmallow-0.6.2 → starmallow-0.6.4}/.pre-commit-config.yaml +0 -0
  13. {starmallow-0.6.2 → starmallow-0.6.4}/Dockerfile +0 -0
  14. {starmallow-0.6.2 → starmallow-0.6.4}/LICENSE.md +0 -0
  15. {starmallow-0.6.2 → starmallow-0.6.4}/README.md +0 -0
  16. {starmallow-0.6.2 → starmallow-0.6.4}/docker-compose.yml +0 -0
  17. {starmallow-0.6.2 → starmallow-0.6.4}/docs/design_ideas.md +0 -0
  18. {starmallow-0.6.2 → starmallow-0.6.4}/examples/__init__.py +0 -0
  19. {starmallow-0.6.2 → starmallow-0.6.4}/examples/cache_server.py +0 -0
  20. {starmallow-0.6.2 → starmallow-0.6.4}/examples/flask_server.py +0 -0
  21. {starmallow-0.6.2 → starmallow-0.6.4}/examples/goals.ipynb +0 -0
  22. {starmallow-0.6.2 → starmallow-0.6.4}/examples/gunicorn.py +0 -0
  23. {starmallow-0.6.2 → starmallow-0.6.4}/examples/recommended_server.py +0 -0
  24. {starmallow-0.6.2 → starmallow-0.6.4}/examples/sample_server.py +0 -0
  25. {starmallow-0.6.2 → starmallow-0.6.4}/pyproject.toml +0 -0
  26. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/applications.py +0 -0
  27. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/concurrency.py +0 -0
  28. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/constants.py +0 -0
  29. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/dataclasses.py +0 -0
  30. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/datastructures.py +0 -0
  31. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/decorators.py +0 -0
  32. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/delimited_field.py +0 -0
  33. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/docs.py +0 -0
  34. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/endpoints.py +0 -0
  35. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/exception_handlers.py +0 -0
  36. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/exceptions.py +0 -0
  37. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/ext/__init__.py +0 -0
  38. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/ext/marshmallow/__init__.py +0 -0
  39. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/ext/marshmallow/openapi.py +0 -0
  40. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/fields.py +0 -0
  41. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/middleware/__init__.py +0 -0
  42. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/middleware/asyncexitstack.py +0 -0
  43. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/params.py +0 -0
  44. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/requests.py +0 -0
  45. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/responses.py +0 -0
  46. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/schema_generator.py +0 -0
  47. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/security/__init__.py +0 -0
  48. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/security/api_key.py +0 -0
  49. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/security/base.py +0 -0
  50. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/security/http.py +0 -0
  51. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/security/oauth2.py +0 -0
  52. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/security/open_id_connect_url.py +0 -0
  53. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/security/utils.py +0 -0
  54. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/serializers.py +0 -0
  55. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/types.py +0 -0
  56. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/utils.py +0 -0
  57. {starmallow-0.6.2 → starmallow-0.6.4}/starmallow/websockets.py +0 -0
  58. {starmallow-0.6.2 → starmallow-0.6.4}/tests/__init__.py +0 -0
  59. {starmallow-0.6.2 → starmallow-0.6.4}/tests/basic_api.py +0 -0
  60. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/__init__.py +0 -0
  61. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/__init__.py +0 -0
  62. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_cookie.py +0 -0
  63. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_cookie_description.py +0 -0
  64. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_cookie_optional.py +0 -0
  65. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_header.py +0 -0
  66. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_header_description.py +0 -0
  67. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_header_optional.py +0 -0
  68. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_query.py +0 -0
  69. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_query_description.py +0 -0
  70. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/api_key/test_api_key_query_optional.py +0 -0
  71. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/__init__.py +0 -0
  72. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_base.py +0 -0
  73. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_base_description.py +0 -0
  74. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_base_optional.py +0 -0
  75. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_basic.py +0 -0
  76. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_basic_realm.py +0 -0
  77. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_basic_realm_description.py +0 -0
  78. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_bearer.py +0 -0
  79. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_bearer_description.py +0 -0
  80. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_bearer_optional.py +0 -0
  81. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_digest.py +0 -0
  82. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_digest_description.py +0 -0
  83. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/http/test_http_digest_optional.py +0 -0
  84. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/oauth2/__init__.py +0 -0
  85. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/oauth2/test_oauth2.py +0 -0
  86. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/oauth2/test_oauth2_authorization_code_bearer.py +0 -0
  87. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/oauth2/test_oauth2_authorization_code_bearer_description.py +0 -0
  88. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/oauth2/test_oauth2_optional.py +0 -0
  89. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/oauth2/test_oauth2_optional_description.py +0 -0
  90. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/oauth2/test_oauth2_password_bearer_optional.py +0 -0
  91. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/oauth2/test_oauth2_password_bearer_optional_description.py +0 -0
  92. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/openid_connect/__init__.py +0 -0
  93. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/openid_connect/test_openid_connect.py +0 -0
  94. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/openid_connect/test_openid_connect_description.py +0 -0
  95. {starmallow-0.6.2 → starmallow-0.6.4}/tests/security/openid_connect/test_openid_connect_optional.py +0 -0
  96. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_additional_properties.py +0 -0
  97. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_additional_response_extra.py +0 -0
  98. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_additional_responses_bad.py +0 -0
  99. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_additional_responses_custom_model_in_callback.py +0 -0
  100. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_additional_responses_custom_validationerror.py +0 -0
  101. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_additional_responses_default_validationerror.py +0 -0
  102. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_additional_responses_response_class.py +0 -0
  103. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_additional_responses_router.py +0 -0
  104. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_basic_api.py +0 -0
  105. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_dataclass_fields.py +0 -0
  106. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_delimited_params.py +0 -0
  107. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_http_endpoints.py +0 -0
  108. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_middleware.py +0 -0
  109. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_requests_orjson.py +0 -0
  110. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_requests_ujson.py +0 -0
  111. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_resolved_param_contextmanagers.py +0 -0
  112. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_resolved_params.py +0 -0
  113. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_responses.py +0 -0
  114. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_responses_orjson.py +0 -0
  115. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_responses_ujson.py +0 -0
  116. {starmallow-0.6.2 → starmallow-0.6.4}/tests/test_ws_router.py +0 -0
  117. {starmallow-0.6.2 → starmallow-0.6.4}/tests/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: starmallow
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Summary: StarMallow framework
5
5
  Project-URL: Homepage, https://github.com/mvanderlee/starmallow
6
6
  Author-email: Michiel Vanderlee <jmt.vanderlee@gmail.com>
@@ -1,4 +1,4 @@
1
- __version__ = "0.6.2"
1
+ __version__ = "0.6.4"
2
2
 
3
3
  from .applications import StarMallow
4
4
  from .exceptions import RequestValidationError
@@ -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: BackgroundTasks,
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, BackgroundTasks):
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: BackgroundTasks,
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[BackgroundTasks] = None,
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]], BackgroundTasks, Response]:
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
- await wrap_app_handling_exceptions(app, request)(scope, receive, send)
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
@@ -913,6 +913,7 @@ openapi_schema = {
913
913
  'required': False,
914
914
  'schema': {
915
915
  'default': 'blue',
916
+ "nullable": True,
916
917
  'title': 'Color',
917
918
  'type': 'string',
918
919
  },