Skip to content

Commit a9714c4

Browse files
authored
Merge pull request #169 from mathsman5133/g7_2.3.0
Release 2.3.0
2 parents 2249c23 + 82a1000 commit a9714c4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+1873
-1182
lines changed

README.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ These can be installed with:
141141

142142
.. code:: sh
143143
144-
pip install .[docs]
144+
pip install -r doc_requirements.txt
145+
cd docs
146+
make html
145147
146148
If you wish to run linting, pylint, black and flake8 have been setup and can be run with:
147149

coc/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
SOFTWARE.
2323
"""
2424

25-
__version__ = "2.2.3"
25+
__version__ = "2.3.0"
2626

2727
from .abc import BasePlayer, BaseClan
2828
from .clans import RankedClan, Clan
@@ -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,
@@ -66,7 +66,9 @@
6666
from .miscmodels import (
6767
Achievement,
6868
Badge,
69+
CapitalDistrict,
6970
Icon,
71+
GoldPassSeason,
7072
League,
7173
LegendStatistics,
7274
LoadGameData,
@@ -78,6 +80,7 @@
7880
)
7981
from .players import Player, ClanMember, RankedPlayer
8082
from .player_clan import PlayerClan
83+
from .raid import RaidClan, RaidMember, RaidLogEntry, RaidDistrict, RaidAttack
8184
from .spell import Spell
8285
from .troop import Troop
8386
from .war_clans import WarClan, ClanWarLeagueClan

coc/abc.py

Lines changed: 88 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,21 @@
2525
from pathlib import Path
2626
from typing import AsyncIterator, Any, Dict, Type, Optional, TYPE_CHECKING
2727

28-
from .enums import Resource
28+
from .enums import PETS_ORDER, Resource
2929
from .miscmodels import try_enum, Badge, TimeDelta
3030
from .iterators import PlayerIterator
3131
from .utils import CaseInsensitiveDict, UnitStat, _get_maybe_first
3232

3333
if TYPE_CHECKING:
3434
from .players import Player
3535

36-
BUILDING_FILE_PATH = Path(__file__).parent.joinpath(Path("static/buildings.json"))
36+
BUILDING_FILE_PATH = Path(__file__).parent.joinpath(
37+
Path("static/buildings.json"))
3738

3839

3940
class BaseClan:
40-
"""An ABC that implements some common operations on clans, regardless of type.
41+
"""
42+
Abstract data class that represents base Clan objects
4143
4244
Attributes
4345
----------
@@ -50,48 +52,51 @@ class BaseClan:
5052
level: :class:`int`
5153
The clan's level.
5254
"""
55+
__slots__ = ("tag", "name", "_client", "badge", "level", "_response_retry", "_raw_data")
5356

54-
__slots__ = ("tag", "name", "_client", "badge", "level", "_response_retry")
57+
def __init__(self, *, data, client, **kwargs):
58+
self._client = client
59+
60+
self._response_retry = data.get("_response_retry")
61+
self.tag = data.get("tag")
62+
self.name = data.get("name")
63+
self.badge = try_enum(Badge, data=data.get("badgeUrls"),
64+
client=self._client)
65+
self.level = data.get("clanLevel")
66+
self._raw_data = data if client and client.raw_attribute else None
5567

5668
def __str__(self):
5769
return self.name
5870

5971
def __repr__(self):
60-
return "<%s tag=%s name=%s>" % (self.__class__.__name__, self.tag, self.name)
72+
return f"<{self.__class__.__name__} tag={self.tag} name={self.name}>"
6173

6274
def __eq__(self, other):
6375
return isinstance(other, BaseClan) and self.tag == other.tag
6476

65-
def __init__(self, *, data, client, **_):
66-
self._client = client
67-
68-
self._response_retry = data.get("_response_retry")
69-
self.tag = data.get("tag")
70-
self.name = data.get("name")
71-
self.badge = try_enum(Badge, data=data.get("badgeUrls"), client=self._client)
72-
self.level = data.get("clanLevel")
73-
7477
@property
7578
def share_link(self) -> str:
76-
""":class:`str` - A formatted link to open the clan in-game"""
77-
return "https://link.clashofclans.com/en?action=OpenClanProfile&tag=%23{}".format(self.tag.strip("#"))
79+
"""str: A formatted link to open the clan in-game"""
80+
return f"https://link.clashofclans.com/en?action=OpenClanProfile&tag=%23{self.tag.strip('#')}"
7881

7982
@property
8083
def members(self):
8184
# pylint: disable=missing-function-docstring
8285
return NotImplemented
8386

84-
def get_detailed_members(self, cls: Type["Player"] = None, load_game_data: bool = None) -> AsyncIterator["Player"]:
87+
def get_detailed_members(self, cls: Type["Player"] = None,
88+
load_game_data: bool = None) -> AsyncIterator["Player"]:
8589
"""Get detailed player information for every player in the clan.
8690
87-
This returns a :class:`PlayerIterator` which fetches all player tags in the clan in parallel.
91+
This returns a :class:`PlayerIterator` which fetches all player
92+
tags in the clan in parallel.
8893
8994
Example
9095
---------
9196
9297
.. code-block:: python3
9398
94-
clan = await client.get_clan('tag')
99+
clan = await client.get_clan(clan_tag)
95100
96101
async for player in clan.get_detailed_members():
97102
print(player.name)
@@ -125,41 +130,37 @@ class BasePlayer:
125130
The player's name
126131
"""
127132

128-
__slots__ = ("tag", "name", "_client", "_response_retry")
133+
__slots__ = ("tag", "name", "_client", "_response_retry", "_raw_data")
129134

130135
def __str__(self):
131136
return self.name
132137

133138
def __repr__(self):
134-
return "<%s tag=%s name=%s>" % (self.__class__.__name__, self.tag, self.name,)
139+
return f"<{self.__class__.__name__} tag={self.tag} name={self.name}>"
135140

136141
def __eq__(self, other):
137142
return isinstance(other, BasePlayer) and self.tag == other.tag
138143

139144
def __init__(self, *, data, client, **_):
140145
self._client = client
141146
self._response_retry = data.get("_response_retry")
142-
147+
self._raw_data = data if client and client.raw_attribute else None
143148
self.tag = data.get("tag")
144149
self.name = data.get("name")
145150

146151
@property
147152
def share_link(self) -> str:
148153
""":class:`str` - A formatted link to open the player in-game"""
149-
return "https://link.clashofclans.com/en?action=OpenPlayerProfile&tag=%23{}".format(self.tag.strip("#"))
154+
return f"https://link.clashofclans.com/en?action=OpenPlayerProfile&tag=%23{self.tag.strip('#')}"
150155

151156

152157
class DataContainerMetaClass(type):
153-
def __repr__(cls):
154-
attrs = [
155-
("name", cls.name),
156-
("id", cls.id),
157-
]
158-
return "<%s %s>" % (cls.__name__, " ".join("%s=%r" % t for t in attrs),)
158+
pass
159159

160160

161161
class DataContainer(metaclass=DataContainerMetaClass):
162162
lab_to_townhall: Dict[int, int]
163+
name: str
163164

164165
def __init__(self, data, townhall):
165166
self.name: str = data["name"]
@@ -176,7 +177,8 @@ def __repr__(self):
176177
("level", self.level),
177178
("is_active", self.is_active),
178179
]
179-
return "<%s %s>" % (self.__class__.__name__, " ".join("%s=%r" % t for t in attrs),)
180+
return "<%s %s>" % (
181+
self.__class__.__name__, " ".join("%s=%r" % t for t in attrs),)
180182

181183
@classmethod
182184
def _load_json_meta(cls, troop_meta, id, name, lab_to_townhall):
@@ -186,7 +188,8 @@ def _load_json_meta(cls, troop_meta, id, name, lab_to_townhall):
186188

187189
cls.range = try_enum(UnitStat, troop_meta.get("AttackRange"))
188190
cls.dps = try_enum(UnitStat, troop_meta.get("DPS"))
189-
cls.ground_target = _get_maybe_first(troop_meta, "GroundTargets", default=True)
191+
cls.ground_target = _get_maybe_first(troop_meta, "GroundTargets",
192+
default=True)
190193
cls.hitpoints = try_enum(UnitStat, troop_meta.get("Hitpoints"))
191194

192195
# get production building
@@ -201,32 +204,58 @@ def _load_json_meta(cls, troop_meta, id, name, lab_to_townhall):
201204
cls.is_elixir_spell = True
202205
elif production_building == "Mini Spell Factory":
203206
cls.is_dark_spell = True
207+
elif name in PETS_ORDER:
208+
production_building = "Pet Shop"
204209

205210
# load buildings
206211
with open(BUILDING_FILE_PATH) as fp:
207212
buildings = ujson.load(fp)
208213

209-
# without production_building, it is a hero or pet
214+
# without production_building, it is a hero
210215
if not production_building:
211216
laboratory_levels = troop_meta.get("LaboratoryLevel")
212217
else:
213-
# it is a troop or spell
218+
# it is a troop or spell or siege
214219
prod_unit = buildings.get(production_building)
215-
min_prod_unit_level = troop_meta.get("BarrackLevel", [None, ])[0]
216-
# there are some special troops, which have no BarrackLevel attribute
217-
if not min_prod_unit_level:
218-
laboratory_levels = troop_meta.get("LaboratoryLevel")
219-
else:
220+
if production_building in ("SiegeWorkshop", "Spell Forge", "Mini Spell Factory",
221+
"Dark Elixir Barrack", "Barrack", "Barrack2"):
222+
min_prod_unit_level = troop_meta.get("BarrackLevel", [None, ])[0]
223+
# there are some special troops, which have no BarrackLevel attribute
224+
if not min_prod_unit_level:
225+
laboratory_levels = troop_meta.get("LaboratoryLevel")
226+
else:
227+
# get the min th level were we can unlock by the required level of the production building
228+
min_th_level = [th for i, th in
229+
enumerate(prod_unit["TownHallLevel"], start=1)
230+
if i == min_prod_unit_level]
231+
# map the min th level to a lab level
232+
[first_lab_level] = [lab_level for lab_level, th_level in
233+
lab_to_townhall.items()
234+
if th_level in min_th_level]
235+
# the first_lab_level is the lowest possible (there are some inconsistencies with siege machines)
236+
# To handle them properly, replacing all lab_level lower than first_lab_level with first_lab_level
237+
laboratory_levels = []
238+
for lab_level in troop_meta.get("LaboratoryLevel"):
239+
laboratory_levels.append(max(lab_level, first_lab_level))
240+
elif production_building == "Pet Shop":
241+
min_prod_unit_level = troop_meta.get("LaboratoryLevel", [None, ])[0]
242+
# there are some special troops, which have no BarrackLevel attribute
243+
220244
# get the min th level were we can unlock by the required level of the production building
221-
min_th_level = [th for i, th in enumerate(prod_unit["TownHallLevel"], start=1)
245+
min_th_level = [th for i, th in
246+
enumerate(prod_unit["TownHallLevel"], start=1)
222247
if i == min_prod_unit_level]
223248
# map the min th level to a lab level
224-
[first_lab_level] = [lab_level for lab_level, th_level in lab_to_townhall.items()
249+
[first_lab_level] = [lab_level for lab_level, th_level in
250+
lab_to_townhall.items()
225251
if th_level in min_th_level]
226252
# the first_lab_level is the lowest possible (there are some inconsistencies with siege machines)
227253
# To handle them properly, replacing all lab_level lower than first_lab_level with first_lab_level
228-
laboratory_levels = [x if x > first_lab_level else first_lab_level
229-
for x in troop_meta.get("LaboratoryLevel")]
254+
laboratory_levels = []
255+
for lab_level in troop_meta.get("LaboratoryLevel"):
256+
laboratory_levels.append(max(lab_level, first_lab_level))
257+
else:
258+
return
230259

231260
cls.lab_level = try_enum(UnitStat, laboratory_levels)
232261
cls.housing_space = _get_maybe_first(troop_meta, "HousingSpace", default=0)
@@ -236,7 +265,9 @@ def _load_json_meta(cls, troop_meta, id, name, lab_to_townhall):
236265
# all 3
237266
cls.upgrade_cost = try_enum(UnitStat, troop_meta.get("UpgradeCost"))
238267
cls.upgrade_resource = Resource(value=troop_meta["UpgradeResource"][0])
239-
cls.upgrade_time = try_enum(UnitStat, [TimeDelta(hours=hours) for hours in troop_meta.get("UpgradeTimeH", [])])
268+
cls.upgrade_time = try_enum(UnitStat,
269+
[TimeDelta(hours=hours) for hours in
270+
troop_meta.get("UpgradeTimeH", [])])
240271
cls._is_home_village = False if troop_meta.get("VillageType") else True
241272
cls.village = "home" if cls._is_home_village else "builderBase"
242273

@@ -247,8 +278,10 @@ def _load_json_meta(cls, troop_meta, id, name, lab_to_townhall):
247278
# only heroes
248279
cls.ability_time = try_enum(UnitStat, troop_meta.get("AbilityTime"))
249280
cls.ability_troop_count = try_enum(UnitStat, troop_meta.get("AbilitySummonTroopCount"))
250-
cls.required_th_level = try_enum(UnitStat, troop_meta.get("RequiredTownHallLevel"))
251-
cls.regeneration_time = try_enum(UnitStat, [TimeDelta(minutes=value) for value in troop_meta.get("RegenerationTimeMinutes", [])])
281+
cls.required_th_level = try_enum(UnitStat, troop_meta.get("RequiredTownHallLevel") or laboratory_levels)
282+
cls.regeneration_time = try_enum(
283+
UnitStat, [TimeDelta(minutes=value) for value in troop_meta.get("RegenerationTimeMinutes", [])]
284+
)
252285

253286
cls.is_loaded = True
254287
return cls
@@ -297,7 +330,7 @@ def _load_json(self, object_ids, english_aliases, lab_to_townhall):
297330
with open(self.FILE_PATH) as fp:
298331
data = ujson.load(fp)
299332

300-
for supercell_name, meta in data.items():
333+
for c, [supercell_name, meta] in enumerate(data.items()):
301334
# Not interested if it doesn't have a TID, since it likely isn't a real troop.
302335
if not meta.get("TID"):
303336
continue
@@ -307,14 +340,17 @@ def _load_json(self, object_ids, english_aliases, lab_to_townhall):
307340
continue
308341

309342
# SC game files have "DisableProduction" true for all pet objects, which we want
310-
if "DisableProduction" in meta and "pets" not in str(self.FILE_PATH):
343+
if "DisableProduction" in meta and "pets" not in str(
344+
self.FILE_PATH):
311345
continue
312346

313-
# Little bit of a hacky way to create a "copy" of a new Troop object that hasn't been initiated yet.
314-
new_item = type(self.data_object.__name__, self.data_object.__bases__, dict(self.data_object.__dict__))
347+
# A bit of a hacky way to create a "copy" of a new Troop object that hasn't been initiated yet.
348+
new_item = type(self.data_object.__name__,
349+
self.data_object.__bases__,
350+
dict(self.data_object.__dict__))
315351
new_item._load_json_meta(
316352
meta,
317-
id=object_ids.get(supercell_name, 0),
353+
id=object_ids.get(supercell_name, c),
318354
name=english_aliases[meta["TID"][0]][0],
319355
lab_to_townhall=lab_to_townhall,
320356
)
@@ -325,8 +361,8 @@ def _load_json(self, object_ids, english_aliases, lab_to_townhall):
325361
self.loaded = True
326362

327363
def load(
328-
self, data, townhall: int, default: Type[DataContainer] = None, load_game_data: bool = True
329-
) -> DataContainer:
364+
self, data: dict, townhall: int, default: Type[DataContainer] = None,
365+
load_game_data: bool = True) -> DataContainer:
330366
if load_game_data is True:
331367
try:
332368
item = self.item_lookup[data["name"]]

0 commit comments

Comments
 (0)