starmallow 0.6.2__tar.gz → 0.6.3__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 (116) hide show
  1. {starmallow-0.6.2 → starmallow-0.6.3}/PKG-INFO +1 -1
  2. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/__init__.py +1 -1
  3. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/endpoint.py +1 -0
  4. starmallow-0.6.3/tests/test_annotated.py +527 -0
  5. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_input.py +1 -0
  6. starmallow-0.6.2/tests/test_annotated.py +0 -244
  7. {starmallow-0.6.2 → starmallow-0.6.3}/.editorconfig +0 -0
  8. {starmallow-0.6.2 → starmallow-0.6.3}/.gitignore +0 -0
  9. {starmallow-0.6.2 → starmallow-0.6.3}/.pre-commit-config.yaml +0 -0
  10. {starmallow-0.6.2 → starmallow-0.6.3}/Dockerfile +0 -0
  11. {starmallow-0.6.2 → starmallow-0.6.3}/LICENSE.md +0 -0
  12. {starmallow-0.6.2 → starmallow-0.6.3}/README.md +0 -0
  13. {starmallow-0.6.2 → starmallow-0.6.3}/docker-compose.yml +0 -0
  14. {starmallow-0.6.2 → starmallow-0.6.3}/docs/design_ideas.md +0 -0
  15. {starmallow-0.6.2 → starmallow-0.6.3}/examples/__init__.py +0 -0
  16. {starmallow-0.6.2 → starmallow-0.6.3}/examples/cache_server.py +0 -0
  17. {starmallow-0.6.2 → starmallow-0.6.3}/examples/flask_server.py +0 -0
  18. {starmallow-0.6.2 → starmallow-0.6.3}/examples/goals.ipynb +0 -0
  19. {starmallow-0.6.2 → starmallow-0.6.3}/examples/gunicorn.py +0 -0
  20. {starmallow-0.6.2 → starmallow-0.6.3}/examples/recommended_server.py +0 -0
  21. {starmallow-0.6.2 → starmallow-0.6.3}/examples/sample_server.py +0 -0
  22. {starmallow-0.6.2 → starmallow-0.6.3}/pyproject.toml +0 -0
  23. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/applications.py +0 -0
  24. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/concurrency.py +0 -0
  25. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/constants.py +0 -0
  26. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/dataclasses.py +0 -0
  27. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/datastructures.py +0 -0
  28. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/decorators.py +0 -0
  29. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/delimited_field.py +0 -0
  30. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/docs.py +0 -0
  31. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/endpoints.py +0 -0
  32. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/exception_handlers.py +0 -0
  33. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/exceptions.py +0 -0
  34. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/ext/__init__.py +0 -0
  35. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/ext/marshmallow/__init__.py +0 -0
  36. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/ext/marshmallow/openapi.py +0 -0
  37. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/fields.py +0 -0
  38. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/middleware/__init__.py +0 -0
  39. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/middleware/asyncexitstack.py +0 -0
  40. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/params.py +0 -0
  41. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/request_resolver.py +0 -0
  42. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/requests.py +0 -0
  43. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/responses.py +0 -0
  44. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/routing.py +0 -0
  45. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/schema_generator.py +0 -0
  46. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/security/__init__.py +0 -0
  47. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/security/api_key.py +0 -0
  48. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/security/base.py +0 -0
  49. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/security/http.py +0 -0
  50. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/security/oauth2.py +0 -0
  51. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/security/open_id_connect_url.py +0 -0
  52. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/security/utils.py +0 -0
  53. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/serializers.py +0 -0
  54. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/types.py +0 -0
  55. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/utils.py +0 -0
  56. {starmallow-0.6.2 → starmallow-0.6.3}/starmallow/websockets.py +0 -0
  57. {starmallow-0.6.2 → starmallow-0.6.3}/tests/__init__.py +0 -0
  58. {starmallow-0.6.2 → starmallow-0.6.3}/tests/basic_api.py +0 -0
  59. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/__init__.py +0 -0
  60. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/api_key/__init__.py +0 -0
  61. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/api_key/test_api_key_cookie.py +0 -0
  62. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/api_key/test_api_key_cookie_description.py +0 -0
  63. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/api_key/test_api_key_cookie_optional.py +0 -0
  64. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/api_key/test_api_key_header.py +0 -0
  65. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/api_key/test_api_key_header_description.py +0 -0
  66. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/api_key/test_api_key_header_optional.py +0 -0
  67. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/api_key/test_api_key_query.py +0 -0
  68. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/api_key/test_api_key_query_description.py +0 -0
  69. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/api_key/test_api_key_query_optional.py +0 -0
  70. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/http/__init__.py +0 -0
  71. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/http/test_http_base.py +0 -0
  72. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/http/test_http_base_description.py +0 -0
  73. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/http/test_http_base_optional.py +0 -0
  74. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/http/test_http_basic.py +0 -0
  75. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/http/test_http_basic_realm.py +0 -0
  76. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/http/test_http_basic_realm_description.py +0 -0
  77. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/http/test_http_bearer.py +0 -0
  78. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/http/test_http_bearer_description.py +0 -0
  79. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/http/test_http_bearer_optional.py +0 -0
  80. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/http/test_http_digest.py +0 -0
  81. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/http/test_http_digest_description.py +0 -0
  82. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/http/test_http_digest_optional.py +0 -0
  83. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/oauth2/__init__.py +0 -0
  84. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/oauth2/test_oauth2.py +0 -0
  85. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/oauth2/test_oauth2_authorization_code_bearer.py +0 -0
  86. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/oauth2/test_oauth2_authorization_code_bearer_description.py +0 -0
  87. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/oauth2/test_oauth2_optional.py +0 -0
  88. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/oauth2/test_oauth2_optional_description.py +0 -0
  89. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/oauth2/test_oauth2_password_bearer_optional.py +0 -0
  90. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/oauth2/test_oauth2_password_bearer_optional_description.py +0 -0
  91. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/openid_connect/__init__.py +0 -0
  92. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/openid_connect/test_openid_connect.py +0 -0
  93. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/openid_connect/test_openid_connect_description.py +0 -0
  94. {starmallow-0.6.2 → starmallow-0.6.3}/tests/security/openid_connect/test_openid_connect_optional.py +0 -0
  95. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_additional_properties.py +0 -0
  96. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_additional_response_extra.py +0 -0
  97. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_additional_responses_bad.py +0 -0
  98. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_additional_responses_custom_model_in_callback.py +0 -0
  99. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_additional_responses_custom_validationerror.py +0 -0
  100. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_additional_responses_default_validationerror.py +0 -0
  101. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_additional_responses_response_class.py +0 -0
  102. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_additional_responses_router.py +0 -0
  103. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_basic_api.py +0 -0
  104. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_dataclass_fields.py +0 -0
  105. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_delimited_params.py +0 -0
  106. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_http_endpoints.py +0 -0
  107. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_middleware.py +0 -0
  108. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_requests_orjson.py +0 -0
  109. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_requests_ujson.py +0 -0
  110. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_resolved_param_contextmanagers.py +0 -0
  111. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_resolved_params.py +0 -0
  112. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_responses.py +0 -0
  113. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_responses_orjson.py +0 -0
  114. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_responses_ujson.py +0 -0
  115. {starmallow-0.6.2 → starmallow-0.6.3}/tests/test_ws_router.py +0 -0
  116. {starmallow-0.6.2 → starmallow-0.6.3}/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.3
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.3"
2
2
 
3
3
  from .applications import StarMallow
4
4
  from .exceptions import RequestValidationError
@@ -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)
@@ -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
  },
@@ -1,244 +0,0 @@
1
- '''Test Resolved Params'''
2
- from typing import Annotated
3
-
4
- import pytest
5
- from starlette.testclient import TestClient
6
-
7
- from starmallow import Path, Query, ResolvedParam, StarMallow
8
-
9
- from .utils import assert_json
10
-
11
- app = StarMallow()
12
-
13
-
14
- ############################################################
15
- # Models - classes and schemas
16
- ############################################################
17
- # region - VS Code folding marker - https://code.visualstudio.com/docs/editor/codebasics#_folding
18
- def paging_parameters(
19
- offset: Annotated[int, Query(0)],
20
- limit: Annotated[int, Query()] = 1000,
21
- ):
22
- return {"offset": offset, "limit": limit}
23
-
24
-
25
- def search_parameters(q: Annotated[str, Path()]):
26
- return {"q": q}
27
-
28
-
29
- # To test nested resolved params
30
- def searchable_page_parameters(
31
- paging_params: Annotated[dict[str, int], ResolvedParam(paging_parameters)],
32
- search_params: Annotated[dict[str, str], ResolvedParam(search_parameters)],
33
- ):
34
- return {
35
- "offset": paging_params["offset"],
36
- "limit": paging_params["limit"],
37
- "q": search_params["q"],
38
- }
39
- # endregion
40
-
41
-
42
- ############################################################
43
- # Test API
44
- ############################################################
45
- # region
46
- @app.get("/paging")
47
- def get_paging(paging_params: Annotated[dict[str, int], ResolvedParam(paging_parameters)]):
48
- return paging_params
49
-
50
-
51
- @app.get("/filtered_paging_1/{q}")
52
- def get_filtered_paging_1(search_params: Annotated[dict[str, str], ResolvedParam(search_parameters)]):
53
- return search_params
54
-
55
-
56
- @app.get("/filtered_paging_2/{q}")
57
- def get_filtered_paging_2(filtered_paging_params: Annotated[dict[str, int | str], ResolvedParam(searchable_page_parameters)]):
58
- return filtered_paging_params
59
- # endregion
60
-
61
-
62
- ############################################################
63
- # Tests
64
- ############################################################
65
- # region
66
- client = TestClient(app)
67
-
68
- openapi_schema = {
69
- "openapi": "3.0.2",
70
- "info": {"title": "StarMallow", "version": "0.1.0"},
71
- "paths": {
72
- "/paging": {
73
- "get": {
74
- "responses": {
75
- "200": {
76
- "description": "Successful Response",
77
- "content": {"application/json": {"schema": {}}},
78
- },
79
- "422": {
80
- "description": "Validation Error",
81
- "content": {
82
- "application/json": {
83
- "schema": {
84
- "$ref": "#/components/schemas/HTTPValidationError",
85
- },
86
- },
87
- },
88
- },
89
- },
90
- "summary": "Get Paging",
91
- "operationId": "get_paging_paging_get",
92
- "parameters": [
93
- {
94
- "required": False,
95
- "schema": {
96
- "default": 0,
97
- "type": "integer",
98
- "title": "Offset",
99
- },
100
- "name": "offset",
101
- "in": "query",
102
- },
103
- {
104
- "required": False,
105
- "schema": {
106
- "default": 1000,
107
- "type": "integer",
108
- "title": "Limit",
109
- },
110
- "name": "limit",
111
- "in": "query",
112
- },
113
- ],
114
- },
115
- },
116
- "/filtered_paging_1/{q}": {
117
- "get": {
118
- "responses": {
119
- "200": {
120
- "description": "Successful Response",
121
- "content": {"application/json": {"schema": {}}},
122
- },
123
- "422": {
124
- "description": "Validation Error",
125
- "content": {
126
- "application/json": {
127
- "schema": {
128
- "$ref": "#/components/schemas/HTTPValidationError",
129
- },
130
- },
131
- },
132
- },
133
- },
134
- "summary": "Get Filtered Paging 1",
135
- "operationId": "get_filtered_paging_1_filtered_paging_1__q__get",
136
- "parameters": [
137
- {
138
- "required": True,
139
- "schema": {
140
- "type": "string",
141
- "title": "Q",
142
- },
143
- "name": "q",
144
- "in": "path",
145
- },
146
- ],
147
- },
148
- },
149
- "/filtered_paging_2/{q}": {
150
- "get": {
151
- "responses": {
152
- "200": {
153
- "description": "Successful Response",
154
- "content": {"application/json": {"schema": {}}},
155
- },
156
- "422": {
157
- "description": "Validation Error",
158
- "content": {
159
- "application/json": {
160
- "schema": {
161
- "$ref": "#/components/schemas/HTTPValidationError",
162
- },
163
- },
164
- },
165
- },
166
- },
167
- "summary": "Get Filtered Paging 2",
168
- "operationId": "get_filtered_paging_2_filtered_paging_2__q__get",
169
- "parameters": [
170
- {
171
- "required": False,
172
- "schema": {
173
- "default": 0,
174
- "type": "integer",
175
- "title": "Offset",
176
- },
177
- "name": "offset",
178
- "in": "query",
179
- },
180
- {
181
- "required": False,
182
- "schema": {
183
- "default": 1000,
184
- "type": "integer",
185
- "title": "Limit",
186
- },
187
- "name": "limit",
188
- "in": "query",
189
- },
190
- {
191
- "required": True,
192
- "schema": {
193
- "type": "string",
194
- "title": "Q",
195
- },
196
- "name": "q",
197
- "in": "path",
198
- },
199
- ],
200
- },
201
- },
202
- },
203
- "components": {
204
- "schemas": {
205
- "HTTPValidationError": {
206
- 'properties': {
207
- 'detail': {
208
- 'description': 'Error detail',
209
- 'title': 'Detail',
210
- },
211
- 'errors': {
212
- 'description': 'Exception or error type',
213
- 'title': 'Errors',
214
- },
215
- 'status_code': {
216
- 'description': 'HTTP status code',
217
- 'title': 'Status Code',
218
- 'type': 'integer',
219
- },
220
- },
221
- 'required': ['detail', 'status_code'],
222
- 'title': 'HTTPValidationError',
223
- 'type': 'object',
224
- },
225
- },
226
- },
227
- }
228
-
229
-
230
- @pytest.mark.parametrize(
231
- "path,expected_status,expected_response",
232
- [
233
- ("/paging?limit=50", 200, {"offset": 0, "limit": 50}),
234
- ("/filtered_paging_1/name=foobar", 200, {"q": "name=foobar"}),
235
- ("/filtered_paging_2/name=foobar?limit=50", 200, {"offset": 0, "limit": 50, "q": "name=foobar"}),
236
- ("/openapi.json", 200, openapi_schema),
237
- ],
238
- )
239
- def test_get_path(path, expected_status, expected_response):
240
- response = client.get(path)
241
- assert response.status_code == expected_status
242
- assert_json(response.json(), expected_response)
243
-
244
- # endregion
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes