diff options
Diffstat (limited to 'thallium-backend/src')
| -rw-r--r-- | thallium-backend/src/__init__.py | 6 | ||||
| -rw-r--r-- | thallium-backend/src/orm/__init__.py | 12 | ||||
| -rw-r--r-- | thallium-backend/src/orm/base.py | 44 | ||||
| -rw-r--r-- | thallium-backend/src/settings.py | 43 |
4 files changed, 102 insertions, 3 deletions
diff --git a/thallium-backend/src/__init__.py b/thallium-backend/src/__init__.py index 87f0d37..a2436a2 100644 --- a/thallium-backend/src/__init__.py +++ b/thallium-backend/src/__init__.py @@ -1,7 +1,13 @@ +import asyncio import logging +import os from src.settings import CONFIG +# On Windows, the selector event loop is required for aiodns. +if os.name == "nt": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + # Console handler prints to terminal console_handler = logging.StreamHandler() level = logging.DEBUG if CONFIG.debug else logging.INFO diff --git a/thallium-backend/src/orm/__init__.py b/thallium-backend/src/orm/__init__.py new file mode 100644 index 0000000..ed803e8 --- /dev/null +++ b/thallium-backend/src/orm/__init__.py @@ -0,0 +1,12 @@ +"""Database models.""" + +from .base import AuditBase, Base +from .products import Product +from .users import User + +__all__ = ( + "AuditBase", + "Base", + "Product", + "User", +) diff --git a/thallium-backend/src/orm/base.py b/thallium-backend/src/orm/base.py new file mode 100644 index 0000000..a1642c7 --- /dev/null +++ b/thallium-backend/src/orm/base.py @@ -0,0 +1,44 @@ +"""The base classes for ORM models.""" + +import re +from datetime import datetime +from uuid import UUID, uuid4 + +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncAttrs +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column +from sqlalchemy.schema import MetaData +from sqlalchemy.sql import func +from sqlalchemy.types import DateTime + +NAMING_CONVENTIONS = { + "ix": "%(column_0_label)s_ix", + "uq": "%(table_name)s_%(column_0_name)s_uq", + "ck": "%(table_name)s_%(constraint_name)s_ck", + "fk": "%(table_name)s_%(column_0_name)s_%(referred_table_name)s_fk", + "pk": "%(table_name)s_pk", +} +table_name_regexp = re.compile("((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))") + + +class Base(AsyncAttrs, DeclarativeBase): + """Classes that inherit this class will be automatically mapped using declarative mapping.""" + + metadata = MetaData(naming_convention=NAMING_CONVENTIONS) + + def patch_from_pydantic(self, pydantic_model: BaseModel) -> None: + """Patch this model using the given pydantic model, unspecified attributes remain the same.""" + for key, value in pydantic_model.model_dump(exclude_unset=True).items(): + setattr(self, key, value) + + +class AuditBase: + """Common columns for a table with UUID PK and datetime audit columns.""" + + id: Mapped[UUID] = mapped_column(default=uuid4, primary_key=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + ) diff --git a/thallium-backend/src/settings.py b/thallium-backend/src/settings.py index 2f9f6c1..931fc0d 100644 --- a/thallium-backend/src/settings.py +++ b/thallium-backend/src/settings.py @@ -1,9 +1,46 @@ -from pydantic_settings import BaseSettings +import typing +from collections.abc import AsyncGenerator +from logging import getLogger +import pydantic +import pydantic_settings +from fastapi import Depends +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine + +log = getLogger(__name__) + + +class _Config( + pydantic_settings.BaseSettings, + env_prefix="backend_", + env_file=".env", + env_file_encoding="utf-8", + env_nested_delimiter="__", + extra="ignore", +): + """General configuration settings for the service.""" -class _CONFIG(BaseSettings, env_file=".env", env_file_encoding="utf-8"): debug: bool = False git_sha: str = "development" + database_url: pydantic.SecretStr + token: pydantic.SecretStr + + +CONFIG = _Config() + + +class Connections: + """How to connect to other, internal services.""" + + DB_ENGINE = create_async_engine(CONFIG.database_url.get_secret_value(), echo=CONFIG.debug) + DB_SESSION_MAKER = async_sessionmaker(DB_ENGINE) + + +async def _get_db_session() -> AsyncGenerator[AsyncSession, None]: + """Yield a database session, for use with a FastAPI dependency.""" + async with Connections.DB_SESSION_MAKER() as session, session.begin(): + yield session + -CONFIG = _CONFIG() +DBSession = typing.Annotated[AsyncSession, Depends(_get_db_session)] |