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.
 
 
 
 
 

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