diff --git a/.flake8 b/.flake8 index 6f3cd761..f7517662 100644 --- a/.flake8 +++ b/.flake8 @@ -6,12 +6,13 @@ type-checking-exempt-modules = from sqlalchemy.orm per-file-ignores = examples/dto/*:T201,TC examples/tests/*:SCS108,PT013 + src/pytest_starlite_saqlalchemy/__init__.py:F401 src/starlite_saqlalchemy/dependencies.py:TC src/starlite_saqlalchemy/health.py:TC src/starlite_saqlalchemy/repository/filters.py:TC src/starlite_saqlalchemy/scripts.py:T201 src/starlite_saqlalchemy/settings.py:TC - src/starlite_saqlalchemy/testing.py:SCS108 + src/starlite_saqlalchemy/testing/controller_test.py:SCS108 src/starlite_saqlalchemy/users/controllers.py:TC tests/*:SCS108,PT013 tests/integration/test_tests.py:TC002,SCS108 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e1b5f8c..a288b8d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: [main] pull_request: - branches: [main] env: FORCE_COLOR: "1" @@ -35,7 +34,7 @@ jobs: with: python-version: ${{env.PYTHON_LATEST}} - name: Install Dependencies - run: python -m pip install --upgrade wheel tox + run: python -m pip install --upgrade -r requirements.ci.txt - run: python -m tox -e pylint mypy: name: mypy @@ -56,7 +55,7 @@ jobs: with: python-version: ${{env.PYTHON_LATEST}} - name: Install Dependencies - run: python -m pip install --upgrade wheel tox + run: python -m pip install --upgrade -r requirements.ci.txt - run: python -m tox -e mypy pyright: name: pyright @@ -78,7 +77,7 @@ jobs: with: python-version: ${{env.PYTHON_LATEST}} - name: Install Dependencies - run: python -m pip install --upgrade wheel tox + run: python -m pip install --upgrade -r requirements.ci.txt - run: python -m tox -e pyright tests: name: tests on ${{matrix.python-version}} @@ -108,7 +107,7 @@ jobs: with: python-version: ${{matrix.python-version}} - name: Install Dependencies - run: python -m pip install --upgrade wheel tox tox-gh-actions + run: python -m pip install --upgrade -r requirements.ci.txt tox-gh-actions - run: python -m tox - name: Upload Coverage Data uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # @v3.1.1 @@ -133,7 +132,7 @@ jobs: - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 #v4.4.0 with: python-version: ${{env.PYTHON_LATEST}} - - run: python -m pip install --upgrade wheel tox + - run: python -m pip install --upgrade -r requirements.ci.txt - name: Download coverage data uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # @v3.0.1 with: diff --git a/docs/testing/pytest_plugin.md b/docs/testing/pytest_plugin.md new file mode 100644 index 00000000..bf6bf6d1 --- /dev/null +++ b/docs/testing/pytest_plugin.md @@ -0,0 +1,137 @@ +# Pytest Plugin + +The nature of applications built with the `starlite-saqlalchemy` pattern is that they rely heavily +on connected services. + +Abstraction of [PostgreSQL][2] and [Redis][3] connectivity boilerplate is a nice convenience, +however to successfully patch the application for testing requires deeper knowledge of the +implementation than would be otherwise necessary. + +So, `starlite-saqlalchemy` ships with a selection of [pytest fixtures][1] that are often necessary +when building applications such as these. + +## `app` + +The `app` fixture provides an instance of a `Starlite` application. + +```python +from __future__ import annotations + +from starlite import Starlite + + +def test_app_fixture(app: Starlite) -> None: + assert isinstance(app, Starlite) +``` + +The value of Pytest ini option, `test_app` is used to determine the application to load. + +```toml +# pyproject.toml + +[tool.pytest.ini_options] +test_app = "app.main:create_app" +``` + +If no value is configured for the `test_app` ini option, the default location of +`"app.main:create_app"` is searched. + +The value of the `test_app` ini option can either point to an application factory or `Starlite` +instance. + +If the object found at the import path is not a `Starlite` instance, the fixture assumes it is +an application factory, and will call the object and return the response. + +The value of `test_app` is resolved using the uvicorn `import_from_string()` function, so it +supports the same format as `uvicorn` supports for its `app` and `factory` parameters. + +## `client` + +A `starlite.testing.TestClient` instance, wired to the same application that is produced by the +`app` fixture. + +## `cap_logger` + +The `cap_logger` fixture provides an instance of [`structlog.testing.CapturingLogger`][4]. + +```python +from __future__ import annotations + +from typing import TYPE_CHECKING + +from structlog.testing import CapturedCall + +if TYPE_CHECKING: + from structlog.testing import CapturingLogger + + +def test_app_fixture(cap_logger: CapturingLogger) -> None: + cap_logger.info("hello") + cap_logger.info("hello", when="again") + assert cap_logger.calls == [ + CapturedCall(method_name="info", args=("hello",), kwargs={}), + CapturedCall(method_name="info", args=("hello",), kwargs={"when": "again"}), + ] +``` + +The `cap_logger` fixture will capture any `structlog` calls made by the starlite application or the +SAQ worker, so that they can be inspected as part of tests. + +```python +from __future__ import annotations + +from typing import TYPE_CHECKING + +from httpx import AsyncClient + +if TYPE_CHECKING: + from starlite import Starlite + from structlog.testing import CapturingLogger + + +async def test_health_logging_skipped( + app: Starlite, cap_logger: CapturingLogger +) -> None: + """Test that calls to the health check route are not logged.""" + + async with AsyncClient(app=app, base_url="http://testserver") as client: + response = await client.get("/health") + assert response.status_code == 200 + + assert [] == cap_logger.calls +``` + +## is_unit_test + +The `is_unit_test` fixture returns a `bool` that indicates if the test suite believes it is running +a unit test, or an integration test. + +To determine this, we compare the path of the running test to the value of the Pytest ini option +`unit_test_pattern`, which by default is `"^.*/tests/unit/.*$"`. + +This fixture is used to make fixtures behave differently between unit and integration test contexts. + +## _patch_http_close + +This is an [`autouse` fixture][5], that prevents HTTP clients that are defined in the global scope +from being closed. + +The application is configured to close all instantiated HTTP clients on app shutdown, however when +apps are defined in a global/class scope, a test that runs after the first application shutdown in +the test suite would fail. + +## _patch_sqlalchemy_plugin + +This is an [`autouse` fixture][5], that mocks out the `on_shutdown` method of the SQLAlchemy config +object for unit tests. + +## _patch_worker + +This is an [`autouse` fixture][5], that mocks out the `on_app_startup` and `stop` methods of +`worker.Worker` type for unit tests. + +[1]: https://docs.pytest.org/en/latest/explanation/fixtures.html#about-fixtures +[2]: https://www.postgresql.org/ +[3]: https://redis.io +[4]: https://www.structlog.org/en/stable/api.html#structlog.testing.CapturingLogger +[5]: https://docs.pytest.org/en/6.2.x/fixture.html#autouse-fixtures-fixtures-you-don-t-have-to-request diff --git a/mkdocs.yml b/mkdocs.yml index 763726ef..cc396d14 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,6 +7,8 @@ nav: - DTOs: dto.md - Async Worker: async_worker.md - Logging: logging.md + - Testing: + - Pytest Plugin: testing/pytest_plugin.md - Reference: reference/ watch: - src/starlite_saqlalchemy diff --git a/poetry.lock b/poetry.lock index a910e534..ae8e4410 100644 --- a/poetry.lock +++ b/poetry.lock @@ -140,22 +140,23 @@ python-dateutil = "*" [[package]] name = "dnspython" -version = "2.2.1" +version = "2.3.0" description = "DNS toolkit" category = "main" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.7,<4.0" files = [ - {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, - {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, + {file = "dnspython-2.3.0-py3-none-any.whl", hash = "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"}, + {file = "dnspython-2.3.0.tar.gz", hash = "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9"}, ] [package.extras] curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] -dnssec = ["cryptography (>=2.6,<37.0)"] -doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"] +dnssec = ["cryptography (>=2.6,<40.0)"] +doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.11.0)"] +doq = ["aioquic (>=0.9.20)"] idna = ["idna (>=2.1,<4.0)"] -trio = ["trio (>=0.14,<0.20)"] +trio = ["trio (>=0.14,<0.23)"] wmi = ["wmi (>=1.5.1,<2.0.0)"] [[package]] @@ -176,14 +177,14 @@ idna = ">=2.0.0" [[package]] name = "faker" -version = "15.3.4" +version = "16.4.0" description = "Faker is a Python package that generates fake data for you." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Faker-15.3.4-py3-none-any.whl", hash = "sha256:c2a2ff9dd8dfd991109b517ab98d5cb465e857acb45f6b643a0e284a9eb2cc76"}, - {file = "Faker-15.3.4.tar.gz", hash = "sha256:2d5443724f640ce07658ca8ca8bbd40d26b58914e63eec6549727869aa67e2cc"}, + {file = "Faker-16.4.0-py3-none-any.whl", hash = "sha256:5420467fad3fa582094057754e5e81326cb1f51ab822bf9df96c077cfb35ae49"}, + {file = "Faker-16.4.0.tar.gz", hash = "sha256:dcffdca8ec9a715982bcd5f53ee688dc4784cd112f9910f8f7183773eb3ec276"}, ] [package.dependencies] @@ -191,30 +192,33 @@ python-dateutil = ">=2.4" [[package]] name = "fast-query-parsers" -version = "0.2.0" +version = "0.3.0" description = "Ultra-fast query string and url-encoded form-data parsers" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "fast_query_parsers-0.2.0-cp38-abi3-macosx_10_7_x86_64.whl", hash = "sha256:4f7219e1dc7480307a32fae000470c2b8979849a34395d7787898da23e7d6a7e"}, - {file = "fast_query_parsers-0.2.0-cp38-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:301154360a39b1dc347e9a9e0cb3578c8877f63bb96367d54e8012b81a4007b3"}, - {file = "fast_query_parsers-0.2.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1556b7ab6614525d5697e9f46bd56200de505e6423ee99df38f88163df7053d"}, - {file = "fast_query_parsers-0.2.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fdec68138ed327f1b893e0a78156a8b2fcfc0753cc26ea28da53d578801ef86f"}, - {file = "fast_query_parsers-0.2.0-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:03520ba888acfd8a26a1a4cfa60c499e29db5d4d2fdcd1a8f393c786ad493d13"}, - {file = "fast_query_parsers-0.2.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9764839c444c025aaad114373e9b1e6eafd16e32321663a870811358f9d65f18"}, - {file = "fast_query_parsers-0.2.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9d86c9e09f1eebb6fa161792f6565b8b4821211ef58f054f3183c210ac01a21"}, - {file = "fast_query_parsers-0.2.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ffaf0cde56652c2b40ec9292b4abd79c2a76a0f095ffb22dbead6129f47209e"}, - {file = "fast_query_parsers-0.2.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:862705292b003d3c1f0d0219be874b586e71a3e9bc462ad24493c7de3d7ca53b"}, - {file = "fast_query_parsers-0.2.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:51335ffed2d835e5f679f8f01c593e8477b21a6e6bc90a0caa9e1eb66016e533"}, - {file = "fast_query_parsers-0.2.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:8e55c12af6235d04cd1ed0ddfbcf959bd1265b0bf6a8fa5b1481f47feb44bed7"}, - {file = "fast_query_parsers-0.2.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:325f267013388b902f9d0a95a84a128c3aeb2f1b252494e7f516dfd433c7e5a8"}, - {file = "fast_query_parsers-0.2.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2916e7fe50b91c9f0576af7c6b06677e6634a7017967fe3d2e9030a86a650afa"}, - {file = "fast_query_parsers-0.2.0-cp38-abi3-win32.whl", hash = "sha256:a5d262709e2f08a5f953070160eabe7ca3c909985026b00ee3c7d45d0bf9b9d6"}, - {file = "fast_query_parsers-0.2.0-cp38-abi3-win_amd64.whl", hash = "sha256:1a0d8b3435a6ab51ac8eb3a96eab8fc715b09e7e196ec74af48ac4b126f3d4bc"}, - {file = "fast_query_parsers-0.2.0.tar.gz", hash = "sha256:3a0105dc3f5c39befe89aaeb5ac0e71e02f9854f75fd09b4b13775085be44ab5"}, + {file = "fast_query_parsers-0.3.0-cp38-abi3-macosx_10_7_x86_64.whl", hash = "sha256:e01e9b794d6fad11207341a8e0a7b179f085c9874b3981a77f862a5e44cb241d"}, + {file = "fast_query_parsers-0.3.0-cp38-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f2ffa7f1298db8ff8e013733e32edb77509a468843f5641875386f1df9bdecb4"}, + {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1becd3a0a06b14c57ee34976bf54d08fc910d4b410170ee5c28ac8d16ef23575"}, + {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd94f7715b6c1ec57b50e63c22298253648d864c9e78f90a258ebf2c07d13a38"}, + {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:52ef0241bcadc93ef47f650070c5f31f616cd5185a3af95f8b365e1fb135d991"}, + {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:189eed1007229c1e4a032076d503e92869eebd4a1ebd2498e6bb1b1b0c525708"}, + {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97d13dcc6cad29c8ef7c6f4bb6240847b476e4eb0445ac7f59d49a11db1644e2"}, + {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:befa91ff707f77e9948759bafdfe2b743d4b587e3f7767f2b5887b1ffa37f41d"}, + {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00d3d519cfd020c7a3bd961c9568b6042f853f25e204b8a307ff5b569dee9c38"}, + {file = "fast_query_parsers-0.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a659eb2acc0aa44f4cce7a79c4e0c76f14a26026fc13db8f094a29a38cf66248"}, + {file = "fast_query_parsers-0.3.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e9b277e8281d7fe9b8e9450767cf1c1158ed729d81e03c1dfa2fe2118b22ffe0"}, + {file = "fast_query_parsers-0.3.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:0395ef9cacf49318a0f3caac4fde8a01dd7b7315c14f8a7d084965dedd764868"}, + {file = "fast_query_parsers-0.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:47639fc331e668b1be405c4e24af70ad21fc01a6847d2730803346ab9586e788"}, + {file = "fast_query_parsers-0.3.0-cp38-abi3-win32.whl", hash = "sha256:89ee582ab4b331f078eca2fbf51ce82396f749879fc30f20668b85a2896ea165"}, + {file = "fast_query_parsers-0.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:4e9dd270f085a2337fab407d230a252664f952561cb83acece343a5c6a7d8e6b"}, + {file = "fast_query_parsers-0.3.0.tar.gz", hash = "sha256:df972c0b58d0bf51fa43b67d2604ab795984015d47552d02175ebcc685e4852b"}, ] +[package.dependencies] +maturin = "*" + [[package]] name = "greenlet" version = "2.0.1" @@ -303,100 +307,101 @@ files = [ [[package]] name = "hiredis" -version = "2.1.0" +version = "2.1.1" description = "Python wrapper for hiredis" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "hiredis-2.1.0-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:7b339a7542a3f6a10b3bbc157e4abc9bae9628e2df7faf5f8a32f730014719ae"}, - {file = "hiredis-2.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:dd82370c2f9f804ec617b95d25edb0fd04882251afb2ecdf08b9ced0c3aa4bcc"}, - {file = "hiredis-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:92077511d3a62109d5d11bf584e41264a993ae3c77c72de63c1f741b7809bacb"}, - {file = "hiredis-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6544c7807cbb75bc6ae9ab85773b4413edbcd55342e9e3d7d3f159f677f7428"}, - {file = "hiredis-2.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8181d73f25943fbdca904154e51b845317103cee08116cfae258f96927ce1e74"}, - {file = "hiredis-2.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040f861e4e43daa9287f3a85979542f9c7ee8cfab695fa662f3b6186c6f7d5e8"}, - {file = "hiredis-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef5ae8c1af82a8000742003cb16a6fa6c57919abb861ab214dcb27db8573ee64"}, - {file = "hiredis-2.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b9aa1b0ec46dec5b05dcec22e50bbd4af33da121fca83bd2601dc60c79183f9"}, - {file = "hiredis-2.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c53c36a630a6c6fd9dfe439f4266e564ca58995015a780c1d964567ebf328466"}, - {file = "hiredis-2.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05aab35210bd7fbd7bd066efb2a42eb5c2878c2c137a9cff597204be2c07475b"}, - {file = "hiredis-2.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e6097e1cef647c665f71cd0e58346389580db98365e804f7a9ad5d96e66b7150"}, - {file = "hiredis-2.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:32f98370efed38088d000df2eb2c8ed43d93d99bbf4a0a740e15eb4a887cc23f"}, - {file = "hiredis-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b85276ed57e0aee8910b48383a38a299851935ba134460bad394988c750985fe"}, - {file = "hiredis-2.1.0-cp310-cp310-win32.whl", hash = "sha256:bd9d99606008a8cfa6b9e950abaa35f5b87496f03e63b73197d02b0fe7ecb6d3"}, - {file = "hiredis-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:6a8e796c94b7b8c63c99757d6ec2075069e4c362dfb0f130aaf874422bea3e7d"}, - {file = "hiredis-2.1.0-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:e7bb5cab604fc45b45cee40e84e84d9e30eeb34c571a3784392ae658273bbd23"}, - {file = "hiredis-2.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e0d4b074ff5ebba00933da27a06f3752b8af2448a6aa9dc895d5279f43011530"}, - {file = "hiredis-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f0c2dbaffd4a9e8df04731a012c8a67b7517abec7e53bb12c3cd749865c63428"}, - {file = "hiredis-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c19151e79b36e0d849899a21fc10539aa1903af94b31754bddab1bea876cd508"}, - {file = "hiredis-2.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:08ec41519a533f5cd1f1f8bd1797929358117c8e4570b679b469f768b45b7dbf"}, - {file = "hiredis-2.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f0db3667fa8abbd37ac66385b460841029033bfc1ba8d7e5b3ff1e01d3346a"}, - {file = "hiredis-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f592d1522b5981890b34b0b814f4bfa4a68b23ee90f538aac321d17e8bf859c8"}, - {file = "hiredis-2.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dddd2be67de25a62b3bf871f091181c13da3b32186d4be6af49dadbf6fdc266d"}, - {file = "hiredis-2.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4ee8f6d0774cd6179c625688201e961a2d03da212230adaa2193cfb7a04f9169"}, - {file = "hiredis-2.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5000942ffb6b6410ccbc87089c15fde5f48bd205664ee8b3067e6b2fb5689485"}, - {file = "hiredis-2.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:21e0017b8f50abd13b4c4c4218c7dfd5a42623e3255b460dfa5f70b45c4e7c3e"}, - {file = "hiredis-2.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:40b55fb46fcc78b04190176c0ae28bfa3cc7f418fca9df06c037028af5942b6a"}, - {file = "hiredis-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:24a55169a7f0bd9458935ac644bf8191f127c8aa50cdd70c0b87928cc515cae5"}, - {file = "hiredis-2.1.0-cp311-cp311-win32.whl", hash = "sha256:bb60f79e8c1eb5971b10fd256764ea0c89c4ad2d55ac4379981f678f349411f2"}, - {file = "hiredis-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:b223668844f26034759a6c24a72f0bb8e4fb64a43b27e2f3e8378639eaac1661"}, - {file = "hiredis-2.1.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:7f7e7d91d6533fcb1939d467cf8bfb98640edf715897959f31ae83f5ad29aed3"}, - {file = "hiredis-2.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:531d1d3955244831b69272b993e16f93489ce2dadfdf800ac856dc2d9a43d353"}, - {file = "hiredis-2.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66ffcbfc4db52dd87cdfd53bda45881ab3ab07c80ec43244fd8d70ee69d42c01"}, - {file = "hiredis-2.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:023b3b3ac410d6cfdb45ee943b8c528c90379f31419a1fd229888aa2b965732d"}, - {file = "hiredis-2.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c972385a0647120d4b0fe0e9567257cad7b2577b9f1315815713c571af0e778d"}, - {file = "hiredis-2.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32893825426e73d57b3290b68110dd76229945e6c79b08a37795f536501935c4"}, - {file = "hiredis-2.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:262148f9b616c0cdd0f2c6bda45cd0f1ce6ce2d1974efd296b85b44e5c7567c2"}, - {file = "hiredis-2.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d601c27b9599fe52cade3096351f92f665e527d29af8d3e29353a76bfcf5615"}, - {file = "hiredis-2.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d248acc7d7713c1b3d48ed8ea67d6ba43b104aa67d63078846a3590adbab6b73"}, - {file = "hiredis-2.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:969ffe37a8980a6e5404993ccfe605a40fa6732fa6d7b26a1a718c9121197002"}, - {file = "hiredis-2.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:288d5d0566d3cbcd800e46c7a547428d321842898b8c7de037a7e78b5644e88a"}, - {file = "hiredis-2.1.0-cp37-cp37m-win32.whl", hash = "sha256:06cb776d3cd3cbec86010f1bab6895ee16af8036aae8c3594a5e96c24f0f83a5"}, - {file = "hiredis-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6766376dc43ef186113422ecacec0ece0d4b12c0e5f4b556669e639b20ccabb1"}, - {file = "hiredis-2.1.0-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:41afba30304adcbe1c93fc8272a7169b7fc4e4d3d470ad8babd391678a519d76"}, - {file = "hiredis-2.1.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6df0115f8b0766cd3d12416e2e2e914efed5b1a1a27605c9f37bc92de086877a"}, - {file = "hiredis-2.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d7d7078f3b841ad86e35459e9f1a49db6d793b796a25fe866333166196d9fec"}, - {file = "hiredis-2.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:835c4cbf8b38c83240b3eb9bd575cd1bfefe5ea5c46cc5ac2bf2d1f47d1fd696"}, - {file = "hiredis-2.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:718589c48e97820bdc2a99e2621b5039884cc23199213756054d10cd309ad56c"}, - {file = "hiredis-2.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2d96be6917ea8f753691a4674f682dd5e145b70edab28c05aa5552ae873e843"}, - {file = "hiredis-2.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5fe1bb4b1525751f3050337097b3b2bfe445836e59a5a0984928dd0797f9abf"}, - {file = "hiredis-2.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91dc73310b92b4aeccffdcd4a762955fe71380f5eaa4e242ee95019e41519101"}, - {file = "hiredis-2.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bb858218de60a930a164a991fff001c70b0c3d923d3ae40fef2acf3321126b00"}, - {file = "hiredis-2.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:53040c3b3488b52f4609775453fc759262f2885b733150ee2e1d88257fdafed8"}, - {file = "hiredis-2.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a1c9b7d6d7bf35e1e2217b2847710154b11d25bf86b77bb7e190161f8b89917e"}, - {file = "hiredis-2.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:dfbe939fdddbc7b90cab4124f3ddd6391099fb964f6dab3386aa8cf56f37b5ba"}, - {file = "hiredis-2.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3a51cb4ea466276a845a940931357b4a876f903eabde514ba95e45050e1c2150"}, - {file = "hiredis-2.1.0-cp38-cp38-win32.whl", hash = "sha256:8bce4c687136bf13df76072072b9baadbd52f7d1b143fbbda96387f50e8ebaeb"}, - {file = "hiredis-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:1f94684b13fbbee1239303018d5ea900d786e486cdb130cde3144d53f4e262e4"}, - {file = "hiredis-2.1.0-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:879668ffab582bdffd9f10f6c8797aac055db183f266e3aa3a6438ff0768bc29"}, - {file = "hiredis-2.1.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:f1d5a99de0fd02438f251e50ec64936d22d542c8e5d80bdec236f9713eeef334"}, - {file = "hiredis-2.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab622bcddcf334b4b1fc4b22e163e93160e3afdd7feaedd77ac6f258e0c77b68"}, - {file = "hiredis-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:964c4f23ff450fb8d73edf06fc7475a4e81a3f9b03a9a04a907ec81c84052fcf"}, - {file = "hiredis-2.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9f8b8daef346ffc0268d7086c213ab24c2a3fcbd4249eacfbb3635602c79d20"}, - {file = "hiredis-2.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e2039cdaa2e6656eae4a2e2537ed77e27f29b7487b97ce7ae6a3cb88d01b968"}, - {file = "hiredis-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43d3168da0a81fa0a9e4bc6e14316beac8e5f1b439ca5cc5af7f9a558cfba741"}, - {file = "hiredis-2.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0021ba034b74c5006f62e4cfdd79d04c7c720731eda256ce29d769ac6483adc3"}, - {file = "hiredis-2.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:39a1bb45bcd698baf70ad4e9a94af164525bf053caea7df3777172d20d69538a"}, - {file = "hiredis-2.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c1b636b05777536a83b4cced157cbdc2d0012d494a9ec2f7b7e07c54296cd773"}, - {file = "hiredis-2.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:58a7ceb71f967fcc1878fb64666a12fbc5f243ab00d0653d3752a811941d8261"}, - {file = "hiredis-2.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c5263c676dc4d55202e7ca0429b949fc6ba7c0dd3a3a2b80538593ab27d82836"}, - {file = "hiredis-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b5879d13025b04903ddf71921812db27fe1156a0952ad253014354d72463aaa9"}, - {file = "hiredis-2.1.0-cp39-cp39-win32.whl", hash = "sha256:9259f637d77544ffeb97acb0a87fdd192a8aced7a2fbd7439160dbee8341d446"}, - {file = "hiredis-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:fb818b6e0981e16dfdfc9e507c9842f8d210e6ecaf3edb8ac3039dbd24768839"}, - {file = "hiredis-2.1.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:648d4648bf6b3dcc418a974df143b2f96627ab8b50bda23a57759c273880ecfb"}, - {file = "hiredis-2.1.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:654949cfc0bc76a5292b6ac111113b2eafb0739e0496495368981ea2e80bf4ec"}, - {file = "hiredis-2.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2a98b835c2088998a47da51b1b3661b587b2d4b3305d03fc9893888cc2aa54"}, - {file = "hiredis-2.1.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7222bd9243387d778245619d0ac62d35cf72ee746ec0efb7b9b230ae3e0c3a39"}, - {file = "hiredis-2.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:778f6de73c3abd67d447a3442f89e7d43a8de1eb5093f416af14dddc1d5c9cb5"}, - {file = "hiredis-2.1.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c4cfb61fe642f30a22789055847004393bc65b5686988c64191e379ea4ccd069"}, - {file = "hiredis-2.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03b6bef7eb50415aca87200a511d66a2fd69f1fcc75cfe1408e1201cbe28ddfb"}, - {file = "hiredis-2.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3195e13a700f6ff35894c4920fcce8f6c2b01cdbc01f76fe567753c495849e9b"}, - {file = "hiredis-2.1.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19f724405c808a89db422ed1010caab80a16d3e5b49632356ae7912513b6d58e"}, - {file = "hiredis-2.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8ecebeff966b412138b0cd105d7572f8d5e65e96355af699863890f8370707e6"}, - {file = "hiredis-2.1.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4f34eefaf164bf43b29ccc809c168248eb95001837ed0e9e3279891f57ae2fab"}, - {file = "hiredis-2.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11fad16beb9d623ea423c9129bab0e392ea4c84363d61c125f679be3d029442f"}, - {file = "hiredis-2.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c763eb9a1414c4d665945c70ae2ef74a843600667b0069fe90e2aabc78e5411"}, - {file = "hiredis-2.1.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edb7f156a8f8a1999574f27bda67dd2bff2d5b180bb6aed996a1792cafbcc668"}, - {file = "hiredis-2.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e057d5545189d4c9e22ae0f7dc283ea0a225f56999511022c062cce7f9589d69"}, + {file = "hiredis-2.1.1-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:f15e48545dadf3760220821d2f3c850e0c67bbc66aad2776c9d716e6216b5103"}, + {file = "hiredis-2.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b3a437e3af246dd06d116f1615cdf4e620e639dfcc923fe3045e00f6a967fc27"}, + {file = "hiredis-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b61732d75e2222a3b0060b97395df78693d5c3487fe4a5d0b75f6ac1affc68b9"}, + {file = "hiredis-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:170c2080966721b42c5a8726e91c5fc271300a4ac9ddf8a5b79856cfd47553e1"}, + {file = "hiredis-2.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2d6e4caaffaf42faf14cfdf20b1d6fff6b557137b44e9569ea6f1877e6f375d"}, + {file = "hiredis-2.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d64b2d90302f0dd9e9ba43e89f8640f35b6d5968668da82ba2d2652b2cc3c3d2"}, + {file = "hiredis-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61fd1c55efb48ba734628f096e7a50baf0df3f18e91183face5c07fba3b4beb7"}, + {file = "hiredis-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfc5e923828714f314737e7f856b3dccf8805e5679fe23f07241b397cd785f6c"}, + {file = "hiredis-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ef2aa0485735c8608a92964e52ab9025ceb6003776184a1eb5d1701742cc910b"}, + {file = "hiredis-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2d39193900a03b900a25d474b9f787434f05a282b402f063d4ca02c62d61bdb9"}, + {file = "hiredis-2.1.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:4b51f5eb47e61c6b82cb044a1815903a77a4f840fa050fd2ff40d617c102d16c"}, + {file = "hiredis-2.1.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d9145d011b74bef972b485a09f391babaa101626dbb54afc2313d5682a746593"}, + {file = "hiredis-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6f45509b43d720d64837c1211fcdea42acd48e71539b7152d74c16413ceea080"}, + {file = "hiredis-2.1.1-cp310-cp310-win32.whl", hash = "sha256:3a284bbf6503cd6ac1183b3542fe853a8be47fb52a631224f6dda46ba229d572"}, + {file = "hiredis-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:f60fad285db733b2badba43f7036a1241cb3e19c17260348f3ff702e6eaa4980"}, + {file = "hiredis-2.1.1-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:69c20816ac2af11701caf10e5b027fd33c6e8dfe7806ab71bc5191aa2a6d50f9"}, + {file = "hiredis-2.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cd43dbaa73322a0c125122114cbc2c37141353b971751d05798f3b9780091e90"}, + {file = "hiredis-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c9632cd480fbc09c14622038a9a5f2f21ef6ce35892e9fa4df8d3308d3f2cedf"}, + {file = "hiredis-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:252d4a254f1566012b94e35cba577a001d3a732fa91e824d2076233222232cf9"}, + {file = "hiredis-2.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b901e68f3a6da279388e5dbe8d3bc562dd6dd3ff8a4b90e4f62e94de36461777"}, + {file = "hiredis-2.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f45f296998043345ecfc4f69a51fa4f3e80ca3659864df80b459095580968a6"}, + {file = "hiredis-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79f2acf237428dd61faa5b49247999ff68f45b3552c57303fcfabd2002eab249"}, + {file = "hiredis-2.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82bc6f5b92c9fcd5b5d6506000dd433006b126b193932c52a9bcc10dcc10e4fc"}, + {file = "hiredis-2.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:19843e4505069085301c3126c91b4e48970070fb242d7c617fb6777e83b55541"}, + {file = "hiredis-2.1.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c7336fddae533cbe786360d7a0316c71fe96313872c06cde20a969765202ab04"}, + {file = "hiredis-2.1.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:90b4355779970e121c219def3e35533ec2b24773a26fc4aa0f8271dd262fa2f2"}, + {file = "hiredis-2.1.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4beaac5047317a73b27cf15b4f4e0d2abaafa8378e1a6ed4cf9ff420d8f88aba"}, + {file = "hiredis-2.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7e25dc06e02689a45a49fa5e2f48bdfdbc11c5b52bef792a8cb37e0b82a7b0ae"}, + {file = "hiredis-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8b3233c1de155743ef34b0cae494e33befed5e0adba77762f5d8a8e417c5015"}, + {file = "hiredis-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:4ced076af04e28761d486501c58259247c1882fd19c7f94c18a257d143248eee"}, + {file = "hiredis-2.1.1-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:f4300e063045e11ee79b79a7c9426813ab8d97e340b15843374093225dde407d"}, + {file = "hiredis-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b04b6c04fe13e1e30ba6f9340d3d0fb776a7e52611d11809fb59341871e050e5"}, + {file = "hiredis-2.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:436dcbbe3104737e8b4e2d63a019a764d107d72d6b6ee3cd107097c1c263fd1e"}, + {file = "hiredis-2.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:11801d9e96f39286ab558c6db940c39fc00150450ae1007d18b35437d2f79ad7"}, + {file = "hiredis-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7d8d0ca7b4f6136f8a29845d31cfbc3f562cbe71f26da6fca55aa4977e45a18"}, + {file = "hiredis-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c040af9eb9b12602b4b714b90a1c2ac1109e939498d47b0748ec33e7a948747"}, + {file = "hiredis-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f448146b86a8693dda5f02bb4cb2ef65c894db2cf743e7bf351978354ce685e3"}, + {file = "hiredis-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:649c5a1f0952af50f008f0bbec5f0b1e519150220c0a71ef80541a0c128d0c13"}, + {file = "hiredis-2.1.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b8e7415b0952b0dd6df3aa2d37b5191c85e54d6a0ac1449ddb1e9039bbb39fa5"}, + {file = "hiredis-2.1.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:38c1a56a30b953e3543662f950f498cfb17afed214b27f4fc497728fb623e0c9"}, + {file = "hiredis-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6050b519fb3b62d68a28a1941ae9dc5122e8820fef2b8e20a65cb3c1577332a0"}, + {file = "hiredis-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:96add2a205efffe5e19a256a50be0ed78fcb5e9503242c65f57928e95cf4c901"}, + {file = "hiredis-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:8ceb101095f8cce9ac672ed7244b002d83ea97af7f27bb73f2fbe7fe8e8f03c7"}, + {file = "hiredis-2.1.1-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:9f068136e5119f2ba939ecd45c47b4e3cf6dd7ca9a65b6078c838029c5c1f564"}, + {file = "hiredis-2.1.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:8a42e246a03086ae1430f789e37d7192113db347417932745c4700d8999f853a"}, + {file = "hiredis-2.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5359811bfdb10fca234cba4629e555a1cde6c8136025395421f486ce43129ae3"}, + {file = "hiredis-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d304746e2163d3d2cbc4c08925539e00d2bb3edc9e79fce531b5468d4e264d15"}, + {file = "hiredis-2.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4fe297a52a8fc1204eef646bebf616263509d089d472e25742913924b1449099"}, + {file = "hiredis-2.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:637e563d5cbf79d8b04224f99cfce8001146647e7ce198f0b032e32e62079e3c"}, + {file = "hiredis-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39b61340ff2dcd99d5ded0ca5fc33c878d89a1426e2f7b6dbc7c7381e330bc8a"}, + {file = "hiredis-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66eaf6d5ea5207177ba8ffb9ee479eea743292267caf1d6b89b51cf9d5885d23"}, + {file = "hiredis-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4d2d0e458c32cdafd9a0f0b0aaeb61b169583d074287721eee740b730b7654bd"}, + {file = "hiredis-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8a92781e466f2f1f9d38720d8920cb094bc0d59f88219591bc12b1c12c9d471c"}, + {file = "hiredis-2.1.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:5560b09304ebaac5323a7402f5090f2a8559843200014f5adf1ff7517dd3805b"}, + {file = "hiredis-2.1.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4732a0bf877bbd69d4d1b38a3db2160252acb31894a48f324fd54f742f6b2123"}, + {file = "hiredis-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b5bd33ac8a572e2aa94b489dec35b0c00ca554b27e56ad19953e0bf2cbcf3ad8"}, + {file = "hiredis-2.1.1-cp38-cp38-win32.whl", hash = "sha256:07e86649773e486a21e170d1396217e15833776d9e8f4a7121c28a1d37e032c9"}, + {file = "hiredis-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:b964d81db8f11a99552621acd24c97381a0fd401a57187ce9f8cb9a53f4b6f4e"}, + {file = "hiredis-2.1.1-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:27e89e7befc785a273cccb105840db54b7f93005adf4e68c516d57b19ea2aac2"}, + {file = "hiredis-2.1.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ea6f0f98e1721741b5bc3167a495a9f16459fe67648054be05365a67e67c29ba"}, + {file = "hiredis-2.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:40c34aeecccb9474999839299c9d2d5ff46a62ed47c58645b7965f48944abd74"}, + {file = "hiredis-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65927e75da4265ec88d06cbdab20113a9e69bbac3aea1ec053d4d940f1c88fc8"}, + {file = "hiredis-2.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72cab67bcceb2e998da2f28aad9ec7b1a5ece5888f7ac3d3723cccba62338703"}, + {file = "hiredis-2.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d67429ff99231137491d8c3daa097c767a9c273bb03ac412ed8f6acb89e2e52f"}, + {file = "hiredis-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c596bce5e9dd379c68c17208716da2767bb6f6f2a71d748f9e4c247ced31e6"}, + {file = "hiredis-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e0aab2d6e60aa9f9e14c83396b4a58fb4aded712806486c79189bcae4a175ac"}, + {file = "hiredis-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:17deb7d218a5ae9f05d2b19d51936231546973303747924fc17a2869aef0029a"}, + {file = "hiredis-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d3d60e2af4ce93d6e45a50a9b5795156a8725495e411c7987a2f81ab14e99665"}, + {file = "hiredis-2.1.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:fbc960cd91e55e2281e1a330e7d1c4970b6a05567dd973c96e412b4d012e17c6"}, + {file = "hiredis-2.1.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0ae718e9db4b622072ff73d38bc9cd7711edfedc8a1e08efe25a6c8170446da4"}, + {file = "hiredis-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e51e3fa176fecd19660f898c4238232e8ca0f5709e6451a664c996f9aec1b8e1"}, + {file = "hiredis-2.1.1-cp39-cp39-win32.whl", hash = "sha256:0258bb84b4a1e015f14f891d91957042fa88f6f4e86cc0808d735ebbc1e3fc88"}, + {file = "hiredis-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:c5a47c964c58c044a323336a798d8729722e09865d7e087eb3512df6146b39a8"}, + {file = "hiredis-2.1.1-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8de0334c212e069d49952e476e16c6b42ba9677cc1e2d2f4588bd9a39489a3ab"}, + {file = "hiredis-2.1.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:653e33f69202c00eca35416ee23091447ad1e9f9a556cc2b715b2befcfc31b3c"}, + {file = "hiredis-2.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14cccf931c859ba3169d766e892a3673a79649ec2ceca7ba95ea376b23fd222"}, + {file = "hiredis-2.1.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86c56359fd7aca6a9ca41af91636aef15d5ad6d19e631ebd662f233c79f7e100"}, + {file = "hiredis-2.1.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:c2b197e3613c3aef3933b2c6eb095bd4be9c84022aea52057697b709b400c4bc"}, + {file = "hiredis-2.1.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ec060d6db9576f6723b5290448aea67160608556b5506eb947997d9d1ca6f7b7"}, + {file = "hiredis-2.1.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8781f5b91d75abef529a33cf3509ba5fe540d2814de0c4602f0f5ba6f1669739"}, + {file = "hiredis-2.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bd6b934794bea92a15b10ac35889df63b28d2abf9d020a7c87c05dd9c6e1edd"}, + {file = "hiredis-2.1.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf6d85c1ffb4ec4a859b2f31cd8845e633f91ed971a3cce6f59a722dcc361b8c"}, + {file = "hiredis-2.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:bbf80c686e3f63d40b0ab42d3605d3b6d415c368a5d8a9764a314ebda6138650"}, + {file = "hiredis-2.1.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c1d85dfdf37a8df0e0174fc0c762b485b80a2fc7ce9592ae109aaf4a5d45ba9a"}, + {file = "hiredis-2.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:816b9ea96e7cc2496a1ac9c4a76db670827c1e31045cc377c66e64a20bb4b3ff"}, + {file = "hiredis-2.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db59afa0edf194bea782e4686bfc496fc1cea2e24f310d769641e343d14cc929"}, + {file = "hiredis-2.1.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c7a7e4ccec7164cdf2a9bbedc0e7430492eb56d9355a41377f40058c481bccc"}, + {file = "hiredis-2.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:646f150fa73f9cbc69419e34a1aae318c9f39bd9640760aa46624b2815da0c2d"}, + {file = "hiredis-2.1.1.tar.gz", hash = "sha256:21751e4b7737aaf7261a068758b22f7670155099592b28d8dde340bf6874313d"}, ] [[package]] @@ -545,6 +550,36 @@ files = [ {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] +[[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" @@ -670,14 +705,14 @@ files = [ [[package]] name = "packaging" -version = "22.0" +version = "23.0" description = "Core utilities for Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"}, - {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"}, + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] [[package]] @@ -735,14 +770,14 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pydantic-factories" -version = "1.17.0" +version = "1.17.1" description = "Mock data generation for pydantic based models and python dataclasses" category = "main" optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "pydantic_factories-1.17.0-py3-none-any.whl", hash = "sha256:11c267ccbb7d54b1b456f68060f31472a5e53151314679e6f9367962b4d3a6f8"}, - {file = "pydantic_factories-1.17.0.tar.gz", hash = "sha256:f6b2cdf715f8c3e4a84e2851c0fb493af2009398159594c000192a38ce192129"}, + {file = "pydantic_factories-1.17.1-py3-none-any.whl", hash = "sha256:05c0b143540f54d9dd9d0d500b7b146ff29e0c1cd4bb5f2ed99c60842ff1d5e6"}, + {file = "pydantic_factories-1.17.1.tar.gz", hash = "sha256:85848136cd768894dc5b6e3ffaf49753c7627c545ef05ff096ff616071cd59ff"}, ] [package.dependencies] @@ -907,14 +942,14 @@ web = ["aiohttp", "aiohttp-basicauth"] [[package]] name = "sentry-sdk" -version = "1.12.1" +version = "1.13.0" description = "Python client for Sentry (https://sentry.io)" category = "main" optional = false python-versions = "*" files = [ - {file = "sentry-sdk-1.12.1.tar.gz", hash = "sha256:5bbe4b72de22f9ac1e67f2a4e6efe8fbd595bb59b7b223443f50fe5802a5551c"}, - {file = "sentry_sdk-1.12.1-py2.py3-none-any.whl", hash = "sha256:9f0b960694e2d8bb04db4ba6ac2a645040caef4e762c65937998ff06064f10d6"}, + {file = "sentry-sdk-1.13.0.tar.gz", hash = "sha256:72da0766c3069a3941eadbdfa0996f83f5a33e55902a19ba399557cfee1dddcc"}, + {file = "sentry_sdk-1.13.0-py2.py3-none-any.whl", hash = "sha256:b7ff6318183e551145b5c4766eb65b59ad5b63ff234dffddc5fb50340cad6729"}, ] [package.dependencies] @@ -941,6 +976,7 @@ rq = ["rq (>=0.6)"] sanic = ["sanic (>=0.8)"] sqlalchemy = ["sqlalchemy (>=1.2)"] starlette = ["starlette (>=0.19.1)"] +starlite = ["starlite (>=1.48)"] tornado = ["tornado (>=5)"] [[package]] @@ -1047,14 +1083,14 @@ sqlcipher = ["sqlcipher3-binary"] [[package]] name = "starlite" -version = "1.48.1" +version = "1.49.0" description = "Performant, light and flexible ASGI API Framework" category = "main" optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "starlite-1.48.1-py3-none-any.whl", hash = "sha256:d2a37240b54199f1fa7ba2d3d2079124ee494f9f81722eec2cf04cccf4abfb8b"}, - {file = "starlite-1.48.1.tar.gz", hash = "sha256:2a79e219d8f6498f2b9cad284615b58186c2043010dee70cefb62b930528efed"}, + {file = "starlite-1.49.0-py3-none-any.whl", hash = "sha256:97748c7915706f5888f6eb245a4305f7c50f3fdb5bc09785421ee5677513cee9"}, + {file = "starlite-1.49.0.tar.gz", hash = "sha256:216c84745617070b766d0e759016a13fcfb467f75b54c8735a0b1e78625363bf"}, ] [package.dependencies] @@ -1073,15 +1109,15 @@ typing-extensions = "*" [package.extras] brotli = ["brotli"] -cli = ["click (>=8.1.3)", "rich (>=12.6.0)"] +cli = ["click", "jsbeautifier", "rich (>=13.0.0)"] cryptography = ["cryptography"] -full = ["aiomcache", "brotli", "click (>=8.1.3)", "cryptography", "opentelemetry-instrumentation-asgi", "picologging", "python-jose", "redis[hiredis]", "rich (>=12.6.0)", "structlog"] +full = ["aiomcache", "brotli", "click", "cryptography", "opentelemetry-instrumentation-asgi", "picologging", "python-jose", "redis[hiredis]", "rich (>=13.0.0)", "structlog"] jwt = ["cryptography", "python-jose"] memcached = ["aiomcache"] opentelemetry = ["opentelemetry-instrumentation-asgi"] picologging = ["picologging"] redis = ["redis[hiredis]"] -standard = ["click (>=8.1.3)", "picologging", "rich (>=12.6.0)"] +standard = ["click", "jsbeautifier", "picologging", "rich (>=13.0.0)"] structlog = ["structlog"] [[package]] @@ -1117,6 +1153,18 @@ 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" @@ -1131,14 +1179,14 @@ files = [ [[package]] name = "urllib3" -version = "1.26.13" +version = "1.26.14" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, - {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, + {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, + {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, ] [package.extras] @@ -1213,4 +1261,4 @@ test = ["Cython (>=0.29.32,<0.30.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "my [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "d9f8dc80dc0e85303fd241049e9fa12e0ea888de741065e8a2dfe215cc5d9317" +content-hash = "02b7b504018c2c2e6633f8028b22dc996e837ba7f1b2a6ae6b879382b9c9d2d0" diff --git a/pyproject.toml b/pyproject.toml index 8a46ebc3..0a0d3858 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,11 +12,11 @@ ignore-words-list = "alog" branch = true omit = ["*/starlite_saqlalchemy/scripts.py", "*/starlite_saqlalchemy/lifespan.py", "tests/*"] relative_files = true -source_pkgs = ["starlite_saqlalchemy"] +source_pkgs = ["starlite_saqlalchemy", "pytest_starlite_saqlalchemy"] [tool.coverage.paths] source = ["src", "*/site-packages"] -others = [".", "*/starlite_saqlalchemy"] +others = [".", "*/starlite_saqlalchemy", "*/pytest_starlite_saqlalchemy"] [tool.coverage.report] show_missing = true @@ -48,9 +48,11 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development", "Topic :: System :: Installation/Setup", + "Framework :: Pytest", ] packages = [ - { include = "starlite_saqlalchemy", from = "src" } + { include = "starlite_saqlalchemy", from = "src" }, + { include = "pytest_starlite_saqlalchemy", from = "src" } ] [tool.poetry.dependencies] @@ -63,7 +65,7 @@ pydantic = "*" python-dotenv = "*" redis = "*" saq = "^0.9.1" -sentry-sdk = "*" +sentry-sdk = ">=1.13.0" sqlalchemy = "==2.0.0rc2" starlite = "^1.40.1" structlog = ">=22.2.0" @@ -71,6 +73,12 @@ tenacity = "*" uvicorn = "*" uvloop = "*" +[tool.poetry.plugins."pytest11"] +pytest_starlite_saqlalchemy = "pytest_starlite_saqlalchemy" + +[tool.poetry.plugins."console_scripts"] +run-app = "starlite_saqlalchemy.scripts:run_app" + [tool.poetry.urls] GitHub = "https://github.com/topsport-com-au" Bugs = "https://github.com/topsport-com-au/starlite-saqlalchemy/issues" @@ -88,6 +96,7 @@ addopts = ["-ra", "--strict-config"] 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.ci.txt b/requirements.ci.txt new file mode 100644 index 00000000..b90d588d --- /dev/null +++ b/requirements.ci.txt @@ -0,0 +1,2 @@ +tox==4.3.1 +wheel==0.38.4 diff --git a/dev.requirements.txt b/requirements.dev.txt similarity index 100% rename from dev.requirements.txt rename to requirements.dev.txt diff --git a/docs.requirements.txt b/requirements.docs.txt similarity index 100% rename from docs.requirements.txt rename to requirements.docs.txt diff --git a/src/pytest_starlite_saqlalchemy/__init__.py b/src/pytest_starlite_saqlalchemy/__init__.py new file mode 100644 index 00000000..f89939e8 --- /dev/null +++ b/src/pytest_starlite_saqlalchemy/__init__.py @@ -0,0 +1,13 @@ +"""Pytest plugin to support testing starlite-saqlalchemy applications.""" +from __future__ import annotations + +from .plugin import ( + _patch_http_close, + _patch_sqlalchemy_plugin, + _patch_worker, + fx_app, + fx_cap_logger, + fx_client, + fx_is_unit_test, + pytest_addoption, +) diff --git a/src/pytest_starlite_saqlalchemy/plugin.py b/src/pytest_starlite_saqlalchemy/plugin.py new file mode 100644 index 00000000..048351c2 --- /dev/null +++ b/src/pytest_starlite_saqlalchemy/plugin.py @@ -0,0 +1,139 @@ +"""Pytest plugin to support testing starlite-saqlalchemy applications.""" +# pylint: disable=import-outside-toplevel +from __future__ import annotations + +import re +from typing import TYPE_CHECKING +from unittest.mock import MagicMock + +import pytest +from starlite import Starlite, TestClient +from structlog.contextvars import clear_contextvars +from structlog.testing import CapturingLogger +from uvicorn.importer import ImportFromStringError, import_from_string + +if TYPE_CHECKING: + from collections.abc import Generator + + from pytest import Config, FixtureRequest, MonkeyPatch, Parser + +__all__ = ( + "_patch_http_close", + "_patch_sqlalchemy_plugin", + "_patch_worker", + "fx_app", + "fx_cap_logger", + "fx_client", + "fx_is_unit_test", + "pytest_addoption", +) + + +def pytest_addoption(parser: Parser) -> None: + """Adds Pytest ini config variables for the plugin.""" + parser.addini( + "test_app", + "Path to application instance, or callable that returns an application instance.", + type="string", + default="app.main:create_app", + ) + parser.addini( + "unit_test_pattern", + ( + "Regex used to identify if a test is running as part of a unit or integration test " + "suite. The pattern is matched against the path of each test function and affects the " + "behavior of fixtures that are shared between unit and integration tests." + ), + type="string", + default=r"^.*/tests/unit/.*$", + ) + + +@pytest.fixture(name="is_unit_test") +def fx_is_unit_test(request: FixtureRequest) -> bool: + """Uses the ini option `unit_test_pattern` to determine if the test is part + of unit or integration tests.""" + unittest_pattern: str = request.config.getini("unit_test_pattern") # pyright:ignore + return bool(re.search(unittest_pattern, str(request.path))) + + +@pytest.fixture(autouse=True) +def _patch_http_close(monkeypatch: MonkeyPatch) -> None: + """We don't want global http clients to get closed between tests.""" + import starlite_saqlalchemy + + monkeypatch.setattr(starlite_saqlalchemy.http, "clients", set()) + + +@pytest.fixture(autouse=True) +def _patch_sqlalchemy_plugin(is_unit_test: bool, monkeypatch: MonkeyPatch) -> None: + if is_unit_test: + from starlite_saqlalchemy import sqlalchemy_plugin + + monkeypatch.setattr( + sqlalchemy_plugin.SQLAlchemyConfig, # type:ignore[attr-defined] + "on_shutdown", + MagicMock(), + ) + + +@pytest.fixture(autouse=True) +def _patch_worker(is_unit_test: bool, monkeypatch: MonkeyPatch) -> None: + """We don't want the worker to start for unittests.""" + if is_unit_test: + from starlite_saqlalchemy import worker + + monkeypatch.setattr(worker.Worker, "on_app_startup", MagicMock()) + monkeypatch.setattr(worker.Worker, "stop", MagicMock()) + + +@pytest.fixture(name="app") +def fx_app(pytestconfig: Config, monkeypatch: MonkeyPatch) -> Starlite: + """ + Returns: + 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): + from starlite_saqlalchemy.init_plugin import ConfigureApp + + app = Starlite(route_handlers=[], on_app_init=[ConfigureApp()], openapi_config=None) + else: + if isinstance(app_or_callable, Starlite): + app = app_or_callable + else: + app = app_or_callable() + + monkeypatch.setattr(app, "before_startup", []) + return app + + +@pytest.fixture(name="client") +def fx_client(app: Starlite) -> Generator[TestClient, None, None]: + """Test client fixture for making calls on the global app instance.""" + with TestClient(app=app) as client: + yield client + + +@pytest.fixture(name="cap_logger") +def fx_cap_logger(monkeypatch: MonkeyPatch) -> CapturingLogger: + """Used to monkeypatch the app logger, so we can inspect output.""" + import starlite_saqlalchemy + + starlite_saqlalchemy.log.configure( + starlite_saqlalchemy.log.default_processors # type:ignore[arg-type] + ) + # clear context for every test + clear_contextvars() + # pylint: disable=protected-access + logger = starlite_saqlalchemy.log.controller.LOGGER.bind() + logger._logger = CapturingLogger() + # drop rendering processor to get a dict, not bytes + # noinspection PyProtectedMember + logger._processors = starlite_saqlalchemy.log.default_processors[:-1] + monkeypatch.setattr(starlite_saqlalchemy.log.controller, "LOGGER", logger) + monkeypatch.setattr(starlite_saqlalchemy.log.worker, "LOGGER", logger) + return logger._logger diff --git a/src/starlite_saqlalchemy/__init__.py b/src/starlite_saqlalchemy/__init__.py index 044ce55f..53496bfe 100644 --- a/src/starlite_saqlalchemy/__init__.py +++ b/src/starlite_saqlalchemy/__init__.py @@ -29,6 +29,7 @@ def example_handler() -> dict: dto, exceptions, health, + http, log, openapi, redis, @@ -51,6 +52,7 @@ def example_handler() -> dict: "dto", "exceptions", "health", + "http", "log", "openapi", "redis", diff --git a/src/starlite_saqlalchemy/constants.py b/src/starlite_saqlalchemy/constants.py new file mode 100644 index 00000000..8a5e75d3 --- /dev/null +++ b/src/starlite_saqlalchemy/constants.py @@ -0,0 +1,11 @@ +"""Application constants.""" +from __future__ import annotations + +from starlite_saqlalchemy.settings import app +from starlite_saqlalchemy.utils import case_insensitive_string_compare + +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.""" diff --git a/src/starlite_saqlalchemy/init_plugin.py b/src/starlite_saqlalchemy/init_plugin.py index b4171c5d..08d4a244 100644 --- a/src/starlite_saqlalchemy/init_plugin.py +++ b/src/starlite_saqlalchemy/init_plugin.py @@ -51,6 +51,7 @@ def example_handler() -> dict: settings, sqlalchemy_plugin, ) +from starlite_saqlalchemy.constants import IS_LOCAL_ENVIRONMENT, IS_TEST_ENVIRONMENT from starlite_saqlalchemy.exceptions import HealthCheckConfigurationError from starlite_saqlalchemy.health import ( AbstractHealthCheck, @@ -132,7 +133,7 @@ class PluginConfig(BaseModel): Set the OpenAPI config object to [`AppConfig.openapi_config`][starlite.config.app.AppConfig.openapi_config]. """ - do_sentry: bool = True + do_sentry: bool | None = None """Configure sentry. Configure the application to initialize Sentry on startup. Adds a handler to @@ -330,7 +331,12 @@ def configure_sentry(self, app_config: AppConfig) -> None: Args: app_config: The Starlite application config object. """ - if self.config.do_sentry: + 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: app_config.on_startup.append(sentry.configure) def configure_sqlalchemy_plugin(self, app_config: AppConfig) -> None: diff --git a/src/starlite_saqlalchemy/log/__init__.py b/src/starlite_saqlalchemy/log/__init__.py index 680aedb6..06105afa 100644 --- a/src/starlite_saqlalchemy/log/__init__.py +++ b/src/starlite_saqlalchemy/log/__init__.py @@ -9,6 +9,7 @@ from starlite.config.logging import LoggingConfig from starlite_saqlalchemy import settings +from starlite_saqlalchemy.constants import IS_LOCAL_ENVIRONMENT from . import controller, worker from .utils import EventFilter, msgspec_json_renderer @@ -43,7 +44,7 @@ structlog.stdlib.ProcessorFormatter.remove_processors_meta, ] -if settings.app.ENVIRONMENT == "local": # pragma: no cover +if IS_LOCAL_ENVIRONMENT: # pragma: no cover LoggerFactory: Any = structlog.WriteLoggerFactory console_processor = structlog.dev.ConsoleRenderer(colors=True) default_processors.extend([console_processor]) diff --git a/src/starlite_saqlalchemy/scripts.py b/src/starlite_saqlalchemy/scripts.py index 076c8d27..326e084b 100644 --- a/src/starlite_saqlalchemy/scripts.py +++ b/src/starlite_saqlalchemy/scripts.py @@ -2,15 +2,12 @@ import uvicorn from starlite_saqlalchemy import settings +from starlite_saqlalchemy.constants import IS_LOCAL_ENVIRONMENT def determine_should_reload() -> bool: """Evaluate whether reloading should be enabled.""" - return ( - settings.server.RELOAD - if settings.server.RELOAD is not None - else settings.app.ENVIRONMENT == "local" - ) + return settings.server.RELOAD if settings.server.RELOAD is not None else IS_LOCAL_ENVIRONMENT def determine_reload_dirs(should_reload: bool) -> list[str] | None: diff --git a/src/starlite_saqlalchemy/sentry.py b/src/starlite_saqlalchemy/sentry.py index 9a2a0c5a..d968fc63 100644 --- a/src/starlite_saqlalchemy/sentry.py +++ b/src/starlite_saqlalchemy/sentry.py @@ -1,26 +1,46 @@ -"""Sentry config for our application. - -The current support for sentry is limited, but still worth having. - -See: https://github.com/getsentry/sentry-python/issues/1549 -""" +"""Sentry config for our application.""" from __future__ import annotations +from typing import TYPE_CHECKING, TypedDict, cast + import sentry_sdk from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration +from sentry_sdk.integrations.starlite import StarliteIntegration from starlite_saqlalchemy import settings +if TYPE_CHECKING: + from collections.abc import Mapping + from typing import Any + + from starlite.types import HTTPScope + + +class SamplingContext(TypedDict): + """Sentry context sent to traces sampler function.""" + + asgi_scope: HTTPScope + parent_sampled: bool | None + transaction_context: dict[str, Any] + + +def sentry_traces_sampler(sampling_context: Mapping[str, Any]) -> float: + """Don't send health check transactions to sentry.""" + sampling_context = cast("SamplingContext", sampling_context) + if sampling_context["asgi_scope"]["path"] == settings.api.HEALTH_PATH: + return 0.0 + return settings.sentry.TRACES_SAMPLE_RATE + def configure() -> None: """Configure sentry on app startup. See [SentrySettings][starlite_saqlalchemy.settings.SentrySettings]. """ - sentry_sdk.init( + sentry_sdk.init( # pragma: no cover dsn=settings.sentry.DSN, environment=settings.app.ENVIRONMENT, release=settings.app.BUILD_NUMBER, - integrations=[SqlalchemyIntegration()], - traces_sample_rate=settings.sentry.TRACES_SAMPLE_RATE, + integrations=[StarliteIntegration(), SqlalchemyIntegration()], + traces_sampler=sentry_traces_sampler, ) diff --git a/src/starlite_saqlalchemy/settings.py b/src/starlite_saqlalchemy/settings.py index f14f4630..b182a6d8 100644 --- a/src/starlite_saqlalchemy/settings.py +++ b/src/starlite_saqlalchemy/settings.py @@ -35,6 +35,17 @@ class Config: """Run `Starlite` with `debug=True`.""" ENVIRONMENT: str = "prod" """'dev', 'prod', etc.""" + TEST_ENVIRONMENT_NAME: str = "test" + """Value of ENVIRONMENT used to determine if running tests. + + This should be the value of `ENVIRONMENT` in `test.env`. + """ + LOCAL_ENVIRONMENT_NAME: str = "local" + """Value of ENVIRONMENT used to determine if running in local development + mode. + + This should be the value of `ENVIRONMENT` in your local `.env` file. + """ NAME: str = "my-starlite-saqlalchemy-app" """Application name.""" @@ -113,7 +124,6 @@ class Config: REQUEST_FIELDS: list[RequestExtractorField] = [ "path", "method", - "content_type", "headers", "cookies", "query", diff --git a/src/starlite_saqlalchemy/sqlalchemy_plugin.py b/src/starlite_saqlalchemy/sqlalchemy_plugin.py index 821b26b8..40626a74 100644 --- a/src/starlite_saqlalchemy/sqlalchemy_plugin.py +++ b/src/starlite_saqlalchemy/sqlalchemy_plugin.py @@ -19,7 +19,7 @@ from starlite.datastructures.state import State from starlite.types import Message, Scope -__all__ = ["config", "plugin"] +__all__ = ["SQLAlchemyHealthCheck", "config", "plugin"] async def before_send_handler(message: "Message", _: "State", scope: "Scope") -> None: @@ -49,6 +49,7 @@ class SQLAlchemyHealthCheck(AbstractHealthCheck): name: str = "db" def __init__(self) -> None: + """Health check with database check.""" self.engine = create_async_engine( settings.db.URL, logging_name="starlite_saqlalchemy.health" ) diff --git a/src/starlite_saqlalchemy/testing/__init__.py b/src/starlite_saqlalchemy/testing/__init__.py new file mode 100644 index 00000000..8d75d555 --- /dev/null +++ b/src/starlite_saqlalchemy/testing/__init__.py @@ -0,0 +1,11 @@ +"""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/testing/controller_test.py b/src/starlite_saqlalchemy/testing/controller_test.py new file mode 100644 index 00000000..78ae14f6 --- /dev/null +++ b/src/starlite_saqlalchemy/testing/controller_test.py @@ -0,0 +1,119 @@ +"""Automated controller testing.""" +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +from starlite.status_codes import ( + HTTP_200_OK, + HTTP_201_CREATED, + HTTP_405_METHOD_NOT_ALLOWED, +) + +if TYPE_CHECKING: + from collections.abc import Sequence + from typing import Any + + from pytest import MonkeyPatch + from starlite.testing import TestClient + + from starlite_saqlalchemy.db import orm + from starlite_saqlalchemy.service import Service + + +class ControllerTest: + """Standard controller testing utility.""" + + def __init__( + self, + client: TestClient, + base_path: str, + collection: Sequence[orm.Base], + raw_collection: Sequence[dict[str, Any]], + service_type: type[Service], + monkeypatch: MonkeyPatch, + collection_filters: dict[str, Any] | None = None, + ) -> None: + """Perform standard tests of controllers. + + Args: + client: Test client instance. + base_path: Path for POST and collection GET requests. + collection: Collection of domain objects. + raw_collection: Collection of raw representations of domain objects. + service_type: The domain Service object type. + monkeypatch: Pytest's monkeypatch. + collection_filters: Collection filters for GET collection request. + """ + self.client = client + self.base_path = base_path + self.collection = collection + self.raw_collection = raw_collection + self.service_type = service_type + self.monkeypatch = monkeypatch + self.collection_filters = collection_filters + + def _get_random_member(self) -> Any: + return random.choice(self.collection) + + def _get_raw_for_member(self, member: Any) -> dict[str, Any]: + return [item for item in self.raw_collection if item["id"] == str(member.id)][0] + + def test_get_collection(self, with_filters: bool = False) -> None: + """Test collection endpoint get request.""" + + async def _list(*_: Any, **__: Any) -> list[Any]: + return list(self.collection) + + self.monkeypatch.setattr(self.service_type, "list", _list) + + resp = self.client.get( + self.base_path, params=self.collection_filters if with_filters else None + ) + + if resp.status_code == HTTP_405_METHOD_NOT_ALLOWED: + return + + assert resp.status_code == HTTP_200_OK + assert resp.json() == self.raw_collection + + def test_member_request(self, method: str, service_method: str, exp_status: int) -> None: + """Test member endpoint request.""" + member = self._get_random_member() + raw = self._get_raw_for_member(member) + + async def _method(*_: Any, **__: Any) -> Any: + return member + + self.monkeypatch.setattr(self.service_type, service_method, _method) + + if method.lower() == "post": + url = self.base_path + else: + url = f"{self.base_path}/{member.id}" + + request_kw: dict[str, Any] = {} + if method.lower() in ("put", "post"): + request_kw["json"] = raw + + resp = self.client.request(method, url, **request_kw) + + if resp.status_code == HTTP_405_METHOD_NOT_ALLOWED: + return + + assert resp.status_code == exp_status + assert resp.json() == raw + + def run(self) -> None: + """Run the tests.""" + # test the collection route with and without filters for branch coverage. + self.test_get_collection() + if self.collection_filters: + self.test_get_collection(with_filters=True) + for method, service_method, status in [ + ("GET", "get", HTTP_200_OK), + ("PUT", "update", HTTP_200_OK), + ("POST", "create", HTTP_201_CREATED), + ("DELETE", "delete", HTTP_200_OK), + ]: + self.test_member_request(method, service_method, status) diff --git a/src/starlite_saqlalchemy/testing.py b/src/starlite_saqlalchemy/testing/generic_mock_repository.py similarity index 56% rename from src/starlite_saqlalchemy/testing.py rename to src/starlite_saqlalchemy/testing/generic_mock_repository.py index 3f341641..9342b5ea 100644 --- a/src/starlite_saqlalchemy/testing.py +++ b/src/starlite_saqlalchemy/testing/generic_mock_repository.py @@ -4,66 +4,24 @@ """ from __future__ import annotations -import random -from contextlib import contextmanager from datetime import datetime from typing import TYPE_CHECKING, Generic, TypeVar from uuid import uuid4 -from starlite.status_codes import HTTP_200_OK, HTTP_201_CREATED - from starlite_saqlalchemy.db import orm from starlite_saqlalchemy.exceptions import ConflictError, StarliteSaqlalchemyError from starlite_saqlalchemy.repository.abc import AbstractRepository if TYPE_CHECKING: - from collections.abc import ( - Callable, - Generator, - Hashable, - Iterable, - MutableMapping, - Sequence, - ) + from collections.abc import Callable, Hashable, Iterable, MutableMapping from typing import Any - from pydantic import BaseSettings - from pytest import MonkeyPatch - from starlite.testing import TestClient - from starlite_saqlalchemy.repository.types import FilterTypes - from starlite_saqlalchemy.service import Service ModelT = TypeVar("ModelT", bound=orm.Base) MockRepoT = TypeVar("MockRepoT", bound="GenericMockRepository") -@contextmanager -def modify_settings(*update: tuple[BaseSettings, dict[str, Any]]) -> Generator[None, None, None]: - """Context manager that modify the desired settings and restore them on - exit. - - >>> assert settings.app.ENVIRONMENT = "local" - >>> with modify_settings((settings.app, {"ENVIRONMENT": "prod"})): - >>> assert settings.app.ENVIRONMENT == "prod" - >>> assert settings.app.ENVIRONMENT == "local" - """ - old_settings: list[tuple[BaseSettings, dict[str, Any]]] = [] - try: - for model, new_values in update: - old_values = {} - for field, value in model.dict().items(): - if field in new_values: - old_values[field] = value - setattr(model, field, new_values[field]) - old_settings.append((model, old_values)) - yield - finally: - for model, old_values in old_settings: - for field, old_val in old_values.items(): - setattr(model, field, old_val) - - class GenericMockRepository(AbstractRepository[ModelT], Generic[ModelT]): """A repository implementation for tests. @@ -228,95 +186,3 @@ def seed_collection(cls, instances: Iterable[ModelT]) -> None: def clear_collection(cls) -> None: """Empty the collection for repository type.""" cls.collection = {} - - -class ControllerTest: - """Standard controller testing utility.""" - - def __init__( - self, - client: TestClient, - base_path: str, - collection: Sequence[orm.Base], - raw_collection: Sequence[dict[str, Any]], - service_type: type[Service], - monkeypatch: MonkeyPatch, - collection_filters: dict[str, Any] | None = None, - ) -> None: - """Perform standard tests of controllers. - - Args: - client: Test client instance. - base_path: Path for POST and collection GET requests. - collection: Collection of domain objects. - raw_collection: Collection of raw representations of domain objects. - service_type: The domain Service object type. - monkeypatch: Pytest's monkeypatch. - collection_filters: Collection filters for GET collection request. - """ - self.client = client - self.base_path = base_path - self.collection = collection - self.raw_collection = raw_collection - self.service_type = service_type - self.monkeypatch = monkeypatch - self.collection_filters = collection_filters - - def _get_random_member(self) -> Any: - return random.choice(self.collection) - - def _get_raw_for_member(self, member: Any) -> dict[str, Any]: - return [item for item in self.raw_collection if item["id"] == str(member.id)][0] - - def test_get_collection(self, with_filters: bool = False) -> None: - """Test collection endpoint get request.""" - - async def _list(*_: Any, **__: Any) -> list[Any]: - return list(self.collection) - - self.monkeypatch.setattr(self.service_type, "list", _list) - - resp = self.client.get( - self.base_path, params=self.collection_filters if with_filters else None - ) - - assert resp.status_code == HTTP_200_OK - assert resp.json() == self.raw_collection - - def test_member_request(self, method: str, service_method: str, exp_status: int) -> None: - """Test member endpoint request.""" - member = self._get_random_member() - raw = self._get_raw_for_member(member) - - async def _method(*_: Any, **__: Any) -> Any: - return member - - self.monkeypatch.setattr(self.service_type, service_method, _method) - - if method.lower() == "post": - url = self.base_path - else: - url = f"{self.base_path}/{member.id}" - - request_kw: dict[str, Any] = {} - if method.lower() in ("put", "post"): - request_kw["json"] = raw - - resp = self.client.request(method, url, **request_kw) - - assert resp.status_code == exp_status - assert resp.json() == raw - - def run(self) -> None: - """Run the tests.""" - # test the collection route with and without filters for branch coverage. - self.test_get_collection() - if self.collection_filters: - self.test_get_collection(with_filters=True) - for method, service_method, status in [ - ("GET", "get", HTTP_200_OK), - ("PUT", "update", HTTP_200_OK), - ("POST", "create", HTTP_201_CREATED), - ("DELETE", "delete", HTTP_200_OK), - ]: - self.test_member_request(method, service_method, status) diff --git a/src/starlite_saqlalchemy/testing/modify_settings.py b/src/starlite_saqlalchemy/testing/modify_settings.py new file mode 100644 index 00000000..95c4b8be --- /dev/null +++ b/src/starlite_saqlalchemy/testing/modify_settings.py @@ -0,0 +1,37 @@ +"""A context manager to support patching application settings.""" +from __future__ import annotations + +from contextlib import contextmanager +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Generator + from typing import Any + + from pydantic import BaseSettings + + +@contextmanager +def modify_settings(*update: tuple[BaseSettings, dict[str, Any]]) -> Generator[None, None, None]: + """Context manager that modify the desired settings and restore them on + exit. + + >>> assert settings.app.ENVIRONMENT = "local" + >>> with modify_settings((settings.app, {"ENVIRONMENT": "prod"})): + >>> assert settings.app.ENVIRONMENT == "prod" + >>> assert settings.app.ENVIRONMENT == "local" + """ + old_settings: list[tuple[BaseSettings, dict[str, Any]]] = [] + try: + for model, new_values in update: + old_values = {} + for field, value in model.dict().items(): + if field in new_values: + old_values[field] = value + setattr(model, field, new_values[field]) + old_settings.append((model, old_values)) + yield + finally: + for model, old_values in old_settings: + for field, old_val in old_values.items(): + setattr(model, field, old_val) diff --git a/src/starlite_saqlalchemy/utils.py b/src/starlite_saqlalchemy/utils.py index c140ac36..d949f2dc 100644 --- a/src/starlite_saqlalchemy/utils.py +++ b/src/starlite_saqlalchemy/utils.py @@ -3,6 +3,11 @@ from typing import Any +def case_insensitive_string_compare(a: str, b: str, /) -> bool: + """Compare `a` and `b`, stripping whitespace and ignoring case.""" + return a.strip().lower() == b.strip().lower() + + def dataclass_as_dict_shallow(dataclass: Any, *, exclude_none: bool = False) -> dict[str, Any]: """Convert a dataclass to dict, without deepcopy.""" ret: dict[str, Any] = {} diff --git a/tests.env b/tests.env index 774c73dd..e5a31403 100644 --- a/tests.env +++ b/tests.env @@ -1,43 +1,3 @@ # App -BUILD_NUMBER= -DEBUG=true ENVIRONMENT=test NAME=my-starlite-app - -# API -API_CACHE_EXPIRATION=60 -API_DEFAULT_PAGINATION_LIMIT=100 -API_HEALTH_PATH=/health -API_DB_SESSION_DEPENDENCY_KEY=db_session - -# OpenAPI -OPENAPI_CONTACT_EMAIL=some_human@email.com -OPENAPI_CONTACT_NAME="Some Human" -OPENAPI_TITLE="My Starlite App" -OPENAPI_VERSION=1.0.0 - -# Sentry -SENTRY_DSN= -SENTRY_TRACES_SAMPLE_RATE=0.0001 - -# Redis -REDIS_URL=redis://cache.local:6379/0 - -# Database -DB_ECHO=false -DB_ECHO_POOL=false -DB_POOL_DISABLE=false -DB_POOL_MAX_OVERFLOW=10 -DB_POOL_SIZE=5 -DB_POOL_TIMEOUT=30 -DB_URL=postgresql+asyncpg://postgres:mysecretpassword@pg.db.local:5432/db - -# Log -LOG_EXCLUDE_PATHS="\A(?!x)x" -LOG_OBFUSCATE_COOKIES=["session"] -LOG_OBFUSCATE_HEADERS=["Authorization", "X-API-KEY"] -LOG_REQUEST_FIELDS=["path","method","content_type","headers","cookies","query","path_params","body"] -LOG_RESPONSE_FIELDS=["status_code","cookies","headers","body"] -LOG_HTTP_EVENT="HTTP" -LOG_WORKER_EVENT="Worker" -LOG_LEVEL=20 diff --git a/tests/conftest.py b/tests/conftest.py index fa2db5ed..761a66f5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,12 +8,7 @@ import pytest from asyncpg.pgproto import pgproto -from starlite import Starlite -from structlog.contextvars import clear_contextvars -from structlog.testing import CapturingLogger -import starlite_saqlalchemy -from starlite_saqlalchemy import ConfigureApp, log from tests.utils.domain import authors, books if TYPE_CHECKING: @@ -25,39 +20,6 @@ from pytest import MonkeyPatch -@pytest.fixture(name="cap_logger") -def fx_capturing_logger(monkeypatch: MonkeyPatch) -> CapturingLogger: - """Used to monkeypatch the app logger, so we can inspect output.""" - cap_logger = CapturingLogger() - starlite_saqlalchemy.log.configure( - starlite_saqlalchemy.log.default_processors # type:ignore[arg-type] - ) - # clear context for every test - clear_contextvars() - # pylint: disable=protected-access - logger = starlite_saqlalchemy.log.controller.LOGGER.bind() - logger._logger = cap_logger - # drop rendering processor to get a dict, not bytes - # noinspection PyProtectedMember - logger._processors = log.default_processors[:-1] - monkeypatch.setattr(starlite_saqlalchemy.log.controller, "LOGGER", logger) - monkeypatch.setattr(starlite_saqlalchemy.log.worker, "LOGGER", logger) - return cap_logger - - -@pytest.fixture(name="app") -def fx_app() -> Starlite: - """Always use this `app` fixture and never do `from app.main import app` - inside a test module. We need to delay import of the `app.main` module - until as late as possible to ensure we can mock everything necessary before - the application instance is constructed. - - Returns: - The application instance. - """ - return Starlite(route_handlers=[], on_app_init=[ConfigureApp()], openapi_config=None) - - @pytest.fixture(name="raw_authors") def fx_raw_authors() -> list[dict[str, Any]]: """Unstructured author representations.""" diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index f960b52a..ad1d9ece 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,9 +1,9 @@ """Config for integration tests.""" -# pylint: disable=redefined-outer-name from __future__ import annotations import asyncio import timeit +from asyncio import AbstractEventLoop, get_event_loop_policy from pathlib import Path from typing import TYPE_CHECKING @@ -16,15 +16,13 @@ from sqlalchemy.engine import URL from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine from sqlalchemy.pool import NullPool -from starlite import Provide, Router from starlite_saqlalchemy import db, sqlalchemy_plugin, worker from starlite_saqlalchemy.health import AppHealthCheck, HealthController from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyHealthCheck -from tests.utils import controllers if TYPE_CHECKING: - from collections import abc + from collections.abc import AsyncIterator, Awaitable, Callable, Iterator from typing import Any from pytest_docker.plugin import Services # type:ignore[import] @@ -37,10 +35,10 @@ @pytest.fixture(scope="session") -def event_loop() -> abc.Iterator[asyncio.AbstractEventLoop]: +def event_loop() -> Iterator[AbstractEventLoop]: """Need the event loop scoped to the session so that we can use it to check containers are ready in session scoped containers fixture.""" - policy = asyncio.get_event_loop_policy() + policy = get_event_loop_policy() loop = policy.new_event_loop() yield loop loop.close() @@ -56,7 +54,7 @@ def docker_compose_file() -> Path: async def wait_until_responsive( - check: abc.Callable[..., abc.Awaitable], timeout: float, pause: float, **kwargs: Any + check: Callable[..., Awaitable], timeout: float, pause: float, **kwargs: Any ) -> None: """Wait until a service is responsive. @@ -130,8 +128,8 @@ async def _containers( await wait_until_responsive(timeout=30.0, pause=0.1, check=redis_responsive, host=docker_ip) -@pytest.fixture() -async def redis(docker_ip: str) -> Redis: +@pytest.fixture(name="redis") +async def fx_redis(docker_ip: str) -> Redis: """ Args: @@ -143,8 +141,8 @@ async def redis(docker_ip: str) -> Redis: return Redis(host=docker_ip, port=6397) -@pytest.fixture() -async def engine(docker_ip: str) -> AsyncEngine: +@pytest.fixture(name="engine") +async def fx_engine(docker_ip: str) -> AsyncEngine: """Postgresql instance for end-to-end testing. Args: @@ -169,7 +167,7 @@ async def engine(docker_ip: str) -> AsyncEngine: @pytest.fixture(autouse=True) -async def _seed_db(engine: AsyncEngine, authors: list[Author]) -> abc.AsyncIterator[None]: +async def _seed_db(engine: AsyncEngine, authors: list[Author]) -> AsyncIterator[None]: """Populate test database with. Args: @@ -207,42 +205,8 @@ def _patch_redis(app: Starlite, redis: Redis, monkeypatch: pytest.MonkeyPatch) - monkeypatch.setattr(worker.queue, "redis", redis) -@pytest.fixture() -def router() -> Router: - """ - Returns: - This is a router with controllers added for testing against the test domain. - """ - return Router( - path="/authors", - route_handlers=[ - controllers.get_authors, - controllers.create_author, - controllers.get_author, - controllers.update_author, - controllers.delete_author, - ], - dependencies={"service": Provide(controllers.provides_service)}, - tags=["Authors"], - ) - - -@pytest.fixture() -def app(app: Starlite, router: Router) -> Starlite: - """ - Args: - app: App from outermost conftest.py - router: Router with controllers for tests. - - Returns: - App with router attached for integration tests. - """ - app.register(router) - return app - - @pytest.fixture(name="client") -async def fx_client(app: Starlite) -> abc.AsyncIterator[AsyncClient]: +async def fx_client(app: Starlite) -> AsyncIterator[AsyncClient]: """Async client that calls requests on the app. We need to use `httpx.AsyncClient` here, as `starlite.TestClient` creates its own event loop to diff --git a/tests/integration/test_authors.py b/tests/integration/test_authors.py index 8efd2f91..5cbe659d 100644 --- a/tests/integration/test_authors.py +++ b/tests/integration/test_authors.py @@ -3,13 +3,10 @@ from typing import TYPE_CHECKING -import pytest - if TYPE_CHECKING: from httpx import AsyncClient -@pytest.mark.xfail() async def test_update_author(client: AsyncClient) -> None: """Integration test for PUT route.""" response = await client.put( diff --git a/tests/integration/test_logging.py b/tests/integration/test_logging.py index 7ec09c88..a3fe8678 100644 --- a/tests/integration/test_logging.py +++ b/tests/integration/test_logging.py @@ -48,7 +48,6 @@ async def test_logging(app: "Starlite", cap_logger: CapturingLogger) -> None: "request": { "path": "/authors/97108ac1-ffcb-411d-8b1e-d9183399f63b", "method": "PUT", - "content_type": ("application/json", {}), "headers": { "host": "testserver", "accept": "*/*", diff --git a/tests/pytest_plugin/__init__.py b/tests/pytest_plugin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/pytest_plugin/conftest.py b/tests/pytest_plugin/conftest.py new file mode 100644 index 00000000..0172cf77 --- /dev/null +++ b/tests/pytest_plugin/conftest.py @@ -0,0 +1,4 @@ +"""Enable the `pytester` fixture for the plugin tests.""" +from __future__ import annotations + +pytest_plugins = ["pytester"] diff --git a/tests/pytest_plugin/test_plugin.py b/tests/pytest_plugin/test_plugin.py new file mode 100644 index 00000000..75d00c93 --- /dev/null +++ b/tests/pytest_plugin/test_plugin.py @@ -0,0 +1,174 @@ +"""Test suite for pytest plugin.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pytest import Pytester + + +def test_pytest_addoption(pytester: Pytester) -> None: + """Test ini options added.""" + pytester.makepyfile( + """ + from pytest import Parser + from pytest_starlite_saqlalchemy import pytest_addoption + + def test_pytest_addoption() -> None: + parser = Parser() + pytest_addoption(parser) + assert parser._ininames == ["test_app", "unit_test_pattern"] + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1) + + +def test_is_unit_test_true(pytester: Pytester) -> None: + """Test is_unit_test fixture True conditions.""" + pytester.makepyprojecttoml( + f""" + [tool.pytest.ini_options] + unit_test_pattern = "^{pytester.path}/test_is_unit_test_true.py$" + """ + ) + pytester.makepyfile( + """ + from unittest.mock import MagicMock + from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyConfig + from starlite_saqlalchemy.worker import Worker + + def test_is_unit_test_true(is_unit_test: bool) -> None: + assert is_unit_test is True + + def test_patch_sqlalchemy_plugin() -> None: + assert isinstance(SQLAlchemyConfig.on_shutdown, MagicMock) + + def test_patch_worker() -> None: + assert isinstance(Worker.on_app_startup, MagicMock) + assert isinstance(Worker.stop, MagicMock) + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=3) + + +def test_is_unit_test_false(pytester: Pytester) -> None: + """Unit is_unit_test fixture False conditions.""" + pytester.makepyprojecttoml( + """ + [tool.pytest.ini_options] + unit_test_pattern = "^definitely/not/the/path/to/test_is_unit_test_false.py$" + """ + ) + pytester.makepyfile( + """ + from unittest.mock import MagicMock + from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyConfig + from starlite_saqlalchemy.worker import Worker + + def test_is_unit_test_false(is_unit_test: bool) -> None: + assert is_unit_test is False + + def test_patch_sqlalchemy_plugin() -> None: + assert not isinstance(SQLAlchemyConfig.on_shutdown, MagicMock) + + def test_patch_worker() -> None: + assert not isinstance(Worker.on_app_startup, MagicMock) + assert not isinstance(Worker.stop, MagicMock) + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=3) + + +def test_patch_http_close(pytester: Pytester) -> None: + """Test that http clients won't be closed in-between tests.""" + pytester.makepyfile( + """ + import starlite_saqlalchemy + + client = starlite_saqlalchemy.http.Client("https://somewhere.com") + assert starlite_saqlalchemy.http.clients + + def test_patch_http_close(is_unit_test: bool) -> None: + assert not starlite_saqlalchemy.http.clients + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1) + + +def test_app_fixture_if_app_factory(pytester: Pytester) -> None: + """Test that the app fixture returns an instance retrieved from a + factory.""" + pytester.makepyprojecttoml( + """ + [tool.pytest.ini_options] + test_app = "tests.utils.app:create_app" + """ + ) + pytester.makepyfile( + """ + from starlite import Starlite + + def test_app(app): + assert isinstance(app, Starlite) + assert "/authors" in app.route_handler_method_map + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1) + + +def test_app_fixture_if_app_instance(pytester: Pytester) -> None: + """Test that the app fixture returns the an instance if the path points to + one.""" + pytester.syspathinsert() + pytester.makepyfile( + test_app=""" + from tests.utils.app import create_app + + app = create_app() + """ + ) + pytester.makepyprojecttoml( + """ + [tool.pytest.ini_options] + test_app = "test_app:app" + """ + ) + pytester.makepyfile( + """ + from starlite import Starlite + + def test_app(app): + assert isinstance(app, Starlite) + assert "/authors" in app.route_handler_method_map + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1) + + +def test_app_fixture_if_test_app_path_does_not_exist(pytester: Pytester) -> None: + """Tests that the app fixture falls back to a new app instance if the + configured path is not found.""" + pytester.makepyprojecttoml( + """ + [tool.pytest.ini_options] + test_app = "definitely.not.the.path.to.app:app" + """ + ) + pytester.makepyfile( + """ + from starlite import Starlite + + def test_app(app): + assert isinstance(app, Starlite) + # the app that is created should not have any handlers attached. + assert app.route_handler_method_map.keys() == {"/health"} + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 386f6eab..16871add 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -2,15 +2,12 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import MagicMock import pytest from saq.job import Job from starlite.datastructures import State from starlite.enums import ScopeType -from starlite.testing import TestClient -from starlite_saqlalchemy import sqlalchemy_plugin, worker from starlite_saqlalchemy.testing import GenericMockRepository from tests.utils.domain.authors import Author from tests.utils.domain.authors import Service as AuthorService @@ -20,34 +17,11 @@ from ..utils import controllers if TYPE_CHECKING: - from collections import abc - from pytest import MonkeyPatch from starlite import Starlite from starlite.types import HTTPResponseBodyEvent, HTTPResponseStartEvent, HTTPScope -@pytest.fixture(scope="session", autouse=True) -def _patch_sqlalchemy_plugin() -> abc.Iterator: - monkeypatch = pytest.MonkeyPatch() - monkeypatch.setattr( - sqlalchemy_plugin.SQLAlchemyConfig, # type:ignore[attr-defined] - "on_shutdown", - MagicMock(), - ) - yield - monkeypatch.undo() - - -@pytest.fixture(scope="session", autouse=True) -def _patch_worker() -> abc.Iterator: - monkeypatch = pytest.MonkeyPatch() - monkeypatch.setattr(worker.Worker, "on_app_startup", MagicMock()) - monkeypatch.setattr(worker.Worker, "stop", MagicMock()) - yield - monkeypatch.undo() - - @pytest.fixture(name="author_repository_type") def fx_author_repository_type( authors: list[Author], monkeypatch: pytest.MonkeyPatch @@ -92,27 +66,6 @@ def fx_book_repository( return book_repository_type() -@pytest.fixture(name="app") -def fx_app(app: Starlite, monkeypatch: MonkeyPatch) -> Starlite: - """Remove service readiness checks for unit tests.""" - monkeypatch.setattr(app, "before_startup", []) - return app - - -@pytest.fixture(name="client") -def fx_client(app: Starlite) -> abc.Iterator[TestClient]: - """Client instance attached to app. - - Args: - app: The app for testing. - - Returns: - Test client instance. - """ - with TestClient(app=app) as client_: - yield client_ - - @pytest.fixture() def http_response_start() -> HTTPResponseStartEvent: """ASGI message for start of response.""" diff --git a/tests/unit/test_init_plugin.py b/tests/unit/test_init_plugin.py index a8a53bd2..d86ed868 100644 --- a/tests/unit/test_init_plugin.py +++ b/tests/unit/test_init_plugin.py @@ -8,7 +8,7 @@ from starlite import Starlite from starlite.cache import SimpleCacheBackend -from starlite_saqlalchemy import init_plugin +from starlite_saqlalchemy import init_plugin, sentry if TYPE_CHECKING: from typing import Any @@ -82,3 +82,15 @@ def test_ensure_list(in_: Any, out: Any) -> None: """Test _ensure_list() functionality.""" # pylint: disable=protected-access assert init_plugin.ConfigureApp._ensure_list(in_) == out + + +@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.""" + 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()]) + assert bool(sentry.configure in app.on_startup) is exp # noqa: SIM901 diff --git a/tests/unit/test_log.py b/tests/unit/test_log.py index 0373b16a..f10ce12b 100644 --- a/tests/unit/test_log.py +++ b/tests/unit/test_log.py @@ -292,7 +292,6 @@ async def test_before_send_handler_extract_request_data( assert data == { "path": "/", "method": "POST", - "content_type": ("application/json", {}), "headers": {"content-length": "10", "content-type": "application/json"}, "cookies": {}, "query": b"", @@ -436,7 +435,6 @@ def test_handler() -> str: assert "exception" not in call.kwargs -@pytest.mark.xfail(reason="starlite is returning 500 for invalid payloads as of v1.48.1") async def test_log_request_with_invalid_json_payload(client: TestClient[Starlite]) -> None: """Test logs emitted with invalid client payload. diff --git a/tests/unit/test_scripts.py b/tests/unit/test_scripts.py index 452e2911..bb950e57 100644 --- a/tests/unit/test_scripts.py +++ b/tests/unit/test_scripts.py @@ -1,27 +1,33 @@ """Tests for scripts.py.""" +from __future__ import annotations + +from typing import TYPE_CHECKING import pytest -from starlite_saqlalchemy import settings -from starlite_saqlalchemy.scripts import determine_reload_dirs, determine_should_reload +from starlite_saqlalchemy import scripts, settings from starlite_saqlalchemy.testing import modify_settings +if TYPE_CHECKING: + from pytest import MonkeyPatch + @pytest.mark.parametrize(("reload", "expected"), [(None, True), (True, True), (False, False)]) -def test_uvicorn_config_auto_reload_local(reload: bool | None, expected: bool) -> None: +def test_uvicorn_config_auto_reload_local( + reload: bool | None, expected: bool, monkeypatch: MonkeyPatch +) -> None: """Test that setting ENVIRONMENT to 'local' triggers auto reload.""" - with modify_settings( - (settings.app, {"ENVIRONMENT": "local"}), (settings.server, {"RELOAD": reload}) - ): - assert determine_should_reload() is expected + monkeypatch.setattr(scripts, "IS_LOCAL_ENVIRONMENT", True) + with modify_settings((settings.server, {"RELOAD": reload})): + assert scripts.determine_should_reload() is expected @pytest.mark.parametrize("reload", [True, False]) def test_uvicorn_config_reload_dirs(reload: bool) -> None: """Test that RELOAD_DIRS is only used when RELOAD is enabled.""" if not reload: - assert determine_reload_dirs(reload) is None + assert scripts.determine_reload_dirs(reload) is None else: - reload_dirs = determine_reload_dirs(reload) + reload_dirs = scripts.determine_reload_dirs(reload) assert reload_dirs is not None assert reload_dirs == settings.server.RELOAD_DIRS diff --git a/tests/unit/test_sentry.py b/tests/unit/test_sentry.py new file mode 100644 index 00000000..398c09e9 --- /dev/null +++ b/tests/unit/test_sentry.py @@ -0,0 +1,24 @@ +"""Tests for sentry integration.""" + +from typing import TYPE_CHECKING + +import pytest + +from starlite_saqlalchemy import settings +from starlite_saqlalchemy.sentry import SamplingContext, sentry_traces_sampler + +if TYPE_CHECKING: + from starlite.types.asgi_types import HTTPScope + + +@pytest.mark.parametrize( + ("path", "sample_rate"), + [("/watever", settings.sentry.TRACES_SAMPLE_RATE), (settings.api.HEALTH_PATH, 0.0)], +) +def test_sentry_traces_sampler(http_scope: "HTTPScope", path: str, sample_rate: float) -> None: + """Test that traces sampler correctly ignore health requests.""" + http_scope["path"] = path + sentry_context = SamplingContext( + asgi_scope=http_scope, parent_sampled=None, transaction_context={} + ) + assert sentry_traces_sampler(sentry_context) == sample_rate diff --git a/tests/unit/test_testing.py b/tests/unit/test_testing.py index 519f5d5f..2474f3ba 100644 --- a/tests/unit/test_testing.py +++ b/tests/unit/test_testing.py @@ -6,7 +6,11 @@ import httpx import pytest -from starlite.status_codes import HTTP_200_OK, HTTP_404_NOT_FOUND +from starlite.status_codes import ( + HTTP_200_OK, + HTTP_404_NOT_FOUND, + HTTP_405_METHOD_NOT_ALLOWED, +) from starlite_saqlalchemy import testing from starlite_saqlalchemy.exceptions import ConflictError, StarliteSaqlalchemyError @@ -210,3 +214,12 @@ def test_tester_run_method(params: dict[str, Any] | None) -> None: ("test_member_request", ("POST", "create", 201), {}), ("test_member_request", ("DELETE", "delete", 200), {}), ] + + +def test_tester_ignores_405_response( + tester: testing.ControllerTest, mock_response: MagicMock +) -> None: + """Test that 405 responses don't raise from asserts.""" + mock_response.status_code = HTTP_405_METHOD_NOT_ALLOWED + tester.test_get_collection() + tester.test_member_request("GET", "get", HTTP_200_OK) diff --git a/tests/utils/app.py b/tests/utils/app.py new file mode 100644 index 00000000..f821df42 --- /dev/null +++ b/tests/utils/app.py @@ -0,0 +1,16 @@ +"""Test application.""" +from __future__ import annotations + +from starlite import Starlite + +from starlite_saqlalchemy import ConfigureApp + +from . import controllers + + +def create_app() -> Starlite: + """App for our test domain.""" + return Starlite( + route_handlers=[controllers.create_router()], + on_app_init=[ConfigureApp()], + ) diff --git a/tests/utils/controllers.py b/tests/utils/controllers.py index d2898450..7f456178 100644 --- a/tests/utils/controllers.py +++ b/tests/utils/controllers.py @@ -4,7 +4,7 @@ from uuid import UUID from sqlalchemy.ext.asyncio import AsyncSession -from starlite import Dependency, delete, get, post, put +from starlite import Dependency, Provide, Router, delete, get, post, put from starlite.status_codes import HTTP_200_OK from starlite_saqlalchemy.repository.types import FilterTypes @@ -49,3 +49,18 @@ async def update_author(data: WriteDTO, service: Service, author_id: UUID) -> Re async def delete_author(service: Service, author_id: UUID) -> ReadDTO: """Delete Author by ID.""" return ReadDTO.from_orm(await service.delete(author_id)) + + +def create_router() -> Router: + """Create a router for our test domain controllers.""" + return Router( + path="/authors", + route_handlers=[ + create_author, + delete_author, + get_author, + get_authors, + update_author, + ], + dependencies={"service": Provide(provides_service)}, + ) diff --git a/tox.ini b/tox.ini index a70b4aa5..93c842a5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,21 +1,25 @@ [gh-actions] python = 3.10: py310 - 3.11: py311,integration + 3.11: py311,pytest-plugin,integration [tox] -envlist = pylint,mypy,pyright,py310,py311,integration,coverage +envlist = pylint,mypy,pyright,py310,py311,pytest-plugin,integration,coverage isolated_build = true [testenv] deps = - -r{toxinidir}/dev.requirements.txt + -r{toxinidir}/requirements.dev.txt commands = coverage run -p -m pytest {posargs} +[testenv:pytest-plugin] +basepython = python3.11 +commands = coverage run -p -m pytest tests/pytest_plugin {posargs} + [testenv:coverage] -depends = py310,py311 +depends = py310,py311,pytest-plugin basepython = python3.11 commands = coverage combine @@ -74,6 +78,6 @@ basepython = python3.11 passenv = HOME deps = - -r{toxinidir}/docs.requirements.txt + -r{toxinidir}/requirements.docs.txt commands = mike {posargs: serve}