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.
Files changed (100) hide show
  1. django_esi-8.1.0.dist-info/METADATA +93 -0
  2. django_esi-8.1.0.dist-info/RECORD +100 -0
  3. django_esi-8.1.0.dist-info/WHEEL +4 -0
  4. django_esi-8.1.0.dist-info/licenses/LICENSE +674 -0
  5. esi/__init__.py +7 -0
  6. esi/admin.py +42 -0
  7. esi/aiopenapi3/client.py +79 -0
  8. esi/aiopenapi3/plugins.py +224 -0
  9. esi/app_settings.py +112 -0
  10. esi/apps.py +11 -0
  11. esi/checks.py +56 -0
  12. esi/clients.py +657 -0
  13. esi/decorators.py +271 -0
  14. esi/errors.py +22 -0
  15. esi/exceptions.py +51 -0
  16. esi/helpers.py +63 -0
  17. esi/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  18. esi/locale/cs_CZ/LC_MESSAGES/django.po +53 -0
  19. esi/locale/de/LC_MESSAGES/django.mo +0 -0
  20. esi/locale/de/LC_MESSAGES/django.po +58 -0
  21. esi/locale/en/LC_MESSAGES/django.mo +0 -0
  22. esi/locale/en/LC_MESSAGES/django.po +54 -0
  23. esi/locale/es/LC_MESSAGES/django.mo +0 -0
  24. esi/locale/es/LC_MESSAGES/django.po +59 -0
  25. esi/locale/fr_FR/LC_MESSAGES/django.mo +0 -0
  26. esi/locale/fr_FR/LC_MESSAGES/django.po +59 -0
  27. esi/locale/it_IT/LC_MESSAGES/django.mo +0 -0
  28. esi/locale/it_IT/LC_MESSAGES/django.po +59 -0
  29. esi/locale/ja/LC_MESSAGES/django.mo +0 -0
  30. esi/locale/ja/LC_MESSAGES/django.po +58 -0
  31. esi/locale/ko_KR/LC_MESSAGES/django.mo +0 -0
  32. esi/locale/ko_KR/LC_MESSAGES/django.po +58 -0
  33. esi/locale/nl_NL/LC_MESSAGES/django.mo +0 -0
  34. esi/locale/nl_NL/LC_MESSAGES/django.po +53 -0
  35. esi/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
  36. esi/locale/pl_PL/LC_MESSAGES/django.po +53 -0
  37. esi/locale/ru/LC_MESSAGES/django.mo +0 -0
  38. esi/locale/ru/LC_MESSAGES/django.po +61 -0
  39. esi/locale/sk/LC_MESSAGES/django.mo +0 -0
  40. esi/locale/sk/LC_MESSAGES/django.po +55 -0
  41. esi/locale/uk/LC_MESSAGES/django.mo +0 -0
  42. esi/locale/uk/LC_MESSAGES/django.po +57 -0
  43. esi/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  44. esi/locale/zh_Hans/LC_MESSAGES/django.po +58 -0
  45. esi/management/commands/__init__.py +0 -0
  46. esi/management/commands/esi_clear_spec_cache.py +21 -0
  47. esi/management/commands/generate_esi_stubs.py +661 -0
  48. esi/management/commands/migrate_to_ssov2.py +188 -0
  49. esi/managers.py +303 -0
  50. esi/managers.pyi +85 -0
  51. esi/migrations/0001_initial.py +55 -0
  52. esi/migrations/0002_scopes_20161208.py +56 -0
  53. esi/migrations/0003_hide_tokens_from_admin_site.py +23 -0
  54. esi/migrations/0004_remove_unique_access_token.py +18 -0
  55. esi/migrations/0005_remove_token_length_limit.py +23 -0
  56. esi/migrations/0006_remove_url_length_limit.py +18 -0
  57. esi/migrations/0007_fix_mysql_8_migration.py +18 -0
  58. esi/migrations/0008_nullable_refresh_token.py +18 -0
  59. esi/migrations/0009_set_old_tokens_to_sso_v1.py +18 -0
  60. esi/migrations/0010_set_new_tokens_to_sso_v2.py +18 -0
  61. esi/migrations/0011_add_token_indices.py +28 -0
  62. esi/migrations/0012_fix_token_type_choices.py +18 -0
  63. esi/migrations/0013_squashed_0012_fix_token_type_choices.py +57 -0
  64. esi/migrations/__init__.py +0 -0
  65. esi/models.py +349 -0
  66. esi/openapi_clients.py +1225 -0
  67. esi/rate_limiting.py +107 -0
  68. esi/signals.py +21 -0
  69. esi/static/esi/img/EVE_SSO_Login_Buttons_Large_Black.png +0 -0
  70. esi/static/esi/img/EVE_SSO_Login_Buttons_Large_White.png +0 -0
  71. esi/static/esi/img/EVE_SSO_Login_Buttons_Small_Black.png +0 -0
  72. esi/static/esi/img/EVE_SSO_Login_Buttons_Small_White.png +0 -0
  73. esi/stubs.py +2 -0
  74. esi/stubs.pyi +6807 -0
  75. esi/tasks.py +78 -0
  76. esi/templates/esi/select_token.html +116 -0
  77. esi/templatetags/__init__.py +0 -0
  78. esi/templatetags/scope_tags.py +8 -0
  79. esi/tests/__init__.py +134 -0
  80. esi/tests/client_authed_pilot.py +63 -0
  81. esi/tests/client_public_pilot.py +53 -0
  82. esi/tests/factories.py +47 -0
  83. esi/tests/factories_2.py +60 -0
  84. esi/tests/jwt_factory.py +135 -0
  85. esi/tests/test_checks.py +48 -0
  86. esi/tests/test_clients.py +1019 -0
  87. esi/tests/test_decorators.py +578 -0
  88. esi/tests/test_management_command.py +307 -0
  89. esi/tests/test_managers.py +673 -0
  90. esi/tests/test_models.py +403 -0
  91. esi/tests/test_openapi.json +854 -0
  92. esi/tests/test_openapi.py +1017 -0
  93. esi/tests/test_swagger.json +489 -0
  94. esi/tests/test_swagger_full.json +51112 -0
  95. esi/tests/test_tasks.py +116 -0
  96. esi/tests/test_templatetags.py +22 -0
  97. esi/tests/test_views.py +331 -0
  98. esi/tests/threading_pilot.py +69 -0
  99. esi/urls.py +9 -0
  100. esi/views.py +129 -0
@@ -0,0 +1,188 @@
1
+ from datetime import datetime
2
+ from django.core.management.base import BaseCommand, CommandError
3
+
4
+ from django.db.migrations.recorder import MigrationRecorder
5
+ from django.utils import timezone
6
+
7
+ from esi.models import Token
8
+ from esi.errors import (
9
+ TokenInvalidError,
10
+ IncompleteResponseError,
11
+ NotRefreshableTokenError,
12
+ )
13
+ from esi import app_settings
14
+ from oauthlib.oauth2.rfc6749.errors import (
15
+ InvalidClientIdError,
16
+ InvalidGrantError,
17
+ InvalidTokenError,
18
+ )
19
+ from requests.auth import HTTPBasicAuth
20
+
21
+ from tqdm import tqdm
22
+
23
+ from requests_oauthlib import OAuth2Session
24
+
25
+ import pytz
26
+
27
+ EVE_SSOV1_END_DATE = pytz.UTC.localize(
28
+ datetime(year=2021, month=11, day=1, hour=0, minute=0)
29
+ )
30
+
31
+
32
+ def _sso_v1_refresh(
33
+ session: OAuth2Session, auth: HTTPBasicAuth, token: Token, message: str
34
+ ):
35
+ """
36
+ SSOv1 Refresh.
37
+ """
38
+ try:
39
+ _data = session.refresh_token(
40
+ "https://login.eveonline.com/oauth/token",
41
+ refresh_token=token.refresh_token,
42
+ auth=auth,
43
+ )
44
+ token.access_token = _data["access_token"]
45
+ token.refresh_token = _data["refresh_token"]
46
+ token.sso_version = 1
47
+ token.created = timezone.now()
48
+ token.save()
49
+ return (True, None)
50
+ except (InvalidGrantError):
51
+ return (
52
+ False,
53
+ (
54
+ "ID:%s '%s' %s SSOv1 Refresh failed"
55
+ " (InvalidGrant)" % (token.id, token.character_name, message)
56
+ ),
57
+ )
58
+ except (InvalidTokenError, InvalidClientIdError):
59
+ return (
60
+ False,
61
+ (
62
+ "ID:%s '%s' %s SSOv1 Refresh Failed "
63
+ "(InvalidToken, InvalidClientId)"
64
+ % (token.id, token.character_name, message)
65
+ ),
66
+ )
67
+ except Exception as e:
68
+ return (
69
+ False,
70
+ (
71
+ "ID:%s '%s' %s SSOv1 "
72
+ "Refresh Failed (%s)" % (token.id, token.character_name, message, e)
73
+ ),
74
+ )
75
+
76
+
77
+ class Command(BaseCommand):
78
+ help = "Attempt to Migrate all SSOv1 Tokens to SSOv2, and report failures."
79
+ requires_migrations_checks = True
80
+ requires_system_checks = '__all__'
81
+
82
+ def add_arguments(self, parser):
83
+ parser.add_argument(
84
+ "--skip-v1-checks",
85
+ action="store_true",
86
+ help="Do not test on SSOv1 Endpoints, both before"
87
+ " updating to SSOv2 and after a failure.",
88
+ )
89
+ parser.add_argument(
90
+ "--purge",
91
+ action="store_true",
92
+ help="Purge Tokens that fail the SSOv2 Update",
93
+ )
94
+ parser.add_argument("-n", help="Number of Tokens to update", type=int)
95
+
96
+ def handle(self, *args, **options):
97
+ if timezone.now() > EVE_SSOV1_END_DATE:
98
+ self.stdout.write(
99
+ self.style.WARNING(
100
+ "WARNING: Conversion of SSO v1 tokens is not garenteered to work "
101
+ f"past {EVE_SSOV1_END_DATE.strftime('%x')}."
102
+ )
103
+ )
104
+ use_v1 = not options.get("skip_v1_checks", False)
105
+ purge = options.get("purge", False)
106
+ batch = options.get("n", False)
107
+
108
+ migration_10 = MigrationRecorder.Migration.objects.filter(
109
+ app="esi", name="0010_set_new_tokens_to_sso_v2"
110
+ ).exists()
111
+
112
+ if not migration_10:
113
+ raise CommandError("Run migrations first and try again!")
114
+ else:
115
+ self.stdout.write("\n\nMigrations up to date. Proceeding to updates!")
116
+
117
+ if not use_v1:
118
+ self.stdout.write("\n\nSkipping SSOv1 pre/post error Verification!")
119
+
120
+ # lets reuse our own session and auth
121
+ session = OAuth2Session(app_settings.ESI_SSO_CLIENT_ID)
122
+ auth = HTTPBasicAuth(
123
+ app_settings.ESI_SSO_CLIENT_ID, app_settings.ESI_SSO_CLIENT_SECRET
124
+ )
125
+
126
+ # only do tokens that are SSOv1
127
+ tokens = Token.objects.filter(sso_version=1)
128
+ if batch:
129
+ tokens = tokens[:batch]
130
+ total = tokens.count()
131
+
132
+ if total > 0:
133
+ self.stdout.write("There are %s Tokens to update." % (total))
134
+ else:
135
+ return self.stdout.write("There are no Tokens to update.")
136
+
137
+ failures = []
138
+ # Nice Progress bar as this may take a while.
139
+ with tqdm(total=total) as t:
140
+ for token in tokens:
141
+ if use_v1:
142
+ result, message = _sso_v1_refresh(session, auth, token, "Initial")
143
+ if not result:
144
+ failures.append(message)
145
+ if purge:
146
+ token.delete()
147
+ t.update(1)
148
+ continue
149
+ try:
150
+ token.refresh(session=session, auth=auth)
151
+ except (TokenInvalidError, IncompleteResponseError):
152
+ if use_v1:
153
+ result, messaage = _sso_v1_refresh(
154
+ session, auth, token, "Post v2 Failure"
155
+ )
156
+ if not result:
157
+ failures.append(messaage)
158
+ if purge:
159
+ token.delete()
160
+ else:
161
+ failures.append(
162
+ "ID:%s '%s' SSOv2 Migration failed"
163
+ " (SSOv1 Re-Verification Ok)"
164
+ % (token.id, token.character_name)
165
+ )
166
+
167
+ else:
168
+ failures.append(
169
+ "ID:%s '%s' SSOv2 Migration failed"
170
+ " (TokenInvalidError, "
171
+ "IncompleteResponseError)"
172
+ % (token.id, token.character_name)
173
+ )
174
+ if purge:
175
+ token.delete()
176
+ except NotRefreshableTokenError:
177
+ failures.append(
178
+ "ID:%s '%s' SSOv2 Migration failed"
179
+ " (NotRefreshableTokenError)" % (token.id, token.character_name)
180
+ )
181
+ if purge:
182
+ token.delete()
183
+
184
+ t.update(1)
185
+
186
+ self.stdout.write("Completed Updates!")
187
+ self.stdout.write("%s Failures!" % (len(failures)))
188
+ self.stdout.write("\n".join(failures))
esi/managers.py ADDED
@@ -0,0 +1,303 @@
1
+ import logging
2
+ from datetime import timedelta
3
+ from typing import Any
4
+
5
+ import requests
6
+ from jose import jwt
7
+ from jose.exceptions import ExpiredSignatureError, JWTError
8
+ from requests_oauthlib import OAuth2Session
9
+
10
+ from django.db import models
11
+ from django.utils import timezone
12
+
13
+ from . import app_settings
14
+ from .errors import IncompleteResponseError, TokenError
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _process_scopes(scopes) -> set[str]:
20
+ if scopes is None:
21
+ # support filtering by no scopes with None passed
22
+ scopes = []
23
+ if not isinstance(scopes, models.QuerySet) and len(scopes) == 1:
24
+ # support a single space-delimited string inside a list because :users:
25
+ scopes = scopes[0]
26
+ # support space-delimited string scopes or lists
27
+ if isinstance(scopes, str):
28
+ scopes = set(scopes.split())
29
+ return {str(s) for s in scopes}
30
+
31
+
32
+ class TokenQueryset(models.QuerySet["Token"]):
33
+ def get_expired(self) -> "TokenQueryset":
34
+ """Get all tokens which have expired.
35
+
36
+ Returns:
37
+ All expired tokens.
38
+ """
39
+ max_age = \
40
+ timezone.now() - timedelta(seconds=app_settings.ESI_TOKEN_VALID_DURATION)
41
+ return self.filter(created__lte=max_age)
42
+
43
+ def bulk_refresh(self) -> "TokenQueryset":
44
+ """Refresh all refreshable tokens in the queryset and delete any expired token
45
+ that fails to refresh or can not be refreshed.
46
+
47
+ Excludes tokens for which the refresh was incomplete for other reasons.
48
+
49
+ Returns:
50
+ All refreshed tokens
51
+ """
52
+ session = OAuth2Session(app_settings.ESI_SSO_CLIENT_ID)
53
+ auth = requests.auth.HTTPBasicAuth(
54
+ app_settings.ESI_SSO_CLIENT_ID, app_settings.ESI_SSO_CLIENT_SECRET
55
+ )
56
+ incomplete = []
57
+ for model in self.filter(refresh_token__isnull=False):
58
+ try:
59
+ model.refresh(session=session, auth=auth)
60
+ logging.debug("Successfully refreshed %r", model)
61
+ except TokenError:
62
+ logger.info("Refresh failed for %r. Deleting.", model)
63
+ model.delete()
64
+ except IncompleteResponseError:
65
+ incomplete.append(model.pk)
66
+ self.filter(refresh_token__isnull=True).get_expired().delete()
67
+ return self.exclude(pk__in=incomplete)
68
+
69
+ def require_valid(self) -> "TokenQueryset":
70
+ """Ensure all tokens are still valid and attempt to refresh any which are expired
71
+
72
+ Deletes those which fail to refresh or cannot be refreshed.
73
+
74
+ Returns:
75
+ All tokens which are still valid.
76
+ """
77
+ expired_pks = set(self.get_expired().values_list("pk", flat=True))
78
+ fresh_pks = set(self.exclude(pk__in=expired_pks).values_list("pk", flat=True))
79
+ refreshed = self.filter(pk__in=expired_pks).bulk_refresh()
80
+ refreshed_pks = set(refreshed.values_list("pk", flat=True))
81
+ qs = self.filter(pk__in=fresh_pks | refreshed_pks)
82
+ return qs
83
+
84
+ def require_scopes(self, scope_string: str | list) -> "TokenQueryset":
85
+ """Filter tokens which have at least a subset of given scopes.
86
+
87
+ Args:
88
+ scope_string: The required scopes.
89
+
90
+ Returns:
91
+ Tokens which have all requested scopes.
92
+ """
93
+ scopes = _process_scopes(scope_string)
94
+ if not scopes:
95
+ # asking for tokens with no scopes
96
+ return self.filter(scopes__isnull=True)
97
+ from .models import Scope
98
+ scope_pks = Scope.objects.filter(name__in=scopes).values_list('pk', flat=True)
99
+ if not len(scopes) == len(scope_pks):
100
+ # there's a scope we don't recognize, so we can't have any tokens for it
101
+ return self.none()
102
+ tokens = self.all()
103
+ for pk in scope_pks:
104
+ tokens = tokens.filter(scopes__pk=pk)
105
+ return tokens
106
+
107
+ def require_scopes_exact(self, scope_string: str | list) -> "TokenQueryset":
108
+ """Filter tokens which exactly have the given scopes.
109
+
110
+ Args:
111
+ scope_string: The required scopes.
112
+
113
+ Returns:
114
+ Tokens which have all requested scopes.
115
+ """
116
+ num_scopes = len(_process_scopes(scope_string))
117
+ scopes_qs = self\
118
+ .annotate(models.Count('scopes'))\
119
+ .require_scopes(scope_string)\
120
+ .filter(scopes__count=num_scopes)\
121
+ .values('pk', 'scopes__id')
122
+ pks = [v['pk'] for v in scopes_qs]
123
+ return self.filter(pk__in=pks)
124
+
125
+ def equivalent_to(self, token) -> "TokenQueryset":
126
+ """Fetch all tokens which match the character and scopes of given reference token
127
+
128
+ Args:
129
+ token: :class:`esi.models.Token` reference token
130
+ """
131
+ return self\
132
+ .filter(character_id=token.character_id)\
133
+ .require_scopes_exact(token.scopes.all())\
134
+ .filter(models.Q(user=token.user) | models.Q(user__isnull=True))\
135
+ .exclude(pk=token.pk)
136
+
137
+
138
+ class TokenManager(models.Manager["Token"]):
139
+ def get_queryset(self) -> TokenQueryset:
140
+ """
141
+ Replace base queryset model with custom TokenQueryset
142
+ :rtype: :class:`esi.managers.TokenQueryset`
143
+ """
144
+ return TokenQueryset(self.model, using=self._db)
145
+
146
+ @staticmethod
147
+ def _decode_jwt(jwt_token: str, jwk_set: dict, issuer: Any) -> dict[str, Any]:
148
+ """
149
+ Helper function to decide the JWT access token supplied by EVE SSO
150
+ """
151
+ logger.debug("Start Decode")
152
+ token_data = jwt.decode(
153
+ jwt_token,
154
+ jwk_set,
155
+ algorithms=jwk_set["alg"],
156
+ audience=app_settings.ESI_TOKEN_JWT_AUDIENCE,
157
+ issuer=issuer
158
+ )
159
+ token_detail = token_data.get("sub", None).split(":")
160
+ token_data['character_id'] = int(token_detail[2])
161
+ token_data['token_type'] = token_detail[0].lower()
162
+ logger.debug(token_data)
163
+ return token_data
164
+
165
+ @staticmethod
166
+ def validate_access_token(token: str) -> dict[str, Any] | None:
167
+ """
168
+ Validate a JWT token retrieved from the EVE SSO.
169
+ :param token: A JWT token originating from the EVE SSO v2
170
+ :return: :class:`dict` The contents of the validated JWT token if
171
+ there are no validation errors
172
+ """
173
+
174
+ res = requests.get(app_settings.ESI_TOKEN_JWK_SET_URL)
175
+ res.raise_for_status()
176
+ data = res.json()
177
+
178
+ try:
179
+ jwk_sets = data["keys"]
180
+ except KeyError as e:
181
+ logger.warning(
182
+ "Something went wrong when retrieving the JWK set. "
183
+ "The returned payload did not have the expected key %s.\n"
184
+ "Payload returned from the SSO looks like: %s",
185
+ e,
186
+ data
187
+ )
188
+ return None
189
+
190
+ jwk_set = [item for item in jwk_sets if item["alg"] == "RS256"].pop()
191
+ try:
192
+ return TokenManager._decode_jwt(
193
+ token,
194
+ jwk_set,
195
+ ("login.eveonline.com", "https://login.eveonline.com")
196
+ )
197
+ except ExpiredSignatureError:
198
+ logger.warning("The JWT token has expired")
199
+ return None
200
+ except JWTError as e:
201
+ logger.warning("The JWT signature was invalid: %s", e)
202
+ return None
203
+
204
+ def create_from_code(self, code, user=None) -> "Token":
205
+ """
206
+ Perform OAuth code exchange to retrieve a token.
207
+ :param code: OAuth grant code.
208
+ :param user: User who will own token.
209
+ :return: :class:`esi.models.Token`
210
+ """
211
+
212
+ # perform code exchange
213
+ logger.debug("Creating new token from code %s", code[:-5])
214
+ oauth = OAuth2Session(
215
+ app_settings.ESI_SSO_CLIENT_ID,
216
+ redirect_uri=app_settings.ESI_SSO_CALLBACK_URL
217
+ )
218
+ token = oauth.fetch_token(
219
+ app_settings.ESI_TOKEN_URL,
220
+ client_secret=app_settings.ESI_SSO_CLIENT_SECRET,
221
+ code=code
222
+ )
223
+
224
+ token_data = TokenManager.validate_access_token(token.get('access_token', None))
225
+
226
+ # translate returned data to a model
227
+ model = self.create(
228
+ character_id=token_data['character_id'],
229
+ character_name=token_data['name'],
230
+ character_owner_hash=token_data['owner'],
231
+ access_token=token['access_token'],
232
+ refresh_token=token['refresh_token'],
233
+ token_type=token_data['token_type'],
234
+ user=user,
235
+ )
236
+
237
+ # parse scopes
238
+ if 'scp' in token_data:
239
+ from esi.models import Scope
240
+
241
+ # if a single scope is supplied its a string... recast to list
242
+ if isinstance(token_data['scp'], str):
243
+ token_data['scp'] = [token_data['scp']]
244
+
245
+ for s in token_data['scp']:
246
+ try:
247
+ scope = Scope.objects.get(name=s)
248
+ model.scopes.add(scope)
249
+ except Scope.DoesNotExist:
250
+ # This scope isn't included in a data migration.
251
+ # Create a placeholder until it updates.
252
+ try:
253
+ help_text = s.split('.')[1].replace('_', ' ').capitalize()
254
+ except IndexError:
255
+ # Unusual scope name, missing periods.
256
+ help_text = s.replace('_', ' ').capitalize()
257
+ scope = Scope.objects.create(name=s, help_text=help_text)
258
+ model.scopes.add(scope)
259
+ logger.debug("Added %d scopes to new token.", model.scopes.all().count())
260
+
261
+ if not app_settings.ESI_ALWAYS_CREATE_TOKEN:
262
+ # see if we already have a token for this character and scope combination
263
+ # if so, we don't need a new one
264
+ queryset = self.get_queryset().equivalent_to(model)
265
+ if queryset.exists():
266
+ logger.debug(
267
+ "Identified %d tokens equivalent to new token. "
268
+ "Updating access and refresh tokens.",
269
+ queryset.count()
270
+ )
271
+ queryset.update(
272
+ access_token=model.access_token,
273
+ refresh_token=model.refresh_token,
274
+ created=model.created,
275
+ )
276
+ if queryset.filter(user=model.user).exists():
277
+ logger.debug(
278
+ "Equivalent token with same user exists. Deleting new token."
279
+ )
280
+ model.delete()
281
+ model = queryset.filter(user=model.user)[0] # pick one at random
282
+
283
+ logger.debug("Successfully created %r for user %s", model, user)
284
+ return model
285
+
286
+ def create_from_request(self, request) -> "Token":
287
+ """
288
+ Generate a token from the OAuth callback request. Must contain 'code' in GET.
289
+ :param request: OAuth callback request.
290
+ :return: :class:`esi.models.Token`
291
+ """
292
+ logger.debug(
293
+ "Creating new token for %s session %s",
294
+ request.user,
295
+ request.session.session_key[:5]
296
+ )
297
+ code = request.GET.get('code')
298
+ # attach a user during creation for some functionality in a post_save created
299
+ # receiver I'm working on elsewhere
300
+ model = self.create_from_code(
301
+ code, user=request.user if request.user.is_authenticated else None
302
+ )
303
+ return model
esi/managers.pyi ADDED
@@ -0,0 +1,85 @@
1
+ from typing import Any
2
+ from django.db.models import QuerySet, Manager
3
+ from .models import Token
4
+
5
+
6
+ class TokenQueryset(QuerySet[Token]):
7
+ def get_expired(self) -> "TokenQueryset":
8
+ ...
9
+
10
+ def bulk_refresh(self) -> "TokenQueryset":
11
+ ...
12
+
13
+ def require_scopes(self, scope_string: str | list) -> "TokenQueryset":
14
+ ...
15
+
16
+ def require_scopes_exact(self, scope_string: str | list) -> "TokenQueryset":
17
+ ...
18
+
19
+ def require_valid(self) -> "TokenQueryset":
20
+ ...
21
+
22
+ def equivalent_to(self, token: Token) -> "TokenQueryset":
23
+ ...
24
+
25
+ def filter(self, *args: Any, **kwargs: Any) -> "TokenQueryset":
26
+ ...
27
+
28
+ def exclude(self, *args: Any, **kwargs: Any) -> "TokenQueryset":
29
+ ...
30
+
31
+ def order_by(self, *field_names: str) -> "TokenQueryset":
32
+ ...
33
+
34
+ def annotate(self, *args: Any, **kwargs: Any) -> "TokenQueryset":
35
+ ...
36
+
37
+ def select_related(self, *fields: str) -> "TokenQueryset":
38
+ ...
39
+
40
+ def prefetch_related(self, *lookups: str) -> "TokenQueryset":
41
+ ...
42
+
43
+ def distinct(self, *field_names: str) -> "TokenQueryset":
44
+ ...
45
+
46
+ def none(self) -> "TokenQueryset":
47
+ ...
48
+
49
+ def all(self) -> "TokenQueryset":
50
+ ...
51
+
52
+
53
+ class TokenManager(Manager[Token]):
54
+ def get_queryset(self) -> "TokenQueryset":
55
+ ...
56
+
57
+ def require_scopes(self, scope_string: str | list) -> "TokenQueryset":
58
+ ...
59
+
60
+ def filter(self, *args: Any, **kwargs: Any) -> "TokenQueryset":
61
+ ...
62
+
63
+ def exclude(self, *args: Any, **kwargs: Any) -> "TokenQueryset":
64
+ ...
65
+
66
+ def order_by(self, *field_names: str) -> "TokenQueryset":
67
+ ...
68
+
69
+ def annotate(self, *args: Any, **kwargs: Any) -> "TokenQueryset":
70
+ ...
71
+
72
+ def select_related(self, *fields: str) -> "TokenQueryset":
73
+ ...
74
+
75
+ def prefetch_related(self, *lookups: str) -> "TokenQueryset":
76
+ ...
77
+
78
+ def distinct(self, *field_names: str) -> "TokenQueryset":
79
+ ...
80
+
81
+ def none(self) -> "TokenQueryset":
82
+ ...
83
+
84
+ def all(self) -> "TokenQueryset":
85
+ ...
@@ -0,0 +1,55 @@
1
+ # Generated by Django 1.10.1 on 2016-11-10 23:27
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+ import django.db.models.deletion
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ initial = True
11
+
12
+ dependencies = [
13
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.CreateModel(
18
+ name='CallbackRedirect',
19
+ fields=[
20
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
+ ('url', models.CharField(default='/', help_text='The internal URL to redirect this callback towards.', max_length=254)),
22
+ ('session_key', models.CharField(help_text='Session key identifying the session this redirect was created for.', max_length=254, unique=True)),
23
+ ('state', models.CharField(help_text='OAuth2 state string representing this session.', max_length=128)),
24
+ ('created', models.DateTimeField(auto_now_add=True)),
25
+ ],
26
+ ),
27
+ migrations.CreateModel(
28
+ name='Scope',
29
+ fields=[
30
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
31
+ ('name', models.CharField(help_text='The official EVE name for the scope.', max_length=100, unique=True)),
32
+ ('help_text', models.TextField(help_text='The official EVE description of the scope.')),
33
+ ],
34
+ ),
35
+ migrations.CreateModel(
36
+ name='Token',
37
+ fields=[
38
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
39
+ ('created', models.DateTimeField(auto_now_add=True)),
40
+ ('access_token', models.CharField(help_text='The access token granted by SSO.', max_length=254, unique=True)),
41
+ ('refresh_token', models.CharField(blank=True, help_text='A re-usable token to generate new access tokens upon expiry.', max_length=254, null=True)),
42
+ ('character_id', models.IntegerField(help_text='The ID of the EVE character who authenticated by SSO.')),
43
+ ('character_name', models.CharField(help_text='The name of the EVE character who authenticated by SSO.', max_length=100)),
44
+ ('token_type', models.CharField(choices=[('Character', 'Character'), ('Corporation', 'Corporation')], default='Character', help_text='The applicable range of the token.', max_length=100)),
45
+ ('character_owner_hash', models.CharField(help_text='The unique string identifying this character and its owning EVE account. Changes if the owning account changes.', max_length=254)),
46
+ ('scopes', models.ManyToManyField(blank=True, help_text='The access scopes granted by this token.', to='esi.Scope')),
47
+ ('user', models.ForeignKey(blank=True, help_text='The user to whom this token belongs.', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
48
+ ],
49
+ ),
50
+ migrations.AddField(
51
+ model_name='callbackredirect',
52
+ name='token',
53
+ field=models.ForeignKey(blank=True, help_text='Token generated by a completed code exchange from callback processing.', null=True, on_delete=django.db.models.deletion.CASCADE, to='esi.Token'),
54
+ ),
55
+ ]
@@ -0,0 +1,56 @@
1
+ # Generated by Django 1.10.3 on 2016-11-10 15:12
2
+
3
+ from django.db import migrations
4
+
5
+ SCOPES = {
6
+ 'esi-planets.manage_planets.v1':
7
+ "Allows reading a list of a character's planetary colonies, and the details of those colonies.",
8
+ 'esi-ui.open_window.v1': "Allows open window in game client remotely.",
9
+ 'esi-assets.read_assets.v1': "Allows reading a list of assets that the character owns.",
10
+ 'esi-calendar.read_calendar_events.v1': "Allows reading a character's calendar, including corporate events.",
11
+ 'esi-bookmarks.read_character_bookmarks.v1': "Allows reading of a character's bookmarks and bookmark folders.",
12
+ 'esi-wallet.read_character_wallet.v1': "Allows reading of a character's wallet, journal and transaction history.",
13
+ 'esi-clones.read_clones.v1': "Allows reading the locations of a character's jump clones and their implants.",
14
+ 'esi-characters.read_contacts.v1':
15
+ "Allows reading of a character's contacts list, and calculation of CSPA charges.",
16
+ 'esi-corporations.read_corporation_membership.v1':
17
+ "Allows reading a list of the ID's and roles of a character's fellow corporation members.",
18
+ 'esi-fleets.read_fleet.v1': "Allows reading information about fleets.",
19
+ 'esi-killmails.read_killmails.v1': "Allows reading of a character's kills and losses.",
20
+ 'esi-location.read_location.v1': "Allows reading of a character's active ship location.",
21
+ 'esi-location.read_ship_type.v1': "Allows reading of a character's active ship class.",
22
+ 'esi-skills.read_skillqueue.v1': "Allows reading of a character's currently training skill queue.",
23
+ 'esi-skills.read_skills.v1': "Allows reading of a character's currently known skills.",
24
+ 'esi-universe.read_structures.v1':
25
+ "Allows querying the location and type of structures that the character has docking access at.",
26
+ 'esi-calendar.respond_calendar_events.v1': "Allows updating of a character's calendar event responses.",
27
+ 'esi-search.search_structures.v1':
28
+ "Allows searching over all structures that a character can see in the structure browser.",
29
+ 'esi-fleets.write_fleet.v1': "Allows manipulating fleets.",
30
+ 'esi-ui.write_waypoint.v1': "Allows manipulating waypoints in game client remotely."
31
+ }
32
+
33
+
34
+ def generate_scopes(apps, schema_editor):
35
+ Scope = apps.get_model('esi', 'Scope')
36
+ for s in SCOPES:
37
+ Scope.objects.update_or_create(name=s, defaults={'help_text': SCOPES[s]})
38
+
39
+
40
+ def delete_scopes(apps, schema_editor):
41
+ Scope = apps.get_model('esi', 'Scope')
42
+ for s in SCOPES:
43
+ try:
44
+ Scope.objects.get(name=s).delete()
45
+ except Scope.DoesNotExist:
46
+ pass
47
+
48
+
49
+ class Migration(migrations.Migration):
50
+ dependencies = [
51
+ ('esi', '0001_initial'),
52
+ ]
53
+
54
+ operations = [
55
+ migrations.RunPython(generate_scopes, delete_scopes)
56
+ ]