Skip to content

Conversation

@Gatsik
Copy link
Contributor

@Gatsik Gatsik commented Jan 2, 2026

Resolves #1149

Summary by CodeRabbit

  • Refactoring

    • Unified API response handling across the app for more consistent data flow.
  • Stability / Bug Fixes

    • Stronger runtime validation and typing to reduce data-related errors; safer resource/file handling.
  • User-facing behavior

    • Matchmaker queue labels reflect team size more clearly; featured-mod listings signal readiness; replay/access and connection flows more robust; rating history loads asynchronously for smoother UI.

✏️ Tip: You can customize this high-level summary in your review settings.

Gatsik added 2 commits January 2, 2026 04:09
* unify `get_by_query` and `get_by_endpoint` methods into single `get`
* remove unreachable according to type checkers code -- let exceptions
  rise and be explicit
* improve types' usage correctness
@coderabbitai
Copy link

coderabbitai bot commented Jan 2, 2026

📝 Walkthrough

Walkthrough

Refactors API parsing to DataApi TypedDicts, consolidates GET surface to unified get/get_parsed/get_converted, replaces many ParsedApiResponse/PreProcessedApiResponse usages with ParsedDataApiResponse and QueryOptions, and updates connectors, UI call sites, utils, models, and tests to the new signatures and flows.

Changes

Cohort / File(s) Summary
API Core
src/api/ApiBase.py, src/api/ApiAccessors.py
Add DataApiResourceObject, DataApiResponse, ParsedDataApiResponse; introduce QueryOptions = MutableMapping[...]; remove old Api TypedDicts; unify GET surface to get/get_parsed/get_converted; rename internal handlers (_handler_parsed, _handler_converted) and update decode/handler typings to dict[str, Any].
Connectors — parsing & conversion
src/api/... (e.g., src/api/coop_api.py, src/api/featured_mod_api.py, src/api/matchmaker_queue_api.py, src/api/player_api.py, src/api/sim_mod_updater.py, src/api/stats_api.py, src/api/vaults_api.py)
Replace ParsedApiResponse/PreProcessedApiResponse with ParsedDataApiResponse; assert parsed["data"] shapes; switch callers to get_parsed/get_converted/get; adopt QueryOptions for queries; adjust conversion logic to new TypedDict shapes and relationship resolution.
UI / Widgets / Consumers
src/replays/_replayswidget.py, src/playercard/ratingtabwidget.py, src/vaults/detailswidget.py, src/vaults/mapvault/mapdetails.py, src/vaults/reviewwidget.py, src/stats/leaderboard_widget.py, src/games/_gameswidget.py, src/games/automatchframe.py
Update handler signatures to ParsedDataApiResponse; replace get_by_query_*/get_by_endpoint_* with get/get_parsed/get_converted; add async parsing worker in rating tab; rename MatchmakerQueue usage to MatchmakerQueueFrame and adopt attribute-style access.
Client / Connection callers
src/chat/ircconnection.py, src/client/connection.py, src/connectivity/IceServersPoller.py, src/fa/replayserver.py
Replace get_by_endpoint(...) with unified get(...); broaden some handler annotations to dict[str, Any]; minor simplifications in response handling.
Models & minor refactors
src/model/player.py, src/model/game.py, src/model/qobjectmapping.py
Add single currentGame setter and remove duplicate; make average-rating None handling explicit; remove redundant # type: ignore[arg-type] comments (no behavior change).
Config & util changes
src/config/__init__.py, src/config/admin.py, src/util/__init__.py, src/util/gameurl.py, src/util/theme.py
Add typed settings cache; change admin check to sys.platform == "win32" and use f-strings; use scandir for log cleanup and remove helper; simplify _get_query_item to return str; extract module-level theme decorators and add themed keyword parameters.
OAuth / Logging
src/oauth/oauth_flow.py
Add _logger: ClassVar[Logger], explicit _expires_in typing, adjust defaults for client_id and scopes, and use logger in lifecycle callbacks.
Tests
tests/unit_tests/themes/test_themeset.py
Update dict literal styles and pass themed= as keyword; remove unused import.
Misc / Zig parser
src/replays/zigparser/parser.zig
Fix typographical alias and function return type name (ParseErorrParseError).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI
  participant Connector
  participant ApiAccessor
  participant Server

  UI->>Connector: requestData(query: QueryOptions)
  Connector->>ApiAccessor: get_parsed(query, response_handler)
  ApiAccessor->>Server: HTTP GET /... with query
  Server-->>ApiAccessor: 200 { data, included?, meta? } (DataApiResponse)
  ApiAccessor->>ApiAccessor: parse_message(DataApiResponse) → ParsedDataApiResponse
  ApiAccessor->>Connector: invoke response_handler(ParsedDataApiResponse)
  Connector->>UI: emit converted domain models
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Improve performance and more #1148 — Large, overlapping API accessor/parsing refactor touching ApiBase/ApiAccessors and many connector call sites.
  • Issue/PR #1149 — Type-hint consistency fixes for parsed-callback signatures that this change addresses.

Poem

🐰
I hopped through types and nibbled names,
Swapped callbacks, chased inconsistent claims,
get_parsed clears the forest wide,
Typed carrots lead the data tide —
I thump, I hop, the code compiles with pride.

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR introduces significant out-of-scope changes beyond fixing type hints for get_by_query_parsed callbacks, including new TypedDict data models (DataApiResourceObject, DataApiResponse, ParsedDataApiResponse), method renames (get_by_query_parsed → get_parsed), removal of old types, and widespread refactoring across multiple API files and downstream consumers. Clarify the scope: either update the PR title/description to reflect the full API refactoring, or split the work into separate focused PRs—one for callback type hints and another for the comprehensive API redesign.
Docstring Coverage ⚠️ Warning Docstring coverage is 1.96% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix most of type errors for src/api' accurately describes the main objective of this changeset, which focuses on addressing type inconsistencies in the API layer.
Linked Issues check ✅ Passed The PR substantially implements the requirements from issue #1149 by updating type hints for callback handlers, though it goes further by introducing a comprehensive API refactoring with new DataApi types and restructured method signatures.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81ea95a and 81a9ca0.

📒 Files selected for processing (1)
  • src/api/ApiAccessors.py
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/api/ApiAccessors.py:81-88
Timestamp: 2025-12-28T02:07:57.647Z
Learning: In src/api/ApiAccessors.py, DataApiAccessor.convert_parsed intentionally raises NotImplementedError as a template method pattern - subclasses using get_by_query_converted, get_by_endpoint_converted, or requestData must implement convert_parsed.
📚 Learning: 2026-01-02T02:53:05.593Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1150
File: src/api/ApiAccessors.py:32-42
Timestamp: 2026-01-02T02:53:05.593Z
Learning: In the FAForever client (Python 3.14+), forward references in TypedDict definitions (e.g., DataApiResourceObject referencing DataApiResponse defined later in src/api/ApiAccessors.py) are supported and should not be flagged as errors by static analysis tools. Treat forward references in TypedDicts as valid; ensure type checkers (e.g., mypy) are configured to allow PEP 695/Typing enhancements and that such patterns pass type checks without false positives.

Applied to files:

  • src/api/ApiAccessors.py
📚 Learning: 2025-12-28T02:07:52.863Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/api/ApiAccessors.py:81-88
Timestamp: 2025-12-28T02:07:52.863Z
Learning: In src/api/ApiAccessors.py, DataApiAccessor.convert_parsed should be a template method that raises NotImplementedError. Subclasses that use get_by_query_converted, get_by_endpoint_converted, or requestData must implement convert_parsed to provide concrete parsing logic. This enforces that derived accessors supply the needed parsing behavior and prevents accidental use of a base implementation.

Applied to files:

  • src/api/ApiAccessors.py
📚 Learning: 2026-01-02T02:50:32.067Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1150
File: src/fa/replayserver.py:80-80
Timestamp: 2026-01-02T02:50:32.067Z
Learning: In the FAForever client codebase, all API response handlers (callbacks passed to `get`, `post`, etc. in ApiBase) must accept `dict[str, Any]` as their parameter type to maintain signature compatibility. More specific types like `dict[Literal["key"], type]` or custom TypedDicts cannot be used directly in handler signatures due to contravariance constraints.

Applied to files:

  • src/api/ApiAccessors.py
📚 Learning: 2026-01-02T02:53:05.593Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1150
File: src/api/ApiAccessors.py:32-42
Timestamp: 2026-01-02T02:53:05.593Z
Learning: In FAForever/client review context, for Python files (e.g., src/api/ApiAccessors.py), do not mention Ruff-specific warnings or propose Ruff configuration changes. Use generic lint guidance or project-approved lints instead; avoid any Ruff-related recommendations in reviews.

Applied to files:

  • src/api/ApiAccessors.py
📚 Learning: 2026-01-02T02:40:22.985Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1150
File: src/util/__init__.py:0-0
Timestamp: 2026-01-02T02:40:22.985Z
Learning: In the FAForever/client repository (Python), os.path.getmtime() can accept DirEntry objects returned by os.scandir() directly. Do not extract the path attribute when using getmtime as a sorting key; pass the DirEntry (which is path-like) to getmtime and sort on that value. This applies to all Python files in the repo, not just a single file, and should be validated against the supported Python version in use.

Applied to files:

  • src/api/ApiAccessors.py
🧬 Code graph analysis (1)
src/api/ApiAccessors.py (6)
src/replays/_replayswidget.py (2)
  • data (367-370)
  • data (468-471)
src/api/ApiBase.py (6)
  • handle (44-45)
  • _do_nothing (29-30)
  • get (72-82)
  • get (140-153)
  • post (84-96)
  • post (155-171)
src/api/coop_api.py (2)
  • convert_parsed (15-20)
  • convert_parsed (65-72)
src/api/matchmaker_queue_api.py (1)
  • convert_parsed (14-19)
src/api/vaults_api.py (3)
  • convert_parsed (79-84)
  • convert_parsed (100-108)
  • convert_parsed (126-133)
src/api/stats_api.py (2)
  • convert_parsed (51-61)
  • convert_parsed (191-196)
🪛 Ruff (0.14.10)
src/api/ApiAccessors.py

36-36: Undefined name DataApiResponse

(F821)


170-170: Do not catch blind exception: Exception

(BLE001)


171-171: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🔇 Additional comments (7)
src/api/ApiAccessors.py (7)

5-7: LGTM!

The new typing imports (NotRequired, TypedDict, cast) are appropriate for the TypedDict definitions and type-safe casting introduced in this refactor.


32-47: Well-structured TypedDict definitions for JSON:API responses.

The TypedDicts accurately model the JSON:API response structure with appropriate use of NotRequired for optional fields. The forward reference to DataApiResponse on line 36 is valid in Python 3.14+. Based on learnings, this pattern is supported in this codebase.


53-71: LGTM!

The _handler_parsed wrapper correctly bridges the dict[str, Any] signature required by the base API with the typed ParsedDataApiResponse expected by callers. The use of cast is appropriate here since the runtime data conforms to DataApiResponse. Based on learnings, this pattern maintains signature compatibility while providing type safety.


73-85: LGTM!

The post_and_parse method correctly uses the _handler_parsed wrapper, maintaining consistency with get_parsed.


87-105: LGTM!

The _handler_converted and get_converted methods properly chain parsing and conversion, aligning with the template method pattern where subclasses implement convert_parsed.


107-134: LGTM!

The parse_message method correctly orchestrates the parsing flow. The parseIncluded method properly extracts and resolves relationships. The type annotations (DataApiResponse, DataApiResourceObject) accurately reflect the expected data structures.


179-191: LGTM!

The requestData method correctly uses get_converted for the conversion flow. The convert_parsed method appropriately raises NotImplementedError as a template method pattern, requiring subclasses to provide concrete implementations. Based on learnings, this is the intended design.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/vaults/detailswidget.py (1)

379-391: Missing type assertion for item_info.

ParsedDataApiResponse["data"] can be either dict[str, Any] or list[dict[str, Any]]. This code accesses item_info["versions"] assuming it's a dict, but there's no assertion to validate this. For consistency with on_review_submitted (line 165), add a type assertion.

🔎 Proposed fix
     def on_reviews_data(self, message: ParsedDataApiResponse) -> None:
         self._comments_initialized = True
         item_info = message["data"]
+        assert isinstance(item_info, dict)
 
         reviews: list[VersionReview] = []
         for version in item_info["versions"]:
src/api/player_api.py (1)

40-49: Potential KeyError if player has no clan membership.

Line 44 accesses player_dict["clanMembership"] without first checking if the key exists. If the player is not in a clan, this will raise a KeyError. Additionally, for consistency with other handlers in this PR (e.g., handle_player_ratings, handle_achievements), consider asserting message["data"] is a list before unpacking.

🔎 Proposed fix
     def handle_player(self, message: ParsedDataApiResponse) -> None:
+        assert isinstance(message["data"], list)
         player_dict, = message["data"]
-        assert isinstance(player_dict, dict)
         player = Player(**player_dict)
-        if "clan" in player_dict["clanMembership"]:
+        if "clanMembership" in player_dict and "clan" in player_dict["clanMembership"]:
             clan = Clan(**player_dict["clanMembership"]["clan"])
             clan_membership = ClanMembership(**player_dict["clanMembership"])
             clan_membership.custom_clan = clan
             player.custom_clan_membership = clan_membership
         self.player_ready.emit(player)
🧹 Nitpick comments (9)
src/config/admin.py (2)

16-16: Unify Windows detection across the file.

Line 16 now uses sys.platform == "win32", but line 33 still uses os.name != 'nt'. For consistency, apply the same platform check throughout the file.

🔎 Proposed fix for consistency
 def runAsAdmin(cmdLine=None, wait=True):

-    if os.name != 'nt':
+    if sys.platform != "win32":
         raise RuntimeError("This function is only implemented on Windows.")

Also applies to: 26-28, 33-34


14-28: Note: Changes appear out of scope for PR objectives.

This PR is titled "Fix most of type errors for src/api" and addresses issue #1149 about type hint consistency in API files. However, src/config/admin.py is not an API file, and these changes (platform detection and f-string conversion) don't address type errors.

While the changes themselves are reasonable refactoring improvements, consider whether they should be in a separate PR focused on general code modernization to keep PRs focused and easier to review.

src/model/game.py (1)

282-291: LGTM! Explicit None handling improves type safety.

The refactored implementation correctly handles the case where to_player returns None, making the None-filtering explicit and type-checker friendly. The logic is sound: gather potential players, filter out None values, and compute the average or return 0 for empty lists.

Optional: Combine steps for efficiency

If you'd like to reduce intermediate list creation, you could use a walrus operator to filter in a single comprehension:

-    players = [
-        self.to_player(name)
-        for team in self.playing_teams.values()
-        for name in team
-    ]
-    online_players = [p for p in players if p is not None]
+    online_players = [
+        player
+        for team in self.playing_teams.values()
+        for name in team
+        if (player := self.to_player(name)) is not None
+    ]

The current two-step approach is more readable, so this is purely optional.

src/fa/replayserver.py (1)

7-7: The Any import suggests overly broad typing.

This import is only necessary because line 80 uses dict[str, Any]. According to the PR objectives, strongly-typed schemas like DataApiResponse were introduced to improve type safety. Consider using those instead of Any.

src/replays/_replayswidget.py (1)

1197-1211: Consider adding type assertion for message["data"].

Since ParsedDataApiResponse["data"] can be either dict or list, but line 1202 iterates over replays assuming it's a list, consider adding an assertion similar to other handlers in this PR for consistency and fail-fast behavior.

Suggested defensive assertion
     def process_replays_data(self, message: ParsedDataApiResponse) -> None:
         self.stopSearchVault()
         self.clear_scoreboard()
         self.onlineReplays = {}
         replays = message["data"]
+        assert isinstance(replays, list)
         for replay_item in replays:
src/playercard/ratingtabwidget.py (1)

164-175: Consider using ParsedDataApiResponse for stronger typing.

The method accesses message["meta"] and message["data"]/message["included"] (via LineSeriesParser), which matches the ParsedDataApiResponse structure. Using dict[str, Any] works but loses the type guarantees that ParsedDataApiResponse provides. If the signal emits this type, consider updating for consistency with the rest of the PR.

src/api/ApiAccessors.py (1)

114-134: Side effect: message.pop('included') mutates the input.

Line 130 mutates the input message by removing the 'included' key. This could cause unexpected behavior if the caller retains a reference to the original response. Consider working with a copy or documenting this mutation clearly.

src/api/stats_api.py (2)

182-184: Generator emitted via signal can only be iterated once.

Line 184 emits a generator expression. If the signal receiver iterates over it more than once, the second iteration will yield no items. Consider converting to a list for safety.

🔎 Proposed fix
     def handle_achievements(self, message: ParsedDataApiResponse) -> None:
         assert isinstance(message["data"], list)
-        self.achievments_ready.emit(PlayerAchievement(**entry) for entry in message["data"])
+        self.achievments_ready.emit([PlayerAchievement(**entry) for entry in message["data"]])

191-196: Same generator pattern used in convert_parsed.

Similar to handle_achievements, this returns a generator in "values". If the consumer iterates twice, the second pass will be empty. This may be intentional if the consumer is known to iterate only once, but worth noting for consistency with other convert_parsed methods that return lists.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2c81427 and 9ecb6d9.

📒 Files selected for processing (30)
  • src/api/ApiAccessors.py
  • src/api/ApiBase.py
  • src/api/coop_api.py
  • src/api/featured_mod_api.py
  • src/api/matchmaker_queue_api.py
  • src/api/player_api.py
  • src/api/sim_mod_updater.py
  • src/api/stats_api.py
  • src/api/vaults_api.py
  • src/chat/ircconnection.py
  • src/client/connection.py
  • src/config/__init__.py
  • src/config/admin.py
  • src/connectivity/IceServersPoller.py
  • src/fa/replayserver.py
  • src/games/_gameswidget.py
  • src/games/automatchframe.py
  • src/model/game.py
  • src/model/player.py
  • src/model/qobjectmapping.py
  • src/oauth/oauth_flow.py
  • src/playercard/ratingtabwidget.py
  • src/replays/_replayswidget.py
  • src/stats/leaderboard_widget.py
  • src/util/__init__.py
  • src/util/gameurl.py
  • src/util/theme.py
  • src/vaults/detailswidget.py
  • src/vaults/mapvault/mapdetails.py
  • src/vaults/reviewwidget.py
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/api/ApiAccessors.py:81-88
Timestamp: 2025-12-28T02:07:57.647Z
Learning: In src/api/ApiAccessors.py, DataApiAccessor.convert_parsed intentionally raises NotImplementedError as a template method pattern - subclasses using get_by_query_converted, get_by_endpoint_converted, or requestData must implement convert_parsed.
📚 Learning: 2025-12-28T16:29:14.773Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/api/sim_mod_updater.py:21-27
Timestamp: 2025-12-28T16:29:14.773Z
Learning: In src/api/sim_mod_updater.py, the assertion `assert isinstance(message["data"], list)` in the `get_url_from_message` method is intentional. If the API returns a dict instead of a list, the application should crash to signal an API contract violation rather than handling it gracefully.

Applied to files:

  • src/api/sim_mod_updater.py
📚 Learning: 2025-12-28T02:07:57.647Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/api/ApiAccessors.py:81-88
Timestamp: 2025-12-28T02:07:57.647Z
Learning: In src/api/ApiAccessors.py, DataApiAccessor.convert_parsed intentionally raises NotImplementedError as a template method pattern - subclasses using get_by_query_converted, get_by_endpoint_converted, or requestData must implement convert_parsed.

Applied to files:

  • src/api/sim_mod_updater.py
  • src/api/matchmaker_queue_api.py
  • src/api/stats_api.py
  • src/api/coop_api.py
  • src/api/vaults_api.py
  • src/api/featured_mod_api.py
  • src/vaults/mapvault/mapdetails.py
📚 Learning: 2025-12-29T03:25:49.981Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/api/sim_mod_updater.py:21-27
Timestamp: 2025-12-29T03:25:49.981Z
Learning: In src/api/sim_mod_updater.py, treat a missing 'downloadUrl' in the API response as a hard API contract violation and fail fast. Do not swallow KeyError or provide fallback handling; allow the exception (or a clearly named custom exception) to crash the process, signaling an unmet contract. This should be explicit in code paths that access response['downloadUrl'], ensuring absence leads to immediate failure rather than silent degradation. If applicable, add a concise comment explaining the fail-fast rationale.

Applied to files:

  • src/api/sim_mod_updater.py
📚 Learning: 2025-11-23T00:07:39.893Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1145
File: src/api/vaults_api.py:114-116
Timestamp: 2025-11-23T00:07:39.893Z
Learning: In src/api/vaults_api.py, the MapPoolApiConnector class uses intentional stateful filter mutation. The request_pool_for_queue method sets self._filters and is the intended public API; request_data() should not be called directly on instances of this class.

Applied to files:

  • src/api/vaults_api.py
📚 Learning: 2025-12-28T02:07:52.863Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/api/ApiAccessors.py:81-88
Timestamp: 2025-12-28T02:07:52.863Z
Learning: In src/api/ApiAccessors.py, DataApiAccessor.convert_parsed should be a template method that raises NotImplementedError. Subclasses that use get_by_query_converted, get_by_endpoint_converted, or requestData must implement convert_parsed to provide concrete parsing logic. This enforces that derived accessors supply the needed parsing behavior and prevents accidental use of a base implementation.

Applied to files:

  • src/api/ApiAccessors.py
📚 Learning: 2025-12-27T22:23:49.101Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/fa/game_updater/updater.py:274-279
Timestamp: 2025-12-27T22:23:49.101Z
Learning: In src/fa/game_updater/updater.py, the single-element unpacking `mod, = data["values"]` in the `process_fmod` method is intentional and serves as validation that exactly one featured mod is returned. Multiple or zero values should raise a ValueError to signal an API error.

Applied to files:

  • src/api/featured_mod_api.py
🧬 Code graph analysis (20)
src/connectivity/IceServersPoller.py (1)
src/api/ApiBase.py (2)
  • get (72-82)
  • get (140-153)
src/replays/_replayswidget.py (1)
src/api/ApiAccessors.py (2)
  • ParsedDataApiResponse (45-47)
  • get_parsed (61-71)
src/games/_gameswidget.py (1)
src/games/automatchframe.py (2)
  • MatchmakerQueueFrame (28-253)
  • handleQueueInfo (123-131)
src/api/sim_mod_updater.py (1)
src/api/ApiAccessors.py (2)
  • ParsedDataApiResponse (45-47)
  • get_parsed (61-71)
src/stats/leaderboard_widget.py (1)
src/api/ApiAccessors.py (2)
  • ParsedDataApiResponse (45-47)
  • get_parsed (61-71)
src/vaults/reviewwidget.py (2)
src/api/ApiAccessors.py (1)
  • ParsedDataApiResponse (45-47)
src/vaults/detailswidget.py (1)
  • on_reviews_data (379-391)
src/api/matchmaker_queue_api.py (2)
src/api/ApiAccessors.py (1)
  • ParsedDataApiResponse (45-47)
src/api/models/MatchmakerQueue.py (1)
  • MatchmakerQueue (10-15)
src/api/player_api.py (1)
src/api/ApiAccessors.py (2)
  • ParsedDataApiResponse (45-47)
  • get_parsed (61-71)
src/vaults/detailswidget.py (3)
src/api/ApiAccessors.py (1)
  • ParsedDataApiResponse (45-47)
src/vaults/mapvault/mapdetails.py (1)
  • allow_review (55-58)
src/vaults/reviewwidget.py (1)
  • on_reviews_data (330-337)
src/api/stats_api.py (1)
src/api/ApiAccessors.py (2)
  • ParsedDataApiResponse (45-47)
  • get_parsed (61-71)
src/model/player.py (1)
src/model/game.py (1)
  • Game (44-302)
src/games/automatchframe.py (1)
src/api/models/MatchmakerQueue.py (1)
  • MatchmakerQueue (10-15)
src/model/game.py (3)
src/chat/chatter_model.py (1)
  • name (187-191)
src/model/qobjectmapping.py (1)
  • values (61-62)
src/model/player.py (1)
  • global_estimate (73-74)
src/api/coop_api.py (3)
src/api/ApiAccessors.py (1)
  • ParsedDataApiResponse (45-47)
src/api/models/CoopScenario.py (1)
  • CoopScenario (7-13)
src/api/models/CoopResult.py (1)
  • CoopResult (7-14)
src/chat/ircconnection.py (1)
src/api/ApiBase.py (2)
  • get (72-82)
  • get (140-153)
src/oauth/oauth_flow.py (1)
src/config/__init__.py (5)
  • Settings (43-207)
  • get (59-69)
  • get_list (73-77)
  • get_list (81-85)
  • get_list (88-151)
src/api/vaults_api.py (3)
src/api/ApiAccessors.py (4)
  • DataApiAccessor (50-189)
  • ParsedDataApiResponse (45-47)
  • convert_parsed (188-189)
  • get_parsed (61-71)
src/api/featured_mod_api.py (2)
  • convert_parsed (18-23)
  • convert_parsed (34-39)
src/api/matchmaker_queue_api.py (1)
  • convert_parsed (14-19)
src/api/ApiAccessors.py (4)
src/api/ApiBase.py (4)
  • handle (44-45)
  • _do_nothing (29-30)
  • get (72-82)
  • get (140-153)
src/api/coop_api.py (2)
  • convert_parsed (15-20)
  • convert_parsed (65-72)
src/api/matchmaker_queue_api.py (1)
  • convert_parsed (14-19)
src/api/vaults_api.py (3)
  • convert_parsed (79-84)
  • convert_parsed (100-108)
  • convert_parsed (126-133)
src/api/featured_mod_api.py (3)
src/api/ApiAccessors.py (3)
  • ParsedDataApiResponse (45-47)
  • requestData (177-186)
  • get_converted (95-105)
src/api/models/FeaturedMod.py (1)
  • FeaturedMod (6-12)
src/api/models/FeaturedModFile.py (1)
  • FeaturedModFile (6-15)
src/vaults/mapvault/mapdetails.py (2)
src/api/ApiAccessors.py (2)
  • ParsedDataApiResponse (45-47)
  • get_parsed (61-71)
src/vaults/detailswidget.py (1)
  • allow_review (90-91)
🪛 GitHub Actions: Checks
src/util/theme.py

[error] 32-32: ValueError in ThemeSet._nullcheck: too many values to unpack (expected 1, got 2)

🪛 Ruff (0.14.10)
src/util/theme.py

17-17: Undefined name Theme

(F821)


18-18: Undefined name Theme

(F821)


29-29: Undefined name ThemeSet

(F821)


30-30: Undefined name ThemeSet

(F821)

src/api/ApiAccessors.py

36-36: Undefined name DataApiResponse

(F821)


168-168: Do not catch blind exception: Exception

(BLE001)


169-169: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🔇 Additional comments (63)
src/chat/ircconnection.py (1)

196-200: LGTM! Correctly adopts the unified API method.

The change from get_by_endpoint to get aligns with the PR's goal of consolidating API access methods. The endpoint path, callback signatures, and error handling remain correct and unchanged.

src/util/gameurl.py (2)

69-69: LGTM: Explicit type conversion improves clarity.

The explicit int() cast makes the type conversion visible and clear. The error handling is preserved since ValueError from invalid integer strings is caught by the exception handler at line 78.


88-91: LGTM: Simplified signature improves maintainability.

The refactor removes the generic type parameter and moves type conversion responsibility to the caller. This makes the code simpler and more explicit about conversions, which improves type safety and readability.

src/config/admin.py (1)

48-51: LGTM! f-string conversion improves readability.

The migration from .format() to f-strings is a clean modernization that improves code readability while maintaining the same functionality.

src/model/player.py (1)

150-153: LGTM! Clean consolidation of duplicate setters.

The property setter correctly delegates to set_currentGame, and the CAVEAT comment appropriately warns that assignments like player.currentGame = game will emit signals immediately (since no transaction context is passed). This design provides both convenience (property setter) and control (transactional method for batched updates).

src/util/theme.py (1)

110-110: LGTM: Correct method name for Python file API.

The change from readLines() to readlines() correctly aligns with Python's standard file API.

src/fa/replayserver.py (1)

78-78: Method signature is compatible—no changes needed.

The get() method signature from JsonApiBase correctly handles the endpoint path as a string and properly wraps the response handler to decode the QNetworkReply into dict[str, Any], matching the expectations of both on_replay_access_url and on_api_error callbacks. The consolidation of get_by_endpoint into get() is complete and correct.

src/config/__init__.py (2)

38-38: LGTM!

The type annotation for _unpersisted_settings correctly matches the key types used in Settings.set (lines 154-158) and Settings.contains (line 206), ensuring type consistency across the Settings class.


307-307: LGTM!

The explicit type annotation and consolidated initialization improve clarity. The type correctly reflects the return type of _settings.value with type=str.

src/oauth/oauth_flow.py (5)

1-3: LGTM!

The imports support the new type annotations and follow standard typing conventions.


29-29: LGTM!

The ClassVar[Logger] annotation correctly declares the logger attribute injected by the @with_logger decorator and matches its usage in the lifecycle callbacks (lines 72, 76, 79, 83).


54-54: LGTM!

The type annotation correctly reflects that _expires_in can be None (when expiration checking is stopped) or int (milliseconds until expiration), matching its usage in check_token (line 64) and on_expiration_at_changed (line 73).


56-59: Good improvement to state management.

Resetting _expires_in to None when stopping expiration checks improves correctness by ensuring stale expiration times don't persist. This aligns with the guard in check_token (line 64) that treats None as "not checking."


86-104: LGTM!

The explicit defaults improve type safety:

  • Line 90: default="" ensures client_id is strictly str rather than str | None
  • Line 91: default=[] ensures scopes is strictly list[str] rather than list[str] | None

Both changes leverage the appropriate Settings.get overloads and improve type checker support.

src/api/ApiBase.py (2)

72-82: Good unification of query and endpoint-based GET requests.

The consolidated get method elegantly handles both QueryOptions (dict-like) and str (endpoint path) inputs through runtime type checking, reducing code duplication while maintaining type safety.


23-23: No action needed. The project is configured to use Python 3.14 in all build workflows, which fully supports the type statement syntax (available since Python 3.12). The code is compatible with the project's Python version.

src/connectivity/IceServersPoller.py (1)

21-24: Acknowledged: Response type mismatch is a known issue.

The type: ignore with FIXME appropriately documents that /ice/session/game/{id} returns a non-standard response shape. The migration to get() is correct; the underlying type inconsistency is a separate concern to address later.

src/games/_gameswidget.py (1)

445-453: Good refactor extracting team_size to a local variable.

The extraction improves readability and ensures consistency across the insert position, queue comparison, constructor call, and tab naming. The change correctly aligns with the MatchmakerQueueFrame constructor signature shown in the relevant snippets.

src/vaults/reviewwidget.py (1)

20-20: Type migration to ParsedDataApiResponse is correct.

The import and type hint changes align with the new API response structure. The existing assertion at line 334 properly validates that data is a list before indexing, which is necessary since ParsedDataApiResponse["data"] can be either a dict or list.

Also applies to: 330-337

src/vaults/mapvault/mapdetails.py (2)

36-46: Migration to get_parsed with correct query structure.

The method call correctly uses the unified get_parsed API with a query dict. The query parameters are properly typed as string values compatible with QueryOptions.


55-58: Type consistency with parent class confirmed.

The allow_review override correctly uses ParsedDataApiResponse, matching the parent class signature shown in src/vaults/detailswidget.py (lines 89-90). The len(response["data"]) check works correctly since data can be a dict or list, both supporting len().

src/api/sim_mod_updater.py (2)

22-28: Intentional fail-fast behavior preserved.

Based on learnings, the assertion at line 23 and the potential KeyError at line 25 are intentional design choices to signal API contract violations immediately rather than degrading silently. The type migration to ParsedDataApiResponse is correct.


31-32: Correct migration to get_parsed with explicit QueryOptions typing.

The explicit type annotation query: QueryOptions improves readability and aligns with the new unified API access pattern.

src/replays/_replayswidget.py (1)

943-947: Correct migration to unified get_parsed method.

The API call correctly uses the new get_parsed method with query parameters, response handler, and error handler arguments matching the expected signature.

src/vaults/detailswidget.py (2)

18-18: LGTM!

Import updated correctly to use the new ParsedDataApiResponse type.


163-165: Good addition of runtime assertion.

The assert isinstance(data, dict) properly validates the data structure before accessing type-specific keys like data["type"] and data["player"].

src/stats/leaderboard_widget.py (3)

7-7: LGTM!

Import updated correctly.


163-167: LGTM!

Signature correctly updated to use ParsedDataApiResponse, and the method properly accesses parsed["meta"].


314-314: LGTM!

API call correctly updated to use get_parsed.

src/playercard/ratingtabwidget.py (1)

1-2: LGTM!

Import added for type annotation.

src/games/automatchframe.py (3)

14-14: LGTM!

Import of MatchmakerQueue model added to support the new typed API response handling.


28-28: Good rename to avoid naming conflict.

Renaming the class to MatchmakerQueueFrame avoids confusion with the imported MatchmakerQueue model.


117-121: LGTM!

Good use of the typed MatchmakerQueue model with proper assertion before accessing the optional leaderboard field. The iteration now uses object attributes (queue.name, queue.leaderboard.technical_name) instead of dictionary keys.

src/api/matchmaker_queue_api.py (1)

14-19: LGTM!

The convert_parsed implementation correctly:

  1. Uses ParsedDataApiResponse parameter type
  2. Adds runtime assertion to validate data is a list
  3. Returns properly typed dict[str, list[MatchmakerQueue]]
  4. Constructs MatchmakerQueue model instances from raw data

This follows the template method pattern as expected.

src/api/featured_mod_api.py (5)

6-7: LGTM!

Imports updated correctly for the new types.


18-23: LGTM!

convert_parsed correctly updated with ParsedDataApiResponse type and runtime assertion.


25-27: LGTM!

Good explicit type annotation for the query variable using QueryOptions.


34-39: LGTM!

convert_parsed correctly updated with assertion and proper return type.


45-49: LGTM!

Methods correctly updated to use get_converted with endpoint paths and signal emitters as handlers.

src/api/coop_api.py (6)

2-3: LGTM!

Imports updated correctly for the new types.


15-20: LGTM!

convert_parsed correctly updated with assertion and proper typing.


27-33: LGTM!

Good rename from prepare_query_dict to prepare_query_options to align with the QueryOptions type.


35-38: LGTM!

The str(cur_filters) cast safely handles the case where filter key might be missing (.get() returns empty string).


48-63: LGTM!

Good defensive coding with:

  1. Explicit type annotations for unique_results and unique_teams
  2. Runtime assertions for result.game and result.game.player_stats
  3. Proper filtering of None players before creating the sorted tuple

65-72: LGTM!

convert_parsed correctly updated with assertion and proper return type.

src/api/player_api.py (2)

22-31: LGTM!

The method correctly uses the new QueryOptions type and migrates to get_parsed with direct signal emission as the handler.


33-38: LGTM!

Consistent migration to QueryOptions typing and get_parsed method.

src/api/vaults_api.py (6)

38-40: LGTM!

Using copy() from the standard library is appropriate for shallow copying the query options dictionary.


79-84: LGTM!

The runtime assertion ensures type safety before iterating over parsed["data"], consistent with the ParsedDataApiResponse type definition where data can be either a dict or list.


100-108: LGTM!

Consistent pattern with assertion guard before list iteration.


126-133: LGTM!

Properly updated signature and assertion pattern.


142-145: LGTM!

The migration from get_by_query_parsed to get_parsed is correct, and emitting directly via self.data_ready.emit is appropriate for this use case.


179-188: LGTM!

Handler type correctly updated to ParsedDataApiResponse.

src/api/ApiAccessors.py (4)

53-71: LGTM!

The _handler_parsed and get_parsed methods are well-structured, properly typed, and provide a clean API for handling parsed responses.


87-105: LGTM!

The _handler_converted and get_converted methods follow the same pattern as the parsed variants, maintaining consistency.


107-112: LGTM!

The parse_message method correctly composes the parsing steps and returns a properly typed ParsedDataApiResponse.


188-189: Intentional template method pattern.

This follows the established pattern where subclasses using get_converted or requestData must implement convert_parsed. Based on learnings from previous reviews, this is the intended design.

src/api/stats_api.py (5)

38-44: LGTM!

Consistent pattern with assertion guard and proper type annotation for ParsedDataApiResponse.


51-61: LGTM!

The convert_parsed method correctly asserts list type and returns a properly structured result.


122-134: LGTM!

The handler correctly extracts and validates score_data before conditional emission.


136-147: LGTM!

Consistent pattern with assertion and proper migration to get_parsed.


163-165: LGTM!

Properly updated handler with type-safe assertion.

src/client/connection.py (1)

260-264: The method change from get_by_endpoint to get is correct. The callback signature dict[Literal["accessUrl"], str] is compatible with the get method's response_handler: Callable[[dict[str, Any]], None] parameter, and using get (rather than get_parsed) is appropriate since the /lobby/access endpoint returns a simple JSON structure, not JSON:API format requiring parsing.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/api/ApiAccessors.py (1)

168-170: Use logger.exception for better error diagnostics.

While the typo has been corrected, the logging should use logger.exception instead of logger.error to automatically capture the stack trace, which is valuable for debugging parsing errors.

🔎 Proposed fix
         except Exception as e:
-            logger.error("Error parsing %s: %s", data, e)
+            logger.exception("Error parsing %s", data)
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d994658 and d8fdae2.

📒 Files selected for processing (3)
  • src/api/ApiAccessors.py
  • src/connectivity/IceServersPoller.py
  • src/replays/zigparser/parser.zig
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/connectivity/IceServersPoller.py
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: Gatsik
Repo: FAForever/client PR: 1150
File: src/fa/replayserver.py:80-80
Timestamp: 2026-01-02T02:50:24.628Z
Learning: In the FAForever client codebase, all API response handlers (callbacks passed to `get`, `post`, etc. in ApiBase) must accept `dict[str, Any]` as their parameter type to maintain signature compatibility. More specific types like `dict[Literal["key"], type]` or custom TypedDicts cannot be used directly in handler signatures due to contravariance constraints.
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/api/ApiAccessors.py:81-88
Timestamp: 2025-12-28T02:07:57.647Z
Learning: In src/api/ApiAccessors.py, DataApiAccessor.convert_parsed intentionally raises NotImplementedError as a template method pattern - subclasses using get_by_query_converted, get_by_endpoint_converted, or requestData must implement convert_parsed.
📚 Learning: 2026-01-02T02:53:05.593Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1150
File: src/api/ApiAccessors.py:32-42
Timestamp: 2026-01-02T02:53:05.593Z
Learning: In the FAForever client (Python 3.14+), forward references in TypedDict definitions (e.g., DataApiResourceObject referencing DataApiResponse defined later in src/api/ApiAccessors.py) are supported and should not be flagged as errors by static analysis tools. Treat forward references in TypedDicts as valid; ensure type checkers (e.g., mypy) are configured to allow PEP 695/Typing enhancements and that such patterns pass type checks without false positives.

Applied to files:

  • src/api/ApiAccessors.py
📚 Learning: 2025-12-28T02:07:52.863Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/api/ApiAccessors.py:81-88
Timestamp: 2025-12-28T02:07:52.863Z
Learning: In src/api/ApiAccessors.py, DataApiAccessor.convert_parsed should be a template method that raises NotImplementedError. Subclasses that use get_by_query_converted, get_by_endpoint_converted, or requestData must implement convert_parsed to provide concrete parsing logic. This enforces that derived accessors supply the needed parsing behavior and prevents accidental use of a base implementation.

Applied to files:

  • src/api/ApiAccessors.py
📚 Learning: 2026-01-02T02:53:05.593Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1150
File: src/api/ApiAccessors.py:32-42
Timestamp: 2026-01-02T02:53:05.593Z
Learning: In FAForever/client review context, for Python files (e.g., src/api/ApiAccessors.py), do not mention Ruff-specific warnings or propose Ruff configuration changes. Use generic lint guidance or project-approved lints instead; avoid any Ruff-related recommendations in reviews.

Applied to files:

  • src/api/ApiAccessors.py
📚 Learning: 2026-01-02T02:50:24.628Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1150
File: src/fa/replayserver.py:80-80
Timestamp: 2026-01-02T02:50:24.628Z
Learning: In the FAForever client codebase, all API response handlers (callbacks passed to `get`, `post`, etc. in ApiBase) must accept `dict[str, Any]` as their parameter type to maintain signature compatibility. More specific types like `dict[Literal["key"], type]` or custom TypedDicts cannot be used directly in handler signatures due to contravariance constraints.

Applied to files:

  • src/api/ApiAccessors.py
📚 Learning: 2026-01-02T02:40:22.985Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1150
File: src/util/__init__.py:0-0
Timestamp: 2026-01-02T02:40:22.985Z
Learning: In the FAForever/client repository (Python), os.path.getmtime() can accept DirEntry objects returned by os.scandir() directly. Do not extract the path attribute when using getmtime as a sorting key; pass the DirEntry (which is path-like) to getmtime and sort on that value. This applies to all Python files in the repo, not just a single file, and should be validated against the supported Python version in use.

Applied to files:

  • src/api/ApiAccessors.py
🧬 Code graph analysis (1)
src/api/ApiAccessors.py (1)
src/api/ApiBase.py (4)
  • handle (44-45)
  • _do_nothing (29-30)
  • get (72-82)
  • get (140-153)
🪛 Ruff (0.14.10)
src/api/ApiAccessors.py

36-36: Undefined name DataApiResponse

(F821)


168-168: Do not catch blind exception: Exception

(BLE001)


169-169: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🔇 Additional comments (5)
src/replays/zigparser/parser.zig (1)

25-25: LGTM - Function signature corrected.

The function signature correctly uses ParseError instead of the misspelled ParseErorr. Once the duplicate declaration issue is resolved, this change properly aligns the function signature with the correct error type.

src/api/ApiAccessors.py (4)

32-47: LGTM - Well-structured TypedDict definitions.

The new TypedDict definitions for DataApiResourceObject, DataApiResponse, and ParsedDataApiResponse provide excellent type safety for the Data API response structure. The use of NotRequired for optional fields and Literal["page"] for the meta structure demonstrates careful attention to the API contract.


53-59: LGTM - Handler refactoring improves type safety.

The refactored handler methods (_handler_parsed and _handler_converted) properly maintain signature compatibility while introducing strong typing through cast(). This approach aligns with the codebase requirements where API handlers must accept dict[str, Any] while providing improved type safety internally.

Also applies to: 87-93


61-71: LGTM - Unified get methods improve API consistency.

The refactored get_parsed and get_converted methods provide a cleaner, more unified interface that accepts either QueryOptions or string paths. The method naming is more concise while maintaining clarity about their parsing behavior.

Also applies to: 95-105


107-145: LGTM - Parsing and accessor methods properly refactored.

The parsing methods (parse_message, parseIncluded, parse_data, parse_single, parse_meta) are well-structured with appropriate type annotations using the new TypedDict types. The requestData method correctly uses the renamed get_converted method, and convert_parsed appropriately serves as a template method requiring subclass implementation.

Also applies to: 172-189

@Gatsik Gatsik merged commit 64d37e6 into FAForever:develop Jan 3, 2026
3 checks passed
@Gatsik Gatsik deleted the api-types branch January 3, 2026 14:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix type hint consistency for get_by_query_parsed callbacks

1 participant