Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,3 @@ repos:
- id: bandit
args: [-c, pyproject.toml]
additional_dependencies: ['bandit[toml]']
default_language_version:
python: python3.9
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Version 2.0.0

* Refactoring of the module has been carried out. Improved class and method interfaces.

## Version 1.0.0

* Initial public release.
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ CRUD tools for working with database via SQLAlchemy.
- [DB-First](#db-first)
- [Features](#features)
- [Installation](#installation)
- [Example](#example)
- [Examples](#examples)
- [Full example](#full-example)

<!--TOC-->

Expand All @@ -29,9 +30,9 @@ Install and update using `pip`:
$ pip install -U db_first
```

## Example
## Examples

File with application initialization `main.py`:
### Full example

```python
from uuid import UUID
Expand Down Expand Up @@ -85,7 +86,7 @@ class ItemController(CreateMixin, ReadMixin, UpdateMixin, DeleteMixin, Paginatio
output_schema_of_create = OutputSchema
output_schema_of_read = OutputSchema
output_schema_of_update = OutputSchema
output_schema_of_paginate = OutputSchema
schema_of_paginate = OutputSchema
sortable = ['created_at']


Expand All @@ -94,24 +95,23 @@ if __name__ == '__main__':

first_new_item = item.create(data={'data': 'first'})
print('Item as object:', first_new_item)
second_new_item = item.create(data={'data': 'second'}, jsonify=True)
second_new_item = item.create(data={'data': 'second'}, serialize=True)
print('Item as dict:', second_new_item)

first_item = item.read(first_new_item.id)
print('Item as object:', first_item)
first_item = item.read(first_new_item.id, jsonify=True)
first_item = item.read(first_new_item.id, serialize=True)
print('Item as dict:', first_item)

updated_first_item = item.update(data={'id': first_new_item.id, 'data': 'updated_first'})
print('Item as object:', updated_first_item)
updated_second_item = item.update(
data={'id': UUID(second_new_item['id']), 'data': 'updated_second'}, jsonify=True
data={'id': UUID(second_new_item['id']), 'data': 'updated_second'}, serialize=True
)
print('Item as dict:', updated_second_item)

items = item.paginate(sort_created_at='desc')
print('Items as objects:', items)
items = item.paginate(sort_created_at='desc', jsonify=True)
items = item.paginate(sort_created_at='desc', serialize=True)
print('Items as dicts:', items)

```
10 changes: 5 additions & 5 deletions examples/full_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class Meta:
output_schema_of_create = OutputSchema
output_schema_of_read = OutputSchema
output_schema_of_update = OutputSchema
output_schema_of_paginate = OutputSchema
schema_of_paginate = OutputSchema
sortable = ['created_at']


Expand All @@ -58,22 +58,22 @@ class Meta:

first_new_item = item.create(data={'data': 'first'})
print('Item as object:', first_new_item)
second_new_item = item.create(data={'data': 'second'}, jsonify=True)
second_new_item = item.create(data={'data': 'second'}, serialize=True)
print('Item as dict:', second_new_item)

first_item = item.read(first_new_item.id)
print('Item as object:', first_item)
first_item = item.read(first_new_item.id, jsonify=True)
first_item = item.read(first_new_item.id, serialize=True)
print('Item as dict:', first_item)

updated_first_item = item.update(data={'id': first_new_item.id, 'data': 'updated_first'})
print('Item as object:', updated_first_item)
updated_second_item = item.update(
data={'id': UUID(second_new_item['id']), 'data': 'updated_second'}, jsonify=True
data={'id': UUID(second_new_item['id']), 'data': 'updated_second'}, serialize=True
)
print('Item as dict:', updated_second_item)

items = item.paginate(sort_created_at='desc')
print('Items as objects:', items)
items = item.paginate(sort_created_at='desc', jsonify=True)
items = item.paginate(sort_created_at='desc', serialize=True)
print('Items as dicts:', items)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ license = {file = "LICENSE"}
name = "DB-First"
readme = "README.md"
requires-python = ">=3.9"
version = "1.0.0"
version = "2.0.0"

[project.optional-dependencies]
dev = [
Expand Down
23 changes: 10 additions & 13 deletions src/db_first/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import Any
from typing import Optional

from sqlalchemy.engine import Result

Expand All @@ -9,24 +8,22 @@

class BaseCRUD:
@classmethod
def _get_option_from_meta(cls, name: str, default: Any = ...) -> Optional[Any]:
meta = getattr(cls, 'Meta', None)
if meta is None:
def _get_option_from_meta(cls, name: str) -> Any:
try:
meta = cls.Meta
except AttributeError:
raise MetaNotFound('You need add class Meta with options.')

try:
option = getattr(meta, name)
except AttributeError:
if default is Ellipsis:
raise OptionNotFound(f'Option <{name}> not set in Meta class.')
else:
option = default
raise OptionNotFound(f'Option <{name}> not set in Meta class.')

return option

@classmethod
def _deserialize_data(cls, schema_name: str, data: dict) -> dict:
schema = cls._get_option_from_meta(schema_name, None)
def deserialize_data(cls, schema_name: str, data: dict) -> dict:
schema = cls._get_option_from_meta(schema_name)
return schema().load(data)

@classmethod
Expand All @@ -39,7 +36,7 @@ def _clean_data(cls, data: Any) -> Any:
:return: cleaned object.
"""

empty_values = ['', None, [], {}, (), set()]
empty_values = ('', None, [], {}, (), set())

if isinstance(data, dict):
cleaned_dict = {k: cls._clean_data(v) for k, v in data.items()}
Expand All @@ -53,8 +50,8 @@ def _clean_data(cls, data: Any) -> Any:
return data

@classmethod
def _data_to_json(cls, schema_name: str, data: Result, fields: list = None) -> dict:
output_schema = cls._get_option_from_meta(schema_name, None)
def serialize_data(cls, schema_name: str, data: Result, fields: list = None) -> dict:
output_schema = cls._get_option_from_meta(schema_name)

if isinstance(data, list):
serialized_data = output_schema(many=True, only=fields).dump(data)
Expand Down
76 changes: 48 additions & 28 deletions src/db_first/mixins/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,32 @@

from sqlalchemy import delete
from sqlalchemy import select
from sqlalchemy import update
from sqlalchemy.engine import Result


class CreateMixin:
"""Create object in database."""
"""Create object in database.

This mixin supports the following options in the Meta class:
```
class CustomController(CreateMixin, BaseCRUD):
class Meta:
session = Session
model = Model
input_schema_of_create = InputSchema
output_schema_of_create = OutputSchema

custom_controller = CustomController()
```

`input_schema_of_create` - marshmallow schema for validating and deserialization input data.
`output_schema_of_create` - marshmallow schema for serialization output data.
"""

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

def _create_object(self, **data) -> Result:
session = self._get_option_from_meta('session')
model = self._get_option_from_meta('model')

Expand All @@ -17,69 +36,70 @@ def _create_object(self, **data) -> Result:
session.commit()
return new_obj

def create(self, data: dict, validating: bool = True, jsonify: bool = False) -> Result or dict:
if validating:
self._deserialize_data('input_schema_of_create', data)

new_object = self._create_object(**data)
def create(self, data: dict, serialize: bool = False) -> Result or dict:
deserialized_data = self.deserialize_data('input_schema_of_create', data)
new_object = self.create_object(**deserialized_data)

if jsonify:
return self._data_to_json('output_schema_of_create', new_object)
if serialize:
return self.serialize_data('output_schema_of_create', new_object)

return new_object


class ReadMixin:
"""Read object from database."""

def _read_object(self, id: Any) -> Result:
def get_object(self, id: Any) -> Result:
"""If this method does not suit you, simply override it in your class."""

session = self._get_option_from_meta('session')
model = self._get_option_from_meta('model')
return session.scalars(select(model).where(model.id == id)).one()

def read(self, id, jsonify: bool = False) -> Result or dict:
obj = self._read_object(id)
def read(self, id, serialize: bool = False) -> Result or dict:
obj = self.get_object(id)

if jsonify:
return self._data_to_json('output_schema_of_read', obj)
if serialize:
return self.serialize_data('output_schema_of_read', obj)

return obj


class UpdateMixin:
"""Update object in database."""

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

session = self._get_option_from_meta('session')
model = self._get_option_from_meta('model')

stmt = update(model).where(model.id == id).values(**data)
session.execute(stmt)

obj = session.scalars(select(model).where(model.id == id)).one()
for k, v in data.items():
setattr(obj, k, v)
session.commit()
return obj

def update(self, data: dict, validating: bool = True, jsonify: bool = False) -> Result or dict:
if validating:
self._deserialize_data('input_schema_of_update', data)
def update(self, data: dict, serialize: bool = False) -> Result or dict:
deserialized_data = self.deserialize_data('input_schema_of_update', data)
updated_object = self.update_object(**deserialized_data)

updated_object = self._update_object(**data)

if jsonify:
return self._data_to_json('output_schema_of_update', updated_object)
if serialize:
return self.serialize_data('output_schema_of_update', updated_object)

return updated_object


class DeleteMixin:
"""Delete object from database."""

def _delete_object(self, id: Any) -> None:
def delete_object(self, id: Any) -> None:
"""If this method does not suit you, simply override it in your class."""

session = self._get_option_from_meta('session')
model = self._get_option_from_meta('model')

session.execute(delete(model).where(model.id == id))
session.commit()

def delete(self, id: Any) -> None:
self._delete_object(id)
self.delete_object(id)
Loading