django-esi 7.0.1__py3-none-any.whl → 8.0.0a1__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-7.0.1.dist-info → django_esi-8.0.0a1.dist-info}/METADATA +4 -2
- {django_esi-7.0.1.dist-info → django_esi-8.0.0a1.dist-info}/RECORD +25 -16
- esi/__init__.py +2 -1
- esi/admin.py +4 -3
- esi/aiopenapi3/plugins.py +78 -0
- esi/app_settings.py +14 -1
- esi/checks.py +1 -1
- esi/clients.py +13 -14
- esi/decorators.py +35 -3
- esi/exceptions.py +18 -0
- esi/helpers.py +25 -0
- esi/management/commands/generate_esi_stubs.py +211 -0
- esi/managers.py +18 -17
- esi/managers.pyi +82 -0
- esi/models.py +9 -16
- esi/openapi_clients.py +845 -0
- esi/rate_limiting.py +78 -0
- esi/stubs.py +2 -0
- esi/stubs.pyi +3913 -0
- esi/tasks.py +1 -2
- esi/tests/jwt_factory.py +3 -4
- esi/urls.py +0 -1
- esi/views.py +4 -3
- {django_esi-7.0.1.dist-info → django_esi-8.0.0a1.dist-info}/WHEEL +0 -0
- {django_esi-7.0.1.dist-info → django_esi-8.0.0a1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-esi
|
|
3
|
-
Version:
|
|
3
|
+
Version: 8.0.0a1
|
|
4
4
|
Summary: Django app for accessing the EVE Swagger Interface (ESI).
|
|
5
5
|
Author-email: Alliance Auth <adarnof@gmail.com>
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -24,14 +24,16 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
24
24
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
25
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
26
26
|
License-File: LICENSE
|
|
27
|
+
Requires-Dist: aiopenapi3
|
|
27
28
|
Requires-Dist: bravado>=10.6,<12
|
|
28
|
-
Requires-Dist: brotli
|
|
29
29
|
Requires-Dist: celery>=4.0.2
|
|
30
30
|
Requires-Dist: django>=4.2,<6
|
|
31
|
+
Requires-Dist: httpx[http2, brotli, zstd]
|
|
31
32
|
Requires-Dist: jsonschema<4
|
|
32
33
|
Requires-Dist: python-jose>=3.3
|
|
33
34
|
Requires-Dist: requests>=2.26
|
|
34
35
|
Requires-Dist: requests-oauthlib>=0.8
|
|
36
|
+
Requires-Dist: tenacity
|
|
35
37
|
Requires-Dist: tqdm>=4.62.3
|
|
36
38
|
Requires-Dist: myst-parser ; extra == "docs"
|
|
37
39
|
Requires-Dist: sphinx ; extra == "docs"
|
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
esi/__init__.py,sha256=
|
|
2
|
-
esi/admin.py,sha256=
|
|
3
|
-
esi/app_settings.py,sha256=
|
|
1
|
+
esi/__init__.py,sha256=YJLUW2OGg6KIB9lxOqyACdI_zrRPfz7OJgtyfErVLT4,199
|
|
2
|
+
esi/admin.py,sha256=9i68WwW_gR0zsGhJKTWK2yMEi44EkhpAaOW0LDpVfM8,1176
|
|
3
|
+
esi/app_settings.py,sha256=FRetsq-ToHwiiRNtJoPSg8bCRruOFCiFqxjg_si4qQs,4286
|
|
4
4
|
esi/apps.py,sha256=HIu1niTkOXYmCzMVAjYcaFhHrrXeBbHvui6I44OCHXw,280
|
|
5
|
-
esi/checks.py,sha256=
|
|
6
|
-
esi/clients.py,sha256=
|
|
7
|
-
esi/decorators.py,sha256=
|
|
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/
|
|
10
|
-
esi/
|
|
11
|
-
esi/
|
|
12
|
-
esi/
|
|
13
|
-
esi/
|
|
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=vMy3u42xna9NzaNmA8IpGTou8FSWHxhBLwncUUnQxkg,31890
|
|
15
|
+
esi/rate_limiting.py,sha256=fUzso1YCHlBes8SVrEAsP58eRWcFNuO2XEkW6RQLhUE,2600
|
|
16
|
+
esi/stubs.py,sha256=UakysEAq5V454oKbxyEUewncr5sCFZCgwHFh0SJvU3Y,30
|
|
17
|
+
esi/stubs.pyi,sha256=J2SN_fZz3gXANqDamnRUiHzBX3LRd0cFMWC8XbcgiFI,236053
|
|
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=B8me42zaBJJJyUmARZAJS2fahbmZFih2HGxpTpJ3LF8,3252
|
|
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=wwCyrl5UooZJ6Dt4YU7gVMdI22GAQSn_gCHxNsfjipo,9467
|
|
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=
|
|
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-
|
|
74
|
-
django_esi-
|
|
75
|
-
django_esi-
|
|
76
|
-
django_esi-
|
|
82
|
+
django_esi-8.0.0a1.dist-info/licenses/LICENSE,sha256=WJ7YI-moTFb-uVrFjnzzhGJrnL9P2iqQe8NuED3hutI,35141
|
|
83
|
+
django_esi-8.0.0a1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
84
|
+
django_esi-8.0.0a1.dist-info/METADATA,sha256=1KLv0PgWJaTuDBUJ8oFKxjMRhZHc_gSBsReo471tHfA,4817
|
|
85
|
+
django_esi-8.0.0a1.dist-info/RECORD,,
|
esi/__init__.py
CHANGED
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
|
|
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,78 @@
|
|
|
1
|
+
from aiopenapi3.plugin import Document
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RemoveSecurityParameter(Document):
|
|
5
|
+
"""
|
|
6
|
+
Removes the whole OAuth2 securityScheme
|
|
7
|
+
"""
|
|
8
|
+
def parsed(self, ctx: Document.Context) -> Document.Context:
|
|
9
|
+
print("RemoveSecurityParameterPlugin: Removing OAuth2 securityScheme")
|
|
10
|
+
spec = ctx.document
|
|
11
|
+
oauth2 = spec.get("components", {}).get("securitySchemes", {}).pop("OAuth2", None)
|
|
12
|
+
# Patch all paths
|
|
13
|
+
for path_item in spec.get("paths", {}).values():
|
|
14
|
+
for method_name in ("get", "post", "put", "delete", "patch", "options", "head"):
|
|
15
|
+
method = path_item.get(method_name)
|
|
16
|
+
if not method:
|
|
17
|
+
continue
|
|
18
|
+
method.pop("security", None)
|
|
19
|
+
|
|
20
|
+
return ctx
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TrimSecurityParameter(Document):
|
|
24
|
+
"""TrimSecurityParameter
|
|
25
|
+
Trims out of Spec OAuth2 attributes. CCP have fixed this.
|
|
26
|
+
Leaving in place in case we need a quick reference again.
|
|
27
|
+
"""
|
|
28
|
+
def parsed(self, ctx: Document.Context) -> Document.Context:
|
|
29
|
+
print("TrimSecurityParameter: Trimming out of spec attributes")
|
|
30
|
+
spec = ctx.document
|
|
31
|
+
oauth2 = spec.get("components", {}).get("securitySchemes", {}).get("OAuth2")
|
|
32
|
+
if oauth2 and oauth2.get("type") == "oauth2":
|
|
33
|
+
oauth2.pop("in", None)
|
|
34
|
+
oauth2.pop("name", None)
|
|
35
|
+
return ctx
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PatchCompatibilityDatePlugin(Document):
|
|
39
|
+
"""
|
|
40
|
+
Makes the X-Compatibility-Date header optional
|
|
41
|
+
|
|
42
|
+
This is because WE specifically add it in the library to the HTTP requests,
|
|
43
|
+
but without this, it will be a required parameter during request generation before it hits the HTTP library.
|
|
44
|
+
"""
|
|
45
|
+
def parsed(self, ctx: Document.Context) -> Document.Context:
|
|
46
|
+
print("PatchCompatibilityDatePlugin: making compatibility date optional")
|
|
47
|
+
spec = ctx.document
|
|
48
|
+
|
|
49
|
+
def patch_param(param):
|
|
50
|
+
# Follow $ref if present
|
|
51
|
+
if "$ref" in param:
|
|
52
|
+
ref = param["$ref"]
|
|
53
|
+
parts = ref.split("/")
|
|
54
|
+
if parts[1] == "components" and parts[2] == "parameters":
|
|
55
|
+
param_name = parts[-1]
|
|
56
|
+
comp_param = spec.get("components", {}).get("parameters", {}).get(param_name)
|
|
57
|
+
if comp_param and comp_param.get("name") == "X-Compatibility-Date":
|
|
58
|
+
comp_param["required"] = False
|
|
59
|
+
else:
|
|
60
|
+
# Inline parameter
|
|
61
|
+
if (param.get("name") == "X-Compatibility-Date" and param.get("in") == "header"):
|
|
62
|
+
param["required"] = False
|
|
63
|
+
|
|
64
|
+
# Patch all paths
|
|
65
|
+
for path_item in spec.get("paths", {}).values():
|
|
66
|
+
for method_name in ("get", "post", "put", "delete", "patch", "options", "head"):
|
|
67
|
+
method = path_item.get(method_name)
|
|
68
|
+
if not method:
|
|
69
|
+
continue
|
|
70
|
+
for param in method.get("parameters", []):
|
|
71
|
+
patch_param(param)
|
|
72
|
+
|
|
73
|
+
# Patch global parameters in components
|
|
74
|
+
for param_name, param in spec.get("components", {}).get("parameters", {}).items():
|
|
75
|
+
if param.get("name") == "X-Compatibility-Date" and param.get("in") == "header":
|
|
76
|
+
param["required"] = False
|
|
77
|
+
|
|
78
|
+
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',
|
|
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
|
"""
|
esi/checks.py
CHANGED
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
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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 .
|
|
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) ->
|
|
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
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from aiopenapi3 import OpenAPI
|
|
5
|
+
from httpx import Client, Timeout
|
|
6
|
+
|
|
7
|
+
from django.core.management.base import BaseCommand
|
|
8
|
+
|
|
9
|
+
from esi import __title__, __url__, __version__, app_settings, stubs, __build_date__
|
|
10
|
+
from esi.aiopenapi3.plugins import PatchCompatibilityDatePlugin
|
|
11
|
+
|
|
12
|
+
# OpenAPI Types to Python
|
|
13
|
+
TYPE_MAP = {
|
|
14
|
+
"integer": "int",
|
|
15
|
+
"number": "float",
|
|
16
|
+
"string": "str",
|
|
17
|
+
"boolean": "bool",
|
|
18
|
+
"array": "list[Any]",
|
|
19
|
+
"object": "dict[str, Any]",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def sanitize_class_name(name: str) -> str:
|
|
24
|
+
"""Convert a tag into a valid Python class name."""
|
|
25
|
+
sanitized = re.sub(r'[^0-9a-zA-Z_]', '_', name.strip())
|
|
26
|
+
if sanitized and not sanitized[0].isalpha():
|
|
27
|
+
sanitized = f"_{sanitized}"
|
|
28
|
+
return sanitized
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def sanitize_operation_class(name: str) -> str:
|
|
32
|
+
return re.sub(r'[^0-9a-zA-Z_]', '', name[0].upper() + name[1:] + "Operation")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def schema_to_type(schema) -> str:
|
|
36
|
+
"""Convert an OpenAPI schema to a Python type hint."""
|
|
37
|
+
if not schema:
|
|
38
|
+
return "Any"
|
|
39
|
+
schema_type = getattr(schema, "type", None)
|
|
40
|
+
if schema_type == "array":
|
|
41
|
+
items_schema = getattr(schema, "items", None)
|
|
42
|
+
inner = schema_to_type(items_schema)
|
|
43
|
+
return f"list[{inner}]"
|
|
44
|
+
if schema_type == "object":
|
|
45
|
+
return "dict[str, Any]"
|
|
46
|
+
return TYPE_MAP.get(schema_type, "Any")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Command(BaseCommand):
|
|
50
|
+
help = "Generate ESI Stubs from the current ESI spec with correct type hints."
|
|
51
|
+
|
|
52
|
+
def add_arguments(self, parser):
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--output",
|
|
55
|
+
default=None,
|
|
56
|
+
help="Custom output path for the generated ESI stub file (default: stubs.pyi next to openapi_clients.py).",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--compatibility_date",
|
|
60
|
+
default=__build_date__,
|
|
61
|
+
help="Compatibility Date to build ESI Stubs from.",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def handle(self, *args, **options):
|
|
65
|
+
self.stdout.write("Starting ESI stub generation...")
|
|
66
|
+
|
|
67
|
+
headers = {
|
|
68
|
+
"User-Agent": f"{__title__}/{__version__} (+{__url__})",
|
|
69
|
+
"X-Tenant": "tranquility",
|
|
70
|
+
"X-Compatibility-Date": options["compatibility_date"]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
def session_factory(**kwargs) -> Client:
|
|
74
|
+
kwargs.pop("headers", None)
|
|
75
|
+
return Client(
|
|
76
|
+
headers=headers,
|
|
77
|
+
timeout=Timeout(
|
|
78
|
+
connect=app_settings.ESI_REQUESTS_CONNECT_TIMEOUT,
|
|
79
|
+
read=app_settings.ESI_REQUESTS_READ_TIMEOUT,
|
|
80
|
+
write=app_settings.ESI_REQUESTS_WRITE_TIMEOUT,
|
|
81
|
+
pool=app_settings.ESI_REQUESTS_POOL_TIMEOUT
|
|
82
|
+
),
|
|
83
|
+
**kwargs
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
spec_url = f"{app_settings.ESI_API_URL}meta/openapi.json"
|
|
87
|
+
stub_api = OpenAPI.load_sync(
|
|
88
|
+
url=spec_url,
|
|
89
|
+
session_factory=session_factory,
|
|
90
|
+
use_operation_tags=True,
|
|
91
|
+
plugins=[PatchCompatibilityDatePlugin()],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
base_path = os.path.dirname(stubs.__file__)
|
|
95
|
+
output_path = options["output"] or os.path.join(base_path, "stubs.pyi")
|
|
96
|
+
|
|
97
|
+
self.stdout.write(f"Outputting to: {output_path}")
|
|
98
|
+
|
|
99
|
+
spec_root = stub_api._root
|
|
100
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
101
|
+
# File headers
|
|
102
|
+
f.write("# flake8: noqa=E501\n")
|
|
103
|
+
f.write("# Auto Generated do not edit\n")
|
|
104
|
+
# Python Imports
|
|
105
|
+
f.write("from typing import Any, Optional\n\n")
|
|
106
|
+
f.write("from esi.openapi_clients import EsiOperation\n")
|
|
107
|
+
f.write("from esi.models import Token\n\n\n")
|
|
108
|
+
|
|
109
|
+
operation_classes = {}
|
|
110
|
+
|
|
111
|
+
for tag in sorted(stub_api._operationindex._tags.keys()):
|
|
112
|
+
# ESI Operation
|
|
113
|
+
# The various methods called on an ESI Operation
|
|
114
|
+
# result(), Results(), Results_Localized() etc. all live here
|
|
115
|
+
ops = stub_api._operationindex._tags[tag]
|
|
116
|
+
for nm, op in sorted(ops._operations.items()):
|
|
117
|
+
op_obj = op[2]
|
|
118
|
+
docstring = (op_obj.description or op_obj.summary or "").replace("\n", " ").strip()
|
|
119
|
+
op_class_name = sanitize_operation_class(nm)
|
|
120
|
+
|
|
121
|
+
response_type = "Any"
|
|
122
|
+
try:
|
|
123
|
+
resp_200 = op_obj.responses.get("200")
|
|
124
|
+
if resp_200 and "application/json" in resp_200.content:
|
|
125
|
+
response_type = schema_to_type(resp_200.content["application/json"].schema_)
|
|
126
|
+
except Exception:
|
|
127
|
+
response_type = "Any"
|
|
128
|
+
|
|
129
|
+
results_type = response_type if response_type.startswith("list[") else f"list[{response_type}]"
|
|
130
|
+
|
|
131
|
+
if op_class_name not in operation_classes:
|
|
132
|
+
f.write(f"class {op_class_name}(EsiOperation):\n")
|
|
133
|
+
f.write(" \"\"\"EsiOperation, use result(), results() or results_localized()\"\"\"\n")
|
|
134
|
+
|
|
135
|
+
# result()
|
|
136
|
+
f.write(f" def result(self, etag: str | None = None, return_response: bool = False, use_cache: bool = True, **extra) -> {response_type}:\n") # noqa: E501
|
|
137
|
+
f.write(f" \"\"\"{docstring}\"\"\"\n") if docstring else None
|
|
138
|
+
f.write(" ...\n\n")
|
|
139
|
+
|
|
140
|
+
# results()
|
|
141
|
+
f.write(f" def results(self, etag: str | None = None, return_response: bool = False, use_cache: bool = True, **extra) -> {results_type}:\n") # noqa: E501
|
|
142
|
+
f.write(f" \"\"\"{docstring}\"\"\"\n") if docstring else None
|
|
143
|
+
f.write(" ...\n\n")
|
|
144
|
+
|
|
145
|
+
# results_localized()
|
|
146
|
+
f.write(f" def results_localized(self, languages: str | list[str] = 'en', **kwargs) -> {results_type}:\n")
|
|
147
|
+
f.write(f" \"\"\"{docstring}\"\"\"\n") if docstring else None
|
|
148
|
+
f.write(" ...\n\n\n")
|
|
149
|
+
|
|
150
|
+
operation_classes[op_class_name] = True
|
|
151
|
+
|
|
152
|
+
f.write("class ESIClientStub:\n")
|
|
153
|
+
for tag in sorted(stub_api._operationindex._tags.keys()):
|
|
154
|
+
# ESI ESIClientStub
|
|
155
|
+
# The various ESI Tags and Operations
|
|
156
|
+
ops = stub_api._operationindex._tags[tag]
|
|
157
|
+
class_name = f"_{sanitize_class_name(tag)}"
|
|
158
|
+
f.write(f" class {class_name}:\n")
|
|
159
|
+
for nm, op in sorted(ops._operations.items()):
|
|
160
|
+
op_obj = op[2]
|
|
161
|
+
effective_security = (
|
|
162
|
+
getattr(op_obj, "security", None) or getattr(spec_root, "security", None)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def _has_oauth2(sec) -> bool:
|
|
166
|
+
data = sec if isinstance(sec, dict) else getattr(sec, "root", None)
|
|
167
|
+
if not isinstance(data, dict):
|
|
168
|
+
return False
|
|
169
|
+
return any(k.lower() == "oauth2" for k in data)
|
|
170
|
+
|
|
171
|
+
needs_oauth = any(_has_oauth2(s) for s in (effective_security or []))
|
|
172
|
+
|
|
173
|
+
params = ["self"]
|
|
174
|
+
optional_params = []
|
|
175
|
+
for p in getattr(op_obj, "parameters", []):
|
|
176
|
+
required = getattr(p, "required", False)
|
|
177
|
+
schema_type_value = getattr(getattr(p, "schema_", None), "type", None)
|
|
178
|
+
if isinstance(schema_type_value, str):
|
|
179
|
+
param_type = TYPE_MAP.get(schema_type_value, "Any")
|
|
180
|
+
else:
|
|
181
|
+
param_type = "Any"
|
|
182
|
+
default = ""
|
|
183
|
+
if not required:
|
|
184
|
+
param_type = f"Optional[{param_type}]"
|
|
185
|
+
default = " = ..."
|
|
186
|
+
param_name = p.name.replace("-", "_")
|
|
187
|
+
if param_name == "authorization" and needs_oauth:
|
|
188
|
+
# Skip the Authorization Parameter, we inject this at HTTP Header Level
|
|
189
|
+
continue
|
|
190
|
+
if required:
|
|
191
|
+
params.append(f"{param_name}: {param_type}{default}")
|
|
192
|
+
else:
|
|
193
|
+
optional_params.append(f"{param_name}: {param_type}{default}")
|
|
194
|
+
if needs_oauth:
|
|
195
|
+
# Here, we add our own custom param instead of the earlier Authorization
|
|
196
|
+
# Our library will pick this up and use it later
|
|
197
|
+
params.append("token: Token")
|
|
198
|
+
optional_params.append("**kwargs: Any")
|
|
199
|
+
params_str = ", ".join(params + optional_params)
|
|
200
|
+
op_class_name = sanitize_operation_class(nm)
|
|
201
|
+
docstring = (op_obj.description or op_obj.summary or "").replace("\n", " ").strip()
|
|
202
|
+
|
|
203
|
+
if docstring:
|
|
204
|
+
f.write(f" def {nm}({params_str}) -> {op_class_name}:\n")
|
|
205
|
+
f.write(f" \"\"\"{docstring}\"\"\"\n")
|
|
206
|
+
f.write(" ...\n\n")
|
|
207
|
+
else:
|
|
208
|
+
f.write(f" def {nm}({params_str}) -> {op_class_name}: ...\n")
|
|
209
|
+
f.write(f"\n {sanitize_class_name(tag)}: {class_name} = {class_name}()\n\n")
|
|
210
|
+
|
|
211
|
+
self.stdout.write(self.style.SUCCESS(f"ESI stubs written to {output_path}"))
|