Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 119 additions & 18 deletions pokebase/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
import requests


BASE_URL = 'http://pokeapi.co/api/v2'
BASE_HOST = 'http://pokeapi.co'
BASE_PATH = '/api/v2'
BASE_URL = BASE_HOST + BASE_PATH
SPRITE_URL = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites'
CACHE = None # To be set after set_cache() definition
SPRITE_CACHE = None # Ditto
Expand All @@ -48,6 +50,7 @@
'pokemon-form', 'pokemon-habitat', 'pokemon-shape',
'pokemon-species', 'region', 'stat', 'super-contest-effect',
'type', 'version', 'version-group']
SUBRESOURCES = ['pokemon/encounters']


def safe_make_dirs(path, mode=0o777):
Expand Down Expand Up @@ -121,7 +124,7 @@ def set_cache(new_path=None):
set_cache()


def lookup_data(sub_dir, name, force_reload=False):
def lookup_data(sub_dir, name, url=None, force_reload=False):
"""Locates and saves a specific reference, and then returns the data.

If the resource desired is already cached, this function will return the
Expand All @@ -131,9 +134,13 @@ def lookup_data(sub_dir, name, force_reload=False):

:param sub_dir: what type of data is requested. (ex. 'move' or 'type')
:param name: the name of the resource to lookup (ex. 'pound' or 'fire')
:param url: the pokeAPI URL of the resource, will be construced from sub_dir
and name if not provided
:param force_reload: force the download, even if it the file exists already
:return the data requested, as a Python `dict` instance
"""
if url is None:
url = '/'.join([BASE_URL, sub_dir, name])

cwd = os.getcwd()
os.chdir(CACHE)
Expand All @@ -156,13 +163,18 @@ def lookup_data(sub_dir, name, force_reload=False):
data = json.load(f)
else:
# If it doesn't exist, go download and save the resources.
r = requests.get('/'.join([BASE_URL, sub_dir, name]))
r = requests.get(url)
r.raise_for_status()
data = json.loads(r.text)
with open('.'.join([name, 'json']), 'w') as f:
json.dump(data, f, indent=2)

os.chdir(cwd) # Return to original working directory.

# Workaround for https://github.com/PokeAPI/pokeapi/issues/332
if "encounters" in url and not isinstance(data, dict):
data = {'results': data}

return data


Expand All @@ -174,12 +186,19 @@ def lookup_resource(name, force_reload=False):
force_reload parameter. Reference are saved to the user's home directory
in a folder `~/.cache/pokebase`.

If the requested resource is a subresource (ie. 'pokemon/encounters'),
this returns False (but does not raise ane exception)

:param name: which resource to download (ex. 'ability' or 'berry')
:param force_reload: force the download, even if it the file exists already
:return Python dict with the data this resource contains
"""

if name not in RESOURCES:
if name in SUBRESOURCES:
if force_reload or not os.path.exists(name):
safe_make_dirs(name)
return False
elif name not in RESOURCES:
raise ValueError('resource not found ({}), check spelling'
.format(name))

Expand Down Expand Up @@ -271,10 +290,27 @@ def make_obj(d):

if isinstance(d, dict):
if 'url' in d.keys():
url = d['url']
name = url.split('/')[-2] # Name of the data.
location = url.split('/')[-3] # Where the data is located.
return NamedAPIResource(location, name, False)
url = d['url']
# work with BASE_PATH, pokeapi inconsistent about http v. https
path_start_idx = url.find(BASE_PATH)
resource_start_idx = path_start_idx + len(BASE_PATH) + 1 # 1 for '/'
path_components = url[resource_start_idx:].split('/')

location = path_components[0]
name = path_components[1]

if len(path_components) == 3:
# eg. pokemon/4/
return NamedAPIResource(location, name, False)
elif len(path_components) == 4:
# ie. pokemon/4/encounters/
subname = path_components[2]
return NamedAPISubresource(location, name, subname, False)
else:
# Nothing should be in this format
raise ValueError('unexpected resource format ({})'
.format(url))

else:
return APIMetadata(d)
else:
Expand Down Expand Up @@ -311,16 +347,10 @@ def __init__(self, resource, name, lookup=True):
r = resource.replace(' ', '-').lower()
n = APIResourceList(r).id_to_name(name)

self.__data = {'type': r, 'name': n,
'url': '/'.join([BASE_URL, r, n])}

self.resource_type = r

if lookup:
self.load()
self.__is_loaded = True
else:
self.__is_loaded = False
self.init_data(r, n, '/'.join([BASE_URL, r, n]))
self.init_lookup(lookup)

def __getattr__(self, attr):
"""Modified method to auto-load the data when it is needed.
Expand All @@ -332,7 +362,6 @@ def __getattr__(self, attr):

if not self.__is_loaded:
self.load()
self.__is_loaded = True

return self.__getattribute__(attr)

Expand All @@ -346,6 +375,30 @@ def __str__(self):
def __repr__(self):
return '<{} - {}>'.format(self.resource_type, self.name)

def init_data(self, resource_dir, resource_name, url):
"""Function to initialize internals for debugging and lazy loading

Internal function, does not usually need to be called by the user, as
it is called automatically during initialization.

:return None
"""
self.__data = {'type': resource_dir, 'name': resource_name, 'url': url}

def init_lookup(self, lookup=False):
"""Function to decide whether to load reference data or indicate that
it has not yet been loaded.

Internal function, does not usually need to be called by the user, as
it is called automatically during initialization.

:return None
"""
if lookup:
self.load()
else:
self.__is_loaded = False

def load(self):
"""Function to collect reference data and connect it to the instance as
attributes.
Expand All @@ -356,8 +409,10 @@ def load(self):
:return None
"""

url = self.__data['url']
self.__data.update(lookup_data(self.__data['type'],
self.__data['name']))
self.__data['name'],
url))

for k, v in self.__data.items():

Expand All @@ -366,6 +421,10 @@ def load(self):

elif isinstance(v, list):
self.__setattr__(k, [make_obj(i) for i in v])

elif isinstance(v, str) and v.find(BASE_PATH) == 0:
obj = make_obj({ 'url': BASE_HOST + v + '/', 'debug': 1 })
self.__setattr__(k, obj)
else:
self.__setattr__(k, v)

Expand Down Expand Up @@ -529,3 +588,45 @@ def load(self):
self.__setattr__('path', path)

return None

class NamedAPISubresource(NamedAPIResource):
"""Extension of the NamedAPIResource, used for accessing endpoints like
/pokemon/1/encounters

"""

def __init__(self, resource, resource_id, subname, lookup=True):
"""Returns a new NamedAPISubresource object.

Specify lookup=False to conserve calls to the API and speed up your
program. This feature is used internally, but you will usually want it
left True. Leaving it False causes the object to act as a placeholder
for the data, until the data is called by the user.

:param str resource: The parent resource's name (ex. 'pokemon')
:param str resource_id: The id of the parent resource (ex. '1')
Note that the resource *name* does not work (so no 'bulbasaur')
:param str subname: The subresource's name (ex. 'encounters')
:param bool lookup: Whether or not to gather all the data on
construction
"""

normed_resource = resource.replace(' ', '-').lower()
normed_subname = subname.replace(' ', '-').lower()
# eg. pokemon/charmander/encounters will not work
# must be pokemon/4/encounters
# so no point in trying id_to_name on resource_id
resource_id = str(resource_id)

# There is no real resource. Just make the directory (sans checks)
dir_name = '/'.join([normed_resource, normed_subname])
safe_make_dirs(dir_name)

self.resource_type = '-'.join([normed_resource, normed_subname])

self.init_data(
dir_name,
resource_id,
'/'.join([BASE_URL, normed_resource, resource_id, normed_subname])
)
self.init_lookup(lookup)
14 changes: 13 additions & 1 deletion pokebase/loaders.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-

from .api import NamedAPIResource, SpriteResource
from .api import NamedAPIResource, NamedAPISubresource, SpriteResource


def berry(id_or_name):
Expand Down Expand Up @@ -591,3 +591,15 @@ def pokemon_sprite(id_):
"""

return SpriteResource('pokemon', id_)

def location_area_encounters(id_):
"""Quick Pokemon encounters lookup.

See https://pokeapi.co/docsv2/#pokemon - particularly the URL returned
by location_area_encounters.

:param id_: id of the pokemon's location_area_encounters to look up
:return: NamedAPISubresource with the appropriate data
"""

return NamedAPISubresource('pokemon', id_, 'encounters')
25 changes: 23 additions & 2 deletions tests/test_pokebase.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@

from __future__ import unicode_literals
import os
import shutil
import unittest

import requests

import pokebase as pb
from pokebase.api import SPRITE_CACHE
import pokebase.api as pb_api


class TestNamedAPIResource(unittest.TestCase):
Expand Down Expand Up @@ -36,6 +37,26 @@ def testMetadataAttr(self):
def testNoneAttr(self):
self.assertIsNone(self.evolution_chain.baby_trigger_item)

class TestNamedAPISubresource(unittest.TestCase):

def setUp(self):
self.OLD_CACHE = pb_api.CACHE
if os.environ.get('POKEBASE_TEST_ALL'):
pb_api.set_cache(self.OLD_CACHE + '-test')

def cleanTestPath ():
shutil.rmtree(pb_api.CACHE)
pb_api.set_cache(self.OLD_CACHE)

self.addCleanup(cleanTestPath)

self.dragonite_encounters = pb.location_area_encounters(149)
self.mew_encounters = pb.location_area_encounters(151)

def testCount(self):
self.assertGreater(len(self.dragonite_encounters.results), 0)
self.assertEqual(len(self.mew_encounters.results), 0)


class TestAPIResourceList(unittest.TestCase):

Expand Down Expand Up @@ -69,7 +90,7 @@ def setUp(self):
self.doesnt_exists = pb.pokemon_sprite(-1)

def testPath(self):
self.assertEqual(self.bulba.path, os.path.join(SPRITE_CACHE,
self.assertEqual(self.bulba.path, os.path.join(pb_api.SPRITE_CACHE,
'pokemon', '1.png'))

def test404(self):
Expand Down