django-esi 8.0.0a1__tar.gz → 8.0.0a2__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.

Files changed (85) hide show
  1. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/PKG-INFO +3 -4
  2. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/__init__.py +2 -2
  3. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/aiopenapi3/plugins.py +18 -0
  4. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/app_settings.py +1 -1
  5. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/management/commands/generate_esi_stubs.py +33 -16
  6. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/openapi_clients.py +72 -14
  7. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/stubs.pyi +441 -567
  8. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/pyproject.toml +4 -4
  9. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/LICENSE +0 -0
  10. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/README.md +0 -0
  11. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/admin.py +0 -0
  12. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/apps.py +0 -0
  13. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/checks.py +0 -0
  14. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/clients.py +0 -0
  15. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/decorators.py +0 -0
  16. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/errors.py +0 -0
  17. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/exceptions.py +0 -0
  18. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/helpers.py +0 -0
  19. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/de/LC_MESSAGES/django.mo +0 -0
  20. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/de/LC_MESSAGES/django.po +0 -0
  21. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/en/LC_MESSAGES/django.mo +0 -0
  22. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/en/LC_MESSAGES/django.po +0 -0
  23. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/es/LC_MESSAGES/django.mo +0 -0
  24. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/es/LC_MESSAGES/django.po +0 -0
  25. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/fr_FR/LC_MESSAGES/django.mo +0 -0
  26. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/fr_FR/LC_MESSAGES/django.po +0 -0
  27. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/it_IT/LC_MESSAGES/django.mo +0 -0
  28. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/it_IT/LC_MESSAGES/django.po +0 -0
  29. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/ja/LC_MESSAGES/django.mo +0 -0
  30. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/ja/LC_MESSAGES/django.po +0 -0
  31. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/ko_KR/LC_MESSAGES/django.mo +0 -0
  32. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/ko_KR/LC_MESSAGES/django.po +0 -0
  33. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/ru/LC_MESSAGES/django.mo +0 -0
  34. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/ru/LC_MESSAGES/django.po +0 -0
  35. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  36. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/locale/zh_Hans/LC_MESSAGES/django.po +0 -0
  37. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/management/commands/__init__.py +0 -0
  38. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/management/commands/migrate_to_ssov2.py +0 -0
  39. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/managers.py +0 -0
  40. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/managers.pyi +0 -0
  41. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/migrations/0001_initial.py +0 -0
  42. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/migrations/0002_scopes_20161208.py +0 -0
  43. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/migrations/0003_hide_tokens_from_admin_site.py +0 -0
  44. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/migrations/0004_remove_unique_access_token.py +0 -0
  45. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/migrations/0005_remove_token_length_limit.py +0 -0
  46. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/migrations/0006_remove_url_length_limit.py +0 -0
  47. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/migrations/0007_fix_mysql_8_migration.py +0 -0
  48. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/migrations/0008_nullable_refresh_token.py +0 -0
  49. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/migrations/0009_set_old_tokens_to_sso_v1.py +0 -0
  50. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/migrations/0010_set_new_tokens_to_sso_v2.py +0 -0
  51. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/migrations/0011_add_token_indices.py +0 -0
  52. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/migrations/0012_fix_token_type_choices.py +0 -0
  53. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/migrations/0013_squashed_0012_fix_token_type_choices.py +0 -0
  54. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/migrations/__init__.py +0 -0
  55. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/models.py +0 -0
  56. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/rate_limiting.py +0 -0
  57. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/static/esi/img/EVE_SSO_Login_Buttons_Large_Black.png +0 -0
  58. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/static/esi/img/EVE_SSO_Login_Buttons_Large_White.png +0 -0
  59. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/static/esi/img/EVE_SSO_Login_Buttons_Small_Black.png +0 -0
  60. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/static/esi/img/EVE_SSO_Login_Buttons_Small_White.png +0 -0
  61. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/stubs.py +0 -0
  62. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tasks.py +0 -0
  63. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/templates/esi/select_token.html +0 -0
  64. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/templatetags/__init__.py +0 -0
  65. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/templatetags/scope_tags.py +0 -0
  66. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/__init__.py +0 -0
  67. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/client_authed_pilot.py +0 -0
  68. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/client_public_pilot.py +0 -0
  69. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/factories.py +0 -0
  70. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/factories_2.py +0 -0
  71. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/jwt_factory.py +0 -0
  72. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/test_checks.py +0 -0
  73. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/test_clients.py +0 -0
  74. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/test_decorators.py +0 -0
  75. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/test_management_command.py +0 -0
  76. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/test_managers.py +0 -0
  77. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/test_models.py +0 -0
  78. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/test_swagger.json +0 -0
  79. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/test_swagger_full.json +0 -0
  80. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/test_tasks.py +0 -0
  81. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/test_templatetags.py +0 -0
  82. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/test_views.py +0 -0
  83. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/tests/threading_pilot.py +0 -0
  84. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/urls.py +0 -0
  85. {django_esi-8.0.0a1 → django_esi-8.0.0a2}/esi/views.py +0 -0
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-esi
3
- Version: 8.0.0a1
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
@@ -1,6 +1,6 @@
1
1
  """Django app for accessing the EVE Swagger Interface (ESI)."""
2
2
 
3
- __version__ = '8.0.0a1'
3
+ __version__ = '8.0.0a2'
4
4
  __title__ = 'Django-ESI'
5
5
  __url__ = 'https://gitlab.com/allianceauth/django-esi'
6
- __build_date__ = "2025-08-26"
6
+ __build_date__ = "2025-09-03"
@@ -1,6 +1,24 @@
1
1
  from aiopenapi3.plugin import Document
2
2
 
3
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
+
4
22
  class RemoveSecurityParameter(Document):
5
23
  """
6
24
  Removes the whole OAuth2 securityScheme
@@ -108,5 +108,5 @@ ESI_TOKEN_JWT_AUDIENCE = str(getattr(settings, "ESI_TOKEN_JWT_AUDIENCE", "EVE On
108
108
 
109
109
  # list of all official language codes supported by ESI
110
110
  ESI_LANGUAGES = getattr(settings, 'ESI_LANGUAGES', [
111
- 'de', 'en-us', 'fr', 'ja', 'ru', 'zh', 'ko'
111
+ 'en', 'de', 'fr', 'ja', 'ru', 'zh', 'ko', 'es'
112
112
  ])
@@ -102,7 +102,7 @@ class Command(BaseCommand):
102
102
  f.write("# flake8: noqa=E501\n")
103
103
  f.write("# Auto Generated do not edit\n")
104
104
  # Python Imports
105
- f.write("from typing import Any, Optional\n\n")
105
+ f.write("from typing import Any\n\n")
106
106
  f.write("from esi.openapi_clients import EsiOperation\n")
107
107
  f.write("from esi.models import Token\n\n\n")
108
108
 
@@ -114,15 +114,25 @@ class Command(BaseCommand):
114
114
  # result(), Results(), Results_Localized() etc. all live here
115
115
  ops = stub_api._operationindex._tags[tag]
116
116
  for nm, op in sorted(ops._operations.items()):
117
+ op_type = op[0]
117
118
  op_obj = op[2]
118
119
  docstring = (op_obj.description or op_obj.summary or "").replace("\n", " ").strip()
119
120
  op_class_name = sanitize_operation_class(nm)
120
121
 
121
122
  response_type = "Any"
122
123
  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_)
124
+ match op_type:
125
+ case "post":
126
+ resp_201 = op_obj.responses.get("201")
127
+ if resp_201 and "application/json" in resp_201.content:
128
+ response_type = schema_to_type(resp_201.content["application/json"].schema_)
129
+ case "put" | "delete":
130
+ response_type = "None"
131
+ case _:
132
+ resp_200 = op_obj.responses.get("200")
133
+ if resp_200 and "application/json" in resp_200.content:
134
+ response_type = schema_to_type(resp_200.content["application/json"].schema_)
135
+
126
136
  except Exception:
127
137
  response_type = "Any"
128
138
 
@@ -130,22 +140,26 @@ class Command(BaseCommand):
130
140
 
131
141
  if op_class_name not in operation_classes:
132
142
  f.write(f"class {op_class_name}(EsiOperation):\n")
133
- f.write(" \"\"\"EsiOperation, use result(), results() or results_localized()\"\"\"\n")
143
+ if response_type != "None":
144
+ f.write(" \"\"\"EsiOperation, use result(), results() or results_localized()\"\"\"\n")
145
+ else:
146
+ f.write(" \"\"\"EsiOperation, use result()\"\"\"\n")
134
147
 
135
148
  # result()
136
149
  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
150
  f.write(f" \"\"\"{docstring}\"\"\"\n") if docstring else None
138
151
  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")
152
+ if response_type != "None":
153
+ # We only need the extra utility functions if its actually an endpoint that returns data
154
+ # results()
155
+ f.write(f" def results(self, etag: str | None = None, return_response: bool = False, use_cache: bool = True, **extra) -> {results_type}:\n") # noqa: E501
156
+ f.write(f" \"\"\"{docstring}\"\"\"\n") if docstring else None
157
+ f.write(" ...\n\n")
158
+
159
+ # results_localized()
160
+ f.write(f" def results_localized(self, languages: list[str] | str | None = None, **extra) -> dict[str, {results_type}]:\n")
161
+ f.write(f" \"\"\"{docstring}\"\"\"\n") if docstring else None
162
+ f.write(" ...\n\n\n")
149
163
 
150
164
  operation_classes[op_class_name] = True
151
165
 
@@ -172,6 +186,9 @@ class Command(BaseCommand):
172
186
 
173
187
  params = ["self"]
174
188
  optional_params = []
189
+ if getattr(op_obj, "requestBody", None):
190
+ params.append(f"body: {schema_to_type(op_obj.requestBody.content['application/json'].schema_)}")
191
+
175
192
  for p in getattr(op_obj, "parameters", []):
176
193
  required = getattr(p, "required", False)
177
194
  schema_type_value = getattr(getattr(p, "schema_", None), "type", None)
@@ -181,7 +198,7 @@ class Command(BaseCommand):
181
198
  param_type = "Any"
182
199
  default = ""
183
200
  if not required:
184
- param_type = f"Optional[{param_type}]"
201
+ param_type = f"{param_type} | None"
185
202
  default = " = ..."
186
203
  param_name = p.name.replace("-", "_")
187
204
  if param_name == "authorization" and needs_oauth:
@@ -18,7 +18,7 @@ from tenacity import (
18
18
  from django.core.cache import cache
19
19
 
20
20
  from esi import app_settings
21
- from esi.aiopenapi3.plugins import PatchCompatibilityDatePlugin
21
+ from esi.aiopenapi3.plugins import PatchCompatibilityDatePlugin, Trim204ContentType
22
22
  from esi.exceptions import ESIErrorLimitException
23
23
  from esi.models import Token
24
24
  from esi.stubs import ESIClientStub
@@ -121,14 +121,14 @@ def _load_aiopenapi_client_sync(
121
121
  path=spec_file,
122
122
  session_factory=session_factory,
123
123
  use_operation_tags=True,
124
- plugins=[PatchCompatibilityDatePlugin()]
124
+ plugins=[PatchCompatibilityDatePlugin(), Trim204ContentType()]
125
125
  )
126
126
  else:
127
127
  return OpenAPI.load_sync(
128
128
  url=spec_url,
129
129
  session_factory=session_factory,
130
130
  use_operation_tags=True,
131
- plugins=[PatchCompatibilityDatePlugin()]
131
+ plugins=[PatchCompatibilityDatePlugin(), Trim204ContentType()]
132
132
  )
133
133
 
134
134
 
@@ -176,14 +176,14 @@ async def _load_aiopenapi_client_async(
176
176
  path=spec_file,
177
177
  session_factory=session_factory,
178
178
  use_operation_tags=True,
179
- plugins=[PatchCompatibilityDatePlugin()]
179
+ plugins=[PatchCompatibilityDatePlugin(), Trim204ContentType()]
180
180
  )
181
181
  else:
182
182
  return await OpenAPI.load_async(
183
183
  url=spec_url,
184
184
  session_factory=session_factory,
185
185
  use_operation_tags=True,
186
- plugins=[PatchCompatibilityDatePlugin()]
186
+ plugins=[PatchCompatibilityDatePlugin(), Trim204ContentType()]
187
187
  )
188
188
 
189
189
 
@@ -326,6 +326,16 @@ class BaseEsiOperation():
326
326
  str_hash = md5(data).hexdigest() # nosec B303
327
327
  return f'esi_{str_hash}'
328
328
 
329
+ def _extract_body_param(self) -> Token | None:
330
+ """Pop the request body from parameters to be able to check the param validity
331
+ Returns:
332
+ Any | None: the request body
333
+ """
334
+ _body = self._kwargs.pop("body", None)
335
+ if _body and not getattr(self.operation, "requestBody", False):
336
+ raise ValueError("Request Body provided on endpoint with no request body paramater.")
337
+ return _body
338
+
329
339
  def _extract_token_param(self) -> Token | None:
330
340
  """Pop token from parameters or use the Client wide token if set
331
341
  Returns:
@@ -450,7 +460,7 @@ class EsiOperation(BaseEsiOperation):
450
460
  req.req.headers["Authorization"] = f"Bearer {self.token.valid_access_token()}"
451
461
  if etag:
452
462
  req.req.headers["If-None-Match"] = etag
453
- return req.request(parameters=self._unnormalize_parameters(parameters))
463
+ return req.request(data=self.body, parameters=self._unnormalize_parameters(parameters))
454
464
  return retry(__func)
455
465
 
456
466
  def result(
@@ -467,6 +477,7 @@ class EsiOperation(BaseEsiOperation):
467
477
  """
468
478
 
469
479
  self.token = self._extract_token_param()
480
+ self.body = self._extract_body_param()
470
481
  parameters = self._kwargs | extra
471
482
  cache_key = self._cache_key()
472
483
  etag_key = f"{cache_key}_etag"
@@ -508,8 +519,8 @@ class EsiOperation(BaseEsiOperation):
508
519
  return_response: bool = False,
509
520
  use_cache: bool = True,
510
521
  **extra) -> tuple[list[Any], Response | Any | None] | list[Any]:
511
- all_results = []
512
- last_response = None
522
+ all_results: list[Any] = []
523
+ last_response: Response | None = None
513
524
  """Executes the request and returns the response from ESI for the current
514
525
  operation. Response will include all pages if there are more available.
515
526
 
@@ -557,10 +568,31 @@ class EsiOperation(BaseEsiOperation):
557
568
 
558
569
  return (all_results, last_response) if return_response else all_results
559
570
 
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()
571
+ def results_localized(
572
+ self,
573
+ languages: list[str] | str | None = None,
574
+ **kwargs) -> dict[str, list[Any]]:
575
+ """Executes the request and returns the response from ESI for all default languages and pages (if any).
576
+ Args:
577
+ languages: (list[str], str, optional) language(s) to return instead of default languages
578
+ Raises:
579
+ ValueError: Invalid or Not Supported Language Code ...
580
+ Returns:
581
+ dict[str, list[Any]]: Dict of all responses with the language code as keys.
582
+ """
583
+ if not languages:
584
+ my_languages = list(app_settings.ESI_LANGUAGES)
585
+ else:
586
+ my_languages = []
587
+ for lang in dict.fromkeys(languages):
588
+ if lang not in app_settings.ESI_LANGUAGES:
589
+ raise ValueError('Invalid or Not Supported Language Code: %s' % lang)
590
+ my_languages.append(lang)
591
+
592
+ return {
593
+ language: self.results(language=language, **kwargs)
594
+ for language in my_languages
595
+ }
564
596
 
565
597
  def required_scopes(self) -> list[str]:
566
598
  """Return a simple list of scopes required for an endpoint. #Requires loading and processing a client
@@ -678,8 +710,31 @@ class EsiOperationAsync(BaseEsiOperation):
678
710
 
679
711
  return (all_results, last_response) if return_response else all_results
680
712
 
681
- async def results_localized(self, languages: str | list[str] = "en", **kwargs) -> list[Any]:
682
- raise NotImplementedError()
713
+ def results_localized(
714
+ self,
715
+ languages: list[str] | str | None = None,
716
+ **extra) -> dict[str, list[Any]]:
717
+ """Executes the request and returns the response from ESI for all default languages and pages (if any).
718
+ Args:
719
+ languages: (list[str], str, optional) language(s) to return instead of default languages
720
+ Raises:
721
+ ValueError: Invalid or Not Supported Language Code ...
722
+ Returns:
723
+ dict[str, list[Any]]: Dict of all responses with the language code as keys.
724
+ """
725
+ if not languages:
726
+ my_languages = list(app_settings.ESI_LANGUAGES)
727
+ else:
728
+ my_languages = []
729
+ for lang in dict.fromkeys(languages):
730
+ if lang not in app_settings.ESI_LANGUAGES:
731
+ raise ValueError('Invalid or Not Supported Language Code: %s' % lang)
732
+ my_languages.append(lang)
733
+
734
+ return {
735
+ language: self.results(language=language, **extra)
736
+ for language in my_languages
737
+ }
683
738
 
684
739
  def required_scopes(self) -> list[str]:
685
740
  """Return a simple list of scopes required for an endpoint. #Requires loading and processing a client
@@ -795,6 +850,9 @@ class ESIClientProvider:
795
850
  client_async(): ESIClientAsync
796
851
  """
797
852
 
853
+ _client: ESIClient | None = None
854
+ _client_async: ESIClientAsync | None = None
855
+
798
856
  def __init__(
799
857
  self,
800
858
  compatibility_date: str,