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.

Files changed (44) hide show
  1. {django_esi-8.0.0a3.dist-info → django_esi-8.0.0b1.dist-info}/METADATA +3 -2
  2. {django_esi-8.0.0a3.dist-info → django_esi-8.0.0b1.dist-info}/RECORD +44 -33
  3. esi/__init__.py +2 -2
  4. esi/aiopenapi3/plugins.py +100 -3
  5. esi/clients.py +15 -6
  6. esi/helpers.py +37 -0
  7. esi/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  8. esi/locale/cs_CZ/LC_MESSAGES/django.po +53 -0
  9. esi/locale/de/LC_MESSAGES/django.mo +0 -0
  10. esi/locale/de/LC_MESSAGES/django.po +10 -9
  11. esi/locale/en/LC_MESSAGES/django.mo +0 -0
  12. esi/locale/en/LC_MESSAGES/django.po +3 -3
  13. esi/locale/es/LC_MESSAGES/django.mo +0 -0
  14. esi/locale/es/LC_MESSAGES/django.po +12 -10
  15. esi/locale/fr_FR/LC_MESSAGES/django.mo +0 -0
  16. esi/locale/fr_FR/LC_MESSAGES/django.po +18 -10
  17. esi/locale/it_IT/LC_MESSAGES/django.mo +0 -0
  18. esi/locale/it_IT/LC_MESSAGES/django.po +12 -10
  19. esi/locale/ja/LC_MESSAGES/django.mo +0 -0
  20. esi/locale/ja/LC_MESSAGES/django.po +10 -9
  21. esi/locale/ko_KR/LC_MESSAGES/django.mo +0 -0
  22. esi/locale/ko_KR/LC_MESSAGES/django.po +10 -9
  23. esi/locale/nl_NL/LC_MESSAGES/django.mo +0 -0
  24. esi/locale/nl_NL/LC_MESSAGES/django.po +53 -0
  25. esi/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
  26. esi/locale/pl_PL/LC_MESSAGES/django.po +53 -0
  27. esi/locale/ru/LC_MESSAGES/django.mo +0 -0
  28. esi/locale/ru/LC_MESSAGES/django.po +13 -10
  29. esi/locale/sk/LC_MESSAGES/django.mo +0 -0
  30. esi/locale/sk/LC_MESSAGES/django.po +55 -0
  31. esi/locale/uk/LC_MESSAGES/django.mo +0 -0
  32. esi/locale/uk/LC_MESSAGES/django.po +57 -0
  33. esi/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  34. esi/locale/zh_Hans/LC_MESSAGES/django.po +10 -9
  35. esi/management/commands/generate_esi_stubs.py +11 -31
  36. esi/models.py +1 -1
  37. esi/openapi_clients.py +207 -54
  38. esi/stubs.pyi +395 -395
  39. esi/tests/__init__.py +3 -3
  40. esi/tests/test_clients.py +77 -19
  41. esi/tests/test_openapi.json +264 -0
  42. esi/tests/test_openapi.py +402 -1
  43. {django_esi-8.0.0a3.dist-info → django_esi-8.0.0b1.dist-info}/WHEEL +0 -0
  44. {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
- from datetime import datetime, date
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 Add304ContentType, PatchCompatibilityDatePlugin, Trim204ContentType
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
- return max(int((expires_dt - datetime.utcnow()).total_seconds()), 0)
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) -> OpenAPI:
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=[PatchCompatibilityDatePlugin(), Trim204ContentType(), Add304ContentType()]
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=[PatchCompatibilityDatePlugin(), Trim204ContentType(), Add304ContentType()]
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=[PatchCompatibilityDatePlugin(), Trim204ContentType(), Add304ContentType()]
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=[PatchCompatibilityDatePlugin(), Trim204ContentType(), Add304ContentType()]
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"{ua_appname}/{ua_version} "
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"{__title__}/{__version__} (+{__url__})"
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(spec_url, compatibility_date, user_agent, tenant, spec_file)
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 name
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 = { key:value for key, value in self._kwargs.items() if key not in ignore_keys }
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('Expires') == etag:
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 with ETag and TTL.
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
- etag: str | None = None,
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 = f"{cache_key}_etag"
582
+ etag_key = self._etag_key()
583
+ etag = None
503
584
 
504
- if not etag and app_settings.ESI_CACHE_RESPONSE:
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
- etag: str | None = None,
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(etag=etag, return_response=True, **extra)
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(etag=etag, return_response=True, use_cache=use_cache, **params)
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(etag=etag, return_response=True, use_cache=use_cache, **extra)
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) -> dict[str, list[Any]]:
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
- headers, data, response = await self._make_request(parameters, etag)
711
- if response.status_code == 420:
712
- reset = response.headers.get("X-RateLimit-Reset", None)
713
- cache.set("esi_error_limit_reset", reset, timeout=reset)
714
- raise ESIErrorLimitException(reset=reset)
715
- # if response.status_code == 304 and app_settings.ESI_CACHE_RESPONSE:
716
- # cached = cache.get(cache_key)
717
- # if cached:
718
- # return (cached, response) if return_response else cached
719
- # we dont want to do this, if we do this we have to store data longer than ttl. rip ram
720
- self._store_cache(cache_key, response)
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})"