django-esi 8.0.0a4__tar.gz → 8.0.0b2__tar.gz
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.0a4 → django_esi-8.0.0b2}/PKG-INFO +5 -3
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/__init__.py +1 -1
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/aiopenapi3/plugins.py +99 -3
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/clients.py +56 -7
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/decorators.py +26 -10
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/exceptions.py +7 -3
- django_esi-8.0.0b2/esi/helpers.py +63 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
- {django_esi-8.0.0a4/esi/locale/pl_PL → django_esi-8.0.0b2/esi/locale/cs_CZ}/LC_MESSAGES/django.po +2 -2
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/de/LC_MESSAGES/django.mo +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/de/LC_MESSAGES/django.po +2 -2
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/en/LC_MESSAGES/django.mo +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/en/LC_MESSAGES/django.po +2 -2
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/es/LC_MESSAGES/django.mo +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/es/LC_MESSAGES/django.po +2 -2
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/fr_FR/LC_MESSAGES/django.mo +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/fr_FR/LC_MESSAGES/django.po +2 -2
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/it_IT/LC_MESSAGES/django.mo +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/it_IT/LC_MESSAGES/django.po +2 -2
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/ja/LC_MESSAGES/django.mo +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/ja/LC_MESSAGES/django.po +2 -2
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/ko_KR/LC_MESSAGES/django.mo +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/ko_KR/LC_MESSAGES/django.po +2 -2
- {django_esi-8.0.0a4/esi/locale/pl_PL → django_esi-8.0.0b2/esi/locale/nl_NL}/LC_MESSAGES/django.mo +0 -0
- {django_esi-8.0.0a4/esi/locale/cs_CZ → django_esi-8.0.0b2/esi/locale/nl_NL}/LC_MESSAGES/django.po +2 -2
- {django_esi-8.0.0a4/esi/locale/nl_NL → django_esi-8.0.0b2/esi/locale/pl_PL}/LC_MESSAGES/django.mo +0 -0
- {django_esi-8.0.0a4/esi/locale/nl_NL → django_esi-8.0.0b2/esi/locale/pl_PL}/LC_MESSAGES/django.po +2 -2
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/ru/LC_MESSAGES/django.mo +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/ru/LC_MESSAGES/django.po +2 -2
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/sk/LC_MESSAGES/django.mo +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/sk/LC_MESSAGES/django.po +2 -2
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/uk/LC_MESSAGES/django.mo +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/uk/LC_MESSAGES/django.po +2 -2
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/locale/zh_Hans/LC_MESSAGES/django.po +2 -2
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/managers.pyi +3 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/openapi_clients.py +188 -44
- django_esi-8.0.0b2/esi/rate_limiting.py +107 -0
- django_esi-8.0.0b2/esi/signals.py +21 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/stubs.pyi +9 -9
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/__init__.py +33 -11
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/test_clients.py +77 -19
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/test_decorators.py +61 -1
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/test_openapi.json +65 -2
- django_esi-8.0.0b2/esi/tests/test_openapi.py +755 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/pyproject.toml +4 -2
- django_esi-8.0.0a4/esi/helpers.py +0 -25
- django_esi-8.0.0a4/esi/rate_limiting.py +0 -78
- django_esi-8.0.0a4/esi/tests/test_openapi.py +0 -261
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/LICENSE +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/README.md +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/admin.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/app_settings.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/apps.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/checks.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/errors.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/management/commands/__init__.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/management/commands/generate_esi_stubs.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/management/commands/migrate_to_ssov2.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/managers.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/migrations/0001_initial.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/migrations/0002_scopes_20161208.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/migrations/0003_hide_tokens_from_admin_site.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/migrations/0004_remove_unique_access_token.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/migrations/0005_remove_token_length_limit.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/migrations/0006_remove_url_length_limit.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/migrations/0007_fix_mysql_8_migration.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/migrations/0008_nullable_refresh_token.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/migrations/0009_set_old_tokens_to_sso_v1.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/migrations/0010_set_new_tokens_to_sso_v2.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/migrations/0011_add_token_indices.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/migrations/0012_fix_token_type_choices.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/migrations/0013_squashed_0012_fix_token_type_choices.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/migrations/__init__.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/models.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/static/esi/img/EVE_SSO_Login_Buttons_Large_Black.png +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/static/esi/img/EVE_SSO_Login_Buttons_Large_White.png +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/static/esi/img/EVE_SSO_Login_Buttons_Small_Black.png +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/static/esi/img/EVE_SSO_Login_Buttons_Small_White.png +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/stubs.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tasks.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/templates/esi/select_token.html +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/templatetags/__init__.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/templatetags/scope_tags.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/client_authed_pilot.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/client_public_pilot.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/factories.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/factories_2.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/jwt_factory.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/test_checks.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/test_management_command.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/test_managers.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/test_models.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/test_swagger.json +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/test_swagger_full.json +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/test_tasks.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/test_templatetags.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/test_views.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/tests/threading_pilot.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/urls.py +0 -0
- {django_esi-8.0.0a4 → django_esi-8.0.0b2}/esi/views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-esi
|
|
3
|
-
Version: 8.0.
|
|
3
|
+
Version: 8.0.0b2
|
|
4
4
|
Summary: Django app for accessing the EVE Swagger Interface (ESI).
|
|
5
5
|
Keywords: eveonline
|
|
6
6
|
Author-email: Alliance Auth <adarnof@gmail.com>
|
|
@@ -9,7 +9,6 @@ Description-Content-Type: text/markdown
|
|
|
9
9
|
Classifier: Environment :: Web Environment
|
|
10
10
|
Classifier: Framework :: Django
|
|
11
11
|
Classifier: Framework :: Django :: 4.2
|
|
12
|
-
Classifier: Framework :: Django :: 5.1
|
|
13
12
|
Classifier: Framework :: Django :: 5.2
|
|
14
13
|
Classifier: Intended Audience :: Developers
|
|
15
14
|
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
@@ -20,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
23
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
24
24
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
25
25
|
License-File: LICENSE
|
|
@@ -27,8 +27,10 @@ Requires-Dist: aiopenapi3
|
|
|
27
27
|
Requires-Dist: bravado>=10.6,<12
|
|
28
28
|
Requires-Dist: celery>=4.0.2
|
|
29
29
|
Requires-Dist: django>=4.2,<6
|
|
30
|
-
Requires-Dist:
|
|
30
|
+
Requires-Dist: django-redis>=5.2
|
|
31
|
+
Requires-Dist: httpx[brotli, http2, zstd]
|
|
31
32
|
Requires-Dist: jsonschema<4
|
|
33
|
+
Requires-Dist: pydantic>=2.12.3
|
|
32
34
|
Requires-Dist: python-jose>=3.3
|
|
33
35
|
Requires-Dist: requests>=2.26
|
|
34
36
|
Requires-Dist: requests-oauthlib>=0.8
|
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from collections.abc import Generator
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
|
|
1
7
|
from aiopenapi3.plugin import Document, Init
|
|
2
8
|
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
3
12
|
class Trim204ContentType(Document):
|
|
4
13
|
"""
|
|
5
14
|
Removes and content-type from responses on a 204 reponses
|
|
6
15
|
A 204 never has content...
|
|
7
16
|
"""
|
|
17
|
+
|
|
8
18
|
def parsed(self, ctx: Document.Context) -> Document.Context:
|
|
9
19
|
spec = ctx.document
|
|
10
20
|
# Patch all paths
|
|
@@ -18,12 +28,95 @@ class Trim204ContentType(Document):
|
|
|
18
28
|
return ctx
|
|
19
29
|
|
|
20
30
|
|
|
31
|
+
def find_refs_recursively(data: Any, parent: Any = None) -> Generator[Any, None, None]:
|
|
32
|
+
"""
|
|
33
|
+
Recursively searches for all instances of "#ref" in a dict+children and returns schemas.
|
|
34
|
+
"""
|
|
35
|
+
if isinstance(data, dict):
|
|
36
|
+
for key, value in data.items():
|
|
37
|
+
if key == "$ref":
|
|
38
|
+
if "#/components/schemas/" in value:
|
|
39
|
+
next = value.split("/")[-1]
|
|
40
|
+
yield next
|
|
41
|
+
if parent:
|
|
42
|
+
yield from find_refs_recursively(parent[next], parent)
|
|
43
|
+
yield from find_refs_recursively(value, parent)
|
|
44
|
+
elif isinstance(data, list):
|
|
45
|
+
for item in data:
|
|
46
|
+
yield from find_refs_recursively(item, parent)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class MinifySpec(Document):
|
|
50
|
+
"""
|
|
51
|
+
Removes operations and schemas from spec to limit memory spam
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, tags: list[str], operations: list[str]):
|
|
55
|
+
super().__init__()
|
|
56
|
+
self.keep_tags = set(tags)
|
|
57
|
+
self.keep_ops = operations
|
|
58
|
+
|
|
59
|
+
def parsed(self, ctx: Document.Context) -> Document.Context:
|
|
60
|
+
|
|
61
|
+
if len(self.keep_tags) == 0 and self.keep_ops == []:
|
|
62
|
+
logger.error("No tag/path filtering supplied to ESI Client. Using all tags. This throw an error with `DEBUG=False`!", stack_info=True)
|
|
63
|
+
if not getattr(settings, "DEBUG", False):
|
|
64
|
+
# we are in production mode throw error to protect RAM.
|
|
65
|
+
raise AttributeError("No tag/path filtering supplied to ESI Client.")
|
|
66
|
+
# We are in debug mode allow an unfiltered client.
|
|
67
|
+
return ctx
|
|
68
|
+
|
|
69
|
+
# filter the client.
|
|
70
|
+
spec = ctx.document
|
|
71
|
+
|
|
72
|
+
remove_paths = set()
|
|
73
|
+
keep_schema = set()
|
|
74
|
+
logger.debug("Filtering Paths/Tags: ")
|
|
75
|
+
for name, path_item in spec.get("paths", {}).items():
|
|
76
|
+
keep = False
|
|
77
|
+
for method_name in ("get", "post", "put", "delete", "patch", "options", "head"):
|
|
78
|
+
method = path_item.get(method_name)
|
|
79
|
+
if not method:
|
|
80
|
+
continue
|
|
81
|
+
if len(self.keep_tags.intersection(method['tags'])) or method["operationId"] in self.keep_ops:
|
|
82
|
+
keep = True
|
|
83
|
+
schemas = find_refs_recursively(
|
|
84
|
+
path_item,
|
|
85
|
+
spec["components"]["schemas"] # find all sub schema's
|
|
86
|
+
)
|
|
87
|
+
for s in schemas:
|
|
88
|
+
keep_schema.add(s)
|
|
89
|
+
|
|
90
|
+
if not keep:
|
|
91
|
+
remove_paths.add(name)
|
|
92
|
+
else:
|
|
93
|
+
logger.debug(f" - {name}")
|
|
94
|
+
|
|
95
|
+
# remove the paths we don't care for
|
|
96
|
+
for name in remove_paths:
|
|
97
|
+
spec["paths"].pop(name)
|
|
98
|
+
|
|
99
|
+
# build new schema from what we need
|
|
100
|
+
logger.debug("Rebuilding Schema: ")
|
|
101
|
+
new_schema = {}
|
|
102
|
+
for name, data in spec["components"]["schemas"].items():
|
|
103
|
+
if name in keep_schema:
|
|
104
|
+
logger.debug(f" - {name}")
|
|
105
|
+
new_schema[name] = data
|
|
106
|
+
|
|
107
|
+
# replace schemas with the new schema
|
|
108
|
+
ctx.document["components"]["schemas"] = new_schema
|
|
109
|
+
|
|
110
|
+
return ctx
|
|
111
|
+
|
|
112
|
+
|
|
21
113
|
class Add304ContentType(Document):
|
|
22
114
|
"""
|
|
23
115
|
Adds 304 content-type to responses
|
|
24
116
|
A 304 never has content. ESI defualt has application/json
|
|
25
117
|
This is a hack for now
|
|
26
118
|
"""
|
|
119
|
+
|
|
27
120
|
def parsed(self, ctx: Document.Context) -> Document.Context:
|
|
28
121
|
spec = ctx.document
|
|
29
122
|
# Patch all paths
|
|
@@ -33,7 +126,7 @@ class Add304ContentType(Document):
|
|
|
33
126
|
if not method:
|
|
34
127
|
continue
|
|
35
128
|
if "304" not in method['responses']:
|
|
36
|
-
method['responses']["304"]={
|
|
129
|
+
method['responses']["304"] = {
|
|
37
130
|
"description": "Not Modified"
|
|
38
131
|
}
|
|
39
132
|
return ctx
|
|
@@ -43,6 +136,7 @@ class RemoveSecurityParameter(Document):
|
|
|
43
136
|
"""
|
|
44
137
|
Removes the whole OAuth2 securityScheme
|
|
45
138
|
"""
|
|
139
|
+
|
|
46
140
|
def parsed(self, ctx: Document.Context) -> Document.Context:
|
|
47
141
|
print("RemoveSecurityParameterPlugin: Removing OAuth2 securityScheme")
|
|
48
142
|
spec = ctx.document
|
|
@@ -63,6 +157,7 @@ class TrimSecurityParameter(Document):
|
|
|
63
157
|
Trims out of Spec OAuth2 attributes. CCP have fixed this.
|
|
64
158
|
Leaving in place in case we need a quick reference again.
|
|
65
159
|
"""
|
|
160
|
+
|
|
66
161
|
def parsed(self, ctx: Document.Context) -> Document.Context:
|
|
67
162
|
print("TrimSecurityParameter: Trimming out of spec attributes")
|
|
68
163
|
spec = ctx.document
|
|
@@ -80,8 +175,9 @@ class PatchCompatibilityDatePlugin(Document):
|
|
|
80
175
|
This is because WE specifically add it in the library to the HTTP requests,
|
|
81
176
|
but without this, it will be a required parameter during request generation before it hits the HTTP library.
|
|
82
177
|
"""
|
|
178
|
+
|
|
83
179
|
def parsed(self, ctx: Document.Context) -> Document.Context:
|
|
84
|
-
|
|
180
|
+
logger.debug("PatchCompatibilityDatePlugin: making compatibility date optional")
|
|
85
181
|
spec = ctx.document
|
|
86
182
|
|
|
87
183
|
def patch_param(param):
|
|
@@ -124,5 +220,5 @@ class DjangoESIInit(Init):
|
|
|
124
220
|
|
|
125
221
|
def initialized(self, ctx: Init.Context) -> Init.Context:
|
|
126
222
|
# Force the app_name into the api client class for etags
|
|
127
|
-
self.api.app_name=self.app_name
|
|
223
|
+
self.api.app_name = self.app_name
|
|
128
224
|
return ctx # noqa
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
+
from timeit import default_timer
|
|
3
4
|
import warnings
|
|
4
|
-
|
|
5
|
+
import datetime as dt
|
|
5
6
|
from hashlib import md5
|
|
6
7
|
from time import sleep
|
|
7
8
|
from typing import Any
|
|
@@ -10,7 +11,7 @@ from urllib import parse as urlparse
|
|
|
10
11
|
from bravado import requests_client
|
|
11
12
|
from bravado.client import SwaggerClient
|
|
12
13
|
from bravado.exception import (
|
|
13
|
-
HTTPBadGateway, HTTPGatewayTimeout, HTTPServiceUnavailable,
|
|
14
|
+
HTTPBadGateway, HTTPGatewayTimeout, HTTPServiceUnavailable, HTTPError,
|
|
14
15
|
)
|
|
15
16
|
from bravado.http_future import HttpFuture
|
|
16
17
|
from bravado.swagger_model import Loader
|
|
@@ -22,6 +23,7 @@ from django.core.cache import cache
|
|
|
22
23
|
|
|
23
24
|
from . import __title__, __url__, __version__, app_settings
|
|
24
25
|
from .errors import TokenExpiredError
|
|
26
|
+
from .signals import esi_request_statistics
|
|
25
27
|
|
|
26
28
|
logger = logging.getLogger(__name__)
|
|
27
29
|
|
|
@@ -68,8 +70,10 @@ class CachingHttpFuture(HttpFuture):
|
|
|
68
70
|
seconds until "Expires" time
|
|
69
71
|
"""
|
|
70
72
|
try:
|
|
71
|
-
expires_dt = datetime.strptime(str(expires), '%a, %d %b %Y %H:%M:%S %Z')
|
|
72
|
-
|
|
73
|
+
expires_dt = dt.datetime.strptime(str(expires), '%a, %d %b %Y %H:%M:%S %Z')
|
|
74
|
+
if expires_dt.tzinfo is None:
|
|
75
|
+
expires_dt = expires_dt.replace(tzinfo=dt.timezone.utc)
|
|
76
|
+
delta = expires_dt - dt.datetime.now(dt.timezone.utc)
|
|
73
77
|
return delta.total_seconds()
|
|
74
78
|
except ValueError:
|
|
75
79
|
return 0
|
|
@@ -142,6 +146,19 @@ class CachingHttpFuture(HttpFuture):
|
|
|
142
146
|
for language in my_languages
|
|
143
147
|
}
|
|
144
148
|
|
|
149
|
+
def _send_signal(self, status_code: int, headers: dict = {}, latency: float = 0) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Dispatch the esi request statistics signal
|
|
152
|
+
"""
|
|
153
|
+
esi_request_statistics.send(
|
|
154
|
+
sender=self.__class__,
|
|
155
|
+
operation=self.operation.path_name,
|
|
156
|
+
status_code=status_code,
|
|
157
|
+
headers=headers,
|
|
158
|
+
latency=latency,
|
|
159
|
+
bucket=""
|
|
160
|
+
)
|
|
161
|
+
|
|
145
162
|
def result(self, **kwargs) -> Any | tuple[Any, IncomingResponse]:
|
|
146
163
|
"""Executes the request and returns the response from ESI. Response will
|
|
147
164
|
include the requested / first page only if there are more pages available.
|
|
@@ -189,6 +206,9 @@ class CachingHttpFuture(HttpFuture):
|
|
|
189
206
|
)
|
|
190
207
|
|
|
191
208
|
if cached:
|
|
209
|
+
self._send_signal(
|
|
210
|
+
status_code=0
|
|
211
|
+
)
|
|
192
212
|
result, response = cached
|
|
193
213
|
expiry = self._time_to_expiry(str(response.headers.get('Expires')))
|
|
194
214
|
if expiry < 0:
|
|
@@ -243,6 +263,7 @@ class CachingHttpFuture(HttpFuture):
|
|
|
243
263
|
|
|
244
264
|
retries = 0
|
|
245
265
|
while retries <= max_retries:
|
|
266
|
+
_t = default_timer()
|
|
246
267
|
try:
|
|
247
268
|
if app_settings.ESI_INFO_LOGGING_ENABLED:
|
|
248
269
|
params = self.future.request.params
|
|
@@ -267,6 +288,11 @@ class CachingHttpFuture(HttpFuture):
|
|
|
267
288
|
logger.debug('ESI response content: %s', response.text)
|
|
268
289
|
break
|
|
269
290
|
except (HTTPBadGateway, HTTPGatewayTimeout, HTTPServiceUnavailable) as ex:
|
|
291
|
+
self._send_signal(
|
|
292
|
+
status_code=ex.status_code,
|
|
293
|
+
headers=ex.response.headers,
|
|
294
|
+
latency=default_timer() - _t
|
|
295
|
+
)
|
|
270
296
|
if retries < max_retries:
|
|
271
297
|
retries += 1
|
|
272
298
|
logger.warning(
|
|
@@ -283,7 +309,23 @@ class CachingHttpFuture(HttpFuture):
|
|
|
283
309
|
sleep(wait_secs)
|
|
284
310
|
else:
|
|
285
311
|
raise ex
|
|
312
|
+
except HTTPError as ex:
|
|
313
|
+
"""
|
|
314
|
+
Throw any other error into the signal
|
|
315
|
+
then just re-raise
|
|
316
|
+
"""
|
|
317
|
+
self._send_signal(
|
|
318
|
+
status_code=ex.status_code,
|
|
319
|
+
headers=ex.response.headers,
|
|
320
|
+
latency=default_timer() - _t
|
|
321
|
+
)
|
|
322
|
+
raise ex
|
|
286
323
|
|
|
324
|
+
self._send_signal(
|
|
325
|
+
status_code=response.status_code,
|
|
326
|
+
headers=response.headers,
|
|
327
|
+
latency=default_timer() - _t
|
|
328
|
+
)
|
|
287
329
|
# restore original value
|
|
288
330
|
self.request_config.also_return_response = _also_return_response
|
|
289
331
|
return result, response
|
|
@@ -482,19 +524,26 @@ def esi_client_factory(
|
|
|
482
524
|
|
|
483
525
|
client = RequestsClientPlus()
|
|
484
526
|
|
|
527
|
+
from esi.helpers import pascal_case_string
|
|
528
|
+
sanitized_appname = pascal_case_string(__title__)
|
|
529
|
+
|
|
485
530
|
if app_info_text:
|
|
486
531
|
# app_info_text (email@example) Django-ESI/1.2.3 (+https://gitlab.com/allianceauth/django-esi)
|
|
487
532
|
# Deprecated
|
|
488
|
-
user_agent = f"{app_info_text} ({app_settings.ESI_USER_CONTACT_EMAIL}) {
|
|
533
|
+
user_agent = f"{app_info_text} ({app_settings.ESI_USER_CONTACT_EMAIL}) {sanitized_appname}/{__version__} (+{__url__})"
|
|
489
534
|
elif ua_appname is None or ua_version is None:
|
|
490
535
|
# Django-ESI/1.2.3 () (email@example; +https://gitlab.com/allianceauth/django-esi)
|
|
491
536
|
# Deprecated
|
|
492
|
-
user_agent = f"{
|
|
537
|
+
user_agent = f"{sanitized_appname}/{__version__} ({app_settings.ESI_USER_CONTACT_EMAIL}; +{__url__})"
|
|
493
538
|
else:
|
|
494
539
|
# AppName/1.2.3 (email@example.com) Django-ESI/1.2.3 (+https://gitlab.com/allianceauth/django-esi)
|
|
495
540
|
# or AppName/1.2.3 (email@example.com; +https://gitlab.com/) Django-ESI/1.2.3 (+https://gitlab.com/allianceauth/django-esi) (+https://gitlab.com/allianceauth/django-esi)
|
|
496
541
|
# Preferred
|
|
497
|
-
|
|
542
|
+
|
|
543
|
+
# Enforce PascalCase for `ua_appname` and strip whitespace
|
|
544
|
+
sanitized_ua_appname = pascal_case_string(ua_appname)
|
|
545
|
+
|
|
546
|
+
user_agent = f"{sanitized_ua_appname}/{ua_version} ({app_settings.ESI_USER_CONTACT_EMAIL}{f'; +{ua_url})' if ua_url else ')'} {sanitized_appname}/{__version__} (+{__url__})"
|
|
498
547
|
|
|
499
548
|
client.user_agent = user_agent
|
|
500
549
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import functools
|
|
2
1
|
import logging
|
|
3
2
|
import time
|
|
4
3
|
from functools import wraps
|
|
4
|
+
from typing import Any
|
|
5
|
+
from collections.abc import Callable
|
|
5
6
|
|
|
6
7
|
from django.core.cache import cache
|
|
7
8
|
|
|
@@ -227,12 +228,20 @@ def single_use_token(scopes='', new=False):
|
|
|
227
228
|
return decorator
|
|
228
229
|
|
|
229
230
|
|
|
230
|
-
def wait_for_esi_errorlimit_reset(cache_key="esi_error_limit_reset", poll_interval=1):
|
|
231
|
+
def wait_for_esi_errorlimit_reset(cache_key="esi_error_limit_reset", poll_interval=1) -> Callable[..., Callable[..., Any]]:
|
|
232
|
+
"""
|
|
233
|
+
Decorator to apply a polling sleep while the ESI Server/Client is in an Error Limit state
|
|
234
|
+
The preferred non-blocking method is to retry your tasks after the limit reset time has passed
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
cache_key (str, optional): NOT USUALLY CHANGED. Defaults to "esi_error_limit_reset".
|
|
238
|
+
poll_interval (int, optional): Interval in seconds to poll redis. Defaults to 1.
|
|
239
|
+
"""
|
|
231
240
|
def decorator(func):
|
|
232
241
|
def wrapper(*args, **kwargs):
|
|
233
242
|
reset = cache.get(cache_key)
|
|
234
243
|
if reset is not None:
|
|
235
|
-
|
|
244
|
+
logger.error(f"ESI Error Limited, waiting {reset}s before retrying...")
|
|
236
245
|
while cache.get(cache_key):
|
|
237
246
|
time.sleep(poll_interval)
|
|
238
247
|
return func(*args, **kwargs)
|
|
@@ -240,16 +249,23 @@ def wait_for_esi_errorlimit_reset(cache_key="esi_error_limit_reset", poll_interv
|
|
|
240
249
|
return decorator
|
|
241
250
|
|
|
242
251
|
|
|
243
|
-
def esi_rate_limiter_bucketed(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
252
|
+
def esi_rate_limiter_bucketed(bucket: ESIRateLimitBucket, raise_on_limit: bool = True):
|
|
253
|
+
"""
|
|
254
|
+
Decorator for custom manual rate limits on some endpoints to apply a polling sleep while the bucket is exhausted.
|
|
255
|
+
MARKET_DATA_HISTORY
|
|
256
|
+
CHARACTER_CORPORATION_HISTORY
|
|
257
|
+
The preferred non-blocking method is to retry your tasks after the limit reset time has passed
|
|
258
|
+
|
|
248
259
|
|
|
260
|
+
Args:
|
|
261
|
+
bucket (ESIRateLimitBucket): The Bucket to rate limit against
|
|
262
|
+
raise_on_limit (bool, optional): Whether to raise an Exception when the limit is reached. Defaults to True.
|
|
263
|
+
"""
|
|
264
|
+
# TODO Investigate esi cache hits.
|
|
249
265
|
def decorator(func):
|
|
250
|
-
@
|
|
266
|
+
@wraps(func)
|
|
251
267
|
def wrapper(*args, **kwargs):
|
|
252
|
-
ESIRateLimits.
|
|
268
|
+
ESIRateLimits.check_decr_bucket(bucket, raise_on_limit)
|
|
253
269
|
return func(*args, **kwargs)
|
|
254
270
|
return wrapper
|
|
255
271
|
return decorator
|
|
@@ -4,10 +4,12 @@ from aiopenapi3.errors import HTTPServerError as base_HTTPServerError
|
|
|
4
4
|
from aiopenapi3.errors import HTTPClientError as base_HTTPClientError
|
|
5
5
|
from aiopenapi3.errors import HTTPError
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
class ESIErrorLimitException(Exception):
|
|
8
9
|
"""ESI Global Error Limit Exceeded
|
|
9
10
|
https://developers.eveonline.com/docs/services/esi/best-practices/#error-limit
|
|
10
11
|
"""
|
|
12
|
+
|
|
11
13
|
def __init__(self, reset=None, *args, **kwargs) -> None:
|
|
12
14
|
self.reset = reset
|
|
13
15
|
msg = kwargs.get("message") or (
|
|
@@ -18,8 +20,10 @@ class ESIErrorLimitException(Exception):
|
|
|
18
20
|
|
|
19
21
|
class ESIBucketLimitException(Exception):
|
|
20
22
|
"""Endpoint (Bucket) Specific Rate Limit Exceeded"""
|
|
21
|
-
|
|
23
|
+
|
|
24
|
+
def __init__(self, bucket, reset: float = 0, *args, **kwargs) -> None:
|
|
22
25
|
self.bucket = bucket
|
|
26
|
+
self.reset = reset
|
|
23
27
|
msg = kwargs.get("message") or f"ESI bucket limit reached for {bucket}."
|
|
24
28
|
super().__init__(msg, *args)
|
|
25
29
|
|
|
@@ -37,11 +41,11 @@ class HTTPNotModified(HTTPError):
|
|
|
37
41
|
|
|
38
42
|
@dataclasses.dataclass(repr=False)
|
|
39
43
|
class HTTPClientError(base_HTTPClientError):
|
|
40
|
-
"""
|
|
44
|
+
"""HTTP Response Code 4xx"""
|
|
41
45
|
pass
|
|
42
46
|
|
|
43
47
|
|
|
44
48
|
@dataclasses.dataclass(repr=False)
|
|
45
49
|
class HTTPServerError(base_HTTPServerError):
|
|
46
|
-
"""
|
|
50
|
+
"""HTTP Response Code 5xx"""
|
|
47
51
|
pass
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from esi.models import Token
|
|
2
|
+
from string import capwords
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_token(character_id: int, scopes: list) -> Token:
|
|
6
|
+
"""Helper method to get a valid token for a specific character with specific scopes.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
character_id: Character to filter on.
|
|
10
|
+
scopes: array of ESI scope strings to search for.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
Matching Token
|
|
14
|
+
"""
|
|
15
|
+
qs = (
|
|
16
|
+
Token.objects
|
|
17
|
+
.filter(character_id=character_id)
|
|
18
|
+
.require_scopes(scopes)
|
|
19
|
+
.require_valid()
|
|
20
|
+
)
|
|
21
|
+
token = qs.first()
|
|
22
|
+
if token is None:
|
|
23
|
+
raise Token.DoesNotExist(
|
|
24
|
+
f"No valid token found for character_id={character_id} with required scopes."
|
|
25
|
+
)
|
|
26
|
+
return token
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def pascal_case_string(string: str) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Convert a string to PascalCase by capitalizing the first letter of each word and removing spaces,
|
|
32
|
+
but only if the string contains spaces or hyphens.
|
|
33
|
+
|
|
34
|
+
This function checks if the input string contains spaces or hyphens. If so, it replaces hyphens with spaces,
|
|
35
|
+
capitalizes the first letter of each word, removes the spaces, and returns the resulting PascalCase string.
|
|
36
|
+
If the input string does not contain spaces or hyphens, it is returned unchanged.
|
|
37
|
+
|
|
38
|
+
Behaviour:
|
|
39
|
+
Any string containing spaces or hyphens will be converted to PascalCase.
|
|
40
|
+
Strings without spaces or hyphens will be returned unchanged.
|
|
41
|
+
This gives you the opportunity to use already formatted strings as needed.
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
- "app name" -> "AppName"
|
|
45
|
+
- "app-name" -> "AppName"
|
|
46
|
+
- "appname" -> "appname"
|
|
47
|
+
- "AppName" -> "AppName"
|
|
48
|
+
- "appName" -> "appName"
|
|
49
|
+
- "app_name" -> "app_name"
|
|
50
|
+
|
|
51
|
+
:param string: The input string to be converted to PascalCase.
|
|
52
|
+
:type string: str
|
|
53
|
+
:return: The PascalCase formatted string, or the original string if no spaces or hyphens are present.
|
|
54
|
+
:rtype: str
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# Check if the string contains spaces or hyphens
|
|
58
|
+
if any(c in string for c in (" ", "-")):
|
|
59
|
+
# Replace hyphens with spaces, capitalize each word, and remove spaces
|
|
60
|
+
return capwords(string.replace("-", " ")).replace(" ", "")
|
|
61
|
+
|
|
62
|
+
# Return the original string if no spaces or hyphens are present
|
|
63
|
+
return string
|
|
Binary file
|
{django_esi-8.0.0a4/esi/locale/pl_PL → django_esi-8.0.0b2/esi/locale/cs_CZ}/LC_MESSAGES/django.po
RENAMED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
#, fuzzy
|
|
7
7
|
msgid ""
|
|
8
8
|
msgstr ""
|
|
9
|
-
"Project-Id-Version: Django ESI 8.0.0-
|
|
9
|
+
"Project-Id-Version: Django ESI 8.0.0-beta.2\n"
|
|
10
10
|
"Report-Msgid-Bugs-To: https://gitlab.com/allianceauth/django-esi/-/issues\n"
|
|
11
|
-
"POT-Creation-Date: 2025-
|
|
11
|
+
"POT-Creation-Date: 2025-10-31 15:18+1000\n"
|
|
12
12
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
13
13
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
14
14
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
Binary file
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
#, fuzzy
|
|
10
10
|
msgid ""
|
|
11
11
|
msgstr ""
|
|
12
|
-
"Project-Id-Version: Django ESI 8.0.0-
|
|
12
|
+
"Project-Id-Version: Django ESI 8.0.0-beta.2\n"
|
|
13
13
|
"Report-Msgid-Bugs-To: https://gitlab.com/allianceauth/django-esi/-/issues\n"
|
|
14
|
-
"POT-Creation-Date: 2025-
|
|
14
|
+
"POT-Creation-Date: 2025-10-31 15:18+1000\n"
|
|
15
15
|
"PO-Revision-Date: 2023-10-25 11:04+0000\n"
|
|
16
16
|
"Last-Translator: Peter Pfeufer, 2023\n"
|
|
17
17
|
"Language-Team: German (https://app.transifex.com/alliance-auth/teams/107430/"
|
|
Binary file
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
#, fuzzy
|
|
7
7
|
msgid ""
|
|
8
8
|
msgstr ""
|
|
9
|
-
"Project-Id-Version: Django ESI 8.0.0-
|
|
9
|
+
"Project-Id-Version: Django ESI 8.0.0-beta.2\n"
|
|
10
10
|
"Report-Msgid-Bugs-To: https://gitlab.com/allianceauth/django-esi/-/issues\n"
|
|
11
|
-
"POT-Creation-Date: 2025-
|
|
11
|
+
"POT-Creation-Date: 2025-10-31 15:18+1000\n"
|
|
12
12
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
13
13
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
14
14
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
Binary file
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
#, fuzzy
|
|
10
10
|
msgid ""
|
|
11
11
|
msgstr ""
|
|
12
|
-
"Project-Id-Version: Django ESI 8.0.0-
|
|
12
|
+
"Project-Id-Version: Django ESI 8.0.0-beta.2\n"
|
|
13
13
|
"Report-Msgid-Bugs-To: https://gitlab.com/allianceauth/django-esi/-/issues\n"
|
|
14
|
-
"POT-Creation-Date: 2025-
|
|
14
|
+
"POT-Creation-Date: 2025-10-31 15:18+1000\n"
|
|
15
15
|
"PO-Revision-Date: 2023-10-25 11:04+0000\n"
|
|
16
16
|
"Last-Translator: trenus, 2023\n"
|
|
17
17
|
"Language-Team: Spanish (https://app.transifex.com/alliance-auth/teams/107430/"
|
|
Binary file
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
#, fuzzy
|
|
10
10
|
msgid ""
|
|
11
11
|
msgstr ""
|
|
12
|
-
"Project-Id-Version: Django ESI 8.0.0-
|
|
12
|
+
"Project-Id-Version: Django ESI 8.0.0-beta.2\n"
|
|
13
13
|
"Report-Msgid-Bugs-To: https://gitlab.com/allianceauth/django-esi/-/issues\n"
|
|
14
|
-
"POT-Creation-Date: 2025-
|
|
14
|
+
"POT-Creation-Date: 2025-10-31 15:18+1000\n"
|
|
15
15
|
"PO-Revision-Date: 2020-12-28 06:44+0000\n"
|
|
16
16
|
"Last-Translator: rockclodbuster, 2023\n"
|
|
17
17
|
"Language-Team: French (France) (https://app.transifex.com/alliance-auth/"
|
|
Binary file
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
#, fuzzy
|
|
10
10
|
msgid ""
|
|
11
11
|
msgstr ""
|
|
12
|
-
"Project-Id-Version: Django ESI 8.0.0-
|
|
12
|
+
"Project-Id-Version: Django ESI 8.0.0-beta.2\n"
|
|
13
13
|
"Report-Msgid-Bugs-To: https://gitlab.com/allianceauth/django-esi/-/issues\n"
|
|
14
|
-
"POT-Creation-Date: 2025-
|
|
14
|
+
"POT-Creation-Date: 2025-10-31 15:18+1000\n"
|
|
15
15
|
"PO-Revision-Date: 2023-10-25 11:04+0000\n"
|
|
16
16
|
"Last-Translator: Thomas Turini, 2024\n"
|
|
17
17
|
"Language-Team: Italian (Italy) (https://app.transifex.com/alliance-auth/"
|
|
Binary file
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
#, fuzzy
|
|
10
10
|
msgid ""
|
|
11
11
|
msgstr ""
|
|
12
|
-
"Project-Id-Version: Django ESI 8.0.0-
|
|
12
|
+
"Project-Id-Version: Django ESI 8.0.0-beta.2\n"
|
|
13
13
|
"Report-Msgid-Bugs-To: https://gitlab.com/allianceauth/django-esi/-/issues\n"
|
|
14
|
-
"POT-Creation-Date: 2025-
|
|
14
|
+
"POT-Creation-Date: 2025-10-31 15:18+1000\n"
|
|
15
15
|
"PO-Revision-Date: 2023-10-25 11:04+0000\n"
|
|
16
16
|
"Last-Translator: kotaneko, 2023\n"
|
|
17
17
|
"Language-Team: Japanese (https://app.transifex.com/alliance-auth/"
|
|
Binary file
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
#, fuzzy
|
|
10
10
|
msgid ""
|
|
11
11
|
msgstr ""
|
|
12
|
-
"Project-Id-Version: Django ESI 8.0.0-
|
|
12
|
+
"Project-Id-Version: Django ESI 8.0.0-beta.2\n"
|
|
13
13
|
"Report-Msgid-Bugs-To: https://gitlab.com/allianceauth/django-esi/-/issues\n"
|
|
14
|
-
"POT-Creation-Date: 2025-
|
|
14
|
+
"POT-Creation-Date: 2025-10-31 15:18+1000\n"
|
|
15
15
|
"PO-Revision-Date: 2023-10-25 11:04+0000\n"
|
|
16
16
|
"Last-Translator: Seowon Jung <seowon@hawaii.edu>, 2023\n"
|
|
17
17
|
"Language-Team: Korean (Korea) (https://app.transifex.com/alliance-auth/"
|
{django_esi-8.0.0a4/esi/locale/pl_PL → django_esi-8.0.0b2/esi/locale/nl_NL}/LC_MESSAGES/django.mo
RENAMED
|
Binary file
|
{django_esi-8.0.0a4/esi/locale/cs_CZ → django_esi-8.0.0b2/esi/locale/nl_NL}/LC_MESSAGES/django.po
RENAMED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
#, fuzzy
|
|
7
7
|
msgid ""
|
|
8
8
|
msgstr ""
|
|
9
|
-
"Project-Id-Version: Django ESI 8.0.0-
|
|
9
|
+
"Project-Id-Version: Django ESI 8.0.0-beta.2\n"
|
|
10
10
|
"Report-Msgid-Bugs-To: https://gitlab.com/allianceauth/django-esi/-/issues\n"
|
|
11
|
-
"POT-Creation-Date: 2025-
|
|
11
|
+
"POT-Creation-Date: 2025-10-31 15:18+1000\n"
|
|
12
12
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
13
13
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
14
14
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|