Skip to content

Commit 82a1000

Browse files
committed
Merge remote-tracking branch 'cocpy_main/g7_2.2.4_candidate' into g7_2.2.4_candidate
# Conflicts: # coc/abc.py # coc/client.py # coc/events.py # coc/events.pyi # coc/ext/fullwarapi/__init__.py # coc/players.py # coc/utils.py # docs/advanced/event.rst # docs/miscellaneous/changelog.rst
2 parents 0e7cfdd + 7c3ccbd commit 82a1000

File tree

10 files changed

+97
-71
lines changed

10 files changed

+97
-71
lines changed

coc/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
ACHIEVEMENT_ORDER,
3636
BUILDER_TROOPS_ORDER,
3737
HERO_ORDER,
38-
HERO_PETS_ORDER,
38+
PETS_ORDER,
3939
HOME_TROOP_ORDER,
4040
SIEGE_MACHINE_ORDER,
4141
SPELL_ORDER,

coc/abc.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@
2525
from pathlib import Path
2626
from typing import AsyncIterator, Any, Dict, Type, Optional, TYPE_CHECKING
2727

28-
import coc
29-
from .enums import Resource
28+
from .enums import PETS_ORDER, Resource
3029
from .miscmodels import try_enum, Badge, TimeDelta
3130
from .iterators import PlayerIterator
3231
from .utils import CaseInsensitiveDict, UnitStat, _get_maybe_first
@@ -205,7 +204,7 @@ def _load_json_meta(cls, troop_meta, id, name, lab_to_townhall):
205204
cls.is_elixir_spell = True
206205
elif production_building == "Mini Spell Factory":
207206
cls.is_dark_spell = True
208-
elif name in coc.HERO_PETS_ORDER:
207+
elif name in PETS_ORDER:
209208
production_building = "Pet Shop"
210209

211210
# load buildings
@@ -218,8 +217,8 @@ def _load_json_meta(cls, troop_meta, id, name, lab_to_townhall):
218217
else:
219218
# it is a troop or spell or siege
220219
prod_unit = buildings.get(production_building)
221-
if "barrack" in production_building.lower() or "Spell Forge" == production_building or \
222-
"SiegeWorkshop" == production_building or "Mini Spell Factory" == production_building:
220+
if production_building in ("SiegeWorkshop", "Spell Forge", "Mini Spell Factory",
221+
"Dark Elixir Barrack", "Barrack", "Barrack2"):
223222
min_prod_unit_level = troop_meta.get("BarrackLevel", [None, ])[0]
224223
# there are some special troops, which have no BarrackLevel attribute
225224
if not min_prod_unit_level:
@@ -259,8 +258,7 @@ def _load_json_meta(cls, troop_meta, id, name, lab_to_townhall):
259258
return
260259

261260
cls.lab_level = try_enum(UnitStat, laboratory_levels)
262-
cls.housing_space = _get_maybe_first(troop_meta, "HousingSpace",
263-
default=0)
261+
cls.housing_space = _get_maybe_first(troop_meta, "HousingSpace", default=0)
264262
cls.speed = try_enum(UnitStat, troop_meta.get("Speed"))
265263
cls.level = cls.dps and UnitStat(range(1, len(cls.dps) + 1))
266264

@@ -332,7 +330,7 @@ def _load_json(self, object_ids, english_aliases, lab_to_townhall):
332330
with open(self.FILE_PATH) as fp:
333331
data = ujson.load(fp)
334332

335-
for supercell_name, meta in data.items():
333+
for c, [supercell_name, meta] in enumerate(data.items()):
336334
# Not interested if it doesn't have a TID, since it likely isn't a real troop.
337335
if not meta.get("TID"):
338336
continue
@@ -352,7 +350,7 @@ def _load_json(self, object_ids, english_aliases, lab_to_townhall):
352350
dict(self.data_object.__dict__))
353351
new_item._load_json_meta(
354352
meta,
355-
id=object_ids.get(supercell_name, 0),
353+
id=object_ids.get(supercell_name, c),
356354
name=english_aliases[meta["TID"][0]][0],
357355
lab_to_townhall=lab_to_townhall,
358356
)
@@ -363,9 +361,8 @@ def _load_json(self, object_ids, english_aliases, lab_to_townhall):
363361
self.loaded = True
364362

365363
def load(
366-
self, data, townhall: int, default: Type[DataContainer] = None,
367-
load_game_data: bool = True
368-
) -> DataContainer:
364+
self, data: dict, townhall: int, default: Type[DataContainer] = None,
365+
load_game_data: bool = True) -> DataContainer:
369366
if load_game_data is True:
370367
try:
371368
item = self.item_lookup[data["name"]]

coc/enums.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ class Resource(Enum):
149149

150150
HERO_ORDER = ["Barbarian King", "Archer Queen", "Grand Warden", "Royal Champion", "Battle Machine"]
151151

152-
HERO_PETS_ORDER = [
152+
PETS_ORDER = [
153153
"L.A.S.S.I",
154154
"Electro Owl",
155155
"Mighty Yak",

coc/ext/fullwarapi/__init__.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@
77

88
from collections import namedtuple
99
from datetime import datetime
10-
from typing import List, Optional, Union, Any, Generator
10+
from typing import Generator, List, Optional, Union
1111

1212
import aiohttp
1313

14-
from ... import ClanWar
14+
import coc
1515
from ...http import json_or_text
1616
from ...utils import correct_tag
1717
from ...wars import ClanWar
1818

1919
LOG = logging.getLogger(__name__)
2020

21-
AccessToken = namedtuple("AccessToken", ["token", "expires_at"])
21+
FWAccessToken = namedtuple("FWAccessToken", ["token", "expires_at"])
2222

2323

2424
def extract_expiry_from_jwt_token(token: Union[str, bytes]) -> Optional[datetime]:
@@ -46,7 +46,7 @@ def extract_expiry_from_jwt_token(token: Union[str, bytes]) -> Optional[datetime
4646
return None
4747

4848

49-
async def login(username: str, password: str, coc_client) -> "FullWarClient":
49+
async def login(username: str, password: str, coc_client: Union[coc.Client, coc.EventsClient]) -> "FullWarClient":
5050
"""Eases logging into the API client.
5151
5252
For more information on this project, please join the discord server - <discord.gg/Eaja7gJ>
@@ -60,16 +60,15 @@ async def login(username: str, password: str, coc_client) -> "FullWarClient":
6060
Your username as given on the discord server.
6161
password : str
6262
Your password as given on the discord server
63-
coc_client: coc.Client
64-
Client to use
63+
coc_client : Union[coc.Client, coc.EventsClient]
64+
A coc_client to grap the loop from and create the response objects with
6565
"""
6666
if not isinstance(username, str) or not isinstance(password, str):
6767
raise TypeError("username and password must both be a string")
6868
if not username or not password:
6969
raise ValueError("username or password must not be an empty string.")
7070

71-
loop = asyncio.get_running_loop()
72-
return FullWarClient(username, password, coc_client, loop)
71+
return FullWarClient(username, password, coc_client)
7372

7473

7574
class FullWarClient:
@@ -86,9 +85,8 @@ class FullWarClient:
8685
Your username as given on the discord server.
8786
password : str
8887
Your password as given on the discord server
89-
coc_client: coc.Client
88+
coc_client: Union[coc.Client, coc.EventClient]
9089
Client to use
91-
9290
loop : Optional[:class:`asyncio.AbstractEventLoop`]
9391
The :class:`asyncio.AbstractEventLoop` to use for HTTP requests.
9492
An :func:`asyncio.get_event_loop()` will be used if ``None`` is passed
@@ -99,12 +97,13 @@ class FullWarClient:
9997

10098
__slots__ = ("username", "password", "coc_client", "loop", "key", "http_session")
10199

102-
def __init__(self, username: str, password: str, coc_client, loop: asyncio.AbstractEventLoop = None):
100+
def __init__(self, username: str, password: str, coc_client: Union[coc.Client, coc.EventsClient],
101+
loop: asyncio.AbstractEventLoop = None):
103102
self.username = username
104103
self.password = password
105104
self.coc_client = coc_client
106105

107-
self.loop = loop or asyncio.get_event_loop()
106+
self.loop = coc_client.loop or asyncio.get_event_loop()
108107
self.key = None # set in get_key()
109108

110109
self.http_session = aiohttp.ClientSession(loop=self.loop)
@@ -136,7 +135,7 @@ async def _request(self, method, url, *, token_request: bool = False, **kwargs):
136135
return await self._request(method, url, **kwargs)
137136

138137
async def _get_key(self):
139-
if not self.key or (self.key.expires_at < datetime.utcnow()):
138+
if not self.key or (self.key.expires_at and self.key.expires_at < datetime.utcnow()):
140139
await self._refresh_key()
141140

142141
return self.key.token
@@ -148,9 +147,9 @@ async def _refresh_key(self):
148147
}
149148

150149
payload = await self._request("POST", "/login", token_request=True, json=data)
151-
self.key = AccessToken(payload["access_token"], extract_expiry_from_jwt_token(payload["access_token"]))
150+
self.key = FWAccessToken(payload["access_token"], extract_expiry_from_jwt_token(payload["access_token"]))
152151

153-
async def war_result(self, clan_tag: str, preparation_start: int = 0) -> ClanWar | None:
152+
async def war_result(self, clan_tag: str, preparation_start: int = 0) -> Optional[ClanWar]:
154153
"""Get a stored war result.
155154
156155
Parameters
@@ -162,7 +161,6 @@ async def war_result(self, clan_tag: str, preparation_start: int = 0) -> ClanWar
162161
163162
Returns
164163
--------
165-
#NOOOOO idea if this is correct xD
166164
Optional[:class:`ClanWar`]
167165
War result, or ``None`` if no war found.
168166
"""
@@ -174,7 +172,7 @@ async def war_result(self, clan_tag: str, preparation_start: int = 0) -> ClanWar
174172
except (IndexError, KeyError, TypeError, ValueError):
175173
return None
176174

177-
async def war_result_log(self, clan_tag: str) -> Generator[ClanWar, Any, None] | None:
175+
async def war_result_log(self, clan_tag: str) -> Optional[Generator[ClanWar]]:
178176
"""Get all stored war results for a clan.
179177
180178
Parameters
@@ -184,9 +182,8 @@ async def war_result_log(self, clan_tag: str) -> Generator[ClanWar, Any, None] |
184182
185183
Returns
186184
--------
187-
#NOOOOO idea if this is correct xD
188-
Optional[:class:`ClanWar`]
189-
List of war results, or ``None`` if no wars found.
185+
Optional[Generator[:class:`ClanWar`]]
186+
Generator of war results, or ``None`` if no wars found.
190187
"""
191188
data = await self._request("GET",
192189
f"/war_result_log?clan_tag={correct_tag(clan_tag, '%23')}")
@@ -206,8 +203,9 @@ async def register_war(self, clan_tag: str, preparation_start: int = 0):
206203
clan_tag : str
207204
The clan to register a war for
208205
preparation_start: int
209-
Preparation time of the war
206+
Preparation time of the war.
210207
"""
211208
return await self._request("POST",
212209
f"/register_war?clan_tag={correct_tag(clan_tag, '%23')}"
213210
f"&prep_start={str(preparation_start)}")
211+

coc/players.py

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
UNRANKED_LEAGUE_DATA,
3636
ACHIEVEMENT_ORDER,
3737
SUPER_TROOP_ORDER,
38-
HERO_PETS_ORDER,
38+
PETS_ORDER,
3939
)
4040
from .abc import BasePlayer
4141
from .hero import Hero, Pet
@@ -237,6 +237,7 @@ class Player(ClanMember):
237237
"war_opted_in",
238238
"_achievements",
239239
"_heroes",
240+
"_pets",
240241
"_labels",
241242
"_spells",
242243
"_home_troops",
@@ -262,7 +263,7 @@ class Player(ClanMember):
262263
"_cs_home_troops",
263264
"_cs_builder_troops",
264265
"_cs_siege_machines",
265-
"_cs_hero_pets",
266+
"_cs_pets",
266267
"_cs_super_troops",
267268
"_cs_heroes",
268269
"_cs_spells",
@@ -281,6 +282,7 @@ def __init__(self, *, data, client, load_game_data=None, **_):
281282
self._home_troops: dict = {}
282283
self._builder_troops: dict = {}
283284
self._super_troops: list = []
285+
self._pets = None # type: Optional[dict]
284286

285287
self.achievement_cls = Achievement
286288
self.hero_cls = Hero
@@ -335,7 +337,7 @@ def _from_data(self, data: dict) -> None:
335337
if self._game_files_loaded:
336338
pet_lookup = [p.name for p in self._client._pet_holder.items]
337339
else:
338-
pet_lookup = HERO_PETS_ORDER
340+
pet_lookup = PETS_ORDER
339341

340342
self._iter_labels = (label_cls(data=ldata, client=self._client) for ldata in data_get("labels", []))
341343
self._iter_achievements = (achievement_cls(data=adata) for adata in data_get("achievements", []))
@@ -400,11 +402,12 @@ def load_game_data(self):
400402
# if self._game_files_loaded:
401403
# return True
402404

403-
holders = (self._client._troop_holder, self._client._hero_holder, self._client._spell_holder)
405+
holders = (self._client._troop_holder, self._client._hero_holder, self._client._spell_holder,
406+
self._client._pet_holder)
404407
if not all(holder.loaded for holder in holders):
405408
self._client._load_holders()
406409

407-
for items, holder in zip((self.troops, self.heroes, self.spells), holders):
410+
for items, holder in zip((self.troops, self.heroes, self.spells, self.pets), holders):
408411
for item in items:
409412
if not item.is_loaded:
410413
if isinstance(item, Troop):
@@ -435,7 +438,7 @@ def achievements(self) -> List[Achievement]:
435438
return list(sorted_achievements.values())
436439

437440
def get_achievement(self, name: str, default_value=None) -> Optional[Achievement]:
438-
"""Returns an achievement with the given name.
441+
"""Gets an achievement with the given name.
439442
440443
Parameters
441444
-----------
@@ -544,18 +547,42 @@ def siege_machines(self) -> List[Troop]:
544547
troops = (t for t in self.troops if t.name in SIEGE_MACHINE_ORDER or t.is_siege_machine)
545548
return list(sorted(troops, key=lambda t: order.get(t.name, 0)))
546549

547-
@cached_property("_cs_hero_pets")
548-
def hero_pets(self) -> List[Pet]:
550+
@cached_property("_cs_pets")
551+
def pets(self) -> List[Pet]:
549552
"""List[:class:`Pet`]: A :class:`List` of the player's hero pets.
550553
551554
This will return hero pets in the order found in the Pet House in-game.
552555
553556
This includes:
554557
- Hero pets only.
555558
"""
556-
order = {k: v for v, k in enumerate(HERO_PETS_ORDER)}
559+
order = {k: v for v, k in enumerate(PETS_ORDER)}
557560
return list(sorted(self._iter_pets, key=lambda t: order.get(t.name, 0)))
558561

562+
def get_pet(self, name: str, default_value=None) -> Optional[Pet]:
563+
"""Gets the pet with the given name.
564+
565+
Parameters
566+
-----------
567+
name: :class:`str`
568+
The name of a pet as found in-game.
569+
default_value:
570+
The default value to return if a pet with ``name`` is not found. Defaults to ``None``.
571+
572+
Returns
573+
--------
574+
Optional[:class:`Pet`]
575+
The returned pet or the ``default_value`` if not found, which defaults to ``None``.
576+
577+
"""
578+
if not self._pets:
579+
_ = self._pets
580+
581+
try:
582+
return self._pets[name]
583+
except KeyError:
584+
return default_value
585+
559586
@cached_property("_cs_super_troops")
560587
def super_troops(self) -> List[Troop]:
561588
"""List[:class:`Troop`]: A :class:`List` of the player's super troops.
@@ -573,7 +600,7 @@ def super_troops(self) -> List[Troop]:
573600
return list(sorted(self._super_troops, key=lambda t: order.get(t.name, 0)))
574601

575602
def get_troop(self, name: str, is_home_troop=None, default_value=None) -> Optional[Troop]:
576-
"""Returns a troop with the given name.
603+
"""Gets a troop with the given name.
577604
578605
Parameters
579606
-----------
@@ -626,16 +653,19 @@ def heroes(self) -> List[Hero]:
626653
return list(sorted_heroes.values())
627654

628655
def get_hero(self, name: str, default_value=None) -> Optional[Hero]:
629-
"""
630-
Gets the hero
656+
"""Gets the hero with the given name.
657+
631658
Parameters
632-
----------
633-
name
634-
default_value
659+
-----------
660+
name: :class:`str`
661+
The name of a hero as found in-game.
662+
default_value:
663+
The default value to return if a hero with ``name`` is not found. Defaults to ``None``.
635664
636665
Returns
637-
-------
638-
Hero
666+
--------
667+
Optional[:class:`Hero`]
668+
The returned hero or the ``default_value`` if not found, which defaults to ``None``.
639669
640670
"""
641671
if not self._heroes:
@@ -661,7 +691,7 @@ def spells(self) -> List[Spell]:
661691
key=lambda s: order.get(s.name, 0)))
662692

663693
def get_spell(self, name: str, default_value=None) -> Optional[Spell]:
664-
"""Returns a spell with the given name.
694+
"""Gets the spell with the given name.
665695
666696
Parameters
667697
-----------

0 commit comments

Comments
 (0)