Skip to content

Commit f8f414b

Browse files
committed
Merge remote-tracking branch 'origin/2.0.2_update'
2 parents 07f97bd + 563270a commit f8f414b

File tree

21 files changed

+784
-327
lines changed

21 files changed

+784
-327
lines changed

.github/workflows/tests.yml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ jobs:
1010
# You can use PyPy versions in python-version.
1111
# For example, pypy2 and pypy3
1212
matrix:
13-
python-version: [3.8]
13+
# python-version: ["3.7.13", "3.8.13", "3.9.13", "3.10"]
14+
python-version: [3.8]
1415

1516
steps:
16-
- uses: actions/checkout@v2
17+
- uses: actions/checkout@v3
1718
- name: Set up Python ${{ matrix.python-version }}
18-
uses: actions/setup-python@v2
19+
uses: actions/setup-python@v4
1920
with:
2021
python-version: ${{ matrix.python-version }}
2122
# You can test your matrix by printing the current Python version
@@ -24,7 +25,6 @@ jobs:
2425
- name: Install dependencies
2526
run: |
2627
python -m pip install --upgrade pip
27-
pip install discord.py==1.5.0
2828
pip install -r requirements.txt
2929
- name: Running examples as tests
3030
env:
@@ -35,7 +35,6 @@ jobs:
3535
LINKS_API_PASSWORD: ${{ secrets.LINKS_API_PASSWORD }}
3636
RUNNING_TESTS: true
3737
run: |
38-
python -m examples.discord_bot
3938
python -m examples.discord_links
40-
python -m examples.events
39+
python -m examples.events_example
4140
python -m examples.war_logs

.gitignore

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Byte-compiled / optimized / DLL files
2+
.idea/
23
__pycache__/
34
*.py[cod]
45
*$py.class
@@ -82,11 +83,11 @@ celerybeat-schedule
8283
*.sage.py
8384

8485
# Environments
85-
.env
86+
examples/.env
8687
.venv
87-
env/
88+
examples/.env/
8889
venv/
89-
ENV/
90+
examples/.env/
9091
env.bak/
9192
venv.bak/
9293

@@ -107,4 +108,4 @@ venv.bak/
107108
examples/creds.py
108109

109110
# vscode
110-
.vscode/
111+
.vscode/

README.rst

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Getting Started
2626

2727
Installing
2828
-----------
29-
**Python 3.5 or higher is required**
29+
**Python 3.7 or higher is required**
3030

3131
.. code:: sh
3232
@@ -47,26 +47,39 @@ This example will get a player with a certain tag, and search for 5 clans with a
4747

4848
.. code:: py
4949
50+
import asyncio
5051
import coc
5152
52-
client = coc.login('email', 'password')
5353
5454
async def main():
55+
coc_client = coc.Client()
56+
try:
57+
await coc_client.login("email", "password")
58+
except coc.invalidcredentials as error:
59+
exit(error)
60+
5561
player = await client.get_player("tag")
56-
print("{0.name} has {0.trophies} trophies!".format(player))
62+
print(f"{player.name} has {player.trophies} trophies!")
5763
58-
clans = await client.search_clans(name="Best Clan Ever", limit=5)
64+
clans = await client.search_clans(name="best clan ever", limit=5)
5965
for clan in clans:
60-
print("{0.name} ({0.tag}) has {0.member_count} members".format(clan))
66+
print(f"{clan.name} ({clan.tag}) has {clan.member_count} members")
6167
6268
try:
6369
war = await client.get_current_war("#clantag")
64-
print("{0.clan_tag} is currently in {0.state} state.".format(war))
65-
except coc.PrivateWarLog:
66-
print("Uh oh, they have a private war log!")
70+
print(f"{war.clan_tag} is currently in {war.state} state.")
71+
except coc.privatewarlog:
72+
print("uh oh, they have a private war log!")
6773
68-
client.loop.run_until_complete(main())
69-
client.close()
74+
# make sure to close the session or you will get asyncio
75+
# task pending errors
76+
await client.close()
77+
78+
if __name__ == "__main__":
79+
try:
80+
asyncio.run(main())
81+
except KeyboardInterrupt:
82+
pass
7083
7184
Basic Events Example
7285
---------------------
@@ -75,24 +88,52 @@ whenever someone joins the clan or a member of the clan donates troops.
7588

7689
.. code:: py
7790
91+
import asyncio
92+
import logging
93+
7894
import coc
7995
80-
client = coc.login('email', 'password', client=coc.EventsClient)
8196
82-
@client.event
83-
@coc.ClanEvents.member_join(tags=["#clantag", "#clantag2"])
97+
@coc.ClanEvents.member_join()
8498
async def foo(player, clan):
85-
print("{0.name} ({0.tag}) just joined {1.name} ({1.tag})!".format(player, clan))
99+
print(f"{player.name} ({player.tag}) just joined {clan.name} ({clan.tag})")
100+
86101
87-
@client.event
88-
@coc.ClanEvents.member_donations(tags=["#clantag", "#clantag2"])
102+
@coc.ClanEvents.member_donations()
89103
async def bar(old_member, member):
90104
troops_donated = member.donations - old_member.donations
91-
print("{0} just donated {1} troops!".format(member.name, troops_donated))
105+
print(f"{member.name} just donated {troops_donated} troops!")
106+
92107
93-
client.run_forever()
108+
async def main():
109+
coc_client = coc.EVentsClient()
110+
try:
111+
await coc.login("email", "password")
112+
except coc.InvalidCredentials as error:
113+
exit(error)
114+
115+
# Register all the clans you want to monitor
116+
list_of_clan_tags = ["tag1", "tag2", "tag3"]
117+
coc_client.add_clan_updates(*list_of_clan_tags)
118+
119+
# Register the callbacks for each of the events you are monitoring
120+
coc_client.add_events(
121+
foo,
122+
bar
123+
)
94124
95125
126+
if __name__ == "__main__":
127+
logging.basicConfig(level=logging.INFO)
128+
log = logging.getLogger()
129+
130+
loop = asyncio.get_event_loop()
131+
try:
132+
loop.run_until_complete(main())
133+
loop.run_forever()
134+
except KeyboardInterrupt:
135+
pass
136+
96137
For more examples see the examples directory
97138

98139
Contributing

coc/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
SOFTWARE.
2323
"""
2424

25-
__version__ = "2.0.1"
25+
__version__ = "2.1.0"
2626

2727
from .abc import BasePlayer, BaseClan
2828
from .clans import RankedClan, Clan

coc/abc.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,11 @@ def get_detailed_members(self, cls: Type["Player"] = None, load_game_data: bool
105105
if load_game_data and not isinstance(load_game_data, bool):
106106
raise TypeError("load_game_data must be either True or False.")
107107

108-
return PlayerIterator(self._client, (p.tag for p in self.members), cls=cls, load_game_data=load_game_data)
108+
return PlayerIterator(self._client,
109+
(p.tag for p in self.members),
110+
cls=cls,
111+
load_game_data=load_game_data,
112+
members=self.members_dict)
109113

110114

111115
class BasePlayer:

coc/clans.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626

2727
from .players import ClanMember
28-
from .miscmodels import try_enum, ChatLanguage, Location, Label, WarLeague
28+
from .miscmodels import try_enum, ChatLanguage, Location, Label, WarLeague, CapitalDistrict
2929
from .utils import get, cached_property, correct_tag
3030
from .abc import BaseClan
3131

@@ -129,6 +129,9 @@ class Clan(BaseClan):
129129
member_cls: :class:`coc.ClanMember`
130130
The type which the members found in :attr:`Clan.members` will be of.
131131
Ensure any overriding of this inherits from :class:`coc.ClanMember`.
132+
capital_district_cls: :class:`coc.CapitalDistrict`
133+
The type which the clan capital districts found in :attr:`Clan.capital_districts` will be of.
134+
Ensure any overriding of this inherits from :class:`coc.CapitalDistrict`.
132135
war_league: :class:`coc.WarLeague`
133136
The clan's CWL league.
134137
"""
@@ -149,24 +152,28 @@ class Clan(BaseClan):
149152
"member_count",
150153
"_labels",
151154
"_members",
155+
"_districts",
152156
"_client",
153157
"label_cls",
154158
"member_cls",
159+
"capital_district_cls",
155160
"war_league",
156161
"chat_language",
157162

158163
"_cs_labels",
159164
"_cs_members",
165+
"_cs_members_dict",
166+
"_cs_capital_districts",
160167
"_iter_labels",
161168
"_iter_members",
169+
"_iter_capital_districts"
162170
)
163171

164172
def __init__(self, *, data, client, **_):
165173
super().__init__(data=data, client=client)
166174
self.label_cls = Label
167175
self.member_cls = ClanMember
168-
169-
self._members = None # type: typing.Optional[typing.Dict[str, ClanMember]]
176+
self.capital_district_cls = CapitalDistrict
170177

171178
self._from_data(data)
172179

@@ -200,6 +207,13 @@ def _from_data(self, data: dict) -> None:
200207
member_cls(data=mdata, client=self._client, clan=self) for mdata in data_get("memberList", [])
201208
)
202209

210+
capital_district_cls = self.capital_district_cls
211+
if data_get("clanCapital"):
212+
self._iter_capital_districts = (capital_district_cls(data=cddata, client=self._client) for cddata in
213+
data_get("clanCapital")["districts"])
214+
else:
215+
self._iter_capital_districts = ()
216+
203217
@cached_property("_cs_labels")
204218
def labels(self) -> typing.List[Label]:
205219
"""List[:class:`Label`]: A :class:`List` of :class:`Label` that the clan has."""
@@ -208,8 +222,18 @@ def labels(self) -> typing.List[Label]:
208222
@cached_property("_cs_members")
209223
def members(self) -> typing.List[ClanMember]:
210224
"""List[:class:`ClanMember`]: A list of members that belong to the clan."""
211-
dict_members = self._members = {m.tag: m for m in self._iter_members}
212-
return list(dict_members.values())
225+
return list(self.members_dict.values())
226+
227+
@cached_property("_cs_members_dict")
228+
def members_dict(self) -> typing.Dict[str, ClanMember]:
229+
"""Dict[str, :class:`ClanMember`]: A dict of members that belong to the clan."""
230+
return {m.tag: m for m in self._iter_members}
231+
232+
233+
@cached_property("_cs_capital_districts")
234+
def capital_districts(self) -> typing.List[CapitalDistrict]:
235+
"""List[:class:`CapitalDistrict`]: A :class:`List` of :class:`CapitalDistrict` that the clan has."""
236+
return list(self._iter_capital_districts)
213237

214238
def get_member(self, tag: str) -> typing.Optional[ClanMember]:
215239
"""Return a :class:`ClanMember` with the tag provided. Returns ``None`` if not found.
@@ -226,11 +250,11 @@ def get_member(self, tag: str) -> typing.Optional[ClanMember]:
226250
The member who matches the tag provided: Optional[:class:`ClanMember`]
227251
"""
228252
tag = correct_tag(tag)
229-
if not self._members:
253+
if not self.members_dict:
230254
_ = self.members
231255

232256
try:
233-
return self._members[tag]
257+
return self.members_dict[tag]
234258
except KeyError:
235259
return None
236260

coc/client.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2222
SOFTWARE.
2323
"""
24-
2524
import asyncio
2625
import logging
2726

@@ -258,8 +257,8 @@ async def login(self, email: str, password: str) -> None:
258257
self.http = http = self._create_client(email, password)
259258
await http.create_session(self.connector, self.timeout)
260259
await http.initialise_keys()
261-
self._create_holders()
262260

261+
self._create_holders()
263262
LOG.debug("HTTP connection created. Client is ready for use.")
264263

265264
def login_with_keys(self, *keys: str) -> None:
@@ -278,13 +277,10 @@ def login_with_keys(self, *keys: str) -> None:
278277

279278
LOG.debug("HTTP connection created. Client is ready for use.")
280279

281-
def close(self) -> None:
282-
"""Closes the HTTP connection
283-
"""
284-
LOG.info("Clash of Clans client logging out...")
285-
self.dispatch("on_client_close")
286-
self.loop.run_until_complete(self.http.close())
287-
self.loop.close()
280+
async def close(self) -> None:
281+
"""Closes the HTTP connection from within a loop function such as
282+
async def main()"""
283+
await self.http.close()
288284

289285
def dispatch(self, event_name: str, *args, **kwargs) -> None:
290286
"""Dispatches an event listener matching the `event_name` parameter."""

coc/errors.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def __init__(self, response=None, data=None):
8181
self.reason = None
8282
self.message = response
8383

84-
fmt = "Unknown Error Occured: {0}"
84+
fmt = "Error Occurred: {0}"
8585
super().__init__(fmt.format(self.message))
8686

8787

@@ -102,6 +102,8 @@ class InvalidCredentials(HTTPException):
102102
were passed. This is when your email/password pair is incorrect.
103103
Subclass of :exc:`HTTPException`
104104
"""
105+
def __init__(self, response="Invalid Credentials"):
106+
super().__init__(response=response)
105107

106108

107109
class Forbidden(HTTPException):

coc/events.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@
4141

4242

4343
class Event:
44-
"""Object that is created for an event. This contains runner functions, tags and type."""
44+
"""
45+
Object that is created for an event. This contains runner functions,
46+
tags and type.
47+
"""
4548

4649
__slots__ = ("runner", "callback", "tags", "type")
4750

@@ -719,13 +722,6 @@ def run_forever(self):
719722
except KeyboardInterrupt:
720723
self.close()
721724

722-
def close(self):
723-
"""Closes the client and all running tasks."""
724-
tasks = {t for t in asyncio.Task.all_tasks(loop=self.loop) if not t.done()}
725-
for task in tasks:
726-
task.cancel()
727-
super().close()
728-
729725
def dispatch(self, event_name: str, *args, **kwargs):
730726
# pylint: disable=broad-except
731727
registered = self._listeners["client"].get(event_name)

coc/events.pyi

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,3 @@ class EventsClient(Client):
185185
def add_events(self, *events: Callable) -> None: ...
186186
def remove_events(self, *events: Callable) -> None: ...
187187
def run_forever(self) -> None: ...
188-
def close(self) -> None: ...

0 commit comments

Comments
 (0)