├── .flake8 ├── .github └── workflows │ └── unit.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.md ├── pokebase ├── __init__.py ├── api.py ├── cache.py ├── common.py ├── interface.py └── loaders.py ├── requirements ├── base.txt └── test.txt ├── setup.py ├── tests ├── __init__.py ├── __main__.py ├── test_module_api.py ├── test_module_cache.py ├── test_module_common.py ├── test_module_interface.py ├── test_module_loaders.py └── test_with_api_calls.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=119 3 | exclude=.venv,__pycache__,venv,.direnv,tests/ 4 | ignore=F405,F403,S403,S301 5 | -------------------------------------------------------------------------------- /.github/workflows/unit.yml: -------------------------------------------------------------------------------- 1 | name: Python tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | unit: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ['3.8', '3.9.18', '3.10', '3.11', '3.12.0'] 11 | steps: 12 | - name: Clone repository 13 | uses: actions/checkout@v3 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies 19 | run: pip install -r requirements/test.txt 20 | - name: Run unit tests 21 | run: python -m tests -v 22 | 23 | style: 24 | runs-on: ubuntu-latest 25 | strategy: 26 | matrix: 27 | python-version: ['3.8'] 28 | steps: 29 | - name: Clone repository 30 | uses: actions/checkout@v3 31 | - name: Set up Python ${{ matrix.python-version }} 32 | uses: actions/setup-python@v4 33 | with: 34 | python-version: ${{ matrix.python-version }} 35 | - name: Install dependencies 36 | run: pip install -r requirements/test.txt 37 | - name: Check style 38 | run: flake8 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Virtual Environment 2 | .venv/ 3 | .envrc 4 | .direnv/ 5 | 6 | # Python file caches 7 | __pycache__/ 8 | **/*.pyc 9 | 10 | # Python setup.py distributions 11 | dist/ 12 | build/ 13 | *.egg-info/ 14 | 15 | # Hypothesis for testing 16 | .hypothesis/ 17 | 18 | # Local testing cache 19 | testing/ 20 | 21 | # Editor support files 22 | # PyCharm 23 | .idea/ 24 | # Visual Studio Code 25 | .vscode/ 26 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v3.2.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-added-large-files 9 | 10 | - repo: https://github.com/pre-commit/pygrep-hooks 11 | rev: v1.5.1 12 | hooks: 13 | - id: python-check-blanket-noqa 14 | 15 | - repo: https://github.com/pre-commit/mirrors-isort 16 | rev: v5.9.2 17 | hooks: 18 | - id: isort 19 | 20 | - repo: https://github.com/pycqa/flake8 21 | rev: 3.9.2 22 | hooks: 23 | - id: flake8 24 | additional_dependencies: 25 | - flake8-bandit~=2.1 26 | - flake8-isort~=4.0 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 Greg Hilmes 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE.txt 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | .SILENT: 3 | 4 | help: 5 | @grep -E '^[a-zA-Z_-]+:.*?# .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?# "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 6 | 7 | install: # Install base requirements to run project 8 | pip install -r requirements/base.txt 9 | 10 | dev-install: install # Install developer requirements + base requirements 11 | pip install -r requirements/test.txt 12 | 13 | test: # Test 14 | python -m tests 15 | 16 | build: # Build package 17 | python -m build 18 | 19 | publish: build # Publish on Pypi (https://packaging.python.org/en/latest/tutorials/packaging-projects/) 20 | python -m twine upload --repository pypi dist/* 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pokebase [![swampert](https://veekun.com/dex/media/pokemon/main-sprites/heartgold-soulsilver/260.png)](https://pokeapi.co/api/v2/pokemon/swampert) 2 | 3 | [![actions](https://github.com/PokeAPI/pokebase/actions/workflows/unit.yml/badge.svg)](https://github.com/PokeAPI/pokebase/actions/workflows/unit.yml) 4 | [![Python 3.6 pypi](https://img.shields.io/badge/Python%203.6%20pypi-1.3.0-blue.svg)](https://pypi.org/project/pokebase/1.3.0/) 5 | [![Python >=3.8 github](https://img.shields.io/badge/Python%20>=3.8%20github-1.4.1-blue.svg)](https://pypi.python.org/pypi/pokebase) 6 | 7 | `pokebase` is a simple but powerful Python interface to the [PokeAPI database](https://pokeapi.co/) 8 | 9 | Maintainer: [GregHilmes](https://github.com/GregHilmes) 10 | 11 | ## Installation 12 | 13 | ### Version Support 14 | 15 | pokebase 1.3.0 supports Python 3.6. Install it with `pip install 'pokebase==1.3.0'` 16 | 17 | pokebase 1.4.1 drops support for Python 3.6 and adds support for Python \>=3.8 \<=3.12. Install it with `pip install pokebase` 18 | 19 | ## Usage 20 | 21 | ```python console 22 | >>> import pokebase as pb 23 | >>> chesto = pb.APIResource('berry', 'chesto') 24 | >>> chesto.name 25 | 'chesto' 26 | >>> 27 | chesto.natural_gift_type.name 28 | 'water' 29 | >>> charmander = pb.pokemon('charmander') # Quick lookup. 30 | >>> charmander.height 31 | 6 32 | >>> # Now with sprites! (again!) 33 | >>> s1 = pb.SpriteResource('pokemon', 17) 34 | 35 | >>> s1.url 36 | '' 37 | >>> s2 = pb.SpriteResource('pokemon', 1, other=True, official_artwork=True) 38 | >>> s2.path 39 | '/home/user/.cache/pokebase/sprite/pokemon/other-sprites/official-artwork/1.png' 40 | >>> s3 = pb.SpriteResource('pokemon', 3, female=True, back=True) 41 | >>> s3.img_data b'x89PNGrnx1anx00x00x00rIHDRx00x00x00 ... xca^x7fxbbd\*x00x00x00x00IENDxaeB`x82' 42 | ``` 43 | 44 | ... And it's just that simple. 45 | 46 | ## Nomenclature 47 | 48 | > - an `endpoint` is the results of an API call like 49 | > `http://pokeapi.co/api/v2/berry` or 50 | > `http://pokeapi.co/api/v2/move` 51 | > - a `resource` is the actual data, from a call to 52 | > `http://pokeapi.co/api/v2/pokemon/1` 53 | 54 | ## Testing 55 | 56 | Python unit tests are in a separate `tests` directory and can be run via 57 | `python -m tests`. 58 | 59 | ### Notes to the developer using this module 60 | 61 | The quick data lookup for a Pokémon type, is `pokebase.type_('type-name')`, not `pokebase.type('type-name')`. This is because of a naming conflict with the built-in `type` function, were you to `from pokebase import *`. 62 | 63 | When changing the cache, avoid importing the cache constants directly. You should only import them with the whole cache module. If you do not do this, calling `set_cache` will not change your local copy of the variable. 64 | 65 | NOT THIS! 66 | 67 | ```python console 68 | >>> from pokebase.cache import API_CACHE 69 | ``` 70 | 71 | Do this :) 72 | 73 | ```python console 74 | >>> from pokebase import cache 75 | >>> cache.API_CACHE 76 | ``` 77 | -------------------------------------------------------------------------------- /pokebase/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .interface import APIMetadata, APIResource, APIResourceList, SpriteResource 4 | from .loaders import * 5 | 6 | __all__ = [ 7 | "APIResource", 8 | "APIMetadata", 9 | "APIResourceList", 10 | "SpriteResource", 11 | "ability", 12 | "berry", 13 | "berry_firmness", 14 | "berry_flavor", 15 | "characteristic", 16 | "contest_effect", 17 | "contest_type", 18 | "egg_group", 19 | "encounter_condition", 20 | "encounter_condition_value", 21 | "encounter_method", 22 | "evolution_chain", 23 | "evolution_trigger", 24 | "gender", 25 | "generation", 26 | "growth_rate", 27 | "item", 28 | "item_attribute", 29 | "item_category", 30 | "item_fling_effect", 31 | "item_pocket", 32 | "language", 33 | "location", 34 | "location_area", 35 | "machine", 36 | "move", 37 | "move_ailment", 38 | "move_battle_style", 39 | "move_category", 40 | "move_damage_class", 41 | "move_learn_method", 42 | "move_target", 43 | "nature", 44 | "pal_park_area", 45 | "pokeathlon_stat", 46 | "pokedex", 47 | "pokemon", 48 | "pokemon_color", 49 | "pokemon_form", 50 | "pokemon_habitat", 51 | "pokemon_shape", 52 | "pokemon_species", 53 | "region", 54 | "stat", 55 | "super_contest_effect", 56 | "type_", 57 | "version", 58 | "version_group", 59 | ] 60 | -------------------------------------------------------------------------------- /pokebase/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import requests 4 | 5 | from .cache import get_sprite_path, load, load_sprite, save, save_sprite 6 | from .common import api_url_build, sprite_url_build 7 | 8 | 9 | def _call_api(endpoint, resource_id=None, subresource=None): 10 | url = api_url_build(endpoint, resource_id, subresource) 11 | 12 | # Get a list of resources at the endpoint, if no resource_id is given. 13 | get_endpoint_list = resource_id is None 14 | 15 | response = requests.get(url) 16 | response.raise_for_status() 17 | 18 | data = response.json() 19 | 20 | if get_endpoint_list and data["count"] != len(data["results"]): 21 | # We got a section of all results; we want ALL of them. 22 | items = data["count"] 23 | num_items = dict(limit=items) 24 | 25 | response = requests.get(url, params=num_items) 26 | response.raise_for_status() 27 | 28 | data = response.json() 29 | 30 | return data 31 | 32 | 33 | def get_data(endpoint, resource_id=None, subresource=None, **kwargs): 34 | if not kwargs.get("force_lookup", False): 35 | try: 36 | data = load(endpoint, resource_id, subresource) 37 | return data 38 | except KeyError: 39 | pass 40 | 41 | data = _call_api(endpoint, resource_id, subresource) 42 | save(data, endpoint, resource_id, subresource) 43 | 44 | return data 45 | 46 | 47 | def _call_sprite_api(sprite_type, sprite_id, **kwargs): 48 | url = sprite_url_build(sprite_type, sprite_id, **kwargs) 49 | 50 | response = requests.get(url) 51 | response.raise_for_status() 52 | 53 | abs_path = get_sprite_path(sprite_type, sprite_id, **kwargs) 54 | data = dict(img_data=response.content, path=abs_path) 55 | 56 | return data 57 | 58 | 59 | def get_sprite(sprite_type, sprite_id, **kwargs): 60 | if not kwargs.get("force_lookup", False): 61 | try: 62 | data = load_sprite(sprite_type, sprite_id, **kwargs) 63 | return data 64 | except FileNotFoundError: 65 | pass 66 | 67 | data = _call_sprite_api(sprite_type, sprite_id, **kwargs) 68 | save_sprite(data, sprite_type, sprite_id, **kwargs) 69 | 70 | return data 71 | -------------------------------------------------------------------------------- /pokebase/cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import shelve 5 | 6 | from .common import cache_uri_build, sprite_filepath_build 7 | 8 | # Cache locations will be set at the end of this file. 9 | CACHE_DIR = None 10 | API_CACHE = None 11 | SPRITE_CACHE = None 12 | 13 | 14 | def save(data, endpoint, resource_id=None, subresource=None): 15 | 16 | if data == dict(): # No point in saving empty data. 17 | return None 18 | 19 | if not isinstance(data, (dict, list)): 20 | raise ValueError("Could not save non-dict data") 21 | 22 | uri = cache_uri_build(endpoint, resource_id, subresource) 23 | 24 | try: 25 | with shelve.open(API_CACHE) as cache: 26 | cache[uri] = data 27 | except OSError as error: 28 | if error.errno == 11: # Cache open by another person/program 29 | # print('Cache unavailable, skipping save') 30 | pass 31 | else: 32 | raise error 33 | 34 | return None 35 | 36 | 37 | def save_sprite(data, sprite_type, sprite_id, **kwargs): 38 | 39 | abs_path = data["path"] 40 | 41 | # Make intermediate directories; this line removes the file+extension. 42 | dirs = abs_path.rpartition(os.path.sep)[0] 43 | safe_make_dirs(dirs) 44 | 45 | with open(abs_path, "wb") as img_file: 46 | img_file.write(data["img_data"]) 47 | 48 | return None 49 | 50 | 51 | def load(endpoint, resource_id=None, subresource=None): 52 | 53 | uri = cache_uri_build(endpoint, resource_id, subresource) 54 | 55 | try: 56 | with shelve.open(API_CACHE) as cache: 57 | return cache[uri] 58 | except OSError as error: 59 | if error.errno == 11: 60 | # Cache open by another person/program 61 | # print('Cache unavailable, skipping load') 62 | raise KeyError("Cache could not be opened.") 63 | else: 64 | raise 65 | 66 | 67 | def load_sprite(sprite_type, sprite_id, **kwargs): 68 | abs_path = get_sprite_path(sprite_type, sprite_id, **kwargs) 69 | 70 | with open(abs_path, "rb") as img_file: 71 | img_data = img_file.read() 72 | 73 | return dict(img_data=img_data, path=abs_path) 74 | 75 | 76 | def safe_make_dirs(path, mode=0o777): 77 | """Create a leaf directory and all intermediate ones in a safe way. 78 | 79 | A wrapper to os.makedirs() that handles existing leaf directories while 80 | avoiding os.path.exists() race conditions. 81 | 82 | :param path: relative or absolute directory tree to create 83 | :param mode: directory permissions in octal 84 | :return: The newly-created path 85 | """ 86 | try: 87 | os.makedirs(path, mode) 88 | except OSError as error: 89 | if error.errno != 17: # File exists 90 | raise 91 | 92 | return path 93 | 94 | 95 | def get_default_cache(): 96 | """Get the default cache location. 97 | 98 | Adheres to the XDG Base Directory specification, as described in 99 | https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html 100 | 101 | :return: the default cache directory absolute path 102 | """ 103 | 104 | xdg_cache_home = os.environ.get("XDG_CACHE_HOME") or os.path.join( 105 | os.path.expanduser("~"), ".cache" 106 | ) 107 | 108 | return os.path.join(xdg_cache_home, "pokebase") 109 | 110 | 111 | def get_sprite_path(sprite_type, sprite_id, **kwargs): 112 | rel_filepath = sprite_filepath_build(sprite_type, sprite_id, **kwargs) 113 | abs_path = os.path.join(SPRITE_CACHE, rel_filepath) 114 | 115 | return abs_path 116 | 117 | 118 | def set_cache(new_path=None): 119 | """Simple function to change the cache location. 120 | 121 | `new_path` can be an absolute or relative path. If the directory does not 122 | exist yet, this function will create it. If None it will set the cache to 123 | the default cache directory. 124 | 125 | If you are going to change the cache directory, this function should be 126 | called at the top of your script, before you make any calls to the API. 127 | This is to avoid duplicate files and excess API calls. 128 | 129 | :param new_path: relative or absolute path to the desired new cache 130 | directory 131 | :return: str, str 132 | """ 133 | 134 | global CACHE_DIR, API_CACHE, SPRITE_CACHE 135 | 136 | if new_path is None: 137 | new_path = get_default_cache() 138 | 139 | CACHE_DIR = safe_make_dirs(os.path.abspath(new_path)) 140 | API_CACHE = os.path.join(CACHE_DIR, "api.cache") 141 | SPRITE_CACHE = safe_make_dirs(os.path.join(CACHE_DIR, "sprite")) 142 | 143 | return CACHE_DIR, API_CACHE, SPRITE_CACHE 144 | 145 | 146 | CACHE_DIR, API_CACHE, SPRITE_CACHE = set_cache() 147 | -------------------------------------------------------------------------------- /pokebase/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | 5 | BASE_URL = "http://pokeapi.co/api/v2" 6 | SPRITE_URL = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites" 7 | ENDPOINTS = [ 8 | "ability", 9 | "berry", 10 | "berry-firmness", 11 | "berry-flavor", 12 | "characteristic", 13 | "contest-effect", 14 | "contest-type", 15 | "egg-group", 16 | "encounter-condition", 17 | "encounter-condition-value", 18 | "encounter-method", 19 | "evolution-chain", 20 | "evolution-trigger", 21 | "gender", 22 | "generation", 23 | "growth-rate", 24 | "item", 25 | "item-attribute", 26 | "item-category", 27 | "item-fling-effect", 28 | "item-pocket", 29 | "language", 30 | "location", 31 | "location-area", 32 | "machine", 33 | "move", 34 | "move-ailment", 35 | "move-battle-style", 36 | "move-category", 37 | "move-damage-class", 38 | "move-learn-method", 39 | "move-target", 40 | "nature", 41 | "pal-park-area", 42 | "pokeathlon-stat", 43 | "pokedex", 44 | "pokemon", 45 | "pokemon-color", 46 | "pokemon-form", 47 | "pokemon-habitat", 48 | "pokemon-shape", 49 | "pokemon-species", 50 | "region", 51 | "stat", 52 | "super-contest-effect", 53 | "type", 54 | "version", 55 | "version-group", 56 | ] 57 | SPRITE_EXT = "png" 58 | 59 | 60 | def validate(endpoint, resource_id=None): 61 | if endpoint not in ENDPOINTS: 62 | raise ValueError("Unknown API endpoint '{}'".format(endpoint)) 63 | 64 | if resource_id is not None and not isinstance(resource_id, int): 65 | 66 | raise ValueError("Bad id '{}'".format(resource_id)) 67 | 68 | return None 69 | 70 | 71 | def api_url_build(endpoint, resource_id=None, subresource=None): 72 | validate(endpoint, resource_id) 73 | 74 | if resource_id is not None: 75 | if subresource is not None: 76 | return "/".join([BASE_URL, endpoint, str(resource_id), subresource, ""]) 77 | 78 | return "/".join([BASE_URL, endpoint, str(resource_id), ""]) 79 | 80 | return "/".join([BASE_URL, endpoint, ""]) 81 | 82 | 83 | def cache_uri_build(endpoint, resource_id=None, subresource=None): 84 | validate(endpoint, resource_id) 85 | 86 | if resource_id is not None: 87 | if subresource is not None: 88 | return "/".join([endpoint, str(resource_id), subresource, ""]) 89 | 90 | return "/".join([endpoint, str(resource_id), ""]) 91 | 92 | return "/".join([endpoint, ""]) 93 | 94 | 95 | def sprite_url_build(sprite_type, sprite_id, **kwargs): 96 | options = parse_sprite_options(sprite_type, **kwargs) 97 | 98 | filename = ".".join([str(sprite_id), SPRITE_EXT]) 99 | url = "/".join([SPRITE_URL, sprite_type, *options, filename]) 100 | 101 | return url 102 | 103 | 104 | def sprite_filepath_build(sprite_type, sprite_id, **kwargs): 105 | """returns the filepath of the sprite *relative to SPRITE_CACHE*""" 106 | 107 | options = parse_sprite_options(sprite_type, **kwargs) 108 | 109 | filename = ".".join([str(sprite_id), SPRITE_EXT]) 110 | filepath = os.path.join(sprite_type, *options, filename) 111 | 112 | return filepath 113 | 114 | 115 | def parse_sprite_options(sprite_type, **kwargs): 116 | options = [] 117 | 118 | if sprite_type == "pokemon": 119 | if kwargs.get("model", False): 120 | options.append("model") 121 | elif kwargs.get("other", False): 122 | options.append("other") 123 | if kwargs.get("official_artwork", False): 124 | options.append("official-artwork") 125 | if kwargs.get("dream_world", False): 126 | options.append("dream-world") 127 | else: 128 | if kwargs.get("back", False): 129 | options.append("back") 130 | if kwargs.get("shiny", False): 131 | options.append("shiny") 132 | if kwargs.get("female", False): 133 | options.append("female") 134 | elif sprite_type == "items": 135 | if kwargs.get("berries", False): 136 | options.append("berries") 137 | elif kwargs.get("dream_world", False): 138 | options.append("dream-world") 139 | elif kwargs.get("gen3", False): 140 | options.append("gen3") 141 | elif kwargs.get("gen5", False): 142 | options.append("gen5") 143 | elif kwargs.get("underground", False): 144 | options.append("underground") 145 | 146 | return options 147 | -------------------------------------------------------------------------------- /pokebase/interface.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .api import get_data, get_sprite 4 | from .common import api_url_build, sprite_url_build 5 | 6 | 7 | def _make_obj(obj): 8 | """Takes an object and returns a corresponding API class. 9 | 10 | The names and values of the data will match exactly with those found 11 | in the online docs at https://pokeapi.co/docsv2/ . In some cases, the data 12 | may be of a standard type, such as an integer or string. For those cases, 13 | the input value is simply returned, unchanged. 14 | 15 | :param obj: the object to be converted 16 | :return either the same value, if it does not need to be converted, or a 17 | APIResource or APIMetadata instance, depending on the data inputted. 18 | """ 19 | 20 | def change_sprite_key(d): 21 | new_dict = {} 22 | for k, v in d.items(): 23 | if isinstance(v, dict): 24 | v = change_sprite_key(v) 25 | new_dict[k.replace('-', '_')] = v 26 | return new_dict 27 | 28 | if isinstance(obj, dict): 29 | if "url" in obj.keys(): 30 | url = obj["url"] 31 | id_ = int(url.split("/")[-2]) # ID of the data. 32 | endpoint = url.split("/")[-3] # Where the data is located. 33 | return APIResource(endpoint, id_, lazy_load=True) 34 | if all(k in obj for k in ("other", "back_default")): 35 | obj = change_sprite_key(obj) # Change hyphens in sprite keys to underscores 36 | 37 | return APIMetadata(obj) 38 | 39 | return obj 40 | 41 | 42 | def name_id_convert(endpoint, name_or_id): 43 | if isinstance(name_or_id, int): 44 | id_ = name_or_id 45 | name = _convert_id_to_name(endpoint, id_) 46 | 47 | elif isinstance(name_or_id, str): 48 | name = name_or_id 49 | id_ = _convert_name_to_id(endpoint, name) 50 | 51 | else: 52 | raise ValueError(f"the name or id '{name_or_id}' could not be converted") 53 | 54 | return name, id_ 55 | 56 | 57 | def _convert_id_to_name(endpoint, id_): 58 | resource_data = get_data(endpoint)["results"] 59 | 60 | for resource in resource_data: 61 | if resource["url"].split("/")[-2] == str(id_): 62 | 63 | # Return the matching name, or id_ if it doesn't exsist. 64 | return resource.get("name", str(id_)) 65 | 66 | return None 67 | 68 | 69 | def _convert_name_to_id(endpoint, name): 70 | resource_data = get_data(endpoint)["results"] 71 | 72 | for resource in resource_data: 73 | if resource.get("name") == name: 74 | return int(resource.get("url").split("/")[-2]) 75 | 76 | return None 77 | 78 | 79 | class APIResource(object): 80 | """Core API class, used for accessing the bulk of the data. 81 | 82 | The class uses a modified __getattr__ function to serve the appropriate 83 | data, so lookup data via the `.` operator, and use the `PokeAPI docs 84 | `_ or the builtin `dir` function to see the 85 | possible lookups. 86 | 87 | This class takes the complexity out of lots of similar classes for each 88 | different kind of data served by the API, all of which are very similar, 89 | but not identical. 90 | """ 91 | 92 | def __init__( 93 | self, endpoint, name_or_id, lazy_load=False, force_lookup=False, custom=None 94 | ): 95 | 96 | name, id_ = name_id_convert(endpoint, name_or_id) 97 | url = api_url_build(endpoint, id_) 98 | 99 | self.__dict__.update({"name": name, "endpoint": endpoint, "id_": id_, "url": url}) 100 | 101 | self.__loaded = False 102 | self.__force_lookup = force_lookup 103 | 104 | if custom: 105 | self._custom = custom 106 | else: 107 | self._custom = {} 108 | 109 | if not lazy_load: 110 | self._load() 111 | self.__loaded = True 112 | 113 | def __getattr__(self, attr): 114 | """Modified method to auto-load the data when it is needed. 115 | 116 | If the data has not yet been looked up, it is loaded, and then checked 117 | for the requested attribute. If it is not found, AttributeError is 118 | raised. 119 | """ 120 | 121 | if not self.__loaded: 122 | self._load() 123 | self.__loaded = True 124 | 125 | return self.__getattribute__(attr) 126 | 127 | else: 128 | raise AttributeError(f"{type(self)} object has no attribute {attr}") 129 | 130 | def __str__(self): 131 | return str(self.name) 132 | 133 | def __repr__(self): 134 | return f"<{self.endpoint}-{self.name}>" 135 | 136 | def _load(self): 137 | """Function to collect reference data and connect it to the instance as 138 | attributes. 139 | 140 | Internal function, does not usually need to be called by the user, as 141 | it is called automatically when an attribute is requested. 142 | 143 | :return None 144 | """ 145 | 146 | data = get_data(self.endpoint, self.id_, force_lookup=self.__force_lookup) 147 | 148 | # Make our custom objects from the data. 149 | for key, val in data.items(): 150 | if key in self._custom: 151 | val = get_data(*self._custom[key](val)) 152 | 153 | if isinstance(val, dict): 154 | data[key] = _make_obj(val) 155 | 156 | elif isinstance(val, list): 157 | data[key] = [_make_obj(i) for i in val] 158 | 159 | self.__dict__.update(data) 160 | 161 | return None 162 | 163 | 164 | class APIResourceList(object): 165 | """Class for a data container. 166 | 167 | Used to access data corresponding to a category, rather than an individual 168 | reference. Ex. APIResourceList('berry') gives information about all 169 | berries, such as which ID's correspond to which berry names, and 170 | how many berries there are. 171 | 172 | You can iterate through all the names or all the urls, using the respective 173 | properties. You can also iterate on the object itself to run through the 174 | `dict`s with names and urls together, whatever floats your boat. 175 | """ 176 | 177 | def __init__(self, endpoint, force_lookup=False): 178 | """Creates a new APIResourceList instance. 179 | 180 | :param name: the name of the resource to get (ex. 'berry' or 'move') 181 | """ 182 | 183 | response = get_data(endpoint, force_lookup=force_lookup) 184 | 185 | self.name = endpoint 186 | self.__results = [i for i in response["results"]] 187 | self.count = response["count"] 188 | 189 | def __len__(self): 190 | return self.count 191 | 192 | def __iter__(self): 193 | return iter(self.__results) 194 | 195 | def __str__(self): 196 | return str(self.__results) 197 | 198 | @property 199 | def names(self): 200 | """Useful iterator for all the resource's names.""" 201 | for result in self.__results: 202 | yield result.get("name", result["url"].split("/")[-2]) 203 | 204 | @property 205 | def urls(self): 206 | """Useful iterator for all of the resource's urls.""" 207 | for result in self.__results: 208 | yield result["url"] 209 | 210 | 211 | class APIMetadata(object): 212 | """Helper class for smaller references. 213 | 214 | This class emulates a dictionary, but attribute lookup is via the `.` 215 | operator, not indexing. (ex. instance.attr, not instance['attr']). 216 | 217 | Used for "Common Models" classes and APIResource helper classes. 218 | https://pokeapi.co/docsv2/#common-models 219 | """ 220 | 221 | def __init__(self, data): 222 | 223 | for key, val in data.items(): 224 | 225 | if isinstance(val, dict): 226 | data[key] = _make_obj(val) 227 | 228 | if isinstance(val, list): 229 | data[key] = [_make_obj(i) for i in val] 230 | 231 | self.__dict__.update(data) 232 | 233 | 234 | class SpriteResource(object): 235 | def __init__(self, sprite_type, sprite_id, **kwargs): 236 | 237 | url = sprite_url_build(sprite_type, sprite_id, **kwargs) 238 | 239 | self.__dict__.update( 240 | {"sprite_id": sprite_id, "sprite_type": sprite_type, "url": url} 241 | ) 242 | 243 | self.__loaded = False 244 | self.__force_lookup = kwargs.get("force_lookup", False) 245 | self.__orginal_kwargs = kwargs 246 | 247 | if not kwargs.get("lazy_load", False): 248 | self._load() 249 | self.__loaded = True 250 | 251 | def _load(self): 252 | data = get_sprite(self.sprite_type, self.sprite_id, **self.__orginal_kwargs) 253 | self.__dict__.update(data) 254 | 255 | return None 256 | 257 | def __getattr__(self, attr): 258 | """Modified method to auto-load the data when it is needed. 259 | 260 | If the data has not yet been looked up, it is loaded, and then checked 261 | for the requested attribute. If it is not found, AttributeError is 262 | raised. 263 | """ 264 | 265 | if not self.__loaded: 266 | self._load() 267 | self.__loaded = True 268 | 269 | return self.__getattribute__(attr) 270 | 271 | else: 272 | raise AttributeError(f"{type(self)} object has no attribute {attr}") 273 | -------------------------------------------------------------------------------- /pokebase/loaders.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .interface import APIResource, SpriteResource 4 | 5 | 6 | def berry(id_or_name, **kwargs): 7 | """Quick berry lookup. 8 | 9 | See https://pokeapi.co/docsv2/#berries for attributes and more detailed 10 | information. 11 | 12 | :param id_or_name: id or name of the resource to lookup 13 | :return: NamedAPIResource with the appropriate data 14 | """ 15 | return APIResource("berry", id_or_name, **kwargs) 16 | 17 | 18 | def berry_firmness(id_or_name, **kwargs): 19 | """Quick berry-firmness lookup. 20 | 21 | See https://pokeapi.co/docsv2/#berry-firmnesses for attributes and more 22 | detailed information. 23 | 24 | :param id_or_name: id or name of the resource to lookup 25 | :return: NamedAPIResource with the appropriate data 26 | """ 27 | return APIResource("berry-firmness", id_or_name, **kwargs) 28 | 29 | 30 | def berry_flavor(id_or_name, **kwargs): 31 | """Quick berry-flavor lookup. 32 | 33 | See https://pokeapi.co/docsv2/#berry-flavors for attributes and more 34 | detailed information. 35 | 36 | :param id_or_name: id or name of the resource to lookup 37 | :return: NamedAPIResource with the appropriate data 38 | """ 39 | return APIResource("berry-flavor", id_or_name, **kwargs) 40 | 41 | 42 | def contest_type(id_or_name, **kwargs): 43 | """Quick contest-type lookup. 44 | 45 | See https://pokeapi.co/docsv2/#contest-types for attributes and more 46 | detailed information. 47 | 48 | :param id_or_name: id or name of the resource to lookup 49 | :return: NamedAPIResource with the appropriate data 50 | """ 51 | return APIResource("contest-type", id_or_name, **kwargs) 52 | 53 | 54 | def contest_effect(id_, **kwargs): 55 | """Quick contest-effect lookup. 56 | 57 | See https://pokeapi.co/docsv2/#contest-effects for attributes and more 58 | detailed information. 59 | 60 | :param id_: id of the resource to lookup 61 | :return: NamedAPIResource with the appropriate data 62 | """ 63 | return APIResource("contest-effect", id_, **kwargs) 64 | 65 | 66 | def super_contest_effect(id_, **kwargs): 67 | """Quick super-contest-effect lookup. 68 | 69 | See https://pokeapi.co/docsv2/#super-contest-effects for attributes and 70 | more detailed information. 71 | 72 | :param id_: id of the resource to lookup 73 | :return: NamedAPIResource with the appropriate data 74 | """ 75 | return APIResource("super-contest-effect", id_, **kwargs) 76 | 77 | 78 | def encounter_method(id_or_name, **kwargs): 79 | """Quick encounter-method lookup. 80 | 81 | See https://pokeapi.co/docsv2/#encounter-methods for attributes and more 82 | detailed information. 83 | 84 | :param id_or_name: id or name of the resource to lookup 85 | :return: NamedAPIResource with the appropriate data 86 | """ 87 | return APIResource("encounter-method", id_or_name, **kwargs) 88 | 89 | 90 | def encounter_condition(id_or_name, **kwargs): 91 | """Quick encounter-condition lookup. 92 | 93 | See https://pokeapi.co/docsv2/#encounter-conditions for attributes and more 94 | detailed information. 95 | 96 | :param id_or_name: id or name of the resource to lookup 97 | :return: NamedAPIResource with the appropriate data 98 | """ 99 | return APIResource("encounter-condition", id_or_name, **kwargs) 100 | 101 | 102 | def encounter_condition_value(id_or_name, **kwargs): 103 | """Quick encounter-condition-value lookup. 104 | 105 | See https://pokeapi.co/docsv2/#encounter-condition-values for attributes 106 | and more detailed information. 107 | 108 | :param id_or_name: id or name of the resource to lookup 109 | :return: NamedAPIResource with the appropriate data 110 | """ 111 | return APIResource("encounter-condition-value", id_or_name, **kwargs) 112 | 113 | 114 | def evolution_chain(id_, **kwargs): 115 | """Quick evolution-chain lookup. 116 | 117 | See https://pokeapi.co/docsv2/#evolution-chains for attributes and more 118 | detailed information. 119 | 120 | :param id_: id of the resource to lookup 121 | :return: NamedAPIResource with the appropriate data 122 | """ 123 | return APIResource("evolution-chain", id_, **kwargs) 124 | 125 | 126 | def evolution_trigger(id_or_name, **kwargs): 127 | """Quick evolution-trigger lookup. 128 | 129 | See https://pokeapi.co/docsv2/#evolution-triggers for attributes and more 130 | detailed information. 131 | 132 | :param id_or_name: id or name of the resource to lookup 133 | :return: NamedAPIResource with the appropriate data 134 | """ 135 | return APIResource("evolution-trigger", id_or_name, **kwargs) 136 | 137 | 138 | def generation(id_or_name, **kwargs): 139 | """Quick generation lookup. 140 | 141 | See https://pokeapi.co/docsv2/#generations for attributes and more detailed 142 | information. 143 | 144 | :param id_or_name: id or name of the resource to lookup 145 | :return: NamedAPIResource with the appropriate data 146 | """ 147 | return APIResource("generation", id_or_name, **kwargs) 148 | 149 | 150 | def pokedex(id_or_name, **kwargs): 151 | """Quick pokedex lookup. 152 | 153 | See https://pokeapi.co/docsv2/#pokedexes for attributes and more detailed 154 | information. 155 | 156 | :param id_or_name: id or name of the resource to lookup 157 | :return: NamedAPIResource with the appropriate data 158 | """ 159 | return APIResource("pokedex", id_or_name, **kwargs) 160 | 161 | 162 | def version(id_or_name, **kwargs): 163 | """Quick version lookup. 164 | 165 | See https://pokeapi.co/docsv2/#versions for attributes and more detailed 166 | information. 167 | 168 | :param id_or_name: id or name of the resource to lookup 169 | :return: NamedAPIResource with the appropriate data 170 | """ 171 | return APIResource("version", id_or_name, **kwargs) 172 | 173 | 174 | def version_group(id_or_name, **kwargs): 175 | """Quick version-group lookup. 176 | 177 | See https://pokeapi.co/docsv2/#version-groups for attributes and more 178 | detailed information. 179 | 180 | :param id_or_name: id or name of the resource to lookup 181 | :return: NamedAPIResource with the appropriate data 182 | """ 183 | return APIResource("version-group", id_or_name, **kwargs) 184 | 185 | 186 | def item(id_or_name, **kwargs): 187 | """Quick item lookup. 188 | 189 | See https://pokeapi.co/docsv2/#items for attributes and more detailed 190 | information. 191 | 192 | :param id_or_name: id or name of the resource to lookup 193 | :return: NamedAPIResource with the appropriate data 194 | """ 195 | return APIResource("item", id_or_name, **kwargs) 196 | 197 | 198 | def item_attribute(id_or_name, **kwargs): 199 | """Quick item-attribute lookup. 200 | 201 | See https://pokeapi.co/docsv2/#item-attributes for attributes and more 202 | detailed information. 203 | 204 | :param id_or_name: id or name of the resource to lookup 205 | :return: NamedAPIResource with the appropriate data 206 | """ 207 | return APIResource("item-attribute", id_or_name, **kwargs) 208 | 209 | 210 | def item_category(id_or_name, **kwargs): 211 | """Quick item-category lookup. 212 | 213 | See https://pokeapi.co/docsv2/#item-categories for attributes and more 214 | detailed information. 215 | 216 | :param id_or_name: id or name of the resource to lookup 217 | :return: NamedAPIResource with the appropriate data 218 | """ 219 | return APIResource("item-category", id_or_name, **kwargs) 220 | 221 | 222 | def item_fling_effect(id_or_name, **kwargs): 223 | """Quick item-fling-effect lookup. 224 | 225 | See https://pokeapi.co/docsv2/#item-fling-effects for attributes and more 226 | detailed information. 227 | 228 | :param id_or_name: id or name of the resource to lookup 229 | :return: NamedAPIResource with the appropriate data 230 | """ 231 | return APIResource("item-fling-effect", id_or_name, **kwargs) 232 | 233 | 234 | def item_pocket(id_or_name, **kwargs): 235 | """Quick item-pocket lookup. 236 | 237 | See https://pokeapi.co/docsv2/#item-pockets for attributes and more 238 | detailed information. 239 | 240 | :param id_or_name: id or name of the resource to lookup 241 | :return: NamedAPIResource with the appropriate data 242 | """ 243 | return APIResource("item-pocket", id_or_name, **kwargs) 244 | 245 | 246 | def machine(id_, **kwargs): 247 | """Quick machine lookup. 248 | 249 | See https://pokeapi.co/docsv2/#machines for attributes and more detailed 250 | information. 251 | 252 | :param id_: id of the resource to lookup 253 | :return: NamedAPIResource with the appropriate data 254 | """ 255 | return APIResource("machine", id_, **kwargs) 256 | 257 | 258 | def move(id_or_name, **kwargs): 259 | """Quick move lookup. 260 | 261 | See https://pokeapi.co/docsv2/#moves for attributes and more detailed 262 | information. 263 | 264 | :param id_or_name: id or name of the resource to lookup 265 | :return: NamedAPIResource with the appropriate data 266 | """ 267 | return APIResource("move", id_or_name, **kwargs) 268 | 269 | 270 | def move_ailment(id_or_name, **kwargs): 271 | """Quick move-ailment lookup. 272 | 273 | See https://pokeapi.co/docsv2/#move-ailments for attributes and more 274 | detailed information. 275 | 276 | :param id_or_name: id or name of the resource to lookup 277 | :return: NamedAPIResource with the appropriate data 278 | """ 279 | return APIResource("move-ailment", id_or_name, **kwargs) 280 | 281 | 282 | def move_battle_style(id_or_name, **kwargs): 283 | """Quick move-battle-style lookup. 284 | 285 | See https://pokeapi.co/docsv2/#move-battle-styles for attributes and more 286 | detailed information. 287 | 288 | :param id_or_name: id or name of the resource to lookup 289 | :return: NamedAPIResource with the appropriate data 290 | """ 291 | return APIResource("move-battle-style", id_or_name, **kwargs) 292 | 293 | 294 | def move_category(id_or_name, **kwargs): 295 | """Quick move-category lookup. 296 | 297 | See https://pokeapi.co/docsv2/#move-categories for attributes and more 298 | detailed information. 299 | 300 | :param id_or_name: id or name of the resource to lookup 301 | :return: NamedAPIResource with the appropriate data 302 | """ 303 | return APIResource("move-category", id_or_name, **kwargs) 304 | 305 | 306 | def move_damage_class(id_or_name, **kwargs): 307 | """Quick move-damage-class lookup. 308 | 309 | See https://pokeapi.co/docsv2/#move-damage-classes for attributes and more 310 | detailed information. 311 | 312 | :param id_or_name: id or name of the resource to lookup 313 | :return: NamedAPIResource with the appropriate data 314 | """ 315 | return APIResource("move-damage-class", id_or_name, **kwargs) 316 | 317 | 318 | def move_learn_method(id_or_name, **kwargs): 319 | """Quick move-learn-method lookup. 320 | 321 | See https://pokeapi.co/docsv2/#move-learn-methods for attributes and more 322 | detailed information. 323 | 324 | :param id_or_name: id or name of the resource to lookup 325 | :return: NamedAPIResource with the appropriate data 326 | """ 327 | return APIResource("move-learn-method", id_or_name, **kwargs) 328 | 329 | 330 | def move_target(id_or_name, **kwargs): 331 | """Quick move-target lookup. 332 | 333 | See https://pokeapi.co/docsv2/#move-targets for attributes and more 334 | detailed information. 335 | 336 | :param id_or_name: id or name of the resource to lookup 337 | :return: NamedAPIResource with the appropriate data 338 | """ 339 | return APIResource("move-target", id_or_name, **kwargs) 340 | 341 | 342 | def location(id_, **kwargs): 343 | """Quick location lookup. 344 | 345 | See https://pokeapi.co/docsv2/#locations for attributes and more detailed 346 | information. 347 | 348 | :param id_: id of the resource to lookup 349 | :return: NamedAPIResource with the appropriate data 350 | """ 351 | return APIResource("location", id_, **kwargs) 352 | 353 | 354 | def location_area(id_, **kwargs): 355 | """Quick location-area lookup. 356 | 357 | See https://pokeapi.co/docsv2/#location-areas for attributes and more 358 | detailed information. 359 | 360 | :param id_: id of the resource to lookup 361 | :return: NamedAPIResource with the appropriate data 362 | """ 363 | return APIResource("location-area", id_, **kwargs) 364 | 365 | 366 | def pal_park_area(id_or_name, **kwargs): 367 | """Quick pal-park-area lookup. 368 | 369 | See https://pokeapi.co/docsv2/#pal-park-areas for attributes and more 370 | detailed information. 371 | 372 | :param id_or_name: id or name of the resource to lookup 373 | :return: NamedAPIResource with the appropriate data 374 | """ 375 | return APIResource("pal-park-area", id_or_name, **kwargs) 376 | 377 | 378 | def region(id_or_name, **kwargs): 379 | """Quick region lookup. 380 | 381 | See https://pokeapi.co/docsv2/#regions for attributes and more detailed 382 | information. 383 | 384 | :param id_or_name: id or name of the resource to lookup 385 | :return: NamedAPIResource with the appropriate data 386 | """ 387 | return APIResource("region", id_or_name, **kwargs) 388 | 389 | 390 | def ability(id_or_name, **kwargs): 391 | """Quick ability lookup. 392 | 393 | See https://pokeapi.co/docsv2/#abilities for attributes and more detailed 394 | information. 395 | 396 | :param id_or_name: id or name of the resource to lookup 397 | :return: NamedAPIResource with the appropriate data 398 | """ 399 | return APIResource("ability", id_or_name, **kwargs) 400 | 401 | 402 | def characteristic(id_, **kwargs): 403 | """Quick characteristic lookup. 404 | 405 | See https://pokeapi.co/docsv2/#characteristics for attributes and more 406 | detailed information. 407 | 408 | :param id_: id of the resource to lookup 409 | :return: NamedAPIResource with the appropriate data 410 | """ 411 | return APIResource("characteristic", id_, **kwargs) 412 | 413 | 414 | def egg_group(id_or_name, **kwargs): 415 | """Quick egg-group lookup. 416 | 417 | See https://pokeapi.co/docsv2/#egg-groups for attributes and more detailed 418 | information. 419 | 420 | :param id_or_name: id or name of the resource to lookup 421 | :return: NamedAPIResource with the appropriate data 422 | """ 423 | return APIResource("egg-group", id_or_name, **kwargs) 424 | 425 | 426 | def gender(id_or_name, **kwargs): 427 | """Quick gender lookup. 428 | 429 | See https://pokeapi.co/docsv2/#genders for attributes and more detailed 430 | information. 431 | 432 | :param id_or_name: id or name of the resource to lookup 433 | :return: NamedAPIResource with the appropriate data 434 | """ 435 | return APIResource("gender", id_or_name, **kwargs) 436 | 437 | 438 | def growth_rate(id_or_name, **kwargs): 439 | """Quick growth-rate lookup. 440 | 441 | See https://pokeapi.co/docsv2/#growth-rates for attributes and more 442 | detailed information. 443 | 444 | :param id_or_name: id or name of the resource to lookup 445 | :return: NamedAPIResource with the appropriate data 446 | """ 447 | return APIResource("growth-rate", id_or_name, **kwargs) 448 | 449 | 450 | def nature(id_or_name, **kwargs): 451 | """Quick nature lookup. 452 | 453 | See https://pokeapi.co/docsv2/#natures for attributes and more detailed 454 | information. 455 | 456 | :param id_or_name: id or name of the resource to lookup 457 | :return: NamedAPIResource with the appropriate data 458 | """ 459 | return APIResource("nature", id_or_name, **kwargs) 460 | 461 | 462 | def pokeathlon_stat(id_or_name, **kwargs): 463 | """Quick pokeathlon-stat lookup. 464 | 465 | See https://pokeapi.co/docsv2/#pokeathlon-stats for attributes and more 466 | detailed information. 467 | 468 | :param id_or_name: id or name of the resource to lookup 469 | :return: NamedAPIResource with the appropriate data 470 | """ 471 | return APIResource("pokeathlon-stat", id_or_name, **kwargs) 472 | 473 | 474 | def pokemon(id_or_name, **kwargs): 475 | """Quick pokemon lookup. 476 | 477 | See https://pokeapi.co/docsv2/#pokemon for attributes and more detailed 478 | information. 479 | 480 | :param id_or_name: id or name of the resource to lookup 481 | :return: NamedAPIResource with the appropriate data 482 | """ 483 | 484 | def get_location_area_encounters(val): 485 | params = val.split("/")[-3:] 486 | params[1] = int(params[1]) 487 | return params 488 | 489 | return APIResource( 490 | "pokemon", 491 | id_or_name, 492 | custom={"location_area_encounters": get_location_area_encounters}, 493 | **kwargs 494 | ) 495 | 496 | 497 | def pokemon_color(id_or_name, **kwargs): 498 | """Quick pokemon-color lookup. 499 | 500 | See https://pokeapi.co/docsv2/#pokemon-colors for attributes and more 501 | detailed information. 502 | 503 | :param id_or_name: id or name of the resource to lookup 504 | :return: NamedAPIResource with the appropriate data 505 | """ 506 | return APIResource("pokemon-color", id_or_name, **kwargs) 507 | 508 | 509 | def pokemon_form(id_or_name, **kwargs): 510 | """Quick pokemon-form lookup. 511 | 512 | See https://pokeapi.co/docsv2/#pokemon-forms for attributes and more 513 | detailed information. 514 | 515 | :param id_or_name: id or name of the resource to lookup 516 | :return: NamedAPIResource with the appropriate data 517 | """ 518 | return APIResource("pokemon-form", id_or_name, **kwargs) 519 | 520 | 521 | def pokemon_habitat(id_or_name, **kwargs): 522 | """Quick pokemon-habitat lookup. 523 | 524 | See https://pokeapi.co/docsv2/#pokemon-habitats for attributes and more 525 | detailed information. 526 | 527 | :param id_or_name: id or name of the resource to lookup 528 | :return: NamedAPIResource with the appropriate data 529 | """ 530 | return APIResource("pokemon-habitat", id_or_name, **kwargs) 531 | 532 | 533 | def pokemon_shape(id_or_name, **kwargs): 534 | """Quick pokemon-shape lookup. 535 | 536 | See https://pokeapi.co/docsv2/#pokemon-shapes for attributes and more 537 | detailed information. 538 | 539 | :param id_or_name: id or name of the resource to lookup 540 | :return: NamedAPIResource with the appropriate data 541 | """ 542 | return APIResource("pokemon-shape", id_or_name, **kwargs) 543 | 544 | 545 | def pokemon_species(id_or_name, **kwargs): 546 | """Quick pokemon-species lookup. 547 | 548 | See https://pokeapi.co/docsv2/#pokemon-species for attributes and more 549 | detailed information. 550 | 551 | :param id_or_name: id or name of the resource to lookup 552 | :return: NamedAPIResource with the appropriate data 553 | """ 554 | 555 | def get_evolution_chain(val): 556 | params = val["url"].split("/")[-3:-1] 557 | params[1] = int(params[1]) 558 | return params 559 | 560 | return APIResource( 561 | "pokemon-species", 562 | id_or_name, 563 | custom={"evolution_chain": get_evolution_chain}, 564 | **kwargs 565 | ) 566 | 567 | 568 | def stat(id_or_name, **kwargs): 569 | """Quick stat lookup. 570 | 571 | See https://pokeapi.co/docsv2/#stats for attributes and more detailed 572 | information. 573 | 574 | :param id_or_name: id or name of the resource to lookup 575 | :return: NamedAPIResource with the appropriate data 576 | """ 577 | return APIResource("stat", id_or_name, **kwargs) 578 | 579 | 580 | def type_(id_or_name, **kwargs): 581 | """Quick type lookup. 582 | 583 | See https://pokeapi.co/docsv2/#types for attributes and more detailed 584 | information. 585 | 586 | :param id_or_name: id or name of the resource to lookup 587 | :return: NamedAPIResource with the appropriate data 588 | """ 589 | return APIResource("type", id_or_name, **kwargs) 590 | 591 | 592 | def language(id_or_name, **kwargs): 593 | """Quick language lookup. 594 | 595 | See https://pokeapi.co/docsv2/#languages for attributes and more detailed 596 | information. 597 | 598 | :param id_or_name: id or name of the resource to lookup 599 | :return: NamedAPIResource with the appropriate data 600 | """ 601 | return APIResource("language", id_or_name, **kwargs) 602 | 603 | 604 | def sprite(sprite_type, sprite_id, **kwargs): 605 | return SpriteResource(sprite_type, sprite_id, **kwargs) 606 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | certifi==2023.11.17 2 | chardet==5.2.0 3 | idna==3.6 4 | requests==2.31.1 5 | urllib3==2.1.0 6 | isort~=5.13.1 7 | pre-commit~=3.5.0 8 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | attrs==23.1.0 2 | certifi==2023.11.17 3 | chardet==5.2.0 4 | idna==3.6 5 | requests==2.31.0 6 | urllib3==2.1.0 7 | coverage==7.3.2 8 | hypothesis==6.92.0 9 | flake8==6.1.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | 4 | def readme_text(): 5 | with open('README.md') as f: 6 | return f.read() 7 | 8 | 9 | setuptools.setup( 10 | name='pokebase', 11 | packages=['pokebase'], 12 | version='1.4.1', 13 | description='A Python wrapper for the friendly PokeAPI database', 14 | long_description=readme_text(), 15 | long_description_content_type='text/markdown', 16 | author='Greg Hilmes', 17 | author_email='99hilmes.g@gmail.com', 18 | url='https://github.com/PokeAPI/pokebase', 19 | keywords=['database', 'pokemon', 'wrapper'], 20 | install_requires=['requests'], 21 | license='BSD License', 22 | requires_python=">=3.8", 23 | classifiers=[ 24 | 'License :: OSI Approved :: BSD License', 25 | 'Programming Language :: Python :: 3.8', 26 | 'Programming Language :: Python :: 3.9', 27 | 'Programming Language :: Python :: 3.10', 28 | 'Programming Language :: Python :: 3.11', 29 | 'Programming Language :: Python :: 3.12' 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokeAPI/pokebase/d40c95ce41e62e1d97cefa4ad4ee8d0500c423d9/tests/__init__.py -------------------------------------------------------------------------------- /tests/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import unittest 5 | 6 | from .test_module_api import * 7 | from .test_module_cache import * 8 | from .test_module_common import * 9 | from .test_module_interface import * 10 | from .test_module_loaders import * 11 | from .test_with_api_calls import * 12 | 13 | unittest.main(argv=sys.argv) 14 | -------------------------------------------------------------------------------- /tests/test_module_api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import shelve 4 | import unittest 5 | from unittest.mock import patch 6 | 7 | from hypothesis import assume, given 8 | from hypothesis.strategies import dictionaries, integers, none, sampled_from, text 9 | from requests.exceptions import HTTPError 10 | 11 | from pokebase import api, cache 12 | from pokebase.cache import save, set_cache 13 | from pokebase.common import ENDPOINTS, cache_uri_build 14 | 15 | 16 | class TestFunction__call_api(unittest.TestCase): 17 | 18 | # _call_api(endpoint, resource_id) 19 | 20 | def setUp(self): 21 | set_cache('testing') 22 | 23 | @given(endpoint=sampled_from(ENDPOINTS), 24 | resource_id=(integers(min_value=1))) 25 | @patch('pokebase.api.requests.get') 26 | def testArgs(self, mock_get, endpoint, resource_id): 27 | 28 | mock_get.return_value.json.return_value = {'id': resource_id} 29 | 30 | self.assertEqual(api._call_api(endpoint, resource_id)['id'], 31 | resource_id) 32 | 33 | @given(endpoint=text(), 34 | resource_id=(integers(min_value=1))) 35 | def testArg_endpoint_Text(self, endpoint, resource_id): 36 | with self.assertRaises(ValueError): 37 | api._call_api(endpoint, resource_id) 38 | 39 | @given(endpoint=sampled_from(ENDPOINTS), 40 | resource_id=none()) 41 | @patch('pokebase.api.requests.get') 42 | def testArg_resource_id_None(self, mock_get, endpoint, resource_id): 43 | 44 | mock_get.return_value.json.return_value = {'count': 100, 'results': ['some', 'reults']} 45 | 46 | self.assertIsNotNone(api._call_api(endpoint, resource_id).get('count')) 47 | 48 | @given(endpoint=sampled_from(ENDPOINTS), 49 | resource_id=integers(min_value=1), 50 | subresource=text()) 51 | @patch('pokebase.api.requests.get') 52 | def testArg_subresource_Text(self, mock_get, endpoint, resource_id, subresource): 53 | mock_get.return_value.json.return_value = {'version_details': 'foo'} 54 | 55 | self.assertIsNotNone(api._call_api(endpoint, resource_id, subresource).get('version_details')) 56 | 57 | @given(endpoint=sampled_from(ENDPOINTS), 58 | resource_id=(integers(min_value=1))) 59 | @patch('pokebase.api.requests.get') 60 | def testEnv_ErrorResponse(self, mock_get, endpoint, resource_id): 61 | mock_get.return_value.raise_for_status.side_effect = HTTPError() 62 | 63 | with self.assertRaises(HTTPError): 64 | api._call_api(endpoint, resource_id) 65 | 66 | 67 | class TestFunction_get_data(unittest.TestCase): 68 | 69 | # get_data(endpoint, resource_id) 70 | 71 | def setUp(self): 72 | set_cache('testing') 73 | 74 | @given(data=dictionaries(text(), text()), 75 | endpoint=sampled_from(ENDPOINTS), 76 | resource_id=integers(min_value=1)) 77 | def testArgs_GettingCachedData(self, data, endpoint, resource_id): 78 | assume(data != dict()) 79 | # save some data to the cache 80 | save(data, endpoint, resource_id) 81 | self.assertEqual(data, api.get_data(endpoint, resource_id)) 82 | 83 | @given(data=dictionaries(text(), text()), 84 | endpoint=sampled_from(ENDPOINTS), 85 | resource_id=integers(min_value=1)) 86 | @patch('pokebase.api.requests.get') 87 | def testArgs_GettingNoncachedData(self, mock_get, data, endpoint, resource_id): 88 | 89 | mock_get.return_value.json.return_value = data 90 | 91 | # assert that the data is not in the cache 92 | with shelve.open(cache.API_CACHE) as cache_file: 93 | key = cache_uri_build(endpoint, resource_id) 94 | if key in cache_file.keys(): 95 | del cache_file[key] 96 | 97 | self.assertEqual(data, api.get_data(endpoint, resource_id)) 98 | 99 | @given(endpoint=sampled_from(ENDPOINTS), 100 | resource_id=integers(min_value=1), 101 | subresource=text()) 102 | @patch('pokebase.api.requests.get') 103 | def testArg_subresource_Text(self, mock_get, endpoint, resource_id, subresource): 104 | mock_get.return_value.json.return_value = {'version_details': 'foo'} 105 | 106 | self.assertIsNotNone(api.get_data(endpoint, resource_id, subresource).get('version_details')) 107 | 108 | @given(endpoint=text(), 109 | resource_id=integers(min_value=1)) 110 | def testArg_endpoint_Text(self, endpoint, resource_id): 111 | with self.assertRaises(ValueError): 112 | api.get_data(endpoint, resource_id) 113 | 114 | @given(endpoint=sampled_from(ENDPOINTS), 115 | resource_id=text()) 116 | def testArg_resource_id_Text(self, endpoint, resource_id): 117 | with self.assertRaises(ValueError): 118 | api.get_data(endpoint, resource_id) 119 | -------------------------------------------------------------------------------- /tests/test_module_cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import importlib 4 | import os 5 | import shelve 6 | import unittest 7 | 8 | from hypothesis import assume, given 9 | from hypothesis.strategies import characters, dictionaries, integers, sampled_from, text 10 | 11 | from pokebase import cache 12 | from pokebase.common import ENDPOINTS 13 | 14 | 15 | class TestFunction_save(unittest.TestCase): 16 | 17 | # cache.save(data, endpoint, resource_id=None) 18 | 19 | def setUp(self): 20 | cache.set_cache('testing') 21 | 22 | @given(data=dictionaries(text(), text()), 23 | endpoint=sampled_from(ENDPOINTS), 24 | resource_id=integers(min_value=1)) 25 | def testArgs(self, data, endpoint, resource_id): 26 | self.assertIsNone(cache.save(data, endpoint, resource_id)) 27 | 28 | @given(data=text(), 29 | endpoint=sampled_from(ENDPOINTS), 30 | resource_id=integers(min_value=1)) 31 | def testArg_data_Text(self, data, endpoint, resource_id): 32 | with self.assertRaises(ValueError): 33 | cache.save(data, endpoint, resource_id) 34 | 35 | @given(data=dictionaries(text(), text()), 36 | endpoint=text(), 37 | resource_id=integers(min_value=1)) 38 | def testArg_endpoint_Text(self, data, endpoint, resource_id): 39 | assume(data != dict()) 40 | with self.assertRaises(ValueError): 41 | cache.save(data, endpoint, resource_id) 42 | 43 | @given(data=dictionaries(text(), text()), 44 | endpoint=sampled_from(ENDPOINTS), 45 | resource_id=text()) 46 | def testArg_resource_id_Text(self, data, endpoint, resource_id): 47 | assume(data != dict()) 48 | with self.assertRaises(ValueError): 49 | cache.save(data, endpoint, resource_id) 50 | 51 | @given(data=dictionaries(text(), text()), 52 | endpoint=sampled_from(ENDPOINTS), 53 | resource_id=integers(min_value=1), 54 | subresource=text()) 55 | def testArg_subresource_Text(self, data, endpoint, resource_id, subresource): 56 | self.assertIsNone(cache.save(data, endpoint, resource_id, subresource)) 57 | 58 | @given(data=dictionaries(text(), text()), 59 | endpoint=sampled_from(ENDPOINTS), 60 | resource_id=integers(min_value=1)) 61 | def testEnv_CacheFileNotFound(self, data, endpoint, resource_id): 62 | assume(data != dict()) 63 | os.remove(cache.API_CACHE) 64 | self.assertIsNone(cache.save(data, endpoint, resource_id)) 65 | 66 | @given(data=dictionaries(text(), text()), 67 | endpoint=sampled_from(ENDPOINTS), 68 | resource_id=integers(min_value=1)) 69 | def testEnd_CacheFileAlreadyOpen(self, data, endpoint, resource_id): 70 | cache_db = shelve.open(cache.API_CACHE) 71 | self.assertIsNone(cache.save(data, endpoint, resource_id)) 72 | cache_db.close() 73 | 74 | 75 | class TestFunction_load(unittest.TestCase): 76 | 77 | # cache.load(endpoint, resource_id=None) 78 | 79 | def setUp(self): 80 | cache.set_cache('testing') 81 | 82 | @given(data=dictionaries(text(), text()), 83 | endpoint=sampled_from(ENDPOINTS), 84 | resource_id=integers(min_value=1)) 85 | def testArgs(self, data, endpoint, resource_id): 86 | assume(data != dict()) 87 | cache.save(data, endpoint, resource_id) 88 | self.assertEqual(data, cache.load(endpoint, resource_id)) 89 | 90 | @given(endpoint=text(), 91 | resource_id=integers(min_value=1)) 92 | def testArg_endpoint_Text(self, endpoint, resource_id): 93 | with self.assertRaises(ValueError): 94 | cache.load(endpoint, resource_id) 95 | 96 | @given(endpoint=sampled_from(ENDPOINTS), 97 | resource_id=text()) 98 | def testArg_resource_id_Text(self, endpoint, resource_id): 99 | with self.assertRaises(ValueError): 100 | cache.load(endpoint, resource_id) 101 | 102 | @given(data=dictionaries(text(), text()), 103 | endpoint=sampled_from(ENDPOINTS), 104 | resource_id=integers(min_value=1), 105 | subresource=text()) 106 | def testArg_subresource_Text(self, data, endpoint, resource_id, subresource): 107 | assume(data != dict()) 108 | cache.save(data, endpoint, resource_id, subresource) 109 | self.assertEqual(data, cache.load(endpoint, resource_id, subresource)) 110 | 111 | @given(endpoint=sampled_from(ENDPOINTS), 112 | resource_id=integers(min_value=1)) 113 | def testEnv_CacheFileNotFound(self, endpoint, resource_id): 114 | # ensure it exsists before we delete it, 115 | cache.set_cache('testing') 116 | os.remove(cache.API_CACHE) 117 | with self.assertRaises(KeyError): 118 | cache.load(endpoint, resource_id) 119 | 120 | @given(endpoint=sampled_from(ENDPOINTS), 121 | resource_id=integers(min_value=1)) 122 | @unittest.skip('inconsistency between Travis-CI/local machine') 123 | def testEnv_CacheFileAlreadyOpen(self, endpoint, resource_id): 124 | cache_db = shelve.open(cache.API_CACHE) 125 | with self.assertRaises(KeyError): 126 | cache.load(endpoint, resource_id) 127 | cache_db.close() 128 | 129 | @given(endpoint=sampled_from(ENDPOINTS), 130 | resource_id=integers(min_value=1)) 131 | def testEnv_KeyNotInCache(self, endpoint, resource_id): 132 | 133 | with shelve.open(cache.API_CACHE) as c: 134 | key = cache.cache_uri_build(endpoint, resource_id) 135 | if key in c: 136 | del c[key] 137 | 138 | with self.assertRaises(KeyError): 139 | cache.load(endpoint, resource_id) 140 | 141 | 142 | class TestFunction_set_cache(unittest.TestCase): 143 | 144 | # cache.set_cache(new_path=None) 145 | 146 | default_home = os.path.join(os.path.expanduser('~'), '.cache') 147 | 148 | def testAttr_Caches_Import(self): 149 | importlib.reload(cache) 150 | self.assertEqual(os.path.join(self.default_home, 'pokebase'), 151 | cache.get_default_cache()) 152 | self.assertEqual(os.path.join(self.default_home, 'pokebase'), 153 | cache.CACHE_DIR) 154 | self.assertEqual(os.path.join(self.default_home, 'pokebase', 'api.cache'), 155 | cache.API_CACHE) 156 | self.assertEqual(os.path.join(self.default_home, 'pokebase', 'sprite'), 157 | cache.SPRITE_CACHE) 158 | 159 | def testAttr_Caches_Default(self): 160 | cache_dir, api_cache, sprite_cache = cache.set_cache() 161 | self.assertEqual(os.path.join(self.default_home, 'pokebase'), 162 | cache_dir) 163 | self.assertEqual(os.path.join(self.default_home, 'pokebase', 'api.cache'), 164 | api_cache) 165 | self.assertEqual(os.path.join(self.default_home, 'pokebase', 'sprite'), 166 | sprite_cache) 167 | 168 | def testEnv_CacheDirNotFound(self): 169 | cache.set_cache('testing') 170 | os.rmdir(cache.SPRITE_CACHE) 171 | if os.path.exists(cache.API_CACHE): os.remove(cache.API_CACHE) 172 | os.rmdir(cache.CACHE_DIR) 173 | self.assertEqual(cache.set_cache(), 174 | (cache.CACHE_DIR, cache.API_CACHE, cache.SPRITE_CACHE)) 175 | 176 | @given(new_path=characters(whitelist_categories=['Lu', 'Ll', 'Nd'])) 177 | def testAttr_Caches_PathsChanged(self, new_path): 178 | cache.set_cache(new_path) 179 | self.assertEqual(os.path.abspath(new_path), 180 | cache.CACHE_DIR) 181 | self.assertEqual(os.path.join(os.path.abspath(new_path), 'api.cache'), 182 | cache.API_CACHE) 183 | self.assertEqual(os.path.join(os.path.abspath(new_path), 'sprite'), 184 | cache.SPRITE_CACHE) 185 | os.rmdir(cache.SPRITE_CACHE) 186 | os.rmdir(cache.CACHE_DIR) 187 | -------------------------------------------------------------------------------- /tests/test_module_common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | 5 | from hypothesis import given 6 | from hypothesis.strategies import integers, none, sampled_from, text 7 | 8 | from pokebase import common 9 | 10 | 11 | class TestFunction_validate(unittest.TestCase): 12 | 13 | @given(endpoint=sampled_from(common.ENDPOINTS), 14 | resource_id=none()) 15 | def testArg_endpoint_Sampled(self, endpoint, resource_id): 16 | self.assertIsNone(common.validate(endpoint, resource_id)) 17 | 18 | @given(endpoint=sampled_from(common.ENDPOINTS), 19 | resource_id=integers(min_value=1)) 20 | def testArg_resource_id_NonNegInt(self, endpoint, resource_id): 21 | self.assertIsNone(common.validate(endpoint, resource_id)) 22 | 23 | @given(endpoint=text(), 24 | resource_id=none()) 25 | def testArg_endpoint_Text(self, endpoint, resource_id): 26 | with self.assertRaises(ValueError): 27 | common.validate(endpoint, resource_id) 28 | 29 | @given(endpoint=sampled_from(common.ENDPOINTS), 30 | resource_id=text()) 31 | def testArg_resource_id_Text(self, endpoint, resource_id): 32 | with self.assertRaises(ValueError): 33 | common.validate(endpoint, resource_id) 34 | 35 | 36 | class TestFunction_api_uri_build(unittest.TestCase): 37 | 38 | @given(endpoint=sampled_from(common.ENDPOINTS), 39 | resource_id=integers(min_value=1)) 40 | def testArg_resource_id_NonNegInt(self, endpoint, resource_id): 41 | self.assertEqual(common.api_url_build(endpoint, resource_id), 42 | 'http://pokeapi.co/api/v2/{}/{}/' 43 | .format(endpoint, resource_id)) 44 | 45 | @given(endpoint=sampled_from(common.ENDPOINTS), 46 | resource_id=none()) 47 | def testArg_resource_id_None(self, endpoint, resource_id): 48 | self.assertEqual(common.api_url_build(endpoint, resource_id), 49 | 'http://pokeapi.co/api/v2/{}/' 50 | .format(endpoint)) 51 | 52 | @given(endpoint=text(), 53 | resource_id=none()) 54 | def testArg_endpoint_Text(self, endpoint, resource_id): 55 | with self.assertRaises(ValueError): 56 | common.api_url_build(endpoint, resource_id) 57 | 58 | @given(endpoint=sampled_from(common.ENDPOINTS), 59 | resource_id=integers(min_value=1), 60 | subresource=text()) 61 | def testArg_subresource_Text(self, endpoint, resource_id, subresource): 62 | self.assertEqual(common.api_url_build(endpoint, resource_id, subresource), 63 | 'http://pokeapi.co/api/v2/{}/{}/{}/' 64 | .format(endpoint, resource_id, subresource)) 65 | 66 | 67 | class TestFunction_cache_uri_build(unittest.TestCase): 68 | 69 | @given(endpoint=sampled_from(common.ENDPOINTS), 70 | resource_id=integers(min_value=1)) 71 | def testArgs(self, endpoint, resource_id): 72 | self.assertEqual(common.cache_uri_build(endpoint, resource_id), 73 | '{}/{}/'.format(endpoint, resource_id)) 74 | 75 | @given(endpoint=sampled_from(common.ENDPOINTS), 76 | resource_id=none()) 77 | def testArg_resource_id_None(self, endpoint, resource_id): 78 | self.assertEqual(common.cache_uri_build(endpoint, resource_id), 79 | '/'.join([endpoint, ''])) 80 | 81 | @given(endpoint=text(), 82 | resource_id=none()) 83 | def testArg_endpoint_Text(self, endpoint, resource_id): 84 | with self.assertRaises(ValueError): 85 | common.cache_uri_build(endpoint, resource_id) 86 | 87 | @given(endpoint=sampled_from(common.ENDPOINTS), 88 | resource_id=integers(min_value=1), 89 | subresource=text()) 90 | def testArg_subresource_Text(self, endpoint, resource_id, subresource): 91 | self.assertEqual(common.cache_uri_build(endpoint, resource_id, subresource), 92 | '/'.join([endpoint, str(resource_id), subresource, ''])) 93 | -------------------------------------------------------------------------------- /tests/test_module_interface.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | from unittest.mock import patch 5 | 6 | from hypothesis import given 7 | from hypothesis.strategies import dictionaries, integers, lists, sampled_from, text 8 | 9 | from pokebase import interface 10 | from pokebase.cache import set_cache 11 | from pokebase.common import ENDPOINTS 12 | from pokebase.loaders import pokemon 13 | 14 | 15 | class TestFunction__make_obj(unittest.TestCase): 16 | 17 | # _make_obj(obj) 18 | 19 | def setUp(self): 20 | set_cache('testing') 21 | 22 | @given(obj=dictionaries(text(), text())) 23 | def testArg_obj_Dictionary(self, obj): 24 | self.assertIsInstance(interface._make_obj(obj), interface.APIMetadata) 25 | 26 | @given(endpoint=sampled_from(ENDPOINTS), 27 | resource_id=integers(min_value=1), 28 | obj=dictionaries(text(), text())) 29 | @patch('pokebase.interface.get_data') 30 | def testArg_obj_DictionaryWithUrl(self, mock_get_data, endpoint, resource_id, obj): 31 | 32 | mock_get_data.return_value = {'count': 1, 'results': [{'url': 'mocked.url/api/v2/{}/{}/'.format(endpoint, resource_id)}]} 33 | 34 | obj['url'] = 'http://base.url/{}/{}/'.format(endpoint, resource_id) 35 | self.assertIsInstance(interface._make_obj(obj), interface.APIResource) 36 | 37 | @given(obj=lists(elements=text())) 38 | def testArg_obj_List(self, obj): 39 | self.assertEqual(obj, interface._make_obj(obj)) 40 | 41 | @given(obj=text()) 42 | def testArg_obj_Str(self, obj): 43 | self.assertEqual(obj, interface._make_obj(obj)) 44 | 45 | 46 | class TestFunction__convert_id_to_name(unittest.TestCase): 47 | 48 | # _convert_id_to_name(endpoint, id_) 49 | 50 | def setUp(self): 51 | set_cache('testing') 52 | 53 | @given(name=text(), 54 | endpoint=sampled_from(ENDPOINTS), 55 | id_=integers(min_value=1)) 56 | @patch('pokebase.interface.get_data') 57 | def testArgs(self, mock_get_data, name, endpoint, id_): 58 | 59 | mock_get_data.return_value = {'count': 1, 'results': [{'url': 'mocked.url/api/v2/{}/{}/'.format(endpoint, id_), 'name': name}]} 60 | 61 | self.assertEqual(name, interface._convert_id_to_name(endpoint, id_)) 62 | 63 | @given(endpoint=sampled_from(ENDPOINTS), 64 | id_=integers(min_value=1)) 65 | @patch('pokebase.interface.get_data') 66 | def testEnv_NotNamedResource(self, mock_get_data, endpoint, id_): 67 | 68 | mock_get_data.return_value = {'count': 1, 'results': [{'url': 'mocked.url/api/v2/{}/{}/'.format(endpoint, id_)}]} 69 | 70 | self.assertEqual(str(id_), interface._convert_id_to_name(endpoint, id_)) 71 | 72 | @given(endpoint=text(), 73 | id_=integers(min_value=1)) 74 | @patch('pokebase.interface.get_data') 75 | def testArg_endpoint_Text(self, mock_get_data, endpoint, id_): 76 | 77 | mock_get_data.side_effect = ValueError() 78 | 79 | with self.assertRaises(ValueError): 80 | interface._convert_id_to_name(endpoint, id_) 81 | 82 | 83 | class TestFunction__convert_name_to_id(unittest.TestCase): 84 | 85 | # _convert_name_to_id(endpoint, name) 86 | @given(id_=integers(min_value=1), 87 | endpoint=sampled_from(ENDPOINTS), 88 | name=text()) 89 | @patch('pokebase.interface.get_data') 90 | def testArgs(self, mock_get_data, id_, endpoint, name): 91 | 92 | mock_get_data.return_value = {'count': 1, 'results': [{'url': 'mocked.url/api/v2/{}/{}/'.format(endpoint, id_), 'name': name}]} 93 | 94 | self.assertEqual(id_, interface._convert_name_to_id(endpoint, name)) 95 | 96 | @given(endpoint=text(), 97 | name=text()) 98 | @patch('pokebase.interface.get_data') 99 | def testArg_endpoint_Text(self, mock_get_data, endpoint, name): 100 | 101 | mock_get_data.side_effect = ValueError() 102 | 103 | with self.assertRaises(ValueError): 104 | interface._convert_id_to_name(endpoint, name) 105 | 106 | 107 | class TestFunction_name_id_convert(unittest.TestCase): 108 | 109 | # name_id_convert(endpont, name_or_id) 110 | 111 | @given(name=text(), 112 | endpoint=sampled_from(ENDPOINTS), 113 | id_=integers(min_value=1)) 114 | @patch('pokebase.interface.get_data') 115 | def testArgs_WithID(self, mock_get_data, name, endpoint, id_): 116 | 117 | mock_get_data.return_value = {'count': 1, 'results': [{'url': 'mocked.url/api/v2/{}/{}/'.format(endpoint, id_), 'name': name}]} 118 | 119 | self.assertEqual((name, id_), interface.name_id_convert(endpoint, id_)) 120 | 121 | @given(id_=integers(min_value=1), 122 | endpoint=sampled_from(ENDPOINTS), 123 | name=text()) 124 | @patch('pokebase.interface.get_data') 125 | def testArgs_WithName(self, mock_get_data, id_, endpoint, name): 126 | 127 | mock_get_data.return_value = {'count': 1, 'results': [{'url': 'mocked.url/api/v2/{}/{}/'.format(endpoint, id_), 'name': name}]} 128 | 129 | self.assertEqual((name, id_), interface.name_id_convert(endpoint, id_)) 130 | 131 | @given(endpoint=text(), 132 | name=text()) 133 | @patch('pokebase.interface.get_data') 134 | def testArg_endpoint_Text(self, mock_get_data, endpoint, name): 135 | 136 | mock_get_data.side_effect = ValueError() 137 | 138 | with self.assertRaises(ValueError): 139 | interface.name_id_convert(endpoint, name) 140 | 141 | 142 | class TestClass_APIResource(unittest.TestCase): 143 | 144 | # APIResource(endpoint, name_or_id, lazy_load=True) 145 | 146 | def setUp(self): 147 | set_cache('testing') 148 | 149 | @given(id_=integers(min_value=1), 150 | endpoint=sampled_from(ENDPOINTS), 151 | name=text()) 152 | @patch('pokebase.interface.get_data') 153 | def testArgs_WithNameWithLazyLoad(self, mock_get_data, id_, endpoint, name): 154 | 155 | mock_get_data.return_value = {'count': 1, 'results': [{'url': 'mocked.url/api/v2/{}/{}/'.format(endpoint, id_), 'name': name}]} 156 | 157 | self.assertIsInstance(interface.APIResource(endpoint, name), 158 | interface.APIResource) 159 | 160 | @given(name=text(), 161 | endpoint=sampled_from(ENDPOINTS), 162 | id_=integers(min_value=1)) 163 | @patch('pokebase.interface.get_data') 164 | def testArgs_WithIDWithLazyLoad(self, mock_get_data, name, endpoint, id_): 165 | 166 | mock_get_data.return_value = {'count': 1, 'results': [{'url': 'mocked.url/api/v2/{}/{}/'.format(endpoint, id_), 'name': name}]} 167 | 168 | self.assertIsInstance(interface.APIResource(endpoint, id_), 169 | interface.APIResource) 170 | 171 | @given(id_=integers(min_value=1), 172 | endpoint=sampled_from(ENDPOINTS), 173 | name=text()) 174 | @patch('pokebase.interface.get_data') 175 | def testAttrs_WithNameWithoutLazyLoad(self, mock_get_data, id_, endpoint, name): 176 | 177 | mock_get_data.side_effect = [{'count': 1, 'results': [{'url': 'mocked.url/api/v2/{}/{}/'.format(endpoint, id_), 'name': name}]}, 178 | {'simple_attr': 10, 'list_attr': [{'name': 'mocked name'}]}] 179 | 180 | sample = interface.APIResource(endpoint, name, lazy_load=False) 181 | 182 | self.assertEqual(sample.simple_attr, 10) 183 | self.assertIsInstance(sample.list_attr, list) 184 | self.assertIsInstance(sample.list_attr[0], interface.APIMetadata) 185 | 186 | @given(name=text(), 187 | endpoint=sampled_from(ENDPOINTS), 188 | id_=integers(min_value=1)) 189 | @patch('pokebase.interface.get_data') 190 | def testAttrs_WithIDWithoutLazyLoad(self, mock_get_data, name, endpoint, id_): 191 | 192 | mock_get_data.side_effect = [{'count': 1, 'results': [{'url': 'mocked.url/api/v2/{}/{}/'.format(endpoint, id_), 'name': name}]}, 193 | {'simple_attr': 10, 'list_attr': [{'name': 'mocked name'}], 'complex_attr': {'url': 'mocked.url/api/v2/{}/{}/'.format(endpoint, 10)}}, 194 | {'count': 1, 'results': [{'url': 'mocked.url/api/v2/{}/{}/'.format(endpoint, id_), 'name': name + '2'}]}] 195 | 196 | sample = interface.APIResource(endpoint, id_, lazy_load=False) 197 | 198 | self.assertEqual(sample.simple_attr, 10) 199 | self.assertIsInstance(sample.list_attr, list) 200 | self.assertIsInstance(sample.list_attr[0], interface.APIMetadata) 201 | self.assertIsInstance(sample.complex_attr, interface.APIResource) 202 | 203 | @given(name=text(), 204 | endpoint=sampled_from(ENDPOINTS), 205 | id_=integers(min_value=1)) 206 | @patch('pokebase.interface.get_data') 207 | def testAttrs_WithLazyLoad(self, mock_get_data, name, endpoint, id_): 208 | mock_get_data.side_effect = [{'count': 1, 'results': [{'url': 'mocked.url/api/v2/{}/{}/'.format(endpoint, id_), 'name': name}]}, 209 | {'simple_attr': 10, 'list_attr': [{'name': 'mocked name'}]}] 210 | 211 | sample = interface.APIResource(endpoint, id_, lazy_load=True) 212 | lazy_len = len(dir(sample)) 213 | sample._load() 214 | loaded_len = len(dir(sample)) 215 | 216 | self.assertGreater(loaded_len, lazy_len) 217 | 218 | @given(endpoint=text(), 219 | id_=integers(min_value=1)) 220 | @patch('pokebase.interface.get_data') 221 | def testArg_endpoint_Text(self, mock_get_data, endpoint, id_): 222 | 223 | mock_get_data.return_value = {'count': 1, 'results': [{'url': 'mocked.url/api/v2/{}/{}/'.format(endpoint, id_)}]} 224 | 225 | with self.assertRaises(ValueError): 226 | interface.APIResource(endpoint, id_) 227 | 228 | @given(id_=integers(min_value=1)) 229 | @patch('pokebase.interface.get_data') 230 | def testHasLocationAreaEncounters(self, mock_get_data, id_): 231 | 232 | mock_get_data.side_effect = [{'count': 1, 'results': [{'url': 'mocked.url/api/v2/pokemon/{}/'.format(id_), 'name': 'mocked name'}]}, 233 | {'location_area_encounters': '/api/v2/pokemon/{}/encounters'.format(id_)}, 234 | [{'version_details': [], 'location_area': {'url': 'mocked.url/api/v2/location-area/1/', 'name': 'mocked location_area'}}], 235 | {'count': 1, 'results': [{'url': 'mocked.url/api/v2/location-area/1/', 'name': 'mocked name'}]}] 236 | pkmn = pokemon(id_) 237 | # Should be list or empty list 238 | self.assertIsInstance(pkmn.location_area_encounters, list) 239 | # would be str if it was not handled 240 | self.assertNotIsInstance(pkmn.location_area_encounters, str) 241 | 242 | 243 | class TestClass_APIResourceList(unittest.TestCase): 244 | 245 | @given(endpoint=sampled_from(ENDPOINTS)) 246 | @patch('pokebase.interface.get_data') 247 | def testArgs(self, mock_get_data, endpoint): 248 | 249 | mock_get_data.return_value = {'count': 1, 'results': [{'url': 'mocked.url/api/v2/{}/1/'.format(endpoint)}]} 250 | 251 | self.assertIsInstance(interface.APIResourceList(endpoint), 252 | interface.APIResourceList) 253 | 254 | @given(endpoint=text()) 255 | @patch('pokebase.interface.get_data') 256 | def testArg_endpoint_Text(self, mock_get_data, endpoint): 257 | 258 | mock_get_data.side_effect = ValueError() 259 | 260 | with self.assertRaises(ValueError): 261 | interface.APIResourceList(endpoint) 262 | 263 | 264 | class TestClass_APIMetadata(unittest.TestCase): 265 | 266 | @given(data=dictionaries(text(), text())) 267 | def testArgs(self, data): 268 | self.assertIsInstance(interface.APIMetadata(data), 269 | interface.APIMetadata) 270 | -------------------------------------------------------------------------------- /tests/test_module_loaders.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | from unittest.mock import patch 5 | 6 | from hypothesis import given 7 | from hypothesis.strategies import integers 8 | 9 | from pokebase import APIResource, loaders 10 | from pokebase.common import ENDPOINTS 11 | 12 | 13 | def builder(func, func_name): 14 | 15 | @given(id_=integers(min_value=1)) 16 | @patch('pokebase.interface.get_data') 17 | def test(self, mock_get_data, id_): 18 | mock_get_data.side_effect = [{'count': 1, 'results': [{'url': 'mocked.url/api/v2/{}/{}/'.format(func_name, id_)}]}, 19 | {'simple_attr': 10, 'list_attr': [{'name': 'mocked name'}], 'complex_attr': {'url': 'mocked.url/api/v2/{}/{}/'.format(func_name, 10)}}, 20 | {'count': 1, 'results': [{'url': 'mocked.url/api/v2/{}/{}/'.format(func_name, id_)}]}] 21 | self.assertIsInstance(func(id_), APIResource) 22 | 23 | return test 24 | 25 | 26 | class TestFunctions_loaders(unittest.TestCase): 27 | 28 | @classmethod 29 | def setUpClass(cls): 30 | for endpoint in ENDPOINTS: 31 | if endpoint in ['type']: 32 | # special cases, need trailing underscore 33 | func_name = ''.join([endpoint.replace('-', '_'), '_']) 34 | else: 35 | func_name = endpoint.replace('-', '_') 36 | 37 | func = getattr(loaders, func_name) 38 | 39 | setattr(cls, 'testLoader_{}'.format(func_name), builder(func, endpoint)) 40 | 41 | TestFunctions_loaders.setUpClass() 42 | -------------------------------------------------------------------------------- /tests/test_with_api_calls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | from os import environ 5 | 6 | import pokebase as pb 7 | 8 | 9 | @unittest.skipIf(environ.get('MOCK_ONLY', False), 'only running mock API calls') 10 | class TestAPICalls(unittest.TestCase): 11 | 12 | def testFunction__call_api(self): 13 | self.assertIsInstance(pb.api._call_api('berry', 1), 14 | dict) 15 | 16 | def testFunction_get_data(self): 17 | self.assertIsInstance(pb.api.get_data('berry', 1, force_lookup=True), 18 | dict) 19 | 20 | def testClass_APIResource(self): 21 | self.assertIsInstance(pb.APIResource('berry', 1, force_lookup=True), 22 | pb.APIResource) 23 | 24 | def testClass_APIResourceList(self): 25 | self.assertIsInstance(pb.APIResourceList('berry', force_lookup=True), 26 | pb.APIResourceList) 27 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [isort] 2 | line_length=100 3 | --------------------------------------------------------------------------------