django-esi 8.1.0__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.
- django_esi-8.1.0.dist-info/METADATA +93 -0
- django_esi-8.1.0.dist-info/RECORD +100 -0
- django_esi-8.1.0.dist-info/WHEEL +4 -0
- django_esi-8.1.0.dist-info/licenses/LICENSE +674 -0
- esi/__init__.py +7 -0
- esi/admin.py +42 -0
- esi/aiopenapi3/client.py +79 -0
- esi/aiopenapi3/plugins.py +224 -0
- esi/app_settings.py +112 -0
- esi/apps.py +11 -0
- esi/checks.py +56 -0
- esi/clients.py +657 -0
- esi/decorators.py +271 -0
- esi/errors.py +22 -0
- esi/exceptions.py +51 -0
- esi/helpers.py +63 -0
- esi/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
- esi/locale/cs_CZ/LC_MESSAGES/django.po +53 -0
- esi/locale/de/LC_MESSAGES/django.mo +0 -0
- esi/locale/de/LC_MESSAGES/django.po +58 -0
- esi/locale/en/LC_MESSAGES/django.mo +0 -0
- esi/locale/en/LC_MESSAGES/django.po +54 -0
- esi/locale/es/LC_MESSAGES/django.mo +0 -0
- esi/locale/es/LC_MESSAGES/django.po +59 -0
- esi/locale/fr_FR/LC_MESSAGES/django.mo +0 -0
- esi/locale/fr_FR/LC_MESSAGES/django.po +59 -0
- esi/locale/it_IT/LC_MESSAGES/django.mo +0 -0
- esi/locale/it_IT/LC_MESSAGES/django.po +59 -0
- esi/locale/ja/LC_MESSAGES/django.mo +0 -0
- esi/locale/ja/LC_MESSAGES/django.po +58 -0
- esi/locale/ko_KR/LC_MESSAGES/django.mo +0 -0
- esi/locale/ko_KR/LC_MESSAGES/django.po +58 -0
- esi/locale/nl_NL/LC_MESSAGES/django.mo +0 -0
- esi/locale/nl_NL/LC_MESSAGES/django.po +53 -0
- esi/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
- esi/locale/pl_PL/LC_MESSAGES/django.po +53 -0
- esi/locale/ru/LC_MESSAGES/django.mo +0 -0
- esi/locale/ru/LC_MESSAGES/django.po +61 -0
- esi/locale/sk/LC_MESSAGES/django.mo +0 -0
- esi/locale/sk/LC_MESSAGES/django.po +55 -0
- esi/locale/uk/LC_MESSAGES/django.mo +0 -0
- esi/locale/uk/LC_MESSAGES/django.po +57 -0
- esi/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- esi/locale/zh_Hans/LC_MESSAGES/django.po +58 -0
- esi/management/commands/__init__.py +0 -0
- esi/management/commands/esi_clear_spec_cache.py +21 -0
- esi/management/commands/generate_esi_stubs.py +661 -0
- esi/management/commands/migrate_to_ssov2.py +188 -0
- esi/managers.py +303 -0
- esi/managers.pyi +85 -0
- esi/migrations/0001_initial.py +55 -0
- esi/migrations/0002_scopes_20161208.py +56 -0
- esi/migrations/0003_hide_tokens_from_admin_site.py +23 -0
- esi/migrations/0004_remove_unique_access_token.py +18 -0
- esi/migrations/0005_remove_token_length_limit.py +23 -0
- esi/migrations/0006_remove_url_length_limit.py +18 -0
- esi/migrations/0007_fix_mysql_8_migration.py +18 -0
- esi/migrations/0008_nullable_refresh_token.py +18 -0
- esi/migrations/0009_set_old_tokens_to_sso_v1.py +18 -0
- esi/migrations/0010_set_new_tokens_to_sso_v2.py +18 -0
- esi/migrations/0011_add_token_indices.py +28 -0
- esi/migrations/0012_fix_token_type_choices.py +18 -0
- esi/migrations/0013_squashed_0012_fix_token_type_choices.py +57 -0
- esi/migrations/__init__.py +0 -0
- esi/models.py +349 -0
- esi/openapi_clients.py +1225 -0
- esi/rate_limiting.py +107 -0
- esi/signals.py +21 -0
- esi/static/esi/img/EVE_SSO_Login_Buttons_Large_Black.png +0 -0
- esi/static/esi/img/EVE_SSO_Login_Buttons_Large_White.png +0 -0
- esi/static/esi/img/EVE_SSO_Login_Buttons_Small_Black.png +0 -0
- esi/static/esi/img/EVE_SSO_Login_Buttons_Small_White.png +0 -0
- esi/stubs.py +2 -0
- esi/stubs.pyi +6807 -0
- esi/tasks.py +78 -0
- esi/templates/esi/select_token.html +116 -0
- esi/templatetags/__init__.py +0 -0
- esi/templatetags/scope_tags.py +8 -0
- esi/tests/__init__.py +134 -0
- esi/tests/client_authed_pilot.py +63 -0
- esi/tests/client_public_pilot.py +53 -0
- esi/tests/factories.py +47 -0
- esi/tests/factories_2.py +60 -0
- esi/tests/jwt_factory.py +135 -0
- esi/tests/test_checks.py +48 -0
- esi/tests/test_clients.py +1019 -0
- esi/tests/test_decorators.py +578 -0
- esi/tests/test_management_command.py +307 -0
- esi/tests/test_managers.py +673 -0
- esi/tests/test_models.py +403 -0
- esi/tests/test_openapi.json +854 -0
- esi/tests/test_openapi.py +1017 -0
- esi/tests/test_swagger.json +489 -0
- esi/tests/test_swagger_full.json +51112 -0
- esi/tests/test_tasks.py +116 -0
- esi/tests/test_templatetags.py +22 -0
- esi/tests/test_views.py +331 -0
- esi/tests/threading_pilot.py +69 -0
- esi/urls.py +9 -0
- esi/views.py +129 -0
esi/decorators.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import Any
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from django.core.cache import cache
|
|
8
|
+
|
|
9
|
+
from esi.rate_limiting import ESIRateLimitBucket, ESIRateLimits
|
|
10
|
+
|
|
11
|
+
from .models import CallbackRedirect, Token
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _check_callback(request) -> Token | None:
|
|
17
|
+
# ensure session installed in database
|
|
18
|
+
if not request.session.exists(request.session.session_key):
|
|
19
|
+
logger.debug("Creating new session for %s", request.user)
|
|
20
|
+
request.session.create()
|
|
21
|
+
|
|
22
|
+
# clean up callback redirect, pass token if new requested
|
|
23
|
+
try:
|
|
24
|
+
model = CallbackRedirect.objects.get(session_key=request.session.session_key)
|
|
25
|
+
token = Token.objects.get(pk=model.token.pk)
|
|
26
|
+
model.delete()
|
|
27
|
+
logger.debug(
|
|
28
|
+
"Retrieved new token from callback for %s session %s",
|
|
29
|
+
request.user,
|
|
30
|
+
request.session.session_key[:5])
|
|
31
|
+
return token
|
|
32
|
+
|
|
33
|
+
except (CallbackRedirect.DoesNotExist, Token.DoesNotExist, AttributeError):
|
|
34
|
+
logger.debug(
|
|
35
|
+
"No callback for %s session %s",
|
|
36
|
+
request.user,
|
|
37
|
+
request.session.session_key[:5],
|
|
38
|
+
exc_info=True
|
|
39
|
+
)
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def tokens_required(scopes='', new=False):
|
|
44
|
+
"""
|
|
45
|
+
Decorator for views to request an ESI Token.
|
|
46
|
+
Accepts required scopes as a space-delimited string
|
|
47
|
+
or list of strings of scope names.
|
|
48
|
+
Can require a new token to be retrieved by SSO.
|
|
49
|
+
Returns a QueryDict of Tokens.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def decorator(view_func):
|
|
53
|
+
@wraps(view_func)
|
|
54
|
+
def _wrapped_view(request, *args, **kwargs):
|
|
55
|
+
|
|
56
|
+
# if we're coming back from SSO with a new token, return it
|
|
57
|
+
token = _check_callback(request)
|
|
58
|
+
if token:
|
|
59
|
+
tokens = Token.objects.filter(pk=token.pk)
|
|
60
|
+
logger.debug("Returning new token.")
|
|
61
|
+
return view_func(request, tokens, *args, **kwargs)
|
|
62
|
+
|
|
63
|
+
if not new:
|
|
64
|
+
# ensure user logged in to check existing tokens
|
|
65
|
+
if not request.user.is_authenticated:
|
|
66
|
+
logger.debug(
|
|
67
|
+
"Session %s is not logged in. Redirecting to login.",
|
|
68
|
+
request.session.session_key[:5]
|
|
69
|
+
)
|
|
70
|
+
from django.contrib.auth.views import redirect_to_login
|
|
71
|
+
return redirect_to_login(request.get_full_path())
|
|
72
|
+
|
|
73
|
+
# collect tokens in db, check if still valid, return if any
|
|
74
|
+
tokens = (
|
|
75
|
+
Token.objects
|
|
76
|
+
.filter(user__pk=request.user.pk)
|
|
77
|
+
.require_scopes(scopes)
|
|
78
|
+
.require_valid()
|
|
79
|
+
)
|
|
80
|
+
if tokens.exists():
|
|
81
|
+
logger.debug(
|
|
82
|
+
"Retrieved %s tokens for %s session %s",
|
|
83
|
+
tokens.count(),
|
|
84
|
+
request.user,
|
|
85
|
+
request.session.session_key[:5]
|
|
86
|
+
)
|
|
87
|
+
return view_func(request, tokens, *args, **kwargs)
|
|
88
|
+
|
|
89
|
+
# trigger creation of new token via sso
|
|
90
|
+
logger.debug(
|
|
91
|
+
"No tokens identified for %s session %s. Redirecting to SSO.",
|
|
92
|
+
request.user,
|
|
93
|
+
request.session.session_key[:5]
|
|
94
|
+
)
|
|
95
|
+
from esi.views import sso_redirect
|
|
96
|
+
return sso_redirect(request, scopes=scopes)
|
|
97
|
+
|
|
98
|
+
return _wrapped_view
|
|
99
|
+
|
|
100
|
+
return decorator
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def token_required(scopes='', new=False):
|
|
104
|
+
"""
|
|
105
|
+
Decorator for views which supplies a single,
|
|
106
|
+
user-selected token for the view to process.
|
|
107
|
+
Same parameters as tokens_required.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def decorator(view_func):
|
|
111
|
+
@wraps(view_func)
|
|
112
|
+
def _wrapped_view(request, *args, **kwargs):
|
|
113
|
+
|
|
114
|
+
# if we're coming back from SSO with a new token, return it
|
|
115
|
+
token = _check_callback(request)
|
|
116
|
+
if token:
|
|
117
|
+
logger.debug(
|
|
118
|
+
"Got new token from %s session %s. Returning to view.",
|
|
119
|
+
request.user,
|
|
120
|
+
request.session.session_key[:5]
|
|
121
|
+
)
|
|
122
|
+
return view_func(request, token, *args, **kwargs)
|
|
123
|
+
|
|
124
|
+
# if we're selecting a token, return it
|
|
125
|
+
if request.method == 'POST':
|
|
126
|
+
if request.POST.get("_add", False):
|
|
127
|
+
logger.debug(
|
|
128
|
+
"%s has selected to add new token. Redirecting to SSO.",
|
|
129
|
+
request.user
|
|
130
|
+
)
|
|
131
|
+
# user has selected to add a new token
|
|
132
|
+
from esi.views import sso_redirect
|
|
133
|
+
return sso_redirect(request, scopes=scopes)
|
|
134
|
+
|
|
135
|
+
token_pk = request.POST.get('_token', None)
|
|
136
|
+
if token_pk:
|
|
137
|
+
logger.debug(
|
|
138
|
+
"%s has selected token %s", request.user, token_pk
|
|
139
|
+
)
|
|
140
|
+
try:
|
|
141
|
+
token = Token.objects.get(pk=token_pk)
|
|
142
|
+
# ensure token belongs to this user and has required scopes
|
|
143
|
+
if (
|
|
144
|
+
(
|
|
145
|
+
(token.user and token.user == request.user)
|
|
146
|
+
or not token.user
|
|
147
|
+
)
|
|
148
|
+
and Token.objects
|
|
149
|
+
.filter(pk=token_pk)
|
|
150
|
+
.require_scopes(scopes)
|
|
151
|
+
.require_valid()
|
|
152
|
+
.exists()
|
|
153
|
+
):
|
|
154
|
+
logger.debug(
|
|
155
|
+
"Selected token fulfills requirements of view. "
|
|
156
|
+
"Returning."
|
|
157
|
+
)
|
|
158
|
+
return view_func(request, token, *args, **kwargs)
|
|
159
|
+
|
|
160
|
+
except Token.DoesNotExist:
|
|
161
|
+
logger.debug("Token %s not found.", token_pk)
|
|
162
|
+
|
|
163
|
+
if not new:
|
|
164
|
+
# present the user with token choices
|
|
165
|
+
tokens = (
|
|
166
|
+
Token.objects
|
|
167
|
+
.filter(user__pk=request.user.pk)
|
|
168
|
+
.require_scopes(scopes)
|
|
169
|
+
.require_valid()
|
|
170
|
+
)
|
|
171
|
+
if tokens.exists():
|
|
172
|
+
logger.debug(
|
|
173
|
+
"Returning list of available tokens for %s.", request.user
|
|
174
|
+
)
|
|
175
|
+
from esi.views import select_token
|
|
176
|
+
return select_token(request, scopes=scopes, new=new)
|
|
177
|
+
else:
|
|
178
|
+
logger.debug(
|
|
179
|
+
"No tokens found for %s session %s with scopes %s",
|
|
180
|
+
request.user,
|
|
181
|
+
request.session.session_key[:5],
|
|
182
|
+
scopes
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# prompt the user to add a new token
|
|
186
|
+
logger.debug(
|
|
187
|
+
"Redirecting %s session %s to SSO.",
|
|
188
|
+
request.user,
|
|
189
|
+
request.session.session_key[:5]
|
|
190
|
+
)
|
|
191
|
+
from esi.views import sso_redirect
|
|
192
|
+
return sso_redirect(request, scopes=scopes)
|
|
193
|
+
|
|
194
|
+
return _wrapped_view
|
|
195
|
+
|
|
196
|
+
return decorator
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def single_use_token(scopes='', new=False):
|
|
200
|
+
"""
|
|
201
|
+
Decorator for views which supplies a single use token granted via sso login
|
|
202
|
+
regardless of login state.
|
|
203
|
+
Same parameters as tokens_required.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def decorator(view_func):
|
|
207
|
+
@wraps(view_func)
|
|
208
|
+
def _wrapped_view(request, *args, **kwargs):
|
|
209
|
+
|
|
210
|
+
# if we're coming back from SSO for a new token, return it
|
|
211
|
+
token = _check_callback(request)
|
|
212
|
+
if token:
|
|
213
|
+
logger.debug(
|
|
214
|
+
"Got new token from session %s. Returning to view.",
|
|
215
|
+
request.session.session_key[:5]
|
|
216
|
+
)
|
|
217
|
+
return view_func(request, token, *args, **kwargs)
|
|
218
|
+
|
|
219
|
+
# prompt the user to login for a new token
|
|
220
|
+
logger.debug(
|
|
221
|
+
"Redirecting session %s to SSO.", request.session.session_key[:5]
|
|
222
|
+
)
|
|
223
|
+
from esi.views import sso_redirect
|
|
224
|
+
return sso_redirect(request, scopes=scopes)
|
|
225
|
+
|
|
226
|
+
return _wrapped_view
|
|
227
|
+
|
|
228
|
+
return decorator
|
|
229
|
+
|
|
230
|
+
|
|
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
|
+
"""
|
|
240
|
+
def decorator(func):
|
|
241
|
+
def wrapper(*args, **kwargs):
|
|
242
|
+
reset = cache.get(cache_key)
|
|
243
|
+
if reset is not None:
|
|
244
|
+
logger.error(f"ESI Error Limited, waiting {reset}s before retrying...")
|
|
245
|
+
while cache.get(cache_key):
|
|
246
|
+
time.sleep(poll_interval)
|
|
247
|
+
return func(*args, **kwargs)
|
|
248
|
+
return wrapper
|
|
249
|
+
return decorator
|
|
250
|
+
|
|
251
|
+
|
|
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
|
+
|
|
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.
|
|
265
|
+
def decorator(func):
|
|
266
|
+
@wraps(func)
|
|
267
|
+
def wrapper(*args, **kwargs):
|
|
268
|
+
ESIRateLimits.check_decr_bucket(bucket, raise_on_limit)
|
|
269
|
+
return func(*args, **kwargs)
|
|
270
|
+
return wrapper
|
|
271
|
+
return decorator
|
esi/errors.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class DjangoEsiException(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TokenError(DjangoEsiException):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TokenInvalidError(TokenError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TokenExpiredError(TokenError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NotRefreshableTokenError(TokenError):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class IncompleteResponseError(DjangoEsiException):
|
|
22
|
+
pass
|
esi/exceptions.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
|
|
3
|
+
from aiopenapi3.errors import HTTPServerError as base_HTTPServerError
|
|
4
|
+
from aiopenapi3.errors import HTTPClientError as base_HTTPClientError
|
|
5
|
+
from aiopenapi3.errors import HTTPError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ESIErrorLimitException(Exception):
|
|
9
|
+
"""ESI Global Error Limit Exceeded
|
|
10
|
+
https://developers.eveonline.com/docs/services/esi/best-practices/#error-limit
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, reset=None, *args, **kwargs) -> None:
|
|
14
|
+
self.reset = reset
|
|
15
|
+
msg = kwargs.get("message") or (
|
|
16
|
+
f"ESI Error limited. Reset in {reset} seconds." if reset else "ESI Error limited."
|
|
17
|
+
)
|
|
18
|
+
super().__init__(msg, *args)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ESIBucketLimitException(Exception):
|
|
22
|
+
"""Endpoint (Bucket) Specific Rate Limit Exceeded"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, bucket, reset: float = 0, *args, **kwargs) -> None:
|
|
25
|
+
self.bucket = bucket
|
|
26
|
+
self.reset = reset
|
|
27
|
+
msg = kwargs.get("message") or f"ESI bucket limit reached for {bucket}."
|
|
28
|
+
super().__init__(msg, *args)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclasses.dataclass(repr=False)
|
|
32
|
+
class HTTPNotModified(HTTPError):
|
|
33
|
+
"""The HTTP Status is 304"""
|
|
34
|
+
|
|
35
|
+
status_code: int
|
|
36
|
+
headers: dict[str, str]
|
|
37
|
+
|
|
38
|
+
def __str__(self):
|
|
39
|
+
return f"""<{self.__class__.__name__} {self.status_code} {self.headers}>"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclasses.dataclass(repr=False)
|
|
43
|
+
class HTTPClientError(base_HTTPClientError):
|
|
44
|
+
"""HTTP Response Code 4xx"""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclasses.dataclass(repr=False)
|
|
49
|
+
class HTTPServerError(base_HTTPServerError):
|
|
50
|
+
"""HTTP Response Code 5xx"""
|
|
51
|
+
pass
|
esi/helpers.py
ADDED
|
@@ -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
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# SOME DESCRIPTIVE TITLE.
|
|
2
|
+
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
3
|
+
# This file is distributed under the same license as the PACKAGE package.
|
|
4
|
+
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
5
|
+
#
|
|
6
|
+
#, fuzzy
|
|
7
|
+
msgid ""
|
|
8
|
+
msgstr ""
|
|
9
|
+
"Project-Id-Version: Django ESI 8.1.0\n"
|
|
10
|
+
"Report-Msgid-Bugs-To: https://gitlab.com/allianceauth/django-esi/-/issues\n"
|
|
11
|
+
"POT-Creation-Date: 2025-11-19 16:22+1000\n"
|
|
12
|
+
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
13
|
+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
14
|
+
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
15
|
+
"Language: \n"
|
|
16
|
+
"MIME-Version: 1.0\n"
|
|
17
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
|
18
|
+
"Content-Transfer-Encoding: 8bit\n"
|
|
19
|
+
|
|
20
|
+
#: esi/models.py:66
|
|
21
|
+
msgid "Character"
|
|
22
|
+
msgstr ""
|
|
23
|
+
|
|
24
|
+
#: esi/models.py:67
|
|
25
|
+
msgid "Corporation"
|
|
26
|
+
msgstr ""
|
|
27
|
+
|
|
28
|
+
#: esi/templates/esi/select_token.html:13
|
|
29
|
+
msgid "ESI Token Selection"
|
|
30
|
+
msgstr ""
|
|
31
|
+
|
|
32
|
+
#: esi/templates/esi/select_token.html:44
|
|
33
|
+
msgid "Select Character"
|
|
34
|
+
msgstr ""
|
|
35
|
+
|
|
36
|
+
#: esi/templates/esi/select_token.html:48
|
|
37
|
+
msgid "Scopes Requested"
|
|
38
|
+
msgstr ""
|
|
39
|
+
|
|
40
|
+
#: esi/templates/esi/select_token.html:61
|
|
41
|
+
#: esi/templates/esi/select_token.html:98
|
|
42
|
+
msgid "New Character"
|
|
43
|
+
msgstr ""
|
|
44
|
+
|
|
45
|
+
#: esi/templates/esi/select_token.html:71
|
|
46
|
+
#: esi/templates/esi/select_token.html:107
|
|
47
|
+
msgid "Add Token"
|
|
48
|
+
msgstr ""
|
|
49
|
+
|
|
50
|
+
#: esi/templates/esi/select_token.html:88
|
|
51
|
+
#: esi/templates/esi/select_token.html:89
|
|
52
|
+
msgid "Select"
|
|
53
|
+
msgstr ""
|
|
Binary file
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# SOME DESCRIPTIVE TITLE.
|
|
2
|
+
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
3
|
+
# This file is distributed under the same license as the PACKAGE package.
|
|
4
|
+
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
5
|
+
#
|
|
6
|
+
# Translators:
|
|
7
|
+
# Peter Pfeufer, 2023
|
|
8
|
+
#
|
|
9
|
+
#, fuzzy
|
|
10
|
+
msgid ""
|
|
11
|
+
msgstr ""
|
|
12
|
+
"Project-Id-Version: Django ESI 8.1.0\n"
|
|
13
|
+
"Report-Msgid-Bugs-To: https://gitlab.com/allianceauth/django-esi/-/issues\n"
|
|
14
|
+
"POT-Creation-Date: 2025-11-19 16:22+1000\n"
|
|
15
|
+
"PO-Revision-Date: 2023-10-25 11:04+0000\n"
|
|
16
|
+
"Last-Translator: Peter Pfeufer, 2023\n"
|
|
17
|
+
"Language-Team: German (https://app.transifex.com/alliance-auth/teams/107430/"
|
|
18
|
+
"de/)\n"
|
|
19
|
+
"Language: de\n"
|
|
20
|
+
"MIME-Version: 1.0\n"
|
|
21
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
|
22
|
+
"Content-Transfer-Encoding: 8bit\n"
|
|
23
|
+
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
|
24
|
+
|
|
25
|
+
#: esi/models.py:66
|
|
26
|
+
msgid "Character"
|
|
27
|
+
msgstr "Charakter"
|
|
28
|
+
|
|
29
|
+
#: esi/models.py:67
|
|
30
|
+
msgid "Corporation"
|
|
31
|
+
msgstr "Corporation"
|
|
32
|
+
|
|
33
|
+
#: esi/templates/esi/select_token.html:13
|
|
34
|
+
msgid "ESI Token Selection"
|
|
35
|
+
msgstr "ESI Token Auswahl"
|
|
36
|
+
|
|
37
|
+
#: esi/templates/esi/select_token.html:44
|
|
38
|
+
msgid "Select Character"
|
|
39
|
+
msgstr "Charakter Auswahl"
|
|
40
|
+
|
|
41
|
+
#: esi/templates/esi/select_token.html:48
|
|
42
|
+
msgid "Scopes Requested"
|
|
43
|
+
msgstr "Bereiche angefordert"
|
|
44
|
+
|
|
45
|
+
#: esi/templates/esi/select_token.html:61
|
|
46
|
+
#: esi/templates/esi/select_token.html:98
|
|
47
|
+
msgid "New Character"
|
|
48
|
+
msgstr "Neuer Charakter"
|
|
49
|
+
|
|
50
|
+
#: esi/templates/esi/select_token.html:71
|
|
51
|
+
#: esi/templates/esi/select_token.html:107
|
|
52
|
+
msgid "Add Token"
|
|
53
|
+
msgstr "Token hinzufügen"
|
|
54
|
+
|
|
55
|
+
#: esi/templates/esi/select_token.html:88
|
|
56
|
+
#: esi/templates/esi/select_token.html:89
|
|
57
|
+
msgid "Select"
|
|
58
|
+
msgstr "Auswählen"
|
|
Binary file
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# SOME DESCRIPTIVE TITLE.
|
|
2
|
+
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
3
|
+
# This file is distributed under the same license as the PACKAGE package.
|
|
4
|
+
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
5
|
+
#
|
|
6
|
+
#, fuzzy
|
|
7
|
+
msgid ""
|
|
8
|
+
msgstr ""
|
|
9
|
+
"Project-Id-Version: Django ESI 8.1.0\n"
|
|
10
|
+
"Report-Msgid-Bugs-To: https://gitlab.com/allianceauth/django-esi/-/issues\n"
|
|
11
|
+
"POT-Creation-Date: 2025-11-19 16:22+1000\n"
|
|
12
|
+
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
13
|
+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
14
|
+
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
15
|
+
"Language: \n"
|
|
16
|
+
"MIME-Version: 1.0\n"
|
|
17
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
|
18
|
+
"Content-Transfer-Encoding: 8bit\n"
|
|
19
|
+
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
|
20
|
+
|
|
21
|
+
#: esi/models.py:66
|
|
22
|
+
msgid "Character"
|
|
23
|
+
msgstr ""
|
|
24
|
+
|
|
25
|
+
#: esi/models.py:67
|
|
26
|
+
msgid "Corporation"
|
|
27
|
+
msgstr ""
|
|
28
|
+
|
|
29
|
+
#: esi/templates/esi/select_token.html:13
|
|
30
|
+
msgid "ESI Token Selection"
|
|
31
|
+
msgstr ""
|
|
32
|
+
|
|
33
|
+
#: esi/templates/esi/select_token.html:44
|
|
34
|
+
msgid "Select Character"
|
|
35
|
+
msgstr ""
|
|
36
|
+
|
|
37
|
+
#: esi/templates/esi/select_token.html:48
|
|
38
|
+
msgid "Scopes Requested"
|
|
39
|
+
msgstr ""
|
|
40
|
+
|
|
41
|
+
#: esi/templates/esi/select_token.html:61
|
|
42
|
+
#: esi/templates/esi/select_token.html:98
|
|
43
|
+
msgid "New Character"
|
|
44
|
+
msgstr ""
|
|
45
|
+
|
|
46
|
+
#: esi/templates/esi/select_token.html:71
|
|
47
|
+
#: esi/templates/esi/select_token.html:107
|
|
48
|
+
msgid "Add Token"
|
|
49
|
+
msgstr ""
|
|
50
|
+
|
|
51
|
+
#: esi/templates/esi/select_token.html:88
|
|
52
|
+
#: esi/templates/esi/select_token.html:89
|
|
53
|
+
msgid "Select"
|
|
54
|
+
msgstr ""
|
|
Binary file
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# SOME DESCRIPTIVE TITLE.
|
|
2
|
+
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
3
|
+
# This file is distributed under the same license as the PACKAGE package.
|
|
4
|
+
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
5
|
+
#
|
|
6
|
+
# Translators:
|
|
7
|
+
# trenus, 2023
|
|
8
|
+
#
|
|
9
|
+
#, fuzzy
|
|
10
|
+
msgid ""
|
|
11
|
+
msgstr ""
|
|
12
|
+
"Project-Id-Version: Django ESI 8.1.0\n"
|
|
13
|
+
"Report-Msgid-Bugs-To: https://gitlab.com/allianceauth/django-esi/-/issues\n"
|
|
14
|
+
"POT-Creation-Date: 2025-11-19 16:22+1000\n"
|
|
15
|
+
"PO-Revision-Date: 2023-10-25 11:04+0000\n"
|
|
16
|
+
"Last-Translator: trenus, 2023\n"
|
|
17
|
+
"Language-Team: Spanish (https://app.transifex.com/alliance-auth/teams/107430/"
|
|
18
|
+
"es/)\n"
|
|
19
|
+
"Language: es\n"
|
|
20
|
+
"MIME-Version: 1.0\n"
|
|
21
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
|
22
|
+
"Content-Transfer-Encoding: 8bit\n"
|
|
23
|
+
"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? "
|
|
24
|
+
"1 : 2;\n"
|
|
25
|
+
|
|
26
|
+
#: esi/models.py:66
|
|
27
|
+
msgid "Character"
|
|
28
|
+
msgstr ""
|
|
29
|
+
|
|
30
|
+
#: esi/models.py:67
|
|
31
|
+
msgid "Corporation"
|
|
32
|
+
msgstr ""
|
|
33
|
+
|
|
34
|
+
#: esi/templates/esi/select_token.html:13
|
|
35
|
+
msgid "ESI Token Selection"
|
|
36
|
+
msgstr "Selección de tokens ESI"
|
|
37
|
+
|
|
38
|
+
#: esi/templates/esi/select_token.html:44
|
|
39
|
+
msgid "Select Character"
|
|
40
|
+
msgstr "Seleccionar personaje"
|
|
41
|
+
|
|
42
|
+
#: esi/templates/esi/select_token.html:48
|
|
43
|
+
msgid "Scopes Requested"
|
|
44
|
+
msgstr "Ámbitos solicitados"
|
|
45
|
+
|
|
46
|
+
#: esi/templates/esi/select_token.html:61
|
|
47
|
+
#: esi/templates/esi/select_token.html:98
|
|
48
|
+
msgid "New Character"
|
|
49
|
+
msgstr "Nuevo personaje"
|
|
50
|
+
|
|
51
|
+
#: esi/templates/esi/select_token.html:71
|
|
52
|
+
#: esi/templates/esi/select_token.html:107
|
|
53
|
+
msgid "Add Token"
|
|
54
|
+
msgstr ""
|
|
55
|
+
|
|
56
|
+
#: esi/templates/esi/select_token.html:88
|
|
57
|
+
#: esi/templates/esi/select_token.html:89
|
|
58
|
+
msgid "Select"
|
|
59
|
+
msgstr "Seleccionar"
|
|
Binary file
|