Add Anime Merchandise Catalog API with FastAPI and SQLite

This commit is contained in:
Automated Action 2025-05-18 14:15:16 +00:00
parent c6e55fcc9f
commit f2f97cdc27
29 changed files with 1054 additions and 2 deletions

106
README.md
View File

@ -1,3 +1,105 @@
# FastAPI Application
# Anime Merchandise Catalog API
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
A FastAPI-based API for managing an anime merchandise catalog.
## Features
- **Product Management:** Create, read, update, and delete anime merchandise products
- **Category System:** Organize products with categories
- **Filtering and Searching:** Filter products by anime title or character name
- **API Documentation:** Auto-generated OpenAPI documentation
## Tech Stack
- **Framework:** FastAPI
- **Database:** SQLite with SQLAlchemy and Alembic
- **Authentication:** (Not implemented in this version)
## Project Structure
```
.
├── app # Main application package
│ ├── api # API routes and endpoint definitions
│ │ └── routes # API route handlers
│ ├── core # Core application components
│ ├── dependencies # Dependency injection components
│ ├── models # SQLAlchemy database models
│ ├── schemas # Pydantic schemas for request/response validation
│ └── services # Business logic services
├── migrations # Alembic database migrations
│ └── versions # Migration version scripts
├── storage # Storage directory for SQLite database
│ └── db # Database files
├── alembic.ini # Alembic configuration
├── main.py # Application entry point
└── requirements.txt # Python dependencies
```
## API Routes
### Health Check
- `GET /health` - Check API health status
### Products
- `GET /api/v1/products` - List all products (with optional filtering)
- `GET /api/v1/products/{product_id}` - Get a specific product by ID
- `POST /api/v1/products` - Create a new product
- `PUT /api/v1/products/{product_id}` - Update an existing product
- `DELETE /api/v1/products/{product_id}` - Delete a product
### Categories
- `GET /api/v1/categories` - List all categories
- `GET /api/v1/categories/{category_id}` - Get a specific category by ID
- `GET /api/v1/categories/{category_id}/products` - Get a category with its products
- `POST /api/v1/categories` - Create a new category
- `PUT /api/v1/categories/{category_id}` - Update an existing category
- `DELETE /api/v1/categories/{category_id}` - Delete a category
## Installation
1. Clone the repository
2. Install dependencies:
```bash
pip install -r requirements.txt
```
3. Apply migrations to create the database schema:
```bash
alembic upgrade head
```
## Running the Application
```bash
uvicorn main:app --reload
```
The API will be available at `http://localhost:8000`
## API Documentation
- Swagger UI: `http://localhost:8000/docs`
- ReDoc: `http://localhost:8000/redoc`
## Database Schema
### Product
- id: Integer (Primary Key)
- name: String
- description: Text (Optional)
- price: Float
- stock: Integer
- image_url: String (Optional)
- anime_title: String (Optional)
- character_name: String (Optional)
- created_at: DateTime
- updated_at: DateTime
- categories: Many-to-Many relationship with Category
### Category
- id: Integer (Primary Key)
- name: String (Unique)
- description: Text (Optional)
- created_at: DateTime
- updated_at: DateTime
- products: Many-to-Many relationship with Product

83
alembic.ini Normal file
View File

@ -0,0 +1,83 @@
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration files
file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(slug)s
# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
timezone = UTC
# max length of characters to apply to the
# "slug" field
truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
sourceless = false
# version location specification; this defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
version_locations = %(here)s/migrations/versions
# the output encoding used when revision files
# are written from script.py.mako
output_encoding = utf-8
# SQLite URL example
sqlalchemy.url = sqlite:////app/storage/db/db.sqlite
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner,
# against the entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration
[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

0
app/__init__.py Normal file
View File

0
app/api/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,101 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies.db import get_db
from app.schemas.category import (
CategoryCreate,
CategoryResponse,
CategoryUpdate,
CategoryWithProducts,
)
from app.services import category as category_service
router = APIRouter(prefix="/categories")
@router.get("", response_model=List[CategoryResponse], summary="Get all categories")
async def get_categories(db: AsyncSession = Depends(get_db)):
"""Get all categories."""
categories = await category_service.get_categories(db)
return categories
@router.get(
"/{category_id}", response_model=CategoryResponse, summary="Get a category by ID"
)
async def get_category(category_id: int, db: AsyncSession = Depends(get_db)):
"""Get a category by ID."""
db_category = await category_service.get_category(db, category_id)
if db_category is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Category with ID {category_id} not found",
)
return db_category
@router.get(
"/{category_id}/products",
response_model=CategoryWithProducts,
summary="Get a category with its products",
)
async def get_category_with_products(
category_id: int, db: AsyncSession = Depends(get_db)
):
"""Get a category with its related products."""
db_category = await category_service.get_category_with_products(db, category_id)
if db_category is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Category with ID {category_id} not found",
)
return db_category
@router.post(
"",
response_model=CategoryResponse,
status_code=status.HTTP_201_CREATED,
summary="Create a new category",
)
async def create_category(category: CategoryCreate, db: AsyncSession = Depends(get_db)):
"""Create a new category."""
return await category_service.create_category(db, category)
@router.put(
"/{category_id}", response_model=CategoryResponse, summary="Update a category"
)
async def update_category(
category_id: int,
category_update: CategoryUpdate,
db: AsyncSession = Depends(get_db),
):
"""Update an existing category."""
db_category = await category_service.get_category(db, category_id)
if db_category is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Category with ID {category_id} not found",
)
return await category_service.update_category(db, db_category, category_update)
@router.delete(
"/{category_id}",
status_code=status.HTTP_204_NO_CONTENT,
response_model=None,
summary="Delete a category",
)
async def delete_category(category_id: int, db: AsyncSession = Depends(get_db)):
"""Delete a category."""
db_category = await category_service.get_category(db, category_id)
if db_category is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Category with ID {category_id} not found",
)
await category_service.delete_category(db, db_category)
return None

15
app/api/routes/health.py Normal file
View File

@ -0,0 +1,15 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies.db import get_db
router = APIRouter()
@router.get("/health", summary="Health check endpoint")
async def health_check(db: AsyncSession = Depends(get_db)):
"""
Health check endpoint that verifies the service is running
and can connect to the database.
"""
return {"status": "healthy", "database": "connected"}

View File

@ -0,0 +1,91 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies.db import get_db
from app.schemas.product import ProductCreate, ProductResponse, ProductUpdate
from app.services import product as product_service
router = APIRouter(prefix="/products")
@router.get(
"",
response_model=List[ProductResponse],
summary="Get all products",
description="Retrieve all products with optional filtering",
)
async def get_products(
skip: int = 0,
limit: int = 100,
anime_title: Optional[str] = Query(None, description="Filter by anime title"),
character_name: Optional[str] = Query(None, description="Filter by character name"),
db: AsyncSession = Depends(get_db),
):
"""Get all products with optional filtering."""
products = await product_service.get_products(
db,
skip=skip,
limit=limit,
anime_title=anime_title,
character_name=character_name,
)
return products
@router.get(
"/{product_id}", response_model=ProductResponse, summary="Get a product by ID"
)
async def get_product(product_id: int, db: AsyncSession = Depends(get_db)):
"""Get a product by ID."""
db_product = await product_service.get_product(db, product_id)
if db_product is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Product with ID {product_id} not found",
)
return db_product
@router.post(
"",
response_model=ProductResponse,
status_code=status.HTTP_201_CREATED,
summary="Create a new product",
)
async def create_product(product: ProductCreate, db: AsyncSession = Depends(get_db)):
"""Create a new product."""
return await product_service.create_product(db, product)
@router.put("/{product_id}", response_model=ProductResponse, summary="Update a product")
async def update_product(
product_id: int, product_update: ProductUpdate, db: AsyncSession = Depends(get_db)
):
"""Update an existing product."""
db_product = await product_service.get_product(db, product_id)
if db_product is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Product with ID {product_id} not found",
)
return await product_service.update_product(db, db_product, product_update)
@router.delete(
"/{product_id}",
status_code=status.HTTP_204_NO_CONTENT,
response_model=None,
summary="Delete a product",
)
async def delete_product(product_id: int, db: AsyncSession = Depends(get_db)):
"""Delete a product."""
db_product = await product_service.get_product(db, product_id)
if db_product is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Product with ID {product_id} not found",
)
await product_service.delete_product(db, db_product)
return None

0
app/core/__init__.py Normal file
View File

24
app/core/config.py Normal file
View File

@ -0,0 +1,24 @@
from pathlib import Path
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
PROJECT_NAME: str = "Anime Merchandise Catalog API"
PROJECT_DESCRIPTION: str = "A simple API for managing anime merchandise catalog"
PROJECT_VERSION: str = "0.1.0"
API_V1_STR: str = "/api/v1"
DEBUG: bool = False
# Database
DB_DIR: Path = Path("/app") / "storage" / "db"
SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_DIR}/db.sqlite"
class Config:
env_file = ".env"
settings = Settings()
# Ensure DB directory exists
settings.DB_DIR.mkdir(parents=True, exist_ok=True)

View File

30
app/dependencies/db.py Normal file
View File

@ -0,0 +1,30 @@
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
# Create async engine
engine = create_async_engine(
settings.SQLALCHEMY_DATABASE_URL.replace("sqlite:///", "sqlite+aiosqlite:///"),
connect_args={"check_same_thread": False},
)
# Create async session
async_session = sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
autocommit=False,
autoflush=False,
)
async def get_db() -> AsyncSession:
"""
Dependency for getting an async database session.
"""
async with async_session() as session:
try:
yield session
finally:
await session.close()

5
app/models/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from app.models.base import Base
from app.models.product import Product, product_category
from app.models.category import Category
__all__ = ["Base", "Product", "Category", "product_category"]

19
app/models/base.py Normal file
View File

@ -0,0 +1,19 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, Integer
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
"""Base class for SQLAlchemy models."""
@declared_attr.directive
def __tablename__(cls) -> str:
return cls.__name__.lower()
id = Column(Integer, primary_key=True, index=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)

20
app/models/category.py Normal file
View File

@ -0,0 +1,20 @@
from sqlalchemy import Column, String, Text
from sqlalchemy.orm import relationship
from app.models.base import Base
from app.models.product import product_category
class Category(Base):
"""Model for anime merchandise categories."""
name = Column(String(100), nullable=False, unique=True, index=True)
description = Column(Text, nullable=True)
# Relationships
products = relationship(
"Product", secondary=product_category, back_populates="categories"
)
def __repr__(self):
return f"<Category {self.name}>"

32
app/models/product.py Normal file
View File

@ -0,0 +1,32 @@
from sqlalchemy import Column, String, Integer, Float, Text, ForeignKey, Table
from sqlalchemy.orm import relationship
from app.models.base import Base
# Association table for the many-to-many relationship between Product and Category
product_category = Table(
"product_category",
Base.metadata,
Column("product_id", Integer, ForeignKey("product.id"), primary_key=True),
Column("category_id", Integer, ForeignKey("category.id"), primary_key=True),
)
class Product(Base):
"""Model for anime merchandise products."""
name = Column(String(255), nullable=False, index=True)
description = Column(Text, nullable=True)
price = Column(Float, nullable=False)
stock = Column(Integer, default=0, nullable=False)
image_url = Column(String(255), nullable=True)
anime_title = Column(String(255), nullable=True, index=True)
character_name = Column(String(255), nullable=True, index=True)
# Relationships
categories = relationship(
"Category", secondary=product_category, back_populates="products"
)
def __repr__(self):
return f"<Product {self.name}>"

29
app/schemas/__init__.py Normal file
View File

@ -0,0 +1,29 @@
from app.schemas.product import (
ProductBase,
ProductCreate,
ProductUpdate,
ProductResponse,
CategoryInProduct,
)
from app.schemas.category import (
CategoryBase,
CategoryCreate,
CategoryUpdate,
CategoryResponse,
CategoryWithProducts,
ProductInCategory,
)
__all__ = [
"ProductBase",
"ProductCreate",
"ProductUpdate",
"ProductResponse",
"CategoryInProduct",
"CategoryBase",
"CategoryCreate",
"CategoryUpdate",
"CategoryResponse",
"CategoryWithProducts",
"ProductInCategory",
]

52
app/schemas/category.py Normal file
View File

@ -0,0 +1,52 @@
from typing import List, Optional
from pydantic import BaseModel
class CategoryBase(BaseModel):
"""Base schema for category data."""
name: str
description: Optional[str] = None
class CategoryCreate(CategoryBase):
"""Schema for creating a new category."""
pass
class CategoryUpdate(BaseModel):
"""Schema for updating a category."""
name: Optional[str] = None
description: Optional[str] = None
class ProductInCategory(BaseModel):
"""Schema for product data in category responses."""
id: int
name: str
price: float
class Config:
from_attributes = True
class CategoryResponse(CategoryBase):
"""Schema for category responses."""
id: int
class Config:
from_attributes = True
class CategoryWithProducts(CategoryResponse):
"""Schema for category with related products."""
products: List[ProductInCategory] = []
class Config:
from_attributes = True

54
app/schemas/product.py Normal file
View File

@ -0,0 +1,54 @@
from typing import List, Optional
from pydantic import BaseModel, Field, HttpUrl
class ProductBase(BaseModel):
"""Base schema for product data."""
name: str
description: Optional[str] = None
price: float = Field(gt=0)
stock: int = Field(ge=0, default=0)
image_url: Optional[HttpUrl] = None
anime_title: Optional[str] = None
character_name: Optional[str] = None
class ProductCreate(ProductBase):
"""Schema for creating a new product."""
category_ids: List[int] = []
class ProductUpdate(BaseModel):
"""Schema for updating a product."""
name: Optional[str] = None
description: Optional[str] = None
price: Optional[float] = Field(gt=0, default=None)
stock: Optional[int] = Field(ge=0, default=None)
image_url: Optional[HttpUrl] = None
anime_title: Optional[str] = None
character_name: Optional[str] = None
category_ids: Optional[List[int]] = None
class CategoryInProduct(BaseModel):
"""Schema for category data in product responses."""
id: int
name: str
class Config:
from_attributes = True
class ProductResponse(ProductBase):
"""Schema for product responses."""
id: int
categories: List[CategoryInProduct] = []
class Config:
from_attributes = True

0
app/services/__init__.py Normal file
View File

60
app/services/category.py Normal file
View File

@ -0,0 +1,60 @@
from typing import List, Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.category import Category
from app.schemas.category import CategoryCreate, CategoryUpdate
async def get_categories(db: AsyncSession) -> List[Category]:
"""Get all categories."""
result = await db.execute(select(Category))
return result.scalars().all()
async def get_category(db: AsyncSession, category_id: int) -> Optional[Category]:
"""Get a category by ID."""
return await db.get(Category, category_id)
async def get_category_with_products(
db: AsyncSession, category_id: int
) -> Optional[Category]:
"""Get a category with related products."""
query = (
select(Category)
.where(Category.id == category_id)
.options(selectinload(Category.products))
)
result = await db.execute(query)
return result.scalar_one_or_none()
async def create_category(db: AsyncSession, category: CategoryCreate) -> Category:
"""Create a new category."""
db_category = Category(**category.model_dump())
db.add(db_category)
await db.commit()
await db.refresh(db_category)
return db_category
async def update_category(
db: AsyncSession, db_category: Category, category_update: CategoryUpdate
) -> Category:
"""Update a category."""
update_data = category_update.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(db_category, field, value)
await db.commit()
await db.refresh(db_category)
return db_category
async def delete_category(db: AsyncSession, db_category: Category) -> None:
"""Delete a category."""
await db.delete(db_category)
await db.commit()

100
app/services/product.py Normal file
View File

@ -0,0 +1,100 @@
from typing import List, Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.category import Category
from app.models.product import Product
from app.schemas.product import ProductCreate, ProductUpdate
async def get_products(
db: AsyncSession,
skip: int = 0,
limit: int = 100,
anime_title: Optional[str] = None,
character_name: Optional[str] = None,
) -> List[Product]:
"""Get all products with optional filtering."""
query = select(Product).options(selectinload(Product.categories))
if anime_title:
query = query.filter(Product.anime_title == anime_title)
if character_name:
query = query.filter(Product.character_name == character_name)
query = query.offset(skip).limit(limit)
result = await db.execute(query)
return result.scalars().all()
async def get_product(db: AsyncSession, product_id: int) -> Optional[Product]:
"""Get a product by ID with its categories."""
query = (
select(Product)
.where(Product.id == product_id)
.options(selectinload(Product.categories))
)
result = await db.execute(query)
return result.scalar_one_or_none()
async def create_product(db: AsyncSession, product: ProductCreate) -> Product:
"""Create a new product with optional category associations."""
# Extract category_ids from the schema
category_ids = product.category_ids
product_data = product.model_dump(exclude={"category_ids"})
# Create the product
db_product = Product(**product_data)
# Add categories if provided
if category_ids:
categories = await get_categories_by_ids(db, category_ids)
db_product.categories = categories
db.add(db_product)
await db.commit()
await db.refresh(db_product)
return db_product
async def update_product(
db: AsyncSession, db_product: Product, product_update: ProductUpdate
) -> Product:
"""Update a product and its category associations."""
# Extract category_ids from the schema if present
update_data = product_update.model_dump(exclude_unset=True)
category_ids = update_data.pop("category_ids", None)
# Update product fields
for field, value in update_data.items():
setattr(db_product, field, value)
# Update categories if provided
if category_ids is not None:
categories = await get_categories_by_ids(db, category_ids)
db_product.categories = categories
await db.commit()
await db.refresh(db_product)
return db_product
async def delete_product(db: AsyncSession, db_product: Product) -> None:
"""Delete a product."""
await db.delete(db_product)
await db.commit()
async def get_categories_by_ids(
db: AsyncSession, category_ids: List[int]
) -> List[Category]:
"""Helper function to get categories by their IDs."""
if not category_ids:
return []
result = await db.execute(select(Category).where(Category.id.in_(category_ids)))
return result.scalars().all()

21
main.py Normal file
View File

@ -0,0 +1,21 @@
import uvicorn
from fastapi import FastAPI
from app.api.routes import health, products, categories
from app.core.config import settings
app = FastAPI(
title=settings.PROJECT_NAME,
description=settings.PROJECT_DESCRIPTION,
version=settings.PROJECT_VERSION,
docs_url="/docs",
redoc_url="/redoc",
)
# Include routers
app.include_router(health.router, tags=["health"])
app.include_router(products.router, prefix="/api/v1", tags=["products"])
app.include_router(categories.router, prefix="/api/v1", tags=["categories"])
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

0
migrations/__init__.py Normal file
View File

89
migrations/env.py Normal file
View File

@ -0,0 +1,89 @@
import asyncio
from logging.config import fileConfig
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
from app.models.base import Base
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
render_as_batch=True, # Enables batch mode for SQLite
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection: Connection) -> None:
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=connection.dialect.name
== "sqlite", # Enables batch mode for SQLite
)
with context.begin_transaction():
context.run_migrations()
async def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = async_engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
if context.is_offline_mode():
run_migrations_offline()
else:
asyncio.run(run_migrations_online())

26
migrations/script.py.mako Normal file
View File

@ -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"}

View File

@ -0,0 +1,87 @@
"""initial schema
Revision ID: 20231216_000000
Revises:
Create Date: 2023-12-16 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "20231216_000000"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Create category table
op.create_table(
"category",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.Column("name", sa.String(length=100), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_category_id"), "category", ["id"], unique=False)
op.create_index(op.f("ix_category_name"), "category", ["name"], unique=True)
# Create product table
op.create_table(
"product",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.Column("name", sa.String(length=255), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("price", sa.Float(), nullable=False),
sa.Column("stock", sa.Integer(), nullable=False),
sa.Column("image_url", sa.String(length=255), nullable=True),
sa.Column("anime_title", sa.String(length=255), nullable=True),
sa.Column("character_name", sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_product_anime_title"), "product", ["anime_title"], unique=False
)
op.create_index(
op.f("ix_product_character_name"), "product", ["character_name"], unique=False
)
op.create_index(op.f("ix_product_id"), "product", ["id"], unique=False)
op.create_index(op.f("ix_product_name"), "product", ["name"], unique=False)
# Create product_category association table
op.create_table(
"product_category",
sa.Column("product_id", sa.Integer(), nullable=False),
sa.Column("category_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["category_id"],
["category.id"],
),
sa.ForeignKeyConstraint(
["product_id"],
["product.id"],
),
sa.PrimaryKeyConstraint("product_id", "category_id"),
)
def downgrade() -> None:
# Drop tables in reverse order
op.drop_table("product_category")
op.drop_index(op.f("ix_product_name"), table_name="product")
op.drop_index(op.f("ix_product_id"), table_name="product")
op.drop_index(op.f("ix_product_character_name"), table_name="product")
op.drop_index(op.f("ix_product_anime_title"), table_name="product")
op.drop_table("product")
op.drop_index(op.f("ix_category_name"), table_name="category")
op.drop_index(op.f("ix_category_id"), table_name="category")
op.drop_table("category")

View File

12
requirements.txt Normal file
View File

@ -0,0 +1,12 @@
fastapi>=0.100.0
uvicorn>=0.22.0
sqlalchemy>=2.0.0
alembic>=1.11.1
pydantic>=2.0.0
pydantic-settings>=2.0.0
python-dotenv>=1.0.0
aiosqlite>=0.19.0
ruff>=0.0.280
httpx>=0.24.1
pytest>=7.4.0
pytest-asyncio>=0.21.1