From 21e1fb10b2af811a9832f8f615931396af2800ea Mon Sep 17 00:00:00 2001 From: Konstantin Fadeev Date: Sun, 14 Sep 2025 23:54:54 +0300 Subject: [PATCH 1/3] unification of method interfaces. --- .pre-commit-config.yaml | 22 ++++++------ CHANGES.md | 5 +++ README.md | 19 +++++----- examples/full_example.py | 12 +++---- pyproject.toml | 7 ++-- src/db_first/base.py | 2 +- src/db_first/mixins/crud.py | 41 ++++++++++++++++------ tests/conftest.py | 10 ++++-- tests/test_crud_mixin.py | 24 +++++++------ tests/test_pagination_mixin.py | 64 ++++++++++++++++++---------------- 10 files changed, 122 insertions(+), 84 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5404fce..3ba6abf 100755 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.17.0 + rev: v3.20.0 hooks: - id: pyupgrade args: [--py39] @@ -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 @@ -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: @@ -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 @@ -57,7 +57,7 @@ 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: @@ -65,14 +65,14 @@ repos: - 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] @@ -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] diff --git a/CHANGES.md b/CHANGES.md index 0419f0e..db0926b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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`. diff --git a/README.md b/README.md index d03cef4..e7b3d94 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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'}) @@ -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) + ``` diff --git a/examples/full_example.py b/examples/full_example.py index 05c57b8..95e3104 100644 --- a/examples/full_example.py +++ b/examples/full_example.py @@ -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'}) @@ -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) diff --git a/pyproject.toml b/pyproject.toml index 663ce0c..01827e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ 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 = [ @@ -39,8 +39,8 @@ dev = [ ] [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"] @@ -78,6 +78,7 @@ where = ["src"] legacy_tox_ini = """ [tox] env_list = + py313 py312 py311 py310 diff --git a/src/db_first/base.py b/src/db_first/base.py index 34e6775..f0dd0b5 100644 --- a/src/db_first/base.py +++ b/src/db_first/base.py @@ -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()} diff --git a/src/db_first/mixins/crud.py b/src/db_first/mixins/crud.py index 6f04391..93254cc 100644 --- a/src/db_first/mixins/crud.py +++ b/src/db_first/mixins/crud.py @@ -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) @@ -146,7 +146,7 @@ def _paginate( return items - def read( + def paginate( self, page: int = 1, per_page: Optional[int] = None, @@ -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. @@ -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( @@ -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) diff --git a/tests/conftest.py b/tests/conftest.py index 0b877d8..fa46758 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 diff --git a/tests/test_crud_mixin.py b/tests/test_crud_mixin.py index 4a358f3..ce46f85 100644 --- a/tests/test_crud_mixin.py +++ b/tests/test_crud_mixin.py @@ -13,6 +13,7 @@ from db_first.mixins.crud import UpdateMixin from marshmallow import fields from marshmallow import Schema +from sqlalchemy.exc import NoResultFound from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column @@ -47,8 +48,8 @@ class Meta: output_schema_of_create = SchemaOfResultCreate data_for_create = {'first': next(UNIQUE_STRING)} - TestCreate().create(**data_for_create, serialize=True) - new_data = TestCreate().create(**data_for_create, serialize=True) + TestCreate().create(data_for_create, serialize=True) + new_data = TestCreate().create(data_for_create, serialize=True) new_data_for_assert = deepcopy(new_data) assert new_data_for_assert.pop('id') assert new_data_for_assert == data_for_create @@ -61,8 +62,8 @@ class Meta: input_schema_of_read = SchemaOfRead output_schema_of_read = SchemaOfResultCreate - data_for_read = TestRead().read(id=UUID(new_data['id']), serialize=True)['items'] - assert new_data == data_for_read[0] + data_for_read = TestRead().read({'id': UUID(new_data['id'])}, serialize=True) + assert new_data == data_for_read class TestUpdate(UpdateMixin, BaseCRUD): class Meta: @@ -83,9 +84,10 @@ class Meta: input_schema_of_update = SchemaOfResultCreate output_schema_of_update = SchemaOfResultCreate - TestDelete().delete(id=UUID(new_data['id'])) + TestDelete().delete({'id': UUID(new_data['id'])}) - assert not TestRead().read(id=UUID(new_data['id']))['items'] + with pytest.raises(NoResultFound): + assert not TestRead().read({'id': UUID(new_data['id'])}) def test_crud_mixin__wrong_meta(fx_db_connection): @@ -99,7 +101,7 @@ class TestController(CreateMixin, ReadMixin, UpdateMixin, DeleteMixin, BaseCRUD) assert e.value.args[0] == 'You need add class Meta with options.' with pytest.raises(MetaNotFound) as e: - TestController().read(id=uuid4(), serialize=True) + TestController().read({'id': uuid4()}, serialize=True) assert e.value.args[0] == 'You need add class Meta with options.' data_for_update = {'id': uuid4(), 'first': next(UNIQUE_STRING)} @@ -108,7 +110,7 @@ class TestController(CreateMixin, ReadMixin, UpdateMixin, DeleteMixin, BaseCRUD) assert e.value.args[0] == 'You need add class Meta with options.' with pytest.raises(MetaNotFound) as e: - TestController().delete(id=uuid4()) + TestController().delete({'id': uuid4()}) assert e.value.args[0] == 'You need add class Meta with options.' @@ -124,8 +126,8 @@ class Meta: assert e.value.args[0] == 'Option not set in Meta class.' with pytest.raises(OptionNotFound) as e: - TestController().read(id=uuid4(), serialize=True) - assert e.value.args[0] == 'Option not set in Meta class.' + TestController().read({'id': uuid4()}, serialize=True) + assert e.value.args[0] == 'Option not set in Meta class.' data_for_update = {'id': uuid4(), 'first': next(UNIQUE_STRING)} with pytest.raises(OptionNotFound) as e: @@ -133,5 +135,5 @@ class Meta: assert e.value.args[0] == 'Option not set in Meta class.' with pytest.raises(OptionNotFound) as e: - TestController().delete(id=uuid4()) + TestController().delete({'id': uuid4()}) assert e.value.args[0] == 'Option not set in Meta class.' diff --git a/tests/test_pagination_mixin.py b/tests/test_pagination_mixin.py index dd45797..7b29cef 100644 --- a/tests/test_pagination_mixin.py +++ b/tests/test_pagination_mixin.py @@ -13,10 +13,10 @@ def test_controller__pagination_without_metadata(fx_parent_controller): - fx_parent_controller.create(first=next(UNIQUE_STRING)) - fx_parent_controller.create(first=next(UNIQUE_STRING)) + fx_parent_controller.create({'first': next(UNIQUE_STRING)}) + fx_parent_controller.create({'first': next(UNIQUE_STRING)}) - items = fx_parent_controller.read() + items = fx_parent_controller.paginate() assert not items.get('_metadata') for item in items['items']: @@ -27,7 +27,7 @@ def test_controller__pagination(fx_parents__non_deletion, fx_parent_controller): total_items_number = 10 ids = [fx_parents__non_deletion().id for _ in range(total_items_number)] - items = fx_parent_controller.read( + items = fx_parent_controller.paginate( page=1, per_page=2, max_per_page=20, include_metadata=True, id=ids ) assert items['items'] @@ -41,9 +41,9 @@ def test_controller__pagination(fx_parents__non_deletion, fx_parent_controller): def test_controller__sorting(fx_parents__non_deletion, fx_parent_controller): _ = [fx_parents__non_deletion() for _ in range(10)] - items = fx_parent_controller.read(sort_created_at='asc') + items = fx_parent_controller.paginate(sort_created_at='asc') asc_first_item = items['items'][0].id - items = fx_parent_controller.read(sort_created_at='desc') + items = fx_parent_controller.paginate(sort_created_at='desc') desc_first_item = items['items'][0].id assert asc_first_item != desc_first_item @@ -52,7 +52,7 @@ def test_controller__searching(fx_parents__non_deletion, fx_parent_controller): new_item = fx_parents__non_deletion() fx_parents__non_deletion() - items = fx_parent_controller.read(search_first=new_item.first) + items = fx_parent_controller.paginate(search_first=new_item.first) for item in items['items']: assert item.first == new_item.first @@ -60,10 +60,10 @@ def test_controller__searching(fx_parents__non_deletion, fx_parent_controller): def test_controller__get_fields_of_list(fx_db, fx_parent_controller): _, _, _, Fathers = fx_db - fx_parent_controller.create(first=next(UNIQUE_STRING)) + fx_parent_controller.create({'first': next(UNIQUE_STRING)}) fields = ['id'] - items = fx_parent_controller.read(fields=fields, serialize=True) + items = fx_parent_controller.paginate(fields=fields, serialize=True) assert items['items'] for item in items['items']: @@ -75,23 +75,23 @@ def test_controller__filtrating(fx_parents__non_deletion, fx_parent_controller): _ = [fx_parents__non_deletion() for _ in range(10)] patched_item_payload = {'id': first_item.id, 'first': 'first for test filtrating'} - patched_first_item = fx_parent_controller.update(data=patched_item_payload) + patched_first_item = fx_parent_controller.update(patched_item_payload) - items = fx_parent_controller.read(first=patched_first_item.first) + items = fx_parent_controller.paginate(first=patched_first_item.first) assert len(items['items']) == 1 assert items['items'][0].id == first_item.id - items = fx_parent_controller.read(page=1, per_page=1, first=patched_first_item.first) + items = fx_parent_controller.paginate(page=1, per_page=1, first=patched_first_item.first) assert items['items'] assert items['items'][0].id == first_item.id def test_controller__interval_filtration(fx_parent_controller): - item_first = fx_parent_controller.create(first=next(UNIQUE_STRING)) - item_second = fx_parent_controller.create(first=next(UNIQUE_STRING)) - item_third = fx_parent_controller.create(first=next(UNIQUE_STRING)) + item_first = fx_parent_controller.create({'first': next(UNIQUE_STRING)}) + item_second = fx_parent_controller.create({'first': next(UNIQUE_STRING)}) + item_third = fx_parent_controller.create({'first': next(UNIQUE_STRING)}) - items_asc = fx_parent_controller.read( + items_asc = fx_parent_controller.paginate( include_metadata=True, id=[item_first.id, item_second.id, item_third.id], sort_created_at='asc', @@ -105,7 +105,7 @@ def test_controller__interval_filtration(fx_parent_controller): assert items_asc['items'][0].id == item_first.id assert items_asc['items'][1].id == item_second.id - items_desc = fx_parent_controller.read( + items_desc = fx_parent_controller.paginate( include_metadata=True, id=[item_first.id, item_second.id, item_third.id], sort_created_at='desc', @@ -123,9 +123,9 @@ def test_controller__interval_filtration(fx_parent_controller): @pytest.mark.parametrize('page', [-1, 0]) @pytest.mark.parametrize('per_page', [-1, 0]) def test_controller__get_non_exist_page(fx_parent_controller, page, per_page): - fx_parent_controller.create(first=next(UNIQUE_STRING)) + fx_parent_controller.create({'first': next(UNIQUE_STRING)}) - items = fx_parent_controller.read(page=page, per_page=per_page, include_metadata=True) + items = fx_parent_controller.paginate(page=page, per_page=per_page, include_metadata=True) assert items['_metadata']['pagination']['per_page'] == 0 assert items['_metadata']['pagination']['pages'] == 0 @@ -136,9 +136,9 @@ def test_controller__get_non_exist_page(fx_parent_controller, page, per_page): @pytest.mark.parametrize('page', [1, 2]) @pytest.mark.parametrize('per_page', [1, 2]) def test_controller__get_pages(fx_parent_controller, page, per_page): - fx_parent_controller.create(first=next(UNIQUE_STRING)) + fx_parent_controller.create({'first': next(UNIQUE_STRING)}) - items = fx_parent_controller.read(page=page, per_page=per_page, include_metadata=True) + items = fx_parent_controller.paginate(page=page, per_page=per_page, include_metadata=True) assert 1 <= items['_metadata']['pagination']['per_page'] <= 100 assert items['_metadata']['pagination']['pages'] == 1 @@ -149,9 +149,9 @@ def test_controller__get_pages(fx_parent_controller, page, per_page): @pytest.mark.parametrize('page', [101, 1_000_000]) @pytest.mark.parametrize('per_page', [101, 1_000_000]) def test_controller__get_over_pages(fx_parent_controller, page, per_page): - fx_parent_controller.create(first=next(UNIQUE_STRING)) + fx_parent_controller.create({'first': next(UNIQUE_STRING)}) - items = fx_parent_controller.read(page=page, per_page=per_page, include_metadata=True) + items = fx_parent_controller.paginate(page=page, per_page=per_page, include_metadata=True) assert items['_metadata']['pagination']['per_page'] == 100 assert items['_metadata']['pagination']['pages'] == 1 @@ -173,9 +173,9 @@ class Meta: input_schema_of_create = fx_parent_schema_of_create custom_controller = CustomController() - custom_controller.create(first=next(UNIQUE_STRING)) + custom_controller.create({'first': next(UNIQUE_STRING)}) - items = custom_controller.read(fields=['id']) + items = custom_controller.paginate(fields=['id']) assert '_metadata' not in items assert items['items'] @@ -190,7 +190,7 @@ def test_controller__statement( statement = sqlalchemy.select(Parents).join(Children) - items = fx_parent_controller.read( + items = fx_parent_controller.paginate( statement=statement, page=1, per_page=2, @@ -213,11 +213,15 @@ def test_controller__statement( def test_controller__fields_for_relations( fx_parent_controller, fx_child_controller, fx_father_controller ): - new_father = fx_father_controller.create(first=next(UNIQUE_STRING)) - new_parent = fx_parent_controller.create(first=next(UNIQUE_STRING), father_id=new_father.id) - new_child = fx_child_controller.create(first=next(UNIQUE_STRING), parent_id=new_parent.id) + new_father = fx_father_controller.create({'first': next(UNIQUE_STRING)}) + new_parent = fx_parent_controller.create( + {'first': next(UNIQUE_STRING), 'father_id': new_father.id} + ) + new_child = fx_child_controller.create( + {'first': next(UNIQUE_STRING), 'parent_id': new_parent.id} + ) - items = fx_parent_controller.read(id=new_parent.id, serialize=True) + items = fx_parent_controller.paginate(id=new_parent.id, serialize=True) assert len(items['items']) == 1 assert items['items'][0]['id'] == str(new_parent.id) assert items['items'][0]['created_at'] == new_parent.created_at.isoformat() From 0cfaa585f0615b405f733e4f6333616acbe377f4 Mon Sep 17 00:00:00 2001 From: Konstantin Fadeev Date: Mon, 15 Sep 2025 00:02:32 +0300 Subject: [PATCH 2/3] unification of method interfaces. --- .github/workflows/testing.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 3cc79d7..e50cea7 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -13,6 +13,7 @@ jobs: 3.10 3.11 3.12 + 3.13 - run: make format test: runs-on: ubuntu-latest From f8983eb5dbb1a9528529d4547fd37e6d5b9f76dc Mon Sep 17 00:00:00 2001 From: Konstantin Fadeev Date: Mon, 15 Sep 2025 19:26:31 +0300 Subject: [PATCH 3/3] unification of method interfaces. --- pyproject.toml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 01827e3..ee3ce75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,13 +29,13 @@ 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]