fix: Cross-platform file locking for Windows
- Fixed fcntl import error on Windows - Added portalocker as Windows fallback - Graceful degradation if no locking available - Updated requirements.txt with platform-specific deps
This commit is contained in:
parent
6d1a17cc30
commit
9cf67c302f
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"skills": {
|
||||||
|
"playwright": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"installedAt": 1771029662552
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
# 2026-02-14 - OpenClaw Configuration & API Setup
|
||||||
|
|
||||||
|
## Configuration Changes Made
|
||||||
|
- Increased concurrency: 8 main agents, 16 subagents
|
||||||
|
- Enabled cron jobs (max 4 concurrent runs)
|
||||||
|
- Optimized for research and development workflows
|
||||||
|
|
||||||
|
## API Keys Status
|
||||||
|
|
||||||
|
### ✅ Configured
|
||||||
|
- **Kimi Coding** - Primary model (kimi-coding/k2p5)
|
||||||
|
- **xAI/Grok-4** - Active model via fallback chain
|
||||||
|
- **OpenRouter** - Auth profile created, needs models configured
|
||||||
|
- **Telegram Bot** - Channel integration working
|
||||||
|
|
||||||
|
### 🔄 Pending Configuration
|
||||||
|
- **Firecrawl** - API key ready, config syntax issues
|
||||||
|
- **Gemini/Google AI** - API key ready, needs provider setup
|
||||||
|
- **Groq** - Fallbacks configured, needs full provider setup
|
||||||
|
|
||||||
|
## Commands Learned
|
||||||
|
- `openclaw gateway config patch --raw '{...}'` - Correct syntax for config updates
|
||||||
|
- `openclaw gateway config edit` - Direct config editing
|
||||||
|
- `openclaw gateway config get` - View current config
|
||||||
|
- Environment variables as fallback for API keys
|
||||||
|
|
||||||
|
## Free API Alternatives for Web Search
|
||||||
|
- **Brave Search** - No longer has free tier ($3/month minimum)
|
||||||
|
- **SerpAPI** - 100 searches/month free
|
||||||
|
- **SearXNG** - Self-hosted, unlimited
|
||||||
|
- **DuckDuckGo** - No API key needed (HTML scraping)
|
||||||
|
- **Jina AI** - Free, no signup (r.jina.ai/http://URL)
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
1. Complete Firecrawl configuration (web scraping)
|
||||||
|
2. Add Gemini for embeddings and image understanding
|
||||||
|
3. Finalize OpenRouter model configuration
|
||||||
|
4. Test all configured APIs
|
||||||
|
|
@ -6,14 +6,27 @@ Provides file locking, auto-backup, and singleton access.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import fcntl
|
|
||||||
import shutil
|
import shutil
|
||||||
import threading
|
import threading
|
||||||
|
import platform
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
# Cross-platform file locking
|
||||||
|
try:
|
||||||
|
import fcntl # Unix/Linux/Mac
|
||||||
|
HAS_FCNTL = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_FCNTL = False
|
||||||
|
# Windows fallback using portalocker or threading lock
|
||||||
|
try:
|
||||||
|
import portalocker
|
||||||
|
HAS_PORTALOCKER = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_PORTALOCKER = False
|
||||||
|
|
||||||
|
|
||||||
class DataStore:
|
class DataStore:
|
||||||
"""
|
"""
|
||||||
|
|
@ -80,12 +93,12 @@ class DataStore:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
# Acquire shared lock for reading
|
# Cross-platform file locking
|
||||||
fcntl.flock(f.fileno(), fcntl.LOCK_SH)
|
self._lock_file(f, exclusive=False)
|
||||||
try:
|
try:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
finally:
|
finally:
|
||||||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
self._unlock_file(f)
|
||||||
|
|
||||||
# Update cache
|
# Update cache
|
||||||
with self._cache_lock:
|
with self._cache_lock:
|
||||||
|
|
@ -108,15 +121,15 @@ class DataStore:
|
||||||
temp_path = file_path.with_suffix('.tmp')
|
temp_path = file_path.with_suffix('.tmp')
|
||||||
|
|
||||||
with open(temp_path, 'w', encoding='utf-8') as f:
|
with open(temp_path, 'w', encoding='utf-8') as f:
|
||||||
# Acquire exclusive lock for writing
|
# Cross-platform file locking
|
||||||
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
self._lock_file(f, exclusive=True)
|
||||||
try:
|
try:
|
||||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
f.flush()
|
f.flush()
|
||||||
import os
|
import os
|
||||||
os.fsync(f.fileno())
|
os.fsync(f.fileno())
|
||||||
finally:
|
finally:
|
||||||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
self._unlock_file(f)
|
||||||
|
|
||||||
# Atomic move
|
# Atomic move
|
||||||
temp_path.replace(file_path)
|
temp_path.replace(file_path)
|
||||||
|
|
@ -134,6 +147,32 @@ class DataStore:
|
||||||
temp_path.unlink()
|
temp_path.unlink()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _lock_file(self, f, exclusive: bool = False):
|
||||||
|
"""Cross-platform file locking."""
|
||||||
|
if HAS_FCNTL:
|
||||||
|
# Unix/Linux/Mac
|
||||||
|
lock_type = fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH
|
||||||
|
fcntl.flock(f.fileno(), lock_type)
|
||||||
|
elif HAS_PORTALOCKER:
|
||||||
|
# Windows with portalocker
|
||||||
|
import portalocker
|
||||||
|
lock_type = portalocker.LOCK_EX if exclusive else portalocker.LOCK_SH
|
||||||
|
portalocker.lock(f, lock_type)
|
||||||
|
else:
|
||||||
|
# Fallback: rely on threading lock (already held)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _unlock_file(self, f):
|
||||||
|
"""Cross-platform file unlock."""
|
||||||
|
if HAS_FCNTL:
|
||||||
|
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||||||
|
elif HAS_PORTALOCKER:
|
||||||
|
import portalocker
|
||||||
|
portalocker.unlock(f)
|
||||||
|
else:
|
||||||
|
# Fallback: nothing to do
|
||||||
|
pass
|
||||||
|
|
||||||
def _create_backup(self, plugin_id: str, file_path: Path):
|
def _create_backup(self, plugin_id: str, file_path: Path):
|
||||||
"""Create a backup of the current data file."""
|
"""Create a backup of the current data file."""
|
||||||
backup_dir = self._get_backup_dir(plugin_id)
|
backup_dir = self._get_backup_dir(plugin_id)
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,59 @@ from datetime import datetime, timedelta
|
||||||
|
|
||||||
class EntityType(Enum):
|
class EntityType(Enum):
|
||||||
"""Types of entities that can be searched."""
|
"""Types of entities that can be searched."""
|
||||||
|
# Core types
|
||||||
ITEM = "items"
|
ITEM = "items"
|
||||||
MOB = "mobs"
|
MOB = "mobs"
|
||||||
ALL = "all"
|
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):
|
class NexusAPIError(Exception):
|
||||||
"""Custom exception for Nexus API errors."""
|
"""Custom exception for Nexus API errors."""
|
||||||
|
|
@ -394,6 +443,146 @@ class NexusAPI:
|
||||||
print(f"[NexusAPI] search_all error: {e}")
|
print(f"[NexusAPI] search_all error: {e}")
|
||||||
return []
|
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 ==========
|
# ========== Detail Methods ==========
|
||||||
|
|
||||||
def get_item_details(self, item_id: str) -> Optional[ItemDetails]:
|
def get_item_details(self, item_id: str) -> Optional[ItemDetails]:
|
||||||
|
|
|
||||||
|
|
@ -848,6 +848,219 @@ class PluginAPI:
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# ========== Nexus API Service ==========
|
||||||
|
|
||||||
|
def register_nexus_service(self, nexus_api) -> None:
|
||||||
|
"""Register the Nexus API service.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nexus_api: NexusAPI instance from core.nexus_api
|
||||||
|
"""
|
||||||
|
self.services['nexus'] = nexus_api
|
||||||
|
print("[API] Nexus API service registered")
|
||||||
|
|
||||||
|
def nexus_search(self, query: str, entity_type: str = "items", limit: int = 20) -> List[Dict[str, Any]]:
|
||||||
|
"""Search for entities via Nexus API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Search query string
|
||||||
|
entity_type: Type of entity to search (items, mobs, weapons, etc.)
|
||||||
|
limit: Maximum number of results (default: 20, max: 100)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of search result dictionaries
|
||||||
|
|
||||||
|
Example:
|
||||||
|
# Search for items
|
||||||
|
results = api.nexus_search("ArMatrix", entity_type="items")
|
||||||
|
|
||||||
|
# Search for mobs
|
||||||
|
mobs = api.nexus_search("Atrox", entity_type="mobs")
|
||||||
|
|
||||||
|
# Search for blueprints
|
||||||
|
bps = api.nexus_search("ArMatrix", entity_type="blueprints")
|
||||||
|
"""
|
||||||
|
nexus = self.services.get('nexus')
|
||||||
|
if not nexus:
|
||||||
|
try:
|
||||||
|
from core.nexus_api import get_nexus_api
|
||||||
|
nexus = get_nexus_api()
|
||||||
|
self.services['nexus'] = nexus
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[API] Nexus API not available: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Map entity type to search method
|
||||||
|
entity_type = entity_type.lower()
|
||||||
|
|
||||||
|
if entity_type in ['item', 'items']:
|
||||||
|
results = nexus.search_items(query, limit)
|
||||||
|
elif entity_type in ['mob', 'mobs']:
|
||||||
|
results = nexus.search_mobs(query, limit)
|
||||||
|
elif entity_type == 'all':
|
||||||
|
results = nexus.search_all(query, limit)
|
||||||
|
else:
|
||||||
|
# For other entity types, use the generic search
|
||||||
|
# This requires the enhanced nexus_api with entity type support
|
||||||
|
if hasattr(nexus, 'search_by_type'):
|
||||||
|
results = nexus.search_by_type(query, entity_type, limit)
|
||||||
|
else:
|
||||||
|
# Fallback to generic search
|
||||||
|
results = nexus.search_all(query, limit)
|
||||||
|
|
||||||
|
# Convert SearchResult objects to dicts for plugin compatibility
|
||||||
|
return [self._search_result_to_dict(r) for r in results]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[API] Nexus search error: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _search_result_to_dict(self, result) -> Dict[str, Any]:
|
||||||
|
"""Convert SearchResult to dictionary."""
|
||||||
|
if isinstance(result, dict):
|
||||||
|
return result
|
||||||
|
return {
|
||||||
|
'id': getattr(result, 'id', ''),
|
||||||
|
'name': getattr(result, 'name', ''),
|
||||||
|
'type': getattr(result, 'type', ''),
|
||||||
|
'category': getattr(result, 'category', None),
|
||||||
|
'icon_url': getattr(result, 'icon_url', None),
|
||||||
|
'data': getattr(result, 'data', {})
|
||||||
|
}
|
||||||
|
|
||||||
|
def nexus_get_item_details(self, item_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get detailed information about a specific item.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item_id: The item's unique identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with item details, or None if not found
|
||||||
|
|
||||||
|
Example:
|
||||||
|
details = api.nexus_get_item_details("armatrix_lp-35")
|
||||||
|
if details:
|
||||||
|
print(f"TT Value: {details.get('tt_value')} PED")
|
||||||
|
print(f"Damage: {details.get('damage')}")
|
||||||
|
"""
|
||||||
|
nexus = self.services.get('nexus')
|
||||||
|
if not nexus:
|
||||||
|
try:
|
||||||
|
from core.nexus_api import get_nexus_api
|
||||||
|
nexus = get_nexus_api()
|
||||||
|
self.services['nexus'] = nexus
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[API] Nexus API not available: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
details = nexus.get_item_details(item_id)
|
||||||
|
if details:
|
||||||
|
return self._item_details_to_dict(details)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[API] Nexus get_item_details error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _item_details_to_dict(self, details) -> Dict[str, Any]:
|
||||||
|
"""Convert ItemDetails to dictionary."""
|
||||||
|
if isinstance(details, dict):
|
||||||
|
return details
|
||||||
|
return {
|
||||||
|
'id': getattr(details, 'id', ''),
|
||||||
|
'name': getattr(details, 'name', ''),
|
||||||
|
'description': getattr(details, 'description', None),
|
||||||
|
'category': getattr(details, 'category', None),
|
||||||
|
'weight': getattr(details, 'weight', None),
|
||||||
|
'tt_value': getattr(details, 'tt_value', None),
|
||||||
|
'decay': getattr(details, 'decay', None),
|
||||||
|
'ammo_consumption': getattr(details, 'ammo_consumption', None),
|
||||||
|
'damage': getattr(details, 'damage', None),
|
||||||
|
'range': getattr(details, 'range', None),
|
||||||
|
'accuracy': getattr(details, 'accuracy', None),
|
||||||
|
'durability': getattr(details, 'durability', None),
|
||||||
|
'requirements': getattr(details, 'requirements', {}),
|
||||||
|
'materials': getattr(details, 'materials', []),
|
||||||
|
'raw_data': getattr(details, 'raw_data', {})
|
||||||
|
}
|
||||||
|
|
||||||
|
def nexus_get_market_data(self, item_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get market data for a specific item.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item_id: The item's unique identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with market data, or None if not found
|
||||||
|
|
||||||
|
Example:
|
||||||
|
market = api.nexus_get_market_data("armatrix_lp-35")
|
||||||
|
if market:
|
||||||
|
print(f"Current markup: {market.get('current_markup'):.1f}%")
|
||||||
|
print(f"24h Volume: {market.get('volume_24h')}")
|
||||||
|
|
||||||
|
# Access order book
|
||||||
|
buy_orders = market.get('buy_orders', [])
|
||||||
|
sell_orders = market.get('sell_orders', [])
|
||||||
|
"""
|
||||||
|
nexus = self.services.get('nexus')
|
||||||
|
if not nexus:
|
||||||
|
try:
|
||||||
|
from core.nexus_api import get_nexus_api
|
||||||
|
nexus = get_nexus_api()
|
||||||
|
self.services['nexus'] = nexus
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[API] Nexus API not available: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
market = nexus.get_market_data(item_id)
|
||||||
|
if market:
|
||||||
|
return self._market_data_to_dict(market)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[API] Nexus get_market_data error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _market_data_to_dict(self, market) -> Dict[str, Any]:
|
||||||
|
"""Convert MarketData to dictionary."""
|
||||||
|
if isinstance(market, dict):
|
||||||
|
return market
|
||||||
|
return {
|
||||||
|
'item_id': getattr(market, 'item_id', ''),
|
||||||
|
'item_name': getattr(market, 'item_name', ''),
|
||||||
|
'current_markup': getattr(market, 'current_markup', None),
|
||||||
|
'avg_markup_7d': getattr(market, 'avg_markup_7d', None),
|
||||||
|
'avg_markup_30d': getattr(market, 'avg_markup_30d', None),
|
||||||
|
'volume_24h': getattr(market, 'volume_24h', None),
|
||||||
|
'volume_7d': getattr(market, 'volume_7d', None),
|
||||||
|
'buy_orders': getattr(market, 'buy_orders', []),
|
||||||
|
'sell_orders': getattr(market, 'sell_orders', []),
|
||||||
|
'last_updated': getattr(market, 'last_updated', None),
|
||||||
|
'raw_data': getattr(market, 'raw_data', {})
|
||||||
|
}
|
||||||
|
|
||||||
|
def nexus_is_available(self) -> bool:
|
||||||
|
"""Check if Nexus API is available.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if Nexus API service is ready
|
||||||
|
"""
|
||||||
|
nexus = self.services.get('nexus')
|
||||||
|
if not nexus:
|
||||||
|
try:
|
||||||
|
from core.nexus_api import get_nexus_api
|
||||||
|
nexus = get_nexus_api()
|
||||||
|
self.services['nexus'] = nexus
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
return nexus.is_available()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# Singleton instance
|
# Singleton instance
|
||||||
_plugin_api = None
|
_plugin_api = None
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,789 @@
|
||||||
|
# Entropia Nexus API Reference
|
||||||
|
|
||||||
|
> Complete technical documentation for the Entropia Nexus API
|
||||||
|
>
|
||||||
|
> **Version:** 1.0
|
||||||
|
> **Last Updated:** 2025-02-13
|
||||||
|
> **Base URL:** `https://api.entropianexus.com`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Overview](#overview)
|
||||||
|
2. [Authentication](#authentication)
|
||||||
|
3. [Base Configuration](#base-configuration)
|
||||||
|
4. [Entity Types](#entity-types)
|
||||||
|
5. [Endpoints](#endpoints)
|
||||||
|
6. [Request Parameters](#request-parameters)
|
||||||
|
7. [Response Formats](#response-formats)
|
||||||
|
8. [Error Handling](#error-handling)
|
||||||
|
9. [Rate Limits](#rate-limits)
|
||||||
|
10. [Usage Examples](#usage-examples)
|
||||||
|
11. [Plugin API Integration](#plugin-api-integration)
|
||||||
|
12. [Field Name Conventions](#field-name-conventions)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Entropia Nexus API provides programmatic access to game data from Entropia Universe. It supports:
|
||||||
|
|
||||||
|
- **25+ entity types** (items, mobs, locations, skills, etc.)
|
||||||
|
- **Full-text search** with fuzzy matching
|
||||||
|
- **Market data** for trading analysis
|
||||||
|
- **Detailed entity information** with stats and properties
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core.nexus_api import get_nexus_api
|
||||||
|
|
||||||
|
# Get API instance
|
||||||
|
api = get_nexus_api()
|
||||||
|
|
||||||
|
# Search for items
|
||||||
|
results = api.search_items("ArMatrix")
|
||||||
|
|
||||||
|
# Get detailed info
|
||||||
|
details = api.get_item_details("armatrix_lp-35")
|
||||||
|
|
||||||
|
# Get market data
|
||||||
|
market = api.get_market_data("armatrix_lp-35")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
The Entropia Nexus API is **public** and requires no authentication for read operations.
|
||||||
|
|
||||||
|
### Request Headers
|
||||||
|
|
||||||
|
Recommended headers for all requests:
|
||||||
|
|
||||||
|
```http
|
||||||
|
User-Agent: EU-Utility/1.0 (Entropia Universe Utility Tool)
|
||||||
|
Accept: application/json
|
||||||
|
Accept-Encoding: gzip
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Base Configuration
|
||||||
|
|
||||||
|
### API Client Settings
|
||||||
|
|
||||||
|
| Setting | Value | Description |
|
||||||
|
|---------|-------|-------------|
|
||||||
|
| `BASE_URL` | `https://api.entropianexus.com` | API endpoint |
|
||||||
|
| `API_VERSION` | `v1` | Current API version |
|
||||||
|
| `MAX_REQUESTS_PER_SECOND` | 5 | Rate limit for requests |
|
||||||
|
| `MIN_REQUEST_INTERVAL` | 0.2s | Minimum time between requests |
|
||||||
|
| `MAX_RETRIES` | 3 | Automatic retry attempts |
|
||||||
|
| `RETRY_DELAY` | 1.0s | Base delay between retries |
|
||||||
|
| `DEFAULT_CACHE_TTL` | 300s | Default cache lifetime (5 min) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entity Types
|
||||||
|
|
||||||
|
### Supported Entity Types
|
||||||
|
|
||||||
|
The API supports 25+ entity types organized into categories:
|
||||||
|
|
||||||
|
#### Equipment & Items
|
||||||
|
|
||||||
|
| Entity Type | Endpoint | Description |
|
||||||
|
|-------------|----------|-------------|
|
||||||
|
| `items` | `/items` | General items and components |
|
||||||
|
| `weapons` | `/weapons` | Ranged and melee weapons |
|
||||||
|
| `armors` | `/armors` | Protective armor sets |
|
||||||
|
| `enhancers` | `/enhancers` | Weapon/armor enhancers |
|
||||||
|
|
||||||
|
#### Tools & Professional
|
||||||
|
|
||||||
|
| Entity Type | Endpoint | Description |
|
||||||
|
|-------------|----------|-------------|
|
||||||
|
| `medicaltools` | `/medicaltools` | First Aid Packs, healing tools |
|
||||||
|
| `finders` | `/finders` | Mining finders/detectors |
|
||||||
|
| `excavators` | `/excavators` | Mining excavators |
|
||||||
|
| `refiners` | `/refiners` | Resource refiners |
|
||||||
|
|
||||||
|
#### Crafting & Materials
|
||||||
|
|
||||||
|
| Entity Type | Endpoint | Description |
|
||||||
|
|-------------|----------|-------------|
|
||||||
|
| `blueprints` | `/blueprints` | Crafting recipes |
|
||||||
|
| `materials` | `/materials` | Raw materials, ores, enmatters |
|
||||||
|
|
||||||
|
#### Creatures & Characters
|
||||||
|
|
||||||
|
| Entity Type | Endpoint | Description |
|
||||||
|
|-------------|----------|-------------|
|
||||||
|
| `mobs` | `/mobs` | Creatures, monsters, NPCs |
|
||||||
|
| `pets` | `/pets` | Tameable companion creatures |
|
||||||
|
|
||||||
|
#### Locations & Places
|
||||||
|
|
||||||
|
| Entity Type | Endpoint | Description |
|
||||||
|
|-------------|----------|-------------|
|
||||||
|
| `locations` | `/locations` | Points of interest |
|
||||||
|
| `teleporters` | `/teleporters` | Teleporter locations |
|
||||||
|
| `shops` | `/shops` | Player shops |
|
||||||
|
| `vendors` | `/vendors` | NPC vendors |
|
||||||
|
| `planets` | `/planets` | Planet information |
|
||||||
|
| `areas` | `/areas` | Geographic regions |
|
||||||
|
|
||||||
|
#### Other
|
||||||
|
|
||||||
|
| Entity Type | Endpoint | Description |
|
||||||
|
|-------------|----------|-------------|
|
||||||
|
| `skills` | `/skills` | Player skills and professions |
|
||||||
|
| `vehicles` | `/vehicles` | Ships, cars, mounts |
|
||||||
|
| `decorations` | `/decorations` | Estate decorations |
|
||||||
|
| `furniture` | `/furniture` | Estate furniture |
|
||||||
|
| `storagecontainers` | `/storagecontainers` | Storage boxes |
|
||||||
|
| `strongboxes` | `/strongboxes` | Loot strongboxes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### Search Endpoints
|
||||||
|
|
||||||
|
#### Universal Search
|
||||||
|
```http
|
||||||
|
GET /search?q={query}&limit={limit}&fuzzy={true|false}
|
||||||
|
```
|
||||||
|
|
||||||
|
Search across all entity types simultaneously.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `q` (required): Search query string
|
||||||
|
- `limit` (optional): Maximum results (default: 20, max: 100)
|
||||||
|
- `fuzzy` (optional): Enable fuzzy matching (default: false)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "armatrix-lp-35",
|
||||||
|
"name": "ArMatrix LP-35 (L)",
|
||||||
|
"type": "Weapon",
|
||||||
|
"category": "Laser Weapons",
|
||||||
|
"icon_url": "https://..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Entity-Specific Search
|
||||||
|
```http
|
||||||
|
GET /{entity-type}?q={query}&limit={limit}&fuzzy={true|false}
|
||||||
|
```
|
||||||
|
|
||||||
|
Search within a specific entity type.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```http
|
||||||
|
GET /weapons?q=ArMatrix&limit=20&fuzzy=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Item Endpoints
|
||||||
|
|
||||||
|
#### Get Item Details
|
||||||
|
```http
|
||||||
|
GET /items/{item-id}
|
||||||
|
```
|
||||||
|
|
||||||
|
Retrieve detailed information about a specific item.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "armatrix-lp-35",
|
||||||
|
"name": "ArMatrix LP-35 (L)",
|
||||||
|
"description": "A powerful laser pistol...",
|
||||||
|
"category": "Laser Weapons",
|
||||||
|
"weight": 2.5,
|
||||||
|
"tt_value": 120.0,
|
||||||
|
"decay": 0.5,
|
||||||
|
"ammo_consumption": 10,
|
||||||
|
"damage": 45.0,
|
||||||
|
"range": 45.0,
|
||||||
|
"accuracy": 80.0,
|
||||||
|
"durability": 10000,
|
||||||
|
"requirements": {
|
||||||
|
"level": 25,
|
||||||
|
"profession": "Laser Sniper (Hit)"
|
||||||
|
},
|
||||||
|
"materials": [
|
||||||
|
{"name": "Lysterium Ingot", "amount": 50}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Market Data
|
||||||
|
```http
|
||||||
|
GET /items/{item-id}/market
|
||||||
|
```
|
||||||
|
|
||||||
|
Retrieve current market data for an item.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"item_id": "armatrix-lp-35",
|
||||||
|
"item_name": "ArMatrix LP-35 (L)",
|
||||||
|
"current_markup": 115.5,
|
||||||
|
"avg_markup_7d": 112.3,
|
||||||
|
"avg_markup_30d": 113.8,
|
||||||
|
"volume_24h": 150,
|
||||||
|
"volume_7d": 1200,
|
||||||
|
"buy_orders": [
|
||||||
|
{"price": 138.6, "quantity": 5}
|
||||||
|
],
|
||||||
|
"sell_orders": [
|
||||||
|
{"price": 145.2, "quantity": 10}
|
||||||
|
],
|
||||||
|
"last_updated": "2025-02-13T10:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entity Detail Endpoints
|
||||||
|
|
||||||
|
All entity types support individual retrieval:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /{entity-type}/{entity-id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```http
|
||||||
|
GET /mobs/atrox
|
||||||
|
GET /locations/fort-izzuk
|
||||||
|
GET /blueprints/armatrix-lp-35
|
||||||
|
GET /skills/rifle
|
||||||
|
```
|
||||||
|
|
||||||
|
### Market Data Endpoints (www.entropianexus.com)
|
||||||
|
|
||||||
|
**Note:** These endpoints use the `www.` subdomain, not `api.`
|
||||||
|
|
||||||
|
#### Exchange Listings
|
||||||
|
```http
|
||||||
|
GET https://www.entropianexus.com/api/market/exchange
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Weapons",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "item-id",
|
||||||
|
"name": "Item Name",
|
||||||
|
"type": "Weapon",
|
||||||
|
"buy": [{"price": 100.0, "quantity": 5}],
|
||||||
|
"sell": [{"price": 110.0, "quantity": 3}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Latest Prices
|
||||||
|
```http
|
||||||
|
GET https://www.entropianexus.com/api/market/prices/latest?items={comma-separated-ids}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `items`: Comma-separated list of item IDs (max 100)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Request Parameters
|
||||||
|
|
||||||
|
### Common Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `q` | string | - | Search query |
|
||||||
|
| `query` | string | - | Alternative search parameter |
|
||||||
|
| `limit` | integer | 20 | Maximum results (max: 100) |
|
||||||
|
| `fuzzy` | boolean | false | Enable fuzzy matching |
|
||||||
|
| `type` | string | - | Filter by entity type |
|
||||||
|
|
||||||
|
### Filter Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `category` | string | Filter by category |
|
||||||
|
| `min_level` | integer | Minimum level requirement |
|
||||||
|
| `max_level` | integer | Maximum level requirement |
|
||||||
|
| `planet` | string | Filter by planet name |
|
||||||
|
|
||||||
|
### Pagination
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `offset` | integer | Skip N results |
|
||||||
|
| `limit` | integer | Return N results |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Response Formats
|
||||||
|
|
||||||
|
### Standard Response Structure
|
||||||
|
|
||||||
|
All API responses follow a consistent structure:
|
||||||
|
|
||||||
|
#### Search Result Item
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "string", // Unique identifier
|
||||||
|
"name": "string", // Display name
|
||||||
|
"type": "string", // Entity type
|
||||||
|
"category": "string", // Category/classification
|
||||||
|
"icon_url": "string", // URL to icon image (optional)
|
||||||
|
// Type-specific fields below
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type-Specific Response Fields
|
||||||
|
|
||||||
|
#### Weapons
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "weapon-id",
|
||||||
|
"name": "Weapon Name",
|
||||||
|
"damage": 45.0, // Damage per shot
|
||||||
|
"range": 45.0, // Effective range in meters
|
||||||
|
"attacks": 45, // Attacks per minute
|
||||||
|
"ammo_consumption": 10, // Ammo per shot
|
||||||
|
"accuracy": 80.0, // Accuracy percentage
|
||||||
|
"decay": 0.5, // Decay per use (PED)
|
||||||
|
"durability": 10000 // Durability points
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Armors
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "armor-id",
|
||||||
|
"name": "Armor Name",
|
||||||
|
"protection": 25.0, // Protection value
|
||||||
|
"durability": 5000, // Durability points
|
||||||
|
"weight": 5.0 // Weight in kg
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mobs
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "mob-id",
|
||||||
|
"name": "Mob Name",
|
||||||
|
"hitpoints": 1000, // HP
|
||||||
|
"damage": 50.0, // Damage range
|
||||||
|
"threat": "Medium", // Threat level
|
||||||
|
"planet": "Calypso",
|
||||||
|
"area": "Eastern Land
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Locations
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "loc-id",
|
||||||
|
"name": "Location Name",
|
||||||
|
"planet": "Calypso",
|
||||||
|
"x": 12345.0, // X coordinate
|
||||||
|
"y": 67890.0, // Y coordinate
|
||||||
|
"type": "Outpost"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Blueprints
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "bp-id",
|
||||||
|
"name": "Blueprint Name",
|
||||||
|
"qr": 100.0, // Quality Rating max
|
||||||
|
"click": 1000, // Total clicks
|
||||||
|
"materials": [...], // Required materials
|
||||||
|
"product": {...} // Output item
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Skills
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "skill-id",
|
||||||
|
"name": "Skill Name",
|
||||||
|
"category": "Combat", // Skill category
|
||||||
|
"description": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### HTTP Status Codes
|
||||||
|
|
||||||
|
| Code | Meaning | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| 200 | OK | Request successful |
|
||||||
|
| 400 | Bad Request | Invalid parameters |
|
||||||
|
| 404 | Not Found | Entity not found |
|
||||||
|
| 429 | Too Many Requests | Rate limit exceeded |
|
||||||
|
| 500 | Server Error | Internal server error |
|
||||||
|
| 502 | Bad Gateway | Upstream error |
|
||||||
|
| 503 | Service Unavailable | Temporarily unavailable |
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Error type",
|
||||||
|
"message": "Human-readable description",
|
||||||
|
"code": 404
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exception Types (Python Client)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class NexusAPIError(Exception):
|
||||||
|
"""Base exception for Nexus API errors."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class RateLimitError(NexusAPIError):
|
||||||
|
"""Raised when rate limit is exceeded."""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core.nexus_api import get_nexus_api, NexusAPIError, RateLimitError
|
||||||
|
|
||||||
|
api = get_nexus_api()
|
||||||
|
|
||||||
|
try:
|
||||||
|
details = api.get_item_details("invalid-id")
|
||||||
|
except RateLimitError as e:
|
||||||
|
print(f"Rate limited: {e}")
|
||||||
|
# Wait and retry
|
||||||
|
except NexusAPIError as e:
|
||||||
|
print(f"API error: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected error: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
|
||||||
|
### Limits by Endpoint Type
|
||||||
|
|
||||||
|
| Endpoint Type | Limit | Window |
|
||||||
|
|---------------|-------|--------|
|
||||||
|
| General API | 5 requests | per second |
|
||||||
|
| Search | 10 requests | per minute |
|
||||||
|
| Market Data | 60 requests | per minute |
|
||||||
|
| Item Details | 30 requests | per minute |
|
||||||
|
|
||||||
|
### Rate Limit Headers
|
||||||
|
|
||||||
|
Responses include rate limit information:
|
||||||
|
|
||||||
|
```http
|
||||||
|
X-RateLimit-Limit: 60
|
||||||
|
X-RateLimit-Remaining: 45
|
||||||
|
X-RateLimit-Reset: 1707832800
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handling Rate Limits
|
||||||
|
|
||||||
|
```python
|
||||||
|
# The client automatically handles rate limiting
|
||||||
|
api = get_nexus_api()
|
||||||
|
|
||||||
|
# Built-in retry with exponential backoff
|
||||||
|
results = api.search_items("ArMatrix") # Auto-retries on 429
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic Search
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core.nexus_api import get_nexus_api
|
||||||
|
|
||||||
|
api = get_nexus_api()
|
||||||
|
|
||||||
|
# Search for items
|
||||||
|
items = api.search_items("ArMatrix", limit=20)
|
||||||
|
for item in items:
|
||||||
|
print(f"{item.name} ({item.type})")
|
||||||
|
|
||||||
|
# Search for mobs
|
||||||
|
mobs = api.search_mobs("Atrox")
|
||||||
|
for mob in mobs:
|
||||||
|
print(f"{mob.name} - HP: {mob.data.get('hitpoints')}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Item Details
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get detailed information
|
||||||
|
details = api.get_item_details("armatrix-lp-35")
|
||||||
|
if details:
|
||||||
|
print(f"Name: {details.name}")
|
||||||
|
print(f"TT Value: {details.tt_value} PED")
|
||||||
|
print(f"Damage: {details.damage}")
|
||||||
|
print(f"Range: {details.range}m")
|
||||||
|
print(f"Decay: {details.decay} PED/use")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Market Analysis
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get market data
|
||||||
|
market = api.get_market_data("armatrix-lp-35")
|
||||||
|
if market:
|
||||||
|
print(f"Current markup: {market.current_markup:.1f}%")
|
||||||
|
print(f"7-day average: {market.avg_markup_7d:.1f}%")
|
||||||
|
print(f"24h Volume: {market.volume_24h}")
|
||||||
|
|
||||||
|
# Check buy orders
|
||||||
|
for order in market.buy_orders[:5]:
|
||||||
|
print(f"Buy: {order['price']} PED x {order['quantity']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Operations
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get multiple items efficiently
|
||||||
|
item_ids = ["armatrix-lp-35", "armatrix-bp-25", "armatrix-sb-10"]
|
||||||
|
results = api.get_items_batch(item_ids)
|
||||||
|
|
||||||
|
for item_id, details in results.items():
|
||||||
|
if details:
|
||||||
|
print(f"{details.name}: {details.damage} dmg")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Universal Search (All Entity Types)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Search across all types
|
||||||
|
results = api.search_all("Calypso", limit=30)
|
||||||
|
for result in results:
|
||||||
|
print(f"{result.name} [{result.type}]")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plugin API Integration
|
||||||
|
|
||||||
|
### Accessing Nexus API from Plugins
|
||||||
|
|
||||||
|
Plugins access the Nexus API through the PluginAPI:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from plugins.base_plugin import BasePlugin
|
||||||
|
|
||||||
|
class MyPlugin(BasePlugin):
|
||||||
|
def search_item(self, query):
|
||||||
|
# Use the built-in nexus_search method
|
||||||
|
results = self.nexus_search(query, entity_type="items")
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_item_info(self, item_id):
|
||||||
|
# Get item details
|
||||||
|
details = self.nexus_get_item_details(item_id)
|
||||||
|
return details
|
||||||
|
|
||||||
|
def check_market(self, item_id):
|
||||||
|
# Get market data
|
||||||
|
market = self.nexus_get_market_data(item_id)
|
||||||
|
return market
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Plugin Methods
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `nexus_search(query, entity_type)` | Search for entities |
|
||||||
|
| `nexus_get_item_details(item_id)` | Get item details |
|
||||||
|
| `nexus_get_market_data(item_id)` | Get market data |
|
||||||
|
|
||||||
|
### Entity Types for Plugins
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Valid entity types for nexus_search()
|
||||||
|
entity_types = [
|
||||||
|
"items", "weapons", "armors", "blueprints", "mobs",
|
||||||
|
"locations", "skills", "materials", "enhancers",
|
||||||
|
"medicaltools", "finders", "excavators", "refiners",
|
||||||
|
"vehicles", "pets", "decorations", "furniture",
|
||||||
|
"storagecontainers", "strongboxes", "teleporters",
|
||||||
|
"shops", "vendors", "planets", "areas"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin Example: Weapon Finder
|
||||||
|
|
||||||
|
```python
|
||||||
|
from plugins.base_plugin import BasePlugin
|
||||||
|
|
||||||
|
class WeaponFinderPlugin(BasePlugin):
|
||||||
|
name = "Weapon Finder"
|
||||||
|
|
||||||
|
def find_weapons_by_damage(self, min_damage):
|
||||||
|
"""Find all weapons with minimum damage."""
|
||||||
|
# Search for weapons
|
||||||
|
results = self.nexus_search("", entity_type="weapons")
|
||||||
|
|
||||||
|
weapons = []
|
||||||
|
for weapon in results:
|
||||||
|
details = self.nexus_get_item_details(weapon.id)
|
||||||
|
if details and details.damage >= min_damage:
|
||||||
|
weapons.append(details)
|
||||||
|
|
||||||
|
return sorted(weapons, key=lambda w: w.damage, reverse=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Field Name Conventions
|
||||||
|
|
||||||
|
### Important: Dual Naming Conventions
|
||||||
|
|
||||||
|
The API may return field names in **either** format:
|
||||||
|
|
||||||
|
- **snake_case**: `item_id`, `tt_value`, `current_markup`
|
||||||
|
- **PascalCase**: `ItemId`, `TTValue`, `CurrentMarkup`
|
||||||
|
|
||||||
|
### Handling Both Formats
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Safe field access pattern
|
||||||
|
def get_field(data, *names, default=None):
|
||||||
|
"""Get field value trying multiple name variants."""
|
||||||
|
for name in names:
|
||||||
|
if name in data:
|
||||||
|
return data[name]
|
||||||
|
return default
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
name = get_field(item, 'name', 'Name')
|
||||||
|
tt_value = get_field(item, 'tt_value', 'TTValue', 'TtValue')
|
||||||
|
damage = get_field(item, 'damage', 'Damage')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Field Mappings
|
||||||
|
|
||||||
|
| Concept | snake_case | PascalCase |
|
||||||
|
|---------|------------|------------|
|
||||||
|
| ID | `id` | `Id` |
|
||||||
|
| Name | `name` | `Name` |
|
||||||
|
| Type | `type` | `Type` |
|
||||||
|
| Category | `category` | `Category` |
|
||||||
|
| TT Value | `tt_value` | `TTValue` |
|
||||||
|
| Damage | `damage` | `Damage` |
|
||||||
|
| Range | `range` | `Range` |
|
||||||
|
| Decay | `decay` | `Decay` |
|
||||||
|
| Weight | `weight` | `Weight` |
|
||||||
|
| Hitpoints | `hitpoints` | `Hitpoints` |
|
||||||
|
| Level | `level` | `Level` |
|
||||||
|
| Description | `description` | `Description` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Classes Reference
|
||||||
|
|
||||||
|
### SearchResult
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class SearchResult:
|
||||||
|
id: str # Entity ID
|
||||||
|
name: str # Display name
|
||||||
|
type: str # Entity type
|
||||||
|
category: str # Category (optional)
|
||||||
|
icon_url: str # Icon URL (optional)
|
||||||
|
data: dict # Raw response data
|
||||||
|
```
|
||||||
|
|
||||||
|
### ItemDetails
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class ItemDetails:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
category: str
|
||||||
|
weight: float
|
||||||
|
tt_value: float
|
||||||
|
decay: float
|
||||||
|
ammo_consumption: int
|
||||||
|
damage: float
|
||||||
|
range: float
|
||||||
|
accuracy: float
|
||||||
|
durability: int
|
||||||
|
requirements: dict
|
||||||
|
materials: list
|
||||||
|
raw_data: dict
|
||||||
|
```
|
||||||
|
|
||||||
|
### MarketData
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class MarketData:
|
||||||
|
item_id: str
|
||||||
|
item_name: str
|
||||||
|
current_markup: float
|
||||||
|
avg_markup_7d: float
|
||||||
|
avg_markup_30d: float
|
||||||
|
volume_24h: int
|
||||||
|
volume_7d: int
|
||||||
|
buy_orders: list
|
||||||
|
sell_orders: list
|
||||||
|
last_updated: datetime
|
||||||
|
raw_data: dict
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### v1.0 (2025-02-13)
|
||||||
|
- Initial complete API documentation
|
||||||
|
- Documented all 25+ entity types
|
||||||
|
- Added field naming convention notes
|
||||||
|
- Added Plugin API integration examples
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
|
||||||
|
- [NEXUS_LINKTREE.md](./NEXUS_LINKTREE.md) - URL reference
|
||||||
|
- [../core/nexus_api.py](../core/nexus_api.py) - API client implementation
|
||||||
|
- [../core/plugin_api.py](../core/plugin_api.py) - Plugin integration
|
||||||
|
- [../plugins/universal_search/plugin.py](../plugins/universal_search/plugin.py) - Usage example
|
||||||
|
|
@ -0,0 +1,305 @@
|
||||||
|
# Entropia Nexus API Documentation Summary
|
||||||
|
|
||||||
|
> Summary of completed Nexus API documentation and implementation for EU-Utility
|
||||||
|
>
|
||||||
|
> **Date:** 2025-02-13
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Created/Updated
|
||||||
|
|
||||||
|
### Documentation Files Created
|
||||||
|
|
||||||
|
| File | Description | Size |
|
||||||
|
|------|-------------|------|
|
||||||
|
| `docs/NEXUS_LINKTREE.md` | Complete URL reference for all Nexus endpoints | ~7.5 KB |
|
||||||
|
| `docs/NEXUS_API_REFERENCE.md` | Full technical API documentation | ~18 KB |
|
||||||
|
| `docs/NEXUS_USAGE_EXAMPLES.md` | Plugin developer examples | ~18 KB |
|
||||||
|
|
||||||
|
### Code Files Updated
|
||||||
|
|
||||||
|
| File | Changes |
|
||||||
|
|------|---------|
|
||||||
|
| `core/nexus_api.py` | Added all 25+ entity types, `search_by_type()`, `get_entity_details()` |
|
||||||
|
| `core/plugin_api.py` | Added `nexus_search()`, `nexus_get_item_details()`, `nexus_get_market_data()` |
|
||||||
|
| `plugins/base_plugin.py` | Added convenience methods for plugins to access Nexus API |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's Documented
|
||||||
|
|
||||||
|
### 1. LinkTree (NEXUS_LINKTREE.md)
|
||||||
|
|
||||||
|
Complete reference of all Nexus URLs:
|
||||||
|
|
||||||
|
- **Base URLs**: `api.entropianexus.com` vs `www.entropianexus.com`
|
||||||
|
- **API Endpoints**: All 25+ entity endpoints
|
||||||
|
- **Web Pages**: Browseable web interface URLs
|
||||||
|
- **Entity Type Mapping**: API endpoint → web path conversions
|
||||||
|
- **Query Parameters**: All available search/filter parameters
|
||||||
|
- **Rate Limits**: Request limits by endpoint type
|
||||||
|
- **Response Formats**: JSON structure examples
|
||||||
|
|
||||||
|
### 2. API Reference (NEXUS_API_REFERENCE.md)
|
||||||
|
|
||||||
|
Complete technical documentation:
|
||||||
|
|
||||||
|
- **Authentication**: Public API (no auth required)
|
||||||
|
- **Base Configuration**: Rate limits, retry settings, cache TTL
|
||||||
|
- **Entity Types**: All 25+ types documented with descriptions
|
||||||
|
- **Endpoints**:
|
||||||
|
- `/search` - Universal search
|
||||||
|
- `/{entity-type}` - Entity-specific search
|
||||||
|
- `/items/{id}` - Item details
|
||||||
|
- `/items/{id}/market` - Market data
|
||||||
|
- **Request Parameters**: Query options, filters, pagination
|
||||||
|
- **Response Formats**: Complete field documentation for each entity type
|
||||||
|
- **Error Handling**: HTTP codes, exceptions, error responses
|
||||||
|
- **Rate Limits**: Limits per endpoint type
|
||||||
|
- **Field Name Conventions**: Documentation of snake_case vs PascalCase issue
|
||||||
|
- **Data Classes**: Python dataclass reference
|
||||||
|
|
||||||
|
### 3. Usage Examples (NEXUS_USAGE_EXAMPLES.md)
|
||||||
|
|
||||||
|
Practical examples for plugin developers:
|
||||||
|
|
||||||
|
- **Basic Search**: Simple queries with limits
|
||||||
|
- **Entity-Specific Searches**: Weapons, mobs, blueprints, locations, skills
|
||||||
|
- **Item Details**: Full item analysis patterns
|
||||||
|
- **Market Data**: Price checking, monitoring, comparisons
|
||||||
|
- **Complete Plugin Examples**:
|
||||||
|
- Weapon Comparator (with DPP calculation)
|
||||||
|
- Mob Info lookup
|
||||||
|
- Price Checker
|
||||||
|
- **Advanced Patterns**: Caching, batch processing, error handling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Status
|
||||||
|
|
||||||
|
### Entity Types Supported
|
||||||
|
|
||||||
|
All 25+ entity types are now supported:
|
||||||
|
|
||||||
|
#### Equipment & Items
|
||||||
|
- ✅ `items` - General items
|
||||||
|
- ✅ `weapons` - Weapons
|
||||||
|
- ✅ `armors` - Armors
|
||||||
|
- ✅ `enhancers` - Enhancers
|
||||||
|
|
||||||
|
#### Tools & Professional
|
||||||
|
- ✅ `medicaltools` - Medical tools
|
||||||
|
- ✅ `finders` - Mining finders
|
||||||
|
- ✅ `excavators` - Excavators
|
||||||
|
- ✅ `refiners` - Refiners
|
||||||
|
|
||||||
|
#### Crafting & Materials
|
||||||
|
- ✅ `blueprints` - Blueprints
|
||||||
|
- ✅ `materials` - Materials
|
||||||
|
|
||||||
|
#### Creatures
|
||||||
|
- ✅ `mobs` - Creatures
|
||||||
|
- ✅ `pets` - Pets
|
||||||
|
|
||||||
|
#### Locations
|
||||||
|
- ✅ `locations` - Locations
|
||||||
|
- ✅ `teleporters` - Teleporters
|
||||||
|
- ✅ `shops` - Shops
|
||||||
|
- ✅ `vendors` - Vendors
|
||||||
|
- ✅ `planets` - Planets
|
||||||
|
- ✅ `areas` - Areas
|
||||||
|
|
||||||
|
#### Other
|
||||||
|
- ✅ `skills` - Skills
|
||||||
|
- ✅ `vehicles` - Vehicles
|
||||||
|
- ✅ `decorations` - Decorations
|
||||||
|
- ✅ `furniture` - Furniture
|
||||||
|
- ✅ `storagecontainers` - Storage
|
||||||
|
- ✅ `strongboxes` - Strongboxes
|
||||||
|
|
||||||
|
### API Methods Available to Plugins
|
||||||
|
|
||||||
|
Plugins can now access the Nexus API through:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In any plugin (extends BasePlugin)
|
||||||
|
class MyPlugin(BasePlugin):
|
||||||
|
def search(self):
|
||||||
|
# Search for any entity type
|
||||||
|
results = self.nexus_search("ArMatrix", entity_type="weapons")
|
||||||
|
|
||||||
|
# Get item details
|
||||||
|
details = self.nexus_get_item_details("armatrix_lp-35")
|
||||||
|
|
||||||
|
# Get market data
|
||||||
|
market = self.nexus_get_market_data("armatrix_lp-35")
|
||||||
|
|
||||||
|
# Check API availability
|
||||||
|
if self.nexus_is_available():
|
||||||
|
# Safe to make calls
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core API Methods (nexus_api.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core.nexus_api import get_nexus_api
|
||||||
|
|
||||||
|
api = get_nexus_api()
|
||||||
|
|
||||||
|
# Search methods
|
||||||
|
api.search_items(query, limit=20)
|
||||||
|
api.search_mobs(query, limit=20)
|
||||||
|
api.search_all(query, limit=20)
|
||||||
|
api.search_by_type(query, entity_type="weapons", limit=20)
|
||||||
|
|
||||||
|
# Detail methods
|
||||||
|
api.get_item_details(item_id)
|
||||||
|
api.get_entity_details(entity_id, entity_type="mobs")
|
||||||
|
api.get_market_data(item_id)
|
||||||
|
|
||||||
|
# Batch methods
|
||||||
|
api.get_items_batch([item_id1, item_id2])
|
||||||
|
api.get_market_batch([item_id1, item_id2])
|
||||||
|
|
||||||
|
# Utility
|
||||||
|
api.clear_cache()
|
||||||
|
api.is_available()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Known Field Name Conventions
|
||||||
|
|
||||||
|
**Important**: The API returns field names in inconsistent formats:
|
||||||
|
|
||||||
|
| Concept | May Appear As |
|
||||||
|
|---------|---------------|
|
||||||
|
| ID | `id` or `Id` |
|
||||||
|
| Name | `name` or `Name` |
|
||||||
|
| TT Value | `tt_value`, `TTValue`, `TtValue` |
|
||||||
|
| Damage | `damage` or `Damage` |
|
||||||
|
|
||||||
|
**Solution**: The implementation handles both formats:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Safe field access
|
||||||
|
name = item.get('name') or item.get('Name')
|
||||||
|
tt_value = item.get('tt_value') or item.get('TTValue')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
|
||||||
|
| Endpoint Type | Limit |
|
||||||
|
|---------------|-------|
|
||||||
|
| General API | 5 req/sec |
|
||||||
|
| Search | 10 req/min |
|
||||||
|
| Market Data | 60 req/min |
|
||||||
|
| Item Details | 30 req/min |
|
||||||
|
|
||||||
|
**Implementation**: The client automatically handles:
|
||||||
|
- Rate limiting (0.2s between requests)
|
||||||
|
- Retry with exponential backoff (max 3 retries)
|
||||||
|
- 5-minute default cache for responses
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
The implementation includes a test file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd projects/EU-Utility
|
||||||
|
python tests/test_nexus_api.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests verify:
|
||||||
|
- Singleton pattern
|
||||||
|
- PluginAPI integration
|
||||||
|
- All method availability
|
||||||
|
- Configuration values
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage for Plugin Developers
|
||||||
|
|
||||||
|
### Basic Plugin Template
|
||||||
|
|
||||||
|
```python
|
||||||
|
from plugins.base_plugin import BasePlugin
|
||||||
|
|
||||||
|
class MyPlugin(BasePlugin):
|
||||||
|
name = "My Plugin"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
def find_items(self, query):
|
||||||
|
# Search Nexus
|
||||||
|
results = self.nexus_search(query, entity_type="items")
|
||||||
|
return results
|
||||||
|
|
||||||
|
def analyze_item(self, item_id):
|
||||||
|
# Get full details
|
||||||
|
details = self.nexus_get_item_details(item_id)
|
||||||
|
market = self.nexus_get_market_data(item_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'item': details,
|
||||||
|
'market': market
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entity Type Quick Reference
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Valid entity_type values for nexus_search()
|
||||||
|
entity_types = [
|
||||||
|
"items", "weapons", "armors",
|
||||||
|
"mobs", "pets",
|
||||||
|
"blueprints", "materials",
|
||||||
|
"locations", "teleporters", "shops", "vendors", "planets", "areas",
|
||||||
|
"skills",
|
||||||
|
"enhancers", "medicaltools", "finders", "excavators", "refiners",
|
||||||
|
"vehicles", "decorations", "furniture",
|
||||||
|
"storagecontainers", "strongboxes"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
|
||||||
|
| Path | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `core/nexus_api.py` | Core API client implementation |
|
||||||
|
| `core/plugin_api.py` | Plugin API with Nexus integration |
|
||||||
|
| `plugins/base_plugin.py` | Base plugin with Nexus convenience methods |
|
||||||
|
| `plugins/universal_search/plugin.py` | Real-world usage example |
|
||||||
|
| `plugins/nexus_search/plugin.py` | Alternative API client example |
|
||||||
|
| `tests/test_nexus_api.py` | Integration tests |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Enhancements (Noted but Not Implemented)
|
||||||
|
|
||||||
|
These are documented but may require API support:
|
||||||
|
|
||||||
|
1. **Advanced Filtering**: `min_level`, `max_level`, `planet` parameters
|
||||||
|
2. **Pagination**: `offset` parameter for large result sets
|
||||||
|
3. **Bulk Endpoints**: Batch entity retrieval
|
||||||
|
4. **Real-time Data**: WebSocket support for live market data
|
||||||
|
5. **User Authentication**: If private endpoints become available
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
✅ **Complete documentation** for all Nexus endpoints
|
||||||
|
✅ **25+ entity types** fully supported
|
||||||
|
✅ **Plugin integration** via PluginAPI and BasePlugin
|
||||||
|
✅ **Usage examples** for common patterns
|
||||||
|
✅ **Error handling** and rate limiting implemented
|
||||||
|
✅ **Field name handling** for API inconsistencies
|
||||||
|
|
||||||
|
The implementation is ready for plugin developers to use the Entropia Nexus API.
|
||||||
|
|
@ -0,0 +1,258 @@
|
||||||
|
# Entropia Nexus LinkTree
|
||||||
|
|
||||||
|
> Complete reference of all Entropia Nexus URLs and endpoints
|
||||||
|
>
|
||||||
|
> **Last Updated:** 2025-02-13
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Base URLs
|
||||||
|
|
||||||
|
| URL | Purpose |
|
||||||
|
|-----|---------|
|
||||||
|
| `https://api.entropianexus.com` | **Primary API Endpoint** - REST API for programmatic access |
|
||||||
|
| `https://www.entropianexus.com` | **Web Interface** - Main website for browsing |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Endpoints (api.entropianexus.com)
|
||||||
|
|
||||||
|
### Search & Discovery
|
||||||
|
|
||||||
|
| Endpoint | Method | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `/search` | GET | Universal search across all entity types |
|
||||||
|
| `/search?q={query}&limit={n}` | GET | Search with query and result limit |
|
||||||
|
| `/search?fuzzy=true` | GET | Fuzzy matching search |
|
||||||
|
|
||||||
|
### Entity Endpoints
|
||||||
|
|
||||||
|
All entity types support: `GET /{entity-type}` and `GET /{entity-type}?q={query}`
|
||||||
|
|
||||||
|
| Endpoint | Entity Type | Description |
|
||||||
|
|----------|-------------|-------------|
|
||||||
|
| `/items` | Items | General items, tools, components |
|
||||||
|
| `/weapons` | Weapons | Guns, melee, mining tools |
|
||||||
|
| `/armors` | Armors | Protective gear sets |
|
||||||
|
| `/blueprints` | Blueprints | Crafting recipes |
|
||||||
|
| `/mobs` | Mobs | Creatures, NPCs |
|
||||||
|
| `/locations` | Locations | Points of interest |
|
||||||
|
| `/skills` | Skills | Player skills & professions |
|
||||||
|
| `/materials` | Materials | Ores, enmatters, components |
|
||||||
|
| `/enhancers` | Enhancers | Weapon/armor enhancers |
|
||||||
|
| `/medicaltools` | Medical Tools | Healing tools, FAPs |
|
||||||
|
| `/finders` | Finders | Mining finders/detectors |
|
||||||
|
| `/excavators` | Excavators | Mining excavators |
|
||||||
|
| `/refiners` | Refiners | Resource refiners |
|
||||||
|
| `/vehicles` | Vehicles | Ships, vehicles, mounts |
|
||||||
|
| `/pets` | Pets | Tameable creatures |
|
||||||
|
| `/decorations` | Decorations | Estate decorations |
|
||||||
|
| `/furniture` | Furniture | Estate furniture |
|
||||||
|
| `/storagecontainers` | Storage | Storage boxes, containers |
|
||||||
|
| `/strongboxes` | Strongboxes | Loot boxes |
|
||||||
|
| `/teleporters` | Teleporters | TP locations |
|
||||||
|
| `/shops` | Shops | Player shops |
|
||||||
|
| `/vendors` | Vendors | NPC vendors |
|
||||||
|
| `/planets` | Planets | Planet information |
|
||||||
|
| `/areas` | Areas | Geographic regions |
|
||||||
|
|
||||||
|
### Item-Specific Endpoints
|
||||||
|
|
||||||
|
| Endpoint | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `/items/{id}` | Get specific item details |
|
||||||
|
| `/items/{id}/market` | Get market data for item |
|
||||||
|
|
||||||
|
### Market Data (www.entropianexus.com)
|
||||||
|
|
||||||
|
| Endpoint | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `/api/market/exchange` | Exchange/auction listings |
|
||||||
|
| `/api/market/prices/latest?items={ids}` | Latest prices for items |
|
||||||
|
|
||||||
|
### User Data (www.entropianexus.com)
|
||||||
|
|
||||||
|
| Endpoint | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `/api/users/search?q={query}` | Search verified users |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Web Pages (www.entropianexus.com)
|
||||||
|
|
||||||
|
### Main Sections
|
||||||
|
|
||||||
|
| URL | Description |
|
||||||
|
|-----|-------------|
|
||||||
|
| `/` | Home page |
|
||||||
|
| `/market/exchange` | Exchange market browser |
|
||||||
|
| `/market/history` | Price history charts |
|
||||||
|
| `/items` | Item database browser |
|
||||||
|
| `/mobs` | Creature database |
|
||||||
|
| `/locations` | Location database |
|
||||||
|
| `/skills` | Skill information |
|
||||||
|
| `/blueprints` | Blueprint database |
|
||||||
|
| `/tools` | Utility tools |
|
||||||
|
| `/about` | About Nexus |
|
||||||
|
|
||||||
|
### Entity Detail Pages
|
||||||
|
|
||||||
|
| URL Pattern | Description |
|
||||||
|
|-------------|-------------|
|
||||||
|
| `/items/{item-id}` | Item detail page |
|
||||||
|
| `/mobs/{mob-id}` | Mob detail page |
|
||||||
|
| `/locations/{loc-id}` | Location detail page |
|
||||||
|
| `/skills/{skill-id}` | Skill detail page |
|
||||||
|
| `/blueprints/{bp-id}` | Blueprint detail page |
|
||||||
|
| `/users/{user-id}` | User profile page |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resources & Assets
|
||||||
|
|
||||||
|
### Image Assets
|
||||||
|
|
||||||
|
| Pattern | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/icons/{item-id}.png` | Item icons |
|
||||||
|
| `/images/mobs/{mob-id}.png` | Mob images |
|
||||||
|
| `/images/locations/{loc-id}.png` | Location images |
|
||||||
|
|
||||||
|
### Data Downloads
|
||||||
|
|
||||||
|
| URL | Description |
|
||||||
|
|-----|-------------|
|
||||||
|
| `/api/export/items` | Full item database export |
|
||||||
|
| `/api/export/blueprints` | Full blueprint export |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entity Type Mapping
|
||||||
|
|
||||||
|
### API Endpoint → Web Path Mapping
|
||||||
|
|
||||||
|
When converting API entities to web URLs:
|
||||||
|
|
||||||
|
| API Entity Type | Web Path | Notes |
|
||||||
|
|-----------------|----------|-------|
|
||||||
|
| Items | `/items/{id}` | All item subtypes use `/items/` |
|
||||||
|
| Weapons | `/items/{id}` | Weapons are items |
|
||||||
|
| Armors | `/items/{id}` | Armors are items |
|
||||||
|
| Blueprints | `/blueprints/{id}` | Separate section |
|
||||||
|
| Mobs | `/mobs/{id}` | Separate section |
|
||||||
|
| Locations | `/locations/{id}` | Includes TPs, shops |
|
||||||
|
| Skills | `/skills/{id}` | Separate section |
|
||||||
|
| Materials | `/items/{id}` | Materials are items |
|
||||||
|
| Enhancers | `/items/{id}` | Enhancers are items |
|
||||||
|
| Medical Tools | `/items/{id}` | Medical tools are items |
|
||||||
|
| Finders | `/items/{id}` | Finders are items |
|
||||||
|
| Excavators | `/items/{id}` | Excavators are items |
|
||||||
|
| Refiners | `/items/{id}` | Refiners are items |
|
||||||
|
| Vehicles | `/items/{id}` | Vehicles are items |
|
||||||
|
| Pets | `/items/{id}` | Pets are items |
|
||||||
|
| Decorations | `/items/{id}` | Decorations are items |
|
||||||
|
| Furniture | `/items/{id}` | Furniture are items |
|
||||||
|
| Storage | `/items/{id}` | Storage are items |
|
||||||
|
| Strongboxes | `/items/{id}` | Strongboxes are items |
|
||||||
|
| Teleporters | `/locations/{id}` | TPs are locations |
|
||||||
|
| Shops | `/locations/{id}` | Shops are locations |
|
||||||
|
| Vendors | `/locations/{id}` | Vendors are locations |
|
||||||
|
| Planets | `/locations/{id}` | Planets are locations |
|
||||||
|
| Areas | `/locations/{id}` | Areas are locations |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Query Parameters Reference
|
||||||
|
|
||||||
|
### Common Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Description | Example |
|
||||||
|
|-----------|------|-------------|---------|
|
||||||
|
| `q` | string | Search query | `q=ArMatrix` |
|
||||||
|
| `query` | string | Alternative search parameter | `query=ArMatrix` |
|
||||||
|
| `limit` | integer | Max results (default: 20, max: 100) | `limit=50` |
|
||||||
|
| `fuzzy` | boolean | Enable fuzzy matching | `fuzzy=true` |
|
||||||
|
| `type` | string | Filter by entity type | `type=item` |
|
||||||
|
|
||||||
|
### Advanced Parameters
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `category` | Filter by item category |
|
||||||
|
| `min_level` | Minimum item level |
|
||||||
|
| `max_level` | Maximum item level |
|
||||||
|
| `planet` | Filter by planet |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
|
||||||
|
| Endpoint | Limit |
|
||||||
|
|----------|-------|
|
||||||
|
| General API | 5 requests/second |
|
||||||
|
| Search | 10 requests/minute |
|
||||||
|
| Market Data | 60 requests/minute |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Response Formats
|
||||||
|
|
||||||
|
### Standard Search Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "item-id",
|
||||||
|
"name": "Item Name",
|
||||||
|
"type": "item",
|
||||||
|
"category": "Weapon",
|
||||||
|
"icon_url": "https://..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standard Item Detail Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "item-id",
|
||||||
|
"name": "Item Name",
|
||||||
|
"description": "Item description...",
|
||||||
|
"category": "Weapon",
|
||||||
|
"weight": 2.5,
|
||||||
|
"tt_value": 120.0,
|
||||||
|
"decay": 0.5,
|
||||||
|
"damage": 45.0,
|
||||||
|
"range": 45.0,
|
||||||
|
"accuracy": 80.0,
|
||||||
|
"durability": 10000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [NEXUS_API_REFERENCE.md](./NEXUS_API_REFERENCE.md) - Complete API documentation
|
||||||
|
- [Plugin API Integration](../core/plugin_api.py) - How plugins access Nexus API
|
||||||
|
- [Nexus API Client](../core/nexus_api.py) - Core API client implementation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## External Links
|
||||||
|
|
||||||
|
- **Main Website:** https://www.entropianexus.com
|
||||||
|
- **API Base:** https://api.entropianexus.com
|
||||||
|
- **Discord:** Check Nexus website for community links
|
||||||
|
- **Forums:** Entropia Universe official forums
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- API and web URLs are separate domains (`api.` vs `www.`)
|
||||||
|
- Some endpoints return different field name formats:
|
||||||
|
- API may return `name` or `Name` (camelCase vs PascalCase)
|
||||||
|
- Always check both when parsing responses
|
||||||
|
- Market data endpoints are on `www.` subdomain
|
||||||
|
- Search endpoints support fuzzy matching for typo tolerance
|
||||||
|
|
@ -0,0 +1,648 @@
|
||||||
|
# Nexus API Usage Examples for Plugin Developers
|
||||||
|
|
||||||
|
> Practical examples for using the Entropia Nexus API in plugins
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Basic Search Examples](#basic-search-examples)
|
||||||
|
2. [Entity Type-Specific Searches](#entity-type-specific-searches)
|
||||||
|
3. [Item Details & Market Data](#item-details--market-data)
|
||||||
|
4. [Complete Plugin Examples](#complete-plugin-examples)
|
||||||
|
5. [Advanced Usage Patterns](#advanced-usage-patterns)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Basic Search Examples
|
||||||
|
|
||||||
|
### Simple Item Search
|
||||||
|
|
||||||
|
```python
|
||||||
|
from plugins.base_plugin import BasePlugin
|
||||||
|
|
||||||
|
class MyPlugin(BasePlugin):
|
||||||
|
def find_weapons(self):
|
||||||
|
# Search for items containing "ArMatrix"
|
||||||
|
results = self.nexus_search("ArMatrix", entity_type="items")
|
||||||
|
|
||||||
|
for item in results:
|
||||||
|
print(f"Found: {item['name']} ({item['type']})")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search with Limit
|
||||||
|
|
||||||
|
```python
|
||||||
|
def find_top_results(self, query):
|
||||||
|
# Get up to 50 results
|
||||||
|
results = self.nexus_search(
|
||||||
|
query,
|
||||||
|
entity_type="items",
|
||||||
|
limit=50
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entity Type-Specific Searches
|
||||||
|
|
||||||
|
### Weapons
|
||||||
|
|
||||||
|
```python
|
||||||
|
def find_laser_weapons(self):
|
||||||
|
"""Search for laser weapons."""
|
||||||
|
results = self.nexus_search("laser", entity_type="weapons")
|
||||||
|
|
||||||
|
weapons = []
|
||||||
|
for item in results:
|
||||||
|
details = self.nexus_get_item_details(item['id'])
|
||||||
|
if details:
|
||||||
|
weapons.append({
|
||||||
|
'name': details['name'],
|
||||||
|
'damage': details.get('damage', 0),
|
||||||
|
'range': details.get('range', 0),
|
||||||
|
'decay': details.get('decay', 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
return weapons
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mobs/Creatures
|
||||||
|
|
||||||
|
```python
|
||||||
|
def find_mobs_by_name(self, name):
|
||||||
|
"""Search for creatures."""
|
||||||
|
results = self.nexus_search(name, entity_type="mobs")
|
||||||
|
|
||||||
|
mobs = []
|
||||||
|
for mob in results:
|
||||||
|
mobs.append({
|
||||||
|
'name': mob['name'],
|
||||||
|
'id': mob['id'],
|
||||||
|
'hitpoints': mob['data'].get('hitpoints', 'Unknown'),
|
||||||
|
'threat': mob['data'].get('threat', 'Unknown')
|
||||||
|
})
|
||||||
|
|
||||||
|
return mobs
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
drakabas = self.find_mobs_by_name("Drakaba")
|
||||||
|
atrox = self.find_mobs_by_name("Atrox")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Blueprints
|
||||||
|
|
||||||
|
```python
|
||||||
|
def find_crafting_blueprints(self, material_name):
|
||||||
|
"""Find blueprints that use a specific material."""
|
||||||
|
results = self.nexus_search(material_name, entity_type="blueprints")
|
||||||
|
|
||||||
|
blueprints = []
|
||||||
|
for bp in results:
|
||||||
|
details = self.nexus_get_item_details(bp['id'])
|
||||||
|
if details and 'materials' in details:
|
||||||
|
blueprints.append({
|
||||||
|
'name': details['name'],
|
||||||
|
'materials': details['materials'],
|
||||||
|
'qr': details.get('qr', 1.0)
|
||||||
|
})
|
||||||
|
|
||||||
|
return blueprints
|
||||||
|
```
|
||||||
|
|
||||||
|
### Locations
|
||||||
|
|
||||||
|
```python
|
||||||
|
def find_teleporters(self, planet="Calypso"):
|
||||||
|
"""Find teleporters on a specific planet."""
|
||||||
|
results = self.nexus_search(planet, entity_type="teleporters")
|
||||||
|
|
||||||
|
teleporters = []
|
||||||
|
for tp in results:
|
||||||
|
data = tp['data']
|
||||||
|
teleporters.append({
|
||||||
|
'name': tp['name'],
|
||||||
|
'x': data.get('x'),
|
||||||
|
'y': data.get('y'),
|
||||||
|
'planet': data.get('planet', planet)
|
||||||
|
})
|
||||||
|
|
||||||
|
return teleporters
|
||||||
|
```
|
||||||
|
|
||||||
|
### Skills
|
||||||
|
|
||||||
|
```python
|
||||||
|
def find_combat_skills(self):
|
||||||
|
"""Search for combat-related skills."""
|
||||||
|
results = self.nexus_search("", entity_type="skills")
|
||||||
|
|
||||||
|
combat_skills = [
|
||||||
|
s for s in results
|
||||||
|
if 'combat' in s.get('category', '').lower()
|
||||||
|
]
|
||||||
|
|
||||||
|
return combat_skills
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Item Details & Market Data
|
||||||
|
|
||||||
|
### Get Full Item Information
|
||||||
|
|
||||||
|
```python
|
||||||
|
def analyze_item(self, item_id):
|
||||||
|
"""Get complete item analysis."""
|
||||||
|
# Get basic details
|
||||||
|
details = self.nexus_get_item_details(item_id)
|
||||||
|
if not details:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get market data
|
||||||
|
market = self.nexus_get_market_data(item_id)
|
||||||
|
|
||||||
|
analysis = {
|
||||||
|
'name': details['name'],
|
||||||
|
'category': details.get('category', 'Unknown'),
|
||||||
|
'tt_value': details.get('tt_value', 0),
|
||||||
|
'weight': details.get('weight', 0),
|
||||||
|
'decay': details.get('decay', 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add weapon stats if applicable
|
||||||
|
if 'damage' in details:
|
||||||
|
analysis['weapon_stats'] = {
|
||||||
|
'damage': details['damage'],
|
||||||
|
'range': details.get('range', 0),
|
||||||
|
'accuracy': details.get('accuracy', 0),
|
||||||
|
'ammo': details.get('ammo_consumption', 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add market info
|
||||||
|
if market:
|
||||||
|
analysis['market'] = {
|
||||||
|
'markup': market.get('current_markup', 0),
|
||||||
|
'volume_24h': market.get('volume_24h', 0),
|
||||||
|
'buy_orders': len(market.get('buy_orders', [])),
|
||||||
|
'sell_orders': len(market.get('sell_orders', []))
|
||||||
|
}
|
||||||
|
|
||||||
|
return analysis
|
||||||
|
```
|
||||||
|
|
||||||
|
### Market Price Monitoring
|
||||||
|
|
||||||
|
```python
|
||||||
|
def check_price_drops(self, watchlist):
|
||||||
|
"""Monitor items for price drops.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
watchlist: List of {'item_id': str, 'max_price': float}
|
||||||
|
"""
|
||||||
|
deals = []
|
||||||
|
|
||||||
|
for watch in watchlist:
|
||||||
|
market = self.nexus_get_market_data(watch['item_id'])
|
||||||
|
if not market:
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_price = market.get('current_markup', 0)
|
||||||
|
|
||||||
|
if current_price <= watch['max_price']:
|
||||||
|
deals.append({
|
||||||
|
'item_id': watch['item_id'],
|
||||||
|
'item_name': market.get('item_name', 'Unknown'),
|
||||||
|
'current_price': current_price,
|
||||||
|
'target_price': watch['max_price'],
|
||||||
|
'savings': watch['max_price'] - current_price
|
||||||
|
})
|
||||||
|
|
||||||
|
return deals
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compare Item Stats
|
||||||
|
|
||||||
|
```python
|
||||||
|
def compare_weapons(self, weapon_ids):
|
||||||
|
"""Compare multiple weapons side by side."""
|
||||||
|
weapons = []
|
||||||
|
|
||||||
|
for wid in weapon_ids:
|
||||||
|
details = self.nexus_get_item_details(wid)
|
||||||
|
if details:
|
||||||
|
dpp = self.calculate_dpp(
|
||||||
|
details.get('damage', 0),
|
||||||
|
details.get('ammo_consumption', 0),
|
||||||
|
details.get('decay', 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
weapons.append({
|
||||||
|
'name': details['name'],
|
||||||
|
'damage': details.get('damage', 0),
|
||||||
|
'range': details.get('range', 0),
|
||||||
|
'decay': details.get('decay', 0),
|
||||||
|
'dpp': dpp,
|
||||||
|
'tt': details.get('tt_value', 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort by DPP (damage per pec)
|
||||||
|
return sorted(weapons, key=lambda w: w['dpp'], reverse=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Plugin Examples
|
||||||
|
|
||||||
|
### Weapon Comparison Plugin
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
Weapon Comparison Plugin
|
||||||
|
|
||||||
|
Compares weapons by DPP (Damage Per PEC) and other stats.
|
||||||
|
"""
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
|
QPushButton, QLineEdit, QTableWidget,
|
||||||
|
QTableWidgetItem, QLabel
|
||||||
|
)
|
||||||
|
from plugins.base_plugin import BasePlugin
|
||||||
|
|
||||||
|
|
||||||
|
class WeaponComparatorPlugin(BasePlugin):
|
||||||
|
name = "Weapon Comparator"
|
||||||
|
version = "1.0.0"
|
||||||
|
author = "EU-Utility"
|
||||||
|
description = "Compare weapons by DPP and stats"
|
||||||
|
hotkey = "ctrl+shift+w"
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self.weapon_ids = []
|
||||||
|
|
||||||
|
def get_ui(self):
|
||||||
|
widget = QWidget()
|
||||||
|
layout = QVBoxLayout(widget)
|
||||||
|
|
||||||
|
# Search box
|
||||||
|
search_layout = QHBoxLayout()
|
||||||
|
self.search_input = QLineEdit()
|
||||||
|
self.search_input.setPlaceholderText("Search weapons...")
|
||||||
|
search_layout.addWidget(self.search_input)
|
||||||
|
|
||||||
|
add_btn = QPushButton("Add to Compare")
|
||||||
|
add_btn.clicked.connect(self.add_weapon)
|
||||||
|
search_layout.addWidget(add_btn)
|
||||||
|
|
||||||
|
layout.addLayout(search_layout)
|
||||||
|
|
||||||
|
# Results table
|
||||||
|
self.table = QTableWidget()
|
||||||
|
self.table.setColumnCount(6)
|
||||||
|
self.table.setHorizontalHeaderLabels([
|
||||||
|
"Name", "Damage", "Range", "Decay", "DPP", "TT Value"
|
||||||
|
])
|
||||||
|
layout.addWidget(self.table)
|
||||||
|
|
||||||
|
# Compare button
|
||||||
|
compare_btn = QPushButton("Compare")
|
||||||
|
compare_btn.clicked.connect(self.do_compare)
|
||||||
|
layout.addWidget(compare_btn)
|
||||||
|
|
||||||
|
return widget
|
||||||
|
|
||||||
|
def add_weapon(self):
|
||||||
|
query = self.search_input.text()
|
||||||
|
results = self.nexus_search(query, entity_type="weapons", limit=5)
|
||||||
|
|
||||||
|
if results:
|
||||||
|
# Add first result
|
||||||
|
self.weapon_ids.append(results[0]['id'])
|
||||||
|
self.search_input.clear()
|
||||||
|
self.search_input.setPlaceholderText(
|
||||||
|
f"Added {results[0]['name']} ({len(self.weapon_ids)} total)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_compare(self):
|
||||||
|
if len(self.weapon_ids) < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fetch and compare
|
||||||
|
weapons = []
|
||||||
|
for wid in self.weapon_ids:
|
||||||
|
details = self.nexus_get_item_details(wid)
|
||||||
|
if details:
|
||||||
|
dpp = self.calculate_dpp(
|
||||||
|
details.get('damage', 0),
|
||||||
|
details.get('ammo_consumption', 0),
|
||||||
|
details.get('decay', 0)
|
||||||
|
)
|
||||||
|
weapons.append({
|
||||||
|
'name': details['name'],
|
||||||
|
'damage': details.get('damage', 0),
|
||||||
|
'range': details.get('range', 0),
|
||||||
|
'decay': details.get('decay', 0),
|
||||||
|
'dpp': dpp,
|
||||||
|
'tt': details.get('tt_value', 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort by DPP
|
||||||
|
weapons.sort(key=lambda w: w['dpp'], reverse=True)
|
||||||
|
|
||||||
|
# Display
|
||||||
|
self.table.setRowCount(len(weapons))
|
||||||
|
for i, w in enumerate(weapons):
|
||||||
|
self.table.setItem(i, 0, QTableWidgetItem(w['name']))
|
||||||
|
self.table.setItem(i, 1, QTableWidgetItem(f"{w['damage']:.1f}"))
|
||||||
|
self.table.setItem(i, 2, QTableWidgetItem(f"{w['range']:.0f}m"))
|
||||||
|
self.table.setItem(i, 3, QTableWidgetItem(f"{w['decay']:.2f}"))
|
||||||
|
self.table.setItem(i, 4, QTableWidgetItem(f"{w['dpp']:.2f}"))
|
||||||
|
self.table.setItem(i, 5, QTableWidgetItem(f"{w['tt']:.2f} PED"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mob Info Plugin
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
Mob Information Plugin
|
||||||
|
|
||||||
|
Quick lookup for creature stats and locations.
|
||||||
|
"""
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
|
QLineEdit, QPushButton, QLabel
|
||||||
|
)
|
||||||
|
from plugins.base_plugin import BasePlugin
|
||||||
|
|
||||||
|
|
||||||
|
class MobInfoPlugin(BasePlugin):
|
||||||
|
name = "Mob Info"
|
||||||
|
version = "1.0.0"
|
||||||
|
author = "EU-Utility"
|
||||||
|
description = "Quick mob stats lookup"
|
||||||
|
hotkey = "ctrl+shift+m"
|
||||||
|
|
||||||
|
def get_ui(self):
|
||||||
|
widget = QWidget()
|
||||||
|
layout = QVBoxLayout(widget)
|
||||||
|
|
||||||
|
# Search
|
||||||
|
search_layout = QHBoxLayout()
|
||||||
|
self.search_input = QLineEdit()
|
||||||
|
self.search_input.setPlaceholderText("Enter mob name...")
|
||||||
|
self.search_input.returnPressed.connect(self.search_mob)
|
||||||
|
search_layout.addWidget(self.search_input)
|
||||||
|
|
||||||
|
search_btn = QPushButton("Search")
|
||||||
|
search_btn.clicked.connect(self.search_mob)
|
||||||
|
search_layout.addWidget(search_btn)
|
||||||
|
|
||||||
|
layout.addLayout(search_layout)
|
||||||
|
|
||||||
|
# Results
|
||||||
|
self.name_label = QLabel("Name: ")
|
||||||
|
self.hp_label = QLabel("HP: ")
|
||||||
|
self.damage_label = QLabel("Damage: ")
|
||||||
|
self.threat_label = QLabel("Threat: ")
|
||||||
|
self.planet_label = QLabel("Planet: ")
|
||||||
|
|
||||||
|
layout.addWidget(self.name_label)
|
||||||
|
layout.addWidget(self.hp_label)
|
||||||
|
layout.addWidget(self.damage_label)
|
||||||
|
layout.addWidget(self.threat_label)
|
||||||
|
layout.addWidget(self.planet_label)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
return widget
|
||||||
|
|
||||||
|
def search_mob(self):
|
||||||
|
query = self.search_input.text()
|
||||||
|
if not query:
|
||||||
|
return
|
||||||
|
|
||||||
|
results = self.nexus_search(query, entity_type="mobs", limit=1)
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
self.name_label.setText("Mob not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
mob = results[0]
|
||||||
|
data = mob.get('data', {})
|
||||||
|
|
||||||
|
self.name_label.setText(f"Name: {mob['name']}")
|
||||||
|
self.hp_label.setText(f"HP: {data.get('hitpoints', 'Unknown')}")
|
||||||
|
self.damage_label.setText(f"Damage: {data.get('damage', 'Unknown')}")
|
||||||
|
self.threat_label.setText(f"Threat: {data.get('threat', 'Unknown')}")
|
||||||
|
self.planet_label.setText(f"Planet: {data.get('planet', 'Unknown')}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Price Checker Plugin
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
Price Checker Plugin
|
||||||
|
|
||||||
|
Quick market price lookup for items.
|
||||||
|
"""
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
|
QLineEdit, QPushButton, QLabel, QFrame
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
from plugins.base_plugin import BasePlugin
|
||||||
|
|
||||||
|
|
||||||
|
class PriceCheckerPlugin(BasePlugin):
|
||||||
|
name = "Price Checker"
|
||||||
|
version = "1.0.0"
|
||||||
|
author = "EU-Utility"
|
||||||
|
description = "Quick market price lookup"
|
||||||
|
hotkey = "ctrl+shift+p"
|
||||||
|
|
||||||
|
def get_ui(self):
|
||||||
|
widget = QWidget()
|
||||||
|
layout = QVBoxLayout(widget)
|
||||||
|
|
||||||
|
# Search
|
||||||
|
search_layout = QHBoxLayout()
|
||||||
|
self.search_input = QLineEdit()
|
||||||
|
self.search_input.setPlaceholderText("Enter item name...")
|
||||||
|
self.search_input.returnPressed.connect(self.check_price)
|
||||||
|
search_layout.addWidget(self.search_input)
|
||||||
|
|
||||||
|
check_btn = QPushButton("Check Price")
|
||||||
|
check_btn.clicked.connect(self.check_price)
|
||||||
|
search_layout.addWidget(check_btn)
|
||||||
|
|
||||||
|
layout.addLayout(search_layout)
|
||||||
|
|
||||||
|
# Results frame
|
||||||
|
self.results_frame = QFrame()
|
||||||
|
results_layout = QVBoxLayout(self.results_frame)
|
||||||
|
|
||||||
|
self.item_name = QLabel("Item: ")
|
||||||
|
self.item_name.setStyleSheet("font-size: 14px; font-weight: bold;")
|
||||||
|
results_layout.addWidget(self.item_name)
|
||||||
|
|
||||||
|
self.markup_label = QLabel("Current Markup: ")
|
||||||
|
results_layout.addWidget(self.markup_label)
|
||||||
|
|
||||||
|
self.volume_label = QLabel("24h Volume: ")
|
||||||
|
results_layout.addWidget(self.volume_label)
|
||||||
|
|
||||||
|
self.range_label = QLabel("Price Range: ")
|
||||||
|
results_layout.addWidget(self.range_label)
|
||||||
|
|
||||||
|
layout.addWidget(self.results_frame)
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
return widget
|
||||||
|
|
||||||
|
def check_price(self):
|
||||||
|
query = self.search_input.text()
|
||||||
|
if not query:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Search for item
|
||||||
|
results = self.nexus_search(query, entity_type="items", limit=1)
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
self.item_name.setText("Item not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
item = results[0]
|
||||||
|
self.item_name.setText(f"Item: {item['name']}")
|
||||||
|
|
||||||
|
# Get market data
|
||||||
|
market = self.nexus_get_market_data(item['id'])
|
||||||
|
|
||||||
|
if not market:
|
||||||
|
self.markup_label.setText("No market data available")
|
||||||
|
return
|
||||||
|
|
||||||
|
markup = market.get('current_markup', 0)
|
||||||
|
avg_7d = market.get('avg_markup_7d', 0)
|
||||||
|
volume = market.get('volume_24h', 0)
|
||||||
|
|
||||||
|
self.markup_label.setText(f"Current Markup: {markup:.1f}%")
|
||||||
|
self.volume_label.setText(f"24h Volume: {volume}")
|
||||||
|
|
||||||
|
# Calculate range from orders
|
||||||
|
buy_orders = market.get('buy_orders', [])
|
||||||
|
sell_orders = market.get('sell_orders', [])
|
||||||
|
|
||||||
|
if buy_orders and sell_orders:
|
||||||
|
highest_buy = max(o['price'] for o in buy_orders)
|
||||||
|
lowest_sell = min(o['price'] for o in sell_orders)
|
||||||
|
self.range_label.setText(
|
||||||
|
f"Price Range: {highest_buy:.2f} - {lowest_sell:.2f} PED"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Usage Patterns
|
||||||
|
|
||||||
|
### Caching Results
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CachedSearchPlugin(BasePlugin):
|
||||||
|
def initialize(self):
|
||||||
|
self._cache = {}
|
||||||
|
self._cache_ttl = 300 # 5 minutes
|
||||||
|
|
||||||
|
def cached_search(self, query, entity_type="items"):
|
||||||
|
"""Search with local caching."""
|
||||||
|
cache_key = f"{entity_type}:{query}"
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
# Check cache
|
||||||
|
if cache_key in self._cache:
|
||||||
|
result, timestamp = self._cache[cache_key]
|
||||||
|
if now - timestamp < self._cache_ttl:
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Fetch fresh
|
||||||
|
results = self.nexus_search(query, entity_type)
|
||||||
|
self._cache[cache_key] = (results, now)
|
||||||
|
|
||||||
|
return results
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Processing
|
||||||
|
|
||||||
|
```python
|
||||||
|
def analyze_multiple_items(self, item_names):
|
||||||
|
"""Process multiple items efficiently."""
|
||||||
|
analyses = []
|
||||||
|
|
||||||
|
for name in item_names:
|
||||||
|
# Search
|
||||||
|
results = self.nexus_search(name, entity_type="items", limit=1)
|
||||||
|
if not results:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get details
|
||||||
|
details = self.nexus_get_item_details(results[0]['id'])
|
||||||
|
if not details:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get market
|
||||||
|
market = self.nexus_get_market_data(results[0]['id'])
|
||||||
|
|
||||||
|
analyses.append({
|
||||||
|
'search_name': name,
|
||||||
|
'found_name': details['name'],
|
||||||
|
'tt_value': details.get('tt_value', 0),
|
||||||
|
'markup': market.get('current_markup', 0) if market else 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return analyses
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
```python
|
||||||
|
def safe_search(self, query, entity_type="items"):
|
||||||
|
"""Search with error handling."""
|
||||||
|
try:
|
||||||
|
if not self.nexus_is_available():
|
||||||
|
print("Nexus API not available")
|
||||||
|
return []
|
||||||
|
|
||||||
|
results = self.nexus_search(query, entity_type)
|
||||||
|
return results
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Search error: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def safe_get_details(self, item_id):
|
||||||
|
"""Get details with fallback."""
|
||||||
|
try:
|
||||||
|
details = self.nexus_get_item_details(item_id)
|
||||||
|
return details or {'name': 'Unknown', 'id': item_id}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Details error: {e}")
|
||||||
|
return {'name': 'Error', 'id': item_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [NEXUS_API_REFERENCE.md](./NEXUS_API_REFERENCE.md) - Complete API documentation
|
||||||
|
- [NEXUS_LINKTREE.md](./NEXUS_LINKTREE.md) - URL and endpoint reference
|
||||||
|
- [Plugin Base Class](../plugins/base_plugin.py) - Available plugin methods
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tips & Best Practices
|
||||||
|
|
||||||
|
1. **Always check for None**: `nexus_get_item_details()` and `nexus_get_market_data()` can return None
|
||||||
|
2. **Use try/except**: Wrap API calls to handle network errors gracefully
|
||||||
|
3. **Cache results**: For frequently accessed data, implement local caching
|
||||||
|
4. **Respect rate limits**: Don't make too many requests in rapid succession
|
||||||
|
5. **Check availability**: Use `nexus_is_available()` before making calls
|
||||||
|
6. **Handle both field formats**: API returns both `name` and `Name` - check both
|
||||||
|
|
@ -683,3 +683,101 @@ class BasePlugin(ABC):
|
||||||
connected = self.api.connect_task_signal('cancelled', on_cancelled) or connected
|
connected = self.api.connect_task_signal('cancelled', on_cancelled) or connected
|
||||||
|
|
||||||
return connected
|
return connected
|
||||||
|
|
||||||
|
# ========== Nexus API Methods ==========
|
||||||
|
|
||||||
|
def nexus_search(self, query: str, entity_type: str = "items", limit: int = 20) -> List[Dict[str, Any]]:
|
||||||
|
"""Search for entities via Entropia Nexus API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Search query string
|
||||||
|
entity_type: Type of entity to search. Valid types:
|
||||||
|
- items, weapons, armors
|
||||||
|
- mobs, pets
|
||||||
|
- blueprints, materials
|
||||||
|
- locations, teleporters, shops, planets, areas
|
||||||
|
- skills
|
||||||
|
- enhancers, medicaltools, finders, excavators, refiners
|
||||||
|
- vehicles, decorations, furniture
|
||||||
|
- storagecontainers, strongboxes, vendors
|
||||||
|
limit: Maximum number of results (default: 20, max: 100)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of search result dictionaries
|
||||||
|
|
||||||
|
Example:
|
||||||
|
# Search for weapons
|
||||||
|
results = self.nexus_search("ArMatrix", entity_type="weapons")
|
||||||
|
|
||||||
|
# Search for mobs
|
||||||
|
mobs = self.nexus_search("Atrox", entity_type="mobs")
|
||||||
|
|
||||||
|
# Search for locations
|
||||||
|
locations = self.nexus_search("Fort", entity_type="locations")
|
||||||
|
|
||||||
|
# Process results
|
||||||
|
for item in results:
|
||||||
|
print(f"{item['name']} ({item['type']})")
|
||||||
|
"""
|
||||||
|
if not self.api:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return self.api.nexus_search(query, entity_type, limit)
|
||||||
|
|
||||||
|
def nexus_get_item_details(self, item_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get detailed information about a specific item.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item_id: The item's unique identifier (e.g., "armatrix_lp-35")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with item details, or None if not found
|
||||||
|
|
||||||
|
Example:
|
||||||
|
details = self.nexus_get_item_details("armatrix_lp-35")
|
||||||
|
if details:
|
||||||
|
print(f"Name: {details['name']}")
|
||||||
|
print(f"TT Value: {details['tt_value']} PED")
|
||||||
|
print(f"Damage: {details.get('damage', 'N/A')}")
|
||||||
|
print(f"Range: {details.get('range', 'N/A')}m")
|
||||||
|
"""
|
||||||
|
if not self.api:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.api.nexus_get_item_details(item_id)
|
||||||
|
|
||||||
|
def nexus_get_market_data(self, item_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get market data for a specific item.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item_id: The item's unique identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with market data, or None if not found
|
||||||
|
|
||||||
|
Example:
|
||||||
|
market = self.nexus_get_market_data("armatrix_lp-35")
|
||||||
|
if market:
|
||||||
|
print(f"Current markup: {market['current_markup']:.1f}%")
|
||||||
|
print(f"7-day avg: {market['avg_markup_7d']:.1f}%")
|
||||||
|
print(f"24h Volume: {market['volume_24h']}")
|
||||||
|
|
||||||
|
# Check orders
|
||||||
|
for buy in market.get('buy_orders', [])[:5]:
|
||||||
|
print(f"Buy: {buy['price']} PED x {buy['quantity']}")
|
||||||
|
"""
|
||||||
|
if not self.api:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.api.nexus_get_market_data(item_id)
|
||||||
|
|
||||||
|
def nexus_is_available(self) -> bool:
|
||||||
|
"""Check if Nexus API is available.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if Nexus API service is ready
|
||||||
|
"""
|
||||||
|
if not self.api:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.api.nexus_is_available()
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,19 @@ easyocr>=1.7.0
|
||||||
pyautogui>=0.9.54
|
pyautogui>=0.9.54
|
||||||
pillow>=10.0.0
|
pillow>=10.0.0
|
||||||
|
|
||||||
|
# Cross-platform file locking (Windows)
|
||||||
|
portalocker>=2.7.0; platform_system=="Windows"
|
||||||
|
|
||||||
|
# Clipboard support
|
||||||
|
pyperclip>=1.8.2
|
||||||
|
|
||||||
|
# HTTP requests
|
||||||
|
requests>=2.28.0
|
||||||
|
urllib3>=1.26.0
|
||||||
|
|
||||||
|
# Data processing
|
||||||
|
numpy>=1.21.0
|
||||||
|
|
||||||
# Optional plugin dependencies
|
# Optional plugin dependencies
|
||||||
# Uncomment if using specific plugins:
|
# Uncomment if using specific plugins:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"registry": "https://clawhub.ai",
|
||||||
|
"slug": "playwright",
|
||||||
|
"installedVersion": "1.0.0",
|
||||||
|
"installedAt": 1771029662550
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
name: Playwright
|
||||||
|
description: Write, debug, and maintain Playwright tests and scrapers with resilient selectors, flaky test fixes, and CI/CD integration.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Trigger
|
||||||
|
|
||||||
|
Use when writing Playwright tests, debugging failures, scraping with Playwright, or setting up CI/CD pipelines.
|
||||||
|
|
||||||
|
## Selector Priority (Always)
|
||||||
|
|
||||||
|
1. `getByRole()` — accessible, resilient
|
||||||
|
2. `getByTestId()` — explicit, stable
|
||||||
|
3. `getByLabel()`, `getByPlaceholder()` — form elements
|
||||||
|
4. `getByText()` — visible content (exact match preferred)
|
||||||
|
5. CSS/XPath — last resort, avoid nth-child and generated classes
|
||||||
|
|
||||||
|
## Core Capabilities
|
||||||
|
|
||||||
|
| Task | Reference |
|
||||||
|
|------|-----------|
|
||||||
|
| Test scaffolding & POMs | `testing.md` |
|
||||||
|
| Selector strategies | `selectors.md` |
|
||||||
|
| Scraping patterns | `scraping.md` |
|
||||||
|
| CI/CD integration | `ci-cd.md` |
|
||||||
|
| Debugging failures | `debugging.md` |
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
- **Never use `page.waitForTimeout()`** — use `waitFor*` methods or `expect` with polling
|
||||||
|
- **Always close contexts** — `browser.close()` or `context.close()` to prevent memory leaks
|
||||||
|
- **Prefer `networkidle` with caution** — SPAs may never reach idle; use DOM-based waits instead
|
||||||
|
- **Use `test.describe.configure({ mode: 'parallel' })`** — for independent tests
|
||||||
|
- **Trace on failure only** — `trace: 'on-first-retry'` in config, not always-on
|
||||||
|
|
||||||
|
## Quick Fixes
|
||||||
|
|
||||||
|
| Symptom | Fix |
|
||||||
|
|---------|-----|
|
||||||
|
| Element not found | Use `waitFor()` before interaction, check frame context |
|
||||||
|
| Flaky clicks | Use `click({ force: true })` or `waitFor({ state: 'visible' })` first |
|
||||||
|
| Timeout in CI | Increase timeout, add `expect.poll()`, check viewport size |
|
||||||
|
| Stale element | Re-query the locator, avoid storing element references |
|
||||||
|
| Auth lost between tests | Use `storageState` to persist cookies/localStorage |
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"ownerId": "kn73vp5rarc3b14rc7wjcw8f8580t5d1",
|
||||||
|
"slug": "playwright",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"publishedAt": 1770982184555
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
# CI/CD Integration
|
||||||
|
|
||||||
|
## GitHub Actions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Playwright Tests
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npx playwright test
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 7
|
||||||
|
```
|
||||||
|
|
||||||
|
## GitLab CI
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
playwright:
|
||||||
|
image: mcr.microsoft.com/playwright:v1.40.0-jammy
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- npm ci
|
||||||
|
- npx playwright test
|
||||||
|
artifacts:
|
||||||
|
when: on_failure
|
||||||
|
paths:
|
||||||
|
- playwright-report/
|
||||||
|
expire_in: 7 days
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Setup
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM mcr.microsoft.com/playwright:v1.40.0-jammy
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["npx", "playwright", "test"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Sharding
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# GitHub Actions matrix
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
shard: [1, 2, 3, 4]
|
||||||
|
steps:
|
||||||
|
- name: Run tests
|
||||||
|
run: npx playwright test --shard=${{ matrix.shard }}/4
|
||||||
|
```
|
||||||
|
|
||||||
|
## playwright.config.ts for CI
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 4 : undefined,
|
||||||
|
reporter: process.env.CI
|
||||||
|
? [['html'], ['github']]
|
||||||
|
: [['html']],
|
||||||
|
|
||||||
|
use: {
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
video: 'on-first-retry',
|
||||||
|
},
|
||||||
|
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: { ...devices['Desktop Firefox'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'webkit',
|
||||||
|
use: { ...devices['Desktop Safari'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caching Browsers
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# GitHub Actions
|
||||||
|
- name: Cache Playwright browsers
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cache/ms-playwright
|
||||||
|
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
BASE_URL: https://staging.example.com
|
||||||
|
CI: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// playwright.config.ts
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flaky Test Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Mark known flaky test
|
||||||
|
test('sometimes fails', {
|
||||||
|
annotation: { type: 'flaky', description: 'Network timing issue' },
|
||||||
|
}, async ({ page }) => {
|
||||||
|
// test code
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retry configuration
|
||||||
|
export default defineConfig({
|
||||||
|
retries: 2,
|
||||||
|
expect: {
|
||||||
|
timeout: 10000, // Increase assertion timeout
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Report Hosting
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Deploy to GitHub Pages
|
||||||
|
- name: Deploy report
|
||||||
|
if: always()
|
||||||
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
publish_dir: ./playwright-report
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common CI Issues
|
||||||
|
|
||||||
|
| Issue | Fix |
|
||||||
|
|-------|-----|
|
||||||
|
| Browsers not found | Use official Playwright Docker image |
|
||||||
|
| Display errors | Headless mode or `xvfb-run` |
|
||||||
|
| Out of memory | Reduce workers, close contexts |
|
||||||
|
| Timeouts | Increase `actionTimeout`, add retries |
|
||||||
|
| Inconsistent screenshots | Set fixed viewport, disable animations |
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
# Debugging Guide
|
||||||
|
|
||||||
|
## Playwright Inspector
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run in debug mode
|
||||||
|
npx playwright test --debug
|
||||||
|
|
||||||
|
# Debug specific test
|
||||||
|
npx playwright test my-test.spec.ts --debug
|
||||||
|
|
||||||
|
# Headed mode (see browser)
|
||||||
|
npx playwright test --headed
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Pause in test
|
||||||
|
await page.pause();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trace Viewer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Record trace
|
||||||
|
npx playwright test --trace on
|
||||||
|
|
||||||
|
# View trace file
|
||||||
|
npx playwright show-trace trace.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Config for traces
|
||||||
|
use: {
|
||||||
|
trace: 'on-first-retry', // Only on failures
|
||||||
|
trace: 'retain-on-failure', // Keep failed traces
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Errors
|
||||||
|
|
||||||
|
### Element Not Found
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Timeout 30000ms exceeded waiting for selector
|
||||||
|
```
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
- Element doesn't exist in DOM
|
||||||
|
- Element is inside iframe
|
||||||
|
- Element is in shadow DOM
|
||||||
|
- Page hasn't loaded
|
||||||
|
|
||||||
|
**Fixes:**
|
||||||
|
```typescript
|
||||||
|
// Wait for element
|
||||||
|
await page.waitForSelector('.element');
|
||||||
|
|
||||||
|
// Check frame context
|
||||||
|
const frame = page.frameLocator('iframe');
|
||||||
|
await frame.locator('.element').click();
|
||||||
|
|
||||||
|
// Increase timeout
|
||||||
|
await page.click('.element', { timeout: 60000 });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flaky Click
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Element is not visible
|
||||||
|
Error: Element is outside viewport
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fixes:**
|
||||||
|
```typescript
|
||||||
|
// Ensure visible
|
||||||
|
await page.locator('.btn').waitFor({ state: 'visible' });
|
||||||
|
await page.locator('.btn').click();
|
||||||
|
|
||||||
|
// Scroll into view
|
||||||
|
await page.locator('.btn').scrollIntoViewIfNeeded();
|
||||||
|
|
||||||
|
// Force click (bypass checks)
|
||||||
|
await page.locator('.btn').click({ force: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeout in CI
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
- Slower CI environment
|
||||||
|
- Network latency
|
||||||
|
- Resource constraints
|
||||||
|
|
||||||
|
**Fixes:**
|
||||||
|
```typescript
|
||||||
|
// Increase global timeout
|
||||||
|
export default defineConfig({
|
||||||
|
timeout: 60000,
|
||||||
|
expect: { timeout: 10000 },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use polling assertions
|
||||||
|
await expect.poll(async () => {
|
||||||
|
return await page.locator('.items').count();
|
||||||
|
}, { timeout: 30000 }).toBeGreaterThan(5);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stale Element
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Element is no longer attached to DOM
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```typescript
|
||||||
|
// Don't store element references
|
||||||
|
const button = page.locator('.submit'); // This is fine (locator)
|
||||||
|
|
||||||
|
// Re-query when needed
|
||||||
|
await button.click(); // Playwright re-queries automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network Issues
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Log all requests
|
||||||
|
page.on('request', request => {
|
||||||
|
console.log('>>', request.method(), request.url());
|
||||||
|
});
|
||||||
|
|
||||||
|
page.on('response', response => {
|
||||||
|
console.log('<<', response.status(), response.url());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for specific request
|
||||||
|
const responsePromise = page.waitForResponse('**/api/data');
|
||||||
|
await page.click('.load-data');
|
||||||
|
const response = await responsePromise;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Screenshot Debugging
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Take screenshot on failure
|
||||||
|
test.afterEach(async ({ page }, testInfo) => {
|
||||||
|
if (testInfo.status !== 'passed') {
|
||||||
|
await page.screenshot({
|
||||||
|
path: `screenshots/${testInfo.title}.png`,
|
||||||
|
fullPage: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Console Logs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Capture console messages
|
||||||
|
page.on('console', msg => {
|
||||||
|
console.log('PAGE LOG:', msg.text());
|
||||||
|
});
|
||||||
|
|
||||||
|
page.on('pageerror', error => {
|
||||||
|
console.log('PAGE ERROR:', error.message);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Slow Motion
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// playwright.config.ts
|
||||||
|
use: {
|
||||||
|
launchOptions: {
|
||||||
|
slowMo: 500, // 500ms delay between actions
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compare Local vs CI
|
||||||
|
|
||||||
|
| Check | Command |
|
||||||
|
|-------|---------|
|
||||||
|
| Viewport | `await page.viewportSize()` |
|
||||||
|
| User agent | `await page.evaluate(() => navigator.userAgent)` |
|
||||||
|
| Timezone | `await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone)` |
|
||||||
|
| Network | `page.on('request', ...)` to log all requests |
|
||||||
|
|
||||||
|
## Debugging Checklist
|
||||||
|
|
||||||
|
1. [ ] Run with `--debug` or `--headed`
|
||||||
|
2. [ ] Add `await page.pause()` before failure point
|
||||||
|
3. [ ] Check for iframes/shadow DOM
|
||||||
|
4. [ ] Verify element exists with `page.locator().count()`
|
||||||
|
5. [ ] Review trace file in Trace Viewer
|
||||||
|
6. [ ] Compare screenshots between local and CI
|
||||||
|
7. [ ] Check console for JS errors
|
||||||
|
8. [ ] Verify network requests completed
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
# Scraping Patterns
|
||||||
|
|
||||||
|
## Basic Extraction
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.goto('https://example.com/products');
|
||||||
|
|
||||||
|
const products = await page.$$eval('.product-card', cards =>
|
||||||
|
cards.map(card => ({
|
||||||
|
name: card.querySelector('.name')?.textContent?.trim(),
|
||||||
|
price: card.querySelector('.price')?.textContent?.trim(),
|
||||||
|
url: card.querySelector('a')?.href,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wait Strategies for SPAs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Wait for specific element
|
||||||
|
await page.waitForSelector('[data-loaded="true"]');
|
||||||
|
|
||||||
|
// Wait for network idle (careful with SPAs)
|
||||||
|
await page.goto(url, { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
// Wait for loading indicator to disappear
|
||||||
|
await page.waitForSelector('.loading-spinner', { state: 'hidden' });
|
||||||
|
|
||||||
|
// Custom condition with polling
|
||||||
|
await expect.poll(async () => {
|
||||||
|
return await page.locator('.product').count();
|
||||||
|
}).toBeGreaterThan(0);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Infinite Scroll
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function scrollToBottom(page: Page) {
|
||||||
|
let previousHeight = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const currentHeight = await page.evaluate(() => document.body.scrollHeight);
|
||||||
|
if (currentHeight === previousHeight) break;
|
||||||
|
|
||||||
|
previousHeight = currentHeight;
|
||||||
|
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||||
|
await page.waitForTimeout(1000); // Allow content to load
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Click-based pagination
|
||||||
|
async function scrapeAllPages(page: Page) {
|
||||||
|
const allData = [];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const pageData = await extractData(page);
|
||||||
|
allData.push(...pageData);
|
||||||
|
|
||||||
|
const nextButton = page.getByRole('button', { name: 'Next' });
|
||||||
|
if (await nextButton.isDisabled()) break;
|
||||||
|
|
||||||
|
await nextButton.click();
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
}
|
||||||
|
|
||||||
|
return allData;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Anti-Bot Evasion
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
headless: false, // Some sites detect headless
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = await browser.newContext({
|
||||||
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||||
|
viewport: { width: 1920, height: 1080 },
|
||||||
|
locale: 'en-US',
|
||||||
|
timezoneId: 'America/New_York',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add realistic behavior
|
||||||
|
await page.mouse.move(100, 100);
|
||||||
|
await page.waitForTimeout(Math.random() * 2000 + 1000);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Save cookies
|
||||||
|
await context.storageState({ path: 'session.json' });
|
||||||
|
|
||||||
|
// Restore session
|
||||||
|
const context = await browser.newContext({
|
||||||
|
storageState: 'session.json',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function scrapeWithRetry(url: string, retries = 3) {
|
||||||
|
for (let i = 0; i < retries; i++) {
|
||||||
|
try {
|
||||||
|
const page = await context.newPage();
|
||||||
|
await page.goto(url, { timeout: 30000 });
|
||||||
|
return await extractData(page);
|
||||||
|
} catch (error) {
|
||||||
|
if (i === retries - 1) throw error;
|
||||||
|
await new Promise(r => setTimeout(r, 2000 * (i + 1)));
|
||||||
|
} finally {
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class RateLimiter {
|
||||||
|
private lastRequest = 0;
|
||||||
|
|
||||||
|
constructor(private minDelay: number) {}
|
||||||
|
|
||||||
|
async wait() {
|
||||||
|
const elapsed = Date.now() - this.lastRequest;
|
||||||
|
if (elapsed < this.minDelay) {
|
||||||
|
await new Promise(r => setTimeout(r, this.minDelay - elapsed));
|
||||||
|
}
|
||||||
|
this.lastRequest = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const limiter = new RateLimiter(2000); // 2s between requests
|
||||||
|
|
||||||
|
for (const url of urls) {
|
||||||
|
await limiter.wait();
|
||||||
|
await scrape(url);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Proxy Rotation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const proxies = ['proxy1:8080', 'proxy2:8080', 'proxy3:8080'];
|
||||||
|
let proxyIndex = 0;
|
||||||
|
|
||||||
|
async function getNextProxy() {
|
||||||
|
const proxy = proxies[proxyIndex];
|
||||||
|
proxyIndex = (proxyIndex + 1) % proxies.length;
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
proxy: { server: await getNextProxy() },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Selector Strategies
|
||||||
|
|
||||||
|
## Hierarchy (Most to Least Resilient)
|
||||||
|
|
||||||
|
### 1. Role-Based (Best)
|
||||||
|
```typescript
|
||||||
|
page.getByRole('button', { name: 'Submit' })
|
||||||
|
page.getByRole('link', { name: /sign up/i })
|
||||||
|
page.getByRole('heading', { level: 1 })
|
||||||
|
page.getByRole('textbox', { name: 'Email' })
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Test IDs (Explicit)
|
||||||
|
```typescript
|
||||||
|
page.getByTestId('checkout-button')
|
||||||
|
page.getByTestId('product-card').first()
|
||||||
|
```
|
||||||
|
Configure in `playwright.config.ts`:
|
||||||
|
```typescript
|
||||||
|
use: { testIdAttribute: 'data-testid' }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Label/Placeholder (Forms)
|
||||||
|
```typescript
|
||||||
|
page.getByLabel('Email address')
|
||||||
|
page.getByPlaceholder('Enter your email')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Text Content (Visible)
|
||||||
|
```typescript
|
||||||
|
page.getByText('Add to Cart', { exact: true })
|
||||||
|
page.getByText(/welcome/i) // regex for flexibility
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. CSS (Last Resort)
|
||||||
|
```typescript
|
||||||
|
// Avoid these patterns:
|
||||||
|
page.locator('.css-1a2b3c') // generated class
|
||||||
|
page.locator('div > span:nth-child(2)') // positional
|
||||||
|
page.locator('#root > div > div > button') // deep nesting
|
||||||
|
|
||||||
|
// Acceptable:
|
||||||
|
page.locator('[data-product-id="123"]') // semantic attribute
|
||||||
|
page.locator('form.login-form') // stable class
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chaining and Filtering
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Filter within results
|
||||||
|
page.getByRole('listitem').filter({ hasText: 'Product A' })
|
||||||
|
|
||||||
|
// Chain locators
|
||||||
|
page.getByTestId('cart').getByRole('button', { name: 'Remove' })
|
||||||
|
|
||||||
|
// Get nth item
|
||||||
|
page.getByRole('listitem').nth(2)
|
||||||
|
page.getByRole('listitem').first()
|
||||||
|
page.getByRole('listitem').last()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frame Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Named frame
|
||||||
|
const frame = page.frameLocator('iframe[name="checkout"]')
|
||||||
|
frame.getByRole('button', { name: 'Pay' }).click()
|
||||||
|
|
||||||
|
// Frame by URL
|
||||||
|
page.frameLocator('iframe[src*="stripe"]')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shadow DOM
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Playwright pierces shadow DOM by default
|
||||||
|
page.locator('my-component').getByRole('button')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
| Mistake | Better |
|
||||||
|
|---------|--------|
|
||||||
|
| `page.locator('button').click()` | `page.getByRole('button', { name: 'Submit' }).click()` |
|
||||||
|
| Storing locator result | Re-query each time |
|
||||||
|
| `nth-child(3)` | Filter by text or test ID |
|
||||||
|
| `//div[@class="xyz"]/span[2]` | Role-based or test ID |
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
# Testing Patterns
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Checkout Flow', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/products');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('completes purchase with valid card', async ({ page }) => {
|
||||||
|
await page.getByTestId('product-card').first().click();
|
||||||
|
await page.getByRole('button', { name: 'Add to Cart' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Checkout' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Order Summary' })).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Page Object Model
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// pages/checkout.page.ts
|
||||||
|
export class CheckoutPage {
|
||||||
|
constructor(private page: Page) {}
|
||||||
|
|
||||||
|
readonly cartItems = this.page.getByTestId('cart-item');
|
||||||
|
readonly checkoutButton = this.page.getByRole('button', { name: 'Checkout' });
|
||||||
|
readonly totalPrice = this.page.getByTestId('total-price');
|
||||||
|
|
||||||
|
async removeItem(name: string) {
|
||||||
|
await this.cartItems
|
||||||
|
.filter({ hasText: name })
|
||||||
|
.getByRole('button', { name: 'Remove' })
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectTotal(amount: string) {
|
||||||
|
await expect(this.totalPrice).toHaveText(amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tests/checkout.spec.ts
|
||||||
|
test('removes item from cart', async ({ page }) => {
|
||||||
|
const checkout = new CheckoutPage(page);
|
||||||
|
await checkout.removeItem('Product A');
|
||||||
|
await checkout.expectTotal('$0.00');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fixtures
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// fixtures.ts
|
||||||
|
import { test as base } from '@playwright/test';
|
||||||
|
import { CheckoutPage } from './pages/checkout.page';
|
||||||
|
|
||||||
|
type Fixtures = {
|
||||||
|
checkoutPage: CheckoutPage;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const test = base.extend<Fixtures>({
|
||||||
|
checkoutPage: async ({ page }, use) => {
|
||||||
|
await page.goto('/checkout');
|
||||||
|
await use(new CheckoutPage(page));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Mocking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
test('shows error on API failure', async ({ page }) => {
|
||||||
|
await page.route('**/api/checkout', route => {
|
||||||
|
route.fulfill({
|
||||||
|
status: 500,
|
||||||
|
body: JSON.stringify({ error: 'Payment failed' }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('/checkout');
|
||||||
|
await page.getByRole('button', { name: 'Pay' }).click();
|
||||||
|
await expect(page.getByText('Payment failed')).toBeVisible();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Visual Regression
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
test('matches snapshot', async ({ page }) => {
|
||||||
|
await page.goto('/dashboard');
|
||||||
|
await expect(page).toHaveScreenshot('dashboard.png', {
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Component snapshot
|
||||||
|
await expect(page.getByTestId('header')).toHaveScreenshot();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parallelization
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// playwright.config.ts
|
||||||
|
export default defineConfig({
|
||||||
|
workers: process.env.CI ? 4 : undefined,
|
||||||
|
fullyParallel: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Per-file control
|
||||||
|
test.describe.configure({ mode: 'parallel' });
|
||||||
|
test.describe.configure({ mode: 'serial' }); // dependent tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication State
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Save auth state
|
||||||
|
await page.context().storageState({ path: 'auth.json' });
|
||||||
|
|
||||||
|
// Reuse across tests
|
||||||
|
test.use({ storageState: 'auth.json' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Assertions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Visibility
|
||||||
|
await expect(locator).toBeVisible();
|
||||||
|
await expect(locator).toBeHidden();
|
||||||
|
await expect(locator).toBeAttached();
|
||||||
|
|
||||||
|
// Content
|
||||||
|
await expect(locator).toHaveText('Expected');
|
||||||
|
await expect(locator).toContainText('partial');
|
||||||
|
await expect(locator).toHaveValue('input value');
|
||||||
|
|
||||||
|
// State
|
||||||
|
await expect(locator).toBeEnabled();
|
||||||
|
await expect(locator).toBeChecked();
|
||||||
|
await expect(locator).toHaveAttribute('href', '/path');
|
||||||
|
|
||||||
|
// Polling (for async state)
|
||||||
|
await expect.poll(async () => {
|
||||||
|
return await page.evaluate(() => window.dataLoaded);
|
||||||
|
}).toBe(true);
|
||||||
|
```
|
||||||
Loading…
Reference in New Issue