Skip to content
This repository has been archived by the owner on Sep 12, 2023. It is now read-only.

Commit

Permalink
feat!: optional dependencies (#213)
Browse files Browse the repository at this point in the history
* ♻️ refactor(deps): require dependencies based on plugin config

* 🐛 fix: tox config

* ♻️ refactor(tox): append coverage in testenv:noextras

* ♻️ refactor(tox): don't run coverage in parallel mode in testenv:noextras

* 🐛 fix: full coverage

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* ♻️ refactor: apply changes from review

* 🐛 fix: linters

* ♻️ refactor: full coverage

* ♻️ refactor: update pytest plugin fixtures

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* ✅ test: fix tests

* ♻️ refactor: add sqlalchemy to optional dependencies

* ♻️ refactor: silent flake8

* ✅ test(tox): make noextras passing the full test suite

* ♻️ refactor: silent pyright

* 📝 docs: update readme

* 📝 docs: update readme

* refactor: requirements/tox/etc

* style: rollback style changes

The bigger the PR, the better it is if we keep the changes to bare
minimum.

* refactor: run pytest plugin tests in sub-process.

This prevents the need to reload any modules, albeit it is a bit slower.

* refactor: no top level conditional imports.

* refactor: makes lifespan check imports and logic conditional

* refactor: no implicit imports of modules with optional functionality

* refactor: move any worker related stuff out of service module

* 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.

* refactor: remove more module scoped conditional imports.

Import from within the test/fixture where appropriate.

* 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.

* refactor: continue splitting tests up by required dependency.

Health checks are up.

* refactor: add method for setting lifecycle handlers.

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Peter Schutt <[email protected]>
  • Loading branch information
3 people committed Jan 20, 2023
1 parent 10a49c8 commit efe26d6
Show file tree
Hide file tree
Showing 53 changed files with 1,166 additions and 861 deletions.
1 change: 0 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,30 @@ Configuration for a [Starlite](https://github.com/starlite-api/starlite) applica
- SAQ async worker
- Lots of features!

## Installation

This will install `starlite-saqlalchemy` with minimal dependencies.

```console
poetry add starlite-saqlalchemy
```

You can also install additional dependencies depending on the features you need:

```console
# Repository implementation, DTOs
poetry add starlite-saqlalchemy[sqlalchemy]
# Async worker using saq
poetry add starlite-saqlalchemy[worker]
# Redis cache backend
poetry add starlite-saqlalchemy[cache]
# Sentry integration for starlite
poetry add starlite-saqlalchemy[sentry]

# or to install them all:
poetry add starlite-saqlalchemy[all]
```

## Example

```python
Expand Down
4 changes: 2 additions & 2 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
184 changes: 78 additions & 106 deletions poetry.lock

Large diffs are not rendered by default.

32 changes: 24 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,29 @@ packages = [
[tool.poetry.dependencies]
python = "^3.10"
asyncpg = "*"
hiredis = "*"
httpx = "*"
msgspec = "*"
pydantic = "*"
python-dotenv = "*"
redis = "*"
saq = "^0.9.1"
sentry-sdk = ">=1.13.0"
sqlalchemy = "==2.0.0rc2"
starlite = "^1.40.1"
structlog = ">=22.2.0"
tenacity = "*"
uvicorn = "*"
uvloop = "*"
structlog = ">=22.2.0"

# Optionals
hiredis = { version = "*", optional = true }
redis = { version = "*", optional = true }
saq = { version = "^0.9.1", optional = true }
sentry-sdk = { version = "*", optional = true }
sqlalchemy = { version = "==2.0.0rc2", optional = true }

[tool.poetry.extras]
cache = ["redis", "hiredis"]
worker = ["saq", "hiredis"]
sentry = ["sentry-sdk"]
sqlalchemy = ["sqlalchemy"]
all = ["redis", "hiredis", "saq", "sentry-sdk", "sqlalchemy"]

[tool.poetry.plugins."pytest11"]
pytest_starlite_saqlalchemy = "pytest_starlite_saqlalchemy"
Expand All @@ -92,11 +101,18 @@ add-select = "D401,D404,D417"
convention = "google"

[tool.pytest.ini_options]
addopts = ["-ra", "--strict-config"]
addopts = [
"-ra",
"--strict-config",
# Plugin are enabled in tests/conftest.py to control loading order.
"-p",
"no:pytest_starlite_saqlalchemy",
"-p",
"no:pytest_dotenv",
]
asyncio_mode = "auto"
env_files = ["tests.env"]
testpaths = ["tests/unit"]
test_app = "tests.utils.app:create_app"

[tool.pylint.main]
disable = [
Expand Down
7 changes: 7 additions & 0 deletions requirements.dev-extras.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-r requirements.dev.txt

sentry-sdk >= "1.13.0"
hiredis
redis
saq >= "0.9.1"
sqlalchemy == 2.0.0rc2
10 changes: 6 additions & 4 deletions src/pytest_starlite_saqlalchemy/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -12,6 +13,8 @@
from structlog.testing import CapturingLogger
from uvicorn.importer import ImportFromStringError, import_from_string

from starlite_saqlalchemy import constants

if TYPE_CHECKING:
from collections.abc import Generator

Expand All @@ -35,7 +38,7 @@ def pytest_addoption(parser: Parser) -> None:
"test_app",
"Path to application instance, or callable that returns an application instance.",
type="string",
default="app.main:create_app",
default=os.environ.get("TEST_APP", "app.main:create_app"),
)
parser.addini(
"unit_test_pattern",
Expand Down Expand Up @@ -65,7 +68,7 @@ def _patch_http_close(monkeypatch: MonkeyPatch) -> None:
monkeypatch.setattr(starlite_saqlalchemy.http, "clients", set())


@pytest.fixture(autouse=True)
@pytest.fixture(autouse=constants.IS_SQLALCHEMY_INSTALLED)
def _patch_sqlalchemy_plugin(is_unit_test: bool, monkeypatch: MonkeyPatch) -> None:
if is_unit_test:
from starlite_saqlalchemy import sqlalchemy_plugin
Expand All @@ -77,7 +80,7 @@ def _patch_sqlalchemy_plugin(is_unit_test: bool, monkeypatch: MonkeyPatch) -> No
)


@pytest.fixture(autouse=True)
@pytest.fixture(autouse=constants.IS_SAQ_INSTALLED)
def _patch_worker(is_unit_test: bool, monkeypatch: MonkeyPatch) -> None:
"""We don't want the worker to start for unittests."""
if is_unit_test:
Expand All @@ -94,7 +97,6 @@ def fx_app(pytestconfig: Config, monkeypatch: MonkeyPatch) -> Starlite:
An application instance, configured via plugin.
"""
test_app_str = pytestconfig.getini("test_app")

try:
app_or_callable = import_from_string(test_app_str)
except (ImportFromStringError, ModuleNotFoundError):
Expand Down
15 changes: 1 addition & 14 deletions src/starlite_saqlalchemy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,48 +22,35 @@ def example_handler() -> dict:
# this is because pycharm wigs out when there is a module called `exceptions`:
# noinspection PyCompatibility
from . import (
cache,
compression,
db,
dependencies,
dto,
exceptions,
health,
http,
log,
openapi,
redis,
repository,
sentry,
service,
settings,
sqlalchemy_plugin,
type_encoders,
worker,
)
from .init_plugin import ConfigureApp, PluginConfig

__all__ = [
"ConfigureApp",
"PluginConfig",
"cache",
"compression",
"db",
"dependencies",
"dto",
"exceptions",
"health",
"http",
"log",
"openapi",
"redis",
"repository",
"sentry",
"service",
"settings",
"sqlalchemy_plugin",
"type_encoders",
"worker",
]


__version__ = "0.28.1"
39 changes: 39 additions & 0 deletions src/starlite_saqlalchemy/constants.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,50 @@
"""Application constants."""
from __future__ import annotations

from importlib import import_module
from typing import TYPE_CHECKING, Any

from starlite_saqlalchemy.settings import app
from starlite_saqlalchemy.utils import case_insensitive_string_compare

if TYPE_CHECKING:
from collections.abc import MutableMapping

from starlite_saqlalchemy.service import Service


IS_TEST_ENVIRONMENT = case_insensitive_string_compare(app.ENVIRONMENT, app.TEST_ENVIRONMENT_NAME)
"""Flag indicating if the application is running in a test environment."""

IS_LOCAL_ENVIRONMENT = case_insensitive_string_compare(app.ENVIRONMENT, app.LOCAL_ENVIRONMENT_NAME)
"""Flag indicating if application is running in local development mode."""

IS_REDIS_INSTALLED = True
"""Flag indicating if redis module is installed."""

IS_SAQ_INSTALLED = True
"""Flag indicating if saq module is installed."""

IS_SENTRY_SDK_INSTALLED = True
"""Flag indicating if sentry_sdk module is installed."""

IS_SQLALCHEMY_INSTALLED = True
"""Flag indicating if sqlalchemy module is installed."""


for package in ("redis", "saq", "sentry_sdk", "sqlalchemy"):
try:
import_module(package)
except ModuleNotFoundError:
match package:
case "redis":
IS_REDIS_INSTALLED = False
case "saq":
IS_SAQ_INSTALLED = False
case "sentry_sdk":
IS_SENTRY_SDK_INSTALLED = False
case "sqlalchemy": # pragma: no cover
IS_SQLALCHEMY_INSTALLED = False

SERVICE_OBJECT_IDENTITY_MAP: MutableMapping[str, type[Service[Any]]] = {}
"""Used by the worker to lookup methods for service object callbacks."""
11 changes: 11 additions & 0 deletions src/starlite_saqlalchemy/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ class AuthorizationError(StarliteSaqlalchemyClientError):
"""A user tried to do something they shouldn't have."""


class MissingDependencyError(StarliteSaqlalchemyError, ValueError):
"""A required dependency is not installed."""

def __init__(self, module: str, config: str | None = None) -> None:
config = config if config else module
super().__init__(
f"You enabled {config} configuration but package {module!r} is not installed. "
f'You may need to run: "poetry install starlite-saqlalchemy[{config}]"'
)


class HealthCheckConfigurationError(StarliteSaqlalchemyError):
"""An error occurred while registering an health check."""

Expand Down
5 changes: 3 additions & 2 deletions src/starlite_saqlalchemy/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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):
Expand All @@ -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):
Expand Down
Loading

0 comments on commit efe26d6

Please sign in to comment.