mirror of
https://github.com/fastapi/full-stack-fastapi-template.git
synced 2026-11-03 12:29:42 +00:00
♻️ Simplify database readiness checks (#2460)
This commit is contained in:
committed by
GitHub
parent
2ccfa25845
commit
f8dd304eeb
@@ -26,7 +26,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: "latest-known"
|
version: "latest-known"
|
||||||
- run: docker compose down -v --remove-orphans
|
- run: docker compose down -v --remove-orphans
|
||||||
- run: docker compose up -d db mailpit
|
- run: docker compose up -d --wait db mailpit
|
||||||
- name: Migrate DB
|
- name: Migrate DB
|
||||||
run: uv run bash scripts/prestart.sh
|
run: uv run bash scripts/prestart.sh
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
|
|||||||
+1
-1
@@ -78,7 +78,7 @@ If your stack is already up and you just want to run the tests, you can use:
|
|||||||
docker compose exec backend bash scripts/tests-start.sh
|
docker compose exec backend bash scripts/tests-start.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The `/app/backend/scripts/tests-start.sh` script calls `pytest` after making sure that the rest of the stack is running. If you need to pass extra arguments to `pytest`, you can pass them to that command and they will be forwarded.
|
The `/app/backend/scripts/tests-start.sh` script calls `pytest`. If you need to pass extra arguments to `pytest`, you can pass them to that command and they will be forwarded.
|
||||||
|
|
||||||
For example, to stop on first error:
|
For example, to stop on first error:
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from sqlalchemy import Engine
|
|
||||||
from sqlmodel import Session, select
|
|
||||||
from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed
|
|
||||||
|
|
||||||
from app.core.db import engine
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
max_tries = 60 * 5 # 5 minutes
|
|
||||||
wait_seconds = 1
|
|
||||||
|
|
||||||
|
|
||||||
@retry(
|
|
||||||
stop=stop_after_attempt(max_tries),
|
|
||||||
wait=wait_fixed(wait_seconds),
|
|
||||||
before=before_log(logger, logging.INFO),
|
|
||||||
after=after_log(logger, logging.WARN),
|
|
||||||
)
|
|
||||||
def init(db_engine: Engine) -> None:
|
|
||||||
try:
|
|
||||||
with Session(db_engine) as session:
|
|
||||||
# Try to create session to check if DB is awake
|
|
||||||
session.exec(select(1))
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
logger.info("Initializing service")
|
|
||||||
init(engine)
|
|
||||||
logger.info("Service finished initializing")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -4,7 +4,7 @@ from app import crud
|
|||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.models import User, UserCreate
|
from app.models import User, UserCreate
|
||||||
|
|
||||||
engine = create_engine(str(settings.DATABASE_URL))
|
engine = create_engine(str(settings.DATABASE_URL), pool_pre_ping=True)
|
||||||
|
|
||||||
|
|
||||||
# make sure all SQLModel models are imported (app.models) before initializing DB
|
# make sure all SQLModel models are imported (app.models) before initializing DB
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from sqlalchemy import Engine
|
|
||||||
from sqlmodel import Session, select
|
|
||||||
from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed
|
|
||||||
|
|
||||||
from app.core.db import engine
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
max_tries = 60 * 5 # 5 minutes
|
|
||||||
wait_seconds = 1
|
|
||||||
|
|
||||||
|
|
||||||
@retry(
|
|
||||||
stop=stop_after_attempt(max_tries),
|
|
||||||
wait=wait_fixed(wait_seconds),
|
|
||||||
before=before_log(logger, logging.INFO),
|
|
||||||
after=after_log(logger, logging.WARN),
|
|
||||||
)
|
|
||||||
def init(db_engine: Engine) -> None:
|
|
||||||
try:
|
|
||||||
# Try to create session to check if DB is awake
|
|
||||||
with Session(db_engine) as session:
|
|
||||||
session.exec(select(1))
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
logger.info("Initializing service")
|
|
||||||
init(engine)
|
|
||||||
logger.info("Service finished initializing")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -7,7 +7,6 @@ dependencies = [
|
|||||||
"fastapi[standard]>=0.141.1,<1.0.0",
|
"fastapi[standard]>=0.141.1,<1.0.0",
|
||||||
"python-multipart<1.0.0,>=0.0.27",
|
"python-multipart<1.0.0,>=0.0.27",
|
||||||
"email-validator<3.0.0.0,>=2.1.0.post1",
|
"email-validator<3.0.0.0,>=2.1.0.post1",
|
||||||
"tenacity<10.0.0,>=8.2.3",
|
|
||||||
"pydantic>2.0",
|
"pydantic>2.0",
|
||||||
"emails>=1.1.2,<2.0",
|
"emails>=1.1.2,<2.0",
|
||||||
"jinja2<4.0.0,>=3.1.4",
|
"jinja2<4.0.0,>=3.1.4",
|
||||||
|
|||||||
@@ -3,9 +3,6 @@
|
|||||||
set -e
|
set -e
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
# Let the DB start
|
|
||||||
python app/backend_pre_start.py
|
|
||||||
|
|
||||||
# Run migrations
|
# Run migrations
|
||||||
alembic upgrade head
|
alembic upgrade head
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,4 @@
|
|||||||
set -e
|
set -e
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
python app/tests_pre_start.py
|
|
||||||
|
|
||||||
bash scripts/test.sh "$@"
|
bash scripts/test.sh "$@"
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
from sqlmodel import select
|
|
||||||
|
|
||||||
from app.backend_pre_start import init, logger
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_successful_connection() -> None:
|
|
||||||
engine_mock = MagicMock()
|
|
||||||
|
|
||||||
session_mock = MagicMock()
|
|
||||||
session_mock.__enter__.return_value = session_mock
|
|
||||||
|
|
||||||
select1 = select(1)
|
|
||||||
|
|
||||||
with (
|
|
||||||
patch("app.backend_pre_start.Session", return_value=session_mock),
|
|
||||||
patch("app.backend_pre_start.select", return_value=select1),
|
|
||||||
patch.object(logger, "info"),
|
|
||||||
patch.object(logger, "error"),
|
|
||||||
patch.object(logger, "warn"),
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
init(engine_mock)
|
|
||||||
connection_successful = True
|
|
||||||
except Exception:
|
|
||||||
connection_successful = False
|
|
||||||
|
|
||||||
assert connection_successful, (
|
|
||||||
"The database connection should be successful and not raise an exception."
|
|
||||||
)
|
|
||||||
|
|
||||||
session_mock.exec.assert_called_once_with(select1)
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
from sqlmodel import select
|
|
||||||
|
|
||||||
from app.tests_pre_start import init, logger
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_successful_connection() -> None:
|
|
||||||
engine_mock = MagicMock()
|
|
||||||
|
|
||||||
session_mock = MagicMock()
|
|
||||||
session_mock.__enter__.return_value = session_mock
|
|
||||||
|
|
||||||
select1 = select(1)
|
|
||||||
|
|
||||||
with (
|
|
||||||
patch("app.tests_pre_start.Session", return_value=session_mock),
|
|
||||||
patch("app.tests_pre_start.select", return_value=select1),
|
|
||||||
patch.object(logger, "info"),
|
|
||||||
patch.object(logger, "error"),
|
|
||||||
patch.object(logger, "warn"),
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
init(engine_mock)
|
|
||||||
connection_successful = True
|
|
||||||
except Exception:
|
|
||||||
connection_successful = False
|
|
||||||
|
|
||||||
assert connection_successful, (
|
|
||||||
"The database connection should be successful and not raise an exception."
|
|
||||||
)
|
|
||||||
|
|
||||||
session_mock.exec.assert_called_once_with(select1)
|
|
||||||
@@ -82,7 +82,6 @@ dependencies = [
|
|||||||
{ name = "python-multipart" },
|
{ name = "python-multipart" },
|
||||||
{ name = "sentry-sdk", extra = ["fastapi"] },
|
{ name = "sentry-sdk", extra = ["fastapi"] },
|
||||||
{ name = "sqlmodel" },
|
{ name = "sqlmodel" },
|
||||||
{ name = "tenacity" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
@@ -110,7 +109,6 @@ requires-dist = [
|
|||||||
{ name = "python-multipart", specifier = ">=0.0.27,<1.0.0" },
|
{ name = "python-multipart", specifier = ">=0.0.27,<1.0.0" },
|
||||||
{ name = "sentry-sdk", extras = ["fastapi"], specifier = ">=2.66.1,<3.0.0" },
|
{ name = "sentry-sdk", extras = ["fastapi"], specifier = ">=2.66.1,<3.0.0" },
|
||||||
{ name = "sqlmodel", specifier = ">=0.0.39,<1.0.0" },
|
{ name = "sqlmodel", specifier = ">=0.0.39,<1.0.0" },
|
||||||
{ name = "tenacity", specifier = ">=8.2.3,<10.0.0" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
@@ -1284,15 +1282,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tenacity"
|
|
||||||
version = "9.1.4"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ty"
|
name = "ty"
|
||||||
version = "0.0.54"
|
version = "0.0.54"
|
||||||
|
|||||||
Reference in New Issue
Block a user