diff --git a/.env.example b/.env.example index cc5c18a..9317121 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,2 @@ -token = # Replace this with your API token -base_url = # Proxy server to handle requests to API due to IP limitations \ No newline at end of file +TOKEN = # Replace this with your API token +BASE_URL = # Proxy server to handle requests to API due to IP limitations \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..567f67e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,41 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: tests + +on: + push: + branches: [ "master", "development" ] + pull_request: + branches: [ "master", "development" ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements-dev.txt + python -m pip install . + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + - name: Test with pytest + env: + TOKEN: ${{ secrets.TOKEN }} + BASE_URL: ${{ secrets.BASE_URL }} + run: | + pytest \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..c58a27c --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,35 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.9" + # You can also specify other tool versions: + # nodejs: "20" + # rust: "1.70" + # golang: "1.20" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs + # builder: "dirhtml" + # Fail on all warnings to avoid broken references + # fail_on_warning: true + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a4bdfa6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -language: python - -python: -- '3.5' -- '3.6' -matrix: - include: - - python: 3.7 - dist: xenial - sudo: true - -install: - - pip install -r requirements-dev.txt - - pip install . -before_script: flake8 -script: tox - -notifications: - email: - - sharpbit3618@gmail.com \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d8563a5..68d8458 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ # Change Log All notable changes to this project will be documented in this file. +## [4.2.0] - 10/8/24 +### Added +- Implemented an endpoint with `Client.get_event_rotation` which gets the events in the current rotation. +- Added a method `Player.get_battle_logs` which directly gets the player's battle log. +### Fixed +- Client actually uses session passed into parameters now instead of creating a new one anyways +- `UnexpectedError` now properly shows the returned text in the message +- The `use_cache` parameter now works for `get_brawlers` and the async client +### Removed +- Removed the prevent_ratelimit option for the Client +- Dropped support for Python 3.5, 3.6, 3.7, and 3.8 +- Removed `Client.get_constants` as the site that was hosting it is no longer running + ## [4.1.1] - 10/31/21 ### Fixed - Installation dependency issue with aiohttp diff --git a/LICENSE b/LICENSE index 20164b3..502a58a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2020 SharpBit +Copyright (c) 2018-2024 SharpBit Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst b/README.rst index 8bfe38f..163085c 100644 --- a/README.rst +++ b/README.rst @@ -8,9 +8,9 @@ Brawl Stats :target: https://pypi.org/project/brawlstats/ :alt: PyPi -.. image:: https://travis-ci.com/SharpBit/brawlstats.svg?branch=master - :target: https://travis-ci.com/SharpBit/brawlstats - :alt: Travis-CI build +.. image:: https://github.com/SharpBit/brawlstats/actions/workflows/tests.yml/badge.svg + :target: https://github.com/SharpBit/brawlstats/actions/workflows/tests.yml + :alt: GitHub Actions Tests .. image:: https://img.shields.io/pypi/pyversions/brawlstats.svg :target: https://pypi.org/project/brawlstats/ @@ -20,8 +20,12 @@ Brawl Stats :target: https://github.com/SharpBit/brawlstats/blob/master/LICENSE :alt: MIT License -- This library is a sync and async wrapper for the Brawl Stars API. -- Python 3.5.3 or later is required. +.. image:: https://readthedocs.org/projects/brawlstats/badge/?version=stable + :target: https://brawlstats.readthedocs.io/en/stable/?badge=stable + :alt: Documentation Status + +- BrawlStats is a sync and async Python API wrapper to fetch statistics from the official Brawl Stars API. +- Python 3.9 or later is required. Features ~~~~~~~~ @@ -31,7 +35,8 @@ Features - Get a player profile and battlelog. - Get a club and its members. - Get the top 200 rankings for players, clubs, or a specific brawler. -- Get information about maps, brawlers, and more! +- Get information about all the brawlers in the game. +- Get information about the current event rotation! Installation ~~~~~~~~~~~~ @@ -72,13 +77,12 @@ Contributing ~~~~~~~~~~~~ Special thanks to this project's contributors ❤️ -- `4JR`_ +- `erickang21`_ +- `fourjr`_ - `golbu`_ -- `kawaii banana`_ - `kjkui`_ -- `Kyber`_ +- `kyb3r`_ - `Papiersnipper`_ -- `Pollen`_ - `OrangutanGaming`_ - `Stitch`_ @@ -86,17 +90,16 @@ If you want to contribute, whether it be a bug fix or new feature, make sure to This project is no longer actively maintained. No new features will be added, only bugfixes and security fixes will be accepted. .. _create an issue: https://github.com/SharpBit/brawlstats/issues -.. _Read the Docs: https://brawlstats.rtfd.io/ +.. _Read the Docs: https://brawlstats.readthedocs.io/en/stable/ .. _examples folder: https://github.com/SharpBit/brawlstats/tree/master/examples .. _discord.py: https://github.com/rapptz/discord.py .. _contributing guidelines: https://github.com/SharpBit/brawlstats/blob/master/CONTRIBUTING.md -.. _4JR: https://github.com/fourjr +.. _erickang21: https://github.com/erickang21 +.. _fourjr: https://github.com/fourjr .. _OrangutanGaming: https://github.com/OrangutanGaming .. _Stitch: https://github.com/Soumil07 .. _kjkui: https://github.com/kjkui -.. _Kyber: https://github.com/kyb3r +.. _kyb3r: https://github.com/kyb3r .. _Papiersnipper: https://github.com/robinmahieu -.. _Pollen: https://github.com/pollen5 -.. _kawaii banana: https://github.com/bananaboy21 .. _golbu: https://github.com/0dminnimda diff --git a/brawlstats/__init__.py b/brawlstats/__init__.py index 7bad5be..ea66010 100644 --- a/brawlstats/__init__.py +++ b/brawlstats/__init__.py @@ -7,7 +7,7 @@ ############ -__version__ = 'v4.1.1' +__version__ = 'v4.2.0' __title__ = 'brawlstats' __license__ = 'MIT' __author__ = 'SharpBit' diff --git a/brawlstats/core.py b/brawlstats/core.py index e576b91..3f756cc 100644 --- a/brawlstats/core.py +++ b/brawlstats/core.py @@ -2,7 +2,6 @@ import json import logging import sys -import time from typing import Union import aiohttp @@ -10,7 +9,7 @@ from cachetools import TTLCache from .errors import Forbidden, NotFoundError, RateLimitError, ServerError, UnexpectedError -from .models import BattleLog, Brawlers, Club, Constants, Members, Player, Ranking +from .models import BattleLog, Brawlers, Club, EventRotation, Members, Player, Ranking from .utils import API, bstag, typecasted log = logging.getLogger(__name__) @@ -29,14 +28,16 @@ class Client: How long to wait in seconds before shutting down requests, by default 30 is_async: bool, optional Setting this to ``True`` makes the client async, by default False - loop: asyncio.window_events._WindowsSelectorEventLoop, optional - The event loop to use for asynchronous operations, by default None - connector: aiohttp.TCPConnector, optional - Pass a TCPConnector into the client (aiohttp), by default None + loop: asyncio.AbstractEventLoop, optional + The event loop to use for asynchronous operations, by default None. + If you are passing in an aiohttp session, using this will not work: + you must set it when initializing the session. + connector: aiohttp.BaseConnector, optional + Pass a Connector into the client (aiohttp), by default None + If you are passing in an aiohttp session, using this will not work: + you must set it when initializing the session. debug: bool, optional Whether or not to log info for debugging, by default False - prevent_ratelimit: bool, optional - Whether or not to wait between requests to prevent being ratelimited, by default False base_url: str, optional Sets a different base URL to make request to, by default None """ @@ -53,7 +54,7 @@ def __init__(self, token, session=None, timeout=30, is_async=False, **options): self.cache = TTLCache(3200 * 3, 60 * 3) # 3200 requests per minute # Session and request options - self.session = options.get('session') or ( + self.session = session or ( aiohttp.ClientSession(loop=self.loop, connector=self.connector) if self.is_async else requests.Session() ) self.timeout = timeout @@ -64,24 +65,36 @@ def __init__(self, token, session=None, timeout=30, is_async=False, **options): # Request/response headers self.headers = { - 'Authorization': 'Bearer {}'.format(token), - 'User-Agent': 'brawlstats/{0} (Python {1[0]}.{1[1]})'.format(self.api.VERSION, sys.version_info), + 'Authorization': f'Bearer {token}', + 'User-Agent': f'brawlstats/{self.api.VERSION} (Python {sys.version_info[0]}.{sys.version_info[1]})', 'Accept-Encoding': 'gzip' } # Load brawlers for get_rankings if self.is_async: - self.loop.create_task(self.__ainit__()) + self.loop.create_task(self._ainit()) else: brawlers_info = self.get_brawlers() self.api.set_brawlers(brawlers_info) - async def __ainit__(self): + async def _ainit(self): """Task created to run `get_brawlers` asynchronously""" self.api.set_brawlers(await self.get_brawlers()) def __repr__(self): - return ''.format(self.is_async, self.timeout, self.debug) + return f'' + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + self.close() def close(self): return self.session.close() @@ -120,7 +133,7 @@ def _resolve_cache(self, url): if not data: return None if self.debug: - log.debug('GET {} got result from cache.'.format(url)) + log.debug(f'GET {url} got result from cache.') return data async def _arequest(self, url, use_cache=True): @@ -148,7 +161,7 @@ async def _arequest(self, url, use_cache=True): def _request(self, url, use_cache=True): """Sync method to request a url.""" if self.is_async: - return self._arequest(url, use_cache) + return self._arequest(url, use_cache=use_cache) # Try and retrieve from cache if use_cache: @@ -171,21 +184,7 @@ def _request(self, url, use_cache=True): async def _aget_model(self, url, model, use_cache=True, key=None): """Method to turn the response data into a Model class for the async client.""" - if self.prevent_ratelimit: - # Use self.lock if prevent_ratelimit=True - async with self.lock: - data = await self._arequest(url, use_cache) - await asyncio.sleep(0.1) - else: - data = await self._arequest(url) - - if model == Constants: - if key: - if data.get(key): - return model(self, data.get(key)) - else: - raise KeyError('No such Constants key "{}"'.format(key)) - + data = await self._arequest(url, use_cache=use_cache) return model(self, data) def _get_model(self, url, model, use_cache=True, key=None): @@ -195,16 +194,6 @@ def _get_model(self, url, model, use_cache=True, key=None): return self._aget_model(url, model=model, use_cache=use_cache, key=key) data = self._request(url, use_cache) - if self.prevent_ratelimit: - time.sleep(0.1) - - if model == Constants: - if key: - if data.get(key): - return model(self, data.get(key)) - else: - raise KeyError('No such Constants key "{}"'.format(key)) - return model(self, data) @typecasted @@ -224,7 +213,7 @@ def get_player(self, tag: bstag, use_cache=True) -> Player: Player A player object with all of its attributes. """ - url = '{}/{}'.format(self.api.PROFILE, tag) + url = f'{self.api.PROFILE}/{tag}' return self._get_model(url, model=Player, use_cache=use_cache) get_profile = get_player @@ -246,7 +235,7 @@ def get_battle_logs(self, tag: bstag, use_cache=True) -> BattleLog: BattleLog A player battle object with all of its attributes. """ - url = '{}/{}/battlelog'.format(self.api.PROFILE, tag) + url = f'{self.api.PROFILE}/{tag}/battlelog' return self._get_model(url, model=BattleLog, use_cache=use_cache) @typecasted @@ -266,7 +255,7 @@ def get_club(self, tag: bstag, use_cache=True) -> Club: Club A club object with all of its attributes. """ - url = '{}/{}'.format(self.api.CLUB, tag) + url = f'{self.api.CLUB}/{tag}' return self._get_model(url, model=Club, use_cache=use_cache) @typecasted @@ -286,7 +275,7 @@ def get_club_members(self, tag: bstag, use_cache=True) -> Members: Members A list of the members in a club. """ - url = '{}/{}/members'.format(self.api.CLUB, tag) + url = f'{self.api.CLUB}/{tag}/members' return self._get_model(url, model=Members, use_cache=use_cache) def get_rankings( @@ -300,7 +289,7 @@ def get_rankings( ranking : str The type of ranking. Must be "players", "clubs", "brawlers". region : str, optional - The region to retrieve from. Must be a 2 letter country code, by default None + The region to retrieve from. Must be a 2 letter country code, 'global', or None: by default None limit : int, optional The number of top players or clubs to fetch, by default 200 brawler : Union[str, int], optional @@ -343,31 +332,29 @@ def get_rankings( raise ValueError('Make sure limit is between 1 and 200.') # Construct URL - url = '{}/{}/{}?limit={}'.format(self.api.RANKINGS, region, ranking, limit) + url = f'{self.api.RANKINGS}/{region}/{ranking}?limit={limit}' if ranking == 'brawlers': - url = '{}/{}/{}/{}?limit={}'.format(self.api.RANKINGS, region, ranking, brawler, limit) + url = f'{self.api.RANKINGS}/{region}/{ranking}/{brawler}?limit={limit}' return self._get_model(url, model=Ranking, use_cache=use_cache) - def get_constants(self, key: str=None, use_cache=True) -> Constants: - """Gets Brawl Stars constants extracted from the app. + def get_brawlers(self, use_cache=True) -> Brawlers: + """Gets available brawlers and information about them. Parameters ---------- - key : str, optional - Any key to get specific data, by default None use_cache : bool, optional Whether to use the internal 3 minutes cache, by default True Returns ------- - Constants - Data containing some Brawl Stars constants. + Brawlers + A list of available brawlers and information about them. """ - return self._get_model(self.api.CONSTANTS, model=Constants, key=key) + return self._get_model(self.api.BRAWLERS, model=Brawlers, use_cache=use_cache) - def get_brawlers(self, use_cache=True) -> Brawlers: - """Gets available brawlers and information about them. + def get_event_rotation(self, use_cache=True) -> EventRotation: + """Gets the current events in rotation. Parameters ---------- @@ -376,7 +363,7 @@ def get_brawlers(self, use_cache=True) -> Brawlers: Returns ------- - Brawlers - A list of available brawlers and information about them. + Events + A list of the current events in rotation. """ - return self._get_model(self.api.BRAWLERS, model=Brawlers) + return self._get_model(self.api.EVENT_ROTATION, model=EventRotation, use_cache=use_cache) diff --git a/brawlstats/errors.py b/brawlstats/errors.py index 8b3a26d..f6f79fd 100644 --- a/brawlstats/errors.py +++ b/brawlstats/errors.py @@ -27,7 +27,7 @@ def __init__(self, code, **kwargs): self.reason = kwargs.pop('reason', None) self.invalid_chars = kwargs.pop('invalid_chars', []) if self.reason: - self.message += '\nReason: {}'.format(self.reason) + self.message += f'\nReason: {self.reason}' elif self.invalid_chars: self.message += 'Invalid characters: {}'.format(', '.join(self.invalid_chars)) super().__init__(self.code, self.message) @@ -49,7 +49,7 @@ class UnexpectedError(RequestError): def __init__(self, url, code, text): self.code = code self.url = url - self.message = 'An unexpected error has occured.\n{text}' + self.message = f'An unexpected error has occured.\n{text}' super().__init__(self.code, self.message) diff --git a/brawlstats/models.py b/brawlstats/models.py index 4669ed6..b04d021 100644 --- a/brawlstats/models.py +++ b/brawlstats/models.py @@ -2,7 +2,7 @@ from .utils import bstag -__all__ = ['Player', 'Club', 'Members', 'Ranking', 'BattleLog', 'Constants', 'Brawlers'] +__all__ = ['Player', 'Club', 'Members', 'Ranking', 'BattleLog', 'Brawlers', 'EventRotation'] class BaseBox: @@ -28,7 +28,7 @@ def __getitem__(self, item): try: return self._boxed_data[item] except IndexError: - raise IndexError('No such index: {}'.format(item)) + raise IndexError(f'No such index: {item}') class BaseBoxList(BaseBox): @@ -48,27 +48,37 @@ def __init__(self, client, data): super().__init__(client, data['items']) def __repr__(self): - return ''.format(len(self)) + return f'' + + +class BattleLog(BaseBoxList): + """A player battle object with all of its attributes.""" + + def __init__(self, client, data): + super().__init__(client, data['items']) class Club(BaseBox): """A club object with all of its attributes.""" def __repr__(self): - return "".format(self) + return f"" def __str__(self): - return '{0.name} ({0.tag})'.format(self) + return f'{self.name} ({self.tag})' def get_members(self) -> Members: """Gets the members of a club. + Note: It is preferred to get the members + via Club.members since this method makes + an extra API call but returns the same data. Returns ------- Members A list of the members in a club. """ - url = '{}/{}/members'.format(self.client.api.CLUB, bstag(self.tag)) + url = f'{self.client.api.CLUB}/{bstag(self.tag)}/members' return self.client._get_model(url, model=Members) @@ -80,10 +90,10 @@ def __init__(self, *args, **kwargs): self.team_victories = self.x3vs3_victories def __repr__(self): - return "".format(self) + return f"" def __str__(self): - return '{0.name} ({0.tag})'.format(self) + return f'{self.name} ({self.tag})' def get_club(self) -> Club: """Gets the player's club. @@ -100,9 +110,20 @@ async def wrapper(): return wrapper() return None - url = '{}/{}'.format(self.client.api.CLUB, bstag(self.club.tag)) + url = f'{self.client.api.CLUB}/{bstag(self.club.tag)}' return self.client._get_model(url, model=Club) + def get_battle_logs(self) -> BattleLog: + """Gets the player's battle logs. + + Returns + ------- + BattleLog + The battle log containing the player's most recent battles. + """ + url = f'{self.client.api.PROFILE}/{bstag(self.tag)}/battlelog' + return self.client._get_model(url, model=BattleLog) + class Ranking(BaseBoxList): """A player or club ranking that contains a list of players or clubs.""" @@ -114,20 +135,13 @@ def __repr__(self): return ''.format(len(self)) -class BattleLog(BaseBoxList): - """A player battle object with all of its attributes.""" +class Brawlers(BaseBoxList): + """A list of available brawlers and information about them.""" def __init__(self, client, data): super().__init__(client, data['items']) -class Constants(BaseBox): - """Data containing some Brawl Stars constants.""" +class EventRotation(BaseBoxList): + """A list of events in the current rotation.""" pass - - -class Brawlers(BaseBoxList): - """A list of available brawlers and information about them.""" - - def __init__(self, client, data): - super().__init__(client, data['items']) diff --git a/brawlstats/utils.py b/brawlstats/utils.py index 710b7b6..e042941 100644 --- a/brawlstats/utils.py +++ b/brawlstats/utils.py @@ -10,12 +10,12 @@ class API: def __init__(self, base_url, version=1): - self.BASE = base_url or 'https://api.brawlstars.com/v{}'.format(version) + self.BASE = base_url or f'https://api.brawlstars.com/v{version}' self.PROFILE = self.BASE + '/players' self.CLUB = self.BASE + '/clubs' self.RANKINGS = self.BASE + '/rankings' - self.CONSTANTS = 'https://fourjr.herokuapp.com/bs/constants' self.BRAWLERS = self.BASE + '/brawlers' + self.EVENT_ROTATION = self.BASE + '/events/rotation' # Get package version from __init__.py path = os.path.dirname(__file__) diff --git a/docs/api.rst b/docs/api.rst index 75ad25b..c9ecaf9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -25,10 +25,10 @@ Data Models .. autoclass:: brawlstats.models.Members :members: -.. autoclass:: brawlstats.models.Constants +.. autoclass:: brawlstats.models.Brawlers :members: -.. autoclass:: brawlstats.models.Brawlers +.. autoclass:: brawlstats.models.EventRotation :members: @@ -42,12 +42,11 @@ Player A full player object (all its statistics) - Attributes: -============================================ ============= +============================================ ====================== Name Type -============================================ ============= +============================================ ====================== ``tag`` str ``name`` str ``name_color`` str @@ -58,16 +57,16 @@ Name Type ``exp_level`` int ``exp_points`` int ``is_qualified_from_championship_challenge`` bool -``x3vs3_victories`` int -``team_victories`` int +``x3vs3_victories`` / ``team_victories`` int ``solo_victories`` int ``duo_victories`` int ``best_robo_rumble_time`` int ``best_time_as_big_brawler`` int +``brawlers`` List[`PlayerBrawler`_] ``club.tag`` str ``club.name`` str -``brawlers`` List[Brawler] -============================================ ============= +``icon.id`` int +============================================ ====================== Club ~~~~ @@ -75,31 +74,34 @@ Club A full club object to get a club's statistics. In order to get this, you must get it from the client or a player object. - Attributes: -===================== ============ +===================== =============== Name Type -===================== ============ +===================== =============== ``tag`` str ``name`` str ``description`` str ``type`` str ``trophies`` int ``required_trophies`` int -``members`` List[Member] -===================== ============ +``members`` List[`Member`_] +``badge_id`` int +===================== =============== -Members -~~~~~~~ +Member +~~~~~~ -Returns a list of club members. Get this by accessing -Club.members or Club.get_members() +Members is a list of club members. Get this by accessing +``Club.members`` or ``Club.get_members()``. +The club's members are sorted in order of descending trophies. +Each Member in the list has the following attributes: .. code:: py members = club.members - print(members[0].name, members[0].role) # prints best player's name and role (sorted by trophies) + # Prints club's best player's name and role + print(members[0].name, members[0].role) Attributes: @@ -111,14 +113,16 @@ Name Type ``name_color`` str ``role`` str ``trophies`` int +``icon.id`` int ============== ==== Ranking ~~~~~~~ -Returns a list of top players, clubs, or brawlers. To access this, do ``ranking[index]`` +A list of top players, clubs, or brawlers. +Each item in the list has the following attributes: -Player/Brawler attributes: +Player/Brawler Ranking attributes: ============== ==== Name Type @@ -129,9 +133,10 @@ Name Type ``trophies`` int ``rank`` int ``club.name`` str +``icon.id`` int ============== ==== -Club attributes: +Club Ranking attributes: ================ ==== Name Type @@ -141,36 +146,43 @@ Name Type ``trophies`` int ``rank`` int ``member_count`` int +``badge_id`` int ================ ==== -Brawler -~~~~~~~ +PlayerBrawler +~~~~~~~~~~~~~ -Returns a brawler object with the following attributes. You can retrieve -a profile’s brawler info by getting ``Profile.brawlers`` +PlayerBrawlers is a list of brawler objects, each with the following attributes. +The brawlers are sorted in order of descending trophies. +Note: ``PlayerBrawler`` only represents a brawler that a player owns and +can only be accessed from ``Player.brawlers``. .. code:: py - brawlers = profile.brawlers - top_brawler = brawlers[0] # first index in list = highest trophies - print(top_brawler.name, top_brawler.trophies) # prints best brawler's name and trophies + brawlers = player.brawlers + # List is sorted by descending trophies + top_brawler = brawlers[0] + # print the player's best brawler's name and trophies + print(top_brawler.name, top_brawler.trophies) Attributes: -==================== ======== +==================== ================== Name Type -==================== ======== +==================== ================== ``id`` int ``name`` str ``power`` int ``rank`` int ``trophies`` int ``highest_trophies`` int -``star_powers`` List[SP] -==================== ======== +``star_powers`` List[`StarPower`_] +``gadgets`` List[`Gadget`_] +``gears`` List[`Gear`_] +==================== ================== -Star Power -~~~~~~~~~~ +StarPower +~~~~~~~~~ Attributes: @@ -181,105 +193,143 @@ Name Type ``name`` str ======== ==== -Battle Logs -~~~~~~~~~~~ +Gadget +~~~~~~ -Returns a list of objects with this structure: +Attributes: + +======== ==== +Name Type +======== ==== +``id`` int +``name`` str +======== ==== + +Gear +~~~~ Attributes: -:: - - { - "battleTime":"20190706T151526.000Z", - "event":{ - "id":15000126, - "mode":"duoShowdown", - "map":"Royal Runway" - }, - "battle":{ - "mode":"duoShowdown", - "type":"ranked", - "rank":1, - "trophyChange":9, - "teams":[ - [ - { - "tag":"#Y2QPGG", - "name":"Lex_YouTube", - "brawler":{ - "id":16000005, - "name":"SPIKE", - "power":10, - "trophies":495 - } - }, - { - "tag":"#8Q229LJY", - "name":"Brandon", - "brawler":{ - "id":16000003, - "name":"BROCK", - "power":10, - "trophies":495 - } - }, - { - "tag":"#29RGL0QJ0", - "name":"smallwhitepeen1", - "brawler":{ - "id":16000007, - "name":"JESSIE", - "power":7, - "trophies":486 - } - } - ], - [ - { - "tag":"#CYLVL8LY", - "name":"TST|ROYER™", - "brawler":{ - "id":16000019, - "name":"PENNY", - "power":8, - "trophies":541 - } - }, - { - "tag":"#8P2URCR0", - "name":"ANOTHER", - "brawler":{ - "id":16000023, - "name":"LEON", - "power":8, - "trophies":559 - } - }, - { - "tag":"#8LRY92QP", - "name":"Marshmello", - "brawler":{ - "id":16000021, - "name":"GENE", - "power":7, - "trophies":448 - } - } - ] - ] - } - } +========= ==== +Name Type +========= ==== +``id`` int +``name`` str +``level`` int +========= ==== + +BattleLog +~~~~~~~~~ + +A BattleLog contains a list of items, each with the following attributes: + +Attributes: + +=============== =============== +Name Type +=============== =============== +``battle_time`` str +``event`` `Event`_ +``battle`` List[`Battle`_] +=============== =============== + +Event +~~~~~ + +An object containing information about an event. + +Attributes: + +======== ==== +Name Type +======== ==== +``id`` int +``mode`` str +``map`` str +======== ==== + +Battle +~~~~~~ + +Each Battle object contains information about a battle. +Note: The ``star_player`` attribute may not exist for certain modes +that do not have a star player (e.g. showdown, duoShowdown). + +Attributes: + +==================== =========================== +Name Type +==================== =========================== +``mode`` str +``type`` str +``result`` str +``duration`` int +``trophy_change`` int +``star_player`` `BattlePlayer`_ +``teams`` List[List[`BattlePlayer`_]] +==================== =========================== + +BattlePlayer +~~~~~~~~~~~~ + +Represents a player who played in a battle. + +=========== ================ +Name Type +=========== ================ +``tag`` str +``name`` str +``brawler`` `BattleBrawler`_ +=========== ================ + +BattleBrawler +~~~~~~~~~~~~~ + +Represents a brawler that was played in a battle. +Note: ``BattlerBrawler`` only reprents brawlers that were played in a battle +and can only be accessed from ``BattlePlayer.brawler``. + +============ ==== +Name Type +============ ==== +``id`` int +``name`` str +``power`` int +``trophies`` int +============ ==== Brawlers ~~~~~~~~ -Returns list of available brawlers and information about them with this structure: +Represents a list of all brawlers in the game and information, +with each item having the following attributes. +Note: ``Brawlers`` only represents the brawler objects returned +from ``Client.get_brawlers()``. Attributes: -:: +==================== ================== +Name Type +==================== ================== +``id`` int +``name`` str +``star_powers`` List[`StarPower`_] +``gadgets`` List[`Gadget`_] +==================== ================== + +EventRotation +~~~~~~~~~~~~~ + +Represents a list of events in the current rotation, +each with the following attributes: - [ - Brawler - ] \ No newline at end of file +Attributes: + +============== ======== +Name Type +============== ======== +``start_time`` str +``end_time`` str +``slot_id`` int +``event`` `Event`_ +============== ======== \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 26f7121..5c837ff 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,16 +5,24 @@ Welcome to Brawl Stats' documentation! :target: https://pypi.org/project/brawlstats/ :alt: PyPi -.. image:: https://travis-ci.com/SharpBit/brawlstats.svg?branch=master - :target: https://travis-ci.com/SharpBit/brawlstats - :alt: Travis-CI build +.. image:: https://github.com/SharpBit/brawlstats/actions/workflows/tests.yml/badge.svg + :target: https://github.com/SharpBit/brawlstats/actions/workflows/tests.yml + :alt: GitHub Actions Tests + +.. image:: https://img.shields.io/pypi/pyversions/brawlstats.svg + :target: https://pypi.org/project/brawlstats/ + :alt: Supported Versions .. image:: https://img.shields.io/github/license/SharpBit/brawlstats.svg :target: https://github.com/SharpBit/brawlstats/blob/master/LICENSE :alt: MIT License -- This library is a sync and async wrapper for the Brawl Stars API. -- Python 3.5.3 or later is required. +.. image:: https://readthedocs.org/projects/brawlstats/badge/?version=stable + :target: https://brawlstats.readthedocs.io/en/stable/?badge=stable + :alt: Documentation Status + +- BrawlStats is a sync and async Python API wrapper to fetch statistics from the official Brawl Stars API. +- Python 3.9 or later is required. Features ~~~~~~~~ @@ -24,8 +32,8 @@ Features - Get a player profile and battlelog. - Get a club and its members. - Get the top 200 rankings for players, clubs, or a specific brawler. -- Get information about maps and more! -- Get information about current available brawlers. +- Get information about all the brawlers in the game. +- Get information about the current event rotation! Installation ~~~~~~~~~~~~ diff --git a/docs/logging.rst b/docs/logging.rst index 1e51730..d3cf275 100644 --- a/docs/logging.rst +++ b/docs/logging.rst @@ -42,7 +42,7 @@ stdout of your program. Currently, the following things are logged: -- ``DEBUG``: API Requests +- ``DEBUG``: API Requests, Cache Hits diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..2b28a47 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +-e . +sphinx \ No newline at end of file diff --git a/examples/discord_cog.py b/examples/discord_cog.py index 0045c12..18732c1 100644 --- a/examples/discord_cog.py +++ b/examples/discord_cog.py @@ -1,3 +1,5 @@ +# NOTE: This discord example is outdated as Discord's Bot API has changed + import discord from discord.ext import commands @@ -17,10 +19,10 @@ async def profile(self, ctx, tag: str): try: player = await self.client.get_profile(tag) except brawlstats.RequestError as e: # catches all exceptions - return await ctx.send('```\n{}: {}\n```'.format(e.code, e.message)) # sends code and error message - em = discord.Embed(title='{0.name} ({0.tag})'.format(player)) + return await ctx.send(f'```\n{e.code}: {e.message}\n```') # sends code and error message + em = discord.Embed(title=f'{player.name} ({player.tag})') - em.description = 'Trophies: {}'.format(player.trophies) # you could make this better by using embed fields + em.description = f'Trophies: {player.trophies}' # you could make this better by using embed fields await ctx.send(embed=em) diff --git a/requirements-dev.txt b/requirements-dev.txt index 8c67d40..b633f3d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,6 @@ -asynctest flake8 pluggy>=0.12.0,<1.0.0 pytest -pytest-asyncio +pytest-asyncio~=0.21.0 python-dotenv tox-travis \ No newline at end of file diff --git a/setup.py b/setup.py index f7b62c8..f29c87e 100644 --- a/setup.py +++ b/setup.py @@ -15,31 +15,35 @@ setup( name='brawlstats', version=version, - description='An easy-to-use wrapper for the Brawl Stars API', + description='BrawlStats is an easy-to-use sync and async Python API wrapper' + 'to fetch statistics from the official Brawl Stars API', long_description=long_description, long_description_content_type='text/x-rst', url='https://github.com/SharpBit/brawlstats', author='SharpBit', author_email='sharpbit3618@gmail.com', license='MIT', - keywords=['brawl stars, brawlstats, supercell'], + keywords=[ + 'brawl stars, brawl stats, brawlstats, supercell, python, sync, async, ' + 'python wrapper, api wrapper, python api wrapper, python 3.9, python 3.10, python 3.11, python 3.12' + ], packages=find_packages(), install_requires=requirements, - python_requires='>=3.5.3', + python_requires='>=3.9.0', project_urls={ 'Source Code': 'https://github.com/SharpBit/brawlstats', 'Issue Tracker': 'https://github.com/SharpBit/brawlstats/issues', - 'Documentation': 'https://brawlstats.readthedocs.io/', + 'Documentation': 'https://brawlstats.readthedocs.io/en/stable', }, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Games/Entertainment :: Real Time Strategy', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Natural Language :: English' ] ) diff --git a/tests/test_async.py b/tests/test_async.py index 5edcf5d..9519275 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -1,31 +1,33 @@ import os +import unittest import aiohttp -import asynctest import brawlstats import pytest + from dotenv import load_dotenv pytestmark = pytest.mark.asyncio load_dotenv() -class TestAsyncClient(asynctest.TestCase): - use_default_loop = True +class TestAsyncClient(unittest.IsolatedAsyncioTestCase): PLAYER_TAG = '#V2LQY9UY' CLUB_TAG = '#UL0GCC8' - async def setUp(self): - session = aiohttp.ClientSession(loop=self.loop) - + async def asyncSetUp(self): + session = aiohttp.ClientSession(trust_env=True) self.client = brawlstats.Client( - os.getenv('token'), - base_url=os.getenv('base_url'), - is_async=True, - session=session + token=os.getenv('TOKEN'), + session=session, + base_url=os.getenv('BASE_URL'), + is_async=True ) + async def asyncTearDown(self): + await self.client.close() + async def test_get_player(self): player = await self.client.get_player(self.PLAYER_TAG) self.assertIsInstance(player, brawlstats.Player) @@ -35,6 +37,9 @@ async def test_get_player(self): self.assertIsInstance(club, brawlstats.Club) self.assertEqual(club.tag, self.CLUB_TAG) + battle_logs = await player.get_battle_logs() + self.assertIsInstance(battle_logs, brawlstats.BattleLog) + with self.assertRaises(brawlstats.NotFoundError): await self.client.get_player('2PPPPPPP') @@ -71,7 +76,8 @@ async def test_get_club_members(self): self.assertIsInstance(club_members, brawlstats.Members) self.assertIn(self.PLAYER_TAG, [x.tag for x in club_members]) - await self.assertAsyncRaises(brawlstats.NotFoundError, self.client.get_club_members('8GGGGGGG')) + with self.assertRaises(brawlstats.NotFoundError): + await self.client.get_club_members('8GGGGGGG') async def test_get_rankings(self): player_ranking = await self.client.get_rankings(ranking='players') @@ -104,22 +110,14 @@ async def test_get_rankings(self): with self.assertRaises(ValueError): await self.client.get_rankings(ranking='brawlers', brawler='SharpBit') - async def test_get_constants(self): - constants = await self.client.get_constants() - self.assertIsInstance(constants, brawlstats.Constants) - - maps = await self.client.get_constants('maps') - self.assertIsInstance(maps, brawlstats.Constants) - - await self.assertAsyncRaises(KeyError, self.client.get_constants('invalid')) - async def test_get_brawlers(self): brawlers = await self.client.get_brawlers() self.assertIsInstance(brawlers, brawlstats.Brawlers) - async def asyncTearDown(self): - await self.client.close() + async def test_get_event_rotation(self): + events = await self.client.get_event_rotation() + self.assertIsInstance(events, brawlstats.EventRotation) if __name__ == '__main__': - asynctest.main() + unittest.main() diff --git a/tests/test_blocking.py b/tests/test_blocking.py index 869a9d9..d5b16c1 100644 --- a/tests/test_blocking.py +++ b/tests/test_blocking.py @@ -13,12 +13,14 @@ class TestBlockingClient(unittest.TestCase): CLUB_TAG = '#UL0GCC8' def setUp(self): - self.client = brawlstats.Client( - os.getenv('token'), - base_url=os.getenv('base_url') + token=os.getenv('TOKEN'), + base_url=os.getenv('BASE_URL') ) + def tearDown(self): + self.client.close() + def test_get_player(self): player = self.client.get_player(self.PLAYER_TAG) self.assertIsInstance(player, brawlstats.Player) @@ -28,6 +30,9 @@ def test_get_player(self): self.assertIsInstance(club, brawlstats.Club) self.assertEqual(club.tag, self.CLUB_TAG) + battle_logs = player.get_battle_logs() + self.assertIsInstance(battle_logs, brawlstats.BattleLog) + self.assertRaises(brawlstats.NotFoundError, self.client.get_player, '2PPPPPPP') self.assertRaises(brawlstats.NotFoundError, self.client.get_player, 'P') self.assertRaises(brawlstats.NotFoundError, self.client.get_player, 'AAA') @@ -82,21 +87,13 @@ def test_get_rankings(self): self.assertIsInstance(us_brawler_ranking, brawlstats.Ranking) self.assertTrue(len(us_brawler_ranking) == 1) - def test_get_constants(self): - constants = self.client.get_constants() - self.assertIsInstance(constants, brawlstats.Constants) - - maps = self.client.get_constants('maps') - self.assertIsInstance(maps, brawlstats.Constants) - - self.assertRaises(KeyError, self.client.get_constants, 'invalid') - def test_get_brawlers(self): brawlers = self.client.get_brawlers() self.assertIsInstance(brawlers, brawlstats.Brawlers) - def tearDown(self): - self.client.close() + def test_get_event_rotation(self): + events = self.client.get_event_rotation() + self.assertIsInstance(events, brawlstats.EventRotation) if __name__ == '__main__': diff --git a/tox.ini b/tox.ini index 4e40de0..a179fd4 100644 --- a/tox.ini +++ b/tox.ini @@ -4,13 +4,13 @@ exclude = .tox,__init__.py ignore = E252,E302,E731,W605 [tox] -envlist = py35,py36,py37,py38 +envlist = py39, py310, py311, py312 [testenv] -deps = -r{toxinidir}/requirements-dev.txt +deps = -Ur{toxinidir}/requirements-dev.txt commands = - flake8 tests + flake8 . pytest passenv = - token - base_url \ No newline at end of file + TOKEN + BASE_URL \ No newline at end of file