Skip to content

Commit

Permalink
Switched to aiohttp
Browse files Browse the repository at this point in the history
  • Loading branch information
LuCkEr- committed Aug 16, 2021
1 parent edf4966 commit a718a0c
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 141 deletions.
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ requests~=2.26.0
OutCache~=0.0.3
setuptools~=57.0.0
marshmallow~=3.13.0
dataclasses-json~=0.5.4
dataclasses-json~=0.5.4
aiohttp~=3.7.4.post0
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="PyScoreSaber",
version="0.0.10",
version="0.0.11",
author="LuCkEr-",
author_email="lucker@lucker.xyz",
description="Score Saber API client",
Expand All @@ -24,7 +24,7 @@
packages=setuptools.find_packages(where="src"),
python_requires=">=3.8",
install_requires=[
"requests",
"aiohttp",
"python-dateutil",
"OutCache"
]
Expand Down
58 changes: 0 additions & 58 deletions src/pyscoresaber/common.py

This file was deleted.

2 changes: 1 addition & 1 deletion src/pyscoresaber/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ def __init__(self, response, message=None):
if message is not None:
self.message = message
else:
self.message = f"Got HTTP status code {response.status_code} for {response.url}"
self.message = f"Got HTTP status code {response.status} for {response.url}"

super().__init__(self.message)

Expand Down
79 changes: 79 additions & 0 deletions src/pyscoresaber/httpClient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import asyncio
import logging
from json import JSONDecodeError

import aiohttp

from .errors import RateLimitedException, NotFoundException, ServerErrorException


class HttpClient:
WAIT_RATE_LIMIT = 60
WAIT_SERVER_ERROR = 5

def __init__(self):
self.__session = None

async def start(self):
if self.__session is None:
self.__session = aiohttp.ClientSession()

async def close(self):
if self.__session:
await self.__session.close()

async def request(self, method, url, **kwargs):
async def attempt():
async with self.__session.request(method, url, **kwargs) as response:
self.verify_response(response)
return await self.get_json(response)

return await self.request_attempt(attempt)

@staticmethod
async def get_json(response):
try:
data = await response.json()
except JSONDecodeError:
logging.exception("JSONDecodeError, response: %r, response.text: %r", response, response.text)
data = {"error": "Failed to decode json from scoresaber. Somethings broken."}

return data

@staticmethod
def verify_response(response):
if response.status == 429:
raise RateLimitedException(response)

if 400 <= response.status < 500:
raise NotFoundException(response)

if 500 <= response.status < 600:
raise ServerErrorException(response)

async def request_attempt(self, func):
attempt = 0
attempting = True
last_exception = None

while attempting and attempt < 6:
try:
result = await func()

attempting = False
return result
except ServerErrorException as error:
attempt += 1
logging.info(str(error))
logging.info(f"Waiting {self.WAIT_SERVER_ERROR} seconds...")
last_exception = error
await asyncio.sleep(self.WAIT_SERVER_ERROR)
except RateLimitedException as error:
attempt += 1
logging.info(str(error))
logging.info(f"Waiting {self.WAIT_RATE_LIMIT} seconds...")
last_exception = error
await asyncio.sleep(self.WAIT_RATE_LIMIT)

if last_exception is not None:
raise last_exception
37 changes: 22 additions & 15 deletions src/pyscoresaber/scoresaber.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import logging
from json.decoder import JSONDecodeError
from typing import List, Dict

import requests
from outcache import CacheAsync

from .httpClient import HttpClient
from .models import Player, Score
from .common import Common


class ScoreSaber:
Expand All @@ -16,20 +14,29 @@ class ScoreSaber:
def __init__(self):
self.log = logging.getLogger(__name__)

async def _process_url(self, url: str) -> Dict:
response = await Common.request(requests.get, url, timeout=self.TIMEOUT)
self._http = HttpClient()

try:
data = response.json()
except JSONDecodeError:
self.log.exception("JSONDecodeError, response: %r, response.text: %r", response, response.text)
data = {"error": "Failed to decode json from scoresaber. Somethings broken."}
async def __aenter__(self):
await self.start()
return self

return data
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.close()

async def start(self):
await self._http.start()

async def close(self):
await self._http.close()

async def _process_url(self, method: str, url: str) -> Dict:
await self._http.start()

return await self._http.request(method, url, timeout=self.TIMEOUT)

@CacheAsync(minutes=2)
async def _get_player_basic(self, player_id: str) -> Dict:
return await self._process_url(f"{self._url}/player/{player_id}/basic")
return await self._process_url('GET', f"{self._url}/player/{player_id}/basic")

async def get_player_basic(self, player_id: str) -> Player:
response = await self._get_player_basic(player_id)
Expand All @@ -38,7 +45,7 @@ async def get_player_basic(self, player_id: str) -> Player:

@CacheAsync(minutes=2)
async def _get_player_full(self, player_id: str) -> Dict:
return await self._process_url(f"{self._url}/player/{player_id}/full")
return await self._process_url('GET', f"{self._url}/player/{player_id}/full")

async def get_player_full(self, player_id: str) -> Player:
response = await self._get_player_full(player_id)
Expand All @@ -47,7 +54,7 @@ async def get_player_full(self, player_id: str) -> Player:

@CacheAsync(minutes=2)
async def _get_recent_scores(self, player_id: str, page: int = 1) -> Dict:
return await self._process_url(f"{self._url}/player/{player_id}/scores/recent/{page}")
return await self._process_url('GET', f"{self._url}/player/{player_id}/scores/recent/{page}")

async def get_recent_scores(self, player_id: str, page: int = 1) -> List[Score]:
response = await self._get_recent_scores(player_id, page)
Expand All @@ -61,7 +68,7 @@ async def get_recent_scores(self, player_id: str, page: int = 1) -> List[Score]:

@CacheAsync(minutes=2)
async def _get_top_scores(self, player_id: str, page: int = 1) -> Dict:
return await self._process_url(f"{self._url}/player/{player_id}/scores/top/{page}")
return await self._process_url('GET', f"{self._url}/player/{player_id}/scores/top/{page}")

async def get_top_scores(self, player_id: str, page: int = 1) -> List[Score]:
response = await self._get_top_scores(player_id, page)
Expand Down
40 changes: 0 additions & 40 deletions tests/test_common.py

This file was deleted.

57 changes: 33 additions & 24 deletions tests/test_scoresaber.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,46 +12,55 @@ def setUp(self):
self.scoresaber = ScoreSaber()

async def test_get_player_basic_valid(self):
player = await self.scoresaber.get_player_basic(self.valid_player_id)
self.assertTrue(player.player_id == self.valid_player_id)
async with self.scoresaber as scoresaber:
player = await scoresaber.get_player_basic(self.valid_player_id)
self.assertTrue(player.player_id == self.valid_player_id)

async def test_get_player_basic_invalid(self):
with self.assertRaises(NotFoundException):
await self.scoresaber.get_player_basic(self.invalid_player_id)
async with self.scoresaber as scoresaber:
with self.assertRaises(NotFoundException):
await scoresaber.get_player_basic(self.invalid_player_id)

async def test_get_player_full_valid(self):
player = await self.scoresaber.get_player_full(self.valid_player_id)
self.assertTrue(player.player_id == self.valid_player_id)
async with self.scoresaber as scoresaber:
player = await scoresaber.get_player_full(self.valid_player_id)
self.assertTrue(player.player_id == self.valid_player_id)

async def test_get_player_full_invalid(self):
with self.assertRaises(NotFoundException):
await self.scoresaber.get_player_full(self.invalid_player_id)
async with self.scoresaber as scoresaber:
with self.assertRaises(NotFoundException):
await scoresaber.get_player_full(self.invalid_player_id)

async def test_get_recent_scores_valid(self):
scores = await self.scoresaber.get_recent_scores(self.valid_player_id)
self.assertGreater(len(scores), 0)
async with self.scoresaber as scoresaber:
scores = await scoresaber.get_recent_scores(self.valid_player_id)
self.assertGreater(len(scores), 0)

async def test_get_recent_scores_invalid(self):
with self.assertRaises(NotFoundException):
await self.scoresaber.get_recent_scores(self.invalid_player_id)
async with self.scoresaber as scoresaber:
with self.assertRaises(NotFoundException):
await scoresaber.get_recent_scores(self.invalid_player_id)

async def test_get_top_scores_valid(self):
scores_1 = await self.scoresaber.get_top_scores(self.valid_player_id, 1)
self.assertGreater(len(scores_1), 0)
async with self.scoresaber as scoresaber:
scores_1 = await scoresaber.get_top_scores(self.valid_player_id, 1)
self.assertGreater(len(scores_1), 0)

scores_2 = await self.scoresaber.get_top_scores(self.valid_player_id, 2)
self.assertGreater(len(scores_2), 0)
scores_2 = await scoresaber.get_top_scores(self.valid_player_id, 2)
self.assertGreater(len(scores_2), 0)

async def test_get_top_scores_invalid(self):
with self.assertRaises(NotFoundException):
await self.scoresaber.get_top_scores(self.invalid_player_id, 1)
async with self.scoresaber as scoresaber:
with self.assertRaises(NotFoundException):
await scoresaber.get_top_scores(self.invalid_player_id, 1)

with self.assertRaises(NotFoundException):
await self.scoresaber.get_top_scores(self.invalid_player_id, 2)
with self.assertRaises(NotFoundException):
await scoresaber.get_top_scores(self.invalid_player_id, 2)

async def test_get_top_scores_invalid_page(self):
scores_1 = await self.scoresaber.get_recent_scores(self.valid_player_id, 248)
self.assertGreater(len(scores_1), 7)
async with self.scoresaber as scoresaber:
scores_1 = await self.scoresaber.get_recent_scores(self.valid_player_id, 248)
self.assertGreater(len(scores_1), 7)

with self.assertRaises(NotFoundException):
await self.scoresaber.get_top_scores(self.valid_player_id, 12412312)
with self.assertRaises(NotFoundException):
await self.scoresaber.get_top_scores(self.valid_player_id, 12412312)

0 comments on commit a718a0c

Please sign in to comment.