Skip to content

Commit

Permalink
V2Client cache-related methods (#34)
Browse files Browse the repository at this point in the history
* Implemented the following V2Client cache-related methods:
   * cache_info_total
   * cache_clear_total
   * cache_location_absolute
  • Loading branch information
Kronopt authored Feb 16, 2019
1 parent 702b507 commit 68c1c87
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 115 deletions.
1 change: 1 addition & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ disable=missing-docstring,
no-member,
keyword-arg-before-vararg,
fixme,
too-many-instance-attributes,

# Disabled by default:

Expand Down
6 changes: 6 additions & 0 deletions docs/history.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
# History
### 0.5.1 (2019-02-16)
* New V2Client cache-related methods:
* cache_info
* cache_clear
* cache_location

### 0.5.0 (2019-01-19)
* Pykemon is now Pokepy!
* Cache (disk- and memory-based)
Expand Down
32 changes: 29 additions & 3 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,22 @@ Resources obtained from the PokéAPI are then saved in RAM. Cache is kept per ge
>>> client_mem_cache = pokepy.V2Client(cache='in_memory')
```

To check the state of the cache of a particular method:
You can check the state of the cache in two ways: per get method or as a whole.

To check the state of the cache of a particular method, call the `cache_info()`
of that get method:
```python
>>> client_mem_cache.get_pokemon.cache_info()
CacheInfo(hits=0, misses=0, size=0)
```

To check the state of the cache as a whole (all get methods combined),
call the `cache_info()` of `V2Client`:
```python
>>> client_mem_cache.cache_info()
CacheInfo(hits=0, misses=0, size=0)
```

`hits` is the number of previously cached parametes which were returned,
`misses` is the number given parameters not previously cached (which are now cached),
and `size` is the total number of cached parameters.
Expand All @@ -170,6 +181,13 @@ To clear the cache of a specific get method:
CacheInfo(hits=0, misses=0, size=0)
```

To clear all cache:
```python
>>> client_mem_cache.cache_clear()
>>> client_mem_cache.cache_info()
CacheInfo(hits=0, misses=0, size=0)
```

#### Disk-based
Disk-based cache is activated by passing `in_disk` to the `cache` parameter of `V2Client`.
Resources obtained from the PokéAPI are then saved to disk. Cache is kept per get method:
Expand All @@ -183,11 +201,19 @@ cache of each get method will be located.
If no cache directory is specified a system-appropriate cache directory is automatically determined by
[appdirs](https://pypi.org/project/appdirs/).

The methods used to check the state and clear the cache are the same as in the memory-based cache.
You can also check the cache directory:
The methods used to check the state and clear the cache are the same as in the memory-based cache,
including the global `V2Client` methods.

You can also check the cache directory, per get method:
```python
>>> client_disk_cache.get_pokemon.cache_location()
/temp/pokepy_cache/39/cache
```

Or check the global cache directory:
```python
>>> client_disk_cache.cache_location()
/temp/pokepy_cache/
```

Disk-based cache is reloaded automatically between runs if the same cache directory is specified.
2 changes: 1 addition & 1 deletion pokepy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
__author__ = 'Paul Hallett'
__email__ = '[email protected]'
__credits__ = ["Paul Hallett", "Owen Hallett", "Kronopt"]
__version__ = '0.5.0'
__version__ = '0.5.1'
__copyright__ = 'Copyright Paul Hallett 2016'
__license__ = 'BSD'

Expand Down
237 changes: 141 additions & 96 deletions pokepy/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,91 +20,6 @@
from . import __version__


def caching(disk_or_memory, cache_directory=None):
"""
Decorator that allows caching the outputs of the BaseClient get methods.
Cache can be either disk- or memory-based.
Disk-based cache is reloaded automatically between runs if the same
cache directory is specified.
Cache is kept per each unique uid.
ex:
>> client.get_pokemon(1) -> output gets cached
>> client.get_pokemon(uid=1) -> output already cached
>> client.get_pokemon(2) -> output gets cached
Parameters
----------
disk_or_memory: str
Specify if the cache is disk- or memory-based. Accepts 'disk' or 'memory'.
cache_directory: str
Specify the directory for the disk-based cache.
Optional, will chose an appropriate and platform-specific directory if not specified.
Ignored if memory-based cache is selected.
"""
if disk_or_memory not in ('disk', 'memory'):
raise ValueError('Accepted values are "disk" or "memory"')

# Because of the way the BaseClient get methods are generated, they don't get a proper __name__.
# As such, it is hard to generate a specific cache directory name for each get method.
# Therefore, I decided to just generate a number for each folder, starting at zero.
# The same get methods get the same number every time because their order doesn't change.
# Also, variable is incremented inside a list because nonlocals are only python 3.0 and up.
get_methods_id = [0]

def memoize(func):
if disk_or_memory == 'disk':
if cache_directory:
# Python 2 workaround
if sys.version_info[0] == 2 and not isinstance(cache_directory, str):
raise TypeError('expected str')

cache_dir = os.path.join(cache_directory, 'pokepy_cache', str(get_methods_id[0]))
else:
cache_dir = os.path.join(
appdirs.user_cache_dir('pokepy_cache', False, opinion=False),
str(get_methods_id[0]))
cache = FileCache('pokepy', flag='cs', app_cache_dir=cache_dir)
get_methods_id[0] += 1
else: # 'memory'
cache = {}

cache_info_ = namedtuple('CacheInfo', ['hits', 'misses', 'size'])
hits = [0]
misses = [0]

def cache_info():
return cache_info_(hits[0], misses[0], len(cache))

def cache_clear():
cache.clear() # for disk-based cache, files are deleted but not the directories
if disk_or_memory == 'disk':
cache.create() # recreate cache file handles
hits[0] = 0
misses[0] = 0

def cache_location():
return 'ram' if disk_or_memory == 'memory' else cache.cache_dir

@functools.wraps(func)
def memoizer(*args, **kwargs):
# arguments to the get methods can be a value or uid=value
key = str(args[1]) if len(args) > 1 else str(kwargs.get("uid"))

if key not in cache:
misses[0] += 1
cache[key] = func(*args, **kwargs)
else:
hits[0] += 1
return cache[key]

memoizer.cache_info = cache_info
memoizer.cache_clear = cache_clear
memoizer.cache_location = cache_location
return memoizer
return memoize


class V2Client(BaseClient):
"""Pokéapi client"""

Expand Down Expand Up @@ -174,23 +89,45 @@ def __init__(self, cache=None, cache_location=None, *args, **kwargs):
cache directory, for disk-based cache.
Optional.
"""
if cache == 'in_memory':
cache_function = caching('memory')
self.cache_type = cache
elif cache == 'in_disk':
cache_function = caching('disk', cache_location)
self.cache_type = cache
elif cache is None: # empty wrapping function
if cache is None: # empty wrapping function
def no_cache(func):
@functools.wraps(func)
def inner(*args, **kwargs):
return func(*args, **kwargs)
return inner
cache_function = no_cache
else: # wrong cache parameter
raise ValueError('Accepted values for cache are "in_memory" or "in_disk"')
else:
if cache in ['in_memory', 'in_disk']:
cache_function = self._caching(cache.split('in_')[1], cache_location)
self.cache_type = cache

def cache_info_total(self):
return self._cache_info_(self._cache_hits_global,
self._cache_misses_global,
self._cache_len_global)

def cache_clear_total(self):
for get_method_name in self._all_get_methods_names:
getattr(self, get_method_name).cache_clear()

self.cache = cache_function
def cache_location_absolute(self):
return self._cache_location_global

# global cache related methods
self.cache_info = types.MethodType(cache_info_total, self)
self.cache_clear = types.MethodType(cache_clear_total, self)
self.cache_location = types.MethodType(cache_location_absolute, self)

self._cache_hits_global = 0
self._cache_misses_global = 0
self._cache_len_global = 0
self._cache_location_global = ''
self._cache_info_ = namedtuple('CacheInfo', ['hits', 'misses', 'size'])
else: # wrong cache parameter
raise ValueError('Accepted values for cache are "in_memory" or "in_disk"')

self._cache = cache_function
self._all_get_methods_names = []
super(V2Client, self).__init__(*args, **kwargs)

def _assign_method(self, resource_class, method_type):
Expand All @@ -199,6 +136,7 @@ def _assign_method(self, resource_class, method_type):
- uid is now first parameter (after self). Therefore, no need to explicitly call 'uid='
- Ignored the other http methods besides GET (as they are not needed for the pokeapi.co API)
- Added cache wrapping function
- Added a way to list all get methods
"""
method_name = resource_class.get_method_name(
resource_class, method_type)
Expand All @@ -209,7 +147,7 @@ def _assign_method(self, resource_class, method_type):
)

# uid is now the first argument (after self)
@self.cache
@self._cache
def get(self, uid=None, method_type=method_type,
method_name=method_name,
valid_status_codes=valid_status_codes,
Expand All @@ -225,3 +163,110 @@ def get(self, uid=None, method_type=method_type,
self, method_name,
types.MethodType(get, self)
)

# for easier listing of get methods
self._all_get_methods_names.append(method_name)

def _caching(self, disk_or_memory, cache_directory=None):
"""
Decorator that allows caching the outputs of the BaseClient get methods.
Cache can be either disk- or memory-based.
Disk-based cache is reloaded automatically between runs if the same
cache directory is specified.
Cache is kept per each unique uid.
ex:
>> client.get_pokemon(1) -> output gets cached
>> client.get_pokemon(uid=1) -> output already cached
>> client.get_pokemon(2) -> output gets cached
Parameters
----------
disk_or_memory: str
Specify if the cache is disk- or memory-based. Accepts 'disk' or 'memory'.
cache_directory: str
Specify the directory for the disk-based cache.
Optional, will chose an appropriate and platform-specific directory if not specified.
Ignored if memory-based cache is selected.
"""
if disk_or_memory not in ('disk', 'memory'):
raise ValueError('Accepted values are "disk" or "memory"')

# Because of how BaseClient get methods are generated, they don't get a proper __name__.
# As such, it is hard to generate a specific cache directory name for each get method.
# Therefore, I decided to just generate a number for each folder, starting at zero.
# The same get methods get the same number every time because their order doesn't change.
# Also, variable is incremented inside a list because nonlocals are only python 3.0 and up.
get_methods_id = [0]

def memoize(func):
_global_cache_dir = ''

if disk_or_memory == 'disk':
if cache_directory:
# Python 2 workaround
if sys.version_info[0] == 2 and not isinstance(cache_directory, str):
raise TypeError('expected str')

_global_cache_dir = os.path.join(cache_directory, 'pokepy_cache')
cache_dir = os.path.join(_global_cache_dir, str(get_methods_id[0]))
else:
_global_cache_dir = appdirs.user_cache_dir('pokepy_cache', False,
opinion=False)
cache_dir = os.path.join(_global_cache_dir, str(get_methods_id[0]))

cache = FileCache('pokepy', flag='cs', app_cache_dir=cache_dir)
get_methods_id[0] += 1
else: # 'memory'
cache = {}
_global_cache_dir = 'ram'

# global cache directory
# should only be set when setting the first get method
if not self._cache_location_global:
self._cache_location_global = _global_cache_dir

hits = [0]
misses = [0]

def cache_info():
return self._cache_info_(hits[0], misses[0], len(cache))

def cache_clear():
# global cache info
self._cache_hits_global -= hits[0]
self._cache_misses_global -= misses[0]
self._cache_len_global -= len(cache)
# local cache info
hits[0] = 0
misses[0] = 0

cache.clear() # for disk-based cache, files are deleted but not the directories
if disk_or_memory == 'disk':
cache.create() # recreate cache file handles

def cache_location():
return 'ram' if disk_or_memory == 'memory' else cache.cache_dir

@functools.wraps(func)
def memoizer(*args, **kwargs):
# arguments to the get methods can be a value or uid=value
key = str(args[1]) if len(args) > 1 else str(kwargs.get("uid"))

if key not in cache:
# local and global cache info
misses[0] += 1
self._cache_misses_global += 1
cache[key] = func(*args, **kwargs)
self._cache_len_global += 1
else:
self._cache_hits_global += 1 # global cache info
hits[0] += 1 # local cache info
return cache[key]

memoizer.cache_info = cache_info
memoizer.cache_clear = cache_clear
memoizer.cache_location = cache_location
return memoizer

return memoize
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
version=pokepy.__version__,
description='A Python wrapper for PokéAPI (https://pokeapi.co)',
long_description=readme + '\n\n' + history,
long_description_content_type='text/markdown',
license=pokepy.__license__,
author=pokepy.__author__,
author_email=pokepy.__email__,
Expand Down
Loading

0 comments on commit 68c1c87

Please sign in to comment.