Skip to content

Commit

Permalink
Sync Support! (#5)
Browse files Browse the repository at this point in the history
* v2.1.0: Sync Support!

* some minor fixes

* remove await in blocking test

* remove unnecessary import
  • Loading branch information
SharpBit authored Nov 30, 2018
1 parent 3f7a400 commit 624417b
Show file tree
Hide file tree
Showing 17 changed files with 260 additions and 109 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Change Log
All notable changes to this project will be documented in this file.

## [2.1.0] - 11/29/18
### Added
- Synchronous support! You can now set if you want an async client by using `is_async=True`
### Fixed
- `asyncio.TimeoutError` now properly raises `ServerError`
### Removed
- `BadRequest` and `NotFoundError` (negates v2.0.6). These were found to not be needed

## [2.0.7] - 11/29/18
### Added
- Support for the new `/events` endpoint for current and upcoming event rotations
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
8. Add the necessary points to `CHANGELOG.md`
9. Fill up the `tests/.env` file with the suitable tokens
10. Run `flake8` from the root folder (there are certain ignored errors defined in `tox.ini`)
11. Run `python tests/test.py` from the root folder and ensure the tests are configured correctly and they return OK. ServerErrors and warnings can be disregarded.
11. Run `tox` from the root folder and ensure the tests are configured correctly and they return OK. ServerErrors can be disregarded.
12. Open your PR

Do not increment version numbers but update `CHANGELOG.md`
14 changes: 12 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ BrawlStats
:target: https://github.com/SharpBit/brawlstats/blob/master/LICENSE
:alt: MIT License

This library is an async wrapper `Brawl Stars API`_
This library is an async/sync wrapper `Brawl Stars API`_

Features
~~~~~~~~

- Covers the full API
- Easy to use async or sync client

Installation
~~~~~~~~~~~~
Expand Down Expand Up @@ -43,7 +49,11 @@ it ASAP. If you need help or an API Key, join the API’s `discord server`_.

Examples
~~~~~~~~
Examples are in the `examples folder`_. ``async.py`` includes a basic usage using asyncio, and ``discord_cog.py`` shows an example Discord Bot cog using discord.py rewrite (v1.0.0a)
Examples are in the `examples folder`_.

- ``sync.py`` includes a basic sync usage
- ``async.py`` includes a basic usage using asyncio
- ``discord_cog.py`` shows an example Discord Bot cog using discord.py rewrite (v1.0.0a)

.. _Brawl Stars API: http://brawlapi.cf/api
.. _docs folder: https://github.com/SharpBit/brawlstats/tree/master/docs
Expand Down
2 changes: 1 addition & 1 deletion brawlstats/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
############


__version__ = 'v2.0.7'
__version__ = 'v2.1.0'
__title__ = 'brawlstats'
__license__ = 'MIT'
__author__ = 'SharpBit'
Expand Down
182 changes: 123 additions & 59 deletions brawlstats/core.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,78 @@
import aiohttp
import asyncio
import requests

from box import Box, BoxKeyError
import json

from .errors import BadRequest, InvalidTag, NotFoundError, Unauthorized, UnexpectedError, ServerError
from box import Box, BoxList

from .errors import InvalidTag, Unauthorized, UnexpectedError, ServerError
from .utils import API


class BaseBox(Box):
def __init__(self, *args, **kwargs):
kwargs['camel_killer_box'] = True
super().__init__(*args, **kwargs)
class BaseBox:
def __init__(self, client, data):
self.client = client
self.from_data(data)

def from_data(self, data):
self.raw_data = data
if isinstance(data, list):
self._boxed_data = BoxList(
data, camel_killer_box=True
)
else:
self._boxed_data = Box(
data, camel_killer_box=True
)
return self

def __getattr__(self, attr):
try:
return getattr(self._boxed_data, attr)
except AttributeError:
try:
return super().__getattr__(attr)
except AttributeError:
return None # makes it easier on the user's end

def __getitem__(self, item):
try:
return getattr(self._boxed_data, item)
except AttributeError:
raise KeyError('No such key: {}'.format(item))


class Client:
"""
This is an async client class that lets you access the API.
This is a sync/async client class that lets you access the API.
Parameters
------------
token: str
The API Key that you can get from https://discord.me/BrawlAPI
timeout: Optional[int] = 5
timeout: Optional[int] = 10
A timeout for requests to the API.
session: Optional[Session] = aiohttp.ClientSession()
Use a current aiohttp session or a new one.
loop: Optional[Loop] = None
Use a current loop. Recommended to remove warnings when you run the program.
session: Optional[Session] = None
Use a current session or a make new one.
is_async: Optional[bool] = False
Makes the client async.
"""

def __init__(self, token, **options):
loop = options.get('loop', asyncio.get_event_loop())
self.session = options.get('session', aiohttp.ClientSession(loop=loop))
self.timeout = options.get('timeout', 5)
self.is_async = options.get('is_async', False)
self.session = options.get('session', aiohttp.ClientSession() if self.is_async else requests.Session())
self.timeout = options.get('timeout', 10)
self.headers = {
'Authorization': token,
'User-Agent': 'brawlstats | Python'
}

def __repr__(self):
return '<BrawlStats-Client timeout={}>'.format(self.timeout)
return '<BrawlStats-Client async={} timeout={}>'.format(self.is_async, self.timeout)

async def close(self):
return await self.session.close()
def close(self):
return self.session.close()

def _check_tag(self, tag, endpoint):
tag = tag.upper().replace('#', '').replace('O', '0')
Expand All @@ -53,26 +83,44 @@ def _check_tag(self, tag, endpoint):
raise InvalidTag(endpoint + '/' + tag, 404)
return tag

def _raise_for_status(self, resp, text, url):
try:
data = json.loads(text)
except json.JSONDecodeError:
data = text

code = getattr(resp, 'status', None) or getattr(resp, 'status_code')

if 300 > code >= 200:
return data
if code == 401:
raise Unauthorized(url, code)
if code in (400, 404):
raise InvalidTag(url, code)
if code >= 500:
raise ServerError(url, code)

raise UnexpectedError(url, code)

async def _aget(self, url):
try:
async with self.session.get(url, timeout=self.timeout, headers=self.headers) as resp:
if resp.status == 200:
raw_data = await resp.json()
elif resp.status == 400:
raise BadRequest(url, resp.status)
elif resp.status == 401:
raise Unauthorized(url, resp.status)
elif resp.status == 404:
raise InvalidTag(url, resp.status)
elif resp.status in (503, 520, 521):
raise ServerError(url, resp.status)
else:
raise UnexpectedError(url, resp.status)
return self._raise_for_status(resp, await resp.text(), url)
except asyncio.TimeoutError:
raise NotFoundError(url, 400)
return raw_data
raise ServerError(url, 503)

async def get_profile(self, tag: str):
def _get(self, url):
try:
with self.session.get(url, timeout=self.timeout, headers=self.headers) as resp:
return self._raise_for_status(resp, resp.text, url)
except requests.Timeout:
raise ServerError(url, 503)

async def _get_profile_async(self, tag: str):
response = await self._aget(API.PROFILE + '/' + tag)
return Profile(self, response)

def get_profile(self, tag: str):
"""Get a player's stats.
Parameters
Expand All @@ -84,14 +132,19 @@ async def get_profile(self, tag: str):
Returns Profile
"""
tag = self._check_tag(tag, API.PROFILE)
response = await self._aget(API.PROFILE + '/' + tag)
response['client'] = self
if self.is_async:
return self._get_profile_async(tag)
response = self._get(API.PROFILE + '/' + tag)

return Profile(response)
return Profile(self, response)

get_player = get_profile

async def get_band(self, tag: str):
async def _get_band_async(self, tag: str):
response = await self._aget(API.BAND + '/' + tag)
return Band(self, response)

def get_band(self, tag: str):
"""Get a band's stats.
Parameters
Expand All @@ -103,11 +156,17 @@ async def get_band(self, tag: str):
Returns Band
"""
tag = self._check_tag(tag, API.BAND)
response = await self._aget(API.BAND + '/' + tag)
if self.is_async:
return self._get_band_async(tag)
response = self._get(API.BAND + '/' + tag)

return Band(self, response)

return Band(response)
async def _get_leaderboard_async(self, url):
response = await self._aget(url)
return Leaderboard(self, response)

async def get_leaderboard(self, player_or_band: str, count: int=200):
def get_leaderboard(self, player_or_band: str, count: int=200):
"""Get the top count players/bands.
Parameters
Expand All @@ -126,17 +185,25 @@ async def get_leaderboard(self, player_or_band: str, count: int=200):
if player_or_band.lower() not in ('players', 'bands') or count > 200 or count < 1:
raise ValueError("Please enter 'players' or 'bands' or make sure 'count' is between 1 and 200.")
url = API.LEADERBOARD + '/' + player_or_band + '/' + str(count)
response = await self._aget(url)
if self.is_async:
return self._get_leaderboard_async(url)
response = self._get(url)

return Leaderboard(self, response)

return Leaderboard(response)
async def _get_events_async(self):
response = await self._aget(API.EVENTS)
return Events(self, response)

async def get_events(self):
def get_events(self):
"""Get current and upcoming events.
Returns Events"""
response = await self._aget(API.EVENTS)
if self.is_async:
return self._get_events_async()
response = self._get(API.EVENTS)

return Events(response)
return Events(self, response)

class Profile(BaseBox):
"""
Expand All @@ -149,7 +216,7 @@ def __repr__(self):
def __str__(self):
return '{0.name} (#{0.tag})'.format(self)

async def get_band(self, full=False):
def get_band(self, full=False):
"""
Gets the player's band.
Expand All @@ -163,10 +230,9 @@ async def get_band(self, full=False):
if not self.band:
return None
if not full:
self.band['client'] = self.client
band = SimpleBand(self.band)
band = SimpleBand(self, self.band)
else:
band = await self.client.get_band(self.band.tag)
band = self.client.get_band(self.band.tag)
return band


Expand All @@ -181,13 +247,13 @@ def __repr__(self):
def __str__(self):
return '{0.name} (#{0.tag})'.format(self)

async def get_full(self):
def get_full(self):
"""
Gets the full band statistics.
Returns Band
"""
return await self.client.get_band(self.tag)
return self.client.get_band(self.tag)


class Band(BaseBox):
Expand All @@ -208,16 +274,14 @@ class Leaderboard(BaseBox):
"""

def __repr__(self):
try:
return "<Leaderboard object type='players' count={}>".format(len(self.players))
except BoxKeyError:
return "<Leaderboard object type='bands' count={}>".format(len(self.bands))
lb_type = 'player' if self.players else 'band'
count = len(self.players) if self.players else len(self.bands)
return "<Leaderboard object type='{}' count={}>".format(lb_type, count)

def __str__(self):
try:
return 'Player Leaderboard containing {} items'.format(len(self.players))
except BoxKeyError:
return 'Band Leaderboard containing {} items'.format(len(self.bands))
lb_type = 'Player' if self.players else 'Band'
count = len(self.players) if self.players else len(self.bands)
return '{} Leaderboard containing {} items'.format(lb_type, count)

class Events(BaseBox):
"""
Expand Down
18 changes: 0 additions & 18 deletions brawlstats/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,6 @@ def __init__(self, code, error):
pass


class BadRequest(RequestError):
"""Raised if you sent a bad request to the API."""

def __init__(self, url, code):
self.code = code
self.error = 'You sent a bad request the API.\nURL: ' + url
super().__init__(self.code, self.error)


class NotFoundError(RequestError):
"""Raised if the tag was not found."""

def __init__(self, url, code):
self.code = code
self.error = 'The tag you entered was not found.\nURL: ' + url
super().__init__(self.code, self.error)


class Unauthorized(RequestError):
"""Raised if your API Key is invalid or blocked."""

Expand Down
Loading

0 comments on commit 624417b

Please sign in to comment.