From d14169273f784b03206e17a94a755da9a13b9369 Mon Sep 17 00:00:00 2001 From: knowlen Date: Sun, 3 Aug 2025 22:33:09 -0700 Subject: [PATCH 01/16] docs: add Enums reference and Advanced Usage guide with filter expressions --- docs/api-reference/enums.md | 430 +++++++++++++++++++++++++ docs/getting-started/advanced-usage.md | 364 +++++++++++++++++++++ mkdocs.yml | 2 + 3 files changed, 796 insertions(+) create mode 100644 docs/api-reference/enums.md create mode 100644 docs/getting-started/advanced-usage.md diff --git a/docs/api-reference/enums.md b/docs/api-reference/enums.md new file mode 100644 index 0000000..d5c06fe --- /dev/null +++ b/docs/api-reference/enums.md @@ -0,0 +1,430 @@ +# Enums + +## Overview + +Enums (Enumerations) in ESO Logs Python provide type-safe constants for various API parameters. They represent fixed sets of allowed values defined by the ESO Logs GraphQL schema, ensuring that your code uses valid options and catches errors at development time rather than runtime. + +### Why Use Enums? + +1. **Type Safety**: Your IDE and type checker will catch invalid values before you run your code +2. **Autocomplete**: Get intelligent suggestions as you type, showing all valid options +3. **Self-Documenting**: Enum names clearly indicate what values are acceptable +4. **Validation**: The API will reject invalid values, but using enums prevents these errors entirely + +### Basic Usage + +```python +from esologs import Client, EventDataType, HostilityType +from esologs.auth import get_access_token + +async def analyze_combat(): + token = get_access_token() + client = Client( + url="https://www.esologs.com/api/v2/client", + headers={"Authorization": f"Bearer {token}"} + ) + + # Using enums for type-safe queries + events = await client.get_report_events( + code="abc123", + data_type=EventDataType.DamageDone, # ✅ Type-safe with autocomplete + hostility_type=HostilityType.Enemies # ✅ IDE will show available options + ) + + # String values also work but lose benefits + events2 = await client.get_report_events( + code="abc123", + data_type="DamageDone", # ⚠️ Works but no validation/autocomplete + ) +``` + +### Importing Enums + +All enums are available from the main package: + +```python +from esologs import ( + EventDataType, + TableDataType, + GraphDataType, + CharacterRankingMetricType, + HostilityType, + RoleType, + KillType, + # ... and more +) +``` + +--- + +## Available Enums + +### CharacterRankingMetricType + +Metrics available for character rankings and leaderboards. + +**Values:** + +- `dps` - Damage per second +- `hps` - Healing per second +- `playerscore` - Overall performance score +- `playerspeed` - Clear time performance + +**Used by:** +- [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) +- [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) + +--- + +### EventDataType + +Types of events that can be filtered when retrieving report events. + +**Values:** + +- `All` - All event types +- `Buffs` - Buff application events +- `Casts` - Ability cast events +- `CombatantInfo` - Combatant information +- `DamageDone` - Damage dealt events +- `DamageTaken` - Damage received events +- `Deaths` - Death events +- `Debuffs` - Debuff application events +- `Dispels` - Dispel events +- `Healing` - Healing events +- `Interrupts` - Interrupt events +- `Resources` - Resource changes (magicka, stamina) +- `Summons` - Pet/summon events +- `Threat` - Threat/aggro events + +**Used by:** +- [`get_report_events`](report-data.md#get_report_events) + +--- + +### ExternalBuffRankFilter + +Filter for rankings based on external buff usage. + +**Values:** + +- `include` - Include rankings with external buffs +- `exclude` - Exclude rankings with external buffs +- `require` - Only show rankings with external buffs + +**Used by:** +- [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) +- [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) + +--- + +### FightRankingMetricType + +Metrics available for fight-specific rankings. + +**Values:** + +- `default` - Default ranking metric +- `bossdps` - Boss damage per second +- `bossrdps` - Boss rDPS (raid-contributing DPS) +- `execution` - Execution score +- `feats` - Feats of strength +- `krsi` - Kill speed index +- `playerscore` - Player performance score +- `playerspeed` - Clear time performance +- `speed` - Clear speed + +**Used by:** +- [`get_report_rankings`](report-data.md#get_report_rankings) + +--- + +### GraphDataType + +Types of data available for time-series graphs. + +**Values:** + +- `Buffs` - Buff uptime over time +- `Casts` - Cast timeline +- `DamageDone` - Damage output graph +- `DamageTaken` - Damage taken graph +- `Deaths` - Death timeline +- `Debuffs` - Debuff uptime over time +- `Dispels` - Dispel timeline +- `Healing` - Healing output graph +- `Interrupts` - Interrupt timeline +- `Resources` - Resource levels over time +- `Summons` - Summon timeline +- `Threat` - Threat over time + +**Used by:** +- [`get_report_graph`](report-data.md#get_report_graph) + +--- + +### GuildRank + +Guild member rank levels. + +**Values:** + +- `Recruit` - New guild member +- `Member` - Standard member +- `Officer` - Guild officer +- `GuildMaster` - Guild leader + +**Used by:** +- [`get_guild_members`](guild-data.md#get_guild_members) + +--- + +### HardModeLevelRankFilter + +Filter for rankings based on hard mode completion. + +**Values:** + +- `any` - Any difficulty level +- `highest` - Highest difficulty only +- `lowest` - Base difficulty only + +**Used by:** +- [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) +- [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) + +--- + +### HostilityType + +Filter events by hostility relationship. + +**Values:** + +- `Friendlies` - Friendly targets (allies, self) +- `Enemies` - Hostile targets + +**Used by:** +- [`get_report_events`](report-data.md#get_report_events) +- [`get_report_graph`](report-data.md#get_report_graph) +- [`get_report_table`](report-data.md#get_report_table) + +--- + +### KillType + +Filter data by encounter outcome. + +**Values:** + +- `Encounters` - All encounter attempts +- `Kills` - Successful kills only +- `Wipes` - Failed attempts only + +**Used by:** +- [`get_report_events`](report-data.md#get_report_events) +- [`get_report_graph`](report-data.md#get_report_graph) +- [`get_report_table`](report-data.md#get_report_table) + +--- + +### LeaderboardRank + +Leaderboard ranking tiers. + +**Values:** + +- `Any` - Any rank +- `Blue` - Blue rank (uncommon) +- `Gold` - Gold rank (legendary) +- `Green` - Green rank (common) +- `Orange` - Orange rank (rare) +- `Purple` - Purple rank (epic) + +**Used by:** +- [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) +- [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) + +--- + +### RankingCompareType + +How to compare rankings. + +**Values:** + +- `Rankings` - Compare by rank position +- `Parses` - Compare by performance percentile + +**Used by:** +- [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) +- [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) + +--- + +### RankingTimeframeType + +Time period for rankings. + +**Values:** + +- `Current` - Current patch/season +- `Historical` - All-time historical + +**Used by:** +- [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) +- [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) + +--- + +### ReportRankingMetricType + +Metrics for report-level rankings. + +**Values:** + +- `bossdps` - Boss damage per second +- `bossrdps` - Boss rDPS +- `default` - Default metric +- `dps` - Damage per second +- `hps` - Healing per second +- `krsi` - Kill speed index +- `playerscore` - Player score +- `rdps` - Raid-contributing DPS +- `tankhps` - Tank healing per second + +**Used by:** +- [`get_report_rankings`](report-data.md#get_report_rankings) +- [`get_report_player_details`](report-data.md#get_report_player_details) + +--- + +### RoleType + +Character role classifications. + +**Values:** + +- `Tank` - Tank role +- `Healer` - Healer role +- `DPS` - Damage dealer role + +**Used by:** +- [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) +- [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) + +--- + +### SubscriptionStatus + +User subscription status levels. + +**Values:** + +- `Free` - Free tier user +- `Silver` - Silver subscription +- `Gold` - Gold subscription +- `Platinum` - Platinum subscription +- `LegacyGold` - Legacy gold status +- `LegacyPlatinum` - Legacy platinum status + +**Used by:** +- [`get_current_user`](user-data.md#get_current_user) +- [`get_user_by_id`](user-data.md#get_user_by_id) + +--- + +### TableDataType + +Types of data available in tabular format. + +**Values:** + +- `Buffs` - Buff uptime statistics +- `Casts` - Cast counts and timings +- `DamageDone` - Damage dealt breakdown +- `DamageTaken` - Damage taken analysis +- `Deaths` - Death log +- `Debuffs` - Debuff uptime statistics +- `Dispels` - Dispel counts +- `Healing` - Healing breakdown +- `Interrupts` - Interrupt counts +- `Resources` - Resource generation/usage +- `Summons` - Summon statistics +- `Threat` - Threat generation + +**Used by:** +- [`get_report_table`](report-data.md#get_report_table) + +--- + +### ViewType + +Perspective for viewing data. + +**Values:** + +- `Default` - Default view +- `Ability` - Group by ability +- `Source` - Group by source +- `Target` - Group by target + +**Used by:** +- [`get_report_graph`](report-data.md#get_report_graph) +- [`get_report_table`](report-data.md#get_report_table) + +--- + +## Best Practices + +### 1. Always Import What You Need + +```python +# Good - explicit imports +from esologs import EventDataType, HostilityType + +# Avoid - importing everything +from esologs import * +``` + +### 2. Use Enums for Type Safety + +```python +# Good - type-safe with IDE support +events = await client.get_report_events( + code="abc", + data_type=EventDataType.DamageDone +) + +# Less ideal - no compile-time checking +events = await client.get_report_events( + code="abc", + data_type="DamageDone" +) +``` + +### 3. Leverage Your IDE + +Modern IDEs will show available enum values as you type: + +```python +# Type "EventDataType." and your IDE shows: +# - EventDataType.All +# - EventDataType.Buffs +# - EventDataType.Casts +# ... etc +``` + +### 4. Enum Values Are Strings + +All enum values are strings, so you can use them anywhere a string is expected: + +```python +# This works +print(f"Fetching {EventDataType.DamageDone} events...") +# Output: "Fetching DamageDone events..." + +# Comparison works +if data_type == EventDataType.DamageDone: + print("Processing damage events") +``` diff --git a/docs/getting-started/advanced-usage.md b/docs/getting-started/advanced-usage.md new file mode 100644 index 0000000..b5aef43 --- /dev/null +++ b/docs/getting-started/advanced-usage.md @@ -0,0 +1,364 @@ +# Advanced Usage + +This guide covers advanced features that help you get the most out of the ESO Logs API while minimizing API calls and maximizing performance. + +## Filter Expressions + +Filter expressions are one of the most powerful features of the ESO Logs API. They allow you to write SQL-like queries to filter events, dramatically reducing the number of API calls needed and improving performance. + +### Why Use Filter Expressions? + +Consider this common scenario: You want to track the uptime of multiple buffs during a fight. Without filter expressions, you'd need to make separate API calls for each buff: + +```python +# ❌ Inefficient: Multiple API calls +major_courage = await client.get_report_table( + code="xb7TKHXR8DJByp4Q", + fight_i_ds=[17], + ability_id=109966 # Major Courage +) + +minor_courage = await client.get_report_table( + code="xb7TKHXR8DJByp4Q", + fight_i_ds=[17], + ability_id=109967 # Minor Courage +) + +major_force = await client.get_report_table( + code="xb7TKHXR8DJByp4Q", + fight_i_ds=[17], + ability_id=40225 # Major Force +) +``` + +With filter expressions, you can get all this data in a single API call: + +```python +# ✅ Efficient: Single API call +events = await client.get_report_events( + code="xb7TKHXR8DJByp4Q", + fight_i_ds=[17], + filter_expression="type in ('applybuff', 'removebuff') and ability.id in (109966, 109967, 40225)" +) +``` + +### Filter Expression Syntax + +Filter expressions use a SQL-like syntax with the following operators: + +#### Comparison Operators +- `=` - Equals +- `!=` - Not equals +- `<`, `>`, `<=`, `>=` - Numeric comparisons +- `in` - Value is in a list +- `not in` - Value is not in a list + +#### Logical Operators +- `and` - Both conditions must be true +- `or` - Either condition must be true +- `not` - Negates a condition +- `()` - Group conditions + +#### Available Fields + +Common fields you can filter on: + +- **Event Fields** + - `type` - Event type (damage, heal, applybuff, removebuff, death, etc.) + - `timestamp` - When the event occurred + +- **Ability Fields** + - `ability.id` - Numeric ability ID + - `ability.name` - Ability name (string) + - `ability.type` - Ability type + +- **Actor Fields** + - `source.id` - Source actor ID + - `source.name` - Source actor name + - `source.type` - Actor type (Player, NPC, etc.) + - `target.id` - Target actor ID + - `target.name` - Target actor name + - `target.type` - Target actor type + +### Common Filter Expression Patterns + +#### 1. Track Multiple Buffs/Debuffs + +```python +# Get all buff/debuff events for specific abilities +filter_expr = """ + type in ('applybuff', 'removebuff', 'applydebuff', 'removedebuff') + and ability.id in (109966, 109967, 40225, 61737) +""" + +events = await client.get_report_events( + code=report_code, + filter_expression=filter_expr +) +``` + +#### 2. Find Damage from Specific Source + +```python +# All damage done by a specific player +filter_expr = """ + type = 'damage' + and source.name = 'PlayerName' +""" + +# All damage done by players (not NPCs) +filter_expr = """ + type = 'damage' + and source.type = 'Player' +""" +``` + +#### 3. Track Specific Mechanics + +```python +# Find all applications of a specific debuff +filter_expr = """ + type = 'applydebuff' + and ability.name = 'Baneful Mark' +""" + +# Find deaths from specific ability +filter_expr = """ + type = 'death' + and ability.name = 'Poison Injection' +""" +``` + +#### 4. Complex Filtering + +```python +# Healing done by healers excluding self-healing +filter_expr = """ + type = 'heal' + and source.type = 'Player' + and source.id != target.id + and source.name in ('Healer1', 'Healer2') +""" + +# Major buffs on tanks only +filter_expr = """ + type = 'applybuff' + and ability.name like 'Major %' + and target.name in ('Tank1', 'Tank2') +""" +``` + +### Complete Example: Buff Uptime Tracker + +Here's a complete example that tracks buff uptimes efficiently: + +```python +import asyncio +from typing import Dict, List +from esologs import Client +from esologs.auth import get_access_token + +async def calculate_buff_uptimes( + events: List[Dict], + start_time: float, + end_time: float +) -> Dict[int, Dict[int, float]]: + """Calculate buff uptimes from event data.""" + # Track active buffs: source_id -> ability_id -> timestamp + active_buffs: Dict[int, Dict[int, float]] = {} + # Track total uptimes: source_id -> ability_id -> seconds + uptimes: Dict[int, Dict[int, float]] = {} + + for event in events: + source_id = event.get('sourceID', 0) + ability_id = event.get('abilityGameID', 0) + timestamp = event.get('timestamp', 0) + event_type = event.get('type', '') + + if source_id == 0 or ability_id == 0: + continue + + # Initialize tracking dicts + if source_id not in active_buffs: + active_buffs[source_id] = {} + uptimes[source_id] = {} + if ability_id not in uptimes[source_id]: + uptimes[source_id][ability_id] = 0.0 + + if event_type == 'applybuff': + # Start tracking this buff + active_buffs[source_id][ability_id] = timestamp + elif event_type == 'removebuff': + # Calculate duration and add to total + if ability_id in active_buffs[source_id]: + duration = (timestamp - active_buffs[source_id][ability_id]) / 1000.0 + uptimes[source_id][ability_id] += duration + del active_buffs[source_id][ability_id] + + # Handle buffs still active at fight end + for source_id, buffs in active_buffs.items(): + for ability_id, start in buffs.items(): + duration = (end_time - start) / 1000.0 + uptimes[source_id][ability_id] += duration + + return uptimes + +async def main(): + # Setup + token = get_access_token() + client = Client( + url="https://www.esologs.com/api/v2/client", + headers={"Authorization": f"Bearer {token}"} + ) + + # Report parameters + report_code = "xb7TKHXR8DJByp4Q" + fight_id = 17 + start_time = 2614035 + end_time = 2777081 + + # Track multiple buffs efficiently + buff_ids = [ + 109966, # Major Courage + 109967, # Minor Courage + 40225, # Major Force + 61737, # Minor Force + ] + + # Build filter expression + filter_expr = f""" + type in ('applybuff', 'removebuff') + and ability.id in ({','.join(map(str, buff_ids))}) + """ + + # Single API call for all buffs + response = await client.get_report_events( + code=report_code, + fight_i_ds=[fight_id], + start_time=start_time, + end_time=end_time, + filter_expression=filter_expr + ) + + # Calculate uptimes + events = response.report_data.report.events.data + uptimes = await calculate_buff_uptimes(events, start_time, end_time) + + # Display results + fight_duration = (end_time - start_time) / 1000.0 + print(f"Fight Duration: {fight_duration:.1f}s\n") + + for source_id, buffs in uptimes.items(): + print(f"Player {source_id}:") + for ability_id, uptime in buffs.items(): + percentage = (uptime / fight_duration) * 100 + print(f" Ability {ability_id}: {uptime:.1f}s ({percentage:.1f}%)") + +if __name__ == "__main__": + asyncio.run(main()) +``` + +### Performance Benefits + +Using filter expressions provides several performance benefits: + +1. **Reduced API Calls**: Get data for multiple conditions in one request +2. **Lower API Point Usage**: Each API call consumes points based on data processed +3. **Faster Response Times**: Server-side filtering is more efficient +4. **Simplified Code**: Less code to maintain and debug + +### Tips for Writing Filter Expressions + +1. **Test with Small Datasets First**: Start with short time ranges to verify your filter works correctly + +2. **Use Specific Conditions**: The more specific your filter, the less data returned and processed + +3. **Combine Related Queries**: If you need multiple types of related data, try to get them in one call + +4. **Watch for Typos**: Filter expressions are strings, so typos won't be caught until runtime + +5. **Check API Documentation**: The [ESO Logs API documentation](https://www.esologs.com/v2-api-docs/eso/) has the complete list of filterable fields + +### When to Use Filter Expressions + +Filter expressions are ideal for: +- Tracking multiple buffs/debuffs +- Analyzing specific mechanics +- Finding events from specific sources +- Complex event analysis +- Reducing API call count + +They may not be necessary for: +- Simple single-ability queries +- When you need all events anyway +- Initial data exploration (start broad, then filter) + +## Other Advanced Features + +### Pagination for Large Datasets + +When dealing with reports that have thousands of events: + +```python +# Use limit and startTime for pagination +all_events = [] +start_time = fight_start +batch_size = 1000 + +while True: + response = await client.get_report_events( + code=report_code, + start_time=start_time, + limit=batch_size + ) + + events = response.report_data.report.events.data + if not events: + break + + all_events.extend(events) + + # Get the timestamp of the last event for next batch + start_time = events[-1]['timestamp'] + 1 + + # Avoid infinite loops + if len(events) < batch_size: + break +``` + +### Using Multiple Data Types + +Some methods support multiple data types in a single call: + +```python +from esologs import EventDataType + +# Get both damage and healing in one call +response = await client.get_report_events( + code=report_code, + filter_expression="type in ('damage', 'heal')" +) +``` + +### Time-Window Analysis + +Analyze specific phases of a fight: + +```python +# Boss phase 2 only (timestamps in milliseconds) +phase2_start = start_time + (60 * 1000) # 1 minute in +phase2_end = start_time + (180 * 1000) # 3 minutes in + +events = await client.get_report_events( + code=report_code, + start_time=phase2_start, + end_time=phase2_end, + filter_expression="type = 'damage' and target.name = 'Boss Name'" +) +``` + +## Next Steps + +- Explore the [API Reference](../api-reference/index.md) for detailed method documentation +- Check out the [examples directory](https://github.com/knowlen/esologs-python/tree/main/examples) for more complex usage patterns +- Join the [ESO Logs Discord](https://discord.gg/esologs) for API discussions and help diff --git a/mkdocs.yml b/mkdocs.yml index 8cce580..3bf13d2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -67,6 +67,7 @@ nav: - Installation: getting-started/installation.md - Authentication: getting-started/authentication.md - Quickstart: getting-started/quickstart.md + - Advanced Usage: getting-started/advanced-usage.md - Troubleshooting: getting-started/troubleshooting.md - API Reference: - Game Data: api-reference/game-data.md @@ -78,6 +79,7 @@ nav: - Progress Race: api-reference/progress-race.md - User Data: api-reference/user-data.md - System Endpoints: api-reference/system.md + - Enums: api-reference/enums.md - Development: - Setup: development/setup.md - Testing: development/testing.md From 07ed54236b465ae5f8d9c1303cde4048b0612df7 Mon Sep 17 00:00:00 2001 From: knowlen Date: Sun, 3 Aug 2025 22:34:47 -0700 Subject: [PATCH 02/16] fix: ensure proper list rendering in enums documentation --- docs/api-reference/enums.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/api-reference/enums.md b/docs/api-reference/enums.md index d5c06fe..00a62c2 100644 --- a/docs/api-reference/enums.md +++ b/docs/api-reference/enums.md @@ -71,6 +71,7 @@ Metrics available for character rankings and leaderboards. - `playerspeed` - Clear time performance **Used by:** + - [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) - [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) @@ -98,6 +99,7 @@ Types of events that can be filtered when retrieving report events. - `Threat` - Threat/aggro events **Used by:** + - [`get_report_events`](report-data.md#get_report_events) --- @@ -113,6 +115,7 @@ Filter for rankings based on external buff usage. - `require` - Only show rankings with external buffs **Used by:** + - [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) - [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) @@ -135,6 +138,7 @@ Metrics available for fight-specific rankings. - `speed` - Clear speed **Used by:** + - [`get_report_rankings`](report-data.md#get_report_rankings) --- @@ -159,6 +163,7 @@ Types of data available for time-series graphs. - `Threat` - Threat over time **Used by:** + - [`get_report_graph`](report-data.md#get_report_graph) --- @@ -175,6 +180,7 @@ Guild member rank levels. - `GuildMaster` - Guild leader **Used by:** + - [`get_guild_members`](guild-data.md#get_guild_members) --- @@ -190,6 +196,7 @@ Filter for rankings based on hard mode completion. - `lowest` - Base difficulty only **Used by:** + - [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) - [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) @@ -205,6 +212,7 @@ Filter events by hostility relationship. - `Enemies` - Hostile targets **Used by:** + - [`get_report_events`](report-data.md#get_report_events) - [`get_report_graph`](report-data.md#get_report_graph) - [`get_report_table`](report-data.md#get_report_table) @@ -222,6 +230,7 @@ Filter data by encounter outcome. - `Wipes` - Failed attempts only **Used by:** + - [`get_report_events`](report-data.md#get_report_events) - [`get_report_graph`](report-data.md#get_report_graph) - [`get_report_table`](report-data.md#get_report_table) @@ -242,6 +251,7 @@ Leaderboard ranking tiers. - `Purple` - Purple rank (epic) **Used by:** + - [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) - [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) @@ -257,6 +267,7 @@ How to compare rankings. - `Parses` - Compare by performance percentile **Used by:** + - [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) - [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) @@ -272,6 +283,7 @@ Time period for rankings. - `Historical` - All-time historical **Used by:** + - [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) - [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) @@ -294,6 +306,7 @@ Metrics for report-level rankings. - `tankhps` - Tank healing per second **Used by:** + - [`get_report_rankings`](report-data.md#get_report_rankings) - [`get_report_player_details`](report-data.md#get_report_player_details) @@ -310,6 +323,7 @@ Character role classifications. - `DPS` - Damage dealer role **Used by:** + - [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) - [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) @@ -329,6 +343,7 @@ User subscription status levels. - `LegacyPlatinum` - Legacy platinum status **Used by:** + - [`get_current_user`](user-data.md#get_current_user) - [`get_user_by_id`](user-data.md#get_user_by_id) @@ -354,6 +369,7 @@ Types of data available in tabular format. - `Threat` - Threat generation **Used by:** + - [`get_report_table`](report-data.md#get_report_table) --- @@ -370,6 +386,7 @@ Perspective for viewing data. - `Target` - Group by target **Used by:** + - [`get_report_graph`](report-data.md#get_report_graph) - [`get_report_table`](report-data.md#get_report_table) From 264dba28d06e75e3093da87715f6bd1a00e67a18 Mon Sep 17 00:00:00 2001 From: knowlen Date: Sun, 3 Aug 2025 22:36:33 -0700 Subject: [PATCH 03/16] fix: correct relative links in enums documentation --- docs/api-reference/enums.md | 62 ++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/api-reference/enums.md b/docs/api-reference/enums.md index 00a62c2..5649d56 100644 --- a/docs/api-reference/enums.md +++ b/docs/api-reference/enums.md @@ -72,8 +72,8 @@ Metrics available for character rankings and leaderboards. **Used by:** -- [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) -- [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) +- [`get_character_encounter_rankings`](../api-reference/character-data/#get_character_encounter_rankings) +- [`get_character_zone_rankings`](../api-reference/character-data/#get_character_zone_rankings) --- @@ -100,7 +100,7 @@ Types of events that can be filtered when retrieving report events. **Used by:** -- [`get_report_events`](report-data.md#get_report_events) +- [`get_report_events`](../api-reference/report-data/#get_report_events) --- @@ -116,8 +116,8 @@ Filter for rankings based on external buff usage. **Used by:** -- [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) -- [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) +- [`get_character_encounter_rankings`](../api-reference/character-data/#get_character_encounter_rankings) +- [`get_character_zone_rankings`](../api-reference/character-data/#get_character_zone_rankings) --- @@ -139,7 +139,7 @@ Metrics available for fight-specific rankings. **Used by:** -- [`get_report_rankings`](report-data.md#get_report_rankings) +- [`get_report_rankings`](../api-reference/report-data/#get_report_rankings) --- @@ -164,7 +164,7 @@ Types of data available for time-series graphs. **Used by:** -- [`get_report_graph`](report-data.md#get_report_graph) +- [`get_report_graph`](../api-reference/report-data/#get_report_graph) --- @@ -181,7 +181,7 @@ Guild member rank levels. **Used by:** -- [`get_guild_members`](guild-data.md#get_guild_members) +- [`get_guild_members`](../api-reference/guild-data/#get_guild_members) --- @@ -197,8 +197,8 @@ Filter for rankings based on hard mode completion. **Used by:** -- [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) -- [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) +- [`get_character_encounter_rankings`](../api-reference/character-data/#get_character_encounter_rankings) +- [`get_character_zone_rankings`](../api-reference/character-data/#get_character_zone_rankings) --- @@ -213,9 +213,9 @@ Filter events by hostility relationship. **Used by:** -- [`get_report_events`](report-data.md#get_report_events) -- [`get_report_graph`](report-data.md#get_report_graph) -- [`get_report_table`](report-data.md#get_report_table) +- [`get_report_events`](../api-reference/report-data/#get_report_events) +- [`get_report_graph`](../api-reference/report-data/#get_report_graph) +- [`get_report_table`](../api-reference/report-data/#get_report_table) --- @@ -231,9 +231,9 @@ Filter data by encounter outcome. **Used by:** -- [`get_report_events`](report-data.md#get_report_events) -- [`get_report_graph`](report-data.md#get_report_graph) -- [`get_report_table`](report-data.md#get_report_table) +- [`get_report_events`](../api-reference/report-data/#get_report_events) +- [`get_report_graph`](../api-reference/report-data/#get_report_graph) +- [`get_report_table`](../api-reference/report-data/#get_report_table) --- @@ -252,8 +252,8 @@ Leaderboard ranking tiers. **Used by:** -- [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) -- [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) +- [`get_character_encounter_rankings`](../api-reference/character-data/#get_character_encounter_rankings) +- [`get_character_zone_rankings`](../api-reference/character-data/#get_character_zone_rankings) --- @@ -268,8 +268,8 @@ How to compare rankings. **Used by:** -- [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) -- [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) +- [`get_character_encounter_rankings`](../api-reference/character-data/#get_character_encounter_rankings) +- [`get_character_zone_rankings`](../api-reference/character-data/#get_character_zone_rankings) --- @@ -284,8 +284,8 @@ Time period for rankings. **Used by:** -- [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) -- [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) +- [`get_character_encounter_rankings`](../api-reference/character-data/#get_character_encounter_rankings) +- [`get_character_zone_rankings`](../api-reference/character-data/#get_character_zone_rankings) --- @@ -307,8 +307,8 @@ Metrics for report-level rankings. **Used by:** -- [`get_report_rankings`](report-data.md#get_report_rankings) -- [`get_report_player_details`](report-data.md#get_report_player_details) +- [`get_report_rankings`](../api-reference/report-data/#get_report_rankings) +- [`get_report_player_details`](../api-reference/report-data/#get_report_player_details) --- @@ -324,8 +324,8 @@ Character role classifications. **Used by:** -- [`get_character_encounter_rankings`](character-data.md#get_character_encounter_rankings) -- [`get_character_zone_rankings`](character-data.md#get_character_zone_rankings) +- [`get_character_encounter_rankings`](../api-reference/character-data/#get_character_encounter_rankings) +- [`get_character_zone_rankings`](../api-reference/character-data/#get_character_zone_rankings) --- @@ -344,8 +344,8 @@ User subscription status levels. **Used by:** -- [`get_current_user`](user-data.md#get_current_user) -- [`get_user_by_id`](user-data.md#get_user_by_id) +- [`get_current_user`](../api-reference/user-data/#get_current_user) +- [`get_user_by_id`](../api-reference/user-data/#get_user_by_id) --- @@ -370,7 +370,7 @@ Types of data available in tabular format. **Used by:** -- [`get_report_table`](report-data.md#get_report_table) +- [`get_report_table`](../api-reference/report-data/#get_report_table) --- @@ -387,8 +387,8 @@ Perspective for viewing data. **Used by:** -- [`get_report_graph`](report-data.md#get_report_graph) -- [`get_report_table`](report-data.md#get_report_table) +- [`get_report_graph`](../api-reference/report-data/#get_report_graph) +- [`get_report_table`](../api-reference/report-data/#get_report_table) --- From 573d5c1c84386ea3c32fba26aabaa6e2768b75e3 Mon Sep 17 00:00:00 2001 From: knowlen Date: Sun, 3 Aug 2025 22:59:17 -0700 Subject: [PATCH 04/16] Fix enum documentation links and add enum hyperlinks to parameter tables - Fixed broken links in enums.md that pointed to report-data instead of report-analysis - Added hyperlinks to enum types in character-data.md parameter tables - Added hyperlinks to enum types in report-analysis.md parameter tables - All enum references in API documentation now link to their definitions --- .../document_symbols_cache_v23-06-25.pkl | Bin 0 -> 354190 bytes .serena/memories/code_style_conventions.md | 115 +++++++++++++ .serena/memories/project_overview.md | 57 +++++++ .serena/memories/project_structure.md | 92 ++++++++++ .serena/memories/suggested_commands.md | 101 +++++++++++ .serena/memories/task_completion_checklist.md | 90 ++++++++++ .serena/project.yml | 68 ++++++++ docs/api-reference/character-data.md | 16 +- docs/api-reference/enums.md | 62 +++---- docs/api-reference/report-analysis.md | 30 ++-- docs/development/changelog.md | 2 +- test.py | 161 ++++++++++++++++++ test_filterexpression.py | 154 +++++++++++++++++ 13 files changed, 893 insertions(+), 55 deletions(-) create mode 100644 .serena/cache/python/document_symbols_cache_v23-06-25.pkl create mode 100644 .serena/memories/code_style_conventions.md create mode 100644 .serena/memories/project_overview.md create mode 100644 .serena/memories/project_structure.md create mode 100644 .serena/memories/suggested_commands.md create mode 100644 .serena/memories/task_completion_checklist.md create mode 100644 .serena/project.yml create mode 100644 test.py create mode 100644 test_filterexpression.py diff --git a/.serena/cache/python/document_symbols_cache_v23-06-25.pkl b/.serena/cache/python/document_symbols_cache_v23-06-25.pkl new file mode 100644 index 0000000000000000000000000000000000000000..ea81c38d8a68046672e69d85379200e86d1e6d00 GIT binary patch literal 354190 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 Wh;t0j1v3Pu1m+T9 ReportEventsResponse: + """Get events from a report.""" + ``` + +## Docstrings +- **Google-style docstrings** +- **Required for all public functions/classes** +- **Include parameters, returns, and examples** +- **Example**: + ```python + def validate_bearer_token_format(token: str) -> str: + """Validate and format a bearer token. + + Args: + token: The token to validate (with or without 'Bearer' prefix) + + Returns: + The formatted token with 'Bearer' prefix + + Raises: + ValueError: If token format is invalid + """ + ``` + +## Naming Conventions +- **Classes**: PascalCase (e.g., `ReportEventsResponse`) +- **Functions/Methods**: snake_case (e.g., `get_report_events`) +- **Constants**: UPPER_SNAKE_CASE (e.g., `BEARER_TOKEN_PATTERN`) +- **Private methods**: Leading underscore (e.g., `_validate_input`) + +## Error Handling +- **Use specific exceptions** (not bare except) +- **Provide helpful error messages** +- **Handle API errors gracefully** +- **Example**: + ```python + if not token: + raise ValueError("Token cannot be empty") + ``` + +## Testing +- **Test file naming**: `test_*.py` +- **Test function naming**: `test_descriptive_name()` +- **Use pytest fixtures for common setup** +- **Include edge cases and error scenarios** + +## Architecture Patterns +- **Factory Method Pattern**: For dynamic method generation +- **Mixin Pattern**: For organizing API methods by category +- **Protocol Types**: For type safety with dynamic methods + +## Generated Code +- **Location**: `esologs/_generated/` +- **Do not edit manually** - use ariadne-codegen +- **Excluded from formatting/linting tools** + +## Security +- **Never commit credentials** +- **Use environment variables for secrets** +- **Validate all user input** +- **No print statements in production code** + +## Performance +- **Use async/await for I/O operations** +- **Implement streaming for large datasets** +- **Cache static data when appropriate** + +## Documentation +- **Update docs with every feature** +- **Include code examples** +- **Keep README current** +- **Use MkDocs for documentation site** diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md new file mode 100644 index 0000000..a0ec667 --- /dev/null +++ b/.serena/memories/project_overview.md @@ -0,0 +1,57 @@ +# ESO Logs Python Project Overview + +## Purpose +esologs-python is a comprehensive Python client library for the ESO Logs API v2, designed for Elder Scrolls Online (ESO) players and developers. It provides both synchronous and asynchronous interfaces to access ESO Logs data for analyzing combat logs, tracking performance metrics, and building tools for the ESO community. + +## Current Status +- **Version**: 0.2.0b1 (Beta) +- **API Coverage**: 100% (42/42 methods implemented) +- **Package Name**: esologs-python (on PyPI) +- **Documentation**: https://esologs-python.readthedocs.io/ +- **Repository**: https://github.com/knowlen/esologs-python + +## Tech Stack +- **Language**: Python 3.8+ +- **Core Dependencies**: + - requests (HTTP client) + - httpx (async HTTP client) + - pydantic (data validation) + - ariadne-codegen (GraphQL code generation) + - aiofiles (async file operations) +- **Development Tools**: + - pytest (testing framework) + - black (code formatter) + - isort (import sorter) + - ruff (linter) + - mypy (type checker) + - pre-commit (git hooks) + - mkdocs (documentation) + +## Architecture +- **Modular Client Design**: Uses Factory Method and Mixin patterns +- **Main client reduced from 1,610 to 86 lines** through refactoring +- **Code Organization**: + - `esologs/client.py` - Main client (86 lines) + - `esologs/mixins/` - API method mixins (7 files) + - `esologs/method_factory.py` - Dynamic method generation + - `esologs/_generated/` - Auto-generated GraphQL code + - `esologs/validators.py` - Input validation + - `esologs/param_builders.py` - Parameter processing + - `esologs/queries.py` - GraphQL queries + +## API Features +1. **Game Data** - 13 methods (abilities, classes, items, NPCs, maps, factions) +2. **Character Data** - 5 methods (info, reports, rankings) +3. **Report Data** - 10 methods (events, tables, rankings, search) +4. **World Data** - 4 methods (regions, zones, encounters, expansions) +5. **Guild Data** - 5 methods (info, reports, attendance, members) +6. **Rate Limit** - 1 method +7. **Progress Race** - 1 method (world/realm first tracking) +8. **User Data** - 3 methods (OAuth2 authenticated endpoints) + +## Testing Strategy +- **428 total tests** across 4 suites: + - Unit Tests (164) - Logic validation, no API required + - Integration Tests (129) - Live API testing + - Documentation Tests (117) - Code example validation + - Sanity Tests (18) - API health check diff --git a/.serena/memories/project_structure.md b/.serena/memories/project_structure.md new file mode 100644 index 0000000..c617262 --- /dev/null +++ b/.serena/memories/project_structure.md @@ -0,0 +1,92 @@ +# ESO Logs Python - Project Structure + +## Repository Layout +``` +esologs-python/ +├── esologs/ # Main package +│ ├── __init__.py # Package initialization, exports +│ ├── client.py # Main client class (86 lines) +│ ├── client_factory.py # Client instantiation +│ ├── auth.py # OAuth2 authentication +│ ├── user_auth.py # User authentication flows +│ ├── method_factory.py # Dynamic method generation +│ ├── validators.py # Input validation functions +│ ├── param_builders.py # Parameter processing +│ ├── queries.py # GraphQL query definitions +│ ├── py.typed # PEP 561 type hint marker +│ ├── mixins/ # API method mixins +│ │ ├── __init__.py +│ │ ├── game_data.py # Game data methods +│ │ ├── character.py # Character methods +│ │ ├── guild.py # Guild methods +│ │ ├── report.py # Report methods +│ │ ├── world_data.py # World data methods +│ │ ├── progress_race.py # Progress race methods +│ │ └── user.py # User methods +│ └── _generated/ # Auto-generated GraphQL code +│ ├── *.py # 30+ generated files +│ └── ... +├── tests/ # Test suites +│ ├── __init__.py +│ ├── conftest.py # Shared fixtures +│ ├── README.md # Test documentation +│ ├── unit/ # Unit tests (164 tests) +│ ├── integration/ # Integration tests (129 tests) +│ ├── sanity/ # Sanity tests (18 tests) +│ └── docs/ # Documentation tests (117 tests) +├── docs/ # MkDocs documentation +│ ├── index.md # Home page +│ ├── installation.md # Installation guide +│ ├── quickstart.md # Getting started +│ ├── api-reference/ # API documentation +│ ├── examples/ # Code examples +│ └── assets/ # Images, logos +├── scripts/ # Utility scripts +│ ├── generate_client.sh # GraphQL code generation +│ ├── post_codegen.py # Post-generation processing +│ └── quick_api_check.py # API health check +├── .github/ # GitHub configuration +│ ├── workflows/ # CI/CD workflows +│ └── ISSUE_TEMPLATE/ # Issue templates +├── .serena/ # Serena MCP configuration +│ ├── project.yml # Project config +│ └── memories/ # Project memories +├── pyproject.toml # Package configuration +├── mkdocs.yml # Documentation config +├── .pre-commit-config.yaml # Pre-commit hooks +├── .gitignore # Git ignore rules +├── .readthedocs.yml # ReadTheDocs config +├── LICENSE # MIT license +├── README.md # Project README +├── MANIFEST.in # Package manifest +├── schema.graphql # ESO Logs GraphQL schema +└── queries.graphql # GraphQL queries + +## Key Directories + +### `/esologs` - Main Package +- Core implementation with modular architecture +- Mixins organize 42 API methods into logical groups +- Generated code isolated in `_generated/` subdirectory + +### `/tests` - Comprehensive Test Suite +- 428 total tests across 4 complementary suites +- Each suite has specific purpose and requirements +- Shared fixtures in conftest.py + +### `/docs` - Documentation +- MkDocs-based documentation site +- Comprehensive API reference +- Examples and guides + +### `/scripts` - Development Tools +- Code generation automation +- API testing utilities +- Build and deployment scripts + +## Important Files +- `pyproject.toml` - Package metadata and dependencies +- `mkdocs.yml` - Documentation configuration +- `.pre-commit-config.yaml` - Code quality automation +- `esologs/queries.py` - All GraphQL queries +- `tests/conftest.py` - Shared test fixtures diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md new file mode 100644 index 0000000..887f9b1 --- /dev/null +++ b/.serena/memories/suggested_commands.md @@ -0,0 +1,101 @@ +# ESO Logs Python - Suggested Commands + +## Development Commands + +### Testing +```bash +# Run all tests +pytest tests/ -v + +# Run specific test suites +pytest tests/unit/ -v # Unit tests (no API required) +pytest tests/integration/ -v # Integration tests (API required) +pytest tests/sanity/ -v # Sanity tests (API required) +pytest tests/docs/ -v # Documentation tests (API required) + +# Run with coverage +pytest tests/ --cov=esologs --cov-report=html + +# Run specific test markers +pytest -m "not oauth2" -v # Skip OAuth2 tests +pytest -m "not slow" -v # Skip slow tests +``` + +### Code Quality +```bash +# Run all pre-commit checks +pre-commit run --all-files + +# Individual tools +black . # Format code +isort . # Sort imports +ruff check --fix . # Lint and fix +mypy . # Type checking +``` + +### Documentation +```bash +# Build documentation +mkdocs build --clean + +# Serve documentation locally +mkdocs serve + +# View at http://127.0.0.1:8000 +``` + +### Code Generation +```bash +# Regenerate GraphQL client code +./scripts/generate_client.sh + +# Run post-codegen processing +python scripts/post_codegen.py +``` + +### Package Management +```bash +# Install development dependencies +pip install -e ".[dev]" + +# Install all optional dependencies +pip install -e ".[all]" + +# Build package +python -m build + +# Check package +twine check dist/* +``` + +## Environment Variables +```bash +# Required for API access +export ESOLOGS_ID="your_client_id" +export ESOLOGS_SECRET="your_client_secret" + +# Alternative: use .env file +echo "ESOLOGS_ID=your_client_id" >> .env +echo "ESOLOGS_SECRET=your_client_secret" >> .env +``` + +## Git Commands +```bash +# Common workflow +git checkout dev # Development branch +git checkout -b feature/my-feature # Create feature branch +git add . +git commit -m "feat: add new feature" +git push -u origin feature/my-feature + +# Create PR to dev branch for review +``` + +## Quick Checks +```bash +# API health check +python scripts/quick_api_check.py + +# Validate installation +python -c "import esologs; print(esologs.__version__)" +``` diff --git a/.serena/memories/task_completion_checklist.md b/.serena/memories/task_completion_checklist.md new file mode 100644 index 0000000..70fb57e --- /dev/null +++ b/.serena/memories/task_completion_checklist.md @@ -0,0 +1,90 @@ +# ESO Logs Python - Task Completion Checklist + +## When You Complete a Task + +### 1. Code Quality Checks +```bash +# Run all pre-commit checks +pre-commit run --all-files + +# If any fail, fix and re-run until all pass +``` + +### 2. Run Tests +```bash +# Run unit tests (always) +pytest tests/unit/ -v + +# Run integration tests if API-related +pytest tests/integration/ -v + +# Run full test suite for major changes +pytest tests/ -v +``` + +### 3. Type Checking +```bash +# Ensure no type errors +mypy . +``` + +### 4. Update Documentation +- Update docstrings for new/modified functions +- Update README.md if adding new features +- Update API documentation if changing public API +- Add/update code examples + +### 5. Check for Common Issues +- [ ] No print statements in code +- [ ] No hardcoded credentials +- [ ] All imports are absolute (not relative) +- [ ] New files have proper headers +- [ ] Tests added for new functionality +- [ ] Edge cases handled + +### 6. Git Workflow +```bash +# Stage changes +git add . + +# Check what's being committed +git status +git diff --cached + +# Commit with descriptive message +git commit -m "type: description" + +# Push to feature branch +git push origin feature/branch-name +``` + +### 7. PR Checklist +- [ ] All tests pass +- [ ] Code follows style conventions +- [ ] Documentation updated +- [ ] No merge conflicts with target branch +- [ ] PR description explains changes +- [ ] Linked to relevant issues + +## Commit Message Format +``` +type: subject + +body (optional) +``` + +Types: +- `feat`: New feature +- `fix`: Bug fix +- `docs`: Documentation changes +- `style`: Code style changes +- `refactor`: Code refactoring +- `test`: Test additions/changes +- `chore`: Maintenance tasks + +## Important Notes +- **Never push directly to main or dev branches** +- **Always create feature branches** +- **Get PR reviewed before merging** +- **Keep commits focused and atomic** +- **Run tests before committing** diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 0000000..e8a3f31 --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,68 @@ +# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby) +# * For C, use cpp +# * For JavaScript, use typescript +# Special requirements: +# * csharp: Requires the presence of a .sln file in the project folder. +language: python + +# whether to use the project's gitignore file to ignore files +# Added on 2025-04-07 +ignore_all_files_in_gitignore: true +# list of additional paths to ignore +# same syntax as gitignore, so you can use * and ** +# Was previously called `ignored_dirs`, please update your config if you are using that. +# Added (renamed)on 2025-04-07 +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + + +# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file or directory. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" + +project_name: "esologs-python" diff --git a/docs/api-reference/character-data.md b/docs/api-reference/character-data.md index 0b68011..f96884c 100644 --- a/docs/api-reference/character-data.md +++ b/docs/api-reference/character-data.md @@ -192,16 +192,16 @@ Available data: ['bestAmount', 'medianPerformance', 'averagePerformance', 'total | `encounter_id` | *int* | Yes | The encounter ID to get rankings for | | `by_bracket` | *bool* | No | Group rankings by bracket | | `class_name` | *str* | No | Filter by class name | -| `compare` | *RankingCompareType* | No | Comparison type for rankings | +| `compare` | [*RankingCompareType*](../enums/#rankingcomparetype) | No | Comparison type for rankings | | `difficulty` | *int* | No | Difficulty level filter | | `include_combatant_info` | *bool* | No | Include combatant information | | `include_private_logs` | *bool* | No | Include private logs in rankings | -| `metric` | *CharacterRankingMetricType* | No | Ranking metric type | +| `metric` | [*CharacterRankingMetricType*](../enums/#characterrankingmetrictype) | No | Ranking metric type | | `partition` | *int* | No | Partition number | -| `role` | *RoleType* | No | Role filter (Tank, Healer, DPS) | +| `role` | [*RoleType*](../enums/#roletype) | No | Role filter (Tank, Healer, DPS) | | `size` | *int* | No | Number of results to return | | `spec_name` | *str* | No | Specialization name filter | -| `timeframe` | *RankingTimeframeType* | No | Time period for rankings | +| `timeframe` | [*RankingTimeframeType*](../enums/#rankingtimeframetype) | No | Time period for rankings | **Returns**: `GetCharacterEncounterRankings` object with the following structure: @@ -265,15 +265,15 @@ Rank percentile: 68.0% | `zone_id` | *int* | No | The zone ID to get rankings for | | `by_bracket` | *bool* | No | Group rankings by bracket | | `class_name` | *str* | No | Filter by class name | -| `compare` | *RankingCompareType* | No | Comparison type for rankings | +| `compare` | [*RankingCompareType*](../enums/#rankingcomparetype) | No | Comparison type for rankings | | `difficulty` | *int* | No | Difficulty level filter | | `include_private_logs` | *bool* | No | Include private logs in rankings | -| `metric` | *CharacterRankingMetricType* | No | Ranking metric type | +| `metric` | [*CharacterRankingMetricType*](../enums/#characterrankingmetrictype) | No | Ranking metric type | | `partition` | *int* | No | Partition number | -| `role` | *RoleType* | No | Role filter (Tank, Healer, DPS) | +| `role` | [*RoleType*](../enums/#roletype) | No | Role filter (Tank, Healer, DPS) | | `size` | *int* | No | Number of results to return | | `spec_name` | *str* | No | Specialization name filter | -| `timeframe` | *RankingTimeframeType* | No | Time period for rankings | +| `timeframe` | [*RankingTimeframeType*](../enums/#rankingtimeframetype) | No | Time period for rankings | **Returns**: `GetCharacterZoneRankings` object with the following structure: diff --git a/docs/api-reference/enums.md b/docs/api-reference/enums.md index 5649d56..c909162 100644 --- a/docs/api-reference/enums.md +++ b/docs/api-reference/enums.md @@ -72,8 +72,8 @@ Metrics available for character rankings and leaderboards. **Used by:** -- [`get_character_encounter_rankings`](../api-reference/character-data/#get_character_encounter_rankings) -- [`get_character_zone_rankings`](../api-reference/character-data/#get_character_zone_rankings) +- [`get_character_encounter_rankings`](../character-data/#get_character_encounter_rankings) +- [`get_character_zone_rankings`](../character-data/#get_character_zone_rankings) --- @@ -100,7 +100,7 @@ Types of events that can be filtered when retrieving report events. **Used by:** -- [`get_report_events`](../api-reference/report-data/#get_report_events) +- [`get_report_events`](../report-analysis/#get_report_events) --- @@ -116,8 +116,8 @@ Filter for rankings based on external buff usage. **Used by:** -- [`get_character_encounter_rankings`](../api-reference/character-data/#get_character_encounter_rankings) -- [`get_character_zone_rankings`](../api-reference/character-data/#get_character_zone_rankings) +- [`get_character_encounter_rankings`](../character-data/#get_character_encounter_rankings) +- [`get_character_zone_rankings`](../character-data/#get_character_zone_rankings) --- @@ -139,7 +139,7 @@ Metrics available for fight-specific rankings. **Used by:** -- [`get_report_rankings`](../api-reference/report-data/#get_report_rankings) +- [`get_report_rankings`](../report-analysis/#get_report_rankings) --- @@ -164,7 +164,7 @@ Types of data available for time-series graphs. **Used by:** -- [`get_report_graph`](../api-reference/report-data/#get_report_graph) +- [`get_report_graph`](../report-analysis/#get_report_graph) --- @@ -181,7 +181,7 @@ Guild member rank levels. **Used by:** -- [`get_guild_members`](../api-reference/guild-data/#get_guild_members) +- [`get_guild_members`](../guild-data/#get_guild_members) --- @@ -197,8 +197,8 @@ Filter for rankings based on hard mode completion. **Used by:** -- [`get_character_encounter_rankings`](../api-reference/character-data/#get_character_encounter_rankings) -- [`get_character_zone_rankings`](../api-reference/character-data/#get_character_zone_rankings) +- [`get_character_encounter_rankings`](../character-data/#get_character_encounter_rankings) +- [`get_character_zone_rankings`](../character-data/#get_character_zone_rankings) --- @@ -213,9 +213,9 @@ Filter events by hostility relationship. **Used by:** -- [`get_report_events`](../api-reference/report-data/#get_report_events) -- [`get_report_graph`](../api-reference/report-data/#get_report_graph) -- [`get_report_table`](../api-reference/report-data/#get_report_table) +- [`get_report_events`](../report-analysis/#get_report_events) +- [`get_report_graph`](../report-analysis/#get_report_graph) +- [`get_report_table`](../report-analysis/#get_report_table) --- @@ -231,9 +231,9 @@ Filter data by encounter outcome. **Used by:** -- [`get_report_events`](../api-reference/report-data/#get_report_events) -- [`get_report_graph`](../api-reference/report-data/#get_report_graph) -- [`get_report_table`](../api-reference/report-data/#get_report_table) +- [`get_report_events`](../report-analysis/#get_report_events) +- [`get_report_graph`](../report-analysis/#get_report_graph) +- [`get_report_table`](../report-analysis/#get_report_table) --- @@ -252,8 +252,8 @@ Leaderboard ranking tiers. **Used by:** -- [`get_character_encounter_rankings`](../api-reference/character-data/#get_character_encounter_rankings) -- [`get_character_zone_rankings`](../api-reference/character-data/#get_character_zone_rankings) +- [`get_character_encounter_rankings`](../character-data/#get_character_encounter_rankings) +- [`get_character_zone_rankings`](../character-data/#get_character_zone_rankings) --- @@ -268,8 +268,8 @@ How to compare rankings. **Used by:** -- [`get_character_encounter_rankings`](../api-reference/character-data/#get_character_encounter_rankings) -- [`get_character_zone_rankings`](../api-reference/character-data/#get_character_zone_rankings) +- [`get_character_encounter_rankings`](../character-data/#get_character_encounter_rankings) +- [`get_character_zone_rankings`](../character-data/#get_character_zone_rankings) --- @@ -284,8 +284,8 @@ Time period for rankings. **Used by:** -- [`get_character_encounter_rankings`](../api-reference/character-data/#get_character_encounter_rankings) -- [`get_character_zone_rankings`](../api-reference/character-data/#get_character_zone_rankings) +- [`get_character_encounter_rankings`](../character-data/#get_character_encounter_rankings) +- [`get_character_zone_rankings`](../character-data/#get_character_zone_rankings) --- @@ -307,8 +307,8 @@ Metrics for report-level rankings. **Used by:** -- [`get_report_rankings`](../api-reference/report-data/#get_report_rankings) -- [`get_report_player_details`](../api-reference/report-data/#get_report_player_details) +- [`get_report_rankings`](../report-analysis/#get_report_rankings) +- [`get_report_player_details`](../report-analysis/#get_report_player_details) --- @@ -324,8 +324,8 @@ Character role classifications. **Used by:** -- [`get_character_encounter_rankings`](../api-reference/character-data/#get_character_encounter_rankings) -- [`get_character_zone_rankings`](../api-reference/character-data/#get_character_zone_rankings) +- [`get_character_encounter_rankings`](../character-data/#get_character_encounter_rankings) +- [`get_character_zone_rankings`](../character-data/#get_character_zone_rankings) --- @@ -344,8 +344,8 @@ User subscription status levels. **Used by:** -- [`get_current_user`](../api-reference/user-data/#get_current_user) -- [`get_user_by_id`](../api-reference/user-data/#get_user_by_id) +- [`get_current_user`](../user-data/#get_current_user) +- [`get_user_by_id`](../user-data/#get_user_by_id) --- @@ -370,7 +370,7 @@ Types of data available in tabular format. **Used by:** -- [`get_report_table`](../api-reference/report-data/#get_report_table) +- [`get_report_table`](../report-analysis/#get_report_table) --- @@ -387,8 +387,8 @@ Perspective for viewing data. **Used by:** -- [`get_report_graph`](../api-reference/report-data/#get_report_graph) -- [`get_report_table`](../api-reference/report-data/#get_report_table) +- [`get_report_graph`](../report-analysis/#get_report_graph) +- [`get_report_table`](../report-analysis/#get_report_table) --- diff --git a/docs/api-reference/report-analysis.md b/docs/api-reference/report-analysis.md index f302352..af88426 100644 --- a/docs/api-reference/report-analysis.md +++ b/docs/api-reference/report-analysis.md @@ -18,16 +18,16 @@ Access detailed (behavioral) combat log data including events, performance graph |-----------|------|----------|-------------| | `code` | *str* | Yes | The report code to analyze | | `ability_id` | *float* | No | Filter events by specific ability ID | -| `data_type` | *EventDataType* | No | Type of events to retrieve (DamageDone, Healing, Deaths, etc.) | +| `data_type` | [*EventDataType*](../enums/#eventdatatype) | No | Type of events to retrieve (DamageDone, Healing, Deaths, etc.) | | `death` | *int* | No | Filter to specific death number | | `difficulty` | *int* | No | Difficulty level filter | | `encounter_id` | *int* | No | Filter to specific encounter | | `end_time` | *float* | No | End time in milliseconds relative to report start | | `fight_i_ds` | *List[int]* | No | List of fight IDs to include | | `filter_expression` | *str* | No | Advanced filter expression | -| `hostility_type` | *HostilityType* | No | Filter by hostility type (Enemies, Friendlies) | +| `hostility_type` | [*HostilityType*](../enums/#hostilitytype) | No | Filter by hostility type (Enemies, Friendlies) | | `include_resources` | *bool* | No | Include resource events | -| `kill_type` | *KillType* | No | Filter by kill type | +| `kill_type` | [*KillType*](../enums/#killtype) | No | Filter by kill type | | `limit` | *int* | No | Maximum number of events to return | | `source_auras_absent` | *str* | No | Filter events where source lacks specific auras | | `source_auras_present` | *str* | No | Filter events where source has specific auras | @@ -109,15 +109,15 @@ More data available after: 264591.0 |-----------|------|----------|-------------| | `code` | *str* | Yes | The report code to analyze | | `ability_id` | *float* | No | Filter by specific ability ID | -| `data_type` | *GraphDataType* | No | Type of graph data (DamageDone, Healing, DamageTaken, etc.) | +| `data_type` | [*GraphDataType*](../enums/#graphdatatype) | No | Type of graph data (DamageDone, Healing, DamageTaken, etc.) | | `death` | *int* | No | Filter to specific death number | | `difficulty` | *int* | No | Difficulty level filter | | `encounter_id` | *int* | No | Filter to specific encounter | | `end_time` | *float* | No | End time in milliseconds | | `fight_i_ds` | *List[int]* | No | List of fight IDs to include | | `filter_expression` | *str* | No | Advanced filter expression | -| `hostility_type` | *HostilityType* | No | Filter by hostility type | -| `kill_type` | *KillType* | No | Filter by kill type | +| `hostility_type` | [*HostilityType*](../enums/#hostilitytype) | No | Filter by hostility type | +| `kill_type` | [*KillType*](../enums/#killtype) | No | Filter by kill type | | `source_auras_absent` | *str* | No | Filter where source lacks specific auras | | `source_auras_present` | *str* | No | Filter where source has specific auras | | `source_class` | *str* | No | Filter by source character class | @@ -131,7 +131,7 @@ More data available after: 264591.0 | `target_instance_id` | *int* | No | Filter by target instance ID | | `translate` | *bool* | No | Translate ability names | | `view_options` | *int* | No | View option flags | -| `view_by` | *ViewType* | No | View aggregation method | +| `view_by` | [*ViewType*](../enums/#viewtype) | No | View aggregation method | | `wipe_cutoff` | *int* | No | Wipe cutoff percentage | **Returns**: `GetReportGraph` object with the following structure: @@ -192,15 +192,15 @@ Data points: 240 |-----------|------|----------|-------------| | `code` | *str* | Yes | The report code to analyze | | `ability_id` | *float* | No | Filter by specific ability ID | -| `data_type` | *TableDataType* | No | Type of table data (DamageDone, Healing, Deaths, etc.) | +| `data_type` | [*TableDataType*](../enums/#tabledatatype) | No | Type of table data (DamageDone, Healing, Deaths, etc.) | | `death` | *int* | No | Filter to specific death number | | `difficulty` | *int* | No | Difficulty level filter | | `encounter_id` | *int* | No | Filter to specific encounter | | `end_time` | *float* | No | End time in milliseconds | | `fight_i_ds` | *List[int]* | No | List of fight IDs to include | | `filter_expression` | *str* | No | Advanced filter expression | -| `hostility_type` | *HostilityType* | No | Filter by hostility type | -| `kill_type` | *KillType* | No | Filter by kill type | +| `hostility_type` | [*HostilityType*](../enums/#hostilitytype) | No | Filter by hostility type | +| `kill_type` | [*KillType*](../enums/#killtype) | No | Filter by kill type | | `source_auras_absent` | *str* | No | Filter where source lacks specific auras | | `source_auras_present` | *str* | No | Filter where source has specific auras | | `source_class` | *str* | No | Filter by source character class | @@ -214,7 +214,7 @@ Data points: 240 | `target_instance_id` | *int* | No | Filter by target instance ID | | `translate` | *bool* | No | Translate ability names | | `view_options` | *int* | No | View option flags | -| `view_by` | *ViewType* | No | View aggregation method | +| `view_by` | [*ViewType*](../enums/#viewtype) | No | View aggregation method | | `wipe_cutoff` | *int* | No | Wipe cutoff percentage | **Returns**: `GetReportTable` object with the following structure: @@ -272,12 +272,12 @@ Number of players: 10 | Parameters | Type | Required | Description | |-----------|------|----------|-------------| | `code` | *str* | Yes | The report code to analyze | -| `compare` | *RankingCompareType* | No | Comparison method for rankings | +| `compare` | [*RankingCompareType*](../enums/#rankingcomparetype) | No | Comparison method for rankings | | `difficulty` | *int* | No | Difficulty level filter | | `encounter_id` | *int* | No | Filter to specific encounter | | `fight_i_ds` | *List[int]* | No | List of fight IDs to include | -| `player_metric` | *ReportRankingMetricType* | No | Ranking metric (dps, hps, playerscore, etc.) | -| `timeframe` | *RankingTimeframeType* | No | Time frame for ranking comparison | +| `player_metric` | [*ReportRankingMetricType*](../enums/#reportrankingmetrictype) | No | Ranking metric (dps, hps, playerscore, etc.) | +| `timeframe` | [*RankingTimeframeType*](../enums/#rankingtimeframetype) | No | Time frame for ranking comparison | **Returns**: `GetReportRankings` object with the following structure: @@ -343,7 +343,7 @@ Top DPS Players: | `encounter_id` | *int* | No | Filter to specific encounter | | `end_time` | *float* | No | End time in milliseconds | | `fight_i_ds` | *List[int]* | No | List of fight IDs to include | -| `kill_type` | *KillType* | No | Filter by kill type | +| `kill_type` | [*KillType*](../enums/#killtype) | No | Filter by kill type | | `start_time` | *float* | No | Start time in milliseconds | | `translate` | *bool* | No | Translate ability names | | `include_combatant_info` | *bool* | No | Include detailed combatant information | diff --git a/docs/development/changelog.md b/docs/development/changelog.md index c0aa02c..423a531 100644 --- a/docs/development/changelog.md +++ b/docs/development/changelog.md @@ -30,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added comprehensive troubleshooting guide - Added OAuth2 examples (sync, async, Flask, FastAPI) - Reorganized docs structure (Getting Started and Development sections) -- **Testing**: Total test count increased from 322 to 428+ tests +- **Testing**: Total test count increased from 322 to 428 tests - Added 10 unit tests for OAuth2 functionality - Added 8 integration tests for UserData methods - Added 8 integration tests for progress race functionality diff --git a/test.py b/test.py new file mode 100644 index 0000000..4cb805d --- /dev/null +++ b/test.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +""" +Example: Using filterExpression to efficiently fetch buff uptimes +This demonstrates how to use filter expressions to reduce API calls +when analyzing multiple buffs/debuffs. +""" + +import asyncio +import os +from typing import Dict, List + +from esologs import Client +from esologs.auth import get_access_token + + +async def calculate_buff_uptimes( + events: List[Dict], start_time: float, end_time: float +) -> Dict[int, Dict[int, float]]: + """ + Calculate buff uptimes for each player from event data. + + Returns: + Dict mapping source_id -> ability_id -> uptime_seconds + """ + # Track buff states: source_id -> ability_id -> apply_timestamp + active_buffs: Dict[int, Dict[int, float]] = {} + # Track total uptimes: source_id -> ability_id -> total_seconds + uptimes: Dict[int, Dict[int, float]] = {} + + for event in events: + source_id = event.get("sourceID", 0) + ability_id = event.get("abilityGameID", 0) + timestamp = event.get("timestamp", 0) + event_type = event.get("type", "") + + if source_id == 0 or ability_id == 0: + continue + + # Initialize nested dicts if needed + if source_id not in active_buffs: + active_buffs[source_id] = {} + uptimes[source_id] = {} + if ability_id not in uptimes[source_id]: + uptimes[source_id][ability_id] = 0.0 + + if event_type == "applybuff": + # Start tracking this buff + active_buffs[source_id][ability_id] = timestamp + elif event_type == "removebuff": + # Calculate duration and add to total + if ability_id in active_buffs[source_id]: + duration = ( + timestamp - active_buffs[source_id][ability_id] + ) / 1000.0 # Convert to seconds + uptimes[source_id][ability_id] += duration + del active_buffs[source_id][ability_id] + + # Handle any buffs still active at end of fight + for source_id, buffs in active_buffs.items(): + for ability_id, apply_time in buffs.items(): + duration = (end_time - apply_time) / 1000.0 + uptimes[source_id][ability_id] += duration + + return uptimes + + +async def main(): + # Initialize client with credentials + client_id = os.getenv("ESOLOGS_ID") + client_secret = os.getenv("ESOLOGS_SECRET") + + if not client_id or not client_secret: + print("Error: Please set ESOLOGS_ID and ESOLOGS_SECRET environment variables") + return + + # Get access token using client credentials + access_token = get_access_token(client_id, client_secret) + + # Create client with proper URL and authorization header + client = Client( + url="https://www.esologs.com/api/v2/client", + headers={"Authorization": f"Bearer {access_token}"}, + ) + + # Example parameters from the Discord discussion + report_code = "xb7TKHXR8DJByp4Q" + fight_id = 17 + start_time = 2614035 + end_time = 2777081 + + # Buff IDs to analyze + # 109966 = Major Courage + # 109967 = Minor Courage (example, actual ID may differ) + buff_ids = [109966, 109967] + buff_names = {109966: "Major Courage", 109967: "Minor Courage"} + + print(f"Analyzing buff uptimes for report {report_code}, fight {fight_id}") + print( + f"Time range: {start_time} - {end_time} ({(end_time - start_time) / 1000:.1f} seconds)" + ) + print(f"Buffs: {', '.join(buff_names.values())}") + print("-" * 60) + + try: + # Method 1: Using filterExpression (efficient - single request) + print("\nMethod 1: Using filterExpression (recommended)") + filter_expr = f"type in ('applybuff', 'removebuff') and ability.id in ({','.join(map(str, buff_ids))})" + + response = await client.get_report_events( + code=report_code, + fight_i_ds=[fight_id], + start_time=start_time, + end_time=end_time, + filter_expression=filter_expr, + ) + + events = response.report_data.report.events.data + print(f"Fetched {len(events)} events in single request") + + # Calculate uptimes + uptimes = await calculate_buff_uptimes(events, start_time, end_time) + fight_duration = (end_time - start_time) / 1000.0 + + # Display results + print("\nBuff Uptimes by Player:") + for source_id, buffs in uptimes.items(): + print(f"\nPlayer {source_id}:") + for ability_id, uptime in buffs.items(): + buff_name = buff_names.get(ability_id, f"Unknown ({ability_id})") + percentage = (uptime / fight_duration) * 100 + print(f" {buff_name}: {uptime:.1f}s ({percentage:.1f}%)") + + # Method 2: Individual requests (less efficient - for comparison) + print("\n" + "-" * 60) + print("\nMethod 2: Individual ability requests (not recommended)") + print("This method would require multiple API calls:") + + for buff_id in buff_ids: + # We're not actually making these calls to save API quota + # This is just to show what the less efficient approach would look like + print( + f" - get_report_table(code='{report_code}', ability_id={buff_id}, ...)" + ) + + print( + "\nNote: Method 1 is more efficient as it uses only 1 API call instead of", + len(buff_ids), + ) + + except Exception as e: + print(f"Error: {e}") + import traceback + + traceback.print_exc() + finally: + # Clean up (Client doesn't have a close method in this version) + pass + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/test_filterexpression.py b/test_filterexpression.py new file mode 100644 index 0000000..5d53d07 --- /dev/null +++ b/test_filterexpression.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +""" +Example: Using filterExpression to efficiently fetch buff uptimes +This demonstrates how to use filter expressions to reduce API calls +when analyzing multiple buffs/debuffs. +""" + +import asyncio +import os +from typing import Dict, List + +from esologs import Client + + +async def calculate_buff_uptimes( + events: List[Dict], start_time: float, end_time: float +) -> Dict[int, Dict[int, float]]: + """ + Calculate buff uptimes for each player from event data. + + Returns: + Dict mapping source_id -> ability_id -> uptime_seconds + """ + # Track buff states: source_id -> ability_id -> apply_timestamp + active_buffs: Dict[int, Dict[int, float]] = {} + # Track total uptimes: source_id -> ability_id -> total_seconds + uptimes: Dict[int, Dict[int, float]] = {} + + for event in events: + source_id = event.get("sourceID", 0) + ability_id = event.get("abilityGameID", 0) + timestamp = event.get("timestamp", 0) + event_type = event.get("type", "") + + if source_id == 0 or ability_id == 0: + continue + + # Initialize nested dicts if needed + if source_id not in active_buffs: + active_buffs[source_id] = {} + uptimes[source_id] = {} + if ability_id not in uptimes[source_id]: + uptimes[source_id][ability_id] = 0.0 + + if event_type == "applybuff": + # Start tracking this buff + active_buffs[source_id][ability_id] = timestamp + elif event_type == "removebuff": + # Calculate duration and add to total + if ability_id in active_buffs[source_id]: + duration = ( + timestamp - active_buffs[source_id][ability_id] + ) / 1000.0 # Convert to seconds + uptimes[source_id][ability_id] += duration + del active_buffs[source_id][ability_id] + + # Handle any buffs still active at end of fight + for source_id, buffs in active_buffs.items(): + for ability_id, apply_time in buffs.items(): + duration = (end_time - apply_time) / 1000.0 + uptimes[source_id][ability_id] += duration + + return uptimes + + +async def main(): + # Initialize client with credentials + client_id = os.getenv("ESOLOGS_ID") + client_secret = os.getenv("ESOLOGS_SECRET") + + if not client_id or not client_secret: + print("Error: Please set ESOLOGS_ID and ESOLOGS_SECRET environment variables") + return + + # Create client + client = Client(client_id=client_id, client_secret=client_secret) + + # Example parameters from the Discord discussion + report_code = "xb7TKHXR8DJByp4Q" + fight_id = 17 + start_time = 2614035 + end_time = 2777081 + + # Buff IDs to analyze + # 109966 = Major Courage + # 109967 = Minor Courage (example, actual ID may differ) + buff_ids = [109966, 109967] + buff_names = {109966: "Major Courage", 109967: "Minor Courage"} + + print(f"Analyzing buff uptimes for report {report_code}, fight {fight_id}") + print( + f"Time range: {start_time} - {end_time} ({(end_time - start_time) / 1000:.1f} seconds)" + ) + print(f"Buffs: {', '.join(buff_names.values())}") + print("-" * 60) + + try: + # Method 1: Using filterExpression (efficient - single request) + print("\nMethod 1: Using filterExpression (recommended)") + filter_expr = f"type in ('applybuff', 'removebuff') and ability.id in ({','.join(map(str, buff_ids))})" + + response = await client.get_report_events( + code=report_code, + fight_i_ds=[fight_id], + start_time=start_time, + end_time=end_time, + filter_expression=filter_expr, + ) + + events = response.report_data.report.events.data + print(f"Fetched {len(events)} events in single request") + + # Calculate uptimes + uptimes = await calculate_buff_uptimes(events, start_time, end_time) + fight_duration = (end_time - start_time) / 1000.0 + + # Display results + print("\nBuff Uptimes by Player:") + for source_id, buffs in uptimes.items(): + print(f"\nPlayer {source_id}:") + for ability_id, uptime in buffs.items(): + buff_name = buff_names.get(ability_id, f"Unknown ({ability_id})") + percentage = (uptime / fight_duration) * 100 + print(f" {buff_name}: {uptime:.1f}s ({percentage:.1f}%)") + + # Method 2: Individual requests (less efficient - for comparison) + print("\n" + "-" * 60) + print("\nMethod 2: Individual ability requests (not recommended)") + print("This method would require multiple API calls:") + + for buff_id in buff_ids: + # We're not actually making these calls to save API quota + # This is just to show what the less efficient approach would look like + print( + f" - get_report_table(code='{report_code}', ability_id={buff_id}, ...)" + ) + + print( + "\nNote: Method 1 is more efficient as it uses only 1 API call instead of", + len(buff_ids), + ) + + except Exception as e: + print(f"Error: {e}") + import traceback + + traceback.print_exc() + finally: + # Clean up + await client.close() + + +if __name__ == "__main__": + asyncio.run(main()) From 008438bea168355a8ead470a5d057c277502c82f Mon Sep 17 00:00:00 2001 From: knowlen Date: Sun, 3 Aug 2025 23:20:28 -0700 Subject: [PATCH 05/16] Fix enum documentation accuracy and improve Advanced Usage formatting --- docs/api-reference/enums.md | 14 +++++----- docs/api-reference/guild-data.md | 14 +++++++++- docs/getting-started/advanced-usage.md | 36 ++++++++++++++++++-------- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/docs/api-reference/enums.md b/docs/api-reference/enums.md index c909162..d4f743d 100644 --- a/docs/api-reference/enums.md +++ b/docs/api-reference/enums.md @@ -116,8 +116,7 @@ Filter for rankings based on external buff usage. **Used by:** -- [`get_character_encounter_rankings`](../character-data/#get_character_encounter_rankings) -- [`get_character_zone_rankings`](../character-data/#get_character_zone_rankings) +*Note: This enum is defined in the GraphQL schema but is not currently exposed through any API methods in the Python client.* --- @@ -197,8 +196,7 @@ Filter for rankings based on hard mode completion. **Used by:** -- [`get_character_encounter_rankings`](../character-data/#get_character_encounter_rankings) -- [`get_character_zone_rankings`](../character-data/#get_character_zone_rankings) +*Note: This enum is defined in the GraphQL schema but is not currently exposed through any API methods in the Python client.* --- @@ -252,8 +250,7 @@ Leaderboard ranking tiers. **Used by:** -- [`get_character_encounter_rankings`](../character-data/#get_character_encounter_rankings) -- [`get_character_zone_rankings`](../character-data/#get_character_zone_rankings) +*Note: This enum is defined in the GraphQL schema but is not currently exposed through any API methods in the Python client.* --- @@ -270,6 +267,7 @@ How to compare rankings. - [`get_character_encounter_rankings`](../character-data/#get_character_encounter_rankings) - [`get_character_zone_rankings`](../character-data/#get_character_zone_rankings) +- [`get_report_rankings`](../report-analysis/#get_report_rankings) --- @@ -286,6 +284,7 @@ Time period for rankings. - [`get_character_encounter_rankings`](../character-data/#get_character_encounter_rankings) - [`get_character_zone_rankings`](../character-data/#get_character_zone_rankings) +- [`get_report_rankings`](../report-analysis/#get_report_rankings) --- @@ -344,8 +343,7 @@ User subscription status levels. **Used by:** -- [`get_current_user`](../user-data/#get_current_user) -- [`get_user_by_id`](../user-data/#get_user_by_id) +*Note: This enum is defined in the GraphQL schema but is not currently exposed through any API methods in the Python client.* --- diff --git a/docs/api-reference/guild-data.md b/docs/api-reference/guild-data.md index 3c7866c..fc28702 100644 --- a/docs/api-reference/guild-data.md +++ b/docs/api-reference/guild-data.md @@ -247,7 +247,19 @@ Players: 12 | `limit` | *int \| None* | No | Members per page (default: 100, max: 100) | | `page` | *int \| None* | No | Page number (default: 1) | -**Returns**: `GetGuildMembers` object with member list +**Returns**: `GetGuildMembers` object with the following structure: + +| Field | Type | Description | +|-------|------|-------------| +| `guild_data.guild.members.data` | *List[Member]* | List of guild members | +| `guild_data.guild.members.data[].id` | *int* | Member character ID | +| `guild_data.guild.members.data[].name` | *str* | Member character name | +| `guild_data.guild.members.data[].guild_rank` | *int* | Member rank (corresponds to [*GuildRank*](../enums/#guildrank) enum values) | +| `guild_data.guild.members.data[].server` | *Server* | Member's server info | +| `guild_data.guild.members.total` | *int* | Total number of members | +| `guild_data.guild.members.per_page` | *int* | Members per page | +| `guild_data.guild.members.current_page` | *int* | Current page number | +| `guild_data.guild.members.has_more_pages` | *bool* | Whether more pages exist | **Note**: Member rosters may not be available for all games (e.g., Classic WoW) diff --git a/docs/getting-started/advanced-usage.md b/docs/getting-started/advanced-usage.md index b5aef43..38aab77 100644 --- a/docs/getting-started/advanced-usage.md +++ b/docs/getting-started/advanced-usage.md @@ -64,21 +64,21 @@ Filter expressions use a SQL-like syntax with the following operators: Common fields you can filter on: - **Event Fields** - - `type` - Event type (damage, heal, applybuff, removebuff, death, etc.) - - `timestamp` - When the event occurred + - `type` - Event type (damage, heal, applybuff, removebuff, death, etc.) + - `timestamp` - When the event occurred - **Ability Fields** - - `ability.id` - Numeric ability ID - - `ability.name` - Ability name (string) - - `ability.type` - Ability type + - `ability.id` - Numeric ability ID + - `ability.name` - Ability name (string) + - `ability.type` - Ability type - **Actor Fields** - - `source.id` - Source actor ID - - `source.name` - Source actor name - - `source.type` - Actor type (Player, NPC, etc.) - - `target.id` - Target actor ID - - `target.name` - Target actor name - - `target.type` - Target actor type + - `source.id` - Source actor ID + - `source.name` - Source actor name + - `source.type` - Actor type (Player, NPC, etc.) + - `target.id` - Target actor ID + - `target.name` - Target actor name + - `target.type` - Target actor type ### Common Filter Expression Patterns @@ -258,6 +258,20 @@ if __name__ == "__main__": asyncio.run(main()) ``` +**Output**: +``` +Fight Duration: 163.0s + +Player 66: + Ability 109966: 62.5s (38.3%) +Player 17: + Ability 61737: 146.8s (90.0%) +Player 22: + Ability 61737: 88.0s (54.0%) +Player 67: + Ability 40225: 17.5s (10.7%) +``` + ### Performance Benefits Using filter expressions provides several performance benefits: From 64c58e247def00ded791ee1f25a37fa9cc441709 Mon Sep 17 00:00:00 2001 From: knowlen Date: Sun, 3 Aug 2025 23:22:27 -0700 Subject: [PATCH 06/16] Add Serena MCP cache files to .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 6aac83a..9d83ac4 100644 --- a/.gitignore +++ b/.gitignore @@ -195,3 +195,7 @@ test_validation.py .esologs_token.json *_token.json *.token.json + +# Serena MCP cache files +.serena/cache/ +.serena/memories/ From 08240b4921936c7d8cfa1ef9a4882acd6abd3c7c Mon Sep 17 00:00:00 2001 From: knowlen Date: Sun, 3 Aug 2025 23:22:46 -0700 Subject: [PATCH 07/16] Remove Serena cache files from version control --- .../document_symbols_cache_v23-06-25.pkl | Bin 354190 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .serena/cache/python/document_symbols_cache_v23-06-25.pkl diff --git a/.serena/cache/python/document_symbols_cache_v23-06-25.pkl b/.serena/cache/python/document_symbols_cache_v23-06-25.pkl deleted file mode 100644 index ea81c38d8a68046672e69d85379200e86d1e6d00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 354190 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 Wh;t0j1v3Pu1m+T9 Date: Sun, 3 Aug 2025 23:24:19 -0700 Subject: [PATCH 08/16] delete testfiles --- test.py | 161 --------------------------------------- test_filterexpression.py | 154 ------------------------------------- 2 files changed, 315 deletions(-) delete mode 100644 test.py delete mode 100644 test_filterexpression.py diff --git a/test.py b/test.py deleted file mode 100644 index 4cb805d..0000000 --- a/test.py +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env python3 -""" -Example: Using filterExpression to efficiently fetch buff uptimes -This demonstrates how to use filter expressions to reduce API calls -when analyzing multiple buffs/debuffs. -""" - -import asyncio -import os -from typing import Dict, List - -from esologs import Client -from esologs.auth import get_access_token - - -async def calculate_buff_uptimes( - events: List[Dict], start_time: float, end_time: float -) -> Dict[int, Dict[int, float]]: - """ - Calculate buff uptimes for each player from event data. - - Returns: - Dict mapping source_id -> ability_id -> uptime_seconds - """ - # Track buff states: source_id -> ability_id -> apply_timestamp - active_buffs: Dict[int, Dict[int, float]] = {} - # Track total uptimes: source_id -> ability_id -> total_seconds - uptimes: Dict[int, Dict[int, float]] = {} - - for event in events: - source_id = event.get("sourceID", 0) - ability_id = event.get("abilityGameID", 0) - timestamp = event.get("timestamp", 0) - event_type = event.get("type", "") - - if source_id == 0 or ability_id == 0: - continue - - # Initialize nested dicts if needed - if source_id not in active_buffs: - active_buffs[source_id] = {} - uptimes[source_id] = {} - if ability_id not in uptimes[source_id]: - uptimes[source_id][ability_id] = 0.0 - - if event_type == "applybuff": - # Start tracking this buff - active_buffs[source_id][ability_id] = timestamp - elif event_type == "removebuff": - # Calculate duration and add to total - if ability_id in active_buffs[source_id]: - duration = ( - timestamp - active_buffs[source_id][ability_id] - ) / 1000.0 # Convert to seconds - uptimes[source_id][ability_id] += duration - del active_buffs[source_id][ability_id] - - # Handle any buffs still active at end of fight - for source_id, buffs in active_buffs.items(): - for ability_id, apply_time in buffs.items(): - duration = (end_time - apply_time) / 1000.0 - uptimes[source_id][ability_id] += duration - - return uptimes - - -async def main(): - # Initialize client with credentials - client_id = os.getenv("ESOLOGS_ID") - client_secret = os.getenv("ESOLOGS_SECRET") - - if not client_id or not client_secret: - print("Error: Please set ESOLOGS_ID and ESOLOGS_SECRET environment variables") - return - - # Get access token using client credentials - access_token = get_access_token(client_id, client_secret) - - # Create client with proper URL and authorization header - client = Client( - url="https://www.esologs.com/api/v2/client", - headers={"Authorization": f"Bearer {access_token}"}, - ) - - # Example parameters from the Discord discussion - report_code = "xb7TKHXR8DJByp4Q" - fight_id = 17 - start_time = 2614035 - end_time = 2777081 - - # Buff IDs to analyze - # 109966 = Major Courage - # 109967 = Minor Courage (example, actual ID may differ) - buff_ids = [109966, 109967] - buff_names = {109966: "Major Courage", 109967: "Minor Courage"} - - print(f"Analyzing buff uptimes for report {report_code}, fight {fight_id}") - print( - f"Time range: {start_time} - {end_time} ({(end_time - start_time) / 1000:.1f} seconds)" - ) - print(f"Buffs: {', '.join(buff_names.values())}") - print("-" * 60) - - try: - # Method 1: Using filterExpression (efficient - single request) - print("\nMethod 1: Using filterExpression (recommended)") - filter_expr = f"type in ('applybuff', 'removebuff') and ability.id in ({','.join(map(str, buff_ids))})" - - response = await client.get_report_events( - code=report_code, - fight_i_ds=[fight_id], - start_time=start_time, - end_time=end_time, - filter_expression=filter_expr, - ) - - events = response.report_data.report.events.data - print(f"Fetched {len(events)} events in single request") - - # Calculate uptimes - uptimes = await calculate_buff_uptimes(events, start_time, end_time) - fight_duration = (end_time - start_time) / 1000.0 - - # Display results - print("\nBuff Uptimes by Player:") - for source_id, buffs in uptimes.items(): - print(f"\nPlayer {source_id}:") - for ability_id, uptime in buffs.items(): - buff_name = buff_names.get(ability_id, f"Unknown ({ability_id})") - percentage = (uptime / fight_duration) * 100 - print(f" {buff_name}: {uptime:.1f}s ({percentage:.1f}%)") - - # Method 2: Individual requests (less efficient - for comparison) - print("\n" + "-" * 60) - print("\nMethod 2: Individual ability requests (not recommended)") - print("This method would require multiple API calls:") - - for buff_id in buff_ids: - # We're not actually making these calls to save API quota - # This is just to show what the less efficient approach would look like - print( - f" - get_report_table(code='{report_code}', ability_id={buff_id}, ...)" - ) - - print( - "\nNote: Method 1 is more efficient as it uses only 1 API call instead of", - len(buff_ids), - ) - - except Exception as e: - print(f"Error: {e}") - import traceback - - traceback.print_exc() - finally: - # Clean up (Client doesn't have a close method in this version) - pass - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/test_filterexpression.py b/test_filterexpression.py deleted file mode 100644 index 5d53d07..0000000 --- a/test_filterexpression.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env python3 -""" -Example: Using filterExpression to efficiently fetch buff uptimes -This demonstrates how to use filter expressions to reduce API calls -when analyzing multiple buffs/debuffs. -""" - -import asyncio -import os -from typing import Dict, List - -from esologs import Client - - -async def calculate_buff_uptimes( - events: List[Dict], start_time: float, end_time: float -) -> Dict[int, Dict[int, float]]: - """ - Calculate buff uptimes for each player from event data. - - Returns: - Dict mapping source_id -> ability_id -> uptime_seconds - """ - # Track buff states: source_id -> ability_id -> apply_timestamp - active_buffs: Dict[int, Dict[int, float]] = {} - # Track total uptimes: source_id -> ability_id -> total_seconds - uptimes: Dict[int, Dict[int, float]] = {} - - for event in events: - source_id = event.get("sourceID", 0) - ability_id = event.get("abilityGameID", 0) - timestamp = event.get("timestamp", 0) - event_type = event.get("type", "") - - if source_id == 0 or ability_id == 0: - continue - - # Initialize nested dicts if needed - if source_id not in active_buffs: - active_buffs[source_id] = {} - uptimes[source_id] = {} - if ability_id not in uptimes[source_id]: - uptimes[source_id][ability_id] = 0.0 - - if event_type == "applybuff": - # Start tracking this buff - active_buffs[source_id][ability_id] = timestamp - elif event_type == "removebuff": - # Calculate duration and add to total - if ability_id in active_buffs[source_id]: - duration = ( - timestamp - active_buffs[source_id][ability_id] - ) / 1000.0 # Convert to seconds - uptimes[source_id][ability_id] += duration - del active_buffs[source_id][ability_id] - - # Handle any buffs still active at end of fight - for source_id, buffs in active_buffs.items(): - for ability_id, apply_time in buffs.items(): - duration = (end_time - apply_time) / 1000.0 - uptimes[source_id][ability_id] += duration - - return uptimes - - -async def main(): - # Initialize client with credentials - client_id = os.getenv("ESOLOGS_ID") - client_secret = os.getenv("ESOLOGS_SECRET") - - if not client_id or not client_secret: - print("Error: Please set ESOLOGS_ID and ESOLOGS_SECRET environment variables") - return - - # Create client - client = Client(client_id=client_id, client_secret=client_secret) - - # Example parameters from the Discord discussion - report_code = "xb7TKHXR8DJByp4Q" - fight_id = 17 - start_time = 2614035 - end_time = 2777081 - - # Buff IDs to analyze - # 109966 = Major Courage - # 109967 = Minor Courage (example, actual ID may differ) - buff_ids = [109966, 109967] - buff_names = {109966: "Major Courage", 109967: "Minor Courage"} - - print(f"Analyzing buff uptimes for report {report_code}, fight {fight_id}") - print( - f"Time range: {start_time} - {end_time} ({(end_time - start_time) / 1000:.1f} seconds)" - ) - print(f"Buffs: {', '.join(buff_names.values())}") - print("-" * 60) - - try: - # Method 1: Using filterExpression (efficient - single request) - print("\nMethod 1: Using filterExpression (recommended)") - filter_expr = f"type in ('applybuff', 'removebuff') and ability.id in ({','.join(map(str, buff_ids))})" - - response = await client.get_report_events( - code=report_code, - fight_i_ds=[fight_id], - start_time=start_time, - end_time=end_time, - filter_expression=filter_expr, - ) - - events = response.report_data.report.events.data - print(f"Fetched {len(events)} events in single request") - - # Calculate uptimes - uptimes = await calculate_buff_uptimes(events, start_time, end_time) - fight_duration = (end_time - start_time) / 1000.0 - - # Display results - print("\nBuff Uptimes by Player:") - for source_id, buffs in uptimes.items(): - print(f"\nPlayer {source_id}:") - for ability_id, uptime in buffs.items(): - buff_name = buff_names.get(ability_id, f"Unknown ({ability_id})") - percentage = (uptime / fight_duration) * 100 - print(f" {buff_name}: {uptime:.1f}s ({percentage:.1f}%)") - - # Method 2: Individual requests (less efficient - for comparison) - print("\n" + "-" * 60) - print("\nMethod 2: Individual ability requests (not recommended)") - print("This method would require multiple API calls:") - - for buff_id in buff_ids: - # We're not actually making these calls to save API quota - # This is just to show what the less efficient approach would look like - print( - f" - get_report_table(code='{report_code}', ability_id={buff_id}, ...)" - ) - - print( - "\nNote: Method 1 is more efficient as it uses only 1 API call instead of", - len(buff_ids), - ) - - except Exception as e: - print(f"Error: {e}") - import traceback - - traceback.print_exc() - finally: - # Clean up - await client.close() - - -if __name__ == "__main__": - asyncio.run(main()) From edfcbc684ec4704553a04274fab0491c78403e97 Mon Sep 17 00:00:00 2001 From: knowlen Date: Sun, 3 Aug 2025 23:25:53 -0700 Subject: [PATCH 09/16] Remove all Serena files from version control and ignore entire .serena directory --- .gitignore | 5 +- .serena/memories/code_style_conventions.md | 115 ------------------ .serena/memories/project_overview.md | 57 --------- .serena/memories/project_structure.md | 92 -------------- .serena/memories/suggested_commands.md | 101 --------------- .serena/memories/task_completion_checklist.md | 90 -------------- .serena/project.yml | 68 ----------- 7 files changed, 2 insertions(+), 526 deletions(-) delete mode 100644 .serena/memories/code_style_conventions.md delete mode 100644 .serena/memories/project_overview.md delete mode 100644 .serena/memories/project_structure.md delete mode 100644 .serena/memories/suggested_commands.md delete mode 100644 .serena/memories/task_completion_checklist.md delete mode 100644 .serena/project.yml diff --git a/.gitignore b/.gitignore index 9d83ac4..2018025 100644 --- a/.gitignore +++ b/.gitignore @@ -196,6 +196,5 @@ test_validation.py *_token.json *.token.json -# Serena MCP cache files -.serena/cache/ -.serena/memories/ +# Serena MCP files +.serena/ diff --git a/.serena/memories/code_style_conventions.md b/.serena/memories/code_style_conventions.md deleted file mode 100644 index 757d337..0000000 --- a/.serena/memories/code_style_conventions.md +++ /dev/null @@ -1,115 +0,0 @@ -# ESO Logs Python - Code Style and Conventions - -## Python Version -- Target: Python 3.8+ -- Use modern Python features but maintain 3.8 compatibility - -## Code Formatting -- **Black**: Line length 88 characters -- **isort**: Black-compatible profile -- **Indentation**: 4 spaces (no tabs) -- **No trailing whitespace** -- **Files end with newline** - -## Import Style -- **Absolute imports only** (no relative imports) -- **Import order** (via isort): - 1. Standard library - 2. Third-party packages - 3. Local application imports -- **Example**: - ```python - import asyncio - from typing import Dict, List, Optional - - import httpx - from pydantic import BaseModel - - from esologs.client import Client - from esologs.validators import validate_parameters - ``` - -## Type Hints -- **Required for all functions and methods** -- **Use typing module for complex types** -- **Pydantic models for data validation** -- **mypy strict mode compliance** -- **Example**: - ```python - async def get_report_events( - self, - code: str, - start_time: Optional[float] = None, - end_time: Optional[float] = None, - ) -> ReportEventsResponse: - """Get events from a report.""" - ``` - -## Docstrings -- **Google-style docstrings** -- **Required for all public functions/classes** -- **Include parameters, returns, and examples** -- **Example**: - ```python - def validate_bearer_token_format(token: str) -> str: - """Validate and format a bearer token. - - Args: - token: The token to validate (with or without 'Bearer' prefix) - - Returns: - The formatted token with 'Bearer' prefix - - Raises: - ValueError: If token format is invalid - """ - ``` - -## Naming Conventions -- **Classes**: PascalCase (e.g., `ReportEventsResponse`) -- **Functions/Methods**: snake_case (e.g., `get_report_events`) -- **Constants**: UPPER_SNAKE_CASE (e.g., `BEARER_TOKEN_PATTERN`) -- **Private methods**: Leading underscore (e.g., `_validate_input`) - -## Error Handling -- **Use specific exceptions** (not bare except) -- **Provide helpful error messages** -- **Handle API errors gracefully** -- **Example**: - ```python - if not token: - raise ValueError("Token cannot be empty") - ``` - -## Testing -- **Test file naming**: `test_*.py` -- **Test function naming**: `test_descriptive_name()` -- **Use pytest fixtures for common setup** -- **Include edge cases and error scenarios** - -## Architecture Patterns -- **Factory Method Pattern**: For dynamic method generation -- **Mixin Pattern**: For organizing API methods by category -- **Protocol Types**: For type safety with dynamic methods - -## Generated Code -- **Location**: `esologs/_generated/` -- **Do not edit manually** - use ariadne-codegen -- **Excluded from formatting/linting tools** - -## Security -- **Never commit credentials** -- **Use environment variables for secrets** -- **Validate all user input** -- **No print statements in production code** - -## Performance -- **Use async/await for I/O operations** -- **Implement streaming for large datasets** -- **Cache static data when appropriate** - -## Documentation -- **Update docs with every feature** -- **Include code examples** -- **Keep README current** -- **Use MkDocs for documentation site** diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md deleted file mode 100644 index a0ec667..0000000 --- a/.serena/memories/project_overview.md +++ /dev/null @@ -1,57 +0,0 @@ -# ESO Logs Python Project Overview - -## Purpose -esologs-python is a comprehensive Python client library for the ESO Logs API v2, designed for Elder Scrolls Online (ESO) players and developers. It provides both synchronous and asynchronous interfaces to access ESO Logs data for analyzing combat logs, tracking performance metrics, and building tools for the ESO community. - -## Current Status -- **Version**: 0.2.0b1 (Beta) -- **API Coverage**: 100% (42/42 methods implemented) -- **Package Name**: esologs-python (on PyPI) -- **Documentation**: https://esologs-python.readthedocs.io/ -- **Repository**: https://github.com/knowlen/esologs-python - -## Tech Stack -- **Language**: Python 3.8+ -- **Core Dependencies**: - - requests (HTTP client) - - httpx (async HTTP client) - - pydantic (data validation) - - ariadne-codegen (GraphQL code generation) - - aiofiles (async file operations) -- **Development Tools**: - - pytest (testing framework) - - black (code formatter) - - isort (import sorter) - - ruff (linter) - - mypy (type checker) - - pre-commit (git hooks) - - mkdocs (documentation) - -## Architecture -- **Modular Client Design**: Uses Factory Method and Mixin patterns -- **Main client reduced from 1,610 to 86 lines** through refactoring -- **Code Organization**: - - `esologs/client.py` - Main client (86 lines) - - `esologs/mixins/` - API method mixins (7 files) - - `esologs/method_factory.py` - Dynamic method generation - - `esologs/_generated/` - Auto-generated GraphQL code - - `esologs/validators.py` - Input validation - - `esologs/param_builders.py` - Parameter processing - - `esologs/queries.py` - GraphQL queries - -## API Features -1. **Game Data** - 13 methods (abilities, classes, items, NPCs, maps, factions) -2. **Character Data** - 5 methods (info, reports, rankings) -3. **Report Data** - 10 methods (events, tables, rankings, search) -4. **World Data** - 4 methods (regions, zones, encounters, expansions) -5. **Guild Data** - 5 methods (info, reports, attendance, members) -6. **Rate Limit** - 1 method -7. **Progress Race** - 1 method (world/realm first tracking) -8. **User Data** - 3 methods (OAuth2 authenticated endpoints) - -## Testing Strategy -- **428 total tests** across 4 suites: - - Unit Tests (164) - Logic validation, no API required - - Integration Tests (129) - Live API testing - - Documentation Tests (117) - Code example validation - - Sanity Tests (18) - API health check diff --git a/.serena/memories/project_structure.md b/.serena/memories/project_structure.md deleted file mode 100644 index c617262..0000000 --- a/.serena/memories/project_structure.md +++ /dev/null @@ -1,92 +0,0 @@ -# ESO Logs Python - Project Structure - -## Repository Layout -``` -esologs-python/ -├── esologs/ # Main package -│ ├── __init__.py # Package initialization, exports -│ ├── client.py # Main client class (86 lines) -│ ├── client_factory.py # Client instantiation -│ ├── auth.py # OAuth2 authentication -│ ├── user_auth.py # User authentication flows -│ ├── method_factory.py # Dynamic method generation -│ ├── validators.py # Input validation functions -│ ├── param_builders.py # Parameter processing -│ ├── queries.py # GraphQL query definitions -│ ├── py.typed # PEP 561 type hint marker -│ ├── mixins/ # API method mixins -│ │ ├── __init__.py -│ │ ├── game_data.py # Game data methods -│ │ ├── character.py # Character methods -│ │ ├── guild.py # Guild methods -│ │ ├── report.py # Report methods -│ │ ├── world_data.py # World data methods -│ │ ├── progress_race.py # Progress race methods -│ │ └── user.py # User methods -│ └── _generated/ # Auto-generated GraphQL code -│ ├── *.py # 30+ generated files -│ └── ... -├── tests/ # Test suites -│ ├── __init__.py -│ ├── conftest.py # Shared fixtures -│ ├── README.md # Test documentation -│ ├── unit/ # Unit tests (164 tests) -│ ├── integration/ # Integration tests (129 tests) -│ ├── sanity/ # Sanity tests (18 tests) -│ └── docs/ # Documentation tests (117 tests) -├── docs/ # MkDocs documentation -│ ├── index.md # Home page -│ ├── installation.md # Installation guide -│ ├── quickstart.md # Getting started -│ ├── api-reference/ # API documentation -│ ├── examples/ # Code examples -│ └── assets/ # Images, logos -├── scripts/ # Utility scripts -│ ├── generate_client.sh # GraphQL code generation -│ ├── post_codegen.py # Post-generation processing -│ └── quick_api_check.py # API health check -├── .github/ # GitHub configuration -│ ├── workflows/ # CI/CD workflows -│ └── ISSUE_TEMPLATE/ # Issue templates -├── .serena/ # Serena MCP configuration -│ ├── project.yml # Project config -│ └── memories/ # Project memories -├── pyproject.toml # Package configuration -├── mkdocs.yml # Documentation config -├── .pre-commit-config.yaml # Pre-commit hooks -├── .gitignore # Git ignore rules -├── .readthedocs.yml # ReadTheDocs config -├── LICENSE # MIT license -├── README.md # Project README -├── MANIFEST.in # Package manifest -├── schema.graphql # ESO Logs GraphQL schema -└── queries.graphql # GraphQL queries - -## Key Directories - -### `/esologs` - Main Package -- Core implementation with modular architecture -- Mixins organize 42 API methods into logical groups -- Generated code isolated in `_generated/` subdirectory - -### `/tests` - Comprehensive Test Suite -- 428 total tests across 4 complementary suites -- Each suite has specific purpose and requirements -- Shared fixtures in conftest.py - -### `/docs` - Documentation -- MkDocs-based documentation site -- Comprehensive API reference -- Examples and guides - -### `/scripts` - Development Tools -- Code generation automation -- API testing utilities -- Build and deployment scripts - -## Important Files -- `pyproject.toml` - Package metadata and dependencies -- `mkdocs.yml` - Documentation configuration -- `.pre-commit-config.yaml` - Code quality automation -- `esologs/queries.py` - All GraphQL queries -- `tests/conftest.py` - Shared test fixtures diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md deleted file mode 100644 index 887f9b1..0000000 --- a/.serena/memories/suggested_commands.md +++ /dev/null @@ -1,101 +0,0 @@ -# ESO Logs Python - Suggested Commands - -## Development Commands - -### Testing -```bash -# Run all tests -pytest tests/ -v - -# Run specific test suites -pytest tests/unit/ -v # Unit tests (no API required) -pytest tests/integration/ -v # Integration tests (API required) -pytest tests/sanity/ -v # Sanity tests (API required) -pytest tests/docs/ -v # Documentation tests (API required) - -# Run with coverage -pytest tests/ --cov=esologs --cov-report=html - -# Run specific test markers -pytest -m "not oauth2" -v # Skip OAuth2 tests -pytest -m "not slow" -v # Skip slow tests -``` - -### Code Quality -```bash -# Run all pre-commit checks -pre-commit run --all-files - -# Individual tools -black . # Format code -isort . # Sort imports -ruff check --fix . # Lint and fix -mypy . # Type checking -``` - -### Documentation -```bash -# Build documentation -mkdocs build --clean - -# Serve documentation locally -mkdocs serve - -# View at http://127.0.0.1:8000 -``` - -### Code Generation -```bash -# Regenerate GraphQL client code -./scripts/generate_client.sh - -# Run post-codegen processing -python scripts/post_codegen.py -``` - -### Package Management -```bash -# Install development dependencies -pip install -e ".[dev]" - -# Install all optional dependencies -pip install -e ".[all]" - -# Build package -python -m build - -# Check package -twine check dist/* -``` - -## Environment Variables -```bash -# Required for API access -export ESOLOGS_ID="your_client_id" -export ESOLOGS_SECRET="your_client_secret" - -# Alternative: use .env file -echo "ESOLOGS_ID=your_client_id" >> .env -echo "ESOLOGS_SECRET=your_client_secret" >> .env -``` - -## Git Commands -```bash -# Common workflow -git checkout dev # Development branch -git checkout -b feature/my-feature # Create feature branch -git add . -git commit -m "feat: add new feature" -git push -u origin feature/my-feature - -# Create PR to dev branch for review -``` - -## Quick Checks -```bash -# API health check -python scripts/quick_api_check.py - -# Validate installation -python -c "import esologs; print(esologs.__version__)" -``` diff --git a/.serena/memories/task_completion_checklist.md b/.serena/memories/task_completion_checklist.md deleted file mode 100644 index 70fb57e..0000000 --- a/.serena/memories/task_completion_checklist.md +++ /dev/null @@ -1,90 +0,0 @@ -# ESO Logs Python - Task Completion Checklist - -## When You Complete a Task - -### 1. Code Quality Checks -```bash -# Run all pre-commit checks -pre-commit run --all-files - -# If any fail, fix and re-run until all pass -``` - -### 2. Run Tests -```bash -# Run unit tests (always) -pytest tests/unit/ -v - -# Run integration tests if API-related -pytest tests/integration/ -v - -# Run full test suite for major changes -pytest tests/ -v -``` - -### 3. Type Checking -```bash -# Ensure no type errors -mypy . -``` - -### 4. Update Documentation -- Update docstrings for new/modified functions -- Update README.md if adding new features -- Update API documentation if changing public API -- Add/update code examples - -### 5. Check for Common Issues -- [ ] No print statements in code -- [ ] No hardcoded credentials -- [ ] All imports are absolute (not relative) -- [ ] New files have proper headers -- [ ] Tests added for new functionality -- [ ] Edge cases handled - -### 6. Git Workflow -```bash -# Stage changes -git add . - -# Check what's being committed -git status -git diff --cached - -# Commit with descriptive message -git commit -m "type: description" - -# Push to feature branch -git push origin feature/branch-name -``` - -### 7. PR Checklist -- [ ] All tests pass -- [ ] Code follows style conventions -- [ ] Documentation updated -- [ ] No merge conflicts with target branch -- [ ] PR description explains changes -- [ ] Linked to relevant issues - -## Commit Message Format -``` -type: subject - -body (optional) -``` - -Types: -- `feat`: New feature -- `fix`: Bug fix -- `docs`: Documentation changes -- `style`: Code style changes -- `refactor`: Code refactoring -- `test`: Test additions/changes -- `chore`: Maintenance tasks - -## Important Notes -- **Never push directly to main or dev branches** -- **Always create feature branches** -- **Get PR reviewed before merging** -- **Keep commits focused and atomic** -- **Run tests before committing** diff --git a/.serena/project.yml b/.serena/project.yml deleted file mode 100644 index e8a3f31..0000000 --- a/.serena/project.yml +++ /dev/null @@ -1,68 +0,0 @@ -# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby) -# * For C, use cpp -# * For JavaScript, use typescript -# Special requirements: -# * csharp: Requires the presence of a .sln file in the project folder. -language: python - -# whether to use the project's gitignore file to ignore files -# Added on 2025-04-07 -ignore_all_files_in_gitignore: true -# list of additional paths to ignore -# same syntax as gitignore, so you can use * and ** -# Was previously called `ignored_dirs`, please update your config if you are using that. -# Added (renamed)on 2025-04-07 -ignored_paths: [] - -# whether the project is in read-only mode -# If set to true, all editing tools will be disabled and attempts to use them will result in an error -# Added on 2025-04-18 -read_only: false - - -# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. -# Below is the complete list of tools for convenience. -# To make sure you have the latest list of tools, and to view their descriptions, -# execute `uv run scripts/print_tool_overview.py`. -# -# * `activate_project`: Activates a project by name. -# * `check_onboarding_performed`: Checks whether project onboarding was already performed. -# * `create_text_file`: Creates/overwrites a file in the project directory. -# * `delete_lines`: Deletes a range of lines within a file. -# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. -# * `execute_shell_command`: Executes a shell command. -# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. -# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). -# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). -# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. -# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file or directory. -# * `initial_instructions`: Gets the initial instructions for the current project. -# Should only be used in settings where the system prompt cannot be set, -# e.g. in clients you have no control over, like Claude Desktop. -# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. -# * `insert_at_line`: Inserts content at a given line in a file. -# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. -# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). -# * `list_memories`: Lists memories in Serena's project-specific memory store. -# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). -# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). -# * `read_file`: Reads a file within the project directory. -# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. -# * `remove_project`: Removes a project from the Serena configuration. -# * `replace_lines`: Replaces a range of lines within a file with new content. -# * `replace_symbol_body`: Replaces the full definition of a symbol. -# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. -# * `search_for_pattern`: Performs a search for a pattern in the project. -# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. -# * `switch_modes`: Activates modes by providing a list of their names -# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. -# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. -# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. -# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. -excluded_tools: [] - -# initial prompt for the project. It will always be given to the LLM upon activating the project -# (contrary to the memories, which are loaded on demand). -initial_prompt: "" - -project_name: "esologs-python" From 67dd5d9546fbdb5ccae52a32cdee7adb1777c7ff Mon Sep 17 00:00:00 2001 From: knowlen Date: Sun, 3 Aug 2025 23:31:08 -0700 Subject: [PATCH 10/16] Fix enum documentation to match actual implementation values --- docs/api-reference/enums.md | 75 +++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/docs/api-reference/enums.md b/docs/api-reference/enums.md index d4f743d..9834aac 100644 --- a/docs/api-reference/enums.md +++ b/docs/api-reference/enums.md @@ -65,10 +65,37 @@ Metrics available for character rankings and leaderboards. **Values:** +- `bosscdps` - Boss conduit DPS +- `bossdps` - Boss damage per second +- `bossndps` - Boss normalized DPS +- `bossrdps` - Boss rDPS (raid-contributing DPS) +- `default` - Default metric - `dps` - Damage per second - `hps` - Healing per second +- `krsi` - Kill speed index - `playerscore` - Overall performance score - `playerspeed` - Clear time performance +- `cdps` - Conduit DPS +- `ndps` - Normalized DPS +- `rdps` - Raid-contributing DPS +- `tankhps` - Tank healing per second +- `wdps` - Weighted DPS +- `healercombineddps` - Healer combined DPS +- `healercombinedbossdps` - Healer combined boss DPS +- `healercombinedcdps` - Healer combined conduit DPS +- `healercombinedbosscdps` - Healer combined boss conduit DPS +- `healercombinedndps` - Healer combined normalized DPS +- `healercombinedbossndps` - Healer combined boss normalized DPS +- `healercombinedrdps` - Healer combined rDPS +- `healercombinedbossrdps` - Healer combined boss rDPS +- `tankcombineddps` - Tank combined DPS +- `tankcombinedbossdps` - Tank combined boss DPS +- `tankcombinedcdps` - Tank combined conduit DPS +- `tankcombinedbosscdps` - Tank combined boss conduit DPS +- `tankcombinedndps` - Tank combined normalized DPS +- `tankcombinedbossndps` - Tank combined boss normalized DPS +- `tankcombinedrdps` - Tank combined rDPS +- `tankcombinedbossrdps` - Tank combined boss rDPS **Used by:** @@ -110,9 +137,9 @@ Filter for rankings based on external buff usage. **Values:** -- `include` - Include rankings with external buffs -- `exclude` - Exclude rankings with external buffs -- `require` - Only show rankings with external buffs +- `Any` - Any external buff usage +- `Exclude` - Exclude rankings with external buffs +- `Require` - Only show rankings with external buffs **Used by:** @@ -127,14 +154,11 @@ Metrics available for fight-specific rankings. **Values:** - `default` - Default ranking metric -- `bossdps` - Boss damage per second -- `bossrdps` - Boss rDPS (raid-contributing DPS) - `execution` - Execution score - `feats` - Feats of strength -- `krsi` - Kill speed index -- `playerscore` - Player performance score -- `playerspeed` - Clear time performance +- `score` - Overall score - `speed` - Clear speed +- `progress` - Progress metric **Used by:** @@ -148,6 +172,7 @@ Types of data available for time-series graphs. **Values:** +- `Summary` - Summary data - `Buffs` - Buff uptime over time - `Casts` - Cast timeline - `DamageDone` - Damage output graph @@ -159,6 +184,7 @@ Types of data available for time-series graphs. - `Interrupts` - Interrupt timeline - `Resources` - Resource levels over time - `Summons` - Summon timeline +- `Survivability` - Survivability metrics - `Threat` - Threat over time **Used by:** @@ -173,6 +199,8 @@ Guild member rank levels. **Values:** +- `NonMember` - Not a guild member +- `Applicant` - Guild applicant - `Recruit` - New guild member - `Member` - Standard member - `Officer` - Guild officer @@ -190,9 +218,14 @@ Filter for rankings based on hard mode completion. **Values:** -- `any` - Any difficulty level -- `highest` - Highest difficulty only -- `lowest` - Base difficulty only +- `Any` - Any difficulty level +- `Highest` - Highest difficulty only +- `NormalMode` - Normal mode (base difficulty) +- `Level0` - Hard mode level 0 +- `Level1` - Hard mode level 1 +- `Level2` - Hard mode level 2 +- `Level3` - Hard mode level 3 +- `Level4` - Hard mode level 4 **Used by:** @@ -223,8 +256,10 @@ Filter data by encounter outcome. **Values:** +- `All` - All fight data - `Encounters` - All encounter attempts - `Kills` - Successful kills only +- `Trash` - Trash fights only - `Wipes` - Failed attempts only **Used by:** @@ -242,11 +277,7 @@ Leaderboard ranking tiers. **Values:** - `Any` - Any rank -- `Blue` - Blue rank (uncommon) -- `Gold` - Gold rank (legendary) -- `Green` - Green rank (common) -- `Orange` - Orange rank (rare) -- `Purple` - Purple rank (epic) +- `LogsOnly` - Logs only (no rank restrictions) **Used by:** @@ -277,7 +308,7 @@ Time period for rankings. **Values:** -- `Current` - Current patch/season +- `Today` - Today's rankings - `Historical` - All-time historical **Used by:** @@ -317,9 +348,10 @@ Character role classifications. **Values:** -- `Tank` - Tank role -- `Healer` - Healer role +- `Any` - Any role - `DPS` - Damage dealer role +- `Healer` - Healer role +- `Tank` - Tank role **Used by:** @@ -334,12 +366,13 @@ User subscription status levels. **Values:** -- `Free` - Free tier user - `Silver` - Silver subscription - `Gold` - Gold subscription - `Platinum` - Platinum subscription +- `LegacySilver` - Legacy silver status - `LegacyGold` - Legacy gold status - `LegacyPlatinum` - Legacy platinum status +- `AlchemicalSociety` - Alchemical Society membership **Used by:** @@ -353,6 +386,7 @@ Types of data available in tabular format. **Values:** +- `Summary` - Summary statistics - `Buffs` - Buff uptime statistics - `Casts` - Cast counts and timings - `DamageDone` - Damage dealt breakdown @@ -364,6 +398,7 @@ Types of data available in tabular format. - `Interrupts` - Interrupt counts - `Resources` - Resource generation/usage - `Summons` - Summon statistics +- `Survivability` - Survivability statistics - `Threat` - Threat generation **Used by:** From dedc58288fe3308cb8db4fbd19c357b813189ce0 Mon Sep 17 00:00:00 2001 From: knowlen Date: Sun, 3 Aug 2025 23:39:30 -0700 Subject: [PATCH 11/16] Clarify why some enums aren't exposed - they're used by Zone.characterRankings which isn't a top-level API method --- docs/api-reference/enums.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api-reference/enums.md b/docs/api-reference/enums.md index 9834aac..24b8c21 100644 --- a/docs/api-reference/enums.md +++ b/docs/api-reference/enums.md @@ -143,7 +143,7 @@ Filter for rankings based on external buff usage. **Used by:** -*Note: This enum is defined in the GraphQL schema but is not currently exposed through any API methods in the Python client.* +*Note: This enum is defined in the GraphQL schema for use with `Zone.characterRankings`, which is not exposed as a top-level query method in the ESO Logs API. The Python client provides 100% coverage of available API methods.* --- @@ -229,7 +229,7 @@ Filter for rankings based on hard mode completion. **Used by:** -*Note: This enum is defined in the GraphQL schema but is not currently exposed through any API methods in the Python client.* +*Note: This enum is defined in the GraphQL schema for use with `Zone.characterRankings`, which is not exposed as a top-level query method in the ESO Logs API. The Python client provides 100% coverage of available API methods.* --- @@ -281,7 +281,7 @@ Leaderboard ranking tiers. **Used by:** -*Note: This enum is defined in the GraphQL schema but is not currently exposed through any API methods in the Python client.* +*Note: This enum is defined in the GraphQL schema for use with `Zone.characterRankings`, which is not exposed as a top-level query method in the ESO Logs API. The Python client provides 100% coverage of available API methods.* --- @@ -376,7 +376,7 @@ User subscription status levels. **Used by:** -*Note: This enum is defined in the GraphQL schema but is not currently exposed through any API methods in the Python client.* +*Note: This enum is defined in the GraphQL schema but is not used by any fields accessible through the ESO Logs API. The Python client provides 100% coverage of available API methods.* --- From 9244789e6a97cd80633ce9d9d56ae62e6c367b0f Mon Sep 17 00:00:00 2001 From: knowlen Date: Sun, 3 Aug 2025 23:47:15 -0700 Subject: [PATCH 12/16] Add documentation for direct GraphQL access and explain nested field limitations --- NESTED_FIELDS_ANALYSIS.md | 73 +++++++++++++ docs/advanced/graphql-escape-hatch.md | 142 ++++++++++++++++++++++++++ docs/api-reference/enums.md | 3 + mkdocs.yml | 2 + 4 files changed, 220 insertions(+) create mode 100644 NESTED_FIELDS_ANALYSIS.md create mode 100644 docs/advanced/graphql-escape-hatch.md diff --git a/NESTED_FIELDS_ANALYSIS.md b/NESTED_FIELDS_ANALYSIS.md new file mode 100644 index 0000000..79ee4c2 --- /dev/null +++ b/NESTED_FIELDS_ANALYSIS.md @@ -0,0 +1,73 @@ +# Analysis: Nested GraphQL Fields Not Exposed in Python Client + +## Overview + +The ESO Logs Python client provides 100% coverage of the top-level API methods (42/42), but some GraphQL schema fields are only accessible through nested queries that our current implementation doesn't expose. + +## Missing Nested Fields + +### 1. Encounter.characterRankings +- **Location**: `worldData.zones[].encounters[].characterRankings` +- **Purpose**: Get character rankings for a specific encounter +- **Additional Parameters**: + - `externalBuffs: ExternalBuffRankFilter` - Filter by external buff usage + - `hardModeLevel: HardModeLevelRankFilter` - Filter by hard mode level + - `leaderboard: LeaderboardRank` - Include/exclude ranks without logs +- **Use Case**: More granular ranking queries with additional filtering options + +### 2. Encounter.fightRankings +- **Location**: `worldData.zones[].encounters[].fightRankings` +- **Purpose**: Get fight-specific rankings +- **Additional Parameters**: + - `hardModeLevel: HardModeLevelRankFilter` + - `leaderboard: LeaderboardRank` + +## Current Limitations + +1. **No External Buff Filtering**: Users cannot filter rankings based on external buff usage +2. **No Hard Mode Level Filtering**: Cannot filter by specific hard mode levels (0-4) +3. **No Leaderboard Filtering**: Cannot exclude ranks without backing logs + +## Potential Solutions + +### Option 1: Add Nested Query Support +```python +# Example API addition +async def get_encounter_character_rankings( + encounter_id: int, + zone_id: int, + external_buffs: Optional[ExternalBuffRankFilter] = None, + hard_mode_level: Optional[HardModeLevelRankFilter] = None, + leaderboard: Optional[LeaderboardRank] = None, + # ... other existing parameters +): + """Get character rankings for a specific encounter with additional filters.""" + pass +``` + +### Option 2: Provide GraphQL Escape Hatch +```python +# Allow users to execute custom GraphQL queries +async def execute_graphql( + query: str, + variables: Optional[Dict[str, Any]] = None +) -> Dict[str, Any]: + """Execute a custom GraphQL query for advanced use cases.""" + pass +``` + +### Option 3: Document the Limitation +- Add a section to docs explaining what's not available +- Show users how to use the underlying GraphQL client directly for advanced queries + +## Recommendation + +1. **Short term**: Document the limitation and show how to access the GraphQL client directly +2. **Medium term**: Add the most requested nested queries (if users ask for them) +3. **Long term**: Consider a GraphQL escape hatch for power users + +## Impact Assessment + +- Most users likely don't need these advanced filtering options +- The missing fields are primarily for competitive/ranking analysis +- Current implementation covers all basic use cases diff --git a/docs/advanced/graphql-escape-hatch.md b/docs/advanced/graphql-escape-hatch.md new file mode 100644 index 0000000..31e4b16 --- /dev/null +++ b/docs/advanced/graphql-escape-hatch.md @@ -0,0 +1,142 @@ +# Using the GraphQL Client Directly + +While the ESO Logs Python client provides 100% coverage of the available top-level API methods, some advanced features are only accessible through nested GraphQL queries. This guide shows you how to access the underlying GraphQL client for these edge cases. + +## What's Not Exposed? + +The following GraphQL schema features aren't available through the Python client's convenience methods: + +### 1. Encounter-specific Character Rankings +The `Encounter.characterRankings` field provides additional filtering options not available in the standard character ranking methods: + +- `externalBuffs` - Filter rankings by external buff usage +- `hardModeLevel` - Filter by specific hard mode levels (0-4) +- `leaderboard` - Include/exclude ranks without backing logs + +### 2. Why These Aren't Exposed + +These fields require nested queries through `worldData.zones.encounters`, making them more complex to use and less commonly needed. The ESO Logs API design focuses on top-level query methods for the most common use cases. + +## Accessing the GraphQL Client + +The ESO Logs Python client is built on `gql`, so you can access the underlying GraphQL client for custom queries: + +```python +from gql import gql +from esologs import Client +from esologs.auth import get_access_token + +async def custom_encounter_rankings(): + token = get_access_token() + + # Use the standard client + async with Client( + url="https://www.esologs.com/api/v2/client", + headers={"Authorization": f"Bearer {token}"} + ) as client: + + # Access the underlying GraphQL transport + # Note: This is the internal implementation detail + query = gql(""" + query getEncounterRankings($zoneId: Int!, $encounterId: Int!) { + worldData { + zone(id: $zoneId) { + encounters { + id + name + characterRankings( + externalBuffs: Exclude + hardModeLevel: Highest + leaderboard: LogsOnly + ) + } + } + } + } + """) + + # Execute using the client's session + result = await client.session.execute( + query, + variable_values={ + "zoneId": 38, # Dreadsail Reef + "encounterId": 88 + } + ) + + return result +``` + +## Alternative: Direct GraphQL Client + +For complete control, you can create your own GraphQL client: + +```python +from gql import Client as GQLClient, gql +from gql.transport.aiohttp import AIOHTTPTransport +from esologs.auth import get_access_token + +async def direct_graphql_query(): + token = get_access_token() + + # Create your own transport + transport = AIOHTTPTransport( + url="https://www.esologs.com/api/v2/client", + headers={"Authorization": f"Bearer {token}"} + ) + + # Create a GraphQL client + async with GQLClient( + transport=transport, + fetch_schema_from_transport=True + ) as session: + + # Your custom query + query = gql(""" + query { + worldData { + zones { + id + name + encounters { + id + name + characterRankings( + metric: dps + difficulty: 122 + externalBuffs: Exclude + ) + } + } + } + } + """) + + result = await session.execute(query) + return result +``` + +## When to Use Direct GraphQL + +Consider using direct GraphQL queries when you need: + +1. **Advanced Ranking Filters**: External buffs, hard mode levels, leaderboard filtering +2. **Nested Data Fetching**: Complex queries that fetch related data in one request +3. **Schema Exploration**: Discovering what other fields might be available +4. **Custom Field Selection**: Optimizing requests by selecting only needed fields + +## Best Practices + +1. **Use the Python Client First**: It handles authentication, rate limiting, and provides type safety +2. **Fall Back to GraphQL When Needed**: Only use direct queries for features not exposed +3. **Share Your Use Cases**: If you frequently need direct GraphQL access, let us know - we may add it to the client +4. **Cache Results**: These complex queries can be expensive - cache when appropriate + +## Future Enhancements + +We're considering adding: +- An `execute_graphql()` method for custom queries +- Support for the most requested nested fields +- Query builder helpers for common patterns + +If you have specific needs, please [open an issue](https://github.com/knowlen/esologs-python/issues) describing your use case! diff --git a/docs/api-reference/enums.md b/docs/api-reference/enums.md index 24b8c21..a1e1cff 100644 --- a/docs/api-reference/enums.md +++ b/docs/api-reference/enums.md @@ -4,6 +4,9 @@ Enums (Enumerations) in ESO Logs Python provide type-safe constants for various API parameters. They represent fixed sets of allowed values defined by the ESO Logs GraphQL schema, ensuring that your code uses valid options and catches errors at development time rather than runtime. +!!! note "Advanced GraphQL Features" + Some enums in the GraphQL schema are only used by nested fields that aren't exposed through the Python client's convenience methods. See [Direct GraphQL Access](../../advanced/graphql-escape-hatch/) for how to use these advanced features. + ### Why Use Enums? 1. **Type Safety**: Your IDE and type checker will catch invalid values before you run your code diff --git a/mkdocs.yml b/mkdocs.yml index 3bf13d2..9d04132 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -69,6 +69,8 @@ nav: - Quickstart: getting-started/quickstart.md - Advanced Usage: getting-started/advanced-usage.md - Troubleshooting: getting-started/troubleshooting.md + - Advanced Topics: + - Direct GraphQL Access: advanced/graphql-escape-hatch.md - API Reference: - Game Data: api-reference/game-data.md - Character Data: api-reference/character-data.md From 24c9609475c95d2012d2d465719257dea2fa2c70 Mon Sep 17 00:00:00 2001 From: knowlen Date: Mon, 4 Aug 2025 00:02:40 -0700 Subject: [PATCH 13/16] =?UTF-8?q?Fix=20incorrect=20GraphQL=20field=20refer?= =?UTF-8?q?ence:=20Zone.characterRankings=20=E2=86=92=20Encounter.characte?= =?UTF-8?q?rRankings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api-reference/enums.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api-reference/enums.md b/docs/api-reference/enums.md index a1e1cff..ebe790f 100644 --- a/docs/api-reference/enums.md +++ b/docs/api-reference/enums.md @@ -146,7 +146,7 @@ Filter for rankings based on external buff usage. **Used by:** -*Note: This enum is defined in the GraphQL schema for use with `Zone.characterRankings`, which is not exposed as a top-level query method in the ESO Logs API. The Python client provides 100% coverage of available API methods.* +*Note: This enum is defined in the GraphQL schema for use with `Encounter.characterRankings`, which is not exposed as a top-level query method in the ESO Logs API. The Python client provides 100% coverage of available API methods.* --- @@ -232,7 +232,7 @@ Filter for rankings based on hard mode completion. **Used by:** -*Note: This enum is defined in the GraphQL schema for use with `Zone.characterRankings`, which is not exposed as a top-level query method in the ESO Logs API. The Python client provides 100% coverage of available API methods.* +*Note: This enum is defined in the GraphQL schema for use with `Encounter.characterRankings`, which is not exposed as a top-level query method in the ESO Logs API. The Python client provides 100% coverage of available API methods.* --- @@ -284,7 +284,7 @@ Leaderboard ranking tiers. **Used by:** -*Note: This enum is defined in the GraphQL schema for use with `Zone.characterRankings`, which is not exposed as a top-level query method in the ESO Logs API. The Python client provides 100% coverage of available API methods.* +*Note: This enum is defined in the GraphQL schema for use with `Encounter.characterRankings`, which is not exposed as a top-level query method in the ESO Logs API. The Python client provides 100% coverage of available API methods.* --- From f6b65986d80505c448f7453f2e3da7ba6fe594f1 Mon Sep 17 00:00:00 2001 From: knowlen Date: Mon, 4 Aug 2025 00:09:13 -0700 Subject: [PATCH 14/16] remove this --- NESTED_FIELDS_ANALYSIS.md | 73 --------------------------------------- 1 file changed, 73 deletions(-) delete mode 100644 NESTED_FIELDS_ANALYSIS.md diff --git a/NESTED_FIELDS_ANALYSIS.md b/NESTED_FIELDS_ANALYSIS.md deleted file mode 100644 index 79ee4c2..0000000 --- a/NESTED_FIELDS_ANALYSIS.md +++ /dev/null @@ -1,73 +0,0 @@ -# Analysis: Nested GraphQL Fields Not Exposed in Python Client - -## Overview - -The ESO Logs Python client provides 100% coverage of the top-level API methods (42/42), but some GraphQL schema fields are only accessible through nested queries that our current implementation doesn't expose. - -## Missing Nested Fields - -### 1. Encounter.characterRankings -- **Location**: `worldData.zones[].encounters[].characterRankings` -- **Purpose**: Get character rankings for a specific encounter -- **Additional Parameters**: - - `externalBuffs: ExternalBuffRankFilter` - Filter by external buff usage - - `hardModeLevel: HardModeLevelRankFilter` - Filter by hard mode level - - `leaderboard: LeaderboardRank` - Include/exclude ranks without logs -- **Use Case**: More granular ranking queries with additional filtering options - -### 2. Encounter.fightRankings -- **Location**: `worldData.zones[].encounters[].fightRankings` -- **Purpose**: Get fight-specific rankings -- **Additional Parameters**: - - `hardModeLevel: HardModeLevelRankFilter` - - `leaderboard: LeaderboardRank` - -## Current Limitations - -1. **No External Buff Filtering**: Users cannot filter rankings based on external buff usage -2. **No Hard Mode Level Filtering**: Cannot filter by specific hard mode levels (0-4) -3. **No Leaderboard Filtering**: Cannot exclude ranks without backing logs - -## Potential Solutions - -### Option 1: Add Nested Query Support -```python -# Example API addition -async def get_encounter_character_rankings( - encounter_id: int, - zone_id: int, - external_buffs: Optional[ExternalBuffRankFilter] = None, - hard_mode_level: Optional[HardModeLevelRankFilter] = None, - leaderboard: Optional[LeaderboardRank] = None, - # ... other existing parameters -): - """Get character rankings for a specific encounter with additional filters.""" - pass -``` - -### Option 2: Provide GraphQL Escape Hatch -```python -# Allow users to execute custom GraphQL queries -async def execute_graphql( - query: str, - variables: Optional[Dict[str, Any]] = None -) -> Dict[str, Any]: - """Execute a custom GraphQL query for advanced use cases.""" - pass -``` - -### Option 3: Document the Limitation -- Add a section to docs explaining what's not available -- Show users how to use the underlying GraphQL client directly for advanced queries - -## Recommendation - -1. **Short term**: Document the limitation and show how to access the GraphQL client directly -2. **Medium term**: Add the most requested nested queries (if users ask for them) -3. **Long term**: Consider a GraphQL escape hatch for power users - -## Impact Assessment - -- Most users likely don't need these advanced filtering options -- The missing fields are primarily for competitive/ranking analysis -- Current implementation covers all basic use cases From b0634b1e332cc706833dd622021f3900e53a99b6 Mon Sep 17 00:00:00 2001 From: knowlen Date: Mon, 4 Aug 2025 00:13:19 -0700 Subject: [PATCH 15/16] Move Direct GraphQL Access to Getting Started section and remove Advanced Topics --- docs/api-reference/enums.md | 2 +- docs/{advanced => getting-started}/graphql-escape-hatch.md | 0 mkdocs.yml | 3 +-- 3 files changed, 2 insertions(+), 3 deletions(-) rename docs/{advanced => getting-started}/graphql-escape-hatch.md (100%) diff --git a/docs/api-reference/enums.md b/docs/api-reference/enums.md index ebe790f..2fd04ff 100644 --- a/docs/api-reference/enums.md +++ b/docs/api-reference/enums.md @@ -5,7 +5,7 @@ Enums (Enumerations) in ESO Logs Python provide type-safe constants for various API parameters. They represent fixed sets of allowed values defined by the ESO Logs GraphQL schema, ensuring that your code uses valid options and catches errors at development time rather than runtime. !!! note "Advanced GraphQL Features" - Some enums in the GraphQL schema are only used by nested fields that aren't exposed through the Python client's convenience methods. See [Direct GraphQL Access](../../advanced/graphql-escape-hatch/) for how to use these advanced features. + Some enums in the GraphQL schema are only used by nested fields that aren't exposed through the Python client's convenience methods. See [Direct GraphQL Access](../../getting-started/graphql-escape-hatch/) for how to use these advanced features. ### Why Use Enums? diff --git a/docs/advanced/graphql-escape-hatch.md b/docs/getting-started/graphql-escape-hatch.md similarity index 100% rename from docs/advanced/graphql-escape-hatch.md rename to docs/getting-started/graphql-escape-hatch.md diff --git a/mkdocs.yml b/mkdocs.yml index 9d04132..59ff97d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -68,9 +68,8 @@ nav: - Authentication: getting-started/authentication.md - Quickstart: getting-started/quickstart.md - Advanced Usage: getting-started/advanced-usage.md + - Direct GraphQL Access: getting-started/graphql-escape-hatch.md - Troubleshooting: getting-started/troubleshooting.md - - Advanced Topics: - - Direct GraphQL Access: advanced/graphql-escape-hatch.md - API Reference: - Game Data: api-reference/game-data.md - Character Data: api-reference/character-data.md From 88c7b5df989ebe0e84c3227315abff48707579f7 Mon Sep 17 00:00:00 2001 From: knowlen Date: Mon, 4 Aug 2025 00:17:52 -0700 Subject: [PATCH 16/16] simplify --- docs/index.md | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/docs/index.md b/docs/index.md index bd5ab96..864d66f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -69,28 +69,6 @@ asyncio.run(main()) ``` -## Status -
-
-

Current Version

-

v0.2.0b1
- 100% API Coverage!

-

All 42 ESO Logs API methods implemented with comprehensive testing and documentation.

-
-
- -
-
-

Completed Features

-
    -
  • User Accounts OAuth2 authentication & user data access
  • -
  • Progress Race World/realm first tracking
  • -
  • Full API Coverage All 42 methods implemented
  • -
-
-
- - ## Architecture ```mermaid graph TB