muffin-rest 8.0.0__tar.gz → 8.1.0__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 (39) hide show
  1. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/PKG-INFO +4 -5
  2. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/README.rst +1 -1
  3. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/api.py +4 -4
  4. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/errors.py +2 -2
  5. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/filters.py +5 -5
  6. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/handler.py +15 -19
  7. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/marshmallow.py +6 -2
  8. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/mongo/__init__.py +9 -9
  9. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/mongo/filters.py +3 -3
  10. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/mongo/types.py +2 -2
  11. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/mongo/utils.py +9 -6
  12. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/openapi.py +10 -10
  13. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/options.py +9 -9
  14. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/peewee/filters.py +4 -4
  15. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/peewee/handler.py +6 -6
  16. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/peewee/openapi.py +2 -2
  17. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/peewee/options.py +5 -7
  18. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/peewee/sorting.py +2 -2
  19. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/schemas.py +3 -1
  20. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/sorting.py +5 -17
  21. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/sqlalchemy/__init__.py +12 -13
  22. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/sqlalchemy/filters.py +5 -5
  23. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/sqlalchemy/types.py +2 -2
  24. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/types.py +3 -6
  25. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/utils.py +3 -3
  26. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/pyproject.toml +13 -9
  27. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/LICENSE +0 -0
  28. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/__init__.py +0 -0
  29. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/limits.py +0 -0
  30. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/mongo/schema.py +0 -0
  31. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/mongo/sorting.py +0 -0
  32. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/peewee/__init__.py +0 -0
  33. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/peewee/schemas.py +0 -0
  34. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/peewee/types.py +0 -0
  35. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/peewee/utils.py +0 -0
  36. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/py.typed +0 -0
  37. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/redoc.html +0 -0
  38. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/sqlalchemy/sorting.py +0 -0
  39. {muffin_rest-8.0.0 → muffin_rest-8.1.0}/muffin_rest/swagger.html +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: muffin-rest
3
- Version: 8.0.0
3
+ Version: 8.1.0
4
4
  Summary: The package provides enhanced support for writing REST APIs with Muffin framework
5
5
  Home-page: https://github.com/klen/muffin-rest
6
6
  License: MIT
7
7
  Keywords: rest,api,muffin,asgi,asyncio,trio
8
8
  Author: Kirill Klenov
9
9
  Author-email: horneds@gmail.com
10
- Requires-Python: >=3.8,<4.0
10
+ Requires-Python: >=3.9,<4.0
11
11
  Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Framework :: AsyncIO
13
13
  Classifier: Framework :: Trio
@@ -15,7 +15,6 @@ Classifier: Intended Audience :: Developers
15
15
  Classifier: License :: OSI Approved :: MIT License
16
16
  Classifier: Programming Language :: Python
17
17
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.8
19
18
  Classifier: Programming Language :: Python :: 3.9
20
19
  Classifier: Programming Language :: Python :: 3.10
21
20
  Classifier: Programming Language :: Python :: 3.11
@@ -27,7 +26,7 @@ Provides-Extra: yaml
27
26
  Requires-Dist: apispec (>=6,<7)
28
27
  Requires-Dist: marshmallow (>=3,<4)
29
28
  Requires-Dist: marshmallow-peewee (>=4,<5) ; extra == "peewee"
30
- Requires-Dist: marshmallow-sqlalchemy (>=0,<1) ; extra == "sqlalchemy"
29
+ Requires-Dist: marshmallow-sqlalchemy ; extra == "sqlalchemy"
31
30
  Requires-Dist: muffin (>=0,<1)
32
31
  Requires-Dist: muffin-databases (>=0.5.0,<0.6.0) ; extra == "sqlalchemy"
33
32
  Requires-Dist: muffin-peewee-aio (>=0,<1) ; extra == "peewee"
@@ -79,7 +78,7 @@ Features
79
78
  Requirements
80
79
  =============
81
80
 
82
- - python >= 3.7
81
+ - python >= 3.9
83
82
 
84
83
  .. note:: Trio is only supported with Peewee ORM
85
84
 
@@ -41,7 +41,7 @@ Features
41
41
  Requirements
42
42
  =============
43
43
 
44
- - python >= 3.7
44
+ - python >= 3.9
45
45
 
46
46
  .. note:: Trio is only supported with Peewee ORM
47
47
 
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import dataclasses as dc
6
6
  from pathlib import Path
7
- from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union, overload
7
+ from typing import TYPE_CHECKING, Any, Callable, Optional, Union, overload
8
8
 
9
9
  from http_router import Router
10
10
  from muffin.utils import TV, to_awaitable
@@ -31,7 +31,7 @@ class API:
31
31
  prefix: str = "",
32
32
  *,
33
33
  openapi: bool = True,
34
- servers: Optional[List] = None,
34
+ servers: Optional[list] = None,
35
35
  **openapi_info,
36
36
  ):
37
37
  """Post initialize the API if we have an application already."""
@@ -39,7 +39,7 @@ class API:
39
39
  self.prefix = prefix
40
40
 
41
41
  self.openapi = openapi
42
- self.openapi_options: Dict[str, Any] = {"info": openapi_info}
42
+ self.openapi_options: dict[str, Any] = {"info": openapi_info}
43
43
  if servers:
44
44
  self.openapi_options["servers"] = servers
45
45
 
@@ -67,7 +67,7 @@ class API:
67
67
  *,
68
68
  prefix: str = "",
69
69
  openapi: Optional[bool] = None,
70
- servers: Optional[List] = None,
70
+ servers: Optional[list] = None,
71
71
  **openapi_info,
72
72
  ):
73
73
  """Initialize the API."""
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  import json
5
5
  from http import HTTPStatus
6
- from typing import TYPE_CHECKING, Dict, Optional
6
+ from typing import TYPE_CHECKING, Optional
7
7
 
8
8
  from muffin import ResponseError
9
9
 
@@ -24,7 +24,7 @@ class APIError(ResponseError):
24
24
  """Create JSON with errors information."""
25
25
  response = {"error": True, "message": HTTPStatus(status_code).description}
26
26
 
27
- if isinstance(content, Dict):
27
+ if isinstance(content, dict):
28
28
  response = content
29
29
 
30
30
  elif content is not None:
@@ -2,7 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import operator
5
- from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Mapping, Optional, Tuple # py38
5
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable, Mapping, Optional # py39
6
6
 
7
7
  import marshmallow as ma
8
8
  from asgi_tools._compat import json_loads # type: ignore[]
@@ -20,7 +20,7 @@ FILTERS_PARAM = "where"
20
20
  class Filter(Mutate):
21
21
  """Base filter class."""
22
22
 
23
- operators: Dict[str, Callable] = {
23
+ operators: ClassVar[dict[str, Callable]] = {
24
24
  "$lt": operator.lt,
25
25
  "$le": operator.le,
26
26
  "$gt": operator.gt,
@@ -87,7 +87,7 @@ class Filter(Mutate):
87
87
 
88
88
  return ops, collection
89
89
 
90
- async def filter(self, collection, *ops: Tuple[Callable, Any], **_):
90
+ async def filter(self, collection, *ops: tuple[Callable, Any], **_) -> Any:
91
91
  """Apply the filter to collection."""
92
92
 
93
93
  def validator(obj):
@@ -129,7 +129,7 @@ class Filters(Mutator):
129
129
 
130
130
  async def apply(
131
131
  self, request: Request, collection: TVCollection
132
- ) -> Tuple[TVCollection, Dict[str, Any]]:
132
+ ) -> tuple[TVCollection, dict[str, Any]]:
133
133
  """Filter the given collection."""
134
134
  raw_data = request.url.query.get(FILTERS_PARAM)
135
135
  filters = {}
@@ -161,7 +161,7 @@ class Filters(Mutator):
161
161
  return self.MUTATE_CLASS(obj, field=field, schema_field=schema_field, **meta)
162
162
 
163
163
  @property
164
- def openapi(self) -> Dict:
164
+ def openapi(self) -> dict:
165
165
  """Prepare OpenAPI params."""
166
166
  return {
167
167
  "name": FILTERS_PARAM,
@@ -1,19 +1,14 @@
1
1
  """Base class for API REST Handlers."""
2
-
3
2
  import abc
4
3
  import inspect
5
4
  from typing import (
6
5
  Any,
7
- Dict,
8
6
  Generator,
9
7
  Generic,
10
8
  Iterable,
11
- List,
12
9
  Literal,
13
10
  Optional,
14
11
  Sequence,
15
- Tuple,
16
- Type,
17
12
  Union,
18
13
  cast,
19
14
  overload,
@@ -42,7 +37,7 @@ class RESTHandlerMeta(HandlerMeta):
42
37
 
43
38
  def __new__(mcs, name, bases, params):
44
39
  """Prepare options for the handler."""
45
- kls = cast(Type["RESTBase"], super().__new__(mcs, name, bases, params))
40
+ kls = cast(type["RESTBase"], super().__new__(mcs, name, bases, params))
46
41
  kls.meta = kls.meta_class(kls)
47
42
 
48
43
  if getattr(kls.meta, kls.meta_class.base_property, None) is not None:
@@ -60,22 +55,23 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
60
55
  resource: Any
61
56
 
62
57
  meta: RESTOptions
63
- meta_class: Type[RESTOptions] = RESTOptions
58
+ meta_class: type[RESTOptions] = RESTOptions
64
59
  _api: Optional[API] = None
65
- filters: Dict[str, Any] = {}
66
- sorting: Dict[str, Any] = {}
60
+
61
+ filters: Optional[dict[str, Any]] = None
62
+ sorting: Optional[dict[str, Any]] = None
67
63
 
68
64
  class Meta:
69
65
  """Tune the handler."""
70
66
 
71
67
  # Resource filters
72
- filters: Sequence[Union[str, Tuple[str, str], Filter]] = ()
68
+ filters: Sequence[Union[str, tuple[str, str], Filter]] = ()
73
69
 
74
70
  # Define allowed resource sorting params
75
- sorting: Sequence[Union[str, Tuple[str, Dict], Sort]] = ()
71
+ sorting: Sequence[Union[str, tuple[str, dict], Sort]] = ()
76
72
 
77
73
  # Serialize/Deserialize Schema class
78
- Schema: Optional[Type[ma.Schema]] = None
74
+ Schema: Optional[type[ma.Schema]] = None
79
75
 
80
76
  @classmethod
81
77
  def __route__(cls, router, *paths, **params):
@@ -173,7 +169,7 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
173
169
  headers["x-total"] = total
174
170
  return headers
175
171
 
176
- def paginate_prepare_params(self, request: Request) -> Tuple[int, int]:
172
+ def paginate_prepare_params(self, request: Request) -> tuple[int, int]:
177
173
  """Prepare pagination params."""
178
174
  meta = self.meta
179
175
  query = request.url.query
@@ -186,7 +182,7 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
186
182
  @abc.abstractmethod
187
183
  async def paginate(
188
184
  self, request: Request, *, limit: int = 0, offset: int = 0
189
- ) -> Tuple[Any, Optional[int]]:
185
+ ) -> tuple[Any, Optional[int]]:
190
186
  """Paginate the results."""
191
187
  raise NotImplementedError
192
188
 
@@ -198,8 +194,8 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
198
194
  return resource
199
195
 
200
196
  async def save_many(
201
- self, request: Request, data: List[TVResource], *, update=False
202
- ) -> List[TVResource]:
197
+ self, request: Request, data: list[TVResource], *, update=False
198
+ ) -> list[TVResource]:
203
199
  """Save many resources."""
204
200
  return [await self.save(request, item, update=update) for item in data]
205
201
 
@@ -231,7 +227,7 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
231
227
  @overload
232
228
  async def dump( # type: ignore[misc]
233
229
  self, request, data: TVData, *, many: Literal[True]
234
- ) -> List[TSchemaRes]:
230
+ ) -> list[TSchemaRes]:
235
231
  ...
236
232
 
237
233
  @overload
@@ -244,7 +240,7 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
244
240
  data: Union[TVResource, Iterable[TVResource]],
245
241
  *,
246
242
  many: bool = False,
247
- ) -> Union[TSchemaRes, List[TSchemaRes]]:
243
+ ) -> Union[TSchemaRes, list[TSchemaRes]]:
248
244
  """Serialize the given response."""
249
245
  schema = self.get_schema(request)
250
246
  return schema.dump(data, many=many)
@@ -298,7 +294,7 @@ class RESTHandler(RESTBase[TVResource], openapi.OpenAPIMixin):
298
294
  """Basic Handler Class."""
299
295
 
300
296
 
301
- def to_sort(sort_params: Sequence[str]) -> Generator[Tuple[str, bool], None, None]:
297
+ def to_sort(sort_params: Sequence[str]) -> Generator[tuple[str, bool], None, None]:
302
298
  """Generate sort params."""
303
299
  for name in sort_params:
304
300
  n, desc = name.strip("-"), name.startswith("-")
@@ -1,11 +1,15 @@
1
+ from __future__ import annotations
2
+
1
3
  from collections.abc import Mapping
2
- from typing import Optional, Union, cast
4
+ from typing import TYPE_CHECKING, Optional, Union, cast
3
5
 
4
- from asgi_tools import Request
5
6
  from marshmallow import Schema, ValidationError
6
7
 
7
8
  from muffin_rest.errors import APIError
8
9
 
10
+ if TYPE_CHECKING:
11
+ from asgi_tools import Request
12
+
9
13
 
10
14
  async def load_data(request: Request, schema: Optional[Schema] = None, **params):
11
15
  try:
@@ -1,7 +1,7 @@
1
1
  """Mongo DB support."""
2
2
  from __future__ import annotations
3
3
 
4
- from typing import TYPE_CHECKING, List, Optional, Tuple, Type, cast
4
+ from typing import TYPE_CHECKING, Optional, cast
5
5
 
6
6
  import bson
7
7
  from bson.errors import InvalidId
@@ -24,17 +24,17 @@ if TYPE_CHECKING:
24
24
  class MongoRESTOptions(RESTOptions):
25
25
  """Support Mongo DB."""
26
26
 
27
- filters_cls: Type[MongoFilters] = MongoFilters
28
- sorting_cls: Type[MongoSorting] = MongoSorting
29
- schema_base: Type[MongoSchema] = MongoSchema
27
+ filters_cls: type[MongoFilters] = MongoFilters
28
+ sorting_cls: type[MongoSorting] = MongoSorting
29
+ schema_base: type[MongoSchema] = MongoSchema
30
30
 
31
- aggregate: Optional[List] = None # Support aggregation. Set to pipeline.
31
+ aggregate: Optional[list] = None # Support aggregation. Set to pipeline.
32
32
  collection_id: str = "_id"
33
33
  collection: motor.AsyncIOMotorCollection
34
34
 
35
35
  base_property: str = "collection"
36
36
 
37
- Schema: Type[MongoSchema]
37
+ Schema: type[MongoSchema]
38
38
 
39
39
  def setup(self, cls):
40
40
  """Prepare meta options."""
@@ -49,7 +49,7 @@ class MongoRESTHandler(RESTHandler[TVResource]):
49
49
  """Support Mongo DB."""
50
50
 
51
51
  meta: MongoRESTOptions
52
- meta_class: Type[MongoRESTOptions] = MongoRESTOptions
52
+ meta_class: type[MongoRESTOptions] = MongoRESTOptions
53
53
 
54
54
  async def prepare_collection(self, _: Request) -> MongoChain:
55
55
  """Initialize Peeewee QuerySet for a binded to the resource model."""
@@ -57,7 +57,7 @@ class MongoRESTHandler(RESTHandler[TVResource]):
57
57
 
58
58
  async def paginate(
59
59
  self, _: Request, *, limit: int = 0, offset: int = 0
60
- ) -> Tuple[motor.AsyncIOMotorCursor, Optional[int]]:
60
+ ) -> tuple[motor.AsyncIOMotorCursor, Optional[int]]:
61
61
  """Paginate collection."""
62
62
  if self.meta.aggregate:
63
63
  pipeline_all = [*self.meta.aggregate, {"$skip": offset}, {"$limit": limit}]
@@ -118,7 +118,7 @@ class MongoRESTHandler(RESTHandler[TVResource]):
118
118
  async def delete(self, request: Request, resource: Optional[TVResource] = None):
119
119
  """Remove the given resource(s)."""
120
120
  meta = self.meta
121
- oids = [resource[meta.collection_id]] if resource else cast(List[str], await request.data())
121
+ oids = [resource[meta.collection_id]] if resource else cast(list[str], await request.data())
122
122
  if not oids:
123
123
  raise APIError.NOT_FOUND()
124
124
 
@@ -1,6 +1,6 @@
1
1
  """Support filters for Mongo."""
2
2
 
3
- import typing as t
3
+ from typing import Any, Callable, ClassVar
4
4
 
5
5
  from muffin_rest.filters import Filter, Filters
6
6
 
@@ -8,7 +8,7 @@ from muffin_rest.filters import Filter, Filters
8
8
  class MongoFilter(Filter):
9
9
  """Custom filter for sqlalchemy."""
10
10
 
11
- operators = {
11
+ operators: ClassVar = {
12
12
  "$eq": lambda _, v: ("$eq", v),
13
13
  "$ge": lambda _, v: ("$ge", v),
14
14
  "$gt": lambda _, v: ("$gt", v),
@@ -21,7 +21,7 @@ class MongoFilter(Filter):
21
21
  "$ends": lambda _, v: ("$regex", f"{ v }$"),
22
22
  }
23
23
 
24
- async def filter(self, collection, *ops: t.Tuple[t.Callable, t.Any], **_):
24
+ async def filter(self, collection, *ops: tuple[Callable, Any], **_):
25
25
  """Apply the filter."""
26
26
  return collection.find({self.field: dict(op(self.name, v) for op, v in ops)})
27
27
 
@@ -1,7 +1,7 @@
1
- from typing import Any, Dict, TypeVar
1
+ from typing import Any, TypeVar
2
2
 
3
3
  from .utils import MongoChain
4
4
 
5
+ TResource = dict[str, Any]
5
6
  TVCollection = TypeVar("TVCollection", bound=MongoChain)
6
- TResource = Dict[str, Any]
7
7
  TVResource = TypeVar("TVResource", bound=TResource)
@@ -2,14 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Awaitable, Dict, List, Tuple, Union
5
+ from typing import TYPE_CHECKING, Awaitable, Union
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from motor import motor_asyncio as motor
9
9
 
10
10
 
11
11
  class MongoChain:
12
-
13
12
  """Support query chains.
14
13
 
15
14
  Only for `find` and `find_one` methods.
@@ -53,12 +52,14 @@ class MongoChain:
53
52
  def __init__(self, collection: motor.AsyncIOMotorCollection):
54
53
  """Initialize the resource."""
55
54
  self.collection = collection
56
- self.query: List = []
55
+ self.query: list = []
57
56
  self.projection = None
58
- self.sorting: List[Tuple[str, int]] = []
57
+ self.sorting: list[tuple[str, int]] = []
59
58
 
60
59
  def find(
61
- self, query: Union[List, Dict, None] = None, projection=None,
60
+ self,
61
+ query: Union[list, dict, None] = None,
62
+ projection=None,
62
63
  ) -> MongoChain:
63
64
  """Store filters in self."""
64
65
  self.query = self.__update__(query)
@@ -66,7 +67,9 @@ class MongoChain:
66
67
  return self
67
68
 
68
69
  def find_one(
69
- self, query: Union[List, Dict, None] = None, projection=None,
70
+ self,
71
+ query: Union[list, dict, None] = None,
72
+ projection=None,
70
73
  ) -> Awaitable:
71
74
  """Apply filters and return cursor."""
72
75
  query = self.__update__(query)
@@ -6,7 +6,7 @@ import re
6
6
  from contextlib import suppress
7
7
  from functools import partial
8
8
  from http import HTTPStatus
9
- from typing import TYPE_CHECKING, Any, Callable, Dict, List, Tuple, cast
9
+ from typing import TYPE_CHECKING, Any, Callable, cast
10
10
 
11
11
  from apispec import utils
12
12
  from apispec.core import APISpec
@@ -81,9 +81,9 @@ def render_openapi(api, request=None):
81
81
  return spec.to_dict()
82
82
 
83
83
 
84
- def route_to_spec(route: Route, spec: APISpec, tags: Dict) -> Dict:
84
+ def route_to_spec(route: Route, spec: APISpec, tags: dict) -> dict:
85
85
  """Convert the given router to openapi operations."""
86
- results: Dict[str, Any] = {"parameters": [], "operations": {}}
86
+ results: dict[str, Any] = {"parameters": [], "operations": {}}
87
87
  if isinstance(route, DynamicRoute):
88
88
  for param in route.params:
89
89
  results["parameters"].append({"in": "path", "name": param})
@@ -109,7 +109,7 @@ def route_to_spec(route: Route, spec: APISpec, tags: Dict) -> Dict:
109
109
  return results
110
110
 
111
111
 
112
- def parse_docs(cb: Callable) -> Tuple[str, str, Dict]:
112
+ def parse_docs(cb: Callable) -> tuple[str, str, dict]:
113
113
  """Parse docs from the given callback."""
114
114
  if yaml_utils is None:
115
115
  return "", "", {}
@@ -122,7 +122,7 @@ def parse_docs(cb: Callable) -> Tuple[str, str, Dict]:
122
122
  return summary, description.strip(), schema
123
123
 
124
124
 
125
- def merge_dicts(source: Dict, merge: Dict) -> Dict:
125
+ def merge_dicts(source: dict, merge: dict) -> dict:
126
126
  """Merge dicts."""
127
127
  return dict(
128
128
  source,
@@ -145,15 +145,15 @@ def merge_dicts(source: Dict, merge: Dict) -> Dict:
145
145
  )
146
146
 
147
147
 
148
- def route_to_methods(route: Route) -> List[str]:
148
+ def route_to_methods(route: Route) -> list[str]:
149
149
  """Get sorted methods from the route."""
150
150
  methods = [m for m in HTTP_METHODS if m in (route.methods or [])]
151
151
  return [m.lower() for m in methods or DEFAULT_METHODS]
152
152
 
153
153
 
154
- def return_type_to_response(fn: Callable) -> Dict:
154
+ def return_type_to_response(fn: Callable) -> dict:
155
155
  """Generate reponses specs based on the given function's return type."""
156
- responses: Dict[int, Dict] = {}
156
+ responses: dict[int, dict] = {}
157
157
  return_type = fn.__annotations__.get("return")
158
158
  if return_type is None:
159
159
  return responses
@@ -180,13 +180,13 @@ class OpenAPIMixin:
180
180
  meta: RESTOptions
181
181
 
182
182
  @classmethod
183
- def openapi(cls, route: Route, spec: APISpec, tags: Dict) -> Dict: # noqa: C901
183
+ def openapi(cls, route: Route, spec: APISpec, tags: dict) -> dict: # noqa: C901
184
184
  """Get openapi specs for the endpoint."""
185
185
  meta = cls.meta
186
186
  if getattr(meta, meta.base_property, None) is None:
187
187
  return {}
188
188
 
189
- operations: Dict = {}
189
+ operations: dict = {}
190
190
  summary, desc, schema = parse_docs(cls)
191
191
  if cls not in tags:
192
192
  tags[cls] = meta.name
@@ -1,6 +1,6 @@
1
1
  """REST Options."""
2
2
 
3
- from typing import Any, Dict, Type
3
+ from typing import Any, ClassVar
4
4
 
5
5
  import marshmallow as ma
6
6
 
@@ -34,23 +34,23 @@ class RESTOptions:
34
34
 
35
35
  # Base class for filters
36
36
  filters: Filters
37
- filters_cls: Type[Filters] = Filters
37
+ filters_cls: type[Filters] = Filters
38
38
 
39
39
  # Sorting
40
40
  # -------
41
41
 
42
42
  # Base class for sorting
43
43
  sorting: Sorting
44
- sorting_cls: Type[Sorting] = Sorting
44
+ sorting_cls: type[Sorting] = Sorting
45
45
 
46
46
  # Serialization/Deserialization
47
47
  # -----------------------------
48
48
 
49
49
  # Auto generation for schemas
50
- Schema: Type[ma.Schema]
51
- schema_base: Type[ma.Schema] = ma.Schema
52
- schema_fields: Dict = {}
53
- schema_meta: Dict = {}
50
+ Schema: type[ma.Schema]
51
+ schema_base: type[ma.Schema] = ma.Schema
52
+ schema_fields: ClassVar[dict] = {}
53
+ schema_meta: ClassVar[dict] = {}
54
54
  schema_unknown: str = ma.EXCLUDE
55
55
 
56
56
  # Rate Limiting
@@ -58,8 +58,8 @@ class RESTOptions:
58
58
 
59
59
  rate_limit: int = 0
60
60
  rate_limit_period: int = 60
61
- rate_limit_cls: Type[RateLimiter] = MemoryRateLimiter
62
- rate_limit_cls_opts: Dict[str, Any] = {}
61
+ rate_limit_cls: type[RateLimiter] = MemoryRateLimiter
62
+ rate_limit_cls_opts: ClassVar[dict[str, Any]] = {}
63
63
 
64
64
  def __init__(self, cls):
65
65
  """Inherit meta options."""
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  import operator
5
5
  from functools import reduce
6
- from typing import Any, Callable, Tuple, Type, Union, cast
6
+ from typing import Any, Callable, ClassVar, Union, cast
7
7
 
8
8
  from peewee import ColumnBase, Field, ModelSelect
9
9
 
@@ -15,7 +15,7 @@ from .utils import get_model_field_by_name
15
15
  class PWFilter(Filter):
16
16
  """Support Peewee."""
17
17
 
18
- operators = dict(Filter.operators)
18
+ operators: ClassVar = dict(Filter.operators)
19
19
  operators["$in"] = operator.lshift
20
20
  operators["$none"] = operator.rshift
21
21
  operators["$like"] = operator.mod
@@ -34,7 +34,7 @@ class PWFilter(Filter):
34
34
  list_ops = (*Filter.list_ops, "$between")
35
35
 
36
36
  async def filter(
37
- self, collection: ModelSelect, *ops: Tuple[Callable, Any], **kwargs
37
+ self, collection: ModelSelect, *ops: tuple[Callable, Any], **kwargs
38
38
  ) -> ModelSelect:
39
39
  """Apply the filters to Peewee QuerySet.."""
40
40
  column = self.field
@@ -46,7 +46,7 @@ class PWFilter(Filter):
46
46
  class PWFilters(Filters):
47
47
  """Bind Peewee filter class."""
48
48
 
49
- MUTATE_CLASS: Type[PWFilter] = PWFilter
49
+ MUTATE_CLASS: type[PWFilter] = PWFilter
50
50
 
51
51
  def convert(self, obj: Union[str, Field, PWFilter], **meta):
52
52
  """Convert params to filters."""
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any, Optional, Tuple, Type, Union, cast, overload
5
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast, overload
6
6
 
7
7
  import marshmallow as ma
8
8
  import peewee as pw
@@ -23,7 +23,7 @@ if TYPE_CHECKING:
23
23
  from peewee_aio.types import TVAIOModel
24
24
 
25
25
 
26
- # XXX: Patch apispec.MarshmallowPlugin to support ForeignKeyField
26
+ # TODO: Patch apispec.MarshmallowPlugin to support ForeignKeyField
27
27
  MarshmallowPlugin.Converter.field_mapping[ForeignKey] = ("integer", None)
28
28
 
29
29
  assert issubclass(EnumField, ma.fields.Field) # just register EnumField
@@ -37,7 +37,7 @@ class PWRESTBase(RESTBase[TVModel], PeeweeOpenAPIMixin):
37
37
  collection: Union[AIOModelSelect, pw.ModelSelect]
38
38
 
39
39
  meta: PWRESTOptions
40
- meta_class: Type[PWRESTOptions] = PWRESTOptions
40
+ meta_class: type[PWRESTOptions] = PWRESTOptions
41
41
 
42
42
  @overload
43
43
  async def prepare_collection(
@@ -70,7 +70,7 @@ class PWRESTBase(RESTBase[TVModel], PeeweeOpenAPIMixin):
70
70
  resource = await meta.manager.fetchone(
71
71
  self.collection.where(meta.model_pk == pk),
72
72
  )
73
- except Exception: # noqa:
73
+ except Exception: # noqa: BLE001
74
74
  resource = None
75
75
 
76
76
  if resource is None:
@@ -81,13 +81,13 @@ class PWRESTBase(RESTBase[TVModel], PeeweeOpenAPIMixin):
81
81
  @overload
82
82
  async def paginate(
83
83
  self: PWRESTBase[TVAIOModel], _: Request, *, limit: int = 0, offset: int = 0
84
- ) -> Tuple[AIOModelSelect[TVAIOModel], int | None]:
84
+ ) -> tuple[AIOModelSelect[TVAIOModel], int | None]:
85
85
  ...
86
86
 
87
87
  @overload
88
88
  async def paginate(
89
89
  self: PWRESTBase[pw.Model], _: Request, *, limit: int = 0, offset: int = 0
90
- ) -> Tuple[pw.ModelSelect, int | None]:
90
+ ) -> tuple[pw.ModelSelect, int | None]:
91
91
  ...
92
92
 
93
93
  async def paginate(self, _: Request, *, limit: int = 0, offset: int = 0):
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Dict
5
+ from typing import TYPE_CHECKING
6
6
 
7
7
  from muffin_rest.openapi import OpenAPIMixin
8
8
 
@@ -19,7 +19,7 @@ class PeeweeOpenAPIMixin(OpenAPIMixin):
19
19
  meta: PWRESTOptions
20
20
 
21
21
  @classmethod
22
- def openapi(cls, route: Route, spec: APISpec, tags: Dict) -> Dict:
22
+ def openapi(cls, route: Route, spec: APISpec, tags: dict) -> dict:
23
23
  """Get openapi specs for the endpoint."""
24
24
  operations = super(PeeweeOpenAPIMixin, cls).openapi(route, spec, tags)
25
25
  is_resource_route = getattr(route, "params", {}).get(cls.meta.name_id)
@@ -1,5 +1,3 @@
1
- from typing import Type
2
-
3
1
  import peewee as pw
4
2
  from marshmallow_peewee import ModelSchema
5
3
  from peewee_aio import Manager
@@ -14,19 +12,19 @@ class PWRESTOptions(RESTOptions):
14
12
  """Support Peewee."""
15
13
 
16
14
  # Base filters class
17
- filters_cls: Type[PWFilters] = PWFilters
15
+ filters_cls: type[PWFilters] = PWFilters
18
16
 
19
17
  # Base sorting class
20
- sorting_cls: Type[PWSorting] = PWSorting
18
+ sorting_cls: type[PWSorting] = PWSorting
21
19
 
22
- Schema: Type[ModelSchema]
20
+ Schema: type[ModelSchema]
23
21
 
24
22
  # Schema auto generation params
25
- schema_base: Type[ModelSchema] = ModelSchema
23
+ schema_base: type[ModelSchema] = ModelSchema
26
24
 
27
25
  base_property: str = "model"
28
26
 
29
- model: Type[pw.Model]
27
+ model: type[pw.Model]
30
28
  model_pk: pw.Field
31
29
 
32
30
  manager: Manager
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Type, Union, cast
5
+ from typing import TYPE_CHECKING, Union, cast
6
6
 
7
7
  from peewee import Field
8
8
 
@@ -28,7 +28,7 @@ class PWSort(Sort):
28
28
  class PWSorting(Sorting):
29
29
  """Sort Peewee ORM Queries."""
30
30
 
31
- MUTATE_CLASS: Type[PWSort] = PWSort
31
+ MUTATE_CLASS: type[PWSort] = PWSort
32
32
 
33
33
  def prepare(self, collection: TVCollection) -> TVCollection:
34
34
  """Prepare collection for sorting."""
@@ -1,8 +1,10 @@
1
+ from typing import ClassVar
2
+
1
3
  import marshmallow as ma
2
4
 
3
5
 
4
6
  class EnumField(ma.fields.Field):
5
- default_error_messages = {
7
+ default_error_messages: ClassVar = { # type: ignore[misc]
6
8
  "unknown": "Must be one of: {choices}.",
7
9
  }
8
10
 
@@ -1,19 +1,7 @@
1
1
  """Implement sorting."""
2
2
  from __future__ import annotations
3
3
 
4
- from typing import (
5
- TYPE_CHECKING,
6
- Any,
7
- Dict,
8
- Generator,
9
- Iterable,
10
- List,
11
- Mapping,
12
- Sequence,
13
- Tuple,
14
- Type,
15
- cast,
16
- )
4
+ from typing import TYPE_CHECKING, Any, Generator, Iterable, Mapping, Sequence, cast
17
5
 
18
6
  from .types import TVCollection
19
7
  from .utils import Mutate, Mutator
@@ -40,14 +28,14 @@ class Sorting(Mutator):
40
28
  MUTATE_CLASS = Sort
41
29
  mutations: Mapping[str, Sort]
42
30
 
43
- def __init__(self, handler: Type[RESTBase], params: Iterable):
31
+ def __init__(self, handler: type[RESTBase], params: Iterable):
44
32
  """Initialize the sorting."""
45
- self.default: List[Sort] = []
33
+ self.default: list[Sort] = []
46
34
  super(Sorting, self).__init__(handler, params)
47
35
 
48
36
  async def apply(
49
37
  self, request: Request, collection: TVCollection
50
- ) -> Tuple[TVCollection, Dict[str, Any]]:
38
+ ) -> tuple[TVCollection, dict[str, Any]]:
51
39
  """Sort the given collection."""
52
40
  data = request.url.query.get(SORT_PARAM)
53
41
  sorting = {}
@@ -97,7 +85,7 @@ class Sorting(Mutator):
97
85
 
98
86
  def to_sort(
99
87
  sort_params: Sequence[str],
100
- ) -> Generator[Tuple[str, bool], None, None]:
88
+ ) -> Generator[tuple[str, bool], None, None]:
101
89
  """Generate sort params."""
102
90
  for name in sort_params:
103
91
  n, desc = name.strip("-"), name.startswith("-")
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any, Optional, Tuple, Type, cast
5
+ from typing import TYPE_CHECKING, Any, Optional, cast
6
6
 
7
7
  import marshmallow as ma
8
8
  import sqlalchemy as sa
@@ -22,8 +22,7 @@ if TYPE_CHECKING:
22
22
 
23
23
  from .types import TVResource
24
24
 
25
- # XXX: Monkey patch ModelConverter
26
- ModelConverter._get_field_name = lambda _, prop_or_column: str(prop_or_column.key) # type: ignore[assignment] # noqa:
25
+ ModelConverter._get_field_name = lambda _, prop_or_column: str(prop_or_column.key) # type: ignore[method-assign]
27
26
 
28
27
 
29
28
  class SQLAlchemyAutoSchema(BaseSQLAlchemyAutoSchema):
@@ -69,12 +68,12 @@ class SQLAlchemyAutoSchema(BaseSQLAlchemyAutoSchema):
69
68
  class SARESTOptions(RESTOptions):
70
69
  """Support SQLAlchemy Core."""
71
70
 
72
- filters_cls: Type[SAFilters] = SAFilters
73
- sorting_cls: Type[SASorting] = SASorting
71
+ filters_cls: type[SAFilters] = SAFilters
72
+ sorting_cls: type[SASorting] = SASorting
74
73
 
75
74
  # Schema auto generation params
76
- Schema: Type[SQLAlchemyAutoSchema]
77
- schema_base: Type[SQLAlchemyAutoSchema] = SQLAlchemyAutoSchema
75
+ Schema: type[SQLAlchemyAutoSchema]
76
+ schema_base: type[SQLAlchemyAutoSchema] = SQLAlchemyAutoSchema
78
77
 
79
78
  table: sa.Table
80
79
  table_pk: sa.Column
@@ -113,7 +112,7 @@ class SARESTHandler(RESTHandler[TVResource]):
113
112
  """Support SQLAlchemy Core."""
114
113
 
115
114
  meta: SARESTOptions
116
- meta_class: Type[SARESTOptions] = SARESTOptions
115
+ meta_class: type[SARESTOptions] = SARESTOptions
117
116
  collection: sa.sql.Select
118
117
 
119
118
  async def prepare_collection(self, _: Request) -> sa.sql.Select:
@@ -126,10 +125,10 @@ class SARESTHandler(RESTHandler[TVResource]):
126
125
  *,
127
126
  limit: int = 0,
128
127
  offset: int = 0,
129
- ) -> Tuple[sa.sql.Select, Optional[int]]:
128
+ ) -> tuple[sa.sql.Select, Optional[int]]:
130
129
  """Paginate the collection."""
131
130
  sqs = self.collection.order_by(None).subquery()
132
- qs = sa.select([sa.func.count()]).select_from(sqs)
131
+ qs = sa.select(sa.func.count()).select_from(sqs)
133
132
  total = None
134
133
  if self.meta.limit_total:
135
134
  total = await self.meta.database.fetch_val(qs)
@@ -167,11 +166,11 @@ class SARESTHandler(RESTHandler[TVResource]):
167
166
  insert_query = meta.table.insert()
168
167
  table_pk = cast(sa.Column, meta.table_pk)
169
168
  if update:
170
- update_query = self.meta.table.update().where(table_pk == resource[table_pk.name])
169
+ update_query = self.meta.table.update().where(table_pk == resource[table_pk.name]) # type: ignore[call-overload]
171
170
  await meta.database.execute(update_query, resource)
172
171
 
173
172
  else:
174
- resource[table_pk.name] = await meta.database.execute(insert_query, resource)
173
+ resource[table_pk.name] = await meta.database.execute(insert_query, resource) # type: ignore[call-overload]
175
174
 
176
175
  return resource
177
176
 
@@ -182,7 +181,7 @@ class SARESTHandler(RESTHandler[TVResource]):
182
181
  if not pks:
183
182
  raise APIError.NOT_FOUND()
184
183
 
185
- delete = self.meta.table.delete(table_pk.in_(pks))
184
+ delete = self.meta.table.delete().where(table_pk.in_(cast(list[Any], pks)))
186
185
  await self.meta.database.execute(delete)
187
186
 
188
187
  delete = remove
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any, Callable, Tuple, Union, cast
5
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar, Union, cast
6
6
 
7
7
  from sqlalchemy import Column
8
8
 
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
15
15
  class SAFilter(Filter):
16
16
  """Custom filter for sqlalchemy."""
17
17
 
18
- operators = dict(Filter.operators)
18
+ operators: ClassVar = dict(Filter.operators)
19
19
  operators["$between"] = lambda c, v: c.between(*v)
20
20
  operators["$ends"] = lambda c, v: c.endswith(v)
21
21
  operators["$ilike"] = lambda c, v: c.ilike(v)
@@ -27,10 +27,10 @@ class SAFilter(Filter):
27
27
  operators["$notlike"] = lambda c, v: c.notlike(v)
28
28
  operators["$starts"] = lambda c, v: c.startswith(v)
29
29
 
30
- list_ops = [*Filter.list_ops, "$between"]
30
+ list_ops = (*Filter.list_ops, "$between")
31
31
 
32
32
  async def filter(
33
- self, collection: TVCollection, *ops: Tuple[Callable, Any], **kwargs
33
+ self, collection: TVCollection, *ops: tuple[Callable, Any], **kwargs
34
34
  ) -> TVCollection:
35
35
  """Apply the filters to SQLAlchemy Select."""
36
36
  column = self.field
@@ -62,7 +62,7 @@ class SAFilters(Filters):
62
62
 
63
63
  if isinstance(obj, Column):
64
64
  name = obj.name
65
- field = obj
65
+ field: Any = obj
66
66
 
67
67
  else:
68
68
  name = obj
@@ -1,8 +1,8 @@
1
- from typing import Any, Dict, TypeVar
1
+ from typing import Any, TypeVar
2
2
 
3
3
  from sqlalchemy import sql
4
4
 
5
5
  TVCollection = TypeVar("TVCollection", bound=sql.Select)
6
6
 
7
- TResource = Dict[str, Any]
7
+ TResource = dict[str, Any]
8
8
  TVResource = TypeVar("TVResource", bound=TResource)
@@ -3,9 +3,6 @@ from typing import (
3
3
  Any,
4
4
  Awaitable,
5
5
  Callable,
6
- Dict,
7
- List,
8
- Type,
9
6
  TypeVar,
10
7
  Union,
11
8
  )
@@ -17,8 +14,8 @@ from muffin import Request
17
14
 
18
15
  TVCollection = TypeVar("TVCollection", bound=Any)
19
16
  TVResource = TypeVar("TVResource", bound=Any)
20
- TVData = Union[TVResource, List[TVResource]]
17
+ TVData = Union[TVResource, list[TVResource]]
21
18
  TAuth = Callable[[Request], Awaitable]
22
19
  TVAuth = TypeVar("TVAuth", bound=TAuth)
23
- TVHandler = TypeVar("TVHandler", bound=Type["RESTBase"])
24
- TSchemaRes = Dict[str, Any]
20
+ TVHandler = TypeVar("TVHandler", bound=type["RESTBase"])
21
+ TSchemaRes = dict[str, Any]
@@ -2,7 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import abc
5
- from typing import TYPE_CHECKING, Any, Dict, Iterable, Mapping, Tuple, Type
5
+ from typing import TYPE_CHECKING, Any, Iterable, Mapping
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from muffin import Request
@@ -36,7 +36,7 @@ class Mutate(abc.ABC):
36
36
  class Mutator(abc.ABC):
37
37
  """Mutate collections."""
38
38
 
39
- MUTATE_CLASS: Type[Mutate]
39
+ MUTATE_CLASS: type[Mutate]
40
40
  mutations: Mapping[str, Mutate]
41
41
 
42
42
  def __init__(self, handler, params: Iterable):
@@ -73,6 +73,6 @@ class Mutator(abc.ABC):
73
73
  @abc.abstractmethod
74
74
  async def apply(
75
75
  self, request: Request, collection: TVCollection
76
- ) -> Tuple[TVCollection, Dict[str, Any]]:
76
+ ) -> tuple[TVCollection, dict[str, Any]]:
77
77
  """Mutate a collection."""
78
78
  raise NotImplementedError
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "muffin-rest"
3
- version = "8.0.0"
3
+ version = "8.1.0"
4
4
  description = "The package provides enhanced support for writing REST APIs with Muffin framework"
5
5
  readme = "README.rst"
6
6
  homepage = "https://github.com/klen/muffin-rest"
@@ -14,17 +14,17 @@ classifiers = [
14
14
  "License :: OSI Approved :: MIT License",
15
15
  "Programming Language :: Python",
16
16
  "Programming Language :: Python :: 3",
17
- "Programming Language :: Python :: 3.8",
18
17
  "Programming Language :: Python :: 3.9",
19
18
  "Programming Language :: Python :: 3.10",
20
19
  "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
21
  "Topic :: Internet :: WWW/HTTP",
22
22
  "Framework :: AsyncIO",
23
23
  "Framework :: Trio",
24
24
  ]
25
25
 
26
26
  [tool.poetry.dependencies]
27
- python = "^3.8"
27
+ python = "^3.9"
28
28
  apispec = "^6"
29
29
  marshmallow = "^3"
30
30
  muffin = "^0"
@@ -34,7 +34,7 @@ pyyaml = { version = "*", optional = true }
34
34
  muffin-peewee-aio = { version = "^0", optional = true }
35
35
  marshmallow-peewee = { version = "^4", optional = true }
36
36
  muffin-databases = { version = "^0.5.0", optional = true }
37
- marshmallow-sqlalchemy = { version = "^0", optional = true }
37
+ marshmallow-sqlalchemy = { version = "*", optional = true }
38
38
  sqlalchemy = { version = "*", optional = true }
39
39
 
40
40
  [tool.poetry.extras]
@@ -45,7 +45,7 @@ sqlalchemy = ["muffin-databases", "marshmallow-sqlalchemy", "sqlalchemy"]
45
45
  [tool.poetry.group.tests.dependencies]
46
46
  aiosqlite = "*"
47
47
  marshmallow-peewee = "^4.0.3"
48
- marshmallow-sqlalchemy = "^0.27.0"
48
+ marshmallow-sqlalchemy = "*"
49
49
  muffin-databases = "^0.5.0"
50
50
  muffin-mongo = "^0.5.1"
51
51
  muffin-peewee-aio = "*"
@@ -67,7 +67,7 @@ marshmallow-peewee = "^4.0.3"
67
67
 
68
68
 
69
69
  [tool.pytest.ini_options]
70
- addopts = "-xsv"
70
+ addopts = "-xsv tests"
71
71
  log_cli = true
72
72
 
73
73
  [tool.mypy]
@@ -77,7 +77,7 @@ ignore_missing_imports = true
77
77
  [tool.tox]
78
78
  legacy_tox_ini = """
79
79
  [tox]
80
- envlist = py38,py39,py310,py311,pypy39
80
+ envlist = py39,py310,py311,py312,pypy39
81
81
 
82
82
  [testenv]
83
83
  deps = -e .[tests]
@@ -87,8 +87,10 @@ commands =
87
87
 
88
88
  [tool.ruff]
89
89
  line-length = 100
90
- target-version = "py38"
90
+ target-version = "py39"
91
91
  exclude = [".venv", "docs", "examples"]
92
+
93
+ [tool.ruff.lint]
92
94
  select = ["ALL"]
93
95
  ignore = [
94
96
  "A003",
@@ -98,6 +100,8 @@ ignore = [
98
100
  "D",
99
101
  "DJ",
100
102
  "EM",
103
+ "FIX",
104
+ "FA100",
101
105
  "N804",
102
106
  "PLR0912",
103
107
  "PLR2004",
@@ -112,7 +116,7 @@ ignore = [
112
116
 
113
117
  [tool.black]
114
118
  line-length = 100
115
- target-version = ["py38", "py39", "py310", "py311"]
119
+ target-version = ["py39", "py310", "py311", "py312"]
116
120
  preview = true
117
121
 
118
122
  [build-system]
File without changes