feat: add single-item upsert/delete endpoints and container/slot columns

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
erik 2026-02-28 15:43:58 +00:00
parent 664bd50388
commit 749652d534
2 changed files with 232 additions and 6 deletions

View file

@ -36,7 +36,11 @@ class Item(Base):
# Equipment status # Equipment status
current_wielded_location = Column(Integer, default=0, index=True) # 0 = not equipped current_wielded_location = Column(Integer, default=0, index=True) # 0 = not equipped
# Container/position tracking
container_id = Column(BigInteger, default=0) # Game container object ID (0 = character)
slot = Column(Integer, default=-1) # Slot position within container (-1 = unknown)
# Item state # Item state
bonded = Column(Integer, default=0) # 0=Normal, 1=Bonded, 2=Sticky, 4=Destroy on drop bonded = Column(Integer, default=0) # 0=Normal, 1=Bonded, 2=Sticky, 4=Destroy on drop
attuned = Column(Integer, default=0) # 0=Normal, 1=Attuned attuned = Column(Integer, default=0) # 0=Normal, 1=Attuned

View file

@ -1401,25 +1401,28 @@ async def process_inventory(inventory: InventoryItem):
burden=basic['burden'], burden=basic['burden'],
has_id_data=basic['has_id_data'], has_id_data=basic['has_id_data'],
last_id_time=item_data.get('LastIdTime', 0), last_id_time=item_data.get('LastIdTime', 0),
# Equipment status # Equipment status
current_wielded_location=basic['current_wielded_location'], current_wielded_location=basic['current_wielded_location'],
# Container/position tracking
container_id=item_data.get('ContainerId', 0),
# Item state # Item state
bonded=basic['bonded'], bonded=basic['bonded'],
attuned=basic['attuned'], attuned=basic['attuned'],
unique=basic['unique'], unique=basic['unique'],
# Stack/Container properties # Stack/Container properties
stack_size=basic['stack_size'], stack_size=basic['stack_size'],
max_stack_size=basic['max_stack_size'], max_stack_size=basic['max_stack_size'],
items_capacity=basic['items_capacity'] if basic['items_capacity'] != -1 else None, items_capacity=basic['items_capacity'] if basic['items_capacity'] != -1 else None,
containers_capacity=basic['containers_capacity'] if basic['containers_capacity'] != -1 else None, containers_capacity=basic['containers_capacity'] if basic['containers_capacity'] != -1 else None,
# Durability # Durability
structure=basic['structure'] if basic['structure'] != -1 else None, structure=basic['structure'] if basic['structure'] != -1 else None,
max_structure=basic['max_structure'] if basic['max_structure'] != -1 else None, max_structure=basic['max_structure'] if basic['max_structure'] != -1 else None,
# Special item flags # Special item flags
rare_id=basic['rare_id'] if basic['rare_id'] != -1 else None, rare_id=basic['rare_id'] if basic['rare_id'] != -1 else None,
lifespan=basic['lifespan'] if basic['lifespan'] != -1 else None, lifespan=basic['lifespan'] if basic['lifespan'] != -1 else None,
@ -1536,6 +1539,225 @@ async def process_inventory(inventory: InventoryItem):
errors=processing_errors if processing_errors else None errors=processing_errors if processing_errors else None
) )
@app.post("/inventory/{character_name}/item",
summary="Upsert a single inventory item",
tags=["Data Processing"])
async def upsert_inventory_item(character_name: str, item: Dict[str, Any]):
"""Process and upsert a single item for a character's inventory."""
item_game_id = item.get('Id') or item.get('id')
if item_game_id is None:
raise HTTPException(status_code=400, detail="Item must have an 'Id' or 'id' field")
processed_count = 0
error_count = 0
async with database.transaction():
# Delete existing item with this character_name + item_id from all related tables
existing = await database.fetch_all(
"SELECT id FROM items WHERE character_name = :character_name AND item_id = :item_id",
{"character_name": character_name, "item_id": item_game_id}
)
if existing:
id_list = ','.join(str(row['id']) for row in existing)
await database.execute(f"DELETE FROM item_raw_data WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_combat_stats WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_requirements WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_enhancements WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_ratings WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_spells WHERE item_id IN ({id_list})")
await database.execute(
"DELETE FROM items WHERE character_name = :character_name AND item_id = :item_id",
{"character_name": character_name, "item_id": item_game_id}
)
# Process and insert the single item using the same logic as process_inventory
try:
properties = extract_item_properties(item)
basic = properties['basic']
timestamp = datetime.utcnow()
item_stmt = sa.insert(Item).values(
character_name=character_name,
item_id=item_game_id,
timestamp=timestamp,
name=basic['name'],
icon=basic['icon'],
object_class=basic['object_class'],
value=basic['value'],
burden=basic['burden'],
has_id_data=basic['has_id_data'],
last_id_time=item.get('LastIdTime', 0),
# Equipment status
current_wielded_location=basic['current_wielded_location'],
# Container/position tracking
container_id=item.get('ContainerId', 0),
# Item state
bonded=basic['bonded'],
attuned=basic['attuned'],
unique=basic['unique'],
# Stack/Container properties
stack_size=basic['stack_size'],
max_stack_size=basic['max_stack_size'],
items_capacity=basic['items_capacity'] if basic['items_capacity'] != -1 else None,
containers_capacity=basic['containers_capacity'] if basic['containers_capacity'] != -1 else None,
# Durability
structure=basic['structure'] if basic['structure'] != -1 else None,
max_structure=basic['max_structure'] if basic['max_structure'] != -1 else None,
# Special item flags
rare_id=basic['rare_id'] if basic['rare_id'] != -1 else None,
lifespan=basic['lifespan'] if basic['lifespan'] != -1 else None,
remaining_lifespan=basic['remaining_lifespan'] if basic['remaining_lifespan'] != -1 else None,
).returning(Item.id)
result = await database.fetch_one(item_stmt)
db_item_id = result['id']
# Store combat stats if applicable
combat = properties['combat']
if any(v != -1 and v != -1.0 for v in combat.values()):
combat_stmt = sa.dialects.postgresql.insert(ItemCombatStats).values(
item_id=db_item_id,
**{k: v if v != -1 and v != -1.0 else None for k, v in combat.items()}
).on_conflict_do_update(
index_elements=['item_id'],
set_=dict(**{k: v if v != -1 and v != -1.0 else None for k, v in combat.items()})
)
await database.execute(combat_stmt)
# Store requirements if applicable
requirements = properties['requirements']
if any(v not in [-1, None, ''] for v in requirements.values()):
req_stmt = sa.dialects.postgresql.insert(ItemRequirements).values(
item_id=db_item_id,
**{k: v if v not in [-1, None, ''] else None for k, v in requirements.items()}
).on_conflict_do_update(
index_elements=['item_id'],
set_=dict(**{k: v if v not in [-1, None, ''] else None for k, v in requirements.items()})
)
await database.execute(req_stmt)
# Store enhancements
enhancements = properties['enhancements']
enh_stmt = sa.dialects.postgresql.insert(ItemEnhancements).values(
item_id=db_item_id,
**{k: v if v not in [-1, -1.0, None, ''] else None for k, v in enhancements.items()}
).on_conflict_do_update(
index_elements=['item_id'],
set_=dict(**{k: v if v not in [-1, -1.0, None, ''] else None for k, v in enhancements.items()})
)
await database.execute(enh_stmt)
# Store ratings if applicable
ratings = properties['ratings']
if any(v not in [-1, -1.0, None] for v in ratings.values()):
rat_stmt = sa.dialects.postgresql.insert(ItemRatings).values(
item_id=db_item_id,
**{k: v if v not in [-1, -1.0, None] else None for k, v in ratings.items()}
).on_conflict_do_update(
index_elements=['item_id'],
set_=dict(**{k: v if v not in [-1, -1.0, None] else None for k, v in ratings.items()})
)
await database.execute(rat_stmt)
# Store spell data if applicable
spells = item.get('Spells', [])
active_spells = item.get('ActiveSpells', [])
all_spells = set(spells + active_spells)
if all_spells:
await database.execute(
"DELETE FROM item_spells WHERE item_id = :item_id",
{"item_id": db_item_id}
)
for spell_id in all_spells:
is_active = spell_id in active_spells
spell_stmt = sa.dialects.postgresql.insert(ItemSpells).values(
item_id=db_item_id,
spell_id=spell_id,
is_active=is_active
).on_conflict_do_nothing()
await database.execute(spell_stmt)
# Store raw data
raw_stmt = sa.dialects.postgresql.insert(ItemRawData).values(
item_id=db_item_id,
int_values=item.get('IntValues', {}),
double_values=item.get('DoubleValues', {}),
string_values=item.get('StringValues', {}),
bool_values=item.get('BoolValues', {}),
original_json=item
).on_conflict_do_update(
index_elements=['item_id'],
set_=dict(
int_values=item.get('IntValues', {}),
double_values=item.get('DoubleValues', {}),
string_values=item.get('StringValues', {}),
bool_values=item.get('BoolValues', {}),
original_json=item
)
)
await database.execute(raw_stmt)
processed_count = 1
except Exception as e:
error_msg = f"Error processing item {item_game_id}: {e}"
logger.error(error_msg)
error_count = 1
raise HTTPException(status_code=500, detail=error_msg)
logger.info(f"Single item upsert for {character_name}: item_id={item_game_id}, processed={processed_count}")
return {"status": "ok", "processed": processed_count}
@app.delete("/inventory/{character_name}/item/{item_id}",
summary="Delete a single inventory item",
tags=["Data Processing"])
async def delete_inventory_item(character_name: str, item_id: int):
"""Delete a single item from a character's inventory."""
deleted_count = 0
async with database.transaction():
# Find all DB rows for this character + game item_id
existing = await database.fetch_all(
"SELECT id FROM items WHERE character_name = :character_name AND item_id = :item_id",
{"character_name": character_name, "item_id": item_id}
)
if existing:
id_list = ','.join(str(row['id']) for row in existing)
# Delete from all related tables first
await database.execute(f"DELETE FROM item_raw_data WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_combat_stats WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_requirements WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_enhancements WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_ratings WHERE item_id IN ({id_list})")
await database.execute(f"DELETE FROM item_spells WHERE item_id IN ({id_list})")
# Delete from main items table
await database.execute(
"DELETE FROM items WHERE character_name = :character_name AND item_id = :item_id",
{"character_name": character_name, "item_id": item_id}
)
deleted_count = len(existing)
logger.info(f"Single item delete for {character_name}: item_id={item_id}, deleted={deleted_count}")
return {"status": "ok", "deleted": deleted_count}
@app.get("/inventory/{character_name}", @app.get("/inventory/{character_name}",
summary="Get Character Inventory", summary="Get Character Inventory",
description="Retrieve processed inventory data for a specific character with normalized item properties.", description="Retrieve processed inventory data for a specific character with normalized item properties.",