Skip to content

Commit cb2d4f6

Browse files
committed
feat: add HyScoresAsyncClient; update requirements
- Split HyScoresClient into _HSClientBase (for common things) and HyScoresClient (for non-async implementation). - Add HyScoresAsyncClient - an asynchronous implementation of hscp. - Update required packages to latest versions.
1 parent f75e4dc commit cb2d4f6

File tree

4 files changed

+247
-33
lines changed

4 files changed

+247
-33
lines changed

hscp.py

Lines changed: 127 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Optional
44
from urllib.parse import urljoin as join
55

6+
import aiohttp
67
import requests
78

89
log = logging.getLogger(__name__)
@@ -26,25 +27,48 @@ class TokenUnavailable(Exception):
2627
pass
2728

2829

29-
class HyScoresClient:
30-
def __init__(
31-
self, url, app: str, timeout: int = 30, user_agent: Optional[str] = None
32-
):
30+
class _HSClientBase:
31+
def __init__(self, url, app: str, timeout: int = 30):
3332
self.url = url
34-
self.session = requests.Session()
3533
self.timeout = max(timeout, 0)
3634
self.app = app
3735

38-
if user_agent:
39-
self.session.headers["user-agent"] = user_agent
40-
4136
self._token = None
4237

4338
@property
4439
def token(self):
4540
return self._token
4641

47-
@token.setter
42+
def require_token(func: callable):
43+
def inner(self, *args, **kwargs):
44+
if not self.token:
45+
raise TokenUnavailable
46+
47+
return func(self, *args, **kwargs)
48+
49+
return inner
50+
51+
@require_token
52+
def logout(self):
53+
self.token = None
54+
55+
56+
class HyScoresClient(_HSClientBase):
57+
def __init__(
58+
self, url, app: str, timeout: int = 30, user_agent: Optional[str] = None
59+
):
60+
super().__init__(
61+
url=url,
62+
app=app,
63+
timeout=timeout,
64+
)
65+
66+
self.session = requests.Session()
67+
68+
if user_agent:
69+
self.session.headers["user-agent"] = user_agent
70+
71+
@_HSClientBase.token.setter
4872
def token(self, val: str):
4973
self._token = val
5074
self.session.headers.update({"x-access-tokens": self._token})
@@ -80,24 +104,15 @@ def login(self, username: str, password: str):
80104

81105
raise AuthError
82106

83-
def require_token(func: callable):
84-
def inner(self, *args, **kwargs):
85-
if not self.token:
86-
raise TokenUnavailable
87-
88-
return func(self, *args, **kwargs)
89-
90-
return inner
91-
92-
@require_token
107+
@_HSClientBase.require_token
93108
def get_scores(self) -> list:
94109
return self.session.get(
95110
join(self.url, "scores"),
96111
timeout=self.timeout,
97112
json={"app": self.app},
98113
).json()["result"]
99114

100-
@require_token
115+
@_HSClientBase.require_token
101116
def get_score(self, nickname: str) -> dict:
102117
result = self.session.get(
103118
join(self.url, "score"),
@@ -112,7 +127,7 @@ def get_score(self, nickname: str) -> dict:
112127
else:
113128
raise InvalidName
114129

115-
@require_token
130+
@_HSClientBase.require_token
116131
def post_score(self, nickname: str, score: int) -> bool:
117132
return self.session.post(
118133
join(self.url, "score"),
@@ -124,6 +139,94 @@ def post_score(self, nickname: str, score: int) -> bool:
124139
},
125140
).json()["result"]
126141

127-
@require_token
128-
def logout(self):
129-
self.token = None
142+
143+
class HyScoresAsyncClient(_HSClientBase):
144+
def __init__(
145+
self, url, app: str, timeout: int = 30, user_agent: Optional[str] = None
146+
):
147+
super().__init__(
148+
url=url,
149+
app=app,
150+
timeout=timeout,
151+
)
152+
153+
self.session = aiohttp.ClientSession(
154+
timeout=self.timeout,
155+
)
156+
157+
if user_agent:
158+
self.session.headers["user-agent"] = user_agent
159+
160+
@_HSClientBase.token.setter
161+
def token(self, val: str):
162+
self._token = val
163+
self.session.headers.update({"x-access-tokens": self._token})
164+
165+
async def register(self, username: str, password: str) -> bool:
166+
async with self.session.post(
167+
join(self.url, "register"),
168+
timeout=self.timeout,
169+
auth=(username, password),
170+
json={"app": self.app},
171+
) as response:
172+
data = await response.json()
173+
return data.get("result", False)
174+
175+
async def login(self, username: str, password: str):
176+
async with self.session.post(
177+
join(self.url, "login"),
178+
timeout=self.timeout,
179+
auth=(username, password),
180+
json={"app": self.app},
181+
) as response:
182+
data = await response.json()
183+
result = data.get("result", None)
184+
185+
if result:
186+
token = result.get("token", None)
187+
if token:
188+
self.token = token
189+
return
190+
191+
raise AuthError
192+
193+
@_HSClientBase.require_token
194+
async def get_scores(self) -> list:
195+
async with self.session.get(
196+
join(self.url, "scores"),
197+
timeout=self.timeout,
198+
json={"app": self.app},
199+
) as response:
200+
data = await response.json()
201+
return data["result"]
202+
203+
@_HSClientBase.require_token
204+
async def get_score(self, nickname: str) -> dict:
205+
async with self.session.get(
206+
join(self.url, "score"),
207+
timeout=self.timeout,
208+
json={
209+
"app": self.app,
210+
"nickname": nickname,
211+
},
212+
) as response:
213+
data = await response.json()
214+
result = data["result"]
215+
if type(result) is dict:
216+
return result
217+
else:
218+
raise InvalidName
219+
220+
@_HSClientBase.require_token
221+
async def post_score(self, nickname: str, score: int) -> bool:
222+
async with self.session.post(
223+
join(self.url, "score"),
224+
timeout=self.timeout,
225+
json={
226+
"app": self.app,
227+
"nickname": nickname,
228+
"score": score,
229+
},
230+
) as response:
231+
data = await response.json()
232+
return data["result"]

pyproject.toml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "hscp"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
description = "Client library for HyScores."
55
authors = ["moonburnt <moonburnt@disroot.org>"]
66
license = "MIT"
@@ -9,12 +9,15 @@ homepage = "https://github.com/moonburnt/hscp"
99

1010
[tool.poetry.dependencies]
1111
python = "^3.9"
12-
requests = "2.26.0"
12+
requests = "2.27.1"
13+
aiohttp = "3.8.1"
1314

1415
[tool.poetry.dev-dependencies]
15-
black = "21.10-beta.0"
16-
pytest = "6.2.5"
16+
black = "22.3.0"
17+
pytest = "7.1.2"
1718
requests-mock = "1.9.3"
19+
aioresponses = "0.7.2"
20+
pytest-asyncio = "0.18.3"
1821

1922
[build-system]
2023
requires = ["poetry-core"]

tests/test_async_hscp.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import asyncio
2+
import pytest
3+
import pytest_asyncio
4+
5+
import hscp
6+
7+
from aioresponses import aioresponses
8+
9+
url = "http://example.com"
10+
app = "hyscores"
11+
login = "asda"
12+
pw = "352354300n00"
13+
token = "324234efs42bt9ffon032r0frnd0fn"
14+
15+
16+
@pytest_asyncio.fixture
17+
async def client() -> hscp.HyScoresAsyncClient:
18+
client = hscp.HyScoresAsyncClient(
19+
url=url,
20+
app=app,
21+
)
22+
yield client
23+
await client.session.close()
24+
25+
26+
@pytest.fixture
27+
def authorized_client(client):
28+
client.token = token
29+
return client
30+
31+
32+
@pytest.mark.asyncio
33+
async def test_client():
34+
client = hscp.HyScoresAsyncClient(
35+
url=url,
36+
app=app,
37+
)
38+
assert client.url == url
39+
assert client.app == app
40+
await client.session.close()
41+
42+
user_agent = "pytest_client"
43+
client = hscp.HyScoresAsyncClient(url=url, app=app, user_agent=user_agent)
44+
45+
assert client.session.headers["user-agent"] == user_agent
46+
await client.session.close()
47+
48+
49+
@pytest.mark.asyncio
50+
async def test_token_fail(client):
51+
with pytest.raises(hscp.TokenUnavailable):
52+
await client.get_scores()
53+
54+
55+
@pytest.mark.asyncio
56+
async def test_register(client):
57+
with aioresponses() as m:
58+
m.post(url + "/register", payload={"result": True})
59+
resp = await client.register(login, pw)
60+
61+
assert resp is True
62+
63+
64+
@pytest.mark.asyncio
65+
async def test_login(client):
66+
with aioresponses() as m:
67+
m.post(url + "/login", payload={"result": {"token": token}})
68+
await client.login(login, pw)
69+
70+
assert client.token is not None
71+
72+
73+
@pytest.mark.asyncio
74+
async def test_scores(authorized_client):
75+
with aioresponses() as m:
76+
m.get(url + "/scores", payload={"result": []})
77+
data = await authorized_client.get_scores()
78+
assert isinstance(data, list)
79+
80+
81+
@pytest.mark.asyncio
82+
async def test_score(authorized_client):
83+
with aioresponses() as m:
84+
m.get(url + "/score", payload={"result": {"sadam": 36}})
85+
data = await authorized_client.get_score("sadam")
86+
assert isinstance(data, dict)
87+
88+
89+
@pytest.mark.asyncio
90+
async def test_score_fail(authorized_client):
91+
with aioresponses() as m:
92+
m.get(url + "/score", payload={"result": "Invalid Name"})
93+
with pytest.raises(hscp.InvalidName):
94+
data = await authorized_client.get_score("your mom")
95+
96+
97+
@pytest.mark.asyncio
98+
async def test_score_uploader(authorized_client):
99+
loop = asyncio.get_event_loop_policy().new_event_loop()
100+
101+
with aioresponses() as m:
102+
m.post(url + "/score", payload={"result": True})
103+
data = await authorized_client.post_score("sadam", 69)
104+
assert data is True
105+
106+
107+
def test_logout(authorized_client):
108+
authorized_client.logout()
109+
assert authorized_client.token is None

tests/test_hscp.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def authorized_client(client):
2222
client.token = token
2323
return client
2424

25+
2526
def test_client():
2627
client = hscp.HyScoresClient(
2728
url=url,
@@ -31,13 +32,10 @@ def test_client():
3132
assert client.app == app
3233

3334
user_agent = "pytest_client"
34-
client = hscp.HyScoresClient(
35-
url=url,
36-
app=app,
37-
user_agent=user_agent
38-
)
35+
client = hscp.HyScoresClient(url=url, app=app, user_agent=user_agent)
3936
assert client.session.headers["user-agent"] == user_agent
4037

38+
4139
def test_token_fail(client):
4240
with pytest.raises(hscp.TokenUnavailable):
4341
client.get_scores()
@@ -74,6 +72,7 @@ def test_score_uploader(requests_mock, authorized_client):
7472
requests_mock.post(url + "/score", json={"result": True})
7573
assert authorized_client.post_score("sadam", 69) is True
7674

75+
7776
def test_logout(authorized_client):
7877
authorized_client.logout()
7978
assert authorized_client.token is None

0 commit comments

Comments
 (0)