muffin-rest 11.0.1__tar.gz → 12.0.1__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 (41) hide show
  1. muffin_rest-12.0.1/PKG-INFO +176 -0
  2. muffin_rest-12.0.1/README.md +138 -0
  3. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/api.py +8 -12
  4. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/errors.py +3 -2
  5. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/filters.py +4 -4
  6. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/handler.py +31 -34
  7. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/marshmallow.py +2 -2
  8. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/mongo/__init__.py +13 -10
  9. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/mongo/utils.py +7 -7
  10. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/openapi.py +3 -4
  11. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/options.py +1 -1
  12. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/filters.py +8 -5
  13. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/handler.py +11 -11
  14. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/openapi.py +1 -1
  15. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/schemas.py +0 -2
  16. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/sorting.py +4 -4
  17. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/utils.py +2 -2
  18. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/sorting.py +4 -3
  19. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/sqlalchemy/__init__.py +14 -15
  20. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/sqlalchemy/filters.py +4 -4
  21. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/sqlalchemy/sorting.py +11 -6
  22. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/types.py +0 -2
  23. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/pyproject.toml +7 -7
  24. muffin_rest-11.0.1/PKG-INFO +0 -182
  25. muffin_rest-11.0.1/README.rst +0 -143
  26. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/LICENSE +0 -0
  27. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/__init__.py +9 -9
  28. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/limits.py +0 -0
  29. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/mongo/filters.py +0 -0
  30. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/mongo/schema.py +0 -0
  31. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/mongo/sorting.py +0 -0
  32. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/mongo/types.py +0 -0
  33. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/__init__.py +0 -0
  34. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/options.py +0 -0
  35. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/peewee/types.py +0 -0
  36. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/py.typed +0 -0
  37. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/redoc.html +0 -0
  38. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/schemas.py +0 -0
  39. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/sqlalchemy/types.py +0 -0
  40. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/swagger.html +0 -0
  41. {muffin_rest-11.0.1 → muffin_rest-12.0.1}/muffin_rest/utils.py +0 -0
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.3
2
+ Name: muffin-rest
3
+ Version: 12.0.1
4
+ Summary: The package provides enhanced support for writing REST APIs with Muffin framework
5
+ License: MIT
6
+ Keywords: rest,api,muffin,asgi,asyncio,trio
7
+ Author: Kirill Klenov
8
+ Author-email: horneds@gmail.com
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Framework :: Trio
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Provides-Extra: peewee
23
+ Provides-Extra: sqlalchemy
24
+ Provides-Extra: yaml
25
+ Requires-Dist: apispec (>=6,<7)
26
+ Requires-Dist: marshmallow (>=3,<4)
27
+ Requires-Dist: marshmallow-peewee ; extra == "peewee"
28
+ Requires-Dist: marshmallow-sqlalchemy ; extra == "sqlalchemy"
29
+ Requires-Dist: muffin
30
+ Requires-Dist: muffin-databases ; extra == "sqlalchemy"
31
+ Requires-Dist: muffin-peewee-aio ; extra == "peewee"
32
+ Requires-Dist: pyyaml ; extra == "yaml"
33
+ Requires-Dist: sqlalchemy ; extra == "sqlalchemy"
34
+ Project-URL: Homepage, https://github.com/klen/muffin-rest
35
+ Project-URL: Repository, https://github.com/klen/muffin-rest
36
+ Description-Content-Type: text/markdown
37
+
38
+ # Muffin‑REST
39
+
40
+ **Muffin‑REST** simplifies building RESTful APIs with [Muffin](https://github.com/klen/muffin) by offering:
41
+
42
+ - Declarative `API` class with resource registration
43
+ - Built-in filtering, sorting, pagination, and search
44
+ - Support for:
45
+ - [Peewee ORM](http://docs.peewee-orm.com/en/latest/) via `PeeweeEndpoint`
46
+ - [SQLAlchemy Core](https://docs.sqlalchemy.org/en/14/core/) via `SAEndpoint`
47
+ - [MongoDB](https://www.mongodb.com/) via `MongoEndpoint`
48
+ - [Swagger/OpenAPI](https://swagger.io/) autodocumentation
49
+ - Works with asyncio, Trio, and Curio
50
+
51
+ [![Tests Status](https://github.com/klen/muffin-rest/workflows/tests/badge.svg)](https://github.com/klen/muffin-rest/actions)
52
+ [![PyPI Version](https://img.shields.io/pypi/v/muffin-rest)](https://pypi.org/project/muffin-rest/)
53
+ [![Python Versions](https://img.shields.io/pypi/pyversions/muffin-rest)](https://pypi.org/project/muffin-rest/)
54
+
55
+ ## Requirements
56
+
57
+ - Python >= 3.10
58
+ - Trio requires Peewee backend
59
+
60
+ ## Installation
61
+
62
+ Install core package:
63
+
64
+ ```bash
65
+ pip install muffin-rest
66
+ ```
67
+
68
+ Add optional backend support:
69
+
70
+ - SQLAlchemy Core: `pip install muffin-rest[sqlalchemy]`
71
+ - Peewee ORM: `pip install muffin-rest[peewee]`
72
+ - YAML support for Swagger: `pip install muffin-rest[yaml]`
73
+
74
+ ## Quickstart (Peewee example)
75
+
76
+ ```python
77
+ from muffin import Application
78
+ from muffin_rest import API
79
+ from muffin_rest.peewee import PeeweeEndpoint
80
+ from models import User # your Peewee model
81
+
82
+ app = Application("myapp")
83
+ api = API(title="User Service", version="1.0")
84
+
85
+ @api.route
86
+ class UsersEndpoint(PeeweeEndpoint):
87
+ class Meta:
88
+ model = User
89
+ lookup_field = "id"
90
+ filters = ["name", "email"]
91
+ ordering = ["-created_at"]
92
+
93
+ api.setup(app, prefix="/api", swagger=True)
94
+ ```
95
+
96
+ Endpoints available:
97
+
98
+ - `GET /api/users/` — list with pagination, search, filtering
99
+ - `POST /api/users/` — create
100
+ - `GET /api/users/{id}/` — retrieve
101
+ - `PUT /api/users/{id}/` — replace
102
+ - `PATCH /api/users/{id}/` — update
103
+ - `DELETE /api/users/{id}/` — remove
104
+ - Docs: `/api/docs/`, OpenAPI spec: `/api/openapi.json`
105
+
106
+ ## Usage with SQLAlchemy
107
+
108
+ ```python
109
+ from muffin_rest import API
110
+ from muffin_rest.sqlalchemy import SAEndpoint
111
+ from models import my_table, db_engine
112
+
113
+ api = API()
114
+ @api.route
115
+ class MySAEndpoint(SAEndpoint):
116
+ class Meta:
117
+ table = my_table
118
+ database = db_engine
119
+
120
+ api.setup(app)
121
+ ```
122
+
123
+ ## Usage with MongoDB
124
+
125
+ ```python
126
+ from muffin_rest import API
127
+ from muffin_rest.mongo import MongoEndpoint
128
+ from models import mongo_collection
129
+
130
+ api = API()
131
+ @api.route
132
+ class MyMongoEndpoint(MongoEndpoint):
133
+ class Meta:
134
+ collection = mongo_collection
135
+
136
+ api.setup(app)
137
+ ```
138
+
139
+ ## Advanced Configuration
140
+
141
+ Customize Swagger and routes via constructor:
142
+
143
+ ```python
144
+ api = API(
145
+ title="Service API",
146
+ version="2.1",
147
+ swagger_ui=True,
148
+ openapi_path="/api/openapi.json",
149
+ docs_path="/api/docs/"
150
+ )
151
+ ```
152
+
153
+ ## Contributing & Examples
154
+
155
+ - See `examples/` for live application demos
156
+ - Tests in `tests/` focus on filtering, pagination, status codes
157
+ - Check `CHANGELOG.md` for latest changes
158
+
159
+ ## Bug Tracker
160
+
161
+ Report bugs or request features:
162
+ https://github.com/klen/muffin-rest/issues
163
+
164
+ ## Contributing
165
+
166
+ Repo: https://github.com/klen/muffin-rest
167
+ Pull requests, example additions, docs improvements welcome!
168
+
169
+ ## Contributors
170
+
171
+ - [klen](https://github.com/klen) (Kirill Klenov)
172
+
173
+ ## License
174
+
175
+ Licensed under the [MIT license](http://opensource.org/licenses/MIT).
176
+
@@ -0,0 +1,138 @@
1
+ # Muffin‑REST
2
+
3
+ **Muffin‑REST** simplifies building RESTful APIs with [Muffin](https://github.com/klen/muffin) by offering:
4
+
5
+ - Declarative `API` class with resource registration
6
+ - Built-in filtering, sorting, pagination, and search
7
+ - Support for:
8
+ - [Peewee ORM](http://docs.peewee-orm.com/en/latest/) via `PeeweeEndpoint`
9
+ - [SQLAlchemy Core](https://docs.sqlalchemy.org/en/14/core/) via `SAEndpoint`
10
+ - [MongoDB](https://www.mongodb.com/) via `MongoEndpoint`
11
+ - [Swagger/OpenAPI](https://swagger.io/) autodocumentation
12
+ - Works with asyncio, Trio, and Curio
13
+
14
+ [![Tests Status](https://github.com/klen/muffin-rest/workflows/tests/badge.svg)](https://github.com/klen/muffin-rest/actions)
15
+ [![PyPI Version](https://img.shields.io/pypi/v/muffin-rest)](https://pypi.org/project/muffin-rest/)
16
+ [![Python Versions](https://img.shields.io/pypi/pyversions/muffin-rest)](https://pypi.org/project/muffin-rest/)
17
+
18
+ ## Requirements
19
+
20
+ - Python >= 3.10
21
+ - Trio requires Peewee backend
22
+
23
+ ## Installation
24
+
25
+ Install core package:
26
+
27
+ ```bash
28
+ pip install muffin-rest
29
+ ```
30
+
31
+ Add optional backend support:
32
+
33
+ - SQLAlchemy Core: `pip install muffin-rest[sqlalchemy]`
34
+ - Peewee ORM: `pip install muffin-rest[peewee]`
35
+ - YAML support for Swagger: `pip install muffin-rest[yaml]`
36
+
37
+ ## Quickstart (Peewee example)
38
+
39
+ ```python
40
+ from muffin import Application
41
+ from muffin_rest import API
42
+ from muffin_rest.peewee import PeeweeEndpoint
43
+ from models import User # your Peewee model
44
+
45
+ app = Application("myapp")
46
+ api = API(title="User Service", version="1.0")
47
+
48
+ @api.route
49
+ class UsersEndpoint(PeeweeEndpoint):
50
+ class Meta:
51
+ model = User
52
+ lookup_field = "id"
53
+ filters = ["name", "email"]
54
+ ordering = ["-created_at"]
55
+
56
+ api.setup(app, prefix="/api", swagger=True)
57
+ ```
58
+
59
+ Endpoints available:
60
+
61
+ - `GET /api/users/` — list with pagination, search, filtering
62
+ - `POST /api/users/` — create
63
+ - `GET /api/users/{id}/` — retrieve
64
+ - `PUT /api/users/{id}/` — replace
65
+ - `PATCH /api/users/{id}/` — update
66
+ - `DELETE /api/users/{id}/` — remove
67
+ - Docs: `/api/docs/`, OpenAPI spec: `/api/openapi.json`
68
+
69
+ ## Usage with SQLAlchemy
70
+
71
+ ```python
72
+ from muffin_rest import API
73
+ from muffin_rest.sqlalchemy import SAEndpoint
74
+ from models import my_table, db_engine
75
+
76
+ api = API()
77
+ @api.route
78
+ class MySAEndpoint(SAEndpoint):
79
+ class Meta:
80
+ table = my_table
81
+ database = db_engine
82
+
83
+ api.setup(app)
84
+ ```
85
+
86
+ ## Usage with MongoDB
87
+
88
+ ```python
89
+ from muffin_rest import API
90
+ from muffin_rest.mongo import MongoEndpoint
91
+ from models import mongo_collection
92
+
93
+ api = API()
94
+ @api.route
95
+ class MyMongoEndpoint(MongoEndpoint):
96
+ class Meta:
97
+ collection = mongo_collection
98
+
99
+ api.setup(app)
100
+ ```
101
+
102
+ ## Advanced Configuration
103
+
104
+ Customize Swagger and routes via constructor:
105
+
106
+ ```python
107
+ api = API(
108
+ title="Service API",
109
+ version="2.1",
110
+ swagger_ui=True,
111
+ openapi_path="/api/openapi.json",
112
+ docs_path="/api/docs/"
113
+ )
114
+ ```
115
+
116
+ ## Contributing & Examples
117
+
118
+ - See `examples/` for live application demos
119
+ - Tests in `tests/` focus on filtering, pagination, status codes
120
+ - Check `CHANGELOG.md` for latest changes
121
+
122
+ ## Bug Tracker
123
+
124
+ Report bugs or request features:
125
+ https://github.com/klen/muffin-rest/issues
126
+
127
+ ## Contributing
128
+
129
+ Repo: https://github.com/klen/muffin-rest
130
+ Pull requests, example additions, docs improvements welcome!
131
+
132
+ ## Contributors
133
+
134
+ - [klen](https://github.com/klen) (Kirill Klenov)
135
+
136
+ ## License
137
+
138
+ Licensed under the [MIT license](http://opensource.org/licenses/MIT).
@@ -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, Optional, Union, overload
7
+ from typing import TYPE_CHECKING, Any, Callable, overload
8
8
 
9
9
  from http_router import Router
10
10
  from muffin.utils import TV, to_awaitable
@@ -27,11 +27,11 @@ class API:
27
27
 
28
28
  def __init__(
29
29
  self,
30
- app: Optional[muffin.Application] = None,
30
+ app: muffin.Application | None = None,
31
31
  prefix: str = "",
32
32
  *,
33
33
  openapi: bool = True,
34
- servers: Optional[list] = None,
34
+ servers: list | None = None,
35
35
  **openapi_info,
36
36
  ):
37
37
  """Post initialize the API if we have an application already."""
@@ -66,8 +66,8 @@ class API:
66
66
  app: muffin.Application,
67
67
  *,
68
68
  prefix: str = "",
69
- openapi: Optional[bool] = None,
70
- servers: Optional[list] = None,
69
+ openapi: bool | None = None,
70
+ servers: list | None = None,
71
71
  **openapi_info,
72
72
  ):
73
73
  """Initialize the API."""
@@ -100,16 +100,12 @@ class API:
100
100
  self.router.route("/openapi.json")(openapi_json)
101
101
 
102
102
  @overload
103
- def route(self, obj: str, *paths: str, **params) -> Callable[[TV], TV]:
104
- ...
103
+ def route(self, obj: str, *paths: str, **params) -> Callable[[TV], TV]: ...
105
104
 
106
105
  @overload
107
- def route(self, obj: TVHandler, *paths: str, **params) -> TVHandler:
108
- ...
106
+ def route(self, obj: TVHandler, *paths: str, **params) -> TVHandler: ...
109
107
 
110
- def route(
111
- self, obj: Union[str, TVHandler], *paths: str, **params
112
- ) -> Union[Callable[[TV], TV], TVHandler]:
108
+ def route(self, obj: str | TVHandler, *paths: str, **params) -> Callable[[TV], TV] | TVHandler:
113
109
  """Route an endpoint by the API."""
114
110
  from .handler import RESTBase
115
111
 
@@ -1,9 +1,10 @@
1
1
  """Helpers to raise API errors as JSON responses."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import json
5
6
  from http import HTTPStatus
6
- from typing import TYPE_CHECKING, Optional
7
+ from typing import TYPE_CHECKING
7
8
 
8
9
  from muffin import ResponseError
9
10
 
@@ -16,7 +17,7 @@ class APIError(ResponseError):
16
17
 
17
18
  def __init__(
18
19
  self,
19
- content: Optional[TJSON] = None,
20
+ content: TJSON | None = None,
20
21
  *,
21
22
  status_code: int = HTTPStatus.BAD_REQUEST.value,
22
23
  **json_data,
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import operator
6
- from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable, Mapping, Optional # py39
6
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable, Mapping
7
7
 
8
8
  import marshmallow as ma
9
9
  from asgi_tools._compat import json_loads
@@ -59,8 +59,8 @@ class Filter(Mutate):
59
59
  name: str,
60
60
  *,
61
61
  field: Any = None,
62
- schema_field: Optional[ma.fields.Field] = None,
63
- operator: Optional[str] = None,
62
+ schema_field: ma.fields.Field | None = None,
63
+ operator: str | None = None,
64
64
  **meta,
65
65
  ):
66
66
  """Initialize filter.
@@ -75,7 +75,7 @@ class Filter(Mutate):
75
75
  self.schema_field = schema_field or self.schema_field
76
76
  self.default_operator = operator or self.default_operator
77
77
 
78
- async def apply(self, collection: Any, data: Optional[Mapping] = None):
78
+ async def apply(self, collection: Any, data: Mapping | None = None):
79
79
  """Filter given collection."""
80
80
  if not data:
81
81
  return None, collection
@@ -9,9 +9,7 @@ from typing import (
9
9
  Iterable,
10
10
  Literal,
11
11
  Mapping,
12
- Optional,
13
12
  Sequence,
14
- Union,
15
13
  cast,
16
14
  overload,
17
15
  )
@@ -31,7 +29,7 @@ from muffin_rest.types import TSchemaRes
31
29
 
32
30
  from .errors import HandlerNotBindedError
33
31
  from .options import RESTOptions
34
- from .types import TVCollection, TVData, TVResource
32
+ from .types import TVCollection, TVResource
35
33
 
36
34
 
37
35
  class RESTHandlerMeta(HandlerMeta):
@@ -39,7 +37,7 @@ class RESTHandlerMeta(HandlerMeta):
39
37
 
40
38
  def __new__(mcs, name, bases, params):
41
39
  """Prepare options for the handler."""
42
- kls = cast(type["RESTBase"], super().__new__(mcs, name, bases, params))
40
+ kls = cast("type[RESTBase]", super().__new__(mcs, name, bases, params))
43
41
  kls.meta = kls.meta_class(kls)
44
42
 
45
43
  if getattr(kls.meta, kls.meta_class.base_property, None) is not None:
@@ -58,22 +56,22 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
58
56
 
59
57
  meta: RESTOptions
60
58
  meta_class: type[RESTOptions] = RESTOptions
61
- _api: Optional[API] = None
59
+ _api: API | None = None
62
60
 
63
- filters: Optional[dict[str, Any]] = None
64
- sorting: Optional[dict[str, Any]] = None
61
+ filters: dict[str, Any] | None = None
62
+ sorting: dict[str, Any] | None = None
65
63
 
66
64
  class Meta:
67
65
  """Tune the handler."""
68
66
 
69
67
  # Resource filters
70
- filters: Sequence[Union[str, tuple[str, str], Filter]] = ()
68
+ filters: Sequence[str | tuple[str, str] | Filter] = ()
71
69
 
72
70
  # Define allowed resource sorting params
73
- sorting: Sequence[Union[str, tuple[str, dict], Sort]] = ()
71
+ sorting: Sequence[str | tuple[str, dict] | Sort] = ()
74
72
 
75
73
  # Serialize/Deserialize Schema class
76
- Schema: Optional[type[ma.Schema]] = None
74
+ Schema: type[ma.Schema] | None = None
77
75
 
78
76
  @classmethod
79
77
  def __route__(cls, router, *paths, **params):
@@ -84,9 +82,7 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
84
82
 
85
83
  else:
86
84
  router.bind(cls, f"/{ cls.meta.name }", methods=methods, **params)
87
- router.bind(
88
- cls, f"/{ cls.meta.name }/{{{ cls.meta.name_id }}}", methods=methods, **params
89
- )
85
+ router.bind(cls, f"/{ cls.meta.name }/{{pk}}", methods=methods, **params)
90
86
 
91
87
  for _, method in inspect.getmembers(cls, lambda m: hasattr(m, "__route__")):
92
88
  paths, methods = method.__route__
@@ -94,7 +90,7 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
94
90
 
95
91
  return cls
96
92
 
97
- async def __call__(self, request: Request, *, method_name: Optional[str] = None, **_) -> Any:
93
+ async def __call__(self, request: Request, *, method_name: str | None = None, **_) -> Any:
98
94
  """Dispatch the given request by HTTP method."""
99
95
  self.auth = await self.authorize(request)
100
96
 
@@ -151,7 +147,7 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
151
147
 
152
148
  async def prepare_resource(self, request: Request) -> Any:
153
149
  """Load a resource."""
154
- return request["path_params"].get(self.meta.name_id)
150
+ return request["path_params"].get("pk")
155
151
 
156
152
  async def filter(self, request: Request, collection: TVCollection) -> tuple[TVCollection, Any]:
157
153
  """Filter the collection."""
@@ -190,7 +186,7 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
190
186
  @abc.abstractmethod
191
187
  async def paginate(
192
188
  self, request: Request, *, limit: int = 0, offset: int = 0
193
- ) -> tuple[Any, Optional[int]]:
189
+ ) -> tuple[Any, int | None]:
194
190
  """Paginate the results."""
195
191
  raise NotImplementedError
196
192
 
@@ -215,7 +211,7 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
215
211
  # Parse data
216
212
  # -----------
217
213
  def get_schema(
218
- self, request: Request, *, resource: Optional[TVResource] = None, **schema_options
214
+ self, request: Request, *, resource: TVResource | None = None, **schema_options
219
215
  ) -> ma.Schema:
220
216
  """Initialize marshmallow schema for serialization/deserialization."""
221
217
  query = request.url.query
@@ -232,34 +228,37 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
232
228
 
233
229
  return data
234
230
 
235
- async def load(
236
- self, request: Request, resource: Optional[TVResource] = None, **schema_options
237
- ) -> TVData[TVResource]:
231
+ async def load(self, request: Request, resource: TVResource | None = None, **schema_options):
238
232
  """Load data from request and create/update a resource."""
239
233
  schema = self.get_schema(request, resource=resource, **schema_options)
240
- data = cast(Union[Mapping, list], await self.load_data(request))
241
- return cast(TVData[TVResource], await load_data(data, schema, partial=resource is not None))
234
+ data = cast("Mapping | list", await self.load_data(request))
235
+ return cast(
236
+ "TVResource | list[TVResource]",
237
+ await load_data(data, schema, partial=resource is not None),
238
+ )
242
239
 
243
240
  @overload
244
241
  async def dump( # type: ignore[misc]
245
- self, request, data: TVData, *, many: Literal[True]
242
+ self, request, data: TVResource | Iterable[TVResource], *, many: Literal[True]
246
243
  ) -> list[TSchemaRes]: ...
247
244
 
248
245
  @overload
249
- async def dump(self, request, data: TVData, *, many: bool = False) -> TSchemaRes: ...
246
+ async def dump(
247
+ self, request, data: TVResource | Iterable[TVResource], *, many: bool = False
248
+ ) -> TSchemaRes: ...
250
249
 
251
250
  async def dump(
252
251
  self,
253
252
  request: Request,
254
- data: Union[TVResource, Iterable[TVResource]],
253
+ data: TVResource | Iterable[TVResource],
255
254
  *,
256
255
  many: bool = False,
257
- ) -> Union[TSchemaRes, list[TSchemaRes]]:
256
+ ) -> TSchemaRes | list[TSchemaRes]:
258
257
  """Serialize the given response."""
259
258
  schema = self.get_schema(request)
260
259
  return schema.dump(data, many=many)
261
260
 
262
- async def get(self, request: Request, *, resource: Optional[TVResource] = None) -> ResponseJSON:
261
+ async def get(self, request: Request, *, resource: TVResource | None = None) -> ResponseJSON:
263
262
  """Get a resource or a collection of resources.
264
263
 
265
264
  Specify a path param to load a resource.
@@ -269,11 +268,9 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
269
268
  if resource
270
269
  else self.dump(request, data=self.collection, many=True)
271
270
  )
272
- return ResponseJSON(res)
271
+ return ResponseJSON(res) # type: ignore[type-var]
273
272
 
274
- async def post(
275
- self, request: Request, *, resource: Optional[TVResource] = None
276
- ) -> ResponseJSON:
273
+ async def post(self, request: Request, *, resource: TVResource | None = None) -> ResponseJSON:
277
274
  """Create a resource.
278
275
 
279
276
  The method accepts a single resource's data or a list of resources to create.
@@ -283,19 +280,19 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
283
280
  if many:
284
281
  data = await self.save_many(request, data, update=resource is not None)
285
282
  else:
286
- data = await self.save(request, cast(TVResource, data), update=resource is not None)
283
+ data = await self.save(request, cast("TVResource", data), update=resource is not None)
287
284
 
288
285
  res = await self.dump(request, data, many=many)
289
286
  return ResponseJSON(res)
290
287
 
291
- async def put(self, request: Request, *, resource: Optional[TVResource] = None) -> ResponseJSON:
288
+ async def put(self, request: Request, *, resource: TVResource | None = None) -> ResponseJSON:
292
289
  """Update a resource."""
293
290
  if resource is None:
294
291
  raise APIError.NOT_FOUND()
295
292
 
296
293
  return await self.post(request, resource=resource)
297
294
 
298
- async def delete(self, request: Request, resource: Optional[TVResource] = None):
295
+ async def delete(self, request: Request, resource: TVResource | None = None):
299
296
  """Delete a resource."""
300
297
  if resource is None:
301
298
  raise APIError.NOT_FOUND()
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Optional, Union
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from marshmallow import Schema, ValidationError
6
6
 
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
10
10
  from collections.abc import Mapping
11
11
 
12
12
 
13
- async def load_data(data: Union[Mapping, list], schema: Optional[Schema] = None, **params):
13
+ async def load_data(data: Mapping | list, schema: Schema | None = None, **params):
14
14
  if schema is None:
15
15
  return data
16
16