django-esi 8.0.0a3__py3-none-any.whl → 8.0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-esi might be problematic. Click here for more details.
- {django_esi-8.0.0a3.dist-info → django_esi-8.0.0b1.dist-info}/METADATA +3 -2
- {django_esi-8.0.0a3.dist-info → django_esi-8.0.0b1.dist-info}/RECORD +44 -33
- esi/__init__.py +2 -2
- esi/aiopenapi3/plugins.py +100 -3
- esi/clients.py +15 -6
- esi/helpers.py +37 -0
- esi/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
- esi/locale/cs_CZ/LC_MESSAGES/django.po +53 -0
- esi/locale/de/LC_MESSAGES/django.mo +0 -0
- esi/locale/de/LC_MESSAGES/django.po +10 -9
- esi/locale/en/LC_MESSAGES/django.mo +0 -0
- esi/locale/en/LC_MESSAGES/django.po +3 -3
- esi/locale/es/LC_MESSAGES/django.mo +0 -0
- esi/locale/es/LC_MESSAGES/django.po +12 -10
- esi/locale/fr_FR/LC_MESSAGES/django.mo +0 -0
- esi/locale/fr_FR/LC_MESSAGES/django.po +18 -10
- esi/locale/it_IT/LC_MESSAGES/django.mo +0 -0
- esi/locale/it_IT/LC_MESSAGES/django.po +12 -10
- esi/locale/ja/LC_MESSAGES/django.mo +0 -0
- esi/locale/ja/LC_MESSAGES/django.po +10 -9
- esi/locale/ko_KR/LC_MESSAGES/django.mo +0 -0
- esi/locale/ko_KR/LC_MESSAGES/django.po +10 -9
- esi/locale/nl_NL/LC_MESSAGES/django.mo +0 -0
- esi/locale/nl_NL/LC_MESSAGES/django.po +53 -0
- esi/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
- esi/locale/pl_PL/LC_MESSAGES/django.po +53 -0
- esi/locale/ru/LC_MESSAGES/django.mo +0 -0
- esi/locale/ru/LC_MESSAGES/django.po +13 -10
- esi/locale/sk/LC_MESSAGES/django.mo +0 -0
- esi/locale/sk/LC_MESSAGES/django.po +55 -0
- esi/locale/uk/LC_MESSAGES/django.mo +0 -0
- esi/locale/uk/LC_MESSAGES/django.po +57 -0
- esi/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- esi/locale/zh_Hans/LC_MESSAGES/django.po +10 -9
- esi/management/commands/generate_esi_stubs.py +11 -31
- esi/models.py +1 -1
- esi/openapi_clients.py +207 -54
- esi/stubs.pyi +395 -395
- esi/tests/__init__.py +3 -3
- esi/tests/test_clients.py +77 -19
- esi/tests/test_openapi.json +264 -0
- esi/tests/test_openapi.py +402 -1
- {django_esi-8.0.0a3.dist-info → django_esi-8.0.0b1.dist-info}/WHEEL +0 -0
- {django_esi-8.0.0a3.dist-info → django_esi-8.0.0b1.dist-info}/licenses/LICENSE +0 -0
esi/openapi_clients.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import pathlib
|
|
2
3
|
import warnings
|
|
3
|
-
|
|
4
|
+
import datetime as dt
|
|
4
5
|
from hashlib import md5
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
|
-
from aiopenapi3 import OpenAPI
|
|
8
|
+
from aiopenapi3 import OpenAPI, FileSystemLoader
|
|
8
9
|
from aiopenapi3._types import ResponseDataType, ResponseHeadersType
|
|
9
10
|
from aiopenapi3.errors import HTTPServerError as base_HTTPServerError
|
|
10
11
|
from aiopenapi3.errors import HTTPClientError as base_HTTPClientError
|
|
@@ -18,18 +19,25 @@ from tenacity import (
|
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
from django.core.cache import cache
|
|
22
|
+
from django.utils.text import slugify
|
|
21
23
|
|
|
22
24
|
from esi import app_settings
|
|
23
25
|
from esi.exceptions import HTTPClientError, HTTPServerError, HTTPNotModified
|
|
24
|
-
from esi.aiopenapi3.plugins import
|
|
26
|
+
from esi.aiopenapi3.plugins import (
|
|
27
|
+
Add304ContentType, DjangoESIInit, PatchCompatibilityDatePlugin,
|
|
28
|
+
Trim204ContentType, MinifySpec
|
|
29
|
+
)
|
|
25
30
|
from esi.exceptions import ESIErrorLimitException
|
|
26
31
|
from esi.models import Token
|
|
27
32
|
from esi.stubs import ESIClientStub
|
|
28
33
|
|
|
29
34
|
from . import __title__, __url__, __version__
|
|
35
|
+
from .helpers import pascal_case_string
|
|
30
36
|
|
|
31
37
|
logger = logging.getLogger(__name__)
|
|
32
38
|
|
|
39
|
+
ETAG_EXPIRY = 60*60*24*7 # 7 days
|
|
40
|
+
|
|
33
41
|
|
|
34
42
|
def _time_to_expiry(expires_header: str) -> int:
|
|
35
43
|
"""Calculate cache TTL from Expires header
|
|
@@ -39,8 +47,10 @@ def _time_to_expiry(expires_header: str) -> int:
|
|
|
39
47
|
int: The cache TTL in seconds
|
|
40
48
|
"""
|
|
41
49
|
try:
|
|
42
|
-
expires_dt = datetime.strptime(str(expires_header), '%a, %d %b %Y %H:%M:%S %Z')
|
|
43
|
-
|
|
50
|
+
expires_dt = dt.datetime.strptime(str(expires_header), '%a, %d %b %Y %H:%M:%S %Z')
|
|
51
|
+
if expires_dt.tzinfo is None:
|
|
52
|
+
expires_dt = expires_dt.replace(tzinfo=dt.timezone.utc)
|
|
53
|
+
return max(int((expires_dt - dt.datetime.now(dt.timezone.utc)).total_seconds()), 0)
|
|
44
54
|
except ValueError:
|
|
45
55
|
return 0
|
|
46
56
|
|
|
@@ -81,17 +91,36 @@ async def http_retry_async() -> AsyncRetrying:
|
|
|
81
91
|
)
|
|
82
92
|
|
|
83
93
|
|
|
94
|
+
def _load_plugins(app_name, tags: list[str]=[], operations: list[str]=[]):
|
|
95
|
+
"""Load the plugins to make ESI work with this lib.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
app_name (str): app name to use for internal etags
|
|
99
|
+
"""
|
|
100
|
+
return [
|
|
101
|
+
PatchCompatibilityDatePlugin(),
|
|
102
|
+
Trim204ContentType(),
|
|
103
|
+
Add304ContentType(),
|
|
104
|
+
DjangoESIInit(app_name),
|
|
105
|
+
MinifySpec(tags, operations)
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
|
|
84
109
|
def _load_aiopenapi_client_sync(
|
|
85
110
|
spec_url: str,
|
|
86
111
|
compatibility_date: str,
|
|
112
|
+
app_name: str,
|
|
87
113
|
user_agent: str,
|
|
88
114
|
tenant: str,
|
|
89
|
-
spec_file: str | None = None
|
|
115
|
+
spec_file: str | None = None,
|
|
116
|
+
tags: list[str] = [],
|
|
117
|
+
operations: list[str] = []) -> OpenAPI:
|
|
90
118
|
"""Create an OpenAPI3 Client from Spec
|
|
91
119
|
|
|
92
120
|
Args:
|
|
93
121
|
spec_url (str): _description_
|
|
94
122
|
compatibility_date (str): _description_
|
|
123
|
+
app_name (str): _description_
|
|
95
124
|
user_agent (str): _description_
|
|
96
125
|
tenant (str): _description_
|
|
97
126
|
spec_file (str | None, optional): _description_. Defaults to None.
|
|
@@ -123,21 +152,23 @@ def _load_aiopenapi_client_sync(
|
|
|
123
152
|
url=spec_url,
|
|
124
153
|
path=spec_file,
|
|
125
154
|
session_factory=session_factory,
|
|
155
|
+
loader=FileSystemLoader(pathlib.Path(spec_file)),
|
|
126
156
|
use_operation_tags=True,
|
|
127
|
-
plugins=
|
|
157
|
+
plugins=_load_plugins(app_name, tags, operations)
|
|
128
158
|
)
|
|
129
159
|
else:
|
|
130
160
|
return OpenAPI.load_sync(
|
|
131
161
|
url=spec_url,
|
|
132
162
|
session_factory=session_factory,
|
|
133
163
|
use_operation_tags=True,
|
|
134
|
-
plugins=
|
|
164
|
+
plugins=_load_plugins(app_name, tags, operations)
|
|
135
165
|
)
|
|
136
166
|
|
|
137
167
|
|
|
138
168
|
async def _load_aiopenapi_client_async(
|
|
139
169
|
spec_url: str,
|
|
140
170
|
compatibility_date: str,
|
|
171
|
+
app_name: str,
|
|
141
172
|
user_agent: str,
|
|
142
173
|
tenant: str,
|
|
143
174
|
spec_file: str | None = None) -> OpenAPI:
|
|
@@ -179,18 +210,18 @@ async def _load_aiopenapi_client_async(
|
|
|
179
210
|
path=spec_file,
|
|
180
211
|
session_factory=session_factory,
|
|
181
212
|
use_operation_tags=True,
|
|
182
|
-
plugins=
|
|
213
|
+
plugins=_load_plugins(app_name)
|
|
183
214
|
)
|
|
184
215
|
else:
|
|
185
216
|
return await OpenAPI.load_async(
|
|
186
217
|
url=spec_url,
|
|
187
218
|
session_factory=session_factory,
|
|
188
219
|
use_operation_tags=True,
|
|
189
|
-
plugins=
|
|
220
|
+
plugins=_load_plugins(app_name)
|
|
190
221
|
)
|
|
191
222
|
|
|
192
223
|
|
|
193
|
-
def _build_user_agent(ua_appname: str, ua_version: str, ua_url: str | None) -> str:
|
|
224
|
+
def _build_user_agent(ua_appname: str, ua_version: str, ua_url: str | None = None) -> str:
|
|
194
225
|
"""
|
|
195
226
|
AppName/1.2.3 (foo@example.com; +https://gitlab.com/) Django-ESI/1.2.3 (+https://gitlab.com/allianceauth/django-esi)
|
|
196
227
|
Contact Email will be inserted from app_settings.
|
|
@@ -201,10 +232,15 @@ def _build_user_agent(ua_appname: str, ua_version: str, ua_url: str | None) -> s
|
|
|
201
232
|
Returns:
|
|
202
233
|
str: User-Agent string
|
|
203
234
|
"""
|
|
235
|
+
|
|
236
|
+
# Enforce PascalCase for `ua_appname` and strip whitespace
|
|
237
|
+
sanitized_ua_appname = pascal_case_string(ua_appname)
|
|
238
|
+
sanitized_appname = pascal_case_string(__title__)
|
|
239
|
+
|
|
204
240
|
return (
|
|
205
|
-
f"{
|
|
241
|
+
f"{sanitized_ua_appname}/{ua_version} "
|
|
206
242
|
f"({app_settings.ESI_USER_CONTACT_EMAIL}{f'; +{ua_url})' if ua_url else ')'} "
|
|
207
|
-
f"{
|
|
243
|
+
f"{sanitized_appname}/{__version__} (+{__url__})"
|
|
208
244
|
)
|
|
209
245
|
|
|
210
246
|
|
|
@@ -217,6 +253,7 @@ def esi_client_factory_sync(
|
|
|
217
253
|
ua_appname: str, ua_version: str, ua_url: str | None = None,
|
|
218
254
|
spec_file: str | None = None,
|
|
219
255
|
tenant: str = "tranquility",
|
|
256
|
+
tags: list[str]=[], operations: list[str]=[],
|
|
220
257
|
**kwargs) -> OpenAPI:
|
|
221
258
|
"""Generate a new OpenAPI ESI client.
|
|
222
259
|
Args:
|
|
@@ -231,7 +268,16 @@ def esi_client_factory_sync(
|
|
|
231
268
|
"""
|
|
232
269
|
user_agent = _build_user_agent(ua_appname, ua_version, ua_url)
|
|
233
270
|
spec_url = _get_spec_url()
|
|
234
|
-
return _load_aiopenapi_client_sync(
|
|
271
|
+
return _load_aiopenapi_client_sync(
|
|
272
|
+
spec_url,
|
|
273
|
+
compatibility_date,
|
|
274
|
+
ua_appname,
|
|
275
|
+
user_agent,
|
|
276
|
+
tenant,
|
|
277
|
+
spec_file,
|
|
278
|
+
tags,
|
|
279
|
+
operations
|
|
280
|
+
)
|
|
235
281
|
|
|
236
282
|
|
|
237
283
|
async def esi_client_factory_async(
|
|
@@ -253,11 +299,11 @@ async def esi_client_factory_async(
|
|
|
253
299
|
"""
|
|
254
300
|
user_agent = _build_user_agent(ua_appname, ua_version, ua_url)
|
|
255
301
|
spec_url = _get_spec_url()
|
|
256
|
-
return await _load_aiopenapi_client_async(spec_url, compatibility_date, user_agent, tenant, spec_file)
|
|
302
|
+
return await _load_aiopenapi_client_async(spec_url, compatibility_date, ua_appname, user_agent, tenant, spec_file)
|
|
257
303
|
|
|
258
304
|
|
|
259
305
|
class BaseEsiOperation():
|
|
260
|
-
def __init__(self, operation, api) -> None:
|
|
306
|
+
def __init__(self, operation, api: OpenAPI) -> None:
|
|
261
307
|
self.method, self.url, self.operation, self.extra = operation
|
|
262
308
|
self.api = api
|
|
263
309
|
self.token: Token | None = None
|
|
@@ -320,16 +366,24 @@ class BaseEsiOperation():
|
|
|
320
366
|
|
|
321
367
|
return normalized
|
|
322
368
|
|
|
369
|
+
def _etag_key(self) -> str:
|
|
370
|
+
"""Generate a key name used to cache etag responses based on app_name and cache_key
|
|
371
|
+
Returns:
|
|
372
|
+
str: Key
|
|
373
|
+
"""
|
|
374
|
+
# ignore the token this will break the cache
|
|
375
|
+
return f"{slugify(self.api.app_name)}_etag_{self._cache_key()}"
|
|
376
|
+
|
|
323
377
|
def _cache_key(self) -> str:
|
|
324
378
|
"""Generate a key name used to cache responses based on method, url, args, kwargs
|
|
325
379
|
Returns:
|
|
326
|
-
str: Key
|
|
380
|
+
str: Key
|
|
327
381
|
"""
|
|
328
382
|
# ignore the token this will break the cache
|
|
329
383
|
ignore_keys = [
|
|
330
384
|
"token",
|
|
331
385
|
]
|
|
332
|
-
_kwargs = {
|
|
386
|
+
_kwargs = {key: value for key, value in self._kwargs.items() if key not in ignore_keys}
|
|
333
387
|
data = (self.method + self.url + str(self._args) + str(_kwargs)).encode('utf-8')
|
|
334
388
|
str_hash = md5(data).hexdigest() # nosec B303
|
|
335
389
|
return f'esi_{str_hash}'
|
|
@@ -376,6 +430,9 @@ class BaseEsiOperation():
|
|
|
376
430
|
tuple[ResponseHeadersType | None, Any, Response | None]: The cached response,
|
|
377
431
|
or None if not found or expired
|
|
378
432
|
"""
|
|
433
|
+
if not app_settings.ESI_CACHE_RESPONSE:
|
|
434
|
+
return None, None, None
|
|
435
|
+
|
|
379
436
|
try:
|
|
380
437
|
cached_response = cache.get(cache_key)
|
|
381
438
|
except Exception as e:
|
|
@@ -393,7 +450,9 @@ class BaseEsiOperation():
|
|
|
393
450
|
|
|
394
451
|
# check if etag is same before building models from cache
|
|
395
452
|
if etag:
|
|
396
|
-
if cached_response.headers.get('
|
|
453
|
+
if cached_response.headers.get('ETag') == etag:
|
|
454
|
+
# refresh/store the etag's TTL
|
|
455
|
+
self._store_etag(cached_response.headers)
|
|
397
456
|
raise HTTPNotModified(
|
|
398
457
|
status_code=304,
|
|
399
458
|
headers=cached_response.headers
|
|
@@ -405,8 +464,23 @@ class BaseEsiOperation():
|
|
|
405
464
|
|
|
406
465
|
return None, None, None
|
|
407
466
|
|
|
467
|
+
def _store_etag(self, headers: dict):
|
|
468
|
+
"""
|
|
469
|
+
Store response etag in cache for 7 days
|
|
470
|
+
"""
|
|
471
|
+
if "ETag" in headers:
|
|
472
|
+
cache.set(self._etag_key(), headers["ETag"], timeout=ETAG_EXPIRY)
|
|
473
|
+
|
|
474
|
+
def _clear_etag(self):
|
|
475
|
+
""" Delete the cached etag for this operation.
|
|
476
|
+
"""
|
|
477
|
+
try:
|
|
478
|
+
cache.delete(self._etag_key())
|
|
479
|
+
except Exception as e:
|
|
480
|
+
logger.error(f"Failed to delete etag {e}", exc_info=True)
|
|
481
|
+
|
|
408
482
|
def _store_cache(self, cache_key: str, response) -> None:
|
|
409
|
-
""" Store the response in cache
|
|
483
|
+
""" Store the response in cache for expiry TTL.
|
|
410
484
|
Args:
|
|
411
485
|
cache_key (str): The cache key to store the response under
|
|
412
486
|
response (Response): The response object to cache
|
|
@@ -414,9 +488,6 @@ class BaseEsiOperation():
|
|
|
414
488
|
if not app_settings.ESI_CACHE_RESPONSE:
|
|
415
489
|
return
|
|
416
490
|
|
|
417
|
-
if "ETag" in response.headers:
|
|
418
|
-
cache.set(f"{cache_key}_etag", response.headers["ETag"])
|
|
419
|
-
|
|
420
491
|
expires = response.headers.get("Expires")
|
|
421
492
|
ttl = _time_to_expiry(expires) if expires else 0
|
|
422
493
|
if ttl > 0:
|
|
@@ -425,6 +496,14 @@ class BaseEsiOperation():
|
|
|
425
496
|
except Exception as e:
|
|
426
497
|
logger.error(f"Failed to cache {e}", exc_info=True)
|
|
427
498
|
|
|
499
|
+
def _clear_cache(self):
|
|
500
|
+
""" Delete the cached data for this operation.
|
|
501
|
+
"""
|
|
502
|
+
try:
|
|
503
|
+
cache.delete(self._cache_key())
|
|
504
|
+
except Exception as e:
|
|
505
|
+
logger.error(f"Failed to delete cache {e}", exc_info=True)
|
|
506
|
+
|
|
428
507
|
def _validate_token_scopes(self, token: Token) -> None:
|
|
429
508
|
"""Validate that the token provided has the required scopes for this ESI operation.
|
|
430
509
|
"""
|
|
@@ -484,8 +563,9 @@ class EsiOperation(BaseEsiOperation):
|
|
|
484
563
|
|
|
485
564
|
def result(
|
|
486
565
|
self,
|
|
487
|
-
|
|
566
|
+
use_etag: bool = True,
|
|
488
567
|
return_response: bool = False,
|
|
568
|
+
force_refresh: bool = False,
|
|
489
569
|
use_cache: bool = True,
|
|
490
570
|
**extra) -> tuple[Any, Response] | Any:
|
|
491
571
|
"""Executes the request and returns the response from ESI for the current operation.
|
|
@@ -499,9 +579,14 @@ class EsiOperation(BaseEsiOperation):
|
|
|
499
579
|
self.body = self._extract_body_param()
|
|
500
580
|
parameters = self._kwargs | extra
|
|
501
581
|
cache_key = self._cache_key()
|
|
502
|
-
etag_key =
|
|
582
|
+
etag_key = self._etag_key()
|
|
583
|
+
etag = None
|
|
503
584
|
|
|
504
|
-
if
|
|
585
|
+
if force_refresh:
|
|
586
|
+
self._clear_cache()
|
|
587
|
+
self._clear_etag()
|
|
588
|
+
|
|
589
|
+
if use_etag:
|
|
505
590
|
etag = cache.get(etag_key)
|
|
506
591
|
|
|
507
592
|
headers, data, response = self._get_cache(cache_key, etag=etag) if use_cache else (None, None, None)
|
|
@@ -524,13 +609,6 @@ class EsiOperation(BaseEsiOperation):
|
|
|
524
609
|
reset = response.headers.get("X-RateLimit-Reset", None)
|
|
525
610
|
cache.set("esi_error_limit_reset", reset, timeout=reset)
|
|
526
611
|
raise ESIErrorLimitException(reset=reset)
|
|
527
|
-
self._store_cache(cache_key, response)
|
|
528
|
-
|
|
529
|
-
# if response.status_code == 304 and app_settings.ESI_CACHE_RESPONSE:
|
|
530
|
-
# cached = cache.get(cache_key)
|
|
531
|
-
# if cached:
|
|
532
|
-
# return (cached, response) if return_response else cached
|
|
533
|
-
# we dont want to do this, if we do this we have to store data longer than ttl. rip ram
|
|
534
612
|
|
|
535
613
|
# Shim our exceptions into Django-ESI
|
|
536
614
|
except base_HTTPServerError as e:
|
|
@@ -549,17 +627,24 @@ class EsiOperation(BaseEsiOperation):
|
|
|
549
627
|
|
|
550
628
|
# Throw a 304 exception for catching.
|
|
551
629
|
if response.status_code == 304:
|
|
630
|
+
# refresh/store the etag's TTL
|
|
631
|
+
self._store_etag(response.headers)
|
|
552
632
|
raise HTTPNotModified(
|
|
553
633
|
status_code=304,
|
|
554
634
|
headers=response.headers
|
|
555
635
|
)
|
|
556
636
|
|
|
637
|
+
# last step store cache we dont want to catch the 304 `None` resonses
|
|
638
|
+
self._store_cache(cache_key, response)
|
|
639
|
+
self._store_etag(response.headers)
|
|
640
|
+
|
|
557
641
|
return (data, response) if return_response else data
|
|
558
642
|
|
|
559
643
|
def results(
|
|
560
644
|
self,
|
|
561
|
-
|
|
645
|
+
use_etag: bool = True,
|
|
562
646
|
return_response: bool = False,
|
|
647
|
+
force_refresh: bool = False,
|
|
563
648
|
use_cache: bool = True,
|
|
564
649
|
**extra) -> tuple[list[Any], Response | Any | None] | list[Any]:
|
|
565
650
|
all_results: list[Any] = []
|
|
@@ -575,7 +660,12 @@ class EsiOperation(BaseEsiOperation):
|
|
|
575
660
|
total_pages = 1
|
|
576
661
|
while current_page <= total_pages:
|
|
577
662
|
self._kwargs["page"] = current_page
|
|
578
|
-
data, response = self.result(
|
|
663
|
+
data, response = self.result(
|
|
664
|
+
use_etag=use_etag,
|
|
665
|
+
return_response=True,
|
|
666
|
+
force_refresh=force_refresh,
|
|
667
|
+
**extra
|
|
668
|
+
)
|
|
579
669
|
last_response = response
|
|
580
670
|
all_results.extend(data if isinstance(data, list) else [data])
|
|
581
671
|
total_pages = int(response.headers.get("X-Pages", 1))
|
|
@@ -594,7 +684,13 @@ class EsiOperation(BaseEsiOperation):
|
|
|
594
684
|
else:
|
|
595
685
|
cursor_param = "after"
|
|
596
686
|
while True:
|
|
597
|
-
data, response = self.result(
|
|
687
|
+
data, response = self.result(
|
|
688
|
+
use_etag=use_etag,
|
|
689
|
+
return_response=True,
|
|
690
|
+
force_refresh=force_refresh,
|
|
691
|
+
use_cache=use_cache,
|
|
692
|
+
**params
|
|
693
|
+
)
|
|
598
694
|
last_response = response
|
|
599
695
|
if not data:
|
|
600
696
|
break
|
|
@@ -605,7 +701,13 @@ class EsiOperation(BaseEsiOperation):
|
|
|
605
701
|
params[cursor_param] = cursor_token
|
|
606
702
|
|
|
607
703
|
else:
|
|
608
|
-
data, response = self.result(
|
|
704
|
+
data, response = self.result(
|
|
705
|
+
use_etag=use_etag,
|
|
706
|
+
return_response=True,
|
|
707
|
+
force_refresh=force_refresh,
|
|
708
|
+
use_cache=use_cache,
|
|
709
|
+
**extra
|
|
710
|
+
)
|
|
609
711
|
all_results.extend(data if isinstance(data, list) else [data])
|
|
610
712
|
last_response = response
|
|
611
713
|
|
|
@@ -614,7 +716,8 @@ class EsiOperation(BaseEsiOperation):
|
|
|
614
716
|
def results_localized(
|
|
615
717
|
self,
|
|
616
718
|
languages: list[str] | str | None = None,
|
|
617
|
-
**kwargs
|
|
719
|
+
**kwargs
|
|
720
|
+
) -> dict[str, list[Any]]:
|
|
618
721
|
"""Executes the request and returns the response from ESI for all default languages and pages (if any).
|
|
619
722
|
Args:
|
|
620
723
|
languages: (list[str], str, optional) language(s) to return instead of default languages
|
|
@@ -707,17 +810,37 @@ class EsiOperationAsync(BaseEsiOperation):
|
|
|
707
810
|
|
|
708
811
|
if not response:
|
|
709
812
|
logger.debug(f"Cache Miss {self.url}")
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
#
|
|
719
|
-
|
|
720
|
-
|
|
813
|
+
try:
|
|
814
|
+
headers, data, response = await self._make_request(parameters, etag)
|
|
815
|
+
if response.status_code == 420:
|
|
816
|
+
reset = response.headers.get("X-RateLimit-Reset", None)
|
|
817
|
+
cache.set("esi_error_limit_reset", reset, timeout=reset)
|
|
818
|
+
raise ESIErrorLimitException(reset=reset)
|
|
819
|
+
self._store_cache(cache_key, response)
|
|
820
|
+
self._store_etag(response.headers)
|
|
821
|
+
# Shim our exceptions into Django-ESI
|
|
822
|
+
except base_HTTPServerError as e:
|
|
823
|
+
raise HTTPServerError(
|
|
824
|
+
status_code=e.status_code,
|
|
825
|
+
headers=e.headers,
|
|
826
|
+
data=e.data
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
except base_HTTPClientError as e:
|
|
830
|
+
raise HTTPClientError(
|
|
831
|
+
status_code=e.status_code,
|
|
832
|
+
headers=e.headers,
|
|
833
|
+
data=e.data
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
# Throw a 304 exception for catching.
|
|
837
|
+
if response.status_code == 304:
|
|
838
|
+
# refresh/store the etag's TTL
|
|
839
|
+
self._store_etag(response.headers)
|
|
840
|
+
raise HTTPNotModified(
|
|
841
|
+
status_code=304,
|
|
842
|
+
headers=response.headers
|
|
843
|
+
)
|
|
721
844
|
|
|
722
845
|
return (data, response) if return_response else data
|
|
723
846
|
|
|
@@ -838,6 +961,7 @@ class ESIClient(ESIClientStub):
|
|
|
838
961
|
Base ESI Client, provides access to Tags Assets, Characters, etc.
|
|
839
962
|
or Raw aiopenapi3 via sad smiley ._.
|
|
840
963
|
"""
|
|
964
|
+
|
|
841
965
|
def __init__(self, api: OpenAPI) -> None:
|
|
842
966
|
self.api = api
|
|
843
967
|
self._tags = set(api._operationindex._tags.keys())
|
|
@@ -859,12 +983,36 @@ class ESIClient(ESIClientStub):
|
|
|
859
983
|
f"Available tags: {', '.join(sorted(self._tags))}"
|
|
860
984
|
)
|
|
861
985
|
|
|
986
|
+
def purge_all_etags(self):
|
|
987
|
+
""" Delete all stored etags from the cache for this application
|
|
988
|
+
|
|
989
|
+
TODO: consider making this more config agnostic
|
|
990
|
+
"""
|
|
991
|
+
try:
|
|
992
|
+
# new lib
|
|
993
|
+
from django_redis import get_redis_connection
|
|
994
|
+
_client = get_redis_connection("default")
|
|
995
|
+
except (NotImplementedError, ModuleNotFoundError):
|
|
996
|
+
# old lib
|
|
997
|
+
from django.core.cache import caches
|
|
998
|
+
default_cache = caches['default']
|
|
999
|
+
_client = default_cache.get_master_client()
|
|
1000
|
+
|
|
1001
|
+
keys = _client.keys(f":?:{slugify(self.api.app_name)}_etag_*")
|
|
1002
|
+
if keys:
|
|
1003
|
+
deleted = _client.delete(*keys)
|
|
1004
|
+
|
|
1005
|
+
logger.info(f"Deleted {deleted} etag keys")
|
|
1006
|
+
|
|
1007
|
+
return deleted
|
|
1008
|
+
|
|
862
1009
|
|
|
863
1010
|
class ESIClientAsync(ESIClientStub):
|
|
864
1011
|
"""
|
|
865
1012
|
Async Base ESI Client, provides access to Tags Assets, Characters, etc.
|
|
866
1013
|
or Raw aiopenapi3 via sad smiley ._.
|
|
867
1014
|
"""
|
|
1015
|
+
|
|
868
1016
|
def __init__(self, api: OpenAPI) -> None:
|
|
869
1017
|
self.api = api
|
|
870
1018
|
self._tags = set(api._operationindex._tags.keys())
|
|
@@ -906,15 +1054,17 @@ class ESIClientProvider:
|
|
|
906
1054
|
|
|
907
1055
|
def __init__(
|
|
908
1056
|
self,
|
|
909
|
-
compatibility_date: str | date,
|
|
1057
|
+
compatibility_date: str | dt.date,
|
|
910
1058
|
ua_appname: str,
|
|
911
1059
|
ua_version: str,
|
|
912
1060
|
ua_url: str | None = None,
|
|
913
1061
|
spec_file: None | str = None,
|
|
914
1062
|
tenant: str = "tranquility",
|
|
1063
|
+
operations: list[str] = [],
|
|
1064
|
+
tags: list[str] = [],
|
|
915
1065
|
**kwargs
|
|
916
1066
|
) -> None:
|
|
917
|
-
if type(compatibility_date) is date:
|
|
1067
|
+
if type(compatibility_date) is dt.date:
|
|
918
1068
|
self._compatibility_date = self._date_to_string(compatibility_date)
|
|
919
1069
|
else:
|
|
920
1070
|
self._compatibility_date = compatibility_date
|
|
@@ -924,6 +1074,8 @@ class ESIClientProvider:
|
|
|
924
1074
|
self._spec_file = spec_file
|
|
925
1075
|
self._tenant = tenant
|
|
926
1076
|
self._kwargs = kwargs
|
|
1077
|
+
self._operations = operations
|
|
1078
|
+
self._tags = tags
|
|
927
1079
|
|
|
928
1080
|
@property
|
|
929
1081
|
def client(self) -> ESIClient:
|
|
@@ -935,6 +1087,8 @@ class ESIClientProvider:
|
|
|
935
1087
|
ua_url=self._ua_url,
|
|
936
1088
|
spec_file=self._spec_file,
|
|
937
1089
|
tenant=self._tenant,
|
|
1090
|
+
operations=self._operations,
|
|
1091
|
+
tags=self._tags,
|
|
938
1092
|
**self._kwargs)
|
|
939
1093
|
self._client = ESIClient(api)
|
|
940
1094
|
return self._client
|
|
@@ -954,10 +1108,9 @@ class ESIClientProvider:
|
|
|
954
1108
|
return self._client_async
|
|
955
1109
|
|
|
956
1110
|
@classmethod
|
|
957
|
-
def _date_to_string(cls, compatibility_date: date) -> str:
|
|
1111
|
+
def _date_to_string(cls, compatibility_date: dt.date) -> str:
|
|
958
1112
|
"""Turns a date object in a compatibility_date string"""
|
|
959
1113
|
return f"{compatibility_date.year}-{compatibility_date.month:02}-{compatibility_date.day:02}"
|
|
960
1114
|
|
|
961
|
-
|
|
962
1115
|
def __str__(self) -> str:
|
|
963
|
-
return "ESIClientProvider"
|
|
1116
|
+
return f"ESIClientProvider - {self._ua_appname} ({self._ua_version})"
|