A corporation manager and dashboard for EVE Online
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 

228 lignes
7.3 KiB

  1. # -*- coding: utf-8 -*-
  2. from datetime import datetime
  3. import random
  4. from threading import Lock
  5. import requests
  6. from ..exceptions import EVEAPIError, EVEAPIForbiddenError
  7. __all__ = ["EVESwaggerInterface"]
  8. class _ESICache:
  9. """Caches ESI API responses according to their headers.
  10. This interface is thread-safe.
  11. """
  12. EXPIRATION_RATE = 0.2
  13. def __init__(self):
  14. self._public = {}
  15. self._private = {}
  16. self._lock = Lock()
  17. @staticmethod
  18. def _get_cache_policy(resp):
  19. """Calculate the expiry and availability of the given response."""
  20. if "Cache-Control" not in resp.headers:
  21. return None
  22. directives = resp.headers["Cache-Control"].lower().split(";")
  23. directives = [d.strip() for d in directives]
  24. if "public" in directives:
  25. public = True
  26. elif "private" in directives:
  27. public = False
  28. else:
  29. return None
  30. expires = resp.headers.get("Expires")
  31. if not expires:
  32. return None
  33. try:
  34. expiry = datetime.strptime(expires, "%a, %d %b %Y %H:%M:%S GMT")
  35. except ValueError:
  36. return None
  37. return public, expiry
  38. @staticmethod
  39. def freeze_dict(d):
  40. """Return a hashable string key for the given dictionary."""
  41. if not d:
  42. return "{}"
  43. items = sorted((repr(str(k)), repr(str(v))) for k, v in d.items())
  44. return "{" + ",".join(":".join(p) for p in items) + "}"
  45. def _expire(self):
  46. """Remove old entries from the cache. Assumes lock is acquired."""
  47. now = datetime.utcnow()
  48. for index in (self._private, self._public):
  49. condemned = []
  50. for key, (expiry, _) in index.items():
  51. if expiry < now:
  52. condemned.append(key)
  53. for key in condemned:
  54. del index[key]
  55. def fetch(self, key, token):
  56. """Try to look up a key in the cache. Return None if not found.
  57. The key should be a string. The token is used if a route is cached
  58. privately.
  59. This will periodically clear the cache of expired entries.
  60. """
  61. with self._lock:
  62. if random.random() < self.EXPIRATION_RATE:
  63. self._expire()
  64. if (key, token) in self._private:
  65. expiry, value = self._private[key, token]
  66. if expiry > datetime.utcnow():
  67. return value
  68. if key in self._public:
  69. expiry, value = self._public[key]
  70. if expiry > datetime.utcnow():
  71. return value
  72. return None
  73. def insert(self, key, token, value, response):
  74. """Store a key-value pair using the response as cache control."""
  75. policy = self._get_cache_policy(response)
  76. if not policy:
  77. return
  78. public, expiry = policy
  79. if expiry > datetime.utcnow():
  80. with self._lock:
  81. if public:
  82. self._public[key] = (expiry, value)
  83. else:
  84. self._private[key, token] = (expiry, value)
  85. class _ESIQueryBuilder:
  86. """Stores an ESI query that is being built by the client."""
  87. def __init__(self, esi, token):
  88. self._esi = esi
  89. self._token = token
  90. self._path = "/"
  91. def __getattr__(self, item):
  92. self._path += str(item) + "/"
  93. return self
  94. def __call__(self, item):
  95. self._path += str(item) + "/"
  96. return self
  97. def get(self, **kwargs):
  98. """Do an HTTP GET request for the built query."""
  99. return self._esi.get(self._path, self._token, params=kwargs)
  100. def post(self, **kwargs):
  101. """Do an HTTP POST request for the built query."""
  102. return self._esi.post(self._path, self._token, data=kwargs)
  103. def put(self, **kwargs):
  104. """Do an HTTP PUT request for the built query."""
  105. return self._esi.put(self._path, self._token, data=kwargs)
  106. def delete(self, **kwargs):
  107. """Do an HTTP DELETE request for the built query."""
  108. return self._esi.delete(self._path, self._token, params=kwargs)
  109. class EVESwaggerInterface:
  110. """EVE API module for the EVE Swagger Interface (ESI).
  111. There are two equivalent ways to use this interface:
  112. data = esi.get("/v3/characters/{char_id}/".format(char_id=char_id), token)
  113. data = esi(token).v3.characters(char_id).get()
  114. For more complex requests:
  115. data = esi.post("/v1/universe/names/", token, data={"ids": [entity_id]})
  116. data = esi(token).v1.universe.names.post(ids=[entity_id]})
  117. """
  118. def __init__(self, session, logger):
  119. self._session = session
  120. self._logger = logger
  121. self._debug = logger.debug
  122. self._base_url = "https://esi.tech.ccp.is"
  123. self._data_source = "tranquility"
  124. self._cache = _ESICache()
  125. def __call__(self, token=None):
  126. return _ESIQueryBuilder(self, token)
  127. def _do(self, method, query, params, data, token, can_cache=False):
  128. """Execute a query using a token with the given session method.
  129. Return the JSON result, if any. Raise EVEAPIError for any errors.
  130. """
  131. if can_cache:
  132. pkey = self._cache.freeze_dict(params)
  133. key = "|".join((method.__name__, self._data_source, query, pkey))
  134. cached = self._cache.fetch(key, token)
  135. else:
  136. cached = None
  137. self._debug("[%s] [%s] %s", method.__name__.upper(),
  138. "fresh" if cached is None else "cache", query)
  139. if cached is not None:
  140. return cached
  141. headers = {"Accept": "application/json"}
  142. if token is not None:
  143. headers["Authorization"] = "Bearer " + token
  144. params = params.copy() if params else {}
  145. params["datasource"] = self._data_source
  146. url = self._base_url + query
  147. try:
  148. resp = method(url, params=params, json=data or None,
  149. headers=headers, timeout=10)
  150. resp.raise_for_status()
  151. result = resp.json() if resp.content else None
  152. except (requests.RequestException, ValueError) as exc:
  153. self._logger.exception("ESI request failed")
  154. if hasattr(exc, "response") and (exc.response and
  155. exc.response.status_code == 403):
  156. raise EVEAPIForbiddenError()
  157. raise EVEAPIError()
  158. if can_cache and result is not None:
  159. self._cache.insert(key, token, result, resp)
  160. return result
  161. def get(self, query, token, params=None):
  162. """Do an HTTP GET request for a query using a token."""
  163. meth = self._session.get
  164. return self._do(meth, query, params, None, token, True)
  165. def post(self, query, token, params=None, data=None):
  166. """Do an HTTP POST request for a query using a token."""
  167. meth = self._session.post
  168. return self._do(meth, query, params, data, token)
  169. def put(self, query, token, params=None, data=None):
  170. """Do an HTTP PUT request for a query using a token."""
  171. meth = self._session.put
  172. return self._do(meth, query, params, data, token)
  173. def delete(self, query, token, params=None):
  174. """Do an HTTP DELETE request for a query using a token."""
  175. meth = self._session.delete
  176. return self._do(meth, query, params, None, token)