Skip to content

Commit 69ffbf0

Browse files
authored
Create FullWarApi.py (#7)
Create FullWarApi.py
1 parent 1395266 commit 69ffbf0

File tree

6 files changed

+232
-6
lines changed

6 files changed

+232
-6
lines changed

coc/abc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ def share_link(self) -> str:
150150
class DataContainerMetaClass(type):
151151
def __repr__(cls):
152152
attrs = [
153-
("name", cls.name),
154-
("id", cls.id),
153+
("name", cls.name if 'name' in cls.__dict__ else "not initialized"),
154+
("id", cls.id if 'id' in cls.__dict__ else "not initialized"),
155155
]
156156
return "<%s %s>" % (cls.__name__, " ".join("%s=%r" % t for t in attrs),)
157157

coc/client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ async def login(self, email: str, password: str) -> None:
270270
self._create_holders()
271271
LOG.debug("HTTP connection created. Client is ready for use.")
272272

273-
def login_with_keys(self, *keys: str) -> None:
273+
async def login_with_keys(self, *keys: str) -> None:
274274
"""Retrieves all keys and creates an HTTP connection ready for use.
275275
276276
Parameters
@@ -281,7 +281,7 @@ def login_with_keys(self, *keys: str) -> None:
281281
http._keys = keys
282282
http.keys = cycle(http._keys)
283283
http.key_count = len(keys)
284-
self.loop.run_until_complete(http.create_session(self.connector, self.timeout))
284+
await http.create_session(self.connector, self.timeout)
285285
self._create_holders()
286286

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

coc/ext/FullWarApi.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
"""An extension that helps interact with the FullWar API."""
2+
3+
import asyncio
4+
import base64
5+
import logging
6+
import json
7+
8+
from collections import namedtuple
9+
from datetime import datetime
10+
from typing import List
11+
12+
import aiohttp
13+
14+
from ..http import json_or_text
15+
from ..utils import correct_tag
16+
from ..wars import ClanWar
17+
18+
LOG = logging.getLogger(__name__)
19+
20+
AccessToken = namedtuple("AccessToken", ["token", "expires_at"])
21+
22+
23+
def extract_expiry_from_jwt_token(token):
24+
if isinstance(token, str):
25+
token = token.encode("utf-8")
26+
elif not isinstance(token, bytes):
27+
# token was wrong somehow
28+
return None
29+
30+
try:
31+
signing, _ = token.rsplit(b".", 1)
32+
_, payload = signing.split(b".", 1)
33+
except ValueError:
34+
return None # not enough segments
35+
36+
if len(payload) % 4 > 0:
37+
payload += b"=" * (4 - len(payload) % 4)
38+
39+
bytes_payload = base64.urlsafe_b64decode(payload)
40+
dict_payload = json.loads(bytes_payload)
41+
try:
42+
expiry = dict_payload["exp"]
43+
return datetime.fromtimestamp(expiry)
44+
except KeyError:
45+
return None
46+
47+
48+
async def login(username: str, password: str, clash_client) -> "FullWarClient":
49+
"""Eases logging into the API client.
50+
51+
For more information on this project, please join the discord server - <discord.gg/Eaja7gJ>
52+
53+
You must have your username and password as given on the server.
54+
If unsure as to what this means, please reach out to an admin.
55+
56+
Parameters
57+
-----------
58+
username : str
59+
Your username as given on the discord server.
60+
password : str
61+
Your password as given on the discord server
62+
loop : Optional[:class:`asyncio.AbstractEventLoop`]
63+
The :class:`asyncio.AbstractEventLoop` to use for HTTP requests.
64+
An :func:`asyncio.get_event_loop()` will be used if ``None`` is passed
65+
"""
66+
if not isinstance(username, str) or not isinstance(password, str):
67+
raise TypeError("username and password must both be a string")
68+
if not username or not password:
69+
raise ValueError("username or password must not be an empty string.")
70+
71+
loop = asyncio.get_running_loop()
72+
return FullWarClient(username, password, clash_client, loop)
73+
74+
75+
class FullWarClient:
76+
"""An extension that helps interact with the Full War API.
77+
78+
For more information on this project, please join the discord server - <discord.gg/Eaja7gJ>
79+
80+
You must have your username and password as given on the server.
81+
If unsure as to what this means, please reach out to an admin.
82+
83+
Parameters
84+
-----------
85+
username : str
86+
Your username as given on the discord server.
87+
password : str
88+
Your password as given on the discord server
89+
clash_client: coc.Client
90+
Client to use
91+
92+
loop : Optional[:class:`asyncio.AbstractEventLoop`]
93+
The :class:`asyncio.AbstractEventLoop` to use for HTTP requests.
94+
An :func:`asyncio.get_event_loop()` will be used if ``None`` is passed
95+
96+
"""
97+
98+
BASE_URL = "http://teamutils.com:8081"
99+
100+
__slots__ = ("username", "password", "clash_client", "loop", "key", "http_session")
101+
102+
def __init__(self, username: str, password: str, clash_client, loop: asyncio.AbstractEventLoop = None):
103+
self.username = username
104+
self.password = password
105+
self.clash_client = clash_client
106+
107+
self.loop = loop or asyncio.get_event_loop()
108+
self.key = None # set in get_key()
109+
110+
self.http_session = aiohttp.ClientSession(loop=self.loop)
111+
112+
async def close(self):
113+
"""Close the client session established"""
114+
await self.http_session.close()
115+
116+
async def _request(self, method, url, *, token_request: bool = False, **kwargs):
117+
url = self.BASE_URL + url
118+
119+
if not token_request:
120+
key = await self._get_key()
121+
122+
headers = {"authorization": "Bearer {}".format(key)}
123+
kwargs["headers"] = headers
124+
125+
async with self.http_session.request(method, url, **kwargs) as response:
126+
LOG.debug("%s (%s) has returned %s", url, method, response.status)
127+
data = await json_or_text(response)
128+
LOG.debug(data)
129+
130+
if 200 <= response.status < 300:
131+
LOG.debug("%s has received %s", url, data)
132+
return data
133+
134+
if response.status == 401:
135+
await self._refresh_key()
136+
return await self._request(method, url, **kwargs)
137+
138+
async def _get_key(self):
139+
if not self.key or self.key.expires_at < datetime.utcnow():
140+
await self._refresh_key()
141+
142+
return self.key.token
143+
144+
async def _refresh_key(self):
145+
#REMOVE THIS EMAIL WHEN POSSIBLE
146+
data = {
147+
"email" : "[email protected]",
148+
"username": self.username,
149+
"password": self.password,
150+
}
151+
152+
payload = await self._request("POST", "/login", token_request=True, json=data)
153+
self.key = AccessToken(payload["access_token"], extract_expiry_from_jwt_token(payload["access_token"]))
154+
155+
async def war_result(self, clan_tag: str, preparation_start: int = 0) -> ClanWar:
156+
"""Get a stored war result.
157+
158+
Parameters
159+
----------
160+
client: coc.Client
161+
instance of the clash client
162+
clan_tag: str
163+
The clan tag to find war result for.
164+
preparation_start: int
165+
Preparation start of a specific war result to find.
166+
167+
Returns
168+
--------
169+
#NOOOOO idea if this is correct xD
170+
Optional[:class:`ClanWar`]
171+
War result, or ``None`` if no war found.
172+
"""
173+
data = await self._request("GET", f"/war_result?clan_tag={correct_tag(clan_tag, '%23')}&prep_start={str(preparation_start)}")
174+
try:
175+
return ClanWar(data=data["response"], client=self.clash_client)
176+
except (IndexError, KeyError, TypeError, ValueError):
177+
return None
178+
179+
async def war_result_log(self, clan_tag: str, preparation_start: int = 0) -> List[ClanWar]:
180+
"""Get all stored war results for a clan.
181+
182+
Parameters
183+
----------
184+
client: coc.Client
185+
instance of the clash client
186+
clan_tag: str
187+
The clan tag to find war result for.
188+
preparation_start: int
189+
Preparation start of a specific war result to find.
190+
191+
Returns
192+
--------
193+
#NOOOOO idea if this is correct xD
194+
Optional[:class:`ClanWar`]
195+
List of war results, or ``None`` if no wars found.
196+
"""
197+
data = await self._request("GET", f"/war_result_log?clan_tag={correct_tag(clan_tag, '%23')}")
198+
try:
199+
responses = data["log"]
200+
return [ClanWar(data=response["response"], client=self.clash_client) for response in responses]
201+
except (IndexError, KeyError, TypeError, ValueError):
202+
return None
203+
204+
205+
async def register_war(self, clan_tag: str, preparation_start: int = 0):
206+
"""Registers a war.
207+
208+
Parameters
209+
----------
210+
clan_tag : str
211+
The clan to register a war for
212+
preparation_start: int
213+
Preparation time of the war
214+
"""
215+
return await self._request("POST", f"/war_result?clan_tag={correct_tag(clan_tag, '%23')}&prep_start={str(preparation_start)}")
216+

coc/hero.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from typing import TYPE_CHECKING, Dict, List, Type
22
from pathlib import Path
33

4+
from attr import dataclass
5+
46
from .abc import DataContainer, DataContainerHolder
57

68
if TYPE_CHECKING:
@@ -106,6 +108,7 @@ class HeroHolder(DataContainerHolder):
106108
data_object = Hero
107109

108110

111+
@dataclass
109112
class Pet(DataContainer):
110113
"""Represents a Pet object as returned by the API, optionally filled with game data.
111114

coc/login.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,5 @@ def login_with_keys(*keys: str, client: Type[Union[Client, EventsClient]] = Clie
7878
Any kwargs you wish to pass into the Client object.
7979
"""
8080
instance = client(**kwargs)
81-
instance.login_with_keys(*keys)
81+
instance.loop.run_until_complete(instance.login_with_keys(*keys))
8282
return instance

docs/code_overview/client.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@ Example
1616
~~~~~~~
1717
.. code-block:: python3
1818
19+
import asyncio
1920
import coc
2021
21-
client = coc.login("email", "password", key_names="keys for my windows pc", key_count=5)
22+
async def main():
23+
async with coc.Client() as coc_client:
24+
await coc_client.login("email", "password")
25+
26+
# do stuff
27+
28+
asyncio.run(main())
2229
2330
With the returned instance, you can complete any of the operations detailed below.
2431

0 commit comments

Comments
 (0)