From 272d13dc4357d6329558f7883cc50d36e2a5d6b3 Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sun, 21 Jan 2024 00:27:14 +0300 Subject: [PATCH 01/33] Ref/add tests (#8) * Add codeclimate coverage * Update dependecies * Add first test * Add tests --- .github/workflows/coverage.yml | 22 +++ poetry.lock | 300 ++++++++++++++++++++------------- pyproject.toml | 7 +- simplecrud/crud.py | 5 +- simplecrud/settings.py | 11 +- simplecrud/tests/factories.py | 15 ++ simplecrud/tests/test.db | Bin 0 -> 8192 bytes simplecrud/tests/test_crud.py | 80 +++++++++ simplecrud/tests/utils.py | 12 ++ 9 files changed, 327 insertions(+), 125 deletions(-) create mode 100644 simplecrud/tests/factories.py create mode 100644 simplecrud/tests/test.db create mode 100644 simplecrud/tests/test_crud.py create mode 100644 simplecrud/tests/utils.py diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index e69de29..9f95239 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -0,0 +1,22 @@ +name: Coverage + +on: + push: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - run: pip install poetry + - run: make install + - name: Test & publish code coverage + uses: paambaati/codeclimate-action@v2.7.4 + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageCommand: make test-coverage + debug: true diff --git a/poetry.lock b/poetry.lock index 58ca2a8..ed19e95 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,20 @@ # This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. +[[package]] +name = "aiosqlite" +version = "0.19.0" +description = "asyncio bridge to the standard sqlite3 module" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosqlite-0.19.0-py3-none-any.whl", hash = "sha256:edba222e03453e094a3ce605db1b970c4b3376264e56f32e2a4959f948d66a96"}, + {file = "aiosqlite-0.19.0.tar.gz", hash = "sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d"}, +] + +[package.extras] +dev = ["aiounittest (==1.4.1)", "attribution (==1.6.2)", "black (==23.3.0)", "coverage[toml] (==7.2.3)", "flake8 (==5.0.4)", "flake8-bugbear (==23.3.12)", "flit (==3.7.1)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"] +docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"] + [[package]] name = "colorama" version = "0.4.6" @@ -25,75 +40,91 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "flake8" +version = "7.0.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, + {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.2.0,<3.3.0" + [[package]] name = "greenlet" -version = "3.0.2" +version = "3.0.3" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" files = [ - {file = "greenlet-3.0.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9acd8fd67c248b8537953cb3af8787c18a87c33d4dcf6830e410ee1f95a63fd4"}, - {file = "greenlet-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:339c0272a62fac7e602e4e6ec32a64ff9abadc638b72f17f6713556ed011d493"}, - {file = "greenlet-3.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38878744926cec29b5cc3654ef47f3003f14bfbba7230e3c8492393fe29cc28b"}, - {file = "greenlet-3.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b3f0497db77cfd034f829678b28267eeeeaf2fc21b3f5041600f7617139e6773"}, - {file = "greenlet-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1a8a08de7f68506a38f9a2ddb26bbd1480689e66d788fcd4b5f77e2d9ecfcc"}, - {file = "greenlet-3.0.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89a6f6ddcbef4000cda7e205c4c20d319488ff03db961d72d4e73519d2465309"}, - {file = "greenlet-3.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c1f647fe5b94b51488b314c82fdda10a8756d650cee8d3cd29f657c6031bdf73"}, - {file = "greenlet-3.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9560c580c896030ff9c311c603aaf2282234643c90d1dec738a1d93e3e53cd51"}, - {file = "greenlet-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2e9c5423046eec21f6651268cb674dfba97280701e04ef23d312776377313206"}, - {file = "greenlet-3.0.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1fd25dfc5879a82103b3d9e43fa952e3026c221996ff4d32a9c72052544835d"}, - {file = "greenlet-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cecfdc950dd25f25d6582952e58521bca749cf3eeb7a9bad69237024308c8196"}, - {file = "greenlet-3.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edf7a1daba1f7c54326291a8cde58da86ab115b78c91d502be8744f0aa8e3ffa"}, - {file = "greenlet-3.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4cf532bf3c58a862196b06947b1b5cc55503884f9b63bf18582a75228d9950e"}, - {file = "greenlet-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e79fb5a9fb2d0bd3b6573784f5e5adabc0b0566ad3180a028af99523ce8f6138"}, - {file = "greenlet-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:006c1028ac0cfcc4e772980cfe73f5476041c8c91d15d64f52482fc571149d46"}, - {file = "greenlet-3.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fefd5eb2c0b1adffdf2802ff7df45bfe65988b15f6b972706a0e55d451bffaea"}, - {file = "greenlet-3.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c0fdb8142742ee68e97c106eb81e7d3e883cc739d9c5f2b28bc38a7bafeb6d1"}, - {file = "greenlet-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:8f8d14a0a4e8c670fbce633d8b9a1ee175673a695475acd838e372966845f764"}, - {file = "greenlet-3.0.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:654b84c9527182036747938b81938f1d03fb8321377510bc1854a9370418ab66"}, - {file = "greenlet-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd5bc4fde0842ff2b9cf33382ad0b4db91c2582db836793d58d174c569637144"}, - {file = "greenlet-3.0.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c27b142a9080bdd5869a2fa7ebf407b3c0b24bd812db925de90e9afe3c417fd6"}, - {file = "greenlet-3.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0df7eed98ea23b20e9db64d46eb05671ba33147df9405330695bcd81a73bb0c9"}, - {file = "greenlet-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb5d60805057d8948065338be6320d35e26b0a72f45db392eb32b70dd6dc9227"}, - {file = "greenlet-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0e28f5233d64c693382f66d47c362b72089ebf8ac77df7e12ac705c9fa1163d"}, - {file = "greenlet-3.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e4bfa752b3688d74ab1186e2159779ff4867644d2b1ebf16db14281f0445377"}, - {file = "greenlet-3.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c42bb589e6e9f9d8bdd79f02f044dff020d30c1afa6e84c0b56d1ce8a324553c"}, - {file = "greenlet-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:b2cedf279ca38ef3f4ed0d013a6a84a7fc3d9495a716b84a5fc5ff448965f251"}, - {file = "greenlet-3.0.2-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:6d65bec56a7bc352bcf11b275b838df618651109074d455a772d3afe25390b7d"}, - {file = "greenlet-3.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0acadbc3f72cb0ee85070e8d36bd2a4673d2abd10731ee73c10222cf2dd4713c"}, - {file = "greenlet-3.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14b5d999aefe9ffd2049ad19079f733c3aaa426190ffecadb1d5feacef8fe397"}, - {file = "greenlet-3.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f27aa32466993c92d326df982c4acccd9530fe354e938d9e9deada563e71ce76"}, - {file = "greenlet-3.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f34a765c5170c0673eb747213a0275ecc749ab3652bdbec324621ed5b2edaef"}, - {file = "greenlet-3.0.2-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:520fcb53a39ef90f5021c77606952dbbc1da75d77114d69b8d7bded4a8e1a813"}, - {file = "greenlet-3.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d1fceb5351ab1601903e714c3028b37f6ea722be6873f46e349a960156c05650"}, - {file = "greenlet-3.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7363756cc439a503505b67983237d1cc19139b66488263eb19f5719a32597836"}, - {file = "greenlet-3.0.2-cp37-cp37m-win32.whl", hash = "sha256:d5547b462b8099b84746461e882a3eb8a6e3f80be46cb6afb8524eeb191d1a30"}, - {file = "greenlet-3.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:950e21562818f9c771989b5b65f990e76f4ac27af66e1bb34634ae67886ede2a"}, - {file = "greenlet-3.0.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d64643317e76b4b41fdba659e7eca29634e5739b8bc394eda3a9127f697ed4b0"}, - {file = "greenlet-3.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f9ea7c2c9795549653b6f7569f6bc75d2c7d1f6b2854eb8ce0bc6ec3cb2dd88"}, - {file = "greenlet-3.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db4233358d3438369051a2f290f1311a360d25c49f255a6c5d10b5bcb3aa2b49"}, - {file = "greenlet-3.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bf77b41798e8417657245b9f3649314218a4a17aefb02bb3992862df32495"}, - {file = "greenlet-3.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d0df07a38e41a10dfb62c6fc75ede196572b580f48ee49b9282c65639f3965"}, - {file = "greenlet-3.0.2-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10d247260db20887ae8857c0cbc750b9170f0b067dd7d38fb68a3f2334393bd3"}, - {file = "greenlet-3.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a37ae53cca36823597fd5f65341b6f7bac2dd69ecd6ca01334bb795460ab150b"}, - {file = "greenlet-3.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:80d068e4b6e2499847d916ef64176811ead6bf210a610859220d537d935ec6fd"}, - {file = "greenlet-3.0.2-cp38-cp38-win32.whl", hash = "sha256:b1405614692ac986490d10d3e1a05e9734f473750d4bee3cf7d1286ef7af7da6"}, - {file = "greenlet-3.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8756a94ed8f293450b0e91119eca2a36332deba69feb2f9ca410d35e74eae1e4"}, - {file = "greenlet-3.0.2-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:2c93cd03acb1499ee4de675e1a4ed8eaaa7227f7949dc55b37182047b006a7aa"}, - {file = "greenlet-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1dac09e3c0b78265d2e6d3cbac2d7c48bd1aa4b04a8ffeda3adde9f1688df2c3"}, - {file = "greenlet-3.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ee59c4627c8c4bb3e15949fbcd499abd6b7f4ad9e0bfcb62c65c5e2cabe0ec4"}, - {file = "greenlet-3.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18fe39d70d482b22f0014e84947c5aaa7211fb8e13dc4cc1c43ed2aa1db06d9a"}, - {file = "greenlet-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84bef3cfb6b6bfe258c98c519811c240dbc5b33a523a14933a252e486797c90"}, - {file = "greenlet-3.0.2-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aecea0442975741e7d69daff9b13c83caff8c13eeb17485afa65f6360a045765"}, - {file = "greenlet-3.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f260e6c2337871a52161824058923df2bbddb38bc11a5cbe71f3474d877c5bd9"}, - {file = "greenlet-3.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fc14dd9554f88c9c1fe04771589ae24db76cd56c8f1104e4381b383d6b71aff8"}, - {file = "greenlet-3.0.2-cp39-cp39-win32.whl", hash = "sha256:bfcecc984d60b20ffe30173b03bfe9ba6cb671b0be1e95c3e2056d4fe7006590"}, - {file = "greenlet-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:c235131bf59d2546bb3ebaa8d436126267392f2e51b85ff45ac60f3a26549af0"}, - {file = "greenlet-3.0.2.tar.gz", hash = "sha256:1c1129bc47266d83444c85a8e990ae22688cf05fb20d7951fd2866007c2ba9bc"}, + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, ] [package.extras] -docs = ["Sphinx"] +docs = ["Sphinx", "furo"] test = ["objgraph", "psutil"] [[package]] @@ -107,6 +138,17 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + [[package]] name = "packaging" version = "23.2" @@ -133,6 +175,28 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, +] + [[package]] name = "pytest" version = "7.4.3" @@ -157,70 +221,70 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "sqlalchemy" -version = "2.0.23" +version = "2.0.25" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-win32.whl", hash = "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-win_amd64.whl", hash = "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-win32.whl", hash = "sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-win_amd64.whl", hash = "sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-win32.whl", hash = "sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-win_amd64.whl", hash = "sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:14aebfe28b99f24f8a4c1346c48bc3d63705b1f919a24c27471136d2f219f02d"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e983fa42164577d073778d06d2cc5d020322425a509a08119bdcee70ad856bf"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e0dc9031baa46ad0dd5a269cb7a92a73284d1309228be1d5935dac8fb3cae24"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5f94aeb99f43729960638e7468d4688f6efccb837a858b34574e01143cf11f89"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:63bfc3acc970776036f6d1d0e65faa7473be9f3135d37a463c5eba5efcdb24c8"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-win32.whl", hash = "sha256:f48ed89dd11c3c586f45e9eec1e437b355b3b6f6884ea4a4c3111a3358fd0c18"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-win_amd64.whl", hash = "sha256:1e018aba8363adb0599e745af245306cb8c46b9ad0a6fc0a86745b6ff7d940fc"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-win32.whl", hash = "sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-win_amd64.whl", hash = "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-win32.whl", hash = "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-win_amd64.whl", hash = "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b"}, - {file = "SQLAlchemy-2.0.23-py3-none-any.whl", hash = "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d"}, - {file = "SQLAlchemy-2.0.23.tar.gz", hash = "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4344d059265cc8b1b1be351bfb88749294b87a8b2bbe21dfbe066c4199541ebd"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9e2e59cbcc6ba1488404aad43de005d05ca56e069477b33ff74e91b6319735"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84daa0a2055df9ca0f148a64fdde12ac635e30edbca80e87df9b3aaf419e144a"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc8b7dabe8e67c4832891a5d322cec6d44ef02f432b4588390017f5cec186a84"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5693145220517b5f42393e07a6898acdfe820e136c98663b971906120549da5"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db854730a25db7c956423bb9fb4bdd1216c839a689bf9cc15fada0a7fb2f4570"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-win32.whl", hash = "sha256:14a6f68e8fc96e5e8f5647ef6cda6250c780612a573d99e4d881581432ef1669"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-win_amd64.whl", hash = "sha256:87f6e732bccd7dcf1741c00f1ecf33797383128bd1c90144ac8adc02cbb98643"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:342d365988ba88ada8af320d43df4e0b13a694dbd75951f537b2d5e4cb5cd002"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f37c0caf14b9e9b9e8f6dbc81bc56db06acb4363eba5a633167781a48ef036ed"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa9373708763ef46782d10e950b49d0235bfe58facebd76917d3f5cbf5971aed"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24f571990c05f6b36a396218f251f3e0dda916e0c687ef6fdca5072743208f5"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75432b5b14dc2fff43c50435e248b45c7cdadef73388e5610852b95280ffd0e9"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:884272dcd3ad97f47702965a0e902b540541890f468d24bd1d98bcfe41c3f018"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-win32.whl", hash = "sha256:e607cdd99cbf9bb80391f54446b86e16eea6ad309361942bf88318bcd452363c"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d505815ac340568fd03f719446a589162d55c52f08abd77ba8964fbb7eb5b5f"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0dacf67aee53b16f365c589ce72e766efaabd2b145f9de7c917777b575e3659d"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b801154027107461ee992ff4b5c09aa7cc6ec91ddfe50d02bca344918c3265c6"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a21853f5daeb50412d459cfb13cb82c089ad4c04ec208cd14dddd99fc23b39"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29049e2c299b5ace92cbed0c1610a7a236f3baf4c6b66eb9547c01179f638ec5"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b64b183d610b424a160b0d4d880995e935208fc043d0302dd29fee32d1ee3f95"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f7a7d7fcc675d3d85fbf3b3828ecd5990b8d61bd6de3f1b260080b3beccf215"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-win32.whl", hash = "sha256:cf18ff7fc9941b8fc23437cc3e68ed4ebeff3599eec6ef5eebf305f3d2e9a7c2"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-win_amd64.whl", hash = "sha256:91f7d9d1c4dd1f4f6e092874c128c11165eafcf7c963128f79e28f8445de82d5"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bb209a73b8307f8fe4fe46f6ad5979649be01607f11af1eb94aa9e8a3aaf77f0"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:798f717ae7c806d67145f6ae94dc7c342d3222d3b9a311a784f371a4333212c7"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd402169aa00df3142149940b3bf9ce7dde075928c1886d9a1df63d4b8de62"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0d3cab3076af2e4aa5693f89622bef7fa770c6fec967143e4da7508b3dceb9b9"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:74b080c897563f81062b74e44f5a72fa44c2b373741a9ade701d5f789a10ba23"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-win32.whl", hash = "sha256:87d91043ea0dc65ee583026cb18e1b458d8ec5fc0a93637126b5fc0bc3ea68c4"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-win_amd64.whl", hash = "sha256:75f99202324383d613ddd1f7455ac908dca9c2dd729ec8584c9541dd41822a2c"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:420362338681eec03f53467804541a854617faed7272fe71a1bfdb07336a381e"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c88f0c7dcc5f99bdb34b4fd9b69b93c89f893f454f40219fe923a3a2fd11625"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3be4987e3ee9d9a380b66393b77a4cd6d742480c951a1c56a23c335caca4ce3"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a159111a0f58fb034c93eeba211b4141137ec4b0a6e75789ab7a3ef3c7e7e3"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8b8cb63d3ea63b29074dcd29da4dc6a97ad1349151f2d2949495418fd6e48db9"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:736ea78cd06de6c21ecba7416499e7236a22374561493b456a1f7ffbe3f6cdb4"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-win32.whl", hash = "sha256:10331f129982a19df4284ceac6fe87353ca3ca6b4ca77ff7d697209ae0a5915e"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-win_amd64.whl", hash = "sha256:c55731c116806836a5d678a70c84cb13f2cedba920212ba7dcad53260997666d"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:605b6b059f4b57b277f75ace81cc5bc6335efcbcc4ccb9066695e515dbdb3900"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:665f0a3954635b5b777a55111ababf44b4fc12b1f3ba0a435b602b6387ffd7cf"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecf6d4cda1f9f6cb0b45803a01ea7f034e2f1aed9475e883410812d9f9e3cfcf"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c51db269513917394faec5e5c00d6f83829742ba62e2ac4fa5c98d58be91662f"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:790f533fa5c8901a62b6fef5811d48980adeb2f51f1290ade8b5e7ba990ba3de"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1b1180cda6df7af84fe72e4530f192231b1f29a7496951db4ff38dac1687202d"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-win32.whl", hash = "sha256:555651adbb503ac7f4cb35834c5e4ae0819aab2cd24857a123370764dc7d7e24"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-win_amd64.whl", hash = "sha256:dc55990143cbd853a5d038c05e79284baedf3e299661389654551bd02a6a68d7"}, + {file = "SQLAlchemy-2.0.25-py3-none-any.whl", hash = "sha256:a86b4240e67d4753dc3092d9511886795b3c2852abe599cffe108952f7af7ac3"}, + {file = "SQLAlchemy-2.0.25.tar.gz", hash = "sha256:a2c69a7664fb2d54b8682dd774c3b54f67f84fa123cf84dda2a5f40dcaa04e08"}, ] [package.dependencies] greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} -typing-extensions = ">=4.2.0" +typing-extensions = ">=4.6.0" [package.extras] aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] @@ -230,7 +294,7 @@ mssql-pyodbc = ["pyodbc"] mypy = ["mypy (>=0.910)"] mysql = ["mysqlclient (>=1.4.0)"] mysql-connector = ["mysql-connector-python"] -oracle = ["cx-oracle (>=8)"] +oracle = ["cx_oracle (>=8)"] oracle-oracledb = ["oracledb (>=1.0.1)"] postgresql = ["psycopg2 (>=2.7)"] postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] @@ -240,7 +304,7 @@ postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2cffi = ["psycopg2cffi"] postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3-binary"] +sqlcipher = ["sqlcipher3_binary"] [[package]] name = "tomli" @@ -255,16 +319,16 @@ files = [ [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.9.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "9b824b5a6d8a3fb04469ea5e17a1807bc86f54f669563af88b0117387d6860eb" +content-hash = "cc24393cb242963e33cb2b5279912ec21a492df69407ed37883a40903cb7ad65" diff --git a/pyproject.toml b/pyproject.toml index 83a3672..2027022 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,11 +10,14 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.9" -sqlalchemy = "^2.0.23" - [tool.poetry.group.dev.dependencies] +flake8 = "^7.0.0" + +[tool.poetry.group.test.dependencies] pytest = "^7.4.3" +sqlalchemy = "^2.0.25" +aiosqlite = "^0.19.0" [build-system] requires = ["poetry-core"] diff --git a/simplecrud/crud.py b/simplecrud/crud.py index ef7b37e..25c1523 100644 --- a/simplecrud/crud.py +++ b/simplecrud/crud.py @@ -3,9 +3,10 @@ # from cachetools import LFUCache from sqlalchemy import select -from .settings import CRUDConfig -session = CRUDConfig().sessionmaker +from .settings import session + + logger = logging.getLogger(__name__) diff --git a/simplecrud/settings.py b/simplecrud/settings.py index c3d1f7f..006d2d9 100644 --- a/simplecrud/settings.py +++ b/simplecrud/settings.py @@ -1,8 +1,10 @@ from __future__ import annotations from typing import TYPE_CHECKING + if TYPE_CHECKING: from sqlalchemy.orm import AsyncSession + class CRUDConfig: """Singleton class for CRUD settings""" _instance: CRUDConfig | None = None @@ -12,9 +14,6 @@ def __new__(cls): cls._instance = super(CRUDConfig, cls).__new__(cls) return cls._instance - def __init__(self): - self._sessionmaker: AsyncSession | None = None - @property def sessionmaker(self) -> AsyncSession: if not self._sessionmaker: @@ -24,3 +23,9 @@ def sessionmaker(self) -> AsyncSession: def set_sessionmaker(self, sessionmaker: AsyncSession) -> None: """Set sessionmaker""" self._sessionmaker = sessionmaker + + +def session() -> AsyncSession: + """Get session""" + config = CRUDConfig() + return config.sessionmaker() diff --git a/simplecrud/tests/factories.py b/simplecrud/tests/factories.py new file mode 100644 index 0000000..4c4c21a --- /dev/null +++ b/simplecrud/tests/factories.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession + +from simplecrud.settings import CRUDConfig + + +class AsyncConnFactory(): + + def __init__(self): + self.async_engine = create_async_engine("sqlite+aiosqlite:///./test.db") + self.async_session_maker = async_sessionmaker(self.async_engine, expire_on_commit=False, class_=AsyncSession) + self.config = CRUDConfig() + self.config.set_sessionmaker(self.async_session_maker) + + def __call__(self, *args, **kwargs): + return self.config.sessionmaker diff --git a/simplecrud/tests/test.db b/simplecrud/tests/test.db new file mode 100644 index 0000000000000000000000000000000000000000..6ae6a211f9f1bffba7e11f144dc52314bdbcfb2c GIT binary patch literal 8192 zcmeIuu?c`M5QX6vu@xO56WF+b1K5ZYSSTonASo@aGn*rsfYt%)|H0$n?y7I-yTwg* z-j8i`woxTx)LJXahh}e0m}h7n`X%mC@v7Imh+madUa5EP2q1s}0tg_000IagfB*sr cAb Date: Sun, 21 Jan 2024 13:56:32 +0300 Subject: [PATCH 02/33] Tests/add crud tests (#9) * Update tests * Add dummy test cases for future * Update TestCRUDConfig * Update TestAsyncCRUDFunctions * Update TestAsyncCRUDFunctions --- simplecrud/tests/test_crud.py | 155 +++++++++++++++++++++++++++--- simplecrud/tests/test_settings.py | 15 ++- 2 files changed, 158 insertions(+), 12 deletions(-) diff --git a/simplecrud/tests/test_crud.py b/simplecrud/tests/test_crud.py index 4ceb16e..fa7bbb8 100644 --- a/simplecrud/tests/test_crud.py +++ b/simplecrud/tests/test_crud.py @@ -1,11 +1,12 @@ import unittest from sqlalchemy import Column, Integer, String, create_engine +from sqlalchemy.exc import InvalidRequestError from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker -from simplecrud.crud import create_obj +from simplecrud.crud import * from simplecrud.tests.factories import AsyncConnFactory -from simplecrud.tests.utils import async_to_sync +from simplecrud.utils import async_to_sync database_url = "sqlite:///./test.db" engine = create_engine(database_url) @@ -59,22 +60,154 @@ def tearDown(self): @async_to_sync async def test_async_create_obj(self): params_1 = dict(name="test1") - new_obj_1 = await create_obj(ExampleModel, **params_1) + new_obj_1 = await create_object(ExampleModel, **params_1) self.assertEqual(new_obj_1.name, "test1") params_2 = dict(name="test2") - new_obj_2 = await create_obj(ExampleModel, **params_2) + new_obj_2 = await create_object(ExampleModel, **params_2) self.assertEqual(new_obj_2.name, "test2") @async_to_sync - async def test_async_create_obj_negative(self): + async def test_create_obj_params_error(self): + params_1 = dict(name="test1", wrong="wrong") + with self.assertRaises(TypeError): + new_obj_1 = await create_object(ExampleModel, **params_1) + + @async_to_sync + async def test_get_object(self): params_1 = dict(name="test1") - new_obj_1 = await create_obj(ExampleModel, **params_1) - self.assertNotEqual(new_obj_1.name, "test0") + await create_object(ExampleModel, **params_1) + obj = await get_object(ExampleModel, **params_1) + self.assertEqual(obj.name, "test1") @async_to_sync - async def test_async_create_obj_params_error(self): - params_1 = dict(name="test1", wrong="wrong") - with self.assertRaises(TypeError): - new_obj_1 = await create_obj(ExampleModel, **params_1) + async def test_get_object_not_exist(self): + params_1 = dict(name="test1") + obj = await create_object(ExampleModel, **params_1) + none_expected = await get_object(ExampleModel, name="test0") + self.assertEqual(none_expected, None) + + @async_to_sync + async def test_get_object_error(self): + params_1 = dict(name="test1") + obj = await create_object(ExampleModel, **params_1) + + with self.assertRaises(InvalidRequestError): + error_expected = await get_object(ExampleModel, wrong="wrong") + + @async_to_sync + async def test_get_all_objects(self): + all_ = await get_all(ExampleModel) + self.assertEqual(len(all_), 0) + for i in range(5): + params_1 = dict(name=f"test{i}") + await create_object(ExampleModel, **params_1) + all_ = await get_all(ExampleModel) + self.assertEqual(len(all_), 5) + self.assertTrue(isinstance(all_, list)) + + @async_to_sync + async def test_get_objects_error(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_get_all(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_get_all_negative(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_get_all_error(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_get_all_with_filter(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_get_all_with_filter_negative(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_get_all_with_filter_error(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_get_object_by_filters(self): + params_1 = dict(name="test1") + new_ = await create_object(ExampleModel, **params_1) + obj = await get_object(ExampleModel, id=new_.id) + self.assertEqual(obj.name ,"test1") + + @async_to_sync + async def test_get_object_by_filters_negative(self): + params_1 = dict(name="test1") + new_ = await create_object(ExampleModel, **params_1) + with self.assertRaises(InvalidRequestError): + obj = await get_object(ExampleModel, pk=new_.id) + + @async_to_sync + async def test_get_object_by_filters_error(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_get_or_create_object(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_get_or_create_object_negative(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_get_or_create_object_error(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_update_object(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_update_object_negative(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_update_object_error(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_update_or_create_object(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_update_or_create_object_negative(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_update_or_create_object_error(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_delete_object(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_delete_object_negative(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_delete_object_error(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_delete_objects(self): + raise Exception("Test not complete") + + @async_to_sync + async def test_delete_objects_negative(self): + raise Exception("Test not complete") + @async_to_sync + async def test_delete_objects_error(self): + raise Exception("Test not complete") diff --git a/simplecrud/tests/test_settings.py b/simplecrud/tests/test_settings.py index e02d76c..677e735 100644 --- a/simplecrud/tests/test_settings.py +++ b/simplecrud/tests/test_settings.py @@ -14,7 +14,20 @@ def test_set_sessionmaker(self): config.set_sessionmaker("test") self.assertEqual(config.sessionmaker, "test") - def test_get_sessionmaker(self): + def test_sessionmaker_saved_between_creation(self): config = CRUDConfig() + config.set_sessionmaker("test") + self.assertEqual(config.sessionmaker, "test") + config = CRUDConfig() + self.assertEqual(config.sessionmaker, "test") + + def test_sessionmaker_value_error(self): + config = CRUDConfig() + config._sessionmaker = None with self.assertRaises(ValueError): config.sessionmaker + + def test_sessionmaker_not_set(self): + config = CRUDConfig() + with self.assertRaises(AttributeError): + config.sessionmaker From 3f85104af4ff43d68713629c63b3377d0baad7b7 Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sun, 21 Jan 2024 13:57:10 +0300 Subject: [PATCH 03/33] Feat/update 2 (#10) Update core logic * Move async_to_sync decorator * Update error * Update crud * Add sync crud functions module --- simplecrud/crud.py | 49 ++++++++++++++++++++++++++------- simplecrud/settings.py | 2 +- simplecrud/sync_crud.py | 0 simplecrud/{tests => }/utils.py | 2 ++ 4 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 simplecrud/sync_crud.py rename simplecrud/{tests => }/utils.py (86%) diff --git a/simplecrud/crud.py b/simplecrud/crud.py index 25c1523..645f7a6 100644 --- a/simplecrud/crud.py +++ b/simplecrud/crud.py @@ -6,7 +6,6 @@ from .settings import session - logger = logging.getLogger(__name__) @@ -20,8 +19,7 @@ async def create_obj(model, **params): await conn.refresh(new_obj) return new_obj - -async def get_obj(model, **filters): +async def get_object(model, **filters): """Get object from db""" key = f"{model.__name__}{filters}" query = select(model).filter_by(**filters) @@ -56,9 +54,9 @@ async def get_all_with_filter(model, filters: dict): return objects -async def get_objects(model, filters: Dict, limit=10, offset=10): +async def get_objects(model, filters: Dict, limit=10, per_page=10): """Get objects from db""" - query = select(model).filter_by(**filters).limit(limit).offset(offset) + query = select(model).filter_by(**filters).limit(limit).offset(per_page) logger.debug(f"{__name__}.get_objects: query = {query}") async with session() as conn: result = await conn.execute(query) @@ -68,21 +66,52 @@ async def get_objects(model, filters: Dict, limit=10, offset=10): return objects -async def get_or_create(model, **params): +async def get_or_create_object(model, **params): """Get object from db or create new one""" key = f"{model.__name__}{params}" - obj = await get_obj(model, **params) + obj = await get_object(model, **params) if not obj: - obj = await create_obj(model, **params) + obj = await create_object(model, **params) return obj -async def update_obj(model, id: int, **params): +async def create_object(model, **params): + """Create object in db""" + logger.debug(f"{__name__}.create_obj: model = {model}, params = {params}") + new_obj = model(**params) + async with session() as conn: + conn.add(new_obj) + await conn.commit() + await conn.refresh(new_obj) + return new_obj + + +def create_objects(): + pass + + +async def update_object(model, id: int, **params): async with session() as conn: - obj = await get_obj(model, id=id) + obj = await get_object(model, id=id) for key, value in params.items(): setattr(obj, key, value) conn.add(obj) await conn.commit() await conn.refresh(obj) return obj + + +def update_objects(): + pass + + +def update_or_create_object(): + pass + + +async def delete_object(model, id: int): + pass + + +def delete_objects(): + pass diff --git a/simplecrud/settings.py b/simplecrud/settings.py index 006d2d9..1d267a6 100644 --- a/simplecrud/settings.py +++ b/simplecrud/settings.py @@ -17,7 +17,7 @@ def __new__(cls): @property def sessionmaker(self) -> AsyncSession: if not self._sessionmaker: - raise ValueError("Sessionmaker is not set") + raise ValueError("Sessionmaker is not set. Use set_sessionmaker() method") return self._sessionmaker def set_sessionmaker(self, sessionmaker: AsyncSession) -> None: diff --git a/simplecrud/sync_crud.py b/simplecrud/sync_crud.py new file mode 100644 index 0000000..e69de29 diff --git a/simplecrud/tests/utils.py b/simplecrud/utils.py similarity index 86% rename from simplecrud/tests/utils.py rename to simplecrud/utils.py index 9b5ac06..1cebd6d 100644 --- a/simplecrud/tests/utils.py +++ b/simplecrud/utils.py @@ -1,9 +1,11 @@ import asyncio +from functools import wraps def async_to_sync(func): """Decorator to convert async function to sync""" + @wraps(func) def wrapper(*args, **kwargs): loop = asyncio.get_event_loop() result = loop.run_until_complete(func(*args, **kwargs)) From f5f2a846f6f7bc580e565d42bf3fd34a020adfca Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sun, 21 Jan 2024 14:13:22 +0300 Subject: [PATCH 04/33] Update README.md (#11) --- README.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 23d5c66..e146de2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,61 @@ -# SimpleCRUD [![Maintainability](https://api.codeclimate.com/v1/badges/d33ecb2661fb7aedf516/maintainability)](https://codeclimate.com/github/hexfrost/sqlalchemy-models-commands/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/d33ecb2661fb7aedf516/test_coverage)](https://codeclimate.com/github/hexfrost/sqlalchemy-models-commands/test_coverage) + +# SimpleCRUD +SimpleCRUD is a library that provides a simple way to create CRUD commands for SQLAlchemy models. + +## Installation + +```bash +pip install simplecrud +``` +``` +poetry add simplecrud +``` + +## Usage + + +### Example usage +```python +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker +from sqlalchemy.orm import DeclarativeBase + +from simplecrud import CRUDConfig, BaseModelWithCRUD, get_all, create_obj, update_obj + +engine = create_async_engine("sqlite+aiosqlite:///test.db", echo=True) +async_sessionmaker = async_sessionmaker(async_engine, expire_on_commit=False, class_=AsyncSession) + +# Create a model +class ExampleModel(DeclarativeBase): + __tablename__ = "example_model" + id = Column(Integer, primary_key=True) + name = Column(String(50), nullable=False) + description = Column(String(50), nullable=False) + + +# Create CRUD config +CRUDConfig.sessionmaker(async_sessionmaker) + +async def example_func(): + + # Create a model + new_model = await create_obj(model, name="test", description="test") + + # Get all models + all_objs = await get_all(model) + + # Update a model + updated_obj = await update_obj(model, name="test2", description="test2") + + # Delete a model + await delete_obj(model, name="test2", description="test2") + +``` + + + + + + + From 3191b0ba64cc0927024c6618ae9d900e6efd6938 Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 12:20:59 +0300 Subject: [PATCH 05/33] Feat/update 3 (#14) Update logic and tests --- .github/workflows/compatibility.yml | 4 +- .github/workflows/coverage.yml | 7 +- .github/workflows/linter.yml | 17 +++ .github/workflows/pypi-publish.yml | 2 +- .github/workflows/tests.yml | 0 pyproject.toml | 2 +- simplecrud/crud.py | 137 +++++++++++++------ simplecrud/tests/test_crud.py | 201 ++++++++++++++++++---------- simplecrud/utils.py | 33 +++++ 9 files changed, 286 insertions(+), 117 deletions(-) delete mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index f481549..fff2050 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -3,7 +3,7 @@ name: Compatibility Check on: push: branches: - - 'dev' + - 'staging' jobs: compatibility: @@ -11,7 +11,7 @@ jobs: strategy: matrix: - python-version: [3.6, "3.10"] + python-version: [ 3.6, 3.7, 3.8, 3.9, "3.10", "3.11", "3.12" ] steps: - name: Checkout code diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 9f95239..289f8e9 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -2,7 +2,7 @@ name: Coverage on: push: - branches: [ "main" ] + branches: [ "dev" ] jobs: build: @@ -11,8 +11,9 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - run: pip install poetry - - run: make install + - run: | + pip install poetry + poetry install - name: Test & publish code coverage uses: paambaati/codeclimate-action@v2.7.4 env: diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index e69de29..d422516 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -0,0 +1,17 @@ +name: lint-test + +on: + push: + branches: [ "dev" ] + +jobs: + build: + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - run: | + pip install poetry + poetry install + poetry run flake8 diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index bf88e4c..00d2904 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -15,4 +15,4 @@ jobs: - uses: actions/setup-python@v2 - run: pip install poetry - run: poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} - - run: poetry publish --build \ No newline at end of file + - run: poetry publish --build diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index e69de29..0000000 diff --git a/pyproject.toml b/pyproject.toml index 2027022..8342647 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,13 +10,13 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.9" +sqlalchemy = "^2.0.0" [tool.poetry.group.dev.dependencies] flake8 = "^7.0.0" [tool.poetry.group.test.dependencies] pytest = "^7.4.3" -sqlalchemy = "^2.0.25" aiosqlite = "^0.19.0" [build-system] diff --git a/simplecrud/crud.py b/simplecrud/crud.py index 645f7a6..d8feca7 100644 --- a/simplecrud/crud.py +++ b/simplecrud/crud.py @@ -2,29 +2,22 @@ from typing import Dict # from cachetools import LFUCache -from sqlalchemy import select +from sqlalchemy import select, delete from .settings import session +from .utils import inject_connection logger = logging.getLogger(__name__) -async def create_obj(model, **params): - """Create object in db""" - logger.debug(f"{__name__}.create_obj: model = {model}, params = {params}") - new_obj = model(**params) - async with session() as conn: - conn.add(new_obj) - await conn.commit() - await conn.refresh(new_obj) - return new_obj - -async def get_object(model, **filters): +# READ / GET +@inject_connection +async def get_object(model, filters, conn=None): """Get object from db""" key = f"{model.__name__}{filters}" query = select(model).filter_by(**filters) logger.debug(f"{__name__}.get_obj: query = {query}") - async with session() as conn: + async with conn: result = await conn.execute(query) logger.debug(f"{__name__}.get_obj: result = {result}") obj = result.scalars().first() @@ -32,10 +25,11 @@ async def get_object(model, **filters): return obj -async def get_all(model): +@inject_connection +async def get_all(model, conn=None): """Get objects from db""" query = select(model) - async with session() as conn: + async with conn: result = await conn.execute(query) logger.debug(f"{__name__}.get_all: result = {result}") objects = result.scalars().all() @@ -43,10 +37,11 @@ async def get_all(model): return objects -async def get_all_with_filter(model, filters: dict): +@inject_connection +async def get_all_with_filter(model, filters: dict, conn=None): """Get objects from db""" query = select(model).filter_by(**filters) - async with session() as conn: + async with conn: result = await conn.execute(query) logger.debug(f"{__name__}.get_all: result = {result}") objects = result.scalars().all() @@ -54,11 +49,12 @@ async def get_all_with_filter(model, filters: dict): return objects -async def get_objects(model, filters: Dict, limit=10, per_page=10): +@inject_connection +async def get_objects(model, filters: Dict, limit=10, per_page=10, conn=None): """Get objects from db""" query = select(model).filter_by(**filters).limit(limit).offset(per_page) logger.debug(f"{__name__}.get_objects: query = {query}") - async with session() as conn: + async with conn: result = await conn.execute(query) logger.debug(f"{__name__}.get_objects: result = {result}") objects = result.scalars().all() @@ -66,52 +62,113 @@ async def get_objects(model, filters: Dict, limit=10, per_page=10): return objects -async def get_or_create_object(model, **params): +async def get_or_create_object(model, params, conn=None): """Get object from db or create new one""" key = f"{model.__name__}{params}" - obj = await get_object(model, **params) + obj = await get_object(model, params, conn=conn) if not obj: - obj = await create_object(model, **params) + obj = await create_object(model, params, conn=conn) return obj -async def create_object(model, **params): +# CREATE +@inject_connection +async def create_object(model, params, conn=None): """Create object in db""" logger.debug(f"{__name__}.create_obj: model = {model}, params = {params}") new_obj = model(**params) - async with session() as conn: + async with conn: conn.add(new_obj) await conn.commit() - await conn.refresh(new_obj) return new_obj -def create_objects(): - pass +@inject_connection +async def bulk_create(objects, conn=None): + for obj in objects: + await create_object(obj, conn=conn) -async def update_object(model, id: int, **params): - async with session() as conn: - obj = await get_object(model, id=id) - for key, value in params.items(): +# UPDATE +@inject_connection +async def update_object(obj, params, conn=None): + """ + Soft Update object in db. + If attribute not exists in model`s fields, then skip field without error + """ + avaliable_fields = obj.__class__.__table__.columns.keys() + for key, value in params.items(): + if key in avaliable_fields: setattr(obj, key, value) - conn.add(obj) + async with conn: await conn.commit() - await conn.refresh(obj) + conn.refresh(obj) return obj -def update_objects(): - pass +@inject_connection +async def update_or_error(obj, params, conn=None): + """ + Soft Update object in db. + If attribute not exists in model`s fields, then skip field without error + """ + avaliable_fields = obj.__class__.__table__.columns.keys() + for key, value in params.items(): + if key in avaliable_fields: + setattr(obj, key, value) + else: + raise AttributeError(f"Attribute {key} not exists in {obj.__class__.__name__}") + async with conn: + await conn.commit() + conn.refresh(obj) + return obj -def update_or_create_object(): - pass +@inject_connection +async def update_object_by_id(model, id: int, params, conn=None): + obj = await get_object(model, id=id) + updated_obj = await update_object(obj, params, conn=conn) + return updated_obj -async def delete_object(model, id: int): +def bulk_update(): pass -def delete_objects(): - pass +@inject_connection +async def update_or_create_object(model, filters, params, conn=None): + obj = await get_or_create_object(model, filters, conn=conn) + return await update_object(obj, params, conn=conn) + + +# DELETE +@inject_connection +async def delete_object(obj, conn=None): + model = obj.__class__ + id_ = obj.id + return await delete_object_by_id(model, id_, conn=conn) + + +@inject_connection +async def delete_object_by_id(model, id_: int, conn=None): + query = delete(model).where(model.id == id_) + async with conn: + await conn.execute(query) + await conn.commit() + logger.debug(f"{__name__}.delete_object_by_id: model = {model}, id = {id_}") + return True + + +@inject_connection +async def bulk_delete(objects, conn=None): + for obj in objects: + await delete_object(obj, conn=conn) + return True + + +@inject_connection +async def bulk_delete_by_id(model, ids, conn=None): + for id_ in ids: + await delete_object_by_id(model, id_, conn=conn) + return True + diff --git a/simplecrud/tests/test_crud.py b/simplecrud/tests/test_crud.py index fa7bbb8..64efb7a 100644 --- a/simplecrud/tests/test_crud.py +++ b/simplecrud/tests/test_crud.py @@ -1,4 +1,6 @@ +import time import unittest +from unittest import skip from sqlalchemy import Column, Integer, String, create_engine from sqlalchemy.exc import InvalidRequestError @@ -59,155 +61,214 @@ def tearDown(self): @async_to_sync async def test_async_create_obj(self): - params_1 = dict(name="test1") - new_obj_1 = await create_object(ExampleModel, **params_1) - self.assertEqual(new_obj_1.name, "test1") + params_1 = dict(name="test_async_create_obj1") + new_obj_1 = await create_object(ExampleModel, params_1) + self.assertEqual(new_obj_1.name, "test_async_create_obj1") - params_2 = dict(name="test2") - new_obj_2 = await create_object(ExampleModel, **params_2) - self.assertEqual(new_obj_2.name, "test2") + params_2 = dict(name="test_async_create_obj2") + new_obj_2 = await create_object(ExampleModel, params_2) + self.assertEqual(new_obj_2.name, "test_async_create_obj2") @async_to_sync async def test_create_obj_params_error(self): - params_1 = dict(name="test1", wrong="wrong") + params_1 = dict(name="test_create_obj_params_error", wrong="wrong") with self.assertRaises(TypeError): new_obj_1 = await create_object(ExampleModel, **params_1) @async_to_sync async def test_get_object(self): - params_1 = dict(name="test1") - await create_object(ExampleModel, **params_1) - obj = await get_object(ExampleModel, **params_1) - self.assertEqual(obj.name, "test1") + params_1 = dict(name="test_get_object") + await create_object(ExampleModel, params_1) + obj = await get_object(ExampleModel, params_1) + self.assertEqual(obj.name, "test_get_object") @async_to_sync async def test_get_object_not_exist(self): - params_1 = dict(name="test1") - obj = await create_object(ExampleModel, **params_1) - none_expected = await get_object(ExampleModel, name="test0") + params_1 = dict(name="test_get_object_not_exist1") + obj = await create_object(ExampleModel, params_1) + none_expected = await get_object(ExampleModel, filters=dict(name="test_get_object_not_exist0")) self.assertEqual(none_expected, None) @async_to_sync async def test_get_object_error(self): - params_1 = dict(name="test1") - obj = await create_object(ExampleModel, **params_1) + params_1 = dict(name="test_get_object_error") + obj = await create_object(ExampleModel, params_1) with self.assertRaises(InvalidRequestError): - error_expected = await get_object(ExampleModel, wrong="wrong") + error_expected = await get_object(ExampleModel, filters=dict(wrong="wrong")) @async_to_sync async def test_get_all_objects(self): all_ = await get_all(ExampleModel) self.assertEqual(len(all_), 0) for i in range(5): - params_1 = dict(name=f"test{i}") - await create_object(ExampleModel, **params_1) + params_1 = dict(name=f"test_get_all_objects{i}") + await create_object(ExampleModel, params_1) all_ = await get_all(ExampleModel) - self.assertEqual(len(all_), 5) + self.assertEqual(5, len(all_)) self.assertTrue(isinstance(all_, list)) + await delete_object(all_[0]) @async_to_sync - async def test_get_objects_error(self): - raise Exception("Test not complete") - - @async_to_sync - async def test_get_all(self): - raise Exception("Test not complete") - - @async_to_sync - async def test_get_all_negative(self): - raise Exception("Test not complete") + async def test_get_all_if_objects_not_exist(self): + all_ = await get_all(ExampleModel) + self.assertEqual(len(all_), 0) + self.assertTrue(isinstance(all_, list)) - @async_to_sync + # + @skip async def test_get_all_error(self): raise Exception("Test not complete") - @async_to_sync + # + @skip async def test_get_all_with_filter(self): raise Exception("Test not complete") - @async_to_sync + # + @skip async def test_get_all_with_filter_negative(self): raise Exception("Test not complete") - @async_to_sync + # + @skip async def test_get_all_with_filter_error(self): raise Exception("Test not complete") @async_to_sync async def test_get_object_by_filters(self): - params_1 = dict(name="test1") - new_ = await create_object(ExampleModel, **params_1) - obj = await get_object(ExampleModel, id=new_.id) - self.assertEqual(obj.name ,"test1") + params_1 = dict(name="test_get_object_by_filters") + new_ = await create_object(ExampleModel, params_1) + obj = await get_object(ExampleModel, filters=dict(id=new_.id)) + self.assertEqual(obj.name, "test_get_object_by_filters") @async_to_sync async def test_get_object_by_filters_negative(self): - params_1 = dict(name="test1") - new_ = await create_object(ExampleModel, **params_1) + params_1 = dict(name="test_get_object_by_filters_negative") + new_ = await create_object(ExampleModel, params_1) with self.assertRaises(InvalidRequestError): - obj = await get_object(ExampleModel, pk=new_.id) + obj = await get_object(ExampleModel, filters=dict(pk=new_.id)) - @async_to_sync + @skip async def test_get_object_by_filters_error(self): raise Exception("Test not complete") @async_to_sync async def test_get_or_create_object(self): - raise Exception("Test not complete") - - @async_to_sync - async def test_get_or_create_object_negative(self): - raise Exception("Test not complete") - - @async_to_sync - async def test_get_or_create_object_error(self): - raise Exception("Test not complete") + self.assertEqual(len(await get_all(ExampleModel)), 0) + params_1 = dict(name="test_get_or_create_object") + new_1 = await get_or_create_object(ExampleModel, params_1) + self.assertEqual(len(await get_all(ExampleModel)), 1) + new_2 = await get_or_create_object(ExampleModel, params_1) + self.assertEqual(len(await get_all(ExampleModel)), 1) + self.assertEqual(new_1.id, new_2.id) @async_to_sync async def test_update_object(self): - raise Exception("Test not complete") + params_1 = dict(name="test_update_object") + obj1 = await create_object(ExampleModel, params_1) + params_2 = dict(name="test_update_object2") + obj2 = await update_object(obj1, params_2) + self.assertEqual(obj2.name, "test_update_object2") + self.assertEqual(obj1.id, obj2.id) @async_to_sync - async def test_update_object_negative(self): - raise Exception("Test not complete") + async def test_update_or_error(self): + params_1 = dict(name="test_update_object") + obj1 = await create_object(ExampleModel, params_1) + wrong_params = dict(wrong="test_update_object2") + with self.assertRaises(AttributeError) as error: + obj2 = await update_or_error(obj1, wrong_params) + error_msg = "Attribute wrong not exists in ExampleModel" + self.assertEqual(error.exception.args[0], error_msg) @async_to_sync - async def test_update_object_error(self): - raise Exception("Test not complete") - - @async_to_sync - async def test_update_or_create_object(self): - raise Exception("Test not complete") + async def test_soft_update_without_error(self): + params_1 = dict(name="test_update_object_negative") + obj1 = await create_object(ExampleModel, params_1) + self.assertFalse(hasattr(obj1, "wrong")) + wrong_params = dict(wrong="wrong") + obj2 = await update_object(obj1, wrong_params) + self.assertEqual(obj1.id, obj2.id) + self.assertFalse(hasattr(obj2, "wrong")) @async_to_sync - async def test_update_or_create_object_negative(self): - raise Exception("Test not complete") + async def test_update_or_error(self): + params_1 = dict(name="test1") + obj1 = await create_object(ExampleModel, params_1) + with self.assertRaises(AttributeError): + wrong_params = dict(wrong="wrong") + obj2 = await update_or_error(obj1, wrong_params) @async_to_sync + async def test_update_or_create_object(self): + self.assertEqual(len(await get_all(ExampleModel)), 0) + params_1 = dict(name="test_update_or_create_object1") + new_1 = await update_or_create_object(ExampleModel, params_1, params_1) + self.assertEqual(len(await get_all(ExampleModel)), 1) + params_2 = dict(name="test_update_or_create_object2") + new_2 = await update_or_create_object(ExampleModel, params_1, params_2) + self.assertEqual(new_2.name, "test_update_or_create_object2") + self.assertEqual(new_1.id, new_2.id) + + @skip async def test_update_or_create_object_error(self): raise Exception("Test not complete") @async_to_sync async def test_delete_object(self): - raise Exception("Test not complete") + all_ = await get_all(ExampleModel) + self.assertEqual(len(all_), 0) + params_1 = dict(name="test_delete_object") + new_1 = await create_object(ExampleModel, params_1) + all_ = await get_all(ExampleModel) + self.assertEqual(len(all_), 1) + result = await delete_object(new_1) + self.assertEqual(result, True) + all_ = await get_all(ExampleModel) + get_ = await get_object(ExampleModel, filters=dict(id=new_1.id)) + self.assertEqual(len(all_), 0) - @async_to_sync + @skip async def test_delete_object_negative(self): raise Exception("Test not complete") - @async_to_sync + @skip async def test_delete_object_error(self): raise Exception("Test not complete") @async_to_sync async def test_delete_objects(self): - raise Exception("Test not complete") + for i in range(1, 12): + params_1 = dict(name=f"test_delete_objects{i}") + await create_object(ExampleModel, params_1) + all_ = await get_all(ExampleModel) + result = await bulk_delete(all_[0:10]) + all_ = await get_all(ExampleModel) + self.assertEqual(1, len(all_)) + self.assertEqual(all_[0].name, "test_delete_objects11") + self.assertEqual(all_[0].id, 11) @async_to_sync - async def test_delete_objects_negative(self): - raise Exception("Test not complete") + async def test_delete_objects(self): + for i in range(1, 12): + params_1 = dict(name=f"test_delete_objects{i}") + await create_object(ExampleModel, params_1) + objects = await get_all(ExampleModel) + self.assertEqual(11, len(objects)) + await bulk_delete(objects) + all_ = await get_all(ExampleModel) + self.assertEqual(0, len(all_)) + + @async_to_sync - async def test_delete_objects_error(self): - raise Exception("Test not complete") + async def test_bulk_delete_by_id(self): + for i in range(1, 12): + params_1 = dict(name=f"test_delete_objects{i}") + await create_object(ExampleModel, params_1) + ids = [i.id for i in await get_all(ExampleModel)] + self.assertEqual(11, len(ids)) + await bulk_delete(ExampleModel, ids) + all_ = await get_all(ExampleModel) + self.assertEqual(0, len(all_)) diff --git a/simplecrud/utils.py b/simplecrud/utils.py index 1cebd6d..9593b6c 100644 --- a/simplecrud/utils.py +++ b/simplecrud/utils.py @@ -1,4 +1,5 @@ import asyncio +import logging from functools import wraps @@ -12,3 +13,35 @@ def wrapper(*args, **kwargs): return result return wrapper + + +def inject_connection(func): + """Decorator to inject database connection to function""" + + @wraps(func) + def inner(*args, **kwargs): + from simplecrud.settings import session + if kwargs.get('conn') is None: + kwargs['conn'] = session() + result = func(*args, **kwargs) + # try: + # kwargs['conn'].close() + # except: + # pass + return result + + return inner + + +def add_log(func): + """Decorator to add log""" + + @wraps(func) + def inner(*args, **kwargs): + logger = logging.getLogger(__name__) + logger.debug(f"{__name__}.{func.__name__}: args = {args}, kwargs = {kwargs}") + result = func(*args, **kwargs) + logger.debug(f"{__name__}.{func.__name__}: result = {result}") + return result + + return inner From 4f078085d14be101152280e17aba0adefa98f52a Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 12:27:44 +0300 Subject: [PATCH 06/33] Update coverage.yml --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 289f8e9..32de4f2 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -19,5 +19,5 @@ jobs: env: CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} with: - coverageCommand: make test-coverage + coverageCommand: poetry run pytest --cov=gendiff --cov-report xml debug: true From ff7237cad8c8023715187454634b46aa0e0ba09d Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 12:33:45 +0300 Subject: [PATCH 07/33] Remove test.db from repo --- .gitignore | 2 +- simplecrud/tests/test.db | Bin 8192 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 simplecrud/tests/test.db diff --git a/.gitignore b/.gitignore index 0eea4eb..a74d124 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ __pycache__/ *$py.class *cache* - +*test.db .idea/ .vscode/ diff --git a/simplecrud/tests/test.db b/simplecrud/tests/test.db deleted file mode 100644 index 6ae6a211f9f1bffba7e11f144dc52314bdbcfb2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8192 zcmeIuu?c`M5QX6vu@xO56WF+b1K5ZYSSTonASo@aGn*rsfYt%)|H0$n?y7I-yTwg* z-j8i`woxTx)LJXahh}e0m}h7n`X%mC@v7Imh+madUa5EP2q1s}0tg_000IagfB*sr cAb Date: Sat, 27 Jan 2024 12:40:27 +0300 Subject: [PATCH 08/33] Remove inside logging --- simplecrud/crud.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/simplecrud/crud.py b/simplecrud/crud.py index d8feca7..1443e08 100644 --- a/simplecrud/crud.py +++ b/simplecrud/crud.py @@ -3,8 +3,6 @@ # from cachetools import LFUCache from sqlalchemy import select, delete - -from .settings import session from .utils import inject_connection logger = logging.getLogger(__name__) @@ -14,14 +12,10 @@ @inject_connection async def get_object(model, filters, conn=None): """Get object from db""" - key = f"{model.__name__}{filters}" query = select(model).filter_by(**filters) - logger.debug(f"{__name__}.get_obj: query = {query}") async with conn: result = await conn.execute(query) - logger.debug(f"{__name__}.get_obj: result = {result}") obj = result.scalars().first() - logger.debug(f"{__name__}.get_obj: obj = {obj}") return obj @@ -31,9 +25,7 @@ async def get_all(model, conn=None): query = select(model) async with conn: result = await conn.execute(query) - logger.debug(f"{__name__}.get_all: result = {result}") objects = result.scalars().all() - logger.debug(f"{__name__}.get_all: obj = {objects}") return objects @@ -43,9 +35,7 @@ async def get_all_with_filter(model, filters: dict, conn=None): query = select(model).filter_by(**filters) async with conn: result = await conn.execute(query) - logger.debug(f"{__name__}.get_all: result = {result}") objects = result.scalars().all() - logger.debug(f"{__name__}.get_all: obj = {objects}") return objects @@ -53,18 +43,14 @@ async def get_all_with_filter(model, filters: dict, conn=None): async def get_objects(model, filters: Dict, limit=10, per_page=10, conn=None): """Get objects from db""" query = select(model).filter_by(**filters).limit(limit).offset(per_page) - logger.debug(f"{__name__}.get_objects: query = {query}") async with conn: result = await conn.execute(query) - logger.debug(f"{__name__}.get_objects: result = {result}") objects = result.scalars().all() - logger.debug(f"{__name__}.get_objects: obj = {objects}") return objects async def get_or_create_object(model, params, conn=None): """Get object from db or create new one""" - key = f"{model.__name__}{params}" obj = await get_object(model, params, conn=conn) if not obj: obj = await create_object(model, params, conn=conn) @@ -75,7 +61,6 @@ async def get_or_create_object(model, params, conn=None): @inject_connection async def create_object(model, params, conn=None): """Create object in db""" - logger.debug(f"{__name__}.create_obj: model = {model}, params = {params}") new_obj = model(**params) async with conn: conn.add(new_obj) @@ -171,4 +156,3 @@ async def bulk_delete_by_id(model, ids, conn=None): for id_ in ids: await delete_object_by_id(model, id_, conn=conn) return True - From 2dfca6ca94314784e64ea230df57f55e59418215 Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 12:40:40 +0300 Subject: [PATCH 09/33] Add linter command --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1eeef08..a1ba989 100644 --- a/Makefile +++ b/Makefile @@ -3,4 +3,6 @@ build: publish: poetry publish --dry-run package-install: - python3 -m pip install --user dist/*.whl --force \ No newline at end of file + python3 -m pip install --user dist/*.whl --force +lint: + poetry run flake8 simplecrud \ No newline at end of file From 9789ebbb31d57aff304256df9f11a38994755e65 Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 12:40:57 +0300 Subject: [PATCH 10/33] Add flake8 config --- setup.cfg | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b033231 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,9 @@ +[flake8] +;extend-ignore = F401 +max-line-length = 95 +exclude = + .git, + */tests/*, + __pycache__, + docs/source/conf.py, + old, \ No newline at end of file From 3d2ac4a0e01778fe4719cab5ba67e80a7eb48cb1 Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 12:55:09 +0300 Subject: [PATCH 11/33] Update --- simplecrud/__init__.py | 2 ++ simplecrud/crud.py | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/simplecrud/__init__.py b/simplecrud/__init__.py index e69de29..a63f632 100644 --- a/simplecrud/__init__.py +++ b/simplecrud/__init__.py @@ -0,0 +1,2 @@ +from . import crud +from .settings import CRUDConfig \ No newline at end of file diff --git a/simplecrud/crud.py b/simplecrud/crud.py index 1443e08..f128d9b 100644 --- a/simplecrud/crud.py +++ b/simplecrud/crud.py @@ -1,12 +1,9 @@ import logging from typing import Dict -# from cachetools import LFUCache from sqlalchemy import select, delete from .utils import inject_connection -logger = logging.getLogger(__name__) - # READ / GET @inject_connection @@ -111,13 +108,17 @@ async def update_or_error(obj, params, conn=None): @inject_connection async def update_object_by_id(model, id: int, params, conn=None): + """Update object in db by id""" obj = await get_object(model, id=id) updated_obj = await update_object(obj, params, conn=conn) return updated_obj -def bulk_update(): - pass +@inject_connection +async def bulk_update(objects, conn=None): + """Bulk update objects in db""" + for obj in objects: + await update_object(obj, conn=conn) @inject_connection @@ -140,7 +141,6 @@ async def delete_object_by_id(model, id_: int, conn=None): async with conn: await conn.execute(query) await conn.commit() - logger.debug(f"{__name__}.delete_object_by_id: model = {model}, id = {id_}") return True From 3ff657d4cc52089a5af89d6a489e1d7031dcfe80 Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 12:58:35 +0300 Subject: [PATCH 12/33] Update --- README.md | 4 ++-- pyproject.toml | 4 ++-- simplecrud/crud.py | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e146de2..bef76a4 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ SimpleCRUD is a library that provides a simple way to create CRUD commands for S ## Installation ```bash -pip install simplecrud +pip install hexfrost-simplecrud ``` ``` -poetry add simplecrud +poetry add hexfrost-simplecrud ``` ## Usage diff --git a/pyproject.toml b/pyproject.toml index 8342647..7ab4cd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "hexfrost-simplecrud" -version = "0.1.0" -description = "library with operation like (get, create, update, delete) for SQLAlchemy ORM" +version = "0.2.0b0" +description = "Library with operation like (get, create, update, delete) for SQLAlchemy ORM" authors = ["Ilia Kaziamov "] packages = [ { include = "simplecrud" }, diff --git a/simplecrud/crud.py b/simplecrud/crud.py index f128d9b..d3c7d00 100644 --- a/simplecrud/crud.py +++ b/simplecrud/crud.py @@ -46,6 +46,7 @@ async def get_objects(model, filters: Dict, limit=10, per_page=10, conn=None): return objects +@inject_connection async def get_or_create_object(model, params, conn=None): """Get object from db or create new one""" obj = await get_object(model, params, conn=conn) From cebf4d2bc5d994bf2310353c7adccc7faf91cdf1 Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 13:02:33 +0300 Subject: [PATCH 13/33] Update setup.cfg --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index b033231..391fbcd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,9 @@ [flake8] -;extend-ignore = F401 +extend-ignore = F401 max-line-length = 95 exclude = .git, */tests/*, __pycache__, docs/source/conf.py, - old, \ No newline at end of file + old, From f973e27c030fde652bedc4266fa1e13b59d479af Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 13:04:33 +0300 Subject: [PATCH 14/33] Update coverage.yml --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 32de4f2..069245c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -19,5 +19,5 @@ jobs: env: CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} with: - coverageCommand: poetry run pytest --cov=gendiff --cov-report xml + coverageCommand: poetry run pytest --cov=simplecrud --cov-report xml debug: true From 099895fede341ee1ac037984afb222b31bcf47a1 Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 14:49:51 +0300 Subject: [PATCH 15/33] Fix test --- simplecrud/tests/test_crud.py | 6 ++---- simplecrud/tests/test_settings.py | 4 ++++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/simplecrud/tests/test_crud.py b/simplecrud/tests/test_crud.py index 64efb7a..280d665 100644 --- a/simplecrud/tests/test_crud.py +++ b/simplecrud/tests/test_crud.py @@ -250,7 +250,7 @@ async def test_delete_objects(self): self.assertEqual(all_[0].id, 11) @async_to_sync - async def test_delete_objects(self): + async def test_bulk_delete(self): for i in range(1, 12): params_1 = dict(name=f"test_delete_objects{i}") await create_object(ExampleModel, params_1) @@ -260,8 +260,6 @@ async def test_delete_objects(self): all_ = await get_all(ExampleModel) self.assertEqual(0, len(all_)) - - @async_to_sync async def test_bulk_delete_by_id(self): for i in range(1, 12): @@ -269,6 +267,6 @@ async def test_bulk_delete_by_id(self): await create_object(ExampleModel, params_1) ids = [i.id for i in await get_all(ExampleModel)] self.assertEqual(11, len(ids)) - await bulk_delete(ExampleModel, ids) + await bulk_delete_by_id(ExampleModel, ids) all_ = await get_all(ExampleModel) self.assertEqual(0, len(all_)) diff --git a/simplecrud/tests/test_settings.py b/simplecrud/tests/test_settings.py index 677e735..b278cce 100644 --- a/simplecrud/tests/test_settings.py +++ b/simplecrud/tests/test_settings.py @@ -4,6 +4,10 @@ class TestCRUDConfig(unittest.TestCase): + + def setUp(self): + CRUDConfig._instance = None + def test_singleton(self): config1 = CRUDConfig() config2 = CRUDConfig() From 69c16aa0c866f736f9f6005cf7421913a7bac593 Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 14:50:00 +0300 Subject: [PATCH 16/33] Add coverage lib --- poetry.lock | 87 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index ed19e95..25622b9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -26,6 +26,73 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.4.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, + {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, + {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, + {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, + {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, + {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, + {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, + {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, + {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, + {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, + {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, + {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, + {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + [[package]] name = "exceptiongroup" version = "1.2.0" @@ -219,6 +286,24 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + [[package]] name = "sqlalchemy" version = "2.0.25" @@ -331,4 +416,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "cc24393cb242963e33cb2b5279912ec21a492df69407ed37883a40903cb7ad65" +content-hash = "c32f9ab0e1ded0357a0faf33611af5df7b188022094f8ae70e2f99187886c1d8" diff --git a/pyproject.toml b/pyproject.toml index 7ab4cd3..d07e9ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.9" sqlalchemy = "^2.0.0" +pytest-cov = "^4.1.0" [tool.poetry.group.dev.dependencies] flake8 = "^7.0.0" From cd4892e1b961c67d26be347e5d4abbce13a0c16d Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 14:50:20 +0300 Subject: [PATCH 17/33] Update import --- simplecrud/utils.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/simplecrud/utils.py b/simplecrud/utils.py index 9593b6c..dc94d86 100644 --- a/simplecrud/utils.py +++ b/simplecrud/utils.py @@ -2,6 +2,8 @@ import logging from functools import wraps +from simplecrud.settings import session + def async_to_sync(func): """Decorator to convert async function to sync""" @@ -20,14 +22,9 @@ def inject_connection(func): @wraps(func) def inner(*args, **kwargs): - from simplecrud.settings import session if kwargs.get('conn') is None: kwargs['conn'] = session() result = func(*args, **kwargs) - # try: - # kwargs['conn'].close() - # except: - # pass return result return inner From 3f1cbd3d2e2934c9067d09622309ff5f1f77d11b Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 14:52:10 +0300 Subject: [PATCH 18/33] Remove useless tests --- simplecrud/tests/test_crud.py | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/simplecrud/tests/test_crud.py b/simplecrud/tests/test_crud.py index 280d665..194d4bb 100644 --- a/simplecrud/tests/test_crud.py +++ b/simplecrud/tests/test_crud.py @@ -26,30 +26,6 @@ class ExampleModel(Base): name = Column(String(255), nullable=False) -class TestCRUDFunctions(unittest.TestCase): - - def setUp(self): - Base.metadata.create_all(engine) - - def tearDown(self): - Base.metadata.drop_all(engine) - - def test_create_obj(self): - params = dict(name="test") - new_obj = ExampleModel(**params) - with Session(engine) as conn: - conn.add(new_obj) - conn.commit() - conn.refresh(new_obj) - - self.assertEqual(new_obj.name, "test") - - def test_create_obj_wrong_params(self): - params = dict(name="test", wrong="wrong") - with self.assertRaises(TypeError): - new_obj = ExampleModel(**params) - - class TestAsyncCRUDFunctions(unittest.TestCase): def setUp(self): @@ -116,7 +92,7 @@ async def test_get_all_if_objects_not_exist(self): self.assertTrue(isinstance(all_, list)) # - @skip + @async_to_sync async def test_get_all_error(self): raise Exception("Test not complete") From 874ee2357f1a529906ce7a5b2022437539b56a73 Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 15:57:41 +0300 Subject: [PATCH 19/33] Update tests --- simplecrud/tests/test_crud.py | 2 +- simplecrud/tests/test_utils.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 simplecrud/tests/test_utils.py diff --git a/simplecrud/tests/test_crud.py b/simplecrud/tests/test_crud.py index 194d4bb..d996828 100644 --- a/simplecrud/tests/test_crud.py +++ b/simplecrud/tests/test_crud.py @@ -92,7 +92,7 @@ async def test_get_all_if_objects_not_exist(self): self.assertTrue(isinstance(all_, list)) # - @async_to_sync + @skip async def test_get_all_error(self): raise Exception("Test not complete") diff --git a/simplecrud/tests/test_utils.py b/simplecrud/tests/test_utils.py new file mode 100644 index 0000000..1978728 --- /dev/null +++ b/simplecrud/tests/test_utils.py @@ -0,0 +1,29 @@ +import unittest + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, Session + +from simplecrud import CRUDConfig +from simplecrud.tests.test_crud import engine, Base +from simplecrud.utils import inject_connection + +database_url = "sqlite:///./test.db" +engine = create_engine(database_url) +session_maker = sessionmaker(bind=engine) + + +class TestAsyncCRUDFunctions(unittest.TestCase): + + def setUp(self): + Base.metadata.create_all(engine) + CRUDConfig().set_sessionmaker(session_maker) + + def tearDown(self): + Base.metadata.drop_all(engine) + + def test_inject_connection(self): + def example_func(conn=None): + return conn + + conn = inject_connection(example_func)() + self.assertIsInstance(conn, Session) From 769a1ffe49182dec294f20d84a91ddd2705e11f9 Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 18:08:06 +0300 Subject: [PATCH 20/33] Add typing --- simplecrud/crud.py | 61 +++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/simplecrud/crud.py b/simplecrud/crud.py index d3c7d00..31feb45 100644 --- a/simplecrud/crud.py +++ b/simplecrud/crud.py @@ -1,5 +1,5 @@ import logging -from typing import Dict +from typing import Dict, List from sqlalchemy import select, delete from .utils import inject_connection @@ -7,7 +7,7 @@ # READ / GET @inject_connection -async def get_object(model, filters, conn=None): +async def get_object(model, filters, conn=None) -> object: """Get object from db""" query = select(model).filter_by(**filters) async with conn: @@ -17,7 +17,7 @@ async def get_object(model, filters, conn=None): @inject_connection -async def get_all(model, conn=None): +async def get_all(model, conn=None) -> List[object]: """Get objects from db""" query = select(model) async with conn: @@ -27,7 +27,7 @@ async def get_all(model, conn=None): @inject_connection -async def get_all_with_filter(model, filters: dict, conn=None): +async def get_all_with_filter(model, filters: dict, conn=None) -> List[object]: """Get objects from db""" query = select(model).filter_by(**filters) async with conn: @@ -37,9 +37,9 @@ async def get_all_with_filter(model, filters: dict, conn=None): @inject_connection -async def get_objects(model, filters: Dict, limit=10, per_page=10, conn=None): +async def get_objects(model, filters: Dict, limit=10, offset=10, conn=None) -> List[object]: """Get objects from db""" - query = select(model).filter_by(**filters).limit(limit).offset(per_page) + query = select(model).filter_by(**filters).limit(limit).offset(offset) async with conn: result = await conn.execute(query) objects = result.scalars().all() @@ -47,7 +47,7 @@ async def get_objects(model, filters: Dict, limit=10, per_page=10, conn=None): @inject_connection -async def get_or_create_object(model, params, conn=None): +async def get_or_create_object(model, params, conn=None) -> object: """Get object from db or create new one""" obj = await get_object(model, params, conn=conn) if not obj: @@ -57,7 +57,7 @@ async def get_or_create_object(model, params, conn=None): # CREATE @inject_connection -async def create_object(model, params, conn=None): +async def create_object(model, params, conn=None) -> object: """Create object in db""" new_obj = model(**params) async with conn: @@ -67,14 +67,14 @@ async def create_object(model, params, conn=None): @inject_connection -async def bulk_create(objects, conn=None): - for obj in objects: - await create_object(obj, conn=conn) +async def bulk_create(model, data: List[Dict], conn=None) -> List[object]: + """Bulk create objects in db""" + return [await create_object(model, params, conn=conn) for params in data] # UPDATE @inject_connection -async def update_object(obj, params, conn=None): +async def update_object(obj, params, conn=None) -> object: """ Soft Update object in db. If attribute not exists in model`s fields, then skip field without error @@ -90,54 +90,53 @@ async def update_object(obj, params, conn=None): @inject_connection -async def update_or_error(obj, params, conn=None): +async def update_or_error(obj, params, conn=None) -> object: """ Soft Update object in db. If attribute not exists in model`s fields, then skip field without error """ avaliable_fields = obj.__class__.__table__.columns.keys() for key, value in params.items(): - if key in avaliable_fields: - setattr(obj, key, value) - else: + if key not in avaliable_fields: raise AttributeError(f"Attribute {key} not exists in {obj.__class__.__name__}") - async with conn: - await conn.commit() - conn.refresh(obj) + obj = await update_object(obj, params, conn=conn) return obj @inject_connection -async def update_object_by_id(model, id: int, params, conn=None): +async def update_object_by_id(model, id: int, params, conn=None) -> object: """Update object in db by id""" - obj = await get_object(model, id=id) + obj = await get_object(model, dict(id=id)) updated_obj = await update_object(obj, params, conn=conn) return updated_obj -@inject_connection -async def bulk_update(objects, conn=None): - """Bulk update objects in db""" - for obj in objects: - await update_object(obj, conn=conn) +# @inject_connection +# async def bulk_update(objects, params, conn=None) -> List[object]: +# """Bulk update objects in db""" +# updated_objects = [] +# for obj in objects: +# updated_obj = await update_object(obj, params, conn=conn) +# updated_objects.append(updated_obj) +# return updated_objects @inject_connection -async def update_or_create_object(model, filters, params, conn=None): +async def update_or_create_object(model, filters, params, conn=None) -> object: obj = await get_or_create_object(model, filters, conn=conn) return await update_object(obj, params, conn=conn) # DELETE @inject_connection -async def delete_object(obj, conn=None): +async def delete_object(obj, conn=None) -> bool: model = obj.__class__ id_ = obj.id return await delete_object_by_id(model, id_, conn=conn) @inject_connection -async def delete_object_by_id(model, id_: int, conn=None): +async def delete_object_by_id(model, id_: int, conn=None) -> bool: query = delete(model).where(model.id == id_) async with conn: await conn.execute(query) @@ -146,14 +145,14 @@ async def delete_object_by_id(model, id_: int, conn=None): @inject_connection -async def bulk_delete(objects, conn=None): +async def bulk_delete(objects, conn=None) -> bool: for obj in objects: await delete_object(obj, conn=conn) return True @inject_connection -async def bulk_delete_by_id(model, ids, conn=None): +async def bulk_delete_by_id(model, ids, conn=None) -> bool: for id_ in ids: await delete_object_by_id(model, id_, conn=conn) return True From 3bb47c224266aa3aace6e2b7ef7e339c6938e88b Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 18:08:25 +0300 Subject: [PATCH 21/33] Update tests --- simplecrud/tests/test_crud.py | 119 ++++++++++++++++++++++++---------- 1 file changed, 85 insertions(+), 34 deletions(-) diff --git a/simplecrud/tests/test_crud.py b/simplecrud/tests/test_crud.py index d996828..8c52e6a 100644 --- a/simplecrud/tests/test_crud.py +++ b/simplecrud/tests/test_crud.py @@ -1,6 +1,5 @@ import time import unittest -from unittest import skip from sqlalchemy import Column, Integer, String, create_engine from sqlalchemy.exc import InvalidRequestError @@ -51,6 +50,17 @@ async def test_create_obj_params_error(self): with self.assertRaises(TypeError): new_obj_1 = await create_object(ExampleModel, **params_1) + @async_to_sync + async def test_bulk_create(self): + all_ = await get_all(ExampleModel) + self.assertEqual(len(all_), 0) + data = [dict(name=f"test_bulk_create{i}") for i in range(1, 11)] + objects = await bulk_create(ExampleModel, data) + all_ = await get_all(ExampleModel) + self.assertEqual(len(all_), 10) + for i in range(1, 11): + self.assertEqual(all_[i - 1].name, f"test_bulk_create{i}") + @async_to_sync async def test_get_object(self): params_1 = dict(name="test_get_object") @@ -91,25 +101,42 @@ async def test_get_all_if_objects_not_exist(self): self.assertEqual(len(all_), 0) self.assertTrue(isinstance(all_, list)) - # - @skip - async def test_get_all_error(self): - raise Exception("Test not complete") - - # - @skip + @async_to_sync async def test_get_all_with_filter(self): - raise Exception("Test not complete") + for i in range(1, 6): + params_1 = dict(name=f"test_get_all_with_filter{i}") + await create_object(ExampleModel, params_1) + all_ = await get_all_with_filter(ExampleModel, dict(name="test_get_all_with_filter1")) + self.assertEqual(len(all_), 1) - # - @skip - async def test_get_all_with_filter_negative(self): - raise Exception("Test not complete") + # TODO: Add test for multiple filter parameters - # - @skip + @async_to_sync async def test_get_all_with_filter_error(self): - raise Exception("Test not complete") + await create_object(ExampleModel, dict(name="test_get_all_with_filter_negative")) + with self.assertRaises(InvalidRequestError): + await get_all_with_filter(ExampleModel, dict(wrong="wrong")) + + @async_to_sync + async def test_get_all_with_filter_negative(self): + for i in range(1, 6): + params_1 = dict(name=f"test_get_all_with_filter{i}") + await create_object(ExampleModel, params_1) + all_ = await get_all_with_filter(ExampleModel, dict(name="not_exist")) + self.assertEqual(len(all_), 0) + + @async_to_sync + async def test_get_objects_with_limit_and_per_page(self): + for i in range(1, 30): + params_1 = dict(name=f"test_get_objects_with_limit_and_ofset{i}") + await create_object(ExampleModel, params_1) + ten = await get_objects(ExampleModel, {}, limit=10, offset=10) + self.assertEqual(len(ten), 10) + one = await get_objects(ExampleModel, {}, limit=1, offset=1) + self.assertEqual(len(one), 1) + second = await get_objects(ExampleModel, {}, limit=1, offset=2) + self.assertEqual(len(second), 1) + self.assertEqual(second[0].id, 3) @async_to_sync async def test_get_object_by_filters(self): @@ -125,10 +152,6 @@ async def test_get_object_by_filters_negative(self): with self.assertRaises(InvalidRequestError): obj = await get_object(ExampleModel, filters=dict(pk=new_.id)) - @skip - async def test_get_object_by_filters_error(self): - raise Exception("Test not complete") - @async_to_sync async def test_get_or_create_object(self): self.assertEqual(len(await get_all(ExampleModel)), 0) @@ -150,6 +173,20 @@ async def test_update_object(self): @async_to_sync async def test_update_or_error(self): + obj = await create_object(ExampleModel, dict(name="test_update_or_error")) + await update_or_error(obj, dict(name="test_update_or_error_updated")) + upd_obj = await get_object(ExampleModel, dict(id=obj.id)) + self.assertEqual(upd_obj.id, obj.id) + self.assertEqual(upd_obj.name, "test_update_or_error_updated") + + @async_to_sync + async def test_update_or_error_error(self): + obj = await create_object(ExampleModel, dict(name="test_update_or_error")) + with self.assertRaises(AttributeError): + await update_or_error(obj, dict(wrong="wrong")) + + @async_to_sync + async def test_update_or_error_negative(self): params_1 = dict(name="test_update_object") obj1 = await create_object(ExampleModel, params_1) wrong_params = dict(wrong="test_update_object2") @@ -172,9 +209,18 @@ async def test_soft_update_without_error(self): async def test_update_or_error(self): params_1 = dict(name="test1") obj1 = await create_object(ExampleModel, params_1) - with self.assertRaises(AttributeError): - wrong_params = dict(wrong="wrong") - obj2 = await update_or_error(obj1, wrong_params) + params_2 = dict(name="test2") + obj2 = await update_or_error(obj1, params_2) + self.assertEqual(obj2.name, "test2") + + @async_to_sync + async def test_update_by_id(self): + params_1 = dict(name="test1") + obj1 = await create_object(ExampleModel, params_1) + id_ = obj1.id + params_2 = dict(name="test2") + obj2 = await update_object_by_id(ExampleModel, id_, params_2) + self.assertEqual(obj2.name, "test2") @async_to_sync async def test_update_or_create_object(self): @@ -187,9 +233,22 @@ async def test_update_or_create_object(self): self.assertEqual(new_2.name, "test_update_or_create_object2") self.assertEqual(new_1.id, new_2.id) - @skip - async def test_update_or_create_object_error(self): - raise Exception("Test not complete") + @async_to_sync + async def test_bulk_update(self): + all_ = await get_all(ExampleModel) + self.assertEqual(len(all_), 0) + for i in range(1, 11): + params_1 = dict(name=f"test_bulk_update{i}") + await create_object(ExampleModel, params_1) + all_ = await get_all(ExampleModel) + self.assertEqual(len(all_), 10) + new_data = dict(name="test_bulk_update_updated") + objects = await bulk_update(all_, new_data) + self.assertEqual(len(objects), 10) + self.assertEqual(list, type(objects), "Result must be list") + all_ = await get_all(ExampleModel) + for obj in all_: + self.assertEqual("test_bulk_update_updated", obj.name) @async_to_sync async def test_delete_object(self): @@ -205,14 +264,6 @@ async def test_delete_object(self): get_ = await get_object(ExampleModel, filters=dict(id=new_1.id)) self.assertEqual(len(all_), 0) - @skip - async def test_delete_object_negative(self): - raise Exception("Test not complete") - - @skip - async def test_delete_object_error(self): - raise Exception("Test not complete") - @async_to_sync async def test_delete_objects(self): for i in range(1, 12): From 4e59b80ef6e51c684eaf7a38c7e705e30c2358df Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 18:12:07 +0300 Subject: [PATCH 22/33] Update __init__.py --- simplecrud/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simplecrud/__init__.py b/simplecrud/__init__.py index a63f632..58c1da5 100644 --- a/simplecrud/__init__.py +++ b/simplecrud/__init__.py @@ -1,2 +1,2 @@ from . import crud -from .settings import CRUDConfig \ No newline at end of file +from .settings import CRUDConfig From c8655269cc120013fe19e30056b125ee5dc035b1 Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 18:14:14 +0300 Subject: [PATCH 23/33] Comment useless test --- simplecrud/tests/test_crud.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/simplecrud/tests/test_crud.py b/simplecrud/tests/test_crud.py index 8c52e6a..9bf3f90 100644 --- a/simplecrud/tests/test_crud.py +++ b/simplecrud/tests/test_crud.py @@ -233,22 +233,22 @@ async def test_update_or_create_object(self): self.assertEqual(new_2.name, "test_update_or_create_object2") self.assertEqual(new_1.id, new_2.id) - @async_to_sync - async def test_bulk_update(self): - all_ = await get_all(ExampleModel) - self.assertEqual(len(all_), 0) - for i in range(1, 11): - params_1 = dict(name=f"test_bulk_update{i}") - await create_object(ExampleModel, params_1) - all_ = await get_all(ExampleModel) - self.assertEqual(len(all_), 10) - new_data = dict(name="test_bulk_update_updated") - objects = await bulk_update(all_, new_data) - self.assertEqual(len(objects), 10) - self.assertEqual(list, type(objects), "Result must be list") - all_ = await get_all(ExampleModel) - for obj in all_: - self.assertEqual("test_bulk_update_updated", obj.name) + # @async_to_sync + # async def test_bulk_update(self): + # all_ = await get_all(ExampleModel) + # self.assertEqual(len(all_), 0) + # for i in range(1, 11): + # params_1 = dict(name=f"test_bulk_update{i}") + # await create_object(ExampleModel, params_1) + # all_ = await get_all(ExampleModel) + # self.assertEqual(len(all_), 10) + # new_data = dict(name="test_bulk_update_updated") + # objects = await bulk_update(all_, new_data) + # self.assertEqual(len(objects), 10) + # self.assertEqual(list, type(objects), "Result must be list") + # all_ = await get_all(ExampleModel) + # for obj in all_: + # self.assertEqual("test_bulk_update_updated", obj.name) @async_to_sync async def test_delete_object(self): From 2cd58003e2bef6a1fefdef4a9ed23b1493dc048a Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 18:30:48 +0300 Subject: [PATCH 24/33] Update README.md --- README.md | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bef76a4..c51d91e 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,14 @@ poetry add hexfrost-simplecrud ## Usage +1. Create a model +2. Create a CRUDConfig +3. Set sessionmaker to CRUDConfig +4. Import CRUD functions +5. Use CRUD functions and enjoy ### Example usage + ```python from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from sqlalchemy.orm import DeclarativeBase @@ -40,19 +46,42 @@ CRUDConfig.sessionmaker(async_sessionmaker) async def example_func(): # Create a model - new_model = await create_obj(model, name="test", description="test") + new_model = await create_object(model, name="test", description="test") # Get all models all_objs = await get_all(model) # Update a model - updated_obj = await update_obj(model, name="test2", description="test2") + updated_obj = await update_object(model, name="test2", description="test2") # Delete a model - await delete_obj(model, name="test2", description="test2") + await delete_object(model, name="test2", description="test2") ``` +### Avaliable functions: +- `get_object` - get a single object +- `get_all` - get all objects +- `get_all_with_filter` - get all objects with filter +- `get_objects` - get all objects with filter, limit and offset +- `get_or_create_object` - get or create an object +- `create_object` - create an object +- `bulk_create` - create multiple objects +- `update_object` - update an object +- `update_or_error` - update an object or raise an error +- `update_object_by_id` - update an object by id +- `update_or_create_object` - update or create an object +- `delete_object` - delete an object +- `delete_object_by_id` - delete an object by id +- `bulk_delete` - bulk delete objects +- `bulk_delete_by_id` - bulk delete objects by id + + +## Contributing + +This project is open for contributions. Feel free to open an issue or create a pull request. + + From 9f4464d67ad0a2ae295382c59bc6c5d2b59be0fa Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 18:35:32 +0300 Subject: [PATCH 25/33] Update linter yml --- .github/workflows/linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index d422516..e772685 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -2,7 +2,7 @@ name: lint-test on: push: - branches: [ "dev" ] + branches: [ "dev", "staging" ] jobs: build: From 08a5c246d1ced4888feec654ed23486ae9b47946 Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 18:53:55 +0300 Subject: [PATCH 26/33] Update configs and README --- .github/workflows/compatibility.yml | 2 +- .github/workflows/linter.yml | 2 +- README.md | 4 ++++ pyproject.toml | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index fff2050..5bfa1db 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -26,7 +26,7 @@ jobs: run: | python -m pip install --upgrade pip pip install poetry - poetry install + poetry install --no-root - name: Run compatibility tests run: poetry run pytest diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index e772685..fe02d84 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,4 +1,4 @@ -name: lint-test +name: flake8 on: push: diff --git a/README.md b/README.md index c51d91e..8f666de 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![Maintainability](https://api.codeclimate.com/v1/badges/d33ecb2661fb7aedf516/maintainability)](https://codeclimate.com/github/hexfrost/sqlalchemy-models-commands/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/d33ecb2661fb7aedf516/test_coverage)](https://codeclimate.com/github/hexfrost/sqlalchemy-models-commands/test_coverage) +[![flake8](https://github.com/hexfrost/simplecrud/actions/workflows/linter.yml/badge.svg?branch=staging)](https://github.com/hexfrost/simplecrud/actions/workflows/linter.yml) # SimpleCRUD SimpleCRUD is a library that provides a simple way to create CRUD commands for SQLAlchemy models. @@ -76,11 +77,14 @@ async def example_func(): - `bulk_delete` - bulk delete objects - `bulk_delete_by_id` - bulk delete objects by id +## License + ## Contributing This project is open for contributions. Feel free to open an issue or create a pull request. +[![flake8](https://github.com/hexfrost/simplecrud/actions/workflows/linter.yml/badge.svg?branch=dev)](https://github.com/hexfrost/simplecrud/actions/workflows/linter.yml) diff --git a/pyproject.toml b/pyproject.toml index d07e9ec..a9ccac2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ packages = [ readme = "README.md" [tool.poetry.dependencies] -python = "^3.9" +python = "^3.6" sqlalchemy = "^2.0.0" pytest-cov = "^4.1.0" From cfdde48c292090db7d71702ad30ef040fc3baede Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 19:02:37 +0300 Subject: [PATCH 27/33] Update README --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8f666de..7694996 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ # SimpleCRUD SimpleCRUD is a library that provides a simple way to create CRUD commands for SQLAlchemy models. +*** + ## Installation ```bash @@ -14,6 +16,8 @@ pip install hexfrost-simplecrud poetry add hexfrost-simplecrud ``` +*** + ## Usage 1. Create a model @@ -77,18 +81,19 @@ async def example_func(): - `bulk_delete` - bulk delete objects - `bulk_delete_by_id` - bulk delete objects by id -## License - +*** ## Contributing This project is open for contributions. Feel free to open an issue or create a pull request. +Dev version status: [![flake8](https://github.com/hexfrost/simplecrud/actions/workflows/linter.yml/badge.svg?branch=dev)](https://github.com/hexfrost/simplecrud/actions/workflows/linter.yml) +[![Coverage](https://github.com/hexfrost/simplecrud/actions/workflows/coverage.yml/badge.svg?branch=dev)](https://github.com/hexfrost/simplecrud/actions/workflows/coverage.yml) +*** +## License - - - +```GNU GENERAL PUBLIC LICENSE Version 3``` From 8b3e93b3e0adbf4bcd6042c5a820dd58fb6315b3 Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 19:04:09 +0300 Subject: [PATCH 28/33] Update project version --- pyproject.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a9ccac2..88f28fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hexfrost-simplecrud" -version = "0.2.0b0" +version = "0.2.0" description = "Library with operation like (get, create, update, delete) for SQLAlchemy ORM" authors = ["Ilia Kaziamov "] packages = [ @@ -15,8 +15,6 @@ pytest-cov = "^4.1.0" [tool.poetry.group.dev.dependencies] flake8 = "^7.0.0" - -[tool.poetry.group.test.dependencies] pytest = "^7.4.3" aiosqlite = "^0.19.0" From e3078d5fe8b72f3110778cfe0177ea674e56d66d Mon Sep 17 00:00:00 2001 From: Ilia Kaziamov Date: Sat, 27 Jan 2024 19:07:56 +0300 Subject: [PATCH 29/33] Update python version --- .github/workflows/compatibility.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 5bfa1db..7c349a4 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: - python-version: [ 3.6, 3.7, 3.8, 3.9, "3.10", "3.11", "3.12" ] + python-version: [ 3.8, 3.9, "3.10", 3.11, 3.12 ] steps: - name: Checkout code diff --git a/pyproject.toml b/pyproject.toml index 88f28fc..86869ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ packages = [ readme = "README.md" [tool.poetry.dependencies] -python = "^3.6" +python = "^3.8" sqlalchemy = "^2.0.0" pytest-cov = "^4.1.0" From 9ebfa7db65123316b19ac9fd657629f9cbd874d6 Mon Sep 17 00:00:00 2001 From: Kaziamov Date: Sat, 2 Mar 2024 05:10:24 +0300 Subject: [PATCH 30/33] Remove problematic code --- poetry.lock | 6 ++--- pyproject.toml | 6 ++--- simplecrud/__init__.py | 1 - simplecrud/crud.py | 55 +++++++++++++++++------------------------- simplecrud/settings.py | 31 ------------------------ simplecrud/utils.py | 14 ----------- 6 files changed, 28 insertions(+), 85 deletions(-) delete mode 100644 simplecrud/settings.py diff --git a/poetry.lock b/poetry.lock index 25622b9..f2bda1c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand. [[package]] name = "aiosqlite" @@ -415,5 +415,5 @@ files = [ [metadata] lock-version = "2.0" -python-versions = "^3.9" -content-hash = "c32f9ab0e1ded0357a0faf33611af5df7b188022094f8ae70e2f99187886c1d8" +python-versions = ">=3.8.1,<4.0" +content-hash = "1334b9d6847a966bdace63596148a2350604f0e0c9a5bc5d2bd12821f21b2c34" diff --git a/pyproject.toml b/pyproject.toml index 86869ec..61dbc11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hexfrost-simplecrud" -version = "0.2.0" +version = "0.3.0" description = "Library with operation like (get, create, update, delete) for SQLAlchemy ORM" authors = ["Ilia Kaziamov "] packages = [ @@ -9,14 +9,14 @@ packages = [ readme = "README.md" [tool.poetry.dependencies] -python = "^3.8" +python = ">=3.8.1,<4.0" sqlalchemy = "^2.0.0" -pytest-cov = "^4.1.0" [tool.poetry.group.dev.dependencies] flake8 = "^7.0.0" pytest = "^7.4.3" aiosqlite = "^0.19.0" +pytest-cov = "^4.1.0" [build-system] requires = ["poetry-core"] diff --git a/simplecrud/__init__.py b/simplecrud/__init__.py index 58c1da5..977c5b0 100644 --- a/simplecrud/__init__.py +++ b/simplecrud/__init__.py @@ -1,2 +1 @@ from . import crud -from .settings import CRUDConfig diff --git a/simplecrud/crud.py b/simplecrud/crud.py index 31feb45..bc570bb 100644 --- a/simplecrud/crud.py +++ b/simplecrud/crud.py @@ -2,12 +2,12 @@ from typing import Dict, List from sqlalchemy import select, delete -from .utils import inject_connection +from sqlalchemy.ext.asyncio import AsyncSession # READ / GET -@inject_connection -async def get_object(model, filters, conn=None) -> object: + +async def get_object(model, filters: dict, conn: AsyncSession = None) -> object: """Get object from db""" query = select(model).filter_by(**filters) async with conn: @@ -16,8 +16,7 @@ async def get_object(model, filters, conn=None) -> object: return obj -@inject_connection -async def get_all(model, conn=None) -> List[object]: +async def get_all(model, conn: AsyncSession = None) -> List[object]: """Get objects from db""" query = select(model) async with conn: @@ -26,8 +25,7 @@ async def get_all(model, conn=None) -> List[object]: return objects -@inject_connection -async def get_all_with_filter(model, filters: dict, conn=None) -> List[object]: +async def get_all_with_filter(model, filters: dict, conn: AsyncSession = None) -> List[object]: """Get objects from db""" query = select(model).filter_by(**filters) async with conn: @@ -36,8 +34,7 @@ async def get_all_with_filter(model, filters: dict, conn=None) -> List[object]: return objects -@inject_connection -async def get_objects(model, filters: Dict, limit=10, offset=10, conn=None) -> List[object]: +async def get_objects(model, filters: Dict, limit: int = 10, offset: int = 10, conn: AsyncSession = None) -> List[object]: """Get objects from db""" query = select(model).filter_by(**filters).limit(limit).offset(offset) async with conn: @@ -46,8 +43,7 @@ async def get_objects(model, filters: Dict, limit=10, offset=10, conn=None) -> L return objects -@inject_connection -async def get_or_create_object(model, params, conn=None) -> object: +async def get_or_create_object(model, params: dict, conn: AsyncSession = None) -> object: """Get object from db or create new one""" obj = await get_object(model, params, conn=conn) if not obj: @@ -56,8 +52,8 @@ async def get_or_create_object(model, params, conn=None) -> object: # CREATE -@inject_connection -async def create_object(model, params, conn=None) -> object: + +async def create_object(model, params, conn: AsyncSession = None) -> object: """Create object in db""" new_obj = model(**params) async with conn: @@ -66,15 +62,14 @@ async def create_object(model, params, conn=None) -> object: return new_obj -@inject_connection -async def bulk_create(model, data: List[Dict], conn=None) -> List[object]: +async def bulk_create(model, data: List[Dict], conn: AsyncSession = None) -> List[object]: """Bulk create objects in db""" return [await create_object(model, params, conn=conn) for params in data] # UPDATE -@inject_connection -async def update_object(obj, params, conn=None) -> object: + +async def update_object(obj, params, conn: AsyncSession = None) -> object: """ Soft Update object in db. If attribute not exists in model`s fields, then skip field without error @@ -89,8 +84,7 @@ async def update_object(obj, params, conn=None) -> object: return obj -@inject_connection -async def update_or_error(obj, params, conn=None) -> object: +async def update_or_error(obj, params, conn: AsyncSession = None) -> object: """ Soft Update object in db. If attribute not exists in model`s fields, then skip field without error @@ -103,16 +97,15 @@ async def update_or_error(obj, params, conn=None) -> object: return obj -@inject_connection -async def update_object_by_id(model, id: int, params, conn=None) -> object: +async def update_object_by_id(model, id: int, params, conn: AsyncSession = None) -> object: """Update object in db by id""" obj = await get_object(model, dict(id=id)) updated_obj = await update_object(obj, params, conn=conn) return updated_obj -# @inject_connection -# async def bulk_update(objects, params, conn=None) -> List[object]: +# +# async def bulk_update(objects, params, conn: AsyncSession = None) -> List[object]: # """Bulk update objects in db""" # updated_objects = [] # for obj in objects: @@ -121,22 +114,20 @@ async def update_object_by_id(model, id: int, params, conn=None) -> object: # return updated_objects -@inject_connection -async def update_or_create_object(model, filters, params, conn=None) -> object: +async def update_or_create_object(model, filters, params, conn: AsyncSession = None) -> object: obj = await get_or_create_object(model, filters, conn=conn) return await update_object(obj, params, conn=conn) # DELETE -@inject_connection -async def delete_object(obj, conn=None) -> bool: + +async def delete_object(obj, conn: AsyncSession = None) -> bool: model = obj.__class__ id_ = obj.id return await delete_object_by_id(model, id_, conn=conn) -@inject_connection -async def delete_object_by_id(model, id_: int, conn=None) -> bool: +async def delete_object_by_id(model, id_: int, conn: AsyncSession = None) -> bool: query = delete(model).where(model.id == id_) async with conn: await conn.execute(query) @@ -144,15 +135,13 @@ async def delete_object_by_id(model, id_: int, conn=None) -> bool: return True -@inject_connection -async def bulk_delete(objects, conn=None) -> bool: +async def bulk_delete(objects, conn: AsyncSession = None) -> bool: for obj in objects: await delete_object(obj, conn=conn) return True -@inject_connection -async def bulk_delete_by_id(model, ids, conn=None) -> bool: +async def bulk_delete_by_id(model, ids, conn: AsyncSession = None) -> bool: for id_ in ids: await delete_object_by_id(model, id_, conn=conn) return True diff --git a/simplecrud/settings.py b/simplecrud/settings.py deleted file mode 100644 index 1d267a6..0000000 --- a/simplecrud/settings.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import annotations -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from sqlalchemy.orm import AsyncSession - - -class CRUDConfig: - """Singleton class for CRUD settings""" - _instance: CRUDConfig | None = None - - def __new__(cls): - if cls._instance is None: - cls._instance = super(CRUDConfig, cls).__new__(cls) - return cls._instance - - @property - def sessionmaker(self) -> AsyncSession: - if not self._sessionmaker: - raise ValueError("Sessionmaker is not set. Use set_sessionmaker() method") - return self._sessionmaker - - def set_sessionmaker(self, sessionmaker: AsyncSession) -> None: - """Set sessionmaker""" - self._sessionmaker = sessionmaker - - -def session() -> AsyncSession: - """Get session""" - config = CRUDConfig() - return config.sessionmaker() diff --git a/simplecrud/utils.py b/simplecrud/utils.py index dc94d86..52a016d 100644 --- a/simplecrud/utils.py +++ b/simplecrud/utils.py @@ -2,7 +2,6 @@ import logging from functools import wraps -from simplecrud.settings import session def async_to_sync(func): @@ -17,19 +16,6 @@ def wrapper(*args, **kwargs): return wrapper -def inject_connection(func): - """Decorator to inject database connection to function""" - - @wraps(func) - def inner(*args, **kwargs): - if kwargs.get('conn') is None: - kwargs['conn'] = session() - result = func(*args, **kwargs) - return result - - return inner - - def add_log(func): """Decorator to add log""" From df22d2514fa1b7c503305f18cd09c5db78e8199e Mon Sep 17 00:00:00 2001 From: Kaziamov Date: Sun, 3 Mar 2024 15:06:59 +0300 Subject: [PATCH 31/33] Fix bug and tests --- simplecrud/crud.py | 2 +- simplecrud/tests/factories.py | 6 +- simplecrud/tests/test_crud.py | 149 +++++++++++++++--------------- simplecrud/tests/test_settings.py | 37 -------- simplecrud/tests/test_utils.py | 19 ---- 5 files changed, 78 insertions(+), 135 deletions(-) delete mode 100644 simplecrud/tests/test_settings.py diff --git a/simplecrud/crud.py b/simplecrud/crud.py index bc570bb..59f294b 100644 --- a/simplecrud/crud.py +++ b/simplecrud/crud.py @@ -99,7 +99,7 @@ async def update_or_error(obj, params, conn: AsyncSession = None) -> object: async def update_object_by_id(model, id: int, params, conn: AsyncSession = None) -> object: """Update object in db by id""" - obj = await get_object(model, dict(id=id)) + obj = await get_object(model, dict(id=id), conn=conn) updated_obj = await update_object(obj, params, conn=conn) return updated_obj diff --git a/simplecrud/tests/factories.py b/simplecrud/tests/factories.py index 4c4c21a..34fbbb3 100644 --- a/simplecrud/tests/factories.py +++ b/simplecrud/tests/factories.py @@ -1,15 +1,11 @@ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession -from simplecrud.settings import CRUDConfig - class AsyncConnFactory(): def __init__(self): self.async_engine = create_async_engine("sqlite+aiosqlite:///./test.db") self.async_session_maker = async_sessionmaker(self.async_engine, expire_on_commit=False, class_=AsyncSession) - self.config = CRUDConfig() - self.config.set_sessionmaker(self.async_session_maker) def __call__(self, *args, **kwargs): - return self.config.sessionmaker + return self.async_session_maker() diff --git a/simplecrud/tests/test_crud.py b/simplecrud/tests/test_crud.py index 9bf3f90..403b138 100644 --- a/simplecrud/tests/test_crud.py +++ b/simplecrud/tests/test_crud.py @@ -3,10 +3,10 @@ from sqlalchemy import Column, Integer, String, create_engine from sqlalchemy.exc import InvalidRequestError +from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker from simplecrud.crud import * -from simplecrud.tests.factories import AsyncConnFactory from simplecrud.utils import async_to_sync database_url = "sqlite:///./test.db" @@ -14,6 +14,10 @@ session_maker = sessionmaker(bind=engine) +async_engine = create_async_engine("sqlite+aiosqlite:///./test.db") +AsyncSession = async_sessionmaker(async_engine, expire_on_commit=False, class_=AsyncSession) + + class Base(DeclarativeBase): pass @@ -29,7 +33,6 @@ class TestAsyncCRUDFunctions(unittest.TestCase): def setUp(self): Base.metadata.create_all(engine) - self.session = AsyncConnFactory() def tearDown(self): Base.metadata.drop_all(engine) @@ -37,26 +40,26 @@ def tearDown(self): @async_to_sync async def test_async_create_obj(self): params_1 = dict(name="test_async_create_obj1") - new_obj_1 = await create_object(ExampleModel, params_1) + new_obj_1 = await create_object(ExampleModel, params_1, conn=AsyncSession()) self.assertEqual(new_obj_1.name, "test_async_create_obj1") params_2 = dict(name="test_async_create_obj2") - new_obj_2 = await create_object(ExampleModel, params_2) + new_obj_2 = await create_object(ExampleModel, params_2, conn=AsyncSession()) self.assertEqual(new_obj_2.name, "test_async_create_obj2") @async_to_sync async def test_create_obj_params_error(self): params_1 = dict(name="test_create_obj_params_error", wrong="wrong") with self.assertRaises(TypeError): - new_obj_1 = await create_object(ExampleModel, **params_1) + new_obj_1 = await create_object(ExampleModel, **params_1, conn=AsyncSession()) @async_to_sync async def test_bulk_create(self): - all_ = await get_all(ExampleModel) + all_ = await get_all(ExampleModel, conn=AsyncSession()) self.assertEqual(len(all_), 0) data = [dict(name=f"test_bulk_create{i}") for i in range(1, 11)] - objects = await bulk_create(ExampleModel, data) - all_ = await get_all(ExampleModel) + objects = await bulk_create(ExampleModel, data, conn=AsyncSession()) + all_ = await get_all(ExampleModel, conn=AsyncSession()) self.assertEqual(len(all_), 10) for i in range(1, 11): self.assertEqual(all_[i - 1].name, f"test_bulk_create{i}") @@ -64,40 +67,40 @@ async def test_bulk_create(self): @async_to_sync async def test_get_object(self): params_1 = dict(name="test_get_object") - await create_object(ExampleModel, params_1) - obj = await get_object(ExampleModel, params_1) + await create_object(ExampleModel, params_1, conn=AsyncSession()) + obj = await get_object(ExampleModel, params_1, conn=AsyncSession()) self.assertEqual(obj.name, "test_get_object") @async_to_sync async def test_get_object_not_exist(self): params_1 = dict(name="test_get_object_not_exist1") - obj = await create_object(ExampleModel, params_1) - none_expected = await get_object(ExampleModel, filters=dict(name="test_get_object_not_exist0")) + obj = await create_object(ExampleModel, params_1, conn=AsyncSession()) + none_expected = await get_object(ExampleModel, filters=dict(name="test_get_object_not_exist0"), conn=AsyncSession()) self.assertEqual(none_expected, None) @async_to_sync async def test_get_object_error(self): params_1 = dict(name="test_get_object_error") - obj = await create_object(ExampleModel, params_1) + obj = await create_object(ExampleModel, params_1, conn=AsyncSession()) with self.assertRaises(InvalidRequestError): error_expected = await get_object(ExampleModel, filters=dict(wrong="wrong")) @async_to_sync async def test_get_all_objects(self): - all_ = await get_all(ExampleModel) + all_ = await get_all(ExampleModel, conn=AsyncSession()) self.assertEqual(len(all_), 0) for i in range(5): params_1 = dict(name=f"test_get_all_objects{i}") - await create_object(ExampleModel, params_1) - all_ = await get_all(ExampleModel) + await create_object(ExampleModel, params_1, conn=AsyncSession()) + all_ = await get_all(ExampleModel, conn=AsyncSession()) self.assertEqual(5, len(all_)) self.assertTrue(isinstance(all_, list)) - await delete_object(all_[0]) + await delete_object(all_[0], conn=AsyncSession()) @async_to_sync async def test_get_all_if_objects_not_exist(self): - all_ = await get_all(ExampleModel) + all_ = await get_all(ExampleModel, conn=AsyncSession()) self.assertEqual(len(all_), 0) self.assertTrue(isinstance(all_, list)) @@ -105,131 +108,131 @@ async def test_get_all_if_objects_not_exist(self): async def test_get_all_with_filter(self): for i in range(1, 6): params_1 = dict(name=f"test_get_all_with_filter{i}") - await create_object(ExampleModel, params_1) - all_ = await get_all_with_filter(ExampleModel, dict(name="test_get_all_with_filter1")) + await create_object(ExampleModel, params_1, conn=AsyncSession()) + all_ = await get_all_with_filter(ExampleModel, dict(name="test_get_all_with_filter1"), conn=AsyncSession()) self.assertEqual(len(all_), 1) # TODO: Add test for multiple filter parameters @async_to_sync async def test_get_all_with_filter_error(self): - await create_object(ExampleModel, dict(name="test_get_all_with_filter_negative")) + await create_object(ExampleModel, dict(name="test_get_all_with_filter_negative"), conn=AsyncSession()) with self.assertRaises(InvalidRequestError): - await get_all_with_filter(ExampleModel, dict(wrong="wrong")) + await get_all_with_filter(ExampleModel, dict(wrong="wrong"), conn=AsyncSession()) @async_to_sync async def test_get_all_with_filter_negative(self): for i in range(1, 6): params_1 = dict(name=f"test_get_all_with_filter{i}") - await create_object(ExampleModel, params_1) - all_ = await get_all_with_filter(ExampleModel, dict(name="not_exist")) + await create_object(ExampleModel, params_1, conn=AsyncSession()) + all_ = await get_all_with_filter(ExampleModel, dict(name="not_exist"), conn=AsyncSession()) self.assertEqual(len(all_), 0) @async_to_sync async def test_get_objects_with_limit_and_per_page(self): for i in range(1, 30): params_1 = dict(name=f"test_get_objects_with_limit_and_ofset{i}") - await create_object(ExampleModel, params_1) - ten = await get_objects(ExampleModel, {}, limit=10, offset=10) + await create_object(ExampleModel, params_1, conn=AsyncSession()) + ten = await get_objects(ExampleModel, {}, limit=10, offset=10, conn=AsyncSession()) self.assertEqual(len(ten), 10) - one = await get_objects(ExampleModel, {}, limit=1, offset=1) + one = await get_objects(ExampleModel, {}, limit=1, offset=1, conn=AsyncSession()) self.assertEqual(len(one), 1) - second = await get_objects(ExampleModel, {}, limit=1, offset=2) + second = await get_objects(ExampleModel, {}, limit=1, offset=2, conn=AsyncSession()) self.assertEqual(len(second), 1) self.assertEqual(second[0].id, 3) @async_to_sync async def test_get_object_by_filters(self): params_1 = dict(name="test_get_object_by_filters") - new_ = await create_object(ExampleModel, params_1) - obj = await get_object(ExampleModel, filters=dict(id=new_.id)) + new_ = await create_object(ExampleModel, params_1, conn=AsyncSession()) + obj = await get_object(ExampleModel, filters=dict(id=new_.id), conn=AsyncSession()) self.assertEqual(obj.name, "test_get_object_by_filters") @async_to_sync async def test_get_object_by_filters_negative(self): params_1 = dict(name="test_get_object_by_filters_negative") - new_ = await create_object(ExampleModel, params_1) + new_ = await create_object(ExampleModel, params_1, conn=AsyncSession()) with self.assertRaises(InvalidRequestError): obj = await get_object(ExampleModel, filters=dict(pk=new_.id)) @async_to_sync async def test_get_or_create_object(self): - self.assertEqual(len(await get_all(ExampleModel)), 0) + self.assertEqual(len(await get_all(ExampleModel, conn=AsyncSession())), 0) params_1 = dict(name="test_get_or_create_object") - new_1 = await get_or_create_object(ExampleModel, params_1) - self.assertEqual(len(await get_all(ExampleModel)), 1) - new_2 = await get_or_create_object(ExampleModel, params_1) - self.assertEqual(len(await get_all(ExampleModel)), 1) + new_1 = await get_or_create_object(ExampleModel, params_1, conn=AsyncSession()) + self.assertEqual(len(await get_all(ExampleModel, conn=AsyncSession())), 1) + new_2 = await get_or_create_object(ExampleModel, params_1, conn=AsyncSession()) + self.assertEqual(len(await get_all(ExampleModel, conn=AsyncSession())), 1) self.assertEqual(new_1.id, new_2.id) @async_to_sync async def test_update_object(self): params_1 = dict(name="test_update_object") - obj1 = await create_object(ExampleModel, params_1) + obj1 = await create_object(ExampleModel, params_1, conn=AsyncSession()) params_2 = dict(name="test_update_object2") - obj2 = await update_object(obj1, params_2) + obj2 = await update_object(obj1, params_2, conn=AsyncSession()) self.assertEqual(obj2.name, "test_update_object2") self.assertEqual(obj1.id, obj2.id) @async_to_sync async def test_update_or_error(self): - obj = await create_object(ExampleModel, dict(name="test_update_or_error")) - await update_or_error(obj, dict(name="test_update_or_error_updated")) - upd_obj = await get_object(ExampleModel, dict(id=obj.id)) + obj = await create_object(ExampleModel, dict(name="test_update_or_error"), conn=AsyncSession()) + await update_or_error(obj, dict(name="test_update_or_error_updated"), conn=AsyncSession()) + upd_obj = await get_object(ExampleModel, dict(id=obj.id), conn=AsyncSession()) self.assertEqual(upd_obj.id, obj.id) self.assertEqual(upd_obj.name, "test_update_or_error_updated") @async_to_sync async def test_update_or_error_error(self): - obj = await create_object(ExampleModel, dict(name="test_update_or_error")) + obj = await create_object(ExampleModel, dict(name="test_update_or_error"), conn=AsyncSession()) with self.assertRaises(AttributeError): - await update_or_error(obj, dict(wrong="wrong")) + await update_or_error(obj, dict(wrong="wrong"), conn=AsyncSession()) @async_to_sync async def test_update_or_error_negative(self): params_1 = dict(name="test_update_object") - obj1 = await create_object(ExampleModel, params_1) + obj1 = await create_object(ExampleModel, params_1, conn=AsyncSession()) wrong_params = dict(wrong="test_update_object2") with self.assertRaises(AttributeError) as error: - obj2 = await update_or_error(obj1, wrong_params) + obj2 = await update_or_error(obj1, wrong_params, conn=AsyncSession()) error_msg = "Attribute wrong not exists in ExampleModel" self.assertEqual(error.exception.args[0], error_msg) @async_to_sync async def test_soft_update_without_error(self): params_1 = dict(name="test_update_object_negative") - obj1 = await create_object(ExampleModel, params_1) + obj1 = await create_object(ExampleModel, params_1, conn=AsyncSession()) self.assertFalse(hasattr(obj1, "wrong")) wrong_params = dict(wrong="wrong") - obj2 = await update_object(obj1, wrong_params) + obj2 = await update_object(obj1, wrong_params, conn=AsyncSession()) self.assertEqual(obj1.id, obj2.id) self.assertFalse(hasattr(obj2, "wrong")) @async_to_sync async def test_update_or_error(self): params_1 = dict(name="test1") - obj1 = await create_object(ExampleModel, params_1) + obj1 = await create_object(ExampleModel, params_1, conn=AsyncSession()) params_2 = dict(name="test2") - obj2 = await update_or_error(obj1, params_2) + obj2 = await update_or_error(obj1, params_2, conn=AsyncSession()) self.assertEqual(obj2.name, "test2") @async_to_sync async def test_update_by_id(self): params_1 = dict(name="test1") - obj1 = await create_object(ExampleModel, params_1) + obj1 = await create_object(ExampleModel, params_1, conn=AsyncSession()) id_ = obj1.id params_2 = dict(name="test2") - obj2 = await update_object_by_id(ExampleModel, id_, params_2) + obj2 = await update_object_by_id(ExampleModel, id_, params_2, conn=AsyncSession()) self.assertEqual(obj2.name, "test2") @async_to_sync async def test_update_or_create_object(self): - self.assertEqual(len(await get_all(ExampleModel)), 0) + self.assertEqual(len(await get_all(ExampleModel, conn=AsyncSession())), 0) params_1 = dict(name="test_update_or_create_object1") - new_1 = await update_or_create_object(ExampleModel, params_1, params_1) - self.assertEqual(len(await get_all(ExampleModel)), 1) + new_1 = await update_or_create_object(ExampleModel, params_1, params_1, conn=AsyncSession()) + self.assertEqual(len(await get_all(ExampleModel, conn=AsyncSession())), 1) params_2 = dict(name="test_update_or_create_object2") - new_2 = await update_or_create_object(ExampleModel, params_1, params_2) + new_2 = await update_or_create_object(ExampleModel, params_1, params_2, conn=AsyncSession()) self.assertEqual(new_2.name, "test_update_or_create_object2") self.assertEqual(new_1.id, new_2.id) @@ -252,26 +255,26 @@ async def test_update_or_create_object(self): @async_to_sync async def test_delete_object(self): - all_ = await get_all(ExampleModel) + all_ = await get_all(ExampleModel, conn=AsyncSession()) self.assertEqual(len(all_), 0) params_1 = dict(name="test_delete_object") - new_1 = await create_object(ExampleModel, params_1) - all_ = await get_all(ExampleModel) + new_1 = await create_object(ExampleModel, params_1, conn=AsyncSession()) + all_ = await get_all(ExampleModel, conn=AsyncSession()) self.assertEqual(len(all_), 1) - result = await delete_object(new_1) + result = await delete_object(new_1, conn=AsyncSession()) self.assertEqual(result, True) - all_ = await get_all(ExampleModel) - get_ = await get_object(ExampleModel, filters=dict(id=new_1.id)) + all_ = await get_all(ExampleModel, conn=AsyncSession()) + get_ = await get_object(ExampleModel, filters=dict(id=new_1.id), conn=AsyncSession()) self.assertEqual(len(all_), 0) @async_to_sync async def test_delete_objects(self): for i in range(1, 12): params_1 = dict(name=f"test_delete_objects{i}") - await create_object(ExampleModel, params_1) - all_ = await get_all(ExampleModel) - result = await bulk_delete(all_[0:10]) - all_ = await get_all(ExampleModel) + await create_object(ExampleModel, params_1, conn=AsyncSession()) + all_ = await get_all(ExampleModel, conn=AsyncSession()) + result = await bulk_delete(all_[0:10], conn=AsyncSession()) + all_ = await get_all(ExampleModel, conn=AsyncSession()) self.assertEqual(1, len(all_)) self.assertEqual(all_[0].name, "test_delete_objects11") self.assertEqual(all_[0].id, 11) @@ -280,20 +283,20 @@ async def test_delete_objects(self): async def test_bulk_delete(self): for i in range(1, 12): params_1 = dict(name=f"test_delete_objects{i}") - await create_object(ExampleModel, params_1) - objects = await get_all(ExampleModel) + await create_object(ExampleModel, params_1, conn=AsyncSession()) + objects = await get_all(ExampleModel, conn=AsyncSession()) self.assertEqual(11, len(objects)) - await bulk_delete(objects) - all_ = await get_all(ExampleModel) + await bulk_delete(objects, conn=AsyncSession()) + all_ = await get_all(ExampleModel, conn=AsyncSession()) self.assertEqual(0, len(all_)) @async_to_sync async def test_bulk_delete_by_id(self): for i in range(1, 12): params_1 = dict(name=f"test_delete_objects{i}") - await create_object(ExampleModel, params_1) - ids = [i.id for i in await get_all(ExampleModel)] + await create_object(ExampleModel, params_1, conn=AsyncSession()) + ids = [i.id for i in await get_all(ExampleModel, conn=AsyncSession())] self.assertEqual(11, len(ids)) - await bulk_delete_by_id(ExampleModel, ids) - all_ = await get_all(ExampleModel) + await bulk_delete_by_id(ExampleModel, ids, conn=AsyncSession()) + all_ = await get_all(ExampleModel, conn=AsyncSession()) self.assertEqual(0, len(all_)) diff --git a/simplecrud/tests/test_settings.py b/simplecrud/tests/test_settings.py deleted file mode 100644 index b278cce..0000000 --- a/simplecrud/tests/test_settings.py +++ /dev/null @@ -1,37 +0,0 @@ -import unittest - -from simplecrud.settings import CRUDConfig - - -class TestCRUDConfig(unittest.TestCase): - - def setUp(self): - CRUDConfig._instance = None - - def test_singleton(self): - config1 = CRUDConfig() - config2 = CRUDConfig() - self.assertEqual(config1, config2) - - def test_set_sessionmaker(self): - config = CRUDConfig() - config.set_sessionmaker("test") - self.assertEqual(config.sessionmaker, "test") - - def test_sessionmaker_saved_between_creation(self): - config = CRUDConfig() - config.set_sessionmaker("test") - self.assertEqual(config.sessionmaker, "test") - config = CRUDConfig() - self.assertEqual(config.sessionmaker, "test") - - def test_sessionmaker_value_error(self): - config = CRUDConfig() - config._sessionmaker = None - with self.assertRaises(ValueError): - config.sessionmaker - - def test_sessionmaker_not_set(self): - config = CRUDConfig() - with self.assertRaises(AttributeError): - config.sessionmaker diff --git a/simplecrud/tests/test_utils.py b/simplecrud/tests/test_utils.py index 1978728..74fd2ab 100644 --- a/simplecrud/tests/test_utils.py +++ b/simplecrud/tests/test_utils.py @@ -3,27 +3,8 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, Session -from simplecrud import CRUDConfig from simplecrud.tests.test_crud import engine, Base -from simplecrud.utils import inject_connection database_url = "sqlite:///./test.db" engine = create_engine(database_url) session_maker = sessionmaker(bind=engine) - - -class TestAsyncCRUDFunctions(unittest.TestCase): - - def setUp(self): - Base.metadata.create_all(engine) - CRUDConfig().set_sessionmaker(session_maker) - - def tearDown(self): - Base.metadata.drop_all(engine) - - def test_inject_connection(self): - def example_func(conn=None): - return conn - - conn = inject_connection(example_func)() - self.assertIsInstance(conn, Session) From e7577eadaf73793ddfe25aae809c93ee3bb0d5a2 Mon Sep 17 00:00:00 2001 From: Kaziamov Date: Sun, 3 Mar 2024 20:40:15 +0300 Subject: [PATCH 32/33] Update max-length settings and reformat code --- setup.cfg | 2 +- simplecrud/crud.py | 9 +++++++-- simplecrud/utils.py | 1 - 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 391fbcd..635e630 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [flake8] extend-ignore = F401 -max-line-length = 95 +max-line-length = 120 exclude = .git, */tests/*, diff --git a/simplecrud/crud.py b/simplecrud/crud.py index 59f294b..fa29295 100644 --- a/simplecrud/crud.py +++ b/simplecrud/crud.py @@ -7,6 +7,7 @@ # READ / GET + async def get_object(model, filters: dict, conn: AsyncSession = None) -> object: """Get object from db""" query = select(model).filter_by(**filters) @@ -34,7 +35,9 @@ async def get_all_with_filter(model, filters: dict, conn: AsyncSession = None) - return objects -async def get_objects(model, filters: Dict, limit: int = 10, offset: int = 10, conn: AsyncSession = None) -> List[object]: +async def get_objects( + model, filters: Dict, limit: int = 10, offset: int = 10, conn: AsyncSession = None +) -> List[object]: """Get objects from db""" query = select(model).filter_by(**filters).limit(limit).offset(offset) async with conn: @@ -53,6 +56,7 @@ async def get_or_create_object(model, params: dict, conn: AsyncSession = None) - # CREATE + async def create_object(model, params, conn: AsyncSession = None) -> object: """Create object in db""" new_obj = model(**params) @@ -69,6 +73,7 @@ async def bulk_create(model, data: List[Dict], conn: AsyncSession = None) -> Lis # UPDATE + async def update_object(obj, params, conn: AsyncSession = None) -> object: """ Soft Update object in db. @@ -104,7 +109,6 @@ async def update_object_by_id(model, id: int, params, conn: AsyncSession = None) return updated_obj -# # async def bulk_update(objects, params, conn: AsyncSession = None) -> List[object]: # """Bulk update objects in db""" # updated_objects = [] @@ -121,6 +125,7 @@ async def update_or_create_object(model, filters, params, conn: AsyncSession = N # DELETE + async def delete_object(obj, conn: AsyncSession = None) -> bool: model = obj.__class__ id_ = obj.id diff --git a/simplecrud/utils.py b/simplecrud/utils.py index 52a016d..8350115 100644 --- a/simplecrud/utils.py +++ b/simplecrud/utils.py @@ -3,7 +3,6 @@ from functools import wraps - def async_to_sync(func): """Decorator to convert async function to sync""" From a9025efc5e97effaddeca6209696b843d049f6a5 Mon Sep 17 00:00:00 2001 From: Kaziamov Date: Sun, 3 Mar 2024 20:49:14 +0300 Subject: [PATCH 33/33] Update README --- README.md | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 7694996..f2f5912 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Test Coverage](https://api.codeclimate.com/v1/badges/d33ecb2661fb7aedf516/test_coverage)](https://codeclimate.com/github/hexfrost/sqlalchemy-models-commands/test_coverage) [![flake8](https://github.com/hexfrost/simplecrud/actions/workflows/linter.yml/badge.svg?branch=staging)](https://github.com/hexfrost/simplecrud/actions/workflows/linter.yml) + # SimpleCRUD SimpleCRUD is a library that provides a simple way to create CRUD commands for SQLAlchemy models. @@ -21,8 +22,7 @@ poetry add hexfrost-simplecrud ## Usage 1. Create a model -2. Create a CRUDConfig -3. Set sessionmaker to CRUDConfig +2. Create a sessionmaker from SQLAlchemy 4. Import CRUD functions 5. Use CRUD functions and enjoy @@ -32,10 +32,10 @@ poetry add hexfrost-simplecrud from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from sqlalchemy.orm import DeclarativeBase -from simplecrud import CRUDConfig, BaseModelWithCRUD, get_all, create_obj, update_obj +from simplecrud import get_all, create_obj, update_obj, delete_object engine = create_async_engine("sqlite+aiosqlite:///test.db", echo=True) -async_sessionmaker = async_sessionmaker(async_engine, expire_on_commit=False, class_=AsyncSession) +session = async_sessionmaker(async_engine, expire_on_commit=False, class_=AsyncSession) # Create a model class ExampleModel(DeclarativeBase): @@ -45,22 +45,19 @@ class ExampleModel(DeclarativeBase): description = Column(String(50), nullable=False) -# Create CRUD config -CRUDConfig.sessionmaker(async_sessionmaker) - async def example_func(): # Create a model - new_model = await create_object(model, name="test", description="test") + new_model = await create_object(model, name="test", description="test", conn=session()) # Get all models - all_objs = await get_all(model) + all_objs = await get_all(model, conn=session()) # Update a model - updated_obj = await update_object(model, name="test2", description="test2") + updated_obj = await update_object(model, name="test2", description="test2", conn=session()) # Delete a model - await delete_object(model, name="test2", description="test2") + await delete_object(model, name="test2", description="test2", conn=session()) ``` @@ -87,10 +84,6 @@ async def example_func(): This project is open for contributions. Feel free to open an issue or create a pull request. -Dev version status: -[![flake8](https://github.com/hexfrost/simplecrud/actions/workflows/linter.yml/badge.svg?branch=dev)](https://github.com/hexfrost/simplecrud/actions/workflows/linter.yml) -[![Coverage](https://github.com/hexfrost/simplecrud/actions/workflows/coverage.yml/badge.svg?branch=dev)](https://github.com/hexfrost/simplecrud/actions/workflows/coverage.yml) - *** ## License