741 lines
23 KiB
Python
741 lines
23 KiB
Python
"""
|
|
EU-Utility - Entropia Nexus API Client
|
|
|
|
API client for https://api.entropianexus.com
|
|
Provides access to game data: items, mobs, market info.
|
|
|
|
Part of core - plugins access via PluginAPI.
|
|
"""
|
|
|
|
import time
|
|
import json
|
|
from typing import Dict, Any, List, Optional, Union
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
class EntityType(Enum):
|
|
"""Types of entities that can be searched."""
|
|
# Core types
|
|
ITEM = "items"
|
|
MOB = "mobs"
|
|
ALL = "all"
|
|
|
|
# Equipment
|
|
WEAPON = "weapons"
|
|
ARMOR = "armors"
|
|
ENHANCER = "enhancers"
|
|
|
|
# Tools
|
|
MEDICAL_TOOL = "medicaltools"
|
|
FINDER = "finders"
|
|
EXCAVATOR = "excavators"
|
|
REFINER = "refiners"
|
|
|
|
# Crafting & Materials
|
|
BLUEPRINT = "blueprints"
|
|
MATERIAL = "materials"
|
|
|
|
# Creatures
|
|
PET = "pets"
|
|
|
|
# Locations
|
|
LOCATION = "locations"
|
|
TELEPORTER = "teleporters"
|
|
SHOP = "shops"
|
|
VENDOR = "vendors"
|
|
PLANET = "planets"
|
|
AREA = "areas"
|
|
|
|
# Other
|
|
SKILL = "skills"
|
|
VEHICLE = "vehicles"
|
|
DECORATION = "decorations"
|
|
FURNITURE = "furniture"
|
|
STORAGE_CONTAINER = "storagecontainers"
|
|
STRONGBOX = "strongboxes"
|
|
|
|
@classmethod
|
|
def get_all_types(cls) -> List[str]:
|
|
"""Get list of all entity type values."""
|
|
return [e.value for e in cls if e != cls.ALL]
|
|
|
|
@classmethod
|
|
def from_string(cls, type_str: str) -> Optional["EntityType"]:
|
|
"""Get EntityType from string value."""
|
|
type_str = type_str.lower()
|
|
for e in cls:
|
|
if e.value.lower() == type_str:
|
|
return e
|
|
return None
|
|
|
|
|
|
class NexusAPIError(Exception):
|
|
"""Custom exception for Nexus API errors."""
|
|
pass
|
|
|
|
|
|
class RateLimitError(NexusAPIError):
|
|
"""Raised when rate limit is exceeded."""
|
|
pass
|
|
|
|
|
|
@dataclass
|
|
class SearchResult:
|
|
"""Result from a search operation."""
|
|
id: str
|
|
name: str
|
|
type: str
|
|
category: Optional[str] = None
|
|
icon_url: Optional[str] = None
|
|
data: Dict[str, Any] = None
|
|
|
|
def __post_init__(self):
|
|
if self.data is None:
|
|
self.data = {}
|
|
|
|
|
|
@dataclass
|
|
class ItemDetails:
|
|
"""Detailed item information."""
|
|
id: str
|
|
name: str
|
|
description: Optional[str] = None
|
|
category: Optional[str] = None
|
|
weight: Optional[float] = None
|
|
tt_value: Optional[float] = None
|
|
decay: Optional[float] = None
|
|
ammo_consumption: Optional[int] = None
|
|
damage: Optional[float] = None
|
|
range: Optional[float] = None
|
|
accuracy: Optional[float] = None
|
|
durability: Optional[int] = None
|
|
requirements: Dict[str, Any] = None
|
|
materials: List[Dict[str, Any]] = None
|
|
raw_data: Dict[str, Any] = None
|
|
|
|
def __post_init__(self):
|
|
if self.requirements is None:
|
|
self.requirements = {}
|
|
if self.materials is None:
|
|
self.materials = []
|
|
if self.raw_data is None:
|
|
self.raw_data = {}
|
|
|
|
|
|
@dataclass
|
|
class MarketData:
|
|
"""Market data for an item."""
|
|
item_id: str
|
|
item_name: str
|
|
current_markup: Optional[float] = None
|
|
avg_markup_7d: Optional[float] = None
|
|
avg_markup_30d: Optional[float] = None
|
|
volume_24h: Optional[int] = None
|
|
volume_7d: Optional[int] = None
|
|
buy_orders: List[Dict[str, Any]] = None
|
|
sell_orders: List[Dict[str, Any]] = None
|
|
last_updated: Optional[datetime] = None
|
|
raw_data: Dict[str, Any] = None
|
|
|
|
def __post_init__(self):
|
|
if self.buy_orders is None:
|
|
self.buy_orders = []
|
|
if self.sell_orders is None:
|
|
self.sell_orders = []
|
|
if self.raw_data is None:
|
|
self.raw_data = {}
|
|
|
|
|
|
class NexusAPI:
|
|
"""
|
|
Singleton client for Entropia Nexus API.
|
|
|
|
Features:
|
|
- Auto-retry on transient failures
|
|
- Rate limiting (max requests per second)
|
|
- Caching for frequently accessed data
|
|
- Proper error handling
|
|
|
|
Usage:
|
|
api = get_nexus_api()
|
|
results = api.search_items("ArMatrix")
|
|
details = api.get_item_details("item_id")
|
|
"""
|
|
|
|
_instance = None
|
|
|
|
# API Configuration
|
|
BASE_URL = "https://api.entropianexus.com"
|
|
API_VERSION = "v1"
|
|
|
|
# Rate limiting
|
|
MAX_REQUESTS_PER_SECOND = 5
|
|
MIN_REQUEST_INTERVAL = 1.0 / MAX_REQUESTS_PER_SECOND
|
|
|
|
# Retry configuration
|
|
MAX_RETRIES = 3
|
|
RETRY_DELAY = 1.0 # seconds
|
|
|
|
def __new__(cls):
|
|
if cls._instance is None:
|
|
cls._instance = super().__new__(cls)
|
|
cls._instance._initialized = False
|
|
return cls._instance
|
|
|
|
def __init__(self):
|
|
if self._initialized:
|
|
return
|
|
|
|
self._last_request_time = 0
|
|
self._request_count = 0
|
|
self._cache: Dict[str, Any] = {}
|
|
self._cache_ttl = 300 # 5 minutes default cache
|
|
self._cache_timestamps: Dict[str, float] = {}
|
|
self._session = None
|
|
self._initialized = True
|
|
self._available = True
|
|
|
|
print("[NexusAPI] Initialized")
|
|
|
|
def _get_session(self):
|
|
"""Get or create HTTP session."""
|
|
if self._session is None:
|
|
try:
|
|
import requests
|
|
self._session = requests.Session()
|
|
self._session.headers.update({
|
|
'User-Agent': 'EU-Utility/1.0 (Entropia Universe Utility Tool)',
|
|
'Accept': 'application/json'
|
|
})
|
|
except ImportError:
|
|
raise NexusAPIError("requests library not installed. Run: pip install requests")
|
|
return self._session
|
|
|
|
def _rate_limit(self):
|
|
"""Enforce rate limiting between requests."""
|
|
current_time = time.time()
|
|
time_since_last = current_time - self._last_request_time
|
|
|
|
if time_since_last < self.MIN_REQUEST_INTERVAL:
|
|
sleep_time = self.MIN_REQUEST_INTERVAL - time_since_last
|
|
time.sleep(sleep_time)
|
|
|
|
self._last_request_time = time.time()
|
|
self._request_count += 1
|
|
|
|
def _make_request(
|
|
self,
|
|
endpoint: str,
|
|
params: Dict[str, Any] = None,
|
|
use_cache: bool = True
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Make HTTP request with retry logic and rate limiting.
|
|
|
|
Args:
|
|
endpoint: API endpoint path
|
|
params: Query parameters
|
|
use_cache: Whether to use caching
|
|
|
|
Returns:
|
|
JSON response as dict
|
|
|
|
Raises:
|
|
NexusAPIError: On API errors
|
|
RateLimitError: On rate limit exceeded
|
|
"""
|
|
# Check cache
|
|
cache_key = f"{endpoint}:{json.dumps(params, sort_keys=True) if params else ''}"
|
|
if use_cache and self._is_cache_valid(cache_key):
|
|
return self._cache[cache_key]
|
|
|
|
url = f"{self.BASE_URL}/{self.API_VERSION}/{endpoint}"
|
|
last_error = None
|
|
|
|
for attempt in range(self.MAX_RETRIES):
|
|
try:
|
|
# Rate limit
|
|
self._rate_limit()
|
|
|
|
# Make request
|
|
session = self._get_session()
|
|
response = session.get(url, params=params, timeout=30)
|
|
|
|
# Handle rate limiting
|
|
if response.status_code == 429:
|
|
retry_after = int(response.headers.get('Retry-After', 60))
|
|
if attempt < self.MAX_RETRIES - 1:
|
|
print(f"[NexusAPI] Rate limited. Waiting {retry_after}s...")
|
|
time.sleep(retry_after)
|
|
continue
|
|
else:
|
|
raise RateLimitError(f"Rate limit exceeded. Retry after {retry_after}s")
|
|
|
|
# Handle other HTTP errors
|
|
if response.status_code >= 500:
|
|
if attempt < self.MAX_RETRIES - 1:
|
|
wait_time = self.RETRY_DELAY * (2 ** attempt) # Exponential backoff
|
|
print(f"[NexusAPI] Server error {response.status_code}. Retrying in {wait_time}s...")
|
|
time.sleep(wait_time)
|
|
continue
|
|
|
|
response.raise_for_status()
|
|
|
|
# Parse response
|
|
data = response.json()
|
|
|
|
# Cache successful response
|
|
if use_cache:
|
|
self._cache[cache_key] = data
|
|
self._cache_timestamps[cache_key] = time.time()
|
|
|
|
return data
|
|
|
|
except RateLimitError:
|
|
raise
|
|
except Exception as e:
|
|
last_error = e
|
|
if attempt < self.MAX_RETRIES - 1:
|
|
wait_time = self.RETRY_DELAY * (2 ** attempt)
|
|
print(f"[NexusAPI] Request failed: {e}. Retrying in {wait_time}s...")
|
|
time.sleep(wait_time)
|
|
else:
|
|
break
|
|
|
|
# All retries exhausted
|
|
error_msg = f"Request failed after {self.MAX_RETRIES} attempts: {last_error}"
|
|
print(f"[NexusAPI] {error_msg}")
|
|
raise NexusAPIError(error_msg)
|
|
|
|
def _is_cache_valid(self, key: str) -> bool:
|
|
"""Check if cached data is still valid."""
|
|
if key not in self._cache_timestamps:
|
|
return False
|
|
age = time.time() - self._cache_timestamps[key]
|
|
return age < self._cache_ttl
|
|
|
|
def clear_cache(self):
|
|
"""Clear all cached data."""
|
|
self._cache.clear()
|
|
self._cache_timestamps.clear()
|
|
print("[NexusAPI] Cache cleared")
|
|
|
|
def is_available(self) -> bool:
|
|
"""Check if API is available."""
|
|
return self._available
|
|
|
|
# ========== Search Methods ==========
|
|
|
|
def search_items(self, query: str, limit: int = 20) -> List[SearchResult]:
|
|
"""
|
|
Search for items by name.
|
|
|
|
Args:
|
|
query: Search term (e.g., "ArMatrix", "Omegaton")
|
|
limit: Maximum results (default 20, max 100)
|
|
|
|
Returns:
|
|
List of SearchResult objects
|
|
|
|
Example:
|
|
results = api.search_items("ArMatrix")
|
|
for r in results:
|
|
print(f"{r.name} ({r.type})")
|
|
"""
|
|
try:
|
|
params = {
|
|
'q': query,
|
|
'limit': min(limit, 100),
|
|
'type': 'item'
|
|
}
|
|
|
|
data = self._make_request('search', params)
|
|
|
|
results = []
|
|
for item in data.get('results', []):
|
|
results.append(SearchResult(
|
|
id=item.get('id', ''),
|
|
name=item.get('name', ''),
|
|
type=item.get('type', 'item'),
|
|
category=item.get('category'),
|
|
icon_url=item.get('icon_url'),
|
|
data=item
|
|
))
|
|
|
|
return results
|
|
|
|
except Exception as e:
|
|
print(f"[NexusAPI] search_items error: {e}")
|
|
return []
|
|
|
|
def search_mobs(self, query: str, limit: int = 20) -> List[SearchResult]:
|
|
"""
|
|
Search for creatures/mobs by name.
|
|
|
|
Args:
|
|
query: Search term (e.g., "Atrox", "Hispidus")
|
|
limit: Maximum results (default 20, max 100)
|
|
|
|
Returns:
|
|
List of SearchResult objects
|
|
"""
|
|
try:
|
|
params = {
|
|
'q': query,
|
|
'limit': min(limit, 100),
|
|
'type': 'mob'
|
|
}
|
|
|
|
data = self._make_request('search', params)
|
|
|
|
results = []
|
|
for item in data.get('results', []):
|
|
results.append(SearchResult(
|
|
id=item.get('id', ''),
|
|
name=item.get('name', ''),
|
|
type=item.get('type', 'mob'),
|
|
category=item.get('category'),
|
|
icon_url=item.get('icon_url'),
|
|
data=item
|
|
))
|
|
|
|
return results
|
|
|
|
except Exception as e:
|
|
print(f"[NexusAPI] search_mobs error: {e}")
|
|
return []
|
|
|
|
def search_all(self, query: str, limit: int = 20) -> List[SearchResult]:
|
|
"""
|
|
Search across all entity types (items, mobs, etc.).
|
|
|
|
Args:
|
|
query: Search term
|
|
limit: Maximum results per type (default 20)
|
|
|
|
Returns:
|
|
List of SearchResult objects
|
|
"""
|
|
try:
|
|
params = {
|
|
'q': query,
|
|
'limit': min(limit, 100)
|
|
}
|
|
|
|
data = self._make_request('search', params)
|
|
|
|
results = []
|
|
for item in data.get('results', []):
|
|
results.append(SearchResult(
|
|
id=item.get('id', ''),
|
|
name=item.get('name', ''),
|
|
type=item.get('type', 'unknown'),
|
|
category=item.get('category'),
|
|
icon_url=item.get('icon_url'),
|
|
data=item
|
|
))
|
|
|
|
return results
|
|
|
|
except Exception as e:
|
|
print(f"[NexusAPI] search_all error: {e}")
|
|
return []
|
|
|
|
def search_by_type(self, query: str, entity_type: str, limit: int = 20) -> List[SearchResult]:
|
|
"""
|
|
Search for entities of a specific type.
|
|
|
|
Args:
|
|
query: Search term
|
|
entity_type: Entity type (e.g., 'weapons', 'blueprints', 'mobs')
|
|
limit: Maximum results (default 20, max 100)
|
|
|
|
Returns:
|
|
List of SearchResult objects
|
|
|
|
Example:
|
|
results = api.search_by_type("ArMatrix", "weapons")
|
|
results = api.search_by_type("Atrox", "mobs")
|
|
"""
|
|
try:
|
|
# Normalize entity type
|
|
entity_type = entity_type.lower().replace(' ', '').replace('_', '')
|
|
|
|
# Map common aliases
|
|
type_mapping = {
|
|
'item': 'items',
|
|
'weapon': 'weapons',
|
|
'armor': 'armors',
|
|
'mob': 'mobs',
|
|
'blueprint': 'blueprints',
|
|
'location': 'locations',
|
|
'skill': 'skills',
|
|
'material': 'materials',
|
|
'enhancer': 'enhancers',
|
|
'medicaltool': 'medicaltools',
|
|
'medical_tool': 'medicaltools',
|
|
'finder': 'finders',
|
|
'excavator': 'excavators',
|
|
'refiner': 'refiners',
|
|
'vehicle': 'vehicles',
|
|
'pet': 'pets',
|
|
'decoration': 'decorations',
|
|
'furniture': 'furniture',
|
|
'storage': 'storagecontainers',
|
|
'storagecontainer': 'storagecontainers',
|
|
'storage_container': 'storagecontainers',
|
|
'strongbox': 'strongboxes',
|
|
'teleporter': 'teleporters',
|
|
'shop': 'shops',
|
|
'vendor': 'vendors',
|
|
'planet': 'planets',
|
|
'area': 'areas',
|
|
}
|
|
|
|
endpoint = type_mapping.get(entity_type, entity_type)
|
|
|
|
params = {
|
|
'q': query,
|
|
'limit': min(limit, 100),
|
|
'fuzzy': 'true'
|
|
}
|
|
|
|
data = self._make_request(f'{endpoint}', params)
|
|
|
|
results = []
|
|
items = data if isinstance(data, list) else data.get('results', [])
|
|
|
|
for item in items:
|
|
results.append(SearchResult(
|
|
id=item.get('id', item.get('Id', '')),
|
|
name=item.get('name', item.get('Name', 'Unknown')),
|
|
type=item.get('type', entity_type),
|
|
category=item.get('category', item.get('Category')),
|
|
icon_url=item.get('icon_url', item.get('IconUrl')),
|
|
data=item
|
|
))
|
|
|
|
return results
|
|
|
|
except Exception as e:
|
|
print(f"[NexusAPI] search_by_type error ({entity_type}): {e}")
|
|
return []
|
|
|
|
def get_entity_details(self, entity_id: str, entity_type: str) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Get detailed information about any entity type.
|
|
|
|
Args:
|
|
entity_id: The entity's unique identifier
|
|
entity_type: Entity type (e.g., 'mobs', 'locations', 'blueprints')
|
|
|
|
Returns:
|
|
Dict with entity details or None if not found
|
|
|
|
Example:
|
|
mob = api.get_entity_details("atrox", "mobs")
|
|
location = api.get_entity_details("fort-izzuk", "locations")
|
|
"""
|
|
try:
|
|
# Normalize entity type
|
|
entity_type = entity_type.lower().replace(' ', '').replace('_', '')
|
|
|
|
# Map common aliases
|
|
type_mapping = {
|
|
'item': 'items',
|
|
'weapon': 'weapons',
|
|
'armor': 'armors',
|
|
'mob': 'mobs',
|
|
'blueprint': 'blueprints',
|
|
'location': 'locations',
|
|
'skill': 'skills',
|
|
'material': 'materials',
|
|
'enhancer': 'enhancers',
|
|
'medicaltool': 'medicaltools',
|
|
'finder': 'finders',
|
|
'excavator': 'excavators',
|
|
'refiner': 'refiners',
|
|
'vehicle': 'vehicles',
|
|
'pet': 'pets',
|
|
'decoration': 'decorations',
|
|
'furniture': 'furniture',
|
|
'storage': 'storagecontainers',
|
|
'storagecontainer': 'storagecontainers',
|
|
'strongbox': 'strongboxes',
|
|
'teleporter': 'teleporters',
|
|
'shop': 'shops',
|
|
'vendor': 'vendors',
|
|
'planet': 'planets',
|
|
'area': 'areas',
|
|
}
|
|
|
|
endpoint = type_mapping.get(entity_type, entity_type)
|
|
data = self._make_request(f'{endpoint}/{entity_id}')
|
|
|
|
if not data or 'error' in data:
|
|
return None
|
|
|
|
return data
|
|
|
|
except Exception as e:
|
|
print(f"[NexusAPI] get_entity_details error ({entity_type}): {e}")
|
|
return None
|
|
|
|
# ========== Detail Methods ==========
|
|
|
|
def get_item_details(self, item_id: str) -> Optional[ItemDetails]:
|
|
"""
|
|
Get detailed information about a specific item.
|
|
|
|
Args:
|
|
item_id: The item's unique identifier
|
|
|
|
Returns:
|
|
ItemDetails object or None if not found
|
|
|
|
Example:
|
|
details = api.get_item_details("armatrix_lp-35")
|
|
print(f"TT Value: {details.tt_value} PED")
|
|
"""
|
|
try:
|
|
data = self._make_request(f'items/{item_id}')
|
|
|
|
if not data or 'error' in data:
|
|
return None
|
|
|
|
return ItemDetails(
|
|
id=data.get('id', item_id),
|
|
name=data.get('name', 'Unknown'),
|
|
description=data.get('description'),
|
|
category=data.get('category'),
|
|
weight=data.get('weight'),
|
|
tt_value=data.get('tt_value'),
|
|
decay=data.get('decay'),
|
|
ammo_consumption=data.get('ammo_consumption'),
|
|
damage=data.get('damage'),
|
|
range=data.get('range'),
|
|
accuracy=data.get('accuracy'),
|
|
durability=data.get('durability'),
|
|
requirements=data.get('requirements', {}),
|
|
materials=data.get('materials', []),
|
|
raw_data=data
|
|
)
|
|
|
|
except Exception as e:
|
|
print(f"[NexusAPI] get_item_details error: {e}")
|
|
return None
|
|
|
|
def get_market_data(self, item_id: str) -> Optional[MarketData]:
|
|
"""
|
|
Get market data for a specific item.
|
|
|
|
Args:
|
|
item_id: The item's unique identifier
|
|
|
|
Returns:
|
|
MarketData object or None if not found
|
|
|
|
Example:
|
|
market = api.get_market_data("armatrix_lp-35")
|
|
print(f"Current markup: {market.current_markup:.1f}%")
|
|
print(f"24h Volume: {market.volume_24h}")
|
|
"""
|
|
try:
|
|
data = self._make_request(f'items/{item_id}/market')
|
|
|
|
if not data or 'error' in data:
|
|
return None
|
|
|
|
# Parse timestamp if present
|
|
last_updated = None
|
|
if 'last_updated' in data:
|
|
try:
|
|
last_updated = datetime.fromisoformat(data['last_updated'].replace('Z', '+00:00'))
|
|
except:
|
|
pass
|
|
|
|
return MarketData(
|
|
item_id=item_id,
|
|
item_name=data.get('item_name', 'Unknown'),
|
|
current_markup=data.get('current_markup'),
|
|
avg_markup_7d=data.get('avg_markup_7d'),
|
|
avg_markup_30d=data.get('avg_markup_30d'),
|
|
volume_24h=data.get('volume_24h'),
|
|
volume_7d=data.get('volume_7d'),
|
|
buy_orders=data.get('buy_orders', []),
|
|
sell_orders=data.get('sell_orders', []),
|
|
last_updated=last_updated,
|
|
raw_data=data
|
|
)
|
|
|
|
except Exception as e:
|
|
print(f"[NexusAPI] get_market_data error: {e}")
|
|
return None
|
|
|
|
# ========== Batch Methods ==========
|
|
|
|
def get_items_batch(self, item_ids: List[str]) -> Dict[str, Optional[ItemDetails]]:
|
|
"""
|
|
Get details for multiple items efficiently.
|
|
|
|
Args:
|
|
item_ids: List of item IDs
|
|
|
|
Returns:
|
|
Dict mapping item_id to ItemDetails (or None if failed)
|
|
"""
|
|
results = {}
|
|
for item_id in item_ids:
|
|
results[item_id] = self.get_item_details(item_id)
|
|
return results
|
|
|
|
def get_market_batch(self, item_ids: List[str]) -> Dict[str, Optional[MarketData]]:
|
|
"""
|
|
Get market data for multiple items efficiently.
|
|
|
|
Args:
|
|
item_ids: List of item IDs
|
|
|
|
Returns:
|
|
Dict mapping item_id to MarketData (or None if failed)
|
|
"""
|
|
results = {}
|
|
for item_id in item_ids:
|
|
results[item_id] = self.get_market_data(item_id)
|
|
return results
|
|
|
|
|
|
# Singleton instance
|
|
_nexus_api = None
|
|
|
|
def get_nexus_api() -> NexusAPI:
|
|
"""Get the global NexusAPI instance."""
|
|
global _nexus_api
|
|
if _nexus_api is None:
|
|
_nexus_api = NexusAPI()
|
|
return _nexus_api
|
|
|
|
|
|
# Convenience functions for quick access
|
|
def search_items(query: str, limit: int = 20) -> List[SearchResult]:
|
|
"""Quick search for items."""
|
|
return get_nexus_api().search_items(query, limit)
|
|
|
|
def search_mobs(query: str, limit: int = 20) -> List[SearchResult]:
|
|
"""Quick search for mobs."""
|
|
return get_nexus_api().search_mobs(query, limit)
|
|
|
|
def search_all(query: str, limit: int = 20) -> List[SearchResult]:
|
|
"""Quick search across all types."""
|
|
return get_nexus_api().search_all(query, limit)
|
|
|
|
def get_item_details(item_id: str) -> Optional[ItemDetails]:
|
|
"""Quick get item details."""
|
|
return get_nexus_api().get_item_details(item_id)
|
|
|
|
def get_market_data(item_id: str) -> Optional[MarketData]:
|
|
"""Quick get market data."""
|
|
return get_nexus_api().get_market_data(item_id)
|