├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── docs.yml │ ├── pypi.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── LICENSE ├── README.md ├── docs ├── Makefile ├── README.md ├── _static │ ├── attribute_tables.css │ └── outdated_code_blocks.css ├── api │ ├── bases.rst │ ├── clients.rst │ ├── exceptions.rst │ ├── index.rst │ └── objects.rst ├── changelog.rst ├── conf.py ├── extensions │ ├── attribute_table.py │ ├── exception_hierarchy.py │ ├── opt_in.py │ └── outdated_code_blocks.py ├── images │ └── logo.ico ├── index.rst ├── make.bat ├── migrating.rst └── response_flags.rst ├── examples ├── basic.py ├── discord_integration.py └── sync_basic.py ├── fortnite_api ├── __init__.py ├── abc.py ├── account.py ├── aes.py ├── all.py ├── asset.py ├── banner.py ├── client.py ├── cosmetics │ ├── __init__.py │ ├── br.py │ ├── car.py │ ├── common.py │ ├── instrument.py │ ├── lego_kit.py │ ├── track.py │ └── variants │ │ ├── __init__.py │ │ ├── bean.py │ │ └── lego.py ├── creator_code.py ├── enums.py ├── errors.py ├── flags.py ├── http.py ├── images.py ├── map.py ├── new.py ├── new_display_asset.py ├── news.py ├── playlist.py ├── proxies.py ├── py.typed ├── shop.py ├── stats.py └── utils.py ├── pyproject.toml ├── requirements.txt └── tests ├── conftest.py ├── test_account.py ├── test_aes.py ├── test_asset.py ├── test_async_methods.py ├── test_beta.py ├── test_client.py ├── test_enum.py ├── test_proxy.py ├── test_ratelimits.py ├── test_reconstruct.py ├── test_repr.py ├── test_stats.py └── test_sync_methods.py /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report broken or incorrect behaviour 3 | labels: unconfirmed bug 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: > 8 | Thanks for taking the time to fill out a bug. 9 | 10 | Please note that this form is for bugs only! 11 | - type: input 12 | attributes: 13 | label: Summary 14 | description: A simple summary of your bug report 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Reproduction Steps 20 | description: What you did to make it happen. 21 | validations: 22 | required: true 23 | - type: textarea 24 | attributes: 25 | label: Minimal Reproducible Code 26 | description: A short snippet of code that showcases the bug. 27 | render: python 28 | - type: textarea 29 | attributes: 30 | label: Expected Results 31 | description: What did you expect to happen? 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: Actual Results 37 | description: What actually happened? 38 | validations: 39 | required: true 40 | - type: checkboxes 41 | attributes: 42 | label: Checklist 43 | description: > 44 | Let's make sure you've properly done due diligence when reporting this issue! 45 | options: 46 | - label: I have searched the open issues for duplicates. 47 | required: true 48 | - label: I have shown the entire traceback, if possible. 49 | required: true 50 | - label: I have removed my API key from display, if visible. 51 | required: true 52 | - type: textarea 53 | attributes: 54 | label: Additional Context 55 | description: If there is anything else to say, please do so here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Discord Server 4 | about: Use our official Discord server to ask for help and questions as well. 5 | url: https://discord.gg/AqzEcMm -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a feature for this library 3 | labels: feature request 4 | body: 5 | - type: input 6 | attributes: 7 | label: Summary 8 | description: A short summary of what your feature request is. 9 | validations: 10 | required: true 11 | - type: textarea 12 | attributes: 13 | label: The Problem 14 | description: > 15 | What problem is your feature trying to solve? 16 | What becomes easier or possible when this feature is implemented? 17 | validations: 18 | required: true 19 | - type: textarea 20 | attributes: 21 | label: The Ideal Solution 22 | description: > 23 | What is your ideal solution to the problem? 24 | What would you like this feature to do? 25 | validations: 26 | required: true 27 | - type: textarea 28 | attributes: 29 | label: The Current Solution 30 | description: > 31 | What is the current solution to the problem, if any? 32 | validations: 33 | required: false 34 | - type: textarea 35 | attributes: 36 | label: Additional Context 37 | description: If there is anything else to say, please do so here. -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | 4 | 5 | ## Checklist 6 | 7 | 8 | 9 | - [ ] If code changes were made then they have been tested. 10 | - [ ] I have updated the documentation to reflect the changes. 11 | - [ ] This PR fixes an issue. 12 | - [ ] This PR adds something new (e.g. new method or parameters). 13 | - [ ] This PR is a breaking change (e.g. methods or parameters removed/renamed) 14 | - [ ] This PR is **not** a code change (e.g. documentation, README, ...) 15 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # This workflow builds the Sphinx documentation, and in doing so - checks for errors in the documentation. 2 | name: Docs 3 | 4 | on: 5 | push: 6 | pull_request: 7 | types: [ opened, reopened, synchronize ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | docs: 12 | name: Test Docs 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup Python 3.9 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: "3.9" 23 | cache: "pip" # Cache the pip packages to speed up the workflow 24 | 25 | - name: Install Dependencies and Package 26 | run: | 27 | python -m pip install -U pip setuptools 28 | pip install -U -r requirements.txt 29 | pip install -e .[docs] 30 | 31 | - name: Build Documentation 32 | run: | 33 | cd docs 34 | sphinx-build -b html -j auto -a -n -T -W --keep-going . _build/html -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: PyPI Release 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | pypi: 10 | runs-on: ubuntu-latest 11 | environment: 12 | name: pypi 13 | url: https://pypi.org/p/fortnite-api 14 | permissions: 15 | id-token: write 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Python 3.9 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: "3.9" 24 | 25 | - name: Install Dependencies 26 | run: python -m pip install -U pip setuptools build 27 | 28 | - name: Build Project 29 | run: python -m build 30 | 31 | - name: Publish a Python distribution to PyPI 32 | uses: pypa/gh-action-pypi-publish@release/v1 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will ensure that the pushed contents to the repo 2 | # are not majorly breaking, and they comply with black standards. 3 | 4 | name: Pytests 5 | 6 | on: 7 | push: 8 | pull_request: 9 | types: [ opened, reopened, synchronize ] 10 | workflow_dispatch: 11 | 12 | jobs: 13 | pytest: 14 | name: Pytest 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Setup Python 3.9 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: "3.9" 25 | cache: "pip" # Cache the pip packages to speed up the workflow 26 | 27 | - name: Install Dependencies and Package 28 | run: | 29 | python -m pip install -U pip setuptools 30 | pip install -U -r requirements.txt 31 | pip install -e .[tests] 32 | 33 | - name: Run Pytest Checks 34 | shell: bash 35 | env: 36 | TEST_API_KEY: ${{ secrets.TEST_API_KEY }} 37 | run: python -m pytest --cov=fortnite_api --import-mode=importlib -vs tests/ 38 | 39 | black: 40 | name: Black Formatting Check 41 | runs-on: ubuntu-latest 42 | 43 | # Checkout the repository 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v4 47 | 48 | - name: Setup Python 3.9 49 | uses: actions/setup-python@v5 50 | with: 51 | python-version: "3.9" 52 | cache: "pip" # Cache the pip packages to speed up the workflow 53 | 54 | - name: Install Dependencies and Project 55 | run: | 56 | python -m pip install -U pip setuptools 57 | pip install -U -r requirements.txt 58 | pip install -e .[dev] 59 | 60 | - name: Run Black Check 61 | run: black --check --diff --verbose fortnite_api 62 | 63 | isort: 64 | name: Isort Formatting Check 65 | runs-on: ubuntu-latest 66 | 67 | # Checkout the repository 68 | steps: 69 | - name: Checkout 70 | uses: actions/checkout@v4 71 | 72 | - name: Setup Python 3.9 73 | uses: actions/setup-python@v5 74 | with: 75 | python-version: "3.9" 76 | cache: "pip" # Cache the pip packages to speed up the workflow 77 | 78 | - name: Install Dependencies and Project 79 | run: | 80 | python -m pip install -U pip setuptools 81 | pip install -U -r requirements.txt 82 | pip install -e .[dev] 83 | 84 | - name: Run Isort Check 85 | run: isort --check --diff fortnite_api -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | 4 | # Items for egg-info 5 | *.egg-info/ 6 | 7 | # Items for pytest 8 | .pytest_cache/ 9 | 10 | # Misc for python local development 11 | __pycache__/ 12 | 13 | # User-specific stuff 14 | .idea/**/workspace.xml 15 | .idea/**/tasks.xml 16 | .idea/**/usage.statistics.xml 17 | .idea/**/dictionaries 18 | .idea/**/shelf 19 | 20 | # Generated files 21 | .idea/**/contentModel.xml 22 | 23 | # Sensitive or high-churn files 24 | .idea/**/dataSources/ 25 | .idea/**/dataSources.ids 26 | .idea/**/dataSources.local.xml 27 | .idea/**/sqlDataSources.xml 28 | .idea/**/dynamic.xml 29 | .idea/**/uiDesigner.xml 30 | .idea/**/dbnavigator.xml 31 | 32 | # Gradle 33 | .idea/**/gradle.xml 34 | .idea/**/libraries 35 | 36 | # Gradle and Maven with auto-import 37 | # When using Gradle or Maven with auto-import, you should exclude module files, 38 | # since they will be recreated, and may cause churn. Uncomment if using 39 | # auto-import. 40 | # .idea/modules.xml 41 | # .idea/*.iml 42 | # .idea/modules 43 | 44 | # CMake 45 | cmake-build-*/ 46 | 47 | # Mongo Explorer plugin 48 | .idea/**/mongoSettings.xml 49 | 50 | # File-based project format 51 | *.iws 52 | 53 | # IntelliJ 54 | out/ 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # VS Code 60 | .vscode/ 61 | dependabot.yml 62 | .devcontainer/ 63 | .coverage 64 | 65 | # mpeltonen/sbt-idea plugin 66 | .idea_modules/ 67 | 68 | # JIRA plugin 69 | atlassian-ide-plugin.xml 70 | 71 | # Cursive Clojure plugin 72 | .idea/replstate.xml 73 | 74 | # Crashlytics plugin (for Android Studio and IntelliJ) 75 | com_crashlytics_export_strings.xml 76 | crashlytics.properties 77 | crashlytics-build.properties 78 | fabric.properties 79 | 80 | # Editor-based Rest Client 81 | .idea/httpRequests 82 | 83 | # Bot Files 84 | *.log 85 | *.log.* 86 | '.idea' 87 | 88 | # Development Test Files 89 | _.py 90 | .env -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Black formatter pre commit hook 2 | repos: 3 | - repo: https://github.com/psf/black 4 | rev: 24.10.0 5 | hooks: 6 | - id: black 7 | 8 | - repo: https://github.com/pycqa/isort 9 | rev: 5.13.2 10 | hooks: 11 | - id: isort 12 | name: isort (python) -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | version: 2 6 | 7 | build: 8 | os: ubuntu-24.04 9 | tools: 10 | python: "3.12" 11 | 12 | # Build documentation in the "docs/" directory with Sphinx 13 | sphinx: 14 | configuration: docs/conf.py 15 | builder: html 16 | 17 | # Optionally build your docs in additional formats such as PDF and ePub 18 | # formats: 19 | # - pdf 20 | # - epub 21 | 22 | # Optional but recommended, declare the Python requirements required 23 | # to build your documentation 24 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 25 | python: 26 | install: 27 | - method: pip 28 | path: . 29 | extra_requirements: 30 | - docs -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Luc1412 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sync/Async Python wrapper for [Fortnite-API.com](https://fortnite-api.com) 2 | 3 | [![Support](https://discordapp.com/api/guilds/621452110558527502/widget.png?style=shield)](https://discord.gg/T4tyYDK) 4 | ![GitHub issues](https://img.shields.io/github/issues/Fortnite-API/py-wrapper?logo=github) 5 | [![PyPI](https://img.shields.io/pypi/v/fortnite-api)](https://pypi.org/project/fortnite-api) 6 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fortnite-api?label=python%20version&logo=python&logoColor=yellow) 7 | [![PyPI - Downloads](https://img.shields.io/pypi/dm/fortnite-api)](https://pypi.org/project/fortnite-api) 8 | [![Documentation](https://img.shields.io/readthedocs/fortnite-api)](https://fortnite-api.readthedocs.io/) 9 | 10 | Welcome to the Fortnite API Python wrapper! This library offers a complete **async** and **sync** wrapper around the endpoints of [Fortnite-API.com](https://fortnite-api.com) 11 | 12 | The library's focus is to provide a simple and easy-to-use interface to interact with the API. The library is designed to be as user-friendly as possible, and it is easy to get started with. If you have any questions or need help, feel free to join the [official Discord server](https://discord.gg/T4tyYDK). 13 | 14 | ## Installation 15 | 16 | Note that **Python 3.9 or higher is required.** 17 | 18 | ```sh 19 | # Linux/macOS 20 | python3 -m pip install fortnite-api 21 | 22 | # Windows 23 | py -3 -m pip install fortnite-api 24 | ``` 25 | 26 | To install the developer version, you can use the following command: 27 | 28 | ```sh 29 | git clone https://github.com/Fortnite-API/py-wrapper 30 | cd py-wrapper 31 | python3 -m pip install . 32 | ``` 33 | 34 | ### Optional Dependencies 35 | 36 | - `speed`: An optional dependency that installs [`orjson`](https://github.com/ijl/orjson) for faster JSON serialization and deserialization. 37 | 38 | ```sh 39 | # Linux/macOS 40 | python3 -m pip install fortnite-api[speed] 41 | 42 | # Windows 43 | py -3 -m pip install fortnite-api[speed] 44 | ``` 45 | 46 | ## API Key 47 | 48 | For most endpoints, you do not need an API key. However, some endpoints, such as fetching statistics, require an API key. To use these endpoints, you need to set the `api_key` parameter in the constructor. 49 | 50 | ```python 51 | import asyncio 52 | import fortnite_api 53 | 54 | async def main(): 55 | async with fortnite_api.Client(api_key="your_api_key") as client: 56 | stats = await client.fetch_br_stats(name='some_username') 57 | print(stats) 58 | 59 | if __name__ == "__main__": 60 | asyncio.run(main()) 61 | ``` 62 | 63 | ### Generating an API Key 64 | 65 | You can generate an API key on by logging in with your Discord account. 66 | 67 | ## Quick Example 68 | 69 | ### Asynchronous Example 70 | 71 | ```python 72 | import asyncio 73 | import fortnite_api 74 | 75 | async def main() -> None: 76 | async with fortnite_api.Client() as client: 77 | all_cosmetics: fortnite_api.CosmeticsAll = await client.fetch_cosmetics_all() 78 | 79 | for br_cosmetic in all_cosmetics.br: 80 | print(br_cosmetic.name) 81 | 82 | if __name__ == "__main__": 83 | asyncio.run(main()) 84 | ``` 85 | 86 | ### Synchronous Example 87 | 88 | ```python 89 | import fortnite_api 90 | 91 | def main() -> None: 92 | client = fortnite_api.SyncClient() 93 | all_cosmetics: fortnite_api.CosmeticsAll = client.fetch_cosmetics_all() 94 | 95 | for br_cosmetic in all_cosmetics.br: 96 | print(br_cosmetic.name) 97 | 98 | if __name__ == "__main__": 99 | main() 100 | ``` 101 | 102 | More examples can be found in the `examples/` directory of the repository. 103 | 104 | ## Links 105 | 106 | - [Python Wrapper Documentation](https://fortnite-api.readthedocs.io/en/rewrite/) 107 | - [FortniteAPI API Documentation](https://fortnite-api.com) 108 | - [Official Discord Server](https://discord.gg/T4tyYDK) 109 | 110 | ## Contribute 111 | 112 | Every type of contribution is appreciated. 113 | 114 | ## Licenses 115 | 116 | - Fortnite-API Wrapper (MIT) [License](https://github.com/Fortnite-API/py-wrapper/blob/master/LICENSE) 117 | - requests (Apache) [License](https://github.com/psf/requests/blob/master/LICENSE) 118 | - aiohttp (Apache) [License](https://github.com/aio-libs/aiohttp/blob/6a5ab96bd9cb404b4abfd5160fe8f34a29d941e5/LICENSE.txt) 119 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | This is the documentation for the project. It is written in reStructuredText and can be found in this directory. 3 | 4 | ## Previewing Documentation 5 | ### Building for Development 6 | It's recommended to use [sphinx auto build](https://pypi.org/project/sphinx-autobuild/) to preview the documentation. 7 | In this directory, run the following command: 8 | 9 | ```bash 10 | sphinx-autobuild . ./_build/html 11 | ``` 12 | 13 | ### Building for Production 14 | To build the documentation for production, run the following command in this directory: 15 | 16 | ```bash 17 | sphinx-build -b html -j auto -a -n -T -W --keep-going . _build/html 18 | ``` 19 | 20 | Ensure you have the following dependencies installed in your environment: 21 | 22 | ```bash 23 | python -m pip install --upgrade pip setuptools 24 | pip install -U -r requirements.txt 25 | pip install -e .[docs] 26 | ``` -------------------------------------------------------------------------------- /docs/_static/attribute_tables.css: -------------------------------------------------------------------------------- 1 | /* 2 | Given the following HTML code we need to make it look as if it was a table 3 | 4 |
5 |
6 | _('Attributes') 7 |
    8 |
  • 9 | 10 |
  • 11 |
12 |
13 |
14 | _('Methods') 15 |
    16 |
  • 17 | D 18 | 19 |
  • 20 |
21 |
22 |
23 | 24 | So, the columns should be side by side and the list items should be displayed as rows. 25 | */ 26 | 27 | .py-attribute-table { 28 | display: flex; 29 | justify-content: space-between; 30 | border-radius: 0.25rem; 31 | padding: 0.5rem 1.25rem; 32 | } 33 | 34 | 35 | .py-attribute-table-column { 36 | flex: 1; 37 | font-size: 0.875em; 38 | } 39 | 40 | /* For the title to be bold */ 41 | .py-attribute-table-column > span { 42 | font-weight: bold; 43 | font-size: 0.9rem; 44 | } 45 | 46 | .py-attribute-table-column ul { 47 | list-style-type: none; 48 | padding: 0; 49 | margin: 0; 50 | } 51 | 52 | .py-attribute-table-entry { 53 | display: flex; 54 | align-items: center; 55 | gap: 0.5rem; /* 8px */ 56 | } 57 | 58 | .py-attribute-table-column li a { 59 | text-decoration: none; 60 | color: var(--color-api-name); 61 | } 62 | 63 | -------------------------------------------------------------------------------- /docs/_static/outdated_code_blocks.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-error-light-mode: #dc2626; 3 | --color-error-dark-mode: #ef4444; 4 | } 5 | 6 | .outdated-code-block { 7 | border: 1px solid var(--color-error-light-mode); 8 | border-radius: 0.25rem; 9 | padding: 0.5rem; 10 | margin: 1rem 0; 11 | box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); 12 | } 13 | 14 | .outdated-code-block div[class*="highlight-"], .outdated-code-block div[class^="highlight-"] { 15 | margin: 0; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | .outdated-code-block { 20 | border: 1px solid var(--color-error-dark-mode); 21 | box-shadow: none; 22 | } 23 | } 24 | 25 | .outdated-code-block-warning { 26 | color: var(--color-error-light-mode); 27 | border-radius: 0.25rem; 28 | padding-left: 0.25rem; 29 | padding-right: 0.25rem; 30 | } 31 | 32 | @media (prefers-color-scheme: dark) { 33 | .outdated-code-block-warning { 34 | color: var(--color-error-dark-mode); 35 | } 36 | } 37 | 38 | 39 | .outdated-code-block-warning-text { 40 | margin: 0.5rem 0 0; 41 | padding: 0; 42 | font-size: 0.8rem; 43 | font-weight: 500; 44 | } -------------------------------------------------------------------------------- /docs/api/bases.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: fortnite_api 2 | 3 | Base Classes 4 | ============= 5 | 6 | The API is built around a few base classes that are used to represent the data returned by the API. 7 | 8 | .. autoclass:: fortnite_api.IdComparable 9 | :members: 10 | 11 | .. autoclass:: fortnite_api.Hashable 12 | :members: 13 | 14 | .. autoclass:: fortnite_api.ReconstructAble 15 | :members: -------------------------------------------------------------------------------- /docs/api/clients.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: fortnite_api 2 | 3 | .. _clients: 4 | 5 | Clients 6 | ======= 7 | 8 | The client classes are the main way to interact with the Fortnite API. They provide all the methods to get data from the API. It's through them that you are served most every object in this library. 9 | 10 | There are two main clients you can use to interact with the API: the :class:`Client` and the :class:`SyncClient`. The :class:`Client` is the main client and is the one you should use in most cases. It is asynchronous and uses the `aiohttp` library to make requests. The :class:`SyncClient` is a synchronous client that uses the `requests` library to make requests. It is useful if you are working in a synchronous environment or if you don't want to deal with the complexity of asynchronous programming. 11 | 12 | Both clients have the same methods and return the same objects. The only difference is that the :class:`Client` has asynchronous methods and the :class:`SyncClient` has synchronous methods. 13 | 14 | .. autoclass:: fortnite_api.Client 15 | :members: 16 | 17 | .. autoclass:: fortnite_api.SyncClient 18 | :members: -------------------------------------------------------------------------------- /docs/api/exceptions.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: fortnite_api 2 | 3 | Exceptions 4 | =========== 5 | 6 | .. _api-exception-hierarchy: 7 | 8 | Exception Hierarchy 9 | ------------------- 10 | 11 | .. exception_hierarchy:: 12 | - :exc:`~fortnite_api.FortniteAPIException` 13 | - :exc:`~fortnite_api.HTTPException` 14 | - :exc:`~fortnite_api.NotFound` 15 | - :exc:`~fortnite_api.Forbidden` 16 | - :exc:`~fortnite_api.ServiceUnavailable` 17 | - :exc:`~fortnite_api.RateLimited` 18 | - :exc:`~fortnite_api.Unauthorized` 19 | - :exc:`~fortnite_api.BetaAccessNotEnabled` 20 | - :exc:`~fortnite_api.BetaUnknownException` 21 | - :exc:`~fortnite_api.MissingAPIKey` 22 | 23 | Exception Classes 24 | ----------------- 25 | 26 | .. autoclass:: fortnite_api.FortniteAPIException 27 | :members: 28 | 29 | .. autoclass:: fortnite_api.HTTPException 30 | :members: 31 | 32 | .. autoclass:: fortnite_api.NotFound 33 | :members: 34 | 35 | .. autoclass:: fortnite_api.Forbidden 36 | :members: 37 | 38 | .. autoclass:: fortnite_api.ServiceUnavailable 39 | :members: 40 | 41 | .. autoclass:: fortnite_api.RateLimited 42 | :members: 43 | 44 | .. autoclass:: fortnite_api.Unauthorized 45 | :members: 46 | 47 | .. autoclass:: fortnite_api.BetaAccessNotEnabled 48 | :members: 49 | 50 | .. autoclass:: fortnite_api.BetaUnknownException 51 | :members: 52 | 53 | .. autoclass:: fortnite_api.MissingAPIKey 54 | :members: -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: fortnite_api 2 | 3 | API Reference 4 | ============== 5 | 6 | This is the API reference for the library. It contains all the classes and functions that are available in the library. See :ref:`installation` for installation instructions. 7 | 8 | Don't know where to start? Check out the :ref:`clients` documentation to get started with the library. 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | clients 14 | bases 15 | objects 16 | exceptions -------------------------------------------------------------------------------- /docs/api/objects.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: fortnite_api 2 | 3 | Objects 4 | ======= 5 | 6 | The API has many objects that represent different parts of the Fortnite API. Below is a list of all the objects and their attributes and methods. 7 | 8 | .. autoclass:: fortnite_api.Account 9 | :members: 10 | 11 | .. autoclass:: fortnite_api.Aes 12 | :members: 13 | 14 | .. autoclass:: fortnite_api.Version 15 | :members: 16 | 17 | .. autoclass:: fortnite_api.DynamicKey 18 | :members: 19 | 20 | .. autoclass:: fortnite_api.CosmeticsAll 21 | :members: 22 | 23 | .. autoclass:: fortnite_api.Asset 24 | :members: 25 | 26 | .. autoclass:: fortnite_api.Banner 27 | :members: 28 | 29 | .. autoclass:: fortnite_api.BannerColor 30 | :members: 31 | 32 | .. autoclass:: fortnite_api.CreatorCode 33 | :members: 34 | 35 | .. autoclass:: fortnite_api.Images 36 | :members: 37 | 38 | .. autoclass:: fortnite_api.Map 39 | :members: 40 | 41 | .. autoclass:: fortnite_api.MapImages 42 | :members: 43 | 44 | .. autoclass:: fortnite_api.POI 45 | :members: 46 | 47 | .. autoclass:: fortnite_api.POILocation 48 | :members: 49 | 50 | .. autoclass:: fortnite_api.NewCosmetic 51 | :members: 52 | 53 | .. autoclass:: fortnite_api.NewCosmetics 54 | :members: 55 | 56 | .. autoclass:: fortnite_api.RenderImage 57 | :members: 58 | 59 | .. autoclass:: fortnite_api.MaterialInstanceImages 60 | :members: 61 | 62 | .. autoclass:: fortnite_api.MaterialInstanceColors 63 | :members: 64 | 65 | .. autoclass:: fortnite_api.MaterialInstance 66 | :members: 67 | 68 | .. autoclass:: fortnite_api.NewDisplayAsset 69 | :members: 70 | 71 | .. autoclass:: fortnite_api.News 72 | :members: 73 | 74 | .. autoclass:: fortnite_api.GameModeNews 75 | :members: 76 | 77 | .. autoclass:: fortnite_api.NewsMotd 78 | :members: 79 | 80 | .. autoclass:: fortnite_api.NewsMessage 81 | :members: 82 | 83 | .. autoclass:: fortnite_api.PlaylistImages 84 | :members: 85 | 86 | .. autoclass:: fortnite_api.Playlist 87 | :members: 88 | 89 | .. autoclass:: fortnite_api.ShopEntryOfferTag 90 | :members: 91 | 92 | .. autoclass:: fortnite_api.ShopEntryBundle 93 | :members: 94 | 95 | .. autoclass:: fortnite_api.ShopEntryBanner 96 | :members: 97 | 98 | .. autoclass:: fortnite_api.ShopEntryLayout 99 | :members: 100 | 101 | .. autoclass:: fortnite_api.ShopEntryColors 102 | :members: 103 | 104 | .. autoclass:: fortnite_api.ShopEntry 105 | :members: 106 | 107 | .. autoclass:: fortnite_api.Shop 108 | :members: 109 | 110 | .. autoclass:: fortnite_api.TileSize 111 | :members: 112 | 113 | .. autoclass:: fortnite_api.BrPlayerStats 114 | :members: 115 | 116 | .. autoclass:: fortnite_api.BrBattlePass 117 | :members: 118 | 119 | .. autoclass:: fortnite_api.BrInputs 120 | :members: 121 | 122 | .. autoclass:: fortnite_api.BrInputStats 123 | :members: 124 | 125 | .. autoclass:: fortnite_api.BrGameModeStats 126 | :members: 127 | 128 | 129 | Cosmetic Objects 130 | ---------------- 131 | 132 | .. autoclass:: fortnite_api.Cosmetic 133 | :members: 134 | 135 | .. autoclass:: fortnite_api.CosmeticTypeInfo 136 | :members: 137 | 138 | .. autoclass:: fortnite_api.CosmeticRarityInfo 139 | :members: 140 | 141 | .. autoclass:: fortnite_api.CosmeticSeriesInfo 142 | :members: 143 | 144 | .. autoclass:: fortnite_api.CosmeticImages 145 | :members: 146 | 147 | .. autoclass:: fortnite_api.CosmeticBrSet 148 | :members: 149 | 150 | .. autoclass:: fortnite_api.CosmeticBrIntroduction 151 | :members: 152 | 153 | .. autoclass:: fortnite_api.CosmeticBrVariant 154 | :members: 155 | 156 | .. autoclass:: fortnite_api.CosmeticBrVariantOption 157 | :members: 158 | 159 | .. autoclass:: fortnite_api.CosmeticBr 160 | :members: 161 | :inherited-members: 162 | 163 | .. autoclass:: fortnite_api.CosmeticCar 164 | :members: 165 | :inherited-members: 166 | 167 | .. autoclass:: fortnite_api.CosmeticInstrument 168 | :members: 169 | :inherited-members: 170 | 171 | .. autoclass:: fortnite_api.CosmeticLegoKit 172 | :members: 173 | :inherited-members: 174 | 175 | .. autoclass:: fortnite_api.CosmeticTrackDifficulty 176 | :members: 177 | 178 | .. autoclass:: fortnite_api.CosmeticTrack 179 | :members: 180 | :inherited-members: 181 | 182 | Cosmetic Variants 183 | ----------------- 184 | 185 | .. autoclass:: fortnite_api.VariantLego 186 | :members: 187 | :inherited-members: 188 | 189 | .. autoclass:: fortnite_api.VariantBean 190 | :members: 191 | :inherited-members: 192 | 193 | Flags 194 | ----- 195 | 196 | .. autoclass:: fortnite_api.ResponseFlags 197 | :members: 198 | 199 | Enumerations 200 | ------------ 201 | 202 | .. autoclass:: fortnite_api.KeyFormat 203 | :members: 204 | 205 | .. autoclass:: fortnite_api.GameLanguage 206 | :members: 207 | 208 | .. autoclass:: fortnite_api.MatchMethod 209 | :members: 210 | 211 | .. autoclass:: fortnite_api.CosmeticCategory 212 | :members: 213 | 214 | .. autoclass:: fortnite_api.CosmeticRarity 215 | :members: 216 | 217 | .. autoclass:: fortnite_api.CosmeticType 218 | :members: 219 | 220 | .. autoclass:: fortnite_api.AccountType 221 | :members: 222 | 223 | .. autoclass:: fortnite_api.TimeWindow 224 | :members: 225 | 226 | .. autoclass:: fortnite_api.StatsImageType 227 | :members: 228 | 229 | .. autoclass:: fortnite_api.CosmeticCompatibleMode 230 | :members: 231 | 232 | .. autoclass:: fortnite_api.BannerIntensity 233 | :members: 234 | 235 | .. autoclass:: fortnite_api.CustomGender 236 | :members: 237 | 238 | .. autoclass:: fortnite_api.ProductTag 239 | :members: 240 | 241 | 242 | Helper Utilities 243 | ---------------- 244 | .. autoclass:: fortnite_api.proxies.TransformerListProxy 245 | :members: -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: fortnite_api 2 | 3 | .. _changelog: 4 | 5 | Changelog 6 | ========= 7 | 8 | .. _vp3p3p0: 9 | 10 | v3.3.0 11 | ------- 12 | 13 | Bug Fixes 14 | ~~~~~~~~~ 15 | - Fixed an issue that caused :class:`fortnite_api.Asset.resize` to raise :class:`TypeError` instead of :class:`ValueError` when the given size isn't a power of 2. 16 | - Fixed an issue that caused :class:`fortnite_api.ServiceUnavailable` to be raised with a static message as a fallback for all unhandled http status codes. Instead :class:`fortnite_api.HTTPException` is raised with the proper error message. 17 | 18 | 19 | .. _vp3p2p1: 20 | 21 | v3.2.1 22 | ------- 23 | 24 | Bug Fixes 25 | ~~~~~~~~~ 26 | - Fixed an issue due a change from Epic that causes :class:`fortnite_api.VariantBean` to not have a :class:`fortnite_api.CustomGender`. It now uses :attr:`fortnite_api.CustomGender.UNKNOWN` in such case instead of raising an exception. 27 | - Fixed typo within fallback system for :class:`fortnite_api.TileSize` as ``raise`` keyword was used instead of ``return``. 28 | - Fixed an issue that caused a :class:`KeyError` to be raised when using :meth:`fortnite_api.Client.search_br_cosmetics` or :meth:`fortnite_api.SyncClient.search_br_cosmetics` without ``multiple`` parameter. 29 | 30 | 31 | .. _vp3p2p0: 32 | 33 | v3.2.0 34 | ------- 35 | This version introduces support for new Shoes cosmetic type, drops support for Python 3.8, and adds safeguards and future proofing against potential API changes. 36 | 37 | Breaking Changes 38 | ~~~~~~~~~~~~~~~~ 39 | - Drop support for Python 3.8. The minimum supported Python version is now 3.9. 40 | - ``CreatorCode.status`` and ``CreatorCode.disabled`` have been removed, since both returned a static value. Disabled creator codes always raise :class:`fortnite_api.NotFound` when trying to fetch them. 41 | - ``CreatorCode.verified`` has been removed, since it isn't used within the affiliate system. It always returns ``False``. 42 | - All enums now use an internal "Enum-like" class to handle unknown values, instead of the built-in :class:`py:enum.Enum`. This potentially breaks type checks, but does not break core functionality or change the enum interface; you can use them the same. 43 | 44 | New Features 45 | ~~~~~~~~~~~~ 46 | - Added support for :attr:`fortnite_api.CosmeticType.SHOES`. 47 | 48 | Documentation 49 | ~~~~~~~~~~~~~ 50 | - Document :class:`fortnite_api.Forbidden` to be raised by :meth:`fortnite_api.Client.fetch_br_stats` and :meth:`fortnite_api.SyncClient.fetch_br_stats`. 51 | 52 | Miscellaneous 53 | ~~~~~~~~~~~~~ 54 | - Add safeguards against Epic Games' API changing or providing invalid values in API responses. 55 | - All enums now can handle unknown values via an internally defined "Enum-like" class. If the API returns a value not in the enum, it will be stored as an attribute on the enum object. The interface for using this class is the same as using :class:`py:enum.Enum`. 56 | - :class:`fortnite_api.TileSize` no longer raises :class:`ValueError` when an unknown value is passed to it. Instead, it now has a fallback value of `-1` for both width and height. 57 | 58 | 59 | .. _vp3p1p0: 60 | 61 | v3.1.0 62 | ------- 63 | This version introduces new data for shop-related objects, reflecting the updated shop layouts and the Fortnite webshop. Additionally, it includes functions that were omitted in version v3.0.0 and addresses a design decision that results in a breaking change. 64 | 65 | Breaking Changes 66 | ~~~~~~~~~~~~~~~~ 67 | - ``ShopEntryNewDisplayAsset`` has been renamed to :class:`fortnite_api.NewDisplayAsset`. 68 | - Alias ``BannerColor.colour`` has been removed for consistency. The API does not use aliases, use :attr:`fortnite_api.BannerColor.color` instead. 69 | 70 | New Features 71 | ~~~~~~~~~~~~ 72 | - Added new object :class:`fortnite_api.ProductTag`. 73 | - Added attribute :attr:`fortnite_api.MaterialInstance.product_tag`. 74 | - Added new object :class:`fortnite_api.ShopEntryOfferTag`. 75 | - Added new object :class:`fortnite_api.ShopEntryColors`. 76 | - Added new object :class:`fortnite_api.RenderImage`. 77 | - Added attribute :attr:`fortnite_api.ShopEntryLayout.rank`. 78 | - Added attribute :attr:`fortnite_api.NewDisplayAsset.render_images`. 79 | - Added attribute :attr:`fortnite_api.ShopEntry.offer_tag`. 80 | - Added attribute :attr:`fortnite_api.ShopEntry.colors`. 81 | 82 | Bug Fixes 83 | ~~~~~~~~~ 84 | - Fixed an issue where ``type`` and ``time_window`` parameters were not respected when fetching stats. 85 | - :attr:`fortnite_api.Playlist.images` now returns ``None`` when no images are available, instead of an empty dict. 86 | - Bug fix for returning naive datetime objects in rare cases. All datetime objects are UTC aware. 87 | 88 | Documentation 89 | ~~~~~~~~~~~~~ 90 | - Added :ref:`response flags ` documentation to explain how to use the ``fortnite_api.ResponseFlags`` class, how to enable response flags, which response flags are available, and when you should enable them. 91 | - Added ``opt-in`` directive in the documentation on attributes that require a specific response flag to be set. This ensures users know of the response flags required to access certain attributes when using the API. 92 | 93 | Miscellaneous 94 | ~~~~~~~~~~~~~ 95 | - Previously, fetching specific game mode news raised :class:`fortnite_api.ServiceUnavailable` due to improper handling from Fortnite-API.com. This has been fixed within the API. Now, when no news is available, :class:`fortnite_api.NotFound` is raised instead. This change is also reflected in the documentation. 96 | 97 | 98 | .. _vp3p0p0: 99 | 100 | v3.0.0 101 | ------- 102 | For help on Migrating to Version 3 from Version 2, and a complete list of all the new features, see the :ref:`Migration guide `. -------------------------------------------------------------------------------- /docs/extensions/exception_hierarchy.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from typing import TYPE_CHECKING 28 | 29 | import sphinx.application 30 | from docutils import nodes 31 | from docutils.parsers.rst import Directive 32 | 33 | if TYPE_CHECKING: 34 | from sphinx.writers.html5 import HTML5Translator 35 | 36 | 37 | class exception_hierarchy(nodes.General, nodes.Element): 38 | pass 39 | 40 | 41 | def visit_exception_hierarchy_node(self: HTML5Translator, node: exception_hierarchy): 42 | self.body.append(self.starttag(node, 'div', CLASS='exception-hierarchy-content')) 43 | 44 | 45 | def depart_exception_hierarchy_node(self: HTML5Translator, node: exception_hierarchy): 46 | self.body.append('\n') 47 | 48 | 49 | class ExceptionHierarchyDirective(Directive): 50 | # Essentials creates a new directive titled "exception_hierarchy" that can be used in the .rst files 51 | # for displaying an exception hierarchy. 52 | has_content = True 53 | 54 | def run(self): 55 | self.assert_has_content() 56 | node = exception_hierarchy('\n'.join(self.content)) 57 | self.state.nested_parse(self.content, self.content_offset, node) # type: ignore 58 | return [node] 59 | 60 | 61 | def setup(app: sphinx.application.Sphinx): 62 | app.add_node( # type: ignore 63 | exception_hierarchy, 64 | html=(visit_exception_hierarchy_node, depart_exception_hierarchy_node), 65 | ) 66 | 67 | app.add_directive('exception_hierarchy', ExceptionHierarchyDirective) 68 | 69 | # Tell sphinx that it is okay for the exception hierarchy to be used in parallel 70 | return { 71 | 'parallel_read_safe': True, 72 | 'parallel_write_safe': True, 73 | } 74 | -------------------------------------------------------------------------------- /docs/extensions/opt_in.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from collections.abc import Sequence 28 | from typing import TYPE_CHECKING, Any 29 | 30 | from docutils import nodes 31 | from docutils.parsers import rst 32 | from sphinx.addnodes import pending_xref 33 | 34 | # from sphinx.roles import XRefRole 35 | 36 | if TYPE_CHECKING: 37 | from sphinx.application import Sphinx 38 | 39 | 40 | class OptInDirective(rst.Directive): 41 | """Denotes a directive that notes a feature is opt-in. 42 | 43 | This directive is used to denote that a feature is opt-in, meaning that it is not enabled by default 44 | and must be enabled using a flag. This is a shorthand for the following: 45 | 46 | .. code-block:: rst 47 | 48 | .. important:: 49 | 50 | This is opt-in. For this parameter to be available, you must enable the 51 | flag on the client. 52 | 53 | See the :ref:`response flags documentation ` for more information on 54 | what response flags are and how to use them. 55 | """ 56 | 57 | required_arguments = 1 58 | optional_arguments = 0 59 | has_content = False 60 | 61 | def run(self) -> Sequence[nodes.Node]: 62 | # (1) Grab the argument from the directive which denotes the flag that must be set. 63 | flag_name = self.arguments[0] 64 | 65 | # (2) Create the important node that denotes the feature is opt-in. 66 | important_node = nodes.important( 67 | '', 68 | nodes.paragraph( 69 | '', 70 | '', 71 | nodes.Text("This attribute is opt-in, meaning it will be unavailable by default. You must enable the "), 72 | self._create_pending_xref('attr', f'fortnite_api.ResponseFlags.{flag_name}', flag_name), 73 | nodes.Text(" flag on the client to have access to this attribute."), 74 | ), 75 | nodes.paragraph( 76 | '', 77 | '', 78 | nodes.Text('See the '), 79 | self._create_pending_xref( 80 | 'ref', 'response_flags', 'response flags documentation', refdomain='std', refexplicit=True 81 | ), 82 | nodes.Text(" for more information on what response flags are and how to use them."), 83 | ), 84 | ) 85 | 86 | return [important_node] 87 | 88 | def _create_pending_xref( 89 | self, reftype: str, target: str, text: str, refdomain: str = 'py', **kwargs: Any 90 | ) -> pending_xref: 91 | refnode = pending_xref( 92 | '', refdomain=refdomain, reftype=reftype, reftarget=target, modname=None, classname=None, **kwargs 93 | ) 94 | refnode += nodes.Text(text) 95 | return refnode 96 | 97 | 98 | def setup(app: Sphinx): 99 | app.add_directive('opt-in', OptInDirective) 100 | 101 | return { 102 | 'version': '0.1', 103 | 'parallel_read_safe': True, 104 | 'parallel_write_safe': True, 105 | } 106 | -------------------------------------------------------------------------------- /docs/extensions/outdated_code_blocks.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from typing import ClassVar 28 | 29 | from docutils.nodes import Element, Node, TextElement 30 | from docutils.parsers.rst import directives 31 | from sphinx.application import Sphinx 32 | from sphinx.directives import optional_int 33 | from sphinx.directives.code import CodeBlock 34 | from sphinx.util.typing import OptionSpec 35 | from sphinx.writers.html5 import HTML5Translator 36 | 37 | 38 | class OutdatedCodeBlockNode(Element): 39 | # The parent node, holds the code block and the warning. 40 | pass 41 | 42 | 43 | # Visits the outdated code block node 44 | def visit_outdated_code_block_node(self: HTML5Translator, node: OutdatedCodeBlockNode) -> None: 45 | # Returns the opening div tag for the outdated code block 46 | self.body.append(self.starttag(node, 'div', CLASS='outdated-code-block')) 47 | 48 | 49 | def depart_outdated_code_block_node(self: HTML5Translator, node: OutdatedCodeBlockNode) -> None: 50 | # Returns the closing div tag for the outdated code block 51 | self.body.append('') 52 | 53 | 54 | class OutdatedCodeBlockWarning(Element): 55 | # Holds the warning at the bottom of the code block. 56 | ... 57 | 58 | 59 | def visit_outdated_code_block_warning(self: HTML5Translator, node: OutdatedCodeBlockWarning) -> None: 60 | # Returns the div that actually holds the warning 61 | # Create some attributes so that the background is dc2626 62 | self.body.append(self.starttag(node, 'div', CLASS='outdated-code-block-warning')) 63 | 64 | 65 | def depart_outdated_code_block_warning(self: HTML5Translator, node: OutdatedCodeBlockWarning) -> None: 66 | # Returns the closing div tag for the warning 67 | self.body.append('') 68 | 69 | 70 | class OutdatedCodeBlockWarningText(TextElement): 71 | # Holds the warning text. 72 | ... 73 | 74 | 75 | def visit_outdated_code_block_warning_text(self: HTML5Translator, node: OutdatedCodeBlockWarningText) -> None: 76 | # Returns the opening p tag for the warning text 77 | self.body.append(self.starttag(node, 'div', CLASS='outdated-code-block-warning-text')) 78 | 79 | 80 | def depart_outdated_code_block_warning_text(self: HTML5Translator, node: OutdatedCodeBlockWarningText) -> None: 81 | # Returns the closing p tag for the warning text 82 | self.body.append('') 83 | 84 | 85 | class OutdatedCodeBlock(CodeBlock): 86 | """A custom Sphinx directive that aims to add a warning to code blocks 87 | that to not work with the latest version of the library. 88 | 89 | .. outdated-code-block:: 90 | :since: # The version where the code block was last working 91 | 92 | 93 | 94 | Generates the following HTML: 95 | 96 |
# Holds the entire outdated warning 97 |
 # Holds the Warning
 99 |             

[Warning Text]

# The text of the warning. Notifies *when* the code block was last working. 100 |
101 | 102 | """ 103 | 104 | has_content = True 105 | required_arguments = 1 106 | optional_arguments = 1 107 | 108 | option_spec: ClassVar[OptionSpec] = { 109 | 'since': directives.unchanged_required, 110 | 'force': directives.flag, 111 | 'linenos': directives.flag, 112 | 'dedent': optional_int, 113 | 'lineno-start': int, 114 | 'emphasize-lines': directives.unchanged_required, 115 | 'caption': directives.unchanged_required, 116 | 'class': directives.class_option, 117 | 'name': directives.unchanged, 118 | } 119 | 120 | def run(self) -> list[Node]: 121 | 122 | # Create the main node that holds the code block and warning 123 | root = OutdatedCodeBlockNode() 124 | 125 | code_block: list[Node] = super().run() 126 | root += code_block 127 | 128 | # Create the underlying warning node 129 | warning = OutdatedCodeBlockWarning() 130 | root.append(warning) 131 | 132 | # The underlying warning text 133 | warning_text_raw = ( 134 | f'This code block is from version {self.options["since"]}. It will not work with the latest version.' 135 | ) 136 | warning_text = OutdatedCodeBlockWarningText(warning_text_raw, warning_text_raw) 137 | warning.append(warning_text) 138 | 139 | return [root] 140 | 141 | 142 | # The setup function for the extension 143 | def setup(app: Sphinx): 144 | app.add_node(OutdatedCodeBlockNode, html=(visit_outdated_code_block_node, depart_outdated_code_block_node)) # type: ignore 145 | app.add_node(OutdatedCodeBlockWarning, html=(visit_outdated_code_block_warning, depart_outdated_code_block_warning)) # type: ignore 146 | app.add_node( # type: ignore 147 | OutdatedCodeBlockWarningText, html=(visit_outdated_code_block_warning_text, depart_outdated_code_block_warning_text) 148 | ) 149 | 150 | app.add_directive('outdated-code-block', OutdatedCodeBlock) 151 | 152 | # Tell sphinx that it is okay for the exception hierarchy to be used in parallel 153 | return { 154 | 'parallel_read_safe': True, 155 | 'parallel_write_safe': True, 156 | } 157 | -------------------------------------------------------------------------------- /docs/images/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fortnite-API/py-wrapper/8f40946c71e8ef8db085add0b2ce8b66a92174e6/docs/images/logo.ico -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Fortnite-API Python Documentation 2 | ============================================ 3 | 4 | Welcome! This is the official documentation for the Fortnite-API Python library. This library allows you to interact with the Fortnite API using Python. You can use this library to get information about Fortnite players, cosmetics, shops, and more. 5 | 6 | .. _installation: 7 | 8 | Installation 9 | ------------ 10 | To install the Fortnite-API Python library, you can use pip. Run the following command in your terminal: 11 | 12 | .. note:: 13 | 14 | Note that Python 3.9 and greater is required to use this library. 15 | 16 | .. code-block:: bash 17 | 18 | # Linux/macOS 19 | python3 -m pip install fortnite-api 20 | 21 | # Windows 22 | py -3 -m pip install fortnite-api 23 | 24 | 25 | To install the latest development version of the library, you can use the following command: 26 | 27 | .. code-block:: bash 28 | 29 | git clone https://github.com/Fortnite-API/py-wrapper 30 | cd py-wrapper 31 | python3 -m pip install . 32 | 33 | Optional Dependencies 34 | --------------------- 35 | 36 | - `speed`: An optional dependency that installs `orjson `_ for faster JSON serialization and deserialization. 37 | 38 | .. code-block:: bash 39 | 40 | # Linux/macOS 41 | python3 -m pip install fortnite-api[speed] 42 | 43 | # Windows 44 | py -3 -m pip install fortnite-api[speed] 45 | 46 | API Key 47 | ------- 48 | 49 | For most endpoints, you do not need an API key. However, some endpoints, such as fetching statistics, require an API key. To use these endpoints, you need to set the `api_key` parameter in the constructor. 50 | 51 | .. code-block:: python3 52 | 53 | import asyncio 54 | import fortnite_api 55 | 56 | async def main(): 57 | async with fortnite_api.Client(api_key="your_api_key"): 58 | stats = await client.fetch_br_stats(name='some_username') 59 | print(stats) 60 | 61 | if __name__ == "__main__": 62 | asyncio.run(main()) 63 | 64 | Generating an API Key 65 | --------------------- 66 | 67 | You can generate an API key on `the dashboard `_ by logging in with your Discord account. 68 | 69 | View Documentation 70 | ------------------ 71 | The entirety of the public API is documented here. If you're looking for a specific method, class, or module, the search bar at the top right is your friend. 72 | 73 | If you're not sure where to start, check out the :class:`fortnite_api.Client` class for a list of all available methods you can use to interact with the API. 74 | 75 | .. toctree:: 76 | :maxdepth: 3 77 | 78 | api/index 79 | 80 | Migrating From Version 2 81 | ------------------------- 82 | 83 | If you're migrating from version 2 of the Fortnite-API Python library, you can find a complete guide on how to do so here. This guide will help you understand the changes that were made in version 3 and how to update your code to work with the new version. 84 | 85 | .. toctree:: 86 | :maxdepth: 3 87 | 88 | migrating 89 | 90 | Changelog 91 | --------- 92 | 93 | The changelog contains a list of all changes made to the Fortnite-API Python library. This includes new features, bug fixes, and other changes that have been made to the library. The changelog is updated with each new release of the library. 94 | 95 | .. toctree:: 96 | :maxdepth: 3 97 | 98 | changelog 99 | 100 | 101 | Additional Resources 102 | -------------------- 103 | 104 | .. toctree:: 105 | :maxdepth: 2 106 | 107 | response_flags -------------------------------------------------------------------------------- /docs/response_flags.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: fortnite_api 2 | 3 | .. _response_flags: 4 | 5 | Response Flags 6 | ============== 7 | 8 | The Fortnite-API uses response flags to indicate if some optional fields are present in an API response. These flags 9 | are optional, meaning that you can choose not to include them in the response. 10 | 11 | By default, any response flags are not enabled on the :class:`~fortnite_api.Client` and :class:`~fortnite_api.SyncClient` class. To enable them, you can set the 12 | ``response_flags`` parameter in the constructor. 13 | 14 | Using the Response Flags Class 15 | ------------------------------ 16 | The :class:`~fortnite_api.ResponseFlags` class provides a set of flags that you can enable or disable and set in the client. 17 | This class is a subclass of the built-in :class:`~py:enum.IntFlag`, and thus, supports bitwise operations. 18 | 19 | To consider how to use response flags, let's consider the following example: 20 | 21 | .. code-block:: python3 22 | 23 | from enum import IntFlag, auto 24 | 25 | class Color(IntFlag): 26 | RED = auto() 27 | GREEN = auto() 28 | BLUE = auto() 29 | 30 | Here, we have defined a class titled ``Color`` as a subclass of :class:`~py:enum.IntFlag`. This class has three fields: ``RED``, ``GREEN``, and ``BLUE``. Each field is an instance of the :class:`~py:enum.auto` class, which automatically assigns a unique value to each field. 31 | 32 | To enable the ``RED`` and ``GREEN`` flags, you can use the bitwise OR operator: 33 | 34 | .. code-block:: python3 35 | 36 | flags = Color.RED | Color.GREEN 37 | 38 | The value of ``flags`` will be the bitwise OR of the values of ``Color.RED`` and ``Color.GREEN``. In this case, the value of ``flags`` will be ``3``. 39 | 40 | To check if a flag is enabled, you can use the bitwise AND operator: 41 | 42 | .. code-block:: python3 43 | 44 | if flags & Color.RED: 45 | print("RED is enabled") 46 | 47 | The value of ``flags & Color.RED`` will be the bitwise AND of the values of ``flags`` and ``Color.RED``. If the result is non-zero, the flag is enabled. Otherwise, the flag is disabled. 48 | 49 | This same concept applies to the response flags. For more information on bit manipulation, see this article on `Bitwise Operators `_. 50 | 51 | Response Flags Class Fields 52 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 53 | 54 | These are several fields on the :class:`~fortnite_api.ResponseFlags` class. 55 | 56 | - :attr:`~fortnite_api.ResponseFlags.INCLUDE_NOTHING`: Disables all response flags. This is the default value on the client. 57 | - :attr:`~fortnite_api.ResponseFlags.INCLUDE_PATHS`: Enables the ``path`` field of a response to be present. By default, the path to a Fortnite cosmetic is not included in the response. 58 | - :attr:`~fortnite_api.ResponseFlags.INCLUDE_GAMEPLAY_TAGS`: Enables the ``gameplay_tags`` field of a response to be present. By default, the gameplay tags of a Fortnite cosmetic are not included in the response and will simply be an empty list. 59 | - :attr:`~fortnite_api.ResponseFlags.INCLUDE_SHOP_HISTORY`: Enables the ``shop_history`` field of a response to be present. By default, the shop history of a Fortnite cosmetic is not included in the response and will simply be an empty list. 60 | 61 | To enable response flags, you can use the bitwise OR operator to combine the flags you want to enable, or use the :meth:`~fortnite_api.ResponseFlags.all` method to enable everything. Note you should **only use response flags that correspond to the fields you are actively using, see below.** 62 | 63 | .. important:: 64 | 65 | You should **only enable the response flags that your client is actively using**. Enabling unnecessary response flags leads to increased bandwidth usage, longer response times, higher memory costs, and decreased performance due to the overhead. It's recommended to disable all response flags by default and enable the flags you need as you develop. 66 | 67 | Using :meth:`fortnite_api.ResponseFlags.all` is only recommended if your client is actively using all the fields. Otherwise, you should enable only the flags you need. The API may be updated to include more fields in the future, many of which your client may not need. 68 | 69 | .. code-block:: python3 70 | :caption: A bad example of enabling all response flags. 71 | :emphasize-lines: 1, 5 72 | 73 | response_flags = ResponseFlags.all() 74 | async with fortnite_api.Client(response_flags=response_flags) as client: 75 | # Fetch a cosmetic 76 | cosmetic = await client.fetch_cosmetic_br('CID_028_Athena_Commando_F') 77 | print(cosmetic.path) 78 | 79 | In this example, the client is only using the ``path`` field, but all response flags are enabled. This is inefficient and should be avoided. 80 | 81 | .. hint:: 82 | 83 | Every attribute that requires a specific response flag to be set in this documentation will have a note indicating 84 | which response flag is required to be set on the client class. If the response flag is not set, the attribute will 85 | not have an expected value. 86 | 87 | Setting Response Flags 88 | ---------------------- 89 | 90 | Setting response flags on the client is simple. You can set the ``response_flags`` parameter in the constructor to enable the flags you want to include in the response. Consider the following example: 91 | 92 | .. code-block:: python3 93 | 94 | import fortnite_api 95 | 96 | # NOTE: Shorthand import for readability. Doing this 97 | # is optional and not required. 98 | from fortnite_api import ResponseFlags 99 | 100 | async def main(): 101 | response_flags = ResponseFlags.INCLUDE_PATHS | ResponseFlags.INCLUDE_GAMEPLAY_TAGS 102 | async with fortnite_api.Client(response_flags=response_flags) as client: 103 | # (1) Fetch the Renegade Raider 104 | cosmetic = await client.fetch_cosmetic_br('CID_028_Athena_Commando_F') 105 | 106 | # (2) Print the path and gameplay tags 107 | print(cosmetic.path) 108 | >>> 'Athena/Items/Cosmetics/Characters/CID_028_Athena_Commando_F' 109 | print(cosmetic.gameplay_tags) 110 | >>> ['Cosmetics.Source.Season1', ...] 111 | 112 | # (3) Print the shop history. Note this will ALWAYS be 113 | # an EMPTY LIST because the shop history response flag has not 114 | # been set on the client class. 115 | print(cosmetic.shop_history) 116 | >>> [] 117 | 118 | We see that the ``path`` and ``gameplay_tags`` fields are present in the response, while the ``shop_history`` field is not. This is because the ``INCLUDE_PATHS`` and ``INCLUDE_GAMEPLAY_TAGS`` flags are enabled, while the ``INCLUDE_SHOP_HISTORY`` flag is not. 119 | 120 | Now, let's enable all response flags using the :meth:`~fortnite_api.ResponseFlags.all` method: 121 | 122 | .. code-block:: python3 123 | 124 | import fortnite_api 125 | 126 | async def main(): 127 | response_flags = fortnite_api.ResponseFlags.all() 128 | async with fortnite_api.Client(response_flags=response_flags) as client: 129 | # (1) Fetch the Renegade Raider 130 | cosmetic = await client.fetch_cosmetic_br('CID_028_Athena_Commando_F') 131 | 132 | # (2) Print the path, gameplay tags, and shop history 133 | print(cosmetic.path) 134 | >>> 'Athena/Items/Cosmetics/Characters/CID_028_Athena_Commando_F' 135 | print(cosmetic.gameplay_tags) 136 | >>> ['Cosmetics.Source.Season1', ...] 137 | print(cosmetic.shop_history) 138 | >>> ['2021-09-01T00:00:00Z', ...] 139 | 140 | We can see now that all fields are present in the response. This is because all response flags are enabled. 141 | -------------------------------------------------------------------------------- /examples/basic.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic Example 3 | ------------- 4 | This example introduces you to the basic usage of the client in an asynchronous context. 5 | It familiarizes you with the client and how to use it to fetch data from the Fortnite API. 6 | """ 7 | 8 | import asyncio 9 | 10 | import fortnite_api 11 | 12 | 13 | async def main(): 14 | # The main way you interact with the API is through the main client. 15 | # This client uses an aiohttp ClientSession for its HTTP requests, so 16 | # it's recommended to use the client with an async context manager. 17 | async with fortnite_api.Client(api_key="YOUR_API_KEY") as client: 18 | # The client has many methods to get cosmetics, news, 19 | # and other data from the API. Every method that fetches 20 | # data from the API is a coroutine, so you must await the result. 21 | cosmetics = await client.fetch_cosmetics_all() 22 | 23 | # In this case, fetch_cosmetics_all returns an instance of "fortnite_api.CosmeticsAll" 24 | assert isinstance(cosmetics, fortnite_api.CosmeticsAll) 25 | 26 | # We can iterate through the BR (battle royale) cosmetics and print out all their IDS. 27 | for cosmetic in cosmetics.br: 28 | print(cosmetic.id) 29 | 30 | # After the context manager, our client is closed and the 31 | # session is closed. If you choose to use the client without 32 | # the context manager, you must be sure to close the session yourself. 33 | 34 | 35 | if __name__ == "__main__": 36 | asyncio.run(main()) 37 | -------------------------------------------------------------------------------- /examples/discord_integration.py: -------------------------------------------------------------------------------- 1 | """ 2 | Discord Integration Example 3 | --------------------------- 4 | This example demonstrates how to integrate the Fortnite API with a Discord bot using discord.py. 5 | It walks you through the best practices to follow and how to structure your code to make the 6 | most out of both libraries. 7 | 8 | NOTE:: 9 | ------- 10 | Do NOT use the synchronous Fortnite API client with discord.py. It will block the event loop, 11 | causing your Discord bot to become unresponsive. Always use the asynchronous client. 12 | """ 13 | 14 | from __future__ import annotations 15 | 16 | import asyncio 17 | import os 18 | from typing import Optional 19 | 20 | import discord 21 | from discord.ext import commands 22 | 23 | import fortnite_api 24 | 25 | 26 | class MyBot(commands.Bot): 27 | """ 28 | This is a custom Discord bot subclass that takes the Fortnite API client as an argument 29 | in its initializer and stores it as an attribute for later use. 30 | 31 | This is so that the Fortnite API client can be accessed anywhere the Discord bot 32 | instance is available. 33 | 34 | This is the recommended way to integrate the Fortnite API with discord.py. 35 | """ 36 | 37 | # (1) Override __init__ to take the Fortnite API client as an argument. 38 | def __init__(self, fortnite_client: fortnite_api.Client) -> None: 39 | # (2) Call super() with the command prefix and intents. 40 | super().__init__( 41 | command_prefix="!", 42 | intents=discord.Intents.default(), 43 | ) 44 | 45 | # (3) And store the Fortnite API client as an attribute 46 | self.fortnite_client: fortnite_api.Client = fortnite_client 47 | 48 | # When the bot is started, add the FortniteCog to the bot (defined below) 49 | async def setup_hook(self) -> None: 50 | await self.add_cog(FortniteCog()) 51 | 52 | 53 | class FortniteCog(commands.Cog): 54 | """ 55 | A commands.Cog instance that holds some Fortnite-related commands. In actuality, 56 | it is recommended for you to put your cogs in separate files, aka "extensions", for 57 | better organization. 58 | """ 59 | 60 | @commands.command(name="aes", description="Get the current main AES key, if any.") 61 | async def aes(self, ctx: commands.Context[MyBot]) -> None: 62 | """ 63 | A command that fetches the current main AES key using the Fortnite API client. 64 | """ 65 | # (1) Get the current Discord bot 66 | bot: MyBot = ctx.bot 67 | 68 | # (2) Access our custom attribute to get the Fortnite API client 69 | fortnite_client: fortnite_api.Client = bot.fortnite_client 70 | 71 | # (3) fetch the AES key 72 | aes = await fortnite_client.fetch_aes() 73 | 74 | # (3.1) The main AES key is marked as optional in the documentation, so we must 75 | # handle the case where it is None. 76 | main_key: Optional[str] = aes.main_key 77 | 78 | # (4) and send a message back to the user. 79 | if main_key is None: 80 | await ctx.send("There is no AES main key available.") 81 | else: 82 | await ctx.send(f"The AES main key is: `{main_key}`") 83 | 84 | @commands.hybrid_command( 85 | name="total-cosmetics", 86 | description="Get the total number of cosmetics in Fortnite.", 87 | ) 88 | async def total_cosmetics(self, ctx: commands.Context[MyBot]) -> None: 89 | """ 90 | A hybrid command that uses the Fortnite API client to fetch 91 | the total number of cosmetics, including variants, in Fortnite. 92 | """ 93 | # (1) Defer the response to acknowledge the command. Sometimes an API call can take 94 | # more than 3 seconds, so deferring is the only way to ensure the interaction 95 | # does not fail. 96 | async with ctx.typing(): 97 | # (2) Get the current Discord bot 98 | bot: MyBot = ctx.bot 99 | 100 | # (3) Access our custom attribute to get the Fortnite API client 101 | fortnite_client: fortnite_api.Client = bot.fortnite_client 102 | 103 | # (4) Fetch all the Fortnite cosmetics 104 | all_cosmetics = await fortnite_client.fetch_cosmetics_all() 105 | 106 | # (4.1) We know that fortnite_api.CosmeticsAll has __len__ implemented 107 | # because we read the documentation, so we can use the built-in len() function. 108 | total_cosmetics = len(all_cosmetics) 109 | 110 | # (5) and send a message back to the user. 111 | await ctx.send( 112 | f"The total number of cosmetics in Fortnite is: `{total_cosmetics}`", 113 | ephemeral=True, 114 | ) 115 | 116 | 117 | async def main() -> None: 118 | """ 119 | When working with both discord.py and fortnite_api, it is recommended to use a 120 | main function to start the bot. This function will handle the setup and teardown 121 | of the Fortnite API client and the Discord bot. 122 | 123 | In a real world example, this is also where you would set up any database connections 124 | or other services that your bot may need throughout its lifetime. 125 | """ 126 | # (1) Define the fortnite client 127 | fortnite_client = fortnite_api.Client(api_key=os.environ["YOUR_API_KEY"]) 128 | 129 | # (2) Create the bot instance 130 | bot = MyBot(fortnite_client=fortnite_client) 131 | 132 | # (3) Use the async context managers for both the Fortnite API client and the bot 133 | async with fortnite_client, bot: 134 | # (4) Start the bot 135 | await bot.start(os.environ["YOUR_BOT_TOKEN"]) 136 | 137 | 138 | # Start the bot only if the script is run directly. 139 | if __name__ == "__main__": 140 | asyncio.run(main()) 141 | -------------------------------------------------------------------------------- /examples/sync_basic.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sync Basic Example 3 | ------------------ 4 | This example introduces you to the basic usage of the Client in a synchronous context. 5 | It familiarizes you with the SyncClient and how to use it to fetch data from the Fortnite API. 6 | 7 | Any asynchronous code in this example directory can be converted to synchronous code simply 8 | by changing the client definition and removing all async/await keywords. The library ensures 9 | that the interfaces are the same for both synchronous and asynchronous contexts. 10 | """ 11 | 12 | import fortnite_api 13 | 14 | 15 | def main(): 16 | # The main way you interact with the API is through the main client. 17 | # This client uses a requests Session for its HTTP requests, so 18 | # it's recommended to use the client with an async context manager. 19 | with fortnite_api.SyncClient(api_key="YOUR_API_KEY") as client: 20 | # The client has many methods to get cosmetics, news, 21 | # and other data from the API. Every method that fetches 22 | # data from the API is an API call, so you may want to keep 23 | # these calls limited to only when you need them. 24 | cosmetics = client.fetch_cosmetics_all() 25 | 26 | # In this case, fetch_cosmetics_all returns an instance of "fortnite_api.CosmeticsAll" 27 | 28 | assert isinstance(cosmetics, fortnite_api.CosmeticsAll) 29 | 30 | # We can iterate through the BR (battle royale) cosmetics and print out all their IDS. 31 | for cosmetic in cosmetics.br: 32 | print(cosmetic.id) 33 | 34 | # After the context manager, our client is closed and the 35 | # session is closed. If you choose to use the client without 36 | # the context manager, you must be sure to close the session yourself. 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /fortnite_api/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | __version__ = '3.3.0a' 26 | 27 | from . import proxies as proxies, utils as utils 28 | from .abc import * 29 | from .account import * 30 | from .aes import * 31 | from .all import * 32 | from .asset import * 33 | from .banner import * 34 | from .client import Client as Client, SyncClient as SyncClient 35 | from .cosmetics import * 36 | from .creator_code import * 37 | from .enums import * 38 | from .errors import * 39 | from .flags import * 40 | from .images import * 41 | from .map import * 42 | from .new import * 43 | from .new_display_asset import * 44 | from .news import * 45 | from .playlist import * 46 | from .shop import * 47 | from .stats import * 48 | -------------------------------------------------------------------------------- /fortnite_api/abc.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | import copy 28 | from typing import TYPE_CHECKING, Generic, TypeVar, Union 29 | 30 | from typing_extensions import Self 31 | 32 | from .http import HTTPClient, HTTPClientT, SyncHTTPClient 33 | 34 | DictT = TypeVar('DictT', bound='Mapping[Any, Any]') 35 | 36 | if TYPE_CHECKING: 37 | from collections.abc import Mapping 38 | from typing import Any 39 | 40 | from .client import Client, SyncClient 41 | 42 | __all__: tuple[str, ...] = ('IdComparable', 'Hashable', 'ReconstructAble') 43 | 44 | 45 | class IdComparable: 46 | """ 47 | .. attributetable:: fortnite_api.IdComparable 48 | 49 | Represents an object that can be compared to another object by id. 50 | 51 | .. container:: operations 52 | 53 | .. describe:: x == y 54 | 55 | Determine if two objects are equal. 56 | 57 | .. describe:: x != y 58 | 59 | Determine if two objects are not equal. 60 | """ 61 | 62 | id: str 63 | 64 | def __eq__(self, __o: object) -> bool: 65 | if not isinstance(__o, self.__class__): 66 | return False 67 | 68 | return self.id == __o.id 69 | 70 | def __ne__(self, __o: object) -> bool: 71 | return not self.__eq__(__o) 72 | 73 | 74 | class Hashable(IdComparable): 75 | """ 76 | .. attributetable:: fortnite_api.Hashable 77 | 78 | Represents a hashable object. 79 | 80 | This inherits :class:`fortnite_api.IdComparable` and adds a hash function. 81 | 82 | .. container:: operations 83 | 84 | .. describe:: hash(x) 85 | 86 | Return the hash of the object. 87 | """ 88 | 89 | id: str 90 | 91 | def __hash__(self) -> int: 92 | return hash(self.id) 93 | 94 | 95 | class ReconstructAble(Generic[DictT, HTTPClientT]): 96 | """ 97 | Denotes a class that can be reconstructed from a raw data dictionary, such as 98 | one returned from any API endpoint. 99 | """ 100 | 101 | # Denotes an internal method that is used to store the instance raw api data, 102 | # and is used to serve this data back to the user when the to_dict method is called. 103 | __raw_data: DictT 104 | 105 | # The internal http client that is used to make requests. 106 | _http: HTTPClientT 107 | 108 | # Denotes that any subclass should have both the data and http params passed to its init. 109 | # The library has been built with this in mind, and 110 | # by default any class that inherits from this protocol will have this __init__ method. 111 | def __init__(self, *, data: DictT, http: HTTPClientT) -> None: 112 | self.__raw_data: DictT = data 113 | self._http: HTTPClientT = http 114 | 115 | # The from_dict method is a class method that allows the user to create an instance 116 | # of this class from the underlying raw dictionary type returned from the API. This 117 | # method is overloaded to allow for both the async and sync clients to be passed, whilst 118 | # still keeping the correct HTTPClient type. 119 | 120 | @classmethod 121 | def from_dict(cls: type[Self], data: DictT, *, client: Union[Client, SyncClient]) -> Self: 122 | """Reconstructs this class from a raw dictionary object. This is useful for when you 123 | store the raw data and want to reconstruct the object later on. 124 | 125 | Parameters 126 | ---------- 127 | data: Dict[Any, Any] 128 | The raw data to reconstruct the object from. 129 | client: Union[:class:`fortnite_api.Client`, :class:`fortnite_api.SyncClient`] 130 | The currently used client to reconstruct the object with. Can either be a sync or async client. 131 | """ 132 | if isinstance(client.http, SyncHTTPClient): 133 | # Whenever the client is a SyncClient, we can safely assume that the http 134 | # attribute is a SyncHTTPClient, as this is the only HTTPClientT possible. 135 | sync_http: SyncHTTPClient = client.http 136 | return cls(data=data, http=sync_http) # type: ignore # Pyright cannot infer the type of cls 137 | else: 138 | # Whenever the client is a Client, we can safely assume that the http 139 | # attribute is a HTTPClient, as this is the only HTTPClientT possible. 140 | http: HTTPClient = client.http 141 | return cls(data=data, http=http) # type: ignore # Pyright cannot infer the type of cls 142 | 143 | def to_dict(self) -> DictT: 144 | """Turns this object into a raw dictionary object. This is useful for when you 145 | want to store the raw data and reconstruct the object later on. 146 | 147 | Returns 148 | ------- 149 | Dict[Any, Any] 150 | The raw data of this object. Note that this is a deep copy of the raw data, 151 | and not a reference to the underlying raw data this object was constructed with. 152 | """ 153 | # NOTE: copy.deepcopy is used to prevent the user from modifying the raw data 154 | # and causing unexpected behavior. The module itself is being used because 155 | # we want to allow Mapping[K, V] types to be used as the raw data (for typed dicts) 156 | return copy.deepcopy(self.__raw_data) 157 | -------------------------------------------------------------------------------- /fortnite_api/account.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from typing import Any 28 | 29 | from .abc import Hashable, ReconstructAble 30 | from .http import HTTPClientT 31 | from .utils import simple_repr 32 | 33 | __all__: tuple[str, ...] = ("Account",) 34 | 35 | 36 | @simple_repr 37 | class Account(Hashable, ReconstructAble[dict[str, Any], HTTPClientT]): 38 | """ 39 | .. attributetable:: fortnite_api.Account 40 | 41 | Represents a Fortnite account. 42 | 43 | This inherits from :class:`~fortnite_api.Hashable` and :class:`~fortnite_api.ReconstructAble`. 44 | 45 | .. container:: operations 46 | 47 | .. describe:: str(x) 48 | 49 | Returns the account's name. 50 | 51 | .. describe:: repr(x) 52 | 53 | Returns a representation of the account in the form of a string. 54 | 55 | Attributes 56 | ---------- 57 | id: :class:`str` 58 | The id of the account. 59 | name: :class:`str` 60 | The display name of the account. 61 | """ 62 | 63 | __slots__: tuple[str, ...] = ( 64 | "id", 65 | "name", 66 | ) 67 | 68 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 69 | super().__init__(data=data, http=http) 70 | 71 | self.id: str = data["id"] 72 | self.name: str = data["name"] 73 | 74 | def __str__(self) -> str: 75 | return self.name 76 | -------------------------------------------------------------------------------- /fortnite_api/all.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from collections.abc import Generator 26 | from typing import Any 27 | 28 | from .abc import ReconstructAble 29 | from .cosmetics import ( 30 | Cosmetic, 31 | CosmeticBr, 32 | CosmeticCar, 33 | CosmeticInstrument, 34 | CosmeticLegoKit, 35 | CosmeticTrack, 36 | VariantBean, 37 | VariantLego, 38 | ) 39 | from .http import HTTPClientT 40 | from .proxies import TransformerListProxy 41 | from .utils import get_with_fallback, simple_repr 42 | 43 | __all__: tuple[str, ...] = ("CosmeticsAll",) 44 | 45 | 46 | @simple_repr 47 | class CosmeticsAll(ReconstructAble[dict[str, Any], HTTPClientT]): 48 | """ 49 | .. attributetable:: fortnite_api.CosmeticsAll 50 | 51 | A class that represents a request to fetch all cosmetics available in Fortnite. This 52 | inherits from :class:`~fortnite_api.ReconstructAble`. 53 | 54 | .. container:: operations 55 | 56 | .. describe:: len(x) 57 | 58 | Returns the total amount of cosmetics available. 59 | 60 | .. describe:: iter(x) 61 | 62 | Returns an iterator of the cosmetics, working through one unique cosmetic type before 63 | continuing onto the next. Works in the following 64 | order: :class:`~fortnite_api.CosmeticBr`, :class:`~fortnite_api.CosmeticTrack`, 65 | :class:`~fortnite_api.CosmeticInstrument`, :class:`~fortnite_api.CosmeticCar`, 66 | :class:`~fortnite_api.VariantLego`, :class:`~fortnite_api.CosmeticLegoKit`. 67 | 68 | .. describe:: repr(x) 69 | 70 | Returns a representation of the account in the form of a string. 71 | 72 | .. code-block:: python3 73 | :caption: Fetching all cosmetics in Fortnite and printing their IDs. 74 | 75 | # (1) Fetch all the cosmetics using the client 76 | all_cosmetics = await client.fetch_cosmetics_all() 77 | 78 | # (2) Walk through each cosmetic 79 | for cosmetic in all_cosmetics: 80 | # (3) Print out their metadata 81 | print(cosmetic.id) 82 | 83 | Attributes 84 | ---------- 85 | br: List[:class:`fortnite_api.CosmeticBr`] 86 | A list of all battle royale cosmetics. 87 | tracks: List[:class:`fortnite_api.CosmeticTrack`] 88 | A list of all track cosmetics. 89 | instruments: List[:class:`fortnite_api.CosmeticInstrument`] 90 | A list of all instrument cosmetics. 91 | cars: List[:class:`fortnite_api.CosmeticCar`] 92 | A list of all car cosmetics. 93 | lego: List[:class:`fortnite_api.VariantLego`] 94 | A list of all lego cosmetic variants. 95 | lego_kits: List[:class:`fortnite_api.CosmeticLegoKit`] 96 | A list of all lego kit cosmetics. 97 | """ 98 | 99 | __slots__: tuple[str, ...] = ( 100 | "br", 101 | "tracks", 102 | "instruments", 103 | "cars", 104 | "lego", 105 | "lego_kits", 106 | "beans", 107 | ) 108 | 109 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 110 | super().__init__(data=data, http=http) 111 | 112 | _br = get_with_fallback(data, "br", list) 113 | self.br: TransformerListProxy[CosmeticBr[HTTPClientT]] = TransformerListProxy( 114 | _br, 115 | lambda x: CosmeticBr(data=x, http=self._http), 116 | ) 117 | 118 | _tracks = get_with_fallback(data, "tracks", list) 119 | self.tracks: TransformerListProxy[CosmeticTrack[HTTPClientT]] = TransformerListProxy( 120 | _tracks, 121 | lambda x: CosmeticTrack(data=x, http=self._http), 122 | ) 123 | 124 | _instruments = get_with_fallback(data, "instruments", list) 125 | self.instruments: TransformerListProxy[CosmeticInstrument[HTTPClientT]] = TransformerListProxy( 126 | _instruments, 127 | lambda x: CosmeticInstrument(data=x, http=self._http), 128 | ) 129 | 130 | _cars = get_with_fallback(data, "cars", list) 131 | self.cars: TransformerListProxy[CosmeticCar[HTTPClientT]] = TransformerListProxy( 132 | _cars, 133 | lambda x: CosmeticCar(data=x, http=self._http), 134 | ) 135 | 136 | _lego = get_with_fallback(data, "lego", list) 137 | self.lego: TransformerListProxy[VariantLego[HTTPClientT]] = TransformerListProxy( 138 | _lego, 139 | lambda x: VariantLego(data=x, http=self._http), 140 | ) 141 | 142 | _lego_kits = get_with_fallback(data, "legoKits", list) 143 | self.lego_kits: TransformerListProxy[CosmeticLegoKit[HTTPClientT]] = TransformerListProxy( 144 | _lego_kits, 145 | lambda x: CosmeticLegoKit(data=x, http=self._http), 146 | ) 147 | 148 | _beans = get_with_fallback(data, "beans", list) 149 | self.beans: TransformerListProxy[VariantBean[HTTPClientT]] = TransformerListProxy( 150 | _beans, 151 | lambda x: VariantBean(data=x, http=self._http), 152 | ) 153 | 154 | def __iter__(self) -> Generator[Cosmetic[dict[str, Any], HTTPClientT], None, None]: 155 | yield from self.br 156 | 157 | yield from self.tracks 158 | 159 | yield from self.instruments 160 | 161 | yield from self.cars 162 | 163 | yield from self.lego 164 | 165 | yield from self.beans 166 | 167 | yield from self.lego_kits 168 | 169 | def __len__(self) -> int: 170 | return ( 171 | len(self.br) 172 | + len(self.tracks) 173 | + len(self.instruments) 174 | + len(self.cars) 175 | + len(self.lego) 176 | + len(self.beans) 177 | + len(self.lego_kits) 178 | ) 179 | -------------------------------------------------------------------------------- /fortnite_api/asset.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from collections.abc import Coroutine 28 | from typing import TYPE_CHECKING, Any, Generic, Optional, Union, overload 29 | 30 | from typing_extensions import Self 31 | 32 | from .http import HTTPClientT, Route 33 | from .utils import MISSING 34 | 35 | if TYPE_CHECKING: 36 | from .http import HTTPClient, SyncHTTPClient 37 | 38 | 39 | __all__: tuple[str, ...] = ('Asset',) 40 | 41 | 42 | class _AssetRoute(Route): 43 | def __init__(self, url: str) -> None: 44 | self.BASE_URL = '' # type: ignore 45 | self.url: str = url 46 | self.method: str = 'GET' 47 | 48 | 49 | class Asset(Generic[HTTPClientT]): 50 | """ 51 | .. attributetable:: fortnite_api.Asset 52 | 53 | Represents an asset given to the client. An asset can represent any image or video that 54 | has been fetched from the API. 55 | 56 | Examples 57 | -------- 58 | .. code-block:: python3 59 | :caption: Fetching a cosmetic and reading the icon data: 60 | 61 | cosmetic = await client.fetch_cosmetic_br('CID_028_Athena_Commando_F') 62 | images = cosmetic.images 63 | if images is not None and images.icon is not None: 64 | icon: bytes = await images.icon.read() 65 | """ 66 | 67 | __slots__: tuple[str, ...] = ('_http', '_url', '_max_size', '_size') 68 | 69 | def __init__(self, *, http: HTTPClientT, url: str, max_size: Optional[int] = MISSING, size: int = MISSING) -> None: 70 | self._http: HTTPClientT = http 71 | self._url: str = url 72 | 73 | # The maximum size of the asset, if any. If provided, the url's default size is the maximum size. 74 | # MISSING for not supported, None for no max size, int for max size. 75 | self._max_size: Optional[int] = max_size 76 | 77 | # The current size of this asset. Will only be set if the asset was resized. 78 | self._size: int = size 79 | 80 | def __eq__(self, __o: object) -> bool: 81 | if not isinstance(__o, self.__class__): 82 | return False 83 | 84 | return self.url == __o.url 85 | 86 | def __ne__(self, __o: object) -> bool: 87 | return not self.__eq__(__o) 88 | 89 | def __hash__(self) -> int: 90 | return hash(self.url) 91 | 92 | def __repr__(self) -> str: 93 | return f'' 94 | 95 | @property 96 | def url(self) -> str: 97 | """:class:`str`: The url of the asset.""" 98 | if self._size is MISSING: 99 | return self._url 100 | 101 | # Resize the URL from the size 102 | return f'{self._url[:-4]}_{self._size}.png' 103 | 104 | @property 105 | def can_resize(self) -> bool: 106 | """ 107 | Returns 108 | -------- 109 | :class:`bool` 110 | Whether this asset can be resized. 111 | """ 112 | return self._max_size is not MISSING 113 | 114 | @property 115 | def max_size(self) -> Optional[int]: 116 | """ 117 | Returns 118 | -------- 119 | Optional[:class:`int`] 120 | The max size of the asset. If ``None``, there is no max size. If `-1`, resizing is not allowed. 121 | """ 122 | if self._max_size is MISSING: 123 | return -1 124 | 125 | return self._max_size 126 | 127 | def resize(self, size: int) -> Self: 128 | """Resizes the asset to the given size. 129 | 130 | Parameters 131 | ---------- 132 | size: :class:`int` 133 | The size to resize the asset to. This must be a power of 2. 134 | 135 | Returns 136 | -------- 137 | :class:`fortnite_api.Asset` 138 | The resized asset. 139 | 140 | Raises 141 | ------ 142 | ValueError 143 | This asset does not support resizing. 144 | """ 145 | if self._max_size is MISSING: 146 | raise ValueError('This asset does not support resizing.') 147 | 148 | if (size & (size - 1) != 0) or size <= 0: 149 | raise ValueError('Size must be a power of 2.') 150 | 151 | if self._max_size is not None: 152 | if size > self._max_size: 153 | raise ValueError(f'Size must be less than or equal to {self._max_size}.') 154 | 155 | self._size = size 156 | return self 157 | 158 | @overload 159 | def read(self: Asset[HTTPClient], /) -> Coroutine[Any, Any, bytes]: ... 160 | 161 | @overload 162 | def read(self: Asset[SyncHTTPClient], /) -> bytes: ... 163 | 164 | def read(self) -> Union[Coroutine[Any, Any, bytes], bytes]: 165 | """|maybecoro| 166 | 167 | Retrieves the content of this asset as a :class:`bytes` object. This is only a coroutine if the client is 168 | an async client, otherwise it is a regular method. 169 | 170 | Returns 171 | -------- 172 | :class:`bytes` 173 | The image bytes. 174 | """ 175 | return self._http.request(_AssetRoute(self.url)) 176 | -------------------------------------------------------------------------------- /fortnite_api/banner.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from typing import Any, Optional 28 | 29 | from .abc import Hashable, ReconstructAble 30 | from .http import HTTPClientT 31 | from .images import Images 32 | from .utils import simple_repr 33 | 34 | __all__: tuple[str, ...] = ( 35 | "Banner", 36 | "BannerColor", 37 | ) 38 | 39 | 40 | @simple_repr 41 | class Banner(Hashable, ReconstructAble[dict[str, Any], HTTPClientT]): 42 | """ 43 | .. attributetable:: fortnite_api.Banner 44 | 45 | Represents a banner within the Fortnite game. 46 | 47 | This inherits from :class:`~fortnite_api.Hashable` and :class:`~fortnite_api.ReconstructAble`. 48 | 49 | .. container:: operations 50 | 51 | .. describe:: repr(x) 52 | 53 | Returns a representation of the account in the form of a string. 54 | 55 | Examples 56 | -------- 57 | .. code-block:: python3 58 | :caption: Fetch all banners in Fortnite. 59 | 60 | # (1) Fetch all the banners using the client 61 | banners = await client.fetch_banners() 62 | 63 | # (2) Walk through each banner 64 | for banner in banners: 65 | # (3) Print out their metadata 66 | print(banner.id, banner.name, banner.description) 67 | 68 | .. code-block:: python3 69 | :caption: Fetch the images of all the banners in Fortnite. 70 | 71 | # (1) Fetch all the banners using the client 72 | banners = await client.fetch_banners() 73 | 74 | # (2) Walk through each banner 75 | for banner in banners: 76 | # (3) Print out the image URL of the banner 77 | print(banner.images.icon.url) 78 | 79 | Attributes 80 | ---------- 81 | id: :class:`str` 82 | The id of the banner. 83 | name: Optional[:class:`str`] 84 | The name of the banner. Can be ``None`` if the banner is not named or 85 | no information is provided by the API. 86 | description: :class:`str` 87 | The description of the banner. 88 | category: Optional[:class:`str`] 89 | The category the banner belongs to. Can be ``None`` if this banner 90 | does not belong to any category. 91 | full_usage_rights: :class:`bool` 92 | Denotes if the banner is full usage rights from Epic Games. 93 | dev_name: :class:`str` 94 | The developer name of the banner, this is used internally by the 95 | Epic Games team. 96 | images: :class:`fortnite_api.Images` 97 | Preview images of the banner. 98 | """ 99 | 100 | __slots__: tuple[str, ...] = ( 101 | "id", 102 | "name", 103 | "description", 104 | "category", 105 | "full_usage_rights", 106 | "dev_name", 107 | "images", 108 | ) 109 | 110 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 111 | super().__init__(data=data, http=http) 112 | 113 | self.id: str = data["id"] 114 | self.name: str = data["name"] 115 | self.dev_name: str = data["devName"] 116 | self.description: str = data["description"] 117 | self.category: Optional[str] = data.get("category") 118 | self.full_usage_rights: bool = data["fullUsageRights"] 119 | 120 | self.images: Images[HTTPClientT] = Images(data=data, http=http) 121 | 122 | 123 | @simple_repr 124 | class BannerColor(Hashable, ReconstructAble[dict[str, Any], HTTPClientT]): 125 | """ 126 | .. attributetable:: fortnite_api.BannerColor 127 | 128 | Represents a color of a :class:`fortnite_api.Banner`. 129 | 130 | This inherits from :class:`~fortnite_api.Hashable` and :class:`~fortnite_api.ReconstructAble`. 131 | 132 | .. container:: operations 133 | 134 | .. describe:: repr(x) 135 | 136 | Returns a representation of the account in the form of a string. 137 | 138 | Attributes 139 | ---------- 140 | id: :class:`str` 141 | The id of the color. 142 | color: :class:`str` 143 | The color of the banner. 144 | category: :class:`str` 145 | The category of the banner. 146 | sub_category_group: :class:`int` 147 | The sub category group of the banner. 148 | """ 149 | 150 | __slots__: tuple[str, ...] = ("id", "color", "category", "sub_category_group") 151 | 152 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 153 | super().__init__(data=data, http=http) 154 | self.id: str = data["id"] 155 | 156 | self.color: str = data["color"] 157 | 158 | self.category: str = data["category"] 159 | self.sub_category_group: int = data["subCategoryGroup"] # TODO: Convert this to enum? 160 | -------------------------------------------------------------------------------- /fortnite_api/cosmetics/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from .br import * 28 | from .car import * 29 | from .common import * 30 | from .instrument import * 31 | from .lego_kit import * 32 | from .track import * 33 | from .variants import * 34 | -------------------------------------------------------------------------------- /fortnite_api/cosmetics/car.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | import datetime 28 | from typing import Any, Optional 29 | 30 | from ..http import HTTPClientT 31 | from ..utils import get_with_fallback, parse_time, simple_repr 32 | from .common import Cosmetic, CosmeticImages, CosmeticRarityInfo, CosmeticSeriesInfo, CosmeticTypeInfo 33 | 34 | __all__: tuple[str, ...] = ('CosmeticCar',) 35 | 36 | 37 | @simple_repr 38 | class CosmeticCar(Cosmetic[dict[str, Any], HTTPClientT]): 39 | """ 40 | .. attributetable:: fortnite_api.CosmeticCar 41 | 42 | Represents a car cosmetic in Fortnite. 43 | 44 | This class inherits from :class:`fortnite_api.Cosmetic`. 45 | 46 | .. container:: operations 47 | 48 | .. describe:: repr(x) 49 | 50 | Returns a representation of the account in the form of a string. 51 | 52 | Attributes 53 | ---------- 54 | vehicle_id: :class:`str` 55 | The ID of the vehicle. 56 | name: :class:`str` 57 | The name of the car. 58 | description: :class:`str` 59 | The description of the car. 60 | type: Optional[:class:`fortnite_api.CosmeticTypeInfo`] 61 | The type of the car. 62 | rarity: Optional[:class:`fortnite_api.CosmeticRarityInfo`] 63 | The rarity of the car. 64 | images: Optional[:class:`fortnite_api.CosmeticImages`] 65 | Any car images. 66 | series: Optional[:class:`fortnite_api.CosmeticSeriesInfo`] 67 | The series of the car. 68 | gameplay_tags: List[:class:`str`] 69 | The gameplay tags of the car. 70 | 71 | .. opt-in:: INCLUDE_GAMEPLAY_TAGS 72 | path: Optional[:class:`str`] 73 | The path of the car. 74 | 75 | .. opt-in:: INCLUDE_PATHS 76 | showcase_video_id: Optional[:class:`str`] 77 | The showcase YouTube video ID of the cosmetic, if available. 78 | shop_history: List[:class:`datetime.datetime`] 79 | A list of datetimes representing the shop history of the car. 80 | 81 | .. opt-in:: INCLUDE_SHOP_HISTORY 82 | """ 83 | 84 | __slots__: tuple[str, ...] = ( 85 | 'vehicle_id', 86 | 'name', 87 | 'description', 88 | 'type', 89 | 'rarity', 90 | 'images', 91 | 'series', 92 | 'gameplay_tags', 93 | 'path', 94 | 'showcase_video_id', 95 | 'shop_history', 96 | ) 97 | 98 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 99 | super().__init__(data=data, http=http) 100 | 101 | self.vehicle_id: str = data['vehicleId'] 102 | self.name: str = data['name'] 103 | self.description: str = data['description'] 104 | 105 | _type = data.get('type') 106 | self.type: Optional[CosmeticTypeInfo[HTTPClientT]] = _type and CosmeticTypeInfo(data=_type, http=http) 107 | 108 | _rarity = data.get('rarity') 109 | self.rarity: Optional[CosmeticRarityInfo[HTTPClientT]] = _rarity and CosmeticRarityInfo(data=_rarity, http=http) 110 | 111 | _images = data.get('images') 112 | self.images: Optional[CosmeticImages[HTTPClientT]] = _images and CosmeticImages(data=_images, http=self._http) 113 | 114 | _series = data.get('series') 115 | self.series: Optional[CosmeticSeriesInfo[HTTPClientT]] = _series and CosmeticSeriesInfo( 116 | data=_series, http=self._http 117 | ) 118 | 119 | self.gameplay_tags: list[str] = get_with_fallback(data, 'gameplayTags', list) 120 | self.path: Optional[str] = data.get('path') 121 | self.showcase_video_id: Optional[str] = data.get('showcaseVideo') 122 | 123 | self.shop_history: list[datetime.datetime] = [ 124 | parse_time(time) for time in get_with_fallback(data, 'shopHistory', list) 125 | ] 126 | 127 | @property 128 | def showcase_video_url(self) -> Optional[str]: 129 | """Optional[:class:`str`]: The URL of the YouTube showcase video of the cosmetic, if any.""" 130 | _id = self.showcase_video_id 131 | if not _id: 132 | return None 133 | 134 | return f"https://www.youtube.com/watch?v={_id}" 135 | -------------------------------------------------------------------------------- /fortnite_api/cosmetics/instrument.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | import datetime 28 | from typing import Any, Optional 29 | 30 | from ..http import HTTPClientT 31 | from ..utils import get_with_fallback, parse_time, simple_repr 32 | from .common import Cosmetic, CosmeticImages, CosmeticRarityInfo, CosmeticSeriesInfo, CosmeticTypeInfo 33 | 34 | __all__: tuple[str, ...] = ('CosmeticInstrument',) 35 | 36 | 37 | @simple_repr 38 | class CosmeticInstrument(Cosmetic[dict[str, Any], HTTPClientT]): 39 | """ 40 | .. attributetable:: fortnite_api.CosmeticInstrument 41 | 42 | Represents an instrument cosmetic in Fortnite. 43 | 44 | This class inherits from :class:`fortnite_api.Cosmetic`. 45 | 46 | .. container:: operations 47 | 48 | .. describe:: repr(x) 49 | 50 | Returns a representation of the account in the form of a string. 51 | 52 | Attributes 53 | ---------- 54 | name: :class:`str` 55 | The name of the instrument. 56 | description: :class:`str` 57 | The description of the instrument. 58 | type: Optional[:class:`fortnite_api.CosmeticTypeInfo`] 59 | The type of the instrument. 60 | rarity: Optional[:class:`fortnite_api.CosmeticRarityInfo`] 61 | The rarity of the instrument. 62 | images: Optional[:class:`fortnite_api.CosmeticImages`] 63 | Any instrument images. 64 | series: Optional[:class:`fortnite_api.CosmeticSeriesInfo`] 65 | The series of the instrument. 66 | gameplay_tags: List[:class:`str`] 67 | The gameplay tags of the instrument. 68 | 69 | .. opt-in:: INCLUDE_GAMEPLAY_TAGS 70 | path: Optional[:class:`str`] 71 | The path of the instrument. 72 | 73 | .. opt-in:: INCLUDE_PATHS 74 | showcase_video_id: Optional[:class:`str`] 75 | The showcase YouTube video ID of the cosmetic, if available. 76 | shop_history: List[:class:`datetime.datetime`] 77 | The shop history of the instrument. 78 | 79 | .. opt-in:: INCLUDE_SHOP_HISTORY 80 | """ 81 | 82 | __slots__: tuple[str, ...] = ( 83 | 'name', 84 | 'description', 85 | 'type', 86 | 'rarity', 87 | 'images', 88 | 'series', 89 | 'gameplay_tags', 90 | 'path', 91 | 'showcase_video_id', 92 | 'shop_history', 93 | ) 94 | 95 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 96 | super().__init__(data=data, http=http) 97 | 98 | self.name: str = data['name'] 99 | self.description: str = data['description'] 100 | 101 | _type = data.get('type') 102 | self.type: Optional[CosmeticTypeInfo[HTTPClientT]] = _type and CosmeticTypeInfo(data=_type, http=http) 103 | 104 | _rarity = data.get('rarity') 105 | self.rarity: Optional[CosmeticRarityInfo[HTTPClientT]] = _rarity and CosmeticRarityInfo(data=_rarity, http=http) 106 | 107 | _images = data.get('images') 108 | self.images: Optional[CosmeticImages[HTTPClientT]] = _images and CosmeticImages(data=_images, http=http) 109 | 110 | _series = data.get('series') 111 | self.series: Optional[CosmeticSeriesInfo[HTTPClientT]] = _series and CosmeticSeriesInfo( 112 | data=_series, http=self._http 113 | ) 114 | 115 | self.gameplay_tags: list[str] = get_with_fallback(data, 'gameplayTags', list) 116 | self.path: Optional[str] = data.get('path') 117 | self.showcase_video_id: Optional[str] = data.get('showcaseVideo') 118 | 119 | self.shop_history: list[datetime.datetime] = [ 120 | parse_time(time) for time in get_with_fallback(data, 'shopHistory', list) 121 | ] 122 | 123 | @property 124 | def showcase_video_url(self) -> Optional[str]: 125 | """Optional[:class:`str`]: The URL of the YouTube showcase video of the cosmetic, if any.""" 126 | _id = self.showcase_video_id 127 | if not _id: 128 | return None 129 | 130 | return f"https://www.youtube.com/watch?v={_id}" 131 | -------------------------------------------------------------------------------- /fortnite_api/cosmetics/lego_kit.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from typing import Any, Optional 28 | 29 | from ..http import HTTPClientT 30 | from ..utils import get_with_fallback, parse_time, simple_repr 31 | from .common import Cosmetic, CosmeticImages, CosmeticSeriesInfo, CosmeticTypeInfo 32 | 33 | __all__: tuple[str, ...] = ('CosmeticLegoKit',) 34 | 35 | 36 | @simple_repr 37 | class CosmeticLegoKit(Cosmetic[dict[str, Any], HTTPClientT]): 38 | """ 39 | .. attributetable:: fortnite_api.CosmeticLegoKit 40 | 41 | Represents a LEGO kit cosmetic in Fortnite. 42 | 43 | This class inherits from :class:`fortnite_api.Cosmetic`. 44 | 45 | .. container:: operations 46 | 47 | .. describe:: repr(x) 48 | 49 | Returns a representation of the account in the form of a string. 50 | 51 | Attributes 52 | ---------- 53 | name: :class:`str` 54 | The name of the LEGO kit. 55 | type: Optional[:class:`fortnite_api.CosmeticTypeInfo`] 56 | The type of the LEGO kit. 57 | gameplay_tags: List[:class:`str`] 58 | The gameplay tags of the LEGO kit. 59 | 60 | .. opt-in:: INCLUDE_GAMEPLAY_TAGS 61 | images: Optional[:class:`fortnite_api.CosmeticImages`] 62 | Any LEGO kit images. 63 | path: Optional[:class:`str`] 64 | The path of the LEGO kit. 65 | 66 | .. opt-in:: INCLUDE_PATHS 67 | shop_history: List[:class:`datetime.datetime`] 68 | The shop history of the LEGO kit. 69 | 70 | .. opt-in:: INCLUDE_SHOP_HISTORY 71 | """ 72 | 73 | __slots__: tuple[str, ...] = ('name', 'type', 'gameplay_tags', 'images', 'path', 'shop_history') 74 | 75 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 76 | super().__init__(data=data, http=http) 77 | 78 | self.name: str = data['name'] 79 | 80 | _type = data.get('type') 81 | self.type: Optional[CosmeticTypeInfo[HTTPClientT]] = _type and CosmeticTypeInfo(data=_type, http=http) 82 | 83 | _series = data.get('series') 84 | self.series: Optional[CosmeticSeriesInfo[HTTPClientT]] = _series and CosmeticSeriesInfo(data=_series, http=http) 85 | 86 | self.gameplay_tags: list[str] = get_with_fallback(data, 'gameplayTags', list) 87 | 88 | _images = data.get('images') 89 | self.images: Optional[CosmeticImages[HTTPClientT]] = _images and CosmeticImages(data=_images, http=http) 90 | 91 | self.path: Optional[str] = data.get('path') 92 | self.shop_history = [parse_time(time) for time in get_with_fallback(data, 'shopHistory', list)] 93 | -------------------------------------------------------------------------------- /fortnite_api/cosmetics/track.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | import datetime 28 | from typing import Any, Optional 29 | 30 | from ..abc import ReconstructAble 31 | from ..asset import Asset 32 | from ..http import HTTPClientT 33 | from ..utils import get_with_fallback, parse_time, simple_repr 34 | from .common import Cosmetic 35 | 36 | __all__: tuple[str, ...] = ('CosmeticTrackDifficulty', 'CosmeticTrack') 37 | 38 | 39 | @simple_repr 40 | class CosmeticTrackDifficulty(ReconstructAble[dict[str, Any], HTTPClientT]): 41 | """ 42 | .. attributetable:: fortnite_api.CosmeticTrackDifficulty 43 | 44 | Represents the difficulty of a track cosmetic in Fortnite. This 45 | class inherits from :class:`fortnite_api.ReconstructAble`. 46 | 47 | .. container:: operations 48 | 49 | .. describe:: repr(x) 50 | 51 | Returns a representation of the account in the form of a string. 52 | 53 | Attributes 54 | ---------- 55 | vocals: :class:`int` 56 | The vocals difficulty of the track. 57 | guitar: :class:`int` 58 | The guitar difficulty of the track. 59 | bass: :class:`int` 60 | The bass difficulty of the track. 61 | plastic_bass: :class:`int` 62 | The plastic bass difficulty of the track. 63 | drums: :class:`int` 64 | The drums difficulty of the track. 65 | plastic_drums: :class:`int` 66 | The plastic drums difficulty of the track. 67 | """ 68 | 69 | __slots__: tuple[str, ...] = ('vocals', 'guitar', 'bass', 'plastic_bass', 'drums', 'plastic_drums') 70 | 71 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 72 | super().__init__(data=data, http=http) 73 | 74 | self.vocals: int = data['vocals'] 75 | self.guitar: int = data['guitar'] 76 | self.bass: int = data['bass'] 77 | self.plastic_bass: int = data['plasticBass'] 78 | self.drums: int = data['drums'] 79 | self.plastic_drums: int = data['plasticDrums'] 80 | 81 | 82 | @simple_repr 83 | class CosmeticTrack(Cosmetic[dict[str, Any], HTTPClientT]): 84 | """ 85 | .. attributetable:: fortnite_api.CosmeticTrack 86 | 87 | Represents a track cosmetic in Fortnite. 88 | 89 | This class inherits from :class:`fortnite_api.Cosmetic`. 90 | 91 | .. container:: operations 92 | 93 | .. describe:: repr(x) 94 | 95 | Returns a representation of the account in the form of a string. 96 | 97 | Attributes 98 | ---------- 99 | dev_name: :class:`str` 100 | The developer name of the track. 101 | title: :class:`str` 102 | The title of the track. 103 | artist: :class:`str` 104 | The artist of the track. 105 | album: Optional[:class:`str`] 106 | The album of the track. 107 | release_year: :class:`int` 108 | The release year of the track. 109 | bpm: :class:`int` 110 | The BPM of the track. 111 | duration: :class:`int` 112 | The duration of the track, in seconds. 113 | difficulty: :class:`fortnite_api.CosmeticTrackDifficulty` 114 | The difficulty of the track. 115 | gameplay_tags: List[:class:`str`] 116 | The gameplay tags of the track. 117 | 118 | .. opt-in:: INCLUDE_GAMEPLAY_TAGS 119 | genres: List[:class:`str`] 120 | The genres of the track. 121 | album_art: :class:`fortnite_api.Asset` 122 | The album art of the track. 123 | shop_history: List[:class:`datetime.datetime`] 124 | The shop history of the track. 125 | 126 | .. opt-in:: INCLUDE_SHOP_HISTORY 127 | """ 128 | 129 | __slots__: tuple[str, ...] = ( 130 | 'dev_name', 131 | 'title', 132 | 'artist', 133 | 'album', 134 | 'release_year', 135 | 'bpm', 136 | 'duration', 137 | 'difficulty', 138 | 'gameplay_tags', 139 | 'genres', 140 | 'album_art', 141 | 'shop_history', 142 | ) 143 | 144 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 145 | super().__init__(data=data, http=http) 146 | 147 | self.dev_name: str = data['devName'] 148 | self.title: str = data['title'] 149 | self.artist: str = data['artist'] 150 | self.album: Optional[str] = data.get('album') 151 | self.release_year: int = data['releaseYear'] 152 | self.bpm: int = data['bpm'] 153 | self.duration: int = data['duration'] 154 | 155 | self.difficulty: CosmeticTrackDifficulty[HTTPClientT] = CosmeticTrackDifficulty(data=data['difficulty'], http=http) 156 | self.gameplay_tags: list[str] = get_with_fallback(data, 'gameplayTags', list) 157 | self.genres: list[str] = get_with_fallback(data, 'genres', list) 158 | self.album_art: Asset[HTTPClientT] = Asset(http=http, url=data['albumArt']) 159 | 160 | self.shop_history: list[datetime.datetime] = [ 161 | parse_time(time) for time in get_with_fallback(data, 'shopHistory', list) 162 | ] 163 | -------------------------------------------------------------------------------- /fortnite_api/cosmetics/variants/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from .bean import * 28 | from .lego import * 29 | -------------------------------------------------------------------------------- /fortnite_api/cosmetics/variants/bean.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from collections.abc import Coroutine 28 | from typing import TYPE_CHECKING, Any, Optional, Union, overload 29 | 30 | from ...enums import CustomGender, GameLanguage, try_enum 31 | from ...http import HTTPClientT 32 | from ...utils import get_with_fallback 33 | from ..br import CosmeticBr 34 | from ..common import Cosmetic, CosmeticImages 35 | 36 | if TYPE_CHECKING: 37 | from ...http import HTTPClient, SyncHTTPClient 38 | 39 | __all__: tuple[str, ...] = ('VariantBean',) 40 | 41 | 42 | class VariantBean(Cosmetic[dict[str, Any], HTTPClientT]): 43 | """ 44 | .. attributetable:: fortnite_api.VariantBean 45 | 46 | This class represents the Bean variant of a cosmetic item. This stems from 47 | the Fortnite x Fall Guys collaboration, where Fortnite cosmetics were 48 | transformed into Fall Guys beans. 49 | 50 | This class inherits from :class:`fortnite_api.Cosmetic`. 51 | 52 | Attributes 53 | ---------- 54 | cosmetic_id: Optional[:class:`str`] 55 | The ID of the cosmetic that this bean represents, if any. 56 | name: :class:`str` 57 | The name of this bean. 58 | gender: :class:`fortnite_api.CustomGender` 59 | Denotes the gender of this bean. 60 | gameplay_tags: List[:class:`str`] 61 | The gameplay tags associated with this bean. 62 | 63 | .. opt-in:: INCLUDE_GAMEPLAY_TAGS 64 | images: Optional[:class:`fortnite_api.CosmeticImages`] 65 | Any display images of this bean in the game. Will be ``None`` 66 | if there are no images. 67 | path: Optional[:class:`str`] 68 | The game path of this bean. Will be ``None`` if there is no path 69 | in the API response. 70 | 71 | .. opt-in:: INCLUDE_PATHS 72 | """ 73 | 74 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 75 | super().__init__(data=data, http=http) 76 | 77 | self.cosmetic_id: Optional[str] = data.get('cosmetic_id') 78 | self.name: str = data['name'] 79 | self.gender: CustomGender = try_enum(CustomGender, data['gender'] if 'gender' in data else 'Unknown') 80 | self.gameplay_tags: list[str] = get_with_fallback(data, 'gameplay_tags', list) 81 | 82 | _images = data.get('images') 83 | self.images: Optional[CosmeticImages[HTTPClientT]] = _images and CosmeticImages(data=_images, http=http) 84 | self.path: Optional[str] = data.get('path') 85 | 86 | @overload 87 | def fetch_cosmetic_br( 88 | self: VariantBean[HTTPClient], *, language: Optional[GameLanguage] = None 89 | ) -> Coroutine[Any, Any, CosmeticBr]: ... 90 | 91 | @overload 92 | def fetch_cosmetic_br(self: VariantBean[SyncHTTPClient], *, language: Optional[GameLanguage] = None) -> CosmeticBr: ... 93 | 94 | def fetch_cosmetic_br( 95 | self, *, language: Optional[GameLanguage] = None 96 | ) -> Union[Coroutine[Any, Any, CosmeticBr], CosmeticBr]: 97 | """|coro| 98 | 99 | Fetches the Battle Royale cosmetic that this bean variant is based on. 100 | 101 | Parameters 102 | ---------- 103 | language: Optional[:class:`fortnite_api.GameLanguage`] 104 | The language to fetch the cosmetic in. 105 | 106 | Returns 107 | ------- 108 | :class:`fortnite_api.CosmeticBr` 109 | The Battle Royale cosmetic that this bean variant is based on. 110 | 111 | Raises 112 | ------ 113 | ValueError 114 | The bean variant does not have a corresponding Battle Royale cosmetic. 115 | I.e. :attr`cosmetic_id` is ``None``. 116 | """ 117 | cosmetic_id = self.cosmetic_id 118 | if cosmetic_id is None: 119 | raise ValueError('This bean variant does not have a corresponding Battle Royale cosmetic.') 120 | 121 | return self._http.get_cosmetic_br(cosmetic_id, language=language and language.value) 122 | -------------------------------------------------------------------------------- /fortnite_api/cosmetics/variants/lego.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from collections.abc import Coroutine 28 | from typing import TYPE_CHECKING, Any, Optional, Union, overload 29 | 30 | from ...enums import GameLanguage 31 | from ...http import HTTPClientT 32 | from ...utils import get_with_fallback, simple_repr 33 | from ..br import CosmeticBr 34 | from ..common import Cosmetic, CosmeticImages 35 | 36 | if TYPE_CHECKING: 37 | from ...http import HTTPClient, SyncHTTPClient 38 | 39 | __all__: tuple[str, ...] = ('VariantLego',) 40 | 41 | 42 | @simple_repr 43 | class VariantLego(Cosmetic[dict[str, Any], HTTPClientT]): 44 | """ 45 | .. attributetable:: fortnite_api.VariantLego 46 | 47 | Represents a Lego cosmetic variant. 48 | 49 | This class inherits from :class:`fortnite_api.Cosmetic`. 50 | 51 | .. container:: operations 52 | 53 | .. describe:: repr(x) 54 | 55 | Returns a representation of the account in the form of a string. 56 | 57 | Attributes 58 | ---------- 59 | cosmetic_id: :class:`str` 60 | The ID of the cosmetic that this lego cosmetic variant is based on. 61 | sound_library_tags: List[:class:`str`] 62 | The sound library tags of the lego cosmetic variant. 63 | images: Optional[:class:`fortnite_api.CosmeticImages`] 64 | The images of the lego cosmetic variant. 65 | path: Optional[:class:`str`] 66 | The path of the lego cosmetic variant. Will be ``None`` if 67 | the API response does not contain a path. 68 | 69 | .. opt-in:: INCLUDE_PATHS 70 | """ 71 | 72 | __slots__: tuple[str, ...] = ('cosmetic_id', 'sound_library_tags', 'images', 'path') 73 | 74 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 75 | super().__init__(data=data, http=http) 76 | 77 | self.cosmetic_id: str = data['cosmeticId'] 78 | self.sound_library_tags: list[str] = get_with_fallback(data, 'soundLibraryTags', list) 79 | 80 | _images = data.get('images') 81 | self.images: Optional[CosmeticImages[HTTPClientT]] = _images and CosmeticImages(data=_images, http=http) 82 | self.path: Optional[str] = data.get('path') 83 | 84 | @overload 85 | def fetch_cosmetic_br( 86 | self: VariantLego[HTTPClient], *, language: Optional[GameLanguage] = None 87 | ) -> Coroutine[Any, Any, CosmeticBr]: ... 88 | 89 | @overload 90 | def fetch_cosmetic_br(self: VariantLego[SyncHTTPClient], *, language: Optional[GameLanguage] = None) -> CosmeticBr: ... 91 | 92 | def fetch_cosmetic_br( 93 | self, *, language: Optional[GameLanguage] = None 94 | ) -> Union[Coroutine[Any, Any, CosmeticBr], CosmeticBr]: 95 | """|coro| 96 | 97 | Fetches the Battle Royale cosmetic that this lego cosmetic variant is based on. 98 | 99 | Parameters 100 | ---------- 101 | language: Optional[:class:`fortnite_api.GameLanguage`] 102 | The language to fetch the cosmetic in. 103 | 104 | Returns 105 | ------- 106 | :class:`fortnite_api.CosmeticBr` 107 | The Battle Royale cosmetic that this lego cosmetic variant is based on. 108 | """ 109 | return self._http.get_cosmetic_br(self.cosmetic_id, language=language and language.value) 110 | -------------------------------------------------------------------------------- /fortnite_api/creator_code.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from typing import Any 28 | 29 | from .abc import ReconstructAble 30 | from .account import Account 31 | from .http import HTTPClientT 32 | from .utils import simple_repr 33 | 34 | __all__: tuple[str, ...] = ("CreatorCode",) 35 | 36 | 37 | @simple_repr 38 | class CreatorCode(ReconstructAble[dict[str, Any], HTTPClientT]): 39 | """ 40 | .. attributetable:: fortnite_api.CreatorCode 41 | 42 | Represents a Creator Code. 43 | 44 | This inherits from :class:`~fortnite_api.ReconstructAble`. 45 | 46 | .. container:: operations 47 | 48 | .. describe:: repr(x) 49 | 50 | Returns a representation of the account in the form of a string. 51 | 52 | Attributes 53 | ----------- 54 | code: :class:`str` 55 | The creator code. 56 | account: :class:`fortnite_api.Account` 57 | The account associated with the creator code. Ie, the account 58 | that owns the creator code. 59 | """ 60 | 61 | __slots__: tuple[str, ...] = ("code", "account") 62 | 63 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 64 | super().__init__(data=data, http=http) 65 | 66 | self.code: str = data["code"] 67 | self.account: Account[HTTPClientT] = Account(data=data["account"], http=http) 68 | -------------------------------------------------------------------------------- /fortnite_api/errors.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from typing import TYPE_CHECKING, Any, Optional, Union 28 | 29 | if TYPE_CHECKING: 30 | import aiohttp 31 | import requests 32 | 33 | __all__: tuple[str, ...] = ( 34 | "FortniteAPIException", 35 | "HTTPException", 36 | "NotFound", 37 | "Forbidden", 38 | "ServiceUnavailable", 39 | "RateLimited", 40 | "Unauthorized", 41 | "BetaAccessNotEnabled", 42 | "BetaUnknownException", 43 | "MissingAPIKey", 44 | ) 45 | 46 | 47 | class FortniteAPIException(Exception): 48 | """The base for all Fortnite API exceptions. 49 | 50 | This class inherits from :class:`Exception`. 51 | 52 | Attributes 53 | ---------- 54 | message: :class:`str` 55 | The error message describing the exception. 56 | """ 57 | 58 | pass 59 | 60 | 61 | class HTTPException(FortniteAPIException): 62 | """ 63 | .. attributetable:: fortnite_api.HTTPException 64 | 65 | Represents a base HTTP Exception. Every HTTP exception inherits from this class. 66 | 67 | Attributes 68 | ---------- 69 | message: Optional[:class:`str`] 70 | The error message describing the exception, if any. 71 | response: Union[:class:`aiohttp.ClientResponse`, :class:`requests.Response`] 72 | The response that was returned from the API. If the client is running async, it will be an aiohttp response, 73 | otherwise it will be a requests response. 74 | data: Any 75 | The raw data that was returned from the API. 76 | """ 77 | 78 | def __init__( 79 | self, message: Optional[str], response: Union[aiohttp.ClientResponse, requests.Response], data: Any, / 80 | ) -> None: 81 | self.message: Optional[str] = message 82 | self.response: Union[aiohttp.ClientResponse, requests.Response] = response 83 | self.data: Any = data 84 | super().__init__(message) 85 | 86 | @property 87 | def status_code(self) -> int: 88 | """Returns the status code of the response. 89 | 90 | Returns 91 | ------- 92 | :class:`int` 93 | The status code of the response. 94 | """ 95 | if isinstance(self.response, requests.Response): 96 | return self.response.status_code 97 | 98 | return self.response.status 99 | 100 | 101 | class NotFound(HTTPException): 102 | """ 103 | .. attributetable:: fortnite_api.NotFound 104 | 105 | Exception raised when a resource is not found. 106 | 107 | This class inherits from :class:`fortnite_api.HTTPException`. 108 | """ 109 | 110 | pass 111 | 112 | 113 | class Forbidden(HTTPException): 114 | """ 115 | .. attributetable:: fortnite_api.Forbidden 116 | 117 | Exception raised when the requested operation is forbidden. 118 | 119 | This class inherits from :class:`fortnite_api.HTTPException`. 120 | """ 121 | 122 | pass 123 | 124 | 125 | class ServiceUnavailable(HTTPException): 126 | """ 127 | .. attributetable:: fortnite_api.ServiceUnavailable 128 | 129 | Exception raised when the services of Fortnite API are unavailable. 130 | 131 | This class inherits from :class:`fortnite_api.HTTPException`. 132 | """ 133 | 134 | pass 135 | 136 | 137 | class RateLimited(HTTPException): 138 | """ 139 | .. attributetable:: fortnite_api.RateLimited 140 | 141 | Exception raised when the client has been rate limited. 142 | 143 | This class inherits from :class:`fortnite_api.HTTPException`. 144 | """ 145 | 146 | pass 147 | 148 | 149 | class Unauthorized(HTTPException): 150 | """ 151 | .. attributetable:: fortnite_api.Unauthorized 152 | 153 | Exception raised when the client is unauthorized to access the requested resource. 154 | 155 | This class inherits from :class:`fortnite_api.HTTPException`. 156 | """ 157 | 158 | pass 159 | 160 | 161 | class BetaAccessNotEnabled(FortniteAPIException): 162 | """ 163 | .. attributetable:: fortnite_api.BetaAccessNotEnabled 164 | 165 | Exception raised when a user tries to access a feature or functionality that requires beta access, 166 | but the beta access is not enabled. 167 | 168 | This class inherits :class:`fortnite_api.FortniteAPIException`. 169 | 170 | Attributes 171 | ---------- 172 | message: :class:`str` 173 | The error message describing the exception. 174 | """ 175 | 176 | pass 177 | 178 | 179 | class BetaUnknownException(FortniteAPIException): 180 | """ 181 | .. attributetable:: fortnite_api.BetaUnknownException 182 | 183 | Exception raised when an unknown exception occurs while trying to access a beta feature. 184 | 185 | This class inherits :class:`fortnite_api.FortniteAPIException`. 186 | 187 | Attributes 188 | ---------- 189 | message: :class:`str` 190 | The error message describing the exception. 191 | original: Exception 192 | The original exception that occurred. 193 | """ 194 | 195 | def __init__(self, *, original: Exception) -> None: 196 | super().__init__( 197 | f"An unknown exception occurred while trying to access a beta feature. Original exception: {original}" 198 | ) 199 | self.original: Exception = original 200 | 201 | 202 | class MissingAPIKey(FortniteAPIException): 203 | """ 204 | .. attributetable:: fortnite_api.MissingAPIKey 205 | 206 | Exception raised when the client does not have an API key set. 207 | 208 | This class inherits :class:`fortnite_api.FortniteAPIException`. 209 | 210 | Attributes 211 | ---------- 212 | message: :class:`str` 213 | The error message describing the exception. 214 | """ 215 | 216 | pass 217 | -------------------------------------------------------------------------------- /fortnite_api/flags.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | import enum 28 | 29 | from typing_extensions import Self 30 | 31 | __all__: tuple[str, ...] = ('ResponseFlags',) 32 | 33 | 34 | class ResponseFlags(enum.IntFlag): 35 | """ 36 | .. attributetable:: fortnite_api.ResponseFlags 37 | 38 | Denotes a "response flag" in the Fortnite API. These are toggle-able 39 | options that denote how a response from the API should be formatted and which 40 | data should be included. 41 | 42 | Attributes 43 | ---------- 44 | INCLUDE_NOTHING: :class:`int` 45 | Include nothing special in the response. This will only include standard 46 | data in all responses, ie, any data fields in this class prefixed 47 | with ``INCLUDE_``. 48 | INCLUDE_PATHS: :class:`int` 49 | Denotes if the response should include the ``paths`` field in the response, 50 | if the endpoint contains it. 51 | INCLUDE_GAMEPLAY_TAGS: :class:`int` 52 | Denotes if the response should include the ``gameplay_tags`` field in the response, 53 | if the endpoint contains it. 54 | INCLUDE_SHOP_HISTORY: :class:`int` 55 | Denotes if the response should include the ``shop_history`` field in the response, 56 | if the endpoint contains it. 57 | """ 58 | 59 | INCLUDE_NOTHING = 0 60 | INCLUDE_PATHS = 1 << 0 61 | INCLUDE_GAMEPLAY_TAGS = 1 << 1 62 | INCLUDE_SHOP_HISTORY = 1 << 2 63 | 64 | @classmethod 65 | def all(cls: type[Self]) -> Self: 66 | """:class:`ResponseFlags`: Returns a flag that includes all flags.""" 67 | self = cls.INCLUDE_NOTHING 68 | for item in cls: 69 | # If this item is not already included in self, include it 70 | if not self & item: 71 | self |= item 72 | 73 | return self 74 | -------------------------------------------------------------------------------- /fortnite_api/images.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from typing import Any, Optional 28 | 29 | from .abc import ReconstructAble 30 | from .asset import Asset 31 | from .http import HTTPClientT 32 | from .utils import simple_repr 33 | 34 | __all__: tuple[str, ...] = ('Images',) 35 | 36 | 37 | @simple_repr 38 | class Images(ReconstructAble[dict[str, Any], HTTPClientT]): 39 | """ 40 | .. attributetable:: fortnite_api.Images 41 | 42 | Represents image data passed from the API. This class is used to represent 43 | commonly provided assets for many API endpoints and object types. 44 | 45 | This inherits from :class:`~fortnite_api.ReconstructAble`. 46 | 47 | .. container:: operations 48 | 49 | .. describe:: repr(x) 50 | 51 | Returns a representation of the account in the form of a string. 52 | 53 | Attributes 54 | ---------- 55 | small_icon: Optional[:class:`fortnite_api.Asset`] 56 | A smaller icon asset. Typically, this is a smaller version of the main image. 57 | icon: Optional[:class:`fortnite_api.Asset`] 58 | An icon asset. Typically, this is the main image of the object. 59 | """ 60 | 61 | __slots__: tuple[str, ...] = ('small_icon', 'icon') 62 | 63 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 64 | super().__init__(data=data, http=http) 65 | 66 | small_icon = data.get('smallIcon') 67 | self.small_icon: Optional[Asset[HTTPClientT]] = small_icon and Asset(http=http, url=small_icon) 68 | 69 | icon = data.get('icon') 70 | self.icon: Optional[Asset[HTTPClientT]] = icon and Asset(http=http, url=icon) 71 | -------------------------------------------------------------------------------- /fortnite_api/map.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from collections.abc import Generator 28 | from typing import Any, Optional 29 | 30 | from .abc import Hashable, ReconstructAble 31 | from .asset import Asset 32 | from .http import HTTPClientT 33 | from .proxies import TransformerListProxy 34 | from .utils import simple_repr 35 | 36 | __all__: tuple[str, ...] = ("Map", "MapImages", "POI", "POILocation") 37 | 38 | 39 | @simple_repr 40 | class MapImages(ReconstructAble[dict[str, Any], HTTPClientT]): 41 | """ 42 | .. attributetable:: fortnite_api.MapImages 43 | 44 | Represents the images of a given POI map. This inherits 45 | from :class:`~fortnite_api.ReconstructAble`. 46 | 47 | .. container:: operations 48 | 49 | .. describe:: repr(x) 50 | 51 | Returns a representation of the account in the form of a string. 52 | 53 | Attributes 54 | ---------- 55 | blank: :class:`fortnite_api.Asset` 56 | The asset pointing to an image of the map that does not contain any POI names. 57 | pois: :class:`fortnite_api.Asset` 58 | The asset pointing to an image of the map that contains the POI names. 59 | """ 60 | 61 | __slots__: tuple[str, ...] = ("blank", "pois") 62 | 63 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 64 | super().__init__(data=data, http=http) 65 | 66 | self.blank: Asset[HTTPClientT] = Asset(http=http, url=data["blank"]) 67 | self.pois: Asset[HTTPClientT] = Asset(http=http, url=data["pois"]) 68 | 69 | 70 | @simple_repr 71 | class Map(ReconstructAble[dict[str, Any], HTTPClientT]): 72 | """ 73 | .. attributetable:: fortnite_api.Map 74 | 75 | Represents a Fortnite map. This inherits from :class:`~fortnite_api.ReconstructAble`. 76 | 77 | .. container:: operations 78 | 79 | .. describe:: repr(x) 80 | 81 | Returns a representation of the account in the form of a string. 82 | 83 | Examples 84 | -------- 85 | .. code-block:: python3 86 | :caption: Getting the images of the Fortnite map. 87 | 88 | map = await client.fetch_map() 89 | print(map.images.pois.url) 90 | print(map.images.blank.url) 91 | 92 | 93 | Attributes 94 | ---------- 95 | images: :class:`fortnite_api.MapImages` 96 | The images of the map. 97 | pois: List[:class:`fortnite_api.POI`] 98 | The list of POIs in the map. 99 | """ 100 | 101 | __slots__: tuple[str, ...] = ("images", "pois") 102 | 103 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 104 | super().__init__(data=data, http=http) 105 | 106 | self.images: MapImages[HTTPClientT] = MapImages(data=data["images"], http=http) 107 | 108 | self.pois: list[POI[HTTPClientT]] = TransformerListProxy( 109 | data["pois"], 110 | transform_data=lambda poi: POI(data=poi, http=http), 111 | ) 112 | 113 | 114 | @simple_repr 115 | class POI(Hashable, ReconstructAble[dict[str, Any], HTTPClientT]): 116 | """ 117 | .. attributetable:: fortnite_api.POI 118 | 119 | Represents a specific POI in a Fortnite map. 120 | 121 | This inherits from :class:`~fortnite_api.Hashable` and :class:`~fortnite_api.ReconstructAble`. 122 | 123 | .. container:: operations 124 | 125 | .. describe:: repr(x) 126 | 127 | Returns a representation of the account in the form of a string. 128 | 129 | Examples 130 | -------- 131 | .. code-block:: python3 132 | :caption: Getting all POIs in the Fortnite map. 133 | 134 | # (1) Fetch the map 135 | map = await client.fetch_map() 136 | 137 | # (2) walk through all the POIs 138 | for poi in map.pois: 139 | # (3) print the name & (x, y, z) coordinates 140 | print(poi.name, tuple(poi.location)) 141 | 142 | 143 | Attributes 144 | ---------- 145 | id: :class:`str` 146 | The ID of the POI. 147 | name: :class:`str` 148 | The name of the POI. 149 | location: :class:`fortnite_api.POILocation` 150 | The location of the POI. 151 | """ 152 | 153 | __slots__: tuple[str, ...] = ("id", "name", "location") 154 | 155 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 156 | super().__init__(data=data, http=http) 157 | 158 | self.id: str = data["id"] 159 | self.name: Optional[str] = data.get("name") 160 | self.location: POILocation[HTTPClientT] = POILocation(data=data["location"], http=http) 161 | 162 | 163 | @simple_repr 164 | class POILocation(ReconstructAble[dict[str, Any], HTTPClientT]): 165 | """ 166 | .. attributetable:: fortnite_api.POILocation 167 | 168 | Holds the x, y, z coordinates of a POI in a Fortnite map. This inherits 169 | from :class:`~fortnite_api.ReconstructAble`. 170 | 171 | .. container:: operations 172 | 173 | .. describe:: repr(x) 174 | 175 | Returns a representation of the account in the form of a string. 176 | 177 | .. describe:: iter(x) 178 | 179 | Returns an iter of the x, y, z coordinates. 180 | 181 | Examples 182 | -------- 183 | .. code-block:: python3 184 | :caption: Unpacking a POI location. 185 | 186 | map = await client.fetch_map() 187 | poi = map.pois[0] 188 | x, y, z = poi.location 189 | print(x, y, z) 190 | 191 | Attributes 192 | ---------- 193 | x: :class:`float` 194 | The x coordinate. 195 | y: :class:`float` 196 | The y coordinate. 197 | z: :class:`float` 198 | The z coordinate. 199 | """ 200 | 201 | __slots__: tuple[str, ...] = ("x", "y", "z") 202 | 203 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 204 | super().__init__(data=data, http=http) 205 | 206 | self.x: float = data["x"] 207 | self.y: float = data["y"] 208 | self.z: float = data["z"] 209 | 210 | # __iter__ method to allow for easy unpacking of the coordinates 211 | # and to allow tuple(loc) to work 212 | def __iter__(self) -> Generator[float, None, None]: 213 | yield self.x 214 | yield self.y 215 | yield self.z 216 | -------------------------------------------------------------------------------- /fortnite_api/playlist.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from typing import TYPE_CHECKING, Any, Optional 28 | 29 | from .abc import Hashable, ReconstructAble 30 | from .asset import Asset 31 | from .http import HTTPClientT 32 | from .utils import get_with_fallback, parse_time 33 | 34 | if TYPE_CHECKING: 35 | import datetime 36 | 37 | 38 | __all__: tuple[str, ...] = ('PlaylistImages', 'Playlist') 39 | 40 | 41 | class PlaylistImages(ReconstructAble[dict[str, Any], HTTPClientT]): 42 | """ 43 | .. attributetable:: fortnite_api.PlaylistImages 44 | 45 | Represents images that are associated with a Fortnite Playlist. This 46 | class inherits from :class:`~fortnite_api.ReconstructAble`. 47 | 48 | Attributes 49 | ------------ 50 | showcase: Optional[:class:`fortnite_api.Asset`] 51 | A showcase image for the playlist, if any. 52 | mission_icon: Optional[:class:`fortnite_api.Asset`] 53 | A mission icon for the playlist, if any. 54 | """ 55 | 56 | __slots__: tuple[str, ...] = ('showcase', 'mission_icon') 57 | 58 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 59 | super().__init__(data=data, http=http) 60 | 61 | _showcase = data.get('showcase') 62 | self.showcase: Optional[Asset[HTTPClientT]] = _showcase and Asset(url=_showcase, http=http) 63 | 64 | _mission_icon = data.get('missionIcon') 65 | self.mission_icon: Optional[Asset[HTTPClientT]] = _mission_icon and Asset(url=_mission_icon, http=http) 66 | 67 | 68 | class Playlist(Hashable, ReconstructAble[dict[str, Any], HTTPClientT]): 69 | """ 70 | .. attributetable:: fortnite_api.Playlist 71 | 72 | Represents a Fortnite Playlist. This class inherits from :class:`~fortnite_api.Hashable` and :class:`~fortnite_api.ReconstructAble`. 73 | 74 | Attributes 75 | ----------- 76 | id: :class:`str` 77 | The ID of the playlist. 78 | name: :class:`str` 79 | The playlist's name. 80 | sub_name: Optional[:class:`str`] 81 | The playlist's sub name, if any. 82 | description: Optional[:class:`str`] 83 | A description of the playlist. 84 | game_type: Optional[:class:`str`] 85 | The type of game the playlist is, if any. 86 | rating_type: Optional[:class:`str`] 87 | The rating type of the playlist, if any. 88 | min_players: :class:`int` 89 | The minimum amount of players required. Will be ``-1`` if there is no limit. 90 | max_players: :class:`int` 91 | The maximum amount of players allowed. Will be ``-1`` if there is no limit. 92 | max_teams: :class:`int` 93 | The maximum amount of teams allowed. Will be ``-1`` if there is no limit. 94 | max_team_size: :class:`int` 95 | The maximum amount of players per team. Will be ``-1`` if there is no limit. 96 | max_squads: :class:`int` 97 | The maximum amount of squads allowed. Will be ``-1`` if there is no limit. 98 | max_squad_size: :class:`int` 99 | The maximum amount of players per squad. Will be ``-1`` if there is no limit. 100 | is_default: :class:`bool` 101 | Whether the playlist is the default one. 102 | is_tournament: :class:`bool` 103 | Whether this playlist is a tournament. 104 | is_limited_time_mode: :class:`bool` 105 | Whether this playlist is a limited time mode. 106 | is_large_team_game: :class:`bool` 107 | Whether this playlist is a large team game. 108 | accumulate_to_profile_stats: :class:`bool` 109 | Whether this playlist accumulates to profile stats. 110 | images: Optional[:class:`fortnite_api.PlaylistImages`] 111 | The images associated with the playlist. 112 | gameplay_tags: List[:class:`str`] 113 | The gameplay tags for the playlist. 114 | path: :class:`str` 115 | The path of the playlist. 116 | added: :class:`datetime.datetime` 117 | The time the playlist was added. 118 | """ 119 | 120 | __slots__: tuple[str, ...] = ( 121 | 'id', 122 | 'name', 123 | 'sub_name', 124 | 'description', 125 | 'game_type', 126 | 'min_players', 127 | 'max_players', 128 | 'max_teams', 129 | 'max_team_size', 130 | 'max_squads', 131 | 'max_squad_size', 132 | 'is_default', 133 | 'is_tournament', 134 | 'is_limited_time_mode', 135 | 'is_large_team_game', 136 | 'accumulate_to_profile_stats', 137 | 'images', 138 | 'gameplay_tags', 139 | 'path', 140 | 'added', 141 | ) 142 | 143 | def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: 144 | super().__init__(data=data, http=http) 145 | self.id: str = data['id'] 146 | self.name: str = data['name'] 147 | self.sub_name: Optional[str] = data.get('subName') 148 | self.description: Optional[str] = data.get('description') 149 | 150 | self.game_type: Optional[str] = data.get('gameType') # TODO: Make this into an enum 151 | self.rating_type: Optional[str] = data.get('ratingType') 152 | 153 | self.min_players: int = data['minPlayers'] 154 | self.max_players: int = data['maxPlayers'] 155 | self.max_teams: int = data['maxTeams'] 156 | self.max_team_size: int = data['maxTeamSize'] 157 | self.max_squads: int = data['maxSquads'] 158 | self.max_squad_size: int = data['maxSquadSize'] 159 | 160 | self.is_default: bool = data['isDefault'] 161 | self.is_tournament: bool = data['isTournament'] 162 | self.is_limited_time_mode: bool = data['isLimitedTimeMode'] 163 | self.is_large_team_game: bool = data['isLargeTeamGame'] 164 | self.accumulate_to_profile_stats: bool = data['accumulateToProfileStats'] 165 | 166 | _images = data.get('images') 167 | self.images: Optional[PlaylistImages[HTTPClientT]] = _images and PlaylistImages(data=_images, http=http) 168 | 169 | self.gameplay_tags: list[str] = get_with_fallback(data, 'gameplayTags', list) 170 | self.path: str = data['path'] 171 | 172 | self.added: datetime.datetime = parse_time(data['added']) 173 | -------------------------------------------------------------------------------- /fortnite_api/proxies.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from collections.abc import Iterable, Iterator 28 | from typing import TYPE_CHECKING, Callable, Generic, SupportsIndex, Union, cast, overload 29 | 30 | from typing_extensions import Self, TypeVar 31 | 32 | T = TypeVar('T') 33 | K_co = TypeVar('K_co', covariant=True, default='str') 34 | V_co = TypeVar('V_co', covariant=True, default='Any') 35 | 36 | if TYPE_CHECKING: 37 | from typing import Any 38 | 39 | 40 | class TransformerListProxy(Generic[T, K_co, V_co], list[T]): 41 | """ 42 | .. attributetable:: fortnite_api.proxies.TransformerListProxy 43 | 44 | A proxy for a list that allows for half-created type T objects to be stored in the list. This is an internal 45 | optimization that allows objects to be created only as-needed when the list is accessed. 46 | 47 | This class has been exposed to the documentation because it may be useful for some high level users. However, you 48 | can consider this as just a ``List[T]`` unless needed otherwise. 49 | 50 | It is important to note that this class is not thread-safe, so it should not be accessed 51 | concurrently from multiple threads. If you wish to access this class concurrently, you 52 | should use a lock to ensure that only one thread is accessing the class at a time. 53 | 54 | This class holds the invariant that when any public method is called, the list will be transformed into a list of 55 | type T. This means that when the list is accessed, the data will be transformed into the correct type. This is done 56 | to ensure that the data is always in a consistent state. 57 | """ 58 | 59 | def __init__(self, raw_data: Iterable[dict[K_co, V_co]], /, transform_data: Callable[[dict[K_co, V_co]], T]) -> None: 60 | self._transform_data: Callable[[dict[K_co, V_co]], T] = transform_data 61 | super().__init__(cast(list[T], raw_data)) 62 | 63 | def _transform_at(self, index: SupportsIndex) -> T: 64 | # Transforms the data at the index. 65 | data = super().__getitem__(index) 66 | if isinstance(data, dict): 67 | # Narrow the type of data to Dict[str, Any] 68 | raw_data: dict[K_co, V_co] = data 69 | result = self._transform_data(raw_data) 70 | super().__setitem__(index, result) 71 | else: 72 | result = data 73 | 74 | return result 75 | 76 | def _transform_all(self): 77 | for index, entry in enumerate(self): 78 | if isinstance(entry, dict): 79 | raw_data: dict[K_co, V_co] = entry 80 | result = self._transform_data(raw_data) 81 | super().__setitem__(index, result) 82 | 83 | def transform_all(self) -> Self: 84 | """A method that transforms all the data in the list to type ``T``.""" 85 | self._transform_all() 86 | return self 87 | 88 | # Allows for indexing of the list. 89 | @overload 90 | def __getitem__(self, index: SupportsIndex) -> T: ... 91 | 92 | @overload 93 | def __getitem__(self, index: slice) -> list[T]: ... 94 | 95 | def __getitem__(self, index: Union[SupportsIndex, slice]) -> Union[list[T], T]: 96 | if isinstance(index, slice): 97 | # This is a slice, so we need to handle each item in the slice and set it to the transformed data. 98 | # For each index in the slice, transform the data at that index then update the item list 99 | # with the transformed data. 100 | for i in range(*index.indices(len(self))): 101 | self._transform_at(i) 102 | 103 | return super().__getitem__(index) 104 | 105 | assert isinstance(index, SupportsIndex) 106 | return self._transform_at(index) 107 | 108 | def __contains__(self, key: object) -> bool: 109 | # If the user is looking for contains, we need to transform all the data so this is correct 110 | self._transform_all() 111 | return super().__contains__(key) 112 | 113 | def __reversed__(self) -> Iterator[T]: 114 | # Calling reversed will transform this list to a new list of type T 115 | # This means we need to transform all the data beforehand. 116 | self._transform_all() 117 | return super().__reversed__() 118 | 119 | # For all the comparison methods, we need to transform all the data so that the comparison is correct. 120 | def __gt__(self, value: list[T]) -> bool: 121 | self._transform_all() 122 | return super().__gt__(value) 123 | 124 | def __ge__(self, value: list[T]) -> bool: 125 | self._transform_all() 126 | return super().__ge__(value) 127 | 128 | def __lt__(self, value: list[T]) -> bool: 129 | self._transform_all() 130 | return super().__lt__(value) 131 | 132 | def __le__(self, value: list[T]) -> bool: 133 | self._transform_all() 134 | return super().__le__(value) 135 | 136 | def __eq__(self, value: object) -> bool: 137 | self._transform_all() 138 | return super().__eq__(value) 139 | 140 | def __ne__(self, value: object) -> bool: 141 | self._transform_all() 142 | return super().__ne__(value) 143 | 144 | def __iter__(self) -> Iterator[T]: 145 | for index in range(super().__len__()): 146 | yield self._transform_at(index) 147 | -------------------------------------------------------------------------------- /fortnite_api/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fortnite-API/py-wrapper/8f40946c71e8ef8db085add0b2ce8b66a92174e6/fortnite_api/py.typed -------------------------------------------------------------------------------- /fortnite_api/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019 Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | import datetime 28 | from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union 29 | 30 | K_co = TypeVar('K_co', bound='Hashable', covariant=True) 31 | V_co = TypeVar('V_co', covariant=True) 32 | T = TypeVar('T') 33 | 34 | if TYPE_CHECKING: 35 | from collections.abc import Hashable 36 | 37 | try: 38 | import orjson # type: ignore 39 | 40 | _has_orjson: bool = True 41 | except ImportError: 42 | import json 43 | 44 | _has_orjson: bool = False 45 | 46 | __all__: tuple[str, ...] = ('parse_time', 'copy_doc', 'prepend_doc', 'to_json', 'MISSING') 47 | 48 | BACKUP_TIMESTAMP: str = '0001-01-01T00:00:00' 49 | 50 | 51 | class _MissingSentinel: 52 | __slots__ = () 53 | 54 | def __eq__(self, other: Any) -> bool: 55 | return False 56 | 57 | def __bool__(self) -> bool: 58 | return False 59 | 60 | def __hash__(self) -> int: 61 | return 0 62 | 63 | def __repr__(self): 64 | return '...' 65 | 66 | 67 | MISSING: Any = _MissingSentinel() 68 | 69 | 70 | if _has_orjson: 71 | 72 | def to_json(string: Union[str, bytes]) -> dict[Any, Any]: 73 | return orjson.loads(string) # type: ignore 74 | 75 | else: 76 | 77 | def to_json(string: Union[str, bytes]) -> dict[Any, Any]: 78 | return json.loads(string) # type: ignore 79 | 80 | 81 | def parse_time(timestamp: str) -> datetime.datetime: 82 | # This can happen when the API is supposed to return a timestamp but there is no timestamp to give, so it yields an improper timestamp without a UTC offset. 83 | if timestamp == BACKUP_TIMESTAMP: 84 | return datetime.datetime.fromisoformat(timestamp).replace(tzinfo=datetime.timezone.utc) 85 | 86 | # If the timestamp str contains ms or us, strptime with them. If not, fallback 87 | # to default strptime. 88 | try: 89 | return datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%f%z') 90 | except ValueError: 91 | return datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S%z') 92 | 93 | 94 | def now() -> datetime.datetime: 95 | # Returns the current time in the same format as the API 96 | return datetime.datetime.now(datetime.timezone.utc) 97 | 98 | 99 | def copy_doc(obj: Any) -> Callable[[T], T]: 100 | """Copy the docstring from another object""" 101 | 102 | def wrapped(funco: T) -> T: 103 | if obj.__doc__: 104 | funco.__doc__ = obj.__doc__ 105 | 106 | return funco 107 | 108 | return wrapped 109 | 110 | 111 | def prepend_doc(obj: Any, sep: str = '') -> Callable[[T], T]: 112 | """A decorator used to prepend a docstring onto another object. 113 | 114 | .. code-block:: python3 115 | 116 | @prepend_doc(discord.Embed) 117 | def foo(self, *args, **kwargs): 118 | '''This is a doc string''' 119 | 120 | print(foo.__doc__) 121 | >>> '<>[sep]This is a doc string' 122 | """ 123 | 124 | def wrapped(funco: T) -> T: 125 | if funco.__doc__ and obj.__doc__: 126 | funco.__doc__ = f'{obj.__doc__}{sep}{funco.__doc__}' 127 | elif funco.__doc__: 128 | funco.__doc__ = f'{sep}{funco.__doc__}' 129 | 130 | return funco 131 | 132 | return wrapped 133 | 134 | 135 | def remove_prefix(text: str) -> Callable[[T], T]: 136 | """A decorator used to remove a prefix from a docstring. 137 | 138 | .. code-block:: python3 139 | 140 | @remove_prefix('This is a doc string') 141 | def foo(self, *args, **kwargs): 142 | '''This is a doc string''' 143 | 144 | print(foo.__doc__) 145 | >>> 'This is a doc string' 146 | """ 147 | 148 | def wrapped(funco: T) -> T: 149 | if funco.__doc__: 150 | funco.__doc__ = funco.__doc__.replace(text, '').strip() 151 | 152 | return funco 153 | 154 | return wrapped 155 | 156 | 157 | def simple_repr(cls: type[T]) -> type[T]: 158 | # If this cls does not have __slots__, return it as is 159 | try: 160 | slots: list[str] = list(getattr(cls, '__slots__')) 161 | except AttributeError: 162 | return cls 163 | 164 | # Walk through all parents, if they gave slots as well, append them to the slots 165 | for parent in cls.__bases__: 166 | try: 167 | slots.extend(getattr(parent, '__slots__')) 168 | except AttributeError: 169 | pass 170 | 171 | # If the cls has __slots__, append the __repr__ method to it using the slots as what to show 172 | def __repr__(self: T) -> str: 173 | attrs = ', '.join(f'{attr}={getattr(self, attr)!r}' for attr in slots if not attr.startswith('_')) 174 | return f'<{cls.__name__} {attrs}>' 175 | 176 | setattr(cls, '__repr__', __repr__) 177 | 178 | return cls 179 | 180 | 181 | def get_with_fallback(dict: dict[K_co, V_co], key: K_co, default_factory: Callable[[], V_co]) -> V_co: 182 | result = dict.get(key, MISSING) 183 | if result is MISSING: 184 | # Use the default factory 185 | return default_factory() 186 | 187 | if not result: 188 | # Use the default factory 189 | return default_factory() 190 | 191 | return result 192 | 193 | 194 | # A function name that transform some large dict into something that can be used in a get 195 | # request as a payload (so turns into camelCase from snake case, and transforms booleans into strings) 196 | def _transform_dict_for_get_request(data: dict[str, Any]) -> dict[str, Any]: 197 | updated = data.copy() 198 | for key, value in updated.items(): 199 | if isinstance(value, bool): 200 | updated[key] = str(value).lower() 201 | 202 | elif isinstance(value, dict): 203 | inner: dict[str, Any] = value # narrow the dict type to pass it along (should always be [str, Any]) 204 | updated[key] = _transform_dict_for_get_request(inner) 205 | 206 | if '_' in key: 207 | # Need to transform this to camelCase, so anything that is after "_" will be capitalized 208 | parts = key.split('_') 209 | updated[''.join(parts[0] + part.capitalize() for part in parts[1:])] = updated.pop(key) 210 | 211 | return updated 212 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | # Load setuptools info and dynamic version 6 | 7 | [tool.setuptools] 8 | packages = [ 9 | "fortnite_api", 10 | "fortnite_api.cosmetics", 11 | "fortnite_api.cosmetics.variants", 12 | ] 13 | 14 | [tool.setuptools.dynamic] 15 | version = { attr = "fortnite_api.__version__" } 16 | dependencies = { file = "requirements.txt" } 17 | 18 | [tool.setuptools.package-data] 19 | fortnite_api = ["py.typed"] 20 | 21 | # And specify project specific metadata 22 | 23 | [project] 24 | name = "fortnite-api" 25 | dynamic = ["version", "dependencies"] 26 | authors = [ 27 | { name = "Luc1412", email = 'Luc1412.lh@gmail.com' }, 28 | { name = "Trevor Flahardy", email = "trevorflahardy@gmail.com" }, 29 | ] 30 | description = "A python wrapper for Fortnite-API.com" 31 | readme = "README.md" 32 | requires-python = ">=3.9.0" 33 | license = { file = "LICENSE" } 34 | keywords = [ 35 | 'fortnite', 36 | 'fortnite-api.com', 37 | 'shop', 38 | 'cosmetics', 39 | 'fortnite api', 40 | 'fortnite shop', 41 | ] 42 | classifiers = [ 43 | 'License :: OSI Approved :: MIT License', 44 | 'Intended Audience :: Developers', 45 | 'Natural Language :: English', 46 | 'Operating System :: OS Independent', 47 | 'Programming Language :: Python :: 3.9', 48 | 'Programming Language :: Python :: 3.10', 49 | 'Programming Language :: Python :: 3.11', 50 | 'Programming Language :: Python :: 3.12', 51 | 'Programming Language :: Python :: 3.13', 52 | 'Topic :: Internet', 53 | 'Topic :: Software Development :: Libraries', 54 | 'Topic :: Software Development :: Libraries :: Python Modules', 55 | 'Topic :: Utilities', 56 | ] 57 | 58 | 59 | [project.optional-dependencies] 60 | tests = [ 61 | 'pytest', 62 | 'pytest-asyncio', 63 | 'pytest-cov', 64 | 'python-dotenv', 65 | 'pytest-mock', 66 | ] 67 | docs = [ 68 | 'sphinx', 69 | 'sphinxcontrib_trio', 70 | 'sphinxcontrib-websupport', 71 | 'typing-extensions', 72 | 'furo', 73 | 'sphinx-copybutton', 74 | ] 75 | dev = ['black', 'isort', 'discord.py', 'pyright', 'pre-commit'] 76 | speed = ['orjson'] 77 | 78 | [project.urls] 79 | "Home Page" = "https://github.com/Fortnite-API/py-wrapper" 80 | Issues = "https://github.com/Fortnite-API/py-wrapper/issues" 81 | Documentation = "https://fortnite-api.readthedocs.io/en/rewrite/" 82 | 83 | # Pytest configuration 84 | 85 | [tool.pytest.ini_options] 86 | asyncio_mode = "strict" 87 | testpaths = ["tests"] 88 | addopts = "--import-mode=importlib" 89 | 90 | # Black formatting 91 | 92 | [tool.black] 93 | line-length = 125 94 | skip-string-normalization = true 95 | force-exclude = "LICENSE|requirements.txt|pyproject.toml|README.md" 96 | 97 | # Pyright configuration 98 | 99 | [tool.pyright] 100 | typeCheckingMode = "strict" 101 | reportUnnecessaryTypeIgnoreComment = "error" 102 | reportUnusedImport = "error" 103 | pythonVersion = "3.10" 104 | reportPrivateUsage = "none" 105 | exclude = ["**/__pycache__", "build", "dist", "docs"] 106 | include = ["fortnite_api/", "tests/", "examples/"] 107 | 108 | # Isort configuration 109 | 110 | [tool.isort] 111 | profile = "black" 112 | combine_as_imports = true 113 | combine_star = true 114 | line_length = 125 115 | src_paths = ["fortnite_api/", "tests/", "examples/"] 116 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests~=2.22 2 | aiohttp~=3.9 3 | typing_extensions -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | import os 28 | from typing import Final 29 | 30 | import pytest 31 | 32 | from fortnite_api.flags import ResponseFlags 33 | from fortnite_api.http import HTTPClient, SyncHTTPClient 34 | 35 | # Constants for general testing 36 | TEST_ACCOUNT_ID: Final[str] = "4735ce9132924caf8a5b17789b40f79c" 37 | TEST_ACCOUNT_NAME: Final[str] = "Ninja" 38 | TEST_CREATOR_CODE: Final[str] = "ninja" 39 | TEST_INVALID_CREATOR_CODE: Final[str] = "invalidcreatorcode" 40 | 41 | # Constant for testing cosmetics 42 | TEST_COSMETIC_ID: Final[str] = "Backpack_BrakePedal" 43 | TEST_INVALID_COSMETIC_ID: Final[str] = "Invalid" 44 | 45 | # Constants for testing playlist fetching 46 | TEST_PLAYLIST_ID: Final[str] = "Playlist_NoBuildBR_Duo" 47 | TEST_INVALID_PLAYLIST_ID: Final[str] = "Invalid" 48 | 49 | # Constants for fetching stats 50 | TEST_STAT_ACCOUNT_NAME = "Luc1412" 51 | TEST_INVALID_STAT_ACCOUNT_NAME = "InvalidAccountName" 52 | TEST_STAT_ACCOUNT_ID = "369644c6224d4845aa2b00e63b60241d" 53 | TEST_INVALID_STAT_ACCOUNT_ID = "21332424543544535435435" 54 | 55 | 56 | @pytest.fixture(scope='session') 57 | def api_key() -> str: 58 | # This fixture is called once per test session, so we can check if we are in a CI environment 59 | # or a local development environment. If we are in a CI environment, we can get the API key from 60 | # the environment variables, otherwise we can load it from a .env file. 61 | 62 | gh_actions = os.environ.get('GITHUB_ACTIONS') 63 | if gh_actions and gh_actions == 'true': 64 | return os.environ['TEST_API_KEY'] 65 | 66 | # This is a local development environment, try and load a .env file and get the API key 67 | from dotenv import load_dotenv 68 | 69 | load_dotenv() 70 | return os.environ['TEST_API_KEY'] 71 | 72 | 73 | @pytest.fixture(scope='session', params=[flag for flag in ResponseFlags]) 74 | def response_flags(request: pytest.FixtureRequest) -> ResponseFlags: 75 | # Returns all the possible flags that can be used in the client. This is to ensure that passing 76 | # flags to both the client and the methods that require them is consistent. 77 | return request.param 78 | 79 | 80 | @pytest.fixture(scope='session') 81 | def mock_sync_http() -> SyncHTTPClient: 82 | return SyncHTTPClient() 83 | 84 | 85 | @pytest.fixture(scope='session') 86 | def mock_async_http() -> HTTPClient: 87 | return HTTPClient() 88 | -------------------------------------------------------------------------------- /tests/test_account.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from typing import Any 28 | 29 | import pytest 30 | 31 | from fortnite_api import Account 32 | from fortnite_api.http import HTTPClient 33 | 34 | 35 | @pytest.fixture 36 | def sample_account_data() -> dict[str, Any]: 37 | return { 38 | 'id': '123', 39 | 'name': 'Test Account', 40 | } 41 | 42 | 43 | def test_account_initialization(sample_account_data: dict[str, Any]): 44 | account = Account(data=sample_account_data, http=HTTPClient()) 45 | 46 | assert account.id == '123' 47 | assert account.name == 'Test Account' 48 | assert account.to_dict() == sample_account_data 49 | 50 | 51 | def test_account_str(sample_account_data: dict[str, Any]): 52 | account = Account(data=sample_account_data, http=HTTPClient()) 53 | 54 | assert str(account) == 'Test Account' 55 | 56 | 57 | def test_account_repr(sample_account_data: dict[str, Any]): 58 | account = Account(data=sample_account_data, http=HTTPClient()) 59 | 60 | assert repr(account) == "" 61 | -------------------------------------------------------------------------------- /tests/test_aes.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from datetime import datetime, timezone 28 | from typing import Any 29 | 30 | import pytest 31 | 32 | from fortnite_api.aes import Aes, Version 33 | from fortnite_api.http import SyncHTTPClient 34 | 35 | 36 | @pytest.fixture 37 | def sample_aes_data() -> dict[str, Any]: 38 | return { 39 | 'mainKey': 'test_main_key', 40 | 'build': '++Fortnite+Release-29.10-CL-32567225-Windows', 41 | 'updated': '2022-01-01T00:00:00Z', 42 | 'dynamicKeys': [ 43 | { 44 | 'pakFilename': 'pak1', 45 | 'pakGuid': 'guid1', 46 | 'key': 'key1', 47 | } 48 | ], 49 | } 50 | 51 | 52 | def test_aes_initialization(sample_aes_data: dict[str, Any], mock_sync_http: SyncHTTPClient): 53 | aes = Aes(data=sample_aes_data, http=mock_sync_http) 54 | 55 | assert aes.main_key == 'test_main_key' 56 | assert aes.build == '++Fortnite+Release-29.10-CL-32567225-Windows' 57 | assert aes.version == Version(29, 10) 58 | assert aes.updated == datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc) 59 | assert len(aes.dynamic_keys) == 1 60 | assert aes.dynamic_keys[0].pak_filename == 'pak1' 61 | assert aes.dynamic_keys[0].pak_guid == 'guid1' 62 | assert aes.dynamic_keys[0].key == 'key1' 63 | assert aes.to_dict() == sample_aes_data 64 | 65 | 66 | def test_aes_equality(sample_aes_data: dict[str, Any], mock_sync_http: SyncHTTPClient): 67 | aes1 = Aes(data=sample_aes_data, http=mock_sync_http) 68 | aes2 = Aes(data=sample_aes_data, http=mock_sync_http) 69 | 70 | assert aes1 == aes2 71 | 72 | aes1.main_key = 'different_main_key' 73 | assert aes1 != aes2 74 | -------------------------------------------------------------------------------- /tests/test_asset.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | import pytest 28 | 29 | import fortnite_api 30 | 31 | V_BUCK_ICON_URL: str = "https://fortnite-api.com/images/vbuck.png" 32 | 33 | 34 | def test_sync_asset_reading(): 35 | with fortnite_api.SyncClient() as client: 36 | 37 | mock_asset = fortnite_api.Asset(http=client.http, url=V_BUCK_ICON_URL) 38 | 39 | # Read the asset and ensure it is bytes 40 | read = mock_asset.read() 41 | assert isinstance(read, bytes) 42 | 43 | 44 | @pytest.mark.asyncio 45 | async def test_async_asset_reading(): 46 | async with fortnite_api.Client() as client: 47 | 48 | mock_asset = fortnite_api.Asset(http=client.http, url=V_BUCK_ICON_URL) 49 | 50 | # Read the asset and ensure it is bytes 51 | read = await mock_asset.read() 52 | assert isinstance(read, bytes) 53 | 54 | 55 | def test_asset(): 56 | with fortnite_api.SyncClient() as client: 57 | 58 | mock_asset = fortnite_api.Asset(http=client.http, url=V_BUCK_ICON_URL) 59 | 60 | assert mock_asset.url == V_BUCK_ICON_URL 61 | assert mock_asset.can_resize is False 62 | assert mock_asset._max_size is fortnite_api.utils.MISSING 63 | assert mock_asset.max_size == -1 64 | 65 | with pytest.raises(ValueError): 66 | mock_asset.resize(256) 67 | 68 | mock_asset._max_size = 256 69 | assert mock_asset.can_resize is True 70 | assert mock_asset._max_size == 256 71 | assert mock_asset.max_size == 256 72 | 73 | # Asset that you cannot resize to something that isn't a power of 2 74 | with pytest.raises(ValueError): 75 | mock_asset.resize(255) 76 | 77 | # Ensure that you cannot resize to something bigger than the max size 78 | with pytest.raises(ValueError): 79 | mock_asset.resize(512) 80 | 81 | assert mock_asset._size is fortnite_api.utils.MISSING 82 | 83 | # Resize this asset and ensure it copies right 84 | resized = mock_asset.resize(8) 85 | assert resized is mock_asset 86 | assert resized._max_size == mock_asset._max_size 87 | assert resized.max_size == mock_asset.max_size 88 | assert resized._size == 8 89 | assert resized.url == f"{V_BUCK_ICON_URL[:-4]}_8.png" 90 | 91 | # Now finally make sure that if the max size is None, you can resize to anything 92 | mock_asset._max_size = None 93 | assert mock_asset.can_resize is True 94 | assert mock_asset._max_size is None 95 | assert mock_asset.max_size is None 96 | -------------------------------------------------------------------------------- /tests/test_beta.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | import pytest 28 | 29 | import fortnite_api 30 | from fortnite_api.client import beta_method 31 | 32 | 33 | def test_sync_cannot_call_beta_method(): 34 | client = fortnite_api.SyncClient(beta=False) 35 | with client, pytest.raises(fortnite_api.BetaAccessNotEnabled): 36 | client.beta_fetch_new_display_assets() 37 | 38 | 39 | @pytest.mark.asyncio 40 | async def test_async_cannot_call_beta_method(): 41 | client = fortnite_api.Client(beta=False) 42 | with pytest.raises(fortnite_api.BetaAccessNotEnabled): 43 | async with client: 44 | await client.beta_fetch_new_display_assets() 45 | 46 | 47 | # A mock of the SyncClient beta function that raises an error 48 | class MockSyncFortniteAPI(fortnite_api.SyncClient): 49 | 50 | @beta_method 51 | def beta_mock_call(self): 52 | raise ValueError('Mock error') 53 | 54 | 55 | # A mock of the Client beta function that raises an error 56 | class MockFortniteAPI(fortnite_api.Client): 57 | 58 | @beta_method 59 | async def beta_mock_call(self): 60 | raise ValueError('Mock error') 61 | 62 | 63 | def test_sync_beta_method_error(): 64 | client = MockSyncFortniteAPI(beta=True) 65 | with client, pytest.raises(fortnite_api.BetaUnknownException) as exc_info: 66 | client.beta_mock_call() 67 | 68 | # Ensure the exception.__cause__ is the original exception 69 | assert exc_info.value.__cause__ is not None 70 | assert isinstance(exc_info.value.__cause__, ValueError) 71 | 72 | 73 | @pytest.mark.asyncio 74 | async def test_async_beta_method_error(): 75 | client = MockFortniteAPI(beta=True) 76 | with pytest.raises(fortnite_api.BetaUnknownException) as exc_info: 77 | async with client: 78 | await client.beta_mock_call() 79 | 80 | # Ensure the exception.__cause__ is the original exception 81 | assert exc_info.value.__cause__ is not None 82 | assert isinstance(exc_info.value.__cause__, ValueError) 83 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | import inspect 28 | 29 | import aiohttp 30 | import pytest 31 | import requests 32 | 33 | import fortnite_api as fn_api 34 | 35 | 36 | def test_sync_client_initialization(): 37 | with requests.Session() as session, fn_api.SyncClient(session=session) as client: 38 | assert client 39 | 40 | with fn_api.SyncClient() as client: 41 | assert client 42 | 43 | 44 | @pytest.mark.asyncio 45 | async def test_async_client_initialization(): 46 | async with aiohttp.ClientSession() as session, fn_api.Client(session=session) as client: 47 | assert client 48 | 49 | assert session.closed == True, "Session should be closed after client is closed" 50 | 51 | async with fn_api.Client() as client: 52 | assert client 53 | 54 | client_session = client.http.session 55 | assert client_session and client_session.closed 56 | 57 | 58 | # A test to ensure that all the methods on async and sync clients are the same. 59 | # The async client has all the main methods, so we'll walk through the async client. 60 | def test_client_method_equivalence(): 61 | for method in fn_api.Client.__dict__.values(): 62 | try: 63 | doc = getattr(method, '__doc__') 64 | except AttributeError: 65 | continue 66 | else: 67 | if doc and inspect.iscoroutinefunction(method): 68 | # This is some documented coroutine function, ensure it's on the sync client 69 | assert hasattr(fn_api.SyncClient, method.__name__) 70 | 71 | 72 | @pytest.mark.asyncio 73 | async def test_async_client_without_content_manager(): 74 | session = aiohttp.ClientSession() 75 | client = fn_api.Client(session=session) 76 | assert client 77 | assert client.http.session is not None 78 | 79 | # Ensure we can make a request 80 | await client.fetch_aes() 81 | 82 | await session.close() 83 | assert client.http.session.closed 84 | 85 | # Ensure we can't make a request after closing the session 86 | with pytest.raises(RuntimeError): 87 | await client.fetch_aes() 88 | 89 | 90 | def test_sync_client_without_content_manager(): 91 | session = requests.Session() 92 | client = fn_api.SyncClient(session=session) 93 | assert client 94 | assert client.http.session is not None 95 | 96 | # Ensure we can make a request 97 | client.fetch_aes() 98 | 99 | # Requests Session close doesn't actually close the session, so we'll just close it and 100 | # assume it's closed. 101 | session.close() 102 | -------------------------------------------------------------------------------- /tests/test_enum.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | import pytest 26 | 27 | from fortnite_api.enums import Enum, try_enum 28 | 29 | 30 | class DummyEnum(Enum): 31 | FOO = "foo" 32 | BAR = "bar" 33 | BAZ = "baz" 34 | 35 | 36 | def test_dummy_enum(): 37 | # Test basic enum functionality 38 | assert len(DummyEnum) == 3 39 | assert list(DummyEnum) == [DummyEnum.FOO, DummyEnum.BAR, DummyEnum.BAZ] 40 | assert list(reversed(DummyEnum)) == [DummyEnum.BAZ, DummyEnum.BAR, DummyEnum.FOO] 41 | 42 | # Test enum member access 43 | assert DummyEnum.FOO.name == "FOO" 44 | assert DummyEnum.FOO.value == "foo" 45 | assert DummyEnum["FOO"] == DummyEnum.FOO 46 | assert DummyEnum("foo") == DummyEnum.FOO 47 | 48 | # Test immutability 49 | with pytest.raises(TypeError): 50 | DummyEnum.FOO = "new" 51 | with pytest.raises(TypeError): 52 | del DummyEnum.FOO 53 | 54 | # Test try_enum functionality 55 | valid_value = "foo" 56 | invalid_value = "invalid" 57 | 58 | valid_instance = try_enum(DummyEnum, valid_value) 59 | assert valid_instance == DummyEnum.FOO 60 | assert valid_instance.value == valid_value 61 | assert isinstance(valid_instance, DummyEnum) 62 | 63 | invalid_instance = try_enum(DummyEnum, invalid_value) 64 | assert invalid_instance.name == f"UNKNOWN_{invalid_value}" 65 | assert invalid_instance.value == invalid_value 66 | assert isinstance(invalid_instance, DummyEnum) 67 | 68 | # Test string representations 69 | assert str(DummyEnum.FOO) == "DummyEnum.FOO" 70 | assert repr(DummyEnum.FOO) == "" 71 | assert repr(DummyEnum) == "" 72 | 73 | # Test members property 74 | assert DummyEnum.__members__ == {"FOO": DummyEnum.FOO, "BAR": DummyEnum.BAR, "BAZ": DummyEnum.BAZ} 75 | -------------------------------------------------------------------------------- /tests/test_proxy.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | import dataclasses 28 | 29 | from fortnite_api.proxies import TransformerListProxy 30 | 31 | 32 | @dataclasses.dataclass(frozen=True, eq=True) 33 | class PlaceholderPerson: 34 | name: str 35 | 36 | 37 | def test_proxy(): 38 | # A random list of people 39 | people_raw_data: list[dict[str, str]] = [ 40 | dict(name="Daniel Maxwell"), 41 | dict(name="Francis Andrews"), 42 | dict(name="Stella Norton"), 43 | dict(name="Nathaniel Reeves"), 44 | ] 45 | 46 | people: list[PlaceholderPerson] = [PlaceholderPerson(**person) for person in people_raw_data] 47 | 48 | proxy = TransformerListProxy(people_raw_data, transform_data=lambda data: PlaceholderPerson(**data)) 49 | 50 | assert len(proxy) == len(people) 51 | 52 | first = people[0] 53 | assert first in people 54 | 55 | for i, person in enumerate(proxy): 56 | assert isinstance(person, PlaceholderPerson) 57 | assert isinstance(proxy[i], PlaceholderPerson) 58 | assert person == proxy[i] 59 | 60 | assert proxy[i] == person 61 | assert proxy[i] == people[i] 62 | 63 | # Ensure slicing is right 64 | assert people[1:3] == proxy[1:3] 65 | assert people[1:] == proxy[1:] 66 | 67 | # Slicing with step 68 | assert people[::2] == proxy[::2] 69 | 70 | # Adding 71 | assert people + people == proxy + proxy 72 | 73 | # Check that the proxy is reversible 74 | reversed_people = list(reversed(people)) 75 | for i, person in enumerate(reversed(proxy)): 76 | assert isinstance(person, PlaceholderPerson) 77 | assert isinstance(proxy[-i - 1], PlaceholderPerson) 78 | assert person == proxy[-i - 1] 79 | 80 | assert proxy[-i - 1] == person 81 | assert proxy[-i - 1] == reversed_people[i] 82 | -------------------------------------------------------------------------------- /tests/test_ratelimits.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from unittest.mock import AsyncMock, MagicMock 28 | 29 | import aiohttp 30 | import pytest 31 | from pytest_mock import MockerFixture 32 | 33 | from fortnite_api.errors import RateLimited 34 | from fortnite_api.http import HTTPClient, Route, SyncHTTPClient 35 | from fortnite_api.utils import now 36 | 37 | 38 | @pytest.fixture 39 | def async_mock_response(mocker: MockerFixture) -> MagicMock: 40 | mock_response = mocker.MagicMock() 41 | mock_response.status = 429 42 | mock_response.headers = { 43 | 'X-Ratelimit-Remaining': '0', 44 | 'Content-Type': 'application/json', 45 | 'X-Ratelimit-Reset': now().isoformat(timespec='milliseconds'), 46 | } 47 | mock_response.text = AsyncMock(return_value='{"data": {"error": "Rate limit exceeded."}, "status": "429"}') 48 | 49 | return mock_response 50 | 51 | 52 | @pytest.fixture 53 | def sync_mock_response(mocker: MockerFixture) -> MagicMock: 54 | # Mocks a requests response object 55 | mock_response = mocker.MagicMock() 56 | mock_response.status_code = 429 57 | mock_response.headers = { 58 | 'X-Ratelimit-Remaining': '0', 59 | 'Content-Type': 'application/json', 60 | 'X-Ratelimit-Reset': now().isoformat(timespec='milliseconds'), 61 | } 62 | mock_response.text = '{"data": {"error": "Rate limit exceeded."}, "status": "429"}' 63 | return mock_response 64 | 65 | 66 | @pytest.fixture 67 | def async_mock_session(mocker: MockerFixture, async_mock_response: MagicMock): 68 | mock_session = mocker.MagicMock() 69 | mock_session.request.return_value.__aenter__.return_value = async_mock_response 70 | 71 | return mock_session 72 | 73 | 74 | @pytest.fixture 75 | def sync_mock_session(mocker: MockerFixture, sync_mock_response: MagicMock): 76 | # Mocks the requests.Session object 77 | mock_session = mocker.MagicMock() 78 | mock_session.request.return_value.__enter__.return_value = sync_mock_response 79 | 80 | return mock_session 81 | 82 | 83 | @pytest.fixture 84 | def async_client(async_mock_session: aiohttp.ClientSession) -> HTTPClient: 85 | return HTTPClient(session=async_mock_session) 86 | 87 | 88 | @pytest.fixture 89 | def sync_client(sync_mock_session: MagicMock) -> SyncHTTPClient: 90 | return SyncHTTPClient(session=sync_mock_session) 91 | 92 | 93 | @pytest.mark.asyncio 94 | async def test_async_rate_limit_handling(async_client: HTTPClient): 95 | # Make a request 96 | route = Route('GET', 'https://example.com') 97 | with pytest.raises(RateLimited) as excinfo: 98 | 99 | # This will try 5 times to request, and each time get a 429 response. After it 100 | # should raise the RateLimited error. Any subsequent requests with the same route 101 | # should immediately raise the RateLimited error. 102 | await async_client.request(route) 103 | 104 | # Check that the rate limit error was raised 105 | assert excinfo.type is RateLimited 106 | 107 | # Assert that the client did in fact try 5 times to request using the mock session 108 | assert async_client.session.request.call_count == 5 # type: ignore 109 | 110 | 111 | def test_sync_rate_limit_handling(sync_client: SyncHTTPClient): 112 | route = Route('GET', 'https://example.com') 113 | with pytest.raises(RateLimited) as excinfo: 114 | # This will try 5 times to request, and each time get a 429 response. After it 115 | # should raise the RateLimited error. Any subsequent requests with the same route 116 | # should immediately raise the RateLimited error. 117 | sync_client.request(route) 118 | 119 | assert excinfo.type is RateLimited 120 | 121 | # Assert that the client did in fact try 5 times to request using the mock session 122 | assert sync_client.session.request.call_count == 5 # type: ignore 123 | -------------------------------------------------------------------------------- /tests/test_reconstruct.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | from typing import Any, TypedDict 28 | 29 | import pytest 30 | 31 | import fortnite_api 32 | from fortnite_api.abc import ReconstructAble 33 | from fortnite_api.http import HTTPClient 34 | 35 | # August 4th, 2024: The goal of this file is to ensure that the reconstruction methods are working as expected. 36 | # Although every object cannot be tested, the most important ones are tested here with the assumption 37 | # that if the overwhelming majority works then the rest should.. in theory... work as well. The 38 | # only way around this "bulk" method would be to write a test for every single object, and with 39 | # the current state of the library, that would be a bit overkill. 40 | # 41 | # If someone has the cojones to do that, then by all means, go ahead. 42 | 43 | 44 | @pytest.mark.asyncio 45 | async def test_reconstruct(api_key: str) -> None: 46 | methods_to_test: list[str] = [ 47 | 'fetch_cosmetics_all', 48 | 'fetch_cosmetics_br', 49 | 'fetch_cosmetics_cars', 50 | 'fetch_cosmetics_instruments', 51 | 'fetch_cosmetics_lego_kits', 52 | 'fetch_variants_lego', 53 | 'fetch_variants_beans', 54 | 'fetch_cosmetics_tracks', 55 | 'fetch_cosmetics_new', 56 | 'fetch_aes', 57 | 'fetch_banners', 58 | 'fetch_banner_colors', 59 | 'fetch_map', 60 | 'fetch_news', 61 | 'fetch_news_br', 62 | 'fetch_news_stw', 63 | 'fetch_playlists', 64 | 'fetch_shop', 65 | ] 66 | 67 | async with fortnite_api.Client(api_key=api_key) as client: 68 | for method in methods_to_test: 69 | # (1) grab the method from the client 70 | coro = getattr(client, method) 71 | 72 | # (2) call the method 73 | try: 74 | result = await coro() 75 | except fortnite_api.NotFound: 76 | continue 77 | 78 | # If this item is reconstruct-able, do some basic checks to ensure 79 | # that the reconstruction is working as expected. 80 | if isinstance(result, fortnite_api.abc.ReconstructAble): 81 | narrowed: fortnite_api.abc.ReconstructAble[dict[str, Any], HTTPClient] = result 82 | 83 | # (3) deconstruct the object 84 | deconstructed = narrowed.to_dict() 85 | 86 | # Recreate a new instance of said object 87 | reconstructed = narrowed.from_dict(deconstructed, client=client) 88 | 89 | # (4) check that the original object and the reconstructed object are the same 90 | # we can't always use __eq__ because not every object has it implemented 91 | assert deconstructed == reconstructed.to_dict() 92 | assert type(narrowed) == type(reconstructed) 93 | 94 | 95 | class DummyData(TypedDict): 96 | id: str 97 | 98 | 99 | class DummyReconstruct(ReconstructAble[DummyData, HTTPClient]): 100 | def __init__(self, *, data: DummyData, http: HTTPClient) -> None: 101 | super().__init__(data=data, http=http) 102 | self.id: str = data['id'] 103 | 104 | def __eq__(self, value: object) -> bool: 105 | return isinstance(value, DummyReconstruct) and self.id == value.id 106 | 107 | def __ne__(self, value: object) -> bool: 108 | return not self.__eq__(value) 109 | 110 | 111 | def test_dummy_reconstruction() -> None: 112 | data: DummyData = {'id': '1'} 113 | 114 | client = fortnite_api.Client() 115 | http: HTTPClient = client.http 116 | dummy = DummyReconstruct(data=data, http=http) 117 | 118 | deconstructed = dummy.to_dict() 119 | reconstructed = DummyReconstruct.from_dict(deconstructed, client=client) 120 | 121 | assert dummy == reconstructed 122 | assert dummy.to_dict() == reconstructed.to_dict() 123 | assert dummy.to_dict() == deconstructed 124 | assert type(dummy) == type(reconstructed) 125 | assert isinstance(reconstructed, DummyReconstruct) 126 | -------------------------------------------------------------------------------- /tests/test_repr.py: -------------------------------------------------------------------------------- 1 | from fortnite_api.utils import simple_repr 2 | 3 | 4 | @simple_repr 5 | class Foo: 6 | __slots__: tuple[str, ...] = ('foo', 'bar', 'baz') 7 | 8 | def __init__(self, foo: int, bar: int, baz: int) -> None: 9 | self.foo = foo 10 | self.bar = bar 11 | self.baz = baz 12 | 13 | 14 | @simple_repr 15 | class Bar(Foo): 16 | __slots__: tuple[str, ...] = ('buz',) 17 | 18 | def __init__(self, foo: int, bar: int, baz: int, buz: int) -> None: 19 | super().__init__(foo, bar, baz) 20 | self.buz: int = buz 21 | 22 | 23 | def test_simple_repr() -> None: 24 | foo = Foo(1, 2, 3) 25 | 26 | assert getattr(foo, '__repr__')() == '' 27 | 28 | 29 | def test_simple_repr_inheritance() -> None: 30 | bar = Bar(1, 2, 3, 4) 31 | 32 | assert getattr(bar, '__repr__')() == '' 33 | -------------------------------------------------------------------------------- /tests/test_stats.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2019-present Luc1412 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | import datetime 28 | from typing import Any 29 | 30 | import pytest 31 | 32 | import fortnite_api 33 | 34 | from .conftest import ( 35 | TEST_INVALID_STAT_ACCOUNT_ID, 36 | TEST_INVALID_STAT_ACCOUNT_NAME, 37 | TEST_STAT_ACCOUNT_ID, 38 | TEST_STAT_ACCOUNT_NAME, 39 | ) 40 | 41 | 42 | def _test_stats(player_stats: fortnite_api.BrPlayerStats[Any]) -> None: 43 | assert player_stats.user 44 | 45 | if player_stats.battle_pass: 46 | assert isinstance(player_stats.battle_pass.level, int) 47 | assert isinstance(player_stats.battle_pass.progress, int) 48 | 49 | stats = player_stats.inputs 50 | overall_stats = stats and stats.all and stats.all.overall 51 | if not overall_stats: 52 | # Nothing we should be testing 53 | return 54 | 55 | assert isinstance(overall_stats, fortnite_api.BrGameModeStats) 56 | assert isinstance(overall_stats.score, int) 57 | assert isinstance(overall_stats.score_per_min, float) 58 | assert isinstance(overall_stats.score_per_match, float) 59 | assert isinstance(overall_stats.wins, int) 60 | 61 | # This is overall stats, so these topX should all be not None 62 | assert isinstance(overall_stats.top3, int) 63 | assert isinstance(overall_stats.top5, int) 64 | assert isinstance(overall_stats.top6, int) 65 | assert isinstance(overall_stats.top10, int) 66 | assert isinstance(overall_stats.top12, int) 67 | assert isinstance(overall_stats.top25, int) 68 | 69 | assert isinstance(overall_stats.kills, int) 70 | assert isinstance(overall_stats.kills_per_min, float) 71 | assert isinstance(overall_stats.kills_per_match, float) 72 | 73 | assert isinstance(overall_stats.deaths, int) 74 | assert isinstance(overall_stats.kd, float) 75 | assert isinstance(overall_stats.win_rate, float) 76 | assert isinstance(overall_stats.minutes_played, int) 77 | assert isinstance(overall_stats.players_outlived, int) 78 | assert isinstance(overall_stats.last_modified, datetime.datetime) 79 | 80 | 81 | @pytest.mark.asyncio 82 | async def test_async_fetch_br_stats_by_name(api_key: str): 83 | async with fortnite_api.Client(api_key=api_key) as client: 84 | with pytest.raises(fortnite_api.NotFound): 85 | await client.fetch_br_stats(name=TEST_INVALID_STAT_ACCOUNT_NAME) 86 | stats = await client.fetch_br_stats(name=TEST_STAT_ACCOUNT_NAME, image=fortnite_api.StatsImageType.ALL) 87 | 88 | assert stats is not None 89 | _test_stats(stats) 90 | 91 | 92 | @pytest.mark.asyncio 93 | async def test_async_fetch_br_stats_by_account_id(api_key: str): 94 | async with fortnite_api.Client(api_key=api_key) as client: 95 | with pytest.raises(fortnite_api.NotFound): 96 | await client.fetch_br_stats(name=TEST_INVALID_STAT_ACCOUNT_ID) 97 | stats = await client.fetch_br_stats(account_id=TEST_STAT_ACCOUNT_ID, image=fortnite_api.StatsImageType.ALL) 98 | 99 | assert stats is not None 100 | _test_stats(stats) 101 | 102 | 103 | def test_sync_fetch_br_stats_by_name(api_key: str): 104 | with fortnite_api.SyncClient(api_key=api_key) as client: 105 | with pytest.raises(fortnite_api.NotFound): 106 | client.fetch_br_stats(name=TEST_INVALID_STAT_ACCOUNT_NAME) 107 | stats = client.fetch_br_stats(name=TEST_STAT_ACCOUNT_NAME, image=fortnite_api.StatsImageType.ALL) 108 | 109 | assert stats is not None 110 | _test_stats(stats) 111 | 112 | 113 | def test_sync_fetch_br_stats_by_account_id(api_key: str): 114 | with fortnite_api.SyncClient(api_key=api_key) as client: 115 | with pytest.raises(fortnite_api.NotFound): 116 | client.fetch_br_stats(name=TEST_INVALID_STAT_ACCOUNT_ID) 117 | stats = client.fetch_br_stats(account_id=TEST_STAT_ACCOUNT_ID, image=fortnite_api.StatsImageType.ALL) 118 | 119 | assert stats is not None 120 | _test_stats(stats) 121 | --------------------------------------------------------------------------------