Desynth page and improved item info api. Added string substitution to utils.
This commit is contained in:
@@ -33,13 +33,13 @@ async def import_inventory_csv(
|
||||
raise HTTPException(status_code=400, detail="CSV must be UTF-8 encoded")
|
||||
|
||||
reader = csv.DictReader(io.StringIO(text_data), delimiter=";", quotechar='"')
|
||||
rows = []
|
||||
raw_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(
|
||||
raw_rows.append(
|
||||
{
|
||||
"character_name": r["char"].strip(),
|
||||
"storage_type": r["storage"].strip(),
|
||||
@@ -48,6 +48,57 @@ async def import_inventory_csv(
|
||||
}
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Resolve stack sizes and split quantities across inventory slots
|
||||
# ---------------------------------------------------------------------
|
||||
item_names = {r["item_name"] for r in raw_rows}
|
||||
stack_sizes: dict[str, int] = {}
|
||||
if item_names:
|
||||
# Discover item tables that have a stack_size column ( *_items )
|
||||
tbl_res = await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT table_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND column_name = 'stack_size'
|
||||
AND table_name LIKE '%_items'
|
||||
"""
|
||||
)
|
||||
)
|
||||
item_tables = [row[0] for row in tbl_res.fetchall()]
|
||||
|
||||
# Query each table for name ▸ stack_size entries we need
|
||||
for t in item_tables:
|
||||
q = text(f"SELECT name, stack_size FROM {t} WHERE name = ANY(:names)")
|
||||
res = await session.execute(q, {"names": list(item_names)})
|
||||
for name, stack in res.fetchall():
|
||||
# Prefer the first non-null value encountered
|
||||
if name not in stack_sizes or not stack_sizes[name]:
|
||||
stack_sizes[name] = (stack or 1) if stack and stack > 0 else 1
|
||||
|
||||
def _stack_for(item_name: str) -> int:
|
||||
return stack_sizes.get(item_name, 1) or 1
|
||||
|
||||
# Resolve item_ids via all_items once
|
||||
id_rows = await session.execute(text("SELECT id,name FROM all_items WHERE name = ANY(:names)"), {"names": list(item_names)})
|
||||
id_map = {n: i for i, n in id_rows.fetchall()}
|
||||
|
||||
rows: list[dict] = []
|
||||
for r in raw_rows:
|
||||
qty = r["quantity"]
|
||||
if qty <= 0:
|
||||
continue
|
||||
stack = _stack_for(r["item_name"])
|
||||
# Split into multiple slot-rows respecting stack size
|
||||
while qty > 0:
|
||||
take = min(stack, qty)
|
||||
slot_row = r.copy()
|
||||
slot_row["quantity"] = take
|
||||
slot_row["item_id"] = id_map.get(r["item_name"]) # may be None
|
||||
rows.append(slot_row)
|
||||
qty -= take
|
||||
|
||||
# Replace table contents inside a transaction
|
||||
try:
|
||||
await session.execute(text("TRUNCATE TABLE inventory;"))
|
||||
@@ -105,6 +156,7 @@ class InventoryItem(BaseModel):
|
||||
description: Optional[str]
|
||||
icon_id: Optional[str]
|
||||
type_description: Optional[str]
|
||||
stack_size: Optional[int]
|
||||
last_updated: Optional[datetime]
|
||||
|
||||
class Config:
|
||||
@@ -119,7 +171,7 @@ async def inventory(
|
||||
):
|
||||
"""Return items for a character, optionally filtered by storage_type."""
|
||||
base_sql = """
|
||||
SELECT i.*, ai.description, ai.icon_id, ai.type_description
|
||||
SELECT i.*, ai.description, ai.icon_id, ai.type_description, ai.stack_size
|
||||
FROM inventory i
|
||||
LEFT JOIN all_items ai ON ai.name = i.item_name
|
||||
WHERE i.character_name = :char
|
||||
@@ -139,6 +191,7 @@ async def inventory(
|
||||
description=r.description,
|
||||
icon_id=r.icon_id,
|
||||
type_description=r.type_description,
|
||||
stack_size=r.stack_size,
|
||||
last_updated=r.last_updated,
|
||||
) for r in rows]
|
||||
|
||||
@@ -217,6 +270,7 @@ class ItemDetail(BaseModel):
|
||||
description: Optional[str]
|
||||
icon_id: Optional[str]
|
||||
type_description: Optional[str]
|
||||
icon_b64: Optional[str] = None
|
||||
|
||||
|
||||
@router.get("/icon/{icon_id}")
|
||||
@@ -238,32 +292,66 @@ async def get_icon(icon_id: str, session: AsyncSession = Depends(get_session)):
|
||||
|
||||
@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")
|
||||
q = text("""
|
||||
SELECT a.*, ic.image_data, ic.image_encoding
|
||||
FROM all_items a
|
||||
LEFT JOIN item_icons ic ON ic.id = a.icon_id
|
||||
WHERE a.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")
|
||||
import base64
|
||||
icon_b64: str | None = None
|
||||
if row.image_data is not None:
|
||||
if row.image_encoding == "base64":
|
||||
icon_b64 = row.image_data
|
||||
else:
|
||||
try:
|
||||
icon_b64 = base64.b64encode(row.image_data).decode()
|
||||
except Exception:
|
||||
icon_b64 = None
|
||||
return ItemDetail(
|
||||
id=row.id,
|
||||
name=row.name,
|
||||
description=row.description,
|
||||
icon_id=row.icon_id,
|
||||
type_description=row.type_description,
|
||||
icon_b64=icon_b64,
|
||||
)
|
||||
|
||||
|
||||
@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()
|
||||
async def item_detail(item_id: int = Path(..., ge=1), session: AsyncSession = Depends(get_session)):
|
||||
"""Retrieve item metadata and icon by numeric ID."""
|
||||
q = text(
|
||||
"""
|
||||
SELECT a.*, ic.image_data, ic.image_encoding
|
||||
FROM all_items a
|
||||
LEFT JOIN item_icons ic ON ic.id = a.icon_id
|
||||
WHERE a.id = :id
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
row = (await session.execute(q, {"id": item_id})).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
|
||||
import base64
|
||||
icon_b64: str | None = None
|
||||
if row.image_data is not None:
|
||||
if row.image_encoding == "base64":
|
||||
icon_b64 = row.image_data
|
||||
else:
|
||||
try:
|
||||
icon_b64 = base64.b64encode(row.image_data).decode()
|
||||
except Exception:
|
||||
icon_b64 = None
|
||||
return ItemDetail(
|
||||
id=row.id,
|
||||
name=row.name,
|
||||
description=row.description,
|
||||
icon_id=row.icon_id,
|
||||
type_description=row.type_description,
|
||||
icon_b64=icon_b64,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user