From 77ac924b9bd37cb318d839be557404fa435c27d6 Mon Sep 17 00:00:00 2001 From: Lucas Hardt Date: Fri, 13 Dec 2019 00:03:27 +0100 Subject: [PATCH] Changed version to 1.0.4 Added type hint for FortniteAPI class arguments Added type check for API Key Implemented type check for search parameters Implemented MissingIDParameter and MissingSearchParameter errors Errors are now a instance of FortniteAPIException Added ServiceUnavailable check Fixed error in async lib Added keywords for lib Added tests Cleaned up some code Took 3 hours 21 minutes --- README.md | 61 ++-------------- fortnite_api/__init__.py | 2 +- fortnite_api/api.py | 5 +- fortnite_api/endpoints.py | 149 +++++++++++++++++--------------------- fortnite_api/errors.py | 16 ++-- fortnite_api/http.py | 43 ++++++----- setup.py | 5 +- tests.py | 26 ++++++- 8 files changed, 136 insertions(+), 171 deletions(-) diff --git a/README.md b/README.md index ef9f9ebb..73947014 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,6 @@ Get all Br cosmetics. - `language` [GameLanguage] (Optional) - Specify the language of the shop. Default is set to english ###### Returns Returns a list of `BrCosmetic` objects. -###### Raises -- `ServerOutage` when the servers are not available -- `RateLimted` when the rate limit has been hit -- `Unauthorized` when no or a wrong API key has been provided ___ @@ -56,12 +52,6 @@ Search one o multiple items by their id. - `language` [GameLanguage] (Optional) - Specify the language of the shop. Default is set to english ###### Returns Returns a list of `BrCosmetic` objects. -###### Raises -- `ServerOutage` if the servers are not available -- `RateLimted` when the rate limit has been hit -- `Unauthorized` when no or a wrong API key has been provided -- `NotFound` if the cosmetic with the given id wasn't found -- `MissingIDParameter` if no id was provided ___ @@ -74,11 +64,6 @@ Search all cosmetics which fit to the search parameters - `language` [GameLanguage] (Optional) - Specify the language of the shop. Default is set to english ###### Returns Returns a list of `BrCosmetic` objects. -###### Raises -- `ServerOutage` if the servers are not available -- `RateLimted` when the rate limit has been hit -- `Unauthorized` when no or a wrong API key has been provided -- `MissingSearchParameter` if no search parameter was provided ___ @@ -91,11 +76,6 @@ Search the first cosmetics which fit to the search parameters - `language` [GameLanguage] (Optional) - Specify the language of the shop. Default is set to english ###### Returns Returns a `BrCosmetic` objects. -###### Raises -- `ServerOutage` if the servers are not available -- `RateLimted` when the rate limit has been hit -- `Unauthorized` when no or a wrong API key has been provided -- `MissingIDParameter` if no id was provided ___ @@ -107,11 +87,7 @@ Get the latest Fortnite shop. ###### Parameters - `language` [GameLanguage] (Optional) - Specify the language of the shop. Default is set to english ###### Returns -Returns a `Shop` object. -###### Raises -- `Server Outage` if the servers are not available -- `RateLimted` when the rate limit has been hit -- `Unauthorized` when no or a wrong API key has been provided +Returns a `Shop` object. ___ @@ -124,10 +100,6 @@ Get the latest Fortnite news of all game modes. - `language` [GameLanguage] (Optional) - Specify the language of the shop. Default is set to english ###### Returns Returns a `News` object. -###### Raises -- `ServerOutage` if the servers are not available -- `RateLimted` when the rate limit has been hit -- `Unauthorized` when no or a wrong API key has been provided ___ @@ -141,10 +113,6 @@ Get the latest Fortnite news of a specified game mode. - `language` [GameLanguage] (Optional) - Specify the language of the shop. Default is set to english ###### Returns Returns a `GameModeNews` object. -###### Raises -- `ServerOutage` if the servers are not available -- `RateLimted` when the rate limit has been hit -- `Unauthorized` when no or a wrong API key has been provided ___ @@ -154,14 +122,9 @@ api.creator_code.fetch() ``` Get information about a creator code. ###### Parameters -- `creator_code` [str] - Specify a creator code. +- `slug` [str] - Specify a creator code. ###### Returns Returns a `CreatorCode` object. -###### Raises -- `ServerOutage` if the servers are not available -- `RateLimted` when the rate limit has been hit -- `Unauthorized` when no or a wrong API key has been provided -- `NotFound` if the creator code wasn't found ___ @@ -170,13 +133,9 @@ api.creator_code.exists() ``` Check if a creator code exists. ###### Parameters -- `creator_code` [str] - Specify a creator code. +- `slug` [str] - Specify a creator code. ###### Returns Returns a `bool` object. -###### Raises -- `ServerOutage` if the servers are not available -- `RateLimted` when the rate limit has been hit -- `Unauthorized` when no or a wrong API key has been provided ___ @@ -186,14 +145,9 @@ api.creator_code.search_all() ``` Search a creator code by name. All results are provided. ###### Parameters -- `creator_code` [str] - Specify a creator code. +- `slug` [str] - Specify a creator code. ###### Returns Returns a `list` of `CreatorCode` objects. -###### Raises -- `ServerOutage` if the servers are not available -- `RateLimted` when the rate limit has been hit -- `Unauthorized` when no or a wrong API key has been provided -- `NotFound` if the creator code wasn't found ___ @@ -203,14 +157,9 @@ api.creator_code.search_first() ``` Search a creator code by name. Only the first result is provided. ###### Parameters -- `creator_code` [str] - Specify a creator code. +- `slug` [str] - Specify a creator code. ###### Returns Returns a `CreatorCode` object. -###### Raises -- `ServerOutage` if the servers are not available -- `RateLimted` when the rate limit has been hit -- `Unauthorized` when no or a wrong API key has been provided -- `NotFound` if the creator code wasn't found ## Contribute Every type of contribution is appreciated! diff --git a/fortnite_api/__init__.py b/fortnite_api/__init__.py index 7648b86f..688aa1f8 100644 --- a/fortnite_api/__init__.py +++ b/fortnite_api/__init__.py @@ -1,4 +1,4 @@ -__version__ = '1.0.3' +__version__ = '1.0.4' from .api import FortniteAPI from .cosmetics import * diff --git a/fortnite_api/api.py b/fortnite_api/api.py index 3420bb0c..a9b7e791 100644 --- a/fortnite_api/api.py +++ b/fortnite_api/api.py @@ -4,9 +4,12 @@ class FortniteAPI: - def __init__(self, api_key, run_async=False): + def __init__(self, api_key: str, run_async: bool = False): + if type(api_key) is not str: + raise TypeError('api_key require a value of type {0}'.format(str(str))) self.http = SyncHTTPClient() if not run_async else AsyncHTTPClient() self.http.add_header('x-api-key', api_key) + self.cosmetics = SyncCosmeticsEndpoints(self) if not run_async else AsyncCosmeticsEndpoints(self) self.creator_code = SyncCreatorCodeEndpoints(self) if not run_async else AsyncCreatorCodeEndpoints(self) self.news = SyncNewsEndpoints(self) if not run_async else AsyncNewsEndpoints(self) diff --git a/fortnite_api/endpoints.py b/fortnite_api/endpoints.py index 58c6a8f3..15ea4760 100644 --- a/fortnite_api/endpoints.py +++ b/fortnite_api/endpoints.py @@ -1,55 +1,55 @@ import typing +from enum import Enum from .creator_code import CreatorCode from .enums import GameLanguage, MatchMethod, NewsType -from .errors import NotFound +from .errors import MissingSearchParameter, MissingIDParameter, NotFound from .cosmetics import BrCosmetic from .news import GameModeNews, News from .shop import BrShop +_SEARCH_PARAMETERS = { + 'language': [None, [GameLanguage]], + 'search_language': ['searchLanguage', [GameLanguage]], + 'match_method': ['matchMethod', [MatchMethod]], + 'type': [None, [str, None]], + 'backend_type': ['backendType', [str, None]], + 'rarity': [None, [str, None]], + 'display_rarity': ['displayRarity', [str, None]], + 'backend_rarity': ['backendRarity', [str, None]], + 'name': [None, [str, None]], + 'short_description': ['shortDescription', [str, None]], + 'description': [None, [str, None]], + 'set': ['set', [str, None]], + 'set_text': ['setText', [str, None]], + 'series': [None, [str, None]], + 'backend_series': ['backendSeries', [str, None]], + 'has_small_icon': ['hasSmallIcon', [bool, None]], + 'has_icon': ['hasIcon', [bool, None]], + 'has_featured_image': ['hasFeaturedImage', [bool, None]], + 'has_background_image': ['hasBackgroundImage', [bool, None]], + 'has_cover_art': ['hasCoverArt', [bool, None]], + 'has_decal': ['hasDecal', [bool, None]], + 'has_variants': ['hasVariants', [bool, None]], + 'has_gameplay_tags': ['hasGameplayTags', [bool, None]], + 'gameplay_tag': ['gameplayTag', [str, None]] +} + def _parse_search_parameter(**search_parameters): - parameters = { - 'language': search_parameters.get('language', GameLanguage.ENGLISH).value, - 'searchLanguage': search_parameters.get('search_language', GameLanguage.ENGLISH).value, - 'matchMethod': search_parameters.get('match_method', MatchMethod.FULL).value - } - if search_parameters.__contains__('type'): - parameters['type'] = search_parameters['type'] - if search_parameters.__contains__('backend_type'): - parameters['backendType'] = search_parameters['backend_type'] - if search_parameters.__contains__('rarity'): - parameters['rarity'] = search_parameters['rarity'] - if search_parameters.__contains__('backend_rarity'): - parameters['backendRarity'] = search_parameters['backend_rarity'] - if search_parameters.__contains__('name'): - parameters['name'] = search_parameters['name'] - if search_parameters.__contains__('short_description'): - parameters['shortDescription'] = search_parameters['short_description'] - if search_parameters.__contains__('description'): - parameters['description'] = search_parameters['description'] - if search_parameters.__contains__('set'): - parameters['set'] = search_parameters['set'] - if search_parameters.__contains__('series'): - parameters['series'] = search_parameters['series'] - if search_parameters.__contains__('has_small_icon'): - parameters['hasSmallIcon'] = search_parameters['has_small_icon'] - if search_parameters.__contains__('has_icon'): - parameters['hasIcon'] = search_parameters['has_icon'] - if search_parameters.__contains__('has_featured_image'): - parameters['hasFeaturedImage'] = search_parameters['has_featured_image'] - if search_parameters.__contains__('has_background_image'): - parameters['hasBackgroundImage'] = search_parameters['has_background_image'] - if search_parameters.__contains__('has_cover_art'): - parameters['hasCoverArt'] = search_parameters['has_cover_art'] - if search_parameters.__contains__('has_decal'): - parameters['hasDecal'] = search_parameters['has_decal'] - if search_parameters.__contains__('has_variants'): - parameters['hasVariants'] = search_parameters['has_variants'] - if search_parameters.__contains__('has_gameplay_tags'): - parameters['hasGameplayTags'] = search_parameters['has_gameplay_tags'] - if search_parameters.__contains__('gameplay_tag'): - parameters['gameplayTag'] = search_parameters['gameplay_tag'] + parameters = {} # TODO: Empty string as search parameter + for key, value in search_parameters.items(): + search_parameter_data = _SEARCH_PARAMETERS.get(key) + if search_parameter_data is None: + continue + if type(value) not in search_parameter_data[1]: + types = ' or '.join([str(t) for t in search_parameter_data[1]]) + raise TypeError('{0} require a value of type {1}'.format(key, types)) + key = search_parameter_data[0] if search_parameter_data[0] else key + value = value if not isinstance(value, Enum) else value.value if value is not None else '' + parameters[key] = str(value) if value is not None else '' + if len(parameters) == 0: + raise MissingSearchParameter('at least one search parameter is required') return parameters @@ -67,8 +67,9 @@ def search_by_id(self, *cosmetic_id: str, language: GameLanguage = GameLanguage. cosmetic_ids = list(cosmetic_id) params = {'language': language.value} - if len(cosmetic_ids) < 1: - return None + if len(cosmetic_ids) == 0: + raise MissingIDParameter('at least one cosmetic id is required') + endpoint = 'cosmetics/br/search/ids' endpoint += '?id=' + cosmetic_ids[0] del cosmetic_ids[0] @@ -101,8 +102,9 @@ async def search_by_id(self, *cosmetic_id: str, language: GameLanguage = GameLan cosmetic_ids = list(cosmetic_id) params = {'language': language.value} - if len(cosmetic_ids) < 1: - return None + if len(cosmetic_ids) == 0: + raise MissingIDParameter('at least one cosmetic id is required') + endpoint = 'cosmetics/br/search/ids' endpoint += '?id=' + cosmetic_ids[0] del cosmetic_ids[0] @@ -116,10 +118,7 @@ async def search_all(self, **search_parameters) -> typing.List[BrCosmetic]: return [BrCosmetic(item_data) for item_data in data['data']] async def search_first(self, **search_parameters) -> BrCosmetic: - data = await self._client.http.get('cosmetics/br/search', - params=_parse_search_parameter(**search_parameters)) - if data['status'] == 400: - raise NotFound('The requested cosmetic was not found.') + data = await self._client.http.get('cosmetics/br/search', params=_parse_search_parameter(**search_parameters)) return BrCosmetic(data['data']) @@ -128,34 +127,27 @@ class SyncCreatorCodeEndpoints: def __init__(self, client): self._client = client - def fetch(self, creator_code: str) -> CreatorCode: - params = {'slug': creator_code} + def fetch(self, slug: str) -> CreatorCode: + params = {'slug': slug} data = self._client.http.get('creatorcode', params=params) - if data['status'] == 400: - raise NotFound('The requested Creator Code was not found.') return CreatorCode(data['data']) - def exists(self, creator_code: str) -> bool: + def exists(self, slug: str) -> bool: try: - self.fetch(creator_code) + self.fetch(slug) return True except NotFound: return False - def search_first(self, creator_code: str) -> CreatorCode: - params = {'slug': creator_code} + def search_first(self, slug: str) -> CreatorCode: + params = {'slug': slug} data = self._client.http.get('creatorcode/search', params=params) - if data['status'] == 400: - raise NotFound('The requested Creator Code was not found.') return CreatorCode(data['data']) - def search_all(self, creator_code: str) -> typing.List[CreatorCode]: - params = {'slug': creator_code} + def search_all(self, slug: str) -> typing.List[CreatorCode]: + params = {'slug': slug} data = self._client.http.get('creatorcode/search/all', params=params) - creator_codes = [CreatorCode(creator_code_data) for creator_code_data in data['data']] - if len(creator_codes) == 0: - raise NotFound('The requested Creator Code was not found.') - return creator_codes + return [CreatorCode(creator_code_data) for creator_code_data in data['data']] class AsyncCreatorCodeEndpoints: @@ -163,34 +155,27 @@ class AsyncCreatorCodeEndpoints: def __init__(self, client): self._client = client - async def fetch(self, creator_code: str) -> CreatorCode: - params = {'slug': creator_code} + async def fetch(self, slug: str) -> CreatorCode: + params = {'slug': slug} data = await self._client.http.get('creatorcode', params=params) - if data['status'] == 400: - raise NotFound('The requested Creator Code was not found.') return CreatorCode(data['data']) - async def exists(self, creator_code: str) -> bool: + async def exists(self, slug: str) -> bool: try: - await self.fetch(creator_code) + await self.fetch(slug) return True except NotFound: return False - async def search_first(self, creator_code: str) -> CreatorCode: - params = {'slug': creator_code} + async def search_first(self, slug: str) -> CreatorCode: + params = {'slug': slug} data = await self._client.http.get('creatorcode/search', params=params) - if data['status'] == 400: - raise NotFound('The requested Creator Code was not found.') return CreatorCode(data['data']) - async def search_all(self, creator_code: str) -> typing.List[CreatorCode]: - params = {'slug': creator_code} + async def search_all(self, slug: str) -> typing.List[CreatorCode]: + params = {'slug': slug} data = await self._client.http.get('creatorcode/search/all', params=params) - creator_codes = [CreatorCode(creator_code_data) for creator_code_data in data['data']] - if len(creator_codes) == 0: - raise NotFound('The requested Creator Code was not found.') - return creator_codes + return [CreatorCode(creator_code_data) for creator_code_data in data['data']] class SyncNewsEndpoints: diff --git a/fortnite_api/errors.py b/fortnite_api/errors.py index cb4ce8ae..51b46e72 100644 --- a/fortnite_api/errors.py +++ b/fortnite_api/errors.py @@ -1,22 +1,26 @@ -class NotFound(Exception): # Add to search by ID +class FortniteAPIException(Exception): pass -class MissingSearchParameter(Exception): +class NotFound(FortniteAPIException): pass -class MissingIDParameter(Exception): +class MissingSearchParameter(FortniteAPIException): pass -class ServerOutage(Exception): +class MissingIDParameter(FortniteAPIException): pass -class RateLimited(Exception): +class ServiceUnavailable(FortniteAPIException): pass -class Unauthorized(Exception): +class RateLimited(FortniteAPIException): + pass + + +class Unauthorized(FortniteAPIException): pass diff --git a/fortnite_api/http.py b/fortnite_api/http.py index edd665f5..874d93e4 100644 --- a/fortnite_api/http.py +++ b/fortnite_api/http.py @@ -1,9 +1,7 @@ -from json import JSONDecodeError - import aiohttp import requests -from .errors import ServerOutage, RateLimited, Unauthorized, NotFound +from .errors import ServiceUnavailable, RateLimited, Unauthorized, NotFound BASE_URL = 'https://fortnite-api.com/' @@ -21,24 +19,26 @@ def remove_header(self, key): def get(self, endpoint, params=None): response = requests.get(BASE_URL + endpoint, params=params, headers=self.headers) - try: - data = response.json() - if response.status_code == 401: - raise Unauthorized(data.get('error', 'Error message not provided!')) - elif response.status_code == 404: - raise NotFound(data.get('error', 'Error message not provided!')) - elif response.status_code == 429: - raise RateLimited(data.get('error', 'Error message not provided!')) + data = response.json() + if response.status_code == 200: return data - except JSONDecodeError: - raise ServerOutage('The Fortnite-API.com server is currently unavailable.') + elif response.status_code == 401: + raise Unauthorized(data.get('error', 'Error message not provided!')) + elif response.status_code == 404: + raise NotFound(data.get('error', 'Error message not provided!')) + elif response.status_code == 429: + raise RateLimited(data.get('error', 'Error message not provided!')) + elif response.status_code == 503: + raise ServiceUnavailable(data.get('error', 'Error message not provided!')) + else: + raise Exception(data.get('error', 'Error message not provided') + '. Status Code: {0}' + .format(str(response.status_code))) class AsyncHTTPClient: def __init__(self): self.headers = {} - self.session = aiohttp.ClientSession() def add_header(self, key, val): self.headers[key] = val @@ -47,16 +47,19 @@ def remove_header(self, key): return self.headers.pop(key) async def get(self, endpoint, params=None): - async with self.session.get(BASE_URL + endpoint, params=params, headers=self.headers) as response: - try: + async with aiohttp.ClientSession() as session: + async with session.get(BASE_URL + endpoint, params=params, headers=self.headers) as response: data = await response.json() + if response.status == 200: + return data if response.status == 401: raise Unauthorized(data.get('error', 'Error message not provided!')) elif response.status == 404: raise NotFound(data.get('error', 'Error message not provided!')) elif response.status == 429: raise RateLimited(data.get('error', 'Error message not provided!')) - return data - except aiohttp.ContentTypeError: - raise ServerOutage('The Fortnite-API.com server is currently unavailable.') - + elif response.status == 503: + raise ServiceUnavailable(data.get('error', 'Error message not provided!')) + else: + raise Exception(data.get('error', 'Error message not provided') + '. Status Code: {0}' + .format(str(response.status))) diff --git a/setup.py b/setup.py index cdddf513..10194dc0 100644 --- a/setup.py +++ b/setup.py @@ -33,8 +33,8 @@ long_description_content_type="text/markdown", install_requires=['requests>=2.22.0', 'aiohttp>=3.3.0,<3.6.0'], python_requires='>=3.5.3', - download_url='https://github.com/Fortnite-API/py-wrapper/archive/v1.0.2.tar.gz', - keywords=['fortnite', 'fortnite-api.com', 'shop', 'cosmetics'], + download_url='https://github.com/Fortnite-API/py-wrapper/archive/v1.0.3.tar.gz', + keywords=['fortnite', 'fortnite-api.com', 'shop', 'cosmetics', 'fortnite api', 'fortnite shop'], classifiers=[ 'License :: OSI Approved :: MIT License', 'Intended Audience :: Developers', @@ -43,6 +43,7 @@ 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Topic :: Internet', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Python Modules', diff --git a/tests.py b/tests.py index c7c3441f..994c5821 100644 --- a/tests.py +++ b/tests.py @@ -1,8 +1,28 @@ +import asyncio + import fortnite_api -from fortnite_api import MatchMethod -fn = fortnite_api.FortniteAPI('api_key') +api_key = 'ac54ca08e4715a72da4ee30eaf9086ef0c89bcbe32e7cc23fd3dee0c1ec82e32' + + +def sync_test(): + fn = fortnite_api.FortniteAPI(api_key) + print(fn.shop.fetch()) + print([[i.name, i.type.value] for i in fn.cosmetics.search_all(name='Drift')]) + print(fn.cosmetics.search_first(name='Drift').id) + print(fn.creator_code.search_first('EasyFnStats').user.name) + +async def async_test(): + fn = fortnite_api.FortniteAPI(api_key, run_async=True) + print(await fn.shop.fetch()) + print([[i.name, i.type.value] for i in (await fn.cosmetics.search_all(name='Drift'))]) + print((await fn.cosmetics.search_first(name='Drift')).id) + print((await fn.creator_code.search_first('EasyFnStats')).user.name) if __name__ == '__main__': - print([e.name for e in fn.cosmetics.search_all(name='ome', match_method=MatchMethod.STARTS)]) + print('------ Sync Tests ------') + sync_test() + loop = asyncio.get_event_loop() + print('------ Async Tests ------') + loop.run_until_complete(async_test())