Skip to content

Conversation

@TeKrop
Copy link
Owner

@TeKrop TeKrop commented Dec 16, 2025

Following the removal of Battle Tag from Blizzard players search URL, the following change was made : added support for Blizzard ID as possible input for player_id on player profile endpoints

Summary by Sourcery

Update player search and profile parsing to work without BattleTag-based search and support Blizzard ID fallbacks for player identification.

New Features:

  • Allow player identifiers to be either BattleTag or Blizzard hexadecimal ID in API responses and documentation.
  • Support using plain usernames as player_id inputs when they uniquely identify a player on Blizzard.

Bug Fixes:

  • Adjust player search and profile parsers to match players by exact username instead of BattleTag where Blizzard no longer exposes BattleTag in search results.
  • Fallback to parsing player profile pages directly when search-based summary data is unavailable, avoiding incorrect 404 errors.

Enhancements:

  • Include public-only filtering and improved logging when multiple or no players are found for a given username.
  • Relax assumptions about summary fields in career parsing by handling missing avatar, title, and lastUpdated values more defensively.

Tests:

  • Update parser and router tests to cover new search behavior, Blizzard ID handling, and the direct-profile fallback path when search results are empty.

@TeKrop TeKrop self-assigned this Dec 16, 2025
@TeKrop TeKrop added the bug Something isn't working label Dec 16, 2025
@TeKrop TeKrop linked an issue Dec 16, 2025 that may be closed by this pull request
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Dec 16, 2025

Reviewer's Guide

Refactors player search and profile parsing to work without BattleTag in Blizzard’s search API by matching on exact-case player names, supporting Blizzard hex IDs as player identifiers, and falling back to direct profile-page parsing when search results are ambiguous or empty, with tests updated accordingly.

Sequence diagram for updated player career retrieval with search fallback

sequenceDiagram
    actor User
    participant APIRouter as APIRouter
    participant BasePlayerParser as BasePlayerParser
    participant SearchDataParser as SearchDataParser
    participant BlizzardSearchAPI as BlizzardSearchAPI
    participant CacheManager as CacheManager
    participant PlayerCareerParser as PlayerCareerParser
    participant BlizzardProfilePage as BlizzardProfilePage

    User->>APIRouter: GET /players/{player_id}/career
    APIRouter->>BasePlayerParser: parse(player_id)

    activate BasePlayerParser
    BasePlayerParser->>SearchDataParser: parse_data()
    activate SearchDataParser
    SearchDataParser->>BlizzardSearchAPI: GET /players/{player_name}/search
    BlizzardSearchAPI-->>SearchDataParser: JSON search results

    SearchDataParser->>SearchDataParser: extract player_name from player_id
    SearchDataParser->>SearchDataParser: filter players by name and isPublic
    SearchDataParser-->>BasePlayerParser: summary data or empty
    deactivate SearchDataParser

    alt summary data found
        BasePlayerParser->>CacheManager: get_player_cache(player_id)
        CacheManager-->>BasePlayerParser: cached data or None
        alt cache hit and up_to_date
            BasePlayerParser->>PlayerCareerParser: use cached data
            PlayerCareerParser-->>BasePlayerParser: parsed career data
        else cache miss or stale
            BasePlayerParser->>BlizzardProfilePage: GET /players/{resolved_player_id}/profile
            BlizzardProfilePage-->>BasePlayerParser: HTML profile page
            BasePlayerParser->>PlayerCareerParser: parse(html, summary)
            PlayerCareerParser-->>BasePlayerParser: parsed career data
            BasePlayerParser->>CacheManager: update cache
        end
        BasePlayerParser-->>APIRouter: career response
        APIRouter-->>User: 200 OK with career data
    else no unique summary (zero or multiple matches)
        BasePlayerParser->>BasePlayerParser: summary is empty
        BasePlayerParser->>BlizzardProfilePage: GET /players/{input_player_id}/profile
        BlizzardProfilePage-->>BasePlayerParser: HTML profile page
        BasePlayerParser->>PlayerCareerParser: parse(html, no summary cache)
        PlayerCareerParser-->>BasePlayerParser: parsed career data
        BasePlayerParser-->>APIRouter: career response (no search cache)
        APIRouter-->>User: 200 OK with career data
    end

    deactivate BasePlayerParser
Loading

Class diagram for updated player parsers and PlayerShort model

classDiagram
    class JSONParser {
        +json_data dict
        +get_blizzard_url(kwargs) str
        +parse() None
    }

    class SearchDataParser {
        +player_id str
        +player_name str
        +parse_data() dict
        +get_blizzard_url(kwargs) str
    }

    class PlayerSearchParser {
        +search_nickname str
        +search_name str
        +order_by str
        +offset int
        +limit int
        +get_blizzard_url(kwargs) str
        +parse_data() dict
        +filter_players() list~dict~
        +apply_transformations(players Iterable~dict~) list~dict~
    }

    class BasePlayerParser {
        +player_id str
        +player_data dict
        +cache_manager CacheManager
        +parse() None
    }

    class PlayerCareerParser {
        +player_data dict
        +parse(html str, summary dict) None
        -__get_summary() dict
    }

    class PlayerShort {
        +player_id str
        +name str
        +avatar HttpUrl
        +namecard HttpUrl
        +title str
        +career_url HttpUrl
        +blizzard_id str
    }

    class CacheManager {
        +get_player_cache(player_id str) dict
        +update_player_cache(player_id str, data dict) None
    }

    JSONParser <|-- SearchDataParser
    JSONParser <|-- PlayerSearchParser
    BasePlayerParser o--> SearchDataParser
    BasePlayerParser o--> PlayerCareerParser
    BasePlayerParser o--> CacheManager
    PlayerSearchParser --> PlayerShort
Loading

Flow diagram for updated player search filtering and player_id selection

flowchart TD
    A[Receive search request with name or BattleTag] --> B[Set search_nickname from input]
    B --> C[Derive search_name by splitting on first dash]
    C --> D[Call Blizzard search endpoint with search_name]
    D --> E[Receive JSON list of players]
    E --> F[Filter players where name equals search_name and isPublic is True]
    F --> G{Any matching players?}

    G -->|No| H[Return empty players list]
    H --> I[Respond with empty results]

    G -->|Yes| J{Number of matching players}
    J -->|1| K[Single matching player]
    K --> L{Input contains dash?}
    L -->|Yes - looks like BattleTag| M[Set player_id to original search_nickname]
    L -->|No - plain username| N[Set player_id to player.url from result]

    J -->|More than 1| O[Multiple matching players]
    O --> P[For each player set player_id to player.url]

    M --> Q[Build PlayerShort objects]
    N --> Q
    P --> Q

    Q --> R[Set name from player.name]
    R --> S[Set avatar, namecard, title, blizzard_id, career_url]
    S --> T[Return list of PlayerShort results]
Loading

File-Level Changes

Change Details Files
Adapt player search and profile resolution logic to use exact-case name matching instead of BattleTag and handle ambiguous or missing search results.
  • Extract player_name from player_id by splitting on the first dash and use it for search matching.
  • Replace BattleTag-based search with filtering by exact-name and public visibility, returning empty data when zero or multiple matches are found.
  • Update search URL construction to use the player name segment instead of an encoded BattleTag.
  • In player search parser, derive search_name and filter results by exact-case name and public visibility only.
app/players/parsers/search_data_parser.py
app/players/parsers/player_search_parser.py
Support Blizzard hexadecimal IDs as valid player identifiers and adjust returned player metadata accordingly.
  • Update PlayerShort model field descriptions and examples to describe player_id as either BattleTag or Blizzard hex ID, and adjust name field examples to be plain usernames.
  • Extend career URL examples to include URLs built from Blizzard hex IDs.
  • Change player search transformation to derive player_id from the original search nickname when a single BattleTag-like input is used, otherwise use the player profile URL; always set the returned name from the player name field instead of BattleTag.
app/players/models.py
app/players/parsers/player_search_parser.py
Change player parsing flow to gracefully handle cases where the player is not found in the search endpoint by falling back to direct profile-page parsing.
  • In the base player parser, remove the hard 404 when the summary from search is empty and instead call the parent parse() to fetch and parse the profile page using the raw player_id.
  • Clarify comments around cache usage and the fallback behavior when search results are missing or ambiguous.
app/players/parsers/base_player_parser.py
Relax assumptions about summary search data presence in the career parser and rely on optional fields from the summary when available.
  • Use .get() when accessing avatar, title, and lastUpdated from the summary data to avoid key errors when search results are missing or incomplete.
  • Ensure title resolution falls back to extracting it from the profile HTML when not present in the summary.
app/players/parsers/player_career_parser.py
Update API documentation for player routes to reflect name-based lookup behavior and the possibility of using plain usernames when unique.
  • Adjust query parameter documentation to mention that player_id can be a BattleTag or a username that is unique on Blizzard, and add corresponding examples.
  • Simplify search endpoint description by removing the requirement to try BattleTag if name search fails and rely on the updated name-based search behavior.
app/players/router.py
Align tests with the new search behavior and fallback parsing, including sequential HTTP calls to search and profile pages for unknown players.
  • Change unknown-player tests for career, stats summary, and career stats parsers to parameterize player_html_data and mock AsyncClient.get with side_effect to simulate both the search call and subsequent profile page fetch.
  • Update the player career route test to mock both an empty search result and a follow-up profile page response, still expecting a 404 for unknown players.
  • Introduce or update fixtures for Blizzard search JSON results to match the new name-based search behavior.
tests/players/parsers/test_player_career_parser.py
tests/players/parsers/test_player_stats_summary_parser.py
tests/players/parsers/test_player_career_stats_parser.py
tests/players/test_player_career_route.py
tests/fixtures/json/search_players_blizzard_result.json

Assessment against linked issues

Issue Objective Addressed Explanation
#337 Update player search and player profile parsing logic to handle Blizzard's removal of the 'battleTag' field from the search endpoint so that player pages work again without raising KeyError('battleTag').

Possibly linked issues

  • #(not specified): PR removes battleTag dependency in search/profile parsers, fixing KeyError and restoring player pages after Blizzard’s change.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • In SearchDataParser and PlayerSearchParser you both derive player_name/search_name by splitting on '-'; consider extracting this into a small shared helper to keep the parsing logic consistent and easier to update if Blizzard changes formats again.
  • In SearchDataParser.parse_data, the warning message "not found in search results" is logged even when multiple players match; you might want to clarify the wording (e.g., "ambiguous search, N matching players") so logs distinguish between 0 and >1 results.
  • In PlayerSearchParser.apply_transformations, player_id falls back to player["url"] when there are multiple matches; it may be worth normalizing or documenting the expected format of this URL-derived ID to ensure it remains stable and compatible with the rest of the API.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `SearchDataParser` and `PlayerSearchParser` you both derive `player_name`/`search_name` by splitting on `'-'`; consider extracting this into a small shared helper to keep the parsing logic consistent and easier to update if Blizzard changes formats again.
- In `SearchDataParser.parse_data`, the warning message "not found in search results" is logged even when multiple players match; you might want to clarify the wording (e.g., "ambiguous search, N matching players") so logs distinguish between 0 and >1 results.
- In `PlayerSearchParser.apply_transformations`, `player_id` falls back to `player["url"]` when there are multiple matches; it may be worth normalizing or documenting the expected format of this URL-derived ID to ensure it remains stable and compatible with the rest of the API.

## Individual Comments

### Comment 1
<location> `tests/players/parsers/test_player_career_stats_parser.py:51` </location>
<code_context>
     indirect=True,
 )
 @pytest.mark.asyncio
 async def test_unknown_player_parser_blizzard_error(
     player_career_stats_parser: PlayerCareerStatsParser,
+    player_html_data: str,
</code_context>

<issue_to_address>
**suggestion (testing):** Conflicting mock configuration (both side_effect and return_value) makes the HTTP mock harder to reason about.

`httpx.AsyncClient.get` is patched with both `side_effect` and `return_value`, but `side_effect` takes precedence so `return_value` is ignored. Please drop `return_value` and use only `side_effect` (as in the other tests) so the call sequence is clear. Also confirm that the asserted exception type still aligns with the new fallback-to-HTML behavior used in the other parser tests.

Suggested implementation:

```python
@pytest.mark.parametrize(
    ("player_career_stats_parser", "player_html_data"),
    [(unknown_player_id, unknown_player_id)],
    indirect=True,
)
@pytest.mark.asyncio
async def test_unknown_player_parser_blizzard_error(
    player_career_stats_parser: PlayerCareerStatsParser,
    player_html_data: str,
    player_search_response_mock: Mock,
):
    with (
        pytest.raises(ParserBlizzardError),
        patch(
            "httpx.AsyncClient.get",
            side_effect=[
                # Players search call first
                player_search_response_mock,
                # Player profile page
            ],
        ) as mock_get,
    ):

```

1. If the second HTTP call in this test should now exercise the "fallback-to-HTML" behavior (as in other parser tests), ensure that the second element of the `side_effect` list is the appropriate mock/response (e.g. a mock Blizzard error response followed by an HTML response, or an exception) that leads to `ParserBlizzardError` being raised. Align it with whatever is used in the other tests that cover the new behavior.
2. Re-run the test suite and, if the failure mode has changed due to the new fallback behavior (e.g. a different custom exception is now raised for this scenario), update `pytest.raises(ParserBlizzardError)` to the correct exception type to keep expectations consistent with the parser’s actual behavior.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@sonarqubecloud
Copy link

@TeKrop TeKrop merged commit f5f02f4 into main Dec 16, 2025
5 checks passed
@TeKrop TeKrop deleted the bugfix/blizzard-search-battletag-removal branch December 16, 2025 00:50
TeKrop added a commit that referenced this pull request Dec 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Player pages not working after Blizzard update

2 participants