diff --git a/README.md b/README.md index 8b4fd14..cebee09 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,9 @@ This repo is deployed on [Google Cloud Run](hhttps://cloud.google.com/run/) Access the Swagger UI interface of this FastAPI at https://dqmonstersdb-api-743047725852.us-central1.run.app/docs -![Swagger UI homepage](static/images/readme/FastAPI-readme-1.jpg) -![Trying out a endpoint](static/images/readme/FastAPI-readme-2.jpg) +![Swagger UI homepage](src/static/images/readme/FastAPI-screenshot-1.jpg) + +![Trying out a endpoint](src/static/images/readme/FastAPI-screenshot-2.jpg) ## How to Run Locally 1. Clone this repo onto your local machine. diff --git a/src/app/create_database.py b/src/app/create_database.py index 06b44ae..3d97cff 100644 --- a/src/app/create_database.py +++ b/src/app/create_database.py @@ -1,92 +1,88 @@ -import csv -from pathlib import Path - -from sqlmodel import Session, select -from sqlalchemy.exc import IntegrityError - -from src.app.database import create_db_and_tables, engine -from src.app.models import ( - Item, - MonsterBreedingLink, - MonsterDetail, - MonsterFamily, - MonsterSkillLink, - Skill, - SkillCombine, -) - -current_dir = Path(__file__).resolve().parent -csv_dir = current_dir.parent / "csv_files" - - -def _insert_data(csv_file, Model): - """ - helper function that uses dictionary unpacking to add csv file data - into database - """ - with Session(engine) as session: - with open(csv_file, encoding="utf-8-sig") as f: - reader = csv.DictReader(f) - for row in reader: - # replace empty string with None - row = {k: (None if v == "" else v) for k, v in row.items()} - if "id" in row: - existing_entry = session.exec( - select(Model).where(Model.id == row["id"]) - ).first() - - if existing_entry: - print( - f"Entry with id {row['id']} already exists. Skipping insertion." - ) - continue - try: - session.add(Model(**row)) - except IntegrityError as e: - session.rollback() - print(f"IntegrityError has occurred: {e.orig}") - - session.commit() - - -def create_item_csv(): - _insert_data(csv_dir / "DQM1_items.csv", Item) - - -def create_monster_family_csv(): - _insert_data(csv_dir / "DQM1_monster_family.csv", MonsterFamily) - - -def create_skill_csv(): - _insert_data(csv_dir / "DQM1_skills.csv", Skill) - - -def create_skillcombo_csv(): - _insert_data(csv_dir / "DQM1_skill_combo.csv", SkillCombine) - - -def create_monster_detail_csv(): - _insert_data(csv_dir / "DQM1_monsterdetails.csv", MonsterDetail) - - -def create_breed_combo(): - _insert_data(csv_dir / "DQM1_breeding_combo.csv", MonsterBreedingLink) - - -def create_monster_skill_link(): - _insert_data(csv_dir / "DQM1_monster_skill_link.csv", MonsterSkillLink) - - -def load_all_csv_data(): - create_db_and_tables() - create_item_csv() - create_monster_family_csv() - create_skill_csv() - create_skillcombo_csv() - create_monster_detail_csv() - create_breed_combo() - create_monster_skill_link() - - -if __name__ == "__main__": - load_all_csv_data() +import csv +from pathlib import Path + +from sqlalchemy.exc import IntegrityError +from sqlmodel import Session, select + +from src.app.database import create_db_and_tables, engine +from src.app.models.dqm1.associations import MonsterSkillLink +from src.app.models.dqm1.item import Item +from src.app.models.dqm1.monster import MonsterBreedingLink, MonsterDetail +from src.app.models.dqm1.monster_family import MonsterFamily +from src.app.models.dqm1.skill import Skill, SkillCombine + +current_dir = Path(__file__).resolve().parent +csv_dir = current_dir.parent / "csv_files" + + +def _insert_data(csv_file, Model): + """ + helper function that uses dictionary unpacking to add csv file data + into database + """ + with Session(engine) as session: + with open(csv_file, encoding="utf-8-sig") as f: + reader = csv.DictReader(f) + for row in reader: + # replace empty string with None + row = {k: (None if v == "" else v) for k, v in row.items()} + if "id" in row: + existing_entry = session.exec( + select(Model).where(Model.id == row["id"]) + ).first() + + if existing_entry: + print( + f"Entry with id {row['id']} already exists. Skipping insertion." + ) + continue + try: + session.add(Model(**row)) + except IntegrityError as e: + session.rollback() + print(f"IntegrityError has occurred: {e.orig}") + + session.commit() + + +def create_item_csv(): + _insert_data(csv_dir / "DQM1_items.csv", Item) + + +def create_monster_family_csv(): + _insert_data(csv_dir / "DQM1_monster_family.csv", MonsterFamily) + + +def create_skill_csv(): + _insert_data(csv_dir / "DQM1_skills.csv", Skill) + + +def create_skillcombo_csv(): + _insert_data(csv_dir / "DQM1_skill_combo.csv", SkillCombine) + + +def create_monster_detail_csv(): + _insert_data(csv_dir / "DQM1_monsterdetails.csv", MonsterDetail) + + +def create_breed_combo(): + _insert_data(csv_dir / "DQM1_breeding_combo.csv", MonsterBreedingLink) + + +def create_monster_skill_link(): + _insert_data(csv_dir / "DQM1_monster_skill_link.csv", MonsterSkillLink) + + +def load_all_csv_data(): + create_db_and_tables() + create_item_csv() + create_monster_family_csv() + create_skill_csv() + create_skillcombo_csv() + create_monster_detail_csv() + create_breed_combo() + create_monster_skill_link() + + +if __name__ == "__main__": + load_all_csv_data() diff --git a/src/app/database.py b/src/app/database.py index 47a1d14..a79540e 100644 --- a/src/app/database.py +++ b/src/app/database.py @@ -1,6 +1,6 @@ from pathlib import Path -from sqlmodel import SQLModel, create_engine +from sqlmodel import Session, SQLModel, create_engine app_dir = Path(__file__).resolve().parent project_dir = app_dir.parent @@ -13,3 +13,8 @@ def create_db_and_tables(): SQLModel.metadata.create_all(engine) + + +async def get_session(): + with Session(engine) as session: + yield session diff --git a/src/app/main.py b/src/app/main.py index 3d3e4ad..0c3eabf 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -1,31 +1,8 @@ -from typing import List, Optional - -from fastapi import Depends, FastAPI, HTTPException +from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles -from sqlmodel import Session, select -from src.app.database import engine -from src.app.model_enums import ( - ItemCategory, - ItemSellLocation, - SkillCategory, - SkillFamily, -) -from src.app.models import ( - Item, - MonsterBreedingLink, - MonsterBreedingLinkReadWithInfo, - MonsterDetail, - MonsterDetailSkill, - MonsterDetailWithFamily, - MonsterFamily, - MonsterFamilyReadWithMonsterDetail, - Skill, - SkillCombine, - SkillCombineRead, - SkillUpgradeRead, -) +from src.app.routers import dqm1_endpoints tags_metadata = [ { @@ -55,6 +32,7 @@ "http://localhost", "http://localhost:8080", "http://localhost:3000", + "https://dqmonsters-db.vercel.app", ] app.add_middleware( @@ -65,10 +43,7 @@ allow_headers=["*"], ) - -async def get_session(): # place in database.py? - with Session(engine) as session: - yield session +app.include_router(dqm1_endpoints.router) @app.get("/") @@ -76,145 +51,3 @@ def root(): return { "message": ("Welcome to the DQMonsters API. " "Go to the Swagger UI interface") } - - -@app.get( - "/dqm1/monsters", - response_model=List[MonsterDetailWithFamily], - tags=["dqm1 monsters"], -) -async def read_monsters( - *, session: Session = Depends(get_session), family: Optional[int] = None -): - """ - **Parameter Descriptions**
- **new_name** : updated name used in later Dragon Quest games
- **old_name** : name used in the game
- **description** : in game beastiary description
- **family** : a monster is part of one of 10 different monster families
- """ - monsters = select(MonsterDetail) - if family: - monsters = monsters.where(MonsterDetail.family_id == family) - monsters_result = session.exec(monsters).all() - return monsters_result - - -@app.get( - "/dqm1/monsters/{monster_id}", - response_model=MonsterDetailWithFamily, - tags=["dqm1 monsters"], -) -async def read_monster(*, session: Session = Depends(get_session), monster_id: int): - monster = session.get(MonsterDetail, monster_id) - if not monster: - raise HTTPException(status_code=404, detail="Monster not found") - return monster - - -@app.get( - "/dqm1/monstersandskill/{monster_id}", - response_model=MonsterDetailSkill, - tags=["dqm1 monsters"], -) -async def read_monster_skill( - *, session: Session = Depends(get_session), monster_id: int -): - monster = session.get(MonsterDetail, monster_id) - if not monster: - raise HTTPException(status_code=404, detail="Monster not found") - return monster - - -@app.get( - "/dqm1/family/{family_id}", - response_model=MonsterFamilyReadWithMonsterDetail, - tags=["dqm1 monsters"], -) -async def read_family(*, session: Session = Depends(get_session), family_id: int): - family = session.get(MonsterFamily, family_id) - if not family: - raise HTTPException(status_code=404, detail="Family not found") - return family - - -@app.get("/dqm1/skills", tags=["dqm1 skills"]) -async def read_skills( - *, - session: Session = Depends(get_session), - category: Optional[SkillCategory] = None, - skill_family: Optional[SkillFamily] = None, -): - skills = select(Skill) - if category: - skills = skills.where(Skill.category_type == category) - if skill_family: - skills = skills.where(Skill.family_type == skill_family) - skills_result = session.exec(skills).all() - return skills_result - - -@app.get( - "/dqm1/skills/{skill_id}", response_model=SkillUpgradeRead, tags=["dqm1 skills"] -) -async def read_skill(*, session: Session = Depends(get_session), skill_id: int): - skill = session.get(Skill, skill_id) - if not skill: - raise HTTPException(status_code=404, detail="Skill not found") - return skill - - -@app.get( - "/dqm1/skillcombine/{skill_id}", - response_model=List[SkillCombineRead], - tags=["dqm1 skills"], -) -async def get_skill_combo(*, session: Session = Depends(get_session), skill_id: int): - query = select(SkillCombine).where(SkillCombine.combo_skill_id == skill_id) - skill = session.exec(query).all() - return skill - - -@app.get("/dqm1/items", tags=["dqm1 items"]) -async def read_items( - *, - session: Session = Depends(get_session), - category: Optional[ItemCategory] = None, - selllocation: Optional[ItemSellLocation] = None, -): - items = select(Item) - if category: - items = items.where(Item.item_category == category) - if selllocation: - items = items.where(Item.sell_location == selllocation) - items_result = session.exec(items).all() - return items_result - - -@app.get("/dqm1/items/{item_id}", tags=["dqm1 items"]) -async def read_item(*, session: Session = Depends(get_session), item_id: int): - item = session.get(Item, item_id) - if not item: - raise HTTPException(status_code=404, detail="Item not found") - return item - - -@app.get( - "/dqm1/breeding/{monster_id}", - response_model=List[MonsterBreedingLinkReadWithInfo], - tags=["dqm1 monsters"], -) -async def get_breeding_combos( - *, session: Session = Depends(get_session), monster_id: int -): - """ - Given a monster_id, finds all breeding combination that results in - the target monster or uses the target monster as a parent - """ - query = select(MonsterBreedingLink).where( - (MonsterBreedingLink.child_id == monster_id) - | (MonsterBreedingLink.pedigree_id == monster_id) - | (MonsterBreedingLink.parent2_id == monster_id) - ) - breeding_combos = session.exec(query).all() - return breeding_combos diff --git a/src/app/models.py b/src/app/models.py deleted file mode 100644 index d372bdc..0000000 --- a/src/app/models.py +++ /dev/null @@ -1,264 +0,0 @@ -from typing import List, Optional - -from sqlmodel import Field, Relationship, SQLModel - - -class MonsterSkillLink(SQLModel, table=True): - """ - many-to-many association table linking a monster to three different skills. - """ - - id: Optional[int] = Field(default=None, primary_key=True) - monster_id: Optional[int] = Field( - default=None, - foreign_key="monsterdetail.id", - ) - skill_id: Optional[int] = Field( - default=None, - foreign_key="skill.id", - ) - - -class MonsterDetailBase(SQLModel): - """ - Monster details from in-game bestiary. Shows name, family, and description. - """ - - new_name: str - old_name: str - description: str - - # one-to-many relation where a family is linked to many monsters - family_id: int = Field(foreign_key="monsterfamily.id") - - -class MonsterDetail(MonsterDetailBase, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - - family: List["MonsterFamily"] = Relationship(back_populates="monsters") - skills: List["Skill"] = Relationship( - back_populates="monsters", link_model=MonsterSkillLink - ) - - -class MonsterDetailRead(MonsterDetailBase): - id: int - - -class MonsterFamilyBase(SQLModel): - """ - There are 10 monster families in the game. - """ - - family_eng: str - - -class MonsterFamily(MonsterFamilyBase, table=True): - """ - one-to-many relation between family and monsters. - """ - - id: Optional[int] = Field(default=None, primary_key=True) - monsters: List[MonsterDetail] = Relationship(back_populates="family") - - -class MonsterFamilyRead(MonsterFamilyBase): - id: int - - -class MonsterDetailWithFamily(MonsterDetailRead): - family: Optional[MonsterFamilyRead] - - -class MonsterFamilyReadWithMonsterDetail(MonsterFamilyRead): - monsters: List[MonsterDetailRead] = [] - - -class MonsterBreedingLinkBase(SQLModel): - child_id: Optional[int] = Field(default=None, foreign_key="monsterdetail.id") - pedigree_id: Optional[int] = Field(default=None, foreign_key="monsterdetail.id") - parent2_id: Optional[int] = Field(default=None, foreign_key="monsterdetail.id") - pedigree_family_id: Optional[int] = Field( - default=None, foreign_key="monsterfamily.id" - ) - family2_id: Optional[int] = Field(default=None, foreign_key="monsterfamily.id") - - -class MonsterBreedingLink(MonsterBreedingLinkBase, table=True): - """ - many-to-many association table between MonsterDetail and MonsterFamily - that represents breeding combinations. - - child_id, pedigree, and parent_2 represent individual monster ids. - pedigree_family and family_2 represent family type. - - In order to make new monster, two parents are required. - - 4 different combinations possible: - pedigree + parent_2 -- specific monster + specific monster - pedigree + family_2 -- specific monster + any monster from the family type - pedigree_family + parent_2 -- specific family type + specific monster - pedigree_family + family_2 -- family + different family type - """ - - id: Optional[int] = Field(default=None, primary_key=True) - child: "MonsterDetail" = Relationship( - sa_relationship_kwargs={ - "primaryjoin": "MonsterBreedingLink.child_id==MonsterDetail.id", - "lazy": "joined", - } - ) - pedigree: "MonsterDetail" = Relationship( - sa_relationship_kwargs={ - "primaryjoin": "MonsterBreedingLink.pedigree_id==MonsterDetail.id", - "lazy": "joined", - } - ) - parent2: "MonsterDetail" = Relationship( - sa_relationship_kwargs={ - "primaryjoin": "MonsterBreedingLink.parent2_id==MonsterDetail.id", - "lazy": "joined", - } - ) - pedigree_family: "MonsterFamily" = Relationship( - sa_relationship_kwargs={ - "primaryjoin": "MonsterBreedingLink.pedigree_family_id" - "==MonsterFamily.id", - "lazy": "joined", - } - ) - family2: "MonsterFamily" = Relationship( - sa_relationship_kwargs={ - "primaryjoin": "MonsterBreedingLink.family2_id==MonsterFamily.id", - "lazy": "joined", - } - ) - - -class MonsterBreedingLinkRead(MonsterBreedingLinkBase): - id: int - - -class MonsterBreedingLinkReadWithInfo(MonsterBreedingLinkRead): - child: Optional[MonsterDetailRead] - pedigree: Optional[MonsterDetailRead] - parent2: Optional[MonsterDetailRead] - pedigree_family: Optional[MonsterFamilyRead] - family2: Optional[MonsterFamilyRead] - - -class SkillBase(SQLModel): - """ - Shows description, MP cost, and required stats to learn skill. - Each monster naturally learns 3 skills. - """ - - category_type: str - family_type: str - new_name: Optional[str] = Field(default=None) - old_name: str - description: str - mp_cost: int - required_level: int - required_hp: Optional[int] = None - required_mp: Optional[int] = None - required_attack: Optional[int] = None - required_defense: Optional[int] = None - required_speed: Optional[int] = None - required_intelligence: Optional[int] = None - - -class Skill(SkillBase, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - - upgrade_to_id: Optional[int] = Field( - foreign_key="skill.id", # lowercase refers to database table name - default=None, - ) - upgrade_to: Optional["Skill"] = Relationship( - sa_relationship_kwargs={ - "primaryjoin": "Skill.upgrade_to_id==Skill.id", - "lazy": "joined", - "remote_side": "Skill.id", # uppercase refers to this Skill class - } - ) - - upgrade_from_id: Optional[int] = Field( - foreign_key="skill.id", - default=None, - ) - upgrade_from: Optional["Skill"] = Relationship( - sa_relationship_kwargs={ - "primaryjoin": "Skill.upgrade_from_id==Skill.id", - "lazy": "joined", - "remote_side": "Skill.id", - } - ) - - monsters: List[MonsterDetail] = Relationship( - back_populates="skills", link_model=MonsterSkillLink - ) - - -class SkillRead(SkillBase): - id: int - - -class SkillReadWithMonster(SkillRead): - monsters: Optional[MonsterDetailRead] - - -class SkillUpgradeRead(SkillRead): - upgrade_to: Optional[Skill] - upgrade_from: Optional[Skill] - - -class MonsterDetailSkill(MonsterDetailWithFamily): - skills: List[SkillRead] = [] - - -class SkillCombineBase(SQLModel): - combo_skill_id: Optional[int] = Field(default=None, foreign_key="skill.id") - needed_skill_id: Optional[int] = Field(default=None, foreign_key="skill.id") - - -class SkillCombine(SkillCombineBase, table=True): - """ - many-to-many association table showing certain needed skills combine to - learn new combo skill. - """ - - id: Optional[int] = Field(default=None, primary_key=True) - - combo_skill: Skill = Relationship( - sa_relationship_kwargs={ - "primaryjoin": "SkillCombine.combo_skill_id==Skill.id", - "lazy": "joined", - } - ) - - needed_skill: Skill = Relationship( - sa_relationship_kwargs={ - "primaryjoin": "SkillCombine.needed_skill_id==Skill.id", - "lazy": "joined", - } - ) - - -class SkillCombineRead(SkillCombineBase): - id: int - needed_skill: Optional[SkillRead] - - -class Item(SQLModel, table=True): - """ - Lists all items sold in shops and found in the field - """ - - id: Optional[int] = Field(default=None, primary_key=True) - item_name: str - item_category: str - item_description: str - price: Optional[int] = Field(default=None) - sell_price: Optional[int] = Field(default=None) - sell_location: str diff --git a/src/app/models/__init__.py b/src/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/models/dqm1/__init__.py b/src/app/models/dqm1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/models/dqm1/associations.py b/src/app/models/dqm1/associations.py new file mode 100644 index 0000000..38abef2 --- /dev/null +++ b/src/app/models/dqm1/associations.py @@ -0,0 +1,19 @@ +from typing import Optional + +from sqlmodel import Field, SQLModel + + +class MonsterSkillLink(SQLModel, table=True): + """ + many-to-many association table linking a monster to three different skills. + """ + + id: Optional[int] = Field(default=None, primary_key=True) + monster_id: Optional[int] = Field( + default=None, + foreign_key="monsterdetail.id", + ) + skill_id: Optional[int] = Field( + default=None, + foreign_key="skill.id", + ) diff --git a/src/app/model_enums.py b/src/app/models/dqm1/enums.py similarity index 100% rename from src/app/model_enums.py rename to src/app/models/dqm1/enums.py diff --git a/src/app/models/dqm1/item.py b/src/app/models/dqm1/item.py new file mode 100644 index 0000000..708732c --- /dev/null +++ b/src/app/models/dqm1/item.py @@ -0,0 +1,17 @@ +from typing import Optional + +from sqlmodel import Field, SQLModel + + +class Item(SQLModel, table=True): + """ + Lists all items sold in shops and found in the field + """ + + id: Optional[int] = Field(default=None, primary_key=True) + item_name: str + item_category: str + item_description: str + price: Optional[int] = Field(default=None) + sell_price: Optional[int] = Field(default=None) + sell_location: str diff --git a/src/app/models/dqm1/monster.py b/src/app/models/dqm1/monster.py new file mode 100644 index 0000000..b3fe453 --- /dev/null +++ b/src/app/models/dqm1/monster.py @@ -0,0 +1,92 @@ +from typing import List, Optional, TYPE_CHECKING + +from sqlmodel import Field, Relationship, SQLModel + +from .associations import MonsterSkillLink + +if TYPE_CHECKING: + from .skill import Skill + from .monster_family import MonsterFamily + + +class MonsterDetailBase(SQLModel): + """ + Monster details from in-game bestiary. Shows name, family, and description. + """ + + new_name: str + old_name: str + description: str + + # one-to-many relation where a family is linked to many monsters + family_id: int = Field(foreign_key="monsterfamily.id") + + +class MonsterDetail(MonsterDetailBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + family: Optional["MonsterFamily"] = Relationship(back_populates="monsters") + skills: List["Skill"] = Relationship( + back_populates="monsters", link_model=MonsterSkillLink + ) + + +class MonsterBreedingLinkBase(SQLModel): + child_id: Optional[int] = Field(default=None, foreign_key="monsterdetail.id") + pedigree_id: Optional[int] = Field(default=None, foreign_key="monsterdetail.id") + parent2_id: Optional[int] = Field(default=None, foreign_key="monsterdetail.id") + pedigree_family_id: Optional[int] = Field( + default=None, foreign_key="monsterfamily.id" + ) + family2_id: Optional[int] = Field(default=None, foreign_key="monsterfamily.id") + + +class MonsterBreedingLink(MonsterBreedingLinkBase, table=True): + """ + many-to-many association table between MonsterDetail and MonsterFamily + that represents breeding combinations. + + child_id, pedigree, and parent_2 represent individual monster ids. + pedigree_family and family_2 represent family type. + + In order to make new monster, two parents are required. + + 4 different combinations possible: + pedigree + parent_2 -- specific monster + specific monster + pedigree + family_2 -- specific monster + any monster from the family type + pedigree_family + parent_2 -- specific family type + specific monster + pedigree_family + family_2 -- family + different family type + """ + + id: Optional[int] = Field(default=None, primary_key=True) + child: "MonsterDetail" = Relationship( + sa_relationship_kwargs={ + "primaryjoin": "MonsterBreedingLink.child_id==MonsterDetail.id", + "lazy": "joined", + } + ) + pedigree: "MonsterDetail" = Relationship( + sa_relationship_kwargs={ + "primaryjoin": "MonsterBreedingLink.pedigree_id==MonsterDetail.id", + "lazy": "joined", + } + ) + parent2: "MonsterDetail" = Relationship( + sa_relationship_kwargs={ + "primaryjoin": "MonsterBreedingLink.parent2_id==MonsterDetail.id", + "lazy": "joined", + } + ) + pedigree_family: "MonsterFamily" = Relationship( + sa_relationship_kwargs={ + "primaryjoin": "MonsterBreedingLink.pedigree_family_id" + "==MonsterFamily.id", + "lazy": "joined", + } + ) + family2: "MonsterFamily" = Relationship( + sa_relationship_kwargs={ + "primaryjoin": "MonsterBreedingLink.family2_id==MonsterFamily.id", + "lazy": "joined", + } + ) diff --git a/src/app/models/dqm1/monster_family.py b/src/app/models/dqm1/monster_family.py new file mode 100644 index 0000000..58e42a8 --- /dev/null +++ b/src/app/models/dqm1/monster_family.py @@ -0,0 +1,23 @@ +from typing import List, Optional, TYPE_CHECKING + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from .monster import MonsterDetail + + +class MonsterFamilyBase(SQLModel): + """ + There are 10 monster families in the game. + """ + + family_eng: str + + +class MonsterFamily(MonsterFamilyBase, table=True): + """ + one-to-many relation between family and monsters. + """ + + id: Optional[int] = Field(default=None, primary_key=True) + monsters: List["MonsterDetail"] = Relationship(back_populates="family") diff --git a/src/app/models/dqm1/schemas.py b/src/app/models/dqm1/schemas.py new file mode 100644 index 0000000..8f880b4 --- /dev/null +++ b/src/app/models/dqm1/schemas.py @@ -0,0 +1,55 @@ +from typing import List, Optional + +from .monster import MonsterBreedingLinkBase, MonsterDetailBase +from .monster_family import MonsterFamilyBase +from .skill import Skill, SkillBase, SkillCombineBase + + +class MonsterFamilyRead(MonsterFamilyBase): + id: int + + +class MonsterFamilyReadWithMonsterDetail(MonsterFamilyRead): + monsters: List["MonsterDetailRead"] = [] + + +class MonsterDetailRead(MonsterDetailBase): + id: int + + +class MonsterDetailWithFamily(MonsterDetailRead): + family: Optional[MonsterFamilyRead] = None + + +class MonsterBreedingLinkRead(MonsterBreedingLinkBase): + id: int + + +class MonsterBreedingLinkReadWithInfo(MonsterBreedingLinkRead): + child: Optional[MonsterDetailRead] + pedigree: Optional[MonsterDetailRead] + parent2: Optional[MonsterDetailRead] + pedigree_family: Optional[MonsterFamilyRead] + family2: Optional[MonsterFamilyRead] + + +class SkillRead(SkillBase): + id: int + + +class SkillReadWithMonster(SkillRead): + monsters: Optional[MonsterDetailRead] + + +class SkillUpgradeRead(SkillRead): + upgrade_to: Optional[Skill] + upgrade_from: Optional[Skill] + + +class MonsterDetailSkill(MonsterDetailWithFamily): + skills: List[SkillRead] = [] + + +class SkillCombineRead(SkillCombineBase): + id: int + needed_skill: Optional[SkillRead] diff --git a/src/app/models/dqm1/skill.py b/src/app/models/dqm1/skill.py new file mode 100644 index 0000000..9603a36 --- /dev/null +++ b/src/app/models/dqm1/skill.py @@ -0,0 +1,89 @@ +from typing import List, Optional, TYPE_CHECKING + +from sqlmodel import Field, Relationship, SQLModel + +from .associations import MonsterSkillLink + +if TYPE_CHECKING: + from .monster import MonsterDetail + + +class SkillBase(SQLModel): + """ + Shows description, MP cost, and required stats to learn skill. + Each monster naturally learns 3 skills. + """ + + category_type: str + family_type: str + new_name: Optional[str] = Field(default=None) + old_name: str + description: str + mp_cost: int + required_level: int + required_hp: Optional[int] = None + required_mp: Optional[int] = None + required_attack: Optional[int] = None + required_defense: Optional[int] = None + required_speed: Optional[int] = None + required_intelligence: Optional[int] = None + + +class Skill(SkillBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + upgrade_to_id: Optional[int] = Field( + foreign_key="skill.id", # lowercase refers to database table name + default=None, + ) + upgrade_to: Optional["Skill"] = Relationship( + sa_relationship_kwargs={ + "primaryjoin": "Skill.upgrade_to_id==Skill.id", + "lazy": "joined", + "remote_side": "Skill.id", # uppercase refers to this Skill class + } + ) + + upgrade_from_id: Optional[int] = Field( + foreign_key="skill.id", + default=None, + ) + upgrade_from: Optional["Skill"] = Relationship( + sa_relationship_kwargs={ + "primaryjoin": "Skill.upgrade_from_id==Skill.id", + "lazy": "joined", + "remote_side": "Skill.id", + } + ) + + monsters: List["MonsterDetail"] = Relationship( + back_populates="skills", link_model=MonsterSkillLink + ) + + +class SkillCombineBase(SQLModel): + combo_skill_id: Optional[int] = Field(default=None, foreign_key="skill.id") + needed_skill_id: Optional[int] = Field(default=None, foreign_key="skill.id") + + +class SkillCombine(SkillCombineBase, table=True): + """ + many-to-many association table showing certain needed skills combine to + learn new combo skill. + """ + + id: Optional[int] = Field(default=None, primary_key=True) + + combo_skill: Skill = Relationship( + sa_relationship_kwargs={ + "primaryjoin": "SkillCombine.combo_skill_id==Skill.id", + "lazy": "joined", + } + ) + + needed_skill: Skill = Relationship( + sa_relationship_kwargs={ + "primaryjoin": "SkillCombine.needed_skill_id==Skill.id", + "lazy": "joined", + } + ) diff --git a/src/app/routers/__init__.py b/src/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/routers/dqm1_endpoints.py b/src/app/routers/dqm1_endpoints.py new file mode 100644 index 0000000..017d042 --- /dev/null +++ b/src/app/routers/dqm1_endpoints.py @@ -0,0 +1,168 @@ +from typing import List, Optional + +from fastapi import APIRouter, Depends, HTTPException +from sqlmodel import Session, select + +from src.app.database import get_session +from src.app.models.dqm1.enums import ( + ItemCategory, + ItemSellLocation, + SkillCategory, + SkillFamily, +) +from src.app.models.dqm1.item import Item +from src.app.models.dqm1.monster import MonsterBreedingLink, MonsterDetail +from src.app.models.dqm1.monster_family import MonsterFamily +from src.app.models.dqm1.schemas import ( + MonsterBreedingLinkReadWithInfo, + MonsterDetailSkill, + MonsterDetailWithFamily, + MonsterFamilyReadWithMonsterDetail, + SkillCombineRead, + SkillUpgradeRead, +) +from src.app.models.dqm1.skill import Skill, SkillCombine + +router = APIRouter( + prefix="/dqm1", +) + + +@router.get( + "/monsters", + response_model=List[MonsterDetailWithFamily], + tags=["dqm1 monsters"], +) +async def read_monsters( + *, session: Session = Depends(get_session), family: Optional[int] = None +): + """ + **Parameter Descriptions**
+ **new_name** : updated name used in later Dragon Quest games
+ **old_name** : name used in the game
+ **description** : in game beastiary description
+ **family** : a monster is part of one of 10 different monster families
+ """ + monsters = select(MonsterDetail) + if family: + monsters = monsters.where(MonsterDetail.family_id == family) + monsters_result = session.exec(monsters).all() + return monsters_result + + +@router.get( + "/monsters/{monster_id}", + response_model=MonsterDetailWithFamily, + tags=["dqm1 monsters"], +) +async def read_monster(*, session: Session = Depends(get_session), monster_id: int): + monster = session.get(MonsterDetail, monster_id) + if not monster: + raise HTTPException(status_code=404, detail="Monster not found") + return monster + + +@router.get( + "/monstersandskill/{monster_id}", + response_model=MonsterDetailSkill, + tags=["dqm1 monsters"], +) +async def read_monster_skill( + *, session: Session = Depends(get_session), monster_id: int +): + monster = session.get(MonsterDetail, monster_id) + if not monster: + raise HTTPException(status_code=404, detail="Monster not found") + return monster + + +@router.get( + "/family/{family_id}", + response_model=MonsterFamilyReadWithMonsterDetail, + tags=["dqm1 monsters"], +) +async def read_family(*, session: Session = Depends(get_session), family_id: int): + family = session.get(MonsterFamily, family_id) + if not family: + raise HTTPException(status_code=404, detail="Family not found") + return family + + +@router.get("/skills", tags=["dqm1 skills"]) +async def read_skills( + *, + session: Session = Depends(get_session), + category: Optional[SkillCategory] = None, + skill_family: Optional[SkillFamily] = None, +): + skills = select(Skill) + if category: + skills = skills.where(Skill.category_type == category) + if skill_family: + skills = skills.where(Skill.family_type == skill_family) + skills_result = session.exec(skills).all() + return skills_result + + +@router.get("/skills/{skill_id}", response_model=SkillUpgradeRead, tags=["dqm1 skills"]) +async def read_skill(*, session: Session = Depends(get_session), skill_id: int): + skill = session.get(Skill, skill_id) + if not skill: + raise HTTPException(status_code=404, detail="Skill not found") + return skill + + +@router.get( + "/skillcombine/{skill_id}", + response_model=List[SkillCombineRead], + tags=["dqm1 skills"], +) +async def get_skill_combo(*, session: Session = Depends(get_session), skill_id: int): + query = select(SkillCombine).where(SkillCombine.combo_skill_id == skill_id) + skill = session.exec(query).all() + return skill + + +@router.get("/items", tags=["dqm1 items"]) +async def read_items( + *, + session: Session = Depends(get_session), + category: Optional[ItemCategory] = None, + selllocation: Optional[ItemSellLocation] = None, +): + items = select(Item) + if category: + items = items.where(Item.item_category == category) + if selllocation: + items = items.where(Item.sell_location == selllocation) + items_result = session.exec(items).all() + return items_result + + +@router.get("/items/{item_id}", tags=["dqm1 items"]) +async def read_item(*, session: Session = Depends(get_session), item_id: int): + item = session.get(Item, item_id) + if not item: + raise HTTPException(status_code=404, detail="Item not found") + return item + + +@router.get( + "/breeding/{monster_id}", + response_model=List[MonsterBreedingLinkReadWithInfo], + tags=["dqm1 monsters"], +) +async def get_breeding_combos( + *, session: Session = Depends(get_session), monster_id: int +): + """ + Given a monster_id, finds all breeding combination that results in + the target monster or uses the target monster as a parent + """ + query = select(MonsterBreedingLink).where( + (MonsterBreedingLink.child_id == monster_id) + | (MonsterBreedingLink.pedigree_id == monster_id) + | (MonsterBreedingLink.parent2_id == monster_id) + ) + breeding_combos = session.exec(query).all() + return breeding_combos diff --git a/src/static/images/readme/FastAPI-readme-1.jpg b/src/static/images/readme/FastAPI-readme-1.jpg deleted file mode 100644 index b299853..0000000 Binary files a/src/static/images/readme/FastAPI-readme-1.jpg and /dev/null differ diff --git a/src/static/images/readme/FastAPI-readme-2.jpg b/src/static/images/readme/FastAPI-readme-2.jpg deleted file mode 100644 index c2b340d..0000000 Binary files a/src/static/images/readme/FastAPI-readme-2.jpg and /dev/null differ diff --git a/src/static/images/readme/FastAPI-screenshot-1.JPG b/src/static/images/readme/FastAPI-screenshot-1.JPG new file mode 100644 index 0000000..44ef8e5 Binary files /dev/null and b/src/static/images/readme/FastAPI-screenshot-1.JPG differ diff --git a/src/static/images/readme/FastAPI-screenshot-2.JPG b/src/static/images/readme/FastAPI-screenshot-2.JPG new file mode 100644 index 0000000..79e9007 Binary files /dev/null and b/src/static/images/readme/FastAPI-screenshot-2.JPG differ diff --git a/tests/conftest.py b/tests/conftest.py index 1c158be..68e365d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,97 +1,97 @@ -import csv -from pathlib import Path - -import pytest -from fastapi.testclient import TestClient -from sqlmodel import Session, SQLModel, create_engine -from sqlmodel.pool import StaticPool - -from src.app.main import app, get_session -from src.app.models import ( - Item, - MonsterBreedingLink, - MonsterDetail, - MonsterFamily, - MonsterSkillLink, - Skill, - SkillCombine, -) - - -@pytest.fixture(name="session") -def session_fixture(): - test_engine = create_engine( - "sqlite://", - connect_args={"check_same_thread": False}, - poolclass=StaticPool, - ) - SQLModel.metadata.create_all(test_engine) - with Session(test_engine) as session: - yield session - - SQLModel.metadata.drop_all(test_engine) - - -@pytest.fixture(name="client") -def client_fixture(session: Session): - def get_session_override(): - yield session - - app.dependency_overrides[get_session] = get_session_override - - client = TestClient(app) - yield client - app.dependency_overrides.clear() - - -@pytest.fixture(name="session_module", scope="module") -def session_module(): - test_engine = create_engine( - "sqlite://", - connect_args={"check_same_thread": False}, - poolclass=StaticPool, - ) - SQLModel.metadata.create_all(test_engine) - with Session(test_engine) as session: - yield session - - SQLModel.metadata.drop_all(test_engine) - - -@pytest.fixture(name="client_module", scope="module") -def client_module(session_module: Session): - def get_session_override(): - yield session_module - - app.dependency_overrides[get_session] = get_session_override - - client = TestClient(app) - yield client - app.dependency_overrides.clear() - - -@pytest.fixture(name="load_all_csvdata", scope="module") -def load_csv_data(session_module: Session): - CSV_FILES_PATH = Path(__file__).resolve().parent.parent / "src" / "csv_files" - - csv_files = { - (CSV_FILES_PATH / "DQM1_items.csv", Item), - (CSV_FILES_PATH / "DQM1_monster_family.csv", MonsterFamily), - (CSV_FILES_PATH / "DQM1_skills.csv", Skill), - (CSV_FILES_PATH / "DQM1_skill_combo.csv", SkillCombine), - (CSV_FILES_PATH / "DQM1_monsterdetails.csv", MonsterDetail), - (CSV_FILES_PATH / "DQM1_breeding_combo.csv", MonsterBreedingLink), - (CSV_FILES_PATH / "DQM1_monster_skill_link.csv", MonsterSkillLink), - } - - for csvfile, Model in csv_files: - try: - with open(csvfile, encoding="utf-8-sig") as file: - reader = csv.DictReader(file) - for row in reader: - # replace empty string with None - row = {k: (None if v == "" else v) for k, v in row.items()} - session_module.add(Model(**row)) - session_module.commit() - except Exception as e: - print(f"Error loading {csvfile} : {e}") +import csv +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine +from sqlmodel.pool import StaticPool + +from src.app.database import get_session +from src.app.main import app +from src.app.models.dqm1.item import Item +from src.app.models.dqm1.associations import MonsterSkillLink +from src.app.models.dqm1.monster import ( + MonsterBreedingLink, + MonsterDetail, +) +from src.app.models.dqm1.monster_family import MonsterFamily +from src.app.models.dqm1.skill import Skill, SkillCombine + + +@pytest.fixture(name="session") +def session_fixture(): + test_engine = create_engine( + "sqlite://", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + SQLModel.metadata.create_all(test_engine) + with Session(test_engine) as session: + yield session + + SQLModel.metadata.drop_all(test_engine) + + +@pytest.fixture(name="client") +def client_fixture(session: Session): + def get_session_override(): + yield session + + app.dependency_overrides[get_session] = get_session_override + + client = TestClient(app) + yield client + app.dependency_overrides.clear() + + +@pytest.fixture(name="session_module", scope="module") +def session_module(): + test_engine = create_engine( + "sqlite://", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + SQLModel.metadata.create_all(test_engine) + with Session(test_engine) as session: + yield session + + SQLModel.metadata.drop_all(test_engine) + + +@pytest.fixture(name="client_module", scope="module") +def client_module(session_module: Session): + def get_session_override(): + yield session_module + + app.dependency_overrides[get_session] = get_session_override + + client = TestClient(app) + yield client + app.dependency_overrides.clear() + + +@pytest.fixture(name="load_all_csvdata", scope="module") +def load_csv_data(session_module: Session): + CSV_FILES_PATH = Path(__file__).resolve().parent.parent / "src" / "csv_files" + + csv_files = { + (CSV_FILES_PATH / "DQM1_items.csv", Item), + (CSV_FILES_PATH / "DQM1_monster_family.csv", MonsterFamily), + (CSV_FILES_PATH / "DQM1_skills.csv", Skill), + (CSV_FILES_PATH / "DQM1_skill_combo.csv", SkillCombine), + (CSV_FILES_PATH / "DQM1_monsterdetails.csv", MonsterDetail), + (CSV_FILES_PATH / "DQM1_breeding_combo.csv", MonsterBreedingLink), + (CSV_FILES_PATH / "DQM1_monster_skill_link.csv", MonsterSkillLink), + } + + for csvfile, Model in csv_files: + try: + with open(csvfile, encoding="utf-8-sig") as file: + reader = csv.DictReader(file) + for row in reader: + # replace empty string with None + row = {k: (None if v == "" else v) for k, v in row.items()} + session_module.add(Model(**row)) + session_module.commit() + except Exception as e: + print(f"Error loading {csvfile} : {e}") diff --git a/tests/test_insert_data.py b/tests/test_insert_data.py index 5169a8c..35c4f2d 100644 --- a/tests/test_insert_data.py +++ b/tests/test_insert_data.py @@ -1,585 +1,584 @@ -from fastapi.testclient import TestClient -from sqlmodel import Session - -from src.app.models import ( - Item, - MonsterBreedingLink, - MonsterDetail, - MonsterFamily, - MonsterSkillLink, - Skill, - SkillCombine, -) - - -def test_read_root(client: TestClient): - response = client.get("/") - assert response.status_code == 200 - assert response.json() == { - "message": ("Welcome to the DQMonsters API. Go to the Swagger UI interface") - } - - -def test_insert_monster(client: TestClient, session: Session): - """ - - Tests individual insertion of monster data entry into monsterdetail - datatable - """ - session.add( - MonsterDetail( - new_name="Slime", - old_name="Slime", - description="The most abundant of this popular specie", - family_id=1, - ) - ) - session.commit() - - response = client.get("/dqm1/monsters/1") - data_entry = response.json() - - monster_comparison = { - "id": 1, - "new_name": "Slime", - "old_name": "Slime", - "description": "The most abundant of this popular specie", - "family_id": 1, - "family": None, - } - - assert response.status_code == 200 - assert data_entry["new_name"] == monster_comparison["new_name"] - assert data_entry["old_name"] == monster_comparison["old_name"] - assert data_entry["description"] == monster_comparison["description"] - assert data_entry["family_id"] == monster_comparison["family_id"] - assert data_entry == monster_comparison - - -def test_insert_monster_family(client: TestClient, session: Session): - """ - Test individual insertion of monster family data into monsterfamily table - """ - family_list = [ - "SLIME", - "DRAGON", - "BEAST", - "BIRD", - "PLANT", - "BUG", - "DEVIL", - "UNDEAD", - "MATERIAL", - "???", - ] - - for family in family_list: - session.add(MonsterFamily(family_eng=f"{family}")) - session.commit() - - for i in range(1, 11): - response = client.get(f"dqm1/family/{i}") - family_entry = response.json() - - assert response.status_code == 200 - assert family_entry["family_eng"] == family_list[i - 1] - - -def test_insert_skill(client: TestClient, session: Session): - """ - Tests individual insertion of skill data into skill datatable - Tests association between skills via upgrade_to and upgrade_from - - 'Blaze' upgrades to 'Blazemore', which upgrades to 'Blazemost' - """ - session.add( - Skill( - category_type="Attack", - family_type="Frizz", - new_name="Frizz", - old_name="Blaze", - description="Inflict damage with small fireball ", - mp_cost=2, - required_level=2, - required_mp=7, - required_intelligence=20, - upgrade_to_id=2, - ) - ) - session.add( - Skill( - category_type="Attack", - family_type="Frizz", - new_name="Frizzle", - old_name="Blazemore", - description="Inflict damage with giant fireball", - mp_cost=4, - required_level=13, - required_mp=46, - required_intelligence=64, - upgrade_to_id=3, - upgrade_from_id=1, - ) - ) - session.add( - Skill( - category_type="Attack", - family_type="Frizz", - new_name="Kafrizzle", - old_name="Blazemost", - description="Inflict damage with pillars of fire", - mp_cost=10, - required_level=28, - required_mp=112, - required_intelligence=146, - upgrade_from_id=2, - ) - ) - session.commit() - - response = client.get("dqm1/skills/1") - skill_entry = response.json() - - skill_comparison = { - "category_type": "Attack", - "family_type": "Frizz", - "new_name": "Frizz", - "old_name": "Blaze", - "description": "Inflict damage with small fireball ", - "mp_cost": 2, - "required_level": 2, - "required_hp": None, - "required_mp": 7, - "required_attack": None, - "required_defense": None, - "required_speed": None, - "required_intelligence": 20, - "id": 1, - "upgrade_to": { - "new_name": "Frizzle", - "required_hp": None, - "required_mp": 46, - "required_attack": None, - "required_defense": None, - "required_speed": None, - "required_intelligence": 64, - "id": 2, - "upgrade_to_id": 3, - "upgrade_from_id": 1, - "category_type": "Attack", - "family_type": "Frizz", - "old_name": "Blazemore", - "description": "Inflict damage with giant fireball", - "mp_cost": 4, - "required_level": 13, - }, - "upgrade_from": None, - } - - assert response.status_code == 200 - assert skill_entry == skill_comparison - - skill_entry_2 = client.get("dqm1/skills/2").json() - assert response.status_code == 200 - assert skill_entry_2["upgrade_from"]["old_name"] == "Blaze" - assert skill_entry_2["upgrade_to"]["old_name"] == "Blazemost" - - -def test_insert_item(client: TestClient, session: Session): - """ - Tests individual insertion of item data into items datatable - """ - session.add( - Item( - item_name="Herb", - item_category="recovery", - item_description="Restores around 30 HP", - price=10, - sell_price=6, - sell_location="Bazaar shop 1", - ) - ) - session.commit() - - response = client.get("/dqm1/items/1") - item_entry = response.json() - - item_comparison = { - "item_name": "Herb", - "item_category": "recovery", - "item_description": "Restores around 30 HP", - "price": 10, - "sell_price": 6, - "sell_location": "Bazaar shop 1", - } - - assert response.status_code == 200 - for key, value in item_comparison.items(): - assert item_entry[key] == value - - -def test_insert_item_with_none(client: TestClient, session: Session): - """ - Tests individual insertion of item data into items datatable that has a - price and sell_price of None - """ - session.add( - Item( - item_name="Tiny medal", - item_category="dungeon use", - item_description="Collect and give to medal master for a prize", - price=None, - sell_price=None, - sell_location="found in field", - ) - ) - session.commit() - - response = client.get("/dqm1/items/1") - item_entry = response.json() - - item_comparison = { - "item_name": "Tiny medal", - "item_category": "dungeon use", - "item_description": "Collect and give to medal master for a prize", - "price": None, - "sell_price": None, - "sell_location": "found in field", - } - - assert response.status_code == 200 - for key, value in item_comparison.items(): - assert item_entry[key] == value - - -def test_monster_skill_link(client: TestClient, session: Session): - """ - Tests monster datatable association with skill datatable - """ - session.add( - MonsterDetail( - new_name="Slime", - old_name="Slime", - description="The most abundant of this popular specie", - family_id=1, - ) - ) - session.add( - Skill( - category_type="Attack", - family_type="Sizz", - new_name="Sizz", - old_name="Firebal", - description="Inflict damage to all enemies with a small blaze", - mp_cost=4, - required_level=3, - required_mp=11, - required_intelligence=23, - ) - ) - session.add( - Skill( - category_type="Attack", - family_type="Magic Burst", - new_name="Magic Burst", - old_name="MegaMagic", - description="The most powerful spell to affect all enemies", - mp_cost=999, - required_level=38, - required_mp=210, - required_attack=114, - required_speed=224, - ) - ) - session.add( - Skill( - category_type="Support", - family_type="Dazzle", - new_name="Dazzleflash", - old_name="Radiant", - description="Blinds all enemies with its bright light", - mp_cost=2, - required_level=12, - required_mp=42, - required_speed=72, - required_intelligence=72, - ) - ) - - session.add( - MonsterSkillLink( - monster_id=1, - skill_id=1, - ) - ) - session.add( - MonsterSkillLink( - monster_id=1, - skill_id=2, - ) - ) - session.add( - MonsterSkillLink( - monster_id=1, - skill_id=3, - ) - ) - session.commit() - - response = client.get("dqm1/monstersandskill/1") - monster_entry = response.json() - - assert response.status_code == 200 - assert len(monster_entry["skills"]) == 3 - assert monster_entry["skills"][0]["old_name"] == "Firebal" - assert monster_entry["skills"][1]["old_name"] == "MegaMagic" - assert monster_entry["skills"][2]["old_name"] == "Radiant" - - -def test_skill_combine(client: TestClient, session: Session): - """ - Tests many-to-many connection between skills via SkillCombine Model - """ - # Add skill to skills datatable. - session.add( - Skill( - # id = 1 - category_type="Attack", - family_type="Frizz", - new_name="Frizz", - old_name="Blaze", - description="Inflict damage with small fireball ", - mp_cost=2, - required_level=2, - required_mp=7, - required_intelligence=20, - upgrade_to_id=2, - ) - ) - session.add( - Skill( - # id = 2 - category_type="Attack", - family_type="Frizz", - new_name="Frizzle", - old_name="Blazemore", - description="Inflict damage with giant fireball", - mp_cost=4, - required_level=13, - required_mp=46, - required_intelligence=64, - upgrade_to_id=3, - upgrade_from_id=1, - ) - ) - session.add( - Skill( - # id = 3 - category_type="Attack", - family_type="Frizz", - new_name="Kafrizzle", - old_name="Blazemost", - description="Inflict damage with pillars of fire", - mp_cost=10, - required_level=28, - required_mp=112, - required_intelligence=146, - upgrade_from_id=2, - ) - ) - session.add( - Skill( - # id = 4 - category_type="Attack", - family_type="Frizz", - new_name="Flame Slash", - old_name="FireSlash", - description="Burning blade sword attack", - mp_cost=3, - required_level=11, - required_hp=77, - required_mp=34, - required_attack=66, - required_intelligence=42, - ) - ) - session.add( - Skill( - # id = 5 - category_type="Support", - family_type="Status support", - new_name="Muster Strength", - old_name="ChargeUP", - description="Additional Damage next turn", - mp_cost=0, - required_level=14, - required_hp=98, - required_defense=84, - ) - ) - - # Add SkillCombine connection - # 'FireSlash' can be learned if 'Blazemore' and 'ChargeUP' is known - session.add( - SkillCombine( - combo_skill_id=4, - needed_skill_id=2, - ) - ) - session.add( - SkillCombine( - combo_skill_id=4, - needed_skill_id=5, - ) - ) - session.commit() - - response = client.get("dqm1/skillcombine/4") - skill_combo = response.json() - - assert response.status_code == 200 - - assert skill_combo[0]["needed_skill_id"] == 2 - assert skill_combo[0]["needed_skill"]["old_name"] == "Blazemore" - - assert skill_combo[1]["needed_skill_id"] == 5 - assert skill_combo[1]["needed_skill"]["old_name"] == "ChargeUP" - - -def test_monster_breeding_link(client: TestClient, session: Session): - """ - Test breeding combo insertion. - """ - family_list = [ - "SLIME", # family id = 1 - "DRAGON", # family id = 2 - "BEAST", - "BIRD", - "PLANT", - "BUG", - "DEVIL", - "UNDEAD", - "MATERIAL", - "???", - ] - - for family in family_list: - session.add(MonsterFamily(family_eng=f"{family}")) - - session.add( - MonsterDetail( - # id = 1 - new_name="Drake Slime", - old_name="DrakSlime", - description="Moves & jumps with its tail and wings", - family_id=1, - ) - ) - session.add( - MonsterDetail( - # id = 2 - new_name="Wild slime", - old_name="FangSlime", - description="Has a red Mohawk and is very brave & proud", - family_id=1, - ) - ) - session.add( - MonsterDetail( - # id = 3 - new_name="Spiked hare", - old_name="Almiraj", - description="When cornered, it charges with its sharp horns", - family_id=3, - ) - ) - - # tests pedigree_family + family2 connection - session.add( - MonsterBreedingLink( - child_id=1, - pedigree_family_id=1, - family2_id=2, - ) - ) - # tests pedigree_family + parent2 connection - session.add( - MonsterBreedingLink( - child_id=2, - parent2_id=3, - pedigree_family_id=1, - ) - ) - - session.commit() - - response1 = client.get("dqm1/breeding/1") - breeding_query1 = response1.json() - - entry_comparison1 = [ - { - "id": 1, - "child_id": 1, - "pedigree_id": None, - "parent2_id": None, - "pedigree_family_id": 1, - "family2_id": 2, - "child": { - "id": 1, - "new_name": "Drake Slime", - "old_name": "DrakSlime", - "description": "Moves & jumps with its tail and wings", - "family_id": 1, - }, - "pedigree": None, - "parent2": None, - "pedigree_family": { - "family_eng": "SLIME", - "id": 1, - }, - "family2": { - "family_eng": "DRAGON", - "id": 2, - }, - }, - ] - - assert response1.status_code == 200 - assert breeding_query1[0]["child_id"] == 1 - assert breeding_query1[0]["child"]["old_name"] == "DrakSlime" - assert breeding_query1 == entry_comparison1 - - response2 = client.get("dqm1/breeding/2") - breeding_query2 = response2.json() - - entry_comparison2 = [ - { - "id": 2, - "child_id": 2, - "pedigree_id": None, - "parent2_id": 3, - "pedigree_family_id": 1, - "family2_id": None, - "child": { - "id": 2, - "new_name": "Wild slime", - "old_name": "FangSlime", - "description": "Has a red Mohawk and is very brave & proud", - "family_id": 1, - }, - "pedigree": None, - "parent2": { - "id": 3, - "new_name": "Spiked hare", - "old_name": "Almiraj", - "description": ("When cornered, it charges with its sharp horns"), - "family_id": 3, - }, - "pedigree_family": { - "family_eng": "SLIME", - "id": 1, - }, - "family2": None, - } - ] - - assert response2.status_code == 200 - assert breeding_query2 == entry_comparison2 +from fastapi.testclient import TestClient +from sqlmodel import Session + +from src.app.models.dqm1.item import Item +from src.app.models.dqm1.monster import ( + MonsterBreedingLink, + MonsterDetail, + MonsterSkillLink, +) +from src.app.models.dqm1.monster_family import MonsterFamily +from src.app.models.dqm1.skill import Skill, SkillCombine + + +def test_read_root(client: TestClient): + response = client.get("/") + assert response.status_code == 200 + assert response.json() == { + "message": ("Welcome to the DQMonsters API. Go to the Swagger UI interface") + } + + +def test_insert_monster(client: TestClient, session: Session): + """ + - Tests individual insertion of monster data entry into monsterdetail + datatable + """ + session.add( + MonsterDetail( + new_name="Slime", + old_name="Slime", + description="The most abundant of this popular specie", + family_id=1, + ) + ) + session.commit() + + response = client.get("/dqm1/monsters/1") + data_entry = response.json() + + monster_comparison = { + "id": 1, + "new_name": "Slime", + "old_name": "Slime", + "description": "The most abundant of this popular specie", + "family_id": 1, + "family": None, + } + + assert response.status_code == 200 + assert data_entry["new_name"] == monster_comparison["new_name"] + assert data_entry["old_name"] == monster_comparison["old_name"] + assert data_entry["description"] == monster_comparison["description"] + assert data_entry["family_id"] == monster_comparison["family_id"] + assert data_entry == monster_comparison + + +def test_insert_monster_family(client: TestClient, session: Session): + """ + Test individual insertion of monster family data into monsterfamily table + """ + family_list = [ + "SLIME", + "DRAGON", + "BEAST", + "BIRD", + "PLANT", + "BUG", + "DEVIL", + "UNDEAD", + "MATERIAL", + "???", + ] + + for family in family_list: + session.add(MonsterFamily(family_eng=f"{family}")) + session.commit() + + for i in range(1, 11): + response = client.get(f"dqm1/family/{i}") + family_entry = response.json() + + assert response.status_code == 200 + assert family_entry["family_eng"] == family_list[i - 1] + + +def test_insert_skill(client: TestClient, session: Session): + """ + Tests individual insertion of skill data into skill datatable + Tests association between skills via upgrade_to and upgrade_from + - 'Blaze' upgrades to 'Blazemore', which upgrades to 'Blazemost' + """ + session.add( + Skill( + category_type="Attack", + family_type="Frizz", + new_name="Frizz", + old_name="Blaze", + description="Inflict damage with small fireball ", + mp_cost=2, + required_level=2, + required_mp=7, + required_intelligence=20, + upgrade_to_id=2, + ) + ) + session.add( + Skill( + category_type="Attack", + family_type="Frizz", + new_name="Frizzle", + old_name="Blazemore", + description="Inflict damage with giant fireball", + mp_cost=4, + required_level=13, + required_mp=46, + required_intelligence=64, + upgrade_to_id=3, + upgrade_from_id=1, + ) + ) + session.add( + Skill( + category_type="Attack", + family_type="Frizz", + new_name="Kafrizzle", + old_name="Blazemost", + description="Inflict damage with pillars of fire", + mp_cost=10, + required_level=28, + required_mp=112, + required_intelligence=146, + upgrade_from_id=2, + ) + ) + session.commit() + + response = client.get("dqm1/skills/1") + skill_entry = response.json() + + skill_comparison = { + "category_type": "Attack", + "family_type": "Frizz", + "new_name": "Frizz", + "old_name": "Blaze", + "description": "Inflict damage with small fireball ", + "mp_cost": 2, + "required_level": 2, + "required_hp": None, + "required_mp": 7, + "required_attack": None, + "required_defense": None, + "required_speed": None, + "required_intelligence": 20, + "id": 1, + "upgrade_to": { + "new_name": "Frizzle", + "required_hp": None, + "required_mp": 46, + "required_attack": None, + "required_defense": None, + "required_speed": None, + "required_intelligence": 64, + "id": 2, + "upgrade_to_id": 3, + "upgrade_from_id": 1, + "category_type": "Attack", + "family_type": "Frizz", + "old_name": "Blazemore", + "description": "Inflict damage with giant fireball", + "mp_cost": 4, + "required_level": 13, + }, + "upgrade_from": None, + } + + assert response.status_code == 200 + assert skill_entry == skill_comparison + + skill_entry_2 = client.get("dqm1/skills/2").json() + assert response.status_code == 200 + assert skill_entry_2["upgrade_from"]["old_name"] == "Blaze" + assert skill_entry_2["upgrade_to"]["old_name"] == "Blazemost" + + +def test_insert_item(client: TestClient, session: Session): + """ + Tests individual insertion of item data into items datatable + """ + session.add( + Item( + item_name="Herb", + item_category="recovery", + item_description="Restores around 30 HP", + price=10, + sell_price=6, + sell_location="Bazaar shop 1", + ) + ) + session.commit() + + response = client.get("/dqm1/items/1") + item_entry = response.json() + + item_comparison = { + "item_name": "Herb", + "item_category": "recovery", + "item_description": "Restores around 30 HP", + "price": 10, + "sell_price": 6, + "sell_location": "Bazaar shop 1", + } + + assert response.status_code == 200 + for key, value in item_comparison.items(): + assert item_entry[key] == value + + +def test_insert_item_with_none(client: TestClient, session: Session): + """ + Tests individual insertion of item data into items datatable that has a + price and sell_price of None + """ + session.add( + Item( + item_name="Tiny medal", + item_category="dungeon use", + item_description="Collect and give to medal master for a prize", + price=None, + sell_price=None, + sell_location="found in field", + ) + ) + session.commit() + + response = client.get("/dqm1/items/1") + item_entry = response.json() + + item_comparison = { + "item_name": "Tiny medal", + "item_category": "dungeon use", + "item_description": "Collect and give to medal master for a prize", + "price": None, + "sell_price": None, + "sell_location": "found in field", + } + + assert response.status_code == 200 + for key, value in item_comparison.items(): + assert item_entry[key] == value + + +def test_monster_skill_link(client: TestClient, session: Session): + """ + Tests monster datatable association with skill datatable + """ + session.add( + MonsterDetail( + new_name="Slime", + old_name="Slime", + description="The most abundant of this popular specie", + family_id=1, + ) + ) + session.add( + Skill( + category_type="Attack", + family_type="Sizz", + new_name="Sizz", + old_name="Firebal", + description="Inflict damage to all enemies with a small blaze", + mp_cost=4, + required_level=3, + required_mp=11, + required_intelligence=23, + ) + ) + session.add( + Skill( + category_type="Attack", + family_type="Magic Burst", + new_name="Magic Burst", + old_name="MegaMagic", + description="The most powerful spell to affect all enemies", + mp_cost=999, + required_level=38, + required_mp=210, + required_attack=114, + required_speed=224, + ) + ) + session.add( + Skill( + category_type="Support", + family_type="Dazzle", + new_name="Dazzleflash", + old_name="Radiant", + description="Blinds all enemies with its bright light", + mp_cost=2, + required_level=12, + required_mp=42, + required_speed=72, + required_intelligence=72, + ) + ) + + session.add( + MonsterSkillLink( + monster_id=1, + skill_id=1, + ) + ) + session.add( + MonsterSkillLink( + monster_id=1, + skill_id=2, + ) + ) + session.add( + MonsterSkillLink( + monster_id=1, + skill_id=3, + ) + ) + session.commit() + + response = client.get("dqm1/monstersandskill/1") + monster_entry = response.json() + + assert response.status_code == 200 + assert len(monster_entry["skills"]) == 3 + assert monster_entry["skills"][0]["old_name"] == "Firebal" + assert monster_entry["skills"][1]["old_name"] == "MegaMagic" + assert monster_entry["skills"][2]["old_name"] == "Radiant" + + +def test_skill_combine(client: TestClient, session: Session): + """ + Tests many-to-many connection between skills via SkillCombine Model + """ + # Add skill to skills datatable. + session.add( + Skill( + # id = 1 + category_type="Attack", + family_type="Frizz", + new_name="Frizz", + old_name="Blaze", + description="Inflict damage with small fireball ", + mp_cost=2, + required_level=2, + required_mp=7, + required_intelligence=20, + upgrade_to_id=2, + ) + ) + session.add( + Skill( + # id = 2 + category_type="Attack", + family_type="Frizz", + new_name="Frizzle", + old_name="Blazemore", + description="Inflict damage with giant fireball", + mp_cost=4, + required_level=13, + required_mp=46, + required_intelligence=64, + upgrade_to_id=3, + upgrade_from_id=1, + ) + ) + session.add( + Skill( + # id = 3 + category_type="Attack", + family_type="Frizz", + new_name="Kafrizzle", + old_name="Blazemost", + description="Inflict damage with pillars of fire", + mp_cost=10, + required_level=28, + required_mp=112, + required_intelligence=146, + upgrade_from_id=2, + ) + ) + session.add( + Skill( + # id = 4 + category_type="Attack", + family_type="Frizz", + new_name="Flame Slash", + old_name="FireSlash", + description="Burning blade sword attack", + mp_cost=3, + required_level=11, + required_hp=77, + required_mp=34, + required_attack=66, + required_intelligence=42, + ) + ) + session.add( + Skill( + # id = 5 + category_type="Support", + family_type="Status support", + new_name="Muster Strength", + old_name="ChargeUP", + description="Additional Damage next turn", + mp_cost=0, + required_level=14, + required_hp=98, + required_defense=84, + ) + ) + + # Add SkillCombine connection + # 'FireSlash' can be learned if 'Blazemore' and 'ChargeUP' is known + session.add( + SkillCombine( + combo_skill_id=4, + needed_skill_id=2, + ) + ) + session.add( + SkillCombine( + combo_skill_id=4, + needed_skill_id=5, + ) + ) + session.commit() + + response = client.get("dqm1/skillcombine/4") + skill_combo = response.json() + + assert response.status_code == 200 + + assert skill_combo[0]["needed_skill_id"] == 2 + assert skill_combo[0]["needed_skill"]["old_name"] == "Blazemore" + + assert skill_combo[1]["needed_skill_id"] == 5 + assert skill_combo[1]["needed_skill"]["old_name"] == "ChargeUP" + + +def test_monster_breeding_link(client: TestClient, session: Session): + """ + Test breeding combo insertion. + """ + family_list = [ + "SLIME", # family id = 1 + "DRAGON", # family id = 2 + "BEAST", + "BIRD", + "PLANT", + "BUG", + "DEVIL", + "UNDEAD", + "MATERIAL", + "???", + ] + + for family in family_list: + session.add(MonsterFamily(family_eng=f"{family}")) + + session.add( + MonsterDetail( + # id = 1 + new_name="Drake Slime", + old_name="DrakSlime", + description="Moves & jumps with its tail and wings", + family_id=1, + ) + ) + session.add( + MonsterDetail( + # id = 2 + new_name="Wild slime", + old_name="FangSlime", + description="Has a red Mohawk and is very brave & proud", + family_id=1, + ) + ) + session.add( + MonsterDetail( + # id = 3 + new_name="Spiked hare", + old_name="Almiraj", + description="When cornered, it charges with its sharp horns", + family_id=3, + ) + ) + + # tests pedigree_family + family2 connection + session.add( + MonsterBreedingLink( + child_id=1, + pedigree_family_id=1, + family2_id=2, + ) + ) + # tests pedigree_family + parent2 connection + session.add( + MonsterBreedingLink( + child_id=2, + parent2_id=3, + pedigree_family_id=1, + ) + ) + + session.commit() + + response1 = client.get("dqm1/breeding/1") + breeding_query1 = response1.json() + + entry_comparison1 = [ + { + "id": 1, + "child_id": 1, + "pedigree_id": None, + "parent2_id": None, + "pedigree_family_id": 1, + "family2_id": 2, + "child": { + "id": 1, + "new_name": "Drake Slime", + "old_name": "DrakSlime", + "description": "Moves & jumps with its tail and wings", + "family_id": 1, + }, + "pedigree": None, + "parent2": None, + "pedigree_family": { + "family_eng": "SLIME", + "id": 1, + }, + "family2": { + "family_eng": "DRAGON", + "id": 2, + }, + }, + ] + + assert response1.status_code == 200 + assert breeding_query1[0]["child_id"] == 1 + assert breeding_query1[0]["child"]["old_name"] == "DrakSlime" + assert breeding_query1 == entry_comparison1 + + response2 = client.get("dqm1/breeding/2") + breeding_query2 = response2.json() + + entry_comparison2 = [ + { + "id": 2, + "child_id": 2, + "pedigree_id": None, + "parent2_id": 3, + "pedigree_family_id": 1, + "family2_id": None, + "child": { + "id": 2, + "new_name": "Wild slime", + "old_name": "FangSlime", + "description": "Has a red Mohawk and is very brave & proud", + "family_id": 1, + }, + "pedigree": None, + "parent2": { + "id": 3, + "new_name": "Spiked hare", + "old_name": "Almiraj", + "description": ("When cornered, it charges with its sharp horns"), + "family_id": 3, + }, + "pedigree_family": { + "family_eng": "SLIME", + "id": 1, + }, + "family2": None, + } + ] + + assert response2.status_code == 200 + assert breeding_query2 == entry_comparison2