diff --git a/Makefile b/Makefile index 6bafa7d..3d6ef3a 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ docker-apply-db-migrations: ## apply alembic migrations to database/schema docker compose run --rm app alembic upgrade head .PHONY: docker-create-db-migration -docker-create-db-migration: ## Create new alembic database migration aka database revision. +docker-create-db-migration: ## Create new alembic database migration aka database revision. Example: make docker-create-db-migration msg="add users table" docker compose up -d db | true docker compose run --no-deps app alembic revision --autogenerate -m "$(msg)" diff --git a/alembic/versions/20250729_1521_d021bd4763a5_add_json_chaos.py b/alembic/versions/20250729_1521_d021bd4763a5_add_json_chaos.py new file mode 100644 index 0000000..a629c14 --- /dev/null +++ b/alembic/versions/20250729_1521_d021bd4763a5_add_json_chaos.py @@ -0,0 +1,37 @@ +"""add json chaos + +Revision ID: d021bd4763a5 +Revises: 0c69050b5a3e +Create Date: 2025-07-29 15:21:19.415583 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'd021bd4763a5' +down_revision = '0c69050b5a3e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('random_stuff', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('chaos', postgresql.JSON(astext_type=sa.Text()), nullable=False), + sa.PrimaryKeyConstraint('id'), + schema='happy_hog' + ) + op.create_unique_constraint(None, 'nonsense', ['name'], schema='happy_hog') + op.create_unique_constraint(None, 'stuff', ['name'], schema='happy_hog') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'stuff', schema='happy_hog', type_='unique') + op.drop_constraint(None, 'nonsense', schema='happy_hog', type_='unique') + op.drop_table('random_stuff', schema='happy_hog') + # ### end Alembic commands ### diff --git a/app/api/stuff.py b/app/api/stuff.py index 379b661..7ff8ee6 100644 --- a/app/api/stuff.py +++ b/app/api/stuff.py @@ -4,7 +4,8 @@ from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db -from app.models.stuff import Stuff +from app.models.stuff import RandomStuff, Stuff +from app.schemas.stuff import RandomStuff as RandomStuffSchema from app.schemas.stuff import StuffResponse, StuffSchema logger = AppStructLogger().get_logger() @@ -12,6 +13,15 @@ logger = AppStructLogger().get_logger() router = APIRouter(prefix="/v1/stuff") +@router.post("/random", status_code=status.HTTP_201_CREATED) +async def create_random_stuff( + payload: RandomStuffSchema, db_session: AsyncSession = Depends(get_db) +) -> dict[str, str]: + random_stuff = RandomStuff(**payload.model_dump()) + await random_stuff.save(db_session) + return {"id": str(random_stuff.id)} + + @router.post("/add_many", status_code=status.HTTP_201_CREATED) async def create_multi_stuff( payload: list[StuffSchema], db_session: AsyncSession = Depends(get_db) diff --git a/app/main.py b/app/main.py index 63a016c..dab1176 100644 --- a/app/main.py +++ b/app/main.py @@ -20,6 +20,7 @@ from app.services.auth import AuthBearer logger = AppStructLogger().get_logger() templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates") + @asynccontextmanager async def lifespan(app: FastAPI): app.redis = await get_redis() @@ -30,12 +31,15 @@ async def lifespan(app: FastAPI): min_size=5, max_size=20, ) - await logger.ainfo("Postgres pool created", idle_size=app.postgres_pool.get_idle_size()) + await logger.ainfo( + "Postgres pool created", idle_size=app.postgres_pool.get_idle_size() + ) yield finally: await app.redis.close() await app.postgres_pool.close() + def create_app() -> FastAPI: app = FastAPI( title="Stuff And Nonsense API", @@ -47,7 +51,9 @@ def create_app() -> FastAPI: app.include_router(shakespeare_router) app.include_router(user_router) app.include_router(ml_router, prefix="/v1/ml", tags=["ML"]) - app.include_router(health_router, prefix="/v1/public/health", tags=["Health, Public"]) + app.include_router( + health_router, prefix="/v1/public/health", tags=["Health, Public"] + ) app.include_router( health_router, prefix="/v1/health", @@ -61,6 +67,7 @@ def create_app() -> FastAPI: return app + app = create_app() # --- Unused/experimental code and TODOs --- diff --git a/app/models/base.py b/app/models/base.py index 5e059df..b8665a1 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -27,7 +27,9 @@ class Base(DeclarativeBase): """ try: db_session.add(self) - return await db_session.commit() + await db_session.commit() + await db_session.refresh(self) + return self except SQLAlchemyError as ex: await logger.aerror(f"Error inserting instance of {self}: {repr(ex)}") raise HTTPException( diff --git a/app/models/stuff.py b/app/models/stuff.py index 248d7e3..1edf5c1 100644 --- a/app/models/stuff.py +++ b/app/models/stuff.py @@ -1,7 +1,7 @@ import uuid from sqlalchemy import ForeignKey, String, select -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.dialects.postgresql import JSON, UUID from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Mapped, joinedload, mapped_column, relationship @@ -10,6 +10,16 @@ from app.models.nonsense import Nonsense from app.utils.decorators import compile_sql_or_scalar +class RandomStuff(Base): + __tablename__ = "random_stuff" + __table_args__ = ({"schema": "happy_hog"},) + + id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), default=uuid.uuid4, primary_key=True + ) + chaos: Mapped[dict] = mapped_column(JSON) + + class Stuff(Base): __tablename__ = "stuff" __table_args__ = ({"schema": "happy_hog"},) diff --git a/app/schemas/stuff.py b/app/schemas/stuff.py index d5703b2..18b3700 100644 --- a/app/schemas/stuff.py +++ b/app/schemas/stuff.py @@ -1,3 +1,4 @@ +from typing import Any from uuid import UUID from pydantic import BaseModel, ConfigDict, Field @@ -5,6 +6,10 @@ from pydantic import BaseModel, ConfigDict, Field config = ConfigDict(from_attributes=True) +class RandomStuff(BaseModel): + chaos: dict[str, Any] = Field(..., description="JSON data for chaos field") + + class StuffSchema(BaseModel): name: str = Field( title="", diff --git a/app/utils/logging.py b/app/utils/logging.py index 293efbe..1aece2b 100644 --- a/app/utils/logging.py +++ b/app/utils/logging.py @@ -30,7 +30,7 @@ class RotatingBytesLogger: lineno=0, msg=message.rstrip("\n"), args=(), - exc_info=None + exc_info=None, ) # Check if rotation is needed before emitting @@ -78,7 +78,7 @@ class AppStructLogger(metaclass=SingletonMetaNoArgs): filename=_log_path, maxBytes=10 * 1024 * 1024, # 10MB backupCount=5, - encoding="utf-8" + encoding="utf-8", ) structlog.configure( cache_logger_on_first_use=True, @@ -90,7 +90,7 @@ class AppStructLogger(metaclass=SingletonMetaNoArgs): structlog.processors.TimeStamper(fmt="iso", utc=True), structlog.processors.JSONRenderer(serializer=orjson.dumps), ], - logger_factory=RotatingBytesLoggerFactory(_handler) + logger_factory=RotatingBytesLoggerFactory(_handler), ) self._logger = structlog.get_logger() diff --git a/compose.yml b/compose.yml index 293db32..d9fdc99 100644 --- a/compose.yml +++ b/compose.yml @@ -18,6 +18,7 @@ services: - ./app:/panettone/app - ./tests:/panettone/tests - ./templates:/panettone/templates + - ./alembic:/panettone/alembic ports: - "8080:8080" depends_on: