Skip to content

Commit 7ab3bf3

Browse files
committed
improve CRUD-methods.
1 parent ccd2f7d commit 7ab3bf3

14 files changed

+500
-502
lines changed

README.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ from db_first import BaseCRUD
4141
from db_first.base_model import ModelMixin
4242
from db_first.mixins import CreateMixin
4343
from db_first.mixins import DeleteMixin
44-
from db_first.mixins import PaginationMixin
4544
from db_first.mixins import ReadMixin
4645
from db_first.mixins import UpdateMixin
4746
from marshmallow import fields
@@ -73,17 +72,22 @@ class InputSchemaOfUpdate(InputSchemaOfCreate):
7372
id = fields.UUID()
7473

7574

75+
class InputSchemaOfRead(Schema):
76+
id = fields.UUID()
77+
78+
7679
class OutputSchema(InputSchemaOfUpdate):
7780
created_at = fields.DateTime()
7881

7982

80-
class ItemController(CreateMixin, ReadMixin, UpdateMixin, DeleteMixin, PaginationMixin, BaseCRUD):
83+
class ItemController(CreateMixin, ReadMixin, UpdateMixin, DeleteMixin, BaseCRUD):
8184
class Meta:
8285
session = session
8386
model = Items
8487
input_schema_of_create = InputSchemaOfCreate
8588
input_schema_of_update = InputSchemaOfUpdate
8689
output_schema_of_create = OutputSchema
90+
input_schema_of_read = InputSchemaOfRead
8791
output_schema_of_read = OutputSchema
8892
output_schema_of_update = OutputSchema
8993
schema_of_paginate = OutputSchema
@@ -93,14 +97,14 @@ class ItemController(CreateMixin, ReadMixin, UpdateMixin, DeleteMixin, Paginatio
9397
if __name__ == '__main__':
9498
item = ItemController()
9599

96-
first_new_item = item.create(data={'data': 'first'})
100+
first_new_item = item.create(deserialize=True, data='first')
97101
print('Item as object:', first_new_item)
98-
second_new_item = item.create(data={'data': 'second'}, serialize=True)
102+
second_new_item = item.create(deserialize=True, data='second', serialize=True)
99103
print('Item as dict:', second_new_item)
100104

101-
first_item = item.read(first_new_item.id)
105+
first_item = item.read(id=first_new_item.id)
102106
print('Item as object:', first_item)
103-
first_item = item.read(first_new_item.id, serialize=True)
107+
first_item = item.read(id=first_new_item.id)
104108
print('Item as dict:', first_item)
105109

106110
updated_first_item = item.update(data={'id': first_new_item.id, 'data': 'updated_first'})
@@ -110,8 +114,9 @@ if __name__ == '__main__':
110114
)
111115
print('Item as dict:', updated_second_item)
112116

113-
items = item.paginate(sort_created_at='desc')
117+
items = item.read(sort_created_at='desc')
114118
print('Items as objects:', items)
115-
items = item.paginate(sort_created_at='desc', serialize=True)
119+
items = item.read(sort_created_at='desc', serialize=True)
116120
print('Items as dicts:', items)
121+
117122
```

examples/full_example.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from db_first.base_model import ModelMixin
55
from db_first.mixins import CreateMixin
66
from db_first.mixins import DeleteMixin
7-
from db_first.mixins import PaginationMixin
87
from db_first.mixins import ReadMixin
98
from db_first.mixins import UpdateMixin
109
from marshmallow import fields
@@ -36,17 +35,22 @@ class InputSchemaOfUpdate(InputSchemaOfCreate):
3635
id = fields.UUID()
3736

3837

38+
class InputSchemaOfRead(Schema):
39+
id = fields.UUID()
40+
41+
3942
class OutputSchema(InputSchemaOfUpdate):
4043
created_at = fields.DateTime()
4144

4245

43-
class ItemController(CreateMixin, ReadMixin, UpdateMixin, DeleteMixin, PaginationMixin, BaseCRUD):
46+
class ItemController(CreateMixin, ReadMixin, UpdateMixin, DeleteMixin, BaseCRUD):
4447
class Meta:
4548
session = session
4649
model = Items
4750
input_schema_of_create = InputSchemaOfCreate
4851
input_schema_of_update = InputSchemaOfUpdate
4952
output_schema_of_create = OutputSchema
53+
input_schema_of_read = InputSchemaOfRead
5054
output_schema_of_read = OutputSchema
5155
output_schema_of_update = OutputSchema
5256
schema_of_paginate = OutputSchema
@@ -56,14 +60,14 @@ class Meta:
5660
if __name__ == '__main__':
5761
item = ItemController()
5862

59-
first_new_item = item.create(data={'data': 'first'})
63+
first_new_item = item.create(deserialize=True, data='first')
6064
print('Item as object:', first_new_item)
61-
second_new_item = item.create(data={'data': 'second'}, serialize=True)
65+
second_new_item = item.create(deserialize=True, data='second', serialize=True)
6266
print('Item as dict:', second_new_item)
6367

64-
first_item = item.read(first_new_item.id)
68+
first_item = item.read(id=first_new_item.id)
6569
print('Item as object:', first_item)
66-
first_item = item.read(first_new_item.id, serialize=True)
70+
first_item = item.read(id=first_new_item.id)
6771
print('Item as dict:', first_item)
6872

6973
updated_first_item = item.update(data={'id': first_new_item.id, 'data': 'updated_first'})
@@ -73,7 +77,7 @@ class Meta:
7377
)
7478
print('Item as dict:', updated_second_item)
7579

76-
items = item.paginate(sort_created_at='desc')
80+
items = item.read(sort_created_at='desc')
7781
print('Items as objects:', items)
78-
items = item.paginate(sort_created_at='desc', serialize=True)
82+
items = item.read(sort_created_at='desc', serialize=True)
7983
print('Items as dicts:', items)

src/db_first/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .base import BaseCRUD
22
from .base_model import ModelMixin
3-
from .query_maker import QueryMaker
3+
from .statement_maker import StatementMaker
44

5-
__all__ = ['QueryMaker', 'BaseCRUD', 'ModelMixin']
5+
__all__ = ['BaseCRUD', 'ModelMixin', 'StatementMaker']

src/db_first/base.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Any
2+
from typing import Optional
23

34
from sqlalchemy.engine import Result
45

@@ -8,7 +9,7 @@
89

910
class BaseCRUD:
1011
@classmethod
11-
def _get_option_from_meta(cls, name: str) -> Any:
12+
def _get_option_from_meta(cls, name: str, default: Optional[Any] = ...) -> Any:
1213
try:
1314
meta = cls.Meta
1415
except AttributeError:
@@ -17,7 +18,10 @@ def _get_option_from_meta(cls, name: str) -> Any:
1718
try:
1819
option = getattr(meta, name)
1920
except AttributeError:
20-
raise OptionNotFound(f'Option <{name}> not set in Meta class.')
21+
if default is Ellipsis:
22+
raise OptionNotFound(f'Option <{name}> not set in Meta class.')
23+
else:
24+
option = default
2125

2226
return option
2327

src/db_first/mixins/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@
22
from .crud import DeleteMixin
33
from .crud import ReadMixin
44
from .crud import UpdateMixin
5-
from .pagination import PaginationMixin
65

7-
__all__ = ['CreateMixin', 'ReadMixin', 'UpdateMixin', 'DeleteMixin', 'PaginationMixin']
6+
__all__ = ['CreateMixin', 'ReadMixin', 'UpdateMixin', 'DeleteMixin']

src/db_first/mixins/crud.py

Lines changed: 152 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1+
from math import ceil
12
from typing import Any
3+
from typing import Optional
24

35
from sqlalchemy import delete
6+
from sqlalchemy import func
7+
from sqlalchemy import Select
48
from sqlalchemy import select
59
from sqlalchemy import update
610
from sqlalchemy.engine import Result
11+
from sqlalchemy.orm import Session
12+
13+
from ..statement_maker import StatementMaker
714

815

916
class CreateMixin:
@@ -25,20 +32,24 @@ class Meta:
2532
`output_schema_of_create` - marshmallow schema for serialization output data.
2633
"""
2734

28-
def create_object(self, **data) -> Result:
35+
def create_object(self, **kwargs) -> Result:
2936
"""If this method does not suit you, simply override it in your class."""
3037

3138
session = self._get_option_from_meta('session')
3239
model = self._get_option_from_meta('model')
3340

34-
new_obj = model(**data)
41+
new_obj = model(**kwargs)
3542
session.add(new_obj)
3643
session.commit()
3744
return new_obj
3845

39-
def create(self, data: dict, serialize: bool = False) -> Result or dict:
40-
deserialized_data = self.deserialize_data('input_schema_of_create', data)
41-
new_object = self.create_object(**deserialized_data)
46+
def create(
47+
self, deserialize: bool = False, serialize: bool = False, **kwargs: dict
48+
) -> Result or dict:
49+
if deserialize:
50+
kwargs = self.deserialize_data('input_schema_of_create', kwargs)
51+
52+
new_object = self.create_object(**kwargs)
4253

4354
if serialize:
4455
return self.serialize_data('output_schema_of_create', new_object)
@@ -47,42 +58,163 @@ def create(self, data: dict, serialize: bool = False) -> Result or dict:
4758

4859

4960
class ReadMixin:
50-
"""Read object from database."""
61+
"""Read object from database.
5162
52-
def get_object(self, id: Any) -> Result:
53-
"""If this method does not suit you, simply override it in your class."""
63+
Mixin with get paginated result.
5464
55-
session = self._get_option_from_meta('session')
56-
model = self._get_option_from_meta('model')
57-
return session.scalars(select(model).where(model.id == id)).one()
65+
This mixin supports the following options in the Meta class:
66+
```
67+
class CustomController(PaginationMixin, BaseCRUD):
68+
class Meta:
69+
session = Session
70+
model = Model
71+
filterable = ['id']
72+
interval_filterable = ['id']
73+
sortable = ['id']
74+
searchable = ['id']
75+
schema_of_paginate = Schema
5876
59-
def read(self, id, serialize: bool = False) -> Result or dict:
60-
obj = self.get_object(id)
77+
custom_controller = CustomController()
78+
```
79+
Required options:
80+
`input_schema_of_read` - marshmallow schema for serialize.
81+
`output_schema_of_read` - marshmallow schema for serialize.
82+
83+
Optional options:
84+
`filterable` - list of fields allowed for filtration.
85+
`interval_filterable` - list of fields allowed for filtration interval.
86+
`sortable` - list of fields allowed for sorting.
87+
`searchable` - a list of fields allowed for search for by substring.
88+
"""
89+
90+
def _calculate_items_per_page(
91+
self, session: Session, statement: Select, per_page: int
92+
) -> tuple[int, int]:
93+
total = session.execute(
94+
statement.with_only_columns(func.count()).order_by(None)
95+
).scalar_one()
96+
97+
if per_page == 0:
98+
pages = 0
99+
else:
100+
pages = ceil(total / per_page)
101+
102+
return pages, total
103+
104+
def _make_metadata(self, session: Session, page, per_page, statement):
105+
pages, total = self._calculate_items_per_page(session, statement, per_page)
106+
return {'pagination': {'page': page, 'per_page': per_page, 'pages': pages, 'total': total}}
107+
108+
def paginate(
109+
self,
110+
statement: Optional[Select],
111+
page: int = 1,
112+
per_page: Optional[int] = 20,
113+
max_per_page: Optional[int] = 100,
114+
serialize: bool = False,
115+
include_metadata: bool = False,
116+
fields: Optional[list] = None,
117+
) -> dict:
118+
session: Session = self._get_option_from_meta('session')
119+
120+
if per_page > max_per_page:
121+
per_page = max_per_page
122+
elif per_page < 0:
123+
per_page = 0
124+
125+
if page < 1:
126+
page = 1
127+
128+
items = {'items': []}
129+
if include_metadata:
130+
items['_metadata'] = self._make_metadata(session, page, per_page, statement)
131+
132+
if per_page != 0:
133+
statement = statement.limit(per_page).offset((page - 1) * per_page)
134+
paginated_rows = session.scalars(statement).all()
135+
else:
136+
paginated_rows = []
61137

62138
if serialize:
63-
return self.serialize_data('output_schema_of_read', obj)
139+
items['items'] = self.serialize_data('output_schema_of_read', paginated_rows, fields)
140+
else:
141+
items['items'] = paginated_rows
142+
143+
return items
144+
145+
def read(
146+
self,
147+
page: int = 1,
148+
per_page: Optional[int] = None,
149+
max_per_page: Optional[int] = None,
150+
statement: Optional[Select] = None,
151+
deserialize: bool = False,
152+
serialize: bool = False,
153+
include_metadata: bool = False,
154+
fields: Optional[list] = None,
155+
**kwargs,
156+
) -> Result or dict:
157+
model = self._get_option_from_meta('model')
64158

65-
return obj
159+
if deserialize:
160+
kwargs = self.deserialize_data('input_schema_of_read', kwargs)
161+
162+
filterable_fields = self._get_option_from_meta('filterable', ())
163+
interval_filterable_fields = self._get_option_from_meta('interval_filterable', ())
164+
searchable_fields = self._get_option_from_meta('searchable', ())
165+
sortable_fields = self._get_option_from_meta('sortable', ())
166+
167+
stmt = StatementMaker(
168+
model,
169+
kwargs,
170+
statement=statement,
171+
filterable_fields=filterable_fields,
172+
interval_filterable_fields=interval_filterable_fields,
173+
searchable_fields=searchable_fields,
174+
sortable_fields=sortable_fields,
175+
).make_statement()
176+
177+
if per_page is None:
178+
per_page = self._get_option_from_meta('per_page', 20)
179+
180+
if max_per_page is None:
181+
max_per_page = self._get_option_from_meta('max_per_page', 100)
182+
183+
items = self.paginate(
184+
statement=stmt,
185+
page=page,
186+
per_page=per_page,
187+
max_per_page=max_per_page,
188+
serialize=serialize,
189+
include_metadata=include_metadata,
190+
fields=fields,
191+
)
192+
193+
return items
66194

67195

68196
class UpdateMixin:
69197
"""Update object in database."""
70198

71-
def update_object(self, id: Any, **data) -> Result:
199+
def update_object(self, id: Any, **kwargs) -> Result:
72200
"""If this method does not suit you, simply override it in your class."""
73201

74202
session = self._get_option_from_meta('session')
75203
model = self._get_option_from_meta('model')
76204

77-
stmt = update(model).where(model.id == id).values(**data)
205+
stmt = update(model).where(model.id == id).values(**kwargs)
78206
session.execute(stmt)
79207

80208
obj = session.scalars(select(model).where(model.id == id)).one()
81209
return obj
82210

83-
def update(self, data: dict, serialize: bool = False) -> Result or dict:
84-
deserialized_data = self.deserialize_data('input_schema_of_update', data)
85-
updated_object = self.update_object(**deserialized_data)
211+
def update(
212+
self, data: dict, deserialize: bool = False, serialize: bool = False
213+
) -> Result or dict:
214+
if deserialize:
215+
data = self.deserialize_data('input_schema_of_update', data)
216+
217+
updated_object = self.update_object(**data)
86218

87219
if serialize:
88220
return self.serialize_data('output_schema_of_update', updated_object)

0 commit comments

Comments
 (0)