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,23 @@
1
+ # Generated by Django 1.10.5 on 2017-01-11 04:46
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('esi', '0002_scopes_20161208'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='token',
15
+ name='access_token',
16
+ field=models.CharField(editable=False, help_text='The access token granted by SSO.', max_length=254, unique=True),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name='token',
20
+ name='refresh_token',
21
+ field=models.CharField(blank=True, editable=False, help_text='A re-usable token to generate new access tokens upon expiry.', max_length=254, null=True),
22
+ ),
23
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 1.10.5 on 2017-03-30 01:43
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('esi', '0003_hide_tokens_from_admin_site'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='token',
15
+ name='access_token',
16
+ field=models.CharField(editable=False, help_text='The access token granted by SSO.', max_length=254),
17
+ ),
18
+ ]
@@ -0,0 +1,23 @@
1
+ # Generated by Django 2.0.4 on 2018-04-30 00:06
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('esi', '0004_remove_unique_access_token'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='token',
15
+ name='access_token',
16
+ field=models.TextField(editable=False, help_text='The access token granted by SSO.'),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name='token',
20
+ name='refresh_token',
21
+ field=models.TextField(editable=False, help_text='A re-usable token to generate new access tokens upon expiry.'),
22
+ ),
23
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 2.2.9 on 2020-04-14 13:17
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('esi', '0005_remove_token_length_limit'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='callbackredirect',
15
+ name='url',
16
+ field=models.TextField(default='/', help_text='The internal URL to redirect this callback towards.'),
17
+ ),
18
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by AaronKable on 2020-10-03
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('esi', '0006_remove_url_length_limit'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='token',
15
+ name='refresh_token',
16
+ field=models.TextField(editable=False, help_text='A re-usable token to generate new access tokens upon expiry.'),
17
+ ),
18
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 3.1.3 on 2020-11-09 23:43
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('esi', '0007_fix_mysql_8_migration'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='token',
15
+ name='refresh_token',
16
+ field=models.TextField(editable=False, help_text='A re-usable token to generate new access tokens upon expiry.', null=True),
17
+ ),
18
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 3.1.1 on 2021-10-08 10:20
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('esi', '0008_nullable_refresh_token'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='token',
15
+ name='sso_version',
16
+ field=models.IntegerField(default=1, help_text='EVE SSO Version.'),
17
+ ),
18
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 3.1.1 on 2021-10-08 10:21
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('esi', '0009_set_old_tokens_to_sso_v1'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='token',
15
+ name='sso_version',
16
+ field=models.IntegerField(default=2, help_text='EVE SSO Version.'),
17
+ ),
18
+ ]
@@ -0,0 +1,28 @@
1
+ # Generated by Django 3.2.12 on 2022-02-19 18:18
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('esi', '0010_set_new_tokens_to_sso_v2'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='token',
15
+ name='character_id',
16
+ field=models.IntegerField(db_index=True, help_text='The ID of the EVE character who authenticated by SSO.'),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name='token',
20
+ name='character_name',
21
+ field=models.CharField(db_index=True, help_text='The name of the EVE character who authenticated by SSO.', max_length=100),
22
+ ),
23
+ migrations.AlterField(
24
+ model_name='token',
25
+ name='character_owner_hash',
26
+ field=models.CharField(db_index=True, help_text='The unique string identifying this character and its owning EVE account. Changes if the owning account changes.', max_length=254),
27
+ ),
28
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 3.2.12 on 2022-05-16 17:49
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('esi', '0011_add_token_indices'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='token',
15
+ name='token_type',
16
+ field=models.CharField(choices=[('character', 'Character'), ('corporation', 'Corporation')], default='character', help_text='The applicable range of the token.', max_length=100),
17
+ ),
18
+ ]
@@ -0,0 +1,57 @@
1
+ # Generated by Django 4.2 on 2025-04-04 01:28
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+ import django.db.models.deletion
6
+
7
+ # Squashmigration imported a RunPython, not needed
8
+ # Manually optimized
9
+
10
+
11
+ class Migration(migrations.Migration):
12
+
13
+ replaces = [('esi', '0001_initial'), ('esi', '0002_scopes_20161208'), ('esi', '0003_hide_tokens_from_admin_site'), ('esi', '0004_remove_unique_access_token'), ('esi', '0005_remove_token_length_limit'), ('esi', '0006_remove_url_length_limit'), ('esi', '0007_fix_mysql_8_migration'), ('esi', '0008_nullable_refresh_token'), ('esi', '0009_set_old_tokens_to_sso_v1'), ('esi', '0010_set_new_tokens_to_sso_v2'), ('esi', '0011_add_token_indices'), ('esi', '0012_fix_token_type_choices')]
14
+
15
+ initial = True
16
+
17
+ dependencies = [
18
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
19
+ ]
20
+
21
+ operations = [
22
+ migrations.CreateModel(
23
+ name='Scope',
24
+ fields=[
25
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
26
+ ('name', models.CharField(help_text='The official EVE name for the scope.', max_length=100, unique=True)),
27
+ ('help_text', models.TextField(help_text='The official EVE description of the scope.')),
28
+ ],
29
+ ),
30
+ migrations.CreateModel(
31
+ name='Token',
32
+ fields=[
33
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
34
+ ('created', models.DateTimeField(auto_now_add=True)),
35
+ ('access_token', models.TextField(editable=False, help_text='The access token granted by SSO.')),
36
+ ('refresh_token', models.TextField(editable=False, help_text='A re-usable token to generate new access tokens upon expiry.', null=True)),
37
+ ('character_id', models.IntegerField(db_index=True, help_text='The ID of the EVE character who authenticated by SSO.')),
38
+ ('character_name', models.CharField(db_index=True, help_text='The name of the EVE character who authenticated by SSO.', max_length=100)),
39
+ ('token_type', models.CharField(choices=[('character', 'Character'), ('corporation', 'Corporation')], default='character', help_text='The applicable range of the token.', max_length=100)),
40
+ ('character_owner_hash', models.CharField(db_index=True, help_text='The unique string identifying this character and its owning EVE account. Changes if the owning account changes.', max_length=254)),
41
+ ('scopes', models.ManyToManyField(blank=True, help_text='The access scopes granted by this token.', to='esi.scope')),
42
+ ('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)),
43
+ ('sso_version', models.IntegerField(default=2, help_text='EVE SSO Version.')),
44
+ ],
45
+ ),
46
+ migrations.CreateModel(
47
+ name='CallbackRedirect',
48
+ fields=[
49
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
50
+ ('url', models.TextField(default='/', help_text='The internal URL to redirect this callback towards.')),
51
+ ('session_key', models.CharField(help_text='Session key identifying the session this redirect was created for.', max_length=254, unique=True)),
52
+ ('state', models.CharField(help_text='OAuth2 state string representing this session.', max_length=128)),
53
+ ('created', models.DateTimeField(auto_now_add=True)),
54
+ ('token', 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')),
55
+ ],
56
+ ),
57
+ ]
File without changes
esi/models.py ADDED
@@ -0,0 +1,349 @@
1
+ import datetime
2
+ import logging
3
+ import re
4
+ from typing import ClassVar
5
+
6
+ from bravado.client import SwaggerClient
7
+ from oauthlib.oauth2.rfc6749.errors import (
8
+ InvalidClientError, InvalidClientIdError, InvalidGrantError,
9
+ InvalidTokenError, MissingTokenError,
10
+ )
11
+ from requests.auth import HTTPBasicAuth
12
+ from requests_oauthlib import OAuth2Session
13
+
14
+ from django.conf import settings
15
+ from django.core.exceptions import ImproperlyConfigured
16
+ from django.db import models
17
+ from django.utils import timezone
18
+ from django.utils.translation import gettext_lazy as _
19
+
20
+ from . import app_settings
21
+ from .clients import esi_client_factory
22
+ from .errors import (
23
+ IncompleteResponseError, NotRefreshableTokenError, TokenError,
24
+ TokenExpiredError, TokenInvalidError,
25
+ )
26
+ from .managers import TokenManager
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class Scope(models.Model):
32
+ """
33
+ Represents an access scope granted by SSO.
34
+ """
35
+ name = models.CharField(
36
+ max_length=100, unique=True, help_text="The official EVE name for the scope."
37
+ )
38
+ help_text = models.TextField(help_text="The official EVE description of the scope.")
39
+
40
+ @property
41
+ def friendly_name(self):
42
+ return self._friendly_name(self.name)
43
+
44
+ @classmethod
45
+ def _friendly_name(cls, name):
46
+ try:
47
+ return re.sub('_', ' ', name.split('.')[1]).strip()
48
+ except IndexError:
49
+ return name
50
+
51
+ def __str__(self) -> str:
52
+ return self.name
53
+
54
+
55
+ class Token(models.Model):
56
+ """EVE Swagger Interface Access Token
57
+
58
+ Contains information about the authenticating character
59
+ and scopes granted to this token.
60
+ Contains the access token required for ESI authentication as well as refreshing.
61
+ """
62
+
63
+ TOKEN_TYPE_CHARACTER = "character"
64
+ TOKEN_TYPE_CORPORATION = "corporation"
65
+ TOKEN_TYPE_CHOICES = [
66
+ (TOKEN_TYPE_CHARACTER, _('Character')),
67
+ (TOKEN_TYPE_CORPORATION, _('Corporation')),
68
+ ]
69
+
70
+ created = models.DateTimeField(auto_now_add=True)
71
+ access_token = models.TextField(
72
+ help_text="The access token granted by SSO.",
73
+ editable=False
74
+ )
75
+ refresh_token = models.TextField(
76
+ null=True, # refresh tokens returned from SSO can be null
77
+ help_text="A re-usable token to generate new access tokens upon expiry.",
78
+ editable=False
79
+ )
80
+ user = models.ForeignKey(
81
+ settings.AUTH_USER_MODEL,
82
+ on_delete=models.CASCADE,
83
+ blank=True,
84
+ null=True,
85
+ help_text="The user to whom this token belongs."
86
+ )
87
+ character_id = models.IntegerField(
88
+ db_index=True,
89
+ help_text="The ID of the EVE character who authenticated by SSO."
90
+ )
91
+ character_name = models.CharField(
92
+ max_length=100,
93
+ db_index=True,
94
+ help_text="The name of the EVE character who authenticated by SSO."
95
+ )
96
+ token_type = models.CharField(
97
+ max_length=100,
98
+ choices=TOKEN_TYPE_CHOICES,
99
+ default=TOKEN_TYPE_CHARACTER,
100
+ help_text="The applicable range of the token."
101
+ )
102
+ character_owner_hash = models.CharField(
103
+ max_length=254,
104
+ db_index=True,
105
+ help_text=(
106
+ "The unique string identifying this character and its owning EVE "
107
+ "account. Changes if the owning account changes."
108
+ )
109
+ )
110
+ scopes = models.ManyToManyField(
111
+ Scope, blank=True, help_text="The access scopes granted by this token."
112
+ )
113
+ sso_version = models.IntegerField(
114
+ help_text="EVE SSO Version.",
115
+ default=2
116
+ )
117
+
118
+ objects: ClassVar[TokenManager] = TokenManager()
119
+
120
+ def __str__(self) -> str:
121
+ try:
122
+ scopes = sorted(s.name for s in self.scopes.all())
123
+ except ValueError:
124
+ scopes = []
125
+ return f'{self.character_name} - {", ".join(scopes)}'
126
+
127
+ def __repr__(self) -> str:
128
+ return "<{}(id={}): {}, {}>".format(
129
+ self.__class__.__name__,
130
+ self.pk,
131
+ self.character_id,
132
+ self.character_name,
133
+ )
134
+
135
+ @property
136
+ def can_refresh(self) -> bool:
137
+ """Determine if this token can be refreshed upon expiry."""
138
+ return bool(self.refresh_token)
139
+
140
+ @property
141
+ def expires(self) -> datetime.datetime:
142
+ """Determines when this token expires.
143
+
144
+ Returns:
145
+ Date & time when this token expires
146
+ """
147
+ return (
148
+ self.created
149
+ + datetime.timedelta(seconds=app_settings.ESI_TOKEN_VALID_DURATION)
150
+ )
151
+
152
+ @property
153
+ def expired(self) -> bool:
154
+ """Determines if this token has expired."""
155
+ return self.expires < timezone.now()
156
+
157
+ def valid_access_token(self) -> str:
158
+ """Refresh and return access token to be used in an authed ESI call.
159
+
160
+ Example:
161
+ .. code-block:: python
162
+
163
+ # fetch medals for a character
164
+ medals = esi.client.Character.get_characters_character_id_medals(
165
+ # required parameter for endpoint
166
+ character_id = token.character_id,
167
+ # provide a valid access token, which will be refreshed if required
168
+ token = token.valid_access_token()
169
+ ).results()
170
+
171
+ Returns:
172
+ Valid access token
173
+
174
+ Raises:
175
+ TokenExpiredError: When token can not be refreshed
176
+ """
177
+ if self.expired:
178
+ if self.can_refresh:
179
+ self.refresh()
180
+ else:
181
+ raise TokenExpiredError()
182
+ return self.access_token
183
+
184
+ def refresh(
185
+ self, session: OAuth2Session = None, auth: HTTPBasicAuth = None
186
+ ) -> None:
187
+ """Refresh this token.
188
+
189
+ Args:
190
+ session: session for refreshing token with
191
+ auth: ESI authentication
192
+ """
193
+ logger.debug("Attempting refresh of %r", self)
194
+ if self.can_refresh:
195
+ if not session:
196
+ session = OAuth2Session(app_settings.ESI_SSO_CLIENT_ID)
197
+ if not auth:
198
+ auth = HTTPBasicAuth(
199
+ app_settings.ESI_SSO_CLIENT_ID, app_settings.ESI_SSO_CLIENT_SECRET
200
+ )
201
+ try:
202
+ token = session.refresh_token(
203
+ app_settings.ESI_TOKEN_URL,
204
+ refresh_token=self.refresh_token,
205
+ auth=auth
206
+ )
207
+ logger.debug("Retrieved new token from SSO servers.")
208
+ # logger.debug(token)
209
+ token_data = TokenManager.validate_access_token(token['access_token'])
210
+
211
+ # TODO verify token properly
212
+ if token_data is not None:
213
+ if self.character_owner_hash != token_data['owner']:
214
+ logger.warning("Invalid Owner")
215
+ raise InvalidTokenError("Ownership Changed! Revoke me!")
216
+
217
+ self.access_token = token['access_token']
218
+ self.refresh_token = token['refresh_token']
219
+ self.sso_version = 2 # we will never be ssov1 again
220
+ self.created = timezone.now()
221
+ self.save()
222
+ logger.debug("Successfully refreshed %r", self)
223
+ except (InvalidGrantError) as e:
224
+ # this token is gone forever
225
+ logger.error("Refresh impossible for %r: %r", self, e)
226
+ raise TokenInvalidError()
227
+ except (InvalidTokenError, InvalidClientIdError) as e:
228
+ # these may be recoverable?
229
+ logger.warning("Refresh failed for %r: %r", self, e)
230
+ raise TokenInvalidError()
231
+ except MissingTokenError as e:
232
+ logger.info("Refresh failed for %r: %r", self, e)
233
+ raise IncompleteResponseError()
234
+ except InvalidClientError:
235
+ logger.debug(
236
+ "ESI client ID and secret rejected by remote. Cannot refresh."
237
+ )
238
+ raise ImproperlyConfigured(
239
+ 'Verify ESI_SSO_CLIENT_ID and ESI_SSO_CLIENT_SECRET settings.'
240
+ )
241
+ else:
242
+ logger.debug("Not a refreshable token.")
243
+ raise NotRefreshableTokenError()
244
+
245
+ def refresh_or_delete(self) -> None:
246
+ """Refresh this token or delete it if it can not be refreshed."""
247
+ try:
248
+ self.refresh()
249
+ except TokenError:
250
+ self.delete()
251
+ logger.warning("%s: Refresh failed. Token deleted.", repr(self))
252
+ else:
253
+ logging.info("%s: Successfully refreshed", self)
254
+
255
+ def get_esi_client(self, **kwargs) -> SwaggerClient:
256
+ """Creates an authenticated ESI client with this token.
257
+
258
+ Args:
259
+ **kwargs: Extra spec versioning as per \
260
+ :class:`esi.clients.esi_client_factory`
261
+
262
+ Returns:
263
+ New ESI client
264
+ """
265
+ return esi_client_factory(token=self, **kwargs)
266
+
267
+ @classmethod
268
+ def get_token_data(cls, access_token):
269
+ return TokenManager.validate_access_token(access_token)
270
+
271
+ # unused?
272
+ def update_token_data(self, commit=True):
273
+ logger.debug("Updating token data for %r", self)
274
+ if self.expired:
275
+ if self.can_refresh:
276
+ self.refresh()
277
+ else:
278
+ raise TokenExpiredError()
279
+ token_data = self.get_token_data(self.access_token)
280
+ logger.debug(token_data)
281
+ self.character_id = token_data['character_id']
282
+ self.character_name = token_data['name']
283
+ self.character_owner_hash = token_data['owner']
284
+ self.token_type = token_data['token_type']
285
+ logger.debug("Successfully updated token data.")
286
+ if commit:
287
+ self.save()
288
+
289
+ @classmethod
290
+ def get_token(cls, character_id: int, scopes: list) -> "Token":
291
+ """Helper method to get a token for a specific character with specific scopes.
292
+
293
+ Args:
294
+ character_id: Character to filter on.
295
+ scopes: array of ESI scope strings to search for.
296
+
297
+ Returns:
298
+ Matching token or `False` when token is not found
299
+ """
300
+ token = (
301
+ Token.objects
302
+ .filter(character_id=character_id)
303
+ .require_scopes(scopes)
304
+ .first()
305
+ )
306
+ if token:
307
+ return token
308
+ else:
309
+ return False
310
+
311
+
312
+ class CallbackRedirect(models.Model):
313
+ """
314
+ Records the intended destination for the SSO callback.
315
+ Used to internally redirect SSO callbacks.
316
+ """
317
+ session_key = models.CharField(
318
+ max_length=254,
319
+ unique=True,
320
+ help_text="Session key identifying the session this redirect was created for."
321
+ )
322
+ url = models.TextField(
323
+ default="/",
324
+ help_text="The internal URL to redirect this callback towards."
325
+ )
326
+ state = models.CharField(
327
+ max_length=128,
328
+ help_text="OAuth2 state string representing this session."
329
+ )
330
+ created = models.DateTimeField(auto_now_add=True)
331
+ token = models.ForeignKey(
332
+ Token,
333
+ on_delete=models.CASCADE,
334
+ blank=True,
335
+ null=True,
336
+ help_text=(
337
+ "Token generated by a completed code exchange "
338
+ "from callback processing."
339
+ )
340
+ )
341
+
342
+ def __str__(self) -> str:
343
+ return f"{self.session_key}: {self.url}"
344
+
345
+ def __repr__(self) -> str:
346
+ return "<{}(pk={}): {} to {}>".format(
347
+ self.__class__.__name__, self.pk,
348
+ self.session_key, self.url
349
+ )