Skip to content

Commit af405b6

Browse files
authored
Merge pull request #231 from grillazz/switch-logger-to-rotoger
Switch logger to rotoger
2 parents 55c3105 + 88a66b1 commit af405b6

10 files changed

Lines changed: 119 additions & 46 deletions

File tree

.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ POSTGRES_HOST=postgres
66
POSTGRES_PORT=5432
77
POSTGRES_DB=devdb
88
POSTGRES_USER=devdb
9+
POSTGRES_TEST_DB=testdb
10+
POSTGRES_TEST_USER=testdb
911
POSTGRES_PASSWORD=secret
1012

1113
# Redis

.github/workflows/build-and-test.yml

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,11 @@ jobs:
4141
POSTGRES_DB: testdb
4242
ports:
4343
- 5432:5432
44-
# needed because the postgres container does not provide a health check
4544
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
4645

4746
steps:
48-
- uses: actions/checkout@v5
49-
- name: Create database schema
50-
run: PGPASSWORD=secret psql -h 127.0.0.1 -d testdb -U panettone -c "CREATE SCHEMA shakespeare; CREATE SCHEMA happy_hog;"
47+
- name: Checkout repository
48+
uses: actions/checkout@v5
5149

5250
- name: Install the latest version of uv
5351
uses: astral-sh/setup-uv@v7
@@ -58,6 +56,4 @@ jobs:
5856
run: uv run --frozen ruff check .
5957

6058
- name: Test with python ${{ matrix.python-version }}
61-
run: uv run --frozen pytest
62-
63-
59+
run: uv run pytest

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ docker-create-db-migration: ## Create a new alembic database migration. Example
4545
# ====================================================================================
4646
.PHONY: docker-test
4747
docker-test: ## Run project tests
48-
docker compose -f compose.yml -f test-compose.yml run --rm api1 pytest tests --durations=0 -vv
48+
docker compose -f compose.yml run --rm api1 pytest tests --durations=0 -vv
4949

5050
.PHONY: docker-test-snapshot
5151
docker-test-snapshot: ## Run project tests and update snapshots

app/config.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ class Settings(BaseSettings):
3333
POSTGRES_PASSWORD: str
3434
POSTGRES_HOST: str
3535
POSTGRES_DB: str
36+
POSTGRES_TEST_USER: str
37+
POSTGRES_TEST_DB: str
3638

3739
@computed_field
3840
@property
@@ -80,6 +82,30 @@ def asyncpg_url(self) -> PostgresDsn:
8082
path=self.POSTGRES_DB,
8183
)
8284

85+
@computed_field
86+
@property
87+
def test_asyncpg_url(self) -> PostgresDsn:
88+
"""
89+
This is a computed field that generates a PostgresDsn URL for the test database using asyncpg.
90+
91+
The URL is built using the MultiHostUrl.build method, which takes the following parameters:
92+
- scheme: The scheme of the URL. In this case, it is "postgresql+asyncpg".
93+
- username: The username for the Postgres database, retrieved from the POSTGRES_USER environment variable.
94+
- password: The password for the Postgres database, retrieved from the POSTGRES_PASSWORD environment variable.
95+
- host: The host of the Postgres database, retrieved from the POSTGRES_HOST environment variable.
96+
- path: The path of the Postgres test database, retrieved from the POSTGRES_TEST_DB environment variable.
97+
98+
Returns:
99+
PostgresDsn: The constructed PostgresDsn URL for the test database with asyncpg.
100+
"""
101+
return MultiHostUrl.build(
102+
scheme="postgresql+asyncpg",
103+
username=self.POSTGRES_USER,
104+
password=self.POSTGRES_PASSWORD,
105+
host=self.POSTGRES_HOST,
106+
path=self.POSTGRES_TEST_DB,
107+
)
108+
83109
@computed_field
84110
@property
85111
def postgres_url(self) -> PostgresDsn:

app/database.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
echo=True,
1616
)
1717

18+
test_engine = create_async_engine(
19+
global_settings.test_asyncpg_url.unicode_string(),
20+
future=True,
21+
echo=True,
22+
)
23+
1824
# expire_on_commit=False will prevent attributes from being expired
1925
# after commit.
2026
AsyncSessionFactory = async_sessionmaker(
@@ -23,6 +29,12 @@
2329
expire_on_commit=False,
2430
)
2531

32+
TestAsyncSessionFactory = async_sessionmaker(
33+
test_engine,
34+
autoflush=False,
35+
expire_on_commit=False,
36+
)
37+
2638

2739
# Dependency
2840
async def get_db() -> AsyncGenerator:
@@ -38,3 +50,18 @@ async def get_db() -> AsyncGenerator:
3850
if not isinstance(ex, ResponseValidationError):
3951
await logger.aerror(f"Database-related error: {repr(ex)}")
4052
raise # Re-raise to be handled by appropriate handlers
53+
54+
55+
async def get_test_db() -> AsyncGenerator:
56+
async with TestAsyncSessionFactory() as session:
57+
try:
58+
yield session
59+
await session.commit()
60+
except SQLAlchemyError:
61+
# Re-raise SQLAlchemy errors to be handled by the global handler
62+
raise
63+
except Exception as ex:
64+
# Only log actual database-related issues, not response validation
65+
if not isinstance(ex, ResponseValidationError):
66+
await logger.aerror(f"Database-related error: {repr(ex)}")
67+
raise # Re-raise to be handled by appropriate handlers

db/Dockerfile

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
# pull official base image
22
FROM postgres:17.6-alpine
33

4-
# run create.sql on init
5-
ADD create.sql /docker-entrypoint-initdb.d
6-
74
WORKDIR /home/gx/code
85

96
COPY shakespeare_chapter.sql /home/gx/code/shakespeare_chapter.sql

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ dependencies = [
3434

3535
[tool.uv]
3636
dev-dependencies = [
37-
"ruff==0.14.9",
37+
"ruff==0.14.10",
3838
"devtools[pygments]==0.12.2",
3939
"pyupgrade==3.21.2",
4040
"ipython==9.8.0",

test-compose.yml

Lines changed: 0 additions & 9 deletions
This file was deleted.

tests/conftest.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
import pytest
55
from httpx import ASGITransport, AsyncClient
6+
from sqlalchemy import text
7+
from sqlalchemy.exc import ProgrammingError
68

7-
from app.database import engine
9+
from app.database import engine, get_db, get_test_db, test_engine
810
from app.main import app
911
from app.models.base import Base
1012
from app.redis import get_redis
@@ -19,15 +21,46 @@
1921
def anyio_backend(request):
2022
return request.param
2123

24+
def _create_db(conn) -> None:
25+
"""Create the test database if it doesn't exist."""
26+
try:
27+
conn.execute(text("CREATE DATABASE testdb"))
28+
except ProgrammingError:
29+
# This might be raised by databases that don't support `IF NOT EXISTS`
30+
# and the schema already exists. You can choose to ignore it.
31+
pass
32+
33+
34+
def _create_db_schema(conn) -> None:
35+
"""Create a database schema if it doesn't exist."""
36+
try:
37+
"""Create a database schema if it doesn't exist."""
38+
conn.execute(text("CREATE SCHEMA IF NOT EXISTS happy_hog"))
39+
conn.execute(text("CREATE SCHEMA IF NOT EXISTS shakespeare"))
40+
except ProgrammingError:
41+
# This might be raised by databases that don't support `IF NOT EXISTS`
42+
# and the schema already exists. You can choose to ignore it.
43+
pass
44+
2245

2346
@pytest.fixture(scope="session")
2447
async def start_db():
25-
async with engine.begin() as conn:
48+
# The `engine` is configured for the default 'postgres' database.
49+
# We connect to it and create the test database.
50+
# A transaction block is not used, as CREATE DATABASE cannot run inside it.
51+
async with engine.connect() as conn:
52+
await conn.execute(text("COMMIT")) # Ensure we're not in a transaction
53+
await conn.run_sync(_create_db)
54+
55+
# Now, connect to the newly created `testdb` with `test_engine`
56+
async with test_engine.begin() as conn:
57+
await conn.run_sync(_create_db_schema)
2658
await conn.run_sync(Base.metadata.drop_all)
2759
await conn.run_sync(Base.metadata.create_all)
2860
# for AsyncEngine created in function scope, close and
2961
# clean-up pooled connections
3062
await engine.dispose()
63+
await test_engine.dispose()
3164

3265

3366
@pytest.fixture(scope="session")
@@ -40,5 +73,6 @@ async def client(start_db) -> AsyncGenerator[AsyncClient, Any]: # noqa: ARG001
4073
headers={"Content-Type": "application/json"},
4174
transport=transport,
4275
) as test_client:
76+
app.dependency_overrides[get_db] = get_test_db
4377
app.redis = await get_redis()
4478
yield test_client

uv.lock

Lines changed: 23 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)