├── .env.example ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── tests.yml ├── .gitignore ├── .readthedocs.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── brawlstats ├── __init__.py ├── core.py ├── errors.py ├── models.py └── utils.py ├── docs ├── Makefile ├── api.rst ├── conf.py ├── exceptions.rst ├── index.rst ├── logging.rst ├── make.bat └── requirements.txt ├── examples ├── async.py ├── discord_cog.py └── sync.py ├── requirements-dev.txt ├── requirements.txt ├── setup.py ├── tests ├── test_async.py └── test_blocking.py └── tox.ini /.env.example: -------------------------------------------------------------------------------- 1 | TOKEN = # Replace this with your API token 2 | BASE_URL = # Proxy server to handle requests to API due to IP limitations -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **Full Code** 11 | Upload to https://gist.github.com if the code is too long. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Actual behavior** 17 | A clear and concise description of what actually happened along with a full traceback if applicable. 18 | 19 | **Additional context** 20 | Python Version: 21 | brawlstats Version: 22 | OS (and version): -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. i.e. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Changes Description 2 | A clear and concise description of what the PR changed. 3 | 4 | # Type of PR 5 | - [ ] Bug Fix 6 | - [ ] Feature Addition 7 | 8 | # Checklist 9 | - [ ] Docstrings added ([NumpyDoc](https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard)) 10 | - [ ] If necessary, add to the documentation files 11 | - [ ] Tox checked 12 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: tests 5 | 6 | on: 7 | push: 8 | branches: [ "master", "development" ] 9 | pull_request: 10 | branches: [ "master", "development" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.9", "3.10", "3.11", "3.12"] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install -r requirements-dev.txt 31 | python -m pip install . 32 | - name: Lint with flake8 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 36 | - name: Test with pytest 37 | env: 38 | TOKEN: ${{ secrets.TOKEN }} 39 | BASE_URL: ${{ secrets.BASE_URL }} 40 | run: | 41 | pytest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # VSCode settings 2 | .vscode/ 3 | 4 | # BAT Files 5 | *.bat 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | *.pyc 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | env/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | .pypirc 29 | sdist/ 30 | var/ 31 | wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .pytest_cache/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | .hypothesis/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | docs/_static/ 76 | docs/_templates/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # celery beat schedule file 88 | celerybeat-schedule 89 | 90 | # SageMath parsed files 91 | *.sage.py 92 | 93 | # dotenv 94 | .env 95 | tests/.env 96 | 97 | # virtualenv 98 | .venv 99 | venv/ 100 | ENV/ 101 | 102 | # Spyder project settings 103 | .spyderproject 104 | .spyproject 105 | 106 | # Rope project settings 107 | .ropeproject 108 | 109 | # mkdocs documentation 110 | /site 111 | 112 | # mypy 113 | .mypy_cache/ 114 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.9" 12 | # You can also specify other tool versions: 13 | # nodejs: "20" 14 | # rust: "1.70" 15 | # golang: "1.20" 16 | 17 | # Build documentation in the "docs/" directory with Sphinx 18 | sphinx: 19 | configuration: docs/conf.py 20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 21 | # builder: "dirhtml" 22 | # Fail on all warnings to avoid broken references 23 | # fail_on_warning: true 24 | 25 | # Optionally build your docs in additional formats such as PDF and ePub 26 | # formats: 27 | # - pdf 28 | # - epub 29 | 30 | # Optional but recommended, declare the Python requirements required 31 | # to build your documentation 32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 33 | python: 34 | install: 35 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [4.2.0] - 10/8/24 5 | ### Added 6 | - Implemented an endpoint with `Client.get_event_rotation` which gets the events in the current rotation. 7 | - Added a method `Player.get_battle_logs` which directly gets the player's battle log. 8 | ### Fixed 9 | - Client actually uses session passed into parameters now instead of creating a new one anyways 10 | - `UnexpectedError` now properly shows the returned text in the message 11 | - The `use_cache` parameter now works for `get_brawlers` and the async client 12 | ### Removed 13 | - Removed the prevent_ratelimit option for the Client 14 | - Dropped support for Python 3.5, 3.6, 3.7, and 3.8 15 | - Removed `Client.get_constants` as the site that was hosting it is no longer running 16 | 17 | ## [4.1.1] - 10/31/21 18 | ### Fixed 19 | - Installation dependency issue with aiohttp 20 | 21 | ## [4.1.0] - 1/4/21 22 | ### Added 23 | - `use_cache` parameter for methods to specify whether or not to use cache for a specific API call 24 | ### Changed 25 | - Wrapper no longer replaces the letter O with 0 for tags to better represent a valid tag 26 | 27 | ## [4.0.6] - 10/6/20 28 | ### Changed 29 | - Format of the string of `utils.get_datetime` 30 | ### Fixed 31 | - `Player.get_club` now works for the async client if the player is not in a club 32 | 33 | ## [4.0.5] - 7/27/20 34 | ### Fixed 35 | - Removed the print statement on client initialization 36 | - Actually uses asyncio.Lock properly if the async client has the option `prevent_ratelimit=True` 37 | 38 | ## [4.0.4] - 7/22/20 39 | ### Added 40 | - `get_brawlers` function to get available brawlers 41 | ### Changed 42 | - Split `BaseBox` into `BaseBox` and `BaseBoxList` for convenience 43 | 44 | ## [4.0.3] - 4/17/20 45 | ### Fixed 46 | - Brawler leaderboards for Python 3.5 47 | 48 | ## [4.0.2] - 4/15/20 49 | ### Fixed 50 | - Player model bug 51 | 52 | ## [4.0.1] - 4/12/20 53 | ### Added 54 | - An alias for `Player.x3vs3_victories` called `team_victories` 55 | 56 | ## [4.0.0] - 3/11/20 57 | ### Added 58 | - `reason` attribute for `NotFoundError` 59 | ### Removed 60 | - BrawlAPI client 61 | ### Changed 62 | - `Client.get_datetime` moved to utils 63 | - `get_rankings` now requires all arguments to be keyword arguments 64 | ### Fixed 65 | - Brawler leaderboard now works 66 | 67 | ## [3.0.4] - 3/8/20 68 | ### Changed 69 | - Leaderboard functions reverted to one function for all types of leaderboards/rankings 70 | 71 | ## [3.0.3] - 2/17/20 72 | ### Added 73 | - `invalid_chars` attribute for `NotFoundError` when applicable 74 | - `url` attribute for all errors that have requested a URL 75 | - `str(error)` will return the message attribute of the error. 76 | ### Changed 77 | - The `error` attribute for all errors have been renamed to `message` 78 | - For BrawlAPI: `get_leaderboard` split up into `get_player_leaderboard`, `get_club_leaderboard`, and `get_brawler_leaderboard` 79 | - For the official API: `get_rankings` split up into `get_player_rankings`, `get_club_rankings`, and `get_brawler_rankings` 80 | 81 | ## [3.0.2] - 12/22/19 82 | ### Fixed 83 | - A bug with brawler leaderboards for the BrawlAPI 84 | 85 | ## [3.0.1] - 10/31/19 86 | ### Changed 87 | - Base url for BrawlAPI now https://api.starlist.pro/v1 88 | 89 | ## [3.0.0] - 9/22/19 90 | ### Added 91 | - Official API support (all endpoints implemented, all methods documented) 92 | - `Forbidden` error raised when code 403 received 93 | - New terminology! "BrawlAPI" refers to the [unofficial API](https://api.brawlapi.cf/v1) while "OfficialAPI" refers to the [official API](https://developer.brawlstars.com) 94 | ### Changed (BREAKING) 95 | - The unofficial API's client will now be accessed as `brawlstats.BrawlAPI` (from `brawlstats.Client`) 96 | - The unofficial API's models will now be accessed as `brawlstats.brawlapi.ModelName` 97 | - The official API's client will be accessed by `brawlstats.OfficialAPI` 98 | - The official API's models will be accessed by `brawlstats.officialapi.ModelName` 99 | - `get_leaderboard()` will now require "brawlers" for the ranking type and the actual brawler name passed through the brawler kwarg. 100 | - `get_leaderboard()` `count` argument has been renamed to `limit` 101 | ### Fixed 102 | - BrawlAPI `get_leaderboard` parameter documentation fixed 103 | - Arguments passed into functions that require player/club tags now properly get formatted correctly. 104 | 105 | ## [2.3.14] - 9/14/19 106 | ### Changed 107 | - Default timeout from 10 to 30 108 | ### Fixed 109 | - Cache is smaller due to smaller 3 r/s ratelimit (from 5) 110 | - Fixed a bug where `UnexpectedError` did not work due to a typo. 111 | 112 | ## [2.3.13] - 8/29/19 113 | ### Added 114 | - New brawler 8-Bit 115 | 116 | ## [2.3.12] - 7/18/19 117 | ### Added 118 | - Player battle logs 119 | - Local leaderboards 120 | 121 | ## [2.3.11] - 7/3/19 122 | ### Fixed 123 | - Fixed the sync version of the wrapper to not raise a RuntimeWarning due to using `asyncio.sleep()` instead of `time.sleep()` 124 | 125 | ## [2.3.10] - 7/2/19 126 | ### Added 127 | - New brawler tick 128 | 129 | ## [2.3.9] - 5/27/19 130 | ### Fixed 131 | - Renamed ricochet to rico 132 | - Fixed the sync client when not using `prevent_ratelimit` 133 | ### Added 134 | - Bibi! 135 | 136 | ## [2.3.8] - 5/21/19 137 | ### Changed 138 | - Changed the Base URL back to the new URL. 139 | - Now waits the number of seconds instead of raising a `RateLimitError` when a rate limit will be detected BEFORE it requests. 140 | 141 | ## [2.3.7] - 5/5/19 142 | ### Changed 143 | - Changed the BASE URL to the old API URL. VERSION 2.3.6 WILL NOT WORK DUE TO API TIMEOUT ISSUES. PLEASE UPDATE. 144 | 145 | ## [2.3.6] - 4/27/19 146 | ### Added 147 | - Rosa to the brawler list 148 | - `prevent_ratelimit` option when initializing a client to wait when chaining requests 149 | ### Changed 150 | - Base URL for requests to [the new API URL](https://api.brawlapi.cf/v1) 151 | - Ratelimit updated to API's 3 requests per second 152 | 153 | ## [2.3.5] - 4/15/19 154 | ### Fixed 155 | - Fixed the rate limit handler when error code 429 was returned by the API. 156 | 157 | ## [2.3.4] - 4/10/19 158 | ### Fixed 159 | - Fixed a [mistake](https://github.com/SharpBit/brawlstats/pull/24) where `time()` was being called directly (instead of `time.time()`) 160 | 161 | ## [2.3.3] - 4/5/19 162 | ### Added 163 | - Added carl to the brawler list 164 | ### Changed 165 | - Renamed `Profile` class to `Player` 166 | 167 | ## [2.3.2] - 3/10/19 168 | ### Fixed 169 | - Allows users to pass in a connector for the async client which fixes issue #19. 170 | 171 | ## [2.3.1] - 3/9/19 172 | ### Added 173 | - Creates requests with gzip encoding enabled to cut request times. 174 | - Detect a rate limit before it requests from the API. 175 | ### Changed 176 | - Changed the request log. 177 | - No longer imports itself in `utils.py` for the version number. 178 | 179 | ## [2.3.0] - 3/4/19 180 | ### Added 181 | - Added caching that clears after 180 seconds to not spam the API with the same requests. 182 | ### Fixed 183 | - Fixed debug on the sync client. 184 | 185 | ## [2.2.8] - 3/4/19 186 | ### Added 187 | - Added the text that the API returns when an `UnexpectedError` is raised. If you see this, you should report the error to the [discord server](https://discord.gg/vbbHXNf) 188 | ### Fixed 189 | - Fixed the club search request URL 190 | - Fixed sync `search_club` 191 | 192 | ## [2.2.7] - 2/27/19 193 | ### Fixed 194 | - Fixed issue [#20](https://github.com/SharpBit/brawlstats/issues/20) 195 | ### Removed 196 | - Removed `pytest` from the package requirements. 197 | 198 | ## [2.2.6] - 2/26/19 199 | ### Fixed 200 | - Club search actually returns a list now. 201 | ### Changed 202 | - Added wrapper and python version numbers to the User Agent when making API requests. 203 | - `url` param in the client changed to `base_url` 204 | 205 | ## [2.2.5] - 2/25/19 206 | ### Added 207 | - `debug` option to pass in when intializing `brawlstats.core.Client` to log requests. 208 | 209 | ## [2.2.4] - 2/24/19 210 | ### Fixed 211 | - Fixed [installation error](https://github.com/SharpBit/brawlstats/issues/17) where the `constants.json` info key was removed. 212 | 213 | ## [2.2.3] - 2/18/19 214 | ### Added 215 | - Added gene to the list of brawlers to get for the brawler leaderboard. 216 | 217 | ## [2.2.2] - 2/3/19 218 | ### Fixed 219 | - Fixed `search_club()` 220 | - Fixed some attribute typos in docs for the Misc category 221 | 222 | ## [2.2.1] - 1/30/19 223 | ### Fixed 224 | - Providing no loop while setting `is_async` to `True` now correctly defaults to `asyncio.get_event_loop()` 225 | - Fixed the URL for `get_club()` 226 | - Fixed some typos in docs 227 | - Fixed attribute charts in docs 228 | 229 | ## [2.2.0] - 1/29/19 230 | ### Changed 231 | - Change the way you get a brawler with `get_leaderboard()` 232 | - Updated documentation for added keys 233 | 234 | ## [2.1.13] - 1/18/19 235 | ### Added 236 | - Search Clubs (`search_club()`) 237 | - Season and Shop Data (`get_misc()`) 238 | 239 | ## [2.1.12] - 1/5/19 240 | ### Added 241 | - Loop parameter for the client for aiohttp sessions if one has not yet been specified. If you specify a session, you must set the loop to that session before you pass it in otherwise the loop will not be applied. 242 | 243 | ## [2.1.11] - 12/24/18 244 | ### Added 245 | - MaintenanceError raised when the game is undergoing maintenance. 246 | 247 | ## [2.1.10] - 12/13/18 248 | ### Fixed 249 | - Fix any data that involves a list (Leaderboard) 250 | 251 | ## [2.1.9] - 12/11/18 252 | ### Added 253 | - `get_datetime` function for easier date and time conversions 254 | 255 | ## [2.1.8] - 12/10/18 256 | ### Changed 257 | - No longer need to access a players or clubs attribute when getting a leaderboard 258 | 259 | ## [2.1.7] - 12/9/18 260 | ### Fixed 261 | - Fixed a bug in the sync version of `get_constants()` where there was an extra `await` 262 | 263 | ## [2.1.6] - 12/8/18 264 | ### Added 265 | - Constants extracted from the Brawl Stars App using `Client.get_constants` 266 | 267 | ## [2.1.5] - 12/5/18 268 | BREAKING CHANGES: Brawl Stars dev team changed "Band" to "Club". This update fixes all of that. 269 | ### Changed 270 | - `Band` has been changed to `Club` 271 | - `SimpleBand` has been changed to `PartialClub` 272 | - Documentation has been updated for this 273 | - All methods that originally had "band" in them have been changed to "club" 274 | - All attributes that originally had "band" in them have been changed to "club" 275 | 276 | ## [2.1.4] - 12/2/18 277 | ### Added 278 | - `RateLimitError` to handle the 2 requests/sec ratelimit of the API. 279 | 280 | ## [2.1.3] - 12/2/18 281 | ### Added 282 | - Remove warnings and stuff to prevent memory leaks and fix session initialization (PR from Kyber) 283 | 284 | ## [2.1.2] - 12/2/18 285 | ### Added 286 | - Resp accessible in data models via `Model.resp` 287 | - Added documentation for below change and new attributes that the API introduced. 288 | ### Changed 289 | - `InvalidTag` changed to `NotFoundError` 290 | 291 | ## [2.1.1] - 12/1/18 292 | ### Added 293 | - Allows developers to change the base URL to make request to. This addresses [issue #6](https://github.com/SharpBit/brawlstats/issues/6) 294 | 295 | ## [2.1.0] - 11/29/18 296 | ### Added 297 | - Synchronous support! You can now set if you want an async client by using `is_async=True` 298 | ### Fixed 299 | - `asyncio.TimeoutError` now properly raises `ServerError` 300 | ### Removed 301 | - `BadRequest` and `NotFoundError` (negates v2.0.6). These were found to not be needed 302 | 303 | ## [2.0.7] - 11/29/18 304 | ### Added 305 | - Support for the new `/events` endpoint for current and upcoming event rotations 306 | ### Changed 307 | - Change the Unauthorized code from 403 to 401 due to API error code changes 308 | 309 | ## [2.0.6] - 11/25/18 310 | ### Added 311 | - `BadRequest` and `NotFoundError` for more API versatility 312 | 313 | ## [2.0.5] - 11/24/18 314 | ### Fixed 315 | - Leaderboards fixed 316 | 317 | ## [2.0.0] - 11/19/18 318 | ### Added 319 | - Support for the brand new API at https://brawlapi.cf/api 320 | 321 | ## [1.5.0] - 2/18/18 322 | ### Added 323 | - Python 3.5 support! 324 | 325 | ## [1.2.0] - 2/13/18 326 | ### Changed 327 | - Base links for the new API changes 328 | 329 | ## [1.1.12] - 2/7/18 330 | ### Added 331 | - Essential core 332 | 333 | ## [1.0.0] 334 | ### Added 335 | - Request maker -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Fork the repository 4 | 2. Clone the repository (`git clone https://github.com/username/brawlstats.git`) 5 | 3. Checkout in a new branch (`git checkout -b feature-name`) 6 | 4. Install all test dependencies (`pip install -r tests/requirements.txt`) 7 | 5. Make your edits 8 | 6. Add the necessary tests in the `tests` folder 9 | 7. Add the necessary documentation and docstrings 10 | 8. Add the necessary points to `CHANGELOG.md` 11 | 9. Fill up the `.env` file 12 | 10. Run `tox` from the root folder and ensure the tests are configured correctly and they return OK. `ServerError` can be disregarded. 13 | 11. Open your PR 14 | 15 | Do not increment version numbers but update `CHANGELOG.md` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2024 SharpBit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include requirements-dev.txt -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://i.imgur.com/5uUkTrn.png 2 | :alt: Brawl Stats 3 | 4 | Brawl Stats 5 | =========== 6 | 7 | .. image:: https://img.shields.io/pypi/v/brawlstats.svg 8 | :target: https://pypi.org/project/brawlstats/ 9 | :alt: PyPi 10 | 11 | .. image:: https://github.com/SharpBit/brawlstats/actions/workflows/tests.yml/badge.svg 12 | :target: https://github.com/SharpBit/brawlstats/actions/workflows/tests.yml 13 | :alt: GitHub Actions Tests 14 | 15 | .. image:: https://img.shields.io/pypi/pyversions/brawlstats.svg 16 | :target: https://pypi.org/project/brawlstats/ 17 | :alt: Supported Versions 18 | 19 | .. image:: https://img.shields.io/github/license/SharpBit/brawlstats.svg 20 | :target: https://github.com/SharpBit/brawlstats/blob/master/LICENSE 21 | :alt: MIT License 22 | 23 | .. image:: https://readthedocs.org/projects/brawlstats/badge/?version=stable 24 | :target: https://brawlstats.readthedocs.io/en/stable/?badge=stable 25 | :alt: Documentation Status 26 | 27 | - BrawlStats is a sync and async Python API wrapper to fetch statistics from the official Brawl Stars API. 28 | - Python 3.9 or later is required. 29 | 30 | Features 31 | ~~~~~~~~ 32 | 33 | - Easy to use with an object oriented design. 34 | - Use the same client for sync and async usage. 35 | - Get a player profile and battlelog. 36 | - Get a club and its members. 37 | - Get the top 200 rankings for players, clubs, or a specific brawler. 38 | - Get information about all the brawlers in the game. 39 | - Get information about the current event rotation! 40 | 41 | Installation 42 | ~~~~~~~~~~~~ 43 | 44 | Install the latest stable build: 45 | 46 | :: 47 | 48 | pip install brawlstats 49 | 50 | Install the development build: 51 | 52 | :: 53 | 54 | pip install git+https://github.com/SharpBit/brawlstats@development 55 | 56 | Documentation 57 | ~~~~~~~~~~~~~ 58 | 59 | Documentation is being hosted on `Read the Docs`_. 60 | 61 | Examples 62 | ~~~~~~~~ 63 | Examples are in the `examples folder`_. 64 | 65 | - ``sync.py`` shows you basic sync usage 66 | - ``async.py`` shows you basic async usage 67 | - ``discord_cog.py`` shows an example Discord Bot cog using `discord.py`_ 68 | 69 | Misc 70 | ~~~~ 71 | 72 | - If you are currently using this wrapper, please star this repository :) 73 | - If you come across an issue in the wrapper, please `create an issue`_. 74 | - If you need an API key, visit https://developer.brawlstars.com 75 | 76 | Contributing 77 | ~~~~~~~~~~~~ 78 | Special thanks to this project's contributors ❤️ 79 | 80 | - `erickang21`_ 81 | - `fourjr`_ 82 | - `golbu`_ 83 | - `kjkui`_ 84 | - `kyb3r`_ 85 | - `Papiersnipper`_ 86 | - `OrangutanGaming`_ 87 | - `Stitch`_ 88 | 89 | If you want to contribute, whether it be a bug fix or new feature, make sure to follow the `contributing guidelines`_. 90 | This project is no longer actively maintained. No new features will be added, only bugfixes and security fixes will be accepted. 91 | 92 | .. _create an issue: https://github.com/SharpBit/brawlstats/issues 93 | .. _Read the Docs: https://brawlstats.readthedocs.io/en/stable/ 94 | .. _examples folder: https://github.com/SharpBit/brawlstats/tree/master/examples 95 | .. _discord.py: https://github.com/rapptz/discord.py 96 | .. _contributing guidelines: https://github.com/SharpBit/brawlstats/blob/master/CONTRIBUTING.md 97 | 98 | .. _erickang21: https://github.com/erickang21 99 | .. _fourjr: https://github.com/fourjr 100 | .. _OrangutanGaming: https://github.com/OrangutanGaming 101 | .. _Stitch: https://github.com/Soumil07 102 | .. _kjkui: https://github.com/kjkui 103 | .. _kyb3r: https://github.com/kyb3r 104 | .. _Papiersnipper: https://github.com/robinmahieu 105 | .. _golbu: https://github.com/0dminnimda 106 | -------------------------------------------------------------------------------- /brawlstats/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import Client 2 | from .models import * 3 | from .errors import * 4 | 5 | ############ 6 | # METADATA # 7 | ############ 8 | 9 | 10 | __version__ = 'v4.2.0' 11 | __title__ = 'brawlstats' 12 | __license__ = 'MIT' 13 | __author__ = 'SharpBit' 14 | __github__ = 'https://github.com/SharpBit/brawlstats' 15 | -------------------------------------------------------------------------------- /brawlstats/core.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import logging 4 | import sys 5 | from typing import Union 6 | 7 | import aiohttp 8 | import requests 9 | from cachetools import TTLCache 10 | 11 | from .errors import Forbidden, NotFoundError, RateLimitError, ServerError, UnexpectedError 12 | from .models import BattleLog, Brawlers, Club, EventRotation, Members, Player, Ranking 13 | from .utils import API, bstag, typecasted 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | 18 | class Client: 19 | """A sync/async client class that lets you access the Brawl Stars API 20 | 21 | Parameters 22 | ------------ 23 | token: str 24 | The API Key that you can get from https://developer.brawlstars.com 25 | session: Union[requests.Session, aiohttp.ClientSession], optional 26 | Use a current session or a make new one, by default None 27 | timeout: int, optional 28 | How long to wait in seconds before shutting down requests, by default 30 29 | is_async: bool, optional 30 | Setting this to ``True`` makes the client async, by default False 31 | loop: asyncio.AbstractEventLoop, optional 32 | The event loop to use for asynchronous operations, by default None. 33 | If you are passing in an aiohttp session, using this will not work: 34 | you must set it when initializing the session. 35 | connector: aiohttp.BaseConnector, optional 36 | Pass a Connector into the client (aiohttp), by default None 37 | If you are passing in an aiohttp session, using this will not work: 38 | you must set it when initializing the session. 39 | debug: bool, optional 40 | Whether or not to log info for debugging, by default False 41 | base_url: str, optional 42 | Sets a different base URL to make request to, by default None 43 | """ 44 | 45 | REQUEST_LOG = '{method} {url} recieved {text} has returned {status}' 46 | 47 | def __init__(self, token, session=None, timeout=30, is_async=False, **options): 48 | # Async options 49 | self.is_async = is_async 50 | self.loop = options.get('loop', asyncio.get_event_loop()) if self.is_async else None 51 | self.connector = options.get('connector') 52 | 53 | self.debug = options.get('debug', False) 54 | self.cache = TTLCache(3200 * 3, 60 * 3) # 3200 requests per minute 55 | 56 | # Session and request options 57 | self.session = session or ( 58 | aiohttp.ClientSession(loop=self.loop, connector=self.connector) if self.is_async else requests.Session() 59 | ) 60 | self.timeout = timeout 61 | self.prevent_ratelimit = options.get('prevent_ratelimit', False) 62 | if self.is_async and self.prevent_ratelimit: 63 | self.lock = asyncio.Lock(loop=self.loop) 64 | self.api = API(base_url=options.get('base_url'), version=1) 65 | 66 | # Request/response headers 67 | self.headers = { 68 | 'Authorization': f'Bearer {token}', 69 | 'User-Agent': f'brawlstats/{self.api.VERSION} (Python {sys.version_info[0]}.{sys.version_info[1]})', 70 | 'Accept-Encoding': 'gzip' 71 | } 72 | 73 | # Load brawlers for get_rankings 74 | if self.is_async: 75 | self.loop.create_task(self._ainit()) 76 | else: 77 | brawlers_info = self.get_brawlers() 78 | self.api.set_brawlers(brawlers_info) 79 | 80 | async def _ainit(self): 81 | """Task created to run `get_brawlers` asynchronously""" 82 | self.api.set_brawlers(await self.get_brawlers()) 83 | 84 | def __repr__(self): 85 | return f'' 86 | 87 | def __enter__(self): 88 | return self 89 | 90 | def __exit__(self, exc_type, exc_value, traceback): 91 | self.close() 92 | 93 | async def __aenter__(self): 94 | return self 95 | 96 | async def __aexit__(self, exc_type, exc_value, traceback): 97 | self.close() 98 | 99 | def close(self): 100 | return self.session.close() 101 | 102 | def _raise_for_status(self, resp, text): 103 | """ 104 | Checks for invalid error codes returned by the API. 105 | """ 106 | try: 107 | data = json.loads(text) 108 | except json.JSONDecodeError: 109 | data = text 110 | 111 | code = getattr(resp, 'status', None) or getattr(resp, 'status_code') 112 | url = resp.url 113 | 114 | if self.debug: 115 | log.debug(self.REQUEST_LOG.format(method='GET', url=url, text=text, status=code)) 116 | 117 | if 300 > code >= 200: 118 | return data 119 | if code == 403: 120 | raise Forbidden(code, url, data['message']) 121 | if code == 404: 122 | raise NotFoundError(code, reason='Resource not found.') 123 | if code == 429: 124 | raise RateLimitError(code, url) 125 | if code == 500: 126 | raise UnexpectedError(code, url, data) 127 | if code == 503: 128 | raise ServerError(code, url) 129 | 130 | def _resolve_cache(self, url): 131 | """Find any cached response for the same requested url.""" 132 | data = self.cache.get(url) 133 | if not data: 134 | return None 135 | if self.debug: 136 | log.debug(f'GET {url} got result from cache.') 137 | return data 138 | 139 | async def _arequest(self, url, use_cache=True): 140 | """Async method to request a url.""" 141 | # Try and retrieve from cache 142 | if use_cache: 143 | cache = self._resolve_cache(url) 144 | else: 145 | cache = None 146 | 147 | if cache is not None: 148 | return cache 149 | 150 | try: 151 | async with self.session.get(url, timeout=self.timeout, headers=self.headers) as resp: 152 | data = self._raise_for_status(resp, await resp.text()) 153 | except asyncio.TimeoutError: 154 | raise ServerError(503, url) 155 | else: 156 | # Cache the data if successful 157 | self.cache[url] = data 158 | 159 | return data 160 | 161 | def _request(self, url, use_cache=True): 162 | """Sync method to request a url.""" 163 | if self.is_async: 164 | return self._arequest(url, use_cache=use_cache) 165 | 166 | # Try and retrieve from cache 167 | if use_cache: 168 | cache = self._resolve_cache(url) 169 | else: 170 | cache = None 171 | if cache is not None: 172 | return cache 173 | 174 | try: 175 | with self.session.get(url, timeout=self.timeout, headers=self.headers) as resp: 176 | data = self._raise_for_status(resp, resp.text) 177 | except requests.Timeout: 178 | raise ServerError(503, url) 179 | else: 180 | # Cache the data if successful 181 | self.cache[url] = data 182 | 183 | return data 184 | 185 | async def _aget_model(self, url, model, use_cache=True, key=None): 186 | """Method to turn the response data into a Model class for the async client.""" 187 | data = await self._arequest(url, use_cache=use_cache) 188 | return model(self, data) 189 | 190 | def _get_model(self, url, model, use_cache=True, key=None): 191 | """Method to turn the response data into a Model class for the sync client.""" 192 | if self.is_async: 193 | # Calls the async function 194 | return self._aget_model(url, model=model, use_cache=use_cache, key=key) 195 | 196 | data = self._request(url, use_cache) 197 | return model(self, data) 198 | 199 | @typecasted 200 | def get_player(self, tag: bstag, use_cache=True) -> Player: 201 | """Gets a player's stats. 202 | 203 | Parameters 204 | ---------- 205 | tag : str 206 | A valid player tag. 207 | Valid characters: 0289PYLQGRJCUV 208 | use_cache : bool, optional 209 | Whether to use the internal 3 minutes cache, by default True 210 | 211 | Returns 212 | ------- 213 | Player 214 | A player object with all of its attributes. 215 | """ 216 | url = f'{self.api.PROFILE}/{tag}' 217 | return self._get_model(url, model=Player, use_cache=use_cache) 218 | 219 | get_profile = get_player 220 | 221 | @typecasted 222 | def get_battle_logs(self, tag: bstag, use_cache=True) -> BattleLog: 223 | """Gets a player's battle logs. 224 | 225 | Parameters 226 | ---------- 227 | tag : str 228 | A valid player tag. 229 | Valid characters: 0289PYLQGRJCUV 230 | use_cache : bool, optional 231 | Whether to use the internal 3 minutes cache, by default True 232 | 233 | Returns 234 | ------- 235 | BattleLog 236 | A player battle object with all of its attributes. 237 | """ 238 | url = f'{self.api.PROFILE}/{tag}/battlelog' 239 | return self._get_model(url, model=BattleLog, use_cache=use_cache) 240 | 241 | @typecasted 242 | def get_club(self, tag: bstag, use_cache=True) -> Club: 243 | """Gets a club's stats. 244 | 245 | Parameters 246 | ---------- 247 | tag : str 248 | A valid club tag. 249 | Valid characters: 0289PYLQGRJCUV 250 | use_cache : bool, optional 251 | Whether to use the internal 3 minutes cache, by default True 252 | 253 | Returns 254 | ------- 255 | Club 256 | A club object with all of its attributes. 257 | """ 258 | url = f'{self.api.CLUB}/{tag}' 259 | return self._get_model(url, model=Club, use_cache=use_cache) 260 | 261 | @typecasted 262 | def get_club_members(self, tag: bstag, use_cache=True) -> Members: 263 | """Gets the members of a club. 264 | 265 | Parameters 266 | ---------- 267 | tag : str 268 | A valid club tag. 269 | Valid characters: 0289PYLQGRJCUV 270 | use_cache : bool, optional 271 | Whether to use the internal 3 minutes cache, by default True 272 | 273 | Returns 274 | ------- 275 | Members 276 | A list of the members in a club. 277 | """ 278 | url = f'{self.api.CLUB}/{tag}/members' 279 | return self._get_model(url, model=Members, use_cache=use_cache) 280 | 281 | def get_rankings( 282 | self, *, ranking: str, region: str=None, limit: int=200, 283 | brawler: Union[str, int]=None, use_cache=True 284 | ) -> Ranking: 285 | """Gets the top count players/clubs/brawlers. 286 | 287 | Parameters 288 | ---------- 289 | ranking : str 290 | The type of ranking. Must be "players", "clubs", "brawlers". 291 | region : str, optional 292 | The region to retrieve from. Must be a 2 letter country code, 'global', or None: by default None 293 | limit : int, optional 294 | The number of top players or clubs to fetch, by default 200 295 | brawler : Union[str, int], optional 296 | The brawler name or ID, by default None 297 | use_cache : bool, optional 298 | Whether to use the internal 3 minutes cache, by default True 299 | 300 | Returns 301 | ------- 302 | Ranking 303 | A player or club ranking that contains a list of players or clubs. 304 | 305 | Raises 306 | ------ 307 | ValueError 308 | The brawler name or ID is invalid. 309 | ValueError 310 | `rankings` is not "players", "clubs", or "brawlers" 311 | ValueError 312 | `limit` is not between 1 and 200, inclusive. 313 | """ 314 | if brawler is not None: 315 | if isinstance(brawler, str): 316 | brawler = brawler.lower() 317 | 318 | # Replace brawler name with ID 319 | if brawler in self.api.CURRENT_BRAWLERS.keys(): 320 | brawler = self.api.CURRENT_BRAWLERS[brawler] 321 | 322 | if brawler not in self.api.CURRENT_BRAWLERS.values(): 323 | raise ValueError('Invalid brawler.') 324 | 325 | if region is None: 326 | region = 'global' 327 | 328 | # Check for invalid parameters 329 | if ranking not in ('players', 'clubs', 'brawlers'): 330 | raise ValueError("'ranking' must be 'players', 'clubs' or 'brawlers'.") 331 | if not 0 < limit <= 200: 332 | raise ValueError('Make sure limit is between 1 and 200.') 333 | 334 | # Construct URL 335 | url = f'{self.api.RANKINGS}/{region}/{ranking}?limit={limit}' 336 | if ranking == 'brawlers': 337 | url = f'{self.api.RANKINGS}/{region}/{ranking}/{brawler}?limit={limit}' 338 | 339 | return self._get_model(url, model=Ranking, use_cache=use_cache) 340 | 341 | def get_brawlers(self, use_cache=True) -> Brawlers: 342 | """Gets available brawlers and information about them. 343 | 344 | Parameters 345 | ---------- 346 | use_cache : bool, optional 347 | Whether to use the internal 3 minutes cache, by default True 348 | 349 | Returns 350 | ------- 351 | Brawlers 352 | A list of available brawlers and information about them. 353 | """ 354 | return self._get_model(self.api.BRAWLERS, model=Brawlers, use_cache=use_cache) 355 | 356 | def get_event_rotation(self, use_cache=True) -> EventRotation: 357 | """Gets the current events in rotation. 358 | 359 | Parameters 360 | ---------- 361 | use_cache : bool, optional 362 | Whether to use the internal 3 minutes cache, by default True 363 | 364 | Returns 365 | ------- 366 | Events 367 | A list of the current events in rotation. 368 | """ 369 | return self._get_model(self.api.EVENT_ROTATION, model=EventRotation, use_cache=use_cache) 370 | -------------------------------------------------------------------------------- /brawlstats/errors.py: -------------------------------------------------------------------------------- 1 | class RequestError(Exception): 2 | """The base class for all errors.""" 3 | 4 | def __init__(self, code, message): 5 | pass 6 | 7 | def __str__(self): 8 | return self.message 9 | 10 | 11 | class Forbidden(RequestError): 12 | """Raised if your API Key is invalid.""" 13 | 14 | def __init__(self, code, url, message): 15 | self.code = code 16 | self.url = url 17 | self.message = message 18 | super().__init__(self.code, self.message) 19 | 20 | 21 | class NotFoundError(RequestError): 22 | """Raised if an invalid player tag or club tag has been passed.""" 23 | 24 | def __init__(self, code, **kwargs): 25 | self.code = code 26 | self.message = 'An incorrect tag has been passed.' 27 | self.reason = kwargs.pop('reason', None) 28 | self.invalid_chars = kwargs.pop('invalid_chars', []) 29 | if self.reason: 30 | self.message += f'\nReason: {self.reason}' 31 | elif self.invalid_chars: 32 | self.message += 'Invalid characters: {}'.format(', '.join(self.invalid_chars)) 33 | super().__init__(self.code, self.message) 34 | 35 | 36 | class RateLimitError(RequestError): 37 | """Raised when the rate limit is reached.""" 38 | 39 | def __init__(self, code, url): 40 | self.code = code 41 | self.url = url 42 | self.message = 'The rate limit has been reached.' 43 | super().__init__(self.code, self.message) 44 | 45 | 46 | class UnexpectedError(RequestError): 47 | """Raised if an unknown error has occured.""" 48 | 49 | def __init__(self, url, code, text): 50 | self.code = code 51 | self.url = url 52 | self.message = f'An unexpected error has occured.\n{text}' 53 | super().__init__(self.code, self.message) 54 | 55 | 56 | class ServerError(RequestError): 57 | """Raised if the API is down.""" 58 | 59 | def __init__(self, code, url): 60 | self.code = code 61 | self.url = url 62 | self.message = 'The API is down. Please be patient and try again later.' 63 | super().__init__(self.code, self.message) 64 | -------------------------------------------------------------------------------- /brawlstats/models.py: -------------------------------------------------------------------------------- 1 | from box import Box, BoxList 2 | 3 | from .utils import bstag 4 | 5 | __all__ = ['Player', 'Club', 'Members', 'Ranking', 'BattleLog', 'Brawlers', 'EventRotation'] 6 | 7 | 8 | class BaseBox: 9 | def __init__(self, client, data): 10 | self.client = client 11 | self.from_data(data) 12 | 13 | def from_data(self, data): 14 | self.raw_data = data 15 | self._boxed_data = Box(data, camel_killer_box=True) 16 | return self 17 | 18 | def __getattr__(self, attr): 19 | try: 20 | return getattr(self._boxed_data, attr) 21 | except AttributeError: 22 | try: 23 | return super().__getattr__(attr) 24 | except AttributeError: 25 | return None # users can use an if statement rather than try/except to find a missing attribute 26 | 27 | def __getitem__(self, item): 28 | try: 29 | return self._boxed_data[item] 30 | except IndexError: 31 | raise IndexError(f'No such index: {item}') 32 | 33 | 34 | class BaseBoxList(BaseBox): 35 | def from_data(self, data): 36 | self.raw_data = data 37 | self._boxed_data = BoxList(data, camel_killer_box=True) 38 | return self 39 | 40 | def __len__(self): 41 | return sum(1 for i in self) 42 | 43 | 44 | class Members(BaseBoxList): 45 | """A list of the members in a club.""" 46 | 47 | def __init__(self, client, data): 48 | super().__init__(client, data['items']) 49 | 50 | def __repr__(self): 51 | return f'' 52 | 53 | 54 | class BattleLog(BaseBoxList): 55 | """A player battle object with all of its attributes.""" 56 | 57 | def __init__(self, client, data): 58 | super().__init__(client, data['items']) 59 | 60 | 61 | class Club(BaseBox): 62 | """A club object with all of its attributes.""" 63 | 64 | def __repr__(self): 65 | return f"" 66 | 67 | def __str__(self): 68 | return f'{self.name} ({self.tag})' 69 | 70 | def get_members(self) -> Members: 71 | """Gets the members of a club. 72 | Note: It is preferred to get the members 73 | via Club.members since this method makes 74 | an extra API call but returns the same data. 75 | 76 | Returns 77 | ------- 78 | Members 79 | A list of the members in a club. 80 | """ 81 | url = f'{self.client.api.CLUB}/{bstag(self.tag)}/members' 82 | return self.client._get_model(url, model=Members) 83 | 84 | 85 | class Player(BaseBox): 86 | """A player object with all of its attributes.""" 87 | 88 | def __init__(self, *args, **kwargs): 89 | super().__init__(*args, **kwargs) 90 | self.team_victories = self.x3vs3_victories 91 | 92 | def __repr__(self): 93 | return f"" 94 | 95 | def __str__(self): 96 | return f'{self.name} ({self.tag})' 97 | 98 | def get_club(self) -> Club: 99 | """Gets the player's club. 100 | 101 | Returns 102 | ------- 103 | Club or None 104 | A list of the members in a club, or None if the player is not in a club. 105 | """ 106 | if not self.club: 107 | if self.client.is_async: 108 | async def wrapper(): 109 | return None 110 | return wrapper() 111 | return None 112 | 113 | url = f'{self.client.api.CLUB}/{bstag(self.club.tag)}' 114 | return self.client._get_model(url, model=Club) 115 | 116 | def get_battle_logs(self) -> BattleLog: 117 | """Gets the player's battle logs. 118 | 119 | Returns 120 | ------- 121 | BattleLog 122 | The battle log containing the player's most recent battles. 123 | """ 124 | url = f'{self.client.api.PROFILE}/{bstag(self.tag)}/battlelog' 125 | return self.client._get_model(url, model=BattleLog) 126 | 127 | 128 | class Ranking(BaseBoxList): 129 | """A player or club ranking that contains a list of players or clubs.""" 130 | 131 | def __init__(self, client, data): 132 | super().__init__(client, data['items']) 133 | 134 | def __repr__(self): 135 | return ''.format(len(self)) 136 | 137 | 138 | class Brawlers(BaseBoxList): 139 | """A list of available brawlers and information about them.""" 140 | 141 | def __init__(self, client, data): 142 | super().__init__(client, data['items']) 143 | 144 | 145 | class EventRotation(BaseBoxList): 146 | """A list of events in the current rotation.""" 147 | pass 148 | -------------------------------------------------------------------------------- /brawlstats/utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import os 3 | import re 4 | from datetime import datetime 5 | from functools import wraps 6 | from typing import Union 7 | 8 | from .errors import NotFoundError 9 | 10 | 11 | class API: 12 | def __init__(self, base_url, version=1): 13 | self.BASE = base_url or f'https://api.brawlstars.com/v{version}' 14 | self.PROFILE = self.BASE + '/players' 15 | self.CLUB = self.BASE + '/clubs' 16 | self.RANKINGS = self.BASE + '/rankings' 17 | self.BRAWLERS = self.BASE + '/brawlers' 18 | self.EVENT_ROTATION = self.BASE + '/events/rotation' 19 | 20 | # Get package version from __init__.py 21 | path = os.path.dirname(__file__) 22 | with open(os.path.join(path, '__init__.py')) as f: 23 | self.VERSION = re.search(r'^__version__ = [\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1) 24 | 25 | self.CURRENT_BRAWLERS = {} 26 | 27 | def set_brawlers(self, brawlers): 28 | self.CURRENT_BRAWLERS = {b['name'].lower(): int(b['id']) for b in brawlers} 29 | 30 | 31 | def bstag(tag): 32 | tag = tag.strip('#').upper() 33 | allowed = '0289PYLQGRJCUV' 34 | 35 | if len(tag) < 3: 36 | raise NotFoundError(404, reason='Tag less than 3 characters.') 37 | invalid = [c for c in tag if c not in allowed] 38 | if invalid: 39 | raise NotFoundError(404, invalid_chars=invalid) 40 | 41 | if not tag.startswith('%23'): 42 | tag = '%23' + tag 43 | 44 | return tag 45 | 46 | 47 | def get_datetime(timestamp: str, unix: bool=True) -> Union[int, datetime]: 48 | """Converts a %Y%m%dT%H%M%S.%fZ to a UNIX timestamp or a datetime.datetime object 49 | 50 | Parameters 51 | ---------- 52 | timestamp : str 53 | A timestamp in the %Y%m%dT%H%M%S.%fZ format, usually returned by the API in the 54 | ``battleTime`` field in battle log responses - e.g., 20200925T184431.000Z 55 | unix : bool, optional 56 | Whether to return a POSIX timestamp (seconds since epoch) or not, by default True 57 | 58 | Returns 59 | ------- 60 | Union[int, datetime.datetime] 61 | If unix=True it will return int, otherwise datetime.datetime 62 | """ 63 | time = datetime.strptime(timestamp, '%Y%m%dT%H%M%S.%fZ') 64 | 65 | if unix: 66 | return int(time.timestamp()) 67 | 68 | return time 69 | 70 | 71 | def nothing(value): 72 | """Function that returns the argument""" 73 | return value 74 | 75 | 76 | def typecasted(func): 77 | """Decorator that converts arguments via annotations. 78 | Source: https://github.com/cgrok/clashroyale/blob/master/clashroyale/official_api/utils.py#L11""" 79 | signature = inspect.signature(func).parameters.items() 80 | 81 | @wraps(func) 82 | def wrapper(*args, **kwargs): 83 | args = list(args) 84 | new_args = [] 85 | new_kwargs = {} 86 | for _, param in signature: 87 | converter = param.annotation 88 | if converter is inspect._empty: 89 | converter = nothing 90 | if param.kind is param.POSITIONAL_OR_KEYWORD: 91 | if args: 92 | to_conv = args.pop(0) 93 | new_args.append(converter(to_conv)) 94 | elif param.kind is param.VAR_POSITIONAL: 95 | for a in args: 96 | new_args.append(converter(a)) 97 | else: 98 | for k, v in kwargs.items(): 99 | nk, nv = converter(k, v) 100 | new_kwargs[nk] = nv 101 | return func(*new_args, **new_kwargs) 102 | return wrapper 103 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | Client 5 | ~~~~~~ 6 | 7 | .. autoclass:: brawlstats.Client 8 | :members: 9 | 10 | Data Models 11 | ~~~~~~~~~~~ 12 | 13 | .. autoclass:: brawlstats.models.Player 14 | :members: 15 | 16 | .. autoclass:: brawlstats.models.Club 17 | :members: 18 | 19 | .. autoclass:: brawlstats.models.Ranking 20 | :members: 21 | 22 | .. autoclass:: brawlstats.models.BattleLog 23 | :members: 24 | 25 | .. autoclass:: brawlstats.models.Members 26 | :members: 27 | 28 | .. autoclass:: brawlstats.models.Brawlers 29 | :members: 30 | 31 | .. autoclass:: brawlstats.models.EventRotation 32 | :members: 33 | 34 | 35 | Attributes of Data Models 36 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 37 | 38 | Note: These are subject to change at any time. Visit https://developer.brawlstars.com/#/documentation to view up-to-date information on the API. 39 | 40 | Player 41 | ~~~~~~ 42 | 43 | A full player object (all its statistics) 44 | 45 | Attributes: 46 | 47 | ============================================ ====================== 48 | Name Type 49 | ============================================ ====================== 50 | ``tag`` str 51 | ``name`` str 52 | ``name_color`` str 53 | ``trophies`` int 54 | ``highest_trophies`` int 55 | ``power_play_points`` int 56 | ``highest_power_play_points`` int 57 | ``exp_level`` int 58 | ``exp_points`` int 59 | ``is_qualified_from_championship_challenge`` bool 60 | ``x3vs3_victories`` / ``team_victories`` int 61 | ``solo_victories`` int 62 | ``duo_victories`` int 63 | ``best_robo_rumble_time`` int 64 | ``best_time_as_big_brawler`` int 65 | ``brawlers`` List[`PlayerBrawler`_] 66 | ``club.tag`` str 67 | ``club.name`` str 68 | ``icon.id`` int 69 | ============================================ ====================== 70 | 71 | Club 72 | ~~~~ 73 | 74 | A full club object to get a club's statistics. In order to get this, you 75 | must get it from the client or a player object. 76 | 77 | Attributes: 78 | 79 | ===================== =============== 80 | Name Type 81 | ===================== =============== 82 | ``tag`` str 83 | ``name`` str 84 | ``description`` str 85 | ``type`` str 86 | ``trophies`` int 87 | ``required_trophies`` int 88 | ``members`` List[`Member`_] 89 | ``badge_id`` int 90 | ===================== =============== 91 | 92 | Member 93 | ~~~~~~ 94 | 95 | Members is a list of club members. Get this by accessing 96 | ``Club.members`` or ``Club.get_members()``. 97 | The club's members are sorted in order of descending trophies. 98 | Each Member in the list has the following attributes: 99 | 100 | .. code:: py 101 | 102 | members = club.members 103 | # Prints club's best player's name and role 104 | print(members[0].name, members[0].role) 105 | 106 | Attributes: 107 | 108 | ============== ==== 109 | Name Type 110 | ============== ==== 111 | ``tag`` str 112 | ``name`` str 113 | ``name_color`` str 114 | ``role`` str 115 | ``trophies`` int 116 | ``icon.id`` int 117 | ============== ==== 118 | 119 | Ranking 120 | ~~~~~~~ 121 | 122 | A list of top players, clubs, or brawlers. 123 | Each item in the list has the following attributes: 124 | 125 | Player/Brawler Ranking attributes: 126 | 127 | ============== ==== 128 | Name Type 129 | ============== ==== 130 | ``tag`` str 131 | ``name`` str 132 | ``name_color`` str 133 | ``trophies`` int 134 | ``rank`` int 135 | ``club.name`` str 136 | ``icon.id`` int 137 | ============== ==== 138 | 139 | Club Ranking attributes: 140 | 141 | ================ ==== 142 | Name Type 143 | ================ ==== 144 | ``tag`` str 145 | ``name`` str 146 | ``trophies`` int 147 | ``rank`` int 148 | ``member_count`` int 149 | ``badge_id`` int 150 | ================ ==== 151 | 152 | PlayerBrawler 153 | ~~~~~~~~~~~~~ 154 | 155 | PlayerBrawlers is a list of brawler objects, each with the following attributes. 156 | The brawlers are sorted in order of descending trophies. 157 | Note: ``PlayerBrawler`` only represents a brawler that a player owns and 158 | can only be accessed from ``Player.brawlers``. 159 | 160 | .. code:: py 161 | 162 | brawlers = player.brawlers 163 | # List is sorted by descending trophies 164 | top_brawler = brawlers[0] 165 | # print the player's best brawler's name and trophies 166 | print(top_brawler.name, top_brawler.trophies) 167 | 168 | Attributes: 169 | 170 | ==================== ================== 171 | Name Type 172 | ==================== ================== 173 | ``id`` int 174 | ``name`` str 175 | ``power`` int 176 | ``rank`` int 177 | ``trophies`` int 178 | ``highest_trophies`` int 179 | ``star_powers`` List[`StarPower`_] 180 | ``gadgets`` List[`Gadget`_] 181 | ``gears`` List[`Gear`_] 182 | ==================== ================== 183 | 184 | StarPower 185 | ~~~~~~~~~ 186 | 187 | Attributes: 188 | 189 | ======== ==== 190 | Name Type 191 | ======== ==== 192 | ``id`` int 193 | ``name`` str 194 | ======== ==== 195 | 196 | Gadget 197 | ~~~~~~ 198 | 199 | Attributes: 200 | 201 | ======== ==== 202 | Name Type 203 | ======== ==== 204 | ``id`` int 205 | ``name`` str 206 | ======== ==== 207 | 208 | Gear 209 | ~~~~ 210 | 211 | Attributes: 212 | 213 | ========= ==== 214 | Name Type 215 | ========= ==== 216 | ``id`` int 217 | ``name`` str 218 | ``level`` int 219 | ========= ==== 220 | 221 | BattleLog 222 | ~~~~~~~~~ 223 | 224 | A BattleLog contains a list of items, each with the following attributes: 225 | 226 | Attributes: 227 | 228 | =============== =============== 229 | Name Type 230 | =============== =============== 231 | ``battle_time`` str 232 | ``event`` `Event`_ 233 | ``battle`` List[`Battle`_] 234 | =============== =============== 235 | 236 | Event 237 | ~~~~~ 238 | 239 | An object containing information about an event. 240 | 241 | Attributes: 242 | 243 | ======== ==== 244 | Name Type 245 | ======== ==== 246 | ``id`` int 247 | ``mode`` str 248 | ``map`` str 249 | ======== ==== 250 | 251 | Battle 252 | ~~~~~~ 253 | 254 | Each Battle object contains information about a battle. 255 | Note: The ``star_player`` attribute may not exist for certain modes 256 | that do not have a star player (e.g. showdown, duoShowdown). 257 | 258 | Attributes: 259 | 260 | ==================== =========================== 261 | Name Type 262 | ==================== =========================== 263 | ``mode`` str 264 | ``type`` str 265 | ``result`` str 266 | ``duration`` int 267 | ``trophy_change`` int 268 | ``star_player`` `BattlePlayer`_ 269 | ``teams`` List[List[`BattlePlayer`_]] 270 | ==================== =========================== 271 | 272 | BattlePlayer 273 | ~~~~~~~~~~~~ 274 | 275 | Represents a player who played in a battle. 276 | 277 | =========== ================ 278 | Name Type 279 | =========== ================ 280 | ``tag`` str 281 | ``name`` str 282 | ``brawler`` `BattleBrawler`_ 283 | =========== ================ 284 | 285 | BattleBrawler 286 | ~~~~~~~~~~~~~ 287 | 288 | Represents a brawler that was played in a battle. 289 | Note: ``BattlerBrawler`` only reprents brawlers that were played in a battle 290 | and can only be accessed from ``BattlePlayer.brawler``. 291 | 292 | ============ ==== 293 | Name Type 294 | ============ ==== 295 | ``id`` int 296 | ``name`` str 297 | ``power`` int 298 | ``trophies`` int 299 | ============ ==== 300 | 301 | Brawlers 302 | ~~~~~~~~ 303 | 304 | Represents a list of all brawlers in the game and information, 305 | with each item having the following attributes. 306 | Note: ``Brawlers`` only represents the brawler objects returned 307 | from ``Client.get_brawlers()``. 308 | 309 | Attributes: 310 | 311 | ==================== ================== 312 | Name Type 313 | ==================== ================== 314 | ``id`` int 315 | ``name`` str 316 | ``star_powers`` List[`StarPower`_] 317 | ``gadgets`` List[`Gadget`_] 318 | ==================== ================== 319 | 320 | EventRotation 321 | ~~~~~~~~~~~~~ 322 | 323 | Represents a list of events in the current rotation, 324 | each with the following attributes: 325 | 326 | Attributes: 327 | 328 | ============== ======== 329 | Name Type 330 | ============== ======== 331 | ``start_time`` str 332 | ``end_time`` str 333 | ``slot_id`` int 334 | ``event`` `Event`_ 335 | ============== ======== -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | # 5 | # Configuration file for the Sphinx documentation builder. 6 | # 7 | # This file does only contain a selection of the most common options. For a 8 | # full list see the documentation: 9 | # http://www.sphinx-doc.org/en/master/config 10 | 11 | # -- Path setup -------------------------------------------------------------- 12 | 13 | # If extensions (or modules to document with autodoc) are in another directory, 14 | # add these directories to sys.path here. If the directory is relative to the 15 | # documentation root, use os.path.abspath to make it absolute, like shown here. 16 | # 17 | # import os 18 | # import sys 19 | # sys.path.insert(0, os.path.abspath('.')) 20 | 21 | with open('../brawlstats/__init__.py') as f: 22 | version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1) 23 | 24 | # -- Project information ----------------------------------------------------- 25 | 26 | project = 'BrawlStats' 27 | copyright = '2018-2020, SharpBit' 28 | author = 'SharpBit' 29 | 30 | # The full version, including alpha/beta/rc tags 31 | release = version 32 | 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # If your documentation needs a minimal Sphinx version, state it here. 37 | # 38 | # needs_sphinx = '1.0' 39 | 40 | # Add any Sphinx extension module names here, as strings. They can be 41 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 42 | # ones. 43 | extensions = [ 44 | 'sphinx.ext.autodoc', 45 | 'sphinx.ext.viewcode', 46 | 'sphinx.ext.napoleon' 47 | ] 48 | 49 | # Add any paths that contain templates here, relative to this directory. 50 | templates_path = ['_templates'] 51 | 52 | # The suffix(es) of source filenames. 53 | # You can specify multiple suffix as a list of string: 54 | # 55 | # source_suffix = ['.rst', '.md'] 56 | source_suffix = '.rst' 57 | 58 | # The master toctree document. 59 | master_doc = 'index' 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | # 64 | # This is also used if you do content translation via gettext catalogs. 65 | # Usually you set "language" from the command line for these cases. 66 | language = None 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | # This pattern also affects html_static_path and html_extra_path. 71 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 72 | 73 | # The name of the Pygments (syntax highlighting) style to use. 74 | pygments_style = None 75 | 76 | 77 | # -- Options for HTML output ------------------------------------------------- 78 | 79 | # The theme to use for HTML and HTML Help pages. See the documentation for 80 | # a list of builtin themes. 81 | # 82 | html_theme = 'alabaster' 83 | 84 | # Theme options are theme-specific and customize the look and feel of a theme 85 | # further. For a list of options available for each theme, see the 86 | # documentation. 87 | # 88 | # html_theme_options = {} 89 | 90 | # Add any paths that contain custom static files (such as style sheets) here, 91 | # relative to this directory. They are copied after the builtin static files, 92 | # so a file named "default.css" will overwrite the builtin "default.css". 93 | html_static_path = ['_static'] 94 | 95 | # Custom sidebar templates, must be a dictionary that maps document names 96 | # to template names. 97 | # 98 | # The default sidebars (for documents that don't match any pattern) are 99 | # defined by theme itself. Builtin themes are using these templates by 100 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 101 | # 'searchbox.html']``. 102 | # 103 | # html_sidebars = {} 104 | 105 | 106 | # -- Options for HTMLHelp output --------------------------------------------- 107 | 108 | # Output file base name for HTML help builder. 109 | htmlhelp_basename = 'BrawlStatsdoc' 110 | 111 | 112 | # -- Options for LaTeX output ------------------------------------------------ 113 | 114 | latex_elements = { 115 | # The paper size ('letterpaper' or 'a4paper'). 116 | # 117 | # 'papersize': 'letterpaper', 118 | 119 | # The font size ('10pt', '11pt' or '12pt'). 120 | # 121 | # 'pointsize': '10pt', 122 | 123 | # Additional stuff for the LaTeX preamble. 124 | # 125 | # 'preamble': '', 126 | 127 | # Latex figure (float) alignment 128 | # 129 | # 'figure_align': 'htbp', 130 | } 131 | 132 | # Grouping the document tree into LaTeX files. List of tuples 133 | # (source start file, target name, title, 134 | # author, documentclass [howto, manual, or own class]). 135 | latex_documents = [ 136 | (master_doc, 'BrawlStats.tex', 'BrawlStats Documentation', 137 | 'SharpBit', 'manual'), 138 | ] 139 | 140 | 141 | # -- Options for manual page output ------------------------------------------ 142 | 143 | # One entry per manual page. List of tuples 144 | # (source start file, name, description, authors, manual section). 145 | man_pages = [ 146 | (master_doc, 'brawlstats', 'BrawlStats Documentation', 147 | [author], 1) 148 | ] 149 | 150 | 151 | # -- Options for Texinfo output ---------------------------------------------- 152 | 153 | # Grouping the document tree into Texinfo files. List of tuples 154 | # (source start file, target name, title, author, 155 | # dir menu entry, description, category) 156 | texinfo_documents = [ 157 | (master_doc, 'BrawlStats', 'BrawlStats Documentation', 158 | author, 'BrawlStats', 'One line description of project.', 159 | 'Miscellaneous'), 160 | ] 161 | 162 | 163 | # -- Options for Epub output ------------------------------------------------- 164 | 165 | # Bibliographic Dublin Core info. 166 | epub_title = project 167 | 168 | # The unique identifier of the text. This can be a ISBN number 169 | # or the project homepage. 170 | # 171 | # epub_identifier = '' 172 | 173 | # A unique identification for the text. 174 | # 175 | # epub_uid = '' 176 | 177 | # A list of files that should not be packed into the epub file. 178 | epub_exclude_files = ['search.html'] 179 | 180 | 181 | # -- Extension configuration ------------------------------------------------- 182 | 183 | # Napoleon settings 184 | napoleon_google_docstring = False 185 | napoleon_numpy_docstring = True 186 | napoleon_include_init_with_doc = False 187 | napoleon_include_private_with_doc = False 188 | napoleon_include_special_with_doc = True 189 | napoleon_use_admonition_for_examples = False 190 | napoleon_use_admonition_for_notes = False 191 | napoleon_use_admonition_for_references = False 192 | napoleon_use_ivar = False 193 | napoleon_use_param = True 194 | napoleon_use_rtype = True 195 | -------------------------------------------------------------------------------- /docs/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | ========== 3 | 4 | The possible exceptions thrown by the library. 5 | 6 | .. autoexception:: brawlstats.errors.RequestError 7 | :members: 8 | 9 | .. autoexception:: brawlstats.errors.Forbidden 10 | :members: 11 | 12 | .. autoexception:: brawlstats.errors.NotFoundError 13 | :members: 14 | 15 | .. autoexception:: brawlstats.errors.RateLimitError 16 | :members: 17 | 18 | .. autoexception:: brawlstats.errors.UnexpectedError 19 | :members: 20 | 21 | .. autoexception:: brawlstats.errors.ServerError 22 | :members: 23 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Brawl Stats' documentation! 2 | ====================================== 3 | 4 | .. image:: https://img.shields.io/pypi/v/brawlstats.svg 5 | :target: https://pypi.org/project/brawlstats/ 6 | :alt: PyPi 7 | 8 | .. image:: https://github.com/SharpBit/brawlstats/actions/workflows/tests.yml/badge.svg 9 | :target: https://github.com/SharpBit/brawlstats/actions/workflows/tests.yml 10 | :alt: GitHub Actions Tests 11 | 12 | .. image:: https://img.shields.io/pypi/pyversions/brawlstats.svg 13 | :target: https://pypi.org/project/brawlstats/ 14 | :alt: Supported Versions 15 | 16 | .. image:: https://img.shields.io/github/license/SharpBit/brawlstats.svg 17 | :target: https://github.com/SharpBit/brawlstats/blob/master/LICENSE 18 | :alt: MIT License 19 | 20 | .. image:: https://readthedocs.org/projects/brawlstats/badge/?version=stable 21 | :target: https://brawlstats.readthedocs.io/en/stable/?badge=stable 22 | :alt: Documentation Status 23 | 24 | - BrawlStats is a sync and async Python API wrapper to fetch statistics from the official Brawl Stars API. 25 | - Python 3.9 or later is required. 26 | 27 | Features 28 | ~~~~~~~~ 29 | 30 | - Easy to use with an object oriented design. 31 | - Use the same client for sync and async usage. 32 | - Get a player profile and battlelog. 33 | - Get a club and its members. 34 | - Get the top 200 rankings for players, clubs, or a specific brawler. 35 | - Get information about all the brawlers in the game. 36 | - Get information about the current event rotation! 37 | 38 | Installation 39 | ~~~~~~~~~~~~ 40 | 41 | Install the latest stable build: 42 | 43 | :: 44 | 45 | pip install brawlstats 46 | 47 | .. toctree:: 48 | :maxdepth: 3 49 | 50 | api 51 | exceptions 52 | logging 53 | 54 | Indices 55 | ~~~~~~~ 56 | 57 | * :ref:`genindex` 58 | * :ref:`search` -------------------------------------------------------------------------------- /docs/logging.rst: -------------------------------------------------------------------------------- 1 | Setting Up Logging 2 | ================== 3 | 4 | *brawlstats* logs errors and debug information via the :mod:`logging` python 5 | module. It is strongly recommended that the logging module is 6 | configured, as no errors or warnings will be output if it is not set up. 7 | Configuration of the ``logging`` module can be as simple as 8 | 9 | .. code-block:: python 10 | 11 | import logging 12 | 13 | logging.basicConfig(level=logging.DEBUG) 14 | 15 | Placed at the start of the application. This will output the logs from 16 | *brawlstats* as well as other libraries that uses the ``logging`` module 17 | directly to the console. 18 | 19 | The optional ``level`` argument specifies what level of events to log 20 | out and can any of ``CRITICAL``, ``ERROR``, ``WARNING``, ``INFO``, and 21 | ``DEBUG`` and if not specified defaults to ``WARNING``. 22 | 23 | More advanced setups are possible with the :mod:`logging` module. For example, 24 | to write the logs to a file called ``brawlstars.log`` instead of 25 | outputting them to to the console, the following snippet can be used: 26 | 27 | .. code-block:: python 28 | 29 | import brawlstats 30 | import logging 31 | 32 | logger = logging.getLogger('brawlstats') 33 | logger.setLevel(logging.DEBUG) 34 | handler = logging.FileHandler(filename='brawlstars.log', encoding='utf-8', mode='w') 35 | handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s')) 36 | logger.addHandler(handler) 37 | 38 | This is recommended, especially at verbose levels such as ``INFO``, 39 | and ``DEBUG`` as there are a lot of events logged and it would clog the 40 | stdout of your program. 41 | 42 | 43 | Currently, the following things are logged: 44 | 45 | - ``DEBUG``: API Requests, Cache Hits 46 | 47 | 48 | 49 | For more information, check the documentation and tutorial of the 50 | :mod:`logging` module. -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | sphinx -------------------------------------------------------------------------------- /examples/async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import brawlstats 4 | 5 | # Do not post your token on a public github! 6 | client = brawlstats.Client('token', is_async=True) 7 | 8 | 9 | # await only works in an async loop 10 | async def main(): 11 | player = await client.get_profile('V2LQY9UY') 12 | print(player.trophies) # access attributes using dot.notation 13 | print(player.solo_victories) # use snake_case instead of camelCase 14 | 15 | club = await player.get_club() 16 | if club is not None: # check if the player is in a club 17 | print(club.tag) 18 | members = await club.get_members() # members sorted by trophies 19 | 20 | # gets best 5 players or returns all members if the club has less than 5 members 21 | index = max(5, len(members)) 22 | best_players = members[:index] 23 | for player in best_players: 24 | print(player.name, player.trophies) # prints name and trophies 25 | 26 | # get top 5 players in the world 27 | ranking = await client.get_rankings(ranking='players', limit=5) 28 | for player in ranking: 29 | print(player.name, player.rank) 30 | 31 | # get top 5 mortis players in the US 32 | ranking = await client.get_rankings( 33 | ranking='brawlers', 34 | region='us', 35 | limit=5, 36 | brawler='mortis' 37 | ) 38 | for player in ranking: 39 | print(player.name, player.rank) 40 | 41 | # Gets a player's recent battles 42 | battles = await client.get_battle_logs('UL0GCC8') 43 | print(battles[0].battle.mode) 44 | 45 | # run the async loop 46 | loop = asyncio.get_event_loop() 47 | loop.run_until_complete(main()) 48 | -------------------------------------------------------------------------------- /examples/discord_cog.py: -------------------------------------------------------------------------------- 1 | # NOTE: This discord example is outdated as Discord's Bot API has changed 2 | 3 | import discord 4 | from discord.ext import commands 5 | 6 | import brawlstats 7 | 8 | 9 | class BrawlStars(commands.Cog, name='Brawl Stars'): 10 | """A simple cog for Brawl Stars commands using discord.py""" 11 | 12 | def __init__(self, bot): 13 | self.bot = bot 14 | self.client = brawlstats.Client('token', is_async=True) 15 | 16 | @commands.command() 17 | async def profile(self, ctx, tag: str): 18 | """Get a brawl stars profile""" 19 | try: 20 | player = await self.client.get_profile(tag) 21 | except brawlstats.RequestError as e: # catches all exceptions 22 | return await ctx.send(f'```\n{e.code}: {e.message}\n```') # sends code and error message 23 | em = discord.Embed(title=f'{player.name} ({player.tag})') 24 | 25 | em.description = f'Trophies: {player.trophies}' # you could make this better by using embed fields 26 | await ctx.send(embed=em) 27 | 28 | 29 | def setup(bot): 30 | bot.add_cog(BrawlStars(bot)) 31 | -------------------------------------------------------------------------------- /examples/sync.py: -------------------------------------------------------------------------------- 1 | import brawlstats 2 | 3 | # Do not post your token on a public github! 4 | client = brawlstats.Client('token') 5 | 6 | 7 | player = client.get_profile('V2LQY9UY') 8 | print(player.trophies) # access attributes using dot.notation 9 | print(player.solo_victories) # use snake_case instead of camelCase 10 | 11 | club = player.get_club() 12 | if club is not None: # check if the player is in a club 13 | print(club.tag) 14 | members = club.get_members() # members sorted by trophies 15 | 16 | # gets best 5 players or returns all members if the club has less than 5 members 17 | index = max(5, len(members)) 18 | best_players = members[:index] 19 | for player in best_players: 20 | print(player.name, player.trophies) # prints name and trophies 21 | 22 | # gets top 5 players in the world 23 | ranking = client.get_rankings(ranking='players', limit=5) 24 | for player in ranking: 25 | print(player.name, player.rank) 26 | 27 | # get top 5 mortis players in the US 28 | ranking = client.get_rankings( 29 | ranking='brawlers', 30 | region='us', 31 | limit=5, 32 | brawler='mortis' 33 | ) 34 | for player in ranking: 35 | print(player.name, player.rank) 36 | 37 | # Gets a player's recent battles 38 | battles = client.get_battle_logs('UL0GCC8') 39 | print(battles[0].battle.mode) 40 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | pluggy>=0.12.0,<1.0.0 3 | pytest 4 | pytest-asyncio~=0.21.0 5 | python-dotenv 6 | tox-travis -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp>=3.6.0 2 | requests 3 | python-box 4 | cachetools>=3.1.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from setuptools import find_packages, setup 4 | 5 | with open('README.rst', encoding='utf8') as f: 6 | long_description = f.read() 7 | 8 | with open('brawlstats/__init__.py') as f: 9 | version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1) 10 | 11 | requirements = [] 12 | with open('requirements.txt') as f: 13 | requirements = f.read().splitlines() 14 | 15 | setup( 16 | name='brawlstats', 17 | version=version, 18 | description='BrawlStats is an easy-to-use sync and async Python API wrapper' 19 | 'to fetch statistics from the official Brawl Stars API', 20 | long_description=long_description, 21 | long_description_content_type='text/x-rst', 22 | url='https://github.com/SharpBit/brawlstats', 23 | author='SharpBit', 24 | author_email='sharpbit3618@gmail.com', 25 | license='MIT', 26 | keywords=[ 27 | 'brawl stars, brawl stats, brawlstats, supercell, python, sync, async, ' 28 | 'python wrapper, api wrapper, python api wrapper, python 3.9, python 3.10, python 3.11, python 3.12' 29 | ], 30 | packages=find_packages(), 31 | install_requires=requirements, 32 | python_requires='>=3.9.0', 33 | project_urls={ 34 | 'Source Code': 'https://github.com/SharpBit/brawlstats', 35 | 'Issue Tracker': 'https://github.com/SharpBit/brawlstats/issues', 36 | 'Documentation': 'https://brawlstats.readthedocs.io/en/stable', 37 | }, 38 | classifiers=[ 39 | 'Development Status :: 5 - Production/Stable', 40 | 'Intended Audience :: Developers', 41 | 'Topic :: Games/Entertainment :: Real Time Strategy', 42 | 'License :: OSI Approved :: MIT License', 43 | 'Programming Language :: Python :: 3.9', 44 | 'Programming Language :: Python :: 3.10', 45 | 'Programming Language :: Python :: 3.11', 46 | 'Programming Language :: Python :: 3.12', 47 | 'Natural Language :: English' 48 | ] 49 | ) 50 | -------------------------------------------------------------------------------- /tests/test_async.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | import aiohttp 5 | import brawlstats 6 | import pytest 7 | 8 | from dotenv import load_dotenv 9 | 10 | pytestmark = pytest.mark.asyncio 11 | load_dotenv() 12 | 13 | 14 | class TestAsyncClient(unittest.IsolatedAsyncioTestCase): 15 | 16 | PLAYER_TAG = '#V2LQY9UY' 17 | CLUB_TAG = '#UL0GCC8' 18 | 19 | async def asyncSetUp(self): 20 | session = aiohttp.ClientSession(trust_env=True) 21 | self.client = brawlstats.Client( 22 | token=os.getenv('TOKEN'), 23 | session=session, 24 | base_url=os.getenv('BASE_URL'), 25 | is_async=True 26 | ) 27 | 28 | async def asyncTearDown(self): 29 | await self.client.close() 30 | 31 | async def test_get_player(self): 32 | player = await self.client.get_player(self.PLAYER_TAG) 33 | self.assertIsInstance(player, brawlstats.Player) 34 | self.assertEqual(player.tag, self.PLAYER_TAG) 35 | 36 | club = await player.get_club() 37 | self.assertIsInstance(club, brawlstats.Club) 38 | self.assertEqual(club.tag, self.CLUB_TAG) 39 | 40 | battle_logs = await player.get_battle_logs() 41 | self.assertIsInstance(battle_logs, brawlstats.BattleLog) 42 | 43 | with self.assertRaises(brawlstats.NotFoundError): 44 | await self.client.get_player('2PPPPPPP') 45 | 46 | with self.assertRaises(brawlstats.NotFoundError): 47 | await self.client.get_player('P') 48 | 49 | with self.assertRaises(brawlstats.NotFoundError): 50 | await self.client.get_player('AAA') 51 | 52 | async def test_get_battle_logs(self): 53 | battle_logs = await self.client.get_battle_logs(self.PLAYER_TAG) 54 | self.assertIsInstance(battle_logs, brawlstats.BattleLog) 55 | 56 | async def test_get_club(self): 57 | club = await self.client.get_club(self.CLUB_TAG) 58 | self.assertIsInstance(club, brawlstats.Club) 59 | self.assertEqual(club.tag, self.CLUB_TAG) 60 | 61 | club_members = await club.get_members() 62 | self.assertIsInstance(club_members, brawlstats.Members) 63 | self.assertIn(self.PLAYER_TAG, [x.tag for x in club_members]) 64 | 65 | with self.assertRaises(brawlstats.NotFoundError): 66 | await self.client.get_club('8GGGGGGG') 67 | 68 | with self.assertRaises(brawlstats.NotFoundError): 69 | await self.client.get_club('P') 70 | 71 | with self.assertRaises(brawlstats.NotFoundError): 72 | await self.client.get_club('AAA') 73 | 74 | async def test_get_club_members(self): 75 | club_members = await self.client.get_club_members(self.CLUB_TAG) 76 | self.assertIsInstance(club_members, brawlstats.Members) 77 | self.assertIn(self.PLAYER_TAG, [x.tag for x in club_members]) 78 | 79 | with self.assertRaises(brawlstats.NotFoundError): 80 | await self.client.get_club_members('8GGGGGGG') 81 | 82 | async def test_get_rankings(self): 83 | player_ranking = await self.client.get_rankings(ranking='players') 84 | self.assertIsInstance(player_ranking, brawlstats.Ranking) 85 | 86 | us_player_ranking = await self.client.get_rankings(ranking='players', region='US', limit=1) 87 | self.assertIsInstance(us_player_ranking, brawlstats.Ranking) 88 | self.assertTrue(len(us_player_ranking) == 1) 89 | 90 | club_ranking = await self.client.get_rankings(ranking='clubs') 91 | self.assertIsInstance(club_ranking, brawlstats.Ranking) 92 | 93 | us_club_ranking = await self.client.get_rankings(ranking='clubs', region='US', limit=1) 94 | self.assertIsInstance(us_club_ranking, brawlstats.Ranking) 95 | self.assertTrue(len(us_club_ranking) == 1) 96 | 97 | brawler_ranking = await self.client.get_rankings(ranking='brawlers', brawler='Shelly') 98 | self.assertIsInstance(brawler_ranking, brawlstats.Ranking) 99 | 100 | us_brawler_ranking = await self.client.get_rankings(ranking='brawlers', brawler=16000000, region='US', limit=1) 101 | self.assertIsInstance(us_brawler_ranking, brawlstats.Ranking) 102 | self.assertTrue(len(us_brawler_ranking) == 1) 103 | 104 | with self.assertRaises(ValueError): 105 | await self.client.get_rankings(ranking='people') 106 | 107 | with self.assertRaises(ValueError): 108 | await self.client.get_rankings(ranking='people', limit=0) 109 | 110 | with self.assertRaises(ValueError): 111 | await self.client.get_rankings(ranking='brawlers', brawler='SharpBit') 112 | 113 | async def test_get_brawlers(self): 114 | brawlers = await self.client.get_brawlers() 115 | self.assertIsInstance(brawlers, brawlstats.Brawlers) 116 | 117 | async def test_get_event_rotation(self): 118 | events = await self.client.get_event_rotation() 119 | self.assertIsInstance(events, brawlstats.EventRotation) 120 | 121 | 122 | if __name__ == '__main__': 123 | unittest.main() 124 | -------------------------------------------------------------------------------- /tests/test_blocking.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | import brawlstats 5 | from dotenv import load_dotenv 6 | 7 | load_dotenv() 8 | 9 | 10 | class TestBlockingClient(unittest.TestCase): 11 | 12 | PLAYER_TAG = '#V2LQY9UY' 13 | CLUB_TAG = '#UL0GCC8' 14 | 15 | def setUp(self): 16 | self.client = brawlstats.Client( 17 | token=os.getenv('TOKEN'), 18 | base_url=os.getenv('BASE_URL') 19 | ) 20 | 21 | def tearDown(self): 22 | self.client.close() 23 | 24 | def test_get_player(self): 25 | player = self.client.get_player(self.PLAYER_TAG) 26 | self.assertIsInstance(player, brawlstats.Player) 27 | self.assertEqual(player.tag, self.PLAYER_TAG) 28 | 29 | club = player.get_club() 30 | self.assertIsInstance(club, brawlstats.Club) 31 | self.assertEqual(club.tag, self.CLUB_TAG) 32 | 33 | battle_logs = player.get_battle_logs() 34 | self.assertIsInstance(battle_logs, brawlstats.BattleLog) 35 | 36 | self.assertRaises(brawlstats.NotFoundError, self.client.get_player, '2PPPPPPP') 37 | self.assertRaises(brawlstats.NotFoundError, self.client.get_player, 'P') 38 | self.assertRaises(brawlstats.NotFoundError, self.client.get_player, 'AAA') 39 | 40 | def test_get_battle_logs(self): 41 | battle_logs = self.client.get_battle_logs(self.PLAYER_TAG) 42 | self.assertIsInstance(battle_logs, brawlstats.BattleLog) 43 | 44 | def test_get_club(self): 45 | club = self.client.get_club(self.CLUB_TAG) 46 | self.assertIsInstance(club, brawlstats.Club) 47 | self.assertEqual(club.tag, self.CLUB_TAG) 48 | 49 | club_members = club.get_members() 50 | self.assertIsInstance(club_members, brawlstats.Members) 51 | self.assertIn(self.PLAYER_TAG, [x.tag for x in club_members]) 52 | 53 | self.assertRaises(brawlstats.NotFoundError, self.client.get_club, '8GGGGGGG') 54 | self.assertRaises(brawlstats.NotFoundError, self.client.get_club, 'P') 55 | self.assertRaises(brawlstats.NotFoundError, self.client.get_club, 'AAA') 56 | 57 | def test_get_club_members(self): 58 | club_members = self.client.get_club_members(self.CLUB_TAG) 59 | self.assertIsInstance(club_members, brawlstats.Members) 60 | self.assertIn(self.PLAYER_TAG, [x.tag for x in club_members]) 61 | 62 | self.assertRaises(brawlstats.NotFoundError, self.client.get_club_members, '8GGGGGGG') 63 | 64 | def test_get_rankings(self): 65 | player_ranking = self.client.get_rankings(ranking='players') 66 | self.assertIsInstance(player_ranking, brawlstats.Ranking) 67 | 68 | us_player_ranking = self.client.get_rankings(ranking='players', region='US', limit=1) 69 | self.assertIsInstance(us_player_ranking, brawlstats.Ranking) 70 | self.assertTrue(len(us_player_ranking) == 1) 71 | 72 | self.assertRaises(ValueError, self.client.get_rankings, ranking='people') 73 | self.assertRaises(ValueError, self.client.get_rankings, ranking='people', limit=0) 74 | self.assertRaises(ValueError, self.client.get_rankings, ranking='brawlers', brawler='SharpBit') 75 | 76 | club_ranking = self.client.get_rankings(ranking='clubs') 77 | self.assertIsInstance(club_ranking, brawlstats.Ranking) 78 | 79 | us_club_ranking = self.client.get_rankings(ranking='clubs', region='US', limit=1) 80 | self.assertIsInstance(us_club_ranking, brawlstats.Ranking) 81 | self.assertTrue(len(us_club_ranking) == 1) 82 | 83 | brawler_ranking = self.client.get_rankings(ranking='brawlers', brawler='Shelly') 84 | self.assertIsInstance(brawler_ranking, brawlstats.Ranking) 85 | 86 | us_brawler_ranking = self.client.get_rankings(ranking='brawlers', brawler=16000000, region='US', limit=1) 87 | self.assertIsInstance(us_brawler_ranking, brawlstats.Ranking) 88 | self.assertTrue(len(us_brawler_ranking) == 1) 89 | 90 | def test_get_brawlers(self): 91 | brawlers = self.client.get_brawlers() 92 | self.assertIsInstance(brawlers, brawlstats.Brawlers) 93 | 94 | def test_get_event_rotation(self): 95 | events = self.client.get_event_rotation() 96 | self.assertIsInstance(events, brawlstats.EventRotation) 97 | 98 | 99 | if __name__ == '__main__': 100 | unittest.main() 101 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=120 3 | exclude = .tox,__init__.py 4 | ignore = E252,E302,E731,W605 5 | 6 | [tox] 7 | envlist = py39, py310, py311, py312 8 | 9 | [testenv] 10 | deps = -Ur{toxinidir}/requirements-dev.txt 11 | commands = 12 | flake8 . 13 | pytest 14 | passenv = 15 | TOKEN 16 | BASE_URL --------------------------------------------------------------------------------