Skip to content

Commit

Permalink
feat: add HyScoresAsyncClient; update requirements
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
moonburnt committed May 11, 2022
1 parent f75e4dc commit cb2d4f6
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 33 deletions.
151 changes: 127 additions & 24 deletions hscp.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Optional
from urllib.parse import urljoin as join

import aiohttp
import requests

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


class HyScoresClient:
def __init__(
self, url, app: str, timeout: int = 30, user_agent: Optional[str] = None
):
class _HSClientBase:
def __init__(self, url, app: str, timeout: int = 30):
self.url = url
self.session = requests.Session()
self.timeout = max(timeout, 0)
self.app = app

if user_agent:
self.session.headers["user-agent"] = user_agent

self._token = None

@property
def token(self):
return self._token

@token.setter
def require_token(func: callable):
def inner(self, *args, **kwargs):
if not self.token:
raise TokenUnavailable

return func(self, *args, **kwargs)

return inner

@require_token
def logout(self):
self.token = None


class HyScoresClient(_HSClientBase):
def __init__(
self, url, app: str, timeout: int = 30, user_agent: Optional[str] = None
):
super().__init__(
url=url,
app=app,
timeout=timeout,
)

self.session = requests.Session()

if user_agent:
self.session.headers["user-agent"] = user_agent

@_HSClientBase.token.setter
def token(self, val: str):
self._token = val
self.session.headers.update({"x-access-tokens": self._token})
Expand Down Expand Up @@ -80,24 +104,15 @@ def login(self, username: str, password: str):

raise AuthError

def require_token(func: callable):
def inner(self, *args, **kwargs):
if not self.token:
raise TokenUnavailable

return func(self, *args, **kwargs)

return inner

@require_token
@_HSClientBase.require_token
def get_scores(self) -> list:
return self.session.get(
join(self.url, "scores"),
timeout=self.timeout,
json={"app": self.app},
).json()["result"]

@require_token
@_HSClientBase.require_token
def get_score(self, nickname: str) -> dict:
result = self.session.get(
join(self.url, "score"),
Expand All @@ -112,7 +127,7 @@ def get_score(self, nickname: str) -> dict:
else:
raise InvalidName

@require_token
@_HSClientBase.require_token
def post_score(self, nickname: str, score: int) -> bool:
return self.session.post(
join(self.url, "score"),
Expand All @@ -124,6 +139,94 @@ def post_score(self, nickname: str, score: int) -> bool:
},
).json()["result"]

@require_token
def logout(self):
self.token = None

class HyScoresAsyncClient(_HSClientBase):
def __init__(
self, url, app: str, timeout: int = 30, user_agent: Optional[str] = None
):
super().__init__(
url=url,
app=app,
timeout=timeout,
)

self.session = aiohttp.ClientSession(
timeout=self.timeout,
)

if user_agent:
self.session.headers["user-agent"] = user_agent

@_HSClientBase.token.setter
def token(self, val: str):
self._token = val
self.session.headers.update({"x-access-tokens": self._token})

async def register(self, username: str, password: str) -> bool:
async with self.session.post(
join(self.url, "register"),
timeout=self.timeout,
auth=(username, password),
json={"app": self.app},
) as response:
data = await response.json()
return data.get("result", False)

async def login(self, username: str, password: str):
async with self.session.post(
join(self.url, "login"),
timeout=self.timeout,
auth=(username, password),
json={"app": self.app},
) as response:
data = await response.json()
result = data.get("result", None)

if result:
token = result.get("token", None)
if token:
self.token = token
return

raise AuthError

@_HSClientBase.require_token
async def get_scores(self) -> list:
async with self.session.get(
join(self.url, "scores"),
timeout=self.timeout,
json={"app": self.app},
) as response:
data = await response.json()
return data["result"]

@_HSClientBase.require_token
async def get_score(self, nickname: str) -> dict:
async with self.session.get(
join(self.url, "score"),
timeout=self.timeout,
json={
"app": self.app,
"nickname": nickname,
},
) as response:
data = await response.json()
result = data["result"]
if type(result) is dict:
return result
else:
raise InvalidName

@_HSClientBase.require_token
async def post_score(self, nickname: str, score: int) -> bool:
async with self.session.post(
join(self.url, "score"),
timeout=self.timeout,
json={
"app": self.app,
"nickname": nickname,
"score": score,
},
) as response:
data = await response.json()
return data["result"]
11 changes: 7 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "hscp"
version = "0.1.0"
version = "0.2.0"
description = "Client library for HyScores."
authors = ["moonburnt <moonburnt@disroot.org>"]
license = "MIT"
Expand All @@ -9,12 +9,15 @@ homepage = "https://github.com/moonburnt/hscp"

[tool.poetry.dependencies]
python = "^3.9"
requests = "2.26.0"
requests = "2.27.1"
aiohttp = "3.8.1"

[tool.poetry.dev-dependencies]
black = "21.10-beta.0"
pytest = "6.2.5"
black = "22.3.0"
pytest = "7.1.2"
requests-mock = "1.9.3"
aioresponses = "0.7.2"
pytest-asyncio = "0.18.3"

[build-system]
requires = ["poetry-core"]
Expand Down
109 changes: 109 additions & 0 deletions tests/test_async_hscp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import asyncio
import pytest
import pytest_asyncio

import hscp

from aioresponses import aioresponses

url = "http://example.com"
app = "hyscores"
login = "asda"
pw = "352354300n00"
token = "324234efs42bt9ffon032r0frnd0fn"


@pytest_asyncio.fixture
async def client() -> hscp.HyScoresAsyncClient:
client = hscp.HyScoresAsyncClient(
url=url,
app=app,
)
yield client
await client.session.close()


@pytest.fixture
def authorized_client(client):
client.token = token
return client


@pytest.mark.asyncio
async def test_client():
client = hscp.HyScoresAsyncClient(
url=url,
app=app,
)
assert client.url == url
assert client.app == app
await client.session.close()

user_agent = "pytest_client"
client = hscp.HyScoresAsyncClient(url=url, app=app, user_agent=user_agent)

assert client.session.headers["user-agent"] == user_agent
await client.session.close()


@pytest.mark.asyncio
async def test_token_fail(client):
with pytest.raises(hscp.TokenUnavailable):
await client.get_scores()


@pytest.mark.asyncio
async def test_register(client):
with aioresponses() as m:
m.post(url + "/register", payload={"result": True})
resp = await client.register(login, pw)

assert resp is True


@pytest.mark.asyncio
async def test_login(client):
with aioresponses() as m:
m.post(url + "/login", payload={"result": {"token": token}})
await client.login(login, pw)

assert client.token is not None


@pytest.mark.asyncio
async def test_scores(authorized_client):
with aioresponses() as m:
m.get(url + "/scores", payload={"result": []})
data = await authorized_client.get_scores()
assert isinstance(data, list)


@pytest.mark.asyncio
async def test_score(authorized_client):
with aioresponses() as m:
m.get(url + "/score", payload={"result": {"sadam": 36}})
data = await authorized_client.get_score("sadam")
assert isinstance(data, dict)


@pytest.mark.asyncio
async def test_score_fail(authorized_client):
with aioresponses() as m:
m.get(url + "/score", payload={"result": "Invalid Name"})
with pytest.raises(hscp.InvalidName):
data = await authorized_client.get_score("your mom")


@pytest.mark.asyncio
async def test_score_uploader(authorized_client):
loop = asyncio.get_event_loop_policy().new_event_loop()

with aioresponses() as m:
m.post(url + "/score", payload={"result": True})
data = await authorized_client.post_score("sadam", 69)
assert data is True


def test_logout(authorized_client):
authorized_client.logout()
assert authorized_client.token is None
9 changes: 4 additions & 5 deletions tests/test_hscp.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def authorized_client(client):
client.token = token
return client


def test_client():
client = hscp.HyScoresClient(
url=url,
Expand All @@ -31,13 +32,10 @@ def test_client():
assert client.app == app

user_agent = "pytest_client"
client = hscp.HyScoresClient(
url=url,
app=app,
user_agent=user_agent
)
client = hscp.HyScoresClient(url=url, app=app, user_agent=user_agent)
assert client.session.headers["user-agent"] == user_agent


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


def test_logout(authorized_client):
authorized_client.logout()
assert authorized_client.token is None

0 comments on commit cb2d4f6

Please sign in to comment.