aboutsummaryrefslogtreecommitdiffstats
path: root/thallium-backend/src
diff options
context:
space:
mode:
Diffstat (limited to 'thallium-backend/src')
-rw-r--r--thallium-backend/src/__init__.py6
-rw-r--r--thallium-backend/src/orm/__init__.py12
-rw-r--r--thallium-backend/src/orm/base.py44
-rw-r--r--thallium-backend/src/settings.py43
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)]