Skip to content

Commit

Permalink
No cache option for PyCrest __init__ and get() calls (#47)
Browse files Browse the repository at this point in the history
* Add DummyCache to fake a cache, not to break existing code but still allow a "no cache" configuration.
Also set DummyCache as default cache (cache=None).
Change existing tests to match this new behavior, and add test for DummyCache

* set DictCache as default, so we manually have to set it to none for no cache

* Add caching argument to __call__ for ApiConnection and ApiObject

* Remove prints...

* Add caching parameters to the doc

* Fix deprecated aliases for assertEqual (was using assertEquals)

* Remove another forgotten print in tests
  • Loading branch information
Kyria authored and hkraal committed Oct 28, 2016
1 parent 9b541ec commit f7a8d28
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 19 deletions.
26 changes: 26 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,29 @@ u'djsdfjklsdf9sd8f908sdf9sd9f8sd9f8sdf8sp9fd89psdf89spdf89spdf89spdf89p'

>>> eve.refr_authorize(refresh_token)
<pycrest.eve.AuthedConnection object at 0x7f06e21f48d0>

.. highlight:: none

Prevent PyCrest from using cache
--------------------------------

**No cache for everything in PyCrest**
This will disable the cache for everything you will do using PyCrest. No call or response will be stored.

.. highlight:: python

>>> pycrest_no_cache = EVE(cache=None)

.. highlight:: none

**Disable caching on demand**
You can disable the caching for a specific ``get()`` call you don't want to cache, by simply adding ``caching=False|None`` to the call parameters.
For example:

.. highlight:: python

>>> crest_with_caching = EVE()
>>> crest_root_not_cached = crest_with_caching(caching=False)
>>> regions = crest_root.regions(caching=False)

.. highlight:: none
52 changes: 35 additions & 17 deletions pycrest/eve.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,22 @@ def put(self, key, value):
def invalidate(self, key):
self._dict.pop(key, None)


class DummyCache(APICache):
""" Provide a fake cache class to allow a "no cache"
use without breaking everything within APIConnection """
def __init__(self):
self._dict = {}

def get(self, key):
return None

def put(self, key, value):
pass

def invalidate(self, key):
pass


class MemcachedCache(APICache):

Expand All @@ -134,7 +150,7 @@ def __init__(
additional_headers=None,
user_agent=None,
cache_dir=None,
cache=None):
cache=DictCache):
# Set up a Requests Session
session = requests.Session()
if additional_headers is None:
Expand All @@ -148,16 +164,16 @@ def __init__(
})
session.headers.update(additional_headers)
self._session = session
if cache:
if cache_dir:
self.cache_dir = cache_dir
self.cache = FileCache(self.cache_dir)
elif cache:
if isinstance(cache, APICache):
self.cache = cache # Inherit from parents
elif isinstance(cache, type):
self.cache = cache() # Instantiate a new cache
elif cache_dir:
self.cache_dir = cache_dir
self.cache = FileCache(self.cache_dir)
else:
self.cache = DictCache()
self.cache = DummyCache()

def _parse_parameters(self, resource, params):
'''Creates a dictionary from query_string and `params`
Expand All @@ -179,10 +195,10 @@ def _parse_parameters(self, resource, params):
prms[key] = params[key]
return resource, prms

def get(self, resource, params={}):
def get(self, resource, params={}, caching=True):
logger.debug('Getting resource %s', resource)
resource, prms = self._parse_parameters(resource, params)

# check cache
key = (
resource, frozenset(
Expand Down Expand Up @@ -218,13 +234,14 @@ def get(self, resource, params={}):

ret = res.json()

# cache result
# cache result only if caching = True (default)
key = (
resource, frozenset(
self._session.headers.items()), frozenset(
prms.items()))

expires = self._get_expires(res)
if expires > 0:
if expires > 0 and caching:
self.cache.put(
key, {
'expires': time.time() + expires, 'payload': ret})
Expand Down Expand Up @@ -300,9 +317,9 @@ def __init__(self, **kwargs):
self._data = None
APIConnection.__init__(self, **kwargs)

def __call__(self):
def __call__(self, caching=True):
if not self._data:
self._data = APIObject(self.get(self._endpoint), self)
self._data = APIObject(self.get(self._endpoint, caching=caching), self)
return self._data

def __getattr__(self, item):
Expand Down Expand Up @@ -397,9 +414,9 @@ def __init__(
self._session.headers.update(
{"Authorization": "Bearer %s" % self.token})

def __call__(self):
def __call__(self, caching=True):
if not self._data:
self._data = APIObject(self.get(self._endpoint), self)
self._data = APIObject(self.get(self._endpoint, caching=caching), self)
return self._data

def whoami(self):
Expand All @@ -420,10 +437,10 @@ def refresh(self):
{"Authorization": "Bearer %s" % self.token})
return self # for backwards compatibility

def get(self, resource, params={}):
def get(self, resource, params={}, caching=True):
if int(time.time()) >= self.expires:
self.refresh()
return super(self.__class__, self).get(resource, params)
return super(self.__class__, self).get(resource, params, caching)

class APIObject(object):

Expand Down Expand Up @@ -477,6 +494,7 @@ def __call__(self, **kwargs):
if 'href' in self._dict:
method = kwargs.pop('method', 'get')#default to get: historic behaviour
data = kwargs.pop('data', {})
caching = kwargs.pop('caching', True) # default caching to true, for get requests

#retain compatibility with historic method of passing parameters.
#Slightly unsatisfactory - what if data is dict-like but not a dict?
Expand All @@ -491,7 +509,7 @@ def __call__(self, **kwargs):
elif method == 'delete':
return APIObject(self.connection.delete(self._dict['href'] ), self.connection)
elif method == 'get':
return APIObject(self.connection.get(self._dict['href'], params=data), self.connection)
return APIObject(self.connection.get(self._dict['href'], params=data, caching=caching), self.connection)
else:
raise UnsupportedHTTPMethodException(method)
else:
Expand Down
55 changes: 53 additions & 2 deletions tests/test_pycrest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
@author: henk
'''
import sys
from pycrest.eve import EVE, DictCache, APICache, FileCache, APIObject,\
from pycrest.eve import EVE, DictCache, DummyCache, APICache, FileCache, APIObject,\
MemcachedCache
import httmock
import pycrest
Expand Down Expand Up @@ -263,6 +263,10 @@ def check_custom_headers(url, request):
def test_default_cache(self):
self.assertTrue(isinstance(self.api.cache, DictCache))

def test_no_cache(self):
eve = EVE(cache=None)
self.assertTrue(isinstance(eve.cache, DummyCache))

def test_callable_cache(self):
class CustomCache(object):
pass
Expand Down Expand Up @@ -319,7 +323,6 @@ def key_mock(url, request):
dict(key='value2'))

def test_cache_hit(self):

@httmock.all_requests
def prime_cache(url, request):
headers = {'content-type': 'application/json',
Expand All @@ -337,6 +340,38 @@ def cached_request(url, request):
with httmock.HTTMock(cached_request):
self.api._data = None
self.assertEqual(self.api()._dict, {})

def test_caching_arg_hit(self):
""" Test the caching argument for ApiConnection and ApiObject __call__() """

@httmock.urlmatch(
scheme="https",
netloc=r"(api-sisi\.test)?(crest-tq\.)?eveonline\.com$",
path=r"^/market/prices/?$")
def market_prices_cached_mock(url, request):
headers = {'content-type': 'application/json',
'Cache-Control': 'max-age=300;'}
return httmock.response(
status_code=200,
headers=headers,
content='{}'.encode('utf-8'))

with httmock.HTTMock(root_mock, market_prices_cached_mock):
self.assertEqual(self.api.cache._dict, {})

self.api(caching=False)
self.assertEqual(self.api.cache._dict, {})

self.api._data = None
self.api()
self.assertEqual(len(self.api.cache._dict), 1)

self.assertEqual(self.api().marketData(caching=False)._dict, {})
self.assertEqual(len(self.api.cache._dict), 1)

self.assertEqual(self.api().marketData()._dict, {})
self.assertEqual(len(self.api.cache._dict), 2)


def test_cache_invalidate(self):
@httmock.all_requests
Expand Down Expand Up @@ -447,6 +482,22 @@ def test_invalidate(self):

def test_cache_dir(self):
pass

class TestDummyCache(unittest.TestCase):

def setUp(self):
self.c = DummyCache()
self.c.put('never_stored', True)

def test_put(self):
self.assertNotIn('never_stored', self.c._dict)

def test_get(self):
self.assertEqual(self.c.get('never_stored'), None)

def test_invalidate(self):
self.c.invalidate('never_stored')
self.assertIsNone(self.c.get('never_stored'))


class TestFileCache(unittest.TestCase):
Expand Down

0 comments on commit f7a8d28

Please sign in to comment.