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.

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.aiopenapi3.plugins import PatchCompatibilityDatePlugin
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
- data = (self.method + self.url + str(self._args) + str(self._kwargs)).encode('utf-8')
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
- headers, data = self.parse_cached_request(cached_response)
387
+ expiry = _time_to_expiry(str(cached_response.headers.get('Expires')))
370
388
 
371
- expiry = _time_to_expiry(str(headers.get('Expires')))
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
- headers, data, response = self._make_request(parameters, etag)
492
- if response.status_code == 420:
493
- reset = response.headers.get("X-RateLimit-Reset", None)
494
- cache.set("esi_error_limit_reset", reset, timeout=reset)
495
- raise ESIErrorLimitException(reset=reset)
496
- # if response.status_code == 304 and app_settings.ESI_CACHE_RESPONSE:
497
- # cached = cache.get(cache_key)
498
- # if cached:
499
- # return (cached, response) if return_response else cached
500
- # we dont want to do this, if we do this we have to store data longer than ttl. rip ram
501
- self._store_cache(cache_key, response)
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(self, languages: str | list[str] = "en", **kwargs) -> list[Any]:
561
- # We can either push Accept-Language up to the library level
562
- # OR we insert the parameter into each request here
563
- raise NotImplementedError()
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
- async def results_localized(self, languages: str | list[str] = "en", **kwargs) -> list[Any]:
682
- raise NotImplementedError()
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
- elif tag in set(self.api._operationindex._tags.keys()):
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
- elif tag in set(self.api._operationindex._tags.keys()):
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
- self._compatibility_date = compatibility_date
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"