From 6fe1887bc8f9dd078282c303f60686e6623a0a00 Mon Sep 17 00:00:00 2001 From: gazorby Date: Sun, 8 Jan 2023 17:30:22 +0100 Subject: [PATCH 01/30] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(deps):=20re?= =?UTF-8?q?quire=20dependencies=20based=20on=20plugin=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev-noextras.requirements.txt | 9 +++++ poetry.lock | 21 ++++++----- pyproject.toml | 29 +++++++++++---- requirements.dev.txt | 4 +++ src/starlite_saqlalchemy/__init__.py | 6 ---- src/starlite_saqlalchemy/exceptions.py | 4 +++ src/starlite_saqlalchemy/init_plugin.py | 39 ++++++++++++++++---- src/starlite_saqlalchemy/service.py | 3 +- tests/unit/conftest.py | 7 ---- tests/unit/test_init_plugin.py | 7 ++-- tests/unit/test_init_plugin_no_extras.py | 45 ++++++++++++++++++++++++ tox.ini | 7 ++++ 12 files changed, 143 insertions(+), 38 deletions(-) create mode 100644 dev-noextras.requirements.txt create mode 100644 tests/unit/test_init_plugin_no_extras.py diff --git a/dev-noextras.requirements.txt b/dev-noextras.requirements.txt new file mode 100644 index 00000000..cca7eb20 --- /dev/null +++ b/dev-noextras.requirements.txt @@ -0,0 +1,9 @@ +asgi-lifespan == 2.0.0 +coverage[toml] == 7.0.0; python_version < '3.11' +coverage == 7.0.1; python_version >= '3.11' +cryptography == 39.0.0 +pytest == 7.2.0 +pytest-asyncio == 0.20.3 +pytest-dotenv == 0.5.2 +pytest_docker == 1.0.1 +rich == 13.0.0 diff --git a/poetry.lock b/poetry.lock index ae8e4410..d073722c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -26,7 +26,7 @@ name = "async-timeout" version = "4.0.2" description = "Timeout context manager for asyncio programs" category = "main" -optional = false +optional = true python-versions = ">=3.6" files = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, @@ -128,7 +128,7 @@ name = "croniter" version = "1.3.8" description = "croniter provides iteration for datetime object with cron like format" category = "main" -optional = false +optional = true python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "croniter-1.3.8-py2.py3-none-any.whl", hash = "sha256:d6ed8386d5f4bbb29419dc1b65c4909c04a2322bd15ec0dc5b2877bfa1b75c7a"}, @@ -310,7 +310,7 @@ name = "hiredis" version = "2.1.1" description = "Python wrapper for hiredis" category = "main" -optional = false +optional = true python-versions = ">=3.7" files = [ {file = "hiredis-2.1.1-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:f15e48545dadf3760220821d2f3c850e0c67bbc66aad2776c9d716e6216b5103"}, @@ -708,7 +708,7 @@ name = "packaging" version = "23.0" description = "Core utilities for Python packages" category = "main" -optional = false +optional = true python-versions = ">=3.7" files = [ {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, @@ -886,7 +886,7 @@ name = "redis" version = "4.3.5" description = "Python client for Redis database and key-value store" category = "main" -optional = false +optional = true python-versions = ">=3.6" files = [ {file = "redis-4.3.5-py3-none-any.whl", hash = "sha256:46652271dc7525cd5a9667e5b0ca983c848c75b2b8f7425403395bb8379dcf25"}, @@ -924,7 +924,7 @@ name = "saq" version = "0.9.2" description = "Distributed Python job queue with asyncio and redis" category = "main" -optional = false +optional = true python-versions = "*" files = [ {file = "saq-0.9.2-py3-none-any.whl", hash = "sha256:9ae0636f8ffe92fa5a9ee68a92828a562d5f0f241762ddf9744df694e33a4ab5"}, @@ -945,7 +945,7 @@ name = "sentry-sdk" version = "1.13.0" description = "Python client for Sentry (https://sentry.io)" category = "main" -optional = false +optional = true python-versions = "*" files = [ {file = "sentry-sdk-1.13.0.tar.gz", hash = "sha256:72da0766c3069a3941eadbdfa0996f83f5a33e55902a19ba399557cfee1dddcc"}, @@ -1182,7 +1182,7 @@ name = "urllib3" version = "1.26.14" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" -optional = false +optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, @@ -1258,6 +1258,11 @@ dev = ["Cython (>=0.29.32,<0.30.0)", "Sphinx (>=4.1.2,<4.2.0)", "aiohttp", "flak docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] test = ["Cython (>=0.29.32,<0.30.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=22.0.0,<22.1.0)", "pycodestyle (>=2.7.0,<2.8.0)"] +[extras] +cache = ["redis", "hiredis"] +sentry = ["sentry-sdk"] +worker = ["saq"] + [metadata] lock-version = "2.0" python-versions = "^3.10" diff --git a/pyproject.toml b/pyproject.toml index ca82afc5..151a2c8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,17 @@ license = "MIT" authors = ["Peter Schutt "] readme = "README.md" repository = "https://github.com/topsport-com-au/starlite-saqlalchemy" -keywords = ["api", "rest", "http", "asgi", "starlite", "saq", "sqlalchemy", "plugin", "python"] +keywords = [ + "api", + "rest", + "http", + "asgi", + "starlite", + "saq", + "sqlalchemy", + "plugin", + "python", +] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Plugins", @@ -58,20 +68,27 @@ packages = [ [tool.poetry.dependencies] python = "^3.10" asyncpg = "*" -hiredis = "*" httpx = "*" msgspec = "*" pydantic = "*" python-dotenv = "*" -redis = "*" -saq = "^0.9.1" -sentry-sdk = ">=1.13.0" sqlalchemy = "==2.0.0rc2" starlite = "^1.40.1" -structlog = ">=22.2.0" tenacity = "*" uvicorn = "*" uvloop = "*" +structlog = ">=22.2.0" + +# Optionals +hiredis = { version = "*", optional = true } +redis = { version = "*", optional = true } +saq = { version = "^0.9.1", optional = true } +sentry-sdk = { version = "*", optional = true } + +[tool.poetry.extras] +cache = ["redis", "hiredis"] +worker = ["saq"] +sentry = ["sentry-sdk"] [tool.poetry.plugins."pytest11"] pytest_starlite_saqlalchemy = "pytest_starlite_saqlalchemy" diff --git a/requirements.dev.txt b/requirements.dev.txt index 3a8a8da6..863aaa55 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -7,3 +7,7 @@ pytest-asyncio == 0.20.3 pytest-dotenv == 0.5.2 pytest_docker == 1.0.1 rich == 13.0.1 +sentry-sdk +hiredis +redis +saq >= "0.9.1" diff --git a/src/starlite_saqlalchemy/__init__.py b/src/starlite_saqlalchemy/__init__.py index 46f0cfc8..b1a6c6f9 100644 --- a/src/starlite_saqlalchemy/__init__.py +++ b/src/starlite_saqlalchemy/__init__.py @@ -22,7 +22,6 @@ def example_handler() -> dict: # this is because pycharm wigs out when there is a module called `exceptions`: # noinspection PyCompatibility from . import ( - cache, compression, db, dependencies, @@ -32,9 +31,7 @@ def example_handler() -> dict: http, log, openapi, - redis, repository, - sentry, service, settings, sqlalchemy_plugin, @@ -46,7 +43,6 @@ def example_handler() -> dict: __all__ = [ "ConfigureApp", "PluginConfig", - "cache", "compression", "db", "dependencies", @@ -56,9 +52,7 @@ def example_handler() -> dict: "http", "log", "openapi", - "redis", "repository", - "sentry", "service", "settings", "sqlalchemy_plugin", diff --git a/src/starlite_saqlalchemy/exceptions.py b/src/starlite_saqlalchemy/exceptions.py index 6f2caef3..a545de3f 100644 --- a/src/starlite_saqlalchemy/exceptions.py +++ b/src/starlite_saqlalchemy/exceptions.py @@ -56,6 +56,10 @@ class AuthorizationError(StarliteSaqlalchemyClientError): """A user tried to do something they shouldn't have.""" +class MissingDependencyError(StarliteSaqlalchemyError): + """A required dependency is not installed.""" + + class HealthCheckConfigurationError(StarliteSaqlalchemyError): """An error occurred while registering an health check.""" diff --git a/src/starlite_saqlalchemy/init_plugin.py b/src/starlite_saqlalchemy/init_plugin.py index 6eb505e4..3da2d746 100644 --- a/src/starlite_saqlalchemy/init_plugin.py +++ b/src/starlite_saqlalchemy/init_plugin.py @@ -30,6 +30,7 @@ def example_handler() -> dict: from __future__ import annotations from collections.abc import Callable, Sequence # noqa: TC003 +from importlib import import_module from typing import TYPE_CHECKING, Any, TypeVar from pydantic import BaseModel @@ -39,7 +40,6 @@ def example_handler() -> dict: from structlog.types import Processor # noqa: TC002 from starlite_saqlalchemy import ( - cache, compression, dependencies, exceptions, @@ -47,13 +47,11 @@ def example_handler() -> dict: lifespan, log, openapi, - redis, - sentry, settings, sqlalchemy_plugin, ) from starlite_saqlalchemy.constants import IS_LOCAL_ENVIRONMENT, IS_TEST_ENVIRONMENT -from starlite_saqlalchemy.exceptions import HealthCheckConfigurationError +from starlite_saqlalchemy.exceptions import HealthCheckConfigurationError, MissingDependencyError from starlite_saqlalchemy.health import ( AbstractHealthCheck, AppHealthCheck, @@ -62,7 +60,6 @@ def example_handler() -> dict: from starlite_saqlalchemy.service import make_service_callback from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyHealthCheck from starlite_saqlalchemy.type_encoders import type_encoders_map -from starlite_saqlalchemy.worker import create_worker_instance if TYPE_CHECKING: from starlite.config.app import AppConfig @@ -209,7 +206,13 @@ def __call__(self, app_config: AppConfig) -> AppConfig: self.configure_worker(app_config) app_config.before_startup = lifespan.before_startup_handler - app_config.on_shutdown.extend([http.on_shutdown, redis.client.close]) + app_config.on_shutdown.append(http.on_shutdown) + if self.config.do_cache: + from starlite_saqlalchemy import ( # pylint: disable=import-outside-toplevel + redis, + ) + + app_config.on_shutdown.append(redis.client.close) return app_config def configure_after_exception(self, app_config: AppConfig) -> None: @@ -232,6 +235,11 @@ def configure_cache(self, app_config: AppConfig) -> None: app_config: The Starlite application config object. """ if self.config.do_cache and app_config.cache_config == DEFAULT_CACHE_CONFIG: + self._check_module_installed("redis", "cache") + from starlite_saqlalchemy import ( # pylint: disable=import-outside-toplevel + cache, + ) + app_config.cache_config = cache.config def configure_collection_dependencies(self, app_config: AppConfig) -> None: @@ -337,6 +345,11 @@ def configure_sentry(self, app_config: AppConfig) -> None: else not (IS_LOCAL_ENVIRONMENT or IS_TEST_ENVIRONMENT) ) if do_sentry: + self._check_module_installed("sentry_sdk", "sentry") + from starlite_saqlalchemy import ( # pylint: disable=import-outside-toplevel + sentry, + ) + app_config.on_startup.append(sentry.configure) def configure_sqlalchemy_plugin(self, app_config: AppConfig) -> None: @@ -370,6 +383,11 @@ def configure_worker(self, app_config: AppConfig) -> None: app_config: The Starlite application config object. """ if self.config.do_worker: + self._check_module_installed("saq", "worker") + from starlite_saqlalchemy.worker import ( # pylint: disable=import-outside-toplevel + create_worker_instance, + ) + worker_kwargs: dict[str, Any] = {"functions": self.config.worker_functions} if self.config.do_logging: worker_kwargs["before_process"] = log.worker.before_process @@ -383,3 +401,12 @@ def _ensure_list(item: list[T] | T) -> list[T]: if isinstance(item, list): return item return [] if item is None else [item] + + def _check_module_installed(self, module: str, config: str) -> None: + try: + import_module(module) + except ModuleNotFoundError as error: + raise MissingDependencyError( + f'You enabled {config} configuration but package "{module}" is not installed. ' + f'You may need to run: "poetry install starlite-saqlalchemy[{config}]"' + ) from error diff --git a/src/starlite_saqlalchemy/service.py b/src/starlite_saqlalchemy/service.py index 8660d177..bf215d97 100644 --- a/src/starlite_saqlalchemy/service.py +++ b/src/starlite_saqlalchemy/service.py @@ -17,7 +17,6 @@ from starlite_saqlalchemy.db import async_session_factory from starlite_saqlalchemy.exceptions import NotFoundError from starlite_saqlalchemy.repository.sqlalchemy import ModelT -from starlite_saqlalchemy.worker import default_job_config_dict, queue if TYPE_CHECKING: from collections.abc import AsyncIterator @@ -135,6 +134,8 @@ async def enqueue_background_task( job_config: Configuration object to control the job that is enqueued. **kwargs: Arguments to be passed to the method when called. Must be JSON serializable. """ + from starlite_saqlalchemy.worker import queue, default_job_config_dict # pylint: disable=C0415 + module = inspect.getmodule(self) if module is None: # pragma: no cover logger.warning("Callback not enqueued, no module resolved for %s", self) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 16871add..72a7eb4a 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING import pytest -from saq.job import Job from starlite.datastructures import State from starlite.enums import ScopeType @@ -113,9 +112,3 @@ def http_scope(app: Starlite) -> HTTPScope: def state() -> State: """Starlite application state datastructure.""" return State() - - -@pytest.fixture() -def job() -> Job: - """SAQ Job instance.""" - return Job(function="whatever", kwargs={"a": "b"}) diff --git a/tests/unit/test_init_plugin.py b/tests/unit/test_init_plugin.py index d86ed868..f65bdee1 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, sentry +from starlite_saqlalchemy import init_plugin, worker, sentry if TYPE_CHECKING: from typing import Any @@ -46,8 +46,7 @@ def test_config_switches() -> None: assert app.openapi_config is None assert app.response_class is None assert isinstance(app.cache.backend, SimpleCacheBackend) - # client.close and redis.close go in there unconditionally atm - assert len(app.on_shutdown) == 2 + assert len(app.on_shutdown) == 1 assert not app.after_exception assert not app.dependencies assert not app.exception_handlers @@ -60,7 +59,7 @@ def test_do_worker_but_not_logging(monkeypatch: MonkeyPatch) -> None: """Tests branch where we can have the worker enabled, but logging disabled.""" mock = MagicMock() - monkeypatch.setattr(init_plugin, "create_worker_instance", mock) + monkeypatch.setattr(worker, "create_worker_instance", mock) config = init_plugin.PluginConfig(do_logging=False) Starlite(route_handlers=[], on_app_init=[init_plugin.ConfigureApp(config=config)]) mock.assert_called_once() diff --git a/tests/unit/test_init_plugin_no_extras.py b/tests/unit/test_init_plugin_no_extras.py new file mode 100644 index 00000000..5d9ee632 --- /dev/null +++ b/tests/unit/test_init_plugin_no_extras.py @@ -0,0 +1,45 @@ +"""Tests for init_plugin.py when no extra dependencies are installed.""" + +import pytest +from starlite import Starlite + +from starlite_saqlalchemy import init_plugin +from starlite_saqlalchemy.exceptions import MissingDependencyError + + +@pytest.mark.parametrize( + ("enabled_config", "error"), + [ + ("do_cache", r"^.*\"redis\" is not installed.*$"), + ("do_sentry", r"^.*\"sentry_sdk\" is not installed.*$"), + ("do_worker", r"^.*\"saq\" is not installed.*$"), + ], +) +def test_extra_dependencies_not_installed(enabled_config: str, error: str) -> None: + """Tests that the plugin test required dependencies for switches needing + them.""" + + kwargs = { + "do_after_exception": False, + "do_cache": False, + "do_compression": False, + "do_collection_dependencies": False, + "do_exception_handlers": False, + "do_health_check": False, + "do_logging": False, + "do_openapi": False, + "do_sentry": False, + "do_set_debug": False, + "do_sqlalchemy_plugin": False, + "do_type_encoders": False, + "do_worker": False, + **{enabled_config: True}, + } + config = init_plugin.PluginConfig(**kwargs) + + with pytest.raises(MissingDependencyError, match=error): + Starlite( + route_handlers=[], + openapi_config=None, + on_app_init=[init_plugin.ConfigureApp(config=config)], + ) diff --git a/tox.ini b/tox.ini index 93c842a5..dfd3119b 100644 --- a/tox.ini +++ b/tox.ini @@ -73,6 +73,13 @@ allowlist_externals = commands = pytest tests/integration {posargs} +[testenv:noextras] +basepython = python3.11 +deps = + -r{toxinidir}/dev-noextras.requirements.txt +commands = + pytest tests/unit/test_missing_dependencies.py {posargs} + [testenv:docs] basepython = python3.11 passenv = From 8fc9720cf8dec13a575074cd94f190cb764cfae6 Mon Sep 17 00:00:00 2001 From: gazorby Date: Sun, 8 Jan 2023 17:34:34 +0100 Subject: [PATCH 02/30] =?UTF-8?q?=F0=9F=90=9B=20fix:=20tox=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index dfd3119b..2930381b 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ deps = -r{toxinidir}/requirements.dev.txt commands = - coverage run -p -m pytest {posargs} + coverage run -p -m pytest --ignore tests/unit/test_init_plugin_no_extras.py {posargs} [testenv:pytest-plugin] basepython = python3.11 @@ -78,7 +78,7 @@ basepython = python3.11 deps = -r{toxinidir}/dev-noextras.requirements.txt commands = - pytest tests/unit/test_missing_dependencies.py {posargs} + pytest tests/unit/test_init_plugin_no_extras.py {posargs} [testenv:docs] basepython = python3.11 From a2f84fb8649b6e31889989c216d816949886a481 Mon Sep 17 00:00:00 2001 From: gazorby Date: Sun, 8 Jan 2023 22:52:25 +0100 Subject: [PATCH 03/30] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(tox):=20app?= =?UTF-8?q?end=20coverage=20in=20testenv:noextras?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 2930381b..843095b3 100644 --- a/tox.ini +++ b/tox.ini @@ -71,7 +71,7 @@ deps = allowlist_externals = docker commands = - pytest tests/integration {posargs} + coverage run -a -p -m pytest tests/integration {posargs} [testenv:noextras] basepython = python3.11 From cb90b5f70359762bb2866238ab58a5587ee67605 Mon Sep 17 00:00:00 2001 From: gazorby Date: Sun, 8 Jan 2023 22:56:49 +0100 Subject: [PATCH 04/30] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(tox):=20don?= =?UTF-8?q?'t=20run=20coverage=20in=20parallel=20mode=20in=20testenv:noext?= =?UTF-8?q?ras?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 843095b3..97c57655 100644 --- a/tox.ini +++ b/tox.ini @@ -71,7 +71,7 @@ deps = allowlist_externals = docker commands = - coverage run -a -p -m pytest tests/integration {posargs} + coverage run -a -m pytest tests/integration {posargs} [testenv:noextras] basepython = python3.11 From ae2c67c2844cbbd804aa1c5ce6a87cb2c89a3214 Mon Sep 17 00:00:00 2001 From: gazorby Date: Sun, 8 Jan 2023 23:16:55 +0100 Subject: [PATCH 05/30] =?UTF-8?q?=F0=9F=90=9B=20fix:=20full=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/starlite_saqlalchemy/log/controller.py | 2 +- tox.ini | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/starlite_saqlalchemy/log/controller.py b/src/starlite_saqlalchemy/log/controller.py index 3dcbc1e8..46427cba 100644 --- a/src/starlite_saqlalchemy/log/controller.py +++ b/src/starlite_saqlalchemy/log/controller.py @@ -197,7 +197,7 @@ async def extract_request_data(self, request: Request) -> dict[str, Any]: value = await value except RuntimeError: if key != REQUEST_BODY_FIELD: - raise + raise # pragma: no cover value = None data[key] = value return data diff --git a/tox.ini b/tox.ini index 97c57655..1a6cad0a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,10 @@ [gh-actions] python = - 3.10: py310 - 3.11: py311,pytest-plugin,integration + 3.10: py310,noextras + 3.11: py311,noextras,pytest-plugin,integration [tox] -envlist = pylint,mypy,pyright,py310,py311,pytest-plugin,integration,coverage +envlist = pylint,mypy,pyright,py310,py311,noextras,pytest-plugin,integration,coverage isolated_build = true [testenv] @@ -71,14 +71,14 @@ deps = allowlist_externals = docker commands = - coverage run -a -m pytest tests/integration {posargs} + pytest tests/integration {posargs} [testenv:noextras] basepython = python3.11 deps = -r{toxinidir}/dev-noextras.requirements.txt commands = - pytest tests/unit/test_init_plugin_no_extras.py {posargs} + coverage run -p -m pytest tests/unit/test_init_plugin_no_extras.py {posargs} [testenv:docs] basepython = python3.11 From 9297233a82a12f3b42d350ab7fb79e6dab6551dc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 12 Jan 2023 07:35:54 +0000 Subject: [PATCH 06/30] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/starlite_saqlalchemy/service.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/starlite_saqlalchemy/service.py b/src/starlite_saqlalchemy/service.py index bf215d97..caa4cabb 100644 --- a/src/starlite_saqlalchemy/service.py +++ b/src/starlite_saqlalchemy/service.py @@ -134,7 +134,10 @@ async def enqueue_background_task( job_config: Configuration object to control the job that is enqueued. **kwargs: Arguments to be passed to the method when called. Must be JSON serializable. """ - from starlite_saqlalchemy.worker import queue, default_job_config_dict # pylint: disable=C0415 + from starlite_saqlalchemy.worker import ( # pylint: disable=C0415 + default_job_config_dict, + queue, + ) module = inspect.getmodule(self) if module is None: # pragma: no cover From a7c8c356f8f295499b30a032793a3bca316c9125 Mon Sep 17 00:00:00 2001 From: gazorby Date: Thu, 12 Jan 2023 09:29:07 +0100 Subject: [PATCH 07/30] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20apply=20?= =?UTF-8?q?changes=20from=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 3 +- src/starlite_saqlalchemy/exceptions.py | 6 ++++ src/starlite_saqlalchemy/init_plugin.py | 41 +++++++++--------------- src/starlite_saqlalchemy/lifespan.py | 8 +++-- src/starlite_saqlalchemy/service.py | 15 ++++----- src/starlite_saqlalchemy/settings.py | 18 +++++++++++ tests/unit/conftest.py | 1 + tests/unit/test_init_plugin_no_extras.py | 7 +++- tox.ini | 4 +-- 9 files changed, 63 insertions(+), 40 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 151a2c8e..2b655944 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,8 +87,9 @@ sentry-sdk = { version = "*", optional = true } [tool.poetry.extras] cache = ["redis", "hiredis"] -worker = ["saq"] +worker = ["saq", "hiredis"] sentry = ["sentry-sdk"] +all = ["redis", "hiredis", "saq", "sentry-sdk"] [tool.poetry.plugins."pytest11"] pytest_starlite_saqlalchemy = "pytest_starlite_saqlalchemy" diff --git a/src/starlite_saqlalchemy/exceptions.py b/src/starlite_saqlalchemy/exceptions.py index a545de3f..9236a9fc 100644 --- a/src/starlite_saqlalchemy/exceptions.py +++ b/src/starlite_saqlalchemy/exceptions.py @@ -59,6 +59,12 @@ class AuthorizationError(StarliteSaqlalchemyClientError): class MissingDependencyError(StarliteSaqlalchemyError): """A required dependency is not installed.""" + def __init__(self, module: str, config: str) -> None: + super().__init__( + f'You enabled {config} configuration but package "{module}" is not installed. ' + f'You may need to run: "poetry install starlite-saqlalchemy[{config}]"' + ) + class HealthCheckConfigurationError(StarliteSaqlalchemyError): """An error occurred while registering an health check.""" diff --git a/src/starlite_saqlalchemy/init_plugin.py b/src/starlite_saqlalchemy/init_plugin.py index 3da2d746..d246f537 100644 --- a/src/starlite_saqlalchemy/init_plugin.py +++ b/src/starlite_saqlalchemy/init_plugin.py @@ -1,3 +1,4 @@ +# pylint: disable=import-outside-toplevel """The application configuration plugin and config object. Example: @@ -30,7 +31,6 @@ def example_handler() -> dict: from __future__ import annotations from collections.abc import Callable, Sequence # noqa: TC003 -from importlib import import_module from typing import TYPE_CHECKING, Any, TypeVar from pydantic import BaseModel @@ -51,7 +51,10 @@ def example_handler() -> dict: sqlalchemy_plugin, ) from starlite_saqlalchemy.constants import IS_LOCAL_ENVIRONMENT, IS_TEST_ENVIRONMENT -from starlite_saqlalchemy.exceptions import HealthCheckConfigurationError, MissingDependencyError +from starlite_saqlalchemy.exceptions import ( + HealthCheckConfigurationError, + MissingDependencyError, +) from starlite_saqlalchemy.health import ( AbstractHealthCheck, AppHealthCheck, @@ -208,9 +211,7 @@ def __call__(self, app_config: AppConfig) -> AppConfig: app_config.before_startup = lifespan.before_startup_handler app_config.on_shutdown.append(http.on_shutdown) if self.config.do_cache: - from starlite_saqlalchemy import ( # pylint: disable=import-outside-toplevel - redis, - ) + from starlite_saqlalchemy import redis app_config.on_shutdown.append(redis.client.close) return app_config @@ -235,10 +236,9 @@ def configure_cache(self, app_config: AppConfig) -> None: app_config: The Starlite application config object. """ if self.config.do_cache and app_config.cache_config == DEFAULT_CACHE_CONFIG: - self._check_module_installed("redis", "cache") - from starlite_saqlalchemy import ( # pylint: disable=import-outside-toplevel - cache, - ) + if not settings.IS_REDIS_INSTALLED: + raise MissingDependencyError(module="redis", config="redis") + from starlite_saqlalchemy import cache app_config.cache_config = cache.config @@ -345,10 +345,9 @@ def configure_sentry(self, app_config: AppConfig) -> None: else not (IS_LOCAL_ENVIRONMENT or IS_TEST_ENVIRONMENT) ) if do_sentry: - self._check_module_installed("sentry_sdk", "sentry") - from starlite_saqlalchemy import ( # pylint: disable=import-outside-toplevel - sentry, - ) + if not settings.IS_SENTRY_SDK_INSTALLED: + raise MissingDependencyError(module="sentry_sdk", config="sentry") + from starlite_saqlalchemy import sentry app_config.on_startup.append(sentry.configure) @@ -383,10 +382,9 @@ def configure_worker(self, app_config: AppConfig) -> None: app_config: The Starlite application config object. """ if self.config.do_worker: - self._check_module_installed("saq", "worker") - from starlite_saqlalchemy.worker import ( # pylint: disable=import-outside-toplevel - create_worker_instance, - ) + if not settings.IS_SAQ_INSTALLED: + raise MissingDependencyError(module="saq", config="worker") + from starlite_saqlalchemy.worker import create_worker_instance worker_kwargs: dict[str, Any] = {"functions": self.config.worker_functions} if self.config.do_logging: @@ -401,12 +399,3 @@ def _ensure_list(item: list[T] | T) -> list[T]: if isinstance(item, list): return item return [] if item is None else [item] - - def _check_module_installed(self, module: str, config: str) -> None: - try: - import_module(module) - except ModuleNotFoundError as error: - raise MissingDependencyError( - f'You enabled {config} configuration but package "{module}" is not installed. ' - f'You may need to run: "poetry install starlite-saqlalchemy[{config}]"' - ) from error diff --git a/src/starlite_saqlalchemy/lifespan.py b/src/starlite_saqlalchemy/lifespan.py index cebb251b..db78d6e5 100644 --- a/src/starlite_saqlalchemy/lifespan.py +++ b/src/starlite_saqlalchemy/lifespan.py @@ -6,9 +6,13 @@ import starlite from sqlalchemy import text -from starlite_saqlalchemy import redis, settings +from starlite_saqlalchemy import settings from starlite_saqlalchemy.db import engine +if settings.IS_REDIS_INSTALLED: + from starlite_saqlalchemy import redis + + logger = logging.getLogger(__name__) @@ -43,5 +47,5 @@ async def before_startup_handler(_: starlite.Starlite) -> None: """Do things before the app starts up.""" if settings.app.CHECK_DB_READY: await _db_ready() - if settings.app.CHECK_REDIS_READY: + if settings.IS_REDIS_INSTALLED and settings.app.CHECK_REDIS_READY: await _redis_ready() diff --git a/src/starlite_saqlalchemy/service.py b/src/starlite_saqlalchemy/service.py index caa4cabb..8896d29f 100644 --- a/src/starlite_saqlalchemy/service.py +++ b/src/starlite_saqlalchemy/service.py @@ -11,13 +11,17 @@ import logging from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar -from saq.job import Job - -from starlite_saqlalchemy import utils +from starlite_saqlalchemy import settings, utils from starlite_saqlalchemy.db import async_session_factory from starlite_saqlalchemy.exceptions import NotFoundError from starlite_saqlalchemy.repository.sqlalchemy import ModelT +if settings.IS_SAQ_INSTALLED: + from saq.job import Job + + from starlite_saqlalchemy.worker import default_job_config_dict, queue + + if TYPE_CHECKING: from collections.abc import AsyncIterator @@ -134,11 +138,6 @@ async def enqueue_background_task( job_config: Configuration object to control the job that is enqueued. **kwargs: Arguments to be passed to the method when called. Must be JSON serializable. """ - from starlite_saqlalchemy.worker import ( # pylint: disable=C0415 - default_job_config_dict, - queue, - ) - module = inspect.getmodule(self) if module is None: # pragma: no cover logger.warning("Callback not enqueued, no module resolved for %s", self) diff --git a/src/starlite_saqlalchemy/settings.py b/src/starlite_saqlalchemy/settings.py index b182a6d8..fe3a35ff 100644 --- a/src/starlite_saqlalchemy/settings.py +++ b/src/starlite_saqlalchemy/settings.py @@ -5,12 +5,30 @@ """ from __future__ import annotations +from importlib import import_module + # pylint: disable=missing-class-docstring from typing import Literal from pydantic import AnyUrl, BaseSettings, PostgresDsn, parse_obj_as from starlite.utils.extractors import RequestExtractorField, ResponseExtractorField +IS_REDIS_INSTALLED = True +IS_SAQ_INSTALLED = True +IS_SENTRY_SDK_INSTALLED = True + +for package in ("redis", "saq", "sentry_sdk"): + try: + import_module(package) + except ModuleNotFoundError: + match package: + case "redis": + IS_REDIS_INSTALLED = False + case "saq": + IS_SAQ_INSTALLED = False + case "sentry_sdk": + IS_SENTRY_SDK_INSTALLED = False + # noinspection PyUnresolvedReferences class AppSettings(BaseSettings): diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 72a7eb4a..7d8d5fd7 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -7,6 +7,7 @@ from starlite.datastructures import State from starlite.enums import ScopeType +from starlite_saqlalchemy import settings, sqlalchemy_plugin from starlite_saqlalchemy.testing import GenericMockRepository from tests.utils.domain.authors import Author from tests.utils.domain.authors import Service as AuthorService diff --git a/tests/unit/test_init_plugin_no_extras.py b/tests/unit/test_init_plugin_no_extras.py index 5d9ee632..614e7d3a 100644 --- a/tests/unit/test_init_plugin_no_extras.py +++ b/tests/unit/test_init_plugin_no_extras.py @@ -3,10 +3,15 @@ import pytest from starlite import Starlite -from starlite_saqlalchemy import init_plugin +from starlite_saqlalchemy import init_plugin, settings from starlite_saqlalchemy.exceptions import MissingDependencyError +SKIP = any( + [settings.IS_SAQ_INSTALLED, settings.IS_SENTRY_SDK_INSTALLED, settings.IS_REDIS_INSTALLED] +) + +@pytest.mark.skipif(SKIP, reason="test will only run if no extras are installed") @pytest.mark.parametrize( ("enabled_config", "error"), [ diff --git a/tox.ini b/tox.ini index 1a6cad0a..adcb73a0 100644 --- a/tox.ini +++ b/tox.ini @@ -12,14 +12,14 @@ deps = -r{toxinidir}/requirements.dev.txt commands = - coverage run -p -m pytest --ignore tests/unit/test_init_plugin_no_extras.py {posargs} + 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,pytest-plugin +depends = py310,py311,noextras,pytest-plugin basepython = python3.11 commands = coverage combine From bc63b103ba6ee19b248c645e14bf2b8d156cf44b Mon Sep 17 00:00:00 2001 From: gazorby Date: Thu, 12 Jan 2023 12:01:24 +0100 Subject: [PATCH 08/30] =?UTF-8?q?=F0=9F=90=9B=20fix:=20linters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/starlite_saqlalchemy/service.py | 3 +-- tests/unit/conftest.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/starlite_saqlalchemy/service.py b/src/starlite_saqlalchemy/service.py index 8896d29f..242f1385 100644 --- a/src/starlite_saqlalchemy/service.py +++ b/src/starlite_saqlalchemy/service.py @@ -17,10 +17,9 @@ from starlite_saqlalchemy.repository.sqlalchemy import ModelT if settings.IS_SAQ_INSTALLED: + from starlite_saqlalchemy.worker import default_job_config_dict, queue # isort:skip from saq.job import Job - from starlite_saqlalchemy.worker import default_job_config_dict, queue - if TYPE_CHECKING: from collections.abc import AsyncIterator diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 7d8d5fd7..8e3966c5 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,3 +1,5 @@ +# pylint: disable=import-outside-toplevel + """Unit test specific config.""" from __future__ import annotations From 8f0008472d502ad643258f0066c023ee523c81d0 Mon Sep 17 00:00:00 2001 From: gazorby Date: Thu, 12 Jan 2023 12:15:19 +0100 Subject: [PATCH 09/30] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20full=20c?= =?UTF-8?q?overage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/starlite_saqlalchemy/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/starlite_saqlalchemy/settings.py b/src/starlite_saqlalchemy/settings.py index fe3a35ff..ca7869cb 100644 --- a/src/starlite_saqlalchemy/settings.py +++ b/src/starlite_saqlalchemy/settings.py @@ -26,7 +26,7 @@ IS_REDIS_INSTALLED = False case "saq": IS_SAQ_INSTALLED = False - case "sentry_sdk": + case "sentry_sdk": # pragma: no cover IS_SENTRY_SDK_INSTALLED = False From f5d276ca50de6af00a140095d4c121949d5540f6 Mon Sep 17 00:00:00 2001 From: gazorby Date: Mon, 16 Jan 2023 18:06:46 +0100 Subject: [PATCH 10/30] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20update?= =?UTF-8?q?=20pytest=20plugin=20fixtures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pytest_starlite_saqlalchemy/plugin.py | 4 +++- tests/unit/conftest.py | 13 ++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/pytest_starlite_saqlalchemy/plugin.py b/src/pytest_starlite_saqlalchemy/plugin.py index 048351c2..ec91ebda 100644 --- a/src/pytest_starlite_saqlalchemy/plugin.py +++ b/src/pytest_starlite_saqlalchemy/plugin.py @@ -12,6 +12,8 @@ from structlog.testing import CapturingLogger from uvicorn.importer import ImportFromStringError, import_from_string +from starlite_saqlalchemy import settings + if TYPE_CHECKING: from collections.abc import Generator @@ -77,7 +79,7 @@ def _patch_sqlalchemy_plugin(is_unit_test: bool, monkeypatch: MonkeyPatch) -> No ) -@pytest.fixture(autouse=True) +@pytest.fixture(autouse=settings.IS_SAQ_INSTALLED) def _patch_worker(is_unit_test: bool, monkeypatch: MonkeyPatch) -> None: """We don't want the worker to start for unittests.""" if is_unit_test: diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 8e3966c5..c0e1c4dd 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,5 +1,3 @@ -# pylint: disable=import-outside-toplevel - """Unit test specific config.""" from __future__ import annotations @@ -9,7 +7,7 @@ from starlite.datastructures import State from starlite.enums import ScopeType -from starlite_saqlalchemy import settings, sqlalchemy_plugin +from starlite_saqlalchemy import settings from starlite_saqlalchemy.testing import GenericMockRepository from tests.utils.domain.authors import Author from tests.utils.domain.authors import Service as AuthorService @@ -18,6 +16,9 @@ from ..utils import controllers +if settings.IS_SAQ_INSTALLED: + from saq.job import Job + if TYPE_CHECKING: from starlite import Starlite @@ -111,6 +112,12 @@ def http_scope(app: Starlite) -> HTTPScope: } +@pytest.fixture(autouse=settings.IS_SAQ_INSTALLED) +def job() -> Job: + """SAQ Job instance.""" + return Job(function="whatever", kwargs={"a": "b"}) + + @pytest.fixture() def state() -> State: """Starlite application state datastructure.""" From 2b5d9c40cabde095cec4502d9adc263bff968cfa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Jan 2023 17:10:00 +0000 Subject: [PATCH 11/30] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/unit/test_init_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_init_plugin.py b/tests/unit/test_init_plugin.py index f65bdee1..97cfd566 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, worker, sentry +from starlite_saqlalchemy import init_plugin, sentry, worker if TYPE_CHECKING: from typing import Any From c18e5095db57a73edcf547168368ed852accfcb2 Mon Sep 17 00:00:00 2001 From: gazorby Date: Tue, 17 Jan 2023 09:10:08 +0100 Subject: [PATCH 12/30] =?UTF-8?q?=E2=9C=85=20test:=20fix=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.dev.txt | 2 +- src/pytest_starlite_saqlalchemy/plugin.py | 4 ++-- src/starlite_saqlalchemy/__init__.py | 2 -- src/starlite_saqlalchemy/constants.py | 24 +++++++++++++++++++++++ src/starlite_saqlalchemy/init_plugin.py | 14 +++++++++---- src/starlite_saqlalchemy/lifespan.py | 6 +++--- src/starlite_saqlalchemy/service.py | 4 ++-- src/starlite_saqlalchemy/settings.py | 18 ----------------- tests/conftest.py | 17 ++++++++++++++++ tests/unit/conftest.py | 6 +++--- tests/unit/test_init_plugin_no_extras.py | 4 ++-- 11 files changed, 64 insertions(+), 37 deletions(-) diff --git a/requirements.dev.txt b/requirements.dev.txt index 863aaa55..3023b326 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -7,7 +7,7 @@ pytest-asyncio == 0.20.3 pytest-dotenv == 0.5.2 pytest_docker == 1.0.1 rich == 13.0.1 -sentry-sdk +sentry-sdk >= "1.13.0" hiredis redis saq >= "0.9.1" diff --git a/src/pytest_starlite_saqlalchemy/plugin.py b/src/pytest_starlite_saqlalchemy/plugin.py index ec91ebda..52f51014 100644 --- a/src/pytest_starlite_saqlalchemy/plugin.py +++ b/src/pytest_starlite_saqlalchemy/plugin.py @@ -12,7 +12,7 @@ from structlog.testing import CapturingLogger from uvicorn.importer import ImportFromStringError, import_from_string -from starlite_saqlalchemy import settings +from starlite_saqlalchemy import constants if TYPE_CHECKING: from collections.abc import Generator @@ -79,7 +79,7 @@ def _patch_sqlalchemy_plugin(is_unit_test: bool, monkeypatch: MonkeyPatch) -> No ) -@pytest.fixture(autouse=settings.IS_SAQ_INSTALLED) +@pytest.fixture(autouse=constants.IS_SAQ_INSTALLED) def _patch_worker(is_unit_test: bool, monkeypatch: MonkeyPatch) -> None: """We don't want the worker to start for unittests.""" if is_unit_test: diff --git a/src/starlite_saqlalchemy/__init__.py b/src/starlite_saqlalchemy/__init__.py index b1a6c6f9..7ce57695 100644 --- a/src/starlite_saqlalchemy/__init__.py +++ b/src/starlite_saqlalchemy/__init__.py @@ -36,7 +36,6 @@ def example_handler() -> dict: settings, sqlalchemy_plugin, type_encoders, - worker, ) from .init_plugin import ConfigureApp, PluginConfig @@ -57,7 +56,6 @@ def example_handler() -> dict: "settings", "sqlalchemy_plugin", "type_encoders", - "worker", ] __version__ = "0.28.1" diff --git a/src/starlite_saqlalchemy/constants.py b/src/starlite_saqlalchemy/constants.py index 8a5e75d3..9d26ecac 100644 --- a/src/starlite_saqlalchemy/constants.py +++ b/src/starlite_saqlalchemy/constants.py @@ -1,6 +1,8 @@ """Application constants.""" from __future__ import annotations +from importlib import import_module + from starlite_saqlalchemy.settings import app from starlite_saqlalchemy.utils import case_insensitive_string_compare @@ -9,3 +11,25 @@ IS_LOCAL_ENVIRONMENT = case_insensitive_string_compare(app.ENVIRONMENT, app.LOCAL_ENVIRONMENT_NAME) """Flag indicating if application is running in local development mode.""" + +IS_REDIS_INSTALLED = True +"""Flag indicating if redis module is installed.""" + +IS_SAQ_INSTALLED = True +"""Flag indicating if saq module is installed.""" + +IS_SENTRY_SDK_INSTALLED = True +"""Flag indicating if sentry_sdk module is installed.""" + + +for package in ("redis", "saq", "sentry_sdk"): + try: + import_module(package) + except ModuleNotFoundError: + match package: + case "redis": + IS_REDIS_INSTALLED = False + case "saq": + IS_SAQ_INSTALLED = False + case "sentry_sdk": # pragma: no cover + IS_SENTRY_SDK_INSTALLED = False diff --git a/src/starlite_saqlalchemy/init_plugin.py b/src/starlite_saqlalchemy/init_plugin.py index d246f537..16c25906 100644 --- a/src/starlite_saqlalchemy/init_plugin.py +++ b/src/starlite_saqlalchemy/init_plugin.py @@ -50,7 +50,13 @@ def example_handler() -> dict: settings, sqlalchemy_plugin, ) -from starlite_saqlalchemy.constants import IS_LOCAL_ENVIRONMENT, IS_TEST_ENVIRONMENT +from starlite_saqlalchemy.constants import ( + IS_LOCAL_ENVIRONMENT, + IS_REDIS_INSTALLED, + IS_SAQ_INSTALLED, + IS_SENTRY_SDK_INSTALLED, + IS_TEST_ENVIRONMENT, +) from starlite_saqlalchemy.exceptions import ( HealthCheckConfigurationError, MissingDependencyError, @@ -236,7 +242,7 @@ def configure_cache(self, app_config: AppConfig) -> None: app_config: The Starlite application config object. """ if self.config.do_cache and app_config.cache_config == DEFAULT_CACHE_CONFIG: - if not settings.IS_REDIS_INSTALLED: + if not IS_REDIS_INSTALLED: raise MissingDependencyError(module="redis", config="redis") from starlite_saqlalchemy import cache @@ -345,7 +351,7 @@ def configure_sentry(self, app_config: AppConfig) -> None: else not (IS_LOCAL_ENVIRONMENT or IS_TEST_ENVIRONMENT) ) if do_sentry: - if not settings.IS_SENTRY_SDK_INSTALLED: + if not IS_SENTRY_SDK_INSTALLED: raise MissingDependencyError(module="sentry_sdk", config="sentry") from starlite_saqlalchemy import sentry @@ -382,7 +388,7 @@ def configure_worker(self, app_config: AppConfig) -> None: app_config: The Starlite application config object. """ if self.config.do_worker: - if not settings.IS_SAQ_INSTALLED: + if not IS_SAQ_INSTALLED: raise MissingDependencyError(module="saq", config="worker") from starlite_saqlalchemy.worker import create_worker_instance diff --git a/src/starlite_saqlalchemy/lifespan.py b/src/starlite_saqlalchemy/lifespan.py index db78d6e5..c396e796 100644 --- a/src/starlite_saqlalchemy/lifespan.py +++ b/src/starlite_saqlalchemy/lifespan.py @@ -6,10 +6,10 @@ import starlite from sqlalchemy import text -from starlite_saqlalchemy import settings +from starlite_saqlalchemy import constants, settings from starlite_saqlalchemy.db import engine -if settings.IS_REDIS_INSTALLED: +if constants.IS_REDIS_INSTALLED: from starlite_saqlalchemy import redis @@ -47,5 +47,5 @@ async def before_startup_handler(_: starlite.Starlite) -> None: """Do things before the app starts up.""" if settings.app.CHECK_DB_READY: await _db_ready() - if settings.IS_REDIS_INSTALLED and settings.app.CHECK_REDIS_READY: + if constants.IS_REDIS_INSTALLED and settings.app.CHECK_REDIS_READY: await _redis_ready() diff --git a/src/starlite_saqlalchemy/service.py b/src/starlite_saqlalchemy/service.py index 242f1385..c2633018 100644 --- a/src/starlite_saqlalchemy/service.py +++ b/src/starlite_saqlalchemy/service.py @@ -11,12 +11,12 @@ import logging from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar -from starlite_saqlalchemy import settings, utils +from starlite_saqlalchemy import constants, utils from starlite_saqlalchemy.db import async_session_factory from starlite_saqlalchemy.exceptions import NotFoundError from starlite_saqlalchemy.repository.sqlalchemy import ModelT -if settings.IS_SAQ_INSTALLED: +if constants.IS_SAQ_INSTALLED: from starlite_saqlalchemy.worker import default_job_config_dict, queue # isort:skip from saq.job import Job diff --git a/src/starlite_saqlalchemy/settings.py b/src/starlite_saqlalchemy/settings.py index ca7869cb..b182a6d8 100644 --- a/src/starlite_saqlalchemy/settings.py +++ b/src/starlite_saqlalchemy/settings.py @@ -5,30 +5,12 @@ """ from __future__ import annotations -from importlib import import_module - # pylint: disable=missing-class-docstring from typing import Literal from pydantic import AnyUrl, BaseSettings, PostgresDsn, parse_obj_as from starlite.utils.extractors import RequestExtractorField, ResponseExtractorField -IS_REDIS_INSTALLED = True -IS_SAQ_INSTALLED = True -IS_SENTRY_SDK_INSTALLED = True - -for package in ("redis", "saq", "sentry_sdk"): - try: - import_module(package) - except ModuleNotFoundError: - match package: - case "redis": - IS_REDIS_INSTALLED = False - case "saq": - IS_SAQ_INSTALLED = False - case "sentry_sdk": # pragma: no cover - IS_SENTRY_SDK_INSTALLED = False - # noinspection PyUnresolvedReferences class AppSettings(BaseSettings): diff --git a/tests/conftest.py b/tests/conftest.py index 761a66f5..93c0b445 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,12 +3,14 @@ import importlib import sys +from importlib import reload from typing import TYPE_CHECKING, TypeVar from uuid import uuid4 import pytest from asyncpg.pgproto import pgproto +from starlite_saqlalchemy import constants, init_plugin, settings from tests.utils.domain import authors, books if TYPE_CHECKING: @@ -20,6 +22,21 @@ from pytest import MonkeyPatch +@pytest.fixture(scope="session", autouse=True) +def _reload_env() -> None: + """Reload needed modules for test env vars to be picked up. + + This is needed because the pytest_starlite_saqlalchemy pytest is + loaded before other plugins, and so before pytest-dotenv which + inject environment variables for the test session. Reloading + settings and dependent modules will enforce pulling values from env + again. + """ + reload(settings) + reload(constants) + reload(init_plugin) + + @pytest.fixture(name="raw_authors") def fx_raw_authors() -> list[dict[str, Any]]: """Unstructured author representations.""" diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index c0e1c4dd..fc8d4426 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -7,7 +7,7 @@ from starlite.datastructures import State from starlite.enums import ScopeType -from starlite_saqlalchemy import settings +from starlite_saqlalchemy import constants from starlite_saqlalchemy.testing import GenericMockRepository from tests.utils.domain.authors import Author from tests.utils.domain.authors import Service as AuthorService @@ -16,7 +16,7 @@ from ..utils import controllers -if settings.IS_SAQ_INSTALLED: +if constants.IS_SAQ_INSTALLED: from saq.job import Job if TYPE_CHECKING: @@ -112,7 +112,7 @@ def http_scope(app: Starlite) -> HTTPScope: } -@pytest.fixture(autouse=settings.IS_SAQ_INSTALLED) +@pytest.fixture(autouse=constants.IS_SAQ_INSTALLED) def job() -> Job: """SAQ Job instance.""" return Job(function="whatever", kwargs={"a": "b"}) diff --git a/tests/unit/test_init_plugin_no_extras.py b/tests/unit/test_init_plugin_no_extras.py index 614e7d3a..20d655ed 100644 --- a/tests/unit/test_init_plugin_no_extras.py +++ b/tests/unit/test_init_plugin_no_extras.py @@ -3,11 +3,11 @@ import pytest from starlite import Starlite -from starlite_saqlalchemy import init_plugin, settings +from starlite_saqlalchemy import constants, init_plugin from starlite_saqlalchemy.exceptions import MissingDependencyError SKIP = any( - [settings.IS_SAQ_INSTALLED, settings.IS_SENTRY_SDK_INSTALLED, settings.IS_REDIS_INSTALLED] + [constants.IS_SAQ_INSTALLED, constants.IS_SENTRY_SDK_INSTALLED, constants.IS_REDIS_INSTALLED] ) From 83df0ba94542522f87d08a803b49fd8a8f211c3b Mon Sep 17 00:00:00 2001 From: gazorby Date: Tue, 17 Jan 2023 16:56:49 +0100 Subject: [PATCH 13/30] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20add=20sq?= =?UTF-8?q?lalchemy=20to=20optional=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev-noextras.requirements.txt | 2 +- poetry.lock | 55 +------- pyproject.toml | 23 +++- requirements.dev.txt | 1 + src/pytest_starlite_saqlalchemy/plugin.py | 6 +- src/starlite_saqlalchemy/__init__.py | 27 +++- src/starlite_saqlalchemy/constants.py | 9 +- src/starlite_saqlalchemy/exceptions.py | 3 +- src/starlite_saqlalchemy/init_plugin.py | 24 ++-- src/starlite_saqlalchemy/lifespan.py | 9 +- .../repository/__init__.py | 16 +-- src/starlite_saqlalchemy/service/__init__.py | 10 ++ .../{service.py => service/generic.py} | 105 --------------- .../service/sqlalchemy.py | 122 ++++++++++++++++++ src/starlite_saqlalchemy/settings.py | 2 +- tests.env | 2 + tests/conftest.py | 24 +--- tests/pytest_plugin/conftest.py | 11 ++ tests/unit/conftest.py | 28 ++-- tests/unit/test_db.py | 7 +- tests/unit/test_dto.py | 8 +- tests/unit/test_init_plugin_no_extras.py | 9 +- tests/unit/test_orm.py | 5 + tests/unit/test_service.py | 6 +- 24 files changed, 289 insertions(+), 225 deletions(-) create mode 100644 src/starlite_saqlalchemy/service/__init__.py rename src/starlite_saqlalchemy/{service.py => service/generic.py} (61%) create mode 100644 src/starlite_saqlalchemy/service/sqlalchemy.py diff --git a/dev-noextras.requirements.txt b/dev-noextras.requirements.txt index cca7eb20..725fa123 100644 --- a/dev-noextras.requirements.txt +++ b/dev-noextras.requirements.txt @@ -6,4 +6,4 @@ pytest == 7.2.0 pytest-asyncio == 0.20.3 pytest-dotenv == 0.5.2 pytest_docker == 1.0.1 -rich == 13.0.0 +rich == 13.0.1 diff --git a/poetry.lock b/poetry.lock index d073722c..773cd726 100644 --- a/poetry.lock +++ b/poetry.lock @@ -216,15 +216,12 @@ files = [ {file = "fast_query_parsers-0.3.0.tar.gz", hash = "sha256:df972c0b58d0bf51fa43b67d2604ab795984015d47552d02175ebcc685e4852b"}, ] -[package.dependencies] -maturin = "*" - [[package]] name = "greenlet" version = "2.0.1" description = "Lightweight in-process concurrent programming" category = "main" -optional = false +optional = true python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" files = [ {file = "greenlet-2.0.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:9ed358312e63bf683b9ef22c8e442ef6c5c02973f0c2a939ec1d7b50c974015c"}, @@ -550,36 +547,6 @@ 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" @@ -1008,7 +975,7 @@ name = "sqlalchemy" version = "2.0.0rc2" description = "Database Abstraction Library" category = "main" -optional = false +optional = true python-versions = ">=3.7" files = [ {file = "SQLAlchemy-2.0.0rc2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:19486279fe24297bf0743c1563735e7cab1f439f36acf165bd8e1be699fb3fcb"}, @@ -1153,18 +1120,6 @@ files = [ [package.extras] doc = ["reno", "sphinx", "tornado (>=4.5)"] -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - [[package]] name = "typing-extensions" version = "4.4.0" @@ -1259,11 +1214,13 @@ docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxc test = ["Cython (>=0.29.32,<0.30.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=22.0.0,<22.1.0)", "pycodestyle (>=2.7.0,<2.8.0)"] [extras] +all = ["redis", "hiredis", "saq", "sentry-sdk", "sqlalchemy"] cache = ["redis", "hiredis"] sentry = ["sentry-sdk"] -worker = ["saq"] +sqlalchemy = ["sqlalchemy"] +worker = ["saq", "hiredis"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "02b7b504018c2c2e6633f8028b22dc996e837ba7f1b2a6ae6b879382b9c9d2d0" +content-hash = "ba44949147c159b66058c6bce3c3ad8753dec183e7ac82a7f9cd8fa55b9e4fd3" diff --git a/pyproject.toml b/pyproject.toml index 2b655944..c466f399 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,11 @@ ignore-words-list = "alog" [tool.coverage.run] branch = true -omit = ["*/starlite_saqlalchemy/scripts.py", "*/starlite_saqlalchemy/lifespan.py", "tests/*"] +omit = [ + "*/starlite_saqlalchemy/scripts.py", + "*/starlite_saqlalchemy/lifespan.py", + "tests/*", +] relative_files = true source_pkgs = ["starlite_saqlalchemy", "pytest_starlite_saqlalchemy"] @@ -62,7 +66,7 @@ classifiers = [ ] packages = [ { include = "starlite_saqlalchemy", from = "src" }, - { include = "pytest_starlite_saqlalchemy", from = "src" } + { include = "pytest_starlite_saqlalchemy", from = "src" }, ] [tool.poetry.dependencies] @@ -72,7 +76,6 @@ httpx = "*" msgspec = "*" pydantic = "*" python-dotenv = "*" -sqlalchemy = "==2.0.0rc2" starlite = "^1.40.1" tenacity = "*" uvicorn = "*" @@ -84,12 +87,14 @@ hiredis = { version = "*", optional = true } redis = { version = "*", optional = true } saq = { version = "^0.9.1", optional = true } sentry-sdk = { version = "*", optional = true } +sqlalchemy = { version = "==2.0.0rc2", optional = true } [tool.poetry.extras] cache = ["redis", "hiredis"] worker = ["saq", "hiredis"] sentry = ["sentry-sdk"] -all = ["redis", "hiredis", "saq", "sentry-sdk"] +sqlalchemy = ["sqlalchemy"] +all = ["redis", "hiredis", "saq", "sentry-sdk", "sqlalchemy"] [tool.poetry.plugins."pytest11"] pytest_starlite_saqlalchemy = "pytest_starlite_saqlalchemy" @@ -110,11 +115,17 @@ add-select = "D401,D404,D417" convention = "google" [tool.pytest.ini_options] -addopts = ["-ra", "--strict-config"] +addopts = [ + "-ra", + "--strict-config", + "-p", + "no:pytest_starlite_saqlalchemy", + "-p", + "no:pytest_dotenv", +] asyncio_mode = "auto" env_files = ["tests.env"] testpaths = ["tests/unit"] -test_app = "tests.utils.app:create_app" [tool.pylint.main] disable = [ diff --git a/requirements.dev.txt b/requirements.dev.txt index 3023b326..fe76a26b 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -11,3 +11,4 @@ sentry-sdk >= "1.13.0" hiredis redis saq >= "0.9.1" +sqlalchemy == 2.0.0rc2 diff --git a/src/pytest_starlite_saqlalchemy/plugin.py b/src/pytest_starlite_saqlalchemy/plugin.py index 52f51014..91e7f205 100644 --- a/src/pytest_starlite_saqlalchemy/plugin.py +++ b/src/pytest_starlite_saqlalchemy/plugin.py @@ -2,6 +2,7 @@ # pylint: disable=import-outside-toplevel from __future__ import annotations +import os import re from typing import TYPE_CHECKING from unittest.mock import MagicMock @@ -37,7 +38,7 @@ def pytest_addoption(parser: Parser) -> None: "test_app", "Path to application instance, or callable that returns an application instance.", type="string", - default="app.main:create_app", + default=os.environ.get("TEST_APP", "app.main:create_app"), ) parser.addini( "unit_test_pattern", @@ -67,7 +68,7 @@ def _patch_http_close(monkeypatch: MonkeyPatch) -> None: monkeypatch.setattr(starlite_saqlalchemy.http, "clients", set()) -@pytest.fixture(autouse=True) +@pytest.fixture(autouse=constants.IS_SQLALCHEMY_INSTALLED) def _patch_sqlalchemy_plugin(is_unit_test: bool, monkeypatch: MonkeyPatch) -> None: if is_unit_test: from starlite_saqlalchemy import sqlalchemy_plugin @@ -96,7 +97,6 @@ def fx_app(pytestconfig: Config, monkeypatch: MonkeyPatch) -> Starlite: An application instance, configured via plugin. """ test_app_str = pytestconfig.getini("test_app") - try: app_or_callable = import_from_string(test_app_str) except (ImportFromStringError, ModuleNotFoundError): diff --git a/src/starlite_saqlalchemy/__init__.py b/src/starlite_saqlalchemy/__init__.py index 7ce57695..edc31c9a 100644 --- a/src/starlite_saqlalchemy/__init__.py +++ b/src/starlite_saqlalchemy/__init__.py @@ -23,9 +23,7 @@ def example_handler() -> dict: # noinspection PyCompatibility from . import ( compression, - db, dependencies, - dto, exceptions, health, http, @@ -34,14 +32,33 @@ def example_handler() -> dict: repository, service, settings, - sqlalchemy_plugin, type_encoders, ) +from .constants import ( + IS_REDIS_INSTALLED, + IS_SAQ_INSTALLED, + IS_SENTRY_SDK_INSTALLED, + IS_SQLALCHEMY_INSTALLED, +) from .init_plugin import ConfigureApp, PluginConfig +if IS_SENTRY_SDK_INSTALLED: + from . import sentry + +if IS_SAQ_INSTALLED: + from . import worker + +if IS_REDIS_INSTALLED: + from . import cache, redis + +if IS_SQLALCHEMY_INSTALLED: + from . import db, dto, sqlalchemy_plugin + + __all__ = [ "ConfigureApp", "PluginConfig", + "cache", "compression", "db", "dependencies", @@ -51,11 +68,15 @@ def example_handler() -> dict: "http", "log", "openapi", + "redis", "repository", + "sentry", "service", "settings", "sqlalchemy_plugin", "type_encoders", + "worker", ] + __version__ = "0.28.1" diff --git a/src/starlite_saqlalchemy/constants.py b/src/starlite_saqlalchemy/constants.py index 9d26ecac..556603cc 100644 --- a/src/starlite_saqlalchemy/constants.py +++ b/src/starlite_saqlalchemy/constants.py @@ -21,8 +21,11 @@ IS_SENTRY_SDK_INSTALLED = True """Flag indicating if sentry_sdk module is installed.""" +IS_SQLALCHEMY_INSTALLED = True +"""Flag indicating if sqlalchemy module is installed.""" -for package in ("redis", "saq", "sentry_sdk"): + +for package in ("redis", "saq", "sentry_sdk", "sqlalchemy"): try: import_module(package) except ModuleNotFoundError: @@ -31,5 +34,7 @@ IS_REDIS_INSTALLED = False case "saq": IS_SAQ_INSTALLED = False - case "sentry_sdk": # pragma: no cover + case "sentry_sdk": IS_SENTRY_SDK_INSTALLED = False + case "sqlalchemy": # pragma: no cover + IS_SQLALCHEMY_INSTALLED = False diff --git a/src/starlite_saqlalchemy/exceptions.py b/src/starlite_saqlalchemy/exceptions.py index 9236a9fc..f784b8af 100644 --- a/src/starlite_saqlalchemy/exceptions.py +++ b/src/starlite_saqlalchemy/exceptions.py @@ -59,7 +59,8 @@ class AuthorizationError(StarliteSaqlalchemyClientError): class MissingDependencyError(StarliteSaqlalchemyError): """A required dependency is not installed.""" - def __init__(self, module: str, config: str) -> None: + def __init__(self, module: str, config: str | None = None) -> None: + config = config if config else module super().__init__( f'You enabled {config} configuration but package "{module}" is not installed. ' f'You may need to run: "poetry install starlite-saqlalchemy[{config}]"' diff --git a/src/starlite_saqlalchemy/init_plugin.py b/src/starlite_saqlalchemy/init_plugin.py index 16c25906..37e2d65b 100644 --- a/src/starlite_saqlalchemy/init_plugin.py +++ b/src/starlite_saqlalchemy/init_plugin.py @@ -35,7 +35,6 @@ def example_handler() -> dict: from pydantic import BaseModel from starlite.app import DEFAULT_CACHE_CONFIG, DEFAULT_OPENAPI_CONFIG -from starlite.plugins.sql_alchemy import SQLAlchemyPlugin from starlite.types import TypeEncodersMap # noqa: TC002 from structlog.types import Processor # noqa: TC002 @@ -48,13 +47,13 @@ def example_handler() -> dict: log, openapi, settings, - sqlalchemy_plugin, ) from starlite_saqlalchemy.constants import ( IS_LOCAL_ENVIRONMENT, IS_REDIS_INSTALLED, IS_SAQ_INSTALLED, IS_SENTRY_SDK_INSTALLED, + IS_SQLALCHEMY_INSTALLED, IS_TEST_ENVIRONMENT, ) from starlite_saqlalchemy.exceptions import ( @@ -67,7 +66,6 @@ def example_handler() -> dict: HealthController, ) from starlite_saqlalchemy.service import make_service_callback -from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyHealthCheck from starlite_saqlalchemy.type_encoders import type_encoders_map if TYPE_CHECKING: @@ -174,7 +172,7 @@ class PluginConfig(BaseModel): """Chain of structlog log processors.""" type_encoders: TypeEncodersMap = type_encoders_map """Map of type to serializer callable.""" - health_checks: Sequence[type[AbstractHealthCheck]] = [AppHealthCheck, SQLAlchemyHealthCheck] + health_checks: list[type[AbstractHealthCheck]] = [AppHealthCheck] class ConfigureApp: @@ -182,12 +180,12 @@ class ConfigureApp: __slots__ = ("config",) - def __init__(self, config: PluginConfig = PluginConfig()) -> None: + def __init__(self, config: PluginConfig | None = None) -> None: """ Args: config: Plugin configuration object. """ - self.config = config + self.config = config if config is not None else PluginConfig() def __call__(self, app_config: AppConfig) -> AppConfig: """Entrypoint to the app config plugin. @@ -206,13 +204,13 @@ def __call__(self, app_config: AppConfig) -> AppConfig: self.configure_compression(app_config) self.configure_debug(app_config) self.configure_exception_handlers(app_config) - self.configure_health_check(app_config) self.configure_logging(app_config) self.configure_openapi(app_config) self.configure_sentry(app_config) self.configure_sqlalchemy_plugin(app_config) self.configure_type_encoders(app_config) self.configure_worker(app_config) + self.configure_health_check(app_config) app_config.before_startup = lifespan.before_startup_handler app_config.on_shutdown.append(http.on_shutdown) @@ -367,7 +365,17 @@ def configure_sqlalchemy_plugin(self, app_config: AppConfig) -> None: app_config: The Starlite application config object. """ if self.config.do_sqlalchemy_plugin: - app_config.plugins.append(SQLAlchemyPlugin(config=sqlalchemy_plugin.config)) + if not IS_SQLALCHEMY_INSTALLED: + raise MissingDependencyError(module="sqlalchemy") + from starlite.plugins.sql_alchemy import SQLAlchemyPlugin + + from starlite_saqlalchemy.sqlalchemy_plugin import ( + SQLAlchemyHealthCheck, + config, + ) + + self.config.health_checks.append(SQLAlchemyHealthCheck) + app_config.plugins.append(SQLAlchemyPlugin(config)) def configure_type_encoders(self, app_config: AppConfig) -> None: """Set mapping of type encoders on the application config. diff --git a/src/starlite_saqlalchemy/lifespan.py b/src/starlite_saqlalchemy/lifespan.py index c396e796..cd9cffcd 100644 --- a/src/starlite_saqlalchemy/lifespan.py +++ b/src/starlite_saqlalchemy/lifespan.py @@ -4,14 +4,17 @@ import logging import starlite -from sqlalchemy import text from starlite_saqlalchemy import constants, settings -from starlite_saqlalchemy.db import engine if constants.IS_REDIS_INSTALLED: from starlite_saqlalchemy import redis +if constants.IS_SQLALCHEMY_INSTALLED: + from sqlalchemy import text + + from starlite_saqlalchemy.db import engine # pylint: disable=ungrouped-imports + logger = logging.getLogger(__name__) @@ -45,7 +48,7 @@ async def _redis_ready() -> None: async def before_startup_handler(_: starlite.Starlite) -> None: """Do things before the app starts up.""" - if settings.app.CHECK_DB_READY: + if constants.IS_SQLALCHEMY_INSTALLED and settings.app.CHECK_DB_READY: await _db_ready() if constants.IS_REDIS_INSTALLED and settings.app.CHECK_REDIS_READY: await _redis_ready() diff --git a/src/starlite_saqlalchemy/repository/__init__.py b/src/starlite_saqlalchemy/repository/__init__.py index 35c2fc5d..354076f5 100644 --- a/src/starlite_saqlalchemy/repository/__init__.py +++ b/src/starlite_saqlalchemy/repository/__init__.py @@ -1,11 +1,11 @@ """Abstraction over the data storage for the application.""" from __future__ import annotations -from . import abc, filters, sqlalchemy, types - -__all__ = [ - "abc", - "filters", - "sqlalchemy", - "types", -] +from starlite_saqlalchemy import constants + +from . import abc, filters, types + +if constants.IS_SQLALCHEMY_INSTALLED: + from . import sqlalchemy + +__all__ = ["abc", "filters", "types", "sqlalchemy"] diff --git a/src/starlite_saqlalchemy/service/__init__.py b/src/starlite_saqlalchemy/service/__init__.py new file mode 100644 index 00000000..99fcfcf7 --- /dev/null +++ b/src/starlite_saqlalchemy/service/__init__.py @@ -0,0 +1,10 @@ +"""Implementations for service object.""" + +from starlite_saqlalchemy import constants + +from .generic import Service, make_service_callback + +if constants.IS_SQLALCHEMY_INSTALLED: + from .sqlalchemy import RepositoryService + +__all__ = ["Service", "make_service_callback", "RepositoryService"] diff --git a/src/starlite_saqlalchemy/service.py b/src/starlite_saqlalchemy/service/generic.py similarity index 61% rename from src/starlite_saqlalchemy/service.py rename to src/starlite_saqlalchemy/service/generic.py index c2633018..3d408d38 100644 --- a/src/starlite_saqlalchemy/service.py +++ b/src/starlite_saqlalchemy/service/generic.py @@ -1,8 +1,6 @@ """A generic service object implementation. Service object is generic on the domain model type. - -RepositoryService object is generic on the domain model type which should be a SQLAlchemy model. """ from __future__ import annotations @@ -12,9 +10,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar from starlite_saqlalchemy import constants, utils -from starlite_saqlalchemy.db import async_session_factory from starlite_saqlalchemy.exceptions import NotFoundError -from starlite_saqlalchemy.repository.sqlalchemy import ModelT if constants.IS_SAQ_INSTALLED: from starlite_saqlalchemy.worker import default_job_config_dict, queue # isort:skip @@ -26,8 +22,6 @@ from saq.types import Context - from starlite_saqlalchemy.repository.abc import AbstractRepository - from starlite_saqlalchemy.repository.types import FilterTypes from starlite_saqlalchemy.worker import JobConfig @@ -35,7 +29,6 @@ T = TypeVar("T") ServiceT = TypeVar("ServiceT", bound="Service") -RepoServiceT = TypeVar("RepoServiceT", bound="RepositoryService") service_object_identity_map: dict[str, type[Service]] = {} @@ -167,104 +160,6 @@ async def new(cls: type[ServiceT]) -> AsyncIterator[ServiceT]: yield cls() -class RepositoryService(Service[ModelT], Generic[ModelT]): - """Service object that operates on a repository object.""" - - repository_type: type[AbstractRepository[ModelT]] - - def __init__(self, **repo_kwargs: Any) -> None: - """Configure the service object. - - Args: - **repo_kwargs: passed as keyword args to repo instantiation. - """ - self.repository = self.repository_type(**repo_kwargs) - - async def create(self, data: ModelT) -> ModelT: - """Wrap repository instance creation. - - Args: - data: Representation to be created. - - Returns: - Representation of created instance. - """ - return await self.repository.add(data) - - async def list(self, *filters: "FilterTypes", **kwargs: Any) -> list[ModelT]: - """Wrap repository scalars operation. - - Args: - *filters: Collection route filters. - **kwargs: Keyword arguments for attribute based filtering. - - Returns: - The list of instances retrieved from the repository. - """ - return await self.repository.list(*filters, **kwargs) - - async def update(self, id_: Any, data: ModelT) -> ModelT: - """Wrap repository update operation. - - Args: - id_: Identifier of item to be updated. - data: Representation to be updated. - - Returns: - Updated representation. - """ - self.repository.set_id_attribute_value(id_, data) - return await self.repository.update(data) - - async def upsert(self, id_: Any, data: ModelT) -> ModelT: - """Wrap repository upsert operation. - - Args: - id_: Identifier of the object for upsert. - data: Representation for upsert. - - Returns: - Updated or created representation. - """ - self.repository.set_id_attribute_value(id_, data) - return await self.repository.upsert(data) - - async def get(self, id_: Any) -> ModelT: - """Wrap repository scalar operation. - - Args: - id_: Identifier of instance to be retrieved. - - Returns: - Representation of instance with identifier `id_`. - """ - return await self.repository.get(id_) - - async def delete(self, id_: Any) -> ModelT: - """Wrap repository delete operation. - - Args: - id_: Identifier of instance to be deleted. - - Returns: - Representation of the deleted instance. - """ - return await self.repository.delete(id_) - - @classmethod - @contextlib.asynccontextmanager - async def new(cls: type[RepoServiceT]) -> AsyncIterator[RepoServiceT]: - """Context manager that returns instance of service object. - - Handles construction of the database session. - - Returns: - The service object instance. - """ - async with async_session_factory() as session: - yield cls(session=session) - - async def make_service_callback( _ctx: Context, *, service_type_id: str, service_method_name: str, **kwargs: Any ) -> None: diff --git a/src/starlite_saqlalchemy/service/sqlalchemy.py b/src/starlite_saqlalchemy/service/sqlalchemy.py new file mode 100644 index 00000000..1b62723b --- /dev/null +++ b/src/starlite_saqlalchemy/service/sqlalchemy.py @@ -0,0 +1,122 @@ +"""Service object implementation for SQLAlchemy. + +RepositoryService object is generic on the domain model type which +should be a SQLAlchemy model. +""" + +from __future__ import annotations + +import contextlib +from typing import TYPE_CHECKING, Any, Generic, TypeVar + +from starlite_saqlalchemy.db import async_session_factory +from starlite_saqlalchemy.repository.sqlalchemy import ModelT + +from .generic import Service + +if TYPE_CHECKING: + from collections.abc import AsyncIterator + + from starlite_saqlalchemy.repository.abc import AbstractRepository + from starlite_saqlalchemy.repository.types import FilterTypes + + +RepoServiceT = TypeVar("RepoServiceT", bound="RepositoryService") + + +class RepositoryService(Service[ModelT], Generic[ModelT]): + """Service object that operates on a repository object.""" + + repository_type: type[AbstractRepository[ModelT]] + + def __init__(self, **repo_kwargs: Any) -> None: + """Configure the service object. + + Args: + **repo_kwargs: passed as keyword args to repo instantiation. + """ + self.repository = self.repository_type(**repo_kwargs) + + async def create(self, data: ModelT) -> ModelT: + """Wrap repository instance creation. + + Args: + data: Representation to be created. + + Returns: + Representation of created instance. + """ + return await self.repository.add(data) + + async def list(self, *filters: "FilterTypes", **kwargs: Any) -> list[ModelT]: + """Wrap repository scalars operation. + + Args: + *filters: Collection route filters. + **kwargs: Keyword arguments for attribute based filtering. + + Returns: + The list of instances retrieved from the repository. + """ + return await self.repository.list(*filters, **kwargs) + + async def update(self, id_: Any, data: ModelT) -> ModelT: + """Wrap repository update operation. + + Args: + id_: Identifier of item to be updated. + data: Representation to be updated. + + Returns: + Updated representation. + """ + self.repository.set_id_attribute_value(id_, data) + return await self.repository.update(data) + + async def upsert(self, id_: Any, data: ModelT) -> ModelT: + """Wrap repository upsert operation. + + Args: + id_: Identifier of the object for upsert. + data: Representation for upsert. + + Returns: + Updated or created representation. + """ + self.repository.set_id_attribute_value(id_, data) + return await self.repository.upsert(data) + + async def get(self, id_: Any) -> ModelT: + """Wrap repository scalar operation. + + Args: + id_: Identifier of instance to be retrieved. + + Returns: + Representation of instance with identifier `id_`. + """ + return await self.repository.get(id_) + + async def delete(self, id_: Any) -> ModelT: + """Wrap repository delete operation. + + Args: + id_: Identifier of instance to be deleted. + + Returns: + Representation of the deleted instance. + """ + return await self.repository.delete(id_) + + @classmethod + @contextlib.asynccontextmanager + async def new(cls: type[RepoServiceT]) -> AsyncIterator[RepoServiceT]: + """Context manager that returns instance of service object. + + Handles construction of the database session. + + Returns: + The service object instance. + """ + async with async_session_factory() as session: + yield cls(session=session) diff --git a/src/starlite_saqlalchemy/settings.py b/src/starlite_saqlalchemy/settings.py index b182a6d8..c9ababe9 100644 --- a/src/starlite_saqlalchemy/settings.py +++ b/src/starlite_saqlalchemy/settings.py @@ -38,7 +38,7 @@ class Config: TEST_ENVIRONMENT_NAME: str = "test" """Value of ENVIRONMENT used to determine if running tests. - This should be the value of `ENVIRONMENT` in `test.env`. + This should be the value of `ENVIRONMENT` in `tests.env`. """ LOCAL_ENVIRONMENT_NAME: str = "local" """Value of ENVIRONMENT used to determine if running in local development diff --git a/tests.env b/tests.env index e5a31403..19adcf9e 100644 --- a/tests.env +++ b/tests.env @@ -1,3 +1,5 @@ # App ENVIRONMENT=test NAME=my-starlite-app + +TEST_APP=tests.utils.app:create_app diff --git a/tests/conftest.py b/tests/conftest.py index 93c0b445..05a2960c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,18 +1,15 @@ """Config that can be shared between all test types.""" +# pylint: disable=import-outside-toplevel from __future__ import annotations import importlib import sys -from importlib import reload from typing import TYPE_CHECKING, TypeVar from uuid import uuid4 import pytest from asyncpg.pgproto import pgproto -from starlite_saqlalchemy import constants, init_plugin, settings -from tests.utils.domain import authors, books - if TYPE_CHECKING: from collections.abc import Callable from pathlib import Path @@ -21,20 +18,9 @@ from pytest import MonkeyPatch + from tests.utils.domain import authors, books -@pytest.fixture(scope="session", autouse=True) -def _reload_env() -> None: - """Reload needed modules for test env vars to be picked up. - - This is needed because the pytest_starlite_saqlalchemy pytest is - loaded before other plugins, and so before pytest-dotenv which - inject environment variables for the test session. Reloading - settings and dependent modules will enforce pulling values from env - again. - """ - reload(settings) - reload(constants) - reload(init_plugin) +pytest_plugins = ("pytest_dotenv", "pytest_starlite_saqlalchemy.plugin") @pytest.fixture(name="raw_authors") @@ -62,6 +48,8 @@ def fx_raw_authors() -> list[dict[str, Any]]: @pytest.fixture(name="authors") def fx_authors(raw_authors: list[dict[str, Any]]) -> list[authors.Author]: """Collection of parsed Author models.""" + from tests.utils.domain import authors + mapped_authors = [authors.ReadDTO(**raw).to_mapped() for raw in raw_authors] # convert these to pgproto UUIDs as that is what we get back from sqlalchemy for author in mapped_authors: @@ -87,6 +75,8 @@ def fx_raw_books(raw_authors: list[dict[str, Any]]) -> list[dict[str, Any]]: @pytest.fixture(name="books") def fx_books(raw_books: list[dict[str, Any]]) -> list[books.Book]: """Collection of parsed Book models.""" + from tests.utils.domain import books + mapped_books = [books.ReadDTO(**raw).to_mapped() for raw in raw_books] # convert these to pgproto UUIDs as that is what we get back from sqlalchemy for book in mapped_books: diff --git a/tests/pytest_plugin/conftest.py b/tests/pytest_plugin/conftest.py index 0172cf77..08003bdf 100644 --- a/tests/pytest_plugin/conftest.py +++ b/tests/pytest_plugin/conftest.py @@ -1,4 +1,15 @@ """Enable the `pytester` fixture for the plugin tests.""" from __future__ import annotations +from importlib import reload + +import pytest + +from starlite_saqlalchemy.db import orm + pytest_plugins = ["pytester"] + + +@pytest.fixture(autouse=True) +def _reload_orm() -> None: + reload(orm) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index fc8d4426..a88ed733 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,4 +1,5 @@ """Unit test specific config.""" +# pylint: disable=import-outside-toplevel from __future__ import annotations from typing import TYPE_CHECKING @@ -8,13 +9,6 @@ from starlite.enums import ScopeType from starlite_saqlalchemy import constants -from starlite_saqlalchemy.testing import GenericMockRepository -from tests.utils.domain.authors import Author -from tests.utils.domain.authors import Service as AuthorService -from tests.utils.domain.books import Book -from tests.utils.domain.books import Service as BookService - -from ..utils import controllers if constants.IS_SAQ_INSTALLED: from saq.job import Job @@ -24,12 +18,19 @@ from starlite import Starlite from starlite.types import HTTPResponseBodyEvent, HTTPResponseStartEvent, HTTPScope + from starlite_saqlalchemy.testing import GenericMockRepository + from tests.utils.domain.authors import Author + from tests.utils.domain.books import Book + -@pytest.fixture(name="author_repository_type") +@pytest.fixture(name="author_repository_type", autouse=constants.IS_SQLALCHEMY_INSTALLED) def fx_author_repository_type( authors: list[Author], monkeypatch: pytest.MonkeyPatch ) -> type[GenericMockRepository[Author]]: """Mock Author repository, pre-seeded with collection data.""" + from starlite_saqlalchemy.testing import GenericMockRepository + from tests.utils.domain.authors import Author + from tests.utils.domain.authors import Service as AuthorService repo = GenericMockRepository[Author] repo.seed_collection(authors) @@ -37,7 +38,7 @@ def fx_author_repository_type( return repo -@pytest.fixture(name="author_repository") +@pytest.fixture(name="author_repository", autouse=constants.IS_SQLALCHEMY_INSTALLED) def fx_author_repository( author_repository_type: type[GenericMockRepository[Author]], ) -> GenericMockRepository[Author]: @@ -45,11 +46,14 @@ def fx_author_repository( return author_repository_type() -@pytest.fixture(name="book_repository_type") +@pytest.fixture(name="book_repository_type", autouse=constants.IS_SQLALCHEMY_INSTALLED) def fx_book_repository_type( books: list[Book], monkeypatch: pytest.MonkeyPatch ) -> type[GenericMockRepository[Book]]: """Mock Book repository, pre-seeded with collection data.""" + from starlite_saqlalchemy.testing import GenericMockRepository + from tests.utils.domain.books import Book + from tests.utils.domain.books import Service as BookService class BookRepository(GenericMockRepository[Book]): """Mock book repo.""" @@ -61,7 +65,7 @@ class BookRepository(GenericMockRepository[Book]): return BookRepository -@pytest.fixture(name="book_repository") +@pytest.fixture(name="book_repository", autouse=constants.IS_SQLALCHEMY_INSTALLED) def fx_book_repository( book_repository_type: type[GenericMockRepository[Book]], ) -> GenericMockRepository[Book]: @@ -88,6 +92,8 @@ def http_response_body() -> HTTPResponseBodyEvent: @pytest.fixture() def http_scope(app: Starlite) -> HTTPScope: """Minimal ASGI HTTP connection scope.""" + from ..utils import controllers + return { "headers": [], "app": app, diff --git a/tests/unit/test_db.py b/tests/unit/test_db.py index 312d73f3..a5eabcc3 100644 --- a/tests/unit/test_db.py +++ b/tests/unit/test_db.py @@ -1,11 +1,14 @@ """Tests for db module.""" # pylint: disable=protected-access +# pylint: disable=wrong-import-position,wrong-import-order from __future__ import annotations -from uuid import uuid4 - import pytest +pytest.importorskip("sqlalchemy") + +from uuid import uuid4 + from starlite_saqlalchemy import db diff --git a/tests/unit/test_dto.py b/tests/unit/test_dto.py index c34b52b7..7dabd478 100644 --- a/tests/unit/test_dto.py +++ b/tests/unit/test_dto.py @@ -1,10 +1,14 @@ """Tests for the dto factory.""" -# pylint: disable=missing-class-docstring,invalid-name +# pylint: disable=missing-class-docstring,invalid-name,wrong-import-position,wrong-import-order + +import pytest + +pytest.importorskip("sqlalchemy") + from datetime import date, datetime, timedelta from typing import TYPE_CHECKING, Annotated, Any, ClassVar from uuid import UUID, uuid4 -import pytest from pydantic import BaseModel, Field, constr, validator from sqlalchemy import ForeignKey, func from sqlalchemy.orm import ( diff --git a/tests/unit/test_init_plugin_no_extras.py b/tests/unit/test_init_plugin_no_extras.py index 20d655ed..33be611e 100644 --- a/tests/unit/test_init_plugin_no_extras.py +++ b/tests/unit/test_init_plugin_no_extras.py @@ -7,7 +7,12 @@ from starlite_saqlalchemy.exceptions import MissingDependencyError SKIP = any( - [constants.IS_SAQ_INSTALLED, constants.IS_SENTRY_SDK_INSTALLED, constants.IS_REDIS_INSTALLED] + [ + constants.IS_SAQ_INSTALLED, + constants.IS_SENTRY_SDK_INSTALLED, + constants.IS_REDIS_INSTALLED, + constants.IS_SQLALCHEMY_INSTALLED, + ] ) @@ -18,12 +23,12 @@ ("do_cache", r"^.*\"redis\" is not installed.*$"), ("do_sentry", r"^.*\"sentry_sdk\" is not installed.*$"), ("do_worker", r"^.*\"saq\" is not installed.*$"), + ("do_sqlalchemy_plugin", r"^.*\"sqlalchemy\" is not installed.*$"), ], ) def test_extra_dependencies_not_installed(enabled_config: str, error: str) -> None: """Tests that the plugin test required dependencies for switches needing them.""" - kwargs = { "do_after_exception": False, "do_cache": False, diff --git a/tests/unit/test_orm.py b/tests/unit/test_orm.py index 52ebb942..513790a5 100644 --- a/tests/unit/test_orm.py +++ b/tests/unit/test_orm.py @@ -1,4 +1,9 @@ """Tests for application ORM configuration.""" +# pylint: disable=wrong-import-position,wrong-import-order +import pytest + +pytest.importorskip("sqlalchemy") + import datetime from unittest.mock import MagicMock diff --git a/tests/unit/test_service.py b/tests/unit/test_service.py index e9f4b57d..c0241a97 100644 --- a/tests/unit/test_service.py +++ b/tests/unit/test_service.py @@ -1,12 +1,16 @@ """Tests for Service object patterns.""" +# pylint: disable=wrong-import-position,wrong-import-order from __future__ import annotations +import pytest + +pytest.importorskip("sqlalchemy") + from datetime import date from typing import TYPE_CHECKING, Any from unittest.mock import AsyncMock from uuid import uuid4 -import pytest from saq import Job from starlite_saqlalchemy import db, service, worker From 47e4670f0f1799eb964be4e32640ef43d0f0c718 Mon Sep 17 00:00:00 2001 From: gazorby Date: Tue, 17 Jan 2023 17:07:32 +0100 Subject: [PATCH 14/30] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20silent?= =?UTF-8?q?=20flake8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 + src/starlite_saqlalchemy/exceptions.py | 2 +- tests/conftest.py | 2 ++ tests/unit/test_init_plugin_no_extras.py | 8 ++++---- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c466f399..302c5818 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -118,6 +118,7 @@ convention = "google" addopts = [ "-ra", "--strict-config", + # Plugin are enabled in tests/conftest.py to control loading order. "-p", "no:pytest_starlite_saqlalchemy", "-p", diff --git a/src/starlite_saqlalchemy/exceptions.py b/src/starlite_saqlalchemy/exceptions.py index f784b8af..84ea1181 100644 --- a/src/starlite_saqlalchemy/exceptions.py +++ b/src/starlite_saqlalchemy/exceptions.py @@ -62,7 +62,7 @@ class MissingDependencyError(StarliteSaqlalchemyError): def __init__(self, module: str, config: str | None = None) -> None: config = config if config else module super().__init__( - f'You enabled {config} configuration but package "{module}" is not installed. ' + f"You enabled {config} configuration but package {module!r} is not installed. " f'You may need to run: "poetry install starlite-saqlalchemy[{config}]"' ) diff --git a/tests/conftest.py b/tests/conftest.py index 05a2960c..8ee50f09 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,8 @@ from tests.utils.domain import authors, books +# Ensure that pytest_dotenv is loaded before +# so pytest_starlite_saqlalchemy uses correct env values pytest_plugins = ("pytest_dotenv", "pytest_starlite_saqlalchemy.plugin") diff --git a/tests/unit/test_init_plugin_no_extras.py b/tests/unit/test_init_plugin_no_extras.py index 33be611e..cb1ff7be 100644 --- a/tests/unit/test_init_plugin_no_extras.py +++ b/tests/unit/test_init_plugin_no_extras.py @@ -20,10 +20,10 @@ @pytest.mark.parametrize( ("enabled_config", "error"), [ - ("do_cache", r"^.*\"redis\" is not installed.*$"), - ("do_sentry", r"^.*\"sentry_sdk\" is not installed.*$"), - ("do_worker", r"^.*\"saq\" is not installed.*$"), - ("do_sqlalchemy_plugin", r"^.*\"sqlalchemy\" is not installed.*$"), + ("do_cache", r"^.*\'redis\' is not installed.*$"), + ("do_sentry", r"^.*\'sentry_sdk\' is not installed.*$"), + ("do_worker", r"^.*\'saq\' is not installed.*$"), + ("do_sqlalchemy_plugin", r"^.*\'sqlalchemy\' is not installed.*$"), ], ) def test_extra_dependencies_not_installed(enabled_config: str, error: str) -> None: From 4a98d700ecb2b542cbe6bbc86bb1b06b0a3a71f2 Mon Sep 17 00:00:00 2001 From: gazorby Date: Tue, 17 Jan 2023 19:24:20 +0100 Subject: [PATCH 15/30] =?UTF-8?q?=E2=9C=85=20test(tox):=20make=20noextras?= =?UTF-8?q?=20passing=20the=20full=20test=20suite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/starlite_saqlalchemy/init_plugin.py | 6 +- src/starlite_saqlalchemy/testing/__init__.py | 6 +- tests/unit/repository/test_abc.py | 7 +- tests/unit/repository/test_sqlalchemy.py | 7 +- tests/unit/test_cache.py | 8 +-- tests/unit/test_health.py | 68 ++++++++++++-------- tests/unit/test_init_plugin.py | 13 +++- tests/unit/test_log.py | 14 ++++ tests/unit/test_sentry.py | 6 +- tests/unit/test_testing.py | 6 +- tests/unit/test_worker.py | 4 ++ tests/utils/app.py | 8 ++- tox.ini | 2 +- 13 files changed, 109 insertions(+), 46 deletions(-) diff --git a/src/starlite_saqlalchemy/init_plugin.py b/src/starlite_saqlalchemy/init_plugin.py index 37e2d65b..95e27322 100644 --- a/src/starlite_saqlalchemy/init_plugin.py +++ b/src/starlite_saqlalchemy/init_plugin.py @@ -95,7 +95,7 @@ class PluginConfig(BaseModel): Add the hook handler to [`AppConfig.after_exception`][starlite.config.app.AppConfig.after_exception]. """ - do_cache: bool = True + do_cache: bool = False """Configure redis cache backend. Add configuration for the redis-backed cache to @@ -149,7 +149,7 @@ class PluginConfig(BaseModel): Allow the plugin to set the starlite `debug` parameter. Parameter set to value of [`AppConfig.debug`][starlite_saqlalchemy.settings.AppSettings.DEBUG]. """ - do_sqlalchemy_plugin: bool = True + do_sqlalchemy_plugin: bool = False """Configure SQLAlchemy plugin. Set the SQLAlchemy plugin on the application. Adds the plugin to @@ -158,7 +158,7 @@ class PluginConfig(BaseModel): do_type_encoders: bool = True """Configure custom type encoders on the app.""" - do_worker: bool = True + do_worker: bool = False """Configure the async worker on the application. This action instantiates a worker instance and sets handlers for diff --git a/src/starlite_saqlalchemy/testing/__init__.py b/src/starlite_saqlalchemy/testing/__init__.py index 8d75d555..e82e268d 100644 --- a/src/starlite_saqlalchemy/testing/__init__.py +++ b/src/starlite_saqlalchemy/testing/__init__.py @@ -1,9 +1,13 @@ """Application testing support.""" +from starlite_saqlalchemy.constants import IS_SQLALCHEMY_INSTALLED from .controller_test import ControllerTest -from .generic_mock_repository import GenericMockRepository from .modify_settings import modify_settings +if IS_SQLALCHEMY_INSTALLED: + from .generic_mock_repository import GenericMockRepository + + __all__ = ( "ControllerTest", "GenericMockRepository", diff --git a/tests/unit/repository/test_abc.py b/tests/unit/repository/test_abc.py index f364bd77..c05db46c 100644 --- a/tests/unit/repository/test_abc.py +++ b/tests/unit/repository/test_abc.py @@ -1,11 +1,14 @@ """Tests for the repository base class.""" +# pylint: disable=wrong-import-position,wrong-import-order from __future__ import annotations +import pytest + +pytest.importorskip("sqlalchemy") + from typing import TYPE_CHECKING from unittest.mock import MagicMock -import pytest - from starlite_saqlalchemy.exceptions import NotFoundError from starlite_saqlalchemy.testing import GenericMockRepository diff --git a/tests/unit/repository/test_sqlalchemy.py b/tests/unit/repository/test_sqlalchemy.py index d93d18c8..f5e4830e 100644 --- a/tests/unit/repository/test_sqlalchemy.py +++ b/tests/unit/repository/test_sqlalchemy.py @@ -1,12 +1,15 @@ """Unit tests for the SQLAlchemy Repository implementation.""" -# pylint: disable=protected-access,redefined-outer-name +# pylint: disable=protected-access,redefined-outer-name,wrong-import-position,wrong-import-order from __future__ import annotations +import pytest + +pytest.importorskip("sqlalchemy") + from datetime import datetime from typing import TYPE_CHECKING from unittest.mock import AsyncMock, MagicMock, call -import pytest from sqlalchemy.exc import IntegrityError, InvalidRequestError, SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py index 179ca3a6..b7f3d9e6 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/test_cache.py @@ -1,14 +1,14 @@ """Test for the application cache configurations.""" -from typing import TYPE_CHECKING +# pylint: disable=wrong-import-position +import pytest + +pytest.importorskip("redis") from starlite.config.cache import default_cache_key_builder from starlite.testing import RequestFactory from starlite_saqlalchemy import cache, settings -if TYPE_CHECKING: - import pytest - def test_cache_key_builder(monkeypatch: "pytest.MonkeyPatch") -> None: """Test that the cache key builder prefixes cache keys.""" diff --git a/tests/unit/test_health.py b/tests/unit/test_health.py index ad6d26e0..0ca8f36c 100644 --- a/tests/unit/test_health.py +++ b/tests/unit/test_health.py @@ -1,4 +1,5 @@ """Tests for application health check behavior.""" +from itertools import product from typing import TYPE_CHECKING from unittest.mock import AsyncMock @@ -7,6 +8,7 @@ from starlite.status_codes import HTTP_200_OK, HTTP_503_SERVICE_UNAVAILABLE from starlite_saqlalchemy import init_plugin, settings +from starlite_saqlalchemy.constants import IS_SQLALCHEMY_INSTALLED from starlite_saqlalchemy.exceptions import HealthCheckConfigurationError from starlite_saqlalchemy.health import ( AbstractHealthCheck, @@ -14,52 +16,66 @@ HealthController, HealthResource, ) -from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyHealthCheck + +if IS_SQLALCHEMY_INSTALLED: + from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyHealthCheck if TYPE_CHECKING: from pytest import MonkeyPatch from starlite.testing import TestClient -def test_health_check(client: "TestClient", monkeypatch: "MonkeyPatch") -> None: +health_checks: "list[AbstractHealthCheck]" = [AppHealthCheck()] + +if IS_SQLALCHEMY_INSTALLED: + health_checks.append(SQLAlchemyHealthCheck()) + + +@pytest.mark.parametrize("health_check", health_checks) +def test_health_check( + client: "TestClient", monkeypatch: "MonkeyPatch", health_check: AbstractHealthCheck +) -> None: """Test health check success response. Checks that we call the repository method and the response content. """ + monkeypatch.setattr(HealthController, "health_checks", health_checks) repo_health_mock = AsyncMock(return_value=True) - monkeypatch.setattr(SQLAlchemyHealthCheck, "ready", repo_health_mock) + for health_check_ in health_checks: + monkeypatch.setattr(health_check_, "ready", repo_health_mock) resp = client.get(settings.api.HEALTH_PATH) assert resp.status_code == HTTP_200_OK health = HealthResource( app=settings.app, - health={SQLAlchemyHealthCheck.name: True, AppHealthCheck.name: True}, + health={ht.name: True for ht in health_checks} | {health_check.name: True}, ) assert resp.json() == health.dict() - repo_health_mock.assert_called_once() + assert repo_health_mock.call_count == len(health_checks) -def test_health_check_false_response(client: "TestClient", monkeypatch: "MonkeyPatch") -> None: +@pytest.mark.parametrize( + ("health_check", "mock"), + product(health_checks, [AsyncMock(return_value=False), AsyncMock(side_effect=ConnectionError)]), +) +def test_health_check_failed( + client: "TestClient", + monkeypatch: "MonkeyPatch", + health_check: AbstractHealthCheck, + mock: AsyncMock, +) -> None: """Test health check response if check method returns `False`""" - repo_health_mock = AsyncMock(return_value=False) - monkeypatch.setattr(SQLAlchemyHealthCheck, "ready", repo_health_mock) - resp = client.get(settings.api.HEALTH_PATH) - assert resp.status_code == HTTP_503_SERVICE_UNAVAILABLE - health = HealthResource( - app=settings.app, - health={SQLAlchemyHealthCheck.name: False, AppHealthCheck.name: True}, - ) - assert resp.json() == health.dict() - - -def test_health_check_exception_raised(client: "TestClient", monkeypatch: "MonkeyPatch") -> None: - """Test expected response from check if exception raised in handler.""" - repo_health_mock = AsyncMock(side_effect=ConnectionError) - monkeypatch.setattr(SQLAlchemyHealthCheck, "ready", repo_health_mock) + # repo_health_mock = AsyncMock(return_value=False) + monkeypatch.setattr(HealthController, "health_checks", health_checks) + for health_check_ in health_checks: + if health_check_ is health_check: + monkeypatch.setattr(health_check_, "ready", mock) + else: + monkeypatch.setattr(health_check_, "ready", AsyncMock(return_value=True)) resp = client.get(settings.api.HEALTH_PATH) assert resp.status_code == HTTP_503_SERVICE_UNAVAILABLE health = HealthResource( app=settings.app, - health={SQLAlchemyHealthCheck.name: False, AppHealthCheck.name: True}, + health={ht.name: True for ht in health_checks} | {health_check.name: False}, ) assert resp.json() == health.dict() @@ -76,16 +92,16 @@ async def ready(self) -> bool: """Readiness check.""" return False - HealthController.health_checks.append(MyHealthCheck()) - repo_health_mock = AsyncMock(return_value=True) - monkeypatch.setattr(SQLAlchemyHealthCheck, "ready", repo_health_mock) + monkeypatch.setattr(HealthController, "health_checks", [AppHealthCheck(), MyHealthCheck()]) + # repo_health_mock = AsyncMock(return_value=True) + # monkeypatch.setattr(SQLAlchemyHealthCheck, "ready", repo_health_mock) resp = client.get(settings.api.HEALTH_PATH) assert resp.status_code == HTTP_503_SERVICE_UNAVAILABLE health = HealthResource( app=settings.app, health={ AppHealthCheck.name: True, - SQLAlchemyHealthCheck.name: True, + # SQLAlchemyHealthCheck.name: True, MyHealthCheck.name: False, }, ) diff --git a/tests/unit/test_init_plugin.py b/tests/unit/test_init_plugin.py index 97cfd566..4cb4a715 100644 --- a/tests/unit/test_init_plugin.py +++ b/tests/unit/test_init_plugin.py @@ -8,7 +8,14 @@ from starlite import Starlite from starlite.cache import SimpleCacheBackend -from starlite_saqlalchemy import init_plugin, sentry, worker +from starlite_saqlalchemy import init_plugin +from starlite_saqlalchemy.constants import IS_SAQ_INSTALLED, IS_SENTRY_SDK_INSTALLED + +if IS_SENTRY_SDK_INSTALLED: + from starlite_saqlalchemy import sentry + +if IS_SAQ_INSTALLED: + from starlite_saqlalchemy import worker if TYPE_CHECKING: from typing import Any @@ -55,12 +62,13 @@ def test_config_switches() -> None: assert not app.routes +@pytest.mark.skipif(not IS_SAQ_INSTALLED, reason="saq is not installed") def test_do_worker_but_not_logging(monkeypatch: MonkeyPatch) -> None: """Tests branch where we can have the worker enabled, but logging disabled.""" mock = MagicMock() monkeypatch.setattr(worker, "create_worker_instance", mock) - config = init_plugin.PluginConfig(do_logging=False) + config = init_plugin.PluginConfig(do_logging=False, do_worker=True) Starlite(route_handlers=[], on_app_init=[init_plugin.ConfigureApp(config=config)]) mock.assert_called_once() call = mock.mock_calls[0] @@ -83,6 +91,7 @@ def test_ensure_list(in_: Any, out: Any) -> None: assert init_plugin.ConfigureApp._ensure_list(in_) == out +@pytest.mark.skipif(not IS_SENTRY_SDK_INSTALLED, reason="sentry_sdk is not installed") @pytest.mark.parametrize( ("env", "exp"), [("dev", True), ("prod", True), ("local", False), ("test", False)] ) diff --git a/tests/unit/test_log.py b/tests/unit/test_log.py index f10ce12b..fe7db35a 100644 --- a/tests/unit/test_log.py +++ b/tests/unit/test_log.py @@ -22,6 +22,7 @@ from structlog import DropEvent from starlite_saqlalchemy import log, settings +from starlite_saqlalchemy.constants import IS_SAQ_INSTALLED, IS_SQLALCHEMY_INSTALLED if TYPE_CHECKING: from typing import Any @@ -86,6 +87,7 @@ async def test_middleware_calls_structlog_contextvars_clear_contextvars( app_mock.assert_called_once_with(1, 2, 3) +@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") @pytest.mark.parametrize( ("pattern", "excluded", "included"), [ @@ -126,6 +128,7 @@ async def call_handler(path_: str) -> dict[str, Any]: assert "http.response.start" in scope_state +@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") @pytest.mark.parametrize( ("status", "level"), [ @@ -151,6 +154,7 @@ async def test_before_send_handler_http_response_start( assert http_scope["state"]["http.response.start"] == http_response_start +@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") async def test_before_send_handler_http_response_body_with_more_body( before_send_handler: log.controller.BeforeSendHandler, cap_logger: CapturingLogger, @@ -164,6 +168,7 @@ async def test_before_send_handler_http_response_body_with_more_body( assert [] == cap_logger.calls +@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") async def test_before_send_handler_http_response_body_without_more_body( before_send_handler: log.controller.BeforeSendHandler, cap_logger: CapturingLogger, @@ -188,6 +193,7 @@ async def test_before_send_handler_http_response_body_without_more_body( assert cap_logger.calls +@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") async def test_before_send_handler_http_response_body_without_more_body_do_log_request_false( before_send_handler: log.controller.BeforeSendHandler, cap_logger: CapturingLogger, @@ -214,6 +220,7 @@ async def test_before_send_handler_http_response_body_without_more_body_do_log_r assert cap_logger.calls +@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") async def test_before_send_handler_does_nothing_with_other_message_types( before_send_handler: log.controller.BeforeSendHandler, cap_logger: CapturingLogger, @@ -226,6 +233,7 @@ async def test_before_send_handler_does_nothing_with_other_message_types( assert [] == cap_logger.calls +@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") async def test_before_send_handler_log_request( before_send_handler: log.controller.BeforeSendHandler, http_scope: HTTPScope, @@ -242,6 +250,7 @@ async def test_before_send_handler_log_request( bind_mock.assert_called_once_with(request=ret_val) +@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") async def test_before_send_handler_log_response( before_send_handler: log.controller.BeforeSendHandler, http_scope: HTTPScope, @@ -258,6 +267,7 @@ async def test_before_send_handler_log_response( bind_mock.assert_called_once_with(response=ret_val) +@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") @pytest.mark.parametrize("include", [True, False]) async def test_before_send_handler_exclude_body_from_log( include: bool, @@ -300,6 +310,7 @@ async def test_before_send_handler_extract_request_data( } +@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") def test_before_send_handler_extract_response_data( before_send_handler: log.controller.BeforeSendHandler, http_response_start: HTTPResponseStartEvent, @@ -324,6 +335,7 @@ async def test_before_process_calls_structlog_contextvars_clear_contextvars( clear_ctx_vars_mock.assert_called_once() +@pytest.mark.skipif(not IS_SAQ_INSTALLED, reason="saq is not installed") async def test_after_process(job: Job, cap_logger: CapturingLogger) -> None: """Tests extraction of job data, and eventual log.""" await log.worker.after_process({"job": job}) @@ -353,6 +365,7 @@ async def test_after_process(job: Job, cap_logger: CapturingLogger) -> None: ] == cap_logger.calls +@pytest.mark.skipif(not IS_SAQ_INSTALLED, reason="saq is not installed") async def test_after_process_logs_at_error(job: Job, cap_logger: CapturingLogger) -> None: """Tests eventual log is at ERROR level if `job.error`.""" job.error = "Yep, this is the traceback." @@ -408,6 +421,7 @@ def test_handler() -> str: assert call.kwargs["exception"] +@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") async def test_exception_in_before_send_handler_read_empty_body( client: TestClient[Starlite], cap_logger: CapturingLogger, diff --git a/tests/unit/test_sentry.py b/tests/unit/test_sentry.py index 398c09e9..9b4b2a6f 100644 --- a/tests/unit/test_sentry.py +++ b/tests/unit/test_sentry.py @@ -1,8 +1,10 @@ """Tests for sentry integration.""" +# pylint: disable=wrong-import-position,wrong-import-order +import pytest -from typing import TYPE_CHECKING +pytest.importorskip("sentry_sdk") -import pytest +from typing import TYPE_CHECKING from starlite_saqlalchemy import settings from starlite_saqlalchemy.sentry import SamplingContext, sentry_traces_sampler diff --git a/tests/unit/test_testing.py b/tests/unit/test_testing.py index 2474f3ba..259a1555 100644 --- a/tests/unit/test_testing.py +++ b/tests/unit/test_testing.py @@ -1,11 +1,15 @@ """Test testing module.""" +# pylint: disable=wrong-import-position,wrong-import-order from __future__ import annotations +import pytest + +pytest.importorskip("sqlalchemy") + from typing import TYPE_CHECKING from unittest.mock import MagicMock import httpx -import pytest from starlite.status_codes import ( HTTP_200_OK, HTTP_404_NOT_FOUND, diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index 304e9251..a8ecc397 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -1,6 +1,10 @@ """Tests for the SAQ async worker functionality.""" +# pylint: disable=wrong-import-position from __future__ import annotations +import pytest + +pytest.importorskip("saq") from asyncpg.pgproto import pgproto from starlite_saqlalchemy import worker diff --git a/tests/utils/app.py b/tests/utils/app.py index f821df42..3c050849 100644 --- a/tests/utils/app.py +++ b/tests/utils/app.py @@ -3,7 +3,7 @@ from starlite import Starlite -from starlite_saqlalchemy import ConfigureApp +from starlite_saqlalchemy import ConfigureApp, PluginConfig from . import controllers @@ -12,5 +12,9 @@ def create_app() -> Starlite: """App for our test domain.""" return Starlite( route_handlers=[controllers.create_router()], - on_app_init=[ConfigureApp()], + on_app_init=[ + ConfigureApp( + config=PluginConfig(do_sqlalchemy_plugin=True, do_worker=True, do_cache=True) + ) + ], ) diff --git a/tox.ini b/tox.ini index adcb73a0..8e3cbc90 100644 --- a/tox.ini +++ b/tox.ini @@ -78,7 +78,7 @@ basepython = python3.11 deps = -r{toxinidir}/dev-noextras.requirements.txt commands = - coverage run -p -m pytest tests/unit/test_init_plugin_no_extras.py {posargs} + coverage run -p -m pytest {posargs} [testenv:docs] basepython = python3.11 From 143030aec3740af7ba62fa79065dca6603ca322f Mon Sep 17 00:00:00 2001 From: gazorby Date: Tue, 17 Jan 2023 19:34:23 +0100 Subject: [PATCH 16/30] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20silent?= =?UTF-8?q?=20pyright?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_health.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_health.py b/tests/unit/test_health.py index 0ca8f36c..f3f98a10 100644 --- a/tests/unit/test_health.py +++ b/tests/unit/test_health.py @@ -1,4 +1,5 @@ """Tests for application health check behavior.""" +# pylint: disable=ungrouped-imports from itertools import product from typing import TYPE_CHECKING from unittest.mock import AsyncMock @@ -17,9 +18,6 @@ HealthResource, ) -if IS_SQLALCHEMY_INSTALLED: - from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyHealthCheck - if TYPE_CHECKING: from pytest import MonkeyPatch from starlite.testing import TestClient @@ -28,6 +26,8 @@ health_checks: "list[AbstractHealthCheck]" = [AppHealthCheck()] if IS_SQLALCHEMY_INSTALLED: + from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyHealthCheck + health_checks.append(SQLAlchemyHealthCheck()) From 1be7382c504abd9a2d60f838a49c4a348bb3eb33 Mon Sep 17 00:00:00 2001 From: gazorby Date: Tue, 17 Jan 2023 20:06:03 +0100 Subject: [PATCH 17/30] =?UTF-8?q?=F0=9F=93=9D=20docs:=20update=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 1fcdc227..c1a68015 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,27 @@ Configuration for a [Starlite](https://github.com/starlite-api/starlite) applica - SAQ async worker - Lots of features! +## Installation + +This will install `starlite-saqlalchemy` with minimal dependencies. + +```console +poetry add starlite-saqlalchemy +``` + +You can also install additional dependencies depending on the features you need: + +```console +# Repository implementation, DTOs +poetry add starlite-saqlalchemy[sqlalchemy] +# Async worker using saq +poetry add starlite-saqlalchemy[worker] +# Redis cache backend +poetry add starlite-saqlalchemy[cache] +# Sentry integration for starlite +poetry add starlite-saqlalchemy[sentry] +``` + ## Example ```python From 978f61e10ed06a34d12e623d26af6d660321c3d3 Mon Sep 17 00:00:00 2001 From: gazorby Date: Tue, 17 Jan 2023 20:08:35 +0100 Subject: [PATCH 18/30] =?UTF-8?q?=F0=9F=93=9D=20docs:=20update=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index c1a68015..ace04af1 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,9 @@ poetry add starlite-saqlalchemy[worker] poetry add starlite-saqlalchemy[cache] # Sentry integration for starlite poetry add starlite-saqlalchemy[sentry] + +# or to install them all: +poetry add starlite-saqlalchemy[all] ``` ## Example From f5c31a55206aac3ca163bf445a4c927fe60bb374 Mon Sep 17 00:00:00 2001 From: Peter Schutt Date: Wed, 18 Jan 2023 11:07:58 +1000 Subject: [PATCH 19/30] refactor: requirements/tox/etc --- CONTRIBUTING.md | 1 - dev-noextras.requirements.txt | 9 --- poetry.lock | 110 ++++++++++++++++------------- requirements.dev-extras.txt | 7 ++ requirements.dev.txt | 5 -- tests/pytest_plugin/test_plugin.py | 2 +- tox.ini | 65 +++++++---------- 7 files changed, 92 insertions(+), 107 deletions(-) delete mode 100644 dev-noextras.requirements.txt create mode 100644 requirements.dev-extras.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f9410b18..d62e0e71 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,7 +60,6 @@ For example: - `tox -e coverage` - unit test coverage report. - `tox -e mypy` - runs mypy static type checker on the source. - `tox -e pyright` - runs pyright static type checker on the source. -- `tox -e refurb` - runs the [refurb](https://github.com/dosisod/refurb) tool over the source. - `tox -e integration` - runs the dockerized integration test suite. - `tox` - run everything, you maniac! diff --git a/dev-noextras.requirements.txt b/dev-noextras.requirements.txt deleted file mode 100644 index 725fa123..00000000 --- a/dev-noextras.requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -asgi-lifespan == 2.0.0 -coverage[toml] == 7.0.0; python_version < '3.11' -coverage == 7.0.1; python_version >= '3.11' -cryptography == 39.0.0 -pytest == 7.2.0 -pytest-asyncio == 0.20.3 -pytest-dotenv == 0.5.2 -pytest_docker == 1.0.1 -rich == 13.0.1 diff --git a/poetry.lock b/poetry.lock index 773cd726..9a88e3a5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -177,14 +177,14 @@ idna = ">=2.0.0" [[package]] name = "faker" -version = "16.4.0" +version = "16.6.0" description = "Faker is a Python package that generates fake data for you." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Faker-16.4.0-py3-none-any.whl", hash = "sha256:5420467fad3fa582094057754e5e81326cb1f51ab822bf9df96c077cfb35ae49"}, - {file = "Faker-16.4.0.tar.gz", hash = "sha256:dcffdca8ec9a715982bcd5f53ee688dc4784cd112f9910f8f7183773eb3ec276"}, + {file = "Faker-16.6.0-py3-none-any.whl", hash = "sha256:0a74514d654db0a3d37b9ca681f2d9182d2ec556f78b4f1a842a2ccc944660cd"}, + {file = "Faker-16.6.0.tar.gz", hash = "sha256:dc8b2a8bf0d852d26eacf7763afd5e7d6e9e50d80ec648b51b8ecd3c505435fd"}, ] [package.dependencies] @@ -499,52 +499,62 @@ testing = ["pytest"] [[package]] name = "markupsafe" -version = "2.1.1" +version = "2.1.2" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, ] [[package]] @@ -754,14 +764,14 @@ typing-extensions = "*" [[package]] name = "pydantic-openapi-schema" -version = "1.5.0" +version = "1.5.1" description = "OpenAPI Schema using pydantic. Forked for Starlite-API from 'openapi-schema-pydantic'." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_openapi_schema-1.5.0-py3-none-any.whl", hash = "sha256:0b60d105a2665287fbf8ee85602919a3558e07cf34261e64fa8f4d2281a3bad0"}, - {file = "pydantic_openapi_schema-1.5.0.tar.gz", hash = "sha256:50bf6ee00fc0dcd1c87b96f30c3273e3bebcc12f23f30c31e5af5ec2e3275c87"}, + {file = "pydantic_openapi_schema-1.5.1-py3-none-any.whl", hash = "sha256:fd5b1bff81ff70faa87fec62bd8193ccd671f31bc15b32a4557623d3db0e5eae"}, + {file = "pydantic_openapi_schema-1.5.1.tar.gz", hash = "sha256:d9b56235f4c4817c6e3693c4f8122c3e5e9c75f6b3b454524ad985012a264daf"}, ] [package.dependencies] @@ -888,14 +898,14 @@ idna2008 = ["idna"] [[package]] name = "saq" -version = "0.9.2" +version = "0.9.3" description = "Distributed Python job queue with asyncio and redis" category = "main" optional = true python-versions = "*" files = [ - {file = "saq-0.9.2-py3-none-any.whl", hash = "sha256:9ae0636f8ffe92fa5a9ee68a92828a562d5f0f241762ddf9744df694e33a4ab5"}, - {file = "saq-0.9.2.tar.gz", hash = "sha256:d0ad2994e7ae11337dc6481e7065399aa02f3d8923b1d9fdde1e02697de8d976"}, + {file = "saq-0.9.3-py3-none-any.whl", hash = "sha256:a5d3bcf58297d94daecf02b4cc37195fe2a8bca5a8fd7bd5bde50511d2bdc475"}, + {file = "saq-0.9.3.tar.gz", hash = "sha256:bea2a1437c17ea64d956da2edb77381b8818bb0d7379de501937290ede52171f"}, ] [package.dependencies] diff --git a/requirements.dev-extras.txt b/requirements.dev-extras.txt new file mode 100644 index 00000000..cbb2ae96 --- /dev/null +++ b/requirements.dev-extras.txt @@ -0,0 +1,7 @@ +-r requirements.dev.txt + +sentry-sdk >= "1.13.0" +hiredis +redis +saq >= "0.9.1" +sqlalchemy == 2.0.0rc2 diff --git a/requirements.dev.txt b/requirements.dev.txt index fe76a26b..3a8a8da6 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -7,8 +7,3 @@ pytest-asyncio == 0.20.3 pytest-dotenv == 0.5.2 pytest_docker == 1.0.1 rich == 13.0.1 -sentry-sdk >= "1.13.0" -hiredis -redis -saq >= "0.9.1" -sqlalchemy == 2.0.0rc2 diff --git a/tests/pytest_plugin/test_plugin.py b/tests/pytest_plugin/test_plugin.py index 75d00c93..c8416b30 100644 --- a/tests/pytest_plugin/test_plugin.py +++ b/tests/pytest_plugin/test_plugin.py @@ -122,7 +122,7 @@ def test_app(app): def test_app_fixture_if_app_instance(pytester: Pytester) -> None: - """Test that the app fixture returns the an instance if the path points to + """Test that the app fixture returns an instance if the path points to one.""" pytester.syspathinsert() pytester.makepyfile( diff --git a/tox.ini b/tox.ini index 8e3cbc90..b8da7704 100644 --- a/tox.ini +++ b/tox.ini @@ -1,25 +1,27 @@ [gh-actions] python = - 3.10: py310,noextras - 3.11: py311,noextras,pytest-plugin,integration + 3.10: py310,no-extras + 3.11: py311,no-extras,pytest-plugin,integration [tox] -envlist = pylint,mypy,pyright,py310,py311,noextras,pytest-plugin,integration,coverage +envlist = pylint,mypy,pyright,py310,py311,no-extras,pytest-plugin,integration,coverage isolated_build = true [testenv] -deps = - -r{toxinidir}/requirements.dev.txt +deps = -r{toxinidir}/requirements.dev.txt -commands = - coverage run -p -m pytest {posargs} +commands = coverage run -p -m pytest {posargs} + +[testenv:py3{10,11}] +deps = -r {toxinidir}/requirements.dev-extras.txt [testenv:pytest-plugin] basepython = python3.11 +deps = -r requirements.dev-extras.txt commands = coverage run -p -m pytest tests/pytest_plugin {posargs} [testenv:coverage] -depends = py310,py311,noextras,pytest-plugin +depends = py310,py311,no-extras,pytest-plugin basepython = python3.11 commands = coverage combine @@ -27,21 +29,12 @@ commands = coverage xml parallel_show_output = true -[testenv:refurb] -basepython = python3.11 -deps = - refurb - {[testenv]deps} -commands = - python -m refurb examples/ src/ tests/ - [testenv:pylint] basepython = python3.11 deps = pylint - {[testenv]deps} -commands = - python -m pylint src/ tests/ + -r requirements.dev-extras.txt +commands = python -m pylint src/ tests/ [testenv:mypy] basepython = python3.11 @@ -49,9 +42,8 @@ deps = asyncpg-stubs mypy types-redis - {[testenv]deps} -commands = - python -m mypy examples/ src/ tests/ + -r requirements.dev-extras.txt +commands = python -m mypy examples/ src/ tests/ [testenv:pyright] basepython = python3.11 @@ -59,32 +51,23 @@ deps = asyncpg-stubs pyright types-redis - {[testenv]deps} -commands = - pyright examples/ src/ tests/ + -r requirements.dev-extras.txt +commands = pyright examples/ src/ tests/ [testenv:integration] basepython = python3.11 deps = docker-compose - {[testenv]deps} -allowlist_externals = - docker -commands = - pytest tests/integration {posargs} + -r requirements.dev-extras.txt +allowlist_externals = docker +commands = pytest tests/integration {posargs} -[testenv:noextras] +[testenv:no-extras] basepython = python3.11 -deps = - -r{toxinidir}/dev-noextras.requirements.txt -commands = - coverage run -p -m pytest {posargs} +commands = coverage run -p -m pytest {posargs} [testenv:docs] basepython = python3.11 -passenv = - HOME -deps = - -r{toxinidir}/requirements.docs.txt -commands = - mike {posargs: serve} +passenv = HOME +deps = -r{toxinidir}/requirements.docs.txt +commands = mike {posargs: serve} From b446f52dd8347722cdce7703f8e8f2e81a2aa7b8 Mon Sep 17 00:00:00 2001 From: Peter Schutt Date: Wed, 18 Jan 2023 11:32:53 +1000 Subject: [PATCH 20/30] style: rollback style changes The bigger the PR, the better it is if we keep the changes to bare minimum. --- pyproject.toml | 20 +++---------------- .../repository/__init__.py | 7 ++++++- src/starlite_saqlalchemy/service/__init__.py | 6 +++++- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 302c5818..bba0ce2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,11 +10,7 @@ ignore-words-list = "alog" [tool.coverage.run] branch = true -omit = [ - "*/starlite_saqlalchemy/scripts.py", - "*/starlite_saqlalchemy/lifespan.py", - "tests/*", -] +omit = ["*/starlite_saqlalchemy/scripts.py", "*/starlite_saqlalchemy/lifespan.py", "tests/*"] relative_files = true source_pkgs = ["starlite_saqlalchemy", "pytest_starlite_saqlalchemy"] @@ -42,17 +38,7 @@ license = "MIT" authors = ["Peter Schutt "] readme = "README.md" repository = "https://github.com/topsport-com-au/starlite-saqlalchemy" -keywords = [ - "api", - "rest", - "http", - "asgi", - "starlite", - "saq", - "sqlalchemy", - "plugin", - "python", -] +keywords = ["api", "rest", "http", "asgi", "starlite", "saq", "sqlalchemy", "plugin", "python"] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Plugins", @@ -66,7 +52,7 @@ classifiers = [ ] packages = [ { include = "starlite_saqlalchemy", from = "src" }, - { include = "pytest_starlite_saqlalchemy", from = "src" }, + { include = "pytest_starlite_saqlalchemy", from = "src" } ] [tool.poetry.dependencies] diff --git a/src/starlite_saqlalchemy/repository/__init__.py b/src/starlite_saqlalchemy/repository/__init__.py index 354076f5..22f79a7e 100644 --- a/src/starlite_saqlalchemy/repository/__init__.py +++ b/src/starlite_saqlalchemy/repository/__init__.py @@ -8,4 +8,9 @@ if constants.IS_SQLALCHEMY_INSTALLED: from . import sqlalchemy -__all__ = ["abc", "filters", "types", "sqlalchemy"] +__all__ = [ + "abc", + "filters", + "sqlalchemy", + "types", +] diff --git a/src/starlite_saqlalchemy/service/__init__.py b/src/starlite_saqlalchemy/service/__init__.py index 99fcfcf7..a58ddd16 100644 --- a/src/starlite_saqlalchemy/service/__init__.py +++ b/src/starlite_saqlalchemy/service/__init__.py @@ -7,4 +7,8 @@ if constants.IS_SQLALCHEMY_INSTALLED: from .sqlalchemy import RepositoryService -__all__ = ["Service", "make_service_callback", "RepositoryService"] +__all__ = [ + "RepositoryService", + "Service", + "make_service_callback", +] From abfa2eac9c2d657451edd44bb8e4ada74807a262 Mon Sep 17 00:00:00 2001 From: Peter Schutt Date: Wed, 18 Jan 2023 12:55:23 +1000 Subject: [PATCH 21/30] refactor: run pytest plugin tests in sub-process. This prevents the need to reload any modules, albeit it is a bit slower. --- tests/pytest_plugin/conftest.py | 11 ----------- tests/pytest_plugin/test_plugin.py | 22 +++++++++++++--------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/tests/pytest_plugin/conftest.py b/tests/pytest_plugin/conftest.py index 08003bdf..0172cf77 100644 --- a/tests/pytest_plugin/conftest.py +++ b/tests/pytest_plugin/conftest.py @@ -1,15 +1,4 @@ """Enable the `pytester` fixture for the plugin tests.""" from __future__ import annotations -from importlib import reload - -import pytest - -from starlite_saqlalchemy.db import orm - pytest_plugins = ["pytester"] - - -@pytest.fixture(autouse=True) -def _reload_orm() -> None: - reload(orm) diff --git a/tests/pytest_plugin/test_plugin.py b/tests/pytest_plugin/test_plugin.py index c8416b30..e787914b 100644 --- a/tests/pytest_plugin/test_plugin.py +++ b/tests/pytest_plugin/test_plugin.py @@ -20,7 +20,7 @@ def test_pytest_addoption() -> None: assert parser._ininames == ["test_app", "unit_test_pattern"] """ ) - result = pytester.runpytest() + result = pytester.runpytest_subprocess() result.assert_outcomes(passed=1) @@ -49,7 +49,7 @@ def test_patch_worker() -> None: assert isinstance(Worker.stop, MagicMock) """ ) - result = pytester.runpytest() + result = pytester.runpytest_subprocess() result.assert_outcomes(passed=3) @@ -78,7 +78,7 @@ def test_patch_worker() -> None: assert not isinstance(Worker.stop, MagicMock) """ ) - result = pytester.runpytest() + result = pytester.runpytest_subprocess() result.assert_outcomes(passed=3) @@ -95,7 +95,7 @@ def test_patch_http_close(is_unit_test: bool) -> None: assert not starlite_saqlalchemy.http.clients """ ) - result = pytester.runpytest() + result = pytester.runpytest_subprocess() result.assert_outcomes(passed=1) @@ -127,9 +127,13 @@ def test_app_fixture_if_app_instance(pytester: Pytester) -> None: pytester.syspathinsert() pytester.makepyfile( test_app=""" - from tests.utils.app import create_app + from starlite import Starlite, get - app = create_app() + @get("/wherever") + def whatever() -> None: + return None + + app = Starlite(route_handlers=[whatever]) """ ) pytester.makepyprojecttoml( @@ -144,10 +148,10 @@ def test_app_fixture_if_app_instance(pytester: Pytester) -> None: def test_app(app): assert isinstance(app, Starlite) - assert "/authors" in app.route_handler_method_map + assert "/wherever" in app.route_handler_method_map """ ) - result = pytester.runpytest() + result = pytester.runpytest_subprocess() result.assert_outcomes(passed=1) @@ -170,5 +174,5 @@ def test_app(app): assert app.route_handler_method_map.keys() == {"/health"} """ ) - result = pytester.runpytest() + result = pytester.runpytest_subprocess() result.assert_outcomes(passed=1) From 273a8abb69fcb94f0b40288b9c006a6a97756de6 Mon Sep 17 00:00:00 2001 From: Peter Schutt Date: Wed, 18 Jan 2023 12:56:05 +1000 Subject: [PATCH 22/30] refactor: no top level conditional imports. --- src/starlite_saqlalchemy/__init__.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/starlite_saqlalchemy/__init__.py b/src/starlite_saqlalchemy/__init__.py index edc31c9a..c8032102 100644 --- a/src/starlite_saqlalchemy/__init__.py +++ b/src/starlite_saqlalchemy/__init__.py @@ -34,48 +34,22 @@ def example_handler() -> dict: settings, type_encoders, ) -from .constants import ( - IS_REDIS_INSTALLED, - IS_SAQ_INSTALLED, - IS_SENTRY_SDK_INSTALLED, - IS_SQLALCHEMY_INSTALLED, -) from .init_plugin import ConfigureApp, PluginConfig -if IS_SENTRY_SDK_INSTALLED: - from . import sentry - -if IS_SAQ_INSTALLED: - from . import worker - -if IS_REDIS_INSTALLED: - from . import cache, redis - -if IS_SQLALCHEMY_INSTALLED: - from . import db, dto, sqlalchemy_plugin - - __all__ = [ "ConfigureApp", "PluginConfig", - "cache", "compression", - "db", "dependencies", - "dto", "exceptions", "health", "http", "log", "openapi", - "redis", "repository", - "sentry", "service", "settings", - "sqlalchemy_plugin", "type_encoders", - "worker", ] From 2381cd7372ef42470e27959e09d47c4f3da7620a Mon Sep 17 00:00:00 2001 From: Peter Schutt Date: Wed, 18 Jan 2023 13:01:41 +1000 Subject: [PATCH 23/30] refactor: makes lifespan check imports and logic conditional --- src/starlite_saqlalchemy/lifespan.py | 61 ++++++++++++++-------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/src/starlite_saqlalchemy/lifespan.py b/src/starlite_saqlalchemy/lifespan.py index cd9cffcd..8be6b4a8 100644 --- a/src/starlite_saqlalchemy/lifespan.py +++ b/src/starlite_saqlalchemy/lifespan.py @@ -1,5 +1,5 @@ """Application lifespan handlers.""" -# pylint: disable=broad-except +# pylint: disable=broad-except,import-outside-toplevel import asyncio import logging @@ -7,48 +7,47 @@ from starlite_saqlalchemy import constants, settings -if constants.IS_REDIS_INSTALLED: - from starlite_saqlalchemy import redis - -if constants.IS_SQLALCHEMY_INSTALLED: - from sqlalchemy import text - - from starlite_saqlalchemy.db import engine # pylint: disable=ungrouped-imports - - logger = logging.getLogger(__name__) async def _db_ready() -> None: """Wait for database to become responsive.""" - while True: - try: - async with engine.begin() as conn: - await conn.execute(text("SELECT 1")) - except Exception as exc: - logger.info("Waiting for DB: %s", exc) - await asyncio.sleep(5) - else: - logger.info("DB OK!") - break + if constants.IS_SQLALCHEMY_INSTALLED: + from sqlalchemy import text + + from starlite_saqlalchemy.db import engine + + while True: + try: + async with engine.begin() as conn: + await conn.execute(text("SELECT 1")) + except Exception as exc: + logger.info("Waiting for DB: %s", exc) + await asyncio.sleep(5) + else: + logger.info("DB OK!") + break async def _redis_ready() -> None: """Wait for redis to become responsive.""" - while True: - try: - await redis.client.ping() - except Exception as exc: - logger.info("Waiting for Redis: %s", exc) - await asyncio.sleep(5) - else: - logger.info("Redis OK!") - break + if constants.IS_REDIS_INSTALLED: + from starlite_saqlalchemy import redis + + while True: + try: + await redis.client.ping() + except Exception as exc: + logger.info("Waiting for Redis: %s", exc) + await asyncio.sleep(5) + else: + logger.info("Redis OK!") + break async def before_startup_handler(_: starlite.Starlite) -> None: """Do things before the app starts up.""" - if constants.IS_SQLALCHEMY_INSTALLED and settings.app.CHECK_DB_READY: + if settings.app.CHECK_DB_READY: await _db_ready() - if constants.IS_REDIS_INSTALLED and settings.app.CHECK_REDIS_READY: + if settings.app.CHECK_REDIS_READY: await _redis_ready() From 53e1f23e8d799b918584a9a19c24ab5b5190cc0d Mon Sep 17 00:00:00 2001 From: Peter Schutt Date: Wed, 18 Jan 2023 13:12:20 +1000 Subject: [PATCH 24/30] refactor: no implicit imports of modules with optional functionality --- src/starlite_saqlalchemy/repository/__init__.py | 6 ------ src/starlite_saqlalchemy/service/__init__.py | 6 ------ src/starlite_saqlalchemy/testing/__init__.py | 7 ------- tests/unit/conftest.py | 12 +++++++++--- tests/unit/repository/test_abc.py | 2 +- tests/unit/test_service.py | 4 +++- tests/unit/test_testing.py | 17 +++++++++-------- tests/utils/domain/authors.py | 8 +++++--- tests/utils/domain/books.py | 5 +++-- 9 files changed, 30 insertions(+), 37 deletions(-) diff --git a/src/starlite_saqlalchemy/repository/__init__.py b/src/starlite_saqlalchemy/repository/__init__.py index 22f79a7e..67ef92f9 100644 --- a/src/starlite_saqlalchemy/repository/__init__.py +++ b/src/starlite_saqlalchemy/repository/__init__.py @@ -1,16 +1,10 @@ """Abstraction over the data storage for the application.""" from __future__ import annotations -from starlite_saqlalchemy import constants - from . import abc, filters, types -if constants.IS_SQLALCHEMY_INSTALLED: - from . import sqlalchemy - __all__ = [ "abc", "filters", - "sqlalchemy", "types", ] diff --git a/src/starlite_saqlalchemy/service/__init__.py b/src/starlite_saqlalchemy/service/__init__.py index a58ddd16..db710c8d 100644 --- a/src/starlite_saqlalchemy/service/__init__.py +++ b/src/starlite_saqlalchemy/service/__init__.py @@ -1,14 +1,8 @@ """Implementations for service object.""" -from starlite_saqlalchemy import constants - from .generic import Service, make_service_callback -if constants.IS_SQLALCHEMY_INSTALLED: - from .sqlalchemy import RepositoryService - __all__ = [ - "RepositoryService", "Service", "make_service_callback", ] diff --git a/src/starlite_saqlalchemy/testing/__init__.py b/src/starlite_saqlalchemy/testing/__init__.py index e82e268d..50f44d01 100644 --- a/src/starlite_saqlalchemy/testing/__init__.py +++ b/src/starlite_saqlalchemy/testing/__init__.py @@ -1,15 +1,8 @@ """Application testing support.""" -from starlite_saqlalchemy.constants import IS_SQLALCHEMY_INSTALLED - from .controller_test import ControllerTest from .modify_settings import modify_settings -if IS_SQLALCHEMY_INSTALLED: - from .generic_mock_repository import GenericMockRepository - - __all__ = ( "ControllerTest", - "GenericMockRepository", "modify_settings", ) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index a88ed733..f5213910 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -18,7 +18,9 @@ from starlite import Starlite from starlite.types import HTTPResponseBodyEvent, HTTPResponseStartEvent, HTTPScope - from starlite_saqlalchemy.testing import GenericMockRepository + from starlite_saqlalchemy.testing.generic_mock_repository import ( + GenericMockRepository, + ) from tests.utils.domain.authors import Author from tests.utils.domain.books import Book @@ -28,7 +30,9 @@ def fx_author_repository_type( authors: list[Author], monkeypatch: pytest.MonkeyPatch ) -> type[GenericMockRepository[Author]]: """Mock Author repository, pre-seeded with collection data.""" - from starlite_saqlalchemy.testing import GenericMockRepository + from starlite_saqlalchemy.testing.generic_mock_repository import ( + GenericMockRepository, + ) from tests.utils.domain.authors import Author from tests.utils.domain.authors import Service as AuthorService @@ -51,7 +55,9 @@ def fx_book_repository_type( books: list[Book], monkeypatch: pytest.MonkeyPatch ) -> type[GenericMockRepository[Book]]: """Mock Book repository, pre-seeded with collection data.""" - from starlite_saqlalchemy.testing import GenericMockRepository + from starlite_saqlalchemy.testing.generic_mock_repository import ( + GenericMockRepository, + ) from tests.utils.domain.books import Book from tests.utils.domain.books import Service as BookService diff --git a/tests/unit/repository/test_abc.py b/tests/unit/repository/test_abc.py index c05db46c..7fb8f7d1 100644 --- a/tests/unit/repository/test_abc.py +++ b/tests/unit/repository/test_abc.py @@ -10,7 +10,7 @@ from unittest.mock import MagicMock from starlite_saqlalchemy.exceptions import NotFoundError -from starlite_saqlalchemy.testing import GenericMockRepository +from starlite_saqlalchemy.testing.generic_mock_repository import GenericMockRepository if TYPE_CHECKING: from pytest import MonkeyPatch diff --git a/tests/unit/test_service.py b/tests/unit/test_service.py index c0241a97..c553616f 100644 --- a/tests/unit/test_service.py +++ b/tests/unit/test_service.py @@ -20,7 +20,9 @@ if TYPE_CHECKING: from pytest import MonkeyPatch - from starlite_saqlalchemy.testing import GenericMockRepository + from starlite_saqlalchemy.testing.generic_mock_repository import ( + GenericMockRepository, + ) @pytest.fixture(autouse=True) diff --git a/tests/unit/test_testing.py b/tests/unit/test_testing.py index 259a1555..8c6d2184 100644 --- a/tests/unit/test_testing.py +++ b/tests/unit/test_testing.py @@ -18,6 +18,7 @@ from starlite_saqlalchemy import testing from starlite_saqlalchemy.exceptions import ConflictError, StarliteSaqlalchemyError +from starlite_saqlalchemy.testing.generic_mock_repository import GenericMockRepository from tests.utils.domain.authors import Author from tests.utils.domain.authors import Service as AuthorService from tests.utils.domain.books import Book @@ -31,7 +32,7 @@ async def test_repo_raises_conflict_if_add_with_id( authors: list[Author], - author_repository: testing.GenericMockRepository[Author], + author_repository: GenericMockRepository[Author], ) -> None: """Test mock repo raises conflict if add identified entity.""" with pytest.raises(ConflictError): @@ -40,14 +41,14 @@ async def test_repo_raises_conflict_if_add_with_id( def test_generic_mock_repository_parametrization() -> None: """Test that the mock repository handles multiple types.""" - author_repo = testing.GenericMockRepository[Author] - book_repo = testing.GenericMockRepository[Book] + author_repo = GenericMockRepository[Author] + book_repo = GenericMockRepository[Book] assert author_repo.model_type is Author # type:ignore[misc] assert book_repo.model_type is Book # type:ignore[misc] def test_generic_mock_repository_seed_collection( - author_repository_type: type[testing.GenericMockRepository[Author]], + author_repository_type: type[GenericMockRepository[Author]], ) -> None: """Test seeding instances.""" author_repository_type.seed_collection([Author(id="abc")]) @@ -55,7 +56,7 @@ def test_generic_mock_repository_seed_collection( def test_generic_mock_repository_clear_collection( - author_repository_type: type[testing.GenericMockRepository[Author]], + author_repository_type: type[GenericMockRepository[Author]], ) -> None: """Test clearing collection for type.""" author_repository_type.clear_collection() @@ -63,7 +64,7 @@ def test_generic_mock_repository_clear_collection( def test_generic_mock_repository_filter_collection_by_kwargs( - author_repository: testing.GenericMockRepository[Author], + author_repository: GenericMockRepository[Author], ) -> None: """Test filtering the repository collection by kwargs.""" author_repository.filter_collection_by_kwargs(name="Leo Tolstoy") @@ -72,7 +73,7 @@ def test_generic_mock_repository_filter_collection_by_kwargs( def test_generic_mock_repository_filter_collection_by_kwargs_and_semantics( - author_repository: testing.GenericMockRepository[Author], + author_repository: GenericMockRepository[Author], ) -> None: """Test that filtering by kwargs has `AND` semantics when multiple kwargs, not `OR`.""" @@ -81,7 +82,7 @@ def test_generic_mock_repository_filter_collection_by_kwargs_and_semantics( def test_generic_mock_repository_raises_repository_exception_if_named_attribute_doesnt_exist( - author_repository: testing.GenericMockRepository[Author], + author_repository: GenericMockRepository[Author], ) -> None: """Test that a repo exception is raised if a named attribute doesn't exist.""" diff --git a/tests/utils/domain/authors.py b/tests/utils/domain/authors.py index 1af36f43..45936e5a 100644 --- a/tests/utils/domain/authors.py +++ b/tests/utils/domain/authors.py @@ -6,7 +6,9 @@ from sqlalchemy.orm import Mapped -from starlite_saqlalchemy import db, dto, repository, service +from starlite_saqlalchemy import db, dto +from starlite_saqlalchemy.repository.sqlalchemy import SQLAlchemyRepository +from starlite_saqlalchemy.service.sqlalchemy import RepositoryService class Author(db.orm.Base): @@ -16,13 +18,13 @@ class Author(db.orm.Base): dob: Mapped[date] -class Repository(repository.sqlalchemy.SQLAlchemyRepository[Author]): +class Repository(SQLAlchemyRepository[Author]): """Author repository.""" model_type = Author -class Service(service.RepositoryService[Author]): +class Service(RepositoryService[Author]): """Author service object.""" repository_type = Repository diff --git a/tests/utils/domain/books.py b/tests/utils/domain/books.py index 845f802a..51f974ab 100644 --- a/tests/utils/domain/books.py +++ b/tests/utils/domain/books.py @@ -7,8 +7,9 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship -from starlite_saqlalchemy import db, dto, service +from starlite_saqlalchemy import db, dto from starlite_saqlalchemy.repository.sqlalchemy import SQLAlchemyRepository +from starlite_saqlalchemy.service.sqlalchemy import RepositoryService from tests.utils.domain.authors import Author @@ -28,7 +29,7 @@ class Repository(SQLAlchemyRepository[Book]): model_type = Book -class Service(service.RepositoryService[Book]): +class Service(RepositoryService[Book]): """Book service.""" repository_type = Repository From 681ae16729c43f923e74a59ec3e36225ff50f24e Mon Sep 17 00:00:00 2001 From: Peter Schutt Date: Wed, 18 Jan 2023 13:52:53 +1000 Subject: [PATCH 25/30] refactor: move any worker related stuff out of service module --- src/starlite_saqlalchemy/constants.py | 10 +++ src/starlite_saqlalchemy/init_plugin.py | 14 ++-- src/starlite_saqlalchemy/service/__init__.py | 7 +- src/starlite_saqlalchemy/service/generic.py | 72 ++--------------- .../service/sqlalchemy.py | 2 + src/starlite_saqlalchemy/worker.py | 60 +++++++++++++- tests/unit/test_service.py | 73 +---------------- tests/unit/test_worker.py | 80 ++++++++++++++++++- 8 files changed, 171 insertions(+), 147 deletions(-) diff --git a/src/starlite_saqlalchemy/constants.py b/src/starlite_saqlalchemy/constants.py index 556603cc..f3e91e3b 100644 --- a/src/starlite_saqlalchemy/constants.py +++ b/src/starlite_saqlalchemy/constants.py @@ -2,10 +2,17 @@ from __future__ import annotations from importlib import import_module +from typing import TYPE_CHECKING, Any from starlite_saqlalchemy.settings import app from starlite_saqlalchemy.utils import case_insensitive_string_compare +if TYPE_CHECKING: + from collections.abc import MutableMapping + + from starlite_saqlalchemy.service import Service + + IS_TEST_ENVIRONMENT = case_insensitive_string_compare(app.ENVIRONMENT, app.TEST_ENVIRONMENT_NAME) """Flag indicating if the application is running in a test environment.""" @@ -38,3 +45,6 @@ IS_SENTRY_SDK_INSTALLED = False case "sqlalchemy": # pragma: no cover IS_SQLALCHEMY_INSTALLED = False + +SERVICE_OBJECT_IDENTITY_MAP: MutableMapping[str, type[Service[Any]]] = {} +"""Used by the worker to lookup methods for service object callbacks.""" diff --git a/src/starlite_saqlalchemy/init_plugin.py b/src/starlite_saqlalchemy/init_plugin.py index 95e27322..7126e155 100644 --- a/src/starlite_saqlalchemy/init_plugin.py +++ b/src/starlite_saqlalchemy/init_plugin.py @@ -65,7 +65,6 @@ def example_handler() -> dict: AppHealthCheck, HealthController, ) -from starlite_saqlalchemy.service import make_service_callback from starlite_saqlalchemy.type_encoders import type_encoders_map if TYPE_CHECKING: @@ -85,9 +84,7 @@ class PluginConfig(BaseModel): application. """ - worker_functions: list[Callable[..., Any] | tuple[str, Callable[..., Any]]] = [ - (make_service_callback.__qualname__, make_service_callback) - ] + worker_functions: list[Callable[..., Any] | tuple[str, Callable[..., Any]]] = [] """Queue worker functions.""" do_after_exception: bool = True """Configure after exception handler. @@ -398,7 +395,14 @@ def configure_worker(self, app_config: AppConfig) -> None: if self.config.do_worker: if not IS_SAQ_INSTALLED: raise MissingDependencyError(module="saq", config="worker") - from starlite_saqlalchemy.worker import create_worker_instance + from starlite_saqlalchemy.worker import ( + create_worker_instance, + make_service_callback, + ) + + self.config.worker_functions.append( + (make_service_callback.__qualname__, make_service_callback) + ) worker_kwargs: dict[str, Any] = {"functions": self.config.worker_functions} if self.config.do_logging: diff --git a/src/starlite_saqlalchemy/service/__init__.py b/src/starlite_saqlalchemy/service/__init__.py index db710c8d..0e3bf0c7 100644 --- a/src/starlite_saqlalchemy/service/__init__.py +++ b/src/starlite_saqlalchemy/service/__init__.py @@ -1,8 +1,5 @@ """Implementations for service object.""" -from .generic import Service, make_service_callback +from .generic import Service -__all__ = [ - "Service", - "make_service_callback", -] +__all__ = ["Service"] diff --git a/src/starlite_saqlalchemy/service/generic.py b/src/starlite_saqlalchemy/service/generic.py index 3d408d38..3390c2f5 100644 --- a/src/starlite_saqlalchemy/service/generic.py +++ b/src/starlite_saqlalchemy/service/generic.py @@ -5,38 +5,23 @@ from __future__ import annotations import contextlib -import inspect -import logging from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar -from starlite_saqlalchemy import constants, utils +from starlite_saqlalchemy import constants from starlite_saqlalchemy.exceptions import NotFoundError -if constants.IS_SAQ_INSTALLED: - from starlite_saqlalchemy.worker import default_job_config_dict, queue # isort:skip - from saq.job import Job - - if TYPE_CHECKING: from collections.abc import AsyncIterator - from saq.types import Context - - from starlite_saqlalchemy.worker import JobConfig - - -logger = logging.getLogger(__name__) T = TypeVar("T") ServiceT = TypeVar("ServiceT", bound="Service") -service_object_identity_map: dict[str, type[Service]] = {} - class Service(Generic[T]): """Generic Service object.""" - __id__: ClassVar[str] + __id__: ClassVar[str] = "starlite_saqlalchemy.service.generic.Service" def __init_subclass__(cls, *_: Any, **__: Any) -> None: """Map the service object to a unique identifier. @@ -48,7 +33,12 @@ def __init_subclass__(cls, *_: Any, **__: Any) -> None: path to the object. """ cls.__id__ = f"{cls.__module__}.{cls.__name__}" - service_object_identity_map[cls.__id__] = cls + # error: Argument of type "Type[Self@Service[T@Service]]" cannot be assigned to parameter + # "__value" of type "Type[Service[Any]]" in function "__setitem__" + # "Type[Service[T@Service]]" is incompatible with "Type[Service[Any]]" + # Type "Type[Self@Service[T@Service]]" cannot be assigned to type "Type[Service[Any]]" + # (reportGeneralTypeIssues) + constants.SERVICE_OBJECT_IDENTITY_MAP[cls.__id__] = cls # pyright:ignore # pylint:disable=unused-argument @@ -120,35 +110,6 @@ async def delete(self, id_: Any) -> T: """ raise NotFoundError - async def enqueue_background_task( - self, method_name: str, job_config: JobConfig | None = None, **kwargs: Any - ) -> None: - """Enqueue an async callback for the operation and data. - - Args: - method_name: Method on the service object that should be called by the async worker. - job_config: Configuration object to control the job that is enqueued. - **kwargs: Arguments to be passed to the method when called. Must be JSON serializable. - """ - module = inspect.getmodule(self) - if module is None: # pragma: no cover - logger.warning("Callback not enqueued, no module resolved for %s", self) - return - job_config_dict: dict[str, Any] - if job_config is None: - job_config_dict = default_job_config_dict - else: - job_config_dict = utils.dataclass_as_dict_shallow(job_config, exclude_none=True) - - kwargs["service_type_id"] = self.__id__ - kwargs["service_method_name"] = method_name - job = Job( - function=make_service_callback.__qualname__, - kwargs=kwargs, - **job_config_dict, - ) - await queue.enqueue(job) - @classmethod @contextlib.asynccontextmanager async def new(cls: type[ServiceT]) -> AsyncIterator[ServiceT]: @@ -158,20 +119,3 @@ async def new(cls: type[ServiceT]) -> AsyncIterator[ServiceT]: The service object instance. """ yield cls() - - -async def make_service_callback( - _ctx: Context, *, service_type_id: str, service_method_name: str, **kwargs: Any -) -> None: - """Make an async service callback. - - Args: - _ctx: the SAQ context - service_type_id: Value of `__id__` class var on service type. - service_method_name: Method to be called on the service object. - **kwargs: Unpacked into the service method call as keyword arguments. - """ - service_type = service_object_identity_map[service_type_id] - async with service_type.new() as service_object: - method = getattr(service_object, service_method_name) - await method(**kwargs) diff --git a/src/starlite_saqlalchemy/service/sqlalchemy.py b/src/starlite_saqlalchemy/service/sqlalchemy.py index 1b62723b..5ede194b 100644 --- a/src/starlite_saqlalchemy/service/sqlalchemy.py +++ b/src/starlite_saqlalchemy/service/sqlalchemy.py @@ -27,6 +27,8 @@ class RepositoryService(Service[ModelT], Generic[ModelT]): """Service object that operates on a repository object.""" + __id__ = "starlite_saqlalchemy.service.sqlalchemy.RepositoryService" + repository_type: type[AbstractRepository[ModelT]] def __init__(self, **repo_kwargs: Any) -> None: diff --git a/src/starlite_saqlalchemy/worker.py b/src/starlite_saqlalchemy/worker.py index de8f0305..ae5e86cc 100644 --- a/src/starlite_saqlalchemy/worker.py +++ b/src/starlite_saqlalchemy/worker.py @@ -3,6 +3,8 @@ import asyncio import dataclasses +import inspect +import logging from functools import partial from typing import TYPE_CHECKING, Any @@ -10,21 +12,29 @@ import saq from starlite.utils.serialization import default_serializer -from starlite_saqlalchemy import redis, settings, type_encoders, utils +from starlite_saqlalchemy import constants, redis, settings, type_encoders, utils if TYPE_CHECKING: from collections.abc import Awaitable, Callable, Collection from signal import Signals + from saq.types import Context + + from starlite_saqlalchemy.service import Service + __all__ = [ "JobConfig", "Queue", "Worker", "create_worker_instance", "default_job_config_dict", + "make_service_callback", + "enqueue_background_task_for_service", "queue", ] +logger = logging.getLogger(__name__) + encoder = msgspec.json.Encoder( enc_hook=partial(default_serializer, type_encoders=type_encoders.type_encoders_map) ) @@ -147,3 +157,51 @@ def create_worker_instance( The worker instance, instantiated with `functions`. """ return Worker(queue, functions, before_process=before_process, after_process=after_process) + + +async def make_service_callback( + _ctx: Context, *, service_type_id: str, service_method_name: str, **kwargs: Any +) -> None: + """Make an async service callback. + + Args: + _ctx: the SAQ context + service_type_id: Value of `__id__` class var on service type. + service_method_name: Method to be called on the service object. + **kwargs: Unpacked into the service method call as keyword arguments. + """ + service_type = constants.SERVICE_OBJECT_IDENTITY_MAP[service_type_id] + async with service_type.new() as service_object: + method = getattr(service_object, service_method_name) + await method(**kwargs) + + +async def enqueue_background_task_for_service( + service_obj: Service, method_name: str, job_config: JobConfig | None = None, **kwargs: Any +) -> None: + """Enqueue an async callback for the operation and data. + + Args: + service_obj: The Service instance that is requesting the callback. + method_name: Method on the service object that should be called by the async worker. + job_config: Configuration object to control the job that is enqueued. + **kwargs: Arguments to be passed to the method when called. Must be JSON serializable. + """ + module = inspect.getmodule(service_obj) + if module is None: # pragma: no cover + logger.warning("Callback not enqueued, no module resolved for %s", service_obj) + return + job_config_dict: dict[str, Any] + if job_config is None: + job_config_dict = default_job_config_dict + else: + job_config_dict = utils.dataclass_as_dict_shallow(job_config, exclude_none=True) + + kwargs["service_type_id"] = service_obj.__id__ + kwargs["service_method_name"] = method_name + job = saq.Job( + function=make_service_callback.__qualname__, + kwargs=kwargs, + **job_config_dict, + ) + await queue.enqueue(job) diff --git a/tests/unit/test_service.py b/tests/unit/test_service.py index c553616f..1b32ecd8 100644 --- a/tests/unit/test_service.py +++ b/tests/unit/test_service.py @@ -7,18 +7,14 @@ pytest.importorskip("sqlalchemy") from datetime import date -from typing import TYPE_CHECKING, Any -from unittest.mock import AsyncMock +from typing import TYPE_CHECKING from uuid import uuid4 -from saq import Job - -from starlite_saqlalchemy import db, service, worker +from starlite_saqlalchemy import service from starlite_saqlalchemy.exceptions import NotFoundError from tests.utils import domain if TYPE_CHECKING: - from pytest import MonkeyPatch from starlite_saqlalchemy.testing.generic_mock_repository import ( GenericMockRepository, @@ -92,71 +88,6 @@ async def test_service_delete() -> None: assert author is deleted -async def test_make_service_callback( - raw_authors: list[dict[str, Any]], monkeypatch: "MonkeyPatch" -) -> None: - """Tests loading and retrieval of service object types.""" - recv_cb_mock = AsyncMock() - monkeypatch.setattr(service.Service, "receive_callback", recv_cb_mock, raising=False) - await service.make_service_callback( - {}, - service_type_id="tests.utils.domain.authors.Service", - service_method_name="receive_callback", - raw_obj=raw_authors[0], - ) - recv_cb_mock.assert_called_once_with(raw_obj=raw_authors[0]) - - -async def test_make_service_callback_raises_runtime_error( - raw_authors: list[dict[str, Any]] -) -> None: - """Tests loading and retrieval of service object types.""" - with pytest.raises(KeyError): - await service.make_service_callback( - {}, - service_type_id="tests.utils.domain.LSKDFJ", - service_method_name="receive_callback", - raw_obj=raw_authors[0], - ) - - -async def test_enqueue_service_callback(monkeypatch: "MonkeyPatch") -> None: - """Tests that job enqueued with desired arguments.""" - enqueue_mock = AsyncMock() - monkeypatch.setattr(worker.queue, "enqueue", enqueue_mock) - service_instance = domain.authors.Service(session=db.async_session_factory()) - await service_instance.enqueue_background_task("receive_callback", raw_obj={"a": "b"}) - enqueue_mock.assert_called_once() - assert isinstance(enqueue_mock.mock_calls[0].args[0], Job) - job = enqueue_mock.mock_calls[0].args[0] - assert job.function == service.make_service_callback.__qualname__ - assert job.kwargs == { - "service_type_id": "tests.utils.domain.authors.Service", - "service_method_name": "receive_callback", - "raw_obj": {"a": "b"}, - } - - -async def test_enqueue_service_callback_with_custom_job_config(monkeypatch: "MonkeyPatch") -> None: - """Tests that job enqueued with desired arguments.""" - enqueue_mock = AsyncMock() - monkeypatch.setattr(worker.queue, "enqueue", enqueue_mock) - service_instance = domain.authors.Service(session=db.async_session_factory()) - await service_instance.enqueue_background_task( - "receive_callback", job_config=worker.JobConfig(timeout=999), raw_obj={"a": "b"} - ) - enqueue_mock.assert_called_once() - assert isinstance(enqueue_mock.mock_calls[0].args[0], Job) - job = enqueue_mock.mock_calls[0].args[0] - assert job.function == service.make_service_callback.__qualname__ - assert job.timeout == 999 - assert job.kwargs == { - "service_type_id": "tests.utils.domain.authors.Service", - "service_method_name": "receive_callback", - "raw_obj": {"a": "b"}, - } - - async def test_service_new_context_manager() -> None: """Simple test of `Service.new()` context manager behavior.""" async with service.Service[domain.authors.Author].new() as service_obj: diff --git a/tests/unit/test_worker.py b/tests/unit/test_worker.py index a8ecc397..6f8fec50 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/test_worker.py @@ -2,14 +2,22 @@ # pylint: disable=wrong-import-position from __future__ import annotations +from typing import TYPE_CHECKING, Any +from unittest.mock import AsyncMock + import pytest pytest.importorskip("saq") from asyncpg.pgproto import pgproto +from saq import Job -from starlite_saqlalchemy import worker +from starlite_saqlalchemy import service, worker from tests.utils.domain.authors import Author, ReadDTO +if TYPE_CHECKING: + + from pytest import MonkeyPatch + def test_worker_decoder_handles_pgproto_uuid() -> None: """Test that the decoder can handle pgproto.UUID instances.""" @@ -26,3 +34,73 @@ def test_worker_decoder_handles_pydantic_models(authors: list[Author]) -> None: encoded == b'{"id":"97108ac1-ffcb-411d-8b1e-d9183399f63b","created":"0001-01-01T00:00:00","updated":"0001-01-01T00:00:00","name":"Agatha Christie","dob":"1890-09-15"}' ) + + +async def test_make_service_callback( + raw_authors: list[dict[str, Any]], monkeypatch: MonkeyPatch +) -> None: + """Tests loading and retrieval of service object types.""" + recv_cb_mock = AsyncMock() + monkeypatch.setattr(service.Service, "receive_callback", recv_cb_mock, raising=False) + await worker.make_service_callback( + {}, + service_type_id="tests.utils.domain.authors.Service", + service_method_name="receive_callback", + raw_obj=raw_authors[0], + ) + recv_cb_mock.assert_called_once_with(raw_obj=raw_authors[0]) + + +async def test_make_service_callback_raises_runtime_error( + raw_authors: list[dict[str, Any]] +) -> None: + """Tests loading and retrieval of service object types.""" + with pytest.raises(KeyError): + await worker.make_service_callback( + {}, + service_type_id="tests.utils.domain.LSKDFJ", + service_method_name="receive_callback", + raw_obj=raw_authors[0], + ) + + +async def test_enqueue_service_callback(monkeypatch: "MonkeyPatch") -> None: + """Tests that job enqueued with desired arguments.""" + enqueue_mock = AsyncMock() + monkeypatch.setattr(worker.queue, "enqueue", enqueue_mock) + service_instance = service.Service[Any]() + await worker.enqueue_background_task_for_service( + service_instance, "receive_callback", raw_obj={"a": "b"} + ) + enqueue_mock.assert_called_once() + assert isinstance(enqueue_mock.mock_calls[0].args[0], Job) + job = enqueue_mock.mock_calls[0].args[0] + assert job.function == worker.make_service_callback.__qualname__ + assert job.kwargs == { + "service_type_id": "starlite_saqlalchemy.service.generic.Service", + "service_method_name": "receive_callback", + "raw_obj": {"a": "b"}, + } + + +async def test_enqueue_service_callback_with_custom_job_config(monkeypatch: "MonkeyPatch") -> None: + """Tests that job enqueued with desired arguments.""" + enqueue_mock = AsyncMock() + monkeypatch.setattr(worker.queue, "enqueue", enqueue_mock) + service_instance = service.Service[Any]() + await worker.enqueue_background_task_for_service( + service_instance, + "receive_callback", + job_config=worker.JobConfig(timeout=999), + raw_obj={"a": "b"}, + ) + enqueue_mock.assert_called_once() + assert isinstance(enqueue_mock.mock_calls[0].args[0], Job) + job = enqueue_mock.mock_calls[0].args[0] + assert job.function == worker.make_service_callback.__qualname__ + assert job.timeout == 999 + assert job.kwargs == { + "service_type_id": "starlite_saqlalchemy.service.generic.Service", + "service_method_name": "receive_callback", + "raw_obj": {"a": "b"}, + } From 4ad9cb3c3c44384ed56d56c994d1fc5fa821e673 Mon Sep 17 00:00:00 2001 From: Peter Schutt Date: Wed, 18 Jan 2023 21:10:10 +1000 Subject: [PATCH 26/30] refactor: default_factory and validator for dependency-based gates Default values for `PluginConfig.do_cache` et al. are based on the `IS_XXX_INSTALLED` constants. We use `default_factory` so we can patch the module values in tests. Use `@validator` to test for missing dependency when gates are explicitly set. --- src/starlite_saqlalchemy/exceptions.py | 2 +- src/starlite_saqlalchemy/init_plugin.py | 54 +++++++++++++++--------- tests/unit/test_init_plugin_no_extras.py | 25 ++++------- tests/utils/app.py | 11 +---- 4 files changed, 47 insertions(+), 45 deletions(-) diff --git a/src/starlite_saqlalchemy/exceptions.py b/src/starlite_saqlalchemy/exceptions.py index 84ea1181..8f25be7a 100644 --- a/src/starlite_saqlalchemy/exceptions.py +++ b/src/starlite_saqlalchemy/exceptions.py @@ -56,7 +56,7 @@ class AuthorizationError(StarliteSaqlalchemyClientError): """A user tried to do something they shouldn't have.""" -class MissingDependencyError(StarliteSaqlalchemyError): +class MissingDependencyError(StarliteSaqlalchemyError, ValueError): """A required dependency is not installed.""" def __init__(self, module: str, config: str | None = None) -> None: diff --git a/src/starlite_saqlalchemy/init_plugin.py b/src/starlite_saqlalchemy/init_plugin.py index 7126e155..4c28efce 100644 --- a/src/starlite_saqlalchemy/init_plugin.py +++ b/src/starlite_saqlalchemy/init_plugin.py @@ -33,7 +33,7 @@ def example_handler() -> dict: from collections.abc import Callable, Sequence # noqa: TC003 from typing import TYPE_CHECKING, Any, TypeVar -from pydantic import BaseModel +from pydantic import BaseModel, Field, validator from starlite.app import DEFAULT_CACHE_CONFIG, DEFAULT_OPENAPI_CONFIG from starlite.types import TypeEncodersMap # noqa: TC002 from structlog.types import Processor # noqa: TC002 @@ -92,7 +92,7 @@ class PluginConfig(BaseModel): Add the hook handler to [`AppConfig.after_exception`][starlite.config.app.AppConfig.after_exception]. """ - do_cache: bool = False + do_cache: bool = Field(default_factory=lambda: IS_REDIS_INSTALLED) """Configure redis cache backend. Add configuration for the redis-backed cache to @@ -134,7 +134,10 @@ class PluginConfig(BaseModel): Set the OpenAPI config object to [`AppConfig.openapi_config`][starlite.config.app.AppConfig.openapi_config]. """ - do_sentry: bool | None = None + do_sentry: bool = Field( + default_factory=lambda: IS_SENTRY_SDK_INSTALLED + and not (IS_LOCAL_ENVIRONMENT or IS_TEST_ENVIRONMENT) + ) """Configure sentry. Configure the application to initialize Sentry on startup. Adds a handler to @@ -146,7 +149,7 @@ class PluginConfig(BaseModel): Allow the plugin to set the starlite `debug` parameter. Parameter set to value of [`AppConfig.debug`][starlite_saqlalchemy.settings.AppSettings.DEBUG]. """ - do_sqlalchemy_plugin: bool = False + do_sqlalchemy_plugin: bool = Field(default_factory=lambda: IS_SQLALCHEMY_INSTALLED) """Configure SQLAlchemy plugin. Set the SQLAlchemy plugin on the application. Adds the plugin to @@ -155,7 +158,7 @@ class PluginConfig(BaseModel): do_type_encoders: bool = True """Configure custom type encoders on the app.""" - do_worker: bool = False + do_worker: bool = Field(default_factory=lambda: IS_SAQ_INSTALLED) """Configure the async worker on the application. This action instantiates a worker instance and sets handlers for @@ -170,6 +173,31 @@ class PluginConfig(BaseModel): type_encoders: TypeEncodersMap = type_encoders_map """Map of type to serializer callable.""" health_checks: list[type[AbstractHealthCheck]] = [AppHealthCheck] + """Checks executed on calls to health route handler.""" + + @validator("do_cache") + def _validate_do_cache(cls, value: bool) -> bool: + if value is True and not IS_REDIS_INSTALLED: + raise MissingDependencyError(module="redis", config="redis") + return value + + @validator("do_sentry") + def _validate_do_sentry(cls, value: bool) -> bool: + if value is True and not IS_SENTRY_SDK_INSTALLED: + raise MissingDependencyError(module="sentry_sdk", config="sentry") + return value + + @validator("do_sqlalchemy_plugin") + def _validate_do_sqlalchemy_plugin(cls, value: bool) -> bool: + if value is True and not IS_SQLALCHEMY_INSTALLED: + raise MissingDependencyError(module="sqlalchemy", config="sqlalchemy_plugin") + return value + + @validator("do_worker") + def _validate_do_worker(cls, value: bool) -> bool: + if value is True and not IS_SAQ_INSTALLED: + raise MissingDependencyError(module="saq", config="worker") + return value class ConfigureApp: @@ -207,6 +235,7 @@ def __call__(self, app_config: AppConfig) -> AppConfig: self.configure_sqlalchemy_plugin(app_config) self.configure_type_encoders(app_config) self.configure_worker(app_config) + # health check is explicitly configured last self.configure_health_check(app_config) app_config.before_startup = lifespan.before_startup_handler @@ -237,8 +266,6 @@ def configure_cache(self, app_config: AppConfig) -> None: app_config: The Starlite application config object. """ if self.config.do_cache and app_config.cache_config == DEFAULT_CACHE_CONFIG: - if not IS_REDIS_INSTALLED: - raise MissingDependencyError(module="redis", config="redis") from starlite_saqlalchemy import cache app_config.cache_config = cache.config @@ -340,14 +367,7 @@ def configure_sentry(self, app_config: AppConfig) -> None: Args: app_config: The Starlite application config object. """ - do_sentry = ( - self.config.do_sentry - if self.config.do_sentry is not None - else not (IS_LOCAL_ENVIRONMENT or IS_TEST_ENVIRONMENT) - ) - if do_sentry: - if not IS_SENTRY_SDK_INSTALLED: - raise MissingDependencyError(module="sentry_sdk", config="sentry") + if self.config.do_sentry: from starlite_saqlalchemy import sentry app_config.on_startup.append(sentry.configure) @@ -362,8 +382,6 @@ def configure_sqlalchemy_plugin(self, app_config: AppConfig) -> None: app_config: The Starlite application config object. """ if self.config.do_sqlalchemy_plugin: - if not IS_SQLALCHEMY_INSTALLED: - raise MissingDependencyError(module="sqlalchemy") from starlite.plugins.sql_alchemy import SQLAlchemyPlugin from starlite_saqlalchemy.sqlalchemy_plugin import ( @@ -393,8 +411,6 @@ def configure_worker(self, app_config: AppConfig) -> None: app_config: The Starlite application config object. """ if self.config.do_worker: - if not IS_SAQ_INSTALLED: - raise MissingDependencyError(module="saq", config="worker") from starlite_saqlalchemy.worker import ( create_worker_instance, make_service_callback, diff --git a/tests/unit/test_init_plugin_no_extras.py b/tests/unit/test_init_plugin_no_extras.py index cb1ff7be..5c682c54 100644 --- a/tests/unit/test_init_plugin_no_extras.py +++ b/tests/unit/test_init_plugin_no_extras.py @@ -1,10 +1,9 @@ """Tests for init_plugin.py when no extra dependencies are installed.""" import pytest -from starlite import Starlite +from pydantic import ValidationError from starlite_saqlalchemy import constants, init_plugin -from starlite_saqlalchemy.exceptions import MissingDependencyError SKIP = any( [ @@ -18,15 +17,15 @@ @pytest.mark.skipif(SKIP, reason="test will only run if no extras are installed") @pytest.mark.parametrize( - ("enabled_config", "error"), + ("enabled_config", "error_pattern"), [ - ("do_cache", r"^.*\'redis\' is not installed.*$"), - ("do_sentry", r"^.*\'sentry_sdk\' is not installed.*$"), - ("do_worker", r"^.*\'saq\' is not installed.*$"), - ("do_sqlalchemy_plugin", r"^.*\'sqlalchemy\' is not installed.*$"), + ("do_cache", r"\'redis\' is not installed."), + ("do_sentry", r"\'sentry_sdk\' is not installed."), + ("do_worker", r"\'saq\' is not installed."), + ("do_sqlalchemy_plugin", r"\'sqlalchemy\' is not installed."), ], ) -def test_extra_dependencies_not_installed(enabled_config: str, error: str) -> None: +def test_extra_dependencies_not_installed(enabled_config: str, error_pattern: str) -> None: """Tests that the plugin test required dependencies for switches needing them.""" kwargs = { @@ -45,11 +44,5 @@ def test_extra_dependencies_not_installed(enabled_config: str, error: str) -> No "do_worker": False, **{enabled_config: True}, } - config = init_plugin.PluginConfig(**kwargs) - - with pytest.raises(MissingDependencyError, match=error): - Starlite( - route_handlers=[], - openapi_config=None, - on_app_init=[init_plugin.ConfigureApp(config=config)], - ) + with pytest.raises(ValidationError, match=error_pattern): + init_plugin.PluginConfig(**kwargs) diff --git a/tests/utils/app.py b/tests/utils/app.py index 3c050849..81750f7b 100644 --- a/tests/utils/app.py +++ b/tests/utils/app.py @@ -3,18 +3,11 @@ from starlite import Starlite -from starlite_saqlalchemy import ConfigureApp, PluginConfig +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( - config=PluginConfig(do_sqlalchemy_plugin=True, do_worker=True, do_cache=True) - ) - ], - ) + return Starlite(route_handlers=[controllers.create_router()], on_app_init=[ConfigureApp()]) From da3e4e6188c8a196b0ed3387e7829ee627ee02e7 Mon Sep 17 00:00:00 2001 From: Peter Schutt Date: Wed, 18 Jan 2023 21:31:47 +1000 Subject: [PATCH 27/30] refactor: remove more module scoped conditional imports. Import from within the test/fixture where appropriate. --- tests/unit/conftest.py | 11 +++++++---- tests/unit/test_init_plugin.py | 11 +++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index f5213910..f0780f05 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -10,11 +10,9 @@ from starlite_saqlalchemy import constants -if constants.IS_SAQ_INSTALLED: - from saq.job import Job - if TYPE_CHECKING: + from saq.job import Job from starlite import Starlite from starlite.types import HTTPResponseBodyEvent, HTTPResponseStartEvent, HTTPScope @@ -124,9 +122,14 @@ def http_scope(app: Starlite) -> HTTPScope: } -@pytest.fixture(autouse=constants.IS_SAQ_INSTALLED) +@pytest.fixture() def job() -> Job: """SAQ Job instance.""" + if not constants.IS_SAQ_INSTALLED: + pytest.skip("SAQ not installed") + + from saq.job import Job + return Job(function="whatever", kwargs={"a": "b"}) diff --git a/tests/unit/test_init_plugin.py b/tests/unit/test_init_plugin.py index 4cb4a715..c3545c53 100644 --- a/tests/unit/test_init_plugin.py +++ b/tests/unit/test_init_plugin.py @@ -1,4 +1,5 @@ """Tests for init_plugin.py.""" +# pylint:disable=import-outside-toplevel from __future__ import annotations from typing import TYPE_CHECKING @@ -11,12 +12,6 @@ from starlite_saqlalchemy import init_plugin from starlite_saqlalchemy.constants import IS_SAQ_INSTALLED, IS_SENTRY_SDK_INSTALLED -if IS_SENTRY_SDK_INSTALLED: - from starlite_saqlalchemy import sentry - -if IS_SAQ_INSTALLED: - from starlite_saqlalchemy import worker - if TYPE_CHECKING: from typing import Any @@ -66,6 +61,8 @@ def test_config_switches() -> None: def test_do_worker_but_not_logging(monkeypatch: MonkeyPatch) -> None: """Tests branch where we can have the worker enabled, but logging disabled.""" + from starlite_saqlalchemy import worker + mock = MagicMock() monkeypatch.setattr(worker, "create_worker_instance", mock) config = init_plugin.PluginConfig(do_logging=False, do_worker=True) @@ -98,6 +95,8 @@ def test_ensure_list(in_: Any, out: Any) -> None: def test_sentry_environment_gate(env: str, exp: bool, monkeypatch: MonkeyPatch) -> None: """Test that the sentry integration is configured under different environment names.""" + from starlite_saqlalchemy import sentry + monkeypatch.setattr(init_plugin, "IS_LOCAL_ENVIRONMENT", env == "local") monkeypatch.setattr(init_plugin, "IS_TEST_ENVIRONMENT", env == "test") app = Starlite(route_handlers=[], on_app_init=[init_plugin.ConfigureApp()]) From 02d3a880c5b98a7f2ae9980fd43910fef26ad3eb Mon Sep 17 00:00:00 2001 From: Peter Schutt Date: Thu, 19 Jan 2023 08:46:11 +1000 Subject: [PATCH 28/30] refactor: organise tests according to dependencies (#257) * refactor: organise tests according to dependencies - organises tests according to their dependencies. - removes unnecessary autouse fixtures - removes gated imports at the module level - uses `--ignore` to exclude redis/sqlalchemy/sentry tests from no-extras tests * refactor: use `test_ignore_glob` pytest global This lets us skip test collection for a whole sub-package of tests if a dependency is unavailable. * refactor: log tests can run without sqlalchemy. This was only necessary due to the dependency on `tests.utils.controllers` in the `http_scope` fixture, which has now been removed. * refactor: don't need to skipif saq log tests As the log tests depend on `job` fixture which will automatically skip if SAQ not available. * refactor: remaining fixtures/tests requiring sqlalchemy moved into sqlalchemy directory. --- mypy.ini | 4 +- tests/conftest.py | 27 ------ tests/integration/conftest.py | 15 ++- tests/unit/__init__.py | 0 tests/unit/conftest.py | 68 ++----------- tests/unit/redis/conftest.py | 4 + tests/unit/{ => redis}/test_cache.py | 4 - tests/unit/redis/worker/conftest.py | 4 + tests/unit/{ => redis/worker}/test_worker.py | 20 ++-- tests/unit/repository/__init__.py | 0 tests/unit/sentry/conftest.py | 4 + tests/unit/{ => sentry}/test_sentry.py | 7 +- tests/unit/sqlalchemy/conftest.py | 96 +++++++++++++++++++ .../{ => sqlalchemy}/repository/test_abc.py | 7 +- .../test_generic_mock_repository.py | 69 +++++++++++++ .../repository/test_sqlalchemy.py | 7 +- tests/unit/{ => sqlalchemy}/test_db.py | 7 +- tests/unit/{ => sqlalchemy}/test_dto.py | 7 +- tests/unit/{ => sqlalchemy}/test_orm.py | 4 - tests/unit/{ => sqlalchemy}/test_service.py | 7 +- tests/unit/{ => sqlalchemy}/test_testing.py | 70 +------------- tests/unit/test_log.py | 14 --- 22 files changed, 220 insertions(+), 225 deletions(-) delete mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/redis/conftest.py rename tests/unit/{ => redis}/test_cache.py (89%) create mode 100644 tests/unit/redis/worker/conftest.py rename tests/unit/{ => redis/worker}/test_worker.py (88%) delete mode 100644 tests/unit/repository/__init__.py create mode 100644 tests/unit/sentry/conftest.py rename tests/unit/{ => sentry}/test_sentry.py (89%) create mode 100644 tests/unit/sqlalchemy/conftest.py rename tests/unit/{ => sqlalchemy}/repository/test_abc.py (94%) create mode 100644 tests/unit/sqlalchemy/repository/test_generic_mock_repository.py rename tests/unit/{ => sqlalchemy}/repository/test_sqlalchemy.py (98%) rename tests/unit/{ => sqlalchemy}/test_db.py (83%) rename tests/unit/{ => sqlalchemy}/test_dto.py (98%) rename tests/unit/{ => sqlalchemy}/test_orm.py (82%) rename tests/unit/{ => sqlalchemy}/test_service.py (97%) rename tests/unit/{ => sqlalchemy}/test_testing.py (69%) diff --git a/mypy.ini b/mypy.ini index d2d74a99..4080fb50 100644 --- a/mypy.ini +++ b/mypy.ini @@ -18,9 +18,9 @@ warn_unused_ignores = True [mypy-tests.*] disallow_untyped_decorators = False -[mypy-tests.unit.test_dto] +[mypy-tests.unit.sqlalchemy.test_dto] # for the declarative base fixture disable_error_code = valid-type,misc -[mypy-tests.unit.repository.test_sqlalchemy] +[mypy-tests.unit.sqlalchemy.repository.test_sqlalchemy] disable_error_code = attr-defined diff --git a/tests/conftest.py b/tests/conftest.py index 8ee50f09..87a87466 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,6 @@ from uuid import uuid4 import pytest -from asyncpg.pgproto import pgproto if TYPE_CHECKING: from collections.abc import Callable @@ -18,8 +17,6 @@ from pytest import MonkeyPatch - from tests.utils.domain import authors, books - # Ensure that pytest_dotenv is loaded before # so pytest_starlite_saqlalchemy uses correct env values pytest_plugins = ("pytest_dotenv", "pytest_starlite_saqlalchemy.plugin") @@ -47,18 +44,6 @@ def fx_raw_authors() -> list[dict[str, Any]]: ] -@pytest.fixture(name="authors") -def fx_authors(raw_authors: list[dict[str, Any]]) -> list[authors.Author]: - """Collection of parsed Author models.""" - from tests.utils.domain import authors - - mapped_authors = [authors.ReadDTO(**raw).to_mapped() for raw in raw_authors] - # convert these to pgproto UUIDs as that is what we get back from sqlalchemy - for author in mapped_authors: - author.id = pgproto.UUID(str(author.id)) - return mapped_authors - - @pytest.fixture(name="raw_books") def fx_raw_books(raw_authors: list[dict[str, Any]]) -> list[dict[str, Any]]: """Unstructured book representations.""" @@ -74,18 +59,6 @@ def fx_raw_books(raw_authors: list[dict[str, Any]]) -> list[dict[str, Any]]: ] -@pytest.fixture(name="books") -def fx_books(raw_books: list[dict[str, Any]]) -> list[books.Book]: - """Collection of parsed Book models.""" - from tests.utils.domain import books - - mapped_books = [books.ReadDTO(**raw).to_mapped() for raw in raw_books] - # convert these to pgproto UUIDs as that is what we get back from sqlalchemy - for book in mapped_books: - book.id = pgproto.UUID(str(book.id)) - return mapped_books - - @pytest.fixture(name="create_module") def fx_create_module(tmp_path: Path, monkeypatch: MonkeyPatch) -> Callable[[str], ModuleType]: """Utility fixture for dynamic module creation.""" diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index ad1d9ece..3f2b3d98 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -4,6 +4,7 @@ import asyncio import timeit from asyncio import AbstractEventLoop, get_event_loop_policy +from datetime import datetime from pathlib import Path from typing import TYPE_CHECKING @@ -28,8 +29,6 @@ from pytest_docker.plugin import Services # type:ignore[import] from starlite import Starlite - from tests.utils.domain.authors import Author - here = Path(__file__).parent @@ -167,19 +166,25 @@ async def fx_engine(docker_ip: str) -> AsyncEngine: @pytest.fixture(autouse=True) -async def _seed_db(engine: AsyncEngine, authors: list[Author]) -> AsyncIterator[None]: +async def _seed_db(engine: AsyncEngine, raw_authors: list[dict[str, Any]]) -> AsyncIterator[None]: """Populate test database with. Args: engine: The SQLAlchemy engine instance. """ - # get models into metadata metadata = db.orm.Base.registry.metadata author_table = metadata.tables["author"] async with engine.begin() as conn: await conn.run_sync(metadata.create_all) + + # convert date/time strings to dt objects. + for raw_author in raw_authors: + raw_author["dob"] = datetime.strptime(raw_author["dob"], "%Y-%m-%d") + raw_author["created"] = datetime.strptime(raw_author["created"], "%Y-%m-%dT%H:%M:%S") + raw_author["updated"] = datetime.strptime(raw_author["updated"], "%Y-%m-%dT%H:%M:%S") + async with engine.begin() as conn: - await conn.execute(author_table.insert(), [vars(item) for item in authors]) + await conn.execute(author_table.insert(), raw_authors) yield async with engine.begin() as conn: await conn.run_sync(metadata.drop_all) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index f0780f05..e4dd2c6d 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING import pytest +from starlite import get from starlite.datastructures import State from starlite.enums import ScopeType @@ -16,66 +17,6 @@ from starlite import Starlite from starlite.types import HTTPResponseBodyEvent, HTTPResponseStartEvent, HTTPScope - from starlite_saqlalchemy.testing.generic_mock_repository import ( - GenericMockRepository, - ) - from tests.utils.domain.authors import Author - from tests.utils.domain.books import Book - - -@pytest.fixture(name="author_repository_type", autouse=constants.IS_SQLALCHEMY_INSTALLED) -def fx_author_repository_type( - authors: list[Author], monkeypatch: pytest.MonkeyPatch -) -> type[GenericMockRepository[Author]]: - """Mock Author repository, pre-seeded with collection data.""" - from starlite_saqlalchemy.testing.generic_mock_repository import ( - GenericMockRepository, - ) - from tests.utils.domain.authors import Author - from tests.utils.domain.authors import Service as AuthorService - - repo = GenericMockRepository[Author] - repo.seed_collection(authors) - monkeypatch.setattr(AuthorService, "repository_type", repo) - return repo - - -@pytest.fixture(name="author_repository", autouse=constants.IS_SQLALCHEMY_INSTALLED) -def fx_author_repository( - author_repository_type: type[GenericMockRepository[Author]], -) -> GenericMockRepository[Author]: - """Mock Author repository instance.""" - return author_repository_type() - - -@pytest.fixture(name="book_repository_type", autouse=constants.IS_SQLALCHEMY_INSTALLED) -def fx_book_repository_type( - books: list[Book], monkeypatch: pytest.MonkeyPatch -) -> type[GenericMockRepository[Book]]: - """Mock Book repository, pre-seeded with collection data.""" - from starlite_saqlalchemy.testing.generic_mock_repository import ( - GenericMockRepository, - ) - from tests.utils.domain.books import Book - from tests.utils.domain.books import Service as BookService - - class BookRepository(GenericMockRepository[Book]): - """Mock book repo.""" - - model_type = Book - - BookRepository.seed_collection(books) - monkeypatch.setattr(BookService, "repository_type", BookRepository) - return BookRepository - - -@pytest.fixture(name="book_repository", autouse=constants.IS_SQLALCHEMY_INSTALLED) -def fx_book_repository( - book_repository_type: type[GenericMockRepository[Book]], -) -> GenericMockRepository[Book]: - """Mock Book repo instance.""" - return book_repository_type() - @pytest.fixture() def http_response_start() -> HTTPResponseStartEvent: @@ -96,7 +37,10 @@ def http_response_body() -> HTTPResponseBodyEvent: @pytest.fixture() def http_scope(app: Starlite) -> HTTPScope: """Minimal ASGI HTTP connection scope.""" - from ..utils import controllers + + @get() + def handler() -> None: + ... return { "headers": [], @@ -111,7 +55,7 @@ def http_scope(app: Starlite) -> HTTPScope: "query_string": b"", "raw_path": b"/wherever", "root_path": "/", - "route_handler": controllers.get_author, + "route_handler": handler, "scheme": "http", "server": None, "session": {}, diff --git a/tests/unit/redis/conftest.py b/tests/unit/redis/conftest.py new file mode 100644 index 00000000..6ddee5a9 --- /dev/null +++ b/tests/unit/redis/conftest.py @@ -0,0 +1,4 @@ +from starlite_saqlalchemy.constants import IS_REDIS_INSTALLED + +if not IS_REDIS_INSTALLED: + collect_ignore_glob = ["*"] diff --git a/tests/unit/test_cache.py b/tests/unit/redis/test_cache.py similarity index 89% rename from tests/unit/test_cache.py rename to tests/unit/redis/test_cache.py index b7f3d9e6..d5991909 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/redis/test_cache.py @@ -1,9 +1,5 @@ """Test for the application cache configurations.""" -# pylint: disable=wrong-import-position import pytest - -pytest.importorskip("redis") - from starlite.config.cache import default_cache_key_builder from starlite.testing import RequestFactory diff --git a/tests/unit/redis/worker/conftest.py b/tests/unit/redis/worker/conftest.py new file mode 100644 index 00000000..fc0c43a3 --- /dev/null +++ b/tests/unit/redis/worker/conftest.py @@ -0,0 +1,4 @@ +from starlite_saqlalchemy.constants import IS_SAQ_INSTALLED + +if not IS_SAQ_INSTALLED: + collect_ignore_glob = ["*"] diff --git a/tests/unit/test_worker.py b/tests/unit/redis/worker/test_worker.py similarity index 88% rename from tests/unit/test_worker.py rename to tests/unit/redis/worker/test_worker.py index 6f8fec50..c0ce9e95 100644 --- a/tests/unit/test_worker.py +++ b/tests/unit/redis/worker/test_worker.py @@ -1,18 +1,15 @@ """Tests for the SAQ async worker functionality.""" -# pylint: disable=wrong-import-position from __future__ import annotations from typing import TYPE_CHECKING, Any from unittest.mock import AsyncMock import pytest - -pytest.importorskip("saq") from asyncpg.pgproto import pgproto +from pydantic import BaseModel from saq import Job from starlite_saqlalchemy import service, worker -from tests.utils.domain.authors import Author, ReadDTO if TYPE_CHECKING: @@ -26,14 +23,17 @@ def test_worker_decoder_handles_pgproto_uuid() -> None: assert encoded == b'"0448bde2-7c69-4e6b-9c03-7b217e3b563d"' -def test_worker_decoder_handles_pydantic_models(authors: list[Author]) -> None: +def test_worker_decoder_handles_pydantic_models() -> None: """Test that the decoder we use for SAQ will encode a pydantic model.""" - pydantic_model = ReadDTO.from_orm(authors[0]) + + class Model(BaseModel): + a: str + b: int + c: float + + pydantic_model = Model(a="a", b=1, c=2.34) encoded = worker.encoder.encode(pydantic_model) - assert ( - encoded - == b'{"id":"97108ac1-ffcb-411d-8b1e-d9183399f63b","created":"0001-01-01T00:00:00","updated":"0001-01-01T00:00:00","name":"Agatha Christie","dob":"1890-09-15"}' - ) + assert encoded == b'{"a":"a","b":1,"c":2.34}' async def test_make_service_callback( diff --git a/tests/unit/repository/__init__.py b/tests/unit/repository/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/unit/sentry/conftest.py b/tests/unit/sentry/conftest.py new file mode 100644 index 00000000..3c91aba5 --- /dev/null +++ b/tests/unit/sentry/conftest.py @@ -0,0 +1,4 @@ +from starlite_saqlalchemy.constants import IS_SENTRY_SDK_INSTALLED + +if not IS_SENTRY_SDK_INSTALLED: + collect_ignore_glob = ["*"] diff --git a/tests/unit/test_sentry.py b/tests/unit/sentry/test_sentry.py similarity index 89% rename from tests/unit/test_sentry.py rename to tests/unit/sentry/test_sentry.py index 9b4b2a6f..90cdb353 100644 --- a/tests/unit/test_sentry.py +++ b/tests/unit/sentry/test_sentry.py @@ -1,11 +1,8 @@ """Tests for sentry integration.""" -# pylint: disable=wrong-import-position,wrong-import-order -import pytest - -pytest.importorskip("sentry_sdk") - from typing import TYPE_CHECKING +import pytest + from starlite_saqlalchemy import settings from starlite_saqlalchemy.sentry import SamplingContext, sentry_traces_sampler diff --git a/tests/unit/sqlalchemy/conftest.py b/tests/unit/sqlalchemy/conftest.py new file mode 100644 index 00000000..7b03fcb2 --- /dev/null +++ b/tests/unit/sqlalchemy/conftest.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +from asyncpg.pgproto import pgproto + +from starlite_saqlalchemy.constants import IS_SQLALCHEMY_INSTALLED + +if TYPE_CHECKING: + from typing import Any + + from starlite_saqlalchemy.testing.generic_mock_repository import ( + GenericMockRepository, + ) + from tests.utils.domain.authors import Author + from tests.utils.domain.books import Book + +if not IS_SQLALCHEMY_INSTALLED: + collect_ignore_glob = ["*"] + + +@pytest.fixture(name="authors") +def fx_authors(raw_authors: list[dict[str, Any]]) -> list[Author]: + """Collection of parsed Author models.""" + from tests.utils.domain import authors + + mapped_authors = [authors.ReadDTO(**raw).to_mapped() for raw in raw_authors] + # convert these to pgproto UUIDs as that is what we get back from sqlalchemy + for author in mapped_authors: + author.id = pgproto.UUID(str(author.id)) + return mapped_authors + + +@pytest.fixture(name="books") +def fx_books(raw_books: list[dict[str, Any]]) -> list[Book]: + """Collection of parsed Book models.""" + from tests.utils.domain import books + + mapped_books = [books.ReadDTO(**raw).to_mapped() for raw in raw_books] + # convert these to pgproto UUIDs as that is what we get back from sqlalchemy + for book in mapped_books: + book.id = pgproto.UUID(str(book.id)) + return mapped_books + + +@pytest.fixture(name="author_repository_type") +def fx_author_repository_type( + authors: list[Author], monkeypatch: pytest.MonkeyPatch +) -> type[GenericMockRepository[Author]]: + from starlite_saqlalchemy.testing.generic_mock_repository import ( + GenericMockRepository, + ) + from tests.utils.domain.authors import Author, Service + + """Mock Author repository, pre-seeded with collection data.""" + repo = GenericMockRepository[Author] + repo.seed_collection(authors) + monkeypatch.setattr(Service, "repository_type", repo) + return repo + + +@pytest.fixture(name="author_repository") +def fx_author_repository( + author_repository_type: type[GenericMockRepository[Author]], +) -> GenericMockRepository[Author]: + """Mock Author repository instance.""" + return author_repository_type() + + +@pytest.fixture(name="book_repository_type") +def fx_book_repository_type( + books: list[Book], monkeypatch: pytest.MonkeyPatch +) -> type[GenericMockRepository[Book]]: + """Mock Book repository, pre-seeded with collection data.""" + from starlite_saqlalchemy.testing.generic_mock_repository import ( + GenericMockRepository, + ) + from tests.utils.domain.books import Book, Service + + class BookRepository(GenericMockRepository[Book]): + """Mock book repo.""" + + model_type = Book + + BookRepository.seed_collection(books) + monkeypatch.setattr(Service, "repository_type", BookRepository) + return BookRepository + + +@pytest.fixture(name="book_repository") +def fx_book_repository( + book_repository_type: type[GenericMockRepository[Book]], +) -> GenericMockRepository[Book]: + """Mock Book repo instance.""" + return book_repository_type() diff --git a/tests/unit/repository/test_abc.py b/tests/unit/sqlalchemy/repository/test_abc.py similarity index 94% rename from tests/unit/repository/test_abc.py rename to tests/unit/sqlalchemy/repository/test_abc.py index 7fb8f7d1..996d1b6f 100644 --- a/tests/unit/repository/test_abc.py +++ b/tests/unit/sqlalchemy/repository/test_abc.py @@ -1,14 +1,11 @@ """Tests for the repository base class.""" -# pylint: disable=wrong-import-position,wrong-import-order from __future__ import annotations -import pytest - -pytest.importorskip("sqlalchemy") - from typing import TYPE_CHECKING from unittest.mock import MagicMock +import pytest + from starlite_saqlalchemy.exceptions import NotFoundError from starlite_saqlalchemy.testing.generic_mock_repository import GenericMockRepository diff --git a/tests/unit/sqlalchemy/repository/test_generic_mock_repository.py b/tests/unit/sqlalchemy/repository/test_generic_mock_repository.py new file mode 100644 index 00000000..97195c1a --- /dev/null +++ b/tests/unit/sqlalchemy/repository/test_generic_mock_repository.py @@ -0,0 +1,69 @@ +# pylint: disable=wrong-import-position,wrong-import-order +from __future__ import annotations + +import pytest + +from starlite_saqlalchemy.exceptions import ConflictError, StarliteSaqlalchemyError +from starlite_saqlalchemy.testing.generic_mock_repository import GenericMockRepository +from tests.utils.domain.authors import Author +from tests.utils.domain.books import Book + + +async def test_repo_raises_conflict_if_add_with_id( + authors: list[Author], + author_repository: GenericMockRepository[Author], +) -> None: + """Test mock repo raises conflict if add identified entity.""" + with pytest.raises(ConflictError): + await author_repository.add(authors[0]) + + +def test_generic_mock_repository_parametrization() -> None: + """Test that the mock repository handles multiple types.""" + author_repo = GenericMockRepository[Author] + book_repo = GenericMockRepository[Book] + assert author_repo.model_type is Author # type:ignore[misc] + assert book_repo.model_type is Book # type:ignore[misc] + + +def test_generic_mock_repository_seed_collection( + author_repository_type: type[GenericMockRepository[Author]], +) -> None: + """Test seeding instances.""" + author_repository_type.seed_collection([Author(id="abc")]) + assert "abc" in author_repository_type.collection + + +def test_generic_mock_repository_clear_collection( + author_repository_type: type[GenericMockRepository[Author]], +) -> None: + """Test clearing collection for type.""" + author_repository_type.clear_collection() + assert not author_repository_type.collection + + +def test_generic_mock_repository_filter_collection_by_kwargs( + author_repository: GenericMockRepository[Author], +) -> None: + """Test filtering the repository collection by kwargs.""" + author_repository.filter_collection_by_kwargs(name="Leo Tolstoy") + assert len(author_repository.collection) == 1 + assert list(author_repository.collection.values())[0].name == "Leo Tolstoy" + + +def test_generic_mock_repository_filter_collection_by_kwargs_and_semantics( + author_repository: GenericMockRepository[Author], +) -> None: + """Test that filtering by kwargs has `AND` semantics when multiple kwargs, + not `OR`.""" + author_repository.filter_collection_by_kwargs(name="Agatha Christie", dob="1828-09-09") + assert len(author_repository.collection) == 0 + + +def test_generic_mock_repository_raises_repository_exception_if_named_attribute_doesnt_exist( + author_repository: GenericMockRepository[Author], +) -> None: + """Test that a repo exception is raised if a named attribute doesn't + exist.""" + with pytest.raises(StarliteSaqlalchemyError): + author_repository.filter_collection_by_kwargs(cricket="ball") diff --git a/tests/unit/repository/test_sqlalchemy.py b/tests/unit/sqlalchemy/repository/test_sqlalchemy.py similarity index 98% rename from tests/unit/repository/test_sqlalchemy.py rename to tests/unit/sqlalchemy/repository/test_sqlalchemy.py index f5e4830e..d93d18c8 100644 --- a/tests/unit/repository/test_sqlalchemy.py +++ b/tests/unit/sqlalchemy/repository/test_sqlalchemy.py @@ -1,15 +1,12 @@ """Unit tests for the SQLAlchemy Repository implementation.""" -# pylint: disable=protected-access,redefined-outer-name,wrong-import-position,wrong-import-order +# pylint: disable=protected-access,redefined-outer-name from __future__ import annotations -import pytest - -pytest.importorskip("sqlalchemy") - from datetime import datetime from typing import TYPE_CHECKING from unittest.mock import AsyncMock, MagicMock, call +import pytest from sqlalchemy.exc import IntegrityError, InvalidRequestError, SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession diff --git a/tests/unit/test_db.py b/tests/unit/sqlalchemy/test_db.py similarity index 83% rename from tests/unit/test_db.py rename to tests/unit/sqlalchemy/test_db.py index a5eabcc3..312d73f3 100644 --- a/tests/unit/test_db.py +++ b/tests/unit/sqlalchemy/test_db.py @@ -1,14 +1,11 @@ """Tests for db module.""" # pylint: disable=protected-access -# pylint: disable=wrong-import-position,wrong-import-order from __future__ import annotations -import pytest - -pytest.importorskip("sqlalchemy") - from uuid import uuid4 +import pytest + from starlite_saqlalchemy import db diff --git a/tests/unit/test_dto.py b/tests/unit/sqlalchemy/test_dto.py similarity index 98% rename from tests/unit/test_dto.py rename to tests/unit/sqlalchemy/test_dto.py index 7dabd478..5c6850b2 100644 --- a/tests/unit/test_dto.py +++ b/tests/unit/sqlalchemy/test_dto.py @@ -1,14 +1,11 @@ """Tests for the dto factory.""" -# pylint: disable=missing-class-docstring,invalid-name,wrong-import-position,wrong-import-order - -import pytest - -pytest.importorskip("sqlalchemy") +# pylint: disable=missing-class-docstring,invalid-name from datetime import date, datetime, timedelta from typing import TYPE_CHECKING, Annotated, Any, ClassVar from uuid import UUID, uuid4 +import pytest from pydantic import BaseModel, Field, constr, validator from sqlalchemy import ForeignKey, func from sqlalchemy.orm import ( diff --git a/tests/unit/test_orm.py b/tests/unit/sqlalchemy/test_orm.py similarity index 82% rename from tests/unit/test_orm.py rename to tests/unit/sqlalchemy/test_orm.py index 513790a5..975afaa0 100644 --- a/tests/unit/test_orm.py +++ b/tests/unit/sqlalchemy/test_orm.py @@ -1,8 +1,4 @@ """Tests for application ORM configuration.""" -# pylint: disable=wrong-import-position,wrong-import-order -import pytest - -pytest.importorskip("sqlalchemy") import datetime from unittest.mock import MagicMock diff --git a/tests/unit/test_service.py b/tests/unit/sqlalchemy/test_service.py similarity index 97% rename from tests/unit/test_service.py rename to tests/unit/sqlalchemy/test_service.py index 1b32ecd8..9aa77b26 100644 --- a/tests/unit/test_service.py +++ b/tests/unit/sqlalchemy/test_service.py @@ -1,15 +1,12 @@ """Tests for Service object patterns.""" -# pylint: disable=wrong-import-position,wrong-import-order from __future__ import annotations -import pytest - -pytest.importorskip("sqlalchemy") - from datetime import date from typing import TYPE_CHECKING from uuid import uuid4 +import pytest + from starlite_saqlalchemy import service from starlite_saqlalchemy.exceptions import NotFoundError from tests.utils import domain diff --git a/tests/unit/test_testing.py b/tests/unit/sqlalchemy/test_testing.py similarity index 69% rename from tests/unit/test_testing.py rename to tests/unit/sqlalchemy/test_testing.py index 8c6d2184..d8764366 100644 --- a/tests/unit/test_testing.py +++ b/tests/unit/sqlalchemy/test_testing.py @@ -2,14 +2,11 @@ # pylint: disable=wrong-import-position,wrong-import-order from __future__ import annotations -import pytest - -pytest.importorskip("sqlalchemy") - from typing import TYPE_CHECKING from unittest.mock import MagicMock import httpx +import pytest from starlite.status_codes import ( HTTP_200_OK, HTTP_404_NOT_FOUND, @@ -17,11 +14,7 @@ ) from starlite_saqlalchemy import testing -from starlite_saqlalchemy.exceptions import ConflictError, StarliteSaqlalchemyError -from starlite_saqlalchemy.testing.generic_mock_repository import GenericMockRepository -from tests.utils.domain.authors import Author from tests.utils.domain.authors import Service as AuthorService -from tests.utils.domain.books import Book if TYPE_CHECKING: from typing import Any @@ -29,65 +22,7 @@ from pytest import MonkeyPatch from starlite import TestClient - -async def test_repo_raises_conflict_if_add_with_id( - authors: list[Author], - author_repository: GenericMockRepository[Author], -) -> None: - """Test mock repo raises conflict if add identified entity.""" - with pytest.raises(ConflictError): - await author_repository.add(authors[0]) - - -def test_generic_mock_repository_parametrization() -> None: - """Test that the mock repository handles multiple types.""" - author_repo = GenericMockRepository[Author] - book_repo = GenericMockRepository[Book] - assert author_repo.model_type is Author # type:ignore[misc] - assert book_repo.model_type is Book # type:ignore[misc] - - -def test_generic_mock_repository_seed_collection( - author_repository_type: type[GenericMockRepository[Author]], -) -> None: - """Test seeding instances.""" - author_repository_type.seed_collection([Author(id="abc")]) - assert "abc" in author_repository_type.collection - - -def test_generic_mock_repository_clear_collection( - author_repository_type: type[GenericMockRepository[Author]], -) -> None: - """Test clearing collection for type.""" - author_repository_type.clear_collection() - assert not author_repository_type.collection - - -def test_generic_mock_repository_filter_collection_by_kwargs( - author_repository: GenericMockRepository[Author], -) -> None: - """Test filtering the repository collection by kwargs.""" - author_repository.filter_collection_by_kwargs(name="Leo Tolstoy") - assert len(author_repository.collection) == 1 - assert list(author_repository.collection.values())[0].name == "Leo Tolstoy" - - -def test_generic_mock_repository_filter_collection_by_kwargs_and_semantics( - author_repository: GenericMockRepository[Author], -) -> None: - """Test that filtering by kwargs has `AND` semantics when multiple kwargs, - not `OR`.""" - author_repository.filter_collection_by_kwargs(name="Agatha Christie", dob="1828-09-09") - assert len(author_repository.collection) == 0 - - -def test_generic_mock_repository_raises_repository_exception_if_named_attribute_doesnt_exist( - author_repository: GenericMockRepository[Author], -) -> None: - """Test that a repo exception is raised if a named attribute doesn't - exist.""" - with pytest.raises(StarliteSaqlalchemyError): - author_repository.filter_collection_by_kwargs(cricket="ball") + from tests.utils.domain.authors import Author @pytest.fixture(name="mock_response") @@ -135,6 +70,7 @@ async def test_tester_get_collection_request_service_method_patch( tester: testing.ControllerTest, mock_response: MagicMock ) -> None: """Test that the "list" service method has been patched.""" + mock_response.json.return_value = tester.raw_collection tester.test_get_collection() assert "._list" in str(AuthorService.list) diff --git a/tests/unit/test_log.py b/tests/unit/test_log.py index fe7db35a..f10ce12b 100644 --- a/tests/unit/test_log.py +++ b/tests/unit/test_log.py @@ -22,7 +22,6 @@ from structlog import DropEvent from starlite_saqlalchemy import log, settings -from starlite_saqlalchemy.constants import IS_SAQ_INSTALLED, IS_SQLALCHEMY_INSTALLED if TYPE_CHECKING: from typing import Any @@ -87,7 +86,6 @@ async def test_middleware_calls_structlog_contextvars_clear_contextvars( app_mock.assert_called_once_with(1, 2, 3) -@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") @pytest.mark.parametrize( ("pattern", "excluded", "included"), [ @@ -128,7 +126,6 @@ async def call_handler(path_: str) -> dict[str, Any]: assert "http.response.start" in scope_state -@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") @pytest.mark.parametrize( ("status", "level"), [ @@ -154,7 +151,6 @@ async def test_before_send_handler_http_response_start( assert http_scope["state"]["http.response.start"] == http_response_start -@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") async def test_before_send_handler_http_response_body_with_more_body( before_send_handler: log.controller.BeforeSendHandler, cap_logger: CapturingLogger, @@ -168,7 +164,6 @@ async def test_before_send_handler_http_response_body_with_more_body( assert [] == cap_logger.calls -@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") async def test_before_send_handler_http_response_body_without_more_body( before_send_handler: log.controller.BeforeSendHandler, cap_logger: CapturingLogger, @@ -193,7 +188,6 @@ async def test_before_send_handler_http_response_body_without_more_body( assert cap_logger.calls -@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") async def test_before_send_handler_http_response_body_without_more_body_do_log_request_false( before_send_handler: log.controller.BeforeSendHandler, cap_logger: CapturingLogger, @@ -220,7 +214,6 @@ async def test_before_send_handler_http_response_body_without_more_body_do_log_r assert cap_logger.calls -@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") async def test_before_send_handler_does_nothing_with_other_message_types( before_send_handler: log.controller.BeforeSendHandler, cap_logger: CapturingLogger, @@ -233,7 +226,6 @@ async def test_before_send_handler_does_nothing_with_other_message_types( assert [] == cap_logger.calls -@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") async def test_before_send_handler_log_request( before_send_handler: log.controller.BeforeSendHandler, http_scope: HTTPScope, @@ -250,7 +242,6 @@ async def test_before_send_handler_log_request( bind_mock.assert_called_once_with(request=ret_val) -@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") async def test_before_send_handler_log_response( before_send_handler: log.controller.BeforeSendHandler, http_scope: HTTPScope, @@ -267,7 +258,6 @@ async def test_before_send_handler_log_response( bind_mock.assert_called_once_with(response=ret_val) -@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") @pytest.mark.parametrize("include", [True, False]) async def test_before_send_handler_exclude_body_from_log( include: bool, @@ -310,7 +300,6 @@ async def test_before_send_handler_extract_request_data( } -@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") def test_before_send_handler_extract_response_data( before_send_handler: log.controller.BeforeSendHandler, http_response_start: HTTPResponseStartEvent, @@ -335,7 +324,6 @@ async def test_before_process_calls_structlog_contextvars_clear_contextvars( clear_ctx_vars_mock.assert_called_once() -@pytest.mark.skipif(not IS_SAQ_INSTALLED, reason="saq is not installed") async def test_after_process(job: Job, cap_logger: CapturingLogger) -> None: """Tests extraction of job data, and eventual log.""" await log.worker.after_process({"job": job}) @@ -365,7 +353,6 @@ async def test_after_process(job: Job, cap_logger: CapturingLogger) -> None: ] == cap_logger.calls -@pytest.mark.skipif(not IS_SAQ_INSTALLED, reason="saq is not installed") async def test_after_process_logs_at_error(job: Job, cap_logger: CapturingLogger) -> None: """Tests eventual log is at ERROR level if `job.error`.""" job.error = "Yep, this is the traceback." @@ -421,7 +408,6 @@ def test_handler() -> str: assert call.kwargs["exception"] -@pytest.mark.skipif(not IS_SQLALCHEMY_INSTALLED, reason="SQLAlchemy is not installed") async def test_exception_in_before_send_handler_read_empty_body( client: TestClient[Starlite], cap_logger: CapturingLogger, From 65ebdaff91bc18a92f89ba54607cbd1b725e82be Mon Sep 17 00:00:00 2001 From: Peter Schutt Date: Thu, 19 Jan 2023 10:35:44 +1000 Subject: [PATCH 29/30] refactor: continue splitting tests up by required dependency. Health checks are up. --- src/starlite_saqlalchemy/health.py | 5 +- tests/conftest.py | 1 - tests/unit/__init__.py | 0 tests/unit/no_extras/__init__.py | 0 tests/unit/no_extras/conftest.py | 15 ++++ tests/unit/no_extras/test_init_plugin.py | 79 +++++++++++++++++++ tests/unit/sqlalchemy/test_health_check.py | 29 +++++++ .../{test_health.py => test_health_check.py} | 77 +++++++----------- tests/unit/test_init_plugin.py | 40 ---------- tests/unit/test_init_plugin_no_extras.py | 48 ----------- 10 files changed, 154 insertions(+), 140 deletions(-) create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/no_extras/__init__.py create mode 100644 tests/unit/no_extras/conftest.py create mode 100644 tests/unit/no_extras/test_init_plugin.py create mode 100644 tests/unit/sqlalchemy/test_health_check.py rename tests/unit/{test_health.py => test_health_check.py} (53%) delete mode 100644 tests/unit/test_init_plugin_no_extras.py diff --git a/src/starlite_saqlalchemy/health.py b/src/starlite_saqlalchemy/health.py index eaf5676b..58e7b3d3 100644 --- a/src/starlite_saqlalchemy/health.py +++ b/src/starlite_saqlalchemy/health.py @@ -48,7 +48,7 @@ async def live(self) -> bool: Returns: True if the service is running, False otherwise """ - return await self.ready() # pragma: no cover + return await self.ready() @abstractmethod async def ready(self) -> bool: @@ -57,6 +57,7 @@ async def ready(self) -> bool: Returns: True if the service is ready to serve requests, False otherwise """ + return True class AppHealthCheck(AbstractHealthCheck): @@ -66,7 +67,7 @@ class AppHealthCheck(AbstractHealthCheck): async def ready(self) -> bool: """Readiness check used when no other health check is available.""" - return True + return await super().ready() class HealthResource(BaseModel): diff --git a/tests/conftest.py b/tests/conftest.py index 87a87466..c1f5e95e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,4 @@ """Config that can be shared between all test types.""" -# pylint: disable=import-outside-toplevel from __future__ import annotations import importlib diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/no_extras/__init__.py b/tests/unit/no_extras/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/no_extras/conftest.py b/tests/unit/no_extras/conftest.py new file mode 100644 index 00000000..0195388a --- /dev/null +++ b/tests/unit/no_extras/conftest.py @@ -0,0 +1,15 @@ +"""Tests are only run if no extra dependencies are installed.""" + +from starlite_saqlalchemy import constants + +SKIP = any( + [ + constants.IS_SAQ_INSTALLED, + constants.IS_SENTRY_SDK_INSTALLED, + constants.IS_REDIS_INSTALLED, + constants.IS_SQLALCHEMY_INSTALLED, + ] +) + +if SKIP: + collect_ignore_glob = ["*"] diff --git a/tests/unit/no_extras/test_init_plugin.py b/tests/unit/no_extras/test_init_plugin.py new file mode 100644 index 00000000..66cf2968 --- /dev/null +++ b/tests/unit/no_extras/test_init_plugin.py @@ -0,0 +1,79 @@ +"""Tests for init_plugin.py when no extra dependencies are installed.""" + +import pytest +from pydantic import ValidationError +from starlite import Starlite +from starlite.cache import SimpleCacheBackend + +from starlite_saqlalchemy import init_plugin + + +def test_config_switches() -> None: + """Tests that the app produced with all config switches off is as we + expect.""" + config = init_plugin.PluginConfig( + do_after_exception=False, + do_cache=False, + do_compression=False, + # pyright reckons this parameter doesn't exist, I beg to differ + do_collection_dependencies=False, # pyright:ignore + do_exception_handlers=False, + do_health_check=False, + do_logging=False, + do_openapi=False, + do_sentry=False, + do_set_debug=False, + do_sqlalchemy_plugin=False, + do_type_encoders=False, + do_worker=False, + ) + app = Starlite( + route_handlers=[], + openapi_config=None, + on_app_init=[init_plugin.ConfigureApp(config=config)], + ) + assert app.compression_config is None + assert app.debug is False + assert app.logging_config is None + assert app.openapi_config is None + assert app.response_class is None + assert isinstance(app.cache.backend, SimpleCacheBackend) + assert len(app.on_shutdown) == 1 + assert not app.after_exception + assert not app.dependencies + assert not app.exception_handlers + assert not app.on_startup + assert not app.plugins + assert not app.routes + + +@pytest.mark.parametrize( + ("enabled_config", "error_pattern"), + [ + ("do_cache", r"\'redis\' is not installed."), + ("do_sentry", r"\'sentry_sdk\' is not installed."), + ("do_worker", r"\'saq\' is not installed."), + ("do_sqlalchemy_plugin", r"\'sqlalchemy\' is not installed."), + ], +) +def test_extra_dependencies_not_installed(enabled_config: str, error_pattern: str) -> None: + """Tests that the plugin test required dependencies for switches needing + them.""" + kwargs = { + "do_after_exception": False, + "do_cache": False, + "do_compression": False, + "do_collection_dependencies": False, + "do_exception_handlers": False, + "do_health_check": False, + "do_logging": False, + "do_openapi": False, + "do_sentry": False, + "do_set_debug": False, + "do_sqlalchemy_plugin": False, + "do_type_encoders": False, + "do_worker": False, + **{enabled_config: True}, + } + with pytest.raises(ValidationError, match=error_pattern): + init_plugin.PluginConfig(**kwargs) diff --git a/tests/unit/sqlalchemy/test_health_check.py b/tests/unit/sqlalchemy/test_health_check.py new file mode 100644 index 00000000..ac9a4731 --- /dev/null +++ b/tests/unit/sqlalchemy/test_health_check.py @@ -0,0 +1,29 @@ +"""Tests for application health check behavior.""" +from typing import TYPE_CHECKING +from unittest.mock import AsyncMock + +from starlite.status_codes import HTTP_200_OK + +from starlite_saqlalchemy import settings +from starlite_saqlalchemy.health import HealthController, HealthResource +from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyHealthCheck + +if TYPE_CHECKING: + from pytest import MonkeyPatch + from starlite.testing import TestClient + + +def test_health_check(client: "TestClient", monkeypatch: "MonkeyPatch") -> None: + """Test health check success response. + + Checks that we call the repository method and the response content. + """ + health_check = SQLAlchemyHealthCheck() + monkeypatch.setattr(HealthController, "health_checks", [health_check]) + repo_health_mock = AsyncMock(return_value=True) + monkeypatch.setattr(health_check, "ready", repo_health_mock) + resp = client.get(settings.api.HEALTH_PATH) + assert resp.status_code == HTTP_200_OK + health = HealthResource(app=settings.app, health={health_check.name: True}) + assert resp.json() == health.dict() + repo_health_mock.assert_called_once() diff --git a/tests/unit/test_health.py b/tests/unit/test_health_check.py similarity index 53% rename from tests/unit/test_health.py rename to tests/unit/test_health_check.py index f3f98a10..a41fb784 100644 --- a/tests/unit/test_health.py +++ b/tests/unit/test_health_check.py @@ -1,6 +1,4 @@ """Tests for application health check behavior.""" -# pylint: disable=ungrouped-imports -from itertools import product from typing import TYPE_CHECKING from unittest.mock import AsyncMock @@ -9,7 +7,6 @@ from starlite.status_codes import HTTP_200_OK, HTTP_503_SERVICE_UNAVAILABLE from starlite_saqlalchemy import init_plugin, settings -from starlite_saqlalchemy.constants import IS_SQLALCHEMY_INSTALLED from starlite_saqlalchemy.exceptions import HealthCheckConfigurationError from starlite_saqlalchemy.health import ( AbstractHealthCheck, @@ -23,61 +20,46 @@ from starlite.testing import TestClient -health_checks: "list[AbstractHealthCheck]" = [AppHealthCheck()] - -if IS_SQLALCHEMY_INSTALLED: - from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyHealthCheck - - health_checks.append(SQLAlchemyHealthCheck()) - - -@pytest.mark.parametrize("health_check", health_checks) -def test_health_check( - client: "TestClient", monkeypatch: "MonkeyPatch", health_check: AbstractHealthCheck -) -> None: +def test_health_check(client: "TestClient", monkeypatch: "MonkeyPatch") -> None: """Test health check success response. Checks that we call the repository method and the response content. """ - monkeypatch.setattr(HealthController, "health_checks", health_checks) - repo_health_mock = AsyncMock(return_value=True) - for health_check_ in health_checks: - monkeypatch.setattr(health_check_, "ready", repo_health_mock) + health_check = AppHealthCheck() + monkeypatch.setattr(HealthController, "health_checks", [health_check]) resp = client.get(settings.api.HEALTH_PATH) assert resp.status_code == HTTP_200_OK - health = HealthResource( - app=settings.app, - health={ht.name: True for ht in health_checks} | {health_check.name: True}, - ) + health = HealthResource(app=settings.app, health={health_check.name: True}) assert resp.json() == health.dict() - assert repo_health_mock.call_count == len(health_checks) -@pytest.mark.parametrize( - ("health_check", "mock"), - product(health_checks, [AsyncMock(return_value=False), AsyncMock(side_effect=ConnectionError)]), -) -def test_health_check_failed( - client: "TestClient", - monkeypatch: "MonkeyPatch", - health_check: AbstractHealthCheck, - mock: AsyncMock, -) -> None: +async def test_health_check_live() -> None: + """Test expected result of calling `live()` health check method.""" + health_check = AppHealthCheck() + assert await health_check.live() is True + + +def test_health_check_failed(client: "TestClient", monkeypatch: "MonkeyPatch") -> None: """Test health check response if check method returns `False`""" - # repo_health_mock = AsyncMock(return_value=False) - monkeypatch.setattr(HealthController, "health_checks", health_checks) - for health_check_ in health_checks: - if health_check_ is health_check: - monkeypatch.setattr(health_check_, "ready", mock) - else: - monkeypatch.setattr(health_check_, "ready", AsyncMock(return_value=True)) + health_check = AppHealthCheck() + monkeypatch.setattr(HealthController, "health_checks", [health_check]) + monkeypatch.setattr(health_check, "ready", AsyncMock(side_effect=RuntimeError)) resp = client.get(settings.api.HEALTH_PATH) assert resp.status_code == HTTP_503_SERVICE_UNAVAILABLE - health = HealthResource( - app=settings.app, - health={ht.name: True for ht in health_checks} | {health_check.name: False}, - ) - assert resp.json() == health.dict() + HealthResource(app=settings.app, health={health_check.name: True}) + assert resp.json() == { + "app": { + "BUILD_NUMBER": "", + "CHECK_DB_READY": True, + "CHECK_REDIS_READY": True, + "DEBUG": False, + "ENVIRONMENT": "test", + "TEST_ENVIRONMENT_NAME": "test", + "LOCAL_ENVIRONMENT_NAME": "local", + "NAME": "my-starlite-app", + }, + "health": {"app": False}, + } def test_health_custom_health_check(client: "TestClient", monkeypatch: "MonkeyPatch") -> None: @@ -93,15 +75,12 @@ async def ready(self) -> bool: return False monkeypatch.setattr(HealthController, "health_checks", [AppHealthCheck(), MyHealthCheck()]) - # repo_health_mock = AsyncMock(return_value=True) - # monkeypatch.setattr(SQLAlchemyHealthCheck, "ready", repo_health_mock) resp = client.get(settings.api.HEALTH_PATH) assert resp.status_code == HTTP_503_SERVICE_UNAVAILABLE health = HealthResource( app=settings.app, health={ AppHealthCheck.name: True, - # SQLAlchemyHealthCheck.name: True, MyHealthCheck.name: False, }, ) diff --git a/tests/unit/test_init_plugin.py b/tests/unit/test_init_plugin.py index c3545c53..f7127f20 100644 --- a/tests/unit/test_init_plugin.py +++ b/tests/unit/test_init_plugin.py @@ -7,7 +7,6 @@ import pytest from starlite import Starlite -from starlite.cache import SimpleCacheBackend from starlite_saqlalchemy import init_plugin from starlite_saqlalchemy.constants import IS_SAQ_INSTALLED, IS_SENTRY_SDK_INSTALLED @@ -18,45 +17,6 @@ from pytest import MonkeyPatch -def test_config_switches() -> None: - """Tests that the app produced with all config switches off is as we - expect.""" - config = init_plugin.PluginConfig( - do_after_exception=False, - do_cache=False, - do_compression=False, - # pyright reckons this parameter doesn't exist, I beg to differ - do_collection_dependencies=False, # pyright:ignore - do_exception_handlers=False, - do_health_check=False, - do_logging=False, - do_openapi=False, - do_sentry=False, - do_set_debug=False, - do_sqlalchemy_plugin=False, - do_type_encoders=False, - do_worker=False, - ) - app = Starlite( - route_handlers=[], - openapi_config=None, - on_app_init=[init_plugin.ConfigureApp(config=config)], - ) - assert app.compression_config is None - assert app.debug is False - assert app.logging_config is None - assert app.openapi_config is None - assert app.response_class is None - assert isinstance(app.cache.backend, SimpleCacheBackend) - assert len(app.on_shutdown) == 1 - assert not app.after_exception - assert not app.dependencies - assert not app.exception_handlers - assert not app.on_startup - assert not app.plugins - assert not app.routes - - @pytest.mark.skipif(not IS_SAQ_INSTALLED, reason="saq is not installed") def test_do_worker_but_not_logging(monkeypatch: MonkeyPatch) -> None: """Tests branch where we can have the worker enabled, but logging diff --git a/tests/unit/test_init_plugin_no_extras.py b/tests/unit/test_init_plugin_no_extras.py deleted file mode 100644 index 5c682c54..00000000 --- a/tests/unit/test_init_plugin_no_extras.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Tests for init_plugin.py when no extra dependencies are installed.""" - -import pytest -from pydantic import ValidationError - -from starlite_saqlalchemy import constants, init_plugin - -SKIP = any( - [ - constants.IS_SAQ_INSTALLED, - constants.IS_SENTRY_SDK_INSTALLED, - constants.IS_REDIS_INSTALLED, - constants.IS_SQLALCHEMY_INSTALLED, - ] -) - - -@pytest.mark.skipif(SKIP, reason="test will only run if no extras are installed") -@pytest.mark.parametrize( - ("enabled_config", "error_pattern"), - [ - ("do_cache", r"\'redis\' is not installed."), - ("do_sentry", r"\'sentry_sdk\' is not installed."), - ("do_worker", r"\'saq\' is not installed."), - ("do_sqlalchemy_plugin", r"\'sqlalchemy\' is not installed."), - ], -) -def test_extra_dependencies_not_installed(enabled_config: str, error_pattern: str) -> None: - """Tests that the plugin test required dependencies for switches needing - them.""" - kwargs = { - "do_after_exception": False, - "do_cache": False, - "do_compression": False, - "do_collection_dependencies": False, - "do_exception_handlers": False, - "do_health_check": False, - "do_logging": False, - "do_openapi": False, - "do_sentry": False, - "do_set_debug": False, - "do_sqlalchemy_plugin": False, - "do_type_encoders": False, - "do_worker": False, - **{enabled_config: True}, - } - with pytest.raises(ValidationError, match=error_pattern): - init_plugin.PluginConfig(**kwargs) From b1f9aa7bd71059d71a153a4ee2f85a9d2952cf24 Mon Sep 17 00:00:00 2001 From: Peter Schutt Date: Thu, 19 Jan 2023 10:36:24 +1000 Subject: [PATCH 30/30] refactor: add method for setting lifecycle handlers. --- src/starlite_saqlalchemy/init_plugin.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/starlite_saqlalchemy/init_plugin.py b/src/starlite_saqlalchemy/init_plugin.py index 4c28efce..cead9ea6 100644 --- a/src/starlite_saqlalchemy/init_plugin.py +++ b/src/starlite_saqlalchemy/init_plugin.py @@ -237,14 +237,21 @@ def __call__(self, app_config: AppConfig) -> AppConfig: self.configure_worker(app_config) # health check is explicitly configured last self.configure_health_check(app_config) + self.set_lifecycle_handlers(app_config) + return app_config + + def set_lifecycle_handlers(self, app_config: AppConfig) -> None: + """Configure any necessary startup/shutdown behaviors. + Args: + app_config: The Starlite application config object. + """ app_config.before_startup = lifespan.before_startup_handler app_config.on_shutdown.append(http.on_shutdown) - if self.config.do_cache: + if IS_REDIS_INSTALLED: from starlite_saqlalchemy import redis app_config.on_shutdown.append(redis.client.close) - return app_config def configure_after_exception(self, app_config: AppConfig) -> None: """Add the logging after exception hook handler.