Skip to content

Commit 624417b

Browse files
authored
Sync Support! (#5)
* v2.1.0: Sync Support! * some minor fixes * remove await in blocking test * remove unnecessary import
1 parent 3f7a400 commit 624417b

File tree

17 files changed

+260
-109
lines changed

17 files changed

+260
-109
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
# Change Log
22
All notable changes to this project will be documented in this file.
33

4+
## [2.1.0] - 11/29/18
5+
### Added
6+
- Synchronous support! You can now set if you want an async client by using `is_async=True`
7+
### Fixed
8+
- `asyncio.TimeoutError` now properly raises `ServerError`
9+
### Removed
10+
- `BadRequest` and `NotFoundError` (negates v2.0.6). These were found to not be needed
11+
412
## [2.0.7] - 11/29/18
513
### Added
614
- Support for the new `/events` endpoint for current and upcoming event rotations

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
8. Add the necessary points to `CHANGELOG.md`
1111
9. Fill up the `tests/.env` file with the suitable tokens
1212
10. Run `flake8` from the root folder (there are certain ignored errors defined in `tox.ini`)
13-
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.
13+
11. Run `tox` from the root folder and ensure the tests are configured correctly and they return OK. ServerErrors can be disregarded.
1414
12. Open your PR
1515

1616
Do not increment version numbers but update `CHANGELOG.md`

README.rst

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ BrawlStats
1313
:target: https://github.com/SharpBit/brawlstats/blob/master/LICENSE
1414
:alt: MIT License
1515

16-
This library is an async wrapper `Brawl Stars API`_
16+
This library is an async/sync wrapper `Brawl Stars API`_
17+
18+
Features
19+
~~~~~~~~
20+
21+
- Covers the full API
22+
- Easy to use async or sync client
1723

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

4450
Examples
4551
~~~~~~~~
46-
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)
52+
Examples are in the `examples folder`_.
53+
54+
- ``sync.py`` includes a basic sync usage
55+
- ``async.py`` includes a basic usage using asyncio
56+
- ``discord_cog.py`` shows an example Discord Bot cog using discord.py rewrite (v1.0.0a)
4757

4858
.. _Brawl Stars API: http://brawlapi.cf/api
4959
.. _docs folder: https://github.com/SharpBit/brawlstats/tree/master/docs

brawlstats/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
############
88

99

10-
__version__ = 'v2.0.7'
10+
__version__ = 'v2.1.0'
1111
__title__ = 'brawlstats'
1212
__license__ = 'MIT'
1313
__author__ = 'SharpBit'

brawlstats/core.py

Lines changed: 123 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,78 @@
11
import aiohttp
22
import asyncio
3+
import requests
34

4-
from box import Box, BoxKeyError
5+
import json
56

6-
from .errors import BadRequest, InvalidTag, NotFoundError, Unauthorized, UnexpectedError, ServerError
7+
from box import Box, BoxList
8+
9+
from .errors import InvalidTag, Unauthorized, UnexpectedError, ServerError
710
from .utils import API
811

912

10-
class BaseBox(Box):
11-
def __init__(self, *args, **kwargs):
12-
kwargs['camel_killer_box'] = True
13-
super().__init__(*args, **kwargs)
13+
class BaseBox:
14+
def __init__(self, client, data):
15+
self.client = client
16+
self.from_data(data)
17+
18+
def from_data(self, data):
19+
self.raw_data = data
20+
if isinstance(data, list):
21+
self._boxed_data = BoxList(
22+
data, camel_killer_box=True
23+
)
24+
else:
25+
self._boxed_data = Box(
26+
data, camel_killer_box=True
27+
)
28+
return self
29+
30+
def __getattr__(self, attr):
31+
try:
32+
return getattr(self._boxed_data, attr)
33+
except AttributeError:
34+
try:
35+
return super().__getattr__(attr)
36+
except AttributeError:
37+
return None # makes it easier on the user's end
38+
39+
def __getitem__(self, item):
40+
try:
41+
return getattr(self._boxed_data, item)
42+
except AttributeError:
43+
raise KeyError('No such key: {}'.format(item))
1444

1545

1646
class Client:
1747
"""
18-
This is an async client class that lets you access the API.
48+
This is a sync/async client class that lets you access the API.
1949
2050
Parameters
2151
------------
2252
token: str
2353
The API Key that you can get from https://discord.me/BrawlAPI
24-
timeout: Optional[int] = 5
54+
timeout: Optional[int] = 10
2555
A timeout for requests to the API.
26-
session: Optional[Session] = aiohttp.ClientSession()
27-
Use a current aiohttp session or a new one.
28-
loop: Optional[Loop] = None
29-
Use a current loop. Recommended to remove warnings when you run the program.
56+
session: Optional[Session] = None
57+
Use a current session or a make new one.
58+
is_async: Optional[bool] = False
59+
Makes the client async.
3060
"""
3161

3262
def __init__(self, token, **options):
33-
loop = options.get('loop', asyncio.get_event_loop())
34-
self.session = options.get('session', aiohttp.ClientSession(loop=loop))
35-
self.timeout = options.get('timeout', 5)
63+
self.is_async = options.get('is_async', False)
64+
self.session = options.get('session', aiohttp.ClientSession() if self.is_async else requests.Session())
65+
self.timeout = options.get('timeout', 10)
3666
self.headers = {
3767
'Authorization': token,
3868
'User-Agent': 'brawlstats | Python'
3969
}
4070

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

44-
async def close(self):
45-
return await self.session.close()
74+
def close(self):
75+
return self.session.close()
4676

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

86+
def _raise_for_status(self, resp, text, url):
87+
try:
88+
data = json.loads(text)
89+
except json.JSONDecodeError:
90+
data = text
91+
92+
code = getattr(resp, 'status', None) or getattr(resp, 'status_code')
93+
94+
if 300 > code >= 200:
95+
return data
96+
if code == 401:
97+
raise Unauthorized(url, code)
98+
if code in (400, 404):
99+
raise InvalidTag(url, code)
100+
if code >= 500:
101+
raise ServerError(url, code)
102+
103+
raise UnexpectedError(url, code)
104+
56105
async def _aget(self, url):
57106
try:
58107
async with self.session.get(url, timeout=self.timeout, headers=self.headers) as resp:
59-
if resp.status == 200:
60-
raw_data = await resp.json()
61-
elif resp.status == 400:
62-
raise BadRequest(url, resp.status)
63-
elif resp.status == 401:
64-
raise Unauthorized(url, resp.status)
65-
elif resp.status == 404:
66-
raise InvalidTag(url, resp.status)
67-
elif resp.status in (503, 520, 521):
68-
raise ServerError(url, resp.status)
69-
else:
70-
raise UnexpectedError(url, resp.status)
108+
return self._raise_for_status(resp, await resp.text(), url)
71109
except asyncio.TimeoutError:
72-
raise NotFoundError(url, 400)
73-
return raw_data
110+
raise ServerError(url, 503)
74111

75-
async def get_profile(self, tag: str):
112+
def _get(self, url):
113+
try:
114+
with self.session.get(url, timeout=self.timeout, headers=self.headers) as resp:
115+
return self._raise_for_status(resp, resp.text, url)
116+
except requests.Timeout:
117+
raise ServerError(url, 503)
118+
119+
async def _get_profile_async(self, tag: str):
120+
response = await self._aget(API.PROFILE + '/' + tag)
121+
return Profile(self, response)
122+
123+
def get_profile(self, tag: str):
76124
"""Get a player's stats.
77125
78126
Parameters
@@ -84,14 +132,19 @@ async def get_profile(self, tag: str):
84132
Returns Profile
85133
"""
86134
tag = self._check_tag(tag, API.PROFILE)
87-
response = await self._aget(API.PROFILE + '/' + tag)
88-
response['client'] = self
135+
if self.is_async:
136+
return self._get_profile_async(tag)
137+
response = self._get(API.PROFILE + '/' + tag)
89138

90-
return Profile(response)
139+
return Profile(self, response)
91140

92141
get_player = get_profile
93142

94-
async def get_band(self, tag: str):
143+
async def _get_band_async(self, tag: str):
144+
response = await self._aget(API.BAND + '/' + tag)
145+
return Band(self, response)
146+
147+
def get_band(self, tag: str):
95148
"""Get a band's stats.
96149
97150
Parameters
@@ -103,11 +156,17 @@ async def get_band(self, tag: str):
103156
Returns Band
104157
"""
105158
tag = self._check_tag(tag, API.BAND)
106-
response = await self._aget(API.BAND + '/' + tag)
159+
if self.is_async:
160+
return self._get_band_async(tag)
161+
response = self._get(API.BAND + '/' + tag)
162+
163+
return Band(self, response)
107164

108-
return Band(response)
165+
async def _get_leaderboard_async(self, url):
166+
response = await self._aget(url)
167+
return Leaderboard(self, response)
109168

110-
async def get_leaderboard(self, player_or_band: str, count: int=200):
169+
def get_leaderboard(self, player_or_band: str, count: int=200):
111170
"""Get the top count players/bands.
112171
113172
Parameters
@@ -126,17 +185,25 @@ async def get_leaderboard(self, player_or_band: str, count: int=200):
126185
if player_or_band.lower() not in ('players', 'bands') or count > 200 or count < 1:
127186
raise ValueError("Please enter 'players' or 'bands' or make sure 'count' is between 1 and 200.")
128187
url = API.LEADERBOARD + '/' + player_or_band + '/' + str(count)
129-
response = await self._aget(url)
188+
if self.is_async:
189+
return self._get_leaderboard_async(url)
190+
response = self._get(url)
191+
192+
return Leaderboard(self, response)
130193

131-
return Leaderboard(response)
194+
async def _get_events_async(self):
195+
response = await self._aget(API.EVENTS)
196+
return Events(self, response)
132197

133-
async def get_events(self):
198+
def get_events(self):
134199
"""Get current and upcoming events.
135200
136201
Returns Events"""
137-
response = await self._aget(API.EVENTS)
202+
if self.is_async:
203+
return self._get_events_async()
204+
response = self._get(API.EVENTS)
138205

139-
return Events(response)
206+
return Events(self, response)
140207

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

152-
async def get_band(self, full=False):
219+
def get_band(self, full=False):
153220
"""
154221
Gets the player's band.
155222
@@ -163,10 +230,9 @@ async def get_band(self, full=False):
163230
if not self.band:
164231
return None
165232
if not full:
166-
self.band['client'] = self.client
167-
band = SimpleBand(self.band)
233+
band = SimpleBand(self, self.band)
168234
else:
169-
band = await self.client.get_band(self.band.tag)
235+
band = self.client.get_band(self.band.tag)
170236
return band
171237

172238

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

184-
async def get_full(self):
250+
def get_full(self):
185251
"""
186252
Gets the full band statistics.
187253
188254
Returns Band
189255
"""
190-
return await self.client.get_band(self.tag)
256+
return self.client.get_band(self.tag)
191257

192258

193259
class Band(BaseBox):
@@ -208,16 +274,14 @@ class Leaderboard(BaseBox):
208274
"""
209275

210276
def __repr__(self):
211-
try:
212-
return "<Leaderboard object type='players' count={}>".format(len(self.players))
213-
except BoxKeyError:
214-
return "<Leaderboard object type='bands' count={}>".format(len(self.bands))
277+
lb_type = 'player' if self.players else 'band'
278+
count = len(self.players) if self.players else len(self.bands)
279+
return "<Leaderboard object type='{}' count={}>".format(lb_type, count)
215280

216281
def __str__(self):
217-
try:
218-
return 'Player Leaderboard containing {} items'.format(len(self.players))
219-
except BoxKeyError:
220-
return 'Band Leaderboard containing {} items'.format(len(self.bands))
282+
lb_type = 'Player' if self.players else 'Band'
283+
count = len(self.players) if self.players else len(self.bands)
284+
return '{} Leaderboard containing {} items'.format(lb_type, count)
221285

222286
class Events(BaseBox):
223287
"""

brawlstats/errors.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,6 @@ def __init__(self, code, error):
55
pass
66

77

8-
class BadRequest(RequestError):
9-
"""Raised if you sent a bad request to the API."""
10-
11-
def __init__(self, url, code):
12-
self.code = code
13-
self.error = 'You sent a bad request the API.\nURL: ' + url
14-
super().__init__(self.code, self.error)
15-
16-
17-
class NotFoundError(RequestError):
18-
"""Raised if the tag was not found."""
19-
20-
def __init__(self, url, code):
21-
self.code = code
22-
self.error = 'The tag you entered was not found.\nURL: ' + url
23-
super().__init__(self.code, self.error)
24-
25-
268
class Unauthorized(RequestError):
279
"""Raised if your API Key is invalid or blocked."""
2810

0 commit comments

Comments
 (0)