From cb2d4f67ca11b5548845a7c7fe323cf2a23785bf Mon Sep 17 00:00:00 2001 From: moonburnt Date: Wed, 11 May 2022 00:51:57 +0300 Subject: [PATCH] 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. --- hscp.py | 151 ++++++++++++++++++++++++++++++++------- pyproject.toml | 11 +-- tests/test_async_hscp.py | 109 ++++++++++++++++++++++++++++ tests/test_hscp.py | 9 ++- 4 files changed, 247 insertions(+), 33 deletions(-) create mode 100644 tests/test_async_hscp.py diff --git a/hscp.py b/hscp.py index f141ef9..64149bb 100644 --- a/hscp.py +++ b/hscp.py @@ -3,6 +3,7 @@ from typing import Optional from urllib.parse import urljoin as join +import aiohttp import requests log = logging.getLogger(__name__) @@ -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}) @@ -80,16 +104,7 @@ 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"), @@ -97,7 +112,7 @@ def get_scores(self) -> list: 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"), @@ -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"), @@ -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"] diff --git a/pyproject.toml b/pyproject.toml index ed598a0..d7ada7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hscp" -version = "0.1.0" +version = "0.2.0" description = "Client library for HyScores." authors = ["moonburnt "] license = "MIT" @@ -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"] diff --git a/tests/test_async_hscp.py b/tests/test_async_hscp.py new file mode 100644 index 0000000..e7dde1f --- /dev/null +++ b/tests/test_async_hscp.py @@ -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 diff --git a/tests/test_hscp.py b/tests/test_hscp.py index b25467a..ce2a74e 100644 --- a/tests/test_hscp.py +++ b/tests/test_hscp.py @@ -22,6 +22,7 @@ def authorized_client(client): client.token = token return client + def test_client(): client = hscp.HyScoresClient( url=url, @@ -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() @@ -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