Add character management system
- Enhance character model with additional fields - Create relationship between anime and characters - Implement character search functionality - Create Alembic migration for character table - Add sample character data for seeding - Update README with character endpoint information
This commit is contained in:
parent
bf28ab6f8e
commit
9623558845
14
README.md
14
README.md
@ -100,7 +100,7 @@ The API will be available at http://localhost:8000, and the interactive API docu
|
||||
- `DELETE /api/v1/genres/{id}`: Delete genre
|
||||
|
||||
### Characters
|
||||
- `GET /api/v1/characters/`: Get all characters
|
||||
- `GET /api/v1/characters/`: Search characters with filters (name, anime_id, role)
|
||||
- `POST /api/v1/characters/`: Create new character
|
||||
- `GET /api/v1/characters/{id}`: Get character by ID
|
||||
- `PUT /api/v1/characters/{id}`: Update character
|
||||
@ -109,6 +109,8 @@ The API will be available at http://localhost:8000, and the interactive API docu
|
||||
|
||||
## Advanced Search Parameters
|
||||
|
||||
### Anime Search Parameters
|
||||
|
||||
The anime search endpoint supports the following parameters:
|
||||
|
||||
- `title`: Filter by title (partial match)
|
||||
@ -125,6 +127,16 @@ The anime search endpoint supports the following parameters:
|
||||
- `skip`: Number of items to skip for pagination
|
||||
- `limit`: Maximum number of items to return
|
||||
|
||||
### Character Search Parameters
|
||||
|
||||
The character search endpoint supports the following parameters:
|
||||
|
||||
- `name`: Filter by character name (partial match)
|
||||
- `anime_id`: Filter by anime ID
|
||||
- `role`: Filter by character role (Main, Supporting, Antagonist, etc.)
|
||||
- `skip`: Number of items to skip for pagination
|
||||
- `limit`: Maximum number of items to return
|
||||
|
||||
## Statistics
|
||||
|
||||
The statistics endpoint provides the following information:
|
||||
|
47
alembic/versions/20231018_add_character_table.py
Normal file
47
alembic/versions/20231018_add_character_table.py
Normal file
@ -0,0 +1,47 @@
|
||||
"""Add character table
|
||||
|
||||
Revision ID: 9a87c5ed9d12
|
||||
Revises: b57a40c2b63d
|
||||
Create Date: 2023-10-18 00:00:00.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '9a87c5ed9d12'
|
||||
down_revision = 'b57a40c2b63d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create character table
|
||||
op.create_table(
|
||||
'character',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(255), nullable=False),
|
||||
sa.Column('role', sa.String(50), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('voice_actor', sa.String(255), nullable=True),
|
||||
sa.Column('image_url', sa.String(255), nullable=True),
|
||||
sa.Column('anime_id', sa.Integer(), nullable=False),
|
||||
sa.Column('age', sa.String(50), nullable=True),
|
||||
sa.Column('gender', sa.String(50), nullable=True),
|
||||
sa.Column('birth_date', sa.Date(), nullable=True),
|
||||
sa.Column('height', sa.String(50), nullable=True),
|
||||
sa.Column('weight', sa.String(50), nullable=True),
|
||||
sa.Column('blood_type', sa.String(10), nullable=True),
|
||||
sa.Column('popularity_rank', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['anime_id'], ['anime.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_character_id'), 'character', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_character_name'), 'character', ['name'], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(op.f('ix_character_name'), table_name='character')
|
||||
op.drop_index(op.f('ix_character_id'), table_name='character')
|
||||
op.drop_table('character')
|
@ -1,8 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.endpoints import anime, genres, health
|
||||
from app.api.endpoints import anime, genres, health, characters
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(health.router, prefix="/health", tags=["health"])
|
||||
api_router.include_router(anime.router, prefix="/api/v1/anime", tags=["anime"])
|
||||
api_router.include_router(genres.router, prefix="/api/v1/genres", tags=["genres"])
|
||||
api_router.include_router(genres.router, prefix="/api/v1/genres", tags=["genres"])
|
||||
api_router.include_router(characters.router, prefix="/api/v1/characters", tags=["characters"])
|
@ -9,16 +9,38 @@ from app.api import deps
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[schemas.character.Character])
|
||||
def read_characters(
|
||||
@router.get("/", response_model=schemas.character.CharacterSearchResults)
|
||||
def search_characters(
|
||||
db: Session = Depends(deps.get_db),
|
||||
name: str = None,
|
||||
anime_id: int = None,
|
||||
role: str = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve characters.
|
||||
Search for characters with filters.
|
||||
"""
|
||||
return crud.character.get_multi(db, skip=skip, limit=limit)
|
||||
characters = crud.character.search(
|
||||
db,
|
||||
name=name,
|
||||
anime_id=anime_id,
|
||||
role=role,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
total = crud.character.search_count(
|
||||
db,
|
||||
name=name,
|
||||
anime_id=anime_id,
|
||||
role=role
|
||||
)
|
||||
return {
|
||||
"results": characters,
|
||||
"total": total,
|
||||
"page": skip // limit + 1 if limit > 0 else 1,
|
||||
"size": limit
|
||||
}
|
||||
|
||||
|
||||
@router.post("/", response_model=schemas.character.Character)
|
||||
|
@ -1,4 +1,5 @@
|
||||
from app.crud.crud_anime import anime as anime
|
||||
from app.crud.crud_genre import genre as genre
|
||||
from app.crud.crud_character import character as character
|
||||
|
||||
__all__ = ["anime", "genre"]
|
||||
__all__ = ["anime", "genre", "character"]
|
@ -1,4 +1,4 @@
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.crud.base import CRUDBase
|
||||
@ -11,5 +11,55 @@ class CRUDCharacter(CRUDBase[Character, CharacterCreate, CharacterUpdate]):
|
||||
"""Get all characters for a specific anime"""
|
||||
return db.query(Character).filter(Character.anime_id == anime_id).all()
|
||||
|
||||
def get_count_by_anime(self, db: Session, *, anime_id: int) -> int:
|
||||
"""Get count of characters for a specific anime"""
|
||||
return db.query(Character).filter(Character.anime_id == anime_id).count()
|
||||
|
||||
def search(
|
||||
self,
|
||||
db: Session,
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
anime_id: Optional[int] = None,
|
||||
role: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
) -> List[Character]:
|
||||
"""Search for characters with filters"""
|
||||
query = db.query(Character)
|
||||
|
||||
if name:
|
||||
query = query.filter(Character.name.ilike(f"%{name}%"))
|
||||
|
||||
if anime_id:
|
||||
query = query.filter(Character.anime_id == anime_id)
|
||||
|
||||
if role:
|
||||
query = query.filter(Character.role == role)
|
||||
|
||||
return query.offset(skip).limit(limit).all()
|
||||
|
||||
def search_count(
|
||||
self,
|
||||
db: Session,
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
anime_id: Optional[int] = None,
|
||||
role: Optional[str] = None,
|
||||
) -> int:
|
||||
"""Get count of characters matching search criteria"""
|
||||
query = db.query(Character)
|
||||
|
||||
if name:
|
||||
query = query.filter(Character.name.ilike(f"%{name}%"))
|
||||
|
||||
if anime_id:
|
||||
query = query.filter(Character.anime_id == anime_id)
|
||||
|
||||
if role:
|
||||
query = query.filter(Character.role == role)
|
||||
|
||||
return query.count()
|
||||
|
||||
|
||||
character = CRUDCharacter(Character)
|
@ -2,4 +2,5 @@
|
||||
from app.db.base_class import Base # noqa
|
||||
from app.models.anime import Anime # noqa
|
||||
from app.models.genre import Genre # noqa
|
||||
from app.models.anime_genre import AnimeGenre # noqa
|
||||
from app.models.anime_genre import AnimeGenre # noqa
|
||||
from app.models.character import Character # noqa
|
@ -80,6 +80,102 @@ INITIAL_ANIME = [
|
||||
},
|
||||
]
|
||||
|
||||
# Example characters
|
||||
INITIAL_CHARACTERS = [
|
||||
# Fullmetal Alchemist: Brotherhood characters
|
||||
{
|
||||
"name": "Edward Elric",
|
||||
"role": "Main",
|
||||
"description": "The primary protagonist of the series, Edward is a young alchemical prodigy who lost his right arm and left leg after a failed attempt to resurrect his mother. He is the youngest State Alchemist in history, joining the military at the age of 12.",
|
||||
"voice_actor": "Romi Park (Japanese), Vic Mignogna (English)",
|
||||
"image_url": "https://example.com/edward_elric.jpg",
|
||||
"anime_title": "Fullmetal Alchemist: Brotherhood",
|
||||
"age": "15-16",
|
||||
"gender": "Male",
|
||||
"birth_date": date(1899, 2, 3),
|
||||
"height": "165 cm (5'5\")",
|
||||
"weight": "55 kg (121 lbs)",
|
||||
"blood_type": "O",
|
||||
"popularity_rank": 1
|
||||
},
|
||||
{
|
||||
"name": "Alphonse Elric",
|
||||
"role": "Main",
|
||||
"description": "Edward's younger brother, Alphonse lost his entire body in the failed human transmutation experiment. His soul was bound to a suit of armor by Edward, and he seeks to restore his original body.",
|
||||
"voice_actor": "Rie Kugimiya (Japanese), Maxey Whitehead (English)",
|
||||
"image_url": "https://example.com/alphonse_elric.jpg",
|
||||
"anime_title": "Fullmetal Alchemist: Brotherhood",
|
||||
"age": "14-15",
|
||||
"gender": "Male",
|
||||
"birth_date": date(1900, 5, 7),
|
||||
"height": "250 cm (in armor)",
|
||||
"blood_type": "B",
|
||||
"popularity_rank": 2
|
||||
},
|
||||
# Death Note characters
|
||||
{
|
||||
"name": "Light Yagami",
|
||||
"role": "Main",
|
||||
"description": "The main protagonist of Death Note, Light is a high school student who discovers the Death Note. Disgusted with the crime and injustice in the world, he uses the Death Note's power to kill criminals and becomes known as 'Kira'.",
|
||||
"voice_actor": "Mamoru Miyano (Japanese), Brad Swaile (English)",
|
||||
"image_url": "https://example.com/light_yagami.jpg",
|
||||
"anime_title": "Death Note",
|
||||
"age": "17-23",
|
||||
"gender": "Male",
|
||||
"birth_date": date(1986, 2, 28),
|
||||
"height": "179 cm (5'10\")",
|
||||
"weight": "64 kg (141 lbs)",
|
||||
"blood_type": "A",
|
||||
"popularity_rank": 3
|
||||
},
|
||||
{
|
||||
"name": "L Lawliet",
|
||||
"role": "Main",
|
||||
"description": "A mysterious detective with an eccentric personality who takes on the challenge of capturing Kira. Considered the world's greatest detective, L opposes Light in a psychological battle of wits.",
|
||||
"voice_actor": "Kappei Yamaguchi (Japanese), Alessandro Juliani (English)",
|
||||
"image_url": "https://example.com/l_lawliet.jpg",
|
||||
"anime_title": "Death Note",
|
||||
"age": "24-25",
|
||||
"gender": "Male",
|
||||
"birth_date": date(1979, 10, 31),
|
||||
"height": "179 cm (5'10\")",
|
||||
"weight": "50 kg (110 lbs)",
|
||||
"blood_type": "Unknown",
|
||||
"popularity_rank": 4
|
||||
},
|
||||
# Attack on Titan characters
|
||||
{
|
||||
"name": "Eren Yeager",
|
||||
"role": "Main",
|
||||
"description": "The main protagonist of Attack on Titan. After his mother is eaten by a Titan, Eren vows to exterminate all Titans. Later, he discovers he has the ability to transform into a Titan.",
|
||||
"voice_actor": "Yuki Kaji (Japanese), Bryce Papenbrook (English)",
|
||||
"image_url": "https://example.com/eren_yeager.jpg",
|
||||
"anime_title": "Attack on Titan",
|
||||
"age": "15-19",
|
||||
"gender": "Male",
|
||||
"birth_date": date(835, 3, 30),
|
||||
"height": "170 cm (5'7\")",
|
||||
"weight": "63 kg (139 lbs)",
|
||||
"blood_type": "Unknown",
|
||||
"popularity_rank": 5
|
||||
},
|
||||
{
|
||||
"name": "Mikasa Ackerman",
|
||||
"role": "Main",
|
||||
"description": "Eren's adoptive sister and one of the main protagonists. After her parents were murdered, Eren saved her life and she has been protective of him ever since. She is considered an exceptionally skilled soldier.",
|
||||
"voice_actor": "Yui Ishikawa (Japanese), Trina Nishimura (English)",
|
||||
"image_url": "https://example.com/mikasa_ackerman.jpg",
|
||||
"anime_title": "Attack on Titan",
|
||||
"age": "15-19",
|
||||
"gender": "Female",
|
||||
"birth_date": date(835, 2, 10),
|
||||
"height": "170 cm (5'7\")",
|
||||
"weight": "68 kg (150 lbs)",
|
||||
"blood_type": "Unknown",
|
||||
"popularity_rank": 6
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def init_db(db: Session) -> None:
|
||||
# Create genres
|
||||
@ -91,12 +187,33 @@ def init_db(db: Session) -> None:
|
||||
logger.info(f"Created genre: {genre_data['name']}")
|
||||
|
||||
# Create anime
|
||||
anime_id_map = {} # Map anime titles to their IDs for character creation
|
||||
for anime_data in INITIAL_ANIME:
|
||||
anime = crud.anime.get_multi(db, limit=1, skip=0)
|
||||
if not anime or all(a.title != anime_data["title"] for a in anime):
|
||||
anime_in = schemas.anime.AnimeCreate(**anime_data)
|
||||
crud.anime.create_with_genres(db, obj_in=anime_in)
|
||||
created_anime = crud.anime.create_with_genres(db, obj_in=anime_in)
|
||||
anime_id_map[anime_data["title"]] = created_anime.id
|
||||
logger.info(f"Created anime: {anime_data['title']}")
|
||||
else:
|
||||
# Get the ID for existing anime
|
||||
for a in anime:
|
||||
if a.title == anime_data["title"]:
|
||||
anime_id_map[anime_data["title"]] = a.id
|
||||
|
||||
# Create characters
|
||||
for character_data in INITIAL_CHARACTERS:
|
||||
# Get anime ID from title
|
||||
anime_title = character_data.pop("anime_title")
|
||||
if anime_title in anime_id_map:
|
||||
character_data["anime_id"] = anime_id_map[anime_title]
|
||||
|
||||
# Check if character already exists to avoid duplicates
|
||||
existing_characters = crud.character.get_by_anime(db, anime_id=character_data["anime_id"])
|
||||
if not any(c.name == character_data["name"] for c in existing_characters):
|
||||
character_in = schemas.character.CharacterCreate(**character_data)
|
||||
crud.character.create(db, obj_in=character_in)
|
||||
logger.info(f"Created character: {character_data['name']}")
|
||||
|
||||
|
||||
def seed_data() -> None:
|
||||
|
Loading…
x
Reference in New Issue
Block a user