django-esi 7.0.1__py3-none-any.whl → 8.0.0a2__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.

@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-esi
3
- Version: 7.0.1
3
+ Version: 8.0.0a2
4
4
  Summary: Django app for accessing the EVE Swagger Interface (ESI).
5
+ Keywords: eveonline
5
6
  Author-email: Alliance Auth <adarnof@gmail.com>
6
- Requires-Python: >=3.8
7
+ Requires-Python: >=3.10
7
8
  Description-Content-Type: text/markdown
8
9
  Classifier: Environment :: Web Environment
9
10
  Classifier: Framework :: Django
@@ -15,8 +16,6 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
15
16
  Classifier: Operating System :: OS Independent
16
17
  Classifier: Programming Language :: Python
17
18
  Classifier: Programming Language :: Python :: 3 :: Only
18
- Classifier: Programming Language :: Python :: 3.8
19
- Classifier: Programming Language :: Python :: 3.9
20
19
  Classifier: Programming Language :: Python :: 3.10
21
20
  Classifier: Programming Language :: Python :: 3.11
22
21
  Classifier: Programming Language :: Python :: 3.12
@@ -24,14 +23,16 @@ Classifier: Programming Language :: Python :: 3.13
24
23
  Classifier: Topic :: Internet :: WWW/HTTP
25
24
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
26
25
  License-File: LICENSE
26
+ Requires-Dist: aiopenapi3
27
27
  Requires-Dist: bravado>=10.6,<12
28
- Requires-Dist: brotli
29
28
  Requires-Dist: celery>=4.0.2
30
29
  Requires-Dist: django>=4.2,<6
30
+ Requires-Dist: httpx[http2, brotli, zstd]
31
31
  Requires-Dist: jsonschema<4
32
32
  Requires-Dist: python-jose>=3.3
33
33
  Requires-Dist: requests>=2.26
34
34
  Requires-Dist: requests-oauthlib>=0.8
35
+ Requires-Dist: tenacity
35
36
  Requires-Dist: tqdm>=4.62.3
36
37
  Requires-Dist: myst-parser ; extra == "docs"
37
38
  Requires-Dist: sphinx ; extra == "docs"
@@ -1,16 +1,24 @@
1
- esi/__init__.py,sha256=bGozFstfV7mCOH_OlhaeM255CAmXhs_byhCy_hBj58Y,167
2
- esi/admin.py,sha256=glkzGf8Z76mEd6BGpsMC0B5LLH5--MWlfOCEkSBL7mI,1110
3
- esi/app_settings.py,sha256=O8TkKonK2zdF7RjMem1vMhEMulAtjvksxKgchrI8jME,3783
1
+ esi/__init__.py,sha256=x_GMW2iG9w-jeVStEapSJo_3MzGJm8XDnCJ6cTscRq4,199
2
+ esi/admin.py,sha256=9i68WwW_gR0zsGhJKTWK2yMEi44EkhpAaOW0LDpVfM8,1176
3
+ esi/app_settings.py,sha256=2Jp_1myzKCL8A20RYvwQnQd04Ds_CBGsCqwDtsrYf0M,4289
4
4
  esi/apps.py,sha256=HIu1niTkOXYmCzMVAjYcaFhHrrXeBbHvui6I44OCHXw,280
5
- esi/checks.py,sha256=Aa3qiEK86w-dhJ4_OfepUxxxjiySMdR-e2fFKGYA9ss,1912
6
- esi/clients.py,sha256=hQ8LMfqMh5DnwomatIzCWaDeDOP76-9M6glFLZbNrDE,23529
7
- esi/decorators.py,sha256=HEaOHv-stiyU-AL4nv24kRNzUbN5TMvT282WA6ZC8pY,8128
5
+ esi/checks.py,sha256=31puQdsrpRUJB-kedp2k7Evo0x2knTWQPZsCUUrJ3dY,1912
6
+ esi/clients.py,sha256=WB5YseJnfZdTeJ9C1Eze-iWXEqIpo5i_8uiQ6w0VkCw,23503
7
+ esi/decorators.py,sha256=2RmPdkrIAwbxOV5Ls8-RIWd_VhmXEDzIG6g5-bs6WZc,9093
8
8
  esi/errors.py,sha256=KfFtgX8Mys4uMoQtco0n_pQeaM83yVrRSyW6StXuUjo,308
9
- esi/managers.py,sha256=0g3J2jgoX-BviisjDEEAypkTayVBY-G2I_ZuskTDUqg,11350
10
- esi/models.py,sha256=bc8DtvX4cTgLK92O-mgNGXtHIyT_TbH3eefVDGoS8O8,11507
11
- esi/tasks.py,sha256=AdX30ADt1OK6alTmVW014aDlDs7GEkTYGpQdx2cYQBY,2659
12
- esi/urls.py,sha256=23dY6ciekJ5wfwzo5LzLp0LFvWudrEAGICpDrscTeD8,152
13
- esi/views.py,sha256=ytfCILjz1rzITGgAGPyCY9bkFpE3KUfnii2UpHu6Bmc,3916
9
+ esi/exceptions.py,sha256=Zf2cPFFbIYL90sYVtGeZXmna2n_3VyLdVApa5BRUvKc,747
10
+ esi/helpers.py,sha256=-lYojtcWYWtlC_olSItWR3O3zJpjMYTQpNvsax81us8,672
11
+ esi/managers.py,sha256=zdri1aSJrSX9W_kwzbXq44CAAhdufikoOViaJntA41A,11443
12
+ esi/managers.pyi,sha256=BxRNX2yOT3domdrxsuGp4G0TrP_bWmf8VVwvM03eHQE,1977
13
+ esi/models.py,sha256=v33dvnWoiDwhcwc5Y4CvNOtAJocNd2qh5JtyNTtu-P8,11483
14
+ esi/openapi_clients.py,sha256=l7XVB9cXiW04aG18Knb_gsCmQP20TghpyDPuyjUzr1Y,34385
15
+ esi/rate_limiting.py,sha256=fUzso1YCHlBes8SVrEAsP58eRWcFNuO2XEkW6RQLhUE,2600
16
+ esi/stubs.py,sha256=UakysEAq5V454oKbxyEUewncr5sCFZCgwHFh0SJvU3Y,30
17
+ esi/stubs.pyi,sha256=wT1aqp4eATKrReFupxmTz4wpvHNBQ0TpCJ-_zf2TjJg,231643
18
+ esi/tasks.py,sha256=TaE_Q03oLk1ohkPw8KknXpIK-nY47C-oaGUUu4nr8pw,2658
19
+ esi/urls.py,sha256=B4EnmT-MZzs4F8a9ORK1ejAHbpGRN_V4TKks8ww3JGE,151
20
+ esi/views.py,sha256=cq2JkqSAwx-Nqi3amRUA9CKLu3ZLQYHSAH8c8g7xIQ4,3917
21
+ esi/aiopenapi3/plugins.py,sha256=hHvWA_Sil5VTR1KLyF8dPSppx4g-daY9cj0kUSUZKxc,3914
14
22
  esi/locale/de/LC_MESSAGES/django.mo,sha256=9_Fc_R8oZpT_CojH8gBxdSqj2K84EMEMn4wkvmOr7Dg,820
15
23
  esi/locale/de/LC_MESSAGES/django.po,sha256=2NQVGYzLroHPPrWgmACSnb2d3nwlkMBLxA1uWU3K0FA,1503
16
24
  esi/locale/en/LC_MESSAGES/django.mo,sha256=N1pb17IfLd0ASiKO8d68-B4ygSpDkhKOCs8YTzMXQo0,380
@@ -30,6 +38,7 @@ esi/locale/ru/LC_MESSAGES/django.po,sha256=_pMs3mMOGMiHGpk27p8ykllpGnw20l5sSDg-N
30
38
  esi/locale/zh_Hans/LC_MESSAGES/django.mo,sha256=hPSO7wlU9cN8f32Qo1Dztq6AHUm2hZfL1hHIDW1R1F4,681
31
39
  esi/locale/zh_Hans/LC_MESSAGES/django.po,sha256=fZhif8jOUvY22xurl7eBYsHNKoqf7obtkW5L3894hk4,1451
32
40
  esi/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
+ esi/management/commands/generate_esi_stubs.py,sha256=MlsLe_otphvkRDXs-MwbNLfvIhESKD9tOJxMdqvGunc,10579
33
42
  esi/management/commands/migrate_to_ssov2.py,sha256=x9W4xaefrNoG_dNscHIWrWTRk4SuVGIW0s44q2QBN9k,6427
34
43
  esi/migrations/0001_initial.py,sha256=ItZOyyCRyfyxUhxCG1P08oylCY355gqOKGF3cJTQmcM,3396
35
44
  esi/migrations/0002_scopes_20161208.py,sha256=AG3qVAoEXh8WTgxuvS1pgG8LxLGMzpqr-jqtMjy3qC0,2846
@@ -57,7 +66,7 @@ esi/tests/client_authed_pilot.py,sha256=GN0J59TcscHM9HZH6OaPODM9Zel3mvObjJ4Dmmzc
57
66
  esi/tests/client_public_pilot.py,sha256=u6P-qbdYSvQ6A_MJqt8Jrkswt-u-FlvYDKtCvA5-PMo,1252
58
67
  esi/tests/factories.py,sha256=2pjkSYKDfO50bzX1nNcUHz_FAAJs7k07u7R8om6AlP0,1375
59
68
  esi/tests/factories_2.py,sha256=25NqVe9zzHkl4U1EN3R7pjdN2ahIBXxrrJhQzZhvw58,1972
60
- esi/tests/jwt_factory.py,sha256=1Ilg99V7Nc_jtUFrYVNm6GfnJ2f5ev0vhpnDhRur8zo,4944
69
+ esi/tests/jwt_factory.py,sha256=0Jj9TjEv18h2XVsRpYc7BWHr3CPtQJgfKeB-9mdHevk,4919
61
70
  esi/tests/test_checks.py,sha256=ntq2ijuJ-6pxGruNh21rM4GlXDLVm-o3sy8DrFbjwEM,1846
62
71
  esi/tests/test_clients.py,sha256=1JWTBV83nZbt82e37DAnJ7h4gm9m_xU1ZDvEjt0sHsc,41363
63
72
  esi/tests/test_decorators.py,sha256=I5MhcLKhN5xD4PxgEQqOWQ2kiXquDqQIEve9dbTOsho,15069
@@ -70,7 +79,7 @@ esi/tests/test_tasks.py,sha256=nIvfXax_8nQCxNQeT-4TJ84i0__t1qWe155z5rnsktQ,4399
70
79
  esi/tests/test_templatetags.py,sha256=b68JWE3HvOlr2aUisJHsTsDS4e7IMjDeqTuzMqC7Re4,517
71
80
  esi/tests/test_views.py,sha256=Kj_f2yIpmPG0kx-lAX_sfkaHlIpgbkm02ieA1V3o-k4,13073
72
81
  esi/tests/threading_pilot.py,sha256=ax_dEdnTNibA-UQHqbZle_2dh_3jcHKRyrYSOKuE_6U,1931
73
- django_esi-7.0.1.dist-info/licenses/LICENSE,sha256=WJ7YI-moTFb-uVrFjnzzhGJrnL9P2iqQe8NuED3hutI,35141
74
- django_esi-7.0.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
75
- django_esi-7.0.1.dist-info/METADATA,sha256=LlxPclW944wcUkCEkX0XU90sqIc8Qjlk8d5r9XTLn8M,4745
76
- django_esi-7.0.1.dist-info/RECORD,,
82
+ django_esi-8.0.0a2.dist-info/licenses/LICENSE,sha256=WJ7YI-moTFb-uVrFjnzzhGJrnL9P2iqQe8NuED3hutI,35141
83
+ django_esi-8.0.0a2.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
84
+ django_esi-8.0.0a2.dist-info/METADATA,sha256=nYKhXDNlrzlcplbMUL_eWoZcHQZ3B-oT4S1pgs8elAM,4738
85
+ django_esi-8.0.0a2.dist-info/RECORD,,
esi/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Django app for accessing the EVE Swagger Interface (ESI)."""
2
2
 
3
- __version__ = '7.0.1'
3
+ __version__ = '8.0.0a2'
4
4
  __title__ = 'Django-ESI'
5
5
  __url__ = 'https://gitlab.com/allianceauth/django-esi'
6
+ __build_date__ = "2025-09-03"
esi/admin.py CHANGED
@@ -1,7 +1,8 @@
1
1
  from django.contrib import admin
2
2
  from django.contrib.auth import get_user_model
3
+ from django.db.models import QuerySet
3
4
 
4
- from .models import Token, Scope, CallbackRedirect
5
+ from .models import CallbackRedirect, Scope, Token
5
6
 
6
7
  admin.site.register(CallbackRedirect)
7
8
 
@@ -13,14 +14,14 @@ class ScopeAdmin(admin.ModelAdmin):
13
14
 
14
15
  @admin.register(Token)
15
16
  class TokenAdmin(admin.ModelAdmin):
16
- def get_queryset(self, request):
17
+ def get_queryset(self, request) -> QuerySet["Token"]:
17
18
  qs = super().get_queryset(request)
18
19
  return qs.select_related('user').prefetch_related('scopes')
19
20
 
20
21
  @admin.display(
21
22
  description='Scopes'
22
23
  )
23
- def get_scopes(self, obj):
24
+ def get_scopes(self, obj) -> str:
24
25
  return ", ".join([x.name for x in obj.scopes.all()])
25
26
 
26
27
  User = get_user_model()
@@ -0,0 +1,96 @@
1
+ from aiopenapi3.plugin import Document
2
+
3
+
4
+ class Trim204ContentType(Document):
5
+ """
6
+ Removes and content-type from responses on a 204 reponses
7
+ A 204 never has content...
8
+ """
9
+ def parsed(self, ctx: Document.Context) -> Document.Context:
10
+ spec = ctx.document
11
+ # Patch all paths
12
+ for path_item in spec.get("paths", {}).values():
13
+ for method_name in ("get", "post", "put", "delete", "patch", "options", "head"):
14
+ method = path_item.get(method_name)
15
+ if not method:
16
+ continue
17
+ if "204" in method['responses']:
18
+ method['responses']["204"].pop("content", [])
19
+ return ctx
20
+
21
+
22
+ class RemoveSecurityParameter(Document):
23
+ """
24
+ Removes the whole OAuth2 securityScheme
25
+ """
26
+ def parsed(self, ctx: Document.Context) -> Document.Context:
27
+ print("RemoveSecurityParameterPlugin: Removing OAuth2 securityScheme")
28
+ spec = ctx.document
29
+ oauth2 = spec.get("components", {}).get("securitySchemes", {}).pop("OAuth2", None)
30
+ # Patch all paths
31
+ for path_item in spec.get("paths", {}).values():
32
+ for method_name in ("get", "post", "put", "delete", "patch", "options", "head"):
33
+ method = path_item.get(method_name)
34
+ if not method:
35
+ continue
36
+ method.pop("security", None)
37
+
38
+ return ctx
39
+
40
+
41
+ class TrimSecurityParameter(Document):
42
+ """TrimSecurityParameter
43
+ Trims out of Spec OAuth2 attributes. CCP have fixed this.
44
+ Leaving in place in case we need a quick reference again.
45
+ """
46
+ def parsed(self, ctx: Document.Context) -> Document.Context:
47
+ print("TrimSecurityParameter: Trimming out of spec attributes")
48
+ spec = ctx.document
49
+ oauth2 = spec.get("components", {}).get("securitySchemes", {}).get("OAuth2")
50
+ if oauth2 and oauth2.get("type") == "oauth2":
51
+ oauth2.pop("in", None)
52
+ oauth2.pop("name", None)
53
+ return ctx
54
+
55
+
56
+ class PatchCompatibilityDatePlugin(Document):
57
+ """
58
+ Makes the X-Compatibility-Date header optional
59
+
60
+ This is because WE specifically add it in the library to the HTTP requests,
61
+ but without this, it will be a required parameter during request generation before it hits the HTTP library.
62
+ """
63
+ def parsed(self, ctx: Document.Context) -> Document.Context:
64
+ print("PatchCompatibilityDatePlugin: making compatibility date optional")
65
+ spec = ctx.document
66
+
67
+ def patch_param(param):
68
+ # Follow $ref if present
69
+ if "$ref" in param:
70
+ ref = param["$ref"]
71
+ parts = ref.split("/")
72
+ if parts[1] == "components" and parts[2] == "parameters":
73
+ param_name = parts[-1]
74
+ comp_param = spec.get("components", {}).get("parameters", {}).get(param_name)
75
+ if comp_param and comp_param.get("name") == "X-Compatibility-Date":
76
+ comp_param["required"] = False
77
+ else:
78
+ # Inline parameter
79
+ if (param.get("name") == "X-Compatibility-Date" and param.get("in") == "header"):
80
+ param["required"] = False
81
+
82
+ # Patch all paths
83
+ for path_item in spec.get("paths", {}).values():
84
+ for method_name in ("get", "post", "put", "delete", "patch", "options", "head"):
85
+ method = path_item.get(method_name)
86
+ if not method:
87
+ continue
88
+ for param in method.get("parameters", []):
89
+ patch_param(param)
90
+
91
+ # Patch global parameters in components
92
+ for param_name, param in spec.get("components", {}).get("parameters", {}).items():
93
+ if param.get("name") == "X-Compatibility-Date" and param.get("in") == "header":
94
+ param["required"] = False
95
+
96
+ return ctx
esi/app_settings.py CHANGED
@@ -66,8 +66,21 @@ ESI_REQUESTS_CONNECT_TIMEOUT = getattr(settings, 'ESI_REQUESTS_CONNECT_TIMEOUT',
66
66
  Can temporarily overwritten with by passing ``timeout`` with ``result()``
67
67
  """
68
68
 
69
- ESI_REQUESTS_READ_TIMEOUT = getattr(settings, 'ESI_REQUESTS_READ_TIMEOUT', 30)
69
+ ESI_REQUESTS_READ_TIMEOUT = getattr(settings, 'ESI_REQUESTS_READ_TIMEOUT', 10)
70
70
  """Default read timeouts for all requests to ESI.
71
+ This should be a maximum of 10s as ESI cuts all requests to the monolith off @ 10s.
72
+
73
+ Can temporarily overwritten with by passing ``timeout`` with ``result()``
74
+ """
75
+
76
+ ESI_REQUESTS_WRITE_TIMEOUT = getattr(settings, 'ESI_REQUESTS_WRITE_TIMEOUT', 5)
77
+ """Default write timeouts for all requests to ESI.
78
+
79
+ Can temporarily overwritten with by passing ``timeout`` with ``result()``
80
+ """
81
+
82
+ ESI_REQUESTS_POOL_TIMEOUT = getattr(settings, 'ESI_REQUESTS_POOL_TIMEOUT', 5)
83
+ """Default pool timeouts for all requests to ESI.
71
84
 
72
85
  Can temporarily overwritten with by passing ``timeout`` with ``result()``
73
86
  """
@@ -95,5 +108,5 @@ ESI_TOKEN_JWT_AUDIENCE = str(getattr(settings, "ESI_TOKEN_JWT_AUDIENCE", "EVE On
95
108
 
96
109
  # list of all official language codes supported by ESI
97
110
  ESI_LANGUAGES = getattr(settings, 'ESI_LANGUAGES', [
98
- 'de', 'en-us', 'fr', 'ja', 'ru', 'zh', 'ko'
111
+ 'en', 'de', 'fr', 'ja', 'ru', 'zh', 'ko', 'es'
99
112
  ])
esi/checks.py CHANGED
@@ -1,5 +1,5 @@
1
- from django.core.checks import Error, Warning, register, Tags
2
1
  from django.conf import settings
2
+ from django.core.checks import Error, Tags, Warning, register
3
3
 
4
4
 
5
5
  @register(Tags.security)
esi/clients.py CHANGED
@@ -1,28 +1,27 @@
1
- from datetime import datetime
2
- from hashlib import md5
3
1
  import json
4
2
  import logging
3
+ import warnings
4
+ from datetime import datetime
5
+ from hashlib import md5
5
6
  from time import sleep
7
+ from typing import Any
6
8
  from urllib import parse as urlparse
7
- from typing import Any, Union, Tuple
8
- import warnings
9
9
 
10
- from bravado.client import SwaggerClient
11
10
  from bravado import requests_client
11
+ from bravado.client import SwaggerClient
12
12
  from bravado.exception import (
13
- HTTPBadGateway, HTTPGatewayTimeout, HTTPServiceUnavailable
13
+ HTTPBadGateway, HTTPGatewayTimeout, HTTPServiceUnavailable,
14
14
  )
15
- from bravado_core.response import IncomingResponse
16
- from bravado.swagger_model import Loader
17
15
  from bravado.http_future import HttpFuture
18
- from bravado_core.spec import Spec, CONFIG_DEFAULTS
16
+ from bravado.swagger_model import Loader
17
+ from bravado_core.response import IncomingResponse
18
+ from bravado_core.spec import CONFIG_DEFAULTS, Spec
19
19
  from requests.adapters import HTTPAdapter
20
20
 
21
21
  from django.core.cache import cache
22
22
 
23
+ from . import __title__, __url__, __version__, app_settings
23
24
  from .errors import TokenExpiredError
24
- from . import app_settings, __version__, __title__, __url__
25
-
26
25
 
27
26
  logger = logging.getLogger(__name__)
28
27
 
@@ -75,7 +74,7 @@ class CachingHttpFuture(HttpFuture):
75
74
  except ValueError:
76
75
  return 0
77
76
 
78
- def results(self, **kwargs) -> Union[Any, Tuple[Any, IncomingResponse]]:
77
+ def results(self, **kwargs) -> Any | tuple[Any, IncomingResponse]:
79
78
  """Executes the request and returns the response from ESI for the current
80
79
  route. Response will include all pages if there are more available.
81
80
 
@@ -143,7 +142,7 @@ class CachingHttpFuture(HttpFuture):
143
142
  for language in my_languages
144
143
  }
145
144
 
146
- def result(self, **kwargs) -> Union[Any, Tuple[Any, IncomingResponse]]:
145
+ def result(self, **kwargs) -> Any | tuple[Any, IncomingResponse]:
147
146
  """Executes the request and returns the response from ESI. Response will
148
147
  include the requested / first page only if there are more pages available.
149
148
 
@@ -222,7 +221,7 @@ class CachingHttpFuture(HttpFuture):
222
221
 
223
222
  return super().result(**kwargs)
224
223
 
225
- def _result_with_retries(self, **kwargs) -> Tuple[Any, IncomingResponse]:
224
+ def _result_with_retries(self, **kwargs) -> tuple[Any, IncomingResponse]:
226
225
  """Execute request and retry on certain HTTP errors.
227
226
 
228
227
  ``kwargs`` are passed through to super().result()
esi/decorators.py CHANGED
@@ -1,14 +1,18 @@
1
+ import functools
1
2
  import logging
3
+ import time
2
4
  from functools import wraps
3
- from typing import Union
4
5
 
5
- from .models import Token, CallbackRedirect
6
+ from django.core.cache import cache
6
7
 
8
+ from esi.rate_limiting import ESIRateLimitBucket, ESIRateLimits
9
+
10
+ from .models import CallbackRedirect, Token
7
11
 
8
12
  logger = logging.getLogger(__name__)
9
13
 
10
14
 
11
- def _check_callback(request) -> Union[Token, None]:
15
+ def _check_callback(request) -> Token | None:
12
16
  # ensure session installed in database
13
17
  if not request.session.exists(request.session.session_key):
14
18
  logger.debug("Creating new session for %s", request.user)
@@ -221,3 +225,31 @@ def single_use_token(scopes='', new=False):
221
225
  return _wrapped_view
222
226
 
223
227
  return decorator
228
+
229
+
230
+ def wait_for_esi_errorlimit_reset(cache_key="esi_error_limit_reset", poll_interval=1):
231
+ def decorator(func):
232
+ def wrapper(*args, **kwargs):
233
+ reset = cache.get(cache_key)
234
+ if reset is not None:
235
+ print(f"ESI Error Limited, waiting {reset}s before retrying...")
236
+ while cache.get(cache_key):
237
+ time.sleep(poll_interval)
238
+ return func(*args, **kwargs)
239
+ return wrapper
240
+ return decorator
241
+
242
+
243
+ def esi_rate_limiter_bucketed(
244
+ bucket: ESIRateLimitBucket,
245
+ raise_on_limit: bool = True,
246
+ ):
247
+ # TODO Investigate esi cache hits.
248
+
249
+ def decorator(func):
250
+ @functools.wraps(func)
251
+ def wrapper(*args, **kwargs):
252
+ ESIRateLimits.check_bucket(bucket, raise_on_limit)
253
+ return func(*args, **kwargs)
254
+ return wrapper
255
+ return decorator
esi/exceptions.py ADDED
@@ -0,0 +1,18 @@
1
+ class ESIErrorLimitException(Exception):
2
+ """ESI Global Error Limit Exceeded
3
+ https://developers.eveonline.com/docs/services/esi/best-practices/#error-limit
4
+ """
5
+ def __init__(self, reset=None, *args, **kwargs) -> None:
6
+ self.reset = reset
7
+ msg = kwargs.get("message") or (
8
+ f"ESI Error limited. Reset in {reset} seconds." if reset else "ESI Error limited."
9
+ )
10
+ super().__init__(msg, *args)
11
+
12
+
13
+ class ESIBucketLimitException(Exception):
14
+ """Endpoint (Bucket) Specific Rate Limit Exceeded"""
15
+ def __init__(self, bucket, *args, **kwargs) -> None:
16
+ self.bucket = bucket
17
+ msg = kwargs.get("message") or f"ESI bucket limit reached for {bucket}."
18
+ super().__init__(msg, *args)
esi/helpers.py ADDED
@@ -0,0 +1,25 @@
1
+ from esi.models import Token
2
+
3
+
4
+ def get_token(character_id: int, scopes: list) -> Token:
5
+ """Helper method to get a valid token for a specific character with specific scopes.
6
+
7
+ Args:
8
+ character_id: Character to filter on.
9
+ scopes: array of ESI scope strings to search for.
10
+
11
+ Returns:
12
+ Matching Token
13
+ """
14
+ qs = (
15
+ Token.objects
16
+ .filter(character_id=character_id)
17
+ .require_scopes(scopes)
18
+ .require_valid()
19
+ )
20
+ token = qs.first()
21
+ if token is None:
22
+ raise Token.DoesNotExist(
23
+ f"No valid token found for character_id={character_id} with required scopes."
24
+ )
25
+ return token