starmallow 0.8.0__py3-none-any.whl → 0.9.0__py3-none-any.whl

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/params.py CHANGED
@@ -1,10 +1,10 @@
1
1
  import logging
2
+ from collections.abc import Callable, Iterable, Sequence
2
3
  from enum import Enum
3
- from typing import Any, Callable, Dict, Iterable, Optional, Sequence
4
+ from typing import Any, ClassVar
4
5
 
5
6
  import marshmallow as ma
6
7
  import marshmallow.fields as mf
7
- from marshmallow.utils import is_iterable_but_not_string
8
8
  from marshmallow.validate import Length, Range, Regexp
9
9
 
10
10
  from starmallow.security.base import SecurityBaseResolver
@@ -27,17 +27,17 @@ class ParamType(Enum):
27
27
 
28
28
 
29
29
  class Param:
30
- in_: ParamType = None
30
+ in_: ClassVar[ParamType]
31
31
 
32
32
  def __init__(
33
33
  self,
34
34
  default: Any = Ellipsis,
35
35
  *,
36
36
  # alias to look the param up by.
37
- alias: Optional[str] = None,
37
+ alias: str | None = None,
38
38
  deprecated: bool | None = None,
39
39
  include_in_schema: bool = True,
40
- model: ma.Schema | mf.Field = None,
40
+ model: ma.Schema | mf.Field | None = None,
41
41
  validators: None
42
42
  | (
43
43
  Callable[[Any], Any]
@@ -52,7 +52,7 @@ class Param:
52
52
  max_length: int | None = None,
53
53
  regex: str | None = None,
54
54
  # OpenAPI title
55
- title: str = None,
55
+ title: str | None = None,
56
56
  ) -> None:
57
57
  self.default = default
58
58
  self.deprecated = deprecated
@@ -79,7 +79,7 @@ class Param:
79
79
  self.validators.append(Regexp(regex))
80
80
 
81
81
  if validators:
82
- if is_iterable_but_not_string(validators):
82
+ if isinstance(validators, Iterable) and not isinstance(validators, str):
83
83
  self.validators += validators
84
84
  elif callable(validators):
85
85
  self.validators.append(validators)
@@ -89,7 +89,7 @@ class Param:
89
89
  if self.model and getattr(self.model, 'validators', None) and self.validators:
90
90
  logger.warning('Provided validators will override model validators')
91
91
 
92
- def __eq__(self, other: "Param") -> bool:
92
+ def __eq__(self, other: "object | Param") -> bool:
93
93
  return (
94
94
  isinstance(other, Param)
95
95
  and self.__class__ == other.__class__
@@ -120,10 +120,10 @@ class Header(Param):
120
120
  default: Any = Ellipsis,
121
121
  *,
122
122
  # alias to look the param up by.
123
- alias: Optional[str] = None,
123
+ alias: str | None = None,
124
124
  deprecated: bool | None = None,
125
125
  include_in_schema: bool = True,
126
- model: ma.Schema | mf.Field = None,
126
+ model: ma.Schema | mf.Field | None = None,
127
127
  validators: None
128
128
  | (
129
129
  Callable[[Any], Any]
@@ -138,7 +138,7 @@ class Header(Param):
138
138
  max_length: int | None = None,
139
139
  regex: str | None = None,
140
140
  # OpenAPI title
141
- title: str = None,
141
+ title: str | None = None,
142
142
  convert_underscores: bool = True,
143
143
  ) -> None:
144
144
  self.convert_underscores = convert_underscores
@@ -174,7 +174,7 @@ class Body(Param):
174
174
  media_type: str = "application/json",
175
175
  deprecated: bool | None = None,
176
176
  include_in_schema: bool = True,
177
- model: ma.Schema | mf.Field = None,
177
+ model: ma.Schema | mf.Field | None = None,
178
178
  validators: None
179
179
  | (
180
180
  Callable[[Any], Any]
@@ -188,7 +188,7 @@ class Body(Param):
188
188
  min_length: int | None = None,
189
189
  max_length: int | None = None,
190
190
  regex: str | None = None,
191
- title: str = None,
191
+ title: str | None = None,
192
192
  ) -> None:
193
193
  super().__init__(
194
194
  default=default,
@@ -218,7 +218,7 @@ class Form(Body):
218
218
  media_type: str = "application/x-www-form-urlencoded",
219
219
  deprecated: bool | None = None,
220
220
  include_in_schema: bool = True,
221
- model: ma.Schema | mf.Field = None,
221
+ model: ma.Schema | mf.Field | None = None,
222
222
  validators: None
223
223
  | (
224
224
  Callable[[Any], Any]
@@ -232,7 +232,7 @@ class Form(Body):
232
232
  min_length: int | None = None,
233
233
  max_length: int | None = None,
234
234
  regex: str | None = None,
235
- title: str = None,
235
+ title: str | None = None,
236
236
  ) -> None:
237
237
  super().__init__(
238
238
  default=default,
@@ -259,14 +259,13 @@ class NoParam:
259
259
 
260
260
  Arguments will not be added to Swagger docs or be validated in any way.
261
261
  '''
262
- pass
263
262
 
264
263
 
265
264
  class ResolvedParam:
266
- def __init__(self, resolver: Callable[[Any], Any] = None, use_cache: bool = True):
265
+ def __init__(self, resolver: Callable[[Any], Any] | None = None, use_cache: bool = True):
267
266
  self.resolver = resolver
268
267
  # Set when we resolve the routes in the EnpointMixin
269
- self.resolver_params: Dict[ParamType, Dict[str, Param]] = {}
268
+ self.resolver_params: dict[ParamType, dict[str, Param]] = {}
270
269
  self.use_cache = use_cache
271
270
  self.cache_key = (self.resolver, None)
272
271
 
@@ -275,14 +274,14 @@ class Security(ResolvedParam):
275
274
 
276
275
  def __init__(
277
276
  self,
278
- resolver: SecurityBaseResolver = None,
279
- scopes: Optional[Sequence[str]] = None,
277
+ resolver: SecurityBaseResolver | None = None,
278
+ scopes: Sequence[str] | None = None,
280
279
  use_cache: bool = True,
281
280
  ):
282
281
  # Not calling super so that the resolver typehinting actually works in VSCode
283
282
  self.resolver = resolver
284
283
  # Set when we resolve the routes in the EnpointMixin
285
- self.resolver_params: Dict[ParamType, Dict[str, Param]] = {}
284
+ self.resolver_params: dict[ParamType, dict[str, Param]] = {}
286
285
  self.scopes = scopes or []
287
286
  self.use_cache = use_cache
288
287
  self.cache_key = (self.resolver, tuple(sorted(set(self.scopes or []))))
starmallow/py.typed ADDED
File without changes
@@ -1,12 +1,14 @@
1
1
  import asyncio
2
2
  import inspect
3
3
  import logging
4
+ from collections.abc import Callable, Mapping
4
5
  from contextlib import AsyncExitStack
5
- from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Union
6
+ from typing import Any, cast
6
7
 
7
8
  import marshmallow as ma
8
9
  import marshmallow.fields as mf
9
10
  from marshmallow.error_store import ErrorStore
11
+ from marshmallow.exceptions import SCHEMA
10
12
  from marshmallow.utils import missing as missing_
11
13
  from starlette.background import BackgroundTasks as StarletteBackgroundTasks
12
14
  from starlette.datastructures import FormData, Headers, QueryParams
@@ -17,6 +19,7 @@ from starlette.websockets import WebSocket
17
19
 
18
20
  from starmallow.background import BackgroundTasks
19
21
  from starmallow.params import Param, ParamType, ResolvedParam
22
+ from starmallow.security.base import SecurityBaseResolver
20
23
  from starmallow.utils import (
21
24
  is_async_gen_callable,
22
25
  is_gen_callable,
@@ -29,9 +32,9 @@ logger = logging.getLogger(__name__)
29
32
 
30
33
  async def get_body(
31
34
  request: Request,
32
- form_params: Dict[str, Param],
33
- body_params: Dict[str, Param],
34
- ) -> Union[FormData, bytes, Dict[str, Any]]:
35
+ form_params: Mapping[str, Param],
36
+ body_params: Mapping[str, Param],
37
+ ) -> FormData | bytes | dict[str, Any]:
35
38
  is_body_form = bool(form_params)
36
39
  should_process_body = is_body_form or body_params
37
40
  try:
@@ -46,18 +49,14 @@ async def get_body(
46
49
  body_bytes = await request.body()
47
50
  if body_bytes:
48
51
  json_body: Any = missing_
49
- content_type_value: str = request.headers.get("content-type")
52
+ content_type_value = request.headers.get("content-type")
50
53
  if not content_type_value:
51
54
  json_body = await request.json()
52
55
  else:
53
56
  main_type, sub_type = content_type_value.split(';')[0].split('/')
54
- if main_type == "application":
55
- if sub_type == "json" or sub_type.endswith("+json"):
56
- json_body = await request.json()
57
- if json_body != missing_:
58
- body = json_body
59
- else:
60
- body = body_bytes
57
+ if main_type == "application" and (sub_type == "json" or sub_type.endswith("+json")):
58
+ json_body = await request.json()
59
+ body = json_body if json_body != missing_ else body_bytes
61
60
 
62
61
  return body
63
62
  except Exception as e:
@@ -67,17 +66,20 @@ async def get_body(
67
66
 
68
67
 
69
68
  def request_params_to_args(
70
- received_params: Union[Mapping[str, Any], QueryParams, Headers],
71
- endpoint_params: Dict[str, Param],
69
+ received_params: Mapping[str, Any] | QueryParams | Headers | None,
70
+ endpoint_params: Mapping[str, Param] | None,
72
71
  ignore_namespace: bool = True,
73
- ) -> Tuple[Dict[str, Any], ErrorStore]:
72
+ ) -> tuple[dict[str, Any], ErrorStore]:
73
+ received_params = received_params or {}
74
+ endpoint_params = endpoint_params or {}
74
75
  values = {}
75
76
  error_store = ErrorStore()
76
77
  for field_name, param in endpoint_params.items():
77
- if not param.alias and getattr(param, "convert_underscores", None):
78
- alias = field_name.replace("_", "-")
79
- else:
80
- alias = param.alias or field_name
78
+ alias = (
79
+ field_name.replace("_", "-")
80
+ if not param.alias and getattr(param, "convert_underscores", None)
81
+ else param.alias or field_name
82
+ )
81
83
 
82
84
  if isinstance(param.model, mf.Field):
83
85
  try:
@@ -91,20 +93,20 @@ def request_params_to_args(
91
93
  error_store.store_error(error.messages, field_name)
92
94
  elif isinstance(param.model, ma.Schema):
93
95
  try:
94
- load_params = received_params if ignore_namespace else received_params.get(alias, {})
96
+ load_params = received_params if ignore_namespace else received_params.get(alias) or {}
95
97
 
96
98
  # Entire model is optional and no data was passed in.
97
99
  if getattr(param.model, 'required', None) is False and not load_params:
98
100
  values[field_name] = None
99
101
  else:
100
- values[field_name] = param.model.load(load_params, unknown=ma.EXCLUDE)
102
+ values[field_name] = param.model.load(cast(Mapping, load_params), unknown=ma.EXCLUDE)
101
103
 
102
104
  except ma.ValidationError as error:
103
105
  # Entire model is optional, so ignore errors
104
106
  if getattr(param.model, 'required', None) is False:
105
107
  values[field_name] = None
106
108
  else:
107
- error_store.store_error(error.messages)
109
+ error_store.store_error(error.messages, field_name=SCHEMA if ignore_namespace else field_name)
108
110
  else:
109
111
  raise Exception(f'Invalid model type {type(param.model)}, expected marshmallow Schema or Field')
110
112
 
@@ -115,8 +117,8 @@ async def resolve_basic_args(
115
117
  request: Request | WebSocket,
116
118
  response: Response,
117
119
  background_tasks: StarletteBackgroundTasks,
118
- params: Dict[ParamType, Dict[str, Param]],
119
- ):
120
+ params: Mapping[ParamType, Mapping[str, Param]],
121
+ ) -> tuple[dict[str, Any], dict[str, Any | list | dict]]:
120
122
  path_values, path_errors = request_params_to_args(
121
123
  request.path_params,
122
124
  params.get(ParamType.path),
@@ -134,27 +136,28 @@ async def resolve_basic_args(
134
136
  params.get(ParamType.cookie),
135
137
  )
136
138
 
137
- form_params = params.get(ParamType.form)
138
- body_params = params.get(ParamType.body)
139
- body = await get_body(request, form_params, body_params)
140
139
  form_values, form_errors = {}, None
141
140
  json_values, json_errors = {}, None
142
- if form_params:
143
- form_values, form_errors = request_params_to_args(
144
- body if body is not None and isinstance(body, FormData) else {},
145
- form_params,
146
- # If there is only one parameter defined, then don't namespace by the parameter name
147
- # Otherwise we honor the namespace: https://fastapi.tiangolo.com/tutorial/body-multiple-params/
148
- ignore_namespace=len(form_params) == 1,
149
- )
150
- if body_params:
151
- json_values, json_errors = request_params_to_args(
152
- body if body is not None and isinstance(body, Mapping) else {},
153
- body_params,
154
- # If there is only one parameter defined, then don't namespace by the parameter name
155
- # Otherwise we honor the namespace: https://fastapi.tiangolo.com/tutorial/body-multiple-params/
156
- ignore_namespace=len(body_params) == 1,
157
- )
141
+ if isinstance(request, Request):
142
+ form_params = params.get(ParamType.form) or {}
143
+ body_params = params.get(ParamType.body) or {}
144
+ body = await get_body(request, form_params, body_params)
145
+ if form_params:
146
+ form_values, form_errors = request_params_to_args(
147
+ body if body is not None and isinstance(body, FormData) else {},
148
+ form_params,
149
+ # If there is only one parameter defined, then don't namespace by the parameter name
150
+ # Otherwise we honor the namespace: https://fastapi.tiangolo.com/tutorial/body-multiple-params/
151
+ ignore_namespace=len(form_params) == 1,
152
+ )
153
+ if body_params:
154
+ json_values, json_errors = request_params_to_args(
155
+ body if body is not None and isinstance(body, Mapping) else {},
156
+ body_params,
157
+ # If there is only one parameter defined, then don't namespace by the parameter name
158
+ # Otherwise we honor the namespace: https://fastapi.tiangolo.com/tutorial/body-multiple-params/
159
+ ignore_namespace=len(body_params) == 1,
160
+ )
158
161
 
159
162
  values = {
160
163
  **path_values,
@@ -179,7 +182,7 @@ async def resolve_basic_args(
179
182
  errors['json'] = json_errors.errors
180
183
 
181
184
  # Handle non-field params
182
- for param_name, param_type in params.get(ParamType.noparam).items():
185
+ for param_name, param_type in (params.get(ParamType.noparam) or {}).items():
183
186
  if lenient_issubclass(param_type, (HTTPConnection, Request, WebSocket)):
184
187
  values[param_name] = request
185
188
  elif lenient_issubclass(param_type, Response):
@@ -194,12 +197,12 @@ async def call_resolver(
194
197
  request: Request | WebSocket,
195
198
  param_name: str,
196
199
  resolved_param: ResolvedParam,
197
- resolver_kwargs: Dict[str, Any],
200
+ resolver_kwargs: dict[str, Any],
198
201
  ):
199
202
  # Resolver can be a class with __call__ function
200
203
  resolver = resolved_param.resolver
201
204
  if not inspect.isfunction(resolver) and callable(resolver):
202
- resolver = resolver.__call__
205
+ resolver = resolver.__call__ # type: ignore
203
206
  elif not inspect.isfunction(resolver):
204
207
  raise TypeError(f'{param_name} = {resolved_param} resolver is not a function or callable')
205
208
 
@@ -219,10 +222,11 @@ async def resolve_subparams(
219
222
  request: Request | WebSocket,
220
223
  response: Response,
221
224
  background_tasks: StarletteBackgroundTasks,
222
- params: Dict[str, ResolvedParam],
223
- dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]],
224
- ) -> Dict[str, Any]:
225
+ params: Mapping[str, ResolvedParam] | None,
226
+ dependency_cache: dict[tuple[Callable[..., Any] | SecurityBaseResolver | None, tuple[str, ...] | None], Any],
227
+ ) -> tuple[dict[str, Any], dict[str, Any | list | dict]]:
225
228
  values = {}
229
+ params = params or {}
226
230
  for param_name, resolved_param in params.items():
227
231
  if resolved_param.use_cache and resolved_param.cache_key in dependency_cache:
228
232
  values[param_name] = dependency_cache[resolved_param.cache_key]
@@ -238,9 +242,9 @@ async def resolve_subparams(
238
242
 
239
243
  # Exit early since other resolvers may rely on this one, which could raise argument exceptions
240
244
  if resolver_errors:
241
- return None, resolver_errors
245
+ return {}, resolver_errors
242
246
 
243
- resolved_value = await call_resolver(request, param_name, resolved_param, resolver_kwargs)
247
+ resolved_value = await call_resolver(request, param_name, resolved_param, resolver_kwargs or {})
244
248
  values[param_name] = resolved_value
245
249
  if resolved_param.use_cache:
246
250
  dependency_cache[resolved_param.cache_key] = resolved_value
@@ -250,11 +254,11 @@ async def resolve_subparams(
250
254
 
251
255
  async def resolve_params(
252
256
  request: Request | WebSocket,
253
- params: Dict[ParamType, Dict[str, Param]],
254
- background_tasks: Optional[StarletteBackgroundTasks] = None,
255
- response: Optional[Response] = None,
256
- dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
257
- ) -> Tuple[Dict[str, Any], Dict[str, Union[Any, List, Dict]], StarletteBackgroundTasks, Response]:
257
+ params: Mapping[ParamType, Mapping[str, Param]],
258
+ background_tasks: StarletteBackgroundTasks | None = None,
259
+ response: Response | None = None,
260
+ dependency_cache: dict[tuple[Callable[..., Any] | SecurityBaseResolver | None, tuple[str, ...] | None], Any] | None = None,
261
+ ) -> tuple[dict[str, Any] | None, dict[str, Any | list | dict], StarletteBackgroundTasks, Response]:
258
262
  dependency_cache = dependency_cache or {}
259
263
 
260
264
  if response is None:
@@ -270,7 +274,7 @@ async def resolve_params(
270
274
  request,
271
275
  response,
272
276
  background_tasks,
273
- params.get(ParamType.security),
277
+ params.get(ParamType.security), # type: ignore
274
278
  dependency_cache=dependency_cache,
275
279
  )
276
280
  if errors:
@@ -289,7 +293,7 @@ async def resolve_params(
289
293
  request,
290
294
  response,
291
295
  background_tasks,
292
- params.get(ParamType.resolved),
296
+ params.get(ParamType.resolved), # type: ignore
293
297
  dependency_cache=dependency_cache,
294
298
  )
295
299
  if errors:
starmallow/responses.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import json
2
- from typing import Any, Dict, Optional
2
+ from typing import Any
3
3
 
4
4
  import marshmallow as ma
5
5
  import marshmallow.fields as mf
@@ -21,14 +21,15 @@ except ImportError: # pragma: nocover
21
21
 
22
22
 
23
23
  class JSONResponse(StarJSONResponse):
24
+ media_type = "application/json"
24
25
 
25
26
  def __init__(
26
27
  self,
27
28
  content: Any,
28
29
  status_code: int = 200,
29
- headers: Optional[Dict[str, str]] = None,
30
- media_type: Optional[str] = None,
31
- background: Optional[BackgroundTask] = None,
30
+ headers: dict[str, str] | None = None,
31
+ media_type: str | None = None,
32
+ background: BackgroundTask | None = None,
32
33
  ) -> None:
33
34
  super().__init__(content, status_code, headers, media_type, background)
34
35