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
1 change: 1 addition & 0 deletions .github/workflows/testing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:
3.10
3.11
3.12
3.13
- run: make format
test:
runs-on: ubuntu-latest
Expand Down
22 changes: 11 additions & 11 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/asottile/pyupgrade
rev: v3.17.0
rev: v3.20.0
hooks:
- id: pyupgrade
args: [--py39]
Expand All @@ -9,11 +9,11 @@ repos:
hooks:
- id: rm-unneeded-f-str
- repo: https://github.com/mxr/unkey
rev: v0.0.1
rev: v0.0.2
hooks:
- id: unkey
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v6.0.0
hooks:
- id: check-ast
- id: fix-byte-order-marker
Expand All @@ -27,13 +27,13 @@ repos:
- id: double-quote-string-fixer
- id: check-merge-conflict
- repo: https://github.com/PyCQA/docformatter
rev: v1.7.5
rev: v1.7.7
hooks:
- id: docformatter
additional_dependencies: [tomli]
args: [--in-place, --config, ./pyproject.toml]
- repo: https://github.com/jshwi/docsig
rev: v0.63.0
rev: v0.71.0
hooks:
- id: docsig
args:
Expand All @@ -43,11 +43,11 @@ repos:
- --check-protected
- --ignore-no-params
- repo: https://github.com/psf/black
rev: 24.8.0
rev: 25.1.0
hooks:
- id: black
- repo: https://github.com/asottile/reorder_python_imports
rev: v3.13.0
rev: v3.15.0
hooks:
- id: reorder-python-imports
- repo: https://github.com/pre-commit/pygrep-hooks
Expand All @@ -57,22 +57,22 @@ repos:
- id: python-no-eval
- id: text-unicode-replacement-char
- repo: https://github.com/pycqa/flake8
rev: 7.1.1
rev: 7.3.0
hooks:
- id: flake8
additional_dependencies:
- flake8-bugbear
- flake8-implicit-str-concat
args: [--max-line-length=100]
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.14.0
rev: v2.15.0
hooks:
- id: pretty-format-yaml
args: [--autofix, --indent, '2']
- id: pretty-format-toml
args: [--autofix, --indent, '2']
- repo: https://codeberg.org/frnmst/md-toc
rev: 8.2.3
rev: 9.0.0
hooks:
- id: md-toc
args: [-p, github]
Expand All @@ -81,7 +81,7 @@ repos:
hooks:
- id: checkmake
- repo: https://github.com/PyCQA/bandit
rev: 1.7.10
rev: 1.8.6
hooks:
- id: bandit
args: [-c, pyproject.toml]
Expand Down
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## Version 3.0.0

* Add method `paginate()` to `ReadMixin`.
* Change interface for methods `read()` from `ReadMixin`, `delete()` from `DeleteMixin`.

## Version 2.1.0

* `PaginateMixin` removed, this functional moved to `ReadMixin`.
Expand Down
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ CRUD tools for working with database via SQLAlchemy.
## Features

* CreateMixin, ReadMixin, UpdateMixin, DeleteMixin for CRUD operation for database.
* PaginateMixin for get paginated data from database.
* QueryMaker class for create query 'per-one-model'.
* Marshmallow (https://github.com/marshmallow-code/marshmallow) schemas for validating input data.
* ReadMixin support paginated data from database.
* StatementMaker class for create query 'per-one-model'.
* Marshmallow (https://github.com/marshmallow-code/marshmallow) schemas for serialization input data.
* Marshmallow schemas for deserialization SQLAlchemy result object to `dict`.

## Installation
Expand Down Expand Up @@ -97,14 +97,14 @@ class ItemController(CreateMixin, ReadMixin, UpdateMixin, DeleteMixin, BaseCRUD)
if __name__ == '__main__':
item = ItemController()

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

first_item = item.read(id=first_new_item.id)
first_item = item.read({'id': first_new_item.id})
print('Item as object:', first_item)
first_item = item.read(id=first_new_item.id)
first_item = item.read({'id': first_new_item.id})
print('Item as dict:', first_item)

updated_first_item = item.update(data={'id': first_new_item.id, 'data': 'updated_first'})
Expand All @@ -114,9 +114,10 @@ if __name__ == '__main__':
)
print('Item as dict:', updated_second_item)

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


```
12 changes: 6 additions & 6 deletions examples/full_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ class Meta:
if __name__ == '__main__':
item = ItemController()

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

first_item = item.read(id=first_new_item.id)
first_item = item.read({'id': first_new_item.id})
print('Item as object:', first_item)
first_item = item.read(id=first_new_item.id)
first_item = item.read({'id': first_new_item.id})
print('Item as dict:', first_item)

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

items = item.read(sort_created_at='desc')
items = item.paginate(sort_created_at='desc')
print('Items as objects:', items)
items = item.read(sort_created_at='desc', serialize=True)
items = item.paginate(sort_created_at='desc', serialize=True)
print('Items as dicts:', items)
21 changes: 11 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,22 @@ license = {file = "LICENSE"}
name = "DB-First"
readme = "README.md"
requires-python = ">=3.9"
version = "2.1.0"
version = "3.0.0"

[project.optional-dependencies]
dev = [
"build==1.2.2",
"pre-commit==3.8.0",
"pytest==8.3.3",
"pytest-cov==5.0.0",
"python-dotenv==1.0.1",
"tox==4.21.2",
"twine==5.1.1"
"build==1.3.0",
"pre-commit==4.3.0",
"pytest==8.4.2",
"pytest-cov==7.0.0",
"python-dotenv==1.1.1",
"tox==4.30.2",
"twine==6.2.0"
]

[project.urls]
changelog = "https://github.com/flask-pro/db_first/blob/master/CHANGES.md"
repository = "https://github.com/flask-pro/db_first"
changelog = "https://github.com/flask-pro/db-first/blob/master/CHANGES.md"
repository = "https://github.com/flask-pro/db-first"

[tool.bandit]
exclude_dirs = ["tests"]
Expand Down Expand Up @@ -78,6 +78,7 @@ where = ["src"]
legacy_tox_ini = """
[tox]
env_list =
py313
py312
py311
py310
Expand Down
2 changes: 1 addition & 1 deletion src/db_first/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,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 Down
41 changes: 31 additions & 10 deletions src/db_first/mixins/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ def create_object(self, **kwargs) -> Result:
return new_obj

def create(
self, deserialize: bool = False, serialize: bool = False, **kwargs: dict
self, data: dict, deserialize: bool = False, serialize: bool = False
) -> Result or dict:
if deserialize:
kwargs = self.deserialize_data('input_schema_of_create', kwargs)
data = self.deserialize_data('input_schema_of_create', data)

new_object = self.create_object(**kwargs)
new_object = self.create_object(**data)

if serialize:
return self.serialize_data('output_schema_of_create', new_object)
Expand Down Expand Up @@ -146,7 +146,7 @@ def _paginate(

return items

def read(
def paginate(
self,
page: int = 1,
per_page: Optional[int] = None,
Expand Down Expand Up @@ -196,6 +196,27 @@ def read(

return items

def read_object(self, id: Any) -> Result:

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

stmt = select(model).where(model.id == id)
return session.scalars(stmt).one()

def read(
self, data: dict, deserialize: bool = False, serialize: bool = False
) -> Result or dict:
if deserialize:
data = self.deserialize_data('input_schema_of_read', data)

object_ = self.read_object(**data)

if serialize:
return self.serialize_data('output_schema_of_read', object_)

return object_


class UpdateMixin:
"""Update object in database.
Expand All @@ -222,10 +243,8 @@ def update_object(self, id: Any, **kwargs) -> Result:
session = self._get_option_from_meta('session')
model = self._get_option_from_meta('model')

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

obj = session.scalars(select(model).where(model.id == id)).one()
stmt = update(model).where(model.id == id).values(**kwargs).returning(model)
obj = session.scalars(stmt).one()
return obj

def update(
Expand Down Expand Up @@ -253,5 +272,7 @@ def delete_object(self, id: Any) -> None:

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

def delete(self, id: Any) -> None:
self.delete_object(id)
def delete(self, data: dict, deserialize: bool = False) -> None:
if deserialize:
data = self.deserialize_data('input_schema_of_read', data)
self.delete_object(**data)
10 changes: 7 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,15 @@ class Meta:
@pytest.fixture
def fx_parents__non_deletion(fx_parent_controller, fx_child_controller, fx_father_controller):
def _create_item() -> Result:
new_father = fx_father_controller.create(first=next(UNIQUE_STRING))
new_father = fx_father_controller.create({'first': next(UNIQUE_STRING)})
new_parent = fx_parent_controller.create(
first=next(UNIQUE_STRING), second=f'full {next(UNIQUE_STRING)}', father_id=new_father.id
{
'first': next(UNIQUE_STRING),
'second': f'full {next(UNIQUE_STRING)}',
'father_id': new_father.id,
}
)
fx_child_controller.create(first=next(UNIQUE_STRING), parent_id=new_parent.id)
fx_child_controller.create({'first': next(UNIQUE_STRING), 'parent_id': new_parent.id})
return new_parent

return _create_item
Loading