From 88b59d5d9b8e34fd05c66bbf34a876af96cd8a12 Mon Sep 17 00:00:00 2001 From: itsjoxdev Date: Fri, 24 Jul 2026 17:39:49 +0300 Subject: [PATCH] feat(api): complete backend foundation --- apps/api/alembic.ini | 38 +++++++++- apps/api/core/config/settings.py | 26 ++++++- apps/api/infrastructure/db/base.py | 5 +- apps/api/infrastructure/db/migrations/env.py | 70 +++++++++++++++++++ .../db/migrations/script.py.mako | 26 +++++++ apps/api/infrastructure/db/session.py | 22 +++++- apps/api/pyproject.toml | 11 +++ apps/api/requirements/base.txt | 2 +- 8 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 apps/api/infrastructure/db/migrations/env.py create mode 100644 apps/api/infrastructure/db/migrations/script.py.mako diff --git a/apps/api/alembic.ini b/apps/api/alembic.ini index a5cc207..9782425 100644 --- a/apps/api/alembic.ini +++ b/apps/api/alembic.ini @@ -1,4 +1,38 @@ -# Alembic configuration [alembic] script_location = infrastructure/db/migrations -# TODO: Configure Alembic settings +prepend_sys_path = . +version_path_separator = os + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/apps/api/core/config/settings.py b/apps/api/core/config/settings.py index c997b57..4704f75 100644 --- a/apps/api/core/config/settings.py +++ b/apps/api/core/config/settings.py @@ -1 +1,25 @@ -# TODO: Implement configuration settings using Pydantic BaseSettings +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict +import os + +# Ensure the .env file from the project root is targeted +ENV_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../.env")) + +class Settings(BaseSettings): + APP_NAME: str = "TicketFlow" + APP_ENV: str = "development" + DEBUG: bool = Field(default=True, alias="APP_DEBUG") + DATABASE_URL: str + SECRET_KEY: str = Field(alias="JWT_SECRET_KEY") + ALGORITHM: str = Field(default="HS256", alias="JWT_ALGORITHM") + ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 + REFRESH_TOKEN_EXPIRE_DAYS: int = 7 + CORS_ORIGINS: list[str] | str = ["*"] + + model_config = SettingsConfigDict( + env_file=ENV_PATH, + env_file_encoding="utf-8", + extra="ignore" + ) + +settings = Settings() diff --git a/apps/api/infrastructure/db/base.py b/apps/api/infrastructure/db/base.py index dcd041b..f75ec44 100644 --- a/apps/api/infrastructure/db/base.py +++ b/apps/api/infrastructure/db/base.py @@ -1 +1,4 @@ -# TODO: Define declarative base for SQLAlchemy +from sqlalchemy.orm import DeclarativeBase + +class Base(DeclarativeBase): + pass diff --git a/apps/api/infrastructure/db/migrations/env.py b/apps/api/infrastructure/db/migrations/env.py new file mode 100644 index 0000000..0eac8e2 --- /dev/null +++ b/apps/api/infrastructure/db/migrations/env.py @@ -0,0 +1,70 @@ +import asyncio +import sys +import os +from logging.config import fileConfig + +from sqlalchemy.ext.asyncio import async_engine_from_config +from sqlalchemy import pool +from alembic import context + +# Add project root to sys.path so apps.api imports work +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../.."))) + +# pyrefly: ignore [missing-import] +from apps.api.core.config.settings import settings +# pyrefly: ignore [missing-import] +from apps.api.infrastructure.db.base import Base + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata + +def get_url(): + return settings.DATABASE_URL + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode.""" + url = get_url() + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + +def do_run_migrations(connection) -> None: + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + +async def run_async_migrations() -> None: + """In this scenario we need to create an Engine + and associate a connection with the context. + """ + configuration = config.get_section(config.config_ini_section) + configuration["sqlalchemy.url"] = get_url() + connectable = async_engine_from_config( + configuration, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + await connectable.dispose() + +def run_migrations_online() -> None: + """Run migrations in 'online' mode.""" + asyncio.run(run_async_migrations()) + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/apps/api/infrastructure/db/migrations/script.py.mako b/apps/api/infrastructure/db/migrations/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/apps/api/infrastructure/db/migrations/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/apps/api/infrastructure/db/session.py b/apps/api/infrastructure/db/session.py index 1ef32ce..1d36543 100644 --- a/apps/api/infrastructure/db/session.py +++ b/apps/api/infrastructure/db/session.py @@ -1 +1,21 @@ -# TODO: Configure database session maker +from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession +from typing import AsyncGenerator +from apps.api.core.config.settings import settings + +engine = create_async_engine( + settings.DATABASE_URL, + echo=settings.DEBUG, + future=True, +) + +SessionLocal = async_sessionmaker( + bind=engine, + class_=AsyncSession, + expire_on_commit=False, + autoflush=False, + autocommit=False, +) + +async def get_db() -> AsyncGenerator[AsyncSession, None]: + async with SessionLocal() as session: + yield session diff --git a/apps/api/pyproject.toml b/apps/api/pyproject.toml index 0533444..dc728ac 100644 --- a/apps/api/pyproject.toml +++ b/apps/api/pyproject.toml @@ -4,6 +4,17 @@ version = "0.1.0" description = "TicketFlow Backend API" authors = ["itsjoxdev"] +[tool.poetry.dependencies] +python = "^3.12" +fastapi = "*" +pydantic = "*" +pydantic-settings = "*" +sqlalchemy = "*" +alembic = "*" +asyncpg = "*" +python-jose = {extras = ["cryptography"], version = "*"} +passlib = {extras = ["bcrypt"], version = "*"} + # TODO: Add build dependencies [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/apps/api/requirements/base.txt b/apps/api/requirements/base.txt index c664f93..47f3ad2 100644 --- a/apps/api/requirements/base.txt +++ b/apps/api/requirements/base.txt @@ -1,9 +1,9 @@ # Base dependencies fastapi pydantic +pydantic-settings sqlalchemy alembic asyncpg python-jose[cryptography] passlib[bcrypt] -# TODO: Add additional base dependencies