django-esi 8.0.0a2__py3-none-any.whl → 8.0.0a4__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.0a2.dist-info → django_esi-8.0.0a4.dist-info}/METADATA +1 -1
- {django_esi-8.0.0a2.dist-info → django_esi-8.0.0a4.dist-info}/RECORD +41 -29
- esi/__init__.py +2 -2
- esi/aiopenapi3/plugins.py +34 -2
- esi/exceptions.py +29 -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 +5 -5
- 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 +18 -14
- 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 +236 -59
- esi/stubs.pyi +426 -384
- esi/tests/test_openapi.json +264 -0
- esi/tests/test_openapi.py +261 -0
- {django_esi-8.0.0a2.dist-info → django_esi-8.0.0a4.dist-info}/WHEEL +0 -0
- {django_esi-8.0.0a2.dist-info → django_esi-8.0.0a4.dist-info}/licenses/LICENSE +0 -0
esi/openapi_clients.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import pathlib
|
|
2
3
|
import warnings
|
|
3
|
-
from datetime import datetime
|
|
4
|
+
from datetime import datetime, date
|
|
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
|
|
10
|
+
from aiopenapi3.errors import HTTPServerError as base_HTTPServerError
|
|
11
|
+
from aiopenapi3.errors import HTTPClientError as base_HTTPClientError
|
|
9
12
|
from aiopenapi3.request import OperationIndex, RequestBase
|
|
10
13
|
from httpx import (
|
|
11
14
|
AsyncClient, Client, HTTPStatusError, RequestError, Response, Timeout,
|
|
@@ -16,9 +19,11 @@ from tenacity import (
|
|
|
16
19
|
)
|
|
17
20
|
|
|
18
21
|
from django.core.cache import cache
|
|
22
|
+
from django.utils.text import slugify
|
|
19
23
|
|
|
20
24
|
from esi import app_settings
|
|
21
|
-
from esi.
|
|
25
|
+
from esi.exceptions import HTTPClientError, HTTPServerError, HTTPNotModified
|
|
26
|
+
from esi.aiopenapi3.plugins import Add304ContentType, DjangoESIInit, PatchCompatibilityDatePlugin, Trim204ContentType
|
|
22
27
|
from esi.exceptions import ESIErrorLimitException
|
|
23
28
|
from esi.models import Token
|
|
24
29
|
from esi.stubs import ESIClientStub
|
|
@@ -27,6 +32,8 @@ from . import __title__, __url__, __version__
|
|
|
27
32
|
|
|
28
33
|
logger = logging.getLogger(__name__)
|
|
29
34
|
|
|
35
|
+
ETAG_EXPIRY = 60*60*24*7 # 7 days
|
|
36
|
+
|
|
30
37
|
|
|
31
38
|
def _time_to_expiry(expires_header: str) -> int:
|
|
32
39
|
"""Calculate cache TTL from Expires header
|
|
@@ -78,9 +85,19 @@ async def http_retry_async() -> AsyncRetrying:
|
|
|
78
85
|
)
|
|
79
86
|
|
|
80
87
|
|
|
88
|
+
def _load_plugins(app_name):
|
|
89
|
+
"""Load the plugins to make ESI work with this lib.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
app_name (str): app name to use for internal etags
|
|
93
|
+
"""
|
|
94
|
+
return [PatchCompatibilityDatePlugin(), Trim204ContentType(), Add304ContentType(), DjangoESIInit(app_name)]
|
|
95
|
+
|
|
96
|
+
|
|
81
97
|
def _load_aiopenapi_client_sync(
|
|
82
98
|
spec_url: str,
|
|
83
99
|
compatibility_date: str,
|
|
100
|
+
app_name: str,
|
|
84
101
|
user_agent: str,
|
|
85
102
|
tenant: str,
|
|
86
103
|
spec_file: str | None = None) -> OpenAPI:
|
|
@@ -89,6 +106,7 @@ def _load_aiopenapi_client_sync(
|
|
|
89
106
|
Args:
|
|
90
107
|
spec_url (str): _description_
|
|
91
108
|
compatibility_date (str): _description_
|
|
109
|
+
app_name (str): _description_
|
|
92
110
|
user_agent (str): _description_
|
|
93
111
|
tenant (str): _description_
|
|
94
112
|
spec_file (str | None, optional): _description_. Defaults to None.
|
|
@@ -120,21 +138,23 @@ def _load_aiopenapi_client_sync(
|
|
|
120
138
|
url=spec_url,
|
|
121
139
|
path=spec_file,
|
|
122
140
|
session_factory=session_factory,
|
|
141
|
+
loader=FileSystemLoader(pathlib.Path(spec_file)),
|
|
123
142
|
use_operation_tags=True,
|
|
124
|
-
plugins=
|
|
143
|
+
plugins=_load_plugins(app_name)
|
|
125
144
|
)
|
|
126
145
|
else:
|
|
127
146
|
return OpenAPI.load_sync(
|
|
128
147
|
url=spec_url,
|
|
129
148
|
session_factory=session_factory,
|
|
130
149
|
use_operation_tags=True,
|
|
131
|
-
plugins=
|
|
150
|
+
plugins=_load_plugins(app_name)
|
|
132
151
|
)
|
|
133
152
|
|
|
134
153
|
|
|
135
154
|
async def _load_aiopenapi_client_async(
|
|
136
155
|
spec_url: str,
|
|
137
156
|
compatibility_date: str,
|
|
157
|
+
app_name: str,
|
|
138
158
|
user_agent: str,
|
|
139
159
|
tenant: str,
|
|
140
160
|
spec_file: str | None = None) -> OpenAPI:
|
|
@@ -176,18 +196,18 @@ async def _load_aiopenapi_client_async(
|
|
|
176
196
|
path=spec_file,
|
|
177
197
|
session_factory=session_factory,
|
|
178
198
|
use_operation_tags=True,
|
|
179
|
-
plugins=
|
|
199
|
+
plugins=_load_plugins(app_name)
|
|
180
200
|
)
|
|
181
201
|
else:
|
|
182
202
|
return await OpenAPI.load_async(
|
|
183
203
|
url=spec_url,
|
|
184
204
|
session_factory=session_factory,
|
|
185
205
|
use_operation_tags=True,
|
|
186
|
-
plugins=
|
|
206
|
+
plugins=_load_plugins(app_name)
|
|
187
207
|
)
|
|
188
208
|
|
|
189
209
|
|
|
190
|
-
def _build_user_agent(ua_appname: str, ua_version: str, ua_url: str | None) -> str:
|
|
210
|
+
def _build_user_agent(ua_appname: str, ua_version: str, ua_url: str | None = None) -> str:
|
|
191
211
|
"""
|
|
192
212
|
AppName/1.2.3 (foo@example.com; +https://gitlab.com/) Django-ESI/1.2.3 (+https://gitlab.com/allianceauth/django-esi)
|
|
193
213
|
Contact Email will be inserted from app_settings.
|
|
@@ -228,7 +248,7 @@ def esi_client_factory_sync(
|
|
|
228
248
|
"""
|
|
229
249
|
user_agent = _build_user_agent(ua_appname, ua_version, ua_url)
|
|
230
250
|
spec_url = _get_spec_url()
|
|
231
|
-
return _load_aiopenapi_client_sync(spec_url, compatibility_date, user_agent, tenant, spec_file)
|
|
251
|
+
return _load_aiopenapi_client_sync(spec_url, compatibility_date, ua_appname, user_agent, tenant, spec_file)
|
|
232
252
|
|
|
233
253
|
|
|
234
254
|
async def esi_client_factory_async(
|
|
@@ -250,11 +270,11 @@ async def esi_client_factory_async(
|
|
|
250
270
|
"""
|
|
251
271
|
user_agent = _build_user_agent(ua_appname, ua_version, ua_url)
|
|
252
272
|
spec_url = _get_spec_url()
|
|
253
|
-
return await _load_aiopenapi_client_async(spec_url, compatibility_date, user_agent, tenant, spec_file)
|
|
273
|
+
return await _load_aiopenapi_client_async(spec_url, compatibility_date, ua_appname, user_agent, tenant, spec_file)
|
|
254
274
|
|
|
255
275
|
|
|
256
276
|
class BaseEsiOperation():
|
|
257
|
-
def __init__(self, operation, api) -> None:
|
|
277
|
+
def __init__(self, operation, api: OpenAPI) -> None:
|
|
258
278
|
self.method, self.url, self.operation, self.extra = operation
|
|
259
279
|
self.api = api
|
|
260
280
|
self.token: Token | None = None
|
|
@@ -317,12 +337,25 @@ class BaseEsiOperation():
|
|
|
317
337
|
|
|
318
338
|
return normalized
|
|
319
339
|
|
|
340
|
+
def _etag_key(self) -> str:
|
|
341
|
+
"""Generate a key name used to cache etag responses based on app_name and cache_key
|
|
342
|
+
Returns:
|
|
343
|
+
str: Key
|
|
344
|
+
"""
|
|
345
|
+
# ignore the token this will break the cache
|
|
346
|
+
return f"{slugify(self.api.app_name)}_etag_{self._cache_key()}"
|
|
347
|
+
|
|
320
348
|
def _cache_key(self) -> str:
|
|
321
349
|
"""Generate a key name used to cache responses based on method, url, args, kwargs
|
|
322
350
|
Returns:
|
|
323
|
-
str: Key
|
|
351
|
+
str: Key
|
|
324
352
|
"""
|
|
325
|
-
|
|
353
|
+
# ignore the token this will break the cache
|
|
354
|
+
ignore_keys = [
|
|
355
|
+
"token",
|
|
356
|
+
]
|
|
357
|
+
_kwargs = {key: value for key, value in self._kwargs.items() if key not in ignore_keys}
|
|
358
|
+
data = (self.method + self.url + str(self._args) + str(_kwargs)).encode('utf-8')
|
|
326
359
|
str_hash = md5(data).hexdigest() # nosec B303
|
|
327
360
|
return f'esi_{str_hash}'
|
|
328
361
|
|
|
@@ -360,7 +393,7 @@ class BaseEsiOperation():
|
|
|
360
393
|
"""
|
|
361
394
|
return any(p.name == "before" or p.name == "after" for p in self.operation.parameters)
|
|
362
395
|
|
|
363
|
-
def _get_cache(self, cache_key: str) -> tuple[ResponseHeadersType | None, Any, Response | None]:
|
|
396
|
+
def _get_cache(self, cache_key: str, etag: str) -> tuple[ResponseHeadersType | None, Any, Response | None]:
|
|
364
397
|
"""Retrieve cached response and validate expiry
|
|
365
398
|
Args:
|
|
366
399
|
cache_key (str): The cache key to retrieve
|
|
@@ -368,6 +401,9 @@ class BaseEsiOperation():
|
|
|
368
401
|
tuple[ResponseHeadersType | None, Any, Response | None]: The cached response,
|
|
369
402
|
or None if not found or expired
|
|
370
403
|
"""
|
|
404
|
+
if not app_settings.ESI_CACHE_RESPONSE:
|
|
405
|
+
return None, None, None
|
|
406
|
+
|
|
371
407
|
try:
|
|
372
408
|
cached_response = cache.get(cache_key)
|
|
373
409
|
except Exception as e:
|
|
@@ -376,18 +412,46 @@ class BaseEsiOperation():
|
|
|
376
412
|
|
|
377
413
|
if cached_response:
|
|
378
414
|
logger.debug(f"Cache Hit {self.url}")
|
|
379
|
-
|
|
415
|
+
expiry = _time_to_expiry(str(cached_response.headers.get('Expires')))
|
|
380
416
|
|
|
381
|
-
|
|
417
|
+
# force check to ensure cache isn't expired
|
|
382
418
|
if expiry < 0:
|
|
383
419
|
logger.warning("Cache expired by %d seconds, forcing expiry", expiry)
|
|
384
420
|
return None, None, None
|
|
421
|
+
|
|
422
|
+
# check if etag is same before building models from cache
|
|
423
|
+
if etag:
|
|
424
|
+
if cached_response.headers.get('Expires') == etag:
|
|
425
|
+
# refresh/store the etag's TTL
|
|
426
|
+
self._store_etag(cached_response.headers)
|
|
427
|
+
raise HTTPNotModified(
|
|
428
|
+
status_code=304,
|
|
429
|
+
headers=cached_response.headers
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# build models
|
|
433
|
+
headers, data = self.parse_cached_request(cached_response)
|
|
385
434
|
return headers, data, cached_response
|
|
386
435
|
|
|
387
436
|
return None, None, None
|
|
388
437
|
|
|
438
|
+
def _store_etag(self, headers: dict):
|
|
439
|
+
"""
|
|
440
|
+
Store response etag in cache for 7 days
|
|
441
|
+
"""
|
|
442
|
+
if "ETag" in headers:
|
|
443
|
+
cache.set(self._etag_key(), headers["ETag"], timeout=ETAG_EXPIRY)
|
|
444
|
+
|
|
445
|
+
def _clear_etag(self):
|
|
446
|
+
""" Delete the cached etag for this operation.
|
|
447
|
+
"""
|
|
448
|
+
try:
|
|
449
|
+
cache.delete(self._etag_key())
|
|
450
|
+
except Exception as e:
|
|
451
|
+
logger.error(f"Failed to delete etag {e}", exc_info=True)
|
|
452
|
+
|
|
389
453
|
def _store_cache(self, cache_key: str, response) -> None:
|
|
390
|
-
""" Store the response in cache
|
|
454
|
+
""" Store the response in cache for expiry TTL.
|
|
391
455
|
Args:
|
|
392
456
|
cache_key (str): The cache key to store the response under
|
|
393
457
|
response (Response): The response object to cache
|
|
@@ -395,9 +459,6 @@ class BaseEsiOperation():
|
|
|
395
459
|
if not app_settings.ESI_CACHE_RESPONSE:
|
|
396
460
|
return
|
|
397
461
|
|
|
398
|
-
if "ETag" in response.headers:
|
|
399
|
-
cache.set(f"{cache_key}_etag", response.headers["ETag"])
|
|
400
|
-
|
|
401
462
|
expires = response.headers.get("Expires")
|
|
402
463
|
ttl = _time_to_expiry(expires) if expires else 0
|
|
403
464
|
if ttl > 0:
|
|
@@ -406,6 +467,14 @@ class BaseEsiOperation():
|
|
|
406
467
|
except Exception as e:
|
|
407
468
|
logger.error(f"Failed to cache {e}", exc_info=True)
|
|
408
469
|
|
|
470
|
+
def _clear_cache(self):
|
|
471
|
+
""" Delete the cached data for this operation.
|
|
472
|
+
"""
|
|
473
|
+
try:
|
|
474
|
+
cache.delete(self._cache_key())
|
|
475
|
+
except Exception as e:
|
|
476
|
+
logger.error(f"Failed to delete cache {e}", exc_info=True)
|
|
477
|
+
|
|
409
478
|
def _validate_token_scopes(self, token: Token) -> None:
|
|
410
479
|
"""Validate that the token provided has the required scopes for this ESI operation.
|
|
411
480
|
"""
|
|
@@ -465,8 +534,9 @@ class EsiOperation(BaseEsiOperation):
|
|
|
465
534
|
|
|
466
535
|
def result(
|
|
467
536
|
self,
|
|
468
|
-
|
|
537
|
+
use_etag: bool = True,
|
|
469
538
|
return_response: bool = False,
|
|
539
|
+
force_refresh: bool = False,
|
|
470
540
|
use_cache: bool = True,
|
|
471
541
|
**extra) -> tuple[Any, Response] | Any:
|
|
472
542
|
"""Executes the request and returns the response from ESI for the current operation.
|
|
@@ -480,12 +550,17 @@ class EsiOperation(BaseEsiOperation):
|
|
|
480
550
|
self.body = self._extract_body_param()
|
|
481
551
|
parameters = self._kwargs | extra
|
|
482
552
|
cache_key = self._cache_key()
|
|
483
|
-
etag_key =
|
|
553
|
+
etag_key = self._etag_key()
|
|
554
|
+
etag = None
|
|
484
555
|
|
|
485
|
-
if
|
|
556
|
+
if force_refresh:
|
|
557
|
+
self._clear_cache()
|
|
558
|
+
self._clear_etag()
|
|
559
|
+
|
|
560
|
+
if use_etag:
|
|
486
561
|
etag = cache.get(etag_key)
|
|
487
562
|
|
|
488
|
-
headers, data, response = self._get_cache(cache_key)
|
|
563
|
+
headers, data, response = self._get_cache(cache_key, etag=etag) if use_cache else (None, None, None)
|
|
489
564
|
|
|
490
565
|
if response and use_cache:
|
|
491
566
|
expiry = _time_to_expiry(str(headers.get('Expires')))
|
|
@@ -499,24 +574,47 @@ class EsiOperation(BaseEsiOperation):
|
|
|
499
574
|
|
|
500
575
|
if not response:
|
|
501
576
|
logger.debug(f"Cache Miss {self.url}")
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
#
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
577
|
+
try:
|
|
578
|
+
headers, data, response = self._make_request(parameters, etag)
|
|
579
|
+
if response.status_code == 420:
|
|
580
|
+
reset = response.headers.get("X-RateLimit-Reset", None)
|
|
581
|
+
cache.set("esi_error_limit_reset", reset, timeout=reset)
|
|
582
|
+
raise ESIErrorLimitException(reset=reset)
|
|
583
|
+
|
|
584
|
+
# Shim our exceptions into Django-ESI
|
|
585
|
+
except base_HTTPServerError as e:
|
|
586
|
+
raise HTTPServerError(
|
|
587
|
+
status_code=e.status_code,
|
|
588
|
+
headers=e.headers,
|
|
589
|
+
data=e.data
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
except base_HTTPClientError as e:
|
|
593
|
+
raise HTTPClientError(
|
|
594
|
+
status_code=e.status_code,
|
|
595
|
+
headers=e.headers,
|
|
596
|
+
data=e.data
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# Throw a 304 exception for catching.
|
|
600
|
+
if response.status_code == 304:
|
|
601
|
+
# refresh/store the etag's TTL
|
|
602
|
+
self._store_etag(response.headers)
|
|
603
|
+
raise HTTPNotModified(
|
|
604
|
+
status_code=304,
|
|
605
|
+
headers=response.headers
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
# last step store cache we dont want to catch the 304 `None` resonses
|
|
609
|
+
self._store_cache(cache_key, response)
|
|
513
610
|
|
|
514
611
|
return (data, response) if return_response else data
|
|
515
612
|
|
|
516
613
|
def results(
|
|
517
614
|
self,
|
|
518
|
-
|
|
615
|
+
use_etag: bool = True,
|
|
519
616
|
return_response: bool = False,
|
|
617
|
+
force_refresh: bool = False,
|
|
520
618
|
use_cache: bool = True,
|
|
521
619
|
**extra) -> tuple[list[Any], Response | Any | None] | list[Any]:
|
|
522
620
|
all_results: list[Any] = []
|
|
@@ -532,7 +630,12 @@ class EsiOperation(BaseEsiOperation):
|
|
|
532
630
|
total_pages = 1
|
|
533
631
|
while current_page <= total_pages:
|
|
534
632
|
self._kwargs["page"] = current_page
|
|
535
|
-
data, response = self.result(
|
|
633
|
+
data, response = self.result(
|
|
634
|
+
use_etag=use_etag,
|
|
635
|
+
return_response=True,
|
|
636
|
+
force_refresh=force_refresh,
|
|
637
|
+
**extra
|
|
638
|
+
)
|
|
536
639
|
last_response = response
|
|
537
640
|
all_results.extend(data if isinstance(data, list) else [data])
|
|
538
641
|
total_pages = int(response.headers.get("X-Pages", 1))
|
|
@@ -551,7 +654,13 @@ class EsiOperation(BaseEsiOperation):
|
|
|
551
654
|
else:
|
|
552
655
|
cursor_param = "after"
|
|
553
656
|
while True:
|
|
554
|
-
data, response = self.result(
|
|
657
|
+
data, response = self.result(
|
|
658
|
+
use_etag=use_etag,
|
|
659
|
+
return_response=True,
|
|
660
|
+
force_refresh=force_refresh,
|
|
661
|
+
use_cache=use_cache,
|
|
662
|
+
**params
|
|
663
|
+
)
|
|
555
664
|
last_response = response
|
|
556
665
|
if not data:
|
|
557
666
|
break
|
|
@@ -562,7 +671,13 @@ class EsiOperation(BaseEsiOperation):
|
|
|
562
671
|
params[cursor_param] = cursor_token
|
|
563
672
|
|
|
564
673
|
else:
|
|
565
|
-
data, response = self.result(
|
|
674
|
+
data, response = self.result(
|
|
675
|
+
use_etag=use_etag,
|
|
676
|
+
return_response=True,
|
|
677
|
+
force_refresh=force_refresh,
|
|
678
|
+
use_cache=use_cache,
|
|
679
|
+
**extra
|
|
680
|
+
)
|
|
566
681
|
all_results.extend(data if isinstance(data, list) else [data])
|
|
567
682
|
last_response = response
|
|
568
683
|
|
|
@@ -571,7 +686,8 @@ class EsiOperation(BaseEsiOperation):
|
|
|
571
686
|
def results_localized(
|
|
572
687
|
self,
|
|
573
688
|
languages: list[str] | str | None = None,
|
|
574
|
-
**kwargs
|
|
689
|
+
**kwargs
|
|
690
|
+
) -> dict[str, list[Any]]:
|
|
575
691
|
"""Executes the request and returns the response from ESI for all default languages and pages (if any).
|
|
576
692
|
Args:
|
|
577
693
|
languages: (list[str], str, optional) language(s) to return instead of default languages
|
|
@@ -590,7 +706,7 @@ class EsiOperation(BaseEsiOperation):
|
|
|
590
706
|
my_languages.append(lang)
|
|
591
707
|
|
|
592
708
|
return {
|
|
593
|
-
language: self.results(
|
|
709
|
+
language: self.results(accept_language=language, **kwargs)
|
|
594
710
|
for language in my_languages
|
|
595
711
|
}
|
|
596
712
|
|
|
@@ -664,17 +780,37 @@ class EsiOperationAsync(BaseEsiOperation):
|
|
|
664
780
|
|
|
665
781
|
if not response:
|
|
666
782
|
logger.debug(f"Cache Miss {self.url}")
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
#
|
|
676
|
-
|
|
677
|
-
|
|
783
|
+
try:
|
|
784
|
+
headers, data, response = await self._make_request(parameters, etag)
|
|
785
|
+
if response.status_code == 420:
|
|
786
|
+
reset = response.headers.get("X-RateLimit-Reset", None)
|
|
787
|
+
cache.set("esi_error_limit_reset", reset, timeout=reset)
|
|
788
|
+
raise ESIErrorLimitException(reset=reset)
|
|
789
|
+
self._store_cache(cache_key, response)
|
|
790
|
+
self._store_etag(response.headers)
|
|
791
|
+
# Shim our exceptions into Django-ESI
|
|
792
|
+
except base_HTTPServerError as e:
|
|
793
|
+
raise HTTPServerError(
|
|
794
|
+
status_code=e.status_code,
|
|
795
|
+
headers=e.headers,
|
|
796
|
+
data=e.data
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
except base_HTTPClientError as e:
|
|
800
|
+
raise HTTPClientError(
|
|
801
|
+
status_code=e.status_code,
|
|
802
|
+
headers=e.headers,
|
|
803
|
+
data=e.data
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
# Throw a 304 exception for catching.
|
|
807
|
+
if response.status_code == 304:
|
|
808
|
+
# refresh/store the etag's TTL
|
|
809
|
+
self._store_etag(response.headers)
|
|
810
|
+
raise HTTPNotModified(
|
|
811
|
+
status_code=304,
|
|
812
|
+
headers=response.headers
|
|
813
|
+
)
|
|
678
814
|
|
|
679
815
|
return (data, response) if return_response else data
|
|
680
816
|
|
|
@@ -732,7 +868,7 @@ class EsiOperationAsync(BaseEsiOperation):
|
|
|
732
868
|
my_languages.append(lang)
|
|
733
869
|
|
|
734
870
|
return {
|
|
735
|
-
language: self.results(
|
|
871
|
+
language: self.results(accept_language=language, **kwargs)
|
|
736
872
|
for language in my_languages
|
|
737
873
|
}
|
|
738
874
|
|
|
@@ -795,6 +931,7 @@ class ESIClient(ESIClientStub):
|
|
|
795
931
|
Base ESI Client, provides access to Tags Assets, Characters, etc.
|
|
796
932
|
or Raw aiopenapi3 via sad smiley ._.
|
|
797
933
|
"""
|
|
934
|
+
|
|
798
935
|
def __init__(self, api: OpenAPI) -> None:
|
|
799
936
|
self.api = api
|
|
800
937
|
self._tags = set(api._operationindex._tags.keys())
|
|
@@ -804,7 +941,11 @@ class ESIClient(ESIClientStub):
|
|
|
804
941
|
if tag == "_":
|
|
805
942
|
return self.api._operationindex
|
|
806
943
|
|
|
807
|
-
|
|
944
|
+
# convert pythonic Planetary_Interaction to Planetary Interaction
|
|
945
|
+
if "_" in tag:
|
|
946
|
+
tag = tag.replace("_", " ")
|
|
947
|
+
|
|
948
|
+
if tag in set(self.api._operationindex._tags.keys()):
|
|
808
949
|
return ESITag(self.api._operationindex._tags[tag], self.api)
|
|
809
950
|
|
|
810
951
|
raise AttributeError(
|
|
@@ -812,12 +953,36 @@ class ESIClient(ESIClientStub):
|
|
|
812
953
|
f"Available tags: {', '.join(sorted(self._tags))}"
|
|
813
954
|
)
|
|
814
955
|
|
|
956
|
+
def purge_all_etags(self):
|
|
957
|
+
""" Delete all stored etags from the cache for this application
|
|
958
|
+
|
|
959
|
+
TODO: consider making this more config agnostic
|
|
960
|
+
"""
|
|
961
|
+
try:
|
|
962
|
+
# new lib
|
|
963
|
+
from django_redis import get_redis_connection
|
|
964
|
+
_client = get_redis_connection("default")
|
|
965
|
+
except (NotImplementedError, ModuleNotFoundError):
|
|
966
|
+
# old lib
|
|
967
|
+
from django.core.cache import caches
|
|
968
|
+
default_cache = caches['default']
|
|
969
|
+
_client = default_cache.get_master_client()
|
|
970
|
+
|
|
971
|
+
keys = _client.keys(f":?:{slugify(self.api.app_name)}_etag_*")
|
|
972
|
+
if keys:
|
|
973
|
+
deleted = _client.delete(*keys)
|
|
974
|
+
|
|
975
|
+
logger.info(f"Deleted {deleted} etag keys")
|
|
976
|
+
|
|
977
|
+
return deleted
|
|
978
|
+
|
|
815
979
|
|
|
816
980
|
class ESIClientAsync(ESIClientStub):
|
|
817
981
|
"""
|
|
818
982
|
Async Base ESI Client, provides access to Tags Assets, Characters, etc.
|
|
819
983
|
or Raw aiopenapi3 via sad smiley ._.
|
|
820
984
|
"""
|
|
985
|
+
|
|
821
986
|
def __init__(self, api: OpenAPI) -> None:
|
|
822
987
|
self.api = api
|
|
823
988
|
self._tags = set(api._operationindex._tags.keys())
|
|
@@ -827,7 +992,11 @@ class ESIClientAsync(ESIClientStub):
|
|
|
827
992
|
if tag == "_":
|
|
828
993
|
return self.api._operationindex
|
|
829
994
|
|
|
830
|
-
|
|
995
|
+
# convert pythonic Planetary_Interaction to Planetary Interaction
|
|
996
|
+
if "_" in tag:
|
|
997
|
+
tag = tag.replace("_", " ")
|
|
998
|
+
|
|
999
|
+
if tag in set(self.api._operationindex._tags.keys()):
|
|
831
1000
|
return ESITagAsync(self.api._operationindex._tags[tag], self.api)
|
|
832
1001
|
|
|
833
1002
|
raise AttributeError(
|
|
@@ -839,7 +1008,7 @@ class ESIClientAsync(ESIClientStub):
|
|
|
839
1008
|
class ESIClientProvider:
|
|
840
1009
|
"""Class for providing a single ESI client instance for a whole app
|
|
841
1010
|
Args:
|
|
842
|
-
compatibility_date (str): The compatibility date for the ESI client.
|
|
1011
|
+
compatibility_date (str | date): The compatibility date for the ESI client.
|
|
843
1012
|
ua_appname (str): Name of the App for generating a User-Agent,
|
|
844
1013
|
ua_version (str): Version of the App for generating a User-Agent,
|
|
845
1014
|
ua_url (str, Optional): URL To the Source Code or Documentation for generating a User-Agent,
|
|
@@ -855,7 +1024,7 @@ class ESIClientProvider:
|
|
|
855
1024
|
|
|
856
1025
|
def __init__(
|
|
857
1026
|
self,
|
|
858
|
-
compatibility_date: str,
|
|
1027
|
+
compatibility_date: str | date,
|
|
859
1028
|
ua_appname: str,
|
|
860
1029
|
ua_version: str,
|
|
861
1030
|
ua_url: str | None = None,
|
|
@@ -863,7 +1032,10 @@ class ESIClientProvider:
|
|
|
863
1032
|
tenant: str = "tranquility",
|
|
864
1033
|
**kwargs
|
|
865
1034
|
) -> None:
|
|
866
|
-
|
|
1035
|
+
if type(compatibility_date) is date:
|
|
1036
|
+
self._compatibility_date = self._date_to_string(compatibility_date)
|
|
1037
|
+
else:
|
|
1038
|
+
self._compatibility_date = compatibility_date
|
|
867
1039
|
self._ua_appname = ua_appname
|
|
868
1040
|
self._ua_version = ua_version
|
|
869
1041
|
self._ua_url = ua_url
|
|
@@ -899,5 +1071,10 @@ class ESIClientProvider:
|
|
|
899
1071
|
self._client_async = ESIClientAsync(api)
|
|
900
1072
|
return self._client_async
|
|
901
1073
|
|
|
1074
|
+
@classmethod
|
|
1075
|
+
def _date_to_string(cls, compatibility_date: date) -> str:
|
|
1076
|
+
"""Turns a date object in a compatibility_date string"""
|
|
1077
|
+
return f"{compatibility_date.year}-{compatibility_date.month:02}-{compatibility_date.day:02}"
|
|
1078
|
+
|
|
902
1079
|
def __str__(self) -> str:
|
|
903
|
-
return "ESIClientProvider"
|
|
1080
|
+
return f"ESIClientProvider - {self._ua_appname} ({self._ua_version})"
|