Skip to content

Commit

Permalink
Merge pull request #235 from mathsman5133/base_url+event-equipement
Browse files Browse the repository at this point in the history
Feature: Base_url, Hero equipment related events, minor bug fixes
  • Loading branch information
doluk authored Apr 27, 2024
2 parents 898ea49 + aeeb730 commit 46d66bd
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 51 deletions.
2 changes: 1 addition & 1 deletion coc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
SOFTWARE.
"""

__version__ = "3.4.2"
__version__ = "3.5.1"

from .abc import BasePlayer, BaseClan
from .clans import RankedClan, Clan
Expand Down
8 changes: 7 additions & 1 deletion coc/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,17 @@ class Client:
for later use or are interested in new things that coc.py does not support otherwise yet. But because this
increases the memory footprint and is not needed for most use cases, this defaults to ``False``.
base_url: :class:`str`
The base URL to use for API requests. Defaults to "https://api.clashofclans.com/v1"
Attributes
----------
loop : :class:`asyncio.AbstractEventLoop`
The loop that is used for HTTP requests
"""

__slots__ = (
"base_url",
"loop",
"correct_key_count",
"key_names",
Expand Down Expand Up @@ -192,6 +196,7 @@ def __init__(
load_game_data: LoadGameData = LoadGameData(default=True),
realtime=False,
raw_attribute=False,
base_url: str = "https://api.clashofclans.com/v1",
**kwargs,
):

Expand All @@ -216,7 +221,7 @@ def __init__(
self.raw_attribute = raw_attribute
self.correct_tags = correct_tags
self.load_game_data = load_game_data

self.base_url = base_url
# cache
self._players = {}
self._clans = {}
Expand All @@ -241,6 +246,7 @@ def _create_client(self, email, password):
throttler=self.throttler,
cache_max_size=self.cache_max_size,
stats_max_size=self.stats_max_size,
base_url=self.base_url,
)

def _load_holders(self):
Expand Down
29 changes: 29 additions & 0 deletions coc/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ def __getattr__(self, item: str):
if self.cls.event_type == "client":
return self.cls.__getattr__(self.cls, item)

if "versus" in item:
item = item.replace("versus", "builder_base")

# handle member_x events:
if "member_" in item and item != "member_count":
item = item.replace("member_", "")
Expand Down Expand Up @@ -274,6 +277,32 @@ async def wrapped(cached_player, player, callback):

return _ValidateEvent.shortcut_register(wrapped, tags, custom_class, retry_interval, PlayerEvents.event_type)

@classmethod
def equipment_change(cls, tags=None, custom_class=None, retry_interval=None):
"""Event for when a player has upgraded or unlocked an equipment."""

async def wrapped(cached_player, player, callback):
equipment_upgrades = (n for n in player.equipment if n not in set(cached_player.equipment))
for equipment in equipment_upgrades:
await callback(cached_player, player, equipment)

return _ValidateEvent.shortcut_register(wrapped, tags, custom_class, retry_interval, PlayerEvents.event_type)

@classmethod
def active_equipment_change(cls, tags=None, custom_class=None, retry_interval=None):
"""Event for when a player has changed their active equipment."""

async def wrapped(cached_player, player, callback):
for hero in player.heroes:
cached_hero = cached_player.get_hero(hero.name)
if not cached_hero:
continue
for equipment in hero.equipment:
if cached_hero and equipment not in cached_hero.equipment:
await callback(cached_player, player, hero, equipment)

return _ValidateEvent.shortcut_register(wrapped, tags, custom_class, retry_interval, PlayerEvents.event_type)

@classmethod
def joined_clan(cls, tags=None, custom_class=None, retry_interval=None):
"""Event for when a player has joined a new clan."""
Expand Down
32 changes: 32 additions & 0 deletions coc/events.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ class ClanEvents:
def points(cls, tags: Iterable = None, custom_class: _ClanType = Clan, retry_interval: int = None) -> _EventDecoratorReturn: ...
@classmethod
def versus_points(cls, tags: Iterable = None, custom_class: _ClanType = Clan, retry_interval: int = None) -> _EventDecoratorReturn: ...

@classmethod
def builder_base_points(cls,
tags: Iterable = None,
custom_class: _ClanType = Clan,
retry_interval: int = None) -> _EventDecoratorReturn: ...
@classmethod
def member_count(cls, tags: Iterable = None, custom_class: _ClanType = Clan, retry_interval: int = None) -> _EventDecoratorReturn: ...
@classmethod
Expand Down Expand Up @@ -77,7 +83,18 @@ class ClanEvents:
@classmethod
def member_versus_trophies(cls, tags: Iterable = None, custom_class: _ClanType = Clan, retry_interval: int = None) -> _EventDecoratorReturn: ...
@classmethod
def member_builder_base_trophies(cls,
tags: Iterable = None,
custom_class: _ClanType = Clan,
retry_interval: int = None) -> _EventDecoratorReturn: ...
@classmethod
def member_versus_rank(cls, tags: Iterable = None, custom_class: _ClanType = Clan, retry_interval: int = None) -> _EventDecoratorReturn: ...

@classmethod
def member_builder_base_rank(cls,
tags: Iterable = None,
custom_class: _ClanType = Clan,
retry_interval: int = None) -> _EventDecoratorReturn: ...
@classmethod
def member_donations(cls, tags: Iterable = None, custom_class: _ClanType = Clan, retry_interval: int = None) -> _EventDecoratorReturn: ...
@classmethod
Expand All @@ -95,6 +112,12 @@ class PlayerEvents:
def trophies(cls, tags: Iterable = None, custom_class: _PlayerType = Player, retry_interval: int = None) -> _EventDecoratorReturn: ...
@classmethod
def versus_trophies(cls, tags: Iterable = None, custom_class: _PlayerType = Player, retry_interval: int = None) -> _EventDecoratorReturn: ...

@classmethod
def builder_base_trophies(cls,
tags: Iterable = None,
custom_class: _PlayerType = Player,
retry_interval: int = None) -> _EventDecoratorReturn: ...
@classmethod
def clan_rank(cls, tags: Iterable = None, custom_class: _PlayerType = Player, retry_interval: int = None) -> _EventDecoratorReturn: ...
@classmethod
Expand Down Expand Up @@ -122,9 +145,18 @@ class PlayerEvents:
@classmethod
def versus_attack_wins(cls, tags: Iterable = None, custom_class: _PlayerType = Player, retry_interval: int = None) -> _EventDecoratorReturn: ...
@classmethod
def best_builder_base_trophies(cls,
tags: Iterable = None,
custom_class: _PlayerType = Player,
retry_interval: int = None) -> _EventDecoratorReturn: ...
@classmethod
def legend_statistics(cls, tags: Iterable = None, custom_class: _PlayerType = Player, retry_interval: int = None) -> _EventDecoratorReturn: ...
@classmethod
def labels(cls, tags: Iterable = None, custom_class: _PlayerType = Player, retry_interval: int = None) -> _EventDecoratorReturn: ...
@classmethod
def equipment_change(cls, tags: Iterable = None, custom_class: _PlayerType = Player, retry_interval: int = None) -> _EventDecoratorReturn: ...
@classmethod
def active_equipment_change(cls, tags: Iterable = None, custom_class: _PlayerType = Player, retry_interval: int = None) -> _EventDecoratorReturn: ...

class WarEvents:
event_type: str
Expand Down
80 changes: 44 additions & 36 deletions coc/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,7 @@ async def __aexit__(self, exception_type, exception, traceback):
class Route:
"""Helper class to create endpoint URLs."""

BASE = "https://api.clashofclans.com/v1"

def __init__(self, method: str, path: str, **kwargs: dict):
def __init__(self, method: str, base: str, path: str, **kwargs: dict):
"""
The class is used to create the final URL used to fetch the data
from the API. The parameters that are passed to the API are all in
Expand All @@ -157,6 +155,8 @@ def __init__(self, method: str, path: str, **kwargs: dict):
----------
method:
:class:`str`: HTTP method used for the HTTP request
base:
:class:`str`: Base URL used for the HTTP request
path:
:class:`str`: URL path used for the HTTP request
kwargs:
Expand All @@ -168,7 +168,9 @@ def __init__(self, method: str, path: str, **kwargs: dict):

self.method = method
self.path = path
url = self.BASE + self.path
self.base = base

url = self.base + self.path

if kwargs:
self.url = "{}?{}".format(url, urlencode({k: v for k, v in kwargs.items() if v is not None}))
Expand Down Expand Up @@ -196,7 +198,8 @@ def __init__(
throttle_limit,
throttler=BasicThrottler,
cache_max_size=10000,
stats_max_size=1000
stats_max_size=1000,
base_url="https://api.clashofclans.com/v1",
):
self.client = client
self.loop = loop
Expand All @@ -213,7 +216,12 @@ def __init__(
self.cache = cache_max_size and FIFO(cache_max_size)
self._cache_remove_count = 0
self.stats = stats_max_size and HTTPStats(max_size=stats_max_size)

if base_url and isinstance(base_url, str) and len(base_url) > 0:
if base_url.endswith("/"):
base_url = base_url[:-1]
self.base_url = base_url
else:
raise ValueError("base_url must be a string and not empty.")
if issubclass(throttler, BasicThrottler):
self.__throttle = throttler(1 / per_second)
elif issubclass(throttler, BatchThrottler):
Expand Down Expand Up @@ -378,108 +386,108 @@ async def request(self, route, **kwargs):
# clans

def search_clans(self, **kwargs):
return self.request(Route("GET", "/clans", **kwargs))
return self.request(Route("GET", self.base_url, "/clans", **kwargs))

def get_clan(self, tag):
return self.request(Route("GET", "/clans/{}".format(tag)))
return self.request(Route("GET", self.base_url, "/clans/{}".format(tag)))

def get_clan_members(self, tag, **kwargs):
return self.request(Route("GET", "/clans/{}/members".format(tag), **kwargs))
return self.request(Route("GET", self.base_url, "/clans/{}/members".format(tag), **kwargs))

def get_clan_war_log(self, tag, **kwargs):
return self.request(Route("GET", "/clans/{}/warlog".format(tag), **kwargs))
return self.request(Route("GET", self.base_url, "/clans/{}/warlog".format(tag), **kwargs))

def get_clan_current_war(self, tag, realtime=None):
return self.request(Route("GET", "/clans/{}/currentwar".format(tag) + (
return self.request(Route("GET", self.base_url, "/clans/{}/currentwar".format(tag) + (
'?realtime=true' if realtime or (realtime is None and self.client.realtime)
else '')))

def get_clan_war_league_group(self, tag, realtime=None):
return self.request(Route("GET", "/clans/{}/currentwar/leaguegroup".format(tag) + (
return self.request(Route("GET", self.base_url, "/clans/{}/currentwar/leaguegroup".format(tag) + (
'?realtime=true' if realtime or (realtime is None and self.client.realtime)
else '')))

def get_cwl_wars(self, war_tag, realtime = None):
return self.request(Route("GET", "/clanwarleagues/wars/{}".format(war_tag) + (
return self.request(Route("GET", self.base_url, "/clanwarleagues/wars/{}".format(war_tag) + (
'?realtime=true' if realtime or (realtime is None and self.client.realtime)
else '')))

def get_clan_raid_log(self, tag, **kwargs):
return self.request(Route("GET", "/clans/{}/capitalraidseasons".format(tag), **kwargs))
return self.request(Route("GET", self.base_url, "/clans/{}/capitalraidseasons".format(tag), **kwargs))

# locations

def search_locations(self, **kwargs):
return self.request(Route("GET", "/locations", **kwargs))
return self.request(Route("GET", self.base_url, "/locations", **kwargs))

def get_location(self, location_id):
return self.request(Route("GET", "/locations/{}".format(location_id)))
return self.request(Route("GET", self.base_url, "/locations/{}".format(location_id)))

def get_location_clans(self, location_id, **kwargs):
return self.request(Route("GET", "/locations/{}/rankings/clans".format(location_id), **kwargs))
return self.request(Route("GET", self.base_url, "/locations/{}/rankings/clans".format(location_id), **kwargs))

def get_location_players(self, location_id, **kwargs):
return self.request(Route("GET", "/locations/{}/rankings/players".format(location_id), **kwargs))
return self.request(Route("GET", self.base_url, "/locations/{}/rankings/players".format(location_id), **kwargs))

def get_location_clans_builder_base(self, location_id, **kwargs):
return self.request(Route("GET", "/locations/{}/rankings/clans-builder-base".format(location_id), **kwargs))
return self.request(Route("GET", self.base_url, "/locations/{}/rankings/clans-builder-base".format(location_id), **kwargs))

def get_location_clans_capital(self, location_id, **kwargs):
return self.request(Route("GET", "/locations/{}/rankings/capitals".format(location_id), **kwargs))
return self.request(Route("GET", self.base_url, "/locations/{}/rankings/capitals".format(location_id), **kwargs))

def get_location_players_builder_base(self, location_id, **kwargs):
return self.request(Route("GET", "/locations/{}/rankings/players-builder-base".format(location_id), **kwargs))
return self.request(Route("GET", self.base_url, "/locations/{}/rankings/players-builder-base".format(location_id), **kwargs))

# leagues

def search_leagues(self, **kwargs):
return self.request(Route("GET", "/leagues", **kwargs))
return self.request(Route("GET", self.base_url, "/leagues", **kwargs))

def search_capital_leagues(self, **kwargs):
return self.request(Route("GET", "/capitalleagues", **kwargs))
return self.request(Route("GET", self.base_url, "/capitalleagues", **kwargs))

def search_war_leagues(self, **kwargs):
return self.request(Route("GET", "/warleagues", **kwargs))
return self.request(Route("GET", self.base_url, "/warleagues", **kwargs))

def search_builder_base_leagues(self, **kwargs):
return self.request(Route("GET", "/builderbaseleagues", **kwargs))
return self.request(Route("GET", self.base_url, "/builderbaseleagues", **kwargs))

def get_league(self, league_id):
return self.request(Route("GET", "/leagues/{}".format(league_id)))
return self.request(Route("GET", self.base_url, "/leagues/{}".format(league_id)))

def get_capital_league(self, league_id):
return self.request(Route("GET", "/capitalleagues/{}".format(league_id)))
return self.request(Route("GET", self.base_url, "/capitalleagues/{}".format(league_id)))

def get_war_league(self, league_id):
return self.request(Route("GET", "/warleagues/{}".format(league_id)))
return self.request(Route("GET", self.base_url, "/warleagues/{}".format(league_id)))

def get_builder_base_league(self, league_id):
return self.request(Route("GET", "/builderbaseleagues/{}".format(league_id)))
return self.request(Route("GET", self.base_url, "/builderbaseleagues/{}".format(league_id)))

def get_league_seasons(self, league_id, **kwargs):
return self.request(Route("GET", "/leagues/{}/seasons".format(league_id), **kwargs))
return self.request(Route("GET", self.base_url, "/leagues/{}/seasons".format(league_id), **kwargs))

def get_league_season_info(self, league_id, season_id, **kwargs):
return self.request(Route("GET", "/leagues/{}/seasons/{}".format(league_id, season_id), **kwargs))
return self.request(Route("GET", self.base_url, "/leagues/{}/seasons/{}".format(league_id, season_id), **kwargs))

# players

def get_player(self, player_tag):
return self.request(Route("GET", "/players/{}".format(player_tag)))
return self.request(Route("GET", self.base_url, "/players/{}".format(player_tag)))

def verify_player_token(self, player_tag, token):
return self.request(Route("POST", "/players/{}/verifytoken".format(player_tag)), json={"token": token})
return self.request(Route("POST", self.base_url, "/players/{}/verifytoken".format(player_tag)), json={"token": token})

# labels

def get_clan_labels(self, **kwargs):
return self.request(Route("GET", "/labels/clan", **kwargs))
return self.request(Route("GET", self.base_url, "/labels/clan", **kwargs))

def get_player_labels(self, **kwargs):
return self.request(Route("GET", "/labels/players", **kwargs))
return self.request(Route("GET", self.base_url, "/labels/players", **kwargs))

def get_current_goldpass_season(self):
return self.request(Route("GET", "/goldpass/seasons/current"))
return self.request(Route("GET", self.base_url, "/goldpass/seasons/current"))

# key updating management

Expand Down
6 changes: 2 additions & 4 deletions coc/players.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ class ClanMember(BasePlayer):
The member's trophy count.
builder_base_trophies: :class:`int`
The member's builder base trophy count.
town_hall: :class:`int`
The player's town hall level. In case the player hasn't logged in since 2019, this will be `0`.
clan_rank: :class:`int`
The member's rank in the clan.
clan_previous_rank: :class:`int`
Expand Down Expand Up @@ -232,8 +234,6 @@ class Player(ClanMember):
The player's best recorded trophies for the home base.
war_stars: :class:`int`
The player's total war stars.
town_hall: :class:`int`
The player's town hall level.
town_hall_weapon: Optional[:class:`int`]
The player's town hall weapon level, or ``None`` if it doesn't exist.
builder_hall: :class:`int`
Expand All @@ -257,7 +257,6 @@ class Player(ClanMember):
"defense_wins",
"best_trophies",
"war_stars",
"town_hall",
"town_hall_weapon",
"builder_hall",
"best_builder_base_trophies",
Expand Down Expand Up @@ -348,7 +347,6 @@ def _from_data(self, data: dict) -> None:
self.defense_wins: int = data_get("defenseWins")
self.best_trophies: int = data_get("bestTrophies")
self.war_stars: int = data_get("warStars")
self.town_hall: int = data_get("townHallLevel")
self.town_hall_weapon: int = data_get("townHallWeaponLevel")
self.builder_hall: int = data_get("builderHallLevel", 0)
self.best_builder_base_trophies: int = data_get("bestBuilderBaseTrophies")
Expand Down
4 changes: 4 additions & 0 deletions docs/advanced/event.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ To get an event for when a new war start, ie :attr:`ClanWar.preparation_start_ti
.. note::
The callback function of :func:`WarEvents.new_war` has only one parameter, the new :class:`ClanWar`.

.. note::
The callback function of :func:`PlayerEvents.active_equipment_change` has four parameters, the old
:class:`Player`, the new :class:`Player`, the :class:`Hero` and the newly equipped :class:`Equipment`.


The pattern is simple, and holds true for all attributes.

Expand Down
Loading

0 comments on commit 46d66bd

Please sign in to comment.