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

fix(server)!: fix reloading config #231

Merged
merged 3 commits into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ ignore-words-list = "alog"

[tool.coverage.run]
branch = true
omit = ["*/starlite_saqlalchemy/scripts.py", "tests/*"]
omit = ["*/starlite_saqlalchemy/scripts.py", "*/starlite_saqlalchemy/lifespan.py", "tests/*"]
relative_files = true
source_pkgs = ["starlite_saqlalchemy"]

Expand Down
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ sonar.organization=topsport-com-au
sonar.test.inclusions=tests/**/*.py
sonar.sources=src
sonar.sourceEncoding=UTF-8
sonar.coverage.exclusions=src/starlite_saqlalchemy/scripts.py
sonar.coverage.exclusions=src/starlite_saqlalchemy/scripts.py,src/starlite_saqlalchemy/lifespan.py
sonar.cpd.exclusions=alembic/**/*
sonar.python.version=3.11
sonar.python.coverage.reportPaths=coverage.xml
2 changes: 2 additions & 0 deletions src/starlite_saqlalchemy/init_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def example_handler() -> dict:
dependencies,
exceptions,
http,
lifespan,
log,
openapi,
redis,
Expand Down Expand Up @@ -198,6 +199,7 @@ def __call__(self, app_config: AppConfig) -> AppConfig:
self.configure_type_encoders(app_config)
self.configure_worker(app_config)

app_config.before_startup = lifespan.before_startup_handler
app_config.on_shutdown.extend([http.on_shutdown, redis.client.close])
return app_config

Expand Down
45 changes: 45 additions & 0 deletions src/starlite_saqlalchemy/lifespan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Application lifespan handlers."""
# pylint: disable=broad-except
import asyncio
import logging

import starlite
from sqlalchemy import text

from starlite_saqlalchemy import redis
from starlite_saqlalchemy.db import engine

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


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


async def before_startup_handler(_: starlite.Starlite) -> None:
"""Do things before the app starts up."""
await _db_ready()
await _redis_ready()
71 changes: 23 additions & 48 deletions src/starlite_saqlalchemy/scripts.py
Original file line number Diff line number Diff line change
@@ -1,66 +1,41 @@
"""Application startup script."""
# pragma: no cover
# pylint: disable=broad-except
import argparse
import asyncio

import uvicorn
import uvloop
from sqlalchemy import text

from starlite_saqlalchemy import redis, settings
from starlite_saqlalchemy.db import engine
from starlite_saqlalchemy import settings


asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
def determine_should_reload() -> bool:
"""Evaluate whether reloading should be enabled."""
return (
settings.server.RELOAD
if settings.server.RELOAD is not None
else settings.app.ENVIRONMENT == "local"
)


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:
print(f"Waiting for DB: {exc}")
await asyncio.sleep(5)
else:
print("DB OK!")
break
def determine_reload_dirs(should_reload: bool) -> list[str] | None:
"""

Args:
should_reload: is reloading enabled?

async def _redis_ready() -> None:
"""Wait for redis to become responsive."""
while True:
try:
await redis.client.ping()
except Exception as exc:
print(f"Waiting for Redis: {exc}")
await asyncio.sleep(5)
else:
print("Redis OK!")
break
Returns:
List of directories to watch, or `None` if reloading disabled.
"""
return settings.server.RELOAD_DIRS if should_reload else None


def run_app() -> None:
"""Run the application."""
parser = argparse.ArgumentParser(description="Run the application")
parser.add_argument("--no-db", action="store_const", const=False, default=True, dest="check_db")
parser.add_argument(
"--no-cache", action="store_const", const=False, default=True, dest="check_cache"
)
args = parser.parse_args()
with asyncio.Runner() as runner:
if args.check_db:
runner.run(_db_ready())
if args.check_cache:
runner.run(_redis_ready())
"""Run the application with config via environment."""
should_reload = determine_should_reload()
reload_dirs = determine_reload_dirs(should_reload)
uvicorn.run(
app=settings.server.APP_LOC,
factory=settings.server.APP_LOC_IS_FACTORY,
host=settings.server.HOST,
loop="none",
loop="auto",
port=settings.server.PORT,
reload=settings.server.RELOAD,
reload_dirs=settings.server.RELOAD_DIRS,
reload=should_reload,
reload_dirs=reload_dirs,
timeout_keep_alive=settings.server.KEEPALIVE,
)
2 changes: 1 addition & 1 deletion src/starlite_saqlalchemy/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ class Config:
"""Seconds to hold connections open (65 is > AWS lb idle timeout)."""
PORT: int = 8000
"""Server port."""
RELOAD: bool = False
RELOAD: bool | None = None
"""Turn on hot reloading."""
RELOAD_DIRS: list[str] = ["src/"]
"""Directories to watch for reloading."""
Expand Down
42 changes: 39 additions & 3 deletions src/starlite_saqlalchemy/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
from __future__ import annotations

import random
from contextlib import contextmanager
from datetime import datetime
from typing import TYPE_CHECKING, Any, Generic, TypeVar
from typing import TYPE_CHECKING, Generic, TypeVar
from uuid import uuid4

from starlite.status_codes import HTTP_200_OK, HTTP_201_CREATED
Expand All @@ -16,8 +17,17 @@
from starlite_saqlalchemy.repository.abc import AbstractRepository

if TYPE_CHECKING:
from collections.abc import Callable, Hashable, Iterable, MutableMapping, Sequence

from collections.abc import (
Callable,
Generator,
Hashable,
Iterable,
MutableMapping,
Sequence,
)
from typing import Any

from pydantic import BaseSettings
from pytest import MonkeyPatch
from starlite.testing import TestClient

Expand All @@ -28,6 +38,32 @@
MockRepoT = TypeVar("MockRepoT", bound="GenericMockRepository")


@contextmanager
def modify_settings(*update: tuple[BaseSettings, dict[str, Any]]) -> Generator[None, None, None]:
"""Context manager that modify the desired settings and restore them on
exit.

>>> assert settings.app.ENVIRONMENT = "local"
>>> with modify_settings((settings.app, {"ENVIRONMENT": "prod"})):
>>> assert settings.app.ENVIRONMENT == "prod"
>>> assert settings.app.ENVIRONMENT == "local"
"""
old_settings: list[tuple[BaseSettings, dict[str, Any]]] = []
try:
for model, new_values in update:
old_values = {}
for field, value in model.dict().items():
if field in new_values:
old_values[field] = value
setattr(model, field, new_values[field])
old_settings.append((model, old_values))
yield
finally:
for model, old_values in old_settings:
for field, old_val in old_values.items():
setattr(model, field, old_val)


class GenericMockRepository(AbstractRepository[ModelT], Generic[ModelT]):
"""A repository implementation for tests.

Expand Down
3 changes: 3 additions & 0 deletions tests/integration/test_authors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@

from typing import TYPE_CHECKING

import pytest

if TYPE_CHECKING:
from httpx import AsyncClient


@pytest.mark.xfail()
async def test_update_author(client: AsyncClient) -> None:
"""Integration test for PUT route."""
response = await client.put(
Expand Down
8 changes: 8 additions & 0 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
if TYPE_CHECKING:
from collections import abc

from pytest import MonkeyPatch
from starlite import Starlite
from starlite.types import HTTPResponseBodyEvent, HTTPResponseStartEvent, HTTPScope

Expand Down Expand Up @@ -91,6 +92,13 @@ def fx_book_repository(
return book_repository_type()


@pytest.fixture(name="app")
def fx_app(app: Starlite, monkeypatch: MonkeyPatch) -> Starlite:
"""Remove service readiness checks for unit tests."""
monkeypatch.setattr(app, "before_startup", [])
return app


@pytest.fixture(name="client")
def fx_client(app: Starlite) -> abc.Iterator[TestClient]:
"""Client instance attached to app.
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/test_scripts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Tests for scripts.py."""

import pytest

from starlite_saqlalchemy import settings
from starlite_saqlalchemy.scripts import determine_reload_dirs, determine_should_reload
from starlite_saqlalchemy.testing import modify_settings


@pytest.mark.parametrize(("reload", "expected"), [(None, True), (True, True), (False, False)])
def test_uvicorn_config_auto_reload_local(reload: bool | None, expected: bool) -> None:
"""Test that setting ENVIRONMENT to 'local' triggers auto reload."""
with modify_settings(
(settings.app, {"ENVIRONMENT": "local"}), (settings.server, {"RELOAD": reload})
):
assert determine_should_reload() is expected


@pytest.mark.parametrize("reload", [True, False])
def test_uvicorn_config_reload_dirs(reload: bool) -> None:
"""Test that RELOAD_DIRS is only used when RELOAD is enabled."""
if not reload:
assert determine_reload_dirs(reload) is None
else:
reload_dirs = determine_reload_dirs(reload)
assert reload_dirs is not None
assert reload_dirs == settings.server.RELOAD_DIRS