Initial commit
This commit is contained in:
205
backend/app/router.py
Normal file
205
backend/app/router.py
Normal file
@@ -0,0 +1,205 @@
|
||||
from typing import List, Optional, Tuple, Any
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException, Path, Response
|
||||
from sqlalchemy import text, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .database import get_session
|
||||
from .models import Inventory
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Crafting Recipes endpoints
|
||||
|
||||
ALLOWED_CRAFTS = {
|
||||
"woodworking": "recipes_woodworking",
|
||||
# Future crafts can be added here, e.g. "smithing": "recipes_smithing"
|
||||
}
|
||||
|
||||
|
||||
|
||||
class MetadataResponse(BaseModel):
|
||||
storage_types: List[str]
|
||||
type_descriptions: List[str]
|
||||
|
||||
|
||||
@router.get("/metadata", response_model=MetadataResponse)
|
||||
async def metadata(session: AsyncSession = Depends(get_session)):
|
||||
"""Return distinct storage types and type descriptions."""
|
||||
storage_q = await session.execute(text("SELECT DISTINCT storage_type FROM inventory ORDER BY storage_type"))
|
||||
storage_types = [row[0] for row in storage_q.fetchall() if row[0]]
|
||||
|
||||
type_q = await session.execute(text("SELECT DISTINCT type_description FROM all_items ORDER BY type_description"))
|
||||
original_types = {row[0] for row in type_q.fetchall() if row[0]}
|
||||
|
||||
processed_types = set(original_types)
|
||||
has_nothing_or_unknown = 'NOTHING' in processed_types or 'UNKNOWN' in processed_types
|
||||
|
||||
if 'NOTHING' in processed_types:
|
||||
processed_types.remove('NOTHING')
|
||||
if 'UNKNOWN' in processed_types:
|
||||
processed_types.remove('UNKNOWN')
|
||||
|
||||
if has_nothing_or_unknown:
|
||||
processed_types.add('MANNEQUIN')
|
||||
|
||||
type_descriptions = sorted(list(processed_types))
|
||||
|
||||
return MetadataResponse(storage_types=storage_types, type_descriptions=type_descriptions)
|
||||
|
||||
|
||||
class InventoryItem(BaseModel):
|
||||
id: int
|
||||
item_name: str
|
||||
quantity: int
|
||||
storage_type: str
|
||||
description: Optional[str]
|
||||
icon_id: Optional[str]
|
||||
type_description: Optional[str]
|
||||
last_updated: Optional[datetime]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.get("/inventory/{character}", response_model=List[InventoryItem])
|
||||
async def inventory(
|
||||
character: str,
|
||||
storage_type: Optional[str] = Query(None),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Return items for a character, optionally filtered by storage_type."""
|
||||
base_sql = """
|
||||
SELECT i.*, ai.description, ai.icon_id, ai.type_description
|
||||
FROM inventory i
|
||||
LEFT JOIN all_items ai ON ai.name = i.item_name
|
||||
WHERE i.character_name = :char
|
||||
"""
|
||||
params = {"char": character}
|
||||
if storage_type:
|
||||
base_sql += " AND i.storage_type = :storage"
|
||||
params["storage"] = storage_type
|
||||
q = text(base_sql)
|
||||
result = await session.execute(q, params)
|
||||
rows = result.fetchall()
|
||||
return [InventoryItem(
|
||||
id=r.id,
|
||||
item_name=r.item_name,
|
||||
quantity=r.quantity,
|
||||
storage_type=r.storage_type,
|
||||
description=r.description,
|
||||
icon_id=r.icon_id,
|
||||
type_description=r.type_description,
|
||||
last_updated=r.last_updated,
|
||||
) for r in rows]
|
||||
|
||||
|
||||
class ItemSummary(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
icon_id: Optional[str]
|
||||
type_description: Optional[str]
|
||||
|
||||
|
||||
@router.get("/items", response_model=List[ItemSummary])
|
||||
async def items(
|
||||
response: Response,
|
||||
type: Optional[str] = Query(None, alias="type"),
|
||||
search: Optional[str] = Query(None, description="Substring search on item name"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(40, ge=1, le=100),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Return items from all_items view with pagination."""
|
||||
offset = (page - 1) * page_size
|
||||
params = {"limit": page_size, "offset": offset}
|
||||
where_clauses = ["name != '.'"]
|
||||
|
||||
if type:
|
||||
if type == 'MANNEQUIN':
|
||||
where_clauses.append("type_description IN ('MANNEQUIN', 'NOTHING', 'UNKNOWN')")
|
||||
else:
|
||||
where_clauses.append("type_description = :type")
|
||||
params["type"] = type
|
||||
|
||||
if search:
|
||||
where_clauses.append("name ILIKE :search")
|
||||
params["search"] = f"%{search}%"
|
||||
|
||||
where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
|
||||
|
||||
# Calculate total count for pagination
|
||||
count_q = text(f"SELECT COUNT(*) FROM all_items {where_sql}")
|
||||
# Use only relevant params for count query (exclude limit/offset)
|
||||
count_params = {k: v for k, v in params.items() if k not in ("limit", "offset")}
|
||||
total_res = await session.execute(count_q, count_params)
|
||||
total_count = total_res.scalar() or 0
|
||||
response.headers["X-Total-Count"] = str(total_count)
|
||||
|
||||
q = text(
|
||||
f"SELECT id, name, icon_id, type_description FROM all_items {where_sql} ORDER BY id LIMIT :limit OFFSET :offset"
|
||||
)
|
||||
result = await session.execute(q, params)
|
||||
rows = result.fetchall()
|
||||
return [ItemSummary(id=r.id, name=r.name, icon_id=r.icon_id, type_description=r.type_description) for r in rows]
|
||||
|
||||
|
||||
class ItemDetail(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str]
|
||||
icon_id: Optional[str]
|
||||
type_description: Optional[str]
|
||||
|
||||
|
||||
@router.get("/icon/{icon_id}")
|
||||
async def get_icon(icon_id: str, session: AsyncSession = Depends(get_session)):
|
||||
q = text("SELECT image_data, image_format, image_encoding FROM item_icons WHERE id = :id LIMIT 1")
|
||||
res = await session.execute(q, {"id": icon_id})
|
||||
row = res.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Icon not found")
|
||||
import base64
|
||||
if row.image_encoding == "base64":
|
||||
data_bytes = base64.b64decode(row.image_data)
|
||||
else:
|
||||
data_bytes = row.image_data
|
||||
media_type = f"image/{row.image_format.split('/')[-1]}" if row.image_format else "image/png"
|
||||
from fastapi.responses import Response
|
||||
return Response(content=data_bytes, media_type=media_type)
|
||||
|
||||
|
||||
@router.get("/items/by-name/{item_name}", response_model=ItemDetail)
|
||||
async def item_detail_by_name(item_name: str, session: AsyncSession = Depends(get_session)):
|
||||
q = text("SELECT * FROM all_items WHERE name = :n LIMIT 1")
|
||||
row = (await session.execute(q, {"n": item_name})).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
return ItemDetail(
|
||||
id=row.id,
|
||||
name=row.name,
|
||||
description=row.description,
|
||||
icon_id=row.icon_id,
|
||||
type_description=row.type_description,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/items/{item_id}", response_model=ItemDetail)
|
||||
async def item_detail(item_id: int, session: AsyncSession = Depends(get_session)):
|
||||
"""Fetch full item record from all_items view."""
|
||||
q = text("SELECT * FROM all_items WHERE id = :id LIMIT 1")
|
||||
result = await session.execute(q, {"id": item_id})
|
||||
row = result.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
|
||||
return ItemDetail(
|
||||
id=row.id,
|
||||
name=row.name,
|
||||
description=row.description,
|
||||
icon_id=row.icon_id,
|
||||
type_description=row.type_description,
|
||||
)
|
||||
Reference in New Issue
Block a user