Lots of stuff

This commit is contained in:
Aodhan
2025-07-08 23:04:43 +01:00
parent cfa2eff6ef
commit 65c1972c49
26 changed files with 4094 additions and 104 deletions

View File

@@ -1,8 +1,8 @@
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 fastapi import APIRouter, Depends, Query, HTTPException, Path, Response, UploadFile, File
from sqlalchemy import text, select, insert
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel
@@ -14,6 +14,52 @@ router = APIRouter()
# ---------------------------------------------------------------------------
# Crafting Recipes endpoints
@router.post("/inventory/import")
async def import_inventory_csv(
file: UploadFile = File(...),
session: AsyncSession = Depends(get_session),
):
"""Replace the entire inventory table with contents from an uploaded CSV.
The CSV must use the delimiter ``;`` and column headers: ``char;storage;item;quantity``.
"""
import csv
import io
# Read file bytes and decode
contents = await file.read()
try:
text_data = contents.decode("utf-8")
except UnicodeDecodeError:
raise HTTPException(status_code=400, detail="CSV must be UTF-8 encoded")
reader = csv.DictReader(io.StringIO(text_data), delimiter=";", quotechar='"')
rows = []
for r in reader:
try:
qty = int(r["quantity"].strip()) if r["quantity"].strip() else 0
except (KeyError, ValueError):
raise HTTPException(status_code=400, detail="Invalid CSV schema or quantity value")
rows.append(
{
"character_name": r["char"].strip(),
"storage_type": r["storage"].strip(),
"item_name": r["item"].strip(),
"quantity": qty,
}
)
# Replace table contents inside a transaction
try:
await session.execute(text("TRUNCATE TABLE inventory;"))
if rows:
await session.execute(insert(Inventory), rows)
await session.commit()
except Exception as e:
await session.rollback()
raise HTTPException(status_code=500, detail=f"Failed to import CSV: {e}")
return {"imported": len(rows)}
ALLOWED_CRAFTS = {
"woodworking": "recipes_woodworking",
# Future crafts can be added here, e.g. "smithing": "recipes_smithing"
@@ -102,6 +148,7 @@ class ItemSummary(BaseModel):
name: str
icon_id: Optional[str]
type_description: Optional[str]
jobs_description: Optional[List[str]]
@router.get("/items", response_model=List[ItemSummary])
@@ -139,12 +186,14 @@ async def items(
total_count = total_res.scalar() or 0
response.headers["X-Total-Count"] = str(total_count)
# Use LEFT JOIN to fetch jobs_description from armor_items table; it will be NULL for non-armor.
join_sql = "FROM all_items a LEFT JOIN armor_items ai ON ai.id = a.id"
q = text(
f"SELECT id, name, icon_id, type_description FROM all_items {where_sql} ORDER BY id LIMIT :limit OFFSET :offset"
f"SELECT a.id, a.name, a.icon_id, a.type_description, ai.jobs_description {join_sql} {where_sql} ORDER BY a.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]
return [ItemSummary(id=r.id, name=r.name, icon_id=r.icon_id, type_description=r.type_description, jobs_description=r.jobs_description) for r in rows]
class ItemDetail(BaseModel):

View File

@@ -4,3 +4,4 @@ SQLAlchemy[asyncio]==2.0.27
asyncpg==0.29.0
pydantic==2.7.1
python-dotenv==1.0.1
python-multipart==0.0.9