Files
pyfa/pycrest/eve.py
2015-10-17 21:41:24 -04:00

332 lines
12 KiB
Python

import os
import base64
import requests
import time
import zlib
from pycrest import version
from pycrest.compat import bytes_, text_
from pycrest.errors import APIException
from pycrest.weak_ciphers import WeakCiphersAdapter
try:
from urllib.parse import urlparse, urlunparse, parse_qsl
except ImportError: # pragma: no cover
from urlparse import urlparse, urlunparse, parse_qsl
try:
import pickle
except ImportError: # pragma: no cover
import cPickle as pickle
try:
from urllib.parse import quote
except ImportError: # pragma: no cover
from urllib import quote
import logging
import re
logger = logging.getLogger("pycrest.eve")
cache_re = re.compile(r'max-age=([0-9]+)')
class APICache(object):
def put(self, key, value):
raise NotImplementedError
def get(self, key):
raise NotImplementedError
def invalidate(self, key):
raise NotImplementedError
class FileCache(APICache):
def __init__(self, path):
self._cache = {}
self.path = path
if not os.path.isdir(self.path):
os.mkdir(self.path, 0o700)
def _getpath(self, key):
return os.path.join(self.path, str(hash(key)) + '.cache')
def put(self, key, value):
with open(self._getpath(key), 'wb') as f:
f.write(zlib.compress(pickle.dumps(value, -1)))
self._cache[key] = value
def get(self, key):
if key in self._cache:
return self._cache[key]
try:
with open(self._getpath(key), 'rb') as f:
return pickle.loads(zlib.decompress(f.read()))
except IOError as ex:
if ex.errno == 2: # file does not exist (yet)
return None
else:
raise
def invalidate(self, key):
self._cache.pop(key, None)
try:
os.unlink(self._getpath(key))
except OSError as ex:
if ex.errno == 2: # does not exist
pass
else:
raise
class DictCache(APICache):
def __init__(self):
self._dict = {}
def get(self, key):
return self._dict.get(key, None)
def put(self, key, value):
self._dict[key] = value
def invalidate(self, key):
self._dict.pop(key, None)
class APIConnection(object):
def __init__(self, additional_headers=None, user_agent=None, cache_dir=None, cache=None):
# Set up a Requests Session
session = requests.Session()
if additional_headers is None:
additional_headers = {}
if user_agent is None:
user_agent = "PyCrest/{0}".format(version)
session.headers.update({
"User-Agent": user_agent,
"Accept": "application/json",
})
session.headers.update(additional_headers)
session.mount('https://public-crest.eveonline.com',
WeakCiphersAdapter())
self._session = session
if 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()
def get(self, resource, params=None):
print resource, params
logger.debug('Getting resource %s', resource)
if params is None:
params = {}
# remove params from resource URI (needed for paginated stuff)
parsed_uri = urlparse(resource)
qs = parsed_uri.query
resource = urlunparse(parsed_uri._replace(query=''))
prms = {}
for tup in parse_qsl(qs):
prms[tup[0]] = tup[1]
# params supplied to self.get() override parsed params
for key in params:
prms[key] = params[key]
# check cache
key = (resource, frozenset(self._session.headers.items()), frozenset(prms.items()))
cached = self.cache.get(key)
if cached and cached['expires'] > time.time():
logger.debug('Cache hit for resource %s (params=%s)', resource, prms)
return cached['payload']
elif cached:
logger.debug('Cache stale for resource %s (params=%s)', resource, prms)
self.cache.invalidate(key)
else:
logger.debug('Cache miss for resource %s (params=%s', resource, prms)
logger.debug('Getting resource %s (params=%s)', resource, prms)
res = self._session.get(resource, params=prms)
if res.status_code != 200:
raise APIException("Got unexpected status code from server: %i" % res.status_code)
ret = res.json()
# cache result
key = (resource, frozenset(self._session.headers.items()), frozenset(prms.items()))
expires = self._get_expires(res)
if expires > 0:
self.cache.put(key, {'expires': time.time() + expires, 'payload': ret})
return ret
def _get_expires(self, response):
if 'Cache-Control' not in response.headers:
return 0
if any([s in response.headers['Cache-Control'] for s in ['no-cache', 'no-store']]):
return 0
match = cache_re.search(response.headers['Cache-Control'])
if match:
return int(match.group(1))
return 0
class EVE(APIConnection):
def __init__(self, **kwargs):
self.api_key = kwargs.pop('api_key', None)
self.client_id = kwargs.pop('client_id', None)
self.redirect_uri = kwargs.pop('redirect_uri', None)
if kwargs.pop('testing', False):
self._public_endpoint = "http://public-crest-sisi.testeveonline.com/"
self._authed_endpoint = "https://api-sisi.testeveonline.com/"
self._image_server = "https://image.testeveonline.com/"
self._oauth_endpoint = "https://sisilogin.testeveonline.com/oauth"
else:
self._public_endpoint = "https://public-crest.eveonline.com/"
self._authed_endpoint = "https://crest-tq.eveonline.com/"
self._image_server = "https://image.eveonline.com/"
self._oauth_endpoint = "https://login.eveonline.com/oauth"
self._endpoint = self._public_endpoint
self._cache = {}
self._data = None
APIConnection.__init__(self, **kwargs)
def __call__(self):
if not self._data:
self._data = APIObject(self.get(self._endpoint), self)
return self._data
def __getattr__(self, item):
return self._data.__getattr__(item)
def auth_uri(self, scopes=None, state=None):
s = [] if not scopes else scopes
return "%s/authorize?response_type=code&redirect_uri=%s&client_id=%s%s%s" % (
self._oauth_endpoint,
quote(self.redirect_uri, safe=''),
self.client_id,
"&scope=%s" % ' '.join(s) if scopes else '',
"&state=%s" % state if state else ''
)
def _authorize(self, params):
auth = text_(base64.b64encode(bytes_("%s:%s" % (self.client_id, self.api_key))))
headers = {"Authorization": "Basic %s" % auth}
res = self._session.post("%s/token" % self._oauth_endpoint, params=params, headers=headers)
if res.status_code != 200:
raise APIException("Got unexpected status code from API: %i" % res.status_code)
return res.json()
def authorize(self, code):
res = self._authorize(params={"grant_type": "authorization_code", "code": code})
return AuthedConnection(res,
self._authed_endpoint,
self._oauth_endpoint,
self.client_id,
self.api_key,
cache=self.cache)
def refr_authorize(self, refresh_token):
res = self._authorize(params={"grant_type": "refresh_token", "refresh_token": refresh_token})
return AuthedConnection({'access_token': res['access_token'],
'refresh_token': refresh_token,
'expires_in': res['expires_in']},
self._authed_endpoint,
self._oauth_endpoint,
self.client_id,
self.api_key,
cache=self.cache)
def temptoken_authorize(self, access_token, expires_in, refresh_token):
return AuthedConnection({'access_token': access_token,
'refresh_token': refresh_token,
'expires_in': expires_in},
self._authed_endpoint,
self._oauth_endpoint,
self.client_id,
self.api_key,
cache=self.cache)
class AuthedConnection(EVE):
def __init__(self, res, endpoint, oauth_endpoint, client_id=None, api_key=None, **kwargs):
EVE.__init__(self, **kwargs)
self.client_id = client_id
self.api_key = api_key
self.token = res['access_token']
self.refresh_token = res['refresh_token']
self.expires = int(time.time()) + res['expires_in']
self._oauth_endpoint = oauth_endpoint
self._endpoint = endpoint
self._session.headers.update({"Authorization": "Bearer %s" % self.token})
def __call__(self):
if not self._data:
self._data = APIObject(self.get(self._endpoint), self)
return self._data
def whoami(self):
if 'whoami' not in self._cache:
self._cache['whoami'] = self.get("%s/verify" % self._oauth_endpoint)
return self._cache['whoami']
def refresh(self):
res = self._authorize(params={"grant_type": "refresh_token", "refresh_token": self.refresh_token})
self.token = res['access_token']
self.expires = int(time.time()) + res['expires_in']
self._session.headers.update({"Authorization": "Bearer %s" % self.token})
return self # for backwards compatibility
def get(self, resource, params=None):
if int(time.time()) >= self.expires:
self.refresh()
return super(self.__class__, self).get(resource, params)
class APIObject(object):
def __init__(self, parent, connection):
self._dict = {}
self.connection = connection
for k, v in parent.items():
if type(v) is dict:
self._dict[k] = APIObject(v, connection)
elif type(v) is list:
self._dict[k] = self._wrap_list(v)
else:
self._dict[k] = v
def _wrap_list(self, list_):
new = []
for item in list_:
if type(item) is dict:
new.append(APIObject(item, self.connection))
elif type(item) is list:
new.append(self._wrap_list(item))
else:
new.append(item)
return new
def __getattr__(self, item):
if item in self._dict:
return self._dict[item]
raise AttributeError(item)
def __call__(self, **kwargs):
# Caching is now handled by APIConnection
if 'href' in self._dict:
return APIObject(self.connection.get(self._dict['href'], params=kwargs), self.connection)
else:
return self
def __str__(self): # pragma: no cover
return self._dict.__str__()
def __repr__(self): # pragma: no cover
return self._dict.__repr__()