django-esi 8.0.0a1__py3-none-any.whl → 8.0.0a3__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.0a1.dist-info → django_esi-8.0.0a3.dist-info}/METADATA +3 -4
- {django_esi-8.0.0a1.dist-info → django_esi-8.0.0a3.dist-info}/RECORD +14 -13
- esi/__init__.py +2 -2
- esi/aiopenapi3/plugins.py +39 -0
- esi/app_settings.py +1 -1
- esi/exceptions.py +29 -0
- esi/locale/en/LC_MESSAGES/django.po +3 -3
- esi/locale/ru/LC_MESSAGES/django.po +8 -7
- esi/management/commands/generate_esi_stubs.py +33 -16
- esi/openapi_clients.py +154 -36
- esi/stubs.pyi +483 -567
- esi/tests/test_openapi.py +14 -0
- {django_esi-8.0.0a1.dist-info → django_esi-8.0.0a3.dist-info}/WHEEL +0 -0
- {django_esi-8.0.0a1.dist-info → django_esi-8.0.0a3.dist-info}/licenses/LICENSE +0 -0
esi/openapi_clients.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import warnings
|
|
3
|
-
from datetime import datetime
|
|
3
|
+
from datetime import datetime, date
|
|
4
4
|
from hashlib import md5
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from aiopenapi3 import OpenAPI
|
|
8
8
|
from aiopenapi3._types import ResponseDataType, ResponseHeadersType
|
|
9
|
+
from aiopenapi3.errors import HTTPServerError as base_HTTPServerError
|
|
10
|
+
from aiopenapi3.errors import HTTPClientError as base_HTTPClientError
|
|
9
11
|
from aiopenapi3.request import OperationIndex, RequestBase
|
|
10
12
|
from httpx import (
|
|
11
13
|
AsyncClient, Client, HTTPStatusError, RequestError, Response, Timeout,
|
|
@@ -18,7 +20,8 @@ from tenacity import (
|
|
|
18
20
|
from django.core.cache import cache
|
|
19
21
|
|
|
20
22
|
from esi import app_settings
|
|
21
|
-
from esi.
|
|
23
|
+
from esi.exceptions import HTTPClientError, HTTPServerError, HTTPNotModified
|
|
24
|
+
from esi.aiopenapi3.plugins import Add304ContentType, PatchCompatibilityDatePlugin, Trim204ContentType
|
|
22
25
|
from esi.exceptions import ESIErrorLimitException
|
|
23
26
|
from esi.models import Token
|
|
24
27
|
from esi.stubs import ESIClientStub
|
|
@@ -121,14 +124,14 @@ def _load_aiopenapi_client_sync(
|
|
|
121
124
|
path=spec_file,
|
|
122
125
|
session_factory=session_factory,
|
|
123
126
|
use_operation_tags=True,
|
|
124
|
-
plugins=[PatchCompatibilityDatePlugin()]
|
|
127
|
+
plugins=[PatchCompatibilityDatePlugin(), Trim204ContentType(), Add304ContentType()]
|
|
125
128
|
)
|
|
126
129
|
else:
|
|
127
130
|
return OpenAPI.load_sync(
|
|
128
131
|
url=spec_url,
|
|
129
132
|
session_factory=session_factory,
|
|
130
133
|
use_operation_tags=True,
|
|
131
|
-
plugins=[PatchCompatibilityDatePlugin()]
|
|
134
|
+
plugins=[PatchCompatibilityDatePlugin(), Trim204ContentType(), Add304ContentType()]
|
|
132
135
|
)
|
|
133
136
|
|
|
134
137
|
|
|
@@ -176,14 +179,14 @@ async def _load_aiopenapi_client_async(
|
|
|
176
179
|
path=spec_file,
|
|
177
180
|
session_factory=session_factory,
|
|
178
181
|
use_operation_tags=True,
|
|
179
|
-
plugins=[PatchCompatibilityDatePlugin()]
|
|
182
|
+
plugins=[PatchCompatibilityDatePlugin(), Trim204ContentType(), Add304ContentType()]
|
|
180
183
|
)
|
|
181
184
|
else:
|
|
182
185
|
return await OpenAPI.load_async(
|
|
183
186
|
url=spec_url,
|
|
184
187
|
session_factory=session_factory,
|
|
185
188
|
use_operation_tags=True,
|
|
186
|
-
plugins=[PatchCompatibilityDatePlugin()]
|
|
189
|
+
plugins=[PatchCompatibilityDatePlugin(), Trim204ContentType(), Add304ContentType()]
|
|
187
190
|
)
|
|
188
191
|
|
|
189
192
|
|
|
@@ -322,10 +325,25 @@ class BaseEsiOperation():
|
|
|
322
325
|
Returns:
|
|
323
326
|
str: Key name
|
|
324
327
|
"""
|
|
325
|
-
|
|
328
|
+
# ignore the token this will break the cache
|
|
329
|
+
ignore_keys = [
|
|
330
|
+
"token",
|
|
331
|
+
]
|
|
332
|
+
_kwargs = { key:value for key, value in self._kwargs.items() if key not in ignore_keys }
|
|
333
|
+
data = (self.method + self.url + str(self._args) + str(_kwargs)).encode('utf-8')
|
|
326
334
|
str_hash = md5(data).hexdigest() # nosec B303
|
|
327
335
|
return f'esi_{str_hash}'
|
|
328
336
|
|
|
337
|
+
def _extract_body_param(self) -> Token | None:
|
|
338
|
+
"""Pop the request body from parameters to be able to check the param validity
|
|
339
|
+
Returns:
|
|
340
|
+
Any | None: the request body
|
|
341
|
+
"""
|
|
342
|
+
_body = self._kwargs.pop("body", None)
|
|
343
|
+
if _body and not getattr(self.operation, "requestBody", False):
|
|
344
|
+
raise ValueError("Request Body provided on endpoint with no request body paramater.")
|
|
345
|
+
return _body
|
|
346
|
+
|
|
329
347
|
def _extract_token_param(self) -> Token | None:
|
|
330
348
|
"""Pop token from parameters or use the Client wide token if set
|
|
331
349
|
Returns:
|
|
@@ -350,7 +368,7 @@ class BaseEsiOperation():
|
|
|
350
368
|
"""
|
|
351
369
|
return any(p.name == "before" or p.name == "after" for p in self.operation.parameters)
|
|
352
370
|
|
|
353
|
-
def _get_cache(self, cache_key: str) -> tuple[ResponseHeadersType | None, Any, Response | None]:
|
|
371
|
+
def _get_cache(self, cache_key: str, etag: str) -> tuple[ResponseHeadersType | None, Any, Response | None]:
|
|
354
372
|
"""Retrieve cached response and validate expiry
|
|
355
373
|
Args:
|
|
356
374
|
cache_key (str): The cache key to retrieve
|
|
@@ -366,12 +384,23 @@ class BaseEsiOperation():
|
|
|
366
384
|
|
|
367
385
|
if cached_response:
|
|
368
386
|
logger.debug(f"Cache Hit {self.url}")
|
|
369
|
-
|
|
387
|
+
expiry = _time_to_expiry(str(cached_response.headers.get('Expires')))
|
|
370
388
|
|
|
371
|
-
|
|
389
|
+
# force check to ensure cache isn't expired
|
|
372
390
|
if expiry < 0:
|
|
373
391
|
logger.warning("Cache expired by %d seconds, forcing expiry", expiry)
|
|
374
392
|
return None, None, None
|
|
393
|
+
|
|
394
|
+
# check if etag is same before building models from cache
|
|
395
|
+
if etag:
|
|
396
|
+
if cached_response.headers.get('Expires') == etag:
|
|
397
|
+
raise HTTPNotModified(
|
|
398
|
+
status_code=304,
|
|
399
|
+
headers=cached_response.headers
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# build models
|
|
403
|
+
headers, data = self.parse_cached_request(cached_response)
|
|
375
404
|
return headers, data, cached_response
|
|
376
405
|
|
|
377
406
|
return None, None, None
|
|
@@ -450,7 +479,7 @@ class EsiOperation(BaseEsiOperation):
|
|
|
450
479
|
req.req.headers["Authorization"] = f"Bearer {self.token.valid_access_token()}"
|
|
451
480
|
if etag:
|
|
452
481
|
req.req.headers["If-None-Match"] = etag
|
|
453
|
-
return req.request(parameters=self._unnormalize_parameters(parameters))
|
|
482
|
+
return req.request(data=self.body, parameters=self._unnormalize_parameters(parameters))
|
|
454
483
|
return retry(__func)
|
|
455
484
|
|
|
456
485
|
def result(
|
|
@@ -467,6 +496,7 @@ class EsiOperation(BaseEsiOperation):
|
|
|
467
496
|
"""
|
|
468
497
|
|
|
469
498
|
self.token = self._extract_token_param()
|
|
499
|
+
self.body = self._extract_body_param()
|
|
470
500
|
parameters = self._kwargs | extra
|
|
471
501
|
cache_key = self._cache_key()
|
|
472
502
|
etag_key = f"{cache_key}_etag"
|
|
@@ -474,7 +504,7 @@ class EsiOperation(BaseEsiOperation):
|
|
|
474
504
|
if not etag and app_settings.ESI_CACHE_RESPONSE:
|
|
475
505
|
etag = cache.get(etag_key)
|
|
476
506
|
|
|
477
|
-
headers, data, response = self._get_cache(cache_key)
|
|
507
|
+
headers, data, response = self._get_cache(cache_key, etag=etag) if use_cache else (None, None, None)
|
|
478
508
|
|
|
479
509
|
if response and use_cache:
|
|
480
510
|
expiry = _time_to_expiry(str(headers.get('Expires')))
|
|
@@ -488,17 +518,41 @@ class EsiOperation(BaseEsiOperation):
|
|
|
488
518
|
|
|
489
519
|
if not response:
|
|
490
520
|
logger.debug(f"Cache Miss {self.url}")
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
521
|
+
try:
|
|
522
|
+
headers, data, response = self._make_request(parameters, etag)
|
|
523
|
+
if response.status_code == 420:
|
|
524
|
+
reset = response.headers.get("X-RateLimit-Reset", None)
|
|
525
|
+
cache.set("esi_error_limit_reset", reset, timeout=reset)
|
|
526
|
+
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
|
+
|
|
535
|
+
# Shim our exceptions into Django-ESI
|
|
536
|
+
except base_HTTPServerError as e:
|
|
537
|
+
raise HTTPServerError(
|
|
538
|
+
status_code=e.status_code,
|
|
539
|
+
headers=e.headers,
|
|
540
|
+
data=e.data
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
except base_HTTPClientError as e:
|
|
544
|
+
raise HTTPClientError(
|
|
545
|
+
status_code=e.status_code,
|
|
546
|
+
headers=e.headers,
|
|
547
|
+
data=e.data
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# Throw a 304 exception for catching.
|
|
551
|
+
if response.status_code == 304:
|
|
552
|
+
raise HTTPNotModified(
|
|
553
|
+
status_code=304,
|
|
554
|
+
headers=response.headers
|
|
555
|
+
)
|
|
502
556
|
|
|
503
557
|
return (data, response) if return_response else data
|
|
504
558
|
|
|
@@ -508,8 +562,8 @@ class EsiOperation(BaseEsiOperation):
|
|
|
508
562
|
return_response: bool = False,
|
|
509
563
|
use_cache: bool = True,
|
|
510
564
|
**extra) -> tuple[list[Any], Response | Any | None] | list[Any]:
|
|
511
|
-
all_results = []
|
|
512
|
-
last_response = None
|
|
565
|
+
all_results: list[Any] = []
|
|
566
|
+
last_response: Response | None = None
|
|
513
567
|
"""Executes the request and returns the response from ESI for the current
|
|
514
568
|
operation. Response will include all pages if there are more available.
|
|
515
569
|
|
|
@@ -557,10 +611,31 @@ class EsiOperation(BaseEsiOperation):
|
|
|
557
611
|
|
|
558
612
|
return (all_results, last_response) if return_response else all_results
|
|
559
613
|
|
|
560
|
-
def results_localized(
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
614
|
+
def results_localized(
|
|
615
|
+
self,
|
|
616
|
+
languages: list[str] | str | None = None,
|
|
617
|
+
**kwargs) -> dict[str, list[Any]]:
|
|
618
|
+
"""Executes the request and returns the response from ESI for all default languages and pages (if any).
|
|
619
|
+
Args:
|
|
620
|
+
languages: (list[str], str, optional) language(s) to return instead of default languages
|
|
621
|
+
Raises:
|
|
622
|
+
ValueError: Invalid or Not Supported Language Code ...
|
|
623
|
+
Returns:
|
|
624
|
+
dict[str, list[Any]]: Dict of all responses with the language code as keys.
|
|
625
|
+
"""
|
|
626
|
+
if not languages:
|
|
627
|
+
my_languages = list(app_settings.ESI_LANGUAGES)
|
|
628
|
+
else:
|
|
629
|
+
my_languages = []
|
|
630
|
+
for lang in dict.fromkeys(languages):
|
|
631
|
+
if lang not in app_settings.ESI_LANGUAGES:
|
|
632
|
+
raise ValueError('Invalid or Not Supported Language Code: %s' % lang)
|
|
633
|
+
my_languages.append(lang)
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
language: self.results(accept_language=language, **kwargs)
|
|
637
|
+
for language in my_languages
|
|
638
|
+
}
|
|
564
639
|
|
|
565
640
|
def required_scopes(self) -> list[str]:
|
|
566
641
|
"""Return a simple list of scopes required for an endpoint. #Requires loading and processing a client
|
|
@@ -678,8 +753,31 @@ class EsiOperationAsync(BaseEsiOperation):
|
|
|
678
753
|
|
|
679
754
|
return (all_results, last_response) if return_response else all_results
|
|
680
755
|
|
|
681
|
-
|
|
682
|
-
|
|
756
|
+
def results_localized(
|
|
757
|
+
self,
|
|
758
|
+
languages: list[str] | str | None = None,
|
|
759
|
+
**extra) -> dict[str, list[Any]]:
|
|
760
|
+
"""Executes the request and returns the response from ESI for all default languages and pages (if any).
|
|
761
|
+
Args:
|
|
762
|
+
languages: (list[str], str, optional) language(s) to return instead of default languages
|
|
763
|
+
Raises:
|
|
764
|
+
ValueError: Invalid or Not Supported Language Code ...
|
|
765
|
+
Returns:
|
|
766
|
+
dict[str, list[Any]]: Dict of all responses with the language code as keys.
|
|
767
|
+
"""
|
|
768
|
+
if not languages:
|
|
769
|
+
my_languages = list(app_settings.ESI_LANGUAGES)
|
|
770
|
+
else:
|
|
771
|
+
my_languages = []
|
|
772
|
+
for lang in dict.fromkeys(languages):
|
|
773
|
+
if lang not in app_settings.ESI_LANGUAGES:
|
|
774
|
+
raise ValueError('Invalid or Not Supported Language Code: %s' % lang)
|
|
775
|
+
my_languages.append(lang)
|
|
776
|
+
|
|
777
|
+
return {
|
|
778
|
+
language: self.results(accept_language=language, **kwargs)
|
|
779
|
+
for language in my_languages
|
|
780
|
+
}
|
|
683
781
|
|
|
684
782
|
def required_scopes(self) -> list[str]:
|
|
685
783
|
"""Return a simple list of scopes required for an endpoint. #Requires loading and processing a client
|
|
@@ -749,7 +847,11 @@ class ESIClient(ESIClientStub):
|
|
|
749
847
|
if tag == "_":
|
|
750
848
|
return self.api._operationindex
|
|
751
849
|
|
|
752
|
-
|
|
850
|
+
# convert pythonic Planetary_Interaction to Planetary Interaction
|
|
851
|
+
if "_" in tag:
|
|
852
|
+
tag = tag.replace("_", " ")
|
|
853
|
+
|
|
854
|
+
if tag in set(self.api._operationindex._tags.keys()):
|
|
753
855
|
return ESITag(self.api._operationindex._tags[tag], self.api)
|
|
754
856
|
|
|
755
857
|
raise AttributeError(
|
|
@@ -772,7 +874,11 @@ class ESIClientAsync(ESIClientStub):
|
|
|
772
874
|
if tag == "_":
|
|
773
875
|
return self.api._operationindex
|
|
774
876
|
|
|
775
|
-
|
|
877
|
+
# convert pythonic Planetary_Interaction to Planetary Interaction
|
|
878
|
+
if "_" in tag:
|
|
879
|
+
tag = tag.replace("_", " ")
|
|
880
|
+
|
|
881
|
+
if tag in set(self.api._operationindex._tags.keys()):
|
|
776
882
|
return ESITagAsync(self.api._operationindex._tags[tag], self.api)
|
|
777
883
|
|
|
778
884
|
raise AttributeError(
|
|
@@ -784,7 +890,7 @@ class ESIClientAsync(ESIClientStub):
|
|
|
784
890
|
class ESIClientProvider:
|
|
785
891
|
"""Class for providing a single ESI client instance for a whole app
|
|
786
892
|
Args:
|
|
787
|
-
compatibility_date (str): The compatibility date for the ESI client.
|
|
893
|
+
compatibility_date (str | date): The compatibility date for the ESI client.
|
|
788
894
|
ua_appname (str): Name of the App for generating a User-Agent,
|
|
789
895
|
ua_version (str): Version of the App for generating a User-Agent,
|
|
790
896
|
ua_url (str, Optional): URL To the Source Code or Documentation for generating a User-Agent,
|
|
@@ -795,9 +901,12 @@ class ESIClientProvider:
|
|
|
795
901
|
client_async(): ESIClientAsync
|
|
796
902
|
"""
|
|
797
903
|
|
|
904
|
+
_client: ESIClient | None = None
|
|
905
|
+
_client_async: ESIClientAsync | None = None
|
|
906
|
+
|
|
798
907
|
def __init__(
|
|
799
908
|
self,
|
|
800
|
-
compatibility_date: str,
|
|
909
|
+
compatibility_date: str | date,
|
|
801
910
|
ua_appname: str,
|
|
802
911
|
ua_version: str,
|
|
803
912
|
ua_url: str | None = None,
|
|
@@ -805,7 +914,10 @@ class ESIClientProvider:
|
|
|
805
914
|
tenant: str = "tranquility",
|
|
806
915
|
**kwargs
|
|
807
916
|
) -> None:
|
|
808
|
-
|
|
917
|
+
if type(compatibility_date) is date:
|
|
918
|
+
self._compatibility_date = self._date_to_string(compatibility_date)
|
|
919
|
+
else:
|
|
920
|
+
self._compatibility_date = compatibility_date
|
|
809
921
|
self._ua_appname = ua_appname
|
|
810
922
|
self._ua_version = ua_version
|
|
811
923
|
self._ua_url = ua_url
|
|
@@ -841,5 +953,11 @@ class ESIClientProvider:
|
|
|
841
953
|
self._client_async = ESIClientAsync(api)
|
|
842
954
|
return self._client_async
|
|
843
955
|
|
|
956
|
+
@classmethod
|
|
957
|
+
def _date_to_string(cls, compatibility_date: date) -> str:
|
|
958
|
+
"""Turns a date object in a compatibility_date string"""
|
|
959
|
+
return f"{compatibility_date.year}-{compatibility_date.month:02}-{compatibility_date.day:02}"
|
|
960
|
+
|
|
961
|
+
|
|
844
962
|
def __str__(self) -> str:
|
|
845
963
|
return "ESIClientProvider"
|