diff --git a/.clawhub/lock.json b/.clawhub/lock.json new file mode 100644 index 0000000..eab13c8 --- /dev/null +++ b/.clawhub/lock.json @@ -0,0 +1,9 @@ +{ + "version": 1, + "skills": { + "playwright": { + "version": "1.0.0", + "installedAt": 1771029662552 + } + } +} diff --git a/memory/2026-02-14.md b/memory/2026-02-14.md new file mode 100644 index 0000000..7f86acd --- /dev/null +++ b/memory/2026-02-14.md @@ -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 diff --git a/projects/EU-Utility/core/data_store.py b/projects/EU-Utility/core/data_store.py index 89aa3d7..1c8486e 100644 --- a/projects/EU-Utility/core/data_store.py +++ b/projects/EU-Utility/core/data_store.py @@ -6,14 +6,27 @@ Provides file locking, auto-backup, and singleton access. """ import json -import fcntl import shutil import threading +import platform from pathlib import Path from typing import Any, Dict, Optional from datetime import datetime 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: """ @@ -80,12 +93,12 @@ class DataStore: try: with open(file_path, 'r', encoding='utf-8') as f: - # Acquire shared lock for reading - fcntl.flock(f.fileno(), fcntl.LOCK_SH) + # Cross-platform file locking + self._lock_file(f, exclusive=False) try: data = json.load(f) finally: - fcntl.flock(f.fileno(), fcntl.LOCK_UN) + self._unlock_file(f) # Update cache with self._cache_lock: @@ -108,15 +121,15 @@ class DataStore: temp_path = file_path.with_suffix('.tmp') with open(temp_path, 'w', encoding='utf-8') as f: - # Acquire exclusive lock for writing - fcntl.flock(f.fileno(), fcntl.LOCK_EX) + # Cross-platform file locking + self._lock_file(f, exclusive=True) try: json.dump(data, f, indent=2, ensure_ascii=False) f.flush() import os os.fsync(f.fileno()) finally: - fcntl.flock(f.fileno(), fcntl.LOCK_UN) + self._unlock_file(f) # Atomic move temp_path.replace(file_path) @@ -134,6 +147,32 @@ class DataStore: temp_path.unlink() 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): """Create a backup of the current data file.""" backup_dir = self._get_backup_dir(plugin_id) diff --git a/projects/EU-Utility/core/nexus_api.py b/projects/EU-Utility/core/nexus_api.py index 31870e7..e3acaf6 100644 --- a/projects/EU-Utility/core/nexus_api.py +++ b/projects/EU-Utility/core/nexus_api.py @@ -17,9 +17,58 @@ from datetime import datetime, timedelta class EntityType(Enum): """Types of entities that can be searched.""" + # Core types ITEM = "items" MOB = "mobs" ALL = "all" + + # Equipment + WEAPON = "weapons" + ARMOR = "armors" + ENHANCER = "enhancers" + + # Tools + MEDICAL_TOOL = "medicaltools" + FINDER = "finders" + EXCAVATOR = "excavators" + REFINER = "refiners" + + # Crafting & Materials + BLUEPRINT = "blueprints" + MATERIAL = "materials" + + # Creatures + PET = "pets" + + # Locations + LOCATION = "locations" + TELEPORTER = "teleporters" + SHOP = "shops" + VENDOR = "vendors" + PLANET = "planets" + AREA = "areas" + + # Other + SKILL = "skills" + VEHICLE = "vehicles" + DECORATION = "decorations" + FURNITURE = "furniture" + STORAGE_CONTAINER = "storagecontainers" + STRONGBOX = "strongboxes" + + @classmethod + def get_all_types(cls) -> List[str]: + """Get list of all entity type values.""" + return [e.value for e in cls if e != cls.ALL] + + @classmethod + def from_string(cls, type_str: str) -> Optional["EntityType"]: + """Get EntityType from string value.""" + type_str = type_str.lower() + for e in cls: + if e.value.lower() == type_str: + return e + return None class NexusAPIError(Exception): @@ -394,6 +443,146 @@ class NexusAPI: print(f"[NexusAPI] search_all error: {e}") return [] + def search_by_type(self, query: str, entity_type: str, limit: int = 20) -> List[SearchResult]: + """ + Search for entities of a specific type. + + Args: + query: Search term + entity_type: Entity type (e.g., 'weapons', 'blueprints', 'mobs') + limit: Maximum results (default 20, max 100) + + Returns: + List of SearchResult objects + + Example: + results = api.search_by_type("ArMatrix", "weapons") + results = api.search_by_type("Atrox", "mobs") + """ + try: + # Normalize entity type + entity_type = entity_type.lower().replace(' ', '').replace('_', '') + + # Map common aliases + type_mapping = { + 'item': 'items', + 'weapon': 'weapons', + 'armor': 'armors', + 'mob': 'mobs', + 'blueprint': 'blueprints', + 'location': 'locations', + 'skill': 'skills', + 'material': 'materials', + 'enhancer': 'enhancers', + 'medicaltool': 'medicaltools', + 'medical_tool': 'medicaltools', + 'finder': 'finders', + 'excavator': 'excavators', + 'refiner': 'refiners', + 'vehicle': 'vehicles', + 'pet': 'pets', + 'decoration': 'decorations', + 'furniture': 'furniture', + 'storage': 'storagecontainers', + 'storagecontainer': 'storagecontainers', + 'storage_container': 'storagecontainers', + 'strongbox': 'strongboxes', + 'teleporter': 'teleporters', + 'shop': 'shops', + 'vendor': 'vendors', + 'planet': 'planets', + 'area': 'areas', + } + + endpoint = type_mapping.get(entity_type, entity_type) + + params = { + 'q': query, + 'limit': min(limit, 100), + 'fuzzy': 'true' + } + + data = self._make_request(f'{endpoint}', params) + + results = [] + items = data if isinstance(data, list) else data.get('results', []) + + for item in items: + results.append(SearchResult( + id=item.get('id', item.get('Id', '')), + name=item.get('name', item.get('Name', 'Unknown')), + type=item.get('type', entity_type), + category=item.get('category', item.get('Category')), + icon_url=item.get('icon_url', item.get('IconUrl')), + data=item + )) + + return results + + except Exception as e: + print(f"[NexusAPI] search_by_type error ({entity_type}): {e}") + return [] + + def get_entity_details(self, entity_id: str, entity_type: str) -> Optional[Dict[str, Any]]: + """ + Get detailed information about any entity type. + + Args: + entity_id: The entity's unique identifier + entity_type: Entity type (e.g., 'mobs', 'locations', 'blueprints') + + Returns: + Dict with entity details or None if not found + + Example: + mob = api.get_entity_details("atrox", "mobs") + location = api.get_entity_details("fort-izzuk", "locations") + """ + try: + # Normalize entity type + entity_type = entity_type.lower().replace(' ', '').replace('_', '') + + # Map common aliases + type_mapping = { + 'item': 'items', + 'weapon': 'weapons', + 'armor': 'armors', + 'mob': 'mobs', + 'blueprint': 'blueprints', + 'location': 'locations', + 'skill': 'skills', + 'material': 'materials', + 'enhancer': 'enhancers', + 'medicaltool': 'medicaltools', + 'finder': 'finders', + 'excavator': 'excavators', + 'refiner': 'refiners', + 'vehicle': 'vehicles', + 'pet': 'pets', + 'decoration': 'decorations', + 'furniture': 'furniture', + 'storage': 'storagecontainers', + 'storagecontainer': 'storagecontainers', + 'strongbox': 'strongboxes', + 'teleporter': 'teleporters', + 'shop': 'shops', + 'vendor': 'vendors', + 'planet': 'planets', + 'area': 'areas', + } + + endpoint = type_mapping.get(entity_type, entity_type) + data = self._make_request(f'{endpoint}/{entity_id}') + + if not data or 'error' in data: + return None + + return data + + except Exception as e: + print(f"[NexusAPI] get_entity_details error ({entity_type}): {e}") + return None + # ========== Detail Methods ========== def get_item_details(self, item_id: str) -> Optional[ItemDetails]: diff --git a/projects/EU-Utility/core/plugin_api.py b/projects/EU-Utility/core/plugin_api.py index 11d66c7..e138dee 100644 --- a/projects/EU-Utility/core/plugin_api.py +++ b/projects/EU-Utility/core/plugin_api.py @@ -848,6 +848,219 @@ class PluginAPI: except Exception: 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 _plugin_api = None diff --git a/projects/EU-Utility/docs/NEXUS_API_REFERENCE.md b/projects/EU-Utility/docs/NEXUS_API_REFERENCE.md new file mode 100644 index 0000000..faedf35 --- /dev/null +++ b/projects/EU-Utility/docs/NEXUS_API_REFERENCE.md @@ -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 diff --git a/projects/EU-Utility/docs/NEXUS_DOCUMENTATION_SUMMARY.md b/projects/EU-Utility/docs/NEXUS_DOCUMENTATION_SUMMARY.md new file mode 100644 index 0000000..aeeae48 --- /dev/null +++ b/projects/EU-Utility/docs/NEXUS_DOCUMENTATION_SUMMARY.md @@ -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. diff --git a/projects/EU-Utility/docs/NEXUS_LINKTREE.md b/projects/EU-Utility/docs/NEXUS_LINKTREE.md new file mode 100644 index 0000000..b058f29 --- /dev/null +++ b/projects/EU-Utility/docs/NEXUS_LINKTREE.md @@ -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 diff --git a/projects/EU-Utility/docs/NEXUS_USAGE_EXAMPLES.md b/projects/EU-Utility/docs/NEXUS_USAGE_EXAMPLES.md new file mode 100644 index 0000000..1081cd4 --- /dev/null +++ b/projects/EU-Utility/docs/NEXUS_USAGE_EXAMPLES.md @@ -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 diff --git a/projects/EU-Utility/plugins/base_plugin.py b/projects/EU-Utility/plugins/base_plugin.py index 456e24e..aa83e9c 100644 --- a/projects/EU-Utility/plugins/base_plugin.py +++ b/projects/EU-Utility/plugins/base_plugin.py @@ -683,3 +683,101 @@ class BasePlugin(ABC): connected = self.api.connect_task_signal('cancelled', on_cancelled) or 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() diff --git a/projects/EU-Utility/requirements.txt b/projects/EU-Utility/requirements.txt index 29a8fab..ba5f96f 100644 --- a/projects/EU-Utility/requirements.txt +++ b/projects/EU-Utility/requirements.txt @@ -8,6 +8,19 @@ easyocr>=1.7.0 pyautogui>=0.9.54 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 # Uncomment if using specific plugins: diff --git a/skills/playwright/.clawhub/origin.json b/skills/playwright/.clawhub/origin.json new file mode 100644 index 0000000..73b753c --- /dev/null +++ b/skills/playwright/.clawhub/origin.json @@ -0,0 +1,7 @@ +{ + "version": 1, + "registry": "https://clawhub.ai", + "slug": "playwright", + "installedVersion": "1.0.0", + "installedAt": 1771029662550 +} diff --git a/skills/playwright/SKILL.md b/skills/playwright/SKILL.md new file mode 100644 index 0000000..f41b9ca --- /dev/null +++ b/skills/playwright/SKILL.md @@ -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 | diff --git a/skills/playwright/_meta.json b/skills/playwright/_meta.json new file mode 100644 index 0000000..58e766d --- /dev/null +++ b/skills/playwright/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn73vp5rarc3b14rc7wjcw8f8580t5d1", + "slug": "playwright", + "version": "1.0.0", + "publishedAt": 1770982184555 +} \ No newline at end of file diff --git a/skills/playwright/ci-cd.md b/skills/playwright/ci-cd.md new file mode 100644 index 0000000..d365b1a --- /dev/null +++ b/skills/playwright/ci-cd.md @@ -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 | diff --git a/skills/playwright/debugging.md b/skills/playwright/debugging.md new file mode 100644 index 0000000..26747a7 --- /dev/null +++ b/skills/playwright/debugging.md @@ -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 diff --git a/skills/playwright/scraping.md b/skills/playwright/scraping.md new file mode 100644 index 0000000..72c2af1 --- /dev/null +++ b/skills/playwright/scraping.md @@ -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() }, +}); +``` diff --git a/skills/playwright/selectors.md b/skills/playwright/selectors.md new file mode 100644 index 0000000..894e900 --- /dev/null +++ b/skills/playwright/selectors.md @@ -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 | diff --git a/skills/playwright/testing.md b/skills/playwright/testing.md new file mode 100644 index 0000000..383e1eb --- /dev/null +++ b/skills/playwright/testing.md @@ -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({ + 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); +```