muffin-rest 13.0.3__tar.gz → 13.1.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 (40) hide show
  1. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/PKG-INFO +17 -19
  2. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/filters.py +4 -4
  3. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/handler.py +10 -10
  4. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/mongo/__init__.py +7 -9
  5. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/mongo/schema.py +2 -4
  6. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/mongo/utils.py +1 -1
  7. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/openapi.py +7 -9
  8. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/peewee/filters.py +6 -6
  9. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/peewee/handler.py +3 -4
  10. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/peewee/openapi.py +1 -1
  11. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/peewee/options.py +4 -4
  12. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/peewee/sorting.py +1 -1
  13. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/sorting.py +7 -6
  14. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/sqlalchemy/__init__.py +9 -9
  15. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/utils.py +16 -11
  16. muffin_rest-13.1.1/pyproject.toml +124 -0
  17. muffin_rest-13.0.3/LICENSE +0 -22
  18. muffin_rest-13.0.3/pyproject.toml +0 -125
  19. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/README.md +0 -0
  20. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/__init__.py +0 -0
  21. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/api.py +0 -0
  22. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/errors.py +0 -0
  23. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/limits.py +0 -0
  24. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/marshmallow.py +0 -0
  25. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/mongo/filters.py +0 -0
  26. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/mongo/sorting.py +0 -0
  27. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/mongo/types.py +0 -0
  28. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/options.py +0 -0
  29. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/peewee/__init__.py +0 -0
  30. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/peewee/schemas.py +0 -0
  31. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/peewee/types.py +0 -0
  32. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/peewee/utils.py +0 -0
  33. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/py.typed +0 -0
  34. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/redoc.html +0 -0
  35. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/schemas.py +0 -0
  36. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/sqlalchemy/filters.py +0 -0
  37. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/sqlalchemy/sorting.py +0 -0
  38. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/sqlalchemy/types.py +0 -0
  39. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/swagger.html +0 -0
  40. {muffin_rest-13.0.3 → muffin_rest-13.1.1}/muffin_rest/types.py +0 -0
@@ -1,16 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: muffin-rest
3
- Version: 13.0.3
3
+ Version: 13.1.1
4
4
  Summary: The package provides enhanced support for writing REST APIs with Muffin framework
5
- License: MIT
6
- License-File: LICENSE
7
5
  Keywords: rest,api,muffin,asgi,asyncio,trio
8
6
  Author: Kirill Klenov
9
- Author-email: horneds@gmail.com
10
- Requires-Python: >=3.10,<4.0
7
+ Author-email: Kirill Klenov <horneds@gmail.com>
8
+ License-Expression: MIT
11
9
  Classifier: Development Status :: 5 - Production/Stable
12
- Classifier: Framework :: AsyncIO
13
- Classifier: Framework :: Trio
14
10
  Classifier: Intended Audience :: Developers
15
11
  Classifier: License :: OSI Approved :: MIT License
16
12
  Classifier: Programming Language :: Python
@@ -21,20 +17,23 @@ Classifier: Programming Language :: Python :: 3.12
21
17
  Classifier: Programming Language :: Python :: 3.13
22
18
  Classifier: Programming Language :: Python :: 3.14
23
19
  Classifier: Topic :: Internet :: WWW/HTTP
24
- Provides-Extra: peewee
25
- Provides-Extra: sqlalchemy
26
- Provides-Extra: yaml
27
- Requires-Dist: apispec (>=6,<7)
28
- Requires-Dist: marshmallow (>=3,<4)
29
- Requires-Dist: marshmallow-peewee ; extra == "peewee"
30
- Requires-Dist: marshmallow-sqlalchemy ; extra == "sqlalchemy"
20
+ Classifier: Framework :: AsyncIO
21
+ Classifier: Framework :: Trio
22
+ Requires-Dist: apispec>=6,<7
23
+ Requires-Dist: marshmallow>=3,<4
31
24
  Requires-Dist: muffin
32
- Requires-Dist: muffin-databases ; extra == "sqlalchemy"
33
- Requires-Dist: muffin-peewee-aio ; extra == "peewee"
34
- Requires-Dist: pyyaml ; extra == "yaml"
35
- Requires-Dist: sqlalchemy ; extra == "sqlalchemy"
25
+ Requires-Dist: muffin-peewee-aio ; extra == 'peewee'
26
+ Requires-Dist: marshmallow-peewee ; extra == 'peewee'
27
+ Requires-Dist: muffin-databases ; extra == 'sqlalchemy'
28
+ Requires-Dist: marshmallow-sqlalchemy ; extra == 'sqlalchemy'
29
+ Requires-Dist: sqlalchemy ; extra == 'sqlalchemy'
30
+ Requires-Dist: pyyaml ; extra == 'yaml'
31
+ Requires-Python: >=3.10, <4
36
32
  Project-URL: Homepage, https://github.com/klen/muffin-rest
37
33
  Project-URL: Repository, https://github.com/klen/muffin-rest
34
+ Provides-Extra: peewee
35
+ Provides-Extra: sqlalchemy
36
+ Provides-Extra: yaml
38
37
  Description-Content-Type: text/markdown
39
38
 
40
39
  # Muffin‑REST
@@ -175,4 +174,3 @@ Pull requests, example additions, docs improvements welcome!
175
174
  ## License
176
175
 
177
176
  Licensed under the [MIT license](http://opensource.org/licenses/MIT).
178
-
@@ -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
6
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable, Mapping, cast
7
7
 
8
8
  import marshmallow as ma
9
9
  from asgi_tools._compat import json_loads
@@ -90,13 +90,13 @@ class Filter(Mutate):
90
90
 
91
91
  return ops, collection
92
92
 
93
- async def filter(self, collection, *ops: TFilterValue) -> Any:
93
+ async def filter(self, collection: TVCollection, *ops: TFilterValue) -> TVCollection:
94
94
  """Apply the filter to collection."""
95
95
 
96
96
  def validator(obj):
97
97
  return all(op(get_value(obj, self.name), val) for op, val in ops)
98
98
 
99
- return [item for item in collection if validator(item)]
99
+ return cast("TVCollection", [item for item in collection if validator(item)])
100
100
 
101
101
  def get_simple_value(self, ops: TFilterOps) -> Any:
102
102
  """Get simple value from filter's data.
@@ -134,7 +134,7 @@ class Filter(Mutate):
134
134
  yield (self.operators[self.default_operator], deserialize(value))
135
135
 
136
136
 
137
- class Filters(Mutator):
137
+ class Filters(Mutator[Filter]):
138
138
  """Build filters for handlers."""
139
139
 
140
140
  MUTATE_CLASS = Filter
@@ -47,7 +47,7 @@ class RESTHandlerMeta(HandlerMeta):
47
47
  return kls
48
48
 
49
49
 
50
- class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
50
+ class RESTBase(Handler, Generic[TVResource], metaclass=RESTHandlerMeta):
51
51
  """Load/save resources."""
52
52
 
53
53
  auth: Any
@@ -81,8 +81,8 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
81
81
  router.bind(cls, *paths, methods=methods, **params)
82
82
 
83
83
  else:
84
- router.bind(cls, f"/{ cls.meta.name }", methods=methods, **params)
85
- router.bind(cls, f"/{ cls.meta.name }/{{id}}", methods=methods, **params)
84
+ router.bind(cls, f"/{cls.meta.name}", methods=methods, **params)
85
+ router.bind(cls, f"/{cls.meta.name}/{{id}}", methods=methods, **params)
86
86
 
87
87
  for _, method in inspect.getmembers(cls, lambda m: hasattr(m, "__route__")):
88
88
  paths, methods = method.__route__
@@ -204,9 +204,9 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
204
204
  return [await self.save(request, item, update=update) for item in data]
205
205
 
206
206
  @abc.abstractmethod
207
- async def remove(self, request: Request, resource):
207
+ async def remove(self, request: Request, resource: TVResource | None = None) -> Any:
208
208
  """Remove the given resource."""
209
- raise NotImplementedError
209
+ ...
210
210
 
211
211
  # Parse data
212
212
  # -----------
@@ -258,7 +258,7 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
258
258
  schema = self.get_schema(request)
259
259
  return schema.dump(data, many=many)
260
260
 
261
- async def get(self, request: Request, *, resource: TVResource | None = None) -> ResponseJSON:
261
+ async def get(self, request: Request, *, resource: TVResource | None = None) -> Any:
262
262
  """Get a resource or a collection of resources.
263
263
 
264
264
  Specify a path param to load a resource.
@@ -268,9 +268,9 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
268
268
  if resource
269
269
  else self.dump(request, data=self.collection, many=True)
270
270
  )
271
- return ResponseJSON(res) # type: ignore[type-var]
271
+ return ResponseJSON(res)
272
272
 
273
- async def post(self, request: Request, *, resource: TVResource | None = None) -> ResponseJSON:
273
+ async def post(self, request: Request, *, resource: TVResource | None = None) -> Any:
274
274
  """Create a resource.
275
275
 
276
276
  The method accepts a single resource's data or a list of resources to create.
@@ -285,14 +285,14 @@ class RESTBase(Generic[TVResource], Handler, metaclass=RESTHandlerMeta):
285
285
  res = await self.dump(request, data, many=many)
286
286
  return ResponseJSON(res)
287
287
 
288
- async def put(self, request: Request, *, resource: TVResource | None = None) -> ResponseJSON:
288
+ async def put(self, request: Request, *, resource: TVResource | None = None) -> Any:
289
289
  """Update a resource."""
290
290
  if resource is None:
291
291
  raise APIError.NOT_FOUND()
292
292
 
293
293
  return await self.post(request, resource=resource)
294
294
 
295
- async def delete(self, request: Request, resource: TVResource | None = None):
295
+ async def delete(self, request: Request, resource: TVResource | None = None) -> Any:
296
296
  """Delete a resource."""
297
297
  if resource is None:
298
298
  raise APIError.NOT_FOUND()
@@ -25,9 +25,9 @@ if TYPE_CHECKING:
25
25
  class MongoRESTOptions(RESTOptions):
26
26
  """Support Mongo DB."""
27
27
 
28
- filters_cls: type[MongoFilters] = MongoFilters
29
- sorting_cls: type[MongoSorting] = MongoSorting
30
- schema_base: type[MongoSchema] = MongoSchema
28
+ filters_cls = MongoFilters
29
+ sorting_cls = MongoSorting
30
+ schema_base = MongoSchema
31
31
 
32
32
  aggregate: list | None = None # Support aggregation. Set to pipeline.
33
33
  collection_id: str = "_id"
@@ -35,7 +35,7 @@ class MongoRESTOptions(RESTOptions):
35
35
 
36
36
  base_property: str = "collection"
37
37
 
38
- Schema: type[MongoSchema]
38
+ Schema: type[MongoSchema] # type: ignore[override]
39
39
 
40
40
  def setup(self, cls):
41
41
  """Prepare meta options."""
@@ -49,16 +49,14 @@ class MongoRESTOptions(RESTOptions):
49
49
  class MongoRESTHandler(RESTHandler[TVResource]):
50
50
  """Support Mongo DB."""
51
51
 
52
- meta: MongoRESTOptions
53
- meta_class: type[MongoRESTOptions] = MongoRESTOptions
52
+ meta: MongoRESTOptions # type: ignore[bad-override]
53
+ meta_class = MongoRESTOptions
54
54
 
55
55
  async def prepare_collection(self, request: Request) -> MongoChain:
56
56
  """Initialize Peeewee QuerySet for a binded to the resource model."""
57
57
  return MongoChain(self.meta.collection)
58
58
 
59
- async def paginate(
60
- self, request: Request, *, limit: int = 0, offset: int = 0
61
- ) -> tuple[motor.AsyncIOMotorCursor, int | None]:
59
+ async def paginate(self, request: Request, *, limit: int = 0, offset: int = 0): # type: ignore[override]
62
60
  """Paginate collection."""
63
61
  if self.meta.aggregate:
64
62
  pipeline_all = [*self.meta.aggregate, {"$skip": offset}, {"$limit": limit}]
@@ -5,23 +5,21 @@ import marshmallow as ma
5
5
 
6
6
 
7
7
  class ObjectId(ma.fields.Field):
8
-
9
8
  """ObjectID Marshmallow Field."""
10
9
 
11
- def _deserialize(self, value, _, __):
10
+ def _deserialize(self, value, attr, data, **kwargs):
12
11
  try:
13
12
  return bson.ObjectId(value)
14
13
  except ValueError as exc:
15
14
  raise ma.ValidationError("invalid ObjectId `%s`" % value) from exc
16
15
 
17
- def _serialize(self, value, _, __):
16
+ def _serialize(self, value, attr, obj, **kwargs):
18
17
  if value is None:
19
18
  return ma.missing
20
19
  return str(value)
21
20
 
22
21
 
23
22
  class MongoSchema(ma.Schema):
24
-
25
23
  """Serialize/deserialize results from mongo."""
26
24
 
27
25
  _id = ObjectId()
@@ -79,7 +79,7 @@ class MongoChain:
79
79
  def count(self) -> Awaitable[int]:
80
80
  """Count documents."""
81
81
  query = (self.query and {"$and": self.query}) or {}
82
- return self.collection.count_documents(query)
82
+ return self.collection.count_documents(query) # type: ignore[]
83
83
 
84
84
  def aggregate(self, pipeline, **kwargs):
85
85
  """Aggregate collection."""
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import inspect
6
- import re
7
6
  from contextlib import suppress
8
7
  from functools import partial
9
8
  from http import HTTPStatus
@@ -40,7 +39,6 @@ HTTP_METHODS = [
40
39
  "TRACE",
41
40
  "CONNECT",
42
41
  ]
43
- RE_URL = re.compile(r"<(?:[^:<>]+:)?([^<>]+)>") # TODO: Not used
44
42
  SKIP_PATH = {"/openapi.json", "/swagger", "/redoc"}
45
43
 
46
44
 
@@ -50,14 +48,14 @@ def render_openapi(api, request=None):
50
48
  options = dict(api.openapi_options)
51
49
  if request:
52
50
  options.setdefault(
53
- "servers",
54
- [{"url": str(request.url.with_query("").with_path(api.prefix))}],
51
+ "servers", [{"url": str(request.url.with_query("").with_path(api.prefix))}]
55
52
  )
56
53
 
54
+ info = cast("dict", options["info"])
57
55
  spec = APISpec(
58
- options["info"].pop("title", f"{ api.app.cfg.name.title() } API"),
59
- options["info"].pop("version", "1.0.0"),
60
- options.pop("openapi_version", "3.0.3"),
56
+ info.pop("title", f"{api.app.cfg.name.title()} API"),
57
+ info.pop("version", "1.0.0"),
58
+ options.pop("openapi_version", "3.0.3"), # type: ignore[]
61
59
  **options,
62
60
  plugins=[MarshmallowPlugin()],
63
61
  )
@@ -188,7 +186,7 @@ class OpenAPIMixin:
188
186
  return {}
189
187
 
190
188
  operations: dict = {}
191
- summary, desc, schema = parse_docs(cls)
189
+ summary, _, schema = parse_docs(cls)
192
190
  if cls not in tags:
193
191
  tags[cls] = meta.name
194
192
  spec.tag({"name": meta.name, "description": summary})
@@ -196,7 +194,7 @@ class OpenAPIMixin:
196
194
  if schema_cls.__name__ not in spec.components.schemas:
197
195
  spec.components.schema(meta.Schema.__name__, schema=meta.Schema)
198
196
 
199
- schema_ref = {"$ref": f"#/components/schemas/{ meta.Schema.__name__ }"}
197
+ schema_ref = {"$ref": f"#/components/schemas/{meta.Schema.__name__}"}
200
198
  for method in route_to_methods(route):
201
199
  operations[method] = {"tags": [tags[cls]]}
202
200
  is_resource_route = isinstance(route, DynamicRoute) and route.params.get("id")
@@ -6,7 +6,7 @@ import operator
6
6
  from functools import reduce
7
7
  from typing import TYPE_CHECKING, ClassVar, cast
8
8
 
9
- from peewee import ColumnBase, Field, ModelSelect
9
+ from peewee import ColumnBase, Field
10
10
 
11
11
  from muffin_rest.filters import Filter, Filters
12
12
 
@@ -39,20 +39,20 @@ class PWFilter(Filter):
39
39
 
40
40
  list_ops = (*Filter.list_ops, "$between")
41
41
 
42
- async def filter(self, collection: ModelSelect, *ops: TFilterValue) -> ModelSelect:
42
+ async def filter(self, collection, *ops: TFilterValue):
43
43
  """Apply the filters to Peewee QuerySet.."""
44
44
  column = self.field
45
+
45
46
  if isinstance(column, ColumnBase):
46
- collection = cast(
47
- "ModelSelect", collection.where(*[op(column, val) for op, val in ops])
48
- )
47
+ collection = collection.where(*[op(column, val) for op, val in ops])
48
+
49
49
  return collection
50
50
 
51
51
 
52
52
  class PWFilters(Filters):
53
53
  """Bind Peewee filter class."""
54
54
 
55
- MUTATE_CLASS: type[PWFilter] = PWFilter
55
+ MUTATE_CLASS = PWFilter
56
56
 
57
57
  def convert(self, obj: str | Field | PWFilter, **meta):
58
58
  """Convert params to filters."""
@@ -36,8 +36,8 @@ class PWRESTBase(RESTBase[TVModel], PeeweeOpenAPIMixin):
36
36
  resource: TVModel
37
37
  collection: AIOModelSelect | pw.ModelSelect
38
38
 
39
- meta: PWRESTOptions
40
- meta_class: type[PWRESTOptions] = PWRESTOptions
39
+ meta: PWRESTOptions # type: ignore[override]
40
+ meta_class = PWRESTOptions
41
41
 
42
42
  @overload
43
43
  async def prepare_collection(
@@ -131,8 +131,7 @@ class PWRESTBase(RESTBase[TVModel], PeeweeOpenAPIMixin):
131
131
  if not data:
132
132
  return
133
133
 
134
- model_pk = cast("pw.Field", meta.model_pk)
135
- resources = await meta.manager.fetchall(self.collection.where(model_pk << data)) # type: ignore[]
134
+ resources = await meta.manager.fetchall(self.collection.where(meta.model_pk << data)) # type: ignore[]
136
135
 
137
136
  if not resources:
138
137
  raise APIError.NOT_FOUND()
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
16
16
  class PeeweeOpenAPIMixin(OpenAPIMixin):
17
17
  """Render openapi."""
18
18
 
19
- meta: PWRESTOptions
19
+ meta: PWRESTOptions # type: ignore[override]
20
20
 
21
21
  @classmethod
22
22
  def openapi(cls, route: Route, spec: APISpec, tags: dict) -> dict:
@@ -12,15 +12,15 @@ class PWRESTOptions(RESTOptions):
12
12
  """Support Peewee."""
13
13
 
14
14
  # Base filters class
15
- filters_cls: type[PWFilters] = PWFilters
15
+ filters_cls = PWFilters
16
16
 
17
17
  # Base sorting class
18
- sorting_cls: type[PWSorting] = PWSorting
18
+ sorting_cls = PWSorting
19
19
 
20
- Schema: type[ModelSchema]
20
+ Schema: type[ModelSchema] # type: ignore[override]
21
21
 
22
22
  # Schema auto generation params
23
- schema_base: type[ModelSchema] = ModelSchema
23
+ schema_base = ModelSchema
24
24
 
25
25
  base_property: str = "model"
26
26
 
@@ -29,7 +29,7 @@ class PWSort(Sort):
29
29
  class PWSorting(Sorting):
30
30
  """Sort Peewee ORM Queries."""
31
31
 
32
- MUTATE_CLASS: type[PWSort] = PWSort
32
+ MUTATE_CLASS = PWSort
33
33
 
34
34
  def prepare(self, collection: TVCollection) -> TVCollection:
35
35
  """Prepare collection for sorting."""
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any, Generator, Iterable, Mapping, Sequence, cast
5
+ from typing import TYPE_CHECKING, Any, Generator, Iterable, Sequence, cast
6
6
 
7
7
  from .utils import Mutate, Mutator
8
8
 
@@ -23,11 +23,10 @@ class Sort(Mutate):
23
23
  return sorted(collection, key=lambda obj: getattr(obj, self.name), reverse=desc)
24
24
 
25
25
 
26
- class Sorting(Mutator):
26
+ class Sorting(Mutator[Sort]):
27
27
  """Build sorters for handlers."""
28
28
 
29
29
  MUTATE_CLASS = Sort
30
- mutations: Mapping[str, Sort]
31
30
 
32
31
  def __init__(self, handler: type[RESTBase], params: Iterable):
33
32
  """Initialize the sorting."""
@@ -59,11 +58,13 @@ class Sorting(Mutator):
59
58
  """Prepare the collection."""
60
59
  return collection
61
60
 
62
- def convert(self, obj, **meta) -> Sort:
61
+ def convert(self, obj, **meta):
63
62
  """Prepare sorters."""
64
- sort = cast("Sort", super(Sorting, self).convert(obj, **meta))
65
- if sort.meta.get("default"):
63
+ sort = super(Sorting, self).convert(obj, **meta)
64
+
65
+ if sort and sort.meta.get("default"):
66
66
  self.default.append(sort)
67
+
67
68
  return sort
68
69
 
69
70
  def sort_default(self, collection: TVCollection) -> TVCollection:
@@ -35,7 +35,8 @@ class SQLAlchemyAutoSchema(BaseSQLAlchemyAutoSchema):
35
35
  """
36
36
  cols_to_fields = {f.attribute or f.name: f for f in self.declared_fields.values()}
37
37
  if not partial:
38
- for column in self.opts.table.columns:
38
+ table = self.opts.table # type: ignore[]
39
+ for column in table.columns:
39
40
  field = cols_to_fields.get(column.name)
40
41
  if not field:
41
42
  continue
@@ -67,12 +68,11 @@ class SQLAlchemyAutoSchema(BaseSQLAlchemyAutoSchema):
67
68
  class SARESTOptions(RESTOptions):
68
69
  """Support SQLAlchemy Core."""
69
70
 
70
- filters_cls: type[SAFilters] = SAFilters
71
- sorting_cls: type[SASorting] = SASorting
71
+ filters_cls = SAFilters
72
+ sorting_cls = SASorting
72
73
 
73
74
  # Schema auto generation params
74
- Schema: type[SQLAlchemyAutoSchema]
75
- schema_base: type[SQLAlchemyAutoSchema] = SQLAlchemyAutoSchema
75
+ schema_base = SQLAlchemyAutoSchema
76
76
 
77
77
  table: sa.Table
78
78
  table_pk: sa.Column
@@ -110,8 +110,8 @@ class SARESTOptions(RESTOptions):
110
110
  class SARESTHandler(RESTHandler[TVResource]):
111
111
  """Support SQLAlchemy Core."""
112
112
 
113
- meta: SARESTOptions
114
- meta_class: type[SARESTOptions] = SARESTOptions
113
+ meta: SARESTOptions # type: ignore[bad-override]
114
+ meta_class = SARESTOptions
115
115
  collection: sa.sql.Select
116
116
 
117
117
  async def prepare_collection(self, request: Request) -> sa.sql.Select:
@@ -163,7 +163,7 @@ class SARESTHandler(RESTHandler[TVResource]):
163
163
  """Save the given resource."""
164
164
  meta = self.meta
165
165
  insert_query = meta.table.insert()
166
- table_pk = cast("sa.Column", meta.table_pk)
166
+ table_pk = meta.table_pk
167
167
  if update:
168
168
  update_query = self.meta.table.update().where(table_pk == resource[table_pk.name]) # type: ignore[call-overload]
169
169
  await meta.database.execute(update_query, resource)
@@ -175,7 +175,7 @@ class SARESTHandler(RESTHandler[TVResource]):
175
175
 
176
176
  async def remove(self, request: Request, resource: TVResource | None = None):
177
177
  """Remove the given resource."""
178
- table_pk = cast("sa.Column", self.meta.table_pk)
178
+ table_pk = self.meta.table_pk
179
179
  keys = [resource[table_pk.name]] if resource else await request.data()
180
180
  if not keys:
181
181
  raise APIError.NOT_FOUND()
@@ -1,8 +1,9 @@
1
1
  """REST Utils."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import abc
5
- from typing import TYPE_CHECKING, Any, Iterable, Mapping
6
+ from typing import TYPE_CHECKING, Any, Generic, Iterable, Mapping, TypeVar
6
7
 
7
8
  if TYPE_CHECKING:
8
9
  from muffin import Request
@@ -28,16 +29,19 @@ class Mutate(abc.ABC):
28
29
  return f"<{self.__class__.__name__} '{self.name}'>"
29
30
 
30
31
  @abc.abstractmethod
31
- async def apply(self, collection: TVCollection) -> TVCollection:
32
+ async def apply(self, collection: TVCollection) -> tuple[Any, TVCollection]:
32
33
  """Apply the mutation."""
33
- raise NotImplementedError
34
+ ...
35
+
36
+
37
+ M = TypeVar("M", bound=Mutate)
34
38
 
35
39
 
36
- class Mutator(abc.ABC):
40
+ class Mutator(abc.ABC, Generic[M]):
37
41
  """Mutate collections."""
38
42
 
39
- MUTATE_CLASS: type[Mutate]
40
- mutations: Mapping[str, Mutate]
43
+ MUTATE_CLASS: type[M]
44
+ mutations: Mapping[str, M]
41
45
 
42
46
  def __init__(self, handler, params: Iterable):
43
47
  """Initialize the mutations."""
@@ -63,16 +67,17 @@ class Mutator(abc.ABC):
63
67
  def __bool__(self):
64
68
  return bool(self.mutations)
65
69
 
66
- def convert(self, obj, **meta) -> Mutate:
70
+ def convert(self, obj, **meta) -> M | None:
67
71
  """Convert params to mutations."""
68
- if isinstance(obj, self.MUTATE_CLASS):
69
- return obj
72
+ factory = self.MUTATE_CLASS
73
+ if not isinstance(obj, factory):
74
+ obj = factory(obj, **meta)
70
75
 
71
- return self.MUTATE_CLASS(obj, **meta)
76
+ return obj
72
77
 
73
78
  @abc.abstractmethod
74
79
  async def apply(
75
80
  self, request: Request, collection: TVCollection
76
81
  ) -> tuple[TVCollection, dict[str, Any]]:
77
82
  """Mutate a collection."""
78
- raise NotImplementedError
83
+ ...
@@ -0,0 +1,124 @@
1
+ [project]
2
+ name = "muffin-rest"
3
+ version = "13.1.1"
4
+ description = "The package provides enhanced support for writing REST APIs with Muffin framework"
5
+ authors = [{ name = "Kirill Klenov", email = "horneds@gmail.com" }]
6
+ requires-python = ">=3.10,<4"
7
+ readme = "README.md"
8
+ license = "MIT"
9
+ keywords = ["rest", "api", "muffin", "asgi", "asyncio", "trio"]
10
+ classifiers = [
11
+ "Development Status :: 5 - Production/Stable",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Topic :: Internet :: WWW/HTTP",
22
+ "Framework :: AsyncIO",
23
+ "Framework :: Trio",
24
+ ]
25
+ dependencies = ["apispec>=6,<7", "marshmallow>=3,<4", "muffin"]
26
+
27
+ [project.optional-dependencies]
28
+ yaml = ["pyyaml"]
29
+ peewee = ["muffin-peewee-aio", "marshmallow-peewee"]
30
+ sqlalchemy = ["muffin-databases", "marshmallow-sqlalchemy", "sqlalchemy"]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/klen/muffin-rest"
34
+ Repository = "https://github.com/klen/muffin-rest"
35
+
36
+ [dependency-groups]
37
+ dev = [
38
+ "aiosqlite",
39
+ "marshmallow-peewee",
40
+ "marshmallow-sqlalchemy",
41
+ "muffin-databases",
42
+ "muffin-mongo",
43
+ "muffin-peewee-aio",
44
+ "pytest",
45
+ "pytest-aio[curio, trio]",
46
+ "pyyaml",
47
+ "ruff",
48
+ "pyrefly",
49
+ "pre-commit",
50
+ "tox",
51
+ ]
52
+ example = ["uvicorn", "muffin-peewee-aio", "marshmallow-peewee"]
53
+
54
+ [tool.uv]
55
+ default-groups = ["dev", "example"]
56
+
57
+ [build-system]
58
+ requires = ["uv_build>=0.9.7,<0.10.0"]
59
+ build-backend = "uv_build"
60
+
61
+ [tool.uv.build-backend]
62
+ module-name = "muffin_rest"
63
+ module-root = ""
64
+
65
+ [tool.pytest.ini_options]
66
+ addopts = "-xsv tests"
67
+ log_cli = true
68
+
69
+ [tool.pyrefly]
70
+ project-includes = ["muffin_rest"]
71
+ project-excludes = ["tests", "examples", "docs", "setup.py", "pyproject.toml"]
72
+
73
+ [tool.tox]
74
+ legacy_tox_ini = """
75
+ [tox]
76
+ envlist = py310,py311,py312,py313,py314,pypy311
77
+
78
+ [testenv]
79
+ allowlist_externals = uv,pytest
80
+ commands =
81
+ uv pip install --system -e .[dev]
82
+ pytest tests
83
+ """
84
+
85
+ [tool.ruff]
86
+ line-length = 100
87
+ target-version = "py310"
88
+ exclude = [".venv", "docs", "examples"]
89
+
90
+ [tool.ruff.lint]
91
+ select = ["ALL"]
92
+ ignore = [
93
+ "A003",
94
+ "ANN",
95
+ "ARG",
96
+ "COM",
97
+ "D",
98
+ "DJ",
99
+ "EM",
100
+ "FA100",
101
+ "FIX",
102
+ "N",
103
+ "PLC0415",
104
+ "PLR0912",
105
+ "PLR2004",
106
+ "RET",
107
+ "RSE",
108
+ "RUF012",
109
+ "S101",
110
+ "SLF",
111
+ "TD",
112
+ "TRY003",
113
+ "UP",
114
+ ]
115
+
116
+ [tool.bumpversion]
117
+ current_version = "13.1.1"
118
+ commit = false
119
+ tag = false
120
+
121
+ [[tool.bumpversion.files]]
122
+ filename = "pyproject.toml"
123
+ search = 'version = "{current_version}"'
124
+ replace = 'version = "{new_version}"'
@@ -1,22 +0,0 @@
1
-
2
- The MIT License (MIT)
3
-
4
- Copyright (c) 2015, Kirill Klenov
5
-
6
- Permission is hereby granted, free of charge, to any person obtaining a copy
7
- of this software and associated documentation files (the "Software"), to deal
8
- in the Software without restriction, including without limitation the rights
9
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- copies of the Software, and to permit persons to whom the Software is
11
- furnished to do so, subject to the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be included in all
14
- copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
- SOFTWARE.
@@ -1,125 +0,0 @@
1
- [tool.poetry]
2
- name = "muffin-rest"
3
- version = "13.0.3"
4
- description = "The package provides enhanced support for writing REST APIs with Muffin framework"
5
- readme = "README.md"
6
- homepage = "https://github.com/klen/muffin-rest"
7
- repository = "https://github.com/klen/muffin-rest"
8
- authors = ["Kirill Klenov <horneds@gmail.com>"]
9
- license = "MIT"
10
- keywords = ["rest", "api", "muffin", "asgi", "asyncio", "trio"]
11
- classifiers = [
12
- "Development Status :: 5 - Production/Stable",
13
- "Intended Audience :: Developers",
14
- "License :: OSI Approved :: MIT License",
15
- "Programming Language :: Python",
16
- "Programming Language :: Python :: 3",
17
- "Programming Language :: Python :: 3.10",
18
- "Programming Language :: Python :: 3.11",
19
- "Programming Language :: Python :: 3.12",
20
- "Programming Language :: Python :: 3.13",
21
- "Topic :: Internet :: WWW/HTTP",
22
- "Framework :: AsyncIO",
23
- "Framework :: Trio",
24
- ]
25
-
26
- [tool.poetry.dependencies]
27
- python = "^3.10"
28
- apispec = "^6"
29
- marshmallow = "^3"
30
- muffin = "*"
31
-
32
- # Optional dependencies
33
- pyyaml = { version = "*", optional = true }
34
- muffin-peewee-aio = { version = "*", optional = true }
35
- marshmallow-peewee = { version = "*", optional = true }
36
- muffin-databases = { version = "*", optional = true }
37
- marshmallow-sqlalchemy = { version = "*", optional = true }
38
- sqlalchemy = { version = "*", optional = true }
39
-
40
- [tool.poetry.extras]
41
- yaml = ["pyyaml"]
42
- peewee = ["muffin-peewee-aio", "marshmallow-peewee"]
43
- sqlalchemy = ["muffin-databases", "marshmallow-sqlalchemy", "sqlalchemy"]
44
-
45
- [tool.poetry.group.tests.dependencies]
46
- aiosqlite = "*"
47
- marshmallow-peewee = "*"
48
- marshmallow-sqlalchemy = "*"
49
- muffin-databases = "*"
50
- muffin-mongo = "*"
51
- muffin-peewee-aio = "*"
52
- pytest = "*"
53
- pytest-aio = { version = "*", extras = ["curio", "trio"] }
54
- pyyaml = "*"
55
- types-PyYAML = "*"
56
- types-ujson = "*"
57
- ruff = "*"
58
- mypy = "*"
59
-
60
- [tool.poetry.group.dev.dependencies]
61
- pre-commit = "*"
62
-
63
- [tool.poetry.group.example.dependencies]
64
- uvicorn = "*"
65
- muffin-peewee-aio = "*"
66
- marshmallow-peewee = "*"
67
-
68
-
69
- [tool.pytest.ini_options]
70
- addopts = "-xsv tests"
71
- log_cli = true
72
-
73
- [tool.mypy]
74
- packages = ["muffin_rest"]
75
- ignore_missing_imports = true
76
-
77
- [tool.tox]
78
- legacy_tox_ini = """
79
- [tox]
80
- envlist = py310,py311,py312,py313,pypy310
81
-
82
- [testenv]
83
- deps = -e .[tests]
84
- commands =
85
- pytest tests
86
- """
87
-
88
- [tool.ruff]
89
- line-length = 100
90
- target-version = "py310"
91
- exclude = [".venv", "docs", "examples"]
92
-
93
- [tool.ruff.lint]
94
- select = ["ALL"]
95
- ignore = [
96
- "A003",
97
- "ANN",
98
- "ARG",
99
- "COM",
100
- "D",
101
- "DJ",
102
- "EM",
103
- "FA100",
104
- "FIX",
105
- "N804",
106
- "PLC0415",
107
- "PLR0912",
108
- "PLR2004",
109
- "RET",
110
- "RSE",
111
- "S101",
112
- "SLF",
113
- "TD",
114
- "TRY003",
115
- "UP",
116
- ]
117
-
118
- [tool.black]
119
- line-length = 100
120
- target-version = ["py310", "py311", "py312", "py313"]
121
- preview = true
122
-
123
- [build-system]
124
- requires = ["poetry-core>=1.0.0"]
125
- build-backend = "poetry.core.masonry.api"
File without changes