├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── README.md ├── docs ├── Makefile ├── make.bat └── source │ ├── api.rst │ ├── conf.py │ ├── index.rst │ ├── intro.rst │ └── models.rst ├── epicstore_api ├── __init__.py ├── api.py ├── exc.py ├── models │ ├── __init__.py │ ├── categories.py │ ├── collection_types.py │ └── product_types.py └── queries.py ├── examples ├── free_games_example.py ├── get_store_games_example.py ├── get_top_sellers.py └── simple_example.py ├── requirements.txt ├── requirements_dev.txt ├── setup.py └── tests └── test_api.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Docs builded 2 | docs/build/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.12" 7 | 8 | sphinx: 9 | configuration: docs/source/conf.py 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SD4RK 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # epicstore_api 2 | 3 | [![Current pypi version](https://img.shields.io/pypi/v/epicstore-api.svg)](https://pypi.org/project/epicstore-api/) 4 | [![Supported py versions](https://img.shields.io/pypi/pyversions/epicstore-api.svg)](https://pypi.org/project/epicstore-api/) 5 | [![Downloads](https://pepy.tech/badge/epicstore-api)](https://pypi.org/project/epicstore-api/) 6 | 7 | An unofficial library to work with Epic Games Store Web API. 8 | 9 | ## Installing 10 | 11 | **Python 3.6 or higher is required** 12 | 13 | To install the library you can just run the following command: 14 | 15 | ``` sh 16 | # Linux/macOS 17 | python3 -m pip install -U epicstore_api 18 | 19 | # Windows 20 | py -3 -m pip install -U epicstore_api 21 | ``` 22 | 23 | 24 | ### Quick Example 25 | 26 | ``` py 27 | api = EpicGamesStoreAPI() 28 | namespace, slug = next(iter(api.get_product_mapping().items())) 29 | first_product = api.get_product(slug) 30 | offers = [ 31 | OfferData(page['namespace'], page['offer']['id']) 32 | for page in first_product['pages'] 33 | if page.get('offer') and 'id' in page['offer'] 34 | ] 35 | offers_data = api.get_offers_data(*offers) 36 | for offer_data in offers_data: 37 | data = offer_data['data']['Catalog']['catalogOffer'] 38 | developer_name = '' 39 | for custom_attribute in data['customAttributes']: 40 | if custom_attribute['key'] == 'developerName': 41 | developer_name = custom_attribute['value'] 42 | print('Offer ID:', data['id'], '\nDeveloper Name:', developer_name) 43 | ``` 44 | 45 | You can find more examples in the examples directory. 46 | 47 | ### Contributing 48 | Feel free to contribute by creating PRs and sending your issues 49 | 50 | ## Links 51 | * [Documentation](https://epicstore-api.readthedocs.io/en/latest/) 52 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API Reference 4 | ============== 5 | 6 | API Wrapper 7 | ------------------------- 8 | .. automodule:: epicstore_api.api 9 | :members: 10 | :show-inheritance: 11 | API Exceptions 12 | ------------------------- 13 | .. automodule:: epicstore_api.exc 14 | :members: 15 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import sys 14 | from pathlib import Path 15 | 16 | sys.path.insert(0, str(Path('../../').resolve())) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = 'epicstore_api' 22 | copyright = '2024, SD4RK' 23 | author = 'SD4RK' 24 | 25 | 26 | # -- General configuration --------------------------------------------------- 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = ['sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.autodoc'] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ['_templates'] 35 | 36 | # List of patterns, relative to source directory, that match files and 37 | # directories to ignore when looking for source files. 38 | # This pattern also affects html_static_path and html_extra_path. 39 | exclude_patterns: list = [] 40 | 41 | 42 | # -- Options for HTML output ------------------------------------------------- 43 | 44 | # The theme to use for HTML and HTML Help pages. See the documentation for 45 | # a list of builtin themes. 46 | # 47 | html_theme = 'alabaster' 48 | 49 | # Add any paths that contain custom static files (such as style sheets) here, 50 | # relative to this directory. They are copied after the builtin static files, 51 | # so a file named "default.css" will overwrite the builtin "default.css". 52 | html_static_path = ['_static'] 53 | 54 | master_doc = 'index' # for old versions of sphinx 55 | 56 | 57 | def remove_module_docstring(app, what, name, obj, options, lines) -> None: 58 | print(app, what, name, obj, options, lines) 59 | if what == "module" and "epicstore_api" in name: 60 | del lines[:] 61 | 62 | 63 | def setup(app) -> None: 64 | app.connect("autodoc-process-docstring", remove_module_docstring) 65 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. epicstore_api documentation master file, created by 2 | sphinx-quickstart on Sat Feb 15 01:34:16 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to epicstore_api's documentation! 7 | ========================================= 8 | 9 | 10 | .. toctree:: 11 | intro 12 | api 13 | models 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/source/intro.rst: -------------------------------------------------------------------------------- 1 | .. _intro: 2 | 3 | Introduction 4 | ============== 5 | 6 | This is the documentation for epicstore_api, 7 | library for working with the Epic Games Store API 8 | 9 | Prerequisites 10 | --------------- 11 | 12 | epicstore_api works with Python 3.6 or higher, other versions may not work. 13 | 14 | 15 | 16 | .. _installing: 17 | 18 | Installing 19 | ----------- 20 | 21 | You can get the library directly from PyPI: :: 22 | 23 | python3 -m pip install -U epicstore_api 24 | 25 | If you are using Windows, then the following should be used instead: :: 26 | 27 | py -3 -m pip install -U epicstore_api 28 | 29 | 30 | Remember to check your permissions! 31 | 32 | 33 | Quick Example 34 | ---------------- 35 | Code that will print offer id(s) and their developer for the first product in mapping. 36 | You can see other examples in ``examples/`` directory: 37 | 38 | .. code-block:: python3 39 | 40 | from epicstore_api import EpicGamesStoreAPI, OfferData 41 | 42 | api = EpicGamesStoreAPI() 43 | namespace, slug = next(iter(api.get_product_mapping().items())) 44 | first_product = api.get_product(slug) 45 | offers = [ 46 | OfferData(page['namespace'], page['offer']['id']) 47 | for page in first_product['pages'] 48 | if page.get('offer') and 'id' in page['offer'] 49 | ] 50 | offers_data = api.get_offers_data(*offers) 51 | for offer_data in offers_data: 52 | data = offer_data['data']['Catalog']['catalogOffer'] 53 | developer_name = '' 54 | for custom_attribute in data['customAttributes']: 55 | if custom_attribute['key'] == 'developerName': 56 | developer_name = custom_attribute['value'] 57 | print('Offer ID:', data['id'], '\nDeveloper Name:', developer_name) -------------------------------------------------------------------------------- /docs/source/models.rst: -------------------------------------------------------------------------------- 1 | .. _models: 2 | Models Reference 3 | ============================= 4 | 5 | 6 | EGS Categories 7 | --------------------------------------- 8 | 9 | .. automodule:: epicstore_api.models.categories 10 | :members: 11 | :show-inheritance: 12 | 13 | EGS Product Types 14 | ------------------------------------------- 15 | 16 | .. automodule:: epicstore_api.models.product_types 17 | :members: 18 | :show-inheritance: -------------------------------------------------------------------------------- /epicstore_api/__init__.py: -------------------------------------------------------------------------------- 1 | """Epic Games Store API Wrapper. 2 | ~~~~~~~~~~~~~~~~~~~ 3 | 4 | An API wrapper for Epic Games Store 5 | 6 | :copyright: (c) 2020-2023 SD4RK 7 | :license: MIT, see LICENSE for more details. 8 | """ 9 | 10 | from epicstore_api.api import * 11 | from epicstore_api.exc import EGSException, EGSNotFound 12 | from epicstore_api.models import * 13 | -------------------------------------------------------------------------------- /epicstore_api/api.py: -------------------------------------------------------------------------------- 1 | """MIT License. 2 | 3 | Copyright (c) 2020-2023 SD4RK 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 | """ 23 | 24 | import json 25 | from typing import NamedTuple 26 | 27 | import requests 28 | 29 | from epicstore_api.exc import EGSException, EGSNotFound 30 | from epicstore_api.models import EGSCategory, EGSCollectionType, EGSProductType 31 | from epicstore_api.queries import ( 32 | ADDONS_QUERY, 33 | CATALOG_QUERY, 34 | CATALOG_TAGS_QUERY, 35 | COLLECTION_QUERY, 36 | FEED_QUERY, 37 | MEDIA_QUERY, 38 | OFFERS_QUERY, 39 | PREREQUISITES_QUERY, 40 | PRODUCT_REVIEWS_QUERY, 41 | PROMOTIONS_QUERY, 42 | STORE_QUERY, 43 | ) 44 | 45 | 46 | class OfferData(NamedTuple): 47 | namespace: str 48 | offer_id: str 49 | 50 | 51 | __all__ = ['EpicGamesStoreAPI', 'OfferData'] 52 | 53 | 54 | def _clean_1004_errors(raw): 55 | # On some responses EGS API returns 1004 errors for no reason, however the responses being sent are valid otherwise. 56 | # Official launcher ignores those errors, so we probably should do that as well. That function cleans up the mess 57 | # from raw response so error handling is still possible. 58 | if 'errors' in raw: 59 | for error in raw['errors'].copy(): 60 | service_response = json.loads(error.get('serviceResponse', {})) 61 | if service_response and service_response.get('numericErrorCode') == 1004: 62 | raw['errors'].remove(error) 63 | if not raw['errors']: 64 | raw.pop('errors') 65 | return raw 66 | 67 | 68 | class EpicGamesStoreAPI: 69 | """Class for interacting with EGS web API without user credentials TODO?.""" 70 | 71 | def __init__(self, locale="en-US", country="US", session=None) -> None: 72 | """:param locale: EGS locale (this parameter depends on responses locale) 73 | :param country: EGS country 74 | """ 75 | self._session = requests.Session() or session 76 | self.locale = locale 77 | self.country = country 78 | 79 | def get_product_mapping(self) -> dict: 80 | """Returns product mapping in {namespace: slug} format.""" 81 | return self._make_api_query('/content/productmapping', method='GET') 82 | 83 | def get_product(self, slug: str) -> dict: 84 | """Returns a product's data by slug. 85 | 86 | :param slug: Product's slug. 87 | """ 88 | return self._make_api_query( 89 | f'/content/products/{slug}', 90 | method='GET', 91 | use_locale=True, 92 | ) 93 | 94 | def get_store(self) -> dict: 95 | """Returns a JSON data about store page.""" 96 | return self._make_api_query('/content/store', method='GET', use_locale=True) 97 | 98 | def get_free_games(self, allow_countries: str | None = None) -> dict: 99 | """Returns the games from "Free Games" section in the EGS.""" 100 | if allow_countries is None: 101 | allow_countries = self.country 102 | api_uri = ( 103 | 'https://store-site-backend-static.ak.epicgames.com/' 104 | f'freeGamesPromotions?locale={self.locale}&country={self.country}&allowCountries={allow_countries}' 105 | ) 106 | data = _clean_1004_errors(self._session.get(api_uri).json()) 107 | self._get_errors(data) 108 | return data 109 | 110 | def get_mver_status(self) -> bool: 111 | return self._make_api_query('/mver-status', method='GET')['result'] 112 | 113 | def get_epic_store_status(self) -> dict: 114 | """Returns an Epic Games Store server status.""" 115 | return self._session.get( 116 | 'https://status.epicgames.com/api/v2/status.json', 117 | ).json() 118 | 119 | def get_offers_data( 120 | self, 121 | *offers: OfferData, 122 | should_calculate_tax: bool = False, 123 | include_sub_items: bool = False, 124 | ) -> dict: 125 | """Get offer(s) full data by offers' id and namespace. 126 | 127 | :param offers: Offers you need to get data from. 128 | :param should_calculate_tax: Should EGS API calculate tax for offers? 129 | :param include_sub_items: Should EGS API include sub-items for offers? 130 | """ 131 | return self._make_graphql_query( 132 | OFFERS_QUERY, 133 | {}, 134 | *[ 135 | { 136 | 'productNamespace': offer.namespace, 137 | 'offerId': offer.offer_id, 138 | 'lineOffers': [{'offerId': offer.offer_id, 'quantity': 1}], 139 | 'calculateTax': should_calculate_tax, 140 | 'includeSubItems': include_sub_items, 141 | } 142 | for offer in offers 143 | ], 144 | ) 145 | 146 | def get_collection(self, collection: EGSCollectionType) -> dict: 147 | """Returns games from the collection by the given collection type 148 | (see the documentation for CollectionType class). 149 | 150 | :param collection: Needed collection type. 151 | """ 152 | # Cleanup for the 1004 errors that always pop up by default to not mess someone up by this. 153 | return _clean_1004_errors( 154 | self._make_graphql_query( 155 | COLLECTION_QUERY, 156 | slug=collection.value, 157 | # This query always returns 1004 error by default. That is not controlled by us and the error itself 158 | # is happening even in the official EGS client itself, they're just ignoring it, so we will too. 159 | suppress_errors=True, 160 | ), 161 | ) 162 | 163 | def fetch_media(self, media_ref_id: str) -> dict: 164 | """Returns media-file (type of the file, its url and so on) by the 165 | file's media ref ID. 166 | 167 | :param media_ref_id: File's media ref ID. 168 | """ 169 | return self._make_graphql_query(MEDIA_QUERY, mediaRefId=media_ref_id) 170 | 171 | def fetch_multiple_media_files(self, *media_ref_ids: str): 172 | """Equivalent to `fetch_media` function, except this one can fetch 173 | a few media files at the same moment (using only one request). 174 | """ 175 | return self._make_graphql_query( 176 | MEDIA_QUERY, 177 | {}, 178 | *[{'mediaRefId': media_ref_id} for media_ref_id in media_ref_ids], 179 | ) 180 | 181 | def get_addons_by_namespace( 182 | self, 183 | namespace: str, 184 | categories: str = 'addons|digitalextras', 185 | count: int = 250, 186 | sort_by: str = 'releaseDate', 187 | sort_dir: str = 'DESC', 188 | ): 189 | """Returns product's addons by product's namespace. 190 | 191 | :param namespace: Product's namespace, can be obtained using the 192 | :meth:`epicstore_api.api.EpicGamesStoreAPI.get_product` function. 193 | 194 | :param categories: Addon's categories. 195 | :param count: Count of addon's you want EGS to give you. 196 | :param sort_by: By which key EGS should sort addons. 197 | :param sort_dir: You can use only **ASC** or **DESC**: 198 | 199 | - **ASC**: Sorts from higher ``sort_by`` parameter to lower; 200 | - **DESC**: Sorts from lower ``sort_by`` parameter to higher. 201 | """ 202 | sort_dir = sort_dir.upper() 203 | if sort_dir not in ('ASC', 'DESC'): 204 | msg = ( 205 | f'Parameter ``sort_dir`` have to be equals to' 206 | f' ASC or DESC, not to {sort_dir}' 207 | ) 208 | raise ValueError( 209 | msg, 210 | ) 211 | return self._make_graphql_query( 212 | ADDONS_QUERY, 213 | namespace=namespace, 214 | count=count, 215 | categories=categories, 216 | sortBy=sort_by, 217 | sortDir=sort_dir, 218 | ) 219 | 220 | def get_product_reviews(self, product_sku: str) -> dict: 221 | """Returns product's reviews by product's sku. 222 | 223 | :param product_sku: SKU of the Product. Usually just slug of the 224 | product with `EPIC_` prefix. 225 | """ 226 | try: 227 | return self._make_graphql_query(PRODUCT_REVIEWS_QUERY, sku=product_sku) 228 | except EGSNotFound as exc: 229 | exc.message = ( 230 | 'There are no reviews for this product, ' 231 | f'or the given sku ({product_sku}) is incorrect.' 232 | ) 233 | raise 234 | 235 | def fetch_prerequisites(self, *offers: OfferData) -> dict: 236 | """Fetches offer(s) prerequisites. 237 | 238 | :param offers: Offer(s) we need to get prerequisites from 239 | """ 240 | return self._make_graphql_query( 241 | PREREQUISITES_QUERY, 242 | offerParams=[ 243 | {'offerId': offer.offer_id, 'namespace': offer.namespace} 244 | for offer in offers 245 | ], # OfferData -> dict for every offer in list 246 | ) 247 | 248 | def fetch_feed(self, offset: int = 0, count: int = 10, category: str = '') -> dict: 249 | """Fetches Epic Games Store feed by given params. 250 | 251 | :param offset: From which news (index) we need to start. 252 | :param count: Count of the news we need to fetch. 253 | :param category: News categories. 254 | """ 255 | return self._make_graphql_query( 256 | FEED_QUERY, 257 | offset=offset, 258 | countryCode=self.country, 259 | postsPerPage=count, 260 | category=category, 261 | ) 262 | 263 | def fetch_catalog_tags(self, namespace: str = 'epic') -> dict: 264 | """Fetches tags for a products with namespace ``namespace``. 265 | 266 | :param namespace: Products' namespace (**epic** = all) 267 | """ 268 | return self._make_graphql_query(CATALOG_TAGS_QUERY, namespace=namespace) 269 | 270 | def fetch_promotions(self, namespace: str = 'epic') -> dict: 271 | """Fetches a global promotions. 272 | 273 | :param namespace: Products' namespace (**epic** = all). 274 | """ 275 | return self._make_graphql_query(PROMOTIONS_QUERY, namespace=namespace) 276 | 277 | def fetch_catalog( 278 | self, 279 | count: int = 30, 280 | product_type: EGSProductType | str = EGSProductType.ALL_PRODUCTS, 281 | namespace: str = 'epic', 282 | sort_by: str = 'effectiveDate', 283 | sort_dir: str = 'DESC', 284 | start: int = 0, 285 | keywords: str = '', 286 | categories: list[EGSCategory] | str | None = None, 287 | ) -> dict: 288 | """Fetches a catalog with given parameters. 289 | 290 | :param count: Count of products you need to fetch. 291 | :param product_type: Product type(s) you need to get from EGS. 292 | :param namespace: Products namespace (epic = all namespaces). 293 | :param sort_by: Parameter which EGS will use to sort products. 294 | :param sort_dir: You can use only **ASC** or **DESC**: 295 | 296 | - **ASC**: Sorts from higher ``sort_by`` parameter to lower; 297 | - **DESC**: Sorts from lower ``sort_by`` parameter to higher. 298 | 299 | :param start: From which game EGS should start. 300 | :param keywords: Search keywords. 301 | :param categories: Categories you need to fetch. 302 | :rtype: dict 303 | :raises: ValueError if ``sort_by`` not equals to **ASC** or **DESC**. 304 | """ 305 | sort_dir = sort_dir.upper() 306 | if sort_dir not in ('ASC', 'DESC'): 307 | msg = ( 308 | f'Parameter ``sort_dir`` have to be equals to' 309 | f' ASC or DESC, not to {sort_dir}' 310 | ) 311 | raise ValueError( 312 | msg, 313 | ) 314 | if categories is None: 315 | categories = '' 316 | else: 317 | categories = EGSCategory.join_categories(*categories) 318 | if isinstance(product_type, EGSProductType): 319 | product_type = product_type.value 320 | return self._make_graphql_query( 321 | CATALOG_QUERY, 322 | count=count, 323 | category=product_type, 324 | namespace=namespace, 325 | sortBy=sort_by, 326 | sortDir=sort_dir, 327 | start=start, 328 | keywords=keywords, 329 | tag=categories, 330 | ) 331 | 332 | def fetch_store_games( 333 | self, 334 | count: int = 30, 335 | product_type: EGSProductType | str = EGSProductType.ALL_PRODUCTS, 336 | allow_countries: str = 'US', 337 | namespace: str = '', 338 | sort_by: str = 'title', 339 | sort_dir: str = 'ASC', 340 | release_date: str | None = None, 341 | start: int = 0, 342 | keywords: str = '', 343 | categories: list[EGSCategory] | str | None = None, 344 | *, 345 | with_price: bool = True, 346 | ) -> dict: 347 | """Fetches a store games with given parameters. 348 | 349 | :param count: Count of products you need to fetch. 350 | :param product_type: Product type(s) you need to get from EGS. 351 | :param allow_countries: Products in the country. Default to 'US'. 352 | :param namespace: Products namespace ('' = all namespaces). 353 | :param sort_by: Parameter which EGS will use to sort products: 354 | 355 | - **releaseDate**: Sorts by release date; 356 | - **title**: Sorts by game title, alphabetical. 357 | 358 | :param sort_dir: You can use only **ASC** or **DESC**: 359 | 360 | - **ASC**: Sorts from higher ``sort_by`` parameter to lower; 361 | - **DESC**: Sorts from lower ``sort_by`` parameter to higher. 362 | 363 | :param release_date: Available when ``sort_by`` is 'releaseDate'. 364 | 365 | - Date is in ISO 8601 format. General format: f'[{startDate}, {endDate}]'. 366 | - Example: '[2019-09-16T14:02:36.304Z, 2019-09-26T14:02:36.304Z]' 367 | - Leaving ``startDate`` or ``endDate`` blank will not limit start/end date. 368 | 369 | :param start: From which game EGS should start. 370 | :param keywords: Search keywords. 371 | :param categories: Categories you need to fetch. 372 | :param with_price: To fetch price or not. 373 | :rtype: dict 374 | :raises: ValueError if ``sort_by`` not equals to **ASC** or **DESC**. 375 | """ 376 | sort_dir = sort_dir.upper() 377 | if sort_dir not in ('ASC', 'DESC'): 378 | msg = ( 379 | f'Parameter ``sort_dir`` have to be equals to' 380 | f' ASC or DESC, not to {sort_dir}' 381 | ) 382 | raise ValueError( 383 | msg, 384 | ) 385 | if categories is None: 386 | categories = '' 387 | else: 388 | categories = EGSCategory.join_categories(*categories) 389 | if isinstance(product_type, EGSProductType): 390 | product_type = product_type.value 391 | return self._make_graphql_query( # This type of fetch needs headers. 392 | STORE_QUERY, 393 | headers={'content-type': 'application/json;charset=UTF-8'}, 394 | count=count, 395 | category=product_type, 396 | allowCountries=allow_countries, 397 | namespace=namespace, 398 | sortBy=sort_by, 399 | sortDir=sort_dir, 400 | releaseDate=release_date, 401 | start=start, 402 | keywords=keywords, 403 | tag=categories, 404 | withPrice=with_price, 405 | ) 406 | 407 | def _make_api_query( 408 | self, 409 | endpoint: str, 410 | method: str, 411 | *, 412 | use_locale: bool = False, 413 | **variables, 414 | ) -> dict: 415 | func = getattr(self._session, method.lower()) 416 | base_url = 'https://store-content.ak.epicgames.com' 417 | base_url += '/api' if not use_locale else f'/api/{self.locale}' 418 | response = func(base_url + endpoint, data=variables) 419 | if response.status_code == 404: 420 | msg = f'Page with endpoint {endpoint} was not found' 421 | raise EGSException(msg) 422 | response = response.json() 423 | self._get_errors(response) 424 | return response 425 | 426 | def _make_graphql_query( 427 | self, 428 | query_string, 429 | headers=None, 430 | *multiple_query_variables, 431 | suppress_errors=False, 432 | **variables, 433 | ) -> dict: 434 | if headers is None: 435 | headers = {} 436 | if not multiple_query_variables: 437 | variables.update({'locale': self.locale, 'country': self.country}) 438 | # This variables are default and exist in all graphql queries 439 | response = self._session.post( 440 | 'https://graphql.epicgames.com/graphql', 441 | json={'query': query_string, 'variables': variables}, 442 | headers=headers, 443 | ).json() 444 | else: 445 | data = [] 446 | for variables in multiple_query_variables: 447 | variables_ = { 448 | 'locale': self.locale, 449 | 'country': self.country, 450 | } 451 | variables_.update(variables) 452 | data.append({'query': query_string, 'variables': variables_}) 453 | response = self._session.post( 454 | 'https://graphql.epicgames.com/graphql', 455 | json=data, 456 | headers=headers, 457 | ).json() 458 | if not suppress_errors: 459 | self._get_errors(response) 460 | return response 461 | 462 | @staticmethod 463 | def _get_errors(resp) -> None: 464 | r = [] 465 | if not isinstance(resp, list): 466 | r.append(resp) 467 | for response in r: 468 | if response.get('errors'): 469 | error = response['errors'][0] 470 | if not error['serviceResponse']: 471 | raise EGSException(error['message'], service_response=error) 472 | service_response = json.loads(error['serviceResponse']) 473 | if isinstance(service_response, dict): 474 | if service_response['errorCode'].endswith('not_found'): 475 | raise EGSNotFound( 476 | service_response['errorMessage'], 477 | service_response['numericErrorCode'], 478 | service_response, 479 | ) 480 | elif ( 481 | isinstance(service_response, str) 482 | and service_response == 'not found' 483 | ): 484 | msg = ( 485 | 'The resource was not found, ' 486 | 'No more data provided by Epic Games Store.' 487 | ) 488 | raise EGSNotFound( 489 | msg, 490 | ) 491 | # FIXME: Need to handle more errors than the code is handling now 492 | -------------------------------------------------------------------------------- /epicstore_api/exc.py: -------------------------------------------------------------------------------- 1 | """MIT License. 2 | 3 | Copyright (c) 2020-2023 SD4RK 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 | """ 23 | 24 | 25 | class EGSException(Exception): 26 | """Class for EGS errors, all data about error is placed in ``exception_data``.""" 27 | 28 | def __init__(self, message, error_code=None, service_response=None) -> None: 29 | super().__init__(message) 30 | self.message = ( 31 | f'Error code: ' 32 | f'{error_code if error_code is not None else "unknown"}. ' 33 | f'{message.capitalize()}' 34 | ) 35 | self.exception_data = service_response 36 | 37 | def __str__(self) -> str: 38 | return self.message 39 | 40 | 41 | class EGSNotFound(EGSException): 42 | """All errors which error code ends with `not_found`.""" 43 | -------------------------------------------------------------------------------- /epicstore_api/models/__init__.py: -------------------------------------------------------------------------------- 1 | """MIT License. 2 | 3 | Copyright (c) 2020-2023 SD4RK 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 | """ 23 | 24 | from epicstore_api.models.categories import EGSCategory 25 | from epicstore_api.models.collection_types import EGSCollectionType 26 | from epicstore_api.models.product_types import EGSProductType 27 | -------------------------------------------------------------------------------- /epicstore_api/models/categories.py: -------------------------------------------------------------------------------- 1 | """MIT License. 2 | 3 | Copyright (c) 2020-2023 SD4RK 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 | """ 23 | 24 | from enum import Enum 25 | 26 | 27 | class EGSCategory(Enum): 28 | """Class that provides a code for every category in the EGS with 29 | a human-readable name and a few useful methods. 30 | 31 | .. note:: 32 | Here you can see only that categories that are displayed in EGS, 33 | for other categories you can call an API function 34 | :meth:`epicstore_api.api.EpicGamesStoreAPI.fetch_catalog_tags` 35 | """ 36 | 37 | CATEGORY_ACTION = "1216" #: Action games 38 | CATEGORY_EDITOR = "9559" #: Editors for games 39 | CATEGORY_ADVENTURE = "1117" #: Adventure games 40 | CATEGORY_PUZZLE = "1298" #: Puzzle games 41 | CATEGORY_RACING = "1212" #: Racing games 42 | CATEGORY_RPG = "1367" #: RPG games 43 | CATEGORY_SHOOTER = "1210" #: Shooter games 44 | CATEGORY_STRATEGY = "1115" #: Strategy games 45 | CATEGORY_SURVIVAL = "1080" #: Survival games 46 | CATEGORY_OSX = "9548" #: Games for OSX (Mac OS) 47 | CATEGORY_WINDOWS = "9547" #: Games for Windows 48 | CATEGORY_SINGLE_PLAYER = "1370" #: Single-player games 49 | CATEGORY_MULTIPLAYER = "1203" #: Multiplayer games 50 | 51 | @staticmethod 52 | def join_categories(*categories_list) -> str: 53 | """Joins the given categories into a string for EGS API queries 54 | :param categories_list: list of categories you need 55 | :type categories_list: List[EGSCategory] 56 | :rtype: str. 57 | """ 58 | return '|'.join([category.value for category in categories_list]) 59 | 60 | def __add__(self, other): 61 | if isinstance(other, EGSCategory): 62 | return self.join_categories(self, other) 63 | raise NotImplementedError 64 | -------------------------------------------------------------------------------- /epicstore_api/models/collection_types.py: -------------------------------------------------------------------------------- 1 | """MIT License. 2 | 3 | Copyright (c) 2020-2023 SD4RK 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 | """ 23 | 24 | from enum import Enum 25 | 26 | 27 | class EGSCollectionType(Enum): 28 | """A helper enum that is used for the collections query 29 | (see :meth:`epicstore_api.api.EpicGamesStoreAPI.get_collection`). You can see the game that fall under particular 30 | collections under the free games on the main page of the Epic Games Store. Collections that are not included 31 | (such as New Releases and Coming Soon can be obtained through catalog query with specific sort queries such as 32 | sortBy=releaseDate and sortBy=comingSoon). 33 | """ 34 | 35 | TOP_SELLERS = "top-sellers" 36 | MOST_PLAYED = "most-played" 37 | TOP_UPCOMING_WISHLISTED = "top-wishlisted" 38 | MOST_POPULAR = "most-popular" 39 | TOP_PLAYER_RATED = "top-player-reviewed" 40 | -------------------------------------------------------------------------------- /epicstore_api/models/product_types.py: -------------------------------------------------------------------------------- 1 | """MIT License. 2 | 3 | Copyright (c) 2020-2023 SD4RK 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 | """ 23 | 24 | from enum import Enum 25 | 26 | 27 | class EGSProductType(Enum): 28 | PRODUCT_ENGINE = "engines" #: Engines for developing games 29 | PRODUCT_GAME = "games" #: Games 30 | PRODUCT_BUNDLE = "bundles" #: An EGS bundle 31 | ALL_PRODUCTS = ( 32 | f"{PRODUCT_ENGINE}|{PRODUCT_GAME}|{PRODUCT_BUNDLE}" #: All possible products 33 | ) 34 | -------------------------------------------------------------------------------- /epicstore_api/queries.py: -------------------------------------------------------------------------------- 1 | """MIT License. 2 | 3 | Copyright (c) 2020-2023 SD4RK 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 | """ 23 | 24 | CATALOG_QUERY = "\n query catalogQuery(\n $category:String,\n $count:Int,\n $country:String!,\n $keywords: String,\n $locale:String,\n $namespace:String!,\n $sortBy:String,\n $sortDir:String,\n $start:Int,\n $tag:String\n ) {\n Catalog {\n catalogOffers(\n namespace: $namespace,\n locale: $locale,\n params: {\n count: $count,\n country: $country,\n category: $category,\n keywords: $keywords,\n sortBy: $sortBy,\n sortDir: $sortDir,\n start: $start,\n tag: $tag\n }\n ) {\n elements {\n isFeatured\n collectionOfferIds\n \n title\n id\n namespace\n description\n keyImages {\n type\n url\n }\n seller {\n id\n name\n }\n productSlug\n urlSlug\n items {\n id\n namespace\n }\n customAttributes {\n key\n value\n }\n categories {\n path\n }\n price(country: $country) {\n totalPrice {\n discountPrice\n originalPrice\n voucherDiscount\n discount\n fmtPrice(locale: $locale) {\n originalPrice\n discountPrice\n intermediatePrice\n }\n }\n lineOffers {\n appliedRules {\n id\n endDate\n }\n }\n }\n linkedOfferId\n linkedOffer {\n effectiveDate\n customAttributes {\n key\n value\n }\n }\n \n }\n paging {\n count,\n total\n }\n }\n }\n }\n " 25 | STORE_QUERY = "query searchStoreQuery($allowCountries: String, $category: String, $count: Int, $country: String!, $keywords: String, $locale: String, $namespace: String, $itemNs: String, $sortBy: String, $sortDir: String, $start: Int, $tag: String, $releaseDate: String, $withPrice: Boolean = false, $withPromotions: Boolean = false) {\n Catalog {\n searchStore(allowCountries: $allowCountries, category: $category, count: $count, country: $country, keywords: $keywords, locale: $locale, namespace: $namespace, itemNs: $itemNs, sortBy: $sortBy, sortDir: $sortDir, releaseDate: $releaseDate, start: $start, tag: $tag) {\n elements {\n title\n id\n namespace\n description\n effectiveDate\n keyImages {\n type\n url\n }\n seller {\n id\n name\n }\n productSlug\n urlSlug\n url\n tags {\n id\n }\n items {\n id\n namespace\n }\n customAttributes {\n key\n value\n }\n categories {\n path\n }\n price(country: $country) @include(if: $withPrice) {\n totalPrice {\n discountPrice\n originalPrice\n voucherDiscount\n discount\n currencyCode\n currencyInfo {\n decimals\n }\n fmtPrice(locale: $locale) {\n originalPrice\n discountPrice\n intermediatePrice\n }\n }\n lineOffers {\n appliedRules {\n id\n endDate\n discountSetting {\n discountType\n }\n }\n }\n }\n promotions(category: $category) @include(if: $withPromotions) {\n promotionalOffers {\n promotionalOffers {\n startDate\n endDate\n discountSetting {\n discountType\n discountPercentage\n }\n }\n }\n upcomingPromotionalOffers {\n promotionalOffers {\n startDate\n endDate\n discountSetting {\n discountType\n discountPercentage\n }\n }\n }\n }\n }\n paging {\n count\n total\n }\n }\n }\n}\n" 26 | PROMOTIONS_QUERY = '\n query promotionsQuery($namespace: String!, $country: String!, $locale: String!) {\n Catalog {\n catalogOffers(namespace: $namespace, locale: $locale, params: {category: "freegames", country: $country, sortBy: "effectiveDate", sortDir: "asc"}) {\n elements {\n title\n description\n id\n namespace\n categories {\n path\n }\n linkedOfferNs\n linkedOfferId\n keyImages {\n type\n url\n }\n productSlug\n promotions {\n promotionalOffers {\n promotionalOffers {\n startDate\n endDate\n discountSetting {\n discountType\n discountPercentage\n }\n }\n }\n upcomingPromotionalOffers {\n promotionalOffers {\n startDate\n endDate\n discountSetting {\n discountType\n discountPercentage\n }\n }\n }\n }\n }\n }\n }\n }\n ' 27 | CATALOG_TAGS_QUERY = "\n query catalogTags($namespace: String!)\n {\n Catalog {\n tags (\n namespace: $namespace,\n start: 0,\n count: 999\n ) {\n elements {\n aliases,\n id,\n name,\n referenceCount,\n status\n }\n }\n }\n }\n " 28 | FEED_QUERY = "\n query feedQuery($locale: String!, $countryCode: String, $offset: Int, $postsPerPage: Int, $category: String) {\n TransientStream {\n myTransientFeed(countryCode: $countryCode, locale: $locale) {\n id\n activity {\n # TODO Comment in to enable welcome post when requirements are finalized\n # ...on SimpleActivity {\n # type\n # created_at\n # }\n ...on LinkAccountActivity {\n type\n created_at\n platforms\n }\n ...on SuggestedFriendsActivity {\n type\n created_at\n platform\n suggestions {\n epicId\n epicDisplayName\n platformFullName\n platformAvatar\n }\n }\n ...on IncomingInvitesActivity {\n type\n created_at\n invites {\n epicId\n epicDisplayName\n }\n }\n ...on RecentPlayersActivity {\n type\n created_at\n players {\n epicId\n epicDisplayName\n playedGameName\n }\n }\n }\n }\n }\n Blog {\n dieselBlogPosts: getPosts(locale: $locale, offset: $offset, postsPerPage: $postsPerPage, category: $category) {\n blogList {\n _id\n author\n category\n content\n urlPattern\n slug\n sticky\n title\n date\n image\n shareImage\n trendingImage\n url\n featured\n link\n externalLink\n }\n }\n }\n }" 29 | PREREQUISITES_QUERY = "\n query fetchPrerequisites($offerParams: [OfferParams]) {\n Launcher{\n prerequisites(offerParams:$offerParams) {\n namespace,\n offerId,\n missingPrerequisiteItems\n satisfiesPrerequisites\n }\n }\n }\n" 30 | OFFERS_QUERY = "\n query catalogQuery($productNamespace: String!, $offerId: String!, $locale: String, $country: String!, $includeSubItems: Boolean!) {\n Catalog {\n catalogOffer(namespace: $productNamespace, id: $offerId, locale: $locale) {\n title\n id\n namespace\n description\n effectiveDate\n expiryDate\n isCodeRedemptionOnly\n keyImages {\n type\n url\n }\n seller {\n id\n name\n }\n productSlug\n urlSlug\n url\n tags {\n id\n }\n items {\n id\n namespace\n }\n customAttributes {\n key\n value\n }\n categories {\n path\n }\n price(country: $country) {\n totalPrice {\n discountPrice\n originalPrice\n voucherDiscount\n discount\n currencyCode\n currencyInfo {\n decimals\n }\n fmtPrice(locale: $locale) {\n originalPrice\n discountPrice\n intermediatePrice\n }\n }\n lineOffers {\n appliedRules {\n id\n endDate\n discountSetting {\n discountType\n }\n }\n }\n }\n }\n offerSubItems(namespace: $productNamespace, id: $offerId) @include(if: $includeSubItems) {\n namespace\n id\n releaseInfo {\n appId\n platform\n }\n }\n }\n}\n" 31 | MEDIA_QUERY = "\n query fetchMediaRef($mediaRefId: String!) {\n Media {\n getMediaRef(mediaRefId: $mediaRefId) {\n accountId\n outputs {\n duration\n url\n width\n height\n key\n contentType\n }\n namespace\n }\n }\n }\n" 32 | PRODUCT_REVIEWS_QUERY = "\n query productReviewsQuery($sku: String!) {\n OpenCritic {\n productReviews(sku: $sku) {\n id\n name\n openCriticScore\n reviewCount\n percentRecommended\n openCriticUrl\n award\n topReviews {\n publishedDate\n externalUrl\n snippet\n language\n score\n author\n ScoreFormat {\n id\n description\n }\n OutletId\n outletName\n displayScore\n }\n }\n }\n }\n " 33 | ADDONS_QUERY = "query getAddonsByNamespace($categories: String!, $count: Int!, $country: String!, $locale: String!, $namespace: String!, $sortBy: String!, $sortDir: String!) {\n Catalog {\n catalogOffers(namespace: $namespace, locale: $locale, params: {category: $categories, count: $count, country: $country, sortBy: $sortBy, sortDir: $sortDir}) {\n elements {\n countriesBlacklist\n customAttributes {\n key\n value\n }\n description\n developer\n effectiveDate\n id\n isFeatured\n keyImages {\n type\n url\n }\n lastModifiedDate\n longDescription\n namespace\n offerType\n productSlug\n releaseDate\n status\n technicalDetails\n title\n urlSlug\n }\n }\n }\n}\n" 34 | COLLECTION_QUERY = 'query collectionLayoutQuery($locale: String, $country: String!, $slug: String) {\n Storefront {\n collectionLayout(locale: $locale, slug: $slug) {\n _activeDate\n _locale\n _metaTags\n _slug\n _title\n _urlPattern\n lastModified\n regionBlock\n affiliateId\n takeover {\n banner {\n altText\n src\n }\n description\n eyebrow\n title\n }\n seo {\n title\n description\n keywords\n image {\n src\n altText\n }\n twitter {\n title\n description\n }\n og {\n title\n description\n image {\n src\n alt\n }\n }\n }\n collectionOffers {\n title\n id\n namespace\n description\n effectiveDate\n countriesBlacklist\n countriesWhitelist\n developerDisplayName\n publisherDisplayName\n keyImages {\n type\n url\n }\n seller {\n id\n name\n }\n releaseDate\n pcReleaseDate\n approximateReleasePlan {\n day\n month\n quarter\n year\n releaseDateType\n }\n prePurchase\n productSlug\n urlSlug\n url\n items {\n id\n namespace\n }\n customAttributes {\n key\n value\n }\n categories {\n path\n }\n linkedOfferId\n linkedOffer {\n effectiveDate\n customAttributes {\n key\n value\n }\n }\n catalogNs {\n mappings(pageType: "productHome") {\n pageSlug\n pageType\n }\n }\n offerMappings {\n pageSlug\n pageType\n }\n price(country: $country) {\n totalPrice {\n currencyCode\n currencyInfo {\n decimals\n symbol\n }\n discountPrice\n originalPrice\n voucherDiscount\n discount\n fmtPrice(locale: $locale) {\n originalPrice\n discountPrice\n intermediatePrice\n }\n }\n lineOffers {\n appliedRules {\n id\n endDate\n }\n }\n }\n }\n pageTheme {\n preferredMode\n light {\n theme\n accent\n }\n dark {\n theme\n accent\n }\n }\n redirect {\n code\n url\n }\n }\n }\n}\n' 35 | # XXX: This code violates PEP 8, line > 79 chars 36 | -------------------------------------------------------------------------------- /examples/free_games_example.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from datetime import datetime 3 | 4 | from epicstore_api import EpicGamesStoreAPI 5 | 6 | 7 | def main() -> None: 8 | """Fetches current free games from the store.""" 9 | api = EpicGamesStoreAPI() 10 | free_games = api.get_free_games()['data']['Catalog']['searchStore']['elements'] 11 | 12 | # Few odd items do not seems game and don't have the promotion attribute, so let's check it ! 13 | free_games = sorted( 14 | filter(lambda g: g.get('promotions'), free_games), 15 | key=operator.itemgetter('title'), 16 | ) 17 | 18 | for game in free_games: 19 | game_title = game['title'] 20 | game_publisher = game['seller']['name'] 21 | 22 | url_type = "bundles" if game['offerType'] == "BUNDLE" else "p" 23 | final_slug = game["catalogNs"]["mappings"][0]["pageSlug"] if game["catalogNs"]["mappings"] else game["urlSlug"] 24 | game_url = f"https://store.epicgames.com/fr/{url_type}/{final_slug}" 25 | 26 | # Can be useful when you need to also show the thumbnail of the game. 27 | # Like in Discord's embeds for example, or anything else. 28 | # Here I showed it just as example and won't use it. 29 | for image in game['keyImages']: 30 | if image['type'] == 'Thumbnail': 31 | game_thumbnail = image['url'] 32 | 33 | game_price = game['price']['totalPrice']['fmtPrice']['originalPrice'] 34 | game_price_promo = game['price']['totalPrice']['fmtPrice']['discountPrice'] 35 | 36 | game_promotions = game['promotions']['promotionalOffers'] 37 | upcoming_promotions = game['promotions']['upcomingPromotionalOffers'] 38 | 39 | if game_promotions and game['price']['totalPrice']['discountPrice'] == 0: 40 | # Promotion is active. 41 | promotion_data = game_promotions[0]['promotionalOffers'][0] 42 | start_date_iso, end_date_iso = ( 43 | promotion_data['startDate'][:-1], 44 | promotion_data['endDate'][:-1], 45 | ) 46 | # Remove the last "Z" character so Python's datetime can parse it. 47 | start_date = datetime.fromisoformat(start_date_iso) 48 | end_date = datetime.fromisoformat(end_date_iso) 49 | print( 50 | f'* {game_title} ({game_price}) is FREE now, until {end_date} --> {game_url}', 51 | ) 52 | elif not game_promotions and upcoming_promotions: 53 | # Promotion is not active yet, but will be active soon. 54 | promotion_data = upcoming_promotions[0]['promotionalOffers'][0] 55 | start_date_iso, end_date_iso = ( 56 | promotion_data['startDate'][:-1], 57 | promotion_data['endDate'][:-1], 58 | ) 59 | # Remove the last "Z" character so Python's datetime can parse it. 60 | start_date = datetime.fromisoformat(start_date_iso) 61 | end_date = datetime.fromisoformat(end_date_iso) 62 | print( 63 | f'* {game_title} ({game_price}) will be free from {start_date} to {end_date} UTC --> {game_url}', 64 | ) 65 | elif game_promotions: 66 | # Promotion is active. 67 | promotion_data = game_promotions[0]['promotionalOffers'][0] 68 | start_date_iso, end_date_iso = ( 69 | promotion_data['startDate'][:-1], 70 | promotion_data['endDate'][:-1], 71 | ) 72 | # Remove the last "Z" character so Python's datetime can parse it. 73 | start_date = datetime.fromisoformat(start_date_iso) 74 | end_date = datetime.fromisoformat(end_date_iso) 75 | print( 76 | f'* {game_title} is in promotion ({game_price} -> {game_price_promo}) from {start_date} to {end_date} UTC --> {game_url}', 77 | ) 78 | else: 79 | print(f'* {game_title} is always free --> {game_url}') 80 | 81 | 82 | if __name__ == '__main__': 83 | main() 84 | -------------------------------------------------------------------------------- /examples/get_store_games_example.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from epicstore_api import EpicGamesStoreAPI 4 | 5 | 6 | def main() -> None: 7 | """Print all games in filter range.""" 8 | games = EpicGamesStoreAPI().fetch_store_games( 9 | product_type='games/edition/base|bundles/games|editors', 10 | # Default filter in store page. 11 | count=30, 12 | sort_by='releaseDate', 13 | sort_dir='DESC', 14 | release_date="[2019-09-16T14:02:36.304Z,2019-09-26T14:02:36.304Z]", 15 | with_price=True, 16 | ) 17 | print('API Response:\n', json.dumps(games, indent=4)) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /examples/get_top_sellers.py: -------------------------------------------------------------------------------- 1 | from epicstore_api import EpicGamesStoreAPI 2 | from epicstore_api.models import EGSCollectionType 3 | 4 | 5 | def main() -> None: 6 | """Prints list of the current top sellers.""" 7 | api = EpicGamesStoreAPI() 8 | top_sellers = api.get_collection(EGSCollectionType.TOP_SELLERS)['data'][ 9 | 'Storefront' 10 | ]['collectionLayout']['collectionOffers'] 11 | print('Top sellers list:') 12 | for game in top_sellers: 13 | print( 14 | f'{game["title"]} - {game["price"]["totalPrice"]["fmtPrice"]["originalPrice"]}', 15 | ) 16 | 17 | 18 | if __name__ == '__main__': 19 | main() 20 | -------------------------------------------------------------------------------- /examples/simple_example.py: -------------------------------------------------------------------------------- 1 | from epicstore_api import EpicGamesStoreAPI, OfferData 2 | 3 | 4 | def main() -> None: 5 | """Prints offer ID and developer for every offer of the first product in mapping.""" 6 | api = EpicGamesStoreAPI() 7 | namespace, slug = next(iter(api.get_product_mapping().items())) 8 | first_product = api.get_product(slug) 9 | offers = [ 10 | OfferData(page['namespace'], page['offer']['id']) 11 | for page in first_product['pages'] 12 | if page.get('offer') and 'id' in page['offer'] 13 | ] 14 | offers_data = api.get_offers_data(*offers) 15 | for offer_data in offers_data: 16 | data = offer_data['data']['Catalog']['catalogOffer'] 17 | developer_name = '' 18 | for custom_attribute in data['customAttributes']: 19 | if custom_attribute['key'] == 'developerName': 20 | developer_name = custom_attribute['value'] 21 | print('Offer ID:', data['id'], '\nDeveloper Name:', developer_name) 22 | 23 | 24 | if __name__ == '__main__': 25 | main() 26 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pytest -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import setuptools 4 | 5 | AUTHOR = 'SD4RK' 6 | VERSION = '0.1.9' 7 | 8 | long_description = Path("README.md").read_text() 9 | 10 | setuptools.setup( 11 | name='epicstore_api', 12 | version=VERSION, 13 | author=AUTHOR, 14 | description='An API wrapper for Epic Games Store written in Python', 15 | long_description=long_description, 16 | long_description_content_type='text/markdown', 17 | url='https://github.com/SD4RK/epicstore_api', 18 | license='MIT', 19 | include_package_data=True, 20 | install_requires=['requests>=2.28.1'], 21 | download_url=f'https://github.com/SD4RK/epicstore_api/archive/v_{VERSION}.tar.gz', 22 | packages=setuptools.find_packages(), 23 | classifiers=[ 24 | 'License :: OSI Approved :: MIT License', 25 | 'Intended Audience :: Developers', 26 | 'Natural Language :: English', 27 | 'Operating System :: OS Independent', 28 | 'Programming Language :: Python :: 3.7', 29 | 'Programming Language :: Python :: 3.8', 30 | 'Programming Language :: Python :: 3.9', 31 | 'Programming Language :: Python :: 3.10', 32 | 'Topic :: Internet', 33 | 'Topic :: Software Development :: Libraries', 34 | 'Topic :: Software Development :: Libraries :: Python Modules', 35 | 'Topic :: Utilities', 36 | ], 37 | python_requires='>=3.7', 38 | ) 39 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from epicstore_api import EGSException, EpicGamesStoreAPI 4 | 5 | 6 | def main() -> None: 7 | api = EpicGamesStoreAPI() 8 | with pytest.raises(EGSException): 9 | api.get_product('this_slug_does_not_exist') 10 | satisfactory_page = api.get_product('satisfactory') 11 | assert satisfactory_page['namespace'] == 'crab' 12 | 13 | 14 | if __name__ == '__main__': 15 | main() 16 | --------------------------------------------------------------------------------