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.
 
 
 
 
 

193 lignes
6.1 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):
  101. self._session = session
  102. self._base_url = "https://esi.tech.ccp.is"
  103. self._data_source = "tranquility"
  104. self._cache = _ESICache()
  105. def __call__(self, token):
  106. return _ESIQueryBuilder(self, token)
  107. def _do(self, method, query, params, data, token, can_cache=False):
  108. """Execute a query using a token with the given session method.
  109. Return the JSON result, if any. Raise EVEAPIError for any errors.
  110. """
  111. if can_cache:
  112. pkey = self._cache.freeze_dict(params)
  113. key = "|".join((method.__name__, self._data_source, query, pkey))
  114. cached = self._cache.fetch(key)
  115. if cached:
  116. return cached
  117. headers = {
  118. "Accept": "application/json",
  119. "Authorization": "Bearer " + token
  120. }
  121. params = params.copy() if params else {}
  122. params["datasource"] = self._data_source
  123. url = self._base_url + query
  124. try:
  125. resp = method(url, params=params, json=data or None,
  126. headers=headers, timeout=10)
  127. resp.raise_for_status()
  128. result = resp.json() if resp.content else None
  129. except (requests.RequestException, ValueError) as exc:
  130. raise EVEAPIError(str(exc))
  131. if can_cache and result is not None:
  132. self._cache.insert(key, result, resp)
  133. return result
  134. def get(self, query, token, params=None):
  135. """Do an HTTP GET request for a query using a token."""
  136. meth = self._session.get
  137. return self._do(meth, query, params, None, token, True)
  138. def post(self, query, token, params=None, data=None):
  139. """Do an HTTP POST request for a query using a token."""
  140. meth = self._session.post
  141. return self._do(meth, query, params, data, token)
  142. def put(self, query, token, params=None, data=None):
  143. """Do an HTTP PUT request for a query using a token."""
  144. meth = self._session.put
  145. return self._do(meth, query, params, data, token)
  146. def delete(self, query, token, params=None):
  147. """Do an HTTP DELETE request for a query using a token."""
  148. meth = self._session.delete
  149. return self._do(meth, query, params, None, token)