diff --git a/pyproject.toml b/pyproject.toml index ebdc2bb..364d711 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,6 +102,19 @@ detached = true compile = "pip-compile {args:} --generate-hashes --output-file requirements.txt" update = "compile --upgrade" +[tool.hatch.envs.docs] +dependencies = [ + "mkdocs~=1.5.2", + "mkdocs-material~=9.1.21", + "mkdocstrings~=0.22.0", +] +detached = true + +[tool.hatch.envs.docs.scripts] +build = ["mkdocs build --clean --strict"] +deploy = ["mkdocs gh-deploy {args:}"] +serve = ["mkdocs serve --dev-addr localhost:8080 --livereload"] + [tool.hatch.envs.lint] dependencies = [ "black~=23.7.0", @@ -183,10 +196,6 @@ select = [ "YTT" ] target-version = "py38" -unfixable = [ - # Don't touch unused imports - "F401" -] [tool.ruff.flake8-tidy-imports] ban-relative-imports = "all" diff --git a/zoo/app.py b/zoo/app.py index 841b9df..6ddbaa7 100644 --- a/zoo/app.py +++ b/zoo/app.py @@ -8,8 +8,8 @@ from fastapi import FastAPI from zoo._version import __application__, __description__, __version__ -from zoo.application.animals import animals_router -from zoo.application.utils import utils_router +from zoo.backend.animals import animals_router +from zoo.backend.utils import utils_router from zoo.config import config from zoo.db import init_db diff --git a/zoo/application/__init__.py b/zoo/backend/__init__.py similarity index 100% rename from zoo/application/__init__.py rename to zoo/backend/__init__.py diff --git a/zoo/application/animals.py b/zoo/backend/animals.py similarity index 80% rename from zoo/application/animals.py rename to zoo/backend/animals.py index 721b2d9..b74db5c 100644 --- a/zoo/application/animals.py +++ b/zoo/backend/animals.py @@ -6,6 +6,7 @@ from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy import func from sqlalchemy.ext.asyncio import AsyncSession from sqlmodel import select @@ -19,12 +20,16 @@ @animals_router.get("/animals", response_model=List[AnimalsRead]) async def get_animals( - offset: int = 0, limit: int = Query(default=100, le=100), session: AsyncSession = Depends(get_session) + offset: int = 0, + limit: int = Query(default=100, le=100), + session: AsyncSession = Depends(get_session), ) -> List[Animals]: """ Get animals from the database """ - result = await session.execute(select(Animals).offset(offset).limit(limit)) + result = await session.execute( + select(Animals).where(Animals.deleted_at.is_(None)).offset(offset).limit(limit) # type: ignore[union-attr] + ) animals: List[Animals] = result.scalars().all() return animals @@ -47,7 +52,7 @@ async def get_animal(animal_id: int, session: AsyncSession = Depends(get_session Get an animal from the database """ animal: Optional[Animals] = await session.get(Animals, animal_id) - if animal is None: + if animal is None or animal.deleted_at is not None: raise HTTPException(status_code=404, detail=f"Error: Animal not found - ID: {animal_id}") return animal @@ -58,10 +63,12 @@ async def delete_animal(animal_id: int, session: AsyncSession = Depends(get_sess Delete an animal from the database """ animal: Optional[Animals] = await session.get(Animals, animal_id) - if animal is None: + if animal is None or animal.deleted_at is not None: raise HTTPException(status_code=404, detail=f"Error: Animal not found - ID # {animal_id}") - await session.delete(animal) + animal.deleted_at = func.CURRENT_TIMESTAMP() # type: ignore[assignment] + session.add(animal) await session.commit() + await session.refresh(animal) return animal @@ -71,7 +78,7 @@ async def update_animal(animal_id: int, animal: AnimalsUpdate, session: AsyncSes Update an animal in the database """ db_animal: Optional[Animals] = await session.get(Animals, animal_id) - if db_animal is None: + if db_animal is None or db_animal.deleted_at is not None: raise HTTPException(status_code=404, detail=f"Error: Animal not found - ID: {animal_id}") for field, value in animal.dict(exclude_unset=True).items(): if value is not None: diff --git a/zoo/application/utils.py b/zoo/backend/utils.py similarity index 100% rename from zoo/application/utils.py rename to zoo/backend/utils.py diff --git a/zoo/db.py b/zoo/db.py index 33cae5e..400f8ca 100644 --- a/zoo/db.py +++ b/zoo/db.py @@ -17,6 +17,7 @@ config = ZooSettings() engine = create_async_engine(config.connection_string, echo=True, future=True) +async_session = sessionmaker(engine, class_=AsyncSession, autocommit=False, expire_on_commit=False, autoflush=False) @listens_for(Animals.__table__, "after_create") # type: ignore[attr-defined] @@ -42,6 +43,8 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]: Used by FastAPI Depends """ - async_session = sessionmaker(engine, class_=AsyncSession, autocommit=False, expire_on_commit=False) - async with async_session() as session: - yield session + try: + async with async_session() as session: + yield session + finally: + await session.close() diff --git a/zoo/models/animals.py b/zoo/models/animals.py index 7868c3e..fadf4cf 100644 --- a/zoo/models/animals.py +++ b/zoo/models/animals.py @@ -8,14 +8,15 @@ from sqlalchemy.future import Connection from sqlmodel import Field, SQLModel -from zoo.models.base import CreatedModifiedMixin, OptionalIdMixin, RequiredIdMixin +from zoo.models.base import CreatedModifiedMixin, DeletedMixin, OptionalIdMixin, RequiredIdMixin _animal_example = { "name": "Lion", "description": "Ferocious kitty", "species": "Panthera leo", - "created": "2021-01-01T00:00:00.000000", - "modified": "2021-01-02T09:12:34.567890", + "deleted_at": None, + "created_at": "2021-01-01T00:00:00.000000", + "modified_at": "2021-01-02T09:12:34.567890", } @@ -42,7 +43,12 @@ class Config: schema_extra: ClassVar[Dict[str, Any]] = {"examples": [_animal_example]} -class AnimalsRead(AnimalsBase, RequiredIdMixin, CreatedModifiedMixin): +class AnimalsRead( + DeletedMixin, + CreatedModifiedMixin, + AnimalsBase, + RequiredIdMixin, +): """ Animals model: read """ @@ -55,7 +61,7 @@ class Config: schema_extra: ClassVar[Dict[str, Any]] = {"examples": [{"id": 1, **_animal_example}]} -class Animals(AnimalsBase, CreatedModifiedMixin, OptionalIdMixin, table=True): +class Animals(DeletedMixin, CreatedModifiedMixin, AnimalsBase, OptionalIdMixin, table=True): """ Animals model: database table """ diff --git a/zoo/models/base.py b/zoo/models/base.py index f06c7a9..a9acfae 100644 --- a/zoo/models/base.py +++ b/zoo/models/base.py @@ -34,13 +34,13 @@ class CreatedModifiedMixin(SQLModel): Created and modified mixin """ - created: datetime.datetime = Field( + created_at: datetime.datetime = Field( default_factory=datetime.datetime.utcnow, nullable=False, description="The date and time the record was created", sa_column=Column(DateTime(timezone=True), server_default=func.CURRENT_TIMESTAMP()), ) - modified: datetime.datetime = Field( + modified_at: datetime.datetime = Field( default_factory=datetime.datetime.utcnow, nullable=False, description="The date and time the record was last modified", @@ -48,3 +48,16 @@ class CreatedModifiedMixin(SQLModel): DateTime(timezone=True), server_default=func.CURRENT_TIMESTAMP(), onupdate=func.CURRENT_TIMESTAMP() ), ) + + +class DeletedMixin(SQLModel): + """ + Deleted mixin + """ + + deleted_at: Optional[datetime.datetime] = Field( + default=None, + nullable=True, + description="The date and time the record was deleted", + sa_column=Column(DateTime(timezone=True), default=None, nullable=True, sort_order=-1), + )