Lemontropia-Suite/core/nexus_api.py

429 lines
16 KiB
Python

"""
Entropia Nexus API Client - Full Implementation
Base URL: https://api.entropianexus.com
Docs: https://api.entropianexus.com/docs
Endpoints:
- /weapons, /armors, /finders, /excavators
- /blueprints, /mobs, /materials
- /search, /items/{id}
"""
import json
import logging
from dataclasses import dataclass, field
from decimal import Decimal
from typing import Dict, List, Optional, Any
from datetime import datetime
try:
import urllib.request
HAS_URLLIB = True
except ImportError:
HAS_URLLIB = False
logger = logging.getLogger(__name__)
# =============================================================================
# Data Classes
# =============================================================================
@dataclass
class WeaponStats:
"""Weapon statistics from Entropia Nexus API."""
id: int
item_id: int
name: str
type: str
category: str
weapon_class: str
weight: Decimal
uses_per_minute: int
range_meters: Optional[Decimal]
efficiency: Optional[Decimal]
max_tt: Decimal
min_tt: Optional[Decimal]
decay: Optional[Decimal]
ammo_burn: Optional[int]
damage_stab: Decimal
damage_cut: Decimal
damage_impact: Decimal
damage_penetration: Decimal
damage_shrapnel: Decimal
damage_burn: Decimal
damage_cold: Decimal
damage_acid: Decimal
damage_electric: Decimal
total_damage: Decimal = field(default_factory=lambda: Decimal("0"))
dpp: Decimal = field(default_factory=lambda: Decimal("0"))
cost_per_hour: Decimal = field(default_factory=lambda: Decimal("0"))
sib: bool = False
hit_skill_start: Optional[int] = None
hit_skill_end: Optional[int] = None
dmg_skill_start: Optional[int] = None
dmg_skill_end: Optional[int] = None
ammo_name: Optional[str] = None
def __post_init__(self):
self.total_damage = (
self.damage_stab + self.damage_cut + self.damage_impact +
self.damage_penetration + self.damage_shrapnel +
self.damage_burn + self.damage_cold + self.damage_acid + self.damage_electric
)
if self.decay and self.decay > 0:
ammo_cost = Decimal(self.ammo_burn) if self.ammo_burn else Decimal("0")
total_cost_pec = self.decay + ammo_cost
if total_cost_pec > 0:
self.dpp = self.total_damage / total_cost_pec
if self.uses_per_minute and self.uses_per_minute > 0 and self.decay:
uses_per_hour = self.uses_per_minute * 60
self.cost_per_hour = (self.decay * uses_per_hour) / 100
@classmethod
def from_api_data(cls, data: Dict[str, Any]) -> 'WeaponStats':
props = data.get('Properties', {})
economy = props.get('Economy', {})
damage = props.get('Damage', {})
skill = props.get('Skill', {})
hit_skill = skill.get('Hit', {})
dmg_skill = skill.get('Dmg', {})
ammo = data.get('Ammo', {})
return cls(
id=data.get('Id', 0),
item_id=data.get('ItemId', 0),
name=data.get('Name', 'Unknown'),
type=props.get('Type', 'Unknown'),
category=props.get('Category', 'Unknown'),
weapon_class=props.get('Class', 'Unknown'),
weight=Decimal(str(props.get('Weight', 0))) if props.get('Weight') else Decimal('0'),
uses_per_minute=props.get('UsesPerMinute', 0) or 0,
range_meters=Decimal(str(props.get('Range'))) if props.get('Range') else None,
efficiency=Decimal(str(economy.get('Efficiency'))) if economy.get('Efficiency') else None,
max_tt=Decimal(str(economy.get('MaxTT', 0))) if economy.get('MaxTT') else Decimal('0'),
min_tt=Decimal(str(economy.get('MinTT'))) if economy.get('MinTT') else None,
decay=Decimal(str(economy.get('Decay'))) if economy.get('Decay') else None,
ammo_burn=economy.get('AmmoBurn'),
damage_stab=Decimal(str(damage.get('Stab', 0))) if damage.get('Stab') else Decimal('0'),
damage_cut=Decimal(str(damage.get('Cut', 0))) if damage.get('Cut') else Decimal('0'),
damage_impact=Decimal(str(damage.get('Impact', 0))) if damage.get('Impact') else Decimal('0'),
damage_penetration=Decimal(str(damage.get('Penetration', 0))) if damage.get('Penetration') else Decimal('0'),
damage_shrapnel=Decimal(str(damage.get('Shrapnel', 0))) if damage.get('Shrapnel') else Decimal('0'),
damage_burn=Decimal(str(damage.get('Burn', 0))) if damage.get('Burn') else Decimal('0'),
damage_cold=Decimal(str(damage.get('Cold', 0))) if damage.get('Cold') else Decimal('0'),
damage_acid=Decimal(str(damage.get('Acid', 0))) if damage.get('Acid') else Decimal('0'),
damage_electric=Decimal(str(damage.get('Electric', 0))) if damage.get('Electric') else Decimal('0'),
sib=skill.get('IsSiB', False),
hit_skill_start=hit_skill.get('LearningIntervalStart'),
hit_skill_end=hit_skill.get('LearningIntervalEnd'),
dmg_skill_start=dmg_skill.get('LearningIntervalStart'),
dmg_skill_end=dmg_skill.get('LearningIntervalEnd'),
ammo_name=ammo.get('Name'),
)
@dataclass
class ArmorStats:
"""Armor statistics."""
id: int
item_id: int
name: str
weight: Decimal
durability: int
protection_stab: Decimal
protection_cut: Decimal
protection_impact: Decimal
protection_penetration: Decimal
protection_shrapnel: Decimal
protection_burn: Decimal
protection_cold: Decimal
protection_acid: Decimal
protection_electric: Decimal
total_protection: Decimal = field(default_factory=lambda: Decimal("0"))
def __post_init__(self):
self.total_protection = (
self.protection_stab + self.protection_cut + self.protection_impact +
self.protection_penetration + self.protection_shrapnel +
self.protection_burn + self.protection_cold + self.protection_acid + self.protection_electric
)
@classmethod
def from_api_data(cls, data: Dict[str, Any]) -> 'ArmorStats':
props = data.get('Properties', {})
protection = props.get('Protection', {})
return cls(
id=data.get('Id', 0),
item_id=data.get('ItemId', 0),
name=data.get('Name', 'Unknown'),
weight=Decimal(str(props.get('Weight', 0))) if props.get('Weight') else Decimal('0'),
durability=props.get('Durability', 0) or 0,
protection_stab=Decimal(str(protection.get('Stab', 0))) if protection.get('Stab') else Decimal('0'),
protection_cut=Decimal(str(protection.get('Cut', 0))) if protection.get('Cut') else Decimal('0'),
protection_impact=Decimal(str(protection.get('Impact', 0))) if protection.get('Impact') else Decimal('0'),
protection_penetration=Decimal(str(protection.get('Penetration', 0))) if protection.get('Penetration') else Decimal('0'),
protection_shrapnel=Decimal(str(protection.get('Shrapnel', 0))) if protection.get('Shrapnel') else Decimal('0'),
protection_burn=Decimal(str(protection.get('Burn', 0))) if protection.get('Burn') else Decimal('0'),
protection_cold=Decimal(str(protection.get('Cold', 0))) if protection.get('Cold') else Decimal('0'),
protection_acid=Decimal(str(protection.get('Acid', 0))) if protection.get('Acid') else Decimal('0'),
protection_electric=Decimal(str(protection.get('Electric', 0))) if protection.get('Electric') else Decimal('0'),
)
@dataclass
class FinderStats:
"""Mining finder statistics."""
id: int
item_id: int
name: str
type: str
category: str
depth: Decimal
radius: Decimal
decay: Decimal
uses_per_minute: int
@classmethod
def from_api_data(cls, data: Dict[str, Any]) -> 'FinderStats':
props = data.get('Properties', {})
economy = props.get('Economy', {})
return cls(
id=data.get('Id', 0),
item_id=data.get('ItemId', 0),
name=data.get('Name', 'Unknown'),
type=props.get('Type', 'Unknown'),
category=props.get('Category', 'Unknown'),
depth=Decimal(str(props.get('Depth', 0))) if props.get('Depth') else Decimal('0'),
radius=Decimal(str(props.get('Radius', 0))) if props.get('Radius') else Decimal('0'),
decay=Decimal(str(economy.get('Decay', 0))) if economy.get('Decay') else Decimal('0'),
uses_per_minute=props.get('UsesPerMinute', 0) or 0,
)
@dataclass
class ExcavatorStats:
"""Mining excavator statistics."""
id: int
item_id: int
name: str
decay: Decimal
max_tt: Decimal
@classmethod
def from_api_data(cls, data: Dict[str, Any]) -> 'ExcavatorStats':
props = data.get('Properties', {})
economy = props.get('Economy', {})
return cls(
id=data.get('Id', 0),
item_id=data.get('ItemId', 0),
name=data.get('Name', 'Unknown'),
decay=Decimal(str(economy.get('Decay', 0))) if economy.get('Decay') else Decimal('0'),
max_tt=Decimal(str(economy.get('MaxTT', 0))) if economy.get('MaxTT') else Decimal('0'),
)
@dataclass
class MobStats:
"""Creature/Mob statistics."""
id: int
name: str
hp: int
damage: int
agility: int
intelligence: int
psyche: int
stamina: int
strength: int
@classmethod
def from_api_data(cls, data: Dict[str, Any]) -> 'MobStats':
return cls(
id=data.get('Id', 0),
name=data.get('Name', 'Unknown'),
hp=data.get('HP', 0) or 0,
damage=data.get('Damage', 0) or 0,
agility=data.get('Agility', 0) or 0,
intelligence=data.get('Intelligence', 0) or 0,
psyche=data.get('Psyche', 0) or 0,
stamina=data.get('Stamina', 0) or 0,
strength=data.get('Strength', 0) or 0,
)
# =============================================================================
# API Client
# =============================================================================
class EntropiaNexusAPI:
"""Full client for Entropia Nexus API."""
BASE_URL = "https://api.entropianexus.com"
def __init__(self, cache_ttl: int = 3600):
self._cache: Dict[str, Any] = {}
self._cache_timestamps: Dict[str, float] = {}
self._cache_ttl = cache_ttl
logger.info("EntropiaNexusAPI initialized")
def _make_request(self, endpoint: str) -> Optional[List[Dict]]:
"""Make API request with caching."""
cache_key = endpoint
if cache_key in self._cache:
timestamp = self._cache_timestamps.get(cache_key, 0)
if (datetime.now().timestamp() - timestamp) < self._cache_ttl:
return self._cache[cache_key]
if not HAS_URLLIB:
return None
url = f"{self.BASE_URL}/{endpoint}"
try:
req = urllib.request.Request(
url,
headers={'Accept': 'application/json', 'User-Agent': 'Lemontropia-Suite/0.2.0'}
)
with urllib.request.urlopen(req, timeout=30) as response:
data = json.loads(response.read().decode('utf-8'))
self._cache[cache_key] = data
self._cache_timestamps[cache_key] = datetime.now().timestamp()
logger.info(f"Fetched {len(data)} items from {endpoint}")
return data
except Exception as e:
logger.error(f"API request failed for {endpoint}: {e}")
return None
def _get_item(self, endpoint: str, item_id: int) -> Optional[Dict]:
"""Get single item by ID."""
cache_key = f"{endpoint}/{item_id}"
if cache_key in self._cache:
timestamp = self._cache_timestamps.get(cache_key, 0)
if (datetime.now().timestamp() - timestamp) < self._cache_ttl:
return self._cache[cache_key]
if not HAS_URLLIB:
return None
url = f"{self.BASE_URL}/{endpoint}/{item_id}"
try:
req = urllib.request.Request(
url,
headers={'Accept': 'application/json', 'User-Agent': 'Lemontropia-Suite/0.2.0'}
)
with urllib.request.urlopen(req, timeout=30) as response:
data = json.loads(response.read().decode('utf-8'))
self._cache[cache_key] = data
self._cache_timestamps[cache_key] = datetime.now().timestamp()
return data
except Exception as e:
logger.error(f"API request failed for {url}: {e}")
return None
# ========================================================================
# Weapons
# ========================================================================
def get_all_weapons(self) -> List[WeaponStats]:
"""Get all weapons."""
data = self._make_request('weapons')
if data:
return [WeaponStats.from_api_data(w) for w in data]
return []
def get_weapon(self, weapon_id: int) -> Optional[WeaponStats]:
"""Get specific weapon by ID."""
data = self._get_item('weapons', weapon_id)
if data:
return WeaponStats.from_api_data(data)
return None
def search_weapons(self, query: str) -> List[WeaponStats]:
"""Search weapons by name."""
weapons = self.get_all_weapons()
query_lower = query.lower()
return [w for w in weapons if query_lower in w.name.lower()]
# ========================================================================
# Armors
# ========================================================================
def get_all_armors(self) -> List[ArmorStats]:
"""Get all armors."""
data = self._make_request('armors')
if data:
return [ArmorStats.from_api_data(a) for a in data]
return []
def get_armor(self, armor_id: int) -> Optional[ArmorStats]:
"""Get specific armor by ID."""
data = self._get_item('armors', armor_id)
if data:
return ArmorStats.from_api_data(data)
return None
# ========================================================================
# Mining Tools
# ========================================================================
def get_all_finders(self) -> List[FinderStats]:
"""Get all mining finders."""
data = self._make_request('finders')
if data:
return [FinderStats.from_api_data(f) for f in data]
return []
def get_all_excavators(self) -> List[ExcavatorStats]:
"""Get all excavators."""
data = self._make_request('excavators')
if data:
return [ExcavatorStats.from_api_data(e) for e in data]
return []
# ========================================================================
# Mobs
# ========================================================================
def get_all_mobs(self) -> List[MobStats]:
"""Get all creatures/mobs."""
data = self._make_request('mobs')
if data:
return [MobStats.from_api_data(m) for m in data]
return []
def get_mob(self, mob_id: int) -> Optional[MobStats]:
"""Get specific mob by ID."""
data = self._get_item('mobs', mob_id)
if data:
return MobStats.from_api_data(data)
return None
def search_mobs(self, query: str) -> List[MobStats]:
"""Search mobs by name."""
mobs = self.get_all_mobs()
query_lower = query.lower()
return [m for m in mobs if query_lower in m.name.lower()]
# ========================================================================
# Cache Management
# ========================================================================
def clear_cache(self):
"""Clear all cached data."""
self._cache.clear()
self._cache_timestamps.clear()
logger.info("Cache cleared")
__all__ = [
'WeaponStats', 'ArmorStats', 'FinderStats', 'ExcavatorStats', 'MobStats',
'EntropiaNexusAPI'
]