Skip to content

Commit 68c1c87

Browse files
authored
V2Client cache-related methods (#34)
* Implemented the following V2Client cache-related methods: * cache_info_total * cache_clear_total * cache_location_absolute
1 parent 702b507 commit 68c1c87

File tree

7 files changed

+312
-115
lines changed

7 files changed

+312
-115
lines changed

.pylintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ disable=missing-docstring,
6666
no-member,
6767
keyword-arg-before-vararg,
6868
fixme,
69+
too-many-instance-attributes,
6970

7071
# Disabled by default:
7172

docs/history.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
# History
2+
### 0.5.1 (2019-02-16)
3+
* New V2Client cache-related methods:
4+
* cache_info
5+
* cache_clear
6+
* cache_location
7+
28
### 0.5.0 (2019-01-19)
39
* Pykemon is now Pokepy!
410
* Cache (disk- and memory-based)

docs/usage.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,22 @@ Resources obtained from the PokéAPI are then saved in RAM. Cache is kept per ge
139139
>>> client_mem_cache = pokepy.V2Client(cache='in_memory')
140140
```
141141

142-
To check the state of the cache of a particular method:
142+
You can check the state of the cache in two ways: per get method or as a whole.
143+
144+
To check the state of the cache of a particular method, call the `cache_info()`
145+
of that get method:
143146
```python
144147
>>> client_mem_cache.get_pokemon.cache_info()
145148
CacheInfo(hits=0, misses=0, size=0)
146149
```
150+
151+
To check the state of the cache as a whole (all get methods combined),
152+
call the `cache_info()` of `V2Client`:
153+
```python
154+
>>> client_mem_cache.cache_info()
155+
CacheInfo(hits=0, misses=0, size=0)
156+
```
157+
147158
`hits` is the number of previously cached parametes which were returned,
148159
`misses` is the number given parameters not previously cached (which are now cached),
149160
and `size` is the total number of cached parameters.
@@ -170,6 +181,13 @@ To clear the cache of a specific get method:
170181
CacheInfo(hits=0, misses=0, size=0)
171182
```
172183

184+
To clear all cache:
185+
```python
186+
>>> client_mem_cache.cache_clear()
187+
>>> client_mem_cache.cache_info()
188+
CacheInfo(hits=0, misses=0, size=0)
189+
```
190+
173191
#### Disk-based
174192
Disk-based cache is activated by passing `in_disk` to the `cache` parameter of `V2Client`.
175193
Resources obtained from the PokéAPI are then saved to disk. Cache is kept per get method:
@@ -183,11 +201,19 @@ cache of each get method will be located.
183201
If no cache directory is specified a system-appropriate cache directory is automatically determined by
184202
[appdirs](https://pypi.org/project/appdirs/).
185203

186-
The methods used to check the state and clear the cache are the same as in the memory-based cache.
187-
You can also check the cache directory:
204+
The methods used to check the state and clear the cache are the same as in the memory-based cache,
205+
including the global `V2Client` methods.
206+
207+
You can also check the cache directory, per get method:
188208
```python
189209
>>> client_disk_cache.get_pokemon.cache_location()
190210
/temp/pokepy_cache/39/cache
191211
```
192212

213+
Or check the global cache directory:
214+
```python
215+
>>> client_disk_cache.cache_location()
216+
/temp/pokepy_cache/
217+
```
218+
193219
Disk-based cache is reloaded automatically between runs if the same cache directory is specified.

pokepy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
__author__ = 'Paul Hallett'
1717
__email__ = 'hello@phalt.co'
1818
__credits__ = ["Paul Hallett", "Owen Hallett", "Kronopt"]
19-
__version__ = '0.5.0'
19+
__version__ = '0.5.1'
2020
__copyright__ = 'Copyright Paul Hallett 2016'
2121
__license__ = 'BSD'
2222

pokepy/api.py

Lines changed: 141 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -20,91 +20,6 @@
2020
from . import __version__
2121

2222

23-
def caching(disk_or_memory, cache_directory=None):
24-
"""
25-
Decorator that allows caching the outputs of the BaseClient get methods.
26-
Cache can be either disk- or memory-based.
27-
Disk-based cache is reloaded automatically between runs if the same
28-
cache directory is specified.
29-
Cache is kept per each unique uid.
30-
31-
ex:
32-
>> client.get_pokemon(1) -> output gets cached
33-
>> client.get_pokemon(uid=1) -> output already cached
34-
>> client.get_pokemon(2) -> output gets cached
35-
36-
Parameters
37-
----------
38-
disk_or_memory: str
39-
Specify if the cache is disk- or memory-based. Accepts 'disk' or 'memory'.
40-
cache_directory: str
41-
Specify the directory for the disk-based cache.
42-
Optional, will chose an appropriate and platform-specific directory if not specified.
43-
Ignored if memory-based cache is selected.
44-
"""
45-
if disk_or_memory not in ('disk', 'memory'):
46-
raise ValueError('Accepted values are "disk" or "memory"')
47-
48-
# Because of the way the BaseClient get methods are generated, they don't get a proper __name__.
49-
# As such, it is hard to generate a specific cache directory name for each get method.
50-
# Therefore, I decided to just generate a number for each folder, starting at zero.
51-
# The same get methods get the same number every time because their order doesn't change.
52-
# Also, variable is incremented inside a list because nonlocals are only python 3.0 and up.
53-
get_methods_id = [0]
54-
55-
def memoize(func):
56-
if disk_or_memory == 'disk':
57-
if cache_directory:
58-
# Python 2 workaround
59-
if sys.version_info[0] == 2 and not isinstance(cache_directory, str):
60-
raise TypeError('expected str')
61-
62-
cache_dir = os.path.join(cache_directory, 'pokepy_cache', str(get_methods_id[0]))
63-
else:
64-
cache_dir = os.path.join(
65-
appdirs.user_cache_dir('pokepy_cache', False, opinion=False),
66-
str(get_methods_id[0]))
67-
cache = FileCache('pokepy', flag='cs', app_cache_dir=cache_dir)
68-
get_methods_id[0] += 1
69-
else: # 'memory'
70-
cache = {}
71-
72-
cache_info_ = namedtuple('CacheInfo', ['hits', 'misses', 'size'])
73-
hits = [0]
74-
misses = [0]
75-
76-
def cache_info():
77-
return cache_info_(hits[0], misses[0], len(cache))
78-
79-
def cache_clear():
80-
cache.clear() # for disk-based cache, files are deleted but not the directories
81-
if disk_or_memory == 'disk':
82-
cache.create() # recreate cache file handles
83-
hits[0] = 0
84-
misses[0] = 0
85-
86-
def cache_location():
87-
return 'ram' if disk_or_memory == 'memory' else cache.cache_dir
88-
89-
@functools.wraps(func)
90-
def memoizer(*args, **kwargs):
91-
# arguments to the get methods can be a value or uid=value
92-
key = str(args[1]) if len(args) > 1 else str(kwargs.get("uid"))
93-
94-
if key not in cache:
95-
misses[0] += 1
96-
cache[key] = func(*args, **kwargs)
97-
else:
98-
hits[0] += 1
99-
return cache[key]
100-
101-
memoizer.cache_info = cache_info
102-
memoizer.cache_clear = cache_clear
103-
memoizer.cache_location = cache_location
104-
return memoizer
105-
return memoize
106-
107-
10823
class V2Client(BaseClient):
10924
"""Pokéapi client"""
11025

@@ -174,23 +89,45 @@ def __init__(self, cache=None, cache_location=None, *args, **kwargs):
17489
cache directory, for disk-based cache.
17590
Optional.
17691
"""
177-
if cache == 'in_memory':
178-
cache_function = caching('memory')
179-
self.cache_type = cache
180-
elif cache == 'in_disk':
181-
cache_function = caching('disk', cache_location)
182-
self.cache_type = cache
183-
elif cache is None: # empty wrapping function
92+
if cache is None: # empty wrapping function
18493
def no_cache(func):
18594
@functools.wraps(func)
18695
def inner(*args, **kwargs):
18796
return func(*args, **kwargs)
18897
return inner
18998
cache_function = no_cache
190-
else: # wrong cache parameter
191-
raise ValueError('Accepted values for cache are "in_memory" or "in_disk"')
99+
else:
100+
if cache in ['in_memory', 'in_disk']:
101+
cache_function = self._caching(cache.split('in_')[1], cache_location)
102+
self.cache_type = cache
103+
104+
def cache_info_total(self):
105+
return self._cache_info_(self._cache_hits_global,
106+
self._cache_misses_global,
107+
self._cache_len_global)
108+
109+
def cache_clear_total(self):
110+
for get_method_name in self._all_get_methods_names:
111+
getattr(self, get_method_name).cache_clear()
192112

193-
self.cache = cache_function
113+
def cache_location_absolute(self):
114+
return self._cache_location_global
115+
116+
# global cache related methods
117+
self.cache_info = types.MethodType(cache_info_total, self)
118+
self.cache_clear = types.MethodType(cache_clear_total, self)
119+
self.cache_location = types.MethodType(cache_location_absolute, self)
120+
121+
self._cache_hits_global = 0
122+
self._cache_misses_global = 0
123+
self._cache_len_global = 0
124+
self._cache_location_global = ''
125+
self._cache_info_ = namedtuple('CacheInfo', ['hits', 'misses', 'size'])
126+
else: # wrong cache parameter
127+
raise ValueError('Accepted values for cache are "in_memory" or "in_disk"')
128+
129+
self._cache = cache_function
130+
self._all_get_methods_names = []
194131
super(V2Client, self).__init__(*args, **kwargs)
195132

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

211149
# uid is now the first argument (after self)
212-
@self.cache
150+
@self._cache
213151
def get(self, uid=None, method_type=method_type,
214152
method_name=method_name,
215153
valid_status_codes=valid_status_codes,
@@ -225,3 +163,110 @@ def get(self, uid=None, method_type=method_type,
225163
self, method_name,
226164
types.MethodType(get, self)
227165
)
166+
167+
# for easier listing of get methods
168+
self._all_get_methods_names.append(method_name)
169+
170+
def _caching(self, disk_or_memory, cache_directory=None):
171+
"""
172+
Decorator that allows caching the outputs of the BaseClient get methods.
173+
Cache can be either disk- or memory-based.
174+
Disk-based cache is reloaded automatically between runs if the same
175+
cache directory is specified.
176+
Cache is kept per each unique uid.
177+
178+
ex:
179+
>> client.get_pokemon(1) -> output gets cached
180+
>> client.get_pokemon(uid=1) -> output already cached
181+
>> client.get_pokemon(2) -> output gets cached
182+
183+
Parameters
184+
----------
185+
disk_or_memory: str
186+
Specify if the cache is disk- or memory-based. Accepts 'disk' or 'memory'.
187+
cache_directory: str
188+
Specify the directory for the disk-based cache.
189+
Optional, will chose an appropriate and platform-specific directory if not specified.
190+
Ignored if memory-based cache is selected.
191+
"""
192+
if disk_or_memory not in ('disk', 'memory'):
193+
raise ValueError('Accepted values are "disk" or "memory"')
194+
195+
# Because of how BaseClient get methods are generated, they don't get a proper __name__.
196+
# As such, it is hard to generate a specific cache directory name for each get method.
197+
# Therefore, I decided to just generate a number for each folder, starting at zero.
198+
# The same get methods get the same number every time because their order doesn't change.
199+
# Also, variable is incremented inside a list because nonlocals are only python 3.0 and up.
200+
get_methods_id = [0]
201+
202+
def memoize(func):
203+
_global_cache_dir = ''
204+
205+
if disk_or_memory == 'disk':
206+
if cache_directory:
207+
# Python 2 workaround
208+
if sys.version_info[0] == 2 and not isinstance(cache_directory, str):
209+
raise TypeError('expected str')
210+
211+
_global_cache_dir = os.path.join(cache_directory, 'pokepy_cache')
212+
cache_dir = os.path.join(_global_cache_dir, str(get_methods_id[0]))
213+
else:
214+
_global_cache_dir = appdirs.user_cache_dir('pokepy_cache', False,
215+
opinion=False)
216+
cache_dir = os.path.join(_global_cache_dir, str(get_methods_id[0]))
217+
218+
cache = FileCache('pokepy', flag='cs', app_cache_dir=cache_dir)
219+
get_methods_id[0] += 1
220+
else: # 'memory'
221+
cache = {}
222+
_global_cache_dir = 'ram'
223+
224+
# global cache directory
225+
# should only be set when setting the first get method
226+
if not self._cache_location_global:
227+
self._cache_location_global = _global_cache_dir
228+
229+
hits = [0]
230+
misses = [0]
231+
232+
def cache_info():
233+
return self._cache_info_(hits[0], misses[0], len(cache))
234+
235+
def cache_clear():
236+
# global cache info
237+
self._cache_hits_global -= hits[0]
238+
self._cache_misses_global -= misses[0]
239+
self._cache_len_global -= len(cache)
240+
# local cache info
241+
hits[0] = 0
242+
misses[0] = 0
243+
244+
cache.clear() # for disk-based cache, files are deleted but not the directories
245+
if disk_or_memory == 'disk':
246+
cache.create() # recreate cache file handles
247+
248+
def cache_location():
249+
return 'ram' if disk_or_memory == 'memory' else cache.cache_dir
250+
251+
@functools.wraps(func)
252+
def memoizer(*args, **kwargs):
253+
# arguments to the get methods can be a value or uid=value
254+
key = str(args[1]) if len(args) > 1 else str(kwargs.get("uid"))
255+
256+
if key not in cache:
257+
# local and global cache info
258+
misses[0] += 1
259+
self._cache_misses_global += 1
260+
cache[key] = func(*args, **kwargs)
261+
self._cache_len_global += 1
262+
else:
263+
self._cache_hits_global += 1 # global cache info
264+
hits[0] += 1 # local cache info
265+
return cache[key]
266+
267+
memoizer.cache_info = cache_info
268+
memoizer.cache_clear = cache_clear
269+
memoizer.cache_location = cache_location
270+
return memoizer
271+
272+
return memoize

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
version=pokepy.__version__,
2121
description='A Python wrapper for PokéAPI (https://pokeapi.co)',
2222
long_description=readme + '\n\n' + history,
23+
long_description_content_type='text/markdown',
2324
license=pokepy.__license__,
2425
author=pokepy.__author__,
2526
author_email=pokepy.__email__,

0 commit comments

Comments
 (0)