Migrating to coc.py v1.0

v1.0 is a complete rewrite of the library, from top to bottom, and no effort was made to maintain backwards compatibility.

Most notably, all models have been rewritten and the EventsClient has undergone a significant refactor.

All exceptions and iterators remain the same, and Client methods remain similar.

Clans

The naming of clan objects has been changed to align with the API naming, and to improve understand of what each model represents and where it may come from.

For example, previously, coc.BasicClan was an abstract name to describe a clan with limit information, belonging to a player, as returned in the Client.get_player() method. It has been renamed to coc.PlayerClan.

Before

After

coc.Clan

coc.BaseClan

coc.BasicClan

coc.PlayerClan

coc.SearchClan

coc.Clan

Additionally, RankedClan was added. This is returned when the Client.get_location_clans() or Client.get_location_clans_versus() are called. Also, ClanWarLeagueClan was added, which represents a CWL clan retrieved in the Client.get_league_group() method.

Properties and Methods

Before

After

Clan.iterlabels

Clan.labels

Clan.itermembers

Clan.members

Clan.members_dict

Removed

Clan.get_member

Clan.get_member_by()

Didn’t exist

Clan.get_member()

WarClan.itermembers

Clan.members

WarClan.iterattacks

WarClan.attacks

WarClan.iterdefenses

WarClan.defenses

A quick explanation and example of the uses of Clan.get_member() and Clan.get_member_by():

The previous Clan.get_member() has been renamed to Clan.get_member_by().

A new method, Clan.get_member() has been introduced. It provides a way to get a member (by tag) that is significantly faster than Clan.get_member().

The below is a slimmed down version of how each function work. This is not a working example, however it highlights the use and efficiency of Clan.get_member() vs Clan.get_member_by()

def get_member(self, tag):
    try:
        return self._members[tag]  # fast dict lookup
    except KeyError:
        return None

def get_member_by(self, **attrs):
    for member in self.members:
        # slow because it potentially has to iterate over every member before finding the correct one
        if all(getattr(member, k) == v for k, v in attrs):
            return member

A quick example about the use of Clan.get_member() and Clan.get_member_by():

# before
member = clan.get_member(name="Bob the Builder")
member = clan.get_member(tag="#P02VLYC")

# after
member = clan.get_member_by(name="Bob the Builder")
member = clan.get_member("#P02VLYC")

This change vastly improves the efficiency of Clan.get_member().

Clans now have a __eq__ comparison that is based on their tag. For example:

# before
player = await client.get_player('#P02VLYC')
clan = await client.get_clan(player.clan.tag)
assert player.clan == clan  # False

# after
player = await client.get_player('#P02VLYC')
clan = await client.get_clan(player.clan.tag)
assert player.clan == clan  # True

Players

As with clans, the naming of player classes have also changed to align with API naming and improve readability of what an object contains.

Before

After

coc.Player

coc.BasePlayer

coc.BasicPlayer

coc.ClanMember

coc.WarMember

coc.ClanWarMember

coc.SearchPlayer

coc.Player

Additionally, RankedPlayer was added. This is returned when the Client.get_location_players() or Client.get_location_players_versus() are called.

Properties and Methods

Before

After

WarMember.iterattacks

ClanWarMember.attacks

WarMember.iterdefenses

ClanWarMember.defenses

SearchPlayer.iterachievements

Player.achievements

SearchPlayer.iterlabels

Player.labels

SearchPlayer.ordered_builder_troops

Player.builder_troops

SearchPlayer.ordered_heroes

Player.heroes

SearchPlayer.ordered_home_troops

Player.home_troops

SearchPlayer.ordered_siege_machines

Player.siege_machines

SearchPlayer.ordered_spells

Player.spells

SearchPlayer.achievements_dict

Removed

SearchPlayer.builder_troops_dict

Removed

SearchPlayer.get_ordered_troops()

Removed

SearchPlayer.heroes_dict

Removed

SearchPlayer.home_troops_dict

Removed

SearchPlayer.ordered_super_troops

Removed

SearchPlayer.siege_machines_dict

Removed

SearchPlayer.spells_dict

Removed

SearchPlayer.super_troops_dict

Removed

Didn’t exist

Player.get_hero()

Didn’t exist

Player.get_achievement()

Didn’t exist

Player.legend_statistics()

You will notice that a number of _dict and ordered_ attributes were removed. All properties returning troops and spells are now ordered, apart from Player.troops. This is due to the baby dragon being in both home and builder troops, making it difficult to sort these troops, at the expense of a less efficient property it has been decided to not order these. Please use Player.home_troops or Player.builder_troops instead.

In addition, all properties return lists of their object (achievements, troops, spells, etc.). This change was made to improve consistency and fluidity. Objects that are commonly used with a lookup (achievements and heroes) are now stored internally as dicts, and a fast lookup can be performed with Player.get_hero() or Player.get_achievement(). This is significantly faster than iterating over their list counterparts.

Super troop support has been removed from the library until Supercell adds support for this to their API.

As with clans, players now have a useful __eq__ operation that compares based on player tag.

# before
clan = await client.get_clan('#P02VLYC')
player = await client.get_player(clan.members[0].tag)
assert player == clan.members[0]  # False

# after
clan = await client.get_clan('#P02VLYC')
player = await client.get_player(clan.members[0].tag)
assert player == clan.members[0]  # True

Wars

As with players and clans, the naming of war classes have also changed to align with API naming and improve readability of what an object contains.

Before

After

coc.BaseWar

Removed

coc.WarLog

coc.ClanWarLogEntry

coc.LeagueGroup

coc.ClanWarLeagueGroup

coc.LeagueWar

coc.ClanWar

Note that coc.ClanWar has not been renamed, although a LeagueWar is now bundled in with a ClanWar.

Properties and Methods

Before

After

ClanWar.get_member

ClanWar.get_member_by()

ClanWar.iterattacks

ClanWar.attacks()

ClanWar.itermembers

ClanWar.members()

Didn’t exist

ClanWar.get_member()

Didn’t exist

ClanWar.get_attack()

Didn’t exist

ClanWar.is_cwl

Didn’t exist

ClanWarLogEntry.is_league_entry

Didn’t exist

WarAttack.is_fresh_attack

Cache

The entire “custom cache” section of the library has been removed. It was inefficient, not used and has been replaced by a basic dictionary lookup-style cache. Please remove any references to the previous coc.Cache and assosiated classes.

Instead, objects will be cached automatically by the HTTP client until they are stale, that is a “fresh” object is available from the API, at which point the library will retrieve and cache that. This is done based on the Cache-Control headers returned by the API. Support may be made for setting custom cache expiries in the future.

Throttlers

A new throttler, coc.BasicThrottler has been introduced as the new default throttler. It works on a “sleep between requests” approach, which means that if you set the throttle limit to be 10, with 1 token, the library will speep for 0.1 seconds between each request.

The previous default throttler, coc.BatchThrottler is still available and can be set by passing a kwarg to coc.login():

client = coc.login("email", "password", throttler=coc.BatchThrottler)

This throttler works based on allowing the number of requests per second in, then sleeping until the next second passes. For example, if you set the throttle limit to be 10, with 1 token, the library will let 10 requests in consecutively, however will sleep until the next second when you request with the 11th, and so forth.

Events

The events partition of the library has undergone a complete rewrite. Events are checked for and dispatched on-demand, making them vastly more efficient.

Dynamic Predicates

For example, if you wanted to know when a clan had changed their description, before the library would check for your description change, as well as checking if every single other attribute of the clan had changed. Now, it will only check to see if the description has changed every loop.

Decorators

Events are no longer hard-coded into the library. This creates a more future-proof design, and adds support for a custom attribute you may have with custom classes. Events are no longer defined by their function name, too, instead the name of the decorator assigned.

For example:

# before
@client.event
async def on_clan_member_donations_change(old_donations, new_donations, player):
    ...

# after
@client.event
@coc.ClanEvents.member_donations()
async def my_cool_function_name(old_player, new_player):
    ...

This may appear more verbose, but it is clearer, more efficient and consistent.

The naming of the attribute following the event group’s class is important. It should represent the name of the attribute you wish to check in the predicate. For clan member events, this should be prefaced with member_, and for a players’ clans, this should be prefaced with clan_.

For example:

@client.event
@coc.PlayerEvents.trophies()  # an event that is run for every player, when their `.trophies` attribute changes.
async def foo(old_player, new_player):
    assert old_player.trophies != new_player.trophies

@client.event
@coc.WarEvents.state()  # an event that is run when a war `.state` has changed
async def foo(old_war, new_war):
    assert old_war.state != new_war.state

@client.event
@coc.ClanEvents.public_war_log()  # an event that is run when a clan's `.public_war_log` attribute has changed.
async def foo(old_clan, new_clan):
    assert old_clan.public_war_log != new_clan.public_war_log

@client.event
@coc.ClanEvents.member_donations()  # an event that is run for every clan member when their `.donations` have changed.
async def foo(old_member, new_member):
    assert old_member.donations != new_member.donations

@client.event
@coc.PlayerEvents.clan_level()  # an event that is called when a player's clan's level has changed.
async def foo(old_player, new_player):
    assert old_player.clan.level != new_player.clan.level

You can also stack decorators to get multiple events reported to one callback:

@client.event
@coc.ClanEvents.public_war_log()
@coc.ClanEvents.description()
@coc.ClanEvents.level()
async def foo(old_clan, new_clan):
    if old_clan.level != new_clan.level:
        ...

Callback Arguments

Previously, events followed a rough old_value, new_value, object argument order. This wasn’t always consistent between objects, and so in v1.0.0 event callbacks will follow a much more consistent design, consisting of old_object, new_object. Inevitably there will be exceptions, but these will be few and well documented.

For example:

# before
@client.event
async def on_clan_member_donations_change(old_donations, new_donations, member):
    assert old_donations != new_donations  # True

# after
@client.event
@coc.ClanEvents.member_donations()
async def foo(old_member, new_member):
    assert old_member.donations != new_member.donations  # True

Retry / Refresh Intervals

Also, unless you wish to only check for new events once every hour or 6 hours, or any time greater than the refresh time for objects in the API, it is suggested to omit the retry_interval parameter. The library will automatically determine when the next fresh object is available, and instead of sleeping for a predefined 60seconds between every loop, it will instead sleep until a fresh object is available from the API. This means some events could see an up to 50% reduction in latency between when the event happens in game and when coc.py reports it.

For example:

# before
@client.event
async def on_clan_level_change(...): ...

client.add_clan_update(tags, retry_interval=60)  # to check for new events every 60 seconds

# after
@client.event
@coc.ClanEvents.level()
async def foo(...): ...  # check as often as API updates the clan for an event

@client.event
@coc.ClanEvents.level(retry_interval=60)  # check every 60 seconds for a new event
async def foo(...): ...

Adding Clan and Player Tags

Previously, events would require a lot of lines of function calls to get events up and running. These have been streamlined into the decorator, and suggested registering of clan tags is via the decorator. This makes for much cleaner and readable code.

For example:

# before
@client.event
async def on_player_name_change(...): ...

client.add_player_updates(['#tag', '#tag2', '#tag3', ...])

# after
@client.event
@coc.PlayerEvents.name(tags=['#tag', '#tag2', '#tag3'])
async def foo(...): ...

# alternatively, if you must:
@client.event
@coc.PlayerEvents.name()
async def foo(...): ...

client.add_player_updates('#tag', '#tag2', '#tag3')

A few points to note:

  • Tags will be automatically corrected via the coc.correct_tag() function.

  • Every tag that is added to the client will be sent to each function. This makes for a much simpler internal design.

For Example:

@client.event
@coc.PlayerEvents.exp_level("#tag1")
async def foo(...):
    # events will be received for #tag1, #tag2, #tag3 and #tag4.

@client.event
@coc.PlayerEvents.name("#tag2")
async def foo(...):
    # events will be received for #tag1, #tag2, #tag3 and #tag4.

@client.event
@coc.PlayerEvents.donations()
async def foo(...):
    # events will be received for #tag1, #tag2, #tag3 and #tag4.

client.add_player_updates("#tag3", "#tag4")

The inverse applies; you only need to register a tag with 1 decorator for it to apply to all events.

The EventsClient.add_player_updates() and corresponding clan, war methods now take a list of positional arguments.

For example:

# before
client.add_player_updates(["#tag1", "#tag2"])

# after
client.add_player_updates("#tag1", "#tag2", "#tag3")

tags = ["#tag1", "#tag2", "#tag3"]
client.add_player_updates(*tags)

Removing Clan and Player Tags

v1.0 provides added functionality of removing player and clan tags from the list of tags being updated: EventsClient.remove_player_updates(), EventsClient.remove_clan_updates() and EventsClient.remove_war_updates().

The usage is intuitive, and identical to adding the tags:

client.remove_player_updates("#tag1", "#tag2", "#tag3")

tags = ["#tag1", "#tag2", "#tag3"]
client.remove_player_updates(*tags)

End of Season Helpers

A common use for users of the API is to do something when the season change occurs, at 5pm UTC on the last monday of each month. This has been integrated into coc.py, and available with the new utility commands, utils.get_season_start() and utils.get_season_end().

In addition, there has been a new event added, @coc.ClientEvents.season_started() which will get fired upon season restart.

For Example:

from coc import utils

season_start = utils.get_season_start()
print("The current season started at " + str(season_start))

@coc.ClientEvents.new_season_start()
async def season_started():
    print("A new season has started, and it will finish at " + utils.get_season_end())

Custom Classes

For more information on custom class support, please see Custom Classes.