diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78f8cc7..42144fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: # Run on PR creation and updates, but with conditions to reduce waste pull_request: types: [opened, synchronize] - branches: [ main, v2-dev ] + branches: [ main, v2-dev, dev ] # Allow manual triggering from GitHub UI workflow_dispatch: inputs: @@ -13,9 +13,9 @@ on: required: false default: 'true' type: boolean - # Still run on pushes to main/v2-dev (for releases) + # Still run on pushes to main/v2-dev/dev (for releases) push: - branches: [ main, v2-dev ] + branches: [ main, v2-dev, dev ] jobs: test: diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 8c7f864..3c52493 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -3,7 +3,7 @@ name: Claude Code Review on: # Run on PR creation, skip on drafts and minor updates pull_request: - types: [opened, synchronize] + types: [opened, synchronize, reopened] # Allow manual triggering from GitHub UI workflow_dispatch: # Optional: Only run on specific file changes @@ -16,7 +16,7 @@ on: jobs: claude-review: # Skip Claude review on synchronize unless specifically requested - if: github.event.action == 'opened' || contains(github.event.head_commit.message, '[review]') + if: github.event.action == 'opened' || github.event.action == 'reopened' || contains(github.event.head_commit.message, '[review]') || github.event_name == 'workflow_dispatch' # Optional: Filter by PR author # if: | # github.event.pull_request.user.login == 'external-contributor' || diff --git a/README.md b/README.md index d76580b..f3f921b 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,11 @@ | Metric | Status | |--------|--------| -| **Current Version** | 0.2.0a2 | +| **Current Version** | 0.2.0a3 | | **API Coverage** | ~83% (comprehensive analysis shows 6/8 API sections fully implemented) | | **Development Stage** | Active development | | **Documentation** | [Read the Docs](https://esologs-python.readthedocs.io/) | -| **Tests** | 278 tests across unit, integration, documentation, and sanity suites | +| **Tests** | 310 tests across unit, integration, documentation, and sanity suites | ### Current API Coverage **Implemented (6/8 sections):** @@ -309,14 +309,24 @@ This project uses several tools to maintain code quality: ``` esologs-python/ ├── esologs/ # Main package -│ ├── client.py # Main client implementation -│ ├── async_base_client.py # Base async GraphQL client -│ ├── auth.py # OAuth2 authentication module -│ ├── exceptions.py # Custom exceptions +│ ├── client.py # Main client (86 lines, uses mixins) +│ ├── method_factory.py # Dynamic method generation (349 lines) +│ ├── param_builders.py # Parameter validation & builders (330 lines) +│ ├── queries.py # Centralized GraphQL queries (770 lines) +│ ├── auth.py # OAuth2 authentication module │ ├── validators.py # Parameter validation utilities -│ └── get_*.py # Generated GraphQL query modules -├── tests/ # Test suite (278 tests) -│ ├── unit/ # Unit tests (76 tests) +│ ├── mixins/ # Modular API functionality +│ │ ├── game_data.py # Game data methods (abilities, items, etc.) +│ │ ├── character.py # Character methods (info, rankings) +│ │ ├── world_data.py # World data methods (zones, regions) +│ │ ├── guild.py # Guild methods +│ │ └── report.py # Report methods (search, analysis) +│ └── _generated/ # Auto-generated GraphQL modules +│ ├── async_base_client.py # Base async GraphQL client +│ ├── exceptions.py # Custom exceptions +│ └── get_*.py # Generated query/response models +├── tests/ # Test suite (310 tests) +│ ├── unit/ # Unit tests (105 tests) │ ├── integration/ # Integration tests (85 tests) │ ├── docs/ # Documentation tests (98 tests) │ └── sanity/ # Sanity tests (19 tests) @@ -364,7 +374,7 @@ We welcome contributions! Please see our contributing guidelines: - ✅ PR #4: Advanced Report Search (Merged) - 🚧 PR #5: Client Architecture Refactor (Next) - **Phase 3** 🚧: Data transformation and pandas integration -- **Phase 4** ✅: Comprehensive testing and documentation (278 tests) +- **Phase 4** ✅: Comprehensive testing and documentation (310 tests) - **Phase 5** 🚧: Performance optimization and caching ## License diff --git a/docs/changelog.md b/docs/changelog.md index b8d2004..32c8c99 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,30 @@ All notable changes to ESO Logs Python will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.0a3] - 2025-07-19 + +### Changed + +- **Major Refactoring**: Reduced `client.py` from 1,610 lines to 86 lines (95% reduction) +- Implemented factory pattern with mixins for better code organization +- Created modular architecture with clear separation of concerns: + - Method factory functions for dynamic method generation + - Parameter builders for complex parameter handling + - Mixins organizing methods by functional area + - Centralized GraphQL queries storage +- Moved all auto-generated code to `_generated/` subdirectory for cleaner structure +- Improved error messages to show available parameters when missing +- Added comprehensive documentation for method registration and naming conventions +- Cached regex patterns for performance improvement +- Fixed type safety issues with proper Protocol usage +- Updated test suite from 278 to 310 tests (added 29 unit tests for new patterns) + +### Fixed + +- Type annotations now satisfy mypy without `# type: ignore` comments +- Parameter validation errors now provide more helpful context +- Fixed kwargs passthrough issue in report methods preventing HTTP client errors + ## [0.2.0a2] - 2025-07-16 ### Fixed @@ -21,6 +45,16 @@ This is the first alpha release of version 0.2.0. See the [0.2.0] section below ## [0.2.0] - 2024-01-XX (Upcoming Release) +### Changed + +#### Architecture Improvements +- **Client Refactoring**: Complete overhaul of client.py architecture + - Reduced from 1,600+ lines to 86 lines (95% reduction) + - Implemented factory pattern for method generation + - Organized methods into logical mixins by functional area + - Moved all generated code to `_generated/` subdirectory + - Maintained 100% backward compatibility + ### Added #### Character Rankings & Performance diff --git a/docs/development/architecture.md b/docs/development/architecture.md index fde1437..1cbd632 100644 --- a/docs/development/architecture.md +++ b/docs/development/architecture.md @@ -27,7 +27,41 @@ graph TB ## Core Components -### 1. GraphQL Client Layer +### 1. Refactored Client Architecture + +**Purpose**: Modular, maintainable client implementation using factory patterns and mixins + +The client has been refactored from a monolithic 1,600+ line file to a clean, modular architecture: + +```python +# esologs/client.py (86 lines) +class Client( + AsyncBaseClient, + GameDataMixin, + CharacterMixin, + WorldDataMixin, + GuildMixin, + ReportMixin, +): + """ESO Logs API client with comprehensive validation and security features.""" +``` + +**Architecture Components**: + +* **Factory Methods** (`method_factory.py`): Dynamic method generation + - `create_simple_getter()`: For single ID parameter methods + - `create_complex_method()`: For methods with multiple parameters + - `create_method_with_builder()`: For methods using parameter builders +* **Mixins** (`mixins/`): Methods organized by functional area + - `GameDataMixin`: Abilities, items, NPCs, classes, factions, maps + - `CharacterMixin`: Character info, reports, rankings + - `WorldDataMixin`: World data, zones, regions, encounters + - `GuildMixin`: Guild information + - `ReportMixin`: Combat reports, events, graphs, rankings +* **Parameter Builders** (`param_builders.py`): Complex parameter handling +* **GraphQL Queries** (`queries.py`): Centralized query storage + +### 2. GraphQL Client Layer **Purpose**: Auto-generated client for type-safe API communication @@ -159,7 +193,8 @@ graph LR ``` 4. **Generated Output** - - `esologs/client.py`: GraphQL client with typed methods + - `esologs/_generated/`: All generated code in subdirectory + - `esologs/client.py`: Refactored client using mixins and factory patterns - `esologs/models/`: Pydantic models for all types - `esologs/exceptions.py`: Custom exception classes @@ -316,7 +351,7 @@ TEST_DATA = { ```toml [project] name = "esologs-python" -version = "0.2.0a2" +version = "0.2.0a3" dependencies = [ "httpx>=0.24.0", "pydantic>=2.0.0", diff --git a/docs/index.md b/docs/index.md index cae3cda..a5d298b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -73,7 +73,7 @@

Current Version

-

v0.2.0a2
+

v0.2.0a3
83% API Coverage

Active development with comprehensive testing and documentation.

diff --git a/esologs/__init__.py b/esologs/__init__.py index dce1d70..101be66 100644 --- a/esologs/__init__.py +++ b/esologs/__init__.py @@ -1,10 +1,8 @@ -__version__ = "0.2.0a2" +__version__ = "0.2.0a3" -from .async_base_client import AsyncBaseClient -from .auth import get_access_token -from .base_model import BaseModel, Upload -from .client import Client -from .enums import ( +from ._generated.async_base_client import AsyncBaseClient +from ._generated.base_model import BaseModel, Upload +from ._generated.enums import ( CharacterRankingMetricType, EventDataType, ExternalBuffRankFilter, @@ -23,38 +21,42 @@ TableDataType, ViewType, ) -from .exceptions import ( +from ._generated.exceptions import ( GraphQLClientError, GraphQLClientGraphQLError, GraphQLClientGraphQLMultiError, GraphQLClientHttpError, GraphQLClientInvalidResponseError, ) -from .get_abilities import ( +from ._generated.get_abilities import ( GetAbilities, GetAbilitiesGameData, GetAbilitiesGameDataAbilities, GetAbilitiesGameDataAbilitiesData, ) -from .get_ability import GetAbility, GetAbilityGameData, GetAbilityGameDataAbility -from .get_character_by_id import ( +from ._generated.get_ability import ( + GetAbility, + GetAbilityGameData, + GetAbilityGameDataAbility, +) +from ._generated.get_character_by_id import ( GetCharacterById, GetCharacterByIdCharacterData, GetCharacterByIdCharacterDataCharacter, GetCharacterByIdCharacterDataCharacterServer, GetCharacterByIdCharacterDataCharacterServerRegion, ) -from .get_character_encounter_ranking import ( +from ._generated.get_character_encounter_ranking import ( GetCharacterEncounterRanking, GetCharacterEncounterRankingCharacterData, GetCharacterEncounterRankingCharacterDataCharacter, ) -from .get_character_encounter_rankings import ( +from ._generated.get_character_encounter_rankings import ( GetCharacterEncounterRankings, GetCharacterEncounterRankingsCharacterData, GetCharacterEncounterRankingsCharacterDataCharacter, ) -from .get_character_reports import ( +from ._generated.get_character_reports import ( GetCharacterReports, GetCharacterReportsCharacterData, GetCharacterReportsCharacterDataCharacter, @@ -62,21 +64,29 @@ GetCharacterReportsCharacterDataCharacterRecentReportsData, GetCharacterReportsCharacterDataCharacterRecentReportsDataZone, ) -from .get_character_zone_rankings import ( +from ._generated.get_character_zone_rankings import ( GetCharacterZoneRankings, GetCharacterZoneRankingsCharacterData, GetCharacterZoneRankingsCharacterDataCharacter, ) -from .get_class import GetClass, GetClassGameData, GetClassGameDataClass -from .get_classes import GetClasses, GetClassesGameData, GetClassesGameDataClasses -from .get_encounters_by_zone import ( +from ._generated.get_class import GetClass, GetClassGameData, GetClassGameDataClass +from ._generated.get_classes import ( + GetClasses, + GetClassesGameData, + GetClassesGameDataClasses, +) +from ._generated.get_encounters_by_zone import ( GetEncountersByZone, GetEncountersByZoneWorldData, GetEncountersByZoneWorldDataZone, GetEncountersByZoneWorldDataZoneEncounters, ) -from .get_factions import GetFactions, GetFactionsGameData, GetFactionsGameDataFactions -from .get_guild_by_id import ( +from ._generated.get_factions import ( + GetFactions, + GetFactionsGameData, + GetFactionsGameDataFactions, +) +from ._generated.get_guild_by_id import ( GetGuildById, GetGuildByIdGuildData, GetGuildByIdGuildDataGuild, @@ -85,75 +95,82 @@ GetGuildByIdGuildDataGuildServerRegion, GetGuildByIdGuildDataGuildTags, ) -from .get_item import GetItem, GetItemGameData, GetItemGameDataItem -from .get_item_set import GetItemSet, GetItemSetGameData, GetItemSetGameDataItemSet -from .get_item_sets import ( +from ._generated.get_item import GetItem, GetItemGameData, GetItemGameDataItem +from ._generated.get_item_set import ( + GetItemSet, + GetItemSetGameData, + GetItemSetGameDataItemSet, +) +from ._generated.get_item_sets import ( GetItemSets, GetItemSetsGameData, GetItemSetsGameDataItemSets, GetItemSetsGameDataItemSetsData, ) -from .get_items import ( +from ._generated.get_items import ( GetItems, GetItemsGameData, GetItemsGameDataItems, GetItemsGameDataItemsData, ) -from .get_map import GetMap, GetMapGameData, GetMapGameDataMap -from .get_maps import ( +from ._generated.get_map import GetMap, GetMapGameData, GetMapGameDataMap +from ._generated.get_maps import ( GetMaps, GetMapsGameData, GetMapsGameDataMaps, GetMapsGameDataMapsData, ) -from .get_npc import GetNPC, GetNPCGameData, GetNPCGameDataNpc -from .get_npcs import ( +from ._generated.get_npc import GetNPC, GetNPCGameData, GetNPCGameDataNpc +from ._generated.get_npcs import ( GetNPCs, GetNPCsGameData, GetNPCsGameDataNpcs, GetNPCsGameDataNpcsData, ) -from .get_rate_limit_data import GetRateLimitData, GetRateLimitDataRateLimitData -from .get_regions import ( +from ._generated.get_rate_limit_data import ( + GetRateLimitData, + GetRateLimitDataRateLimitData, +) +from ._generated.get_regions import ( GetRegions, GetRegionsWorldData, GetRegionsWorldDataRegions, GetRegionsWorldDataRegionsSubregions, ) -from .get_report_by_code import ( +from ._generated.get_report_by_code import ( GetReportByCode, GetReportByCodeReportData, GetReportByCodeReportDataReport, GetReportByCodeReportDataReportFights, GetReportByCodeReportDataReportZone, ) -from .get_report_events import ( +from ._generated.get_report_events import ( GetReportEvents, GetReportEventsReportData, GetReportEventsReportDataReport, GetReportEventsReportDataReportEvents, ) -from .get_report_graph import ( +from ._generated.get_report_graph import ( GetReportGraph, GetReportGraphReportData, GetReportGraphReportDataReport, ) -from .get_report_player_details import ( +from ._generated.get_report_player_details import ( GetReportPlayerDetails, GetReportPlayerDetailsReportData, GetReportPlayerDetailsReportDataReport, ) -from .get_report_rankings import ( +from ._generated.get_report_rankings import ( GetReportRankings, GetReportRankingsReportData, GetReportRankingsReportDataReport, ) -from .get_report_table import ( +from ._generated.get_report_table import ( GetReportTable, GetReportTableReportData, GetReportTableReportDataReport, ) -from .get_reports import ( +from ._generated.get_reports import ( GetReports, GetReportsReportData, GetReportsReportDataReports, @@ -164,7 +181,7 @@ GetReportsReportDataReportsDataOwner, GetReportsReportDataReportsDataZone, ) -from .get_world_data import ( +from ._generated.get_world_data import ( GetWorldData, GetWorldDataWorldData, GetWorldDataWorldDataEncounter, @@ -186,7 +203,7 @@ GetWorldDataWorldDataZonesExpansion, GetWorldDataWorldDataZonesPartitions, ) -from .get_zones import ( +from ._generated.get_zones import ( GetZones, GetZonesWorldData, GetZonesWorldDataZones, @@ -195,6 +212,8 @@ GetZonesWorldDataZonesEncounters, GetZonesWorldDataZonesExpansion, ) +from .auth import get_access_token +from .client import Client __all__ = [ "AsyncBaseClient", diff --git a/esologs/input_types.py b/esologs/_generated/__init__.py similarity index 100% rename from esologs/input_types.py rename to esologs/_generated/__init__.py diff --git a/esologs/async_base_client.py b/esologs/_generated/async_base_client.py similarity index 98% rename from esologs/async_base_client.py rename to esologs/_generated/async_base_client.py index be01e12..5358ced 100644 --- a/esologs/async_base_client.py +++ b/esologs/_generated/async_base_client.py @@ -16,9 +16,9 @@ ) try: - from websockets.client import WebSocketClientProtocol - from websockets.client import ( - connect as ws_connect, # type: ignore[import-not-found,unused-ignore] + from websockets.client import ( # type: ignore[import-not-found,unused-ignore] + WebSocketClientProtocol, + connect as ws_connect, ) from websockets.typing import ( # type: ignore[import-not-found,unused-ignore] Data, diff --git a/esologs/base_model.py b/esologs/_generated/base_model.py similarity index 86% rename from esologs/base_model.py rename to esologs/_generated/base_model.py index 68e2f9e..ccde397 100644 --- a/esologs/base_model.py +++ b/esologs/_generated/base_model.py @@ -1,7 +1,6 @@ from io import IOBase -from pydantic import BaseModel as PydanticBaseModel -from pydantic import ConfigDict +from pydantic import BaseModel as PydanticBaseModel, ConfigDict class UnsetType: diff --git a/esologs/enums.py b/esologs/_generated/enums.py similarity index 100% rename from esologs/enums.py rename to esologs/_generated/enums.py diff --git a/esologs/exceptions.py b/esologs/_generated/exceptions.py similarity index 91% rename from esologs/exceptions.py rename to esologs/_generated/exceptions.py index 44d747b..b34acfe 100644 --- a/esologs/exceptions.py +++ b/esologs/_generated/exceptions.py @@ -7,10 +7,6 @@ class GraphQLClientError(Exception): """Base exception.""" -class ValidationError(Exception): - """Raised when parameter validation fails.""" - - class GraphQLClientHttpError(GraphQLClientError): def __init__(self, status_code: int, response: httpx.Response) -> None: self.status_code = status_code @@ -35,13 +31,13 @@ def __init__( locations: Optional[List[Dict[str, int]]] = None, path: Optional[List[str]] = None, extensions: Optional[Dict[str, object]] = None, - original: Optional[Dict[str, object]] = None, + orginal: Optional[Dict[str, object]] = None, ): self.message = message self.locations = locations self.path = path self.extensions = extensions - self.original = original + self.orginal = orginal def __str__(self) -> str: return self.message @@ -53,7 +49,7 @@ def from_dict(cls, error: Dict[str, Any]) -> "GraphQLClientGraphQLError": locations=error.get("locations"), path=error.get("path"), extensions=error.get("extensions"), - original=error, + orginal=error, ) diff --git a/esologs/_generated/generated_client.py b/esologs/_generated/generated_client.py new file mode 100644 index 0000000..7ddc0a0 --- /dev/null +++ b/esologs/_generated/generated_client.py @@ -0,0 +1,1419 @@ +from typing import Any, Dict, List, Optional, Union + +from .async_base_client import AsyncBaseClient +from .base_model import UNSET, UnsetType +from .enums import ( + CharacterRankingMetricType, + EventDataType, + GraphDataType, + HostilityType, + KillType, + RankingCompareType, + RankingTimeframeType, + ReportRankingMetricType, + RoleType, + TableDataType, + ViewType, +) +from .get_abilities import GetAbilities +from .get_ability import GetAbility +from .get_character_by_id import GetCharacterById +from .get_character_encounter_ranking import GetCharacterEncounterRanking +from .get_character_encounter_rankings import GetCharacterEncounterRankings +from .get_character_reports import GetCharacterReports +from .get_character_zone_rankings import GetCharacterZoneRankings +from .get_class import GetClass +from .get_classes import GetClasses +from .get_encounters_by_zone import GetEncountersByZone +from .get_factions import GetFactions +from .get_guild_by_id import GetGuildById +from .get_item import GetItem +from .get_item_set import GetItemSet +from .get_item_sets import GetItemSets +from .get_items import GetItems +from .get_map import GetMap +from .get_maps import GetMaps +from .get_np_cs import GetNPCs +from .get_npc import GetNPC +from .get_rate_limit_data import GetRateLimitData +from .get_regions import GetRegions +from .get_report_by_code import GetReportByCode +from .get_report_events import GetReportEvents +from .get_report_graph import GetReportGraph +from .get_report_player_details import GetReportPlayerDetails +from .get_report_rankings import GetReportRankings +from .get_report_table import GetReportTable +from .get_reports import GetReports +from .get_world_data import GetWorldData +from .get_zones import GetZones + + +def gql(q: str) -> str: + return q + + +class Client(AsyncBaseClient): + async def get_ability(self, id: int, **kwargs: Any) -> GetAbility: + query = gql( + """ + query getAbility($id: Int!) { + gameData { + ability(id: $id) { + id + name + icon + description + } + } + } + """ + ) + variables: Dict[str, object] = {"id": id} + response = await self.execute( + query=query, operation_name="getAbility", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetAbility.model_validate(data) + + async def get_abilities( + self, + limit: Union[Optional[int], UnsetType] = UNSET, + page: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetAbilities: + query = gql( + """ + query getAbilities($limit: Int, $page: Int) { + gameData { + abilities(limit: $limit, page: $page) { + data { + id + name + icon + } + total + per_page + current_page + from + to + last_page + has_more_pages + } + } + } + """ + ) + variables: Dict[str, object] = {"limit": limit, "page": page} + response = await self.execute( + query=query, operation_name="getAbilities", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetAbilities.model_validate(data) + + async def get_class(self, id: int, **kwargs: Any) -> GetClass: + query = gql( + """ + query getClass($id: Int!) { + gameData { + class(id: $id) { + id + name + slug + } + } + } + """ + ) + variables: Dict[str, object] = {"id": id} + response = await self.execute( + query=query, operation_name="getClass", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetClass.model_validate(data) + + async def get_world_data(self, **kwargs: Any) -> GetWorldData: + query = gql( + """ + query getWorldData { + worldData { + encounter { + id + name + } + expansion { + id + name + } + expansions { + id + name + } + region { + id + name + } + regions { + id + name + } + server { + id + name + } + subregion { + id + name + } + zone { + id + name + frozen + expansion { + id + name + } + difficulties { + id + name + sizes + } + encounters { + id + name + } + partitions { + id + name + compactName + default + } + } + zones { + id + name + frozen + expansion { + id + name + } + brackets { + min + max + bucket + type + } + difficulties { + id + name + sizes + } + encounters { + id + name + } + partitions { + id + name + compactName + default + } + } + } + } + """ + ) + variables: Dict[str, object] = {} + response = await self.execute( + query=query, operation_name="getWorldData", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetWorldData.model_validate(data) + + async def get_character_by_id(self, id: int, **kwargs: Any) -> GetCharacterById: + query = gql( + """ + query getCharacterById($id: Int!) { + characterData { + character(id: $id) { + id + name + classID + raceID + guildRank + hidden + server { + name + region { + name + } + } + } + } + } + """ + ) + variables: Dict[str, object] = {"id": id} + response = await self.execute( + query=query, + operation_name="getCharacterById", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return GetCharacterById.model_validate(data) + + async def get_character_reports( + self, + character_id: int, + limit: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetCharacterReports: + query = gql( + """ + query getCharacterReports($characterId: Int!, $limit: Int = 10) { + characterData { + character(id: $characterId) { + recentReports(limit: $limit) { + data { + code + startTime + endTime + zone { + name + } + } + total + per_page + current_page + from + to + last_page + has_more_pages + } + } + } + } + """ + ) + variables: Dict[str, object] = {"characterId": character_id, "limit": limit} + response = await self.execute( + query=query, + operation_name="getCharacterReports", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return GetCharacterReports.model_validate(data) + + async def get_guild_by_id(self, guild_id: int, **kwargs: Any) -> GetGuildById: + query = gql( + """ + query getGuildById($guildId: Int!) { + guildData { + guild(id: $guildId) { + id + name + description + faction { + name + } + server { + name + region { + name + } + } + tags { + id + name + } + } + } + } + """ + ) + variables: Dict[str, object] = {"guildId": guild_id} + response = await self.execute( + query=query, operation_name="getGuildById", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetGuildById.model_validate(data) + + async def get_encounters_by_zone( + self, zone_id: int, **kwargs: Any + ) -> GetEncountersByZone: + query = gql( + """ + query getEncountersByZone($zoneId: Int!) { + worldData { + zone(id: $zoneId) { + id + name + encounters { + id + name + } + } + } + } + """ + ) + variables: Dict[str, object] = {"zoneId": zone_id} + response = await self.execute( + query=query, + operation_name="getEncountersByZone", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return GetEncountersByZone.model_validate(data) + + async def get_regions(self, **kwargs: Any) -> GetRegions: + query = gql( + """ + query getRegions { + worldData { + regions { + id + name + subregions { + id + name + } + } + } + } + """ + ) + variables: Dict[str, object] = {} + response = await self.execute( + query=query, operation_name="getRegions", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetRegions.model_validate(data) + + async def get_report_by_code(self, code: str, **kwargs: Any) -> GetReportByCode: + query = gql( + """ + query getReportByCode($code: String!) { + reportData { + report(code: $code) { + code + startTime + endTime + title + visibility + zone { + name + } + fights { + id + name + difficulty + startTime + endTime + } + } + } + } + """ + ) + variables: Dict[str, object] = {"code": code} + response = await self.execute( + query=query, operation_name="getReportByCode", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetReportByCode.model_validate(data) + + async def get_character_encounter_ranking( + self, character_id: int, encounter_id: int, **kwargs: Any + ) -> GetCharacterEncounterRanking: + query = gql( + """ + query getCharacterEncounterRanking($characterId: Int!, $encounterId: Int!) { + characterData { + character(id: $characterId) { + encounterRankings(encounterID: $encounterId) + } + } + } + """ + ) + variables: Dict[str, object] = { + "characterId": character_id, + "encounterId": encounter_id, + } + response = await self.execute( + query=query, + operation_name="getCharacterEncounterRanking", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return GetCharacterEncounterRanking.model_validate(data) + + async def get_character_encounter_rankings( + self, + character_id: int, + encounter_id: int, + by_bracket: Union[Optional[bool], UnsetType] = UNSET, + class_name: Union[Optional[str], UnsetType] = UNSET, + compare: Union[Optional[RankingCompareType], UnsetType] = UNSET, + difficulty: Union[Optional[int], UnsetType] = UNSET, + include_combatant_info: Union[Optional[bool], UnsetType] = UNSET, + include_private_logs: Union[Optional[bool], UnsetType] = UNSET, + metric: Union[Optional[CharacterRankingMetricType], UnsetType] = UNSET, + partition: Union[Optional[int], UnsetType] = UNSET, + role: Union[Optional[RoleType], UnsetType] = UNSET, + size: Union[Optional[int], UnsetType] = UNSET, + spec_name: Union[Optional[str], UnsetType] = UNSET, + timeframe: Union[Optional[RankingTimeframeType], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetCharacterEncounterRankings: + query = gql( + """ + query getCharacterEncounterRankings($characterId: Int!, $encounterId: Int!, $byBracket: Boolean, $className: String, $compare: RankingCompareType, $difficulty: Int, $includeCombatantInfo: Boolean, $includePrivateLogs: Boolean, $metric: CharacterRankingMetricType, $partition: Int, $role: RoleType, $size: Int, $specName: String, $timeframe: RankingTimeframeType) { + characterData { + character(id: $characterId) { + encounterRankings( + encounterID: $encounterId + byBracket: $byBracket + className: $className + compare: $compare + difficulty: $difficulty + includeCombatantInfo: $includeCombatantInfo + includePrivateLogs: $includePrivateLogs + metric: $metric + partition: $partition + role: $role + size: $size + specName: $specName + timeframe: $timeframe + ) + } + } + } + """ + ) + variables: Dict[str, object] = { + "characterId": character_id, + "encounterId": encounter_id, + "byBracket": by_bracket, + "className": class_name, + "compare": compare, + "difficulty": difficulty, + "includeCombatantInfo": include_combatant_info, + "includePrivateLogs": include_private_logs, + "metric": metric, + "partition": partition, + "role": role, + "size": size, + "specName": spec_name, + "timeframe": timeframe, + } + response = await self.execute( + query=query, + operation_name="getCharacterEncounterRankings", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return GetCharacterEncounterRankings.model_validate(data) + + async def get_character_zone_rankings( + self, + character_id: int, + zone_id: Union[Optional[int], UnsetType] = UNSET, + by_bracket: Union[Optional[bool], UnsetType] = UNSET, + class_name: Union[Optional[str], UnsetType] = UNSET, + compare: Union[Optional[RankingCompareType], UnsetType] = UNSET, + difficulty: Union[Optional[int], UnsetType] = UNSET, + include_private_logs: Union[Optional[bool], UnsetType] = UNSET, + metric: Union[Optional[CharacterRankingMetricType], UnsetType] = UNSET, + partition: Union[Optional[int], UnsetType] = UNSET, + role: Union[Optional[RoleType], UnsetType] = UNSET, + size: Union[Optional[int], UnsetType] = UNSET, + spec_name: Union[Optional[str], UnsetType] = UNSET, + timeframe: Union[Optional[RankingTimeframeType], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetCharacterZoneRankings: + query = gql( + """ + query getCharacterZoneRankings($characterId: Int!, $zoneId: Int, $byBracket: Boolean, $className: String, $compare: RankingCompareType, $difficulty: Int, $includePrivateLogs: Boolean, $metric: CharacterRankingMetricType, $partition: Int, $role: RoleType, $size: Int, $specName: String, $timeframe: RankingTimeframeType) { + characterData { + character(id: $characterId) { + zoneRankings( + zoneID: $zoneId + byBracket: $byBracket + className: $className + compare: $compare + difficulty: $difficulty + includePrivateLogs: $includePrivateLogs + metric: $metric + partition: $partition + role: $role + size: $size + specName: $specName + timeframe: $timeframe + ) + } + } + } + """ + ) + variables: Dict[str, object] = { + "characterId": character_id, + "zoneId": zone_id, + "byBracket": by_bracket, + "className": class_name, + "compare": compare, + "difficulty": difficulty, + "includePrivateLogs": include_private_logs, + "metric": metric, + "partition": partition, + "role": role, + "size": size, + "specName": spec_name, + "timeframe": timeframe, + } + response = await self.execute( + query=query, + operation_name="getCharacterZoneRankings", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return GetCharacterZoneRankings.model_validate(data) + + async def get_zones(self, **kwargs: Any) -> GetZones: + query = gql( + """ + query getZones { + worldData { + zones { + id + name + frozen + brackets { + type + min + max + bucket + } + encounters { + id + name + } + difficulties { + id + name + sizes + } + expansion { + id + name + } + } + } + } + """ + ) + variables: Dict[str, object] = {} + response = await self.execute( + query=query, operation_name="getZones", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetZones.model_validate(data) + + async def get_classes( + self, + faction_id: Union[Optional[int], UnsetType] = UNSET, + zone_id: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetClasses: + query = gql( + """ + query getClasses($faction_id: Int, $zone_id: Int) { + gameData { + classes(faction_id: $faction_id, zone_id: $zone_id) { + id + name + slug + } + } + } + """ + ) + variables: Dict[str, object] = {"faction_id": faction_id, "zone_id": zone_id} + response = await self.execute( + query=query, operation_name="getClasses", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetClasses.model_validate(data) + + async def get_factions(self, **kwargs: Any) -> GetFactions: + query = gql( + """ + query getFactions { + gameData { + factions { + id + name + } + } + } + """ + ) + variables: Dict[str, object] = {} + response = await self.execute( + query=query, operation_name="getFactions", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetFactions.model_validate(data) + + async def get_item(self, id: int, **kwargs: Any) -> GetItem: + query = gql( + """ + query getItem($id: Int!) { + gameData { + item(id: $id) { + id + name + icon + } + } + } + """ + ) + variables: Dict[str, object] = {"id": id} + response = await self.execute( + query=query, operation_name="getItem", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetItem.model_validate(data) + + async def get_item_set(self, id: int, **kwargs: Any) -> GetItemSet: + query = gql( + """ + query getItemSet($id: Int!) { + gameData { + item_set(id: $id) { + id + name + } + } + } + """ + ) + variables: Dict[str, object] = {"id": id} + response = await self.execute( + query=query, operation_name="getItemSet", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetItemSet.model_validate(data) + + async def get_item_sets( + self, + limit: Union[Optional[int], UnsetType] = UNSET, + page: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetItemSets: + query = gql( + """ + query getItemSets($limit: Int, $page: Int) { + gameData { + item_sets(limit: $limit, page: $page) { + data { + id + name + } + total + per_page + current_page + from + to + last_page + has_more_pages + } + } + } + """ + ) + variables: Dict[str, object] = {"limit": limit, "page": page} + response = await self.execute( + query=query, operation_name="getItemSets", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetItemSets.model_validate(data) + + async def get_items( + self, + limit: Union[Optional[int], UnsetType] = UNSET, + page: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetItems: + query = gql( + """ + query getItems($limit: Int, $page: Int) { + gameData { + items(limit: $limit, page: $page) { + data { + id + name + icon + } + total + per_page + current_page + from + to + last_page + has_more_pages + } + } + } + """ + ) + variables: Dict[str, object] = {"limit": limit, "page": page} + response = await self.execute( + query=query, operation_name="getItems", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetItems.model_validate(data) + + async def get_map(self, id: int, **kwargs: Any) -> GetMap: + query = gql( + """ + query getMap($id: Int!) { + gameData { + map(id: $id) { + id + name + } + } + } + """ + ) + variables: Dict[str, object] = {"id": id} + response = await self.execute( + query=query, operation_name="getMap", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetMap.model_validate(data) + + async def get_maps( + self, + limit: Union[Optional[int], UnsetType] = UNSET, + page: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetMaps: + query = gql( + """ + query getMaps($limit: Int, $page: Int) { + gameData { + maps(limit: $limit, page: $page) { + data { + id + name + } + total + per_page + current_page + from + to + last_page + has_more_pages + } + } + } + """ + ) + variables: Dict[str, object] = {"limit": limit, "page": page} + response = await self.execute( + query=query, operation_name="getMaps", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetMaps.model_validate(data) + + async def get_npc(self, id: int, **kwargs: Any) -> GetNPC: + query = gql( + """ + query getNPC($id: Int!) { + gameData { + npc(id: $id) { + id + name + } + } + } + """ + ) + variables: Dict[str, object] = {"id": id} + response = await self.execute( + query=query, operation_name="getNPC", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetNPC.model_validate(data) + + async def get_np_cs( + self, + limit: Union[Optional[int], UnsetType] = UNSET, + page: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetNPCs: + query = gql( + """ + query getNPCs($limit: Int, $page: Int) { + gameData { + npcs(limit: $limit, page: $page) { + data { + id + name + } + total + per_page + current_page + from + to + last_page + has_more_pages + } + } + } + """ + ) + variables: Dict[str, object] = {"limit": limit, "page": page} + response = await self.execute( + query=query, operation_name="getNPCs", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetNPCs.model_validate(data) + + async def get_rate_limit_data(self, **kwargs: Any) -> GetRateLimitData: + query = gql( + """ + query getRateLimitData { + rateLimitData { + limitPerHour + pointsSpentThisHour + pointsResetIn + } + } + """ + ) + variables: Dict[str, object] = {} + response = await self.execute( + query=query, + operation_name="getRateLimitData", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return GetRateLimitData.model_validate(data) + + async def get_report_events( + self, + code: str, + ability_id: Union[Optional[float], UnsetType] = UNSET, + data_type: Union[Optional[EventDataType], UnsetType] = UNSET, + death: Union[Optional[int], UnsetType] = UNSET, + difficulty: Union[Optional[int], UnsetType] = UNSET, + encounter_id: Union[Optional[int], UnsetType] = UNSET, + end_time: Union[Optional[float], UnsetType] = UNSET, + fight_i_ds: Union[Optional[List[Optional[int]]], UnsetType] = UNSET, + filter_expression: Union[Optional[str], UnsetType] = UNSET, + hostility_type: Union[Optional[HostilityType], UnsetType] = UNSET, + include_resources: Union[Optional[bool], UnsetType] = UNSET, + kill_type: Union[Optional[KillType], UnsetType] = UNSET, + limit: Union[Optional[int], UnsetType] = UNSET, + source_auras_absent: Union[Optional[str], UnsetType] = UNSET, + source_auras_present: Union[Optional[str], UnsetType] = UNSET, + source_class: Union[Optional[str], UnsetType] = UNSET, + source_id: Union[Optional[int], UnsetType] = UNSET, + source_instance_id: Union[Optional[int], UnsetType] = UNSET, + start_time: Union[Optional[float], UnsetType] = UNSET, + target_auras_absent: Union[Optional[str], UnsetType] = UNSET, + target_auras_present: Union[Optional[str], UnsetType] = UNSET, + target_class: Union[Optional[str], UnsetType] = UNSET, + target_id: Union[Optional[int], UnsetType] = UNSET, + target_instance_id: Union[Optional[int], UnsetType] = UNSET, + translate: Union[Optional[bool], UnsetType] = UNSET, + use_ability_i_ds: Union[Optional[bool], UnsetType] = UNSET, + use_actor_i_ds: Union[Optional[bool], UnsetType] = UNSET, + view_options: Union[Optional[int], UnsetType] = UNSET, + wipe_cutoff: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetReportEvents: + query = gql( + """ + query getReportEvents($code: String!, $abilityID: Float, $dataType: EventDataType, $death: Int, $difficulty: Int, $encounterID: Int, $endTime: Float, $fightIDs: [Int], $filterExpression: String, $hostilityType: HostilityType, $includeResources: Boolean, $killType: KillType, $limit: Int, $sourceAurasAbsent: String, $sourceAurasPresent: String, $sourceClass: String, $sourceID: Int, $sourceInstanceID: Int, $startTime: Float, $targetAurasAbsent: String, $targetAurasPresent: String, $targetClass: String, $targetID: Int, $targetInstanceID: Int, $translate: Boolean, $useAbilityIDs: Boolean, $useActorIDs: Boolean, $viewOptions: Int, $wipeCutoff: Int) { + reportData { + report(code: $code) { + events( + abilityID: $abilityID + dataType: $dataType + death: $death + difficulty: $difficulty + encounterID: $encounterID + endTime: $endTime + fightIDs: $fightIDs + filterExpression: $filterExpression + hostilityType: $hostilityType + includeResources: $includeResources + killType: $killType + limit: $limit + sourceAurasAbsent: $sourceAurasAbsent + sourceAurasPresent: $sourceAurasPresent + sourceClass: $sourceClass + sourceID: $sourceID + sourceInstanceID: $sourceInstanceID + startTime: $startTime + targetAurasAbsent: $targetAurasAbsent + targetAurasPresent: $targetAurasPresent + targetClass: $targetClass + targetID: $targetID + targetInstanceID: $targetInstanceID + translate: $translate + useAbilityIDs: $useAbilityIDs + useActorIDs: $useActorIDs + viewOptions: $viewOptions + wipeCutoff: $wipeCutoff + ) { + data + nextPageTimestamp + } + } + } + } + """ + ) + variables: Dict[str, object] = { + "code": code, + "abilityID": ability_id, + "dataType": data_type, + "death": death, + "difficulty": difficulty, + "encounterID": encounter_id, + "endTime": end_time, + "fightIDs": fight_i_ds, + "filterExpression": filter_expression, + "hostilityType": hostility_type, + "includeResources": include_resources, + "killType": kill_type, + "limit": limit, + "sourceAurasAbsent": source_auras_absent, + "sourceAurasPresent": source_auras_present, + "sourceClass": source_class, + "sourceID": source_id, + "sourceInstanceID": source_instance_id, + "startTime": start_time, + "targetAurasAbsent": target_auras_absent, + "targetAurasPresent": target_auras_present, + "targetClass": target_class, + "targetID": target_id, + "targetInstanceID": target_instance_id, + "translate": translate, + "useAbilityIDs": use_ability_i_ds, + "useActorIDs": use_actor_i_ds, + "viewOptions": view_options, + "wipeCutoff": wipe_cutoff, + } + response = await self.execute( + query=query, operation_name="getReportEvents", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetReportEvents.model_validate(data) + + async def get_report_graph( + self, + code: str, + ability_id: Union[Optional[float], UnsetType] = UNSET, + data_type: Union[Optional[GraphDataType], UnsetType] = UNSET, + death: Union[Optional[int], UnsetType] = UNSET, + difficulty: Union[Optional[int], UnsetType] = UNSET, + encounter_id: Union[Optional[int], UnsetType] = UNSET, + end_time: Union[Optional[float], UnsetType] = UNSET, + fight_i_ds: Union[Optional[List[Optional[int]]], UnsetType] = UNSET, + filter_expression: Union[Optional[str], UnsetType] = UNSET, + hostility_type: Union[Optional[HostilityType], UnsetType] = UNSET, + kill_type: Union[Optional[KillType], UnsetType] = UNSET, + source_auras_absent: Union[Optional[str], UnsetType] = UNSET, + source_auras_present: Union[Optional[str], UnsetType] = UNSET, + source_class: Union[Optional[str], UnsetType] = UNSET, + source_id: Union[Optional[int], UnsetType] = UNSET, + source_instance_id: Union[Optional[int], UnsetType] = UNSET, + start_time: Union[Optional[float], UnsetType] = UNSET, + target_auras_absent: Union[Optional[str], UnsetType] = UNSET, + target_auras_present: Union[Optional[str], UnsetType] = UNSET, + target_class: Union[Optional[str], UnsetType] = UNSET, + target_id: Union[Optional[int], UnsetType] = UNSET, + target_instance_id: Union[Optional[int], UnsetType] = UNSET, + translate: Union[Optional[bool], UnsetType] = UNSET, + view_options: Union[Optional[int], UnsetType] = UNSET, + view_by: Union[Optional[ViewType], UnsetType] = UNSET, + wipe_cutoff: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetReportGraph: + query = gql( + """ + query getReportGraph($code: String!, $abilityID: Float, $dataType: GraphDataType, $death: Int, $difficulty: Int, $encounterID: Int, $endTime: Float, $fightIDs: [Int], $filterExpression: String, $hostilityType: HostilityType, $killType: KillType, $sourceAurasAbsent: String, $sourceAurasPresent: String, $sourceClass: String, $sourceID: Int, $sourceInstanceID: Int, $startTime: Float, $targetAurasAbsent: String, $targetAurasPresent: String, $targetClass: String, $targetID: Int, $targetInstanceID: Int, $translate: Boolean, $viewOptions: Int, $viewBy: ViewType, $wipeCutoff: Int) { + reportData { + report(code: $code) { + graph( + abilityID: $abilityID + dataType: $dataType + death: $death + difficulty: $difficulty + encounterID: $encounterID + endTime: $endTime + fightIDs: $fightIDs + filterExpression: $filterExpression + hostilityType: $hostilityType + killType: $killType + sourceAurasAbsent: $sourceAurasAbsent + sourceAurasPresent: $sourceAurasPresent + sourceClass: $sourceClass + sourceID: $sourceID + sourceInstanceID: $sourceInstanceID + startTime: $startTime + targetAurasAbsent: $targetAurasAbsent + targetAurasPresent: $targetAurasPresent + targetClass: $targetClass + targetID: $targetID + targetInstanceID: $targetInstanceID + translate: $translate + viewOptions: $viewOptions + viewBy: $viewBy + wipeCutoff: $wipeCutoff + ) + } + } + } + """ + ) + variables: Dict[str, object] = { + "code": code, + "abilityID": ability_id, + "dataType": data_type, + "death": death, + "difficulty": difficulty, + "encounterID": encounter_id, + "endTime": end_time, + "fightIDs": fight_i_ds, + "filterExpression": filter_expression, + "hostilityType": hostility_type, + "killType": kill_type, + "sourceAurasAbsent": source_auras_absent, + "sourceAurasPresent": source_auras_present, + "sourceClass": source_class, + "sourceID": source_id, + "sourceInstanceID": source_instance_id, + "startTime": start_time, + "targetAurasAbsent": target_auras_absent, + "targetAurasPresent": target_auras_present, + "targetClass": target_class, + "targetID": target_id, + "targetInstanceID": target_instance_id, + "translate": translate, + "viewOptions": view_options, + "viewBy": view_by, + "wipeCutoff": wipe_cutoff, + } + response = await self.execute( + query=query, operation_name="getReportGraph", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetReportGraph.model_validate(data) + + async def get_report_table( + self, + code: str, + ability_id: Union[Optional[float], UnsetType] = UNSET, + data_type: Union[Optional[TableDataType], UnsetType] = UNSET, + death: Union[Optional[int], UnsetType] = UNSET, + difficulty: Union[Optional[int], UnsetType] = UNSET, + encounter_id: Union[Optional[int], UnsetType] = UNSET, + end_time: Union[Optional[float], UnsetType] = UNSET, + fight_i_ds: Union[Optional[List[Optional[int]]], UnsetType] = UNSET, + filter_expression: Union[Optional[str], UnsetType] = UNSET, + hostility_type: Union[Optional[HostilityType], UnsetType] = UNSET, + kill_type: Union[Optional[KillType], UnsetType] = UNSET, + source_auras_absent: Union[Optional[str], UnsetType] = UNSET, + source_auras_present: Union[Optional[str], UnsetType] = UNSET, + source_class: Union[Optional[str], UnsetType] = UNSET, + source_id: Union[Optional[int], UnsetType] = UNSET, + source_instance_id: Union[Optional[int], UnsetType] = UNSET, + start_time: Union[Optional[float], UnsetType] = UNSET, + target_auras_absent: Union[Optional[str], UnsetType] = UNSET, + target_auras_present: Union[Optional[str], UnsetType] = UNSET, + target_class: Union[Optional[str], UnsetType] = UNSET, + target_id: Union[Optional[int], UnsetType] = UNSET, + target_instance_id: Union[Optional[int], UnsetType] = UNSET, + translate: Union[Optional[bool], UnsetType] = UNSET, + view_options: Union[Optional[int], UnsetType] = UNSET, + view_by: Union[Optional[ViewType], UnsetType] = UNSET, + wipe_cutoff: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetReportTable: + query = gql( + """ + query getReportTable($code: String!, $abilityID: Float, $dataType: TableDataType, $death: Int, $difficulty: Int, $encounterID: Int, $endTime: Float, $fightIDs: [Int], $filterExpression: String, $hostilityType: HostilityType, $killType: KillType, $sourceAurasAbsent: String, $sourceAurasPresent: String, $sourceClass: String, $sourceID: Int, $sourceInstanceID: Int, $startTime: Float, $targetAurasAbsent: String, $targetAurasPresent: String, $targetClass: String, $targetID: Int, $targetInstanceID: Int, $translate: Boolean, $viewOptions: Int, $viewBy: ViewType, $wipeCutoff: Int) { + reportData { + report(code: $code) { + table( + abilityID: $abilityID + dataType: $dataType + death: $death + difficulty: $difficulty + encounterID: $encounterID + endTime: $endTime + fightIDs: $fightIDs + filterExpression: $filterExpression + hostilityType: $hostilityType + killType: $killType + sourceAurasAbsent: $sourceAurasAbsent + sourceAurasPresent: $sourceAurasPresent + sourceClass: $sourceClass + sourceID: $sourceID + sourceInstanceID: $sourceInstanceID + startTime: $startTime + targetAurasAbsent: $targetAurasAbsent + targetAurasPresent: $targetAurasPresent + targetClass: $targetClass + targetID: $targetID + targetInstanceID: $targetInstanceID + translate: $translate + viewOptions: $viewOptions + viewBy: $viewBy + wipeCutoff: $wipeCutoff + ) + } + } + } + """ + ) + variables: Dict[str, object] = { + "code": code, + "abilityID": ability_id, + "dataType": data_type, + "death": death, + "difficulty": difficulty, + "encounterID": encounter_id, + "endTime": end_time, + "fightIDs": fight_i_ds, + "filterExpression": filter_expression, + "hostilityType": hostility_type, + "killType": kill_type, + "sourceAurasAbsent": source_auras_absent, + "sourceAurasPresent": source_auras_present, + "sourceClass": source_class, + "sourceID": source_id, + "sourceInstanceID": source_instance_id, + "startTime": start_time, + "targetAurasAbsent": target_auras_absent, + "targetAurasPresent": target_auras_present, + "targetClass": target_class, + "targetID": target_id, + "targetInstanceID": target_instance_id, + "translate": translate, + "viewOptions": view_options, + "viewBy": view_by, + "wipeCutoff": wipe_cutoff, + } + response = await self.execute( + query=query, operation_name="getReportTable", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetReportTable.model_validate(data) + + async def get_report_rankings( + self, + code: str, + compare: Union[Optional[RankingCompareType], UnsetType] = UNSET, + difficulty: Union[Optional[int], UnsetType] = UNSET, + encounter_id: Union[Optional[int], UnsetType] = UNSET, + fight_i_ds: Union[Optional[List[Optional[int]]], UnsetType] = UNSET, + player_metric: Union[Optional[ReportRankingMetricType], UnsetType] = UNSET, + timeframe: Union[Optional[RankingTimeframeType], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetReportRankings: + query = gql( + """ + query getReportRankings($code: String!, $compare: RankingCompareType, $difficulty: Int, $encounterID: Int, $fightIDs: [Int], $playerMetric: ReportRankingMetricType, $timeframe: RankingTimeframeType) { + reportData { + report(code: $code) { + rankings( + compare: $compare + difficulty: $difficulty + encounterID: $encounterID + fightIDs: $fightIDs + playerMetric: $playerMetric + timeframe: $timeframe + ) + } + } + } + """ + ) + variables: Dict[str, object] = { + "code": code, + "compare": compare, + "difficulty": difficulty, + "encounterID": encounter_id, + "fightIDs": fight_i_ds, + "playerMetric": player_metric, + "timeframe": timeframe, + } + response = await self.execute( + query=query, + operation_name="getReportRankings", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return GetReportRankings.model_validate(data) + + async def get_report_player_details( + self, + code: str, + difficulty: Union[Optional[int], UnsetType] = UNSET, + encounter_id: Union[Optional[int], UnsetType] = UNSET, + end_time: Union[Optional[float], UnsetType] = UNSET, + fight_i_ds: Union[Optional[List[Optional[int]]], UnsetType] = UNSET, + kill_type: Union[Optional[KillType], UnsetType] = UNSET, + start_time: Union[Optional[float], UnsetType] = UNSET, + translate: Union[Optional[bool], UnsetType] = UNSET, + include_combatant_info: Union[Optional[bool], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetReportPlayerDetails: + query = gql( + """ + query getReportPlayerDetails($code: String!, $difficulty: Int, $encounterID: Int, $endTime: Float, $fightIDs: [Int], $killType: KillType, $startTime: Float, $translate: Boolean, $includeCombatantInfo: Boolean) { + reportData { + report(code: $code) { + playerDetails( + difficulty: $difficulty + encounterID: $encounterID + endTime: $endTime + fightIDs: $fightIDs + killType: $killType + startTime: $startTime + translate: $translate + includeCombatantInfo: $includeCombatantInfo + ) + } + } + } + """ + ) + variables: Dict[str, object] = { + "code": code, + "difficulty": difficulty, + "encounterID": encounter_id, + "endTime": end_time, + "fightIDs": fight_i_ds, + "killType": kill_type, + "startTime": start_time, + "translate": translate, + "includeCombatantInfo": include_combatant_info, + } + response = await self.execute( + query=query, + operation_name="getReportPlayerDetails", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return GetReportPlayerDetails.model_validate(data) + + async def get_reports( + self, + end_time: Union[Optional[float], UnsetType] = UNSET, + guild_id: Union[Optional[int], UnsetType] = UNSET, + guild_name: Union[Optional[str], UnsetType] = UNSET, + guild_server_slug: Union[Optional[str], UnsetType] = UNSET, + guild_server_region: Union[Optional[str], UnsetType] = UNSET, + guild_tag_id: Union[Optional[int], UnsetType] = UNSET, + user_id: Union[Optional[int], UnsetType] = UNSET, + limit: Union[Optional[int], UnsetType] = UNSET, + page: Union[Optional[int], UnsetType] = UNSET, + start_time: Union[Optional[float], UnsetType] = UNSET, + zone_id: Union[Optional[int], UnsetType] = UNSET, + game_zone_id: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetReports: + query = gql( + """ + query getReports($endTime: Float, $guildID: Int, $guildName: String, $guildServerSlug: String, $guildServerRegion: String, $guildTagID: Int, $userID: Int, $limit: Int, $page: Int, $startTime: Float, $zoneID: Int, $gameZoneID: Int) { + reportData { + reports( + endTime: $endTime + guildID: $guildID + guildName: $guildName + guildServerSlug: $guildServerSlug + guildServerRegion: $guildServerRegion + guildTagID: $guildTagID + userID: $userID + limit: $limit + page: $page + startTime: $startTime + zoneID: $zoneID + gameZoneID: $gameZoneID + ) { + data { + code + title + startTime + endTime + zone { + id + name + } + guild { + id + name + server { + name + slug + region { + name + slug + } + } + } + owner { + id + name + } + } + total + per_page + current_page + from + to + last_page + has_more_pages + } + } + } + """ + ) + variables: Dict[str, object] = { + "endTime": end_time, + "guildID": guild_id, + "guildName": guild_name, + "guildServerSlug": guild_server_slug, + "guildServerRegion": guild_server_region, + "guildTagID": guild_tag_id, + "userID": user_id, + "limit": limit, + "page": page, + "startTime": start_time, + "zoneID": zone_id, + "gameZoneID": game_zone_id, + } + response = await self.execute( + query=query, operation_name="getReports", variables=variables, **kwargs + ) + data = self.get_data(response) + return GetReports.model_validate(data) diff --git a/esologs/get_abilities.py b/esologs/_generated/get_abilities.py similarity index 100% rename from esologs/get_abilities.py rename to esologs/_generated/get_abilities.py diff --git a/esologs/get_ability.py b/esologs/_generated/get_ability.py similarity index 100% rename from esologs/get_ability.py rename to esologs/_generated/get_ability.py diff --git a/esologs/get_character_by_id.py b/esologs/_generated/get_character_by_id.py similarity index 100% rename from esologs/get_character_by_id.py rename to esologs/_generated/get_character_by_id.py diff --git a/esologs/get_character_encounter_ranking.py b/esologs/_generated/get_character_encounter_ranking.py similarity index 100% rename from esologs/get_character_encounter_ranking.py rename to esologs/_generated/get_character_encounter_ranking.py diff --git a/esologs/get_character_encounter_rankings.py b/esologs/_generated/get_character_encounter_rankings.py similarity index 100% rename from esologs/get_character_encounter_rankings.py rename to esologs/_generated/get_character_encounter_rankings.py diff --git a/esologs/get_character_reports.py b/esologs/_generated/get_character_reports.py similarity index 100% rename from esologs/get_character_reports.py rename to esologs/_generated/get_character_reports.py diff --git a/esologs/get_character_zone_rankings.py b/esologs/_generated/get_character_zone_rankings.py similarity index 100% rename from esologs/get_character_zone_rankings.py rename to esologs/_generated/get_character_zone_rankings.py diff --git a/esologs/get_class.py b/esologs/_generated/get_class.py similarity index 100% rename from esologs/get_class.py rename to esologs/_generated/get_class.py diff --git a/esologs/get_classes.py b/esologs/_generated/get_classes.py similarity index 100% rename from esologs/get_classes.py rename to esologs/_generated/get_classes.py diff --git a/esologs/get_encounters_by_zone.py b/esologs/_generated/get_encounters_by_zone.py similarity index 100% rename from esologs/get_encounters_by_zone.py rename to esologs/_generated/get_encounters_by_zone.py diff --git a/esologs/get_factions.py b/esologs/_generated/get_factions.py similarity index 100% rename from esologs/get_factions.py rename to esologs/_generated/get_factions.py diff --git a/esologs/get_guild_by_id.py b/esologs/_generated/get_guild_by_id.py similarity index 100% rename from esologs/get_guild_by_id.py rename to esologs/_generated/get_guild_by_id.py diff --git a/esologs/get_item.py b/esologs/_generated/get_item.py similarity index 100% rename from esologs/get_item.py rename to esologs/_generated/get_item.py diff --git a/esologs/get_item_set.py b/esologs/_generated/get_item_set.py similarity index 100% rename from esologs/get_item_set.py rename to esologs/_generated/get_item_set.py diff --git a/esologs/get_item_sets.py b/esologs/_generated/get_item_sets.py similarity index 100% rename from esologs/get_item_sets.py rename to esologs/_generated/get_item_sets.py diff --git a/esologs/get_items.py b/esologs/_generated/get_items.py similarity index 100% rename from esologs/get_items.py rename to esologs/_generated/get_items.py diff --git a/esologs/get_map.py b/esologs/_generated/get_map.py similarity index 100% rename from esologs/get_map.py rename to esologs/_generated/get_map.py diff --git a/esologs/get_maps.py b/esologs/_generated/get_maps.py similarity index 100% rename from esologs/get_maps.py rename to esologs/_generated/get_maps.py diff --git a/esologs/get_npcs.py b/esologs/_generated/get_np_cs.py similarity index 100% rename from esologs/get_npcs.py rename to esologs/_generated/get_np_cs.py diff --git a/esologs/get_npc.py b/esologs/_generated/get_npc.py similarity index 100% rename from esologs/get_npc.py rename to esologs/_generated/get_npc.py diff --git a/esologs/_generated/get_npcs.py b/esologs/_generated/get_npcs.py new file mode 100644 index 0000000..ebe57b4 --- /dev/null +++ b/esologs/_generated/get_npcs.py @@ -0,0 +1,34 @@ +from typing import List, Optional + +from pydantic import Field + +from .base_model import BaseModel + + +class GetNPCs(BaseModel): + game_data: Optional["GetNPCsGameData"] = Field(alias="gameData") + + +class GetNPCsGameData(BaseModel): + npcs: Optional["GetNPCsGameDataNpcs"] + + +class GetNPCsGameDataNpcs(BaseModel): + data: Optional[List[Optional["GetNPCsGameDataNpcsData"]]] + total: int + per_page: int + current_page: int + from_: Optional[int] = Field(alias="from") + to: Optional[int] + last_page: int + has_more_pages: bool + + +class GetNPCsGameDataNpcsData(BaseModel): + id: int + name: Optional[str] + + +GetNPCs.model_rebuild() +GetNPCsGameData.model_rebuild() +GetNPCsGameDataNpcs.model_rebuild() diff --git a/esologs/get_rate_limit_data.py b/esologs/_generated/get_rate_limit_data.py similarity index 100% rename from esologs/get_rate_limit_data.py rename to esologs/_generated/get_rate_limit_data.py diff --git a/esologs/get_regions.py b/esologs/_generated/get_regions.py similarity index 100% rename from esologs/get_regions.py rename to esologs/_generated/get_regions.py diff --git a/esologs/get_report_by_code.py b/esologs/_generated/get_report_by_code.py similarity index 100% rename from esologs/get_report_by_code.py rename to esologs/_generated/get_report_by_code.py diff --git a/esologs/get_report_events.py b/esologs/_generated/get_report_events.py similarity index 100% rename from esologs/get_report_events.py rename to esologs/_generated/get_report_events.py diff --git a/esologs/get_report_graph.py b/esologs/_generated/get_report_graph.py similarity index 100% rename from esologs/get_report_graph.py rename to esologs/_generated/get_report_graph.py diff --git a/esologs/get_report_player_details.py b/esologs/_generated/get_report_player_details.py similarity index 100% rename from esologs/get_report_player_details.py rename to esologs/_generated/get_report_player_details.py diff --git a/esologs/get_report_rankings.py b/esologs/_generated/get_report_rankings.py similarity index 100% rename from esologs/get_report_rankings.py rename to esologs/_generated/get_report_rankings.py diff --git a/esologs/get_report_table.py b/esologs/_generated/get_report_table.py similarity index 100% rename from esologs/get_report_table.py rename to esologs/_generated/get_report_table.py diff --git a/esologs/get_reports.py b/esologs/_generated/get_reports.py similarity index 100% rename from esologs/get_reports.py rename to esologs/_generated/get_reports.py diff --git a/esologs/get_world_data.py b/esologs/_generated/get_world_data.py similarity index 100% rename from esologs/get_world_data.py rename to esologs/_generated/get_world_data.py diff --git a/esologs/get_zones.py b/esologs/_generated/get_zones.py similarity index 100% rename from esologs/get_zones.py rename to esologs/_generated/get_zones.py diff --git a/esologs/_generated/input_types.py b/esologs/_generated/input_types.py new file mode 100644 index 0000000..e69de29 diff --git a/esologs/client.py b/esologs/client.py index 8ff7003..7f120be 100644 --- a/esologs/client.py +++ b/esologs/client.py @@ -1,66 +1,48 @@ -from typing import Any, Dict, List, Optional, Union - -from .async_base_client import AsyncBaseClient -from .base_model import UNSET, UnsetType -from .enums import ( - CharacterRankingMetricType, - EventDataType, - GraphDataType, - HostilityType, - KillType, - RankingCompareType, - RankingTimeframeType, - ReportRankingMetricType, - RoleType, - TableDataType, - ViewType, -) -from .get_abilities import GetAbilities -from .get_ability import GetAbility -from .get_character_by_id import GetCharacterById -from .get_character_encounter_ranking import GetCharacterEncounterRanking -from .get_character_encounter_rankings import GetCharacterEncounterRankings -from .get_character_reports import GetCharacterReports -from .get_character_zone_rankings import GetCharacterZoneRankings -from .get_class import GetClass -from .get_classes import GetClasses -from .get_encounters_by_zone import GetEncountersByZone -from .get_factions import GetFactions -from .get_guild_by_id import GetGuildById -from .get_item import GetItem -from .get_item_set import GetItemSet -from .get_item_sets import GetItemSets -from .get_items import GetItems -from .get_map import GetMap -from .get_maps import GetMaps -from .get_npc import GetNPC -from .get_npcs import GetNPCs -from .get_rate_limit_data import GetRateLimitData -from .get_regions import GetRegions -from .get_report_by_code import GetReportByCode -from .get_report_events import GetReportEvents -from .get_report_graph import GetReportGraph -from .get_report_player_details import GetReportPlayerDetails -from .get_report_rankings import GetReportRankings -from .get_report_table import GetReportTable -from .get_reports import GetReports -from .get_world_data import GetWorldData -from .get_zones import GetZones -from .validators import ( - validate_limit_parameter, - validate_positive_integer, - validate_report_search_params, +""" +Refactored ESO Logs API client using mixins and factory methods. + +This is a cleaner, more maintainable implementation of the client. +""" + +from typing import Any + +from ._generated.async_base_client import AsyncBaseClient +from ._generated.base_model import UNSET, UnsetType +from .mixins import ( + CharacterMixin, + GameDataMixin, + GuildMixin, + ReportMixin, + WorldDataMixin, ) +# Re-export UNSET for backward compatibility +__all__ = ["Client", "UNSET", "UnsetType"] + def gql(q: str) -> str: + """Helper function for GraphQL queries.""" return q -class Client(AsyncBaseClient): +class Client( + AsyncBaseClient, + GameDataMixin, + CharacterMixin, + WorldDataMixin, + GuildMixin, + ReportMixin, +): """ ESO Logs API client with comprehensive validation and security features. + This refactored client uses mixins to organize methods by functional area: + - GameDataMixin: Abilities, items, NPCs, classes, factions, maps + - CharacterMixin: Character info, reports, rankings + - WorldDataMixin: World data, zones, regions, encounters + - GuildMixin: Guild information + - ReportMixin: Combat reports, events, graphs, rankings, analysis + Security Features: - Input validation with length limits to prevent DoS attacks - API key sanitization in error messages @@ -70,1541 +52,37 @@ class Client(AsyncBaseClient): - ESO Logs API has rate limits (typically 300 requests/minute) - Users should implement rate limiting in production applications - Consider using exponential backoff for failed requests - """ - - async def get_ability(self, id: int, **kwargs: Any) -> GetAbility: - query = gql( - """ - query getAbility($id: Int!) { - gameData { - ability(id: $id) { - id - name - icon - description - } - } - } - """ - ) - variables: Dict[str, object] = {"id": id} - response = await self.execute( - query=query, operation_name="getAbility", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetAbility.model_validate(data) - - async def get_abilities( - self, - limit: Union[Optional[int], UnsetType] = UNSET, - page: Union[Optional[int], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetAbilities: - query = gql( - """ - query getAbilities($limit: Int, $page: Int) { - gameData { - abilities(limit: $limit, page: $page) { - data { - id - name - icon - } - total - per_page - current_page - from - to - last_page - has_more_pages - } - } - } - """ - ) - variables: Dict[str, object] = {"limit": limit, "page": page} - response = await self.execute( - query=query, operation_name="getAbilities", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetAbilities.model_validate(data) - - async def get_class(self, id: int, **kwargs: Any) -> GetClass: - query = gql( - """ - query getClass($id: Int!) { - gameData { - class(id: $id) { - id - name - slug - } - } - } - """ - ) - variables: Dict[str, object] = {"id": id} - response = await self.execute( - query=query, operation_name="getClass", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetClass.model_validate(data) - - async def get_world_data(self, **kwargs: Any) -> GetWorldData: - query = gql( - """ - query getWorldData { - worldData { - encounter { - id - name - } - expansion { - id - name - } - expansions { - id - name - } - region { - id - name - } - regions { - id - name - } - server { - id - name - } - subregion { - id - name - } - zone { - id - name - frozen - expansion { - id - name - } - difficulties { - id - name - sizes - } - encounters { - id - name - } - partitions { - id - name - compactName - default - } - } - zones { - id - name - frozen - expansion { - id - name - } - brackets { - min - max - bucket - type - } - difficulties { - id - name - sizes - } - encounters { - id - name - } - partitions { - id - name - compactName - default - } - } - } - } - """ - ) - variables: Dict[str, object] = {} - response = await self.execute( - query=query, operation_name="getWorldData", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetWorldData.model_validate(data) - - async def get_character_by_id(self, id: int, **kwargs: Any) -> GetCharacterById: - query = gql( - """ - query getCharacterById($id: Int!) { - characterData { - character(id: $id) { - id - name - classID - raceID - guildRank - hidden - server { - name - region { - name - } - } - } - } - } - """ - ) - variables: Dict[str, object] = {"id": id} - response = await self.execute( - query=query, - operation_name="getCharacterById", - variables=variables, - **kwargs, - ) - data = self.get_data(response) - return GetCharacterById.model_validate(data) - - async def get_character_reports( - self, - character_id: int, - limit: Union[Optional[int], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetCharacterReports: - query = gql( - """ - query getCharacterReports($characterId: Int!, $limit: Int = 10) { - characterData { - character(id: $characterId) { - recentReports(limit: $limit) { - data { - code - startTime - endTime - zone { - name - } - } - total - per_page - current_page - from - to - last_page - has_more_pages - } - } - } - } - """ - ) - variables: Dict[str, object] = {"characterId": character_id, "limit": limit} - response = await self.execute( - query=query, - operation_name="getCharacterReports", - variables=variables, - **kwargs, - ) - data = self.get_data(response) - return GetCharacterReports.model_validate(data) - - async def get_guild_by_id(self, guild_id: int, **kwargs: Any) -> GetGuildById: - query = gql( - """ - query getGuildById($guildId: Int!) { - guildData { - guild(id: $guildId) { - id - name - description - faction { - name - } - server { - name - region { - name - } - } - tags { - id - name - } - } - } - } - """ - ) - variables: Dict[str, object] = {"guildId": guild_id} - response = await self.execute( - query=query, operation_name="getGuildById", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetGuildById.model_validate(data) - - async def get_encounters_by_zone( - self, zone_id: int, **kwargs: Any - ) -> GetEncountersByZone: - query = gql( - """ - query getEncountersByZone($zoneId: Int!) { - worldData { - zone(id: $zoneId) { - id - name - encounters { - id - name - } - } - } - } - """ - ) - variables: Dict[str, object] = {"zoneId": zone_id} - response = await self.execute( - query=query, - operation_name="getEncountersByZone", - variables=variables, - **kwargs, - ) - data = self.get_data(response) - return GetEncountersByZone.model_validate(data) - - async def get_regions(self, **kwargs: Any) -> GetRegions: - query = gql( - """ - query getRegions { - worldData { - regions { - id - name - subregions { - id - name - } - } - } - } - """ - ) - variables: Dict[str, object] = {} - response = await self.execute( - query=query, operation_name="getRegions", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetRegions.model_validate(data) - - async def get_report_by_code(self, code: str, **kwargs: Any) -> GetReportByCode: - query = gql( - """ - query getReportByCode($code: String!) { - reportData { - report(code: $code) { - code - startTime - endTime - title - visibility - zone { - name - } - fights { - id - name - difficulty - startTime - endTime - } - } - } - } - """ - ) - variables: Dict[str, object] = {"code": code} - response = await self.execute( - query=query, operation_name="getReportByCode", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetReportByCode.model_validate(data) - - async def get_character_encounter_ranking( - self, character_id: int, encounter_id: int, **kwargs: Any - ) -> GetCharacterEncounterRanking: - query = gql( - """ - query getCharacterEncounterRanking($characterId: Int!, $encounterId: Int!) { - characterData { - character(id: $characterId) { - encounterRankings(encounterID: $encounterId) - } - } - } - """ - ) - variables: Dict[str, object] = { - "characterId": character_id, - "encounterId": encounter_id, - } - response = await self.execute( - query=query, - operation_name="getCharacterEncounterRanking", - variables=variables, - **kwargs, - ) - data = self.get_data(response) - return GetCharacterEncounterRanking.model_validate(data) - - async def get_character_encounter_rankings( - self, - character_id: int, - encounter_id: int, - by_bracket: Union[Optional[bool], UnsetType] = UNSET, - class_name: Union[Optional[str], UnsetType] = UNSET, - compare: Union[Optional[RankingCompareType], UnsetType] = UNSET, - difficulty: Union[Optional[int], UnsetType] = UNSET, - include_combatant_info: Union[Optional[bool], UnsetType] = UNSET, - include_private_logs: Union[Optional[bool], UnsetType] = UNSET, - metric: Union[Optional[CharacterRankingMetricType], UnsetType] = UNSET, - partition: Union[Optional[int], UnsetType] = UNSET, - role: Union[Optional[RoleType], UnsetType] = UNSET, - size: Union[Optional[int], UnsetType] = UNSET, - spec_name: Union[Optional[str], UnsetType] = UNSET, - timeframe: Union[Optional[RankingTimeframeType], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetCharacterEncounterRankings: - query = gql( - """ - query getCharacterEncounterRankings($characterId: Int!, $encounterId: Int!, $byBracket: Boolean, $className: String, $compare: RankingCompareType, $difficulty: Int, $includeCombatantInfo: Boolean, $includePrivateLogs: Boolean, $metric: CharacterRankingMetricType, $partition: Int, $role: RoleType, $size: Int, $specName: String, $timeframe: RankingTimeframeType) { - characterData { - character(id: $characterId) { - encounterRankings( - encounterID: $encounterId - byBracket: $byBracket - className: $className - compare: $compare - difficulty: $difficulty - includeCombatantInfo: $includeCombatantInfo - includePrivateLogs: $includePrivateLogs - metric: $metric - partition: $partition - role: $role - size: $size - specName: $specName - timeframe: $timeframe - ) - } - } - } - """ - ) - variables: Dict[str, object] = { - "characterId": character_id, - "encounterId": encounter_id, - "byBracket": by_bracket, - "className": class_name, - "compare": compare, - "difficulty": difficulty, - "includeCombatantInfo": include_combatant_info, - "includePrivateLogs": include_private_logs, - "metric": metric, - "partition": partition, - "role": role, - "size": size, - "specName": spec_name, - "timeframe": timeframe, - } - response = await self.execute( - query=query, - operation_name="getCharacterEncounterRankings", - variables=variables, - **kwargs, - ) - data = self.get_data(response) - return GetCharacterEncounterRankings.model_validate(data) - - async def get_character_zone_rankings( - self, - character_id: int, - zone_id: Union[Optional[int], UnsetType] = UNSET, - by_bracket: Union[Optional[bool], UnsetType] = UNSET, - class_name: Union[Optional[str], UnsetType] = UNSET, - compare: Union[Optional[RankingCompareType], UnsetType] = UNSET, - difficulty: Union[Optional[int], UnsetType] = UNSET, - include_private_logs: Union[Optional[bool], UnsetType] = UNSET, - metric: Union[Optional[CharacterRankingMetricType], UnsetType] = UNSET, - partition: Union[Optional[int], UnsetType] = UNSET, - role: Union[Optional[RoleType], UnsetType] = UNSET, - size: Union[Optional[int], UnsetType] = UNSET, - spec_name: Union[Optional[str], UnsetType] = UNSET, - timeframe: Union[Optional[RankingTimeframeType], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetCharacterZoneRankings: - query = gql( - """ - query getCharacterZoneRankings($characterId: Int!, $zoneId: Int, $byBracket: Boolean, $className: String, $compare: RankingCompareType, $difficulty: Int, $includePrivateLogs: Boolean, $metric: CharacterRankingMetricType, $partition: Int, $role: RoleType, $size: Int, $specName: String, $timeframe: RankingTimeframeType) { - characterData { - character(id: $characterId) { - zoneRankings( - zoneID: $zoneId - byBracket: $byBracket - className: $className - compare: $compare - difficulty: $difficulty - includePrivateLogs: $includePrivateLogs - metric: $metric - partition: $partition - role: $role - size: $size - specName: $specName - timeframe: $timeframe - ) - } - } - } - """ - ) - variables: Dict[str, object] = { - "characterId": character_id, - "zoneId": zone_id, - "byBracket": by_bracket, - "className": class_name, - "compare": compare, - "difficulty": difficulty, - "includePrivateLogs": include_private_logs, - "metric": metric, - "partition": partition, - "role": role, - "size": size, - "specName": spec_name, - "timeframe": timeframe, - } - response = await self.execute( - query=query, - operation_name="getCharacterZoneRankings", - variables=variables, - **kwargs, - ) - data = self.get_data(response) - return GetCharacterZoneRankings.model_validate(data) - - async def get_zones(self, **kwargs: Any) -> GetZones: - query = gql( - """ - query getZones { - worldData { - zones { - id - name - frozen - brackets { - type - min - max - bucket - } - encounters { - id - name - } - difficulties { - id - name - sizes - } - expansion { - id - name - } - } - } - } - """ - ) - variables: Dict[str, object] = {} - response = await self.execute( - query=query, operation_name="getZones", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetZones.model_validate(data) - - async def get_classes( - self, - faction_id: Union[Optional[int], UnsetType] = UNSET, - zone_id: Union[Optional[int], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetClasses: - query = gql( - """ - query getClasses($faction_id: Int, $zone_id: Int) { - gameData { - classes(faction_id: $faction_id, zone_id: $zone_id) { - id - name - slug - } - } - } - """ - ) - variables: Dict[str, object] = {"faction_id": faction_id, "zone_id": zone_id} - response = await self.execute( - query=query, operation_name="getClasses", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetClasses.model_validate(data) - - async def get_factions(self, **kwargs: Any) -> GetFactions: - query = gql( - """ - query getFactions { - gameData { - factions { - id - name - } - } - } - """ - ) - variables: Dict[str, object] = {} - response = await self.execute( - query=query, operation_name="getFactions", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetFactions.model_validate(data) - - async def get_item(self, id: int, **kwargs: Any) -> GetItem: - query = gql( - """ - query getItem($id: Int!) { - gameData { - item(id: $id) { - id - name - icon - } - } - } - """ - ) - variables: Dict[str, object] = {"id": id} - response = await self.execute( - query=query, operation_name="getItem", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetItem.model_validate(data) - - async def get_item_set(self, id: int, **kwargs: Any) -> GetItemSet: - query = gql( - """ - query getItemSet($id: Int!) { - gameData { - item_set(id: $id) { - id - name - } - } - } - """ - ) - variables: Dict[str, object] = {"id": id} - response = await self.execute( - query=query, operation_name="getItemSet", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetItemSet.model_validate(data) - - async def get_item_sets( - self, - limit: Union[Optional[int], UnsetType] = UNSET, - page: Union[Optional[int], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetItemSets: - query = gql( - """ - query getItemSets($limit: Int, $page: Int) { - gameData { - item_sets(limit: $limit, page: $page) { - data { - id - name - } - total - per_page - current_page - from - to - last_page - has_more_pages - } - } - } - """ - ) - variables: Dict[str, object] = {"limit": limit, "page": page} - response = await self.execute( - query=query, operation_name="getItemSets", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetItemSets.model_validate(data) - - async def get_items( - self, - limit: Union[Optional[int], UnsetType] = UNSET, - page: Union[Optional[int], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetItems: - query = gql( - """ - query getItems($limit: Int, $page: Int) { - gameData { - items(limit: $limit, page: $page) { - data { - id - name - icon - } - total - per_page - current_page - from - to - last_page - has_more_pages - } - } - } - """ - ) - variables: Dict[str, object] = {"limit": limit, "page": page} - response = await self.execute( - query=query, operation_name="getItems", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetItems.model_validate(data) - async def get_map(self, id: int, **kwargs: Any) -> GetMap: - query = gql( - """ - query getMap($id: Int!) { - gameData { - map(id: $id) { - id - name - } - } - } - """ - ) - variables: Dict[str, object] = {"id": id} - response = await self.execute( - query=query, operation_name="getMap", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetMap.model_validate(data) + Example: + ```python + from esologs import Client - async def get_maps( - self, - limit: Union[Optional[int], UnsetType] = UNSET, - page: Union[Optional[int], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetMaps: - query = gql( - """ - query getMaps($limit: Int, $page: Int) { - gameData { - maps(limit: $limit, page: $page) { - data { - id - name - } - total - per_page - current_page - from - to - last_page - has_more_pages - } - } - } - """ - ) - variables: Dict[str, object] = {"limit": limit, "page": page} - response = await self.execute( - query=query, operation_name="getMaps", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetMaps.model_validate(data) + async with Client( + client_id="your-client-id", + client_secret="your-client-secret" + ) as client: + # Get ability information + ability = await client.get_ability(id=12345) - async def get_npc(self, id: int, **kwargs: Any) -> GetNPC: - query = gql( - """ - query getNPC($id: Int!) { - gameData { - npc(id: $id) { - id - name - } - } - } - """ - ) - variables: Dict[str, object] = {"id": id} - response = await self.execute( - query=query, operation_name="getNPC", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetNPC.model_validate(data) - - async def get_npcs( - self, - limit: Union[Optional[int], UnsetType] = UNSET, - page: Union[Optional[int], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetNPCs: - query = gql( - """ - query getNPCs($limit: Int, $page: Int) { - gameData { - npcs(limit: $limit, page: $page) { - data { - id - name - } - total - per_page - current_page - from - to - last_page - has_more_pages - } - } - } - """ - ) - variables: Dict[str, object] = {"limit": limit, "page": page} - response = await self.execute( - query=query, operation_name="getNPCs", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetNPCs.model_validate(data) - - async def get_rate_limit_data(self, **kwargs: Any) -> GetRateLimitData: - query = gql( - """ - query getRateLimitData { - rateLimitData { - limitPerHour - pointsSpentThisHour - pointsResetIn - } - } - """ - ) - variables: Dict[str, object] = {} - response = await self.execute( - query=query, - operation_name="getRateLimitData", - variables=variables, - **kwargs, - ) - data = self.get_data(response) - return GetRateLimitData.model_validate(data) - - async def get_report_events( - self, - code: str, - ability_id: Union[Optional[float], UnsetType] = UNSET, - data_type: Union[Optional[EventDataType], UnsetType] = UNSET, - death: Union[Optional[int], UnsetType] = UNSET, - difficulty: Union[Optional[int], UnsetType] = UNSET, - encounter_id: Union[Optional[int], UnsetType] = UNSET, - end_time: Union[Optional[float], UnsetType] = UNSET, - fight_i_ds: Union[Optional[List[Optional[int]]], UnsetType] = UNSET, - filter_expression: Union[Optional[str], UnsetType] = UNSET, - hostility_type: Union[Optional[HostilityType], UnsetType] = UNSET, - include_resources: Union[Optional[bool], UnsetType] = UNSET, - kill_type: Union[Optional[KillType], UnsetType] = UNSET, - limit: Union[Optional[int], UnsetType] = UNSET, - source_auras_absent: Union[Optional[str], UnsetType] = UNSET, - source_auras_present: Union[Optional[str], UnsetType] = UNSET, - source_class: Union[Optional[str], UnsetType] = UNSET, - source_id: Union[Optional[int], UnsetType] = UNSET, - source_instance_id: Union[Optional[int], UnsetType] = UNSET, - start_time: Union[Optional[float], UnsetType] = UNSET, - target_auras_absent: Union[Optional[str], UnsetType] = UNSET, - target_auras_present: Union[Optional[str], UnsetType] = UNSET, - target_class: Union[Optional[str], UnsetType] = UNSET, - target_id: Union[Optional[int], UnsetType] = UNSET, - target_instance_id: Union[Optional[int], UnsetType] = UNSET, - translate: Union[Optional[bool], UnsetType] = UNSET, - use_ability_i_ds: Union[Optional[bool], UnsetType] = UNSET, - use_actor_i_ds: Union[Optional[bool], UnsetType] = UNSET, - view_options: Union[Optional[int], UnsetType] = UNSET, - wipe_cutoff: Union[Optional[int], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetReportEvents: - query = gql( - """ - query getReportEvents($code: String!, $abilityID: Float, $dataType: EventDataType, $death: Int, $difficulty: Int, $encounterID: Int, $endTime: Float, $fightIDs: [Int], $filterExpression: String, $hostilityType: HostilityType, $includeResources: Boolean, $killType: KillType, $limit: Int, $sourceAurasAbsent: String, $sourceAurasPresent: String, $sourceClass: String, $sourceID: Int, $sourceInstanceID: Int, $startTime: Float, $targetAurasAbsent: String, $targetAurasPresent: String, $targetClass: String, $targetID: Int, $targetInstanceID: Int, $translate: Boolean, $useAbilityIDs: Boolean, $useActorIDs: Boolean, $viewOptions: Int, $wipeCutoff: Int) { - reportData { - report(code: $code) { - events( - abilityID: $abilityID - dataType: $dataType - death: $death - difficulty: $difficulty - encounterID: $encounterID - endTime: $endTime - fightIDs: $fightIDs - filterExpression: $filterExpression - hostilityType: $hostilityType - includeResources: $includeResources - killType: $killType - limit: $limit - sourceAurasAbsent: $sourceAurasAbsent - sourceAurasPresent: $sourceAurasPresent - sourceClass: $sourceClass - sourceID: $sourceID - sourceInstanceID: $sourceInstanceID - startTime: $startTime - targetAurasAbsent: $targetAurasAbsent - targetAurasPresent: $targetAurasPresent - targetClass: $targetClass - targetID: $targetID - targetInstanceID: $targetInstanceID - translate: $translate - useAbilityIDs: $useAbilityIDs - useActorIDs: $useActorIDs - viewOptions: $viewOptions - wipeCutoff: $wipeCutoff - ) { - data - nextPageTimestamp - } - } - } - } - """ - ) - variables: Dict[str, object] = { - "code": code, - "abilityID": ability_id, - "dataType": data_type, - "death": death, - "difficulty": difficulty, - "encounterID": encounter_id, - "endTime": end_time, - "fightIDs": fight_i_ds, - "filterExpression": filter_expression, - "hostilityType": hostility_type, - "includeResources": include_resources, - "killType": kill_type, - "limit": limit, - "sourceAurasAbsent": source_auras_absent, - "sourceAurasPresent": source_auras_present, - "sourceClass": source_class, - "sourceID": source_id, - "sourceInstanceID": source_instance_id, - "startTime": start_time, - "targetAurasAbsent": target_auras_absent, - "targetAurasPresent": target_auras_present, - "targetClass": target_class, - "targetID": target_id, - "targetInstanceID": target_instance_id, - "translate": translate, - "useAbilityIDs": use_ability_i_ds, - "useActorIDs": use_actor_i_ds, - "viewOptions": view_options, - "wipeCutoff": wipe_cutoff, - } - response = await self.execute( - query=query, operation_name="getReportEvents", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetReportEvents.model_validate(data) - - async def get_report_graph( - self, - code: str, - ability_id: Union[Optional[float], UnsetType] = UNSET, - data_type: Union[Optional[GraphDataType], UnsetType] = UNSET, - death: Union[Optional[int], UnsetType] = UNSET, - difficulty: Union[Optional[int], UnsetType] = UNSET, - encounter_id: Union[Optional[int], UnsetType] = UNSET, - end_time: Union[Optional[float], UnsetType] = UNSET, - fight_i_ds: Union[Optional[List[Optional[int]]], UnsetType] = UNSET, - filter_expression: Union[Optional[str], UnsetType] = UNSET, - hostility_type: Union[Optional[HostilityType], UnsetType] = UNSET, - kill_type: Union[Optional[KillType], UnsetType] = UNSET, - source_auras_absent: Union[Optional[str], UnsetType] = UNSET, - source_auras_present: Union[Optional[str], UnsetType] = UNSET, - source_class: Union[Optional[str], UnsetType] = UNSET, - source_id: Union[Optional[int], UnsetType] = UNSET, - source_instance_id: Union[Optional[int], UnsetType] = UNSET, - start_time: Union[Optional[float], UnsetType] = UNSET, - target_auras_absent: Union[Optional[str], UnsetType] = UNSET, - target_auras_present: Union[Optional[str], UnsetType] = UNSET, - target_class: Union[Optional[str], UnsetType] = UNSET, - target_id: Union[Optional[int], UnsetType] = UNSET, - target_instance_id: Union[Optional[int], UnsetType] = UNSET, - translate: Union[Optional[bool], UnsetType] = UNSET, - view_options: Union[Optional[int], UnsetType] = UNSET, - view_by: Union[Optional[ViewType], UnsetType] = UNSET, - wipe_cutoff: Union[Optional[int], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetReportGraph: - query = gql( - """ - query getReportGraph($code: String!, $abilityID: Float, $dataType: GraphDataType, $death: Int, $difficulty: Int, $encounterID: Int, $endTime: Float, $fightIDs: [Int], $filterExpression: String, $hostilityType: HostilityType, $killType: KillType, $sourceAurasAbsent: String, $sourceAurasPresent: String, $sourceClass: String, $sourceID: Int, $sourceInstanceID: Int, $startTime: Float, $targetAurasAbsent: String, $targetAurasPresent: String, $targetClass: String, $targetID: Int, $targetInstanceID: Int, $translate: Boolean, $viewOptions: Int, $viewBy: ViewType, $wipeCutoff: Int) { - reportData { - report(code: $code) { - graph( - abilityID: $abilityID - dataType: $dataType - death: $death - difficulty: $difficulty - encounterID: $encounterID - endTime: $endTime - fightIDs: $fightIDs - filterExpression: $filterExpression - hostilityType: $hostilityType - killType: $killType - sourceAurasAbsent: $sourceAurasAbsent - sourceAurasPresent: $sourceAurasPresent - sourceClass: $sourceClass - sourceID: $sourceID - sourceInstanceID: $sourceInstanceID - startTime: $startTime - targetAurasAbsent: $targetAurasAbsent - targetAurasPresent: $targetAurasPresent - targetClass: $targetClass - targetID: $targetID - targetInstanceID: $targetInstanceID - translate: $translate - viewOptions: $viewOptions - viewBy: $viewBy - wipeCutoff: $wipeCutoff - ) - } - } - } - """ - ) - variables: Dict[str, object] = { - "code": code, - "abilityID": ability_id, - "dataType": data_type, - "death": death, - "difficulty": difficulty, - "encounterID": encounter_id, - "endTime": end_time, - "fightIDs": fight_i_ds, - "filterExpression": filter_expression, - "hostilityType": hostility_type, - "killType": kill_type, - "sourceAurasAbsent": source_auras_absent, - "sourceAurasPresent": source_auras_present, - "sourceClass": source_class, - "sourceID": source_id, - "sourceInstanceID": source_instance_id, - "startTime": start_time, - "targetAurasAbsent": target_auras_absent, - "targetAurasPresent": target_auras_present, - "targetClass": target_class, - "targetID": target_id, - "targetInstanceID": target_instance_id, - "translate": translate, - "viewOptions": view_options, - "viewBy": view_by, - "wipeCutoff": wipe_cutoff, - } - response = await self.execute( - query=query, operation_name="getReportGraph", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetReportGraph.model_validate(data) - - async def get_report_table( - self, - code: str, - ability_id: Union[Optional[float], UnsetType] = UNSET, - data_type: Union[Optional[TableDataType], UnsetType] = UNSET, - death: Union[Optional[int], UnsetType] = UNSET, - difficulty: Union[Optional[int], UnsetType] = UNSET, - encounter_id: Union[Optional[int], UnsetType] = UNSET, - end_time: Union[Optional[float], UnsetType] = UNSET, - fight_i_ds: Union[Optional[List[Optional[int]]], UnsetType] = UNSET, - filter_expression: Union[Optional[str], UnsetType] = UNSET, - hostility_type: Union[Optional[HostilityType], UnsetType] = UNSET, - kill_type: Union[Optional[KillType], UnsetType] = UNSET, - source_auras_absent: Union[Optional[str], UnsetType] = UNSET, - source_auras_present: Union[Optional[str], UnsetType] = UNSET, - source_class: Union[Optional[str], UnsetType] = UNSET, - source_id: Union[Optional[int], UnsetType] = UNSET, - source_instance_id: Union[Optional[int], UnsetType] = UNSET, - start_time: Union[Optional[float], UnsetType] = UNSET, - target_auras_absent: Union[Optional[str], UnsetType] = UNSET, - target_auras_present: Union[Optional[str], UnsetType] = UNSET, - target_class: Union[Optional[str], UnsetType] = UNSET, - target_id: Union[Optional[int], UnsetType] = UNSET, - target_instance_id: Union[Optional[int], UnsetType] = UNSET, - translate: Union[Optional[bool], UnsetType] = UNSET, - view_options: Union[Optional[int], UnsetType] = UNSET, - view_by: Union[Optional[ViewType], UnsetType] = UNSET, - wipe_cutoff: Union[Optional[int], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetReportTable: - query = gql( - """ - query getReportTable($code: String!, $abilityID: Float, $dataType: TableDataType, $death: Int, $difficulty: Int, $encounterID: Int, $endTime: Float, $fightIDs: [Int], $filterExpression: String, $hostilityType: HostilityType, $killType: KillType, $sourceAurasAbsent: String, $sourceAurasPresent: String, $sourceClass: String, $sourceID: Int, $sourceInstanceID: Int, $startTime: Float, $targetAurasAbsent: String, $targetAurasPresent: String, $targetClass: String, $targetID: Int, $targetInstanceID: Int, $translate: Boolean, $viewOptions: Int, $viewBy: ViewType, $wipeCutoff: Int) { - reportData { - report(code: $code) { - table( - abilityID: $abilityID - dataType: $dataType - death: $death - difficulty: $difficulty - encounterID: $encounterID - endTime: $endTime - fightIDs: $fightIDs - filterExpression: $filterExpression - hostilityType: $hostilityType - killType: $killType - sourceAurasAbsent: $sourceAurasAbsent - sourceAurasPresent: $sourceAurasPresent - sourceClass: $sourceClass - sourceID: $sourceID - sourceInstanceID: $sourceInstanceID - startTime: $startTime - targetAurasAbsent: $targetAurasAbsent - targetAurasPresent: $targetAurasPresent - targetClass: $targetClass - targetID: $targetID - targetInstanceID: $targetInstanceID - translate: $translate - viewOptions: $viewOptions - viewBy: $viewBy - wipeCutoff: $wipeCutoff - ) - } - } - } - """ - ) - variables: Dict[str, object] = { - "code": code, - "abilityID": ability_id, - "dataType": data_type, - "death": death, - "difficulty": difficulty, - "encounterID": encounter_id, - "endTime": end_time, - "fightIDs": fight_i_ds, - "filterExpression": filter_expression, - "hostilityType": hostility_type, - "killType": kill_type, - "sourceAurasAbsent": source_auras_absent, - "sourceAurasPresent": source_auras_present, - "sourceClass": source_class, - "sourceID": source_id, - "sourceInstanceID": source_instance_id, - "startTime": start_time, - "targetAurasAbsent": target_auras_absent, - "targetAurasPresent": target_auras_present, - "targetClass": target_class, - "targetID": target_id, - "targetInstanceID": target_instance_id, - "translate": translate, - "viewOptions": view_options, - "viewBy": view_by, - "wipeCutoff": wipe_cutoff, - } - response = await self.execute( - query=query, operation_name="getReportTable", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetReportTable.model_validate(data) - - async def get_report_rankings( - self, - code: str, - compare: Union[Optional[RankingCompareType], UnsetType] = UNSET, - difficulty: Union[Optional[int], UnsetType] = UNSET, - encounter_id: Union[Optional[int], UnsetType] = UNSET, - fight_i_ds: Union[Optional[List[Optional[int]]], UnsetType] = UNSET, - player_metric: Union[Optional[ReportRankingMetricType], UnsetType] = UNSET, - timeframe: Union[Optional[RankingTimeframeType], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetReportRankings: - query = gql( - """ - query getReportRankings($code: String!, $compare: RankingCompareType, $difficulty: Int, $encounterID: Int, $fightIDs: [Int], $playerMetric: ReportRankingMetricType, $timeframe: RankingTimeframeType) { - reportData { - report(code: $code) { - rankings( - compare: $compare - difficulty: $difficulty - encounterID: $encounterID - fightIDs: $fightIDs - playerMetric: $playerMetric - timeframe: $timeframe - ) - } - } - } - """ - ) - variables: Dict[str, object] = { - "code": code, - "compare": compare, - "difficulty": difficulty, - "encounterID": encounter_id, - "fightIDs": fight_i_ds, - "playerMetric": player_metric, - "timeframe": timeframe, - } - response = await self.execute( - query=query, - operation_name="getReportRankings", - variables=variables, - **kwargs, - ) - data = self.get_data(response) - return GetReportRankings.model_validate(data) - - async def get_report_player_details( - self, - code: str, - difficulty: Union[Optional[int], UnsetType] = UNSET, - encounter_id: Union[Optional[int], UnsetType] = UNSET, - end_time: Union[Optional[float], UnsetType] = UNSET, - fight_i_ds: Union[Optional[List[Optional[int]]], UnsetType] = UNSET, - kill_type: Union[Optional[KillType], UnsetType] = UNSET, - start_time: Union[Optional[float], UnsetType] = UNSET, - translate: Union[Optional[bool], UnsetType] = UNSET, - include_combatant_info: Union[Optional[bool], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetReportPlayerDetails: - query = gql( - """ - query getReportPlayerDetails($code: String!, $difficulty: Int, $encounterID: Int, $endTime: Float, $fightIDs: [Int], $killType: KillType, $startTime: Float, $translate: Boolean, $includeCombatantInfo: Boolean) { - reportData { - report(code: $code) { - playerDetails( - difficulty: $difficulty - encounterID: $encounterID - endTime: $endTime - fightIDs: $fightIDs - killType: $killType - startTime: $startTime - translate: $translate - includeCombatantInfo: $includeCombatantInfo - ) - } - } - } - """ - ) - variables: Dict[str, object] = { - "code": code, - "difficulty": difficulty, - "encounterID": encounter_id, - "endTime": end_time, - "fightIDs": fight_i_ds, - "killType": kill_type, - "startTime": start_time, - "translate": translate, - "includeCombatantInfo": include_combatant_info, - } - response = await self.execute( - query=query, - operation_name="getReportPlayerDetails", - variables=variables, - **kwargs, - ) - data = self.get_data(response) - return GetReportPlayerDetails.model_validate(data) - - async def get_reports( - self, - end_time: Union[Optional[float], UnsetType] = UNSET, - guild_id: Union[Optional[int], UnsetType] = UNSET, - guild_name: Union[Optional[str], UnsetType] = UNSET, - guild_server_slug: Union[Optional[str], UnsetType] = UNSET, - guild_server_region: Union[Optional[str], UnsetType] = UNSET, - guild_tag_id: Union[Optional[int], UnsetType] = UNSET, - user_id: Union[Optional[int], UnsetType] = UNSET, - limit: Union[Optional[int], UnsetType] = UNSET, - page: Union[Optional[int], UnsetType] = UNSET, - start_time: Union[Optional[float], UnsetType] = UNSET, - zone_id: Union[Optional[int], UnsetType] = UNSET, - game_zone_id: Union[Optional[int], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetReports: - query = gql( - """ - query getReports($endTime: Float, $guildID: Int, $guildName: String, $guildServerSlug: String, $guildServerRegion: String, $guildTagID: Int, $userID: Int, $limit: Int, $page: Int, $startTime: Float, $zoneID: Int, $gameZoneID: Int) { - reportData { - reports( - endTime: $endTime - guildID: $guildID - guildName: $guildName - guildServerSlug: $guildServerSlug - guildServerRegion: $guildServerRegion - guildTagID: $guildTagID - userID: $userID - limit: $limit - page: $page - startTime: $startTime - zoneID: $zoneID - gameZoneID: $gameZoneID - ) { - data { - code - title - startTime - endTime - zone { - id - name - } - guild { - id - name - server { - name - slug - region { - name - slug - } - } - } - owner { - id - name - } - } - total - per_page - current_page - from - to - last_page - has_more_pages - } - } - } - """ - ) - variables: Dict[str, object] = { - "endTime": end_time, - "guildID": guild_id, - "guildName": guild_name, - "guildServerSlug": guild_server_slug, - "guildServerRegion": guild_server_region, - "guildTagID": guild_tag_id, - "userID": user_id, - "limit": limit, - "page": page, - "startTime": start_time, - "zoneID": zone_id, - "gameZoneID": game_zone_id, - } - response = await self.execute( - query=query, operation_name="getReports", variables=variables, **kwargs - ) - data = self.get_data(response) - return GetReports.model_validate(data) - - async def search_reports( - self, - guild_id: Union[Optional[int], UnsetType] = UNSET, - guild_name: Union[Optional[str], UnsetType] = UNSET, - guild_server_slug: Union[Optional[str], UnsetType] = UNSET, - guild_server_region: Union[Optional[str], UnsetType] = UNSET, - guild_tag_id: Union[Optional[int], UnsetType] = UNSET, - user_id: Union[Optional[int], UnsetType] = UNSET, - zone_id: Union[Optional[int], UnsetType] = UNSET, - game_zone_id: Union[Optional[int], UnsetType] = UNSET, - start_time: Union[Optional[float], UnsetType] = UNSET, - end_time: Union[Optional[float], UnsetType] = UNSET, - limit: Union[Optional[int], UnsetType] = UNSET, - page: Union[Optional[int], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetReports: - """ - Search for reports with flexible filtering options. - - Args: - guild_id: Filter by specific guild ID - guild_name: Filter by guild name (requires guild_server_slug and guild_server_region) - guild_server_slug: Guild server slug (required with guild_name) - guild_server_region: Guild server region (required with guild_name) - guild_tag_id: Filter by guild tag/team ID - user_id: Filter by specific user ID - zone_id: Filter by zone ID - game_zone_id: Filter by game zone ID - start_time: Start time filter (UNIX timestamp with milliseconds) - end_time: End time filter (UNIX timestamp with milliseconds) - limit: Number of reports per page (1-25, default 16) - page: Page number (default 1) - - Returns: - GetReports: Paginated list of reports matching the criteria - - Examples: - # Search by guild ID - reports = await client.search_reports(guild_id=123) - - # Search by guild name + # Search for reports reports = await client.search_reports( - guild_name="My Guild", - guild_server_slug="server-name", - guild_server_region="NA" + guild_id=123, + limit=25 ) - # Search with date range - reports = await client.search_reports( - user_id=456, - start_time=1640995200000, # Jan 1, 2022 - end_time=1672531200000 # Jan 1, 2023 + # Get character rankings + rankings = await client.get_character_zone_rankings( + character_id=456, + metric=CharacterRankingMetricType.dps ) - """ - # Validate parameters before making API call - validate_report_search_params( - guild_name=guild_name, - guild_server_slug=guild_server_slug, - guild_server_region=guild_server_region, - limit=limit, - page=page, - start_time=start_time, - end_time=end_time, - **kwargs, - ) - - return await self.get_reports( - end_time=end_time, - guild_id=guild_id, - guild_name=guild_name, - guild_server_slug=guild_server_slug, - guild_server_region=guild_server_region, - guild_tag_id=guild_tag_id, - user_id=user_id, - limit=limit, - page=page, - start_time=start_time, - zone_id=zone_id, - game_zone_id=game_zone_id, - **kwargs, - ) - - async def get_guild_reports( - self, - guild_id: int, - limit: Union[Optional[int], UnsetType] = UNSET, - page: Union[Optional[int], UnsetType] = UNSET, - start_time: Union[Optional[float], UnsetType] = UNSET, - end_time: Union[Optional[float], UnsetType] = UNSET, - zone_id: Union[Optional[int], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetReports: - """ - Convenience method to get reports for a specific guild. - - Args: - guild_id: The guild ID to search for - limit: Number of reports per page (1-25, default 16) - page: Page number (default 1) - start_time: Start time filter (UNIX timestamp with milliseconds) - end_time: End time filter (UNIX timestamp with milliseconds) - zone_id: Filter by specific zone - - Returns: - GetReports: Paginated list of guild reports - - Example: - # Get recent reports for guild - reports = await client.get_guild_reports(guild_id=123, limit=25) - """ - # Validate guild-specific parameters - validate_positive_integer(guild_id, "guild_id") - if limit is not UNSET and limit is not None and isinstance(limit, int): - validate_limit_parameter(limit) - if page is not UNSET and page is not None and isinstance(page, int): - validate_positive_integer(page, "page") - - return await self.search_reports( - guild_id=guild_id, - limit=limit, - page=page, - start_time=start_time, - end_time=end_time, - zone_id=zone_id, - **kwargs, - ) - - async def get_user_reports( - self, - user_id: int, - limit: Union[Optional[int], UnsetType] = UNSET, - page: Union[Optional[int], UnsetType] = UNSET, - start_time: Union[Optional[float], UnsetType] = UNSET, - end_time: Union[Optional[float], UnsetType] = UNSET, - zone_id: Union[Optional[int], UnsetType] = UNSET, - **kwargs: Any, - ) -> GetReports: - """ - Convenience method to get reports for a specific user. - - Args: - user_id: The user ID to search for - limit: Number of reports per page (1-25, default 16) - page: Page number (default 1) - start_time: Start time filter (UNIX timestamp with milliseconds) - end_time: End time filter (UNIX timestamp with milliseconds) - zone_id: Filter by specific zone - - Returns: - GetReports: Paginated list of user reports + ``` + """ - Example: - # Get recent reports for user - reports = await client.get_user_reports(user_id=456, limit=25) - """ - # Validate user-specific parameters - validate_positive_integer(user_id, "user_id") - if limit is not UNSET and limit is not None and isinstance(limit, int): - validate_limit_parameter(limit) - if page is not UNSET and page is not None and isinstance(page, int): - validate_positive_integer(page, "page") + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the client and register all methods from mixins.""" + super().__init__(*args, **kwargs) + # Methods are automatically registered by mixin __init_subclass__ hooks - return await self.search_reports( - user_id=user_id, - limit=limit, - page=page, - start_time=start_time, - end_time=end_time, - zone_id=zone_id, - **kwargs, - ) + def __repr__(self) -> str: + """Return a string representation of the client.""" + return "" diff --git a/esologs/method_factory.py b/esologs/method_factory.py new file mode 100644 index 0000000..99ea7ce --- /dev/null +++ b/esologs/method_factory.py @@ -0,0 +1,415 @@ +""" +Method factory functions for dynamically creating API client methods. + +This module provides factory functions that generate async methods +following common patterns in the ESO Logs API. + +## Method Registration and Naming Conventions + +The factory functions automatically convert GraphQL operation names to Python method names: + +1. **Naming Convention**: camelCase GraphQL operations → snake_case Python methods + - `getAbility` → `get_ability()` + - `getCharacterById` → `get_character_by_id()` + - `getGuildReports` → `get_guild_reports()` + +2. **Method Registration**: Methods are dynamically created and registered on the Client class + through mixin classes using the `__init_subclass__` hook. + +3. **Operation Mapping**: The mapping from Python method to GraphQL operation is stored in: + - `SIMPLE_GETTER_CONFIGS` for single ID parameter methods + - `NO_PARAM_GETTER_CONFIGS` for parameterless methods + - `PAGINATED_GETTER_CONFIGS` for paginated methods + - Direct operation names passed to factory functions for complex methods + +Example: + ```python + # In a mixin class: + method = create_simple_getter( + operation_name="getAbility", # GraphQL operation + return_type=GetAbility, + id_param_name="id" + ) + # Registers as: client.get_ability(id=123) + ``` +""" + +import re +from typing import Any, Callable, Dict, Optional, Protocol, Type, TypeVar, Union, cast + +from ._generated.base_model import UNSET, UnsetType +from .queries import QUERIES + + +class ModelWithValidate(Protocol): + """Protocol for types that have a model_validate class method.""" + + @classmethod + def model_validate(cls, obj: Any) -> Any: + """Validate and create an instance from a dictionary.""" + ... + + +T = TypeVar("T", bound=ModelWithValidate) + +# Cache compiled regex patterns for performance +_CAMEL_TO_SNAKE_PATTERN = re.compile(r"([a-z0-9])([A-Z])") + + +def create_simple_getter( + operation_name: str, + return_type: Type[T], + id_param_name: str = "id", +) -> Callable: + """ + Create a simple getter method that takes a single ID parameter. + + This factory handles methods like: + - get_ability(id) + - get_class(id) + - get_item(id) + - get_guild_by_id(guild_id) + + Args: + operation_name: The GraphQL operation name + return_type: The pydantic model class for the return type + id_param_name: The name of the ID parameter (default: "id") + + Returns: + An async method that executes the query + """ + + # Convert camelCase to snake_case properly + snake_name = _CAMEL_TO_SNAKE_PATTERN.sub(r"\1_\2", operation_name).lower() + + async def method(self: Any, id: Optional[int] = None, **kwargs: Any) -> T: + """Execute a simple ID-based query.""" + # Support both positional and keyword arguments + if id is None: + # Try to get from kwargs using various possible names + if "id" in kwargs: + id = kwargs.pop("id") + elif id_param_name in kwargs: + id = kwargs.pop(id_param_name) + else: + # Try snake_case version of id_param_name + param_key = _CAMEL_TO_SNAKE_PATTERN.sub(r"\1_\2", id_param_name).lower() + if param_key in kwargs: + id = kwargs.pop(param_key) + else: + available_params = list(kwargs.keys()) + param_hint = ( + f" (available: {', '.join(available_params)})" + if available_params + else "" + ) + raise TypeError( + f"{snake_name}() missing required parameter 'id'. " + f"Expected one of: 'id', '{id_param_name}', or '{param_key}'{param_hint}" + ) + + query = QUERIES[operation_name] + variables: Dict[str, object] = {id_param_name: id} + + response = await self.execute( + query=query, operation_name=operation_name, variables=variables + ) + data = self.get_data(response) + return cast(T, return_type.model_validate(data)) + + # Update method metadata + method.__name__ = snake_name + method.__doc__ = f"Get {return_type.__name__} by {id_param_name}." + + return method + + +def create_no_params_getter( + operation_name: str, + return_type: Type[T], +) -> Callable: + """ + Create a getter method that takes no parameters. + + This factory handles methods like: + - get_world_data() + - get_regions() + - get_factions() + - get_rate_limit_data() + + Args: + operation_name: The GraphQL operation name + return_type: The pydantic model class for the return type + + Returns: + An async method that executes the query + """ + + async def method(self: Any, **kwargs: Any) -> T: + """Execute a parameterless query.""" + query = QUERIES[operation_name] + variables: Dict[str, object] = {} + + response = await self.execute( + query=query, operation_name=operation_name, variables=variables + ) + data = self.get_data(response) + return cast(T, return_type.model_validate(data)) + + # Update method metadata + # Convert camelCase to snake_case properly + snake_name = _CAMEL_TO_SNAKE_PATTERN.sub(r"\1_\2", operation_name).lower() + method.__name__ = snake_name + method.__doc__ = f"Get {return_type.__name__}." + + return method + + +def create_paginated_getter( + operation_name: str, + return_type: Type[T], + extra_params: Optional[Dict[str, Type]] = None, +) -> Callable: + """ + Create a paginated getter method with limit and page parameters. + + This factory handles methods like: + - get_abilities(limit, page) + - get_items(limit, page) + - get_classes(faction_id, zone_id) - with extra params + + Args: + operation_name: The GraphQL operation name + return_type: The pydantic model class for the return type + extra_params: Additional parameters beyond limit/page + + Returns: + An async method that executes the paginated query + """ + extra_params = extra_params or {} + + async def method( + self: Any, + limit: Union[Optional[int], UnsetType] = UNSET, + page: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any, + ) -> T: + """Execute a paginated query.""" + query = QUERIES[operation_name] + variables: Dict[str, object] = { + "limit": limit, + "page": page, + } + + # Add any extra parameters from kwargs + for param_name in extra_params: + if param_name in kwargs: + variables[param_name] = kwargs.pop(param_name) + else: + variables[param_name] = UNSET + + response = await self.execute( + query=query, operation_name=operation_name, variables=variables + ) + data = self.get_data(response) + return cast(T, return_type.model_validate(data)) + + # Update method metadata + # Convert camelCase to snake_case properly + snake_name = _CAMEL_TO_SNAKE_PATTERN.sub(r"\1_\2", operation_name).lower() + method.__name__ = snake_name + method.__doc__ = f"Get paginated {return_type.__name__}." + + return method + + +def create_complex_method( + operation_name: str, + return_type: Type[T], + required_params: Dict[str, Type], + optional_params: Optional[Dict[str, Type]] = None, + param_mapping: Optional[Dict[str, str]] = None, +) -> Callable: + """ + Create a method with complex parameter requirements. + + This factory handles methods with many parameters like: + - get_report_events(code, many optional params...) + - get_character_encounter_rankings(character_id, encounter_id, many optional params...) + + Args: + operation_name: The GraphQL operation name + return_type: The pydantic model class for the return type + required_params: Dict of required parameter names to types + optional_params: Dict of optional parameter names to types + param_mapping: Dict to map Python param names to GraphQL names + + Returns: + An async method that executes the complex query + """ + optional_params = optional_params or {} + param_mapping = param_mapping or {} + + # Build the full parameter list for the method signature + + async def method(self: Any, **kwargs: Any) -> T: + """Execute a complex query with many parameters.""" + query = QUERIES[operation_name] + variables: Dict[str, object] = {} + + # Process required parameters + for param_name, param_type in required_params.items(): + if param_name not in kwargs: + available = list(kwargs.keys()) + available_hint = ( + f" Available: {', '.join(available)}" if available else "" + ) + raise TypeError( + f"{snake_name}() missing required parameter '{param_name}' (type: {param_type.__name__}).{available_hint}" + ) + mapped_name = ( + param_mapping.get(param_name, param_name) + if param_mapping + else param_name + ) + variables[mapped_name] = kwargs.pop(param_name) + + # Process optional parameters + if optional_params: + for param_name in optional_params: + value = kwargs.pop(param_name, UNSET) + mapped_name = ( + param_mapping.get(param_name, param_name) + if param_mapping + else param_name + ) + variables[mapped_name] = value + + response = await self.execute( + query=query, operation_name=operation_name, variables=variables + ) + data = self.get_data(response) + return cast(T, return_type.model_validate(data)) + + # Update method metadata + # Convert camelCase to snake_case properly + snake_name = _CAMEL_TO_SNAKE_PATTERN.sub(r"\1_\2", operation_name).lower() + method.__name__ = snake_name + method.__doc__ = f"Execute {operation_name} with complex parameters." + + # This is a simplified version - in production, we'd want to preserve + # the full signature with proper type hints + return method + + +def create_method_with_builder( + operation_name: str, + return_type: Type[T], + param_builder: Callable[..., Dict[str, object]], +) -> Callable: + """ + Create a method that uses a parameter builder function. + + This factory is useful for methods that need custom parameter processing + or validation before execution. + + Args: + operation_name: The GraphQL operation name + return_type: The pydantic model class for the return type + param_builder: Function that builds variables dict from kwargs + + Returns: + An async method that executes the query + """ + + async def method(self: Any, **kwargs: Any) -> T: + """Execute a query with custom parameter building.""" + query = QUERIES[operation_name] + variables = param_builder(**kwargs) + + response = await self.execute( + query=query, operation_name=operation_name, variables=variables + ) + data = self.get_data(response) + return cast(T, return_type.model_validate(data)) + + # Update method metadata + # Convert camelCase to snake_case properly + snake_name = _CAMEL_TO_SNAKE_PATTERN.sub(r"\1_\2", operation_name).lower() + method.__name__ = snake_name + method.__doc__ = f"Execute {operation_name} with custom parameter building." + + return method + + +# Method configuration for simple getters +SIMPLE_GETTER_CONFIGS = { + "get_ability": { + "operation_name": "getAbility", + "id_param_name": "id", + }, + "get_class": { + "operation_name": "getClass", + "id_param_name": "id", + }, + "get_item": { + "operation_name": "getItem", + "id_param_name": "id", + }, + "get_item_set": { + "operation_name": "getItemSet", + "id_param_name": "id", + }, + "get_map": { + "operation_name": "getMap", + "id_param_name": "id", + }, + "get_npc": { + "operation_name": "getNPC", + "id_param_name": "id", + }, + "get_character_by_id": { + "operation_name": "getCharacterById", + "id_param_name": "id", + }, + "get_guild_by_id": { + "operation_name": "getGuildById", + "id_param_name": "guildId", + }, + "get_encounters_by_zone": { + "operation_name": "getEncountersByZone", + "id_param_name": "zoneId", + }, +} + +# Method configuration for no-param getters +NO_PARAM_GETTER_CONFIGS = { + "get_world_data": "getWorldData", + "get_regions": "getRegions", + "get_zones": "getZones", + "get_factions": "getFactions", + "get_rate_limit_data": "getRateLimitData", +} + +# Method configuration for paginated getters +PAGINATED_GETTER_CONFIGS = { + "get_abilities": { + "operation_name": "getAbilities", + }, + "get_items": { + "operation_name": "getItems", + }, + "get_item_sets": { + "operation_name": "getItemSets", + }, + "get_maps": { + "operation_name": "getMaps", + }, + "get_npcs": { + "operation_name": "getNPCs", + }, + "get_classes": { + "operation_name": "getClasses", + "extra_params": {"faction_id": int, "zone_id": int}, + }, +} diff --git a/esologs/mixins/__init__.py b/esologs/mixins/__init__.py new file mode 100644 index 0000000..d6c0cc9 --- /dev/null +++ b/esologs/mixins/__init__.py @@ -0,0 +1,19 @@ +""" +Mixin classes for ESO Logs API client. + +These mixins organize API methods by functional area. +""" + +from .character import CharacterMixin +from .game_data import GameDataMixin +from .guild import GuildMixin +from .report import ReportMixin +from .world_data import WorldDataMixin + +__all__ = [ + "CharacterMixin", + "GameDataMixin", + "GuildMixin", + "ReportMixin", + "WorldDataMixin", +] diff --git a/esologs/mixins/character.py b/esologs/mixins/character.py new file mode 100644 index 0000000..3d654bd --- /dev/null +++ b/esologs/mixins/character.py @@ -0,0 +1,130 @@ +""" +Character related methods for ESO Logs API client. +""" + +from typing import TYPE_CHECKING, Any + +from .._generated.enums import ( + CharacterRankingMetricType, + RankingCompareType, + RankingTimeframeType, + RoleType, +) +from .._generated.get_character_by_id import GetCharacterById +from .._generated.get_character_encounter_ranking import GetCharacterEncounterRanking +from .._generated.get_character_encounter_rankings import GetCharacterEncounterRankings +from .._generated.get_character_reports import GetCharacterReports +from .._generated.get_character_zone_rankings import GetCharacterZoneRankings +from ..method_factory import ( + SIMPLE_GETTER_CONFIGS, + create_complex_method, + create_simple_getter, +) + +if TYPE_CHECKING: + pass + + +class CharacterMixin: + """Mixin providing character related API methods.""" + + def __init_subclass__(cls, **kwargs: Any) -> None: + """Initialize character methods when subclass is created.""" + super().__init_subclass__(**kwargs) + cls._register_character_methods() + + @classmethod + def _register_character_methods(cls) -> None: + """Register all character methods on the class.""" + # Simple getter: get_character_by_id + if "get_character_by_id" in SIMPLE_GETTER_CONFIGS: + config = SIMPLE_GETTER_CONFIGS["get_character_by_id"] + method = create_simple_getter( + operation_name=config["operation_name"], + return_type=GetCharacterById, + id_param_name=config["id_param_name"], + ) + cls.get_character_by_id = method # type: ignore[attr-defined] + + # get_character_reports (has limit parameter) + method = create_complex_method( + operation_name="getCharacterReports", + return_type=GetCharacterReports, + required_params={"character_id": int}, + optional_params={"limit": int}, + param_mapping={"character_id": "characterId"}, + ) + cls.get_character_reports = method # type: ignore[attr-defined] + + # get_character_encounter_ranking (simple version) + method = create_complex_method( + operation_name="getCharacterEncounterRanking", + return_type=GetCharacterEncounterRanking, + required_params={"character_id": int, "encounter_id": int}, + param_mapping={ + "character_id": "characterId", + "encounter_id": "encounterId", + }, + ) + cls.get_character_encounter_ranking = method # type: ignore[attr-defined] + + # get_character_encounter_rankings (complex version with many params) + method = create_complex_method( + operation_name="getCharacterEncounterRankings", + return_type=GetCharacterEncounterRankings, + required_params={"character_id": int, "encounter_id": int}, + optional_params={ + "by_bracket": bool, + "class_name": str, + "compare": RankingCompareType, + "difficulty": int, + "include_combatant_info": bool, + "include_private_logs": bool, + "metric": CharacterRankingMetricType, + "partition": int, + "role": RoleType, + "size": int, + "spec_name": str, + "timeframe": RankingTimeframeType, + }, + param_mapping={ + "character_id": "characterId", + "encounter_id": "encounterId", + "by_bracket": "byBracket", + "class_name": "className", + "spec_name": "specName", + "include_combatant_info": "includeCombatantInfo", + "include_private_logs": "includePrivateLogs", + }, + ) + cls.get_character_encounter_rankings = method # type: ignore[attr-defined] + + # get_character_zone_rankings + method = create_complex_method( + operation_name="getCharacterZoneRankings", + return_type=GetCharacterZoneRankings, + required_params={"character_id": int}, + optional_params={ + "zone_id": int, + "by_bracket": bool, + "class_name": str, + "compare": RankingCompareType, + "difficulty": int, + "include_private_logs": bool, + "metric": CharacterRankingMetricType, + "partition": int, + "role": RoleType, + "size": int, + "spec_name": str, + "timeframe": RankingTimeframeType, + }, + param_mapping={ + "character_id": "characterId", + "zone_id": "zoneId", + "by_bracket": "byBracket", + "class_name": "className", + "spec_name": "specName", + "include_private_logs": "includePrivateLogs", + }, + ) + cls.get_character_zone_rankings = method # type: ignore[attr-defined] diff --git a/esologs/mixins/game_data.py b/esologs/mixins/game_data.py new file mode 100644 index 0000000..e4725e9 --- /dev/null +++ b/esologs/mixins/game_data.py @@ -0,0 +1,114 @@ +""" +Game data related methods for ESO Logs API client. +""" + +from typing import TYPE_CHECKING, Any, Dict, cast + +from .._generated.get_abilities import GetAbilities +from .._generated.get_ability import GetAbility +from .._generated.get_class import GetClass +from .._generated.get_classes import GetClasses +from .._generated.get_factions import GetFactions +from .._generated.get_item import GetItem +from .._generated.get_item_set import GetItemSet +from .._generated.get_item_sets import GetItemSets +from .._generated.get_items import GetItems +from .._generated.get_map import GetMap +from .._generated.get_maps import GetMaps +from .._generated.get_np_cs import GetNPCs +from .._generated.get_npc import GetNPC +from ..method_factory import ( + NO_PARAM_GETTER_CONFIGS, + PAGINATED_GETTER_CONFIGS, + SIMPLE_GETTER_CONFIGS, + create_no_params_getter, + create_paginated_getter, + create_simple_getter, +) + +if TYPE_CHECKING: + pass + + +class GameDataMixin: + """Mixin providing game data related API methods.""" + + def __init_subclass__(cls, **kwargs: Any) -> None: + """Initialize game data methods when subclass is created.""" + super().__init_subclass__(**kwargs) + cls._register_game_data_methods() + + @classmethod + def _register_game_data_methods(cls) -> None: + """Register all game data methods on the class.""" + # Simple getters (single ID parameter) + game_data_simple_getters = { + "get_ability": (GetAbility, "getAbility"), + "get_class": (GetClass, "getClass"), + "get_item": (GetItem, "getItem"), + "get_item_set": (GetItemSet, "getItemSet"), + "get_map": (GetMap, "getMap"), + "get_npc": (GetNPC, "getNPC"), + } + + for method_name, ( + return_type, + operation_name, + ) in game_data_simple_getters.items(): + config = SIMPLE_GETTER_CONFIGS.get( + method_name, {"operation_name": operation_name, "id_param_name": "id"} + ) + method = create_simple_getter( + operation_name=config["operation_name"], + return_type=return_type, + id_param_name=config.get("id_param_name", "id"), + ) + setattr(cls, method_name, method) + + # No parameter getters + if "get_factions" in NO_PARAM_GETTER_CONFIGS: + method = create_no_params_getter( + operation_name=NO_PARAM_GETTER_CONFIGS["get_factions"], + return_type=GetFactions, + ) + cls.get_factions = method # type: ignore[attr-defined] + + # Paginated getters + paginated_getters = { + "get_abilities": GetAbilities, + "get_items": GetItems, + "get_item_sets": GetItemSets, + "get_maps": GetMaps, + "get_npcs": GetNPCs, + } + + for method_name, return_type in paginated_getters.items(): + getter_config = cast( + Dict[str, Any], + PAGINATED_GETTER_CONFIGS.get( + method_name, + { + "operation_name": method_name.replace("get_", "get") + .title() + .replace("_", "") + }, + ), + ) + method = create_paginated_getter( + operation_name=getter_config["operation_name"], + return_type=return_type, + extra_params=getter_config.get("extra_params"), + ) + setattr(cls, method_name, method) + + # Special case: get_classes with extra parameters + if "get_classes" in PAGINATED_GETTER_CONFIGS: + classes_config = cast( + Dict[str, Any], PAGINATED_GETTER_CONFIGS["get_classes"] + ) + method = create_paginated_getter( + operation_name=classes_config["operation_name"], + return_type=GetClasses, + extra_params=classes_config.get("extra_params"), + ) + cls.get_classes = method # type: ignore[attr-defined] diff --git a/esologs/mixins/guild.py b/esologs/mixins/guild.py new file mode 100644 index 0000000..35b957e --- /dev/null +++ b/esologs/mixins/guild.py @@ -0,0 +1,33 @@ +""" +Guild related methods for ESO Logs API client. +""" + +from typing import TYPE_CHECKING, Any + +from .._generated.get_guild_by_id import GetGuildById +from ..method_factory import SIMPLE_GETTER_CONFIGS, create_simple_getter + +if TYPE_CHECKING: + pass + + +class GuildMixin: + """Mixin providing guild related API methods.""" + + def __init_subclass__(cls, **kwargs: Any) -> None: + """Initialize guild methods when subclass is created.""" + super().__init_subclass__(**kwargs) + cls._register_guild_methods() + + @classmethod + def _register_guild_methods(cls) -> None: + """Register all guild methods on the class.""" + # Simple getter: get_guild_by_id + if "get_guild_by_id" in SIMPLE_GETTER_CONFIGS: + config = SIMPLE_GETTER_CONFIGS["get_guild_by_id"] + method = create_simple_getter( + operation_name=config["operation_name"], + return_type=GetGuildById, + id_param_name=config["id_param_name"], + ) + cls.get_guild_by_id = method # type: ignore[attr-defined] diff --git a/esologs/mixins/report.py b/esologs/mixins/report.py new file mode 100644 index 0000000..8ab3a3b --- /dev/null +++ b/esologs/mixins/report.py @@ -0,0 +1,260 @@ +""" +Report related methods for ESO Logs API client. +""" + +from typing import TYPE_CHECKING, Any, Optional, Union + +from .._generated.base_model import UNSET, UnsetType +from .._generated.get_rate_limit_data import GetRateLimitData +from .._generated.get_report_by_code import GetReportByCode +from .._generated.get_report_events import GetReportEvents +from .._generated.get_report_graph import GetReportGraph +from .._generated.get_report_player_details import GetReportPlayerDetails +from .._generated.get_report_rankings import GetReportRankings +from .._generated.get_report_table import GetReportTable +from .._generated.get_reports import GetReports +from ..method_factory import create_method_with_builder, create_no_params_getter +from ..param_builders import ( + build_report_event_params, + build_report_graph_params, + build_report_player_details_params, + build_report_rankings_params, + build_report_search_params, + build_report_table_params, +) +from ..validators import ( + validate_limit_parameter, + validate_positive_integer, + validate_report_search_params, +) + +if TYPE_CHECKING: + pass + + +class ReportMixin: + """Mixin providing report related API methods.""" + + def __init_subclass__(cls, **kwargs: Any) -> None: + """Initialize report methods when subclass is created.""" + super().__init_subclass__(**kwargs) + cls._register_report_methods() + + @classmethod + def _register_report_methods(cls) -> None: + """Register all report methods on the class.""" + + # Simple getter: get_report_by_code (uses 'code' instead of 'id') + async def get_report_by_code( + self: Any, code: str, **kwargs: Any + ) -> GetReportByCode: + from ..queries import QUERIES + + query = QUERIES["getReportByCode"] + variables = {"code": code} + response = await self.execute( + query=query, + operation_name="getReportByCode", + variables=variables, + ) + data = self.get_data(response) + return GetReportByCode.model_validate(data) + + cls.get_report_by_code = get_report_by_code # type: ignore[attr-defined] + + # No params getter: get_rate_limit_data + method = create_no_params_getter( + operation_name="getRateLimitData", return_type=GetRateLimitData + ) + cls.get_rate_limit_data = method # type: ignore[attr-defined] + + # Complex report methods using builders + report_methods = { + "get_report_events": (GetReportEvents, build_report_event_params), + "get_report_graph": (GetReportGraph, build_report_graph_params), + "get_report_table": (GetReportTable, build_report_table_params), + "get_report_rankings": (GetReportRankings, build_report_rankings_params), + "get_report_player_details": ( + GetReportPlayerDetails, + build_report_player_details_params, + ), + "get_reports": (GetReports, build_report_search_params), + } + + for method_name, (return_type, builder) in report_methods.items(): + # Convert snake_case to camelCase for operation names + parts = method_name.split("_") + operation_name = parts[0] + "".join(word.capitalize() for word in parts[1:]) + # Special case mappings if needed + operation_name_map = { + "getReports": "getReports", # Already correct + } + operation_name = operation_name_map.get(operation_name, operation_name) + + method = create_method_with_builder( + operation_name=operation_name, + return_type=return_type, + param_builder=builder, + ) + setattr(cls, method_name, method) + + # Convenience methods that wrap get_reports + cls._register_report_convenience_methods() + + @classmethod + def _register_report_convenience_methods(cls) -> None: + """Register convenience methods for report searching.""" + + async def search_reports( + self: Any, + guild_id: Union[Optional[int], UnsetType] = UNSET, + guild_name: Union[Optional[str], UnsetType] = UNSET, + guild_server_slug: Union[Optional[str], UnsetType] = UNSET, + guild_server_region: Union[Optional[str], UnsetType] = UNSET, + guild_tag_id: Union[Optional[int], UnsetType] = UNSET, + user_id: Union[Optional[int], UnsetType] = UNSET, + zone_id: Union[Optional[int], UnsetType] = UNSET, + game_zone_id: Union[Optional[int], UnsetType] = UNSET, + start_time: Union[Optional[float], UnsetType] = UNSET, + end_time: Union[Optional[float], UnsetType] = UNSET, + limit: Union[Optional[int], UnsetType] = UNSET, + page: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetReports: + """Search for reports with flexible filtering options.""" + # Validate parameters before making API call + validate_report_search_params( + guild_name=guild_name, + guild_server_slug=guild_server_slug, + guild_server_region=guild_server_region, + limit=limit, + page=page, + start_time=start_time, + end_time=end_time, + **kwargs, + ) + + return await self.get_reports( + end_time=end_time, + guild_id=guild_id, + guild_name=guild_name, + guild_server_slug=guild_server_slug, + guild_server_region=guild_server_region, + guild_tag_id=guild_tag_id, + user_id=user_id, + limit=limit, + page=page, + start_time=start_time, + zone_id=zone_id, + game_zone_id=game_zone_id, + ) + + async def get_guild_reports( + self: Any, + guild_id: int, + limit: Union[Optional[int], UnsetType] = UNSET, + page: Union[Optional[int], UnsetType] = UNSET, + start_time: Union[Optional[float], UnsetType] = UNSET, + end_time: Union[Optional[float], UnsetType] = UNSET, + zone_id: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetReports: + """Convenience method to get reports for a specific guild.""" + # Validate guild-specific parameters + validate_positive_integer(guild_id, "guild_id") + if limit is not UNSET and limit is not None and isinstance(limit, int): + validate_limit_parameter(limit) + if page is not UNSET and page is not None and isinstance(page, int): + validate_positive_integer(page, "page") + + return await self.search_reports( + guild_id=guild_id, + limit=limit, + page=page, + start_time=start_time, + end_time=end_time, + zone_id=zone_id, + ) + + async def get_user_reports( + self: Any, + user_id: int, + limit: Union[Optional[int], UnsetType] = UNSET, + page: Union[Optional[int], UnsetType] = UNSET, + start_time: Union[Optional[float], UnsetType] = UNSET, + end_time: Union[Optional[float], UnsetType] = UNSET, + zone_id: Union[Optional[int], UnsetType] = UNSET, + **kwargs: Any, + ) -> GetReports: + """Convenience method to get reports for a specific user.""" + # Validate user-specific parameters + validate_positive_integer(user_id, "user_id") + if limit is not UNSET and limit is not None and isinstance(limit, int): + validate_limit_parameter(limit) + if page is not UNSET and page is not None and isinstance(page, int): + validate_positive_integer(page, "page") + + return await self.search_reports( + user_id=user_id, + limit=limit, + page=page, + start_time=start_time, + end_time=end_time, + zone_id=zone_id, + ) + + # Add docstrings + search_reports.__doc__ = """ + Search for reports with flexible filtering options. + + Args: + guild_id: Filter by specific guild ID + guild_name: Filter by guild name (requires guild_server_slug and guild_server_region) + guild_server_slug: Guild server slug (required with guild_name) + guild_server_region: Guild server region (required with guild_name) + guild_tag_id: Filter by guild tag/team ID + user_id: Filter by specific user ID + zone_id: Filter by zone ID + game_zone_id: Filter by game zone ID + start_time: Start time filter (UNIX timestamp with milliseconds) + end_time: End time filter (UNIX timestamp with milliseconds) + limit: Number of reports per page (1-25, default 16) + page: Page number (default 1) + + Returns: + GetReports: Paginated list of reports matching the criteria + """ + + get_guild_reports.__doc__ = """ + Convenience method to get reports for a specific guild. + + Args: + guild_id: The guild ID to search for + limit: Number of reports per page (1-25, default 16) + page: Page number (default 1) + start_time: Start time filter (UNIX timestamp with milliseconds) + end_time: End time filter (UNIX timestamp with milliseconds) + zone_id: Filter by specific zone + + Returns: + GetReports: Paginated list of guild reports + """ + + get_user_reports.__doc__ = """ + Convenience method to get reports for a specific user. + + Args: + user_id: The user ID to search for + limit: Number of reports per page (1-25, default 16) + page: Page number (default 1) + start_time: Start time filter (UNIX timestamp with milliseconds) + end_time: End time filter (UNIX timestamp with milliseconds) + zone_id: Filter by specific zone + + Returns: + GetReports: Paginated list of user reports + """ + + cls.search_reports = search_reports # type: ignore[attr-defined] + cls.get_guild_reports = get_guild_reports # type: ignore[attr-defined] + cls.get_user_reports = get_user_reports # type: ignore[attr-defined] diff --git a/esologs/mixins/world_data.py b/esologs/mixins/world_data.py new file mode 100644 index 0000000..5190187 --- /dev/null +++ b/esologs/mixins/world_data.py @@ -0,0 +1,56 @@ +""" +World data related methods for ESO Logs API client. +""" + +from typing import TYPE_CHECKING, Any + +from .._generated.get_encounters_by_zone import GetEncountersByZone +from .._generated.get_regions import GetRegions +from .._generated.get_world_data import GetWorldData +from .._generated.get_zones import GetZones +from ..method_factory import ( + NO_PARAM_GETTER_CONFIGS, + SIMPLE_GETTER_CONFIGS, + create_no_params_getter, + create_simple_getter, +) + +if TYPE_CHECKING: + pass + + +class WorldDataMixin: + """Mixin providing world data related API methods.""" + + def __init_subclass__(cls, **kwargs: Any) -> None: + """Initialize world data methods when subclass is created.""" + super().__init_subclass__(**kwargs) + cls._register_world_data_methods() + + @classmethod + def _register_world_data_methods(cls) -> None: + """Register all world data methods on the class.""" + # No parameter getters + no_param_methods = { + "get_world_data": GetWorldData, + "get_zones": GetZones, + "get_regions": GetRegions, + } + + for method_name, return_type in no_param_methods.items(): + if method_name in NO_PARAM_GETTER_CONFIGS: + operation_name = NO_PARAM_GETTER_CONFIGS[method_name] + method = create_no_params_getter( + operation_name=operation_name, return_type=return_type + ) + setattr(cls, method_name, method) + + # Simple getter: get_encounters_by_zone + if "get_encounters_by_zone" in SIMPLE_GETTER_CONFIGS: + config = SIMPLE_GETTER_CONFIGS["get_encounters_by_zone"] + method = create_simple_getter( + operation_name=config["operation_name"], + return_type=GetEncountersByZone, + id_param_name=config["id_param_name"], + ) + cls.get_encounters_by_zone = method # type: ignore[attr-defined] diff --git a/esologs/param_builders.py b/esologs/param_builders.py new file mode 100644 index 0000000..e8ad916 --- /dev/null +++ b/esologs/param_builders.py @@ -0,0 +1,330 @@ +""" +Parameter builder utilities for ESO Logs API client. + +This module provides functions and classes for building and validating +complex parameter sets for API methods. +""" + +from typing import Any, Dict, List, Optional, Union + +from ._generated.base_model import UNSET, UnsetType +from ._generated.enums import ( + CharacterRankingMetricType, + RankingCompareType, + RankingTimeframeType, + RoleType, +) + + +class ParameterBuilder: + """Base class for parameter builders.""" + + def __init__(self) -> None: + self.params: Dict[str, Any] = {} + + def add_param(self, name: str, value: Any) -> "ParameterBuilder": + """Add a parameter if it's not UNSET.""" + if value is not UNSET: + self.params[name] = value + return self + + def build(self) -> Dict[str, object]: + """Build the final parameter dictionary.""" + return self.params + + +class ReportFilterBuilder(ParameterBuilder): + """Builder for report filtering parameters used across multiple methods.""" + + def __init__(self) -> None: + super().__init__() + self._param_mapping = { + "fight_i_ds": "fightIDs", + "encounter_id": "encounterID", + "ability_id": "abilityID", + "source_id": "sourceID", + "target_id": "targetID", + "source_instance_id": "sourceInstanceID", + "target_instance_id": "targetInstanceID", + "start_time": "startTime", + "end_time": "endTime", + "data_type": "dataType", + "hostility_type": "hostilityType", + "kill_type": "killType", + "wipe_cutoff": "wipeCutoff", + "filter_expression": "filterExpression", + "source_class": "sourceClass", + "target_class": "targetClass", + "source_auras_present": "sourceAurasPresent", + "source_auras_absent": "sourceAurasAbsent", + "target_auras_present": "targetAurasPresent", + "target_auras_absent": "targetAurasAbsent", + "include_resources": "includeResources", + "use_ability_i_ds": "useAbilityIDs", + "use_actor_i_ds": "useActorIDs", + "view_options": "viewOptions", + "view_by": "viewBy", + } + + def add_time_range( + self, + start_time: Union[Optional[float], UnsetType] = UNSET, + end_time: Union[Optional[float], UnsetType] = UNSET, + ) -> "ReportFilterBuilder": + """Add time range parameters.""" + self.add_param("startTime", start_time).add_param("endTime", end_time) + return self + + def add_combat_filters( + self, + ability_id: Union[Optional[float], UnsetType] = UNSET, + source_id: Union[Optional[int], UnsetType] = UNSET, + target_id: Union[Optional[int], UnsetType] = UNSET, + source_class: Union[Optional[str], UnsetType] = UNSET, + target_class: Union[Optional[str], UnsetType] = UNSET, + ) -> "ReportFilterBuilder": + """Add combat-related filters.""" + self.add_param("abilityID", ability_id) + self.add_param("sourceID", source_id) + self.add_param("targetID", target_id) + self.add_param("sourceClass", source_class) + self.add_param("targetClass", target_class) + return self + + def add_aura_filters( + self, + source_auras_present: Union[Optional[str], UnsetType] = UNSET, + source_auras_absent: Union[Optional[str], UnsetType] = UNSET, + target_auras_present: Union[Optional[str], UnsetType] = UNSET, + target_auras_absent: Union[Optional[str], UnsetType] = UNSET, + ) -> "ReportFilterBuilder": + """Add aura-based filters.""" + self.add_param("sourceAurasPresent", source_auras_present) + self.add_param("sourceAurasAbsent", source_auras_absent) + self.add_param("targetAurasPresent", target_auras_present) + self.add_param("targetAurasAbsent", target_auras_absent) + return self + + def build_from_kwargs(self, **kwargs: Any) -> Dict[str, object]: + """Build parameters from kwargs with proper mapping.""" + result = {} + + for python_name, graphql_name in self._param_mapping.items(): + if python_name in kwargs: + value = kwargs[python_name] + if value is not UNSET: + result[graphql_name] = value + + # Handle parameters that don't need mapping + for param in ["code", "death", "difficulty", "limit", "translate"]: + if param in kwargs and kwargs[param] is not UNSET: + result[param] = kwargs[param] + + return result + + +class RankingParameterBuilder(ParameterBuilder): + """Builder for ranking-related parameters.""" + + def __init__(self) -> None: + super().__init__() + self._param_mapping = { + "character_id": "characterId", + "encounter_id": "encounterId", + "zone_id": "zoneId", + "by_bracket": "byBracket", + "class_name": "className", + "spec_name": "specName", + "include_combatant_info": "includeCombatantInfo", + "include_private_logs": "includePrivateLogs", + } + + def add_ranking_filters( + self, + metric: Union[Optional[CharacterRankingMetricType], UnsetType] = UNSET, + partition: Union[Optional[int], UnsetType] = UNSET, + timeframe: Union[Optional[RankingTimeframeType], UnsetType] = UNSET, + compare: Union[Optional[RankingCompareType], UnsetType] = UNSET, + ) -> "RankingParameterBuilder": + """Add ranking-specific filters.""" + self.add_param("metric", metric) + self.add_param("partition", partition) + self.add_param("timeframe", timeframe) + self.add_param("compare", compare) + return self + + def add_class_filters( + self, + class_name: Union[Optional[str], UnsetType] = UNSET, + spec_name: Union[Optional[str], UnsetType] = UNSET, + role: Union[Optional[RoleType], UnsetType] = UNSET, + ) -> "RankingParameterBuilder": + """Add class/spec/role filters.""" + self.add_param("className", class_name) + self.add_param("specName", spec_name) + self.add_param("role", role) + return self + + def build_from_kwargs(self, **kwargs: Any) -> Dict[str, object]: + """Build parameters from kwargs with proper mapping.""" + result = {} + + for python_name, graphql_name in self._param_mapping.items(): + if python_name in kwargs: + value = kwargs[python_name] + if value is not UNSET: + result[graphql_name] = value + + # Handle parameters that don't need mapping + direct_params = [ + "metric", + "partition", + "timeframe", + "compare", + "difficulty", + "size", + "role", + ] + for param in direct_params: + if param in kwargs and kwargs[param] is not UNSET: + result[param] = kwargs[param] + + return result + + +# Convenience functions for building specific parameter sets + + +def build_report_event_params(**kwargs: Any) -> Dict[str, object]: + """Build parameters for get_report_events method.""" + builder = ReportFilterBuilder() + return builder.build_from_kwargs(**kwargs) + + +def build_report_graph_params(**kwargs: Any) -> Dict[str, object]: + """Build parameters for get_report_graph method.""" + builder = ReportFilterBuilder() + return builder.build_from_kwargs(**kwargs) + + +def build_report_table_params(**kwargs: Any) -> Dict[str, object]: + """Build parameters for get_report_table method.""" + builder = ReportFilterBuilder() + return builder.build_from_kwargs(**kwargs) + + +def build_character_ranking_params(**kwargs: Any) -> Dict[str, object]: + """Build parameters for character ranking methods.""" + builder = RankingParameterBuilder() + return builder.build_from_kwargs(**kwargs) + + +def build_report_search_params(**kwargs: Any) -> Dict[str, object]: + """Build parameters for report search methods.""" + param_mapping = { + "guild_id": "guildID", + "guild_name": "guildName", + "guild_server_slug": "guildServerSlug", + "guild_server_region": "guildServerRegion", + "guild_tag_id": "guildTagID", + "user_id": "userID", + "zone_id": "zoneID", + "game_zone_id": "gameZoneID", + "start_time": "startTime", + "end_time": "endTime", + } + + result = {} + for python_name, graphql_name in param_mapping.items(): + if python_name in kwargs: + value = kwargs[python_name] + if value is not UNSET: + result[graphql_name] = value + + # Handle direct params + for param in ["limit", "page"]: + if param in kwargs and kwargs[param] is not UNSET: + result[param] = kwargs[param] + + return result + + +def build_report_player_details_params(**kwargs: Any) -> Dict[str, object]: + """Build parameters for get_report_player_details method.""" + param_mapping = { + "encounter_id": "encounterID", + "fight_i_ds": "fightIDs", + "kill_type": "killType", + "start_time": "startTime", + "end_time": "endTime", + "include_combatant_info": "includeCombatantInfo", + } + + result = {"code": kwargs["code"]} + + for python_name, graphql_name in param_mapping.items(): + if python_name in kwargs: + value = kwargs[python_name] + if value is not UNSET: + result[graphql_name] = value + + # Handle direct params + for param in ["difficulty", "translate"]: + if param in kwargs and kwargs[param] is not UNSET: + result[param] = kwargs[param] + + return result + + +def build_report_rankings_params(**kwargs: Any) -> Dict[str, object]: + """Build parameters for get_report_rankings method.""" + param_mapping = { + "encounter_id": "encounterID", + "fight_i_ds": "fightIDs", + "player_metric": "playerMetric", + } + + result = {"code": kwargs["code"]} + + for python_name, graphql_name in param_mapping.items(): + if python_name in kwargs: + value = kwargs[python_name] + if value is not UNSET: + result[graphql_name] = value + + # Handle direct params + for param in ["compare", "difficulty", "timeframe"]: + if param in kwargs and kwargs[param] is not UNSET: + result[param] = kwargs[param] + + return result + + +# Parameter validation helpers + + +def validate_param_combination( + params: Dict[str, Any], required_together: List[List[str]] +) -> None: + """ + Validate that certain parameters are provided together. + + Args: + params: The parameter dictionary + required_together: List of parameter groups that must be provided together + + Raises: + ValueError: If required parameters are not provided together + """ + for group in required_together: + provided = [p for p in group if params.get(p) not in (None, UNSET)] + if provided and len(provided) != len(group): + raise ValueError( + f"Parameters {group} must be provided together. " + f"Only got: {provided}" + ) + + +def clean_unset_params(params: Dict[str, Any]) -> Dict[str, Any]: + """Remove UNSET values from a parameter dictionary.""" + return {k: v for k, v in params.items() if v is not UNSET} diff --git a/esologs/queries.py b/esologs/queries.py new file mode 100644 index 0000000..5bdcacd --- /dev/null +++ b/esologs/queries.py @@ -0,0 +1,766 @@ +""" +GraphQL queries for ESO Logs API. + +This module contains all GraphQL query strings used by the client. +Queries are organized by their functional area for easy maintenance. +""" + +# Game Data Queries +GET_ABILITY = """ +query getAbility($id: Int!) { + gameData { + ability(id: $id) { + id + name + icon + description + } + } +} +""" + +GET_ABILITIES = """ +query getAbilities($limit: Int, $page: Int) { + gameData { + abilities(limit: $limit, page: $page) { + data { + id + name + icon + } + total + per_page + current_page + from + to + last_page + has_more_pages + } + } +} +""" + +GET_CLASS = """ +query getClass($id: Int!) { + gameData { + class(id: $id) { + id + name + slug + } + } +} +""" + +GET_CLASSES = """ +query getClasses($faction_id: Int, $zone_id: Int) { + gameData { + classes(faction_id: $faction_id, zone_id: $zone_id) { + id + name + slug + } + } +} +""" + +GET_FACTIONS = """ +query getFactions { + gameData { + factions { + id + name + } + } +} +""" + +GET_ITEM = """ +query getItem($id: Int!) { + gameData { + item(id: $id) { + id + name + icon + } + } +} +""" + +GET_ITEMS = """ +query getItems($limit: Int, $page: Int) { + gameData { + items(limit: $limit, page: $page) { + data { + id + name + icon + } + total + per_page + current_page + from + to + last_page + has_more_pages + } + } +} +""" + +GET_ITEM_SET = """ +query getItemSet($id: Int!) { + gameData { + item_set(id: $id) { + id + name + } + } +} +""" + +GET_ITEM_SETS = """ +query getItemSets($limit: Int, $page: Int) { + gameData { + item_sets(limit: $limit, page: $page) { + data { + id + name + } + total + per_page + current_page + from + to + last_page + has_more_pages + } + } +} +""" + +GET_MAP = """ +query getMap($id: Int!) { + gameData { + map(id: $id) { + id + name + } + } +} +""" + +GET_MAPS = """ +query getMaps($limit: Int, $page: Int) { + gameData { + maps(limit: $limit, page: $page) { + data { + id + name + } + total + per_page + current_page + from + to + last_page + has_more_pages + } + } +} +""" + +GET_NPC = """ +query getNPC($id: Int!) { + gameData { + npc(id: $id) { + id + name + } + } +} +""" + +GET_NPCS = """ +query getNPCs($limit: Int, $page: Int) { + gameData { + npcs(limit: $limit, page: $page) { + data { + id + name + } + total + per_page + current_page + from + to + last_page + has_more_pages + } + } +} +""" + +# World Data Queries +GET_WORLD_DATA = """ +query getWorldData { + worldData { + encounter { + id + name + } + expansion { + id + name + } + expansions { + id + name + } + region { + id + name + } + regions { + id + name + } + server { + id + name + } + subregion { + id + name + } + zone { + id + name + frozen + expansion { + id + name + } + difficulties { + id + name + sizes + } + encounters { + id + name + } + partitions { + id + name + compactName + default + } + } + zones { + id + name + frozen + expansion { + id + name + } + brackets { + min + max + bucket + type + } + difficulties { + id + name + sizes + } + encounters { + id + name + } + partitions { + id + name + compactName + default + } + } + } +} +""" + +GET_ZONES = """ +query getZones { + worldData { + zones { + id + name + frozen + brackets { + type + min + max + bucket + } + encounters { + id + name + } + difficulties { + id + name + sizes + } + expansion { + id + name + } + } + } +} +""" + +GET_ENCOUNTERS_BY_ZONE = """ +query getEncountersByZone($zoneId: Int!) { + worldData { + zone(id: $zoneId) { + id + name + encounters { + id + name + } + } + } +} +""" + +GET_REGIONS = """ +query getRegions { + worldData { + regions { + id + name + subregions { + id + name + } + } + } +} +""" + +# Character Queries +GET_CHARACTER_BY_ID = """ +query getCharacterById($id: Int!) { + characterData { + character(id: $id) { + id + name + classID + raceID + guildRank + hidden + server { + name + region { + name + } + } + } + } +} +""" + +GET_CHARACTER_REPORTS = """ +query getCharacterReports($characterId: Int!, $limit: Int = 10) { + characterData { + character(id: $characterId) { + recentReports(limit: $limit) { + data { + code + startTime + endTime + zone { + name + } + } + total + per_page + current_page + from + to + last_page + has_more_pages + } + } + } +} +""" + +GET_CHARACTER_ENCOUNTER_RANKING = """ +query getCharacterEncounterRanking($characterId: Int!, $encounterId: Int!) { + characterData { + character(id: $characterId) { + encounterRankings(encounterID: $encounterId) + } + } +} +""" + +GET_CHARACTER_ENCOUNTER_RANKINGS = """ +query getCharacterEncounterRankings($characterId: Int!, $encounterId: Int!, $byBracket: Boolean, $className: String, $compare: RankingCompareType, $difficulty: Int, $includeCombatantInfo: Boolean, $includePrivateLogs: Boolean, $metric: CharacterRankingMetricType, $partition: Int, $role: RoleType, $size: Int, $specName: String, $timeframe: RankingTimeframeType) { + characterData { + character(id: $characterId) { + encounterRankings( + encounterID: $encounterId + byBracket: $byBracket + className: $className + compare: $compare + difficulty: $difficulty + includeCombatantInfo: $includeCombatantInfo + includePrivateLogs: $includePrivateLogs + metric: $metric + partition: $partition + role: $role + size: $size + specName: $specName + timeframe: $timeframe + ) + } + } +} +""" + +GET_CHARACTER_ZONE_RANKINGS = """ +query getCharacterZoneRankings($characterId: Int!, $zoneId: Int, $byBracket: Boolean, $className: String, $compare: RankingCompareType, $difficulty: Int, $includePrivateLogs: Boolean, $metric: CharacterRankingMetricType, $partition: Int, $role: RoleType, $size: Int, $specName: String, $timeframe: RankingTimeframeType) { + characterData { + character(id: $characterId) { + zoneRankings( + zoneID: $zoneId + byBracket: $byBracket + className: $className + compare: $compare + difficulty: $difficulty + includePrivateLogs: $includePrivateLogs + metric: $metric + partition: $partition + role: $role + size: $size + specName: $specName + timeframe: $timeframe + ) + } + } +} +""" + +# Guild Queries +GET_GUILD_BY_ID = """ +query getGuildById($guildId: Int!) { + guildData { + guild(id: $guildId) { + id + name + description + faction { + name + } + server { + name + region { + name + } + } + tags { + id + name + } + } + } +} +""" + +# Report Queries +GET_REPORT_BY_CODE = """ +query getReportByCode($code: String!) { + reportData { + report(code: $code) { + code + startTime + endTime + title + visibility + zone { + name + } + fights { + id + name + difficulty + startTime + endTime + } + } + } +} +""" + +GET_REPORTS = """ +query getReports($endTime: Float, $guildID: Int, $guildName: String, $guildServerSlug: String, $guildServerRegion: String, $guildTagID: Int, $userID: Int, $limit: Int, $page: Int, $startTime: Float, $zoneID: Int, $gameZoneID: Int) { + reportData { + reports( + endTime: $endTime + guildID: $guildID + guildName: $guildName + guildServerSlug: $guildServerSlug + guildServerRegion: $guildServerRegion + guildTagID: $guildTagID + userID: $userID + limit: $limit + page: $page + startTime: $startTime + zoneID: $zoneID + gameZoneID: $gameZoneID + ) { + data { + code + title + startTime + endTime + zone { + id + name + } + guild { + id + name + server { + name + slug + region { + name + slug + } + } + } + owner { + id + name + } + } + total + per_page + current_page + from + to + last_page + has_more_pages + } + } +} +""" + +GET_REPORT_EVENTS = """ +query getReportEvents($code: String!, $abilityID: Float, $dataType: EventDataType, $death: Int, $difficulty: Int, $encounterID: Int, $endTime: Float, $fightIDs: [Int], $filterExpression: String, $hostilityType: HostilityType, $includeResources: Boolean, $killType: KillType, $limit: Int, $sourceAurasAbsent: String, $sourceAurasPresent: String, $sourceClass: String, $sourceID: Int, $sourceInstanceID: Int, $startTime: Float, $targetAurasAbsent: String, $targetAurasPresent: String, $targetClass: String, $targetID: Int, $targetInstanceID: Int, $translate: Boolean, $useAbilityIDs: Boolean, $useActorIDs: Boolean, $viewOptions: Int, $wipeCutoff: Int) { + reportData { + report(code: $code) { + events( + abilityID: $abilityID + dataType: $dataType + death: $death + difficulty: $difficulty + encounterID: $encounterID + endTime: $endTime + fightIDs: $fightIDs + filterExpression: $filterExpression + hostilityType: $hostilityType + includeResources: $includeResources + killType: $killType + limit: $limit + sourceAurasAbsent: $sourceAurasAbsent + sourceAurasPresent: $sourceAurasPresent + sourceClass: $sourceClass + sourceID: $sourceID + sourceInstanceID: $sourceInstanceID + startTime: $startTime + targetAurasAbsent: $targetAurasAbsent + targetAurasPresent: $targetAurasPresent + targetClass: $targetClass + targetID: $targetID + targetInstanceID: $targetInstanceID + translate: $translate + useAbilityIDs: $useAbilityIDs + useActorIDs: $useActorIDs + viewOptions: $viewOptions + wipeCutoff: $wipeCutoff + ) { + data + nextPageTimestamp + } + } + } +} +""" + +GET_REPORT_GRAPH = """ +query getReportGraph($code: String!, $abilityID: Float, $dataType: GraphDataType, $death: Int, $difficulty: Int, $encounterID: Int, $endTime: Float, $fightIDs: [Int], $filterExpression: String, $hostilityType: HostilityType, $killType: KillType, $sourceAurasAbsent: String, $sourceAurasPresent: String, $sourceClass: String, $sourceID: Int, $sourceInstanceID: Int, $startTime: Float, $targetAurasAbsent: String, $targetAurasPresent: String, $targetClass: String, $targetID: Int, $targetInstanceID: Int, $translate: Boolean, $viewOptions: Int, $viewBy: ViewType, $wipeCutoff: Int) { + reportData { + report(code: $code) { + graph( + abilityID: $abilityID + dataType: $dataType + death: $death + difficulty: $difficulty + encounterID: $encounterID + endTime: $endTime + fightIDs: $fightIDs + filterExpression: $filterExpression + hostilityType: $hostilityType + killType: $killType + sourceAurasAbsent: $sourceAurasAbsent + sourceAurasPresent: $sourceAurasPresent + sourceClass: $sourceClass + sourceID: $sourceID + sourceInstanceID: $sourceInstanceID + startTime: $startTime + targetAurasAbsent: $targetAurasAbsent + targetAurasPresent: $targetAurasPresent + targetClass: $targetClass + targetID: $targetID + targetInstanceID: $targetInstanceID + translate: $translate + viewOptions: $viewOptions + viewBy: $viewBy + wipeCutoff: $wipeCutoff + ) + } + } +} +""" + +GET_REPORT_TABLE = """ +query getReportTable($code: String!, $abilityID: Float, $dataType: TableDataType, $death: Int, $difficulty: Int, $encounterID: Int, $endTime: Float, $fightIDs: [Int], $filterExpression: String, $hostilityType: HostilityType, $killType: KillType, $sourceAurasAbsent: String, $sourceAurasPresent: String, $sourceClass: String, $sourceID: Int, $sourceInstanceID: Int, $startTime: Float, $targetAurasAbsent: String, $targetAurasPresent: String, $targetClass: String, $targetID: Int, $targetInstanceID: Int, $translate: Boolean, $viewOptions: Int, $viewBy: ViewType, $wipeCutoff: Int) { + reportData { + report(code: $code) { + table( + abilityID: $abilityID + dataType: $dataType + death: $death + difficulty: $difficulty + encounterID: $encounterID + endTime: $endTime + fightIDs: $fightIDs + filterExpression: $filterExpression + hostilityType: $hostilityType + killType: $killType + sourceAurasAbsent: $sourceAurasAbsent + sourceAurasPresent: $sourceAurasPresent + sourceClass: $sourceClass + sourceID: $sourceID + sourceInstanceID: $sourceInstanceID + startTime: $startTime + targetAurasAbsent: $targetAurasAbsent + targetAurasPresent: $targetAurasPresent + targetClass: $targetClass + targetID: $targetID + targetInstanceID: $targetInstanceID + translate: $translate + viewOptions: $viewOptions + viewBy: $viewBy + wipeCutoff: $wipeCutoff + ) + } + } +} +""" + +GET_REPORT_RANKINGS = """ +query getReportRankings($code: String!, $compare: RankingCompareType, $difficulty: Int, $encounterID: Int, $fightIDs: [Int], $playerMetric: ReportRankingMetricType, $timeframe: RankingTimeframeType) { + reportData { + report(code: $code) { + rankings( + compare: $compare + difficulty: $difficulty + encounterID: $encounterID + fightIDs: $fightIDs + playerMetric: $playerMetric + timeframe: $timeframe + ) + } + } +} +""" + +GET_REPORT_PLAYER_DETAILS = """ +query getReportPlayerDetails($code: String!, $difficulty: Int, $encounterID: Int, $endTime: Float, $fightIDs: [Int], $killType: KillType, $startTime: Float, $translate: Boolean, $includeCombatantInfo: Boolean) { + reportData { + report(code: $code) { + playerDetails( + difficulty: $difficulty + encounterID: $encounterID + endTime: $endTime + fightIDs: $fightIDs + killType: $killType + startTime: $startTime + translate: $translate + includeCombatantInfo: $includeCombatantInfo + ) + } + } +} +""" + +# Rate Limit Query +GET_RATE_LIMIT_DATA = """ +query getRateLimitData { + rateLimitData { + limitPerHour + pointsSpentThisHour + pointsResetIn + } +} +""" + +# Query mapping for easy access +QUERIES = { + # Game Data + "getAbility": GET_ABILITY, + "getAbilities": GET_ABILITIES, + "getClass": GET_CLASS, + "getClasses": GET_CLASSES, + "getFactions": GET_FACTIONS, + "getItem": GET_ITEM, + "getItems": GET_ITEMS, + "getItemSet": GET_ITEM_SET, + "getItemSets": GET_ITEM_SETS, + "getMap": GET_MAP, + "getMaps": GET_MAPS, + "getNPC": GET_NPC, + "getNPCs": GET_NPCS, + # World Data + "getWorldData": GET_WORLD_DATA, + "getZones": GET_ZONES, + "getEncountersByZone": GET_ENCOUNTERS_BY_ZONE, + "getRegions": GET_REGIONS, + # Character + "getCharacterById": GET_CHARACTER_BY_ID, + "getCharacterReports": GET_CHARACTER_REPORTS, + "getCharacterEncounterRanking": GET_CHARACTER_ENCOUNTER_RANKING, + "getCharacterEncounterRankings": GET_CHARACTER_ENCOUNTER_RANKINGS, + "getCharacterZoneRankings": GET_CHARACTER_ZONE_RANKINGS, + # Guild + "getGuildById": GET_GUILD_BY_ID, + # Reports + "getReportByCode": GET_REPORT_BY_CODE, + "getReports": GET_REPORTS, + "getReportEvents": GET_REPORT_EVENTS, + "getReportGraph": GET_REPORT_GRAPH, + "getReportTable": GET_REPORT_TABLE, + "getReportRankings": GET_REPORT_RANKINGS, + "getReportPlayerDetails": GET_REPORT_PLAYER_DETAILS, + # Rate Limit + "getRateLimitData": GET_RATE_LIMIT_DATA, +} diff --git a/esologs/validators.py b/esologs/validators.py index 048629b..a2fd92e 100644 --- a/esologs/validators.py +++ b/esologs/validators.py @@ -4,8 +4,14 @@ from datetime import datetime from typing import Any, Optional, Union -from .base_model import UNSET, UnsetType -from .exceptions import ValidationError +from ._generated.base_model import UNSET, UnsetType + + +class ValidationError(Exception): + """Validation error for API parameters.""" + + pass + # Security constants MAX_STRING_LENGTH = 1000 # Prevent DoS via large strings diff --git a/mini.toml b/mini.toml index 754441a..0b58cbc 100644 --- a/mini.toml +++ b/mini.toml @@ -2,4 +2,5 @@ schema_path = "schema.graphql" queries_path = "queries.graphql" target_package_name = "esologs" +target_package_path = "." include_comments = "none" diff --git a/pyproject.toml b/pyproject.toml index dc9b254..19ca32c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "esologs-python" -version = "0.2.0a2" +version = "0.2.0a3" description = "A Python client library for the ESO Logs API v2" readme = "README.md" license = {text = "MIT"} @@ -95,7 +95,7 @@ extend-exclude = ''' | buck-out | build | dist - | esologs/get_.*\.py # Exclude generated files + | esologs/_generated # Exclude all generated files )/ ''' @@ -107,18 +107,13 @@ include_trailing_comma = true force_grid_wrap = 0 use_parentheses = true ensure_newline_before_comments = true -extend_skip_glob = ["esologs/get_*.py"] +extend_skip_glob = ["esologs/_generated/**"] [tool.ruff] line-length = 88 target-version = "py38" extend-exclude = [ - "esologs/get_*.py", # Exclude generated files - "esologs/input_types.py", - "esologs/enums.py", - "esologs/base_model.py", - "esologs/exceptions.py", - "esologs/async_base_client.py", + "esologs/_generated/**", # Exclude all generated files ] select = [ "E", # pycodestyle errors @@ -151,17 +146,7 @@ warn_unreachable = true strict_equality = true [[tool.mypy.overrides]] -module = "esologs.get_*" -ignore_errors = true - -[[tool.mypy.overrides]] -module = [ - "esologs.input_types", - "esologs.enums", - "esologs.base_model", - "esologs.exceptions", - "esologs.async_base_client", -] +module = "esologs._generated.*" ignore_errors = true [tool.pytest.ini_options] @@ -185,18 +170,19 @@ markers = [ "timeout: marks tests with timeout requirements", ] asyncio_mode = "auto" +filterwarnings = [ + # Ignore deprecation warnings from generated websockets code + "ignore:websockets.client.WebSocketClientProtocol is deprecated:DeprecationWarning", + "ignore:websockets.legacy is deprecated:DeprecationWarning", + "ignore:websockets.client.connect is deprecated:DeprecationWarning", +] [tool.coverage.run] source = ["esologs"] omit = [ "*/tests/*", "*/test_*", - "esologs/get_*.py", - "esologs/input_types.py", - "esologs/enums.py", - "esologs/base_model.py", - "esologs/exceptions.py", - "esologs/async_base_client.py", + "esologs/_generated/**", ] [tool.coverage.report] diff --git a/scripts/generate_client.sh b/scripts/generate_client.sh new file mode 100755 index 0000000..299f507 --- /dev/null +++ b/scripts/generate_client.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Wrapper script for generating GraphQL client with proper file organization + +echo "Generating GraphQL client..." + +# Activate virtual environment if not already active +if [[ -z "$VIRTUAL_ENV" ]]; then + source venv/bin/activate +fi + +# Run ariadne-codegen +echo "Running ariadne-codegen..." +ariadne-codegen client --config mini.toml + +# Move generated files to _generated subdirectory +echo "Organizing generated files..." +python scripts/post_codegen.py + +echo "Done! Generated files are in esologs/_generated/" diff --git a/scripts/post_codegen.py b/scripts/post_codegen.py new file mode 100755 index 0000000..364dd3a --- /dev/null +++ b/scripts/post_codegen.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +""" +Post-processing script for ariadne-codegen output. +Moves generated files to the _generated subdirectory. +""" +import shutil +from pathlib import Path + + +def move_generated_files() -> None: + """Move generated files to _generated subdirectory.""" + source_dir = Path("esologs") + target_dir = Path("esologs/_generated") + + # Files to move (generated by ariadne-codegen) + generated_files = [ + "async_base_client.py", + "base_model.py", + "enums.py", + "exceptions.py", + "input_types.py", + ] + + # Check if ariadne-codegen generated a client.py (it shouldn't be in our case) + # If it did, move it to generated_client.py to preserve our custom client.py + if (source_dir / "client.py").exists(): + # Check if it's the generated one by looking for specific patterns + with open(source_dir / "client.py") as f: + content = f.read() + + # If it contains imports from relative paths (generated code pattern) + if "from .async_base_client import AsyncBaseClient" in content: + print("Found generated client.py, moving to _generated/generated_client.py") + shutil.move( + str(source_dir / "client.py"), str(target_dir / "generated_client.py") + ) + + # Also move all get_*.py files + for file in source_dir.glob("get_*.py"): + generated_files.append(file.name) + + # Move files + moved_count = 0 + for filename in generated_files: + source_file = source_dir / filename + target_file = target_dir / filename + + if source_file.exists(): + print(f"Moving {filename} to _generated/") + shutil.move(str(source_file), str(target_file)) + moved_count += 1 + + print(f"\nMoved {moved_count} files to _generated/") + + # Update imports in generated files + print("\nUpdating imports in generated files...") + for file_path in target_dir.glob("*.py"): + if file_path.name == "__init__.py": + continue + + with open(file_path) as f: + content = f.read() + + # Update relative imports to work from _generated subdirectory + # This is a simplified version - might need refinement + updated = False + + # Check if file has local imports that need updating + if "from ." in content and file_path.name.startswith("get_"): + # These files import from base_model, enums, etc. + # No changes needed since they're all in same directory + pass + + if updated: + with open(file_path, "w") as f: + f.write(content) + print(f"Updated imports in {file_path.name}") + + +if __name__ == "__main__": + move_generated_files() diff --git a/tests/README.md b/tests/README.md index d424efc..d197ac1 100644 --- a/tests/README.md +++ b/tests/README.md @@ -6,7 +6,7 @@ Comprehensive testing framework for the esologs-python library, providing three | Test Suite | Purpose | API Required | Speed | Coverage | Test Count | |-----------|---------|--------------|-------|----------|------------| -| **[Unit Tests](unit/)** | Logic validation | ❌ No | Very Fast | Deep, Narrow | 76 tests | +| **[Unit Tests](unit/)** | Logic validation | ❌ No | Very Fast | Deep, Narrow | 105 tests | | **[Integration Tests](integration/)** | API functionality | ✅ Yes | Medium | Focused, Thorough | 85 tests | | **[Sanity Tests](sanity/)** | API health check | ✅ Yes | Medium | Broad, Shallow | 19 tests | | **[Documentation Tests](docs/)** | Code examples validation | ✅ Yes | Fast | Examples, Accuracy | 98 tests | @@ -248,10 +248,10 @@ black . && isort . && ruff check --fix . && mypy . | Suite | Execution Time | Tests | Purpose | |-------|---------------|-------|---------| -| Unit | < 5 seconds | 76 | Development feedback | +| Unit | < 5 seconds | 105 | Development feedback | | Integration | ~30 seconds | 85 | API validation | | Documentation | ~25 seconds | 98 | Examples validation | | Sanity | ~15 seconds | 19 | Health check | -| **Total** | **~75 seconds** | **278** | **Complete validation** | +| **Total** | **~80 seconds** | **310** | **Complete validation** | The test suite provides comprehensive coverage while maintaining fast execution times for efficient development workflows. diff --git a/tests/docs/test_authentication_examples.py b/tests/docs/test_authentication_examples.py index 19c3e97..d75e740 100644 --- a/tests/docs/test_authentication_examples.py +++ b/tests/docs/test_authentication_examples.py @@ -7,9 +7,9 @@ import pytest +from esologs._generated.exceptions import GraphQLClientHttpError from esologs.auth import get_access_token from esologs.client import Client -from esologs.exceptions import GraphQLClientHttpError class TestAuthenticationExamples: @@ -160,9 +160,9 @@ class TestAuthenticationDocumentationIntegrity: def test_authentication_imports(self): """Test that all modules used in auth docs are importable.""" # Test basic imports + from esologs._generated.exceptions import GraphQLClientHttpError from esologs.auth import get_access_token from esologs.client import Client - from esologs.exceptions import GraphQLClientHttpError assert callable(get_access_token) assert Client is not None @@ -210,7 +210,7 @@ def test_http_error_status_codes(self): # verify the exception class has the expected interface import inspect - from esologs.exceptions import GraphQLClientHttpError + from esologs._generated.exceptions import GraphQLClientHttpError # Check that GraphQLClientHttpError has status_code in its __init__ init_sig = inspect.signature(GraphQLClientHttpError.__init__) diff --git a/tests/docs/test_character_data_examples.py b/tests/docs/test_character_data_examples.py index 7746860..03d14c5 100644 --- a/tests/docs/test_character_data_examples.py +++ b/tests/docs/test_character_data_examples.py @@ -8,12 +8,12 @@ import pytest -from esologs.client import Client -from esologs.exceptions import ( +from esologs._generated.exceptions import ( GraphQLClientGraphQLMultiError, GraphQLClientHttpError, - ValidationError, ) +from esologs.client import Client +from esologs.validators import ValidationError class TestCharacterDataExamples: diff --git a/tests/docs/test_game_data_examples.py b/tests/docs/test_game_data_examples.py index 4919892..e9bc35f 100644 --- a/tests/docs/test_game_data_examples.py +++ b/tests/docs/test_game_data_examples.py @@ -8,12 +8,12 @@ import pytest -from esologs.client import Client -from esologs.exceptions import ( +from esologs._generated.exceptions import ( GraphQLClientGraphQLMultiError, GraphQLClientHttpError, - ValidationError, ) +from esologs.client import Client +from esologs.validators import ValidationError class TestGameDataExamples: diff --git a/tests/docs/test_guild_data_examples.py b/tests/docs/test_guild_data_examples.py index b4c6ac7..24ff3c3 100644 --- a/tests/docs/test_guild_data_examples.py +++ b/tests/docs/test_guild_data_examples.py @@ -9,12 +9,12 @@ import pytest -from esologs.client import Client -from esologs.exceptions import ( +from esologs._generated.exceptions import ( GraphQLClientGraphQLMultiError, GraphQLClientHttpError, - ValidationError, ) +from esologs.client import Client +from esologs.validators import ValidationError class TestGuildDataExamples: diff --git a/tests/docs/test_quickstart_examples.py b/tests/docs/test_quickstart_examples.py index 5b27d94..2ac28fb 100644 --- a/tests/docs/test_quickstart_examples.py +++ b/tests/docs/test_quickstart_examples.py @@ -7,13 +7,13 @@ import pytest -from esologs.auth import get_access_token -from esologs.client import Client -from esologs.exceptions import ( +from esologs._generated.exceptions import ( GraphQLClientGraphQLError, GraphQLClientHttpError, - ValidationError, ) +from esologs.auth import get_access_token +from esologs.client import Client +from esologs.validators import ValidationError class TestQuickstartExamples: @@ -282,11 +282,11 @@ def test_access_token_import(self): def test_required_exceptions_importable(self): """Test that all exceptions used in docs are importable.""" - from esologs.exceptions import ( + from esologs._generated.exceptions import ( GraphQLClientGraphQLError, GraphQLClientHttpError, - ValidationError, ) + from esologs.validators import ValidationError # Verify they're proper exception classes assert issubclass(GraphQLClientHttpError, Exception) diff --git a/tests/docs/test_report_analysis_examples.py b/tests/docs/test_report_analysis_examples.py index 15d55da..8ae285a 100644 --- a/tests/docs/test_report_analysis_examples.py +++ b/tests/docs/test_report_analysis_examples.py @@ -10,14 +10,17 @@ import pytest from pydantic import ValidationError -from esologs.client import Client -from esologs.enums import ( +from esologs._generated.enums import ( EventDataType, GraphDataType, ReportRankingMetricType, TableDataType, ) -from esologs.exceptions import GraphQLClientGraphQLMultiError, GraphQLClientHttpError +from esologs._generated.exceptions import ( + GraphQLClientGraphQLMultiError, + GraphQLClientHttpError, +) +from esologs.client import Client class TestReportAnalysisExamples: diff --git a/tests/docs/test_report_search_examples.py b/tests/docs/test_report_search_examples.py index d12b627..3a5c395 100644 --- a/tests/docs/test_report_search_examples.py +++ b/tests/docs/test_report_search_examples.py @@ -11,7 +11,7 @@ import pytest from esologs.client import Client -from esologs.exceptions import ValidationError +from esologs.validators import ValidationError class TestReportSearchExamples: diff --git a/tests/docs/test_system_examples.py b/tests/docs/test_system_examples.py index 75f5f15..9c81b71 100644 --- a/tests/docs/test_system_examples.py +++ b/tests/docs/test_system_examples.py @@ -10,12 +10,12 @@ import httpx import pytest -from esologs.client import Client -from esologs.exceptions import ( +from esologs._generated.exceptions import ( GraphQLClientGraphQLError, GraphQLClientGraphQLMultiError, GraphQLClientHttpError, ) +from esologs.client import Client class TestSystemExamples: diff --git a/tests/integration/test_character_rankings.py b/tests/integration/test_character_rankings.py index be60f7e..307d784 100644 --- a/tests/integration/test_character_rankings.py +++ b/tests/integration/test_character_rankings.py @@ -4,9 +4,9 @@ import pytest +from esologs._generated.enums import CharacterRankingMetricType from esologs.auth import get_access_token from esologs.client import Client -from esologs.enums import CharacterRankingMetricType # Fixtures are now centralized in conftest.py diff --git a/tests/integration/test_error_handling.py b/tests/integration/test_error_handling.py index a2d108c..ce5a488 100644 --- a/tests/integration/test_error_handling.py +++ b/tests/integration/test_error_handling.py @@ -4,9 +4,9 @@ import pytest +from esologs._generated.enums import CharacterRankingMetricType, EventDataType from esologs.auth import get_access_token from esologs.client import Client -from esologs.enums import CharacterRankingMetricType, EventDataType # Fixtures are now centralized in conftest.py diff --git a/tests/integration/test_report_analysis.py b/tests/integration/test_report_analysis.py index f9d6a3c..c508f79 100644 --- a/tests/integration/test_report_analysis.py +++ b/tests/integration/test_report_analysis.py @@ -4,15 +4,15 @@ import pytest -from esologs.auth import get_access_token -from esologs.client import Client -from esologs.enums import ( +from esologs._generated.enums import ( EventDataType, GraphDataType, HostilityType, ReportRankingMetricType, TableDataType, ) +from esologs.auth import get_access_token +from esologs.client import Client # Fixtures are now centralized in conftest.py diff --git a/tests/integration/test_report_search.py b/tests/integration/test_report_search.py index cab8967..c2795fb 100644 --- a/tests/integration/test_report_search.py +++ b/tests/integration/test_report_search.py @@ -218,7 +218,7 @@ class TestReportSearchErrorHandling: @pytest.mark.asyncio async def test_search_reports_invalid_guild_id(self, client): """Test search with invalid guild ID.""" - from esologs.exceptions import GraphQLClientGraphQLMultiError + from esologs._generated.exceptions import GraphQLClientGraphQLMultiError # Very large guild ID that likely doesn't exist with pytest.raises(GraphQLClientGraphQLMultiError) as exc_info: diff --git a/tests/sanity/test_api_sanity.py b/tests/sanity/test_api_sanity.py index 26d3a16..493d79f 100644 --- a/tests/sanity/test_api_sanity.py +++ b/tests/sanity/test_api_sanity.py @@ -9,7 +9,7 @@ import pytest -from esologs.enums import ( +from esologs._generated.enums import ( CharacterRankingMetricType, EventDataType, ReportRankingMetricType, diff --git a/tests/unit/README.md b/tests/unit/README.md index f71de1d..84e23f6 100644 --- a/tests/unit/README.md +++ b/tests/unit/README.md @@ -20,6 +20,8 @@ The unit tests provide: - **`test_character_rankings.py`**: Character ranking method logic and validation - **`test_report_analysis.py`**: Report analysis method signatures and validation - **`test_report_search.py`**: Report search validation, date parsing, and method logic +- **`test_method_factory.py`**: Method factory pattern tests (11 tests) +- **`test_param_builders.py`**: Parameter builder pattern tests (20 tests) ### Test Categories @@ -84,7 +86,7 @@ pytest tests/unit/ -n auto ## Test Coverage -### Current Coverage (76 tests) +### Current Coverage (105 tests) | Component | Tests | Coverage Focus | |-----------|-------|----------------| @@ -92,7 +94,9 @@ pytest tests/unit/ -n auto | **Access Token** | 8 tests | OAuth2 flow and credential handling | | **Character Rankings** | 8 tests | Method logic and parameter validation | | **Report Analysis** | 8 tests | Method signatures and basic validation | -| **Report Search** | 8 tests | Advanced validation and date parsing | +| **Report Search** | 18 tests | Advanced validation, date parsing, and search methods | +| **Method Factory** | 11 tests | Dynamic method generation and factory patterns | +| **Parameter Builders** | 20 tests | Parameter builder patterns and validation | ### Validation Test Coverage - ✅ **Report Codes**: Valid/invalid format testing diff --git a/tests/unit/test_character_rankings.py b/tests/unit/test_character_rankings.py index 77bf984..e122e9c 100644 --- a/tests/unit/test_character_rankings.py +++ b/tests/unit/test_character_rankings.py @@ -4,10 +4,16 @@ import pytest +from esologs._generated.enums import ( + CharacterRankingMetricType, + RankingTimeframeType, + RoleType, +) +from esologs._generated.get_character_encounter_rankings import ( + GetCharacterEncounterRankings, +) +from esologs._generated.get_character_zone_rankings import GetCharacterZoneRankings from esologs.client import Client -from esologs.enums import CharacterRankingMetricType, RankingTimeframeType, RoleType -from esologs.get_character_encounter_rankings import GetCharacterEncounterRankings -from esologs.get_character_zone_rankings import GetCharacterZoneRankings class TestCharacterRankings: diff --git a/tests/unit/test_method_factory.py b/tests/unit/test_method_factory.py new file mode 100644 index 0000000..7de5fdc --- /dev/null +++ b/tests/unit/test_method_factory.py @@ -0,0 +1,330 @@ +""" +Unit tests for method factory functions. +""" + +from typing import Any, Dict +from unittest.mock import AsyncMock, MagicMock, Mock + +import pytest + +from esologs._generated.base_model import UNSET +from esologs._generated.get_abilities import GetAbilities +from esologs._generated.get_ability import GetAbility +from esologs._generated.get_world_data import GetWorldData +from esologs.method_factory import ( + create_complex_method, + create_method_with_builder, + create_no_params_getter, + create_paginated_getter, + create_simple_getter, +) +from esologs.queries import QUERIES + + +class MockReturnType(MagicMock): + """Mock return type that implements model_validate.""" + + @classmethod + def model_validate(cls, obj): + """Mock model_validate method.""" + instance = cls() + instance._data = obj + return instance + + +class MockClient: + """Mock client for testing factory methods.""" + + def __init__(self): + self.execute = AsyncMock() + self.get_data = Mock() + + +class TestMethodFactory: + """Test suite for method factory functions.""" + + @pytest.fixture + def mock_client(self): + """Create a mock client instance.""" + return MockClient() + + @pytest.fixture + def mock_response_data(self): + """Mock response data for testing.""" + return { + "gameData": { + "ability": { + "id": 123, + "name": "Test Ability", + "icon": "test.png", + "description": "Test description", + } + } + } + + @pytest.mark.asyncio + async def test_create_simple_getter(self, mock_client, mock_response_data): + """Test create_simple_getter factory.""" + # Setup + mock_client.get_data.return_value = mock_response_data + + # Create method + method = create_simple_getter( + operation_name="getAbility", return_type=GetAbility, id_param_name="id" + ) + + # Bind method to mock client + bound_method = method.__get__(mock_client, MockClient) + + # Execute + result = await bound_method(id=123) + + # Verify + mock_client.execute.assert_called_once() + call_args = mock_client.execute.call_args + + assert call_args[1]["query"] == QUERIES["getAbility"] + assert call_args[1]["operation_name"] == "getAbility" + assert call_args[1]["variables"] == {"id": 123} + + mock_client.get_data.assert_called_once() + assert isinstance(result, GetAbility) + + @pytest.mark.asyncio + async def test_create_simple_getter_with_custom_param_name(self, mock_client): + """Test create_simple_getter with custom parameter name.""" + # Setup + mock_client.get_data.return_value = {"guildData": {"guild": {"id": 456}}} + + # Create method with custom param name + method = create_simple_getter( + operation_name="getGuildById", + return_type=MockReturnType, + id_param_name="guildId", + ) + + # Bind and execute + bound_method = method.__get__(mock_client, MockClient) + await bound_method(id=456) + + # Verify correct parameter mapping + variables = mock_client.execute.call_args[1]["variables"] + assert variables == {"guildId": 456} + + @pytest.mark.asyncio + async def test_create_no_params_getter(self, mock_client): + """Test create_no_params_getter factory.""" + # Setup + mock_client.get_data.return_value = {"worldData": {}} + + # Create method + method = create_no_params_getter( + operation_name="getWorldData", return_type=MockReturnType + ) + + # Bind and execute + bound_method = method.__get__(mock_client, MockClient) + await bound_method() + + # Verify + mock_client.execute.assert_called_once() + call_args = mock_client.execute.call_args + + assert call_args[1]["query"] == QUERIES["getWorldData"] + assert call_args[1]["operation_name"] == "getWorldData" + assert call_args[1]["variables"] == {} + + @pytest.mark.asyncio + async def test_create_paginated_getter(self, mock_client): + """Test create_paginated_getter factory.""" + # Setup + mock_client.get_data.return_value = { + "gameData": { + "abilities": { + "data": [], + "total": 100, + "per_page": 10, + "current_page": 1, + } + } + } + + # Create method + method = create_paginated_getter( + operation_name="getAbilities", return_type=MockReturnType + ) + + # Bind and execute + bound_method = method.__get__(mock_client, MockClient) + await bound_method(limit=20, page=2) + + # Verify + variables = mock_client.execute.call_args[1]["variables"] + assert variables == {"limit": 20, "page": 2} + + @pytest.mark.asyncio + async def test_create_paginated_getter_with_extra_params(self, mock_client): + """Test create_paginated_getter with extra parameters.""" + # Setup + mock_client.get_data.return_value = {"gameData": {"classes": []}} + + # Create method with extra params + method = create_paginated_getter( + operation_name="getClasses", + return_type=MockReturnType, + extra_params={"faction_id": int, "zone_id": int}, + ) + + # Bind and execute + bound_method = method.__get__(mock_client, MockClient) + await bound_method(limit=10, page=1, faction_id=1, zone_id=2) + + # Verify all parameters are passed + variables = mock_client.execute.call_args[1]["variables"] + assert variables == {"limit": 10, "page": 1, "faction_id": 1, "zone_id": 2} + + @pytest.mark.asyncio + async def test_create_paginated_getter_with_unset_params(self, mock_client): + """Test paginated getter handles UNSET correctly.""" + # Setup + mock_client.get_data.return_value = {"gameData": {"items": {"data": []}}} + + # Create method + method = create_paginated_getter( + operation_name="getItems", return_type=MockReturnType + ) + + # Bind and execute with UNSET params + bound_method = method.__get__(mock_client, MockClient) + await bound_method(limit=UNSET, page=5) + + # Verify UNSET is preserved + variables = mock_client.execute.call_args[1]["variables"] + assert variables["limit"] is UNSET + assert variables["page"] == 5 + + @pytest.mark.asyncio + async def test_create_complex_method(self, mock_client): + """Test create_complex_method factory.""" + # Setup + mock_client.get_data.return_value = {"reportData": {"report": {"events": {}}}} + + # Create method + method = create_complex_method( + operation_name="getReportEvents", + return_type=MockReturnType, + required_params={"code": str}, + optional_params={"start_time": float, "end_time": float, "limit": int}, + param_mapping={"start_time": "startTime", "end_time": "endTime"}, + ) + + # Bind and execute + bound_method = method.__get__(mock_client, MockClient) + await bound_method( + code="ABC123", start_time=1000.0, end_time=2000.0, limit=UNSET + ) + + # Verify parameter mapping + variables = mock_client.execute.call_args[1]["variables"] + assert variables == { + "code": "ABC123", + "startTime": 1000.0, + "endTime": 2000.0, + "limit": UNSET, + } + + @pytest.mark.asyncio + async def test_create_complex_method_missing_required(self, mock_client): + """Test complex method raises error for missing required params.""" + # Create method + method = create_complex_method( + operation_name="getReportEvents", + return_type=MockReturnType, + required_params={"code": str, "report_id": int}, + optional_params={}, + ) + + # Bind method + bound_method = method.__get__(mock_client, MockClient) + + # Should raise TypeError for missing required param + with pytest.raises(TypeError, match="missing required parameter 'report_id'"): + await bound_method(code="ABC123") + + @pytest.mark.asyncio + async def test_create_method_with_builder(self, mock_client): + """Test create_method_with_builder factory.""" + # Setup + mock_client.get_data.return_value = {"data": {}} + + # Create custom builder function + def custom_builder(**kwargs: Any) -> Dict[str, object]: + # Transform parameters + result = {} + if "user_id" in kwargs: + result["userID"] = kwargs["user_id"] + if "start_date" in kwargs: + result["startTime"] = kwargs["start_date"] * 1000 # Convert to ms + return result + + # Create method - use an existing query name + method = create_method_with_builder( + operation_name="getReports", + return_type=MockReturnType, + param_builder=custom_builder, + ) + + # Bind and execute + bound_method = method.__get__(mock_client, MockClient) + await bound_method(user_id=123, start_date=1640995200) + + # Verify builder was used + variables = mock_client.execute.call_args[1]["variables"] + assert variables == {"userID": 123, "startTime": 1640995200000} + + def test_method_metadata(self): + """Test that factory methods set proper metadata.""" + # Test simple getter + method = create_simple_getter( + operation_name="getAbility", return_type=GetAbility + ) + assert method.__name__ == "get_ability" + assert "Get GetAbility by id" in method.__doc__ + + # Test no params getter + method = create_no_params_getter( + operation_name="getWorldData", return_type=GetWorldData + ) + assert method.__name__ == "get_world_data" + assert "Get GetWorldData" in method.__doc__ + + # Test paginated getter + method = create_paginated_getter( + operation_name="getAbilities", return_type=GetAbilities + ) + assert method.__name__ == "get_abilities" + assert "Get paginated GetAbilities" in method.__doc__ + + @pytest.mark.asyncio + async def test_kwargs_not_passed_to_execute(self, mock_client): + """Test that extra kwargs are NOT passed through to execute.""" + # Setup + mock_client.get_data.return_value = {"data": {}} + + # Create method + method = create_simple_getter( + operation_name="getAbility", return_type=MockReturnType + ) + + # Bind and execute with extra kwargs + bound_method = method.__get__(mock_client, MockClient) + await bound_method(id=123, custom_header="value", timeout=30) + + # Verify only required args were passed to execute + call_kwargs = mock_client.execute.call_args[1] + assert "query" in call_kwargs + assert "operation_name" in call_kwargs + assert "variables" in call_kwargs + # These should NOT be passed through + assert "custom_header" not in call_kwargs + assert "timeout" not in call_kwargs diff --git a/tests/unit/test_param_builders.py b/tests/unit/test_param_builders.py new file mode 100644 index 0000000..155cdb8 --- /dev/null +++ b/tests/unit/test_param_builders.py @@ -0,0 +1,403 @@ +""" +Unit tests for parameter builder utilities. +""" + + +import pytest + +from esologs._generated.base_model import UNSET +from esologs._generated.enums import ( + CharacterRankingMetricType, + EventDataType, + HostilityType, + KillType, + RankingCompareType, + RankingTimeframeType, + RoleType, +) +from esologs.param_builders import ( + ParameterBuilder, + RankingParameterBuilder, + ReportFilterBuilder, + build_character_ranking_params, + build_report_event_params, + build_report_player_details_params, + build_report_rankings_params, + build_report_search_params, + clean_unset_params, + validate_param_combination, +) + + +class TestParameterBuilder: + """Test base ParameterBuilder class.""" + + def test_add_param(self): + """Test adding parameters.""" + builder = ParameterBuilder() + builder.add_param("key1", "value1") + builder.add_param("key2", 123) + + result = builder.build() + assert result == {"key1": "value1", "key2": 123} + + def test_add_param_ignores_unset(self): + """Test that UNSET values are ignored.""" + builder = ParameterBuilder() + builder.add_param("key1", "value1") + builder.add_param("key2", UNSET) + builder.add_param("key3", None) # None is kept + + result = builder.build() + assert result == {"key1": "value1", "key3": None} + assert "key2" not in result + + def test_chaining(self): + """Test method chaining.""" + builder = ParameterBuilder() + result = builder.add_param("a", 1).add_param("b", 2).add_param("c", 3).build() + assert result == {"a": 1, "b": 2, "c": 3} + + +class TestReportFilterBuilder: + """Test ReportFilterBuilder class.""" + + def test_add_time_range(self): + """Test adding time range parameters.""" + builder = ReportFilterBuilder() + builder.add_time_range(start_time=1000.0, end_time=2000.0) + + result = builder.build() + assert result == {"startTime": 1000.0, "endTime": 2000.0} + + def test_add_time_range_partial(self): + """Test adding partial time range.""" + builder = ReportFilterBuilder() + builder.add_time_range(start_time=1000.0, end_time=UNSET) + + result = builder.build() + assert result == {"startTime": 1000.0} + assert "endTime" not in result + + def test_add_combat_filters(self): + """Test adding combat filters.""" + builder = ReportFilterBuilder() + builder.add_combat_filters( + ability_id=12345.0, + source_id=1, + target_id=2, + source_class="Dragonknight", + target_class=UNSET, + ) + + result = builder.build() + assert result == { + "abilityID": 12345.0, + "sourceID": 1, + "targetID": 2, + "sourceClass": "Dragonknight", + } + + def test_add_aura_filters(self): + """Test adding aura filters.""" + builder = ReportFilterBuilder() + builder.add_aura_filters( + source_auras_present="buff1,buff2", + source_auras_absent="debuff1", + target_auras_present=UNSET, + target_auras_absent="debuff2", + ) + + result = builder.build() + assert result == { + "sourceAurasPresent": "buff1,buff2", + "sourceAurasAbsent": "debuff1", + "targetAurasAbsent": "debuff2", + } + + def test_build_from_kwargs(self): + """Test building from kwargs with parameter mapping.""" + builder = ReportFilterBuilder() + params = builder.build_from_kwargs( + code="ABC123", + fight_i_ds=[1, 2, 3], + encounter_id=27, + start_time=1000.0, + end_time=2000.0, + data_type=EventDataType.DamageDone, + hostility_type=HostilityType.Enemies, + kill_type=KillType.Kills, + limit=100, + use_ability_i_ds=True, + use_actor_i_ds=False, + translate=True, + ) + + assert params == { + "code": "ABC123", + "fightIDs": [1, 2, 3], + "encounterID": 27, + "startTime": 1000.0, + "endTime": 2000.0, + "dataType": EventDataType.DamageDone, + "hostilityType": HostilityType.Enemies, + "killType": KillType.Kills, + "limit": 100, + "useAbilityIDs": True, + "useActorIDs": False, + "translate": True, + } + + def test_build_from_kwargs_with_unset(self): + """Test that UNSET values are filtered out.""" + builder = ReportFilterBuilder() + params = builder.build_from_kwargs( + code="ABC123", + start_time=1000.0, + end_time=UNSET, + limit=UNSET, + difficulty=125, + ) + + assert params == {"code": "ABC123", "startTime": 1000.0, "difficulty": 125} + assert "endTime" not in params + assert "limit" not in params + + +class TestRankingParameterBuilder: + """Test RankingParameterBuilder class.""" + + def test_add_ranking_filters(self): + """Test adding ranking filters.""" + builder = RankingParameterBuilder() + builder.add_ranking_filters( + metric=CharacterRankingMetricType.dps, + partition=25, + timeframe=RankingTimeframeType.Historical, + compare=RankingCompareType.Rankings, + ) + + result = builder.build() + assert result == { + "metric": CharacterRankingMetricType.dps, + "partition": 25, + "timeframe": RankingTimeframeType.Historical, + "compare": RankingCompareType.Rankings, + } + + def test_add_class_filters(self): + """Test adding class/spec/role filters.""" + builder = RankingParameterBuilder() + builder.add_class_filters( + class_name="Nightblade", spec_name="Nightblade", role=RoleType.DPS + ) + + result = builder.build() + assert result == { + "className": "Nightblade", + "specName": "Nightblade", + "role": RoleType.DPS, + } + + def test_build_from_kwargs(self): + """Test building from kwargs with parameter mapping.""" + builder = RankingParameterBuilder() + params = builder.build_from_kwargs( + character_id=12345, + encounter_id=27, + zone_id=1, + by_bracket=True, + class_name="Dragonknight", + spec_name="Dragonknight", + include_combatant_info=True, + include_private_logs=False, + metric=CharacterRankingMetricType.hps, + partition=25, + difficulty=125, + size=12, + role=RoleType.Healer, + ) + + assert params == { + "characterId": 12345, + "encounterId": 27, + "zoneId": 1, + "byBracket": True, + "className": "Dragonknight", + "specName": "Dragonknight", + "includeCombatantInfo": True, + "includePrivateLogs": False, + "metric": CharacterRankingMetricType.hps, + "partition": 25, + "difficulty": 125, + "size": 12, + "role": RoleType.Healer, + } + + +class TestConvenienceFunctions: + """Test convenience builder functions.""" + + def test_build_report_event_params(self): + """Test build_report_event_params function.""" + params = build_report_event_params( + code="ABC123", + fight_i_ds=[1, 2], + start_time=1000.0, + data_type=EventDataType.Healing, + ) + + assert params == { + "code": "ABC123", + "fightIDs": [1, 2], + "startTime": 1000.0, + "dataType": EventDataType.Healing, + } + + def test_build_character_ranking_params(self): + """Test build_character_ranking_params function.""" + params = build_character_ranking_params( + character_id=12345, + encounter_id=27, + metric=CharacterRankingMetricType.playerscore, + by_bracket=False, + ) + + assert params == { + "characterId": 12345, + "encounterId": 27, + "metric": CharacterRankingMetricType.playerscore, + "byBracket": False, + } + + def test_build_report_search_params(self): + """Test build_report_search_params function.""" + params = build_report_search_params( + guild_id=123, + guild_name="Test Guild", + guild_server_slug="test-server", + guild_server_region="NA", + start_time=1000000.0, + end_time=2000000.0, + limit=25, + page=1, + ) + + assert params == { + "guildID": 123, + "guildName": "Test Guild", + "guildServerSlug": "test-server", + "guildServerRegion": "NA", + "startTime": 1000000.0, + "endTime": 2000000.0, + "limit": 25, + "page": 1, + } + + def test_build_report_player_details_params(self): + """Test build_report_player_details_params function.""" + params = build_report_player_details_params( + code="ABC123", + encounter_id=27, + fight_i_ds=[1, 2, 3], + kill_type=KillType.Kills, + start_time=1000.0, + end_time=2000.0, + difficulty=125, + translate=True, + include_combatant_info=True, + ) + + assert params == { + "code": "ABC123", + "encounterID": 27, + "fightIDs": [1, 2, 3], + "killType": KillType.Kills, + "startTime": 1000.0, + "endTime": 2000.0, + "difficulty": 125, + "translate": True, + "includeCombatantInfo": True, + } + + def test_build_report_rankings_params(self): + """Test build_report_rankings_params function.""" + params = build_report_rankings_params( + code="ABC123", + encounter_id=27, + fight_i_ds=[1], + player_metric="dps", + compare="Rankings", + difficulty=125, + timeframe="Historical", + ) + + assert params == { + "code": "ABC123", + "encounterID": 27, + "fightIDs": [1], + "playerMetric": "dps", + "compare": "Rankings", + "difficulty": 125, + "timeframe": "Historical", + } + + +class TestValidationHelpers: + """Test validation helper functions.""" + + def test_validate_param_combination_valid(self): + """Test validate_param_combination with valid params.""" + params = { + "guild_name": "Test Guild", + "guild_server_slug": "test-server", + "guild_server_region": "NA", + } + + # Should not raise + validate_param_combination( + params, [["guild_name", "guild_server_slug", "guild_server_region"]] + ) + + def test_validate_param_combination_invalid(self): + """Test validate_param_combination with invalid params.""" + params = { + "guild_name": "Test Guild", + "guild_server_slug": "test-server" + # Missing guild_server_region + } + + with pytest.raises(ValueError, match="must be provided together"): + validate_param_combination( + params, [["guild_name", "guild_server_slug", "guild_server_region"]] + ) + + def test_validate_param_combination_none_provided(self): + """Test validate_param_combination when none are provided.""" + params = {"other_param": "value"} + + # Should not raise when none of the group is provided + validate_param_combination( + params, [["guild_name", "guild_server_slug", "guild_server_region"]] + ) + + def test_clean_unset_params(self): + """Test clean_unset_params function.""" + params = { + "key1": "value1", + "key2": UNSET, + "key3": None, + "key4": 0, + "key5": "", + "key6": UNSET, + } + + cleaned = clean_unset_params(params) + assert cleaned == { + "key1": "value1", + "key3": None, # None is kept + "key4": 0, # 0 is kept + "key5": "", # Empty string is kept + } + assert "key2" not in cleaned + assert "key6" not in cleaned diff --git a/tests/unit/test_report_analysis.py b/tests/unit/test_report_analysis.py index 1147e8d..b72ea25 100644 --- a/tests/unit/test_report_analysis.py +++ b/tests/unit/test_report_analysis.py @@ -4,8 +4,7 @@ import pytest -from esologs.client import Client -from esologs.enums import ( +from esologs._generated.enums import ( EventDataType, GraphDataType, HostilityType, @@ -16,11 +15,12 @@ TableDataType, ViewType, ) -from esologs.get_report_events import GetReportEvents -from esologs.get_report_graph import GetReportGraph -from esologs.get_report_player_details import GetReportPlayerDetails -from esologs.get_report_rankings import GetReportRankings -from esologs.get_report_table import GetReportTable +from esologs._generated.get_report_events import GetReportEvents +from esologs._generated.get_report_graph import GetReportGraph +from esologs._generated.get_report_player_details import GetReportPlayerDetails +from esologs._generated.get_report_rankings import GetReportRankings +from esologs._generated.get_report_table import GetReportTable +from esologs.client import Client class TestReportAnalysis: diff --git a/tests/unit/test_report_search.py b/tests/unit/test_report_search.py index bb78b4d..0e58d0c 100644 --- a/tests/unit/test_report_search.py +++ b/tests/unit/test_report_search.py @@ -6,8 +6,8 @@ import pytest from esologs.client import Client -from esologs.exceptions import ValidationError from esologs.validators import ( + ValidationError, parse_date_to_timestamp, validate_guild_search_params, validate_report_search_params, @@ -241,15 +241,20 @@ async def test_get_user_reports(self, mock_client): assert call_kwargs["end_time"] == 1672531200000 @pytest.mark.asyncio - async def test_convenience_methods_kwargs_passthrough(self, mock_client): - """Test that kwargs are passed through in convenience methods.""" + async def test_convenience_methods_kwargs_not_passed(self, mock_client): + """Test that extra kwargs are NOT passed through in convenience methods.""" custom_kwarg = {"custom_param": "test_value"} + # This should work without error, but kwargs won't be passed through await mock_client.get_guild_reports(guild_id=123, **custom_kwarg) - # Verify custom kwargs were passed through + # Verify only the expected parameters were passed through call_kwargs = mock_client.get_reports.call_args.kwargs - assert call_kwargs["custom_param"] == "test_value" + # Only standard parameters should be present + assert "guild_id" in call_kwargs + assert call_kwargs["guild_id"] == 123 + # Custom kwargs should NOT be passed through + assert "custom_param" not in call_kwargs class TestReportSearchIntegration: