# Clash Royale Python documentation Clash-Royale-Python: a friendly wrapper around the [Clash Royale API](https://developer.clashroyale.com). --- **Source Code**: https://github.com/guiepi/clash-royale-python --- ```{toctree} :caption: Installation & Usage :maxdepth: 2 installation usage pagination async ``` ```{toctree} :caption: API Reference :maxdepth: 3 api_reference/toc ``` ```{toctree} :caption: AI :maxdepth: 1 ai ``` # Installation The package is published on [PyPI](https://pypi.org/project/clash-royale-python/) and can be installed with `pip` (or any equivalent): ::::{tab-set} :::{tab-item} uv uv add clash-royale-python ::: :::{tab-item} poetry poetry add clash-royale-python ::: :::{tab-item} pip pip install clash-royale-python ::: :::: # Usage ## First Steps To start calling the API, you first need to instantiate a `Client` with your API key: ```python client = clash_royale.Client(api_key='your-api-key') ``` While this usage is simple and convenient for using in the Python console, it's best to use it as a context manager: ```python with clash_royale.Client(api_key='your-api-key') as client: ... ``` This is [the recommended way to use it by httpx](https://www.python-httpx.org/advanced/clients/#usage), which is the library we use under the hood. From there, you can search for clans: ```python client.clans.search('Legend') ``` ``` , , , , , '...']> ``` The above returned a lot of clans, wrapped in a `PaginatedList`, which is a list-like object (see the dedicated page about pagination for more details). You can also get information about a specific player by their tag: ```python client.players.get('#9G9JL8QU') ``` ``` ``` ## Main Concepts As we have just seen above, the entry point is the `Client` class, which gives access to various resource endpoints. The methods are attempting to map to the REST API endpoints from Clash Royale. You may have noticed from the above examples, but depending on the endpoint that is being called, the methods will return various types of resources. All the resources are listed in the resources reference page. ## More Examples ### Getting Fields About a Resource When you get a resource, you have access to all the fields that are in the REST API response. For example, all the fields presented in the documentation for the player object are accessible as attributes on the `Player` resource: ```python >>> player = client.players.get('#9G9JL8QU') >>> player.name 'PlayerName' >>> player.trophies 5432 >>> player.level 14 >>> player.wins 1234 ``` ### Getting Related Resources As well as giving access to its own attributes, resources provide methods to fetch related data. For example, when you get a player, you can get their battle log or upcoming chests. From a clan, you can get its members or river race log: ```python # Get a clan >>> clan = client.clans.get('#2Q8CCP0') >>> clan.name 'ClanName' >>> clan.members 47 # Get clan members >>> members = client.clans.get_members('#2Q8CCP0') >>> members[:3] [, , ] # Get player's battle log >>> player = client.players.get('#9G9JL8QU') >>> battlelog = client.players.get_battlelog('#9G9JL8QU') >>> battlelog[:3] [, , ] ``` ### Working with Leaderboards You can fetch leaderboard data with pagination control: ```python # Get available leaderboards >>> leaderboards = client.leaderboards.list() >>> leaderboards[0] # Get top players from a leaderboard >>> players = client.leaderboards.get(leaderboard_id=57000000, limit=10) ... for player in players: ... print(f"{player.rank}. {player.name} - {player.trophies}") ``` ### Searching with Filters When searching for clans or tournaments, you can use filters: ```python # Search clans with filters clans = client.clans.search( name='Legend', min_members=40, location_id=57000249, # United States limit=20 ) for clan in clans: print(f"{clan.name} - {clan.members} members") ``` ## Getting the Raw Data At some point, you might want to get the resources exported as Python dictionaries to store them somewhere else or transform them further. Each resource has a `model_dump()` method to export its content as a dictionary: ```python >>> player = client.players.get('#9G9JL8QU') >>> player.model_dump() {'tag': '#9G9JL8QU', 'name': 'PlayerName', 'level': 14, 'trophies': 5432, 'best_trophies': 6123, 'wins': 1234, 'losses': 987, ...} ``` You can also use `model_dump_json()` to get a JSON string directly: ```python >>> player.model_dump_json() '{"tag":"#9G9JL8QU","name":"PlayerName",...}' ``` ## Authentication The Clash Royale API requires an API key for all requests. You can get an API key by: 1. Creating an account at [https://developer.clashroyale.com](https://developer.clashroyale.com) 2. Creating an API key with your IP address whitelisted Once you have your API key, pass it to the `Client`: ```python client = clash_royale.Client(api_key='your-api-key') ``` **Important:** Keep your API key secret! Don't commit it to version control. Consider using environment variables: ```python import os client = clash_royale.Client(api_key=os.environ['CLASH_ROYALE_API_KEY']) ``` ### Using a proxy If your server does not have a static IP address, you can use a proxy server. For exemple you can use the proxy solution provided by the [RoyaleAPI team](https://royaleapi.com/) by setting the `proxy` parameter to `https://proxy.royaleapi.dev`. ```python >>> client = clash_royale.Client( api_key="your_api_key_here", proxy="https://proxy.royaleapi.dev", ) ``` For more detail about RoyaleAPI proxy usage, check their [documentation](https://docs.royaleapi.com/proxy.html). If you want to host your own proxy server, there is a nuxt web app you can use available at [https://github.com/AndreVarandas/royale-proxy-api](https://github.com/AndreVarandas/royale-proxy-api). Made by [@AndreVarandas](https://github.com/AndreVarandas/) ## Error Handling The client will raise specific exceptions for different error scenarios: ```python import clash_royale try: player = client.players.get('#INVALID') except clash_royale.ClashRoyaleNotFoundError: print("Player not found") except clash_royale.UnauthorizedError: print("Invalid API key") except clash_royale.RateLimitError: print("Rate limit exceeded") except clash_royale.ClashRoyaleHTTPError as e: print(f"API error: {e}") ``` # Pagination For endpoints returning a paginated response, the list of items are wrapped in a {class}`PaginatedList ` class which makes working with pagination more Pythonic while doing the necessary API calls transparently. ## Controlling Results ### The `limit` Parameter The `limit` parameter controls the **maximum total number of items** to fetch across all pages: ```python # Fetch at most 50 players players = client.leaderboards.get(leaderboard_id, limit=50) for player in players: # Safe: will only iterate over 50 players max print(player.name) ``` Without a `limit`, iteration will fetch **all available items** (potentially thousands): ```python # ⚠️ Warning: No limit = fetches ALL pages! players = client.leaderboards.get(leaderboard_id) for player in players: # Could make hundreds of API requests! print(player.name) ``` **Recommendation:** Always set a `limit` unless you genuinely need all results. ### The `page_size` Parameter The `page_size` parameter controls how many items are fetched per API request (default: 100): ```python # Fetch 500 players using pages of 250 items (2 API requests) players = client.leaderboards.get(leaderboard_id, limit=500, page_size=250) ``` **When to adjust `page_size`:** - **Large result sets** (500+ items): Increase to 250-1000 to reduce API calls - **Need first result fast**: Decrease to 10-50 to get initial results quickly ## Iterating over Elements The class is an iterator, meaning you can go through instances in the list with a for loop, or by calling `next()` to get the following item: ```python players = client.leaderboards.get(leaderboard_id, limit=100) # Iterable style for player in players: print(player.name) # Iterator player_1 = next(players) player_2 = next(players) player_3 = next(players) ``` This will take care of fetching extra pages if needed. Once all the elements have been fetched (or the `limit` is reached), no further network calls will happen. This will work if you iterate over the same paginated response again: ```python # No API calls: players is reused from above for player in players: print(player.name) ``` However, API calls would be repeated if you get a fresh paginated response: ```python # New API calls: get() returns a fresh paginated list for player in client.leaderboards.get(leaderboard_id, limit=100): print(player.name) ``` Be mindful of that when writing your code otherwise you'll consume your API quota quickly! ## Total Number To get the total number of items fetched (respecting the `limit`), use the built-in `len()` on the materialized list: ```python players = client.leaderboards.get(leaderboard_id, limit=50) player_list = list(players) print(len(player_list)) # 50 (or fewer if less available) ``` Note: The Clash Royale API does not provide a total count in advance, so you must fetch items to know how many there are. ## Indexing You can access elements by index: ```python players = client.leaderboards.get(leaderboard_id, limit=100) second_player = players[1] tenth_player = players[9] ``` Beware that accessing a large index may produce extra network calls to the Clash Royale API as pages preceding the given index will be fetched. For example, assuming the page size is 100, this will perform 2 API calls: ```python players = client.leaderboards.get(leaderboard_id) players[150] # Fetches first 2 pages ``` If the index exceeds the `limit` or available items, an `IndexError` will be raised, as if it were a list. Unlike list, this feature doesn't support negative values at the time. ## Slicing Slicing is supported and provides an easy way to get a specific range of results: ```python players = client.leaderboards.get(leaderboard_id) # With start & end players[2:5] # Players at positions 2, 3, 4 # Without start (get first N) players[:10] # Top 10 players # Without end (get from position onwards) players[10:] # All players from position 10 onwards # With start, end & step players[0:20:2] # Every other player in top 20 ``` **Tip:** Slicing without an end (e.g., `players[10:]`) will fetch all remaining items, which could produce many API calls. Consider setting a `limit` first: ```python # Safe: limit total results first, then slice players = client.leaderboards.get(leaderboard_id, limit=100) top_10 = players[:10] ``` ## Best Practices ### ✅ DO ```python # Set explicit limits players = client.leaderboards.get(lb_id, limit=50) # Use slicing for subsets top_10 = players[:10] # Use slicing for exact counts top_50 = client.clans.search(name="Legend", limit=50)[:50] # Break early in loops for player in players: if player.score > 5000: break ``` ### ❌ DON'T ```python # Don't fetch everything without a limit players = client.leaderboards.get(lb_id) all_players = list(players) # Danger: could be thousands! # Don't use tiny page sizes for large fetches players = client.leaderboards.get(lb_id, limit=1000, page_size=10) # 100 API requests! # Don't ignore pagination when you only need a few items for i, player in enumerate(client.leaderboards.get(lb_id)): if i >= 10: break # Better: players = client.leaderboards.get(lb_id, limit=10) ``` ## Performance Considerations The number of API requests is calculated as: `ceil(limit / page_size)` ```python # Examples: # limit=50, page_size=100 → 1 request # limit=150, page_size=100 → 2 requests # limit=500, page_size=250 → 2 requests # limit=1000, page_size=100 → 10 requests ``` Optimize by: 1. Setting appropriate `limit` values (don't fetch more than you need) 2. Increasing `page_size` for large bulk operations 3. Using slicing to get exact counts 4. Reusing paginated lists instead of re-fetching ## Complete Example ```python import clash_royale client = clash_royale.Client(api_key="your_api_key") # Get top 50 players efficiently players = client.leaderboards.get( leaderboard_id=170000008, limit=50, page_size=50 # Single API request ) # Work with top 10 top_10 = players[:10] for i, player in enumerate(top_10, 1): print(f"{i}. {player.name} - {player.score} points") # Or use slicing next_10 = players[10:20] print(f"Players 11-20: {[p.name for p in next_10]}") # Search clans with limit clans = client.clans.search(name="Legend", min_members=40, limit=25) for clan in clans: print(f"{clan.name} - {clan.members} members") ``` # Async Usage The library provides a fully async client under `clash_royale.aio`. It mirrors the synchronous API but uses `async`/`await`, making it suitable for asyncio-based applications. ## Getting Started ```python from clash_royale.aio import Client async with Client(api_key="your-api-key") as client: player = await client.players.get("#9G9JL8QU") print(player.name) ``` The async client **must** be used as an async context manager (or closed manually with `await client.aclose()`). This ensures the underlying HTTP connection pool is properly cleaned up. ### Using a proxy Proxy usage works the same as the sync client: ```python from clash_royale.aio import Client async with Client( api_key="your-api-key", proxy="https://proxy.royaleapi.dev", ) as client: ... ``` ## Resources All resource methods that perform a single API call are `async`: ```python # Single-item endpoints are async player = await client.players.get("#9G9JL8QU") clan = await client.clans.get("#2Q8CCP0") tournament = await client.tournaments.get("#2PP") # Battlelog and upcoming chests are also async battles = await client.players.get_battlelog("#9G9JL8QU") chests = await client.players.get_upcoming_chests("#9G9JL8QU") ``` Methods that return a {class}`PaginatedList ` are **not** async themselves (they return immediately), but the list fetches pages asynchronously when you access its items: ```python # Returns a PaginatedList immediately (no await) results = client.clans.search(name="Legend", limit=10) members = client.clans.get_members("#2Q8CCP0", limit=20) cards = client.cards.list(limit=10) ``` ## Async Pagination The async {class}`PaginatedList ` provides explicit async methods instead of magic methods like `__getitem__`. ### Async iteration ```python results = client.clans.search(name="Legend", limit=20) async for clan in results: print(clan.name) ``` ### Fetching all items ```{warning} `all()` fetches **every page** from the API, which may result in many requests and large memory usage. Always set a `limit` when creating the paginated list, or prefer async iteration with an early `break`. ``` ```python # Safe: limit is set results = client.clans.search(name="Legend", limit=20) clans = await results.all() print(len(clans)) # At most 20 ``` ### Index access ```python results = client.leaderboards.get(leaderboard_id, limit=100) first = await results.get(0) fifth = await results.get(4) ``` Negative indexing is not supported — use `all()` and index the resulting list instead. ### Slicing ```python top_10 = await results.slice(0, 10) next_10 = await results.slice(10, 20) ``` ### First item ```python first = await results.first() # Returns None if empty ``` ### `limit` and `page_size` These work identically to the sync client: ```python # Fetch at most 50 players, 25 per API request players = client.leaderboards.get(leaderboard_id, limit=50, page_size=25) async for player in players: print(player.name) ``` ## Comparison with Sync Client | Operation | Sync | Async | |-----------|------|-------| | Get a resource | `client.players.get(tag)` | `await client.players.get(tag)` | | Context manager | `with client:` | `async with client:` | | Iterate | `for item in results:` | `async for item in results:` | | Get all items | `list(results)` | `await results.all()` | | Index access | `results[0]` | `await results.get(0)` | | Slice | `results[:10]` | `await results.slice(0, 10)` | | First item | `next(results)` | `await results.first()` | Models, exceptions, and types are shared between the sync and async clients. You import them from the top-level `clash_royale` package: ```python from clash_royale.aio import Client from clash_royale import ClashRoyaleNotFoundError, Player ``` ## Error Handling Error handling is the same as the sync client, using the same exception classes: ```python import clash_royale from clash_royale.aio import Client async with Client(api_key="your-api-key") as client: try: player = await client.players.get("#INVALID") except clash_royale.ClashRoyaleNotFoundError: print("Player not found") except clash_royale.ClashRoyaleHTTPError as e: print(f"API error: {e}") ``` ## Complete Example ```python import asyncio from clash_royale.aio import Client async def main(): async with Client(api_key="your_api_key") as client: # Get a player player = await client.players.get("#9G9JL8QU") print(f"{player.name} - {player.trophies} trophies") # Search clans results = client.clans.search(name="Legend", min_members=40, limit=10) async for clan in results: print(f"{clan.name} - {clan.members} members") # Get top leaderboard players players = client.leaderboards.get(170000008, limit=10, page_size=10) top_10 = await players.all() for p in top_10: print(f"{p.rank}. {p.name} - {p.score} points") asyncio.run(main()) ``` Reference ========= This is the auto-generated documentation for all the main modules in the library. :Release: |version| :Date: |today| .. toctree:: :maxdepth: 2 client pagination types resources models aio/toc Client ====== The client class is the main entry point to start querying the `Clash Royale API `_. .. autoclass:: clash_royale.Client :members: :undoc-members: :show-inheritance: Pagination ========== Utilities for handling paginated API responses. .. autoclass:: clash_royale.PaginatedList :members: :undoc-members: :show-inheritance: Types ===== Parameter types used for API requests. .. automodule:: clash_royale.types :members: :undoc-members: (resources-reference)= # Resources A collection of Python classes modelling each type of content that is returned by the Clash Royale API. ```{toctree} :maxdepth: 1 resources/cards resources/clans resources/global_tournaments resources/leaderboards resources/locations resources/players resources/tournaments ``` Cards ===== .. autoclass:: clash_royale.Cards :members: :undoc-members: :show-inheritance: Clans ===== .. autoclass:: clash_royale.Clans :members: :undoc-members: :show-inheritance: Global Tournaments ================== .. autoclass:: clash_royale.GlobalTournaments :members: :undoc-members: :show-inheritance: Leaderboards ============ .. autoclass:: clash_royale.Leaderboards :members: :undoc-members: :show-inheritance: Locations ========= .. autoclass:: clash_royale.Locations :members: :undoc-members: :show-inheritance: Players ======= .. autoclass:: clash_royale.Players :members: :undoc-members: :show-inheritance: Tournaments =========== .. autoclass:: clash_royale.Tournaments :members: :undoc-members: :show-inheritance: (models-reference)= # Models A collection of Python classes modelling each type of content that is returned by the Clash Royale API. ```{toctree} :maxdepth: 1 models/card models/clan models/global_tournament models/leaderboard models/location models/player models/tournament ``` Card ==== .. automodule:: clash_royale.models.card :members: :undoc-members: :exclude-members: model_config Clan ==== .. automodule:: clash_royale.models.clan :members: :undoc-members: :exclude-members: model_config Global Tournament ================= .. automodule:: clash_royale.models.global_tournament :members: :undoc-members: :exclude-members: model_config Leaderboard =========== .. automodule:: clash_royale.models.leaderboard :members: :undoc-members: :exclude-members: model_config Location ======== .. automodule:: clash_royale.models.location :members: :undoc-members: :exclude-members: model_config Player ====== .. automodule:: clash_royale.models.player :members: :undoc-members: :exclude-members: model_config Tournament ========== .. automodule:: clash_royale.models.tournament :members: :undoc-members: :exclude-members: model_config Async (aio) =========== Async implementation of the Clash Royale API client, using ``async``/``await``. Models, exceptions, and types are shared with the sync client. .. toctree:: :maxdepth: 2 client pagination resources Client ====== The async client class for querying the `Clash Royale API `_. .. autoclass:: clash_royale.aio.Client :members: :undoc-members: :show-inheritance: Pagination ========== Async utilities for handling paginated API responses. .. autoclass:: clash_royale.aio.PaginatedList :members: :undoc-members: :show-inheritance: (aio-resources-reference)= # Resources Async resource classes for the Clash Royale API. These mirror the sync resources but use `async`/`await` for methods that perform API calls. ```{toctree} :maxdepth: 1 resources/cards resources/clans resources/global_tournaments resources/leaderboards resources/locations resources/players resources/tournaments ``` Cards ===== .. autoclass:: clash_royale.aio.Cards :members: :undoc-members: :show-inheritance: Clans ===== .. autoclass:: clash_royale.aio.Clans :members: :undoc-members: :show-inheritance: GlobalTournaments ================= .. autoclass:: clash_royale.aio.GlobalTournaments :members: :undoc-members: :show-inheritance: Leaderboards ============ .. autoclass:: clash_royale.aio.Leaderboards :members: :undoc-members: :show-inheritance: Locations ========= .. autoclass:: clash_royale.aio.Locations :members: :undoc-members: :show-inheritance: Players ======= .. autoclass:: clash_royale.aio.Players :members: :undoc-members: :show-inheritance: Tournaments =========== .. autoclass:: clash_royale.aio.Tournaments :members: :undoc-members: :show-inheritance: # AI / LLM Integration This project provides LLM-friendly documentation following the [llms.txt](https://llmstxt.org) standard. Use these files to give your AI coding assistant (Cursor, Windsurf, Claude Code, etc.) full context about this library: | File | Description | |------|-------------| | [`llms.txt`](https://guiepi.github.io/clash-royale-python/llms.txt) | Summary with links to each documentation page | | [`llms-full.txt`](https://guiepi.github.io/clash-royale-python/llms-full.txt) | Complete documentation + source code in a single file | ***************** Source Code Files ***************** This section contains source code files from the project repository. These files are included to provide implementation context and technical details that complement the documentation above. **Files included:** .. code-block:: text __init__.py auth.py base.py card.py cards.py clan.py clans.py client.py common.py exceptions.py global_tournament.py global_tournaments.py leaderboard.py leaderboards.py location.py locations.py pagination.py player.py players.py resource.py tournament.py tournaments.py types.py __init__.py =========== .. code-block:: python from __future__ import annotations from .client import Client from .exceptions import ( ClashRoyaleBadRequestError, ClashRoyaleException, ClashRoyaleHTTPError, ClashRoyaleNotFoundError, ClashRoyaleRateLimitError, ClashRoyaleServerError, ClashRoyaleUnauthorizedError, InvalidAPIKeyError, ) from .models import ( Arena, Badge, Battle, Card, Clan, ClanMember, ClanSearchResult, Icon, Location, Player, RiverRace, RiverRaceLog, UpcomingChest, ) from .models.base import ISO8601DateTime from .pagination import PaginatedList from .resources import ( Cards, Clans, GlobalTournaments, Leaderboards, Locations, Players, Tournaments, ) from .types import ClanSearchParams, PaginationParams __version__ = "0.2.1" __all__ = [ "Arena", "Badge", "Battle", "Card", "Cards", "Clan", "ClanMember", "ClanSearchParams", "ClanSearchResult", "Clans", "ClashRoyaleBadRequestError", "ClashRoyaleException", "ClashRoyaleHTTPError", "ClashRoyaleNotFoundError", "ClashRoyaleRateLimitError", "ClashRoyaleServerError", "ClashRoyaleUnauthorizedError", "Client", "GlobalTournaments", "Icon", "InvalidAPIKeyError", "ISO8601DateTime", "Leaderboards", "Location", "Locations", "PaginatedList", "PaginationParams", "Player", "Players", "RiverRace", "RiverRaceLog", "Tournaments", "UpcomingChest", ] __init__.py =========== .. code-block:: python """Async implementation of the Clash Royale API client.""" from __future__ import annotations from .client import Client from .pagination import PaginatedList from .resources import ( Cards, Clans, GlobalTournaments, Leaderboards, Locations, Players, Tournaments, ) __all__ = [ "Cards", "Clans", "Client", "GlobalTournaments", "Leaderboards", "Locations", "PaginatedList", "Players", "Tournaments", ] client.py ========= .. code-block:: python from __future__ import annotations import httpx from ..auth import CRAuth from ..exceptions import ClashRoyaleHTTPError from .resources import ( Cards, Clans, GlobalTournaments, Leaderboards, Locations, Players, Tournaments, ) CURRENT_VERSION = "v1" BASE_URL = f"https://api.clashroyale.com/{CURRENT_VERSION}" class Client(httpx.AsyncClient): """ An async client to retrieve data from the Clash Royale API. Create a client instance with the given options. >>> from clash_royale.aio import Client >>> async with Client(api_key="your_api_key_here") as client: ... player = await client.players.get("#ABC123") This client provides several methods to retrieve the content most kinds of Clash Royale objects, based on their json structure. If your server does not have a static IP address, you can use a proxy server by setting the `proxy` parameter. >>> client = Client( api_key="your_api_key_here", proxy="https://my.proxy.com", ) For more detail about proxy usage, check the usage section of the documentation. :param api_key: The Clash Royale API key. :param proxy: Proxy URL (default: None). :param timeout: Request timeout in seconds (default: 10.0). :param base_url: Base URL for the API (default: https://api.clashroyale.com/v1). """ players: Players clans: Clans cards: Cards tournaments: Tournaments leaderboards: Leaderboards locations: Locations global_tournaments: GlobalTournaments _auth: CRAuth def __init__( self, api_key: str, proxy: str | None = None, timeout: float = 10.0, base_url: str = BASE_URL, ): self._auth = CRAuth(api_key) if proxy: # Ensure proxy URL has the version suffix if not proxy.endswith(CURRENT_VERSION): base_url = proxy.rstrip("/") + "/" + CURRENT_VERSION else: base_url = proxy super().__init__( base_url=base_url, auth=self._auth, timeout=timeout, ) self.players = Players(self) self.clans = Clans(self) self.cards = Cards(self) self.tournaments = Tournaments(self) self.leaderboards = Leaderboards(self) self.locations = Locations(self) self.global_tournaments = GlobalTournaments(self) async def _request( self, method: str, endpoint: str, params: dict[str, str | int] | None = None, ) -> dict: """Make an async HTTP request to the API. :param method: HTTP method (GET, POST, etc.). :param endpoint: API endpoint (e.g., /players/{tag}). :param params: Query parameters. :returns: The JSON response as a dictionary. :raises ClashRoyaleNotFoundError: If resource is not found (404). :raises ClashRoyaleUnauthorizedError: If API key is invalid (403). :raises ClashRoyaleRateLimitError: If rate limit is exceeded (429). :raises ClashRoyaleBadRequestError: If request is malformed (400). :raises ClashRoyaleServerError: If server error occurs (5xx). :raises ClashRoyaleHTTPError: For other API errors. """ # Convert snake_case parameter names to camelCase for API compatibility if params: params = self._convert_params_to_camel_case(params) try: response = await super().request(method, endpoint, params=params) response.raise_for_status() return response.json() except httpx.HTTPStatusError as exc: raise ClashRoyaleHTTPError.from_http_error(exc) from exc def _convert_params_to_camel_case( self, params: dict[str, str | int] ) -> dict[str, str | int]: """Convert snake_case parameter keys to camelCase for API. :param params: Dictionary with snake_case keys. :returns: Dictionary with camelCase keys. """ converted = {} for key, value in params.items(): # Convert snake_case to camelCase if "_" in key: parts = key.split("_") camel_key = parts[0] + "".join(word.capitalize() for word in parts[1:]) converted[camel_key] = value else: converted[key] = value return converted pagination.py ============= .. code-block:: python from __future__ import annotations from collections.abc import AsyncGenerator from typing import TYPE_CHECKING, Generic, TypeVar from ..models.base import CRBaseModel from ..types import ClanSearchParams, PaginationParams if TYPE_CHECKING: from .client import Client ResourceType = TypeVar("ResourceType", bound="CRBaseModel") class PaginatedList(Generic[ResourceType]): """Async lazy-loading paginated list that fetches pages on demand. Supports async iteration and explicit async methods for indexed access. Example usage:: # Async iteration (lazy loading) async for clan in client.clans.search("royal"): print(clan.name) # Explicit index access clan = await results.get(5) # Explicit slice access clans = await results.slice(0, 10) # Fetch all results all_clans = await results.all() """ def __init__( self, client: Client, endpoint: str, model: type[ResourceType], params: PaginationParams | ClanSearchParams | None = None, ): self._client = client self._endpoint = endpoint self._model = model # Extract client-side pagination control params _params = params.copy() if params else {} self._limit: int | None = _params.pop("limit", None) self._page_size: int | None = _params.pop("page_size", None) # Remaining params are for the API (after, before, etc.) self._params: dict[str, str | int] = _params self._elements: list[ResourceType] = [] self._after_cursor: str | None = None self._has_more: bool = True def __repr__(self) -> str: loaded = len(self._elements) status = "more available" if self._has_more else "complete" return f"<{self.__class__.__name__} [{loaded} loaded, {status}]>" async def __aiter__(self) -> AsyncGenerator[ResourceType, None]: """Async iterate over all items, fetching pages as needed.""" for item in self._elements: yield item while self._has_more and ( self._limit is None or len(self._elements) < self._limit ): new_items = await self._grow() for item in new_items: yield item async def get(self, index: int) -> ResourceType: """Get item at index, fetching pages as needed. :param index: The index of the item to retrieve. :returns: The item at the specified index. :raises IndexError: If index is out of range. :raises ValueError: If index is negative. """ if index < 0: raise ValueError( "Negative indexing is not supported because it requires " "fetching all pages. Use `await results.all()` and index " "the resulting list instead." ) await self._fetch_to_index(index) return self._elements[index] async def slice(self, start: int, stop: int) -> list[ResourceType]: """Get a slice of items, fetching pages as needed. :param start: Start index (inclusive). :param stop: Stop index (exclusive). :returns: List of items in the specified range. """ await self._fetch_to_index(stop - 1) return self._elements[start:stop] async def all(self) -> list[ResourceType]: """Fetch and return all items. .. warning:: This method fetches all pages from the API, which may result in many requests and large memory usage for endpoints with many results. Consider using ``limit`` or async iteration for large datasets. :returns: List of all items. """ await self._fetch_all() return list(self._elements) async def first(self) -> ResourceType | None: """Get the first item, or None if empty. :returns: The first item or None. """ try: return await self.get(0) except IndexError: return None def _could_grow(self) -> bool: """Check if more items can be fetched.""" if self._limit is not None and len(self._elements) >= self._limit: return False return self._has_more async def _grow(self) -> list[ResourceType]: """Fetch the next page and add items to the list.""" new_elements = await self._fetch_next_page() # If limit is set, only add elements up to the limit if self._limit is not None: remaining = self._limit - len(self._elements) if remaining <= 0: return [] new_elements = new_elements[:remaining] self._elements.extend(new_elements) return new_elements async def _fetch_next_page(self) -> list[ResourceType]: """Fetch the next page from the API.""" params = self._params.copy() if self._after_cursor: params["after"] = self._after_cursor if self._page_size: # Send page_size as "limit" to the API params["limit"] = self._page_size response = await self._client._request("GET", self._endpoint, params=params) # Update cursor for next page paging = response.get("paging", {}) cursors = paging.get("cursors", {}) self._after_cursor = cursors.get("after") self._has_more = self._after_cursor is not None # Parse items with model items = response.get("items", []) return [self._model.model_validate(item) for item in items] async def _fetch_to_index(self, index: int) -> None: """Fetch pages until the specified index is available.""" while len(self._elements) <= index and self._could_grow(): await self._grow() async def _fetch_all(self) -> None: """Fetch all remaining pages.""" while self._could_grow(): await self._grow() __init__.py =========== .. code-block:: python from __future__ import annotations from .cards import Cards from .clans import Clans from .global_tournaments import GlobalTournaments from .leaderboards import Leaderboards from .locations import Locations from .players import Players from .tournaments import Tournaments __all__ = [ "Cards", "Clans", "GlobalTournaments", "Leaderboards", "Locations", "Players", "Tournaments", ] cards.py ======== .. code-block:: python from __future__ import annotations from typing_extensions import Unpack from ...models.card import Card from ...types import PaginationParams from ..pagination import PaginatedList from .resource import Resource class Cards(Resource): """ Async resource for card-related endpoints. Check the :clash-royale-api:`cards` for more detailed information about each endpoint. """ def list(self, **params: Unpack[PaginationParams]) -> PaginatedList[Card]: """List all available cards.""" return PaginatedList( client=self._client, endpoint="/cards", model=Card, params=params, # ty:ignore[invalid-argument-type] ) clans.py ======== .. code-block:: python from __future__ import annotations from typing_extensions import Unpack from ...models.clan import Clan, ClanMember, ClanSearchResult, RiverRace, RiverRaceLog from ...types import ClanSearchParams, PaginationParams from ..pagination import PaginatedList from .resource import Resource class Clans(Resource): """ Async resource for clan-related endpoints. Check the :clash-royale-api:`clans` for more detailed information about each endpoint. """ async def get(self, tag: str) -> Clan: """Get clan information by tag.""" encoded_tag = self._encode_tag(tag) response = await self._client._request("GET", f"/clans/{encoded_tag}") return Clan.model_validate(response) def search( self, name: str, **params: Unpack[ClanSearchParams] ) -> PaginatedList[ClanSearchResult]: """Search clans by name.""" api_params = {"name": name, **params} return PaginatedList( client=self._client, endpoint="/clans", model=ClanSearchResult, params=api_params, # ty:ignore[invalid-argument-type] ) def get_members( self, tag: str, **params: Unpack[PaginationParams] ) -> PaginatedList[ClanMember]: """Get clan members.""" encoded_tag = self._encode_tag(tag) return PaginatedList( client=self._client, endpoint=f"/clans/{encoded_tag}/members", model=ClanMember, params=params, # ty:ignore[invalid-argument-type] ) async def get_current_river_race(self, tag: str) -> RiverRace: """Get current river race information for a clan.""" encoded_tag = self._encode_tag(tag) response = await self._client._request( "GET", f"/clans/{encoded_tag}/currentriverrace" ) return RiverRace.model_validate(response) def get_river_race_log( self, tag: str, **params: Unpack[PaginationParams] ) -> PaginatedList[RiverRaceLog]: """Get river race log for a clan.""" encoded_tag = self._encode_tag(tag) return PaginatedList( client=self._client, endpoint=f"/clans/{encoded_tag}/riverracelog", model=RiverRaceLog, params=params, # ty:ignore[invalid-argument-type] ) global_tournaments.py ===================== .. code-block:: python from __future__ import annotations from typing_extensions import Unpack from ...models.global_tournament import GlobalTournament from ...types import PaginationParams from ..pagination import PaginatedList from .resource import Resource class GlobalTournaments(Resource): """ Async resource for global tournament related endpoints. Check the :clash-royale-api:`globaltournaments` for more detailed information about each endpoint. """ def list( self, **params: Unpack[PaginationParams] ) -> PaginatedList[GlobalTournament]: """Get list of global tournaments.""" return PaginatedList( client=self._client, endpoint="/globaltournaments", model=GlobalTournament, params=params, # ty:ignore[invalid-argument-type] ) leaderboards.py =============== .. code-block:: python from __future__ import annotations from typing_extensions import Unpack from ...models.leaderboard import Leaderboard, LeaderboardPlayer from ...types import PaginationParams from ..pagination import PaginatedList from .resource import Resource class Leaderboards(Resource): """ Async resource for leaderboard-related endpoints. Check the :clash-royale-api:`leaderboards` for more detailed information about each endpoint. """ async def list(self) -> list[Leaderboard]: # ty:ignore[invalid-type-form] """List all available leaderboards.""" response = await self._client._request("GET", "/leaderboards") items = response.get("items", []) return [Leaderboard.model_validate(item) for item in items] def get( self, leaderboard_id: int, **params: Unpack[PaginationParams] ) -> PaginatedList[LeaderboardPlayer]: """Get player rankings for a specific leaderboard. :param leaderboard_id: The leaderboard ID from the leaderboards list. """ return PaginatedList( client=self._client, endpoint=f"/leaderboard/{leaderboard_id}", model=LeaderboardPlayer, params=params, # ty:ignore[invalid-argument-type] ) locations.py ============ .. code-block:: python from __future__ import annotations import warnings from typing_extensions import Unpack from ...models.location import ( ClanRanking, LadderTournamentRanking, LeagueSeason, LeagueSeasonV2, Location, PlayerPathOfLegendRanking, PlayerRanking, PlayerSeasonRanking, ) from ...types import PaginationParams from ..pagination import PaginatedList from .resource import Resource class Locations(Resource): """ Async resource for location-related endpoints. Check the :clash-royale-api:`locations` for more detailed information about each endpoint. """ def list(self, **params: Unpack[PaginationParams]) -> PaginatedList[Location]: """List all available locations.""" return PaginatedList( client=self._client, endpoint="/locations", model=Location, params=params, # ty:ignore[invalid-argument-type] ) async def get(self, location_id: int) -> Location: """Get location information by ID. :param location_id: The location ID from the locations list. """ response = await self._client._request("GET", f"/locations/{location_id}") return Location.model_validate(response) def get_clan_rankings( self, location_id: int, **params: Unpack[PaginationParams], ) -> PaginatedList[ClanRanking]: """Get clan rankings for a specific location. :param location_id: The location ID from the locations list. """ return PaginatedList( client=self._client, endpoint=f"/locations/{location_id}/rankings/clans", model=ClanRanking, params=params, # ty:ignore[invalid-argument-type] ) def get_player_rankings( self, location_id: int, **params: Unpack[PaginationParams], ) -> PaginatedList[PlayerRanking]: """Get player rankings for a specific location. :param location_id: The location ID from the locations list. """ return PaginatedList( client=self._client, endpoint=f"/locations/{location_id}/rankings/players", model=PlayerRanking, params=params, # ty:ignore[invalid-argument-type] ) def get_clan_war_rankings( self, location_id: int, **params: Unpack[PaginationParams], ) -> PaginatedList[ClanRanking]: """Get clan war rankings for a specific location. :param location_id: The location ID from the locations list. """ return PaginatedList( client=self._client, endpoint=f"/locations/{location_id}/rankings/clanwars", model=ClanRanking, params=params, # ty:ignore[invalid-argument-type] ) def get_path_of_legend_player_rankings( self, location_id: int, **params: Unpack[PaginationParams], ) -> PaginatedList[PlayerPathOfLegendRanking]: """Get player rankings in Path of Legend for a specific location. :param location_id: The location ID from the locations list. """ return PaginatedList( client=self._client, endpoint=f"/locations/{location_id}/pathoflegend/players", model=PlayerPathOfLegendRanking, params=params, # ty:ignore[invalid-argument-type] ) def get_path_of_legend_season_rankings( self, season_id: str, **params: Unpack[PaginationParams], ) -> PaginatedList[PlayerPathOfLegendRanking]: """Get top Path of Legend players for a given season. :param season_id: The season ID (e.g., "2024-01"). """ return PaginatedList( client=self._client, endpoint=f"/locations/global/pathoflegend/{season_id}/rankings/players", model=PlayerPathOfLegendRanking, params=params, # ty:ignore[invalid-argument-type] ) async def get_season(self, season_id: str) -> LeagueSeason | LeagueSeasonV2: """Get top player league season. This method automatically selects the appropriate API endpoint based on the ``season_id`` format: - **Numeric ID** (e.g., ``"1"``, ``"2"``): Uses the V2 endpoint and returns :class:`~clash_royale.models.leaderboard.LeagueSeasonV2`. - **Date-based code** (e.g., ``"2016-02"``, ``"2025-07"``): Uses the legacy endpoint and returns :class:`~clash_royale.models.leaderboard.LeagueSeason`. .. warning:: The Clash Royale API may occasionally return incomplete season data with null values. This is a known API issue, not a library bug. :param season_id: Identifier of the season. Can be either a numeric unique ID (e.g., ``"1"``) or a date-based code (e.g., ``"2016-02"``). :return: :class:`~clash_royale.models.leaderboard.LeagueSeasonV2` if ``season_id`` is numeric, otherwise :class:`~clash_royale.models.leaderboard.LeagueSeason`. """ warnings.warn( "The API may return incomplete season data with null values.", UserWarning, stacklevel=2, ) if season_id.isdigit(): response = await self._client._request( "GET", f"/locations/global/seasons/{season_id}" ) return LeagueSeasonV2.model_validate(response) response = await self._client._request( "GET", f"/locations/global/seasons/{season_id}" ) return LeagueSeason.model_validate(response) def get_season_player_rankings( self, season_id: str, **params: Unpack[PaginationParams], ) -> PaginatedList[PlayerSeasonRanking]: """Get top player rankings for a season. :param season_id: The season ID (e.g., "2017-03"). """ return PaginatedList( client=self._client, endpoint=f"/locations/global/seasons/{season_id}/rankings/players", model=PlayerSeasonRanking, params=params, # ty:ignore[invalid-argument-type] ) def list_seasons(self) -> PaginatedList[LeagueSeason]: """List top player league seasons. .. warning:: The Clash Royale API may occasionally return incomplete season data with null values. This is a known API issue, not a library bug. Consider using :meth:`list_seasons_v2` instead. """ warnings.warn( "The API may return incomplete season data with null values.", UserWarning, stacklevel=2, ) return PaginatedList( client=self._client, endpoint="/locations/global/seasons", model=LeagueSeason, params={}, ) def list_seasons_v2(self) -> PaginatedList[LeagueSeasonV2]: """List league seasons with more details. .. warning:: The Clash Royale API may occasionally return incomplete season data with null values. This is a known API issue, not a library bug. """ warnings.warn( "The API may return incomplete season data with null values.", UserWarning, stacklevel=2, ) return PaginatedList( client=self._client, endpoint="/locations/global/seasonsV2", model=LeagueSeasonV2, params={}, ) def get_global_tournament_rankings( self, tournament_tag: str, **params: Unpack[PaginationParams], ) -> PaginatedList[LadderTournamentRanking]: """Get global tournament rankings.""" encoded_tag = self._encode_tag(tournament_tag) return PaginatedList( client=self._client, endpoint=f"/locations/global/rankings/tournaments/{encoded_tag}", model=LadderTournamentRanking, params=params, # ty:ignore[invalid-argument-type] ) players.py ========== .. code-block:: python from __future__ import annotations from ...models.player import Battle, Player, UpcomingChest from .resource import Resource class Players(Resource): """ Async resource for player-related endpoints. Check the :clash-royale-api:`players` for more detailed information about each endpoint. """ async def get(self, tag: str) -> Player: """Get player information by tag.""" encoded_tag = self._encode_tag(tag) response = await self._client._request("GET", f"/players/{encoded_tag}") return Player.model_validate(response) async def get_battlelog(self, tag: str) -> list[Battle]: """Get player's battle log.""" encoded_tag = self._encode_tag(tag) response = await self._client._request( "GET", f"/players/{encoded_tag}/battlelog" ) return [Battle.model_validate(battle) for battle in response] async def get_upcoming_chests(self, tag: str) -> list[UpcomingChest]: """Get player's upcoming chests.""" encoded_tag = self._encode_tag(tag) response = await self._client._request( "GET", f"/players/{encoded_tag}/upcomingchests" ) return [ UpcomingChest.model_validate(upcoming_chest) for upcoming_chest in response["items"] ] resource.py =========== .. code-block:: python from __future__ import annotations from typing import TYPE_CHECKING from urllib.parse import quote if TYPE_CHECKING: from ..client import Client class Resource: """Base class for all async API resources.""" def __init__(self, client: Client): """Initialize the resource with an async client.""" self._client = client def _encode_tag(self, tag: str) -> str: """Encode a Clash Royale tag for use in URLs.""" tag = self._normalize_tag(tag) return quote(tag, safe="") def _normalize_tag(self, tag: str) -> str: """Normalize a Clash Royale tag by ensuring it starts with '#' and is uppercase.""" if not tag.startswith("#"): tag = f"#{tag}" return tag.upper() tournaments.py ============== .. code-block:: python from __future__ import annotations from typing_extensions import Unpack from ...models import TournamentHeader from ...models.tournament import Tournament from ...types import PaginationParams from ..pagination import PaginatedList from .resource import Resource class Tournaments(Resource): """ Async resource for tournament-related endpoints. Check the :clash-royale-api:`tournaments` for more detailed information about each endpoint. """ async def get(self, tag: str) -> Tournament: """Get tournament information by tag.""" encoded_tag = self._encode_tag(tag) response = await self._client._request("GET", f"/tournaments/{encoded_tag}") return Tournament.model_validate(response) def search( self, name: str, **params: Unpack[PaginationParams] ) -> PaginatedList[TournamentHeader]: """Search tournaments by name.""" api_params = {"name": name, **params} return PaginatedList( client=self._client, endpoint="/tournaments", model=TournamentHeader, params=api_params, # ty:ignore[invalid-argument-type] ) auth.py ======= .. code-block:: python from __future__ import annotations from collections.abc import AsyncGenerator, Generator import httpx from .exceptions import InvalidAPIKeyError class CRAuth(httpx.Auth): """ Clash Royale Auth for httpx. Attaches the API key to the Authorization header of each request. Supports both sync and async httpx clients. :params api_key: Your Clash Royale API key. :raises ValueError: If the API key is empty or None. """ def __init__(self, api_key: str): if not api_key or (isinstance(api_key, str) and not api_key.strip()): raise InvalidAPIKeyError() self.api_key = api_key def auth_flow( self, request: httpx.Request ) -> Generator[httpx.Request, httpx.Response, None]: """Sync auth flow for httpx.Client.""" request.headers["Authorization"] = f"Bearer {self.api_key}" yield request async def async_auth_flow( self, request: httpx.Request ) -> AsyncGenerator[httpx.Request, httpx.Response]: """Async auth flow for httpx.AsyncClient.""" request.headers["Authorization"] = f"Bearer {self.api_key}" yield request client.py ========= .. code-block:: python from __future__ import annotations import httpx from .auth import CRAuth from .exceptions import ClashRoyaleHTTPError from .resources import ( Cards, Clans, GlobalTournaments, Leaderboards, Locations, Players, Tournaments, ) CURRENT_VERSION = "v1" BASE_URL = f"https://api.clashroyale.com/{CURRENT_VERSION}" class Client(httpx.Client): """ A client to retrieve data from the Clash Royale API. Create a client instance with the given options. >>> import clash_royale >>> client = clash_royale.Client(api_key="your_api_key_here") This client provides several methods to retrieve the content most kinds of Clash Royale objects, based on their json structure. If your server does not have a static IP address, you can use a proxy server by setting the `proxy` parameter. >>> client = clash_royale.Client( api_key="your_api_key_here", proxy="https://my.proxy.com", ) For more detail about proxy usage, check the usage section of the documentation. :param api_key: The Clash Royale API key. :param proxy: Proxy URL (default: None). :param timeout: Request timeout in seconds (default: 10.0). :param base_url: Base URL for the API (default: https://api.clashroyale.com/v1). """ players: Players clans: Clans cards: Cards tournaments: Tournaments leaderboards: Leaderboards locations: Locations global_tournaments: GlobalTournaments _auth: CRAuth def __init__( self, api_key: str, proxy: str | None = None, timeout: float = 10.0, base_url: str = BASE_URL, ): self._auth = CRAuth(api_key) if proxy: # Ensure proxy URL has the version suffix if not proxy.endswith(CURRENT_VERSION): base_url = proxy.rstrip("/") + "/" + CURRENT_VERSION else: base_url = proxy super().__init__( base_url=base_url, auth=self._auth, timeout=timeout, ) self.players = Players(self) self.clans = Clans(self) self.cards = Cards(self) self.tournaments = Tournaments(self) self.leaderboards = Leaderboards(self) self.locations = Locations(self) self.global_tournaments = GlobalTournaments(self) def _request( self, method: str, endpoint: str, params: dict[str, str | int] | None = None, ) -> dict: """Make an HTTP request to the API. :param method: HTTP method (GET, POST, etc.). :param endpoint: API endpoint (e.g., /players/{tag}). :param params: Query parameters. :returns: The JSON response as a dictionary. :raises ClashRoyaleNotFoundError: If resource is not found (404). :raises ClashRoyaleUnauthorizedError: If API key is invalid (403). :raises ClashRoyaleRateLimitError: If rate limit is exceeded (429). :raises ClashRoyaleBadRequestError: If request is malformed (400). :raises ClashRoyaleServerError: If server error occurs (5xx). :raises ClashRoyaleHTTPError: For other API errors. """ # Convert snake_case parameter names to camelCase for API compatibility if params: params = self._convert_params_to_camel_case(params) try: response = super().request(method, endpoint, params=params) response.raise_for_status() return response.json() except httpx.HTTPStatusError as exc: raise ClashRoyaleHTTPError.from_http_error(exc) from exc def _convert_params_to_camel_case( self, params: dict[str, str | int] ) -> dict[str, str | int]: """Convert snake_case parameter keys to camelCase for API. :param params: Dictionary with snake_case keys. :returns: Dictionary with camelCase keys. """ converted = {} for key, value in params.items(): # Convert snake_case to camelCase if "_" in key: parts = key.split("_") camel_key = parts[0] + "".join(word.capitalize() for word in parts[1:]) converted[camel_key] = value else: converted[key] = value return converted exceptions.py ============= .. code-block:: python from __future__ import annotations import httpx class ClashRoyaleException(Exception): """Base exception for all library errors.""" class ClashRoyaleHTTPError(ClashRoyaleException): """API returned an error response.""" def __init__(self, http_exception: httpx.HTTPStatusError, *args: object) -> None: if http_exception.response is not None and http_exception.response.text: url = http_exception.response.request.url status_code = http_exception.response.status_code text = http_exception.response.text super().__init__(status_code, url, text, *args) else: super().__init__(http_exception, *args) @classmethod def from_http_error(cls, exc: httpx.HTTPStatusError) -> ClashRoyaleHTTPError: """Initialize the appropriate internal exception from a HTTPError.""" if exc.response is not None: if exc.response.status_code in {500, 503}: return ClashRoyaleServerError(exc) if exc.response.status_code == 403: return ClashRoyaleUnauthorizedError(exc) if exc.response.status_code == 404: return ClashRoyaleNotFoundError(exc) if exc.response.status_code == 429: return ClashRoyaleRateLimitError(exc) if exc.response.status_code == 400: return ClashRoyaleBadRequestError(exc) return ClashRoyaleHTTPError(exc) class ClashRoyaleNotFoundError(ClashRoyaleHTTPError): """Resource was not found (404).""" class ClashRoyaleUnauthorizedError(ClashRoyaleHTTPError): """Access denied, either because of missing/incorrect credentials or used API token does not grant access to the requested resource (403). """ class ClashRoyaleRateLimitError(ClashRoyaleHTTPError): """Request was throttled, because amount of requests was above the threshold defined for the used API token (429). """ class ClashRoyaleBadRequestError(ClashRoyaleHTTPError): """Client provided incorrect parameters for the request (400).""" class ClashRoyaleServerError(ClashRoyaleHTTPError): """Server error (5xx).""" class InvalidAPIKeyError(ClashRoyaleException): """Invalid or missing API key.""" def __init__( self, message: str = "API key is required. Please provide a valid Clash Royale API key. You can get one from https://developer.clashroyale.com/#/account/", ): super().__init__(message) __init__.py =========== .. code-block:: python """Pydantic models for Clash Royale API responses.""" from __future__ import annotations from .card import Card from .clan import Clan, ClanMember, ClanSearchResult, RiverRace, RiverRaceLog from .common import Arena, Badge, Icon from .global_tournament import GlobalTournament from .leaderboard import Leaderboard, LeaderboardPlayer from .location import ( ClanRanking, LadderTournamentRanking, LeagueSeason, LeagueSeasonV2, Location, PlayerPathOfLegendRanking, PlayerRanking, PlayerSeasonRanking, ) from .player import Battle, Player, UpcomingChest from .tournament import Tournament, TournamentHeader, TournamentMember __all__ = [ "Arena", "Badge", "Battle", "Card", "Clan", "ClanMember", "ClanRanking", "ClanSearchResult", "GlobalTournament", "Icon", "LadderTournamentRanking", "Leaderboard", "LeaderboardPlayer", "LeagueSeason", "LeagueSeasonV2", "Location", "Player", "PlayerPathOfLegendRanking", "PlayerRanking", "PlayerSeasonRanking", "RiverRace", "RiverRaceLog", "Tournament", "TournamentHeader", "TournamentMember", "UpcomingChest", ] base.py ======= .. code-block:: python from __future__ import annotations from datetime import datetime from typing import Annotated from pydantic import BaseModel, BeforeValidator, ConfigDict def parse_iso8601_datetime(v: str | datetime) -> datetime: """Parse ISO 8601 datetime string to datetime object. Handles format like: "20260109T222745.000Z" """ if isinstance(v, datetime): return v # Parse ISO 8601 format with Z timezone return datetime.fromisoformat(v.replace("Z", "+00:00")) ISO8601DateTime = Annotated[datetime, BeforeValidator(parse_iso8601_datetime)] """Type annotation for ISO 8601 datetime fields.""" class CRBaseModel(BaseModel): """Base model for all Clash Royale API models.""" model_config = ConfigDict( populate_by_name=True, extra="ignore", ) card.py ======= .. code-block:: python from __future__ import annotations from pydantic import Field from typing_extensions import Literal from .base import CRBaseModel from .common import Icon class Card(CRBaseModel): """Represents a card in Clash Royale.""" name: str id: int max_level: int = Field(alias="maxLevel") max_evolution_level: int | None = Field(default=None, alias="maxEvolutionLevel") elixir_cost: int | None = Field( default=None, alias="elixirCost" ) # elxir_cost may be `None` due to the "mirror" card icon_urls: Icon = Field(alias="iconUrls") rarity: Literal["common", "rare", "epic", "legendary", "champion"] class SupportCard(CRBaseModel): """Represents a support card in Clash Royale with additional attributes.""" name: str id: int level: int max_level: int = Field(alias="maxLevel") icon_urls: Icon = Field(alias="iconUrls") rarity: Literal["common", "rare", "epic", "legendary", "champion"] clan.py ======= .. code-block:: python from __future__ import annotations from pydantic import Field from typing_extensions import Literal from .base import CRBaseModel from .common import Arena, Badge from .location import Location class ClanMember(CRBaseModel): """Represents a member of a clan.""" tag: str name: str role: Literal["leader", "coLeader", "elder", "admin", "member", "notMember"] exp_level: int = Field(alias="expLevel") trophies: int arena: Arena clan_rank: int = Field(alias="clanRank") previous_clan_rank: int = Field(alias="previousClanRank") donations: int donations_received: int = Field(alias="donationsReceived") clan_chest_points: int = Field(alias="clanChestPoints") class Clan(CRBaseModel): """Represents a Clash Royale clan.""" tag: str name: str type: Literal["open", "inviteOnly", "closed"] description: str badge_id: int = Field(alias="badgeId") badge_urls: Badge | None = Field(default=None, alias="badgeUrls") clan_score: int = Field(alias="clanScore") clan_war_trophies: int = Field(alias="clanWarTrophies") location: Location required_trophies: int = Field(alias="requiredTrophies") donations_per_week: int = Field(alias="donationsPerWeek") clan_chest_status: Literal["inactive", "active", "completed", "unknown"] = Field( alias="clanChestStatus" ) clan_chest_level: int = Field(alias="clanChestLevel") clan_chest_max_level: int = Field(alias="clanChestMaxLevel") members: int member_list: list[ClanMember] = Field(default_factory=list, alias="memberList") class ClanSearchResult(CRBaseModel): """Represents a clan in search results.""" tag: str name: str type: Literal["open", "inviteOnly", "closed"] badge_id: int = Field(alias="badgeId") badge_urls: Badge | None = Field(default=None, alias="badgeUrls") clan_score: int = Field(alias="clanScore") clan_war_trophies: int = Field(alias="clanWarTrophies") location: Location required_trophies: int = Field(alias="requiredTrophies") donations_per_week: int = Field(alias="donationsPerWeek") members: int class RiverRaceParticipant(CRBaseModel): """Represents a participant in a river race.""" tag: str name: str fame: int repair_points: int = Field(alias="repairPoints") boat_attacks: int = Field(alias="boatAttacks") decks_used: int = Field(alias="decksUsed") decks_used_today: int = Field(alias="decksUsedToday") class RiverRaceClan(CRBaseModel): """Represents a clan participating in a river race.""" tag: str name: str badge_id: int = Field(alias="badgeId") clan_score: int = Field(alias="clanScore") fame: int repair_points: int = Field(alias="repairPoints") finish_time: str | None = Field(default=None, alias="finishTime") period_points: int = Field(alias="periodPoints") participants: list[RiverRaceParticipant] = Field(default_factory=list) class RiverRaceLogStanding(CRBaseModel): """Represents a clan standing in a river race log.""" rank: int trophy_change: int = Field(alias="trophyChange") clan: RiverRaceClan class RiverRace(CRBaseModel): """Represents a river race.""" state: Literal[ "clanNotFound", "accessDenied", "matchmaking", "matched", "full", "ended" ] clan: RiverRaceClan clans: list[RiverRaceClan] = Field(default_factory=list) collection_end_time: str | None = Field(default=None, alias="collectionEndTime") war_end_time: str | None = Field(default=None, alias="warEndTime") section_index: int = Field(alias="sectionIndex") period_index: int = Field(alias="periodIndex") period_type: Literal["training", "warDay", "colosseum"] = Field(alias="periodType") period_logs: list[PeriodLog] = Field(default_factory=list, alias="periodLogs") class RiverRaceLog(CRBaseModel): """Represents a river race log entry.""" season_id: int = Field(alias="seasonId") section_index: int = Field(alias="sectionIndex") created_date: str = Field(alias="createdDate") standings: list[RiverRaceLogStanding] = Field(default_factory=list) class PeriodLog(CRBaseModel): """Represents a period log.""" period_index: int = Field(alias="periodIndex") items: list[PeriodLogEntry] = Field(default_factory=list) class PeriodLogEntry(CRBaseModel): """Represents a period log entry.""" clan: PeriodLogEntryClan points_earned: int = Field(alias="pointsEarned") progress_start_of_day: int = Field(alias="progressStartOfDay") progress_end_of_day: int = Field(alias="progressEndOfDay") end_of_day_rank: int = Field(alias="endOfDayRank") progress_earned: int = Field(alias="progressEarned") num_of_defenses_remaining: int = Field(alias="numOfDefensesRemaining") progress_earned_from_defenses: int = Field(alias="progressEarnedFromDefenses") class PeriodLogEntryClan(CRBaseModel): """Represents a clan in a period log entry.""" tag: str common.py ========= .. code-block:: python from __future__ import annotations from pydantic import Field from .base import CRBaseModel class Icon(CRBaseModel): """Icon URLs for various game elements.""" medium: str evolution_medium: str | None = Field(default=None, alias="evolutionMedium") hero_medium: str | None = Field(default=None, alias="heroMedium") class Arena(CRBaseModel): """Arena information.""" id: int name: str raw_name: str | None = Field(default=None, alias="rawName") class GameMode(CRBaseModel): """Represents a game mode in Clash Royale.""" id: int name: str | None = None class PlayerClan(CRBaseModel): """Basic clan information for a player.""" tag: str name: str badge_id: int | None = Field(default=None, alias="badgeId") class Badge(CRBaseModel): """Clan badge information.""" url: str global_tournament.py ==================== .. code-block:: python from __future__ import annotations from pydantic import Field from typing_extensions import Literal from .base import CRBaseModel from .common import GameMode class GlobalTournament(CRBaseModel): """Represents a global tournament.""" tag: str title: str start_time: str = Field(alias="startTime") end_time: str = Field(alias="endTime") max_losses: int = Field(alias="maxLosses") min_exp_level: int = Field(alias="minExpLevel") tournament_level: int = Field(alias="tournamentLevel") milestones: list[SurvivalMilestoneReward] = Field(default_factory=list) free_tier_rewards: list[SurvivalMilestoneReward] | None = Field( default=None, alias="freeTierRewards" ) top_rank_reward: list[SurvivalMilestoneReward] | None = Field( default=None, alias="topRankReward" ) game_mode: GameMode | None = Field(default=None, alias="gameMode") max_top_reward_rank: int | None = Field(default=None, alias="maxTopRewardRank") class SurvivalMilestoneReward(CRBaseModel): """Represents a survival milestone reward in Clash Royale.""" rarity: Literal["common", "rare", "epic", "legendary", "champion"] chest: str resources: Literal["gold", "unknown"] type: Literal[ "none", "card_stack", "chest", "card_stack_random", "resource", "trade_token", "consumable", ] amount: int card: SurvivalMilestoneRewardCard | None = None consumable_name: str | None = Field(default=None, alias="consumableName") wins: int class SurvivalMilestoneRewardCard(CRBaseModel): """Represents a card in a survival milestone reward.""" id: int name: str rarity: Literal["common", "rare", "epic", "legendary", "champion"] max_level: int = Field(alias="maxLevel") elixir_cost: int = Field(alias="elixirCost") max_evolution_level: int | None = Field(default=None, alias="maxEvolutionLevel") leaderboard.py ============== .. code-block:: python from __future__ import annotations from .base import CRBaseModel from .player import PlayerClan class Leaderboard(CRBaseModel): """Represents a leaderboard.""" id: int name: str class LeaderboardPlayer(CRBaseModel): """Represents a player in a leaderboard.""" tag: str name: str rank: int score: int clan: PlayerClan | None = None location.py =========== .. code-block:: python from __future__ import annotations from pydantic import Field from clash_royale.models.common import Arena, Badge from .base import CRBaseModel class Location(CRBaseModel): """Represents a location.""" id: int name: str is_country: bool = Field(alias="isCountry") country_code: str | None = Field(default=None, alias="countryCode") class ClanRanking(CRBaseModel): """Represents a clan ranking.""" clan_score: int = Field(alias="clanScore") badge_id: int = Field(alias="badgeId") location: Location members: int tag: str name: str rank: int previous_rank: int = Field(alias="previousRank") badge_urls: Badge | None = Field(default=None, alias="badgeUrls") class PlayerRanking(CRBaseModel): """Represents a player ranking.""" trophies: int clan: PlayerRankingClan | None = None tag: str name: str rank: int previous_rank: int = Field(alias="previousRank") badge_urls: Badge | None = Field(default=None, alias="badgeUrls") class PlayerRankingClan(CRBaseModel): """Represents a player's clan in rankings.""" tag: str name: str badge_id: int = Field(alias="badgeId") badge_urls: Badge | None = Field(default=None, alias="badgeUrls") class PlayerPathOfLegendRanking(CRBaseModel): """Represents a player ranking in Path of Legends.""" tag: str name: str exp_level: int = Field(alias="expLevel") elo_rating: int = Field(alias="eloRating") rank: int clan: PlayerRankingClan | None = None class PlayerSeasonRanking(CRBaseModel): """Represents a player ranking for a season.""" tag: str name: str exp_level: int = Field(alias="expLevel") trophies: int rank: int previous_rank: int | None = Field(default=None, alias="previousRank") clan: PlayerRankingClan | None = None arena: Arena | None = None class LeagueSeason(CRBaseModel): """Represents a league season. .. note:: The API may return null values due to a known bug. """ id: str | None = None class LeagueSeasonV2(CRBaseModel): """Represents a league season with end dates. .. note:: The API may return null/missing values due to a known bug. """ code: str | None = None unique_id: str | None = Field(default=None, alias="uniqueId") end_time: str | None = Field(default=None, alias="endTime") class LadderTournamentRanking(CRBaseModel): """Represents a ladder tournament ranking.""" clan: PlayerRankingClan | None = None wins: int losses: int tag: str name: str rank: int previous_rank: int = Field(alias="previousRank") player.py ========= .. code-block:: python from __future__ import annotations from typing import Literal from pydantic import Field from .base import CRBaseModel, ISO8601DateTime from .card import Card, SupportCard from .common import Arena, GameMode, PlayerClan class PlayerCard(Card): """Represents a card in a player's collection.""" level: int star_level: int | None = Field(default=None, alias="starLevel") evolution_level: int | None = Field(default=None, alias="evolutionLevel") class LeagueStatistics(CRBaseModel): """Player's league statistics.""" current_season: dict | None = Field(default=None, alias="currentSeason") previous_season: dict | None = Field(default=None, alias="previousSeason") best_season: dict | None = Field(default=None, alias="bestSeason") class Player(CRBaseModel): """Represents a Clash Royale player.""" tag: str name: str exp_level: int = Field(alias="expLevel") trophies: int best_trophies: int = Field(alias="bestTrophies") wins: int losses: int battle_count: int = Field(alias="battleCount") three_crown_wins: int = Field(alias="threeCrownWins") challenge_cards_won: int = Field(alias="challengeCardsWon") challenge_max_wins: int = Field(alias="challengeMaxWins") tournament_cards_won: int = Field(alias="tournamentCardsWon") tournament_battle_count: int = Field(alias="tournamentBattleCount") role: str | None = None donations: int | None = None donations_received: int | None = Field(default=None, alias="donationsReceived") total_donations: int = Field(alias="totalDonations") clan: PlayerClan | None = None arena: Arena | None = None league_statistics: LeagueStatistics | None = Field( default=None, alias="leagueStatistics" ) cards: list[Card] = [] current_deck: list[PlayerCard] = Field(default_factory=list, alias="currentDeck") current_favourite_card: Card | None = Field( default=None, alias="currentFavouriteCard" ) star_points: int | None = Field(default=None, alias="starPoints") exp_points: int | None = Field(default=None, alias="expPoints") class BattlePlayer(CRBaseModel): """Player information in a battle.""" tag: str name: str crowns: int king_tower_hit_points: int | None = Field(default=None, alias="kingTowerHitPoints") princess_towers_hit_points: list[int] | None = Field( default=None, alias="princessTowersHitPoints" ) cards: list[Card] = [] support_cards: list[SupportCard] | None = Field(default=None, alias="supportCards") starting_trophies: int | None = Field(default=None, alias="startingTrophies") trophy_change: int | None = Field(default=None, alias="trophyChange") clan: PlayerClan | None = None global_rank: int | None = Field(default=None, alias="globalRank") elixir_leaked: float | None = Field(default=None, alias="elixirLeaked") class BattleTeam(CRBaseModel): """Team information in a battle.""" crowns: int elixir_leaked: float | None = Field(default=None, alias="elixirLeaked") class Battle(CRBaseModel): """Represents a battle in a player's battle log.""" type: Literal[ "PvP", "PvE", "clanMate", "tournament", "friendly", "survival", "PvP2v2", "clanMate2v2", "challenge2v2", "clanWarCollectionDay", "clanWarWarDay", "casual1v1", "casual2v2", "boatBattle", "boatBattlePractice", "riverRacePvP", "riverRacePvp", "riverRaceDuel", "riverRaceDuelColosseum", "tutorial", "pathOfLegend", "seasonalBattle", "practice", "trail", "unknown", ] battle_time: ISO8601DateTime = Field(alias="battleTime") is_ladder_tournament: bool = Field(alias="isLadderTournament") arena: Arena game_mode: GameMode = Field(alias="gameMode") deck_selection: ( Literal[ "collection", "draft", "draftCompetitive", "predefined", "eventDeck", "pick", "wardeckPick", "warDeckPick", "quaddeckPick", "unknown", ] | None ) = Field(default=None, alias="deckSelection") team: list[BattlePlayer] = [] opponent: list[BattlePlayer] = [] is_hosted_match: bool = Field(alias="isHostedMatch") league_number: int = Field(alias="leagueNumber") class UpcomingChest(CRBaseModel): """Represents an upcoming chest for a player.""" index: int name: str tournament.py ============= .. code-block:: python from __future__ import annotations from pydantic import Field from typing_extensions import Literal from clash_royale.models.common import PlayerClan from .base import CRBaseModel, ISO8601DateTime class TournamentMember(CRBaseModel): """Represents a member in a tournament.""" tag: str name: str score: int rank: int clan: PlayerClan | None = None class TournamentHeader(CRBaseModel): """Represents a Clash Royale tournament header.""" tag: str type: Literal["open", "passwordProtected", "unknown"] status: Literal["inPreparation", "inProgress", "ended", "unknown"] creator_tag: str = Field(alias="creatorTag") name: str description: str | None = None capacity: int max_capacity: int = Field(alias="maxCapacity") preparation_duration: int = Field(alias="preparationDuration") duration: int created_time: ISO8601DateTime = Field(alias="createdTime") first_place_card_prize: int = Field(alias="firstPlaceCardPrize") level_cap: int = Field(alias="levelCap") class Tournament(TournamentHeader): """Represents a Clash Royale tournament.""" started_time: ISO8601DateTime | None = Field(default=None, alias="startedTime") ended_time: ISO8601DateTime | None = Field(default=None, alias="endedTime") members_list: list[TournamentMember] = Field( default_factory=list, alias="membersList" ) pagination.py ============= .. code-block:: python from __future__ import annotations from collections.abc import Generator from typing import TYPE_CHECKING, Generic, TypeVar, overload from .models.base import CRBaseModel from .types import ClanSearchParams, PaginationParams if TYPE_CHECKING: from .client import Client ResourceType = TypeVar("ResourceType", bound="CRBaseModel") class PaginatedList(Generic[ResourceType]): """Lazy-loading paginated list that fetches pages on demand.""" def __init__( self, client: Client, endpoint: str, model: type[ResourceType], params: PaginationParams | ClanSearchParams | None = None, ): self._client = client self._endpoint = endpoint self._model = model # Extract client-side pagination control params _params = params.copy() if params else {} self._limit: int | None = _params.pop("limit", None) self._page_size: int | None = _params.pop("page_size", None) # Remaining params are for the API (after, before, etc.) self._params: dict[str, str | int] = _params self._elements: list[ResourceType] = [] self._after_cursor: str | None = None self._has_more: bool = True self._iter = iter(self) def __repr__(self) -> str: repr_size = 5 data: list[ResourceType | str] = list(self[: repr_size + 1]) if len(data) > repr_size: data[-1] = "..." return f"<{self.__class__.__name__} {data!r}>" @overload def __getitem__(self, index: int) -> ResourceType: ... @overload def __getitem__(self, index: slice) -> list[ResourceType]: ... def __getitem__(self, index: int | slice) -> ResourceType | list[ResourceType]: if isinstance(index, int): self._fetch_to_index(index) return self._elements[index] if index.stop is not None: self._fetch_to_index(index.stop) else: self._fetch_all() return self._elements[index] def __iter__(self) -> Generator[ResourceType, None, None]: yield from self._elements while self._has_more and ( self._limit is None or len(self._elements) < self._limit ): yield from self._grow() def __next__(self) -> ResourceType: return next(self._iter) def _could_grow(self) -> bool: if self._limit is not None and len(self._elements) >= self._limit: return False return self._has_more def _grow(self) -> list[ResourceType]: new_elements = self._fetch_next_page() # If limit is set, only add elements up to the limit if self._limit is not None: remaining = self._limit - len(self._elements) if remaining <= 0: return [] new_elements = new_elements[:remaining] self._elements.extend(new_elements) return new_elements def _fetch_next_page(self) -> list[ResourceType]: params = self._params.copy() if self._after_cursor: params["after"] = self._after_cursor if self._page_size: # Send page_size as "limit" to the API params["limit"] = self._page_size response = self._client._request("GET", self._endpoint, params=params) # Update cursor for next page paging = response.get("paging", {}) cursors = paging.get("cursors", {}) self._after_cursor = cursors.get("after") self._has_more = self._after_cursor is not None # Parse items with model items = response.get("items", []) return [self._model.model_validate(item) for item in items] def _fetch_to_index(self, index: int) -> None: while len(self._elements) <= index and self._could_grow(): self._grow() def _fetch_all(self) -> None: while self._could_grow(): self._grow() __init__.py =========== .. code-block:: python from __future__ import annotations from .cards import Cards from .clans import Clans from .global_tournaments import GlobalTournaments from .leaderboards import Leaderboards from .locations import Locations from .players import Players from .tournaments import Tournaments __all__ = [ "Cards", "Clans", "GlobalTournaments", "Leaderboards", "Locations", "Players", "Tournaments", ] cards.py ======== .. code-block:: python from __future__ import annotations from typing_extensions import Unpack from ..models.card import Card from ..pagination import PaginatedList from ..types import PaginationParams from .resource import Resource class Cards(Resource): """ Resource for card-related endpoints. Check the :clash-royale-api:`cards` for more detailed information about each endpoint. """ def list(self, **params: Unpack[PaginationParams]) -> PaginatedList[Card]: """List all available cards.""" return PaginatedList( client=self._client, endpoint="/cards", model=Card, params=params, # ty:ignore[invalid-argument-type] ) clans.py ======== .. code-block:: python from __future__ import annotations from typing_extensions import Unpack from ..models.clan import Clan, ClanMember, ClanSearchResult, RiverRace, RiverRaceLog from ..pagination import PaginatedList from ..types import ClanSearchParams, PaginationParams from .resource import Resource class Clans(Resource): """ Resource for clan-related endpoints. Check the :clash-royale-api:`clans` for more detailed information about each endpoint. """ def get(self, tag: str) -> Clan: """Get clan information by tag.""" encoded_tag = self._encode_tag(tag) response = self._client._request("GET", f"/clans/{encoded_tag}") return Clan.model_validate(response) def search( self, name: str, **params: Unpack[ClanSearchParams] ) -> PaginatedList[ClanSearchResult]: """Search clans by name.""" api_params = {"name": name, **params} return PaginatedList( client=self._client, endpoint="/clans", model=ClanSearchResult, params=api_params, # ty:ignore[invalid-argument-type] ) def get_members( self, tag: str, **params: Unpack[PaginationParams] ) -> PaginatedList[ClanMember]: """Get clan members.""" encoded_tag = self._encode_tag(tag) return PaginatedList( client=self._client, endpoint=f"/clans/{encoded_tag}/members", model=ClanMember, params=params, # ty:ignore[invalid-argument-type] ) def get_current_river_race(self, tag: str) -> RiverRace: """Get current river race information for a clan.""" encoded_tag = self._encode_tag(tag) response = self._client._request( "GET", f"/clans/{encoded_tag}/currentriverrace" ) return RiverRace.model_validate(response) def get_river_race_log( self, tag: str, **params: Unpack[PaginationParams] ) -> PaginatedList[RiverRaceLog]: """Get river race log for a clan.""" encoded_tag = self._encode_tag(tag) return PaginatedList( client=self._client, endpoint=f"/clans/{encoded_tag}/riverracelog", model=RiverRaceLog, params=params, # ty:ignore[invalid-argument-type] ) global_tournaments.py ===================== .. code-block:: python from __future__ import annotations from typing_extensions import Unpack from ..models.global_tournament import GlobalTournament from ..pagination import PaginatedList from ..types import PaginationParams from .resource import Resource class GlobalTournaments(Resource): """ Resource for global tournament related endpoints. Check the :clash-royale-api:`globaltournaments` for more detailed information about each endpoint. """ def list( self, **params: Unpack[PaginationParams] ) -> PaginatedList[GlobalTournament]: """Get list of global tournaments.""" return PaginatedList( client=self._client, endpoint="/globaltournaments", model=GlobalTournament, params=params, # ty:ignore[invalid-argument-type] ) leaderboards.py =============== .. code-block:: python from __future__ import annotations from typing_extensions import Unpack from ..models.leaderboard import Leaderboard, LeaderboardPlayer from ..pagination import PaginatedList from ..types import PaginationParams from .resource import Resource class Leaderboards(Resource): """ Resource for leaderboard-related endpoints. Check the :clash-royale-api:`leaderboards` for more detailed information about each endpoint. """ def list(self) -> list[Leaderboard]: # ty:ignore[invalid-type-form] """List all available leaderboards.""" response = self._client._request("GET", "/leaderboards") items = response.get("items", []) return [Leaderboard.model_validate(item) for item in items] def get( self, leaderboard_id: int, **params: Unpack[PaginationParams] ) -> PaginatedList[LeaderboardPlayer]: """Get player rankings for a specific leaderboard. :param leaderboard_id: The leaderboard ID from the leaderboards list. """ return PaginatedList( client=self._client, endpoint=f"/leaderboard/{leaderboard_id}", model=LeaderboardPlayer, params=params, # ty:ignore[invalid-argument-type] ) locations.py ============ .. code-block:: python from __future__ import annotations import warnings from typing_extensions import Unpack from ..models.location import ( ClanRanking, LadderTournamentRanking, LeagueSeason, LeagueSeasonV2, Location, PlayerPathOfLegendRanking, PlayerRanking, PlayerSeasonRanking, ) from ..pagination import PaginatedList from ..types import PaginationParams from .resource import Resource class Locations(Resource): """ Resource for location-related endpoints. Check the :clash-royale-api:`locations` for more detailed information about each endpoint. """ def list(self, **params: Unpack[PaginationParams]) -> PaginatedList[Location]: """List all available locations.""" return PaginatedList( client=self._client, endpoint="/locations", model=Location, params=params, # ty:ignore[invalid-argument-type] ) def get(self, location_id: int) -> Location: """Get location information by ID. :param location_id: The location ID from the locations list. """ response = self._client._request("GET", f"/locations/{location_id}") return Location.model_validate(response) def get_clan_rankings( self, location_id: int, **params: Unpack[PaginationParams], ) -> PaginatedList[ClanRanking]: """Get clan rankings for a specific location. :param location_id: The location ID from the locations list. """ return PaginatedList( client=self._client, endpoint=f"/locations/{location_id}/rankings/clans", model=ClanRanking, params=params, # ty:ignore[invalid-argument-type] ) def get_player_rankings( self, location_id: int, **params: Unpack[PaginationParams], ) -> PaginatedList[PlayerRanking]: """Get player rankings for a specific location. :param location_id: The location ID from the locations list. """ return PaginatedList( client=self._client, endpoint=f"/locations/{location_id}/rankings/players", model=PlayerRanking, params=params, # ty:ignore[invalid-argument-type] ) def get_clan_war_rankings( self, location_id: int, **params: Unpack[PaginationParams], ) -> PaginatedList[ClanRanking]: """Get clan war rankings for a specific location. :param location_id: The location ID from the locations list. """ return PaginatedList( client=self._client, endpoint=f"/locations/{location_id}/rankings/clanwars", model=ClanRanking, params=params, # ty:ignore[invalid-argument-type] ) def get_path_of_legend_player_rankings( self, location_id: int, **params: Unpack[PaginationParams], ) -> PaginatedList[PlayerPathOfLegendRanking]: """Get player rankings in Path of Legend for a specific location. :param location_id: The location ID from the locations list. """ return PaginatedList( client=self._client, endpoint=f"/locations/{location_id}/pathoflegend/players", model=PlayerPathOfLegendRanking, params=params, # ty:ignore[invalid-argument-type] ) def get_path_of_legend_season_rankings( self, season_id: str, **params: Unpack[PaginationParams], ) -> PaginatedList[PlayerPathOfLegendRanking]: """Get top Path of Legend players for a given season. :param season_id: The season ID (e.g., "2024-01"). """ return PaginatedList( client=self._client, endpoint=f"/locations/global/pathoflegend/{season_id}/rankings/players", model=PlayerPathOfLegendRanking, params=params, # ty:ignore[invalid-argument-type] ) def get_season(self, season_id: str) -> LeagueSeason | LeagueSeasonV2: """Get top player league season. This method automatically selects the appropriate API endpoint based on the ``season_id`` format: - **Numeric ID** (e.g., ``"1"``, ``"2"``): Uses the V2 endpoint and returns :class:`~clash_royale.models.leaderboard.LeagueSeasonV2`. - **Date-based code** (e.g., ``"2016-02"``, ``"2025-07"``): Uses the legacy endpoint and returns :class:`~clash_royale.models.leaderboard.LeagueSeason`. .. warning:: The Clash Royale API may occasionally return incomplete season data with null values. This is a known API issue, not a library bug. :param season_id: Identifier of the season. Can be either a numeric unique ID (e.g., ``"1"``) or a date-based code (e.g., ``"2016-02"``). :return: :class:`~clash_royale.models.leaderboard.LeagueSeasonV2` if ``season_id`` is numeric, otherwise :class:`~clash_royale.models.leaderboard.LeagueSeason`. """ warnings.warn( "The API may return incomplete season data with null values.", UserWarning, stacklevel=2, ) if season_id.isdigit(): response = self._client._request( "GET", f"/locations/global/seasons/{season_id}" ) return LeagueSeasonV2.model_validate(response) response = self._client._request( "GET", f"/locations/global/seasons/{season_id}" ) return LeagueSeason.model_validate(response) def get_season_player_rankings( self, season_id: str, **params: Unpack[PaginationParams], ) -> PaginatedList[PlayerSeasonRanking]: """Get top player rankings for a season. :param season_id: The season ID (e.g., "2017-03"). """ return PaginatedList( client=self._client, endpoint=f"/locations/global/seasons/{season_id}/rankings/players", model=PlayerSeasonRanking, params=params, # ty:ignore[invalid-argument-type] ) def list_seasons(self) -> PaginatedList[LeagueSeason]: """List top player league seasons. .. warning:: The Clash Royale API may occasionally return incomplete season data with null values. This is a known API issue, not a library bug. Consider using :meth:`list_seasons_v2` instead. """ warnings.warn( "The API may return incomplete season data with null values.", UserWarning, stacklevel=2, ) return PaginatedList( client=self._client, endpoint="/locations/global/seasons", model=LeagueSeason, params={}, ) def list_seasons_v2(self) -> PaginatedList[LeagueSeasonV2]: """List league seasons with more details. .. warning:: The Clash Royale API may occasionally return incomplete season data with null values. This is a known API issue, not a library bug. """ warnings.warn( "The API may return incomplete season data with null values.", UserWarning, stacklevel=2, ) return PaginatedList( client=self._client, endpoint="/locations/global/seasonsV2", model=LeagueSeasonV2, params={}, ) def get_global_tournament_rankings( self, tournament_tag: str, **params: Unpack[PaginationParams], ) -> PaginatedList[LadderTournamentRanking]: """Get global tournament rankings.""" encoded_tag = self._encode_tag(tournament_tag) return PaginatedList( client=self._client, endpoint=f"/locations/global/rankings/tournaments/{encoded_tag}", model=LadderTournamentRanking, params=params, # ty:ignore[invalid-argument-type] ) players.py ========== .. code-block:: python from __future__ import annotations from ..models.player import Battle, Player, UpcomingChest from .resource import Resource class Players(Resource): """ Resource for player-related endpoints. Check the :clash-royale-api:`players` for more detailed information about each endpoint. """ def get(self, tag: str) -> Player: """Get player information by tag.""" encoded_tag = self._encode_tag(tag) response = self._client._request("GET", f"/players/{encoded_tag}") return Player.model_validate(response) def get_battlelog(self, tag: str) -> list[Battle]: """Get player's battle log.""" encoded_tag = self._encode_tag(tag) response = self._client._request("GET", f"/players/{encoded_tag}/battlelog") return [Battle.model_validate(battle) for battle in response] def get_upcoming_chests(self, tag: str) -> list[UpcomingChest]: """Get player's upcoming chests.""" encoded_tag = self._encode_tag(tag) response = self._client._request( "GET", f"/players/{encoded_tag}/upcomingchests" ) return [ UpcomingChest.model_validate(upcoming_chest) for upcoming_chest in response["items"] ] resource.py =========== .. code-block:: python from __future__ import annotations from typing import TYPE_CHECKING from urllib.parse import quote from ..types import ClanSearchParams, PaginationParams if TYPE_CHECKING: from ..client import Client # Re-export for backward compatibility __all__ = ["Resource", "PaginationParams", "ClanSearchParams"] class Resource: """Base class for all API resources.""" def __init__(self, client: Client): """Initialize the resource with a client.""" self._client = client def _encode_tag(self, tag: str) -> str: """Encode a Clash Royale tag for use in URLs.""" tag = self._normalize_tag(tag) return quote(tag, safe="") def _normalize_tag(self, tag: str) -> str: """Normalize a Clash Royale tag by ensuring it starts with '#' and is uppercase.""" if not tag.startswith("#"): tag = f"#{tag}" return tag.upper() tournaments.py ============== .. code-block:: python from __future__ import annotations from typing_extensions import Unpack from clash_royale.models import TournamentHeader from ..models.tournament import Tournament from ..pagination import PaginatedList from ..types import PaginationParams from .resource import Resource class Tournaments(Resource): """ Resource for tournament-related endpoints. Check the :clash-royale-api:`tournaments` for more detailed information about each endpoint. """ def get(self, tag: str) -> Tournament: """Get tournament information by tag.""" encoded_tag = self._encode_tag(tag) response = self._client._request("GET", f"/tournaments/{encoded_tag}") return Tournament.model_validate(response) def search( self, name: str, **params: Unpack[PaginationParams] ) -> PaginatedList[TournamentHeader]: """Search tournaments by name.""" api_params = {"name": name, **params} return PaginatedList( client=self._client, endpoint="/tournaments", model=TournamentHeader, params=api_params, # ty:ignore[invalid-argument-type] ) types.py ======== .. code-block:: python from __future__ import annotations from typing import TypedDict class PaginationParams(TypedDict, total=False): """Common pagination parameters for API requests. :param limit: Maximum total number of items to fetch (default: unlimited). Controls how many results you get in total. Example: `limit=50` will fetch at most 50 items across all pages. :param page_size: Number of items per API request. This is sent as "limit" to the API. Only change if you need to optimize request sizes or work around API constraints. :param after: Cursor for pagination (automatically managed by PaginatedList). :param before: Cursor for backward pagination (automatically managed by PaginatedList). """ limit: int page_size: int after: str before: str class ClanSearchParams(PaginationParams, total=False): """Parameters for clan search endpoint. :param limit: Maximum total number of items to fetch (default: unlimited). Controls how many results you get in total. Example: `limit=50` will fetch at most 50 items across all pages. :param page_size: Number of items per API request. This is sent as "limit" to the API. Only change if you need to optimize request sizes or work around API constraints. :param after: Cursor for pagination (automatically managed by PaginatedList). :param before: Cursor for backward pagination (automatically managed by PaginatedList). :param location_id: Filter by clan location identifier (e.g. 57000094). :param min_members: Filter by minimum number of clan members. :param max_members: Filter by maximum number of clan members. :param min_score: Filter by minimum amount of clan score. """ location_id: int min_members: int max_members: int min_score: int