diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f9410b18..d62e0e71 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,7 +60,6 @@ For example: - `tox -e coverage` - unit test coverage report. - `tox -e mypy` - runs mypy static type checker on the source. - `tox -e pyright` - runs pyright static type checker on the source. -- `tox -e refurb` - runs the [refurb](https://github.com/dosisod/refurb) tool over the source. - `tox -e integration` - runs the dockerized integration test suite. - `tox` - run everything, you maniac! diff --git a/README.md b/README.md index 1fcdc227..ace04af1 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,30 @@ Configuration for a [Starlite](https://github.com/starlite-api/starlite) applica - SAQ async worker - Lots of features! +## Installation + +This will install `starlite-saqlalchemy` with minimal dependencies. + +```console +poetry add starlite-saqlalchemy +``` + +You can also install additional dependencies depending on the features you need: + +```console +# Repository implementation, DTOs +poetry add starlite-saqlalchemy[sqlalchemy] +# Async worker using saq +poetry add starlite-saqlalchemy[worker] +# Redis cache backend +poetry add starlite-saqlalchemy[cache] +# Sentry integration for starlite +poetry add starlite-saqlalchemy[sentry] + +# or to install them all: +poetry add starlite-saqlalchemy[all] +``` + ## Example ```python diff --git a/mypy.ini b/mypy.ini index d2d74a99..4080fb50 100644 --- a/mypy.ini +++ b/mypy.ini @@ -18,9 +18,9 @@ warn_unused_ignores = True [mypy-tests.*] disallow_untyped_decorators = False -[mypy-tests.unit.test_dto] +[mypy-tests.unit.sqlalchemy.test_dto] # for the declarative base fixture disable_error_code = valid-type,misc -[mypy-tests.unit.repository.test_sqlalchemy] +[mypy-tests.unit.sqlalchemy.repository.test_sqlalchemy] disable_error_code = attr-defined diff --git a/poetry.lock b/poetry.lock index ae8e4410..9a88e3a5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -26,7 +26,7 @@ name = "async-timeout" version = "4.0.2" description = "Timeout context manager for asyncio programs" category = "main" -optional = false +optional = true python-versions = ">=3.6" files = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, @@ -128,7 +128,7 @@ name = "croniter" version = "1.3.8" description = "croniter provides iteration for datetime object with cron like format" category = "main" -optional = false +optional = true python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "croniter-1.3.8-py2.py3-none-any.whl", hash = "sha256:d6ed8386d5f4bbb29419dc1b65c4909c04a2322bd15ec0dc5b2877bfa1b75c7a"}, @@ -177,14 +177,14 @@ idna = ">=2.0.0" [[package]] name = "faker" -version = "16.4.0" +version = "16.6.0" description = "Faker is a Python package that generates fake data for you." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Faker-16.4.0-py3-none-any.whl", hash = "sha256:5420467fad3fa582094057754e5e81326cb1f51ab822bf9df96c077cfb35ae49"}, - {file = "Faker-16.4.0.tar.gz", hash = "sha256:dcffdca8ec9a715982bcd5f53ee688dc4784cd112f9910f8f7183773eb3ec276"}, + {file = "Faker-16.6.0-py3-none-any.whl", hash = "sha256:0a74514d654db0a3d37b9ca681f2d9182d2ec556f78b4f1a842a2ccc944660cd"}, + {file = "Faker-16.6.0.tar.gz", hash = "sha256:dc8b2a8bf0d852d26eacf7763afd5e7d6e9e50d80ec648b51b8ecd3c505435fd"}, ] [package.dependencies] @@ -216,15 +216,12 @@ files = [ {file = "fast_query_parsers-0.3.0.tar.gz", hash = "sha256:df972c0b58d0bf51fa43b67d2604ab795984015d47552d02175ebcc685e4852b"}, ] -[package.dependencies] -maturin = "*" - [[package]] name = "greenlet" version = "2.0.1" description = "Lightweight in-process concurrent programming" category = "main" -optional = false +optional = true python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" files = [ {file = "greenlet-2.0.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:9ed358312e63bf683b9ef22c8e442ef6c5c02973f0c2a939ec1d7b50c974015c"}, @@ -310,7 +307,7 @@ name = "hiredis" version = "2.1.1" description = "Python wrapper for hiredis" category = "main" -optional = false +optional = true python-versions = ">=3.7" files = [ {file = "hiredis-2.1.1-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:f15e48545dadf3760220821d2f3c850e0c67bbc66aad2776c9d716e6216b5103"}, @@ -502,84 +499,64 @@ testing = ["pytest"] [[package]] name = "markupsafe" -version = "2.1.1" +version = "2.1.2" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, ] -[[package]] -name = "maturin" -version = "0.14.10" -description = "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "maturin-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:ec8269c02cc435893308dfd50f57f14fb1be3554e4e61c5bf49b97363b289775"}, - {file = "maturin-0.14.10-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:e9c19dc0a28109280f7d091ca7b78e25f3fc340fcfac92801829a21198fa20eb"}, - {file = "maturin-0.14.10-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:cf950ebfe449a97617b91d75e09766509e21a389ce3f7b6ef15130ad8a95430a"}, - {file = "maturin-0.14.10-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:c0d25e82cb6e5de9f1c028fcf069784be4165b083e79412371edce05010b68f3"}, - {file = "maturin-0.14.10-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:9da98bee0a548ecaaa924cc8cb94e49075d5e71511c62a1633a6962c7831a29b"}, - {file = "maturin-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2f097a63f3bed20a7da56fc7ce4d44ef8376ee9870604da16b685f2d02c87c79"}, - {file = "maturin-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:4946ad7545ba5fc0ad08bc98bc8e9f6ffabb6ded71db9ed282ad4596b998d42a"}, - {file = "maturin-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:98bfed21c3498857b3381efeb041d77e004a93b22261bf9690fe2b9fbb4c210f"}, - {file = "maturin-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b157e2e8a0216d02df1d0451201fcb977baf0dcd223890abfbfbfd01e0b44630"}, - {file = "maturin-0.14.10-py3-none-win32.whl", hash = "sha256:5abf311d4618b673efa30cacdac5ae2d462e49da58db9a5bf0d8bde16d9c16be"}, - {file = "maturin-0.14.10-py3-none-win_amd64.whl", hash = "sha256:11b8550ceba5b81465a18d06f0d3a4cfc1cd6cbf68eda117c253bbf3324b1264"}, - {file = "maturin-0.14.10-py3-none-win_arm64.whl", hash = "sha256:6cc9afb89f28bd591b62f8f3c29736c81c322cffe88f9ab8eb1749377bbc3521"}, - {file = "maturin-0.14.10.tar.gz", hash = "sha256:895c48cbe56ae994c2a1eeeef19475ca4819aa4c6412af727a63a772e8ef2d87"}, -] - -[package.dependencies] -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} - -[package.extras] -patchelf = ["patchelf"] -zig = ["ziglang (>=0.10.0,<0.11.0)"] - [[package]] name = "msgspec" version = "0.12.0" @@ -708,7 +685,7 @@ name = "packaging" version = "23.0" description = "Core utilities for Python packages" category = "main" -optional = false +optional = true python-versions = ">=3.7" files = [ {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, @@ -787,14 +764,14 @@ typing-extensions = "*" [[package]] name = "pydantic-openapi-schema" -version = "1.5.0" +version = "1.5.1" description = "OpenAPI Schema using pydantic. Forked for Starlite-API from 'openapi-schema-pydantic'." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_openapi_schema-1.5.0-py3-none-any.whl", hash = "sha256:0b60d105a2665287fbf8ee85602919a3558e07cf34261e64fa8f4d2281a3bad0"}, - {file = "pydantic_openapi_schema-1.5.0.tar.gz", hash = "sha256:50bf6ee00fc0dcd1c87b96f30c3273e3bebcc12f23f30c31e5af5ec2e3275c87"}, + {file = "pydantic_openapi_schema-1.5.1-py3-none-any.whl", hash = "sha256:fd5b1bff81ff70faa87fec62bd8193ccd671f31bc15b32a4557623d3db0e5eae"}, + {file = "pydantic_openapi_schema-1.5.1.tar.gz", hash = "sha256:d9b56235f4c4817c6e3693c4f8122c3e5e9c75f6b3b454524ad985012a264daf"}, ] [package.dependencies] @@ -886,7 +863,7 @@ name = "redis" version = "4.3.5" description = "Python client for Redis database and key-value store" category = "main" -optional = false +optional = true python-versions = ">=3.6" files = [ {file = "redis-4.3.5-py3-none-any.whl", hash = "sha256:46652271dc7525cd5a9667e5b0ca983c848c75b2b8f7425403395bb8379dcf25"}, @@ -921,14 +898,14 @@ idna2008 = ["idna"] [[package]] name = "saq" -version = "0.9.2" +version = "0.9.3" description = "Distributed Python job queue with asyncio and redis" category = "main" -optional = false +optional = true python-versions = "*" files = [ - {file = "saq-0.9.2-py3-none-any.whl", hash = "sha256:9ae0636f8ffe92fa5a9ee68a92828a562d5f0f241762ddf9744df694e33a4ab5"}, - {file = "saq-0.9.2.tar.gz", hash = "sha256:d0ad2994e7ae11337dc6481e7065399aa02f3d8923b1d9fdde1e02697de8d976"}, + {file = "saq-0.9.3-py3-none-any.whl", hash = "sha256:a5d3bcf58297d94daecf02b4cc37195fe2a8bca5a8fd7bd5bde50511d2bdc475"}, + {file = "saq-0.9.3.tar.gz", hash = "sha256:bea2a1437c17ea64d956da2edb77381b8818bb0d7379de501937290ede52171f"}, ] [package.dependencies] @@ -945,7 +922,7 @@ name = "sentry-sdk" version = "1.13.0" description = "Python client for Sentry (https://sentry.io)" category = "main" -optional = false +optional = true python-versions = "*" files = [ {file = "sentry-sdk-1.13.0.tar.gz", hash = "sha256:72da0766c3069a3941eadbdfa0996f83f5a33e55902a19ba399557cfee1dddcc"}, @@ -1008,7 +985,7 @@ name = "sqlalchemy" version = "2.0.0rc2" description = "Database Abstraction Library" category = "main" -optional = false +optional = true python-versions = ">=3.7" files = [ {file = "SQLAlchemy-2.0.0rc2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:19486279fe24297bf0743c1563735e7cab1f439f36acf165bd8e1be699fb3fcb"}, @@ -1153,18 +1130,6 @@ files = [ [package.extras] doc = ["reno", "sphinx", "tornado (>=4.5)"] -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - [[package]] name = "typing-extensions" version = "4.4.0" @@ -1182,7 +1147,7 @@ name = "urllib3" version = "1.26.14" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" -optional = false +optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, @@ -1258,7 +1223,14 @@ dev = ["Cython (>=0.29.32,<0.30.0)", "Sphinx (>=4.1.2,<4.2.0)", "aiohttp", "flak docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] test = ["Cython (>=0.29.32,<0.30.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=22.0.0,<22.1.0)", "pycodestyle (>=2.7.0,<2.8.0)"] +[extras] +all = ["redis", "hiredis", "saq", "sentry-sdk", "sqlalchemy"] +cache = ["redis", "hiredis"] +sentry = ["sentry-sdk"] +sqlalchemy = ["sqlalchemy"] +worker = ["saq", "hiredis"] + [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "02b7b504018c2c2e6633f8028b22dc996e837ba7f1b2a6ae6b879382b9c9d2d0" +content-hash = "ba44949147c159b66058c6bce3c3ad8753dec183e7ac82a7f9cd8fa55b9e4fd3" diff --git a/pyproject.toml b/pyproject.toml index ca82afc5..bba0ce2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,20 +58,29 @@ packages = [ [tool.poetry.dependencies] python = "^3.10" asyncpg = "*" -hiredis = "*" httpx = "*" msgspec = "*" pydantic = "*" python-dotenv = "*" -redis = "*" -saq = "^0.9.1" -sentry-sdk = ">=1.13.0" -sqlalchemy = "==2.0.0rc2" starlite = "^1.40.1" -structlog = ">=22.2.0" tenacity = "*" uvicorn = "*" uvloop = "*" +structlog = ">=22.2.0" + +# Optionals +hiredis = { version = "*", optional = true } +redis = { version = "*", optional = true } +saq = { version = "^0.9.1", optional = true } +sentry-sdk = { version = "*", optional = true } +sqlalchemy = { version = "==2.0.0rc2", optional = true } + +[tool.poetry.extras] +cache = ["redis", "hiredis"] +worker = ["saq", "hiredis"] +sentry = ["sentry-sdk"] +sqlalchemy = ["sqlalchemy"] +all = ["redis", "hiredis", "saq", "sentry-sdk", "sqlalchemy"] [tool.poetry.plugins."pytest11"] pytest_starlite_saqlalchemy = "pytest_starlite_saqlalchemy" @@ -92,11 +101,18 @@ add-select = "D401,D404,D417" convention = "google" [tool.pytest.ini_options] -addopts = ["-ra", "--strict-config"] +addopts = [ + "-ra", + "--strict-config", + # Plugin are enabled in tests/conftest.py to control loading order. + "-p", + "no:pytest_starlite_saqlalchemy", + "-p", + "no:pytest_dotenv", +] asyncio_mode = "auto" env_files = ["tests.env"] testpaths = ["tests/unit"] -test_app = "tests.utils.app:create_app" [tool.pylint.main] disable = [ diff --git a/requirements.dev-extras.txt b/requirements.dev-extras.txt new file mode 100644 index 00000000..cbb2ae96 --- /dev/null +++ b/requirements.dev-extras.txt @@ -0,0 +1,7 @@ +-r requirements.dev.txt + +sentry-sdk >= "1.13.0" +hiredis +redis +saq >= "0.9.1" +sqlalchemy == 2.0.0rc2 diff --git a/src/pytest_starlite_saqlalchemy/plugin.py b/src/pytest_starlite_saqlalchemy/plugin.py index 048351c2..91e7f205 100644 --- a/src/pytest_starlite_saqlalchemy/plugin.py +++ b/src/pytest_starlite_saqlalchemy/plugin.py @@ -2,6 +2,7 @@ # pylint: disable=import-outside-toplevel from __future__ import annotations +import os import re from typing import TYPE_CHECKING from unittest.mock import MagicMock @@ -12,6 +13,8 @@ from structlog.testing import CapturingLogger from uvicorn.importer import ImportFromStringError, import_from_string +from starlite_saqlalchemy import constants + if TYPE_CHECKING: from collections.abc import Generator @@ -35,7 +38,7 @@ def pytest_addoption(parser: Parser) -> None: "test_app", "Path to application instance, or callable that returns an application instance.", type="string", - default="app.main:create_app", + default=os.environ.get("TEST_APP", "app.main:create_app"), ) parser.addini( "unit_test_pattern", @@ -65,7 +68,7 @@ def _patch_http_close(monkeypatch: MonkeyPatch) -> None: monkeypatch.setattr(starlite_saqlalchemy.http, "clients", set()) -@pytest.fixture(autouse=True) +@pytest.fixture(autouse=constants.IS_SQLALCHEMY_INSTALLED) def _patch_sqlalchemy_plugin(is_unit_test: bool, monkeypatch: MonkeyPatch) -> None: if is_unit_test: from starlite_saqlalchemy import sqlalchemy_plugin @@ -77,7 +80,7 @@ def _patch_sqlalchemy_plugin(is_unit_test: bool, monkeypatch: MonkeyPatch) -> No ) -@pytest.fixture(autouse=True) +@pytest.fixture(autouse=constants.IS_SAQ_INSTALLED) def _patch_worker(is_unit_test: bool, monkeypatch: MonkeyPatch) -> None: """We don't want the worker to start for unittests.""" if is_unit_test: @@ -94,7 +97,6 @@ def fx_app(pytestconfig: Config, monkeypatch: MonkeyPatch) -> Starlite: An application instance, configured via plugin. """ test_app_str = pytestconfig.getini("test_app") - try: app_or_callable = import_from_string(test_app_str) except (ImportFromStringError, ModuleNotFoundError): diff --git a/src/starlite_saqlalchemy/__init__.py b/src/starlite_saqlalchemy/__init__.py index 46f0cfc8..c8032102 100644 --- a/src/starlite_saqlalchemy/__init__.py +++ b/src/starlite_saqlalchemy/__init__.py @@ -22,48 +22,35 @@ def example_handler() -> dict: # this is because pycharm wigs out when there is a module called `exceptions`: # noinspection PyCompatibility from . import ( - cache, compression, - db, dependencies, - dto, exceptions, health, http, log, openapi, - redis, repository, - sentry, service, settings, - sqlalchemy_plugin, type_encoders, - worker, ) from .init_plugin import ConfigureApp, PluginConfig __all__ = [ "ConfigureApp", "PluginConfig", - "cache", "compression", - "db", "dependencies", - "dto", "exceptions", "health", "http", "log", "openapi", - "redis", "repository", - "sentry", "service", "settings", - "sqlalchemy_plugin", "type_encoders", - "worker", ] + __version__ = "0.28.1" diff --git a/src/starlite_saqlalchemy/constants.py b/src/starlite_saqlalchemy/constants.py index 8a5e75d3..f3e91e3b 100644 --- a/src/starlite_saqlalchemy/constants.py +++ b/src/starlite_saqlalchemy/constants.py @@ -1,11 +1,50 @@ """Application constants.""" from __future__ import annotations +from importlib import import_module +from typing import TYPE_CHECKING, Any + from starlite_saqlalchemy.settings import app from starlite_saqlalchemy.utils import case_insensitive_string_compare +if TYPE_CHECKING: + from collections.abc import MutableMapping + + from starlite_saqlalchemy.service import Service + + IS_TEST_ENVIRONMENT = case_insensitive_string_compare(app.ENVIRONMENT, app.TEST_ENVIRONMENT_NAME) """Flag indicating if the application is running in a test environment.""" IS_LOCAL_ENVIRONMENT = case_insensitive_string_compare(app.ENVIRONMENT, app.LOCAL_ENVIRONMENT_NAME) """Flag indicating if application is running in local development mode.""" + +IS_REDIS_INSTALLED = True +"""Flag indicating if redis module is installed.""" + +IS_SAQ_INSTALLED = True +"""Flag indicating if saq module is installed.""" + +IS_SENTRY_SDK_INSTALLED = True +"""Flag indicating if sentry_sdk module is installed.""" + +IS_SQLALCHEMY_INSTALLED = True +"""Flag indicating if sqlalchemy module is installed.""" + + +for package in ("redis", "saq", "sentry_sdk", "sqlalchemy"): + try: + import_module(package) + except ModuleNotFoundError: + match package: + case "redis": + IS_REDIS_INSTALLED = False + case "saq": + IS_SAQ_INSTALLED = False + case "sentry_sdk": + IS_SENTRY_SDK_INSTALLED = False + case "sqlalchemy": # pragma: no cover + IS_SQLALCHEMY_INSTALLED = False + +SERVICE_OBJECT_IDENTITY_MAP: MutableMapping[str, type[Service[Any]]] = {} +"""Used by the worker to lookup methods for service object callbacks.""" diff --git a/src/starlite_saqlalchemy/exceptions.py b/src/starlite_saqlalchemy/exceptions.py index 6f2caef3..8f25be7a 100644 --- a/src/starlite_saqlalchemy/exceptions.py +++ b/src/starlite_saqlalchemy/exceptions.py @@ -56,6 +56,17 @@ class AuthorizationError(StarliteSaqlalchemyClientError): """A user tried to do something they shouldn't have.""" +class MissingDependencyError(StarliteSaqlalchemyError, ValueError): + """A required dependency is not installed.""" + + def __init__(self, module: str, config: str | None = None) -> None: + config = config if config else module + super().__init__( + f"You enabled {config} configuration but package {module!r} is not installed. " + f'You may need to run: "poetry install starlite-saqlalchemy[{config}]"' + ) + + class HealthCheckConfigurationError(StarliteSaqlalchemyError): """An error occurred while registering an health check.""" diff --git a/src/starlite_saqlalchemy/health.py b/src/starlite_saqlalchemy/health.py index eaf5676b..58e7b3d3 100644 --- a/src/starlite_saqlalchemy/health.py +++ b/src/starlite_saqlalchemy/health.py @@ -48,7 +48,7 @@ async def live(self) -> bool: Returns: True if the service is running, False otherwise """ - return await self.ready() # pragma: no cover + return await self.ready() @abstractmethod async def ready(self) -> bool: @@ -57,6 +57,7 @@ async def ready(self) -> bool: Returns: True if the service is ready to serve requests, False otherwise """ + return True class AppHealthCheck(AbstractHealthCheck): @@ -66,7 +67,7 @@ class AppHealthCheck(AbstractHealthCheck): async def ready(self) -> bool: """Readiness check used when no other health check is available.""" - return True + return await super().ready() class HealthResource(BaseModel): diff --git a/src/starlite_saqlalchemy/init_plugin.py b/src/starlite_saqlalchemy/init_plugin.py index 6eb505e4..cead9ea6 100644 --- a/src/starlite_saqlalchemy/init_plugin.py +++ b/src/starlite_saqlalchemy/init_plugin.py @@ -1,3 +1,4 @@ +# pylint: disable=import-outside-toplevel """The application configuration plugin and config object. Example: @@ -32,14 +33,12 @@ def example_handler() -> dict: from collections.abc import Callable, Sequence # noqa: TC003 from typing import TYPE_CHECKING, Any, TypeVar -from pydantic import BaseModel +from pydantic import BaseModel, Field, validator from starlite.app import DEFAULT_CACHE_CONFIG, DEFAULT_OPENAPI_CONFIG -from starlite.plugins.sql_alchemy import SQLAlchemyPlugin from starlite.types import TypeEncodersMap # noqa: TC002 from structlog.types import Processor # noqa: TC002 from starlite_saqlalchemy import ( - cache, compression, dependencies, exceptions, @@ -47,22 +46,26 @@ def example_handler() -> dict: lifespan, log, openapi, - redis, - sentry, settings, - sqlalchemy_plugin, ) -from starlite_saqlalchemy.constants import IS_LOCAL_ENVIRONMENT, IS_TEST_ENVIRONMENT -from starlite_saqlalchemy.exceptions import HealthCheckConfigurationError +from starlite_saqlalchemy.constants import ( + IS_LOCAL_ENVIRONMENT, + IS_REDIS_INSTALLED, + IS_SAQ_INSTALLED, + IS_SENTRY_SDK_INSTALLED, + IS_SQLALCHEMY_INSTALLED, + IS_TEST_ENVIRONMENT, +) +from starlite_saqlalchemy.exceptions import ( + HealthCheckConfigurationError, + MissingDependencyError, +) from starlite_saqlalchemy.health import ( AbstractHealthCheck, AppHealthCheck, HealthController, ) -from starlite_saqlalchemy.service import make_service_callback -from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyHealthCheck from starlite_saqlalchemy.type_encoders import type_encoders_map -from starlite_saqlalchemy.worker import create_worker_instance if TYPE_CHECKING: from starlite.config.app import AppConfig @@ -81,9 +84,7 @@ class PluginConfig(BaseModel): application. """ - worker_functions: list[Callable[..., Any] | tuple[str, Callable[..., Any]]] = [ - (make_service_callback.__qualname__, make_service_callback) - ] + worker_functions: list[Callable[..., Any] | tuple[str, Callable[..., Any]]] = [] """Queue worker functions.""" do_after_exception: bool = True """Configure after exception handler. @@ -91,7 +92,7 @@ class PluginConfig(BaseModel): Add the hook handler to [`AppConfig.after_exception`][starlite.config.app.AppConfig.after_exception]. """ - do_cache: bool = True + do_cache: bool = Field(default_factory=lambda: IS_REDIS_INSTALLED) """Configure redis cache backend. Add configuration for the redis-backed cache to @@ -133,7 +134,10 @@ class PluginConfig(BaseModel): Set the OpenAPI config object to [`AppConfig.openapi_config`][starlite.config.app.AppConfig.openapi_config]. """ - do_sentry: bool | None = None + do_sentry: bool = Field( + default_factory=lambda: IS_SENTRY_SDK_INSTALLED + and not (IS_LOCAL_ENVIRONMENT or IS_TEST_ENVIRONMENT) + ) """Configure sentry. Configure the application to initialize Sentry on startup. Adds a handler to @@ -145,7 +149,7 @@ class PluginConfig(BaseModel): Allow the plugin to set the starlite `debug` parameter. Parameter set to value of [`AppConfig.debug`][starlite_saqlalchemy.settings.AppSettings.DEBUG]. """ - do_sqlalchemy_plugin: bool = True + do_sqlalchemy_plugin: bool = Field(default_factory=lambda: IS_SQLALCHEMY_INSTALLED) """Configure SQLAlchemy plugin. Set the SQLAlchemy plugin on the application. Adds the plugin to @@ -154,7 +158,7 @@ class PluginConfig(BaseModel): do_type_encoders: bool = True """Configure custom type encoders on the app.""" - do_worker: bool = True + do_worker: bool = Field(default_factory=lambda: IS_SAQ_INSTALLED) """Configure the async worker on the application. This action instantiates a worker instance and sets handlers for @@ -168,7 +172,32 @@ class PluginConfig(BaseModel): """Chain of structlog log processors.""" type_encoders: TypeEncodersMap = type_encoders_map """Map of type to serializer callable.""" - health_checks: Sequence[type[AbstractHealthCheck]] = [AppHealthCheck, SQLAlchemyHealthCheck] + health_checks: list[type[AbstractHealthCheck]] = [AppHealthCheck] + """Checks executed on calls to health route handler.""" + + @validator("do_cache") + def _validate_do_cache(cls, value: bool) -> bool: + if value is True and not IS_REDIS_INSTALLED: + raise MissingDependencyError(module="redis", config="redis") + return value + + @validator("do_sentry") + def _validate_do_sentry(cls, value: bool) -> bool: + if value is True and not IS_SENTRY_SDK_INSTALLED: + raise MissingDependencyError(module="sentry_sdk", config="sentry") + return value + + @validator("do_sqlalchemy_plugin") + def _validate_do_sqlalchemy_plugin(cls, value: bool) -> bool: + if value is True and not IS_SQLALCHEMY_INSTALLED: + raise MissingDependencyError(module="sqlalchemy", config="sqlalchemy_plugin") + return value + + @validator("do_worker") + def _validate_do_worker(cls, value: bool) -> bool: + if value is True and not IS_SAQ_INSTALLED: + raise MissingDependencyError(module="saq", config="worker") + return value class ConfigureApp: @@ -176,12 +205,12 @@ class ConfigureApp: __slots__ = ("config",) - def __init__(self, config: PluginConfig = PluginConfig()) -> None: + def __init__(self, config: PluginConfig | None = None) -> None: """ Args: config: Plugin configuration object. """ - self.config = config + self.config = config if config is not None else PluginConfig() def __call__(self, app_config: AppConfig) -> AppConfig: """Entrypoint to the app config plugin. @@ -200,17 +229,29 @@ def __call__(self, app_config: AppConfig) -> AppConfig: self.configure_compression(app_config) self.configure_debug(app_config) self.configure_exception_handlers(app_config) - self.configure_health_check(app_config) self.configure_logging(app_config) self.configure_openapi(app_config) self.configure_sentry(app_config) self.configure_sqlalchemy_plugin(app_config) self.configure_type_encoders(app_config) self.configure_worker(app_config) + # health check is explicitly configured last + self.configure_health_check(app_config) + self.set_lifecycle_handlers(app_config) + return app_config + + def set_lifecycle_handlers(self, app_config: AppConfig) -> None: + """Configure any necessary startup/shutdown behaviors. + Args: + app_config: The Starlite application config object. + """ app_config.before_startup = lifespan.before_startup_handler - app_config.on_shutdown.extend([http.on_shutdown, redis.client.close]) - return app_config + app_config.on_shutdown.append(http.on_shutdown) + if IS_REDIS_INSTALLED: + from starlite_saqlalchemy import redis + + app_config.on_shutdown.append(redis.client.close) def configure_after_exception(self, app_config: AppConfig) -> None: """Add the logging after exception hook handler. @@ -232,6 +273,8 @@ def configure_cache(self, app_config: AppConfig) -> None: app_config: The Starlite application config object. """ if self.config.do_cache and app_config.cache_config == DEFAULT_CACHE_CONFIG: + from starlite_saqlalchemy import cache + app_config.cache_config = cache.config def configure_collection_dependencies(self, app_config: AppConfig) -> None: @@ -331,12 +374,9 @@ def configure_sentry(self, app_config: AppConfig) -> None: Args: app_config: The Starlite application config object. """ - do_sentry = ( - self.config.do_sentry - if self.config.do_sentry is not None - else not (IS_LOCAL_ENVIRONMENT or IS_TEST_ENVIRONMENT) - ) - if do_sentry: + if self.config.do_sentry: + from starlite_saqlalchemy import sentry + app_config.on_startup.append(sentry.configure) def configure_sqlalchemy_plugin(self, app_config: AppConfig) -> None: @@ -349,7 +389,15 @@ def configure_sqlalchemy_plugin(self, app_config: AppConfig) -> None: app_config: The Starlite application config object. """ if self.config.do_sqlalchemy_plugin: - app_config.plugins.append(SQLAlchemyPlugin(config=sqlalchemy_plugin.config)) + from starlite.plugins.sql_alchemy import SQLAlchemyPlugin + + from starlite_saqlalchemy.sqlalchemy_plugin import ( + SQLAlchemyHealthCheck, + config, + ) + + self.config.health_checks.append(SQLAlchemyHealthCheck) + app_config.plugins.append(SQLAlchemyPlugin(config)) def configure_type_encoders(self, app_config: AppConfig) -> None: """Set mapping of type encoders on the application config. @@ -370,6 +418,15 @@ def configure_worker(self, app_config: AppConfig) -> None: app_config: The Starlite application config object. """ if self.config.do_worker: + from starlite_saqlalchemy.worker import ( + create_worker_instance, + make_service_callback, + ) + + self.config.worker_functions.append( + (make_service_callback.__qualname__, make_service_callback) + ) + worker_kwargs: dict[str, Any] = {"functions": self.config.worker_functions} if self.config.do_logging: worker_kwargs["before_process"] = log.worker.before_process diff --git a/src/starlite_saqlalchemy/lifespan.py b/src/starlite_saqlalchemy/lifespan.py index cebb251b..8be6b4a8 100644 --- a/src/starlite_saqlalchemy/lifespan.py +++ b/src/starlite_saqlalchemy/lifespan.py @@ -1,42 +1,48 @@ """Application lifespan handlers.""" -# pylint: disable=broad-except +# pylint: disable=broad-except,import-outside-toplevel import asyncio import logging import starlite -from sqlalchemy import text -from starlite_saqlalchemy import redis, settings -from starlite_saqlalchemy.db import engine +from starlite_saqlalchemy import constants, settings logger = logging.getLogger(__name__) async def _db_ready() -> None: """Wait for database to become responsive.""" - while True: - try: - async with engine.begin() as conn: - await conn.execute(text("SELECT 1")) - except Exception as exc: - logger.info("Waiting for DB: %s", exc) - await asyncio.sleep(5) - else: - logger.info("DB OK!") - break + if constants.IS_SQLALCHEMY_INSTALLED: + from sqlalchemy import text + + from starlite_saqlalchemy.db import engine + + while True: + try: + async with engine.begin() as conn: + await conn.execute(text("SELECT 1")) + except Exception as exc: + logger.info("Waiting for DB: %s", exc) + await asyncio.sleep(5) + else: + logger.info("DB OK!") + break async def _redis_ready() -> None: """Wait for redis to become responsive.""" - while True: - try: - await redis.client.ping() - except Exception as exc: - logger.info("Waiting for Redis: %s", exc) - await asyncio.sleep(5) - else: - logger.info("Redis OK!") - break + if constants.IS_REDIS_INSTALLED: + from starlite_saqlalchemy import redis + + while True: + try: + await redis.client.ping() + except Exception as exc: + logger.info("Waiting for Redis: %s", exc) + await asyncio.sleep(5) + else: + logger.info("Redis OK!") + break async def before_startup_handler(_: starlite.Starlite) -> None: diff --git a/src/starlite_saqlalchemy/log/controller.py b/src/starlite_saqlalchemy/log/controller.py index 3dcbc1e8..46427cba 100644 --- a/src/starlite_saqlalchemy/log/controller.py +++ b/src/starlite_saqlalchemy/log/controller.py @@ -197,7 +197,7 @@ async def extract_request_data(self, request: Request) -> dict[str, Any]: value = await value except RuntimeError: if key != REQUEST_BODY_FIELD: - raise + raise # pragma: no cover value = None data[key] = value return data diff --git a/src/starlite_saqlalchemy/repository/__init__.py b/src/starlite_saqlalchemy/repository/__init__.py index 35c2fc5d..67ef92f9 100644 --- a/src/starlite_saqlalchemy/repository/__init__.py +++ b/src/starlite_saqlalchemy/repository/__init__.py @@ -1,11 +1,10 @@ """Abstraction over the data storage for the application.""" from __future__ import annotations -from . import abc, filters, sqlalchemy, types +from . import abc, filters, types __all__ = [ "abc", "filters", - "sqlalchemy", "types", ] diff --git a/src/starlite_saqlalchemy/service.py b/src/starlite_saqlalchemy/service.py deleted file mode 100644 index 8660d177..00000000 --- a/src/starlite_saqlalchemy/service.py +++ /dev/null @@ -1,280 +0,0 @@ -"""A generic service object implementation. - -Service object is generic on the domain model type. - -RepositoryService object is generic on the domain model type which should be a SQLAlchemy model. -""" -from __future__ import annotations - -import contextlib -import inspect -import logging -from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar - -from saq.job import Job - -from starlite_saqlalchemy import utils -from starlite_saqlalchemy.db import async_session_factory -from starlite_saqlalchemy.exceptions import NotFoundError -from starlite_saqlalchemy.repository.sqlalchemy import ModelT -from starlite_saqlalchemy.worker import default_job_config_dict, queue - -if TYPE_CHECKING: - from collections.abc import AsyncIterator - - from saq.types import Context - - from starlite_saqlalchemy.repository.abc import AbstractRepository - from starlite_saqlalchemy.repository.types import FilterTypes - from starlite_saqlalchemy.worker import JobConfig - - -logger = logging.getLogger(__name__) - -T = TypeVar("T") -ServiceT = TypeVar("ServiceT", bound="Service") -RepoServiceT = TypeVar("RepoServiceT", bound="RepositoryService") - -service_object_identity_map: dict[str, type[Service]] = {} - - -class Service(Generic[T]): - """Generic Service object.""" - - __id__: ClassVar[str] - - def __init_subclass__(cls, *_: Any, **__: Any) -> None: - """Map the service object to a unique identifier. - - Important that the id is deterministic across running - application instances, e.g., using something like `hash()` or - `id()` won't work as those would be different on different - instances of the running application. So we use the full import - path to the object. - """ - cls.__id__ = f"{cls.__module__}.{cls.__name__}" - service_object_identity_map[cls.__id__] = cls - - # pylint:disable=unused-argument - - async def create(self, data: T) -> T: - """Create an instance of `T`. - - Args: - data: Representation to be created. - - Returns: - Representation of created instance. - """ - return data - - async def list(self, **kwargs: Any) -> list[T]: - """Return view of the collection of `T`. - - Args: - **kwargs: Keyword arguments for filtering. - - Returns: - The list of instances retrieved from the repository. - """ - return [] - - async def update(self, id_: Any, data: T) -> T: - """Update existing instance of `T` with `data`. - - Args: - id_: Identifier of item to be updated. - data: Representation to be updated. - - Returns: - Updated representation. - """ - return data - - async def upsert(self, id_: Any, data: T) -> T: - """Create or update an instance of `T` with `data`. - - Args: - id_: Identifier of the object for upsert. - data: Representation for upsert. - - Returns: - Updated or created representation. - """ - return data - - async def get(self, id_: Any) -> T: - """Retrieve a representation of `T` with that is identified by `id_` - - Args: - id_: Identifier of instance to be retrieved. - - Returns: - Representation of instance with identifier `id_`. - """ - raise NotFoundError - - async def delete(self, id_: Any) -> T: - """Delete `T` that is identified by `id_`. - - Args: - id_: Identifier of instance to be deleted. - - Returns: - Representation of the deleted instance. - """ - raise NotFoundError - - async def enqueue_background_task( - self, method_name: str, job_config: JobConfig | None = None, **kwargs: Any - ) -> None: - """Enqueue an async callback for the operation and data. - - Args: - method_name: Method on the service object that should be called by the async worker. - job_config: Configuration object to control the job that is enqueued. - **kwargs: Arguments to be passed to the method when called. Must be JSON serializable. - """ - module = inspect.getmodule(self) - if module is None: # pragma: no cover - logger.warning("Callback not enqueued, no module resolved for %s", self) - return - job_config_dict: dict[str, Any] - if job_config is None: - job_config_dict = default_job_config_dict - else: - job_config_dict = utils.dataclass_as_dict_shallow(job_config, exclude_none=True) - - kwargs["service_type_id"] = self.__id__ - kwargs["service_method_name"] = method_name - job = Job( - function=make_service_callback.__qualname__, - kwargs=kwargs, - **job_config_dict, - ) - await queue.enqueue(job) - - @classmethod - @contextlib.asynccontextmanager - async def new(cls: type[ServiceT]) -> AsyncIterator[ServiceT]: - """Context manager that returns instance of service object. - - Returns: - The service object instance. - """ - yield cls() - - -class RepositoryService(Service[ModelT], Generic[ModelT]): - """Service object that operates on a repository object.""" - - repository_type: type[AbstractRepository[ModelT]] - - def __init__(self, **repo_kwargs: Any) -> None: - """Configure the service object. - - Args: - **repo_kwargs: passed as keyword args to repo instantiation. - """ - self.repository = self.repository_type(**repo_kwargs) - - async def create(self, data: ModelT) -> ModelT: - """Wrap repository instance creation. - - Args: - data: Representation to be created. - - Returns: - Representation of created instance. - """ - return await self.repository.add(data) - - async def list(self, *filters: "FilterTypes", **kwargs: Any) -> list[ModelT]: - """Wrap repository scalars operation. - - Args: - *filters: Collection route filters. - **kwargs: Keyword arguments for attribute based filtering. - - Returns: - The list of instances retrieved from the repository. - """ - return await self.repository.list(*filters, **kwargs) - - async def update(self, id_: Any, data: ModelT) -> ModelT: - """Wrap repository update operation. - - Args: - id_: Identifier of item to be updated. - data: Representation to be updated. - - Returns: - Updated representation. - """ - self.repository.set_id_attribute_value(id_, data) - return await self.repository.update(data) - - async def upsert(self, id_: Any, data: ModelT) -> ModelT: - """Wrap repository upsert operation. - - Args: - id_: Identifier of the object for upsert. - data: Representation for upsert. - - Returns: - Updated or created representation. - """ - self.repository.set_id_attribute_value(id_, data) - return await self.repository.upsert(data) - - async def get(self, id_: Any) -> ModelT: - """Wrap repository scalar operation. - - Args: - id_: Identifier of instance to be retrieved. - - Returns: - Representation of instance with identifier `id_`. - """ - return await self.repository.get(id_) - - async def delete(self, id_: Any) -> ModelT: - """Wrap repository delete operation. - - Args: - id_: Identifier of instance to be deleted. - - Returns: - Representation of the deleted instance. - """ - return await self.repository.delete(id_) - - @classmethod - @contextlib.asynccontextmanager - async def new(cls: type[RepoServiceT]) -> AsyncIterator[RepoServiceT]: - """Context manager that returns instance of service object. - - Handles construction of the database session. - - Returns: - The service object instance. - """ - async with async_session_factory() as session: - yield cls(session=session) - - -async def make_service_callback( - _ctx: Context, *, service_type_id: str, service_method_name: str, **kwargs: Any -) -> None: - """Make an async service callback. - - Args: - _ctx: the SAQ context - service_type_id: Value of `__id__` class var on service type. - service_method_name: Method to be called on the service object. - **kwargs: Unpacked into the service method call as keyword arguments. - """ - service_type = service_object_identity_map[service_type_id] - async with service_type.new() as service_object: - method = getattr(service_object, service_method_name) - await method(**kwargs) diff --git a/src/starlite_saqlalchemy/service/__init__.py b/src/starlite_saqlalchemy/service/__init__.py new file mode 100644 index 00000000..0e3bf0c7 --- /dev/null +++ b/src/starlite_saqlalchemy/service/__init__.py @@ -0,0 +1,5 @@ +"""Implementations for service object.""" + +from .generic import Service + +__all__ = ["Service"] diff --git a/src/starlite_saqlalchemy/service/generic.py b/src/starlite_saqlalchemy/service/generic.py new file mode 100644 index 00000000..3390c2f5 --- /dev/null +++ b/src/starlite_saqlalchemy/service/generic.py @@ -0,0 +1,121 @@ +"""A generic service object implementation. + +Service object is generic on the domain model type. +""" +from __future__ import annotations + +import contextlib +from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar + +from starlite_saqlalchemy import constants +from starlite_saqlalchemy.exceptions import NotFoundError + +if TYPE_CHECKING: + from collections.abc import AsyncIterator + + +T = TypeVar("T") +ServiceT = TypeVar("ServiceT", bound="Service") + + +class Service(Generic[T]): + """Generic Service object.""" + + __id__: ClassVar[str] = "starlite_saqlalchemy.service.generic.Service" + + def __init_subclass__(cls, *_: Any, **__: Any) -> None: + """Map the service object to a unique identifier. + + Important that the id is deterministic across running + application instances, e.g., using something like `hash()` or + `id()` won't work as those would be different on different + instances of the running application. So we use the full import + path to the object. + """ + cls.__id__ = f"{cls.__module__}.{cls.__name__}" + # error: Argument of type "Type[Self@Service[T@Service]]" cannot be assigned to parameter + # "__value" of type "Type[Service[Any]]" in function "__setitem__" + # "Type[Service[T@Service]]" is incompatible with "Type[Service[Any]]" + # Type "Type[Self@Service[T@Service]]" cannot be assigned to type "Type[Service[Any]]" + # (reportGeneralTypeIssues) + constants.SERVICE_OBJECT_IDENTITY_MAP[cls.__id__] = cls # pyright:ignore + + # pylint:disable=unused-argument + + async def create(self, data: T) -> T: + """Create an instance of `T`. + + Args: + data: Representation to be created. + + Returns: + Representation of created instance. + """ + return data + + async def list(self, **kwargs: Any) -> list[T]: + """Return view of the collection of `T`. + + Args: + **kwargs: Keyword arguments for filtering. + + Returns: + The list of instances retrieved from the repository. + """ + return [] + + async def update(self, id_: Any, data: T) -> T: + """Update existing instance of `T` with `data`. + + Args: + id_: Identifier of item to be updated. + data: Representation to be updated. + + Returns: + Updated representation. + """ + return data + + async def upsert(self, id_: Any, data: T) -> T: + """Create or update an instance of `T` with `data`. + + Args: + id_: Identifier of the object for upsert. + data: Representation for upsert. + + Returns: + Updated or created representation. + """ + return data + + async def get(self, id_: Any) -> T: + """Retrieve a representation of `T` with that is identified by `id_` + + Args: + id_: Identifier of instance to be retrieved. + + Returns: + Representation of instance with identifier `id_`. + """ + raise NotFoundError + + async def delete(self, id_: Any) -> T: + """Delete `T` that is identified by `id_`. + + Args: + id_: Identifier of instance to be deleted. + + Returns: + Representation of the deleted instance. + """ + raise NotFoundError + + @classmethod + @contextlib.asynccontextmanager + async def new(cls: type[ServiceT]) -> AsyncIterator[ServiceT]: + """Context manager that returns instance of service object. + + Returns: + The service object instance. + """ + yield cls() diff --git a/src/starlite_saqlalchemy/service/sqlalchemy.py b/src/starlite_saqlalchemy/service/sqlalchemy.py new file mode 100644 index 00000000..5ede194b --- /dev/null +++ b/src/starlite_saqlalchemy/service/sqlalchemy.py @@ -0,0 +1,124 @@ +"""Service object implementation for SQLAlchemy. + +RepositoryService object is generic on the domain model type which +should be a SQLAlchemy model. +""" + +from __future__ import annotations + +import contextlib +from typing import TYPE_CHECKING, Any, Generic, TypeVar + +from starlite_saqlalchemy.db import async_session_factory +from starlite_saqlalchemy.repository.sqlalchemy import ModelT + +from .generic import Service + +if TYPE_CHECKING: + from collections.abc import AsyncIterator + + from starlite_saqlalchemy.repository.abc import AbstractRepository + from starlite_saqlalchemy.repository.types import FilterTypes + + +RepoServiceT = TypeVar("RepoServiceT", bound="RepositoryService") + + +class RepositoryService(Service[ModelT], Generic[ModelT]): + """Service object that operates on a repository object.""" + + __id__ = "starlite_saqlalchemy.service.sqlalchemy.RepositoryService" + + repository_type: type[AbstractRepository[ModelT]] + + def __init__(self, **repo_kwargs: Any) -> None: + """Configure the service object. + + Args: + **repo_kwargs: passed as keyword args to repo instantiation. + """ + self.repository = self.repository_type(**repo_kwargs) + + async def create(self, data: ModelT) -> ModelT: + """Wrap repository instance creation. + + Args: + data: Representation to be created. + + Returns: + Representation of created instance. + """ + return await self.repository.add(data) + + async def list(self, *filters: "FilterTypes", **kwargs: Any) -> list[ModelT]: + """Wrap repository scalars operation. + + Args: + *filters: Collection route filters. + **kwargs: Keyword arguments for attribute based filtering. + + Returns: + The list of instances retrieved from the repository. + """ + return await self.repository.list(*filters, **kwargs) + + async def update(self, id_: Any, data: ModelT) -> ModelT: + """Wrap repository update operation. + + Args: + id_: Identifier of item to be updated. + data: Representation to be updated. + + Returns: + Updated representation. + """ + self.repository.set_id_attribute_value(id_, data) + return await self.repository.update(data) + + async def upsert(self, id_: Any, data: ModelT) -> ModelT: + """Wrap repository upsert operation. + + Args: + id_: Identifier of the object for upsert. + data: Representation for upsert. + + Returns: + Updated or created representation. + """ + self.repository.set_id_attribute_value(id_, data) + return await self.repository.upsert(data) + + async def get(self, id_: Any) -> ModelT: + """Wrap repository scalar operation. + + Args: + id_: Identifier of instance to be retrieved. + + Returns: + Representation of instance with identifier `id_`. + """ + return await self.repository.get(id_) + + async def delete(self, id_: Any) -> ModelT: + """Wrap repository delete operation. + + Args: + id_: Identifier of instance to be deleted. + + Returns: + Representation of the deleted instance. + """ + return await self.repository.delete(id_) + + @classmethod + @contextlib.asynccontextmanager + async def new(cls: type[RepoServiceT]) -> AsyncIterator[RepoServiceT]: + """Context manager that returns instance of service object. + + Handles construction of the database session. + + Returns: + The service object instance. + """ + async with async_session_factory() as session: + yield cls(session=session) diff --git a/src/starlite_saqlalchemy/settings.py b/src/starlite_saqlalchemy/settings.py index b182a6d8..c9ababe9 100644 --- a/src/starlite_saqlalchemy/settings.py +++ b/src/starlite_saqlalchemy/settings.py @@ -38,7 +38,7 @@ class Config: TEST_ENVIRONMENT_NAME: str = "test" """Value of ENVIRONMENT used to determine if running tests. - This should be the value of `ENVIRONMENT` in `test.env`. + This should be the value of `ENVIRONMENT` in `tests.env`. """ LOCAL_ENVIRONMENT_NAME: str = "local" """Value of ENVIRONMENT used to determine if running in local development diff --git a/src/starlite_saqlalchemy/testing/__init__.py b/src/starlite_saqlalchemy/testing/__init__.py index 8d75d555..50f44d01 100644 --- a/src/starlite_saqlalchemy/testing/__init__.py +++ b/src/starlite_saqlalchemy/testing/__init__.py @@ -1,11 +1,8 @@ """Application testing support.""" - from .controller_test import ControllerTest -from .generic_mock_repository import GenericMockRepository from .modify_settings import modify_settings __all__ = ( "ControllerTest", - "GenericMockRepository", "modify_settings", ) diff --git a/src/starlite_saqlalchemy/worker.py b/src/starlite_saqlalchemy/worker.py index de8f0305..ae5e86cc 100644 --- a/src/starlite_saqlalchemy/worker.py +++ b/src/starlite_saqlalchemy/worker.py @@ -3,6 +3,8 @@ import asyncio import dataclasses +import inspect +import logging from functools import partial from typing import TYPE_CHECKING, Any @@ -10,21 +12,29 @@ import saq from starlite.utils.serialization import default_serializer -from starlite_saqlalchemy import redis, settings, type_encoders, utils +from starlite_saqlalchemy import constants, redis, settings, type_encoders, utils if TYPE_CHECKING: from collections.abc import Awaitable, Callable, Collection from signal import Signals + from saq.types import Context + + from starlite_saqlalchemy.service import Service + __all__ = [ "JobConfig", "Queue", "Worker", "create_worker_instance", "default_job_config_dict", + "make_service_callback", + "enqueue_background_task_for_service", "queue", ] +logger = logging.getLogger(__name__) + encoder = msgspec.json.Encoder( enc_hook=partial(default_serializer, type_encoders=type_encoders.type_encoders_map) ) @@ -147,3 +157,51 @@ def create_worker_instance( The worker instance, instantiated with `functions`. """ return Worker(queue, functions, before_process=before_process, after_process=after_process) + + +async def make_service_callback( + _ctx: Context, *, service_type_id: str, service_method_name: str, **kwargs: Any +) -> None: + """Make an async service callback. + + Args: + _ctx: the SAQ context + service_type_id: Value of `__id__` class var on service type. + service_method_name: Method to be called on the service object. + **kwargs: Unpacked into the service method call as keyword arguments. + """ + service_type = constants.SERVICE_OBJECT_IDENTITY_MAP[service_type_id] + async with service_type.new() as service_object: + method = getattr(service_object, service_method_name) + await method(**kwargs) + + +async def enqueue_background_task_for_service( + service_obj: Service, method_name: str, job_config: JobConfig | None = None, **kwargs: Any +) -> None: + """Enqueue an async callback for the operation and data. + + Args: + service_obj: The Service instance that is requesting the callback. + method_name: Method on the service object that should be called by the async worker. + job_config: Configuration object to control the job that is enqueued. + **kwargs: Arguments to be passed to the method when called. Must be JSON serializable. + """ + module = inspect.getmodule(service_obj) + if module is None: # pragma: no cover + logger.warning("Callback not enqueued, no module resolved for %s", service_obj) + return + job_config_dict: dict[str, Any] + if job_config is None: + job_config_dict = default_job_config_dict + else: + job_config_dict = utils.dataclass_as_dict_shallow(job_config, exclude_none=True) + + kwargs["service_type_id"] = service_obj.__id__ + kwargs["service_method_name"] = method_name + job = saq.Job( + function=make_service_callback.__qualname__, + kwargs=kwargs, + **job_config_dict, + ) + await queue.enqueue(job) diff --git a/tests.env b/tests.env index e5a31403..19adcf9e 100644 --- a/tests.env +++ b/tests.env @@ -1,3 +1,5 @@ # App ENVIRONMENT=test NAME=my-starlite-app + +TEST_APP=tests.utils.app:create_app diff --git a/tests/conftest.py b/tests/conftest.py index 761a66f5..c1f5e95e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,9 +7,6 @@ from uuid import uuid4 import pytest -from asyncpg.pgproto import pgproto - -from tests.utils.domain import authors, books if TYPE_CHECKING: from collections.abc import Callable @@ -19,6 +16,10 @@ from pytest import MonkeyPatch +# Ensure that pytest_dotenv is loaded before +# so pytest_starlite_saqlalchemy uses correct env values +pytest_plugins = ("pytest_dotenv", "pytest_starlite_saqlalchemy.plugin") + @pytest.fixture(name="raw_authors") def fx_raw_authors() -> list[dict[str, Any]]: @@ -42,16 +43,6 @@ def fx_raw_authors() -> list[dict[str, Any]]: ] -@pytest.fixture(name="authors") -def fx_authors(raw_authors: list[dict[str, Any]]) -> list[authors.Author]: - """Collection of parsed Author models.""" - mapped_authors = [authors.ReadDTO(**raw).to_mapped() for raw in raw_authors] - # convert these to pgproto UUIDs as that is what we get back from sqlalchemy - for author in mapped_authors: - author.id = pgproto.UUID(str(author.id)) - return mapped_authors - - @pytest.fixture(name="raw_books") def fx_raw_books(raw_authors: list[dict[str, Any]]) -> list[dict[str, Any]]: """Unstructured book representations.""" @@ -67,16 +58,6 @@ def fx_raw_books(raw_authors: list[dict[str, Any]]) -> list[dict[str, Any]]: ] -@pytest.fixture(name="books") -def fx_books(raw_books: list[dict[str, Any]]) -> list[books.Book]: - """Collection of parsed Book models.""" - mapped_books = [books.ReadDTO(**raw).to_mapped() for raw in raw_books] - # convert these to pgproto UUIDs as that is what we get back from sqlalchemy - for book in mapped_books: - book.id = pgproto.UUID(str(book.id)) - return mapped_books - - @pytest.fixture(name="create_module") def fx_create_module(tmp_path: Path, monkeypatch: MonkeyPatch) -> Callable[[str], ModuleType]: """Utility fixture for dynamic module creation.""" diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index ad1d9ece..3f2b3d98 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -4,6 +4,7 @@ import asyncio import timeit from asyncio import AbstractEventLoop, get_event_loop_policy +from datetime import datetime from pathlib import Path from typing import TYPE_CHECKING @@ -28,8 +29,6 @@ from pytest_docker.plugin import Services # type:ignore[import] from starlite import Starlite - from tests.utils.domain.authors import Author - here = Path(__file__).parent @@ -167,19 +166,25 @@ async def fx_engine(docker_ip: str) -> AsyncEngine: @pytest.fixture(autouse=True) -async def _seed_db(engine: AsyncEngine, authors: list[Author]) -> AsyncIterator[None]: +async def _seed_db(engine: AsyncEngine, raw_authors: list[dict[str, Any]]) -> AsyncIterator[None]: """Populate test database with. Args: engine: The SQLAlchemy engine instance. """ - # get models into metadata metadata = db.orm.Base.registry.metadata author_table = metadata.tables["author"] async with engine.begin() as conn: await conn.run_sync(metadata.create_all) + + # convert date/time strings to dt objects. + for raw_author in raw_authors: + raw_author["dob"] = datetime.strptime(raw_author["dob"], "%Y-%m-%d") + raw_author["created"] = datetime.strptime(raw_author["created"], "%Y-%m-%dT%H:%M:%S") + raw_author["updated"] = datetime.strptime(raw_author["updated"], "%Y-%m-%dT%H:%M:%S") + async with engine.begin() as conn: - await conn.execute(author_table.insert(), [vars(item) for item in authors]) + await conn.execute(author_table.insert(), raw_authors) yield async with engine.begin() as conn: await conn.run_sync(metadata.drop_all) diff --git a/tests/pytest_plugin/test_plugin.py b/tests/pytest_plugin/test_plugin.py index 75d00c93..e787914b 100644 --- a/tests/pytest_plugin/test_plugin.py +++ b/tests/pytest_plugin/test_plugin.py @@ -20,7 +20,7 @@ def test_pytest_addoption() -> None: assert parser._ininames == ["test_app", "unit_test_pattern"] """ ) - result = pytester.runpytest() + result = pytester.runpytest_subprocess() result.assert_outcomes(passed=1) @@ -49,7 +49,7 @@ def test_patch_worker() -> None: assert isinstance(Worker.stop, MagicMock) """ ) - result = pytester.runpytest() + result = pytester.runpytest_subprocess() result.assert_outcomes(passed=3) @@ -78,7 +78,7 @@ def test_patch_worker() -> None: assert not isinstance(Worker.stop, MagicMock) """ ) - result = pytester.runpytest() + result = pytester.runpytest_subprocess() result.assert_outcomes(passed=3) @@ -95,7 +95,7 @@ def test_patch_http_close(is_unit_test: bool) -> None: assert not starlite_saqlalchemy.http.clients """ ) - result = pytester.runpytest() + result = pytester.runpytest_subprocess() result.assert_outcomes(passed=1) @@ -122,14 +122,18 @@ def test_app(app): def test_app_fixture_if_app_instance(pytester: Pytester) -> None: - """Test that the app fixture returns the an instance if the path points to + """Test that the app fixture returns an instance if the path points to one.""" pytester.syspathinsert() pytester.makepyfile( test_app=""" - from tests.utils.app import create_app + from starlite import Starlite, get - app = create_app() + @get("/wherever") + def whatever() -> None: + return None + + app = Starlite(route_handlers=[whatever]) """ ) pytester.makepyprojecttoml( @@ -144,10 +148,10 @@ def test_app_fixture_if_app_instance(pytester: Pytester) -> None: def test_app(app): assert isinstance(app, Starlite) - assert "/authors" in app.route_handler_method_map + assert "/wherever" in app.route_handler_method_map """ ) - result = pytester.runpytest() + result = pytester.runpytest_subprocess() result.assert_outcomes(passed=1) @@ -170,5 +174,5 @@ def test_app(app): assert app.route_handler_method_map.keys() == {"/health"} """ ) - result = pytester.runpytest() + result = pytester.runpytest_subprocess() result.assert_outcomes(passed=1) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 16871add..e4dd2c6d 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,71 +1,23 @@ """Unit test specific config.""" +# pylint: disable=import-outside-toplevel from __future__ import annotations from typing import TYPE_CHECKING import pytest -from saq.job import Job +from starlite import get from starlite.datastructures import State from starlite.enums import ScopeType -from starlite_saqlalchemy.testing import GenericMockRepository -from tests.utils.domain.authors import Author -from tests.utils.domain.authors import Service as AuthorService -from tests.utils.domain.books import Book -from tests.utils.domain.books import Service as BookService - -from ..utils import controllers +from starlite_saqlalchemy import constants if TYPE_CHECKING: + from saq.job import Job from starlite import Starlite from starlite.types import HTTPResponseBodyEvent, HTTPResponseStartEvent, HTTPScope -@pytest.fixture(name="author_repository_type") -def fx_author_repository_type( - authors: list[Author], monkeypatch: pytest.MonkeyPatch -) -> type[GenericMockRepository[Author]]: - """Mock Author repository, pre-seeded with collection data.""" - - repo = GenericMockRepository[Author] - repo.seed_collection(authors) - monkeypatch.setattr(AuthorService, "repository_type", repo) - return repo - - -@pytest.fixture(name="author_repository") -def fx_author_repository( - author_repository_type: type[GenericMockRepository[Author]], -) -> GenericMockRepository[Author]: - """Mock Author repository instance.""" - return author_repository_type() - - -@pytest.fixture(name="book_repository_type") -def fx_book_repository_type( - books: list[Book], monkeypatch: pytest.MonkeyPatch -) -> type[GenericMockRepository[Book]]: - """Mock Book repository, pre-seeded with collection data.""" - - class BookRepository(GenericMockRepository[Book]): - """Mock book repo.""" - - model_type = Book - - BookRepository.seed_collection(books) - monkeypatch.setattr(BookService, "repository_type", BookRepository) - return BookRepository - - -@pytest.fixture(name="book_repository") -def fx_book_repository( - book_repository_type: type[GenericMockRepository[Book]], -) -> GenericMockRepository[Book]: - """Mock Book repo instance.""" - return book_repository_type() - - @pytest.fixture() def http_response_start() -> HTTPResponseStartEvent: """ASGI message for start of response.""" @@ -85,6 +37,11 @@ def http_response_body() -> HTTPResponseBodyEvent: @pytest.fixture() def http_scope(app: Starlite) -> HTTPScope: """Minimal ASGI HTTP connection scope.""" + + @get() + def handler() -> None: + ... + return { "headers": [], "app": app, @@ -98,7 +55,7 @@ def http_scope(app: Starlite) -> HTTPScope: "query_string": b"", "raw_path": b"/wherever", "root_path": "/", - "route_handler": controllers.get_author, + "route_handler": handler, "scheme": "http", "server": None, "session": {}, @@ -109,13 +66,18 @@ def http_scope(app: Starlite) -> HTTPScope: } -@pytest.fixture() -def state() -> State: - """Starlite application state datastructure.""" - return State() - - @pytest.fixture() def job() -> Job: """SAQ Job instance.""" + if not constants.IS_SAQ_INSTALLED: + pytest.skip("SAQ not installed") + + from saq.job import Job + return Job(function="whatever", kwargs={"a": "b"}) + + +@pytest.fixture() +def state() -> State: + """Starlite application state datastructure.""" + return State() diff --git a/tests/unit/repository/__init__.py b/tests/unit/no_extras/__init__.py similarity index 100% rename from tests/unit/repository/__init__.py rename to tests/unit/no_extras/__init__.py diff --git a/tests/unit/no_extras/conftest.py b/tests/unit/no_extras/conftest.py new file mode 100644 index 00000000..0195388a --- /dev/null +++ b/tests/unit/no_extras/conftest.py @@ -0,0 +1,15 @@ +"""Tests are only run if no extra dependencies are installed.""" + +from starlite_saqlalchemy import constants + +SKIP = any( + [ + constants.IS_SAQ_INSTALLED, + constants.IS_SENTRY_SDK_INSTALLED, + constants.IS_REDIS_INSTALLED, + constants.IS_SQLALCHEMY_INSTALLED, + ] +) + +if SKIP: + collect_ignore_glob = ["*"] diff --git a/tests/unit/no_extras/test_init_plugin.py b/tests/unit/no_extras/test_init_plugin.py new file mode 100644 index 00000000..66cf2968 --- /dev/null +++ b/tests/unit/no_extras/test_init_plugin.py @@ -0,0 +1,79 @@ +"""Tests for init_plugin.py when no extra dependencies are installed.""" + +import pytest +from pydantic import ValidationError +from starlite import Starlite +from starlite.cache import SimpleCacheBackend + +from starlite_saqlalchemy import init_plugin + + +def test_config_switches() -> None: + """Tests that the app produced with all config switches off is as we + expect.""" + config = init_plugin.PluginConfig( + do_after_exception=False, + do_cache=False, + do_compression=False, + # pyright reckons this parameter doesn't exist, I beg to differ + do_collection_dependencies=False, # pyright:ignore + do_exception_handlers=False, + do_health_check=False, + do_logging=False, + do_openapi=False, + do_sentry=False, + do_set_debug=False, + do_sqlalchemy_plugin=False, + do_type_encoders=False, + do_worker=False, + ) + app = Starlite( + route_handlers=[], + openapi_config=None, + on_app_init=[init_plugin.ConfigureApp(config=config)], + ) + assert app.compression_config is None + assert app.debug is False + assert app.logging_config is None + assert app.openapi_config is None + assert app.response_class is None + assert isinstance(app.cache.backend, SimpleCacheBackend) + assert len(app.on_shutdown) == 1 + assert not app.after_exception + assert not app.dependencies + assert not app.exception_handlers + assert not app.on_startup + assert not app.plugins + assert not app.routes + + +@pytest.mark.parametrize( + ("enabled_config", "error_pattern"), + [ + ("do_cache", r"\'redis\' is not installed."), + ("do_sentry", r"\'sentry_sdk\' is not installed."), + ("do_worker", r"\'saq\' is not installed."), + ("do_sqlalchemy_plugin", r"\'sqlalchemy\' is not installed."), + ], +) +def test_extra_dependencies_not_installed(enabled_config: str, error_pattern: str) -> None: + """Tests that the plugin test required dependencies for switches needing + them.""" + kwargs = { + "do_after_exception": False, + "do_cache": False, + "do_compression": False, + "do_collection_dependencies": False, + "do_exception_handlers": False, + "do_health_check": False, + "do_logging": False, + "do_openapi": False, + "do_sentry": False, + "do_set_debug": False, + "do_sqlalchemy_plugin": False, + "do_type_encoders": False, + "do_worker": False, + **{enabled_config: True}, + } + with pytest.raises(ValidationError, match=error_pattern): + init_plugin.PluginConfig(**kwargs) diff --git a/tests/unit/redis/conftest.py b/tests/unit/redis/conftest.py new file mode 100644 index 00000000..6ddee5a9 --- /dev/null +++ b/tests/unit/redis/conftest.py @@ -0,0 +1,4 @@ +from starlite_saqlalchemy.constants import IS_REDIS_INSTALLED + +if not IS_REDIS_INSTALLED: + collect_ignore_glob = ["*"] diff --git a/tests/unit/test_cache.py b/tests/unit/redis/test_cache.py similarity index 89% rename from tests/unit/test_cache.py rename to tests/unit/redis/test_cache.py index 179ca3a6..d5991909 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/redis/test_cache.py @@ -1,14 +1,10 @@ """Test for the application cache configurations.""" -from typing import TYPE_CHECKING - +import pytest from starlite.config.cache import default_cache_key_builder from starlite.testing import RequestFactory from starlite_saqlalchemy import cache, settings -if TYPE_CHECKING: - import pytest - def test_cache_key_builder(monkeypatch: "pytest.MonkeyPatch") -> None: """Test that the cache key builder prefixes cache keys.""" diff --git a/tests/unit/redis/worker/conftest.py b/tests/unit/redis/worker/conftest.py new file mode 100644 index 00000000..fc0c43a3 --- /dev/null +++ b/tests/unit/redis/worker/conftest.py @@ -0,0 +1,4 @@ +from starlite_saqlalchemy.constants import IS_SAQ_INSTALLED + +if not IS_SAQ_INSTALLED: + collect_ignore_glob = ["*"] diff --git a/tests/unit/redis/worker/test_worker.py b/tests/unit/redis/worker/test_worker.py new file mode 100644 index 00000000..c0ce9e95 --- /dev/null +++ b/tests/unit/redis/worker/test_worker.py @@ -0,0 +1,106 @@ +"""Tests for the SAQ async worker functionality.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any +from unittest.mock import AsyncMock + +import pytest +from asyncpg.pgproto import pgproto +from pydantic import BaseModel +from saq import Job + +from starlite_saqlalchemy import service, worker + +if TYPE_CHECKING: + + from pytest import MonkeyPatch + + +def test_worker_decoder_handles_pgproto_uuid() -> None: + """Test that the decoder can handle pgproto.UUID instances.""" + pg_uuid = pgproto.UUID("0448bde2-7c69-4e6b-9c03-7b217e3b563d") + encoded = worker.encoder.encode(pg_uuid) + assert encoded == b'"0448bde2-7c69-4e6b-9c03-7b217e3b563d"' + + +def test_worker_decoder_handles_pydantic_models() -> None: + """Test that the decoder we use for SAQ will encode a pydantic model.""" + + class Model(BaseModel): + a: str + b: int + c: float + + pydantic_model = Model(a="a", b=1, c=2.34) + encoded = worker.encoder.encode(pydantic_model) + assert encoded == b'{"a":"a","b":1,"c":2.34}' + + +async def test_make_service_callback( + raw_authors: list[dict[str, Any]], monkeypatch: MonkeyPatch +) -> None: + """Tests loading and retrieval of service object types.""" + recv_cb_mock = AsyncMock() + monkeypatch.setattr(service.Service, "receive_callback", recv_cb_mock, raising=False) + await worker.make_service_callback( + {}, + service_type_id="tests.utils.domain.authors.Service", + service_method_name="receive_callback", + raw_obj=raw_authors[0], + ) + recv_cb_mock.assert_called_once_with(raw_obj=raw_authors[0]) + + +async def test_make_service_callback_raises_runtime_error( + raw_authors: list[dict[str, Any]] +) -> None: + """Tests loading and retrieval of service object types.""" + with pytest.raises(KeyError): + await worker.make_service_callback( + {}, + service_type_id="tests.utils.domain.LSKDFJ", + service_method_name="receive_callback", + raw_obj=raw_authors[0], + ) + + +async def test_enqueue_service_callback(monkeypatch: "MonkeyPatch") -> None: + """Tests that job enqueued with desired arguments.""" + enqueue_mock = AsyncMock() + monkeypatch.setattr(worker.queue, "enqueue", enqueue_mock) + service_instance = service.Service[Any]() + await worker.enqueue_background_task_for_service( + service_instance, "receive_callback", raw_obj={"a": "b"} + ) + enqueue_mock.assert_called_once() + assert isinstance(enqueue_mock.mock_calls[0].args[0], Job) + job = enqueue_mock.mock_calls[0].args[0] + assert job.function == worker.make_service_callback.__qualname__ + assert job.kwargs == { + "service_type_id": "starlite_saqlalchemy.service.generic.Service", + "service_method_name": "receive_callback", + "raw_obj": {"a": "b"}, + } + + +async def test_enqueue_service_callback_with_custom_job_config(monkeypatch: "MonkeyPatch") -> None: + """Tests that job enqueued with desired arguments.""" + enqueue_mock = AsyncMock() + monkeypatch.setattr(worker.queue, "enqueue", enqueue_mock) + service_instance = service.Service[Any]() + await worker.enqueue_background_task_for_service( + service_instance, + "receive_callback", + job_config=worker.JobConfig(timeout=999), + raw_obj={"a": "b"}, + ) + enqueue_mock.assert_called_once() + assert isinstance(enqueue_mock.mock_calls[0].args[0], Job) + job = enqueue_mock.mock_calls[0].args[0] + assert job.function == worker.make_service_callback.__qualname__ + assert job.timeout == 999 + assert job.kwargs == { + "service_type_id": "starlite_saqlalchemy.service.generic.Service", + "service_method_name": "receive_callback", + "raw_obj": {"a": "b"}, + } diff --git a/tests/unit/sentry/conftest.py b/tests/unit/sentry/conftest.py new file mode 100644 index 00000000..3c91aba5 --- /dev/null +++ b/tests/unit/sentry/conftest.py @@ -0,0 +1,4 @@ +from starlite_saqlalchemy.constants import IS_SENTRY_SDK_INSTALLED + +if not IS_SENTRY_SDK_INSTALLED: + collect_ignore_glob = ["*"] diff --git a/tests/unit/test_sentry.py b/tests/unit/sentry/test_sentry.py similarity index 99% rename from tests/unit/test_sentry.py rename to tests/unit/sentry/test_sentry.py index 398c09e9..90cdb353 100644 --- a/tests/unit/test_sentry.py +++ b/tests/unit/sentry/test_sentry.py @@ -1,5 +1,4 @@ """Tests for sentry integration.""" - from typing import TYPE_CHECKING import pytest diff --git a/tests/unit/sqlalchemy/conftest.py b/tests/unit/sqlalchemy/conftest.py new file mode 100644 index 00000000..7b03fcb2 --- /dev/null +++ b/tests/unit/sqlalchemy/conftest.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +from asyncpg.pgproto import pgproto + +from starlite_saqlalchemy.constants import IS_SQLALCHEMY_INSTALLED + +if TYPE_CHECKING: + from typing import Any + + from starlite_saqlalchemy.testing.generic_mock_repository import ( + GenericMockRepository, + ) + from tests.utils.domain.authors import Author + from tests.utils.domain.books import Book + +if not IS_SQLALCHEMY_INSTALLED: + collect_ignore_glob = ["*"] + + +@pytest.fixture(name="authors") +def fx_authors(raw_authors: list[dict[str, Any]]) -> list[Author]: + """Collection of parsed Author models.""" + from tests.utils.domain import authors + + mapped_authors = [authors.ReadDTO(**raw).to_mapped() for raw in raw_authors] + # convert these to pgproto UUIDs as that is what we get back from sqlalchemy + for author in mapped_authors: + author.id = pgproto.UUID(str(author.id)) + return mapped_authors + + +@pytest.fixture(name="books") +def fx_books(raw_books: list[dict[str, Any]]) -> list[Book]: + """Collection of parsed Book models.""" + from tests.utils.domain import books + + mapped_books = [books.ReadDTO(**raw).to_mapped() for raw in raw_books] + # convert these to pgproto UUIDs as that is what we get back from sqlalchemy + for book in mapped_books: + book.id = pgproto.UUID(str(book.id)) + return mapped_books + + +@pytest.fixture(name="author_repository_type") +def fx_author_repository_type( + authors: list[Author], monkeypatch: pytest.MonkeyPatch +) -> type[GenericMockRepository[Author]]: + from starlite_saqlalchemy.testing.generic_mock_repository import ( + GenericMockRepository, + ) + from tests.utils.domain.authors import Author, Service + + """Mock Author repository, pre-seeded with collection data.""" + repo = GenericMockRepository[Author] + repo.seed_collection(authors) + monkeypatch.setattr(Service, "repository_type", repo) + return repo + + +@pytest.fixture(name="author_repository") +def fx_author_repository( + author_repository_type: type[GenericMockRepository[Author]], +) -> GenericMockRepository[Author]: + """Mock Author repository instance.""" + return author_repository_type() + + +@pytest.fixture(name="book_repository_type") +def fx_book_repository_type( + books: list[Book], monkeypatch: pytest.MonkeyPatch +) -> type[GenericMockRepository[Book]]: + """Mock Book repository, pre-seeded with collection data.""" + from starlite_saqlalchemy.testing.generic_mock_repository import ( + GenericMockRepository, + ) + from tests.utils.domain.books import Book, Service + + class BookRepository(GenericMockRepository[Book]): + """Mock book repo.""" + + model_type = Book + + BookRepository.seed_collection(books) + monkeypatch.setattr(Service, "repository_type", BookRepository) + return BookRepository + + +@pytest.fixture(name="book_repository") +def fx_book_repository( + book_repository_type: type[GenericMockRepository[Book]], +) -> GenericMockRepository[Book]: + """Mock Book repo instance.""" + return book_repository_type() diff --git a/tests/unit/repository/test_abc.py b/tests/unit/sqlalchemy/repository/test_abc.py similarity index 94% rename from tests/unit/repository/test_abc.py rename to tests/unit/sqlalchemy/repository/test_abc.py index f364bd77..996d1b6f 100644 --- a/tests/unit/repository/test_abc.py +++ b/tests/unit/sqlalchemy/repository/test_abc.py @@ -7,7 +7,7 @@ import pytest from starlite_saqlalchemy.exceptions import NotFoundError -from starlite_saqlalchemy.testing import GenericMockRepository +from starlite_saqlalchemy.testing.generic_mock_repository import GenericMockRepository if TYPE_CHECKING: from pytest import MonkeyPatch diff --git a/tests/unit/sqlalchemy/repository/test_generic_mock_repository.py b/tests/unit/sqlalchemy/repository/test_generic_mock_repository.py new file mode 100644 index 00000000..97195c1a --- /dev/null +++ b/tests/unit/sqlalchemy/repository/test_generic_mock_repository.py @@ -0,0 +1,69 @@ +# pylint: disable=wrong-import-position,wrong-import-order +from __future__ import annotations + +import pytest + +from starlite_saqlalchemy.exceptions import ConflictError, StarliteSaqlalchemyError +from starlite_saqlalchemy.testing.generic_mock_repository import GenericMockRepository +from tests.utils.domain.authors import Author +from tests.utils.domain.books import Book + + +async def test_repo_raises_conflict_if_add_with_id( + authors: list[Author], + author_repository: GenericMockRepository[Author], +) -> None: + """Test mock repo raises conflict if add identified entity.""" + with pytest.raises(ConflictError): + await author_repository.add(authors[0]) + + +def test_generic_mock_repository_parametrization() -> None: + """Test that the mock repository handles multiple types.""" + author_repo = GenericMockRepository[Author] + book_repo = GenericMockRepository[Book] + assert author_repo.model_type is Author # type:ignore[misc] + assert book_repo.model_type is Book # type:ignore[misc] + + +def test_generic_mock_repository_seed_collection( + author_repository_type: type[GenericMockRepository[Author]], +) -> None: + """Test seeding instances.""" + author_repository_type.seed_collection([Author(id="abc")]) + assert "abc" in author_repository_type.collection + + +def test_generic_mock_repository_clear_collection( + author_repository_type: type[GenericMockRepository[Author]], +) -> None: + """Test clearing collection for type.""" + author_repository_type.clear_collection() + assert not author_repository_type.collection + + +def test_generic_mock_repository_filter_collection_by_kwargs( + author_repository: GenericMockRepository[Author], +) -> None: + """Test filtering the repository collection by kwargs.""" + author_repository.filter_collection_by_kwargs(name="Leo Tolstoy") + assert len(author_repository.collection) == 1 + assert list(author_repository.collection.values())[0].name == "Leo Tolstoy" + + +def test_generic_mock_repository_filter_collection_by_kwargs_and_semantics( + author_repository: GenericMockRepository[Author], +) -> None: + """Test that filtering by kwargs has `AND` semantics when multiple kwargs, + not `OR`.""" + author_repository.filter_collection_by_kwargs(name="Agatha Christie", dob="1828-09-09") + assert len(author_repository.collection) == 0 + + +def test_generic_mock_repository_raises_repository_exception_if_named_attribute_doesnt_exist( + author_repository: GenericMockRepository[Author], +) -> None: + """Test that a repo exception is raised if a named attribute doesn't + exist.""" + with pytest.raises(StarliteSaqlalchemyError): + author_repository.filter_collection_by_kwargs(cricket="ball") diff --git a/tests/unit/repository/test_sqlalchemy.py b/tests/unit/sqlalchemy/repository/test_sqlalchemy.py similarity index 100% rename from tests/unit/repository/test_sqlalchemy.py rename to tests/unit/sqlalchemy/repository/test_sqlalchemy.py diff --git a/tests/unit/test_db.py b/tests/unit/sqlalchemy/test_db.py similarity index 100% rename from tests/unit/test_db.py rename to tests/unit/sqlalchemy/test_db.py diff --git a/tests/unit/test_dto.py b/tests/unit/sqlalchemy/test_dto.py similarity index 99% rename from tests/unit/test_dto.py rename to tests/unit/sqlalchemy/test_dto.py index c34b52b7..5c6850b2 100644 --- a/tests/unit/test_dto.py +++ b/tests/unit/sqlalchemy/test_dto.py @@ -1,5 +1,6 @@ """Tests for the dto factory.""" # pylint: disable=missing-class-docstring,invalid-name + from datetime import date, datetime, timedelta from typing import TYPE_CHECKING, Annotated, Any, ClassVar from uuid import UUID, uuid4 diff --git a/tests/unit/sqlalchemy/test_health_check.py b/tests/unit/sqlalchemy/test_health_check.py new file mode 100644 index 00000000..ac9a4731 --- /dev/null +++ b/tests/unit/sqlalchemy/test_health_check.py @@ -0,0 +1,29 @@ +"""Tests for application health check behavior.""" +from typing import TYPE_CHECKING +from unittest.mock import AsyncMock + +from starlite.status_codes import HTTP_200_OK + +from starlite_saqlalchemy import settings +from starlite_saqlalchemy.health import HealthController, HealthResource +from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyHealthCheck + +if TYPE_CHECKING: + from pytest import MonkeyPatch + from starlite.testing import TestClient + + +def test_health_check(client: "TestClient", monkeypatch: "MonkeyPatch") -> None: + """Test health check success response. + + Checks that we call the repository method and the response content. + """ + health_check = SQLAlchemyHealthCheck() + monkeypatch.setattr(HealthController, "health_checks", [health_check]) + repo_health_mock = AsyncMock(return_value=True) + monkeypatch.setattr(health_check, "ready", repo_health_mock) + resp = client.get(settings.api.HEALTH_PATH) + assert resp.status_code == HTTP_200_OK + health = HealthResource(app=settings.app, health={health_check.name: True}) + assert resp.json() == health.dict() + repo_health_mock.assert_called_once() diff --git a/tests/unit/test_orm.py b/tests/unit/sqlalchemy/test_orm.py similarity index 99% rename from tests/unit/test_orm.py rename to tests/unit/sqlalchemy/test_orm.py index 52ebb942..975afaa0 100644 --- a/tests/unit/test_orm.py +++ b/tests/unit/sqlalchemy/test_orm.py @@ -1,4 +1,5 @@ """Tests for application ORM configuration.""" + import datetime from unittest.mock import MagicMock diff --git a/tests/unit/test_service.py b/tests/unit/sqlalchemy/test_service.py similarity index 51% rename from tests/unit/test_service.py rename to tests/unit/sqlalchemy/test_service.py index e9f4b57d..9aa77b26 100644 --- a/tests/unit/test_service.py +++ b/tests/unit/sqlalchemy/test_service.py @@ -2,21 +2,20 @@ from __future__ import annotations from datetime import date -from typing import TYPE_CHECKING, Any -from unittest.mock import AsyncMock +from typing import TYPE_CHECKING from uuid import uuid4 import pytest -from saq import Job -from starlite_saqlalchemy import db, service, worker +from starlite_saqlalchemy import service from starlite_saqlalchemy.exceptions import NotFoundError from tests.utils import domain if TYPE_CHECKING: - from pytest import MonkeyPatch - from starlite_saqlalchemy.testing import GenericMockRepository + from starlite_saqlalchemy.testing.generic_mock_repository import ( + GenericMockRepository, + ) @pytest.fixture(autouse=True) @@ -86,71 +85,6 @@ async def test_service_delete() -> None: assert author is deleted -async def test_make_service_callback( - raw_authors: list[dict[str, Any]], monkeypatch: "MonkeyPatch" -) -> None: - """Tests loading and retrieval of service object types.""" - recv_cb_mock = AsyncMock() - monkeypatch.setattr(service.Service, "receive_callback", recv_cb_mock, raising=False) - await service.make_service_callback( - {}, - service_type_id="tests.utils.domain.authors.Service", - service_method_name="receive_callback", - raw_obj=raw_authors[0], - ) - recv_cb_mock.assert_called_once_with(raw_obj=raw_authors[0]) - - -async def test_make_service_callback_raises_runtime_error( - raw_authors: list[dict[str, Any]] -) -> None: - """Tests loading and retrieval of service object types.""" - with pytest.raises(KeyError): - await service.make_service_callback( - {}, - service_type_id="tests.utils.domain.LSKDFJ", - service_method_name="receive_callback", - raw_obj=raw_authors[0], - ) - - -async def test_enqueue_service_callback(monkeypatch: "MonkeyPatch") -> None: - """Tests that job enqueued with desired arguments.""" - enqueue_mock = AsyncMock() - monkeypatch.setattr(worker.queue, "enqueue", enqueue_mock) - service_instance = domain.authors.Service(session=db.async_session_factory()) - await service_instance.enqueue_background_task("receive_callback", raw_obj={"a": "b"}) - enqueue_mock.assert_called_once() - assert isinstance(enqueue_mock.mock_calls[0].args[0], Job) - job = enqueue_mock.mock_calls[0].args[0] - assert job.function == service.make_service_callback.__qualname__ - assert job.kwargs == { - "service_type_id": "tests.utils.domain.authors.Service", - "service_method_name": "receive_callback", - "raw_obj": {"a": "b"}, - } - - -async def test_enqueue_service_callback_with_custom_job_config(monkeypatch: "MonkeyPatch") -> None: - """Tests that job enqueued with desired arguments.""" - enqueue_mock = AsyncMock() - monkeypatch.setattr(worker.queue, "enqueue", enqueue_mock) - service_instance = domain.authors.Service(session=db.async_session_factory()) - await service_instance.enqueue_background_task( - "receive_callback", job_config=worker.JobConfig(timeout=999), raw_obj={"a": "b"} - ) - enqueue_mock.assert_called_once() - assert isinstance(enqueue_mock.mock_calls[0].args[0], Job) - job = enqueue_mock.mock_calls[0].args[0] - assert job.function == service.make_service_callback.__qualname__ - assert job.timeout == 999 - assert job.kwargs == { - "service_type_id": "tests.utils.domain.authors.Service", - "service_method_name": "receive_callback", - "raw_obj": {"a": "b"}, - } - - async def test_service_new_context_manager() -> None: """Simple test of `Service.new()` context manager behavior.""" async with service.Service[domain.authors.Author].new() as service_obj: diff --git a/tests/unit/test_testing.py b/tests/unit/sqlalchemy/test_testing.py similarity index 69% rename from tests/unit/test_testing.py rename to tests/unit/sqlalchemy/test_testing.py index 2474f3ba..d8764366 100644 --- a/tests/unit/test_testing.py +++ b/tests/unit/sqlalchemy/test_testing.py @@ -1,4 +1,5 @@ """Test testing module.""" +# pylint: disable=wrong-import-position,wrong-import-order from __future__ import annotations from typing import TYPE_CHECKING @@ -13,10 +14,7 @@ ) from starlite_saqlalchemy import testing -from starlite_saqlalchemy.exceptions import ConflictError, StarliteSaqlalchemyError -from tests.utils.domain.authors import Author from tests.utils.domain.authors import Service as AuthorService -from tests.utils.domain.books import Book if TYPE_CHECKING: from typing import Any @@ -24,65 +22,7 @@ from pytest import MonkeyPatch from starlite import TestClient - -async def test_repo_raises_conflict_if_add_with_id( - authors: list[Author], - author_repository: testing.GenericMockRepository[Author], -) -> None: - """Test mock repo raises conflict if add identified entity.""" - with pytest.raises(ConflictError): - await author_repository.add(authors[0]) - - -def test_generic_mock_repository_parametrization() -> None: - """Test that the mock repository handles multiple types.""" - author_repo = testing.GenericMockRepository[Author] - book_repo = testing.GenericMockRepository[Book] - assert author_repo.model_type is Author # type:ignore[misc] - assert book_repo.model_type is Book # type:ignore[misc] - - -def test_generic_mock_repository_seed_collection( - author_repository_type: type[testing.GenericMockRepository[Author]], -) -> None: - """Test seeding instances.""" - author_repository_type.seed_collection([Author(id="abc")]) - assert "abc" in author_repository_type.collection - - -def test_generic_mock_repository_clear_collection( - author_repository_type: type[testing.GenericMockRepository[Author]], -) -> None: - """Test clearing collection for type.""" - author_repository_type.clear_collection() - assert not author_repository_type.collection - - -def test_generic_mock_repository_filter_collection_by_kwargs( - author_repository: testing.GenericMockRepository[Author], -) -> None: - """Test filtering the repository collection by kwargs.""" - author_repository.filter_collection_by_kwargs(name="Leo Tolstoy") - assert len(author_repository.collection) == 1 - assert list(author_repository.collection.values())[0].name == "Leo Tolstoy" - - -def test_generic_mock_repository_filter_collection_by_kwargs_and_semantics( - author_repository: testing.GenericMockRepository[Author], -) -> None: - """Test that filtering by kwargs has `AND` semantics when multiple kwargs, - not `OR`.""" - author_repository.filter_collection_by_kwargs(name="Agatha Christie", dob="1828-09-09") - assert len(author_repository.collection) == 0 - - -def test_generic_mock_repository_raises_repository_exception_if_named_attribute_doesnt_exist( - author_repository: testing.GenericMockRepository[Author], -) -> None: - """Test that a repo exception is raised if a named attribute doesn't - exist.""" - with pytest.raises(StarliteSaqlalchemyError): - author_repository.filter_collection_by_kwargs(cricket="ball") + from tests.utils.domain.authors import Author @pytest.fixture(name="mock_response") @@ -130,6 +70,7 @@ async def test_tester_get_collection_request_service_method_patch( tester: testing.ControllerTest, mock_response: MagicMock ) -> None: """Test that the "list" service method has been patched.""" + mock_response.json.return_value = tester.raw_collection tester.test_get_collection() assert "._list" in str(AuthorService.list) diff --git a/tests/unit/test_health.py b/tests/unit/test_health_check.py similarity index 59% rename from tests/unit/test_health.py rename to tests/unit/test_health_check.py index ad6d26e0..a41fb784 100644 --- a/tests/unit/test_health.py +++ b/tests/unit/test_health_check.py @@ -14,7 +14,6 @@ HealthController, HealthResource, ) -from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyHealthCheck if TYPE_CHECKING: from pytest import MonkeyPatch @@ -26,42 +25,41 @@ def test_health_check(client: "TestClient", monkeypatch: "MonkeyPatch") -> None: Checks that we call the repository method and the response content. """ - repo_health_mock = AsyncMock(return_value=True) - monkeypatch.setattr(SQLAlchemyHealthCheck, "ready", repo_health_mock) + health_check = AppHealthCheck() + monkeypatch.setattr(HealthController, "health_checks", [health_check]) resp = client.get(settings.api.HEALTH_PATH) assert resp.status_code == HTTP_200_OK - health = HealthResource( - app=settings.app, - health={SQLAlchemyHealthCheck.name: True, AppHealthCheck.name: True}, - ) + health = HealthResource(app=settings.app, health={health_check.name: True}) assert resp.json() == health.dict() - repo_health_mock.assert_called_once() -def test_health_check_false_response(client: "TestClient", monkeypatch: "MonkeyPatch") -> None: - """Test health check response if check method returns `False`""" - repo_health_mock = AsyncMock(return_value=False) - monkeypatch.setattr(SQLAlchemyHealthCheck, "ready", repo_health_mock) - resp = client.get(settings.api.HEALTH_PATH) - assert resp.status_code == HTTP_503_SERVICE_UNAVAILABLE - health = HealthResource( - app=settings.app, - health={SQLAlchemyHealthCheck.name: False, AppHealthCheck.name: True}, - ) - assert resp.json() == health.dict() +async def test_health_check_live() -> None: + """Test expected result of calling `live()` health check method.""" + health_check = AppHealthCheck() + assert await health_check.live() is True -def test_health_check_exception_raised(client: "TestClient", monkeypatch: "MonkeyPatch") -> None: - """Test expected response from check if exception raised in handler.""" - repo_health_mock = AsyncMock(side_effect=ConnectionError) - monkeypatch.setattr(SQLAlchemyHealthCheck, "ready", repo_health_mock) +def test_health_check_failed(client: "TestClient", monkeypatch: "MonkeyPatch") -> None: + """Test health check response if check method returns `False`""" + health_check = AppHealthCheck() + monkeypatch.setattr(HealthController, "health_checks", [health_check]) + monkeypatch.setattr(health_check, "ready", AsyncMock(side_effect=RuntimeError)) resp = client.get(settings.api.HEALTH_PATH) assert resp.status_code == HTTP_503_SERVICE_UNAVAILABLE - health = HealthResource( - app=settings.app, - health={SQLAlchemyHealthCheck.name: False, AppHealthCheck.name: True}, - ) - assert resp.json() == health.dict() + HealthResource(app=settings.app, health={health_check.name: True}) + assert resp.json() == { + "app": { + "BUILD_NUMBER": "", + "CHECK_DB_READY": True, + "CHECK_REDIS_READY": True, + "DEBUG": False, + "ENVIRONMENT": "test", + "TEST_ENVIRONMENT_NAME": "test", + "LOCAL_ENVIRONMENT_NAME": "local", + "NAME": "my-starlite-app", + }, + "health": {"app": False}, + } def test_health_custom_health_check(client: "TestClient", monkeypatch: "MonkeyPatch") -> None: @@ -76,16 +74,13 @@ async def ready(self) -> bool: """Readiness check.""" return False - HealthController.health_checks.append(MyHealthCheck()) - repo_health_mock = AsyncMock(return_value=True) - monkeypatch.setattr(SQLAlchemyHealthCheck, "ready", repo_health_mock) + monkeypatch.setattr(HealthController, "health_checks", [AppHealthCheck(), MyHealthCheck()]) resp = client.get(settings.api.HEALTH_PATH) assert resp.status_code == HTTP_503_SERVICE_UNAVAILABLE health = HealthResource( app=settings.app, health={ AppHealthCheck.name: True, - SQLAlchemyHealthCheck.name: True, MyHealthCheck.name: False, }, ) diff --git a/tests/unit/test_init_plugin.py b/tests/unit/test_init_plugin.py index d86ed868..f7127f20 100644 --- a/tests/unit/test_init_plugin.py +++ b/tests/unit/test_init_plugin.py @@ -1,4 +1,5 @@ """Tests for init_plugin.py.""" +# pylint:disable=import-outside-toplevel from __future__ import annotations from typing import TYPE_CHECKING @@ -6,9 +7,9 @@ import pytest from starlite import Starlite -from starlite.cache import SimpleCacheBackend -from starlite_saqlalchemy import init_plugin, sentry +from starlite_saqlalchemy import init_plugin +from starlite_saqlalchemy.constants import IS_SAQ_INSTALLED, IS_SENTRY_SDK_INSTALLED if TYPE_CHECKING: from typing import Any @@ -16,52 +17,15 @@ from pytest import MonkeyPatch -def test_config_switches() -> None: - """Tests that the app produced with all config switches off is as we - expect.""" - config = init_plugin.PluginConfig( - do_after_exception=False, - do_cache=False, - do_compression=False, - # pyright reckons this parameter doesn't exist, I beg to differ - do_collection_dependencies=False, # pyright:ignore - do_exception_handlers=False, - do_health_check=False, - do_logging=False, - do_openapi=False, - do_sentry=False, - do_set_debug=False, - do_sqlalchemy_plugin=False, - do_type_encoders=False, - do_worker=False, - ) - app = Starlite( - route_handlers=[], - openapi_config=None, - on_app_init=[init_plugin.ConfigureApp(config=config)], - ) - assert app.compression_config is None - assert app.debug is False - assert app.logging_config is None - assert app.openapi_config is None - assert app.response_class is None - assert isinstance(app.cache.backend, SimpleCacheBackend) - # client.close and redis.close go in there unconditionally atm - assert len(app.on_shutdown) == 2 - assert not app.after_exception - assert not app.dependencies - assert not app.exception_handlers - assert not app.on_startup - assert not app.plugins - assert not app.routes - - +@pytest.mark.skipif(not IS_SAQ_INSTALLED, reason="saq is not installed") def test_do_worker_but_not_logging(monkeypatch: MonkeyPatch) -> None: """Tests branch where we can have the worker enabled, but logging disabled.""" + from starlite_saqlalchemy import worker + mock = MagicMock() - monkeypatch.setattr(init_plugin, "create_worker_instance", mock) - config = init_plugin.PluginConfig(do_logging=False) + monkeypatch.setattr(worker, "create_worker_instance", mock) + config = init_plugin.PluginConfig(do_logging=False, do_worker=True) Starlite(route_handlers=[], on_app_init=[init_plugin.ConfigureApp(config=config)]) mock.assert_called_once() call = mock.mock_calls[0] @@ -84,12 +48,15 @@ def test_ensure_list(in_: Any, out: Any) -> None: assert init_plugin.ConfigureApp._ensure_list(in_) == out +@pytest.mark.skipif(not IS_SENTRY_SDK_INSTALLED, reason="sentry_sdk is not installed") @pytest.mark.parametrize( ("env", "exp"), [("dev", True), ("prod", True), ("local", False), ("test", False)] ) def test_sentry_environment_gate(env: str, exp: bool, monkeypatch: MonkeyPatch) -> None: """Test that the sentry integration is configured under different environment names.""" + from starlite_saqlalchemy import sentry + monkeypatch.setattr(init_plugin, "IS_LOCAL_ENVIRONMENT", env == "local") monkeypatch.setattr(init_plugin, "IS_TEST_ENVIRONMENT", env == "test") app = Starlite(route_handlers=[], on_app_init=[init_plugin.ConfigureApp()]) diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py deleted file mode 100644 index 304e9251..00000000 --- a/tests/unit/test_worker.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Tests for the SAQ async worker functionality.""" -from __future__ import annotations - -from asyncpg.pgproto import pgproto - -from starlite_saqlalchemy import worker -from tests.utils.domain.authors import Author, ReadDTO - - -def test_worker_decoder_handles_pgproto_uuid() -> None: - """Test that the decoder can handle pgproto.UUID instances.""" - pg_uuid = pgproto.UUID("0448bde2-7c69-4e6b-9c03-7b217e3b563d") - encoded = worker.encoder.encode(pg_uuid) - assert encoded == b'"0448bde2-7c69-4e6b-9c03-7b217e3b563d"' - - -def test_worker_decoder_handles_pydantic_models(authors: list[Author]) -> None: - """Test that the decoder we use for SAQ will encode a pydantic model.""" - pydantic_model = ReadDTO.from_orm(authors[0]) - encoded = worker.encoder.encode(pydantic_model) - assert ( - encoded - == b'{"id":"97108ac1-ffcb-411d-8b1e-d9183399f63b","created":"0001-01-01T00:00:00","updated":"0001-01-01T00:00:00","name":"Agatha Christie","dob":"1890-09-15"}' - ) diff --git a/tests/utils/app.py b/tests/utils/app.py index f821df42..81750f7b 100644 --- a/tests/utils/app.py +++ b/tests/utils/app.py @@ -10,7 +10,4 @@ def create_app() -> Starlite: """App for our test domain.""" - return Starlite( - route_handlers=[controllers.create_router()], - on_app_init=[ConfigureApp()], - ) + return Starlite(route_handlers=[controllers.create_router()], on_app_init=[ConfigureApp()]) diff --git a/tests/utils/domain/authors.py b/tests/utils/domain/authors.py index 1af36f43..45936e5a 100644 --- a/tests/utils/domain/authors.py +++ b/tests/utils/domain/authors.py @@ -6,7 +6,9 @@ from sqlalchemy.orm import Mapped -from starlite_saqlalchemy import db, dto, repository, service +from starlite_saqlalchemy import db, dto +from starlite_saqlalchemy.repository.sqlalchemy import SQLAlchemyRepository +from starlite_saqlalchemy.service.sqlalchemy import RepositoryService class Author(db.orm.Base): @@ -16,13 +18,13 @@ class Author(db.orm.Base): dob: Mapped[date] -class Repository(repository.sqlalchemy.SQLAlchemyRepository[Author]): +class Repository(SQLAlchemyRepository[Author]): """Author repository.""" model_type = Author -class Service(service.RepositoryService[Author]): +class Service(RepositoryService[Author]): """Author service object.""" repository_type = Repository diff --git a/tests/utils/domain/books.py b/tests/utils/domain/books.py index 845f802a..51f974ab 100644 --- a/tests/utils/domain/books.py +++ b/tests/utils/domain/books.py @@ -7,8 +7,9 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship -from starlite_saqlalchemy import db, dto, service +from starlite_saqlalchemy import db, dto from starlite_saqlalchemy.repository.sqlalchemy import SQLAlchemyRepository +from starlite_saqlalchemy.service.sqlalchemy import RepositoryService from tests.utils.domain.authors import Author @@ -28,7 +29,7 @@ class Repository(SQLAlchemyRepository[Book]): model_type = Book -class Service(service.RepositoryService[Book]): +class Service(RepositoryService[Book]): """Book service.""" repository_type = Repository diff --git a/tox.ini b/tox.ini index 93c842a5..b8da7704 100644 --- a/tox.ini +++ b/tox.ini @@ -1,25 +1,27 @@ [gh-actions] python = - 3.10: py310 - 3.11: py311,pytest-plugin,integration + 3.10: py310,no-extras + 3.11: py311,no-extras,pytest-plugin,integration [tox] -envlist = pylint,mypy,pyright,py310,py311,pytest-plugin,integration,coverage +envlist = pylint,mypy,pyright,py310,py311,no-extras,pytest-plugin,integration,coverage isolated_build = true [testenv] -deps = - -r{toxinidir}/requirements.dev.txt +deps = -r{toxinidir}/requirements.dev.txt -commands = - coverage run -p -m pytest {posargs} +commands = coverage run -p -m pytest {posargs} + +[testenv:py3{10,11}] +deps = -r {toxinidir}/requirements.dev-extras.txt [testenv:pytest-plugin] basepython = python3.11 +deps = -r requirements.dev-extras.txt commands = coverage run -p -m pytest tests/pytest_plugin {posargs} [testenv:coverage] -depends = py310,py311,pytest-plugin +depends = py310,py311,no-extras,pytest-plugin basepython = python3.11 commands = coverage combine @@ -27,21 +29,12 @@ commands = coverage xml parallel_show_output = true -[testenv:refurb] -basepython = python3.11 -deps = - refurb - {[testenv]deps} -commands = - python -m refurb examples/ src/ tests/ - [testenv:pylint] basepython = python3.11 deps = pylint - {[testenv]deps} -commands = - python -m pylint src/ tests/ + -r requirements.dev-extras.txt +commands = python -m pylint src/ tests/ [testenv:mypy] basepython = python3.11 @@ -49,9 +42,8 @@ deps = asyncpg-stubs mypy types-redis - {[testenv]deps} -commands = - python -m mypy examples/ src/ tests/ + -r requirements.dev-extras.txt +commands = python -m mypy examples/ src/ tests/ [testenv:pyright] basepython = python3.11 @@ -59,25 +51,23 @@ deps = asyncpg-stubs pyright types-redis - {[testenv]deps} -commands = - pyright examples/ src/ tests/ + -r requirements.dev-extras.txt +commands = pyright examples/ src/ tests/ [testenv:integration] basepython = python3.11 deps = docker-compose - {[testenv]deps} -allowlist_externals = - docker -commands = - pytest tests/integration {posargs} + -r requirements.dev-extras.txt +allowlist_externals = docker +commands = pytest tests/integration {posargs} + +[testenv:no-extras] +basepython = python3.11 +commands = coverage run -p -m pytest {posargs} [testenv:docs] basepython = python3.11 -passenv = - HOME -deps = - -r{toxinidir}/requirements.docs.txt -commands = - mike {posargs: serve} +passenv = HOME +deps = -r{toxinidir}/requirements.docs.txt +commands = mike {posargs: serve}