added inventory service for armor and jewelry
This commit is contained in:
parent
09a6cd4946
commit
57a2384511
13 changed files with 2630 additions and 25 deletions
318
main.py
318
main.py
|
|
@ -13,8 +13,8 @@ import sys
|
|||
from typing import Dict, List, Any
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, Header, HTTPException, Query, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import FastAPI, Header, HTTPException, Query, WebSocket, WebSocketDisconnect, Request
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
from fastapi.routing import APIRoute
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
|
@ -386,6 +386,64 @@ async def get_total_rares():
|
|||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
# --- GET Spawn Heat Map Endpoint ---------------------------------
|
||||
@app.get("/spawns/heatmap")
|
||||
async def get_spawn_heatmap_data(
|
||||
hours: int = Query(24, ge=1, le=168, description="Lookback window in hours (1-168)"),
|
||||
limit: int = Query(10000, ge=100, le=50000, description="Maximum number of spawn points to return")
|
||||
):
|
||||
"""
|
||||
Aggregate spawn locations for heat-map visualization.
|
||||
|
||||
Returns spawn event coordinates grouped by location with intensity counts
|
||||
for the specified time window.
|
||||
|
||||
Response format:
|
||||
{
|
||||
"spawn_points": [{"ew": float, "ns": float, "intensity": int}, ...],
|
||||
"total_points": int,
|
||||
"timestamp": "UTC-ISO"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
|
||||
|
||||
# Aggregate spawn events by coordinates within time window
|
||||
query = """
|
||||
SELECT ew, ns, COUNT(*) AS spawn_count
|
||||
FROM spawn_events
|
||||
WHERE timestamp >= :cutoff
|
||||
GROUP BY ew, ns
|
||||
ORDER BY spawn_count DESC
|
||||
LIMIT :limit
|
||||
"""
|
||||
|
||||
rows = await database.fetch_all(query, {"cutoff": cutoff, "limit": limit})
|
||||
|
||||
spawn_points = [
|
||||
{
|
||||
"ew": float(row["ew"]),
|
||||
"ns": float(row["ns"]),
|
||||
"intensity": int(row["spawn_count"])
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
result = {
|
||||
"spawn_points": spawn_points,
|
||||
"total_points": len(spawn_points),
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"hours_window": hours
|
||||
}
|
||||
|
||||
logger.debug(f"Heat map data: {len(spawn_points)} unique spawn locations from last {hours} hours")
|
||||
return JSONResponse(content=jsonable_encoder(result))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Heat map query failed: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Spawn heat map query failed")
|
||||
|
||||
|
||||
# --- GET Inventory Endpoints ---------------------------------
|
||||
@app.get("/inventory/{character_name}")
|
||||
async def get_character_inventory(character_name: str):
|
||||
|
|
@ -514,6 +572,230 @@ async def list_characters_with_inventories():
|
|||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
# --- Inventory Service Character List Proxy ---------------------
|
||||
@app.get("/inventory-characters")
|
||||
async def get_inventory_characters():
|
||||
"""Get character list from inventory service - proxy to avoid routing conflicts."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(f"{INVENTORY_SERVICE_URL}/characters/list")
|
||||
|
||||
if response.status_code == 200:
|
||||
return JSONResponse(content=response.json())
|
||||
else:
|
||||
logger.error(f"Inventory service returned {response.status_code}: {response.text}")
|
||||
raise HTTPException(status_code=response.status_code, detail="Failed to get characters from inventory service")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to proxy inventory characters request: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Failed to get inventory characters")
|
||||
|
||||
|
||||
# --- Inventory Search Service Proxy Endpoints -------------------
|
||||
@app.get("/search/items")
|
||||
async def search_items_proxy(
|
||||
text: str = Query(None, description="Search item names, descriptions, or properties"),
|
||||
character: str = Query(None, description="Limit search to specific character"),
|
||||
include_all_characters: bool = Query(False, description="Search across all characters"),
|
||||
equipment_status: str = Query(None, description="equipped, unequipped, or all"),
|
||||
equipment_slot: int = Query(None, description="Equipment slot mask"),
|
||||
# Item category filtering
|
||||
armor_only: bool = Query(False, description="Show only armor items"),
|
||||
jewelry_only: bool = Query(False, description="Show only jewelry items"),
|
||||
weapon_only: bool = Query(False, description="Show only weapon items"),
|
||||
# Spell filtering
|
||||
has_spell: str = Query(None, description="Must have this specific spell (by name)"),
|
||||
spell_contains: str = Query(None, description="Spell name contains this text"),
|
||||
legendary_cantrips: str = Query(None, description="Comma-separated list of legendary cantrip names"),
|
||||
# Combat properties
|
||||
min_damage: int = Query(None, description="Minimum damage"),
|
||||
max_damage: int = Query(None, description="Maximum damage"),
|
||||
min_armor: int = Query(None, description="Minimum armor level"),
|
||||
max_armor: int = Query(None, description="Maximum armor level"),
|
||||
min_attack_bonus: float = Query(None, description="Minimum attack bonus"),
|
||||
min_crit_damage_rating: int = Query(None, description="Minimum critical damage rating"),
|
||||
min_damage_rating: int = Query(None, description="Minimum damage rating"),
|
||||
min_heal_boost_rating: int = Query(None, description="Minimum heal boost rating"),
|
||||
max_level: int = Query(None, description="Maximum wield level requirement"),
|
||||
min_level: int = Query(None, description="Minimum wield level requirement"),
|
||||
material: str = Query(None, description="Material type (partial match)"),
|
||||
min_workmanship: float = Query(None, description="Minimum workmanship"),
|
||||
has_imbue: bool = Query(None, description="Has imbue effects"),
|
||||
item_set: str = Query(None, description="Item set name (partial match)"),
|
||||
min_tinks: int = Query(None, description="Minimum tinker count"),
|
||||
bonded: bool = Query(None, description="Bonded status"),
|
||||
attuned: bool = Query(None, description="Attuned status"),
|
||||
unique: bool = Query(None, description="Unique item status"),
|
||||
is_rare: bool = Query(None, description="Rare item status"),
|
||||
min_condition: int = Query(None, description="Minimum condition percentage"),
|
||||
min_value: int = Query(None, description="Minimum item value"),
|
||||
max_value: int = Query(None, description="Maximum item value"),
|
||||
max_burden: int = Query(None, description="Maximum burden"),
|
||||
sort_by: str = Query("name", description="Sort field: name, value, damage, armor, workmanship"),
|
||||
sort_dir: str = Query("asc", description="Sort direction: asc or desc"),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
limit: int = Query(50, ge=1, le=200, description="Items per page")
|
||||
):
|
||||
"""Proxy to inventory service comprehensive item search."""
|
||||
try:
|
||||
# Build query parameters
|
||||
params = {}
|
||||
if text: params["text"] = text
|
||||
if character: params["character"] = character
|
||||
if include_all_characters: params["include_all_characters"] = include_all_characters
|
||||
if equipment_status: params["equipment_status"] = equipment_status
|
||||
if equipment_slot is not None: params["equipment_slot"] = equipment_slot
|
||||
# Category filtering
|
||||
if armor_only: params["armor_only"] = armor_only
|
||||
if jewelry_only: params["jewelry_only"] = jewelry_only
|
||||
if weapon_only: params["weapon_only"] = weapon_only
|
||||
# Spell filtering
|
||||
if has_spell: params["has_spell"] = has_spell
|
||||
if spell_contains: params["spell_contains"] = spell_contains
|
||||
if legendary_cantrips: params["legendary_cantrips"] = legendary_cantrips
|
||||
# Combat properties
|
||||
if min_damage is not None: params["min_damage"] = min_damage
|
||||
if max_damage is not None: params["max_damage"] = max_damage
|
||||
if min_armor is not None: params["min_armor"] = min_armor
|
||||
if max_armor is not None: params["max_armor"] = max_armor
|
||||
if min_attack_bonus is not None: params["min_attack_bonus"] = min_attack_bonus
|
||||
if min_crit_damage_rating is not None: params["min_crit_damage_rating"] = min_crit_damage_rating
|
||||
if min_damage_rating is not None: params["min_damage_rating"] = min_damage_rating
|
||||
if min_heal_boost_rating is not None: params["min_heal_boost_rating"] = min_heal_boost_rating
|
||||
if max_level is not None: params["max_level"] = max_level
|
||||
if min_level is not None: params["min_level"] = min_level
|
||||
if material: params["material"] = material
|
||||
if min_workmanship is not None: params["min_workmanship"] = min_workmanship
|
||||
if has_imbue is not None: params["has_imbue"] = has_imbue
|
||||
if item_set: params["item_set"] = item_set
|
||||
if min_tinks is not None: params["min_tinks"] = min_tinks
|
||||
if bonded is not None: params["bonded"] = bonded
|
||||
if attuned is not None: params["attuned"] = attuned
|
||||
if unique is not None: params["unique"] = unique
|
||||
if is_rare is not None: params["is_rare"] = is_rare
|
||||
if min_condition is not None: params["min_condition"] = min_condition
|
||||
if min_value is not None: params["min_value"] = min_value
|
||||
if max_value is not None: params["max_value"] = max_value
|
||||
if max_burden is not None: params["max_burden"] = max_burden
|
||||
params["sort_by"] = sort_by
|
||||
params["sort_dir"] = sort_dir
|
||||
params["page"] = page
|
||||
params["limit"] = limit
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.get(
|
||||
f"{INVENTORY_SERVICE_URL}/search/items",
|
||||
params=params
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return JSONResponse(content=response.json())
|
||||
else:
|
||||
logger.error(f"Inventory search service returned {response.status_code}")
|
||||
raise HTTPException(status_code=response.status_code, detail="Inventory search service error")
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Could not reach inventory service: {e}")
|
||||
raise HTTPException(status_code=503, detail="Inventory service unavailable")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to search items: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@app.get("/search/equipped/{character_name}")
|
||||
async def search_equipped_items_proxy(
|
||||
character_name: str,
|
||||
slot: int = Query(None, description="Specific equipment slot mask")
|
||||
):
|
||||
"""Proxy to inventory service equipped items search."""
|
||||
try:
|
||||
params = {}
|
||||
if slot is not None:
|
||||
params["slot"] = slot
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(
|
||||
f"{INVENTORY_SERVICE_URL}/search/equipped/{character_name}",
|
||||
params=params
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return JSONResponse(content=response.json())
|
||||
elif response.status_code == 404:
|
||||
raise HTTPException(status_code=404, detail=f"No equipped items found for character '{character_name}'")
|
||||
else:
|
||||
logger.error(f"Inventory service returned {response.status_code} for equipped items search")
|
||||
raise HTTPException(status_code=response.status_code, detail="Inventory service error")
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Could not reach inventory service: {e}")
|
||||
raise HTTPException(status_code=503, detail="Inventory service unavailable")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to search equipped items for {character_name}: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@app.get("/search/upgrades/{character_name}/{slot}")
|
||||
async def find_equipment_upgrades_proxy(
|
||||
character_name: str,
|
||||
slot: int,
|
||||
upgrade_type: str = Query("damage", description="What to optimize for: damage, armor, workmanship, value")
|
||||
):
|
||||
"""Proxy to inventory service equipment upgrades search."""
|
||||
try:
|
||||
params = {"upgrade_type": upgrade_type}
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(
|
||||
f"{INVENTORY_SERVICE_URL}/search/upgrades/{character_name}/{slot}",
|
||||
params=params
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return JSONResponse(content=response.json())
|
||||
elif response.status_code == 404:
|
||||
raise HTTPException(status_code=404, detail=f"No upgrade options found for character '{character_name}' slot {slot}")
|
||||
else:
|
||||
logger.error(f"Inventory service returned {response.status_code} for upgrades search")
|
||||
raise HTTPException(status_code=response.status_code, detail="Inventory service error")
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Could not reach inventory service: {e}")
|
||||
raise HTTPException(status_code=503, detail="Inventory service unavailable")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to find equipment upgrades for {character_name} slot {slot}: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@app.get("/sets/list")
|
||||
async def list_equipment_sets_proxy():
|
||||
"""Proxy to inventory service equipment sets list."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(f"{INVENTORY_SERVICE_URL}/sets/list")
|
||||
|
||||
if response.status_code == 200:
|
||||
return JSONResponse(content=response.json())
|
||||
else:
|
||||
logger.error(f"Inventory service returned {response.status_code} for sets list")
|
||||
raise HTTPException(status_code=response.status_code, detail="Inventory service error")
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Could not reach inventory service: {e}")
|
||||
raise HTTPException(status_code=503, detail="Inventory service unavailable")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list equipment sets: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
# -------------------- WebSocket endpoints -----------------------
|
||||
## WebSocket connection tracking
|
||||
# Set of browser WebSocket clients subscribed to live updates
|
||||
|
|
@ -994,6 +1276,38 @@ async def serve_icon(icon_filename: str):
|
|||
# Icon not found
|
||||
raise HTTPException(status_code=404, detail="Icon not found")
|
||||
|
||||
# -------------------- Inventory Service Proxy ---------------------------
|
||||
|
||||
@app.get("/inv/test")
|
||||
async def test_inventory_route():
|
||||
"""Test route to verify inventory proxy is working"""
|
||||
return {"message": "Inventory proxy route is working"}
|
||||
|
||||
@app.api_route("/inv/{path:path}", methods=["GET", "POST"])
|
||||
async def proxy_inventory_service(path: str, request: Request):
|
||||
"""Proxy all inventory service requests"""
|
||||
try:
|
||||
inventory_service_url = os.getenv('INVENTORY_SERVICE_URL', 'http://inventory-service:8000')
|
||||
logger.info(f"Proxying to inventory service: {inventory_service_url}/{path}")
|
||||
|
||||
# Forward the request to inventory service
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.request(
|
||||
method=request.method,
|
||||
url=f"{inventory_service_url}/{path}",
|
||||
params=request.query_params,
|
||||
headers=dict(request.headers),
|
||||
content=await request.body()
|
||||
)
|
||||
return Response(
|
||||
content=response.content,
|
||||
status_code=response.status_code,
|
||||
headers=dict(response.headers)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to proxy inventory request: {e}")
|
||||
raise HTTPException(status_code=500, detail="Inventory service unavailable")
|
||||
|
||||
# Icons are now served from static/icons directory
|
||||
# Serve SPA files (catch-all for frontend routes)
|
||||
# Mount the single-page application frontend (static assets) at root path
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue