django-esi 6.0.0__py3-none-any.whl → 7.0.0b1__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.

Potentially problematic release.


This version of django-esi might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: django-esi
3
- Version: 6.0.0
3
+ Version: 7.0.0b1
4
4
  Summary: Django app for accessing the EVE Swagger Interface (ESI).
5
5
  Author-email: Alliance Auth <adarnof@gmail.com>
6
6
  Requires-Python: >=3.8
@@ -8,8 +8,8 @@ Description-Content-Type: text/markdown
8
8
  Classifier: Environment :: Web Environment
9
9
  Classifier: Framework :: Django
10
10
  Classifier: Framework :: Django :: 4.2
11
- Classifier: Framework :: Django :: 5.0
12
11
  Classifier: Framework :: Django :: 5.1
12
+ Classifier: Framework :: Django :: 5.2
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
15
15
  Classifier: Operating System :: OS Independent
@@ -20,12 +20,14 @@ Classifier: Programming Language :: Python :: 3.9
20
20
  Classifier: Programming Language :: Python :: 3.10
21
21
  Classifier: Programming Language :: Python :: 3.11
22
22
  Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
23
24
  Classifier: Topic :: Internet :: WWW/HTTP
24
25
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
25
- Requires-Dist: bravado>=10.6,<11
26
+ License-File: LICENSE
27
+ Requires-Dist: bravado>=10.6,<12
26
28
  Requires-Dist: brotli
27
29
  Requires-Dist: celery>=4.0.2
28
- Requires-Dist: django>=4.2,<5.2
30
+ Requires-Dist: django>=4.2,<6
29
31
  Requires-Dist: jsonschema<4
30
32
  Requires-Dist: python-jose>=3.3
31
33
  Requires-Dist: requests>=2.26
@@ -34,7 +36,7 @@ Requires-Dist: tqdm>=4.62.3
34
36
  Requires-Dist: myst-parser ; extra == "docs"
35
37
  Requires-Dist: sphinx ; extra == "docs"
36
38
  Requires-Dist: sphinx-copybutton ; extra == "docs"
37
- Requires-Dist: sphinx-rtd-theme<3,>=2 ; extra == "docs"
39
+ Requires-Dist: sphinx-rtd-theme>=3,<4 ; extra == "docs"
38
40
  Requires-Dist: sphinx-tabs ; extra == "docs"
39
41
  Requires-Dist: sphinxcontrib-django ; extra == "docs"
40
42
  Requires-Dist: coverage ; extra == "test"
@@ -47,27 +49,27 @@ Project-URL: Tracker, https://gitlab.com/allianceauth/django-esi/-/issues
47
49
  Provides-Extra: docs
48
50
  Provides-Extra: test
49
51
 
50
- # django-esi
52
+ # Django-ESI
51
53
 
52
54
  Django app for easy access to the EVE Swagger Interface (ESI)
53
55
 
54
- [![version](https://img.shields.io/pypi/v/django-esi)](https://pypi.org/project/django-esi/)
55
- [![python](https://img.shields.io/pypi/pyversions/django-esi)](https://pypi.org/project/django-esi/)
56
- [![django](https://img.shields.io/pypi/djversions/django-esi)](https://pypi.org/project/django-esi/)
57
- [![license](https://img.shields.io/badge/license-GPLv3-green)](https://pypi.org/project/django-esi/)
58
- [![pipeline-status](https://gitlab.com/allianceauth/django-esi/badges/master/pipeline.svg)](https://gitlab.com/allianceauth/django-esi/pipelines)
59
- [![coverage](https://gitlab.com/allianceauth/django-esi/badges/master/coverage.svg)](https://gitlab.com/allianceauth/django-esi/pipelines)
56
+ [![Version](https://img.shields.io/pypi/v/django-esi)](https://pypi.org/project/django-esi/)
57
+ [![Python Versions](https://img.shields.io/pypi/pyversions/django-esi)](https://pypi.org/project/django-esi/)
58
+ [![Django Versions](https://img.shields.io/pypi/djversions/django-esi)](https://pypi.org/project/django-esi/)
59
+ [![License](https://img.shields.io/badge/license-GPLv3-green)](https://pypi.org/project/django-esi/)
60
+ [![Pipeline Status](https://gitlab.com/allianceauth/django-esi/badges/master/pipeline.svg)](https://gitlab.com/allianceauth/django-esi/pipelines)
61
+ [![Coverage](https://gitlab.com/allianceauth/django-esi/badges/master/coverage.svg)](https://gitlab.com/allianceauth/django-esi/pipelines)
60
62
  [![Documentation Status](https://readthedocs.org/projects/django-esi/badge/?version=latest)](https://django-esi.readthedocs.io/en/latest/?badge=latest)
61
63
  [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
62
64
  [![Chat on Discord](https://img.shields.io/discord/399006117012832262.svg)](https://discord.gg/fjnHAmk)
63
65
 
64
66
  ## Overview
65
67
 
66
- Django-esi is a Django app that provides an interface for easy access to the EVE Swagger Interface (ESI), the official API for the game [EVE Online](https://www.eveonline.com/).
68
+ Django-ESI is a Django app that provides an interface for easy access to the EVE Swagger Interface (ESI), the official API for the game [EVE Online](https://www.eveonline.com/).
67
69
 
68
- It is build upon [Bravado](https://github.com/Yelp/bravado) - a python client library for Swagger 2.0 services.
70
+ It is built upon [Bravado](https://github.com/Yelp/bravado) - a python client library for Swagger 2.0 services.
69
71
 
70
- Django-esi adds the following main functionalities to a Django site:
72
+ Django-ESI adds the following main functionalities to a Django site:
71
73
 
72
74
  - Dynamically generated client for interacting with public and private ESI endpoints
73
75
  - Support for adding EVE SSO to authenticate characters and retrieve tokens
@@ -75,8 +77,8 @@ Django-esi adds the following main functionalities to a Django site:
75
77
 
76
78
  ## Python Support
77
79
 
78
- Django-esi follows the Django Python support schedule, The supported version of Python will differ based on the version of Django used.
79
- <https://docs.djangoproject.com/en/5.0/faq/install/#what-python-version-can-i-use-with-django>
80
+ Django-ESI follows the Django Python support schedule, The supported version of Python will differ based on the version of Django used.
81
+ <https://docs.djangoproject.com/en/5.2/faq/install/#what-python-version-can-i-use-with-django>
80
82
 
81
83
  ## History of this app
82
84
 
@@ -84,5 +86,5 @@ This app is a fork from [adarnauth-esi](https://gitlab.com/Adarnof/adarnauth-esi
84
86
 
85
87
  ## Documentation
86
88
 
87
- For all details on how to install and use django-esi please see the [documentation](https://django-esi.readthedocs.io/en/latest/).
89
+ For all details on how to install and use Django-ESI please see the [Documentation](https://django-esi.readthedocs.io/en/latest/).
88
90
 
@@ -1,14 +1,14 @@
1
- esi/__init__.py,sha256=NtkIRztRvZcWgRF9JOeRXIdygDh6Yy5at20njzh1b5g,113
2
- esi/admin.py,sha256=Cr9cMMS9WA64-OqVcw-jpes1hTnMnqrhK9eyfz6QwC4,1111
1
+ esi/__init__.py,sha256=H-zKZVSyUvnk6nsG5H5S1P1GjtLAitPB2NqcBd1oHoM,169
2
+ esi/admin.py,sha256=glkzGf8Z76mEd6BGpsMC0B5LLH5--MWlfOCEkSBL7mI,1110
3
3
  esi/app_settings.py,sha256=O8TkKonK2zdF7RjMem1vMhEMulAtjvksxKgchrI8jME,3783
4
- esi/apps.py,sha256=RXDeRh62DO8NcKEzpiQovOay9wmb7MoVyC1W-cc-Aqc,272
5
- esi/checks.py,sha256=o3ka-Ux3QqTaf0u-oFGCPxwuG7DNJ3XYXiC7_2rQ1Ic,1154
6
- esi/clients.py,sha256=gkkJVZ9xqcd0TOI-5VduqYLaave1LjI16ucckr3UX9o,21303
7
- esi/decorators.py,sha256=USqGaIrVoyMpKlUB_ddZPe7QK0fBrFqAnoZDj8MMlSI,8081
4
+ esi/apps.py,sha256=HIu1niTkOXYmCzMVAjYcaFhHrrXeBbHvui6I44OCHXw,280
5
+ esi/checks.py,sha256=Aa3qiEK86w-dhJ4_OfepUxxxjiySMdR-e2fFKGYA9ss,1912
6
+ esi/clients.py,sha256=hQ8LMfqMh5DnwomatIzCWaDeDOP76-9M6glFLZbNrDE,23529
7
+ esi/decorators.py,sha256=HEaOHv-stiyU-AL4nv24kRNzUbN5TMvT282WA6ZC8pY,8128
8
8
  esi/errors.py,sha256=KfFtgX8Mys4uMoQtco0n_pQeaM83yVrRSyW6StXuUjo,308
9
9
  esi/managers.py,sha256=0g3J2jgoX-BviisjDEEAypkTayVBY-G2I_ZuskTDUqg,11350
10
- esi/models.py,sha256=fJJu8rLDMGW3ChxwFAZtFBWKB7PgMuDBatTuT06gWiA,11412
11
- esi/tasks.py,sha256=6wOqlANbQ83uvOt0L4_QHaLz0YZe1jtrQyr7zmxKY6g,1516
10
+ esi/models.py,sha256=bc8DtvX4cTgLK92O-mgNGXtHIyT_TbH3eefVDGoS8O8,11507
11
+ esi/tasks.py,sha256=AdX30ADt1OK6alTmVW014aDlDs7GEkTYGpQdx2cYQBY,2659
12
12
  esi/urls.py,sha256=23dY6ciekJ5wfwzo5LzLp0LFvWudrEAGICpDrscTeD8,152
13
13
  esi/views.py,sha256=ytfCILjz1rzITGgAGPyCY9bkFpE3KUfnii2UpHu6Bmc,3916
14
14
  esi/locale/de/LC_MESSAGES/django.mo,sha256=9_Fc_R8oZpT_CojH8gBxdSqj2K84EMEMn4wkvmOr7Dg,820
@@ -43,33 +43,34 @@ esi/migrations/0009_set_old_tokens_to_sso_v1.py,sha256=8NqOauSw3_bU12MKyHEJOXe2u
43
43
  esi/migrations/0010_set_new_tokens_to_sso_v2.py,sha256=d0YlpAWE6rQm7FQ9h39fF7eJTuOIgBLIouPKF7hj0gU,421
44
44
  esi/migrations/0011_add_token_indices.py,sha256=xPvfdIUJ3ewc4zv-7oWRe2nWBL9k5VI74C5sVvQJDD8,1016
45
45
  esi/migrations/0012_fix_token_type_choices.py,sha256=QyQs8pi5KqDtHUsbA7w7nH-AGda3GPvyUQs74RXEiGY,525
46
+ esi/migrations/0013_squashed_0012_fix_token_type_choices.py,sha256=eZnYDNevow9XrqlYEtSeGQy_PYDbxHU-GS_oRumGlrA,3951
46
47
  esi/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
48
  esi/static/esi/img/EVE_SSO_Login_Buttons_Large_Black.png,sha256=XjjhXr6EIum1ZsmXPXFOMDdYcezbkoAFmDjwC7fpi6E,2308
48
49
  esi/static/esi/img/EVE_SSO_Login_Buttons_Large_White.png,sha256=FUsOnYwHvYTiLZN5dN-_nPgOfLfZmmuFpVLbpcnOXE8,2248
49
50
  esi/static/esi/img/EVE_SSO_Login_Buttons_Small_Black.png,sha256=l-YlGVyegcTrfMRhdxDLorRAlkz5xMKiRR5LYvjEZYE,1622
50
51
  esi/static/esi/img/EVE_SSO_Login_Buttons_Small_White.png,sha256=al3NRjIngZtrNzExqBqPFDI4d7oSRp6LSdT3p5o9GOU,1546
51
- esi/templates/esi/select_token.html,sha256=v8F-tRFLe6y4rszysQUskbED1rdXlgqB4zhI95HVbc4,6905
52
+ esi/templates/esi/select_token.html,sha256=iBdVN0EYuha9CIWG2FgbTbyhJR_HcurDjt3NZy2xQxg,6941
52
53
  esi/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
54
  esi/templatetags/scope_tags.py,sha256=5-7vEwe-B91xo7oSnn-tSuudHSJCoGIhbddDpNrqUnw,167
54
55
  esi/tests/__init__.py,sha256=1ymL-74_4alt8AQNs7cFiXO5MmQKlIeOcA4OPOdAitM,3092
55
56
  esi/tests/client_authed_pilot.py,sha256=GN0J59TcscHM9HZH6OaPODM9Zel3mvObjJ4Dmmzc2j8,1622
56
57
  esi/tests/client_public_pilot.py,sha256=u6P-qbdYSvQ6A_MJqt8Jrkswt-u-FlvYDKtCvA5-PMo,1252
57
- esi/tests/factories.py,sha256=6I5Z9CKBtP26MwUwWEE8vSh_HFCvKGIGO-BLPoJeo7g,1368
58
+ esi/tests/factories.py,sha256=2pjkSYKDfO50bzX1nNcUHz_FAAJs7k07u7R8om6AlP0,1375
58
59
  esi/tests/factories_2.py,sha256=25NqVe9zzHkl4U1EN3R7pjdN2ahIBXxrrJhQzZhvw58,1972
59
60
  esi/tests/jwt_factory.py,sha256=1Ilg99V7Nc_jtUFrYVNm6GfnJ2f5ev0vhpnDhRur8zo,4944
60
61
  esi/tests/test_checks.py,sha256=ntq2ijuJ-6pxGruNh21rM4GlXDLVm-o3sy8DrFbjwEM,1846
61
- esi/tests/test_clients.py,sha256=eaB0DniNrpaQiAQGokLMmcwWYq0H1iswz3fdKvuf3E4,36037
62
+ esi/tests/test_clients.py,sha256=1JWTBV83nZbt82e37DAnJ7h4gm9m_xU1ZDvEjt0sHsc,41363
62
63
  esi/tests/test_decorators.py,sha256=I5MhcLKhN5xD4PxgEQqOWQ2kiXquDqQIEve9dbTOsho,15069
63
64
  esi/tests/test_management_command.py,sha256=mtxfBtG6CHP1bTy1tJEO-djX4SQwMT3T_mqGnUIM6qs,9863
64
65
  esi/tests/test_managers.py,sha256=CEpjXSXyVY6xxgrs0f9FnEh-KY9JPaKjKYiutXLLShE,24309
65
66
  esi/tests/test_models.py,sha256=lDj5IcYgXHeOFTHXTsWGg13CSooFaxD0c51kJn_ytAc,13748
66
67
  esi/tests/test_swagger.json,sha256=HOrPgbvwm5N521QNcE3baWcZJkSjmuN_VWrR06wEQoo,17241
67
68
  esi/tests/test_swagger_full.json,sha256=JCEAZNMFhkdZhquTx4lDhrqGgCrzzzlYU64HdbEv8E4,2548369
68
- esi/tests/test_tasks.py,sha256=P3vtqSno92DywNYu38nIo-_UiSicRAbun1AAsav0EBo,2104
69
+ esi/tests/test_tasks.py,sha256=nIvfXax_8nQCxNQeT-4TJ84i0__t1qWe155z5rnsktQ,4399
69
70
  esi/tests/test_templatetags.py,sha256=b68JWE3HvOlr2aUisJHsTsDS4e7IMjDeqTuzMqC7Re4,517
70
71
  esi/tests/test_views.py,sha256=Kj_f2yIpmPG0kx-lAX_sfkaHlIpgbkm02ieA1V3o-k4,13073
71
72
  esi/tests/threading_pilot.py,sha256=ax_dEdnTNibA-UQHqbZle_2dh_3jcHKRyrYSOKuE_6U,1931
72
- django_esi-6.0.0.dist-info/LICENSE,sha256=WJ7YI-moTFb-uVrFjnzzhGJrnL9P2iqQe8NuED3hutI,35141
73
- django_esi-6.0.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
74
- django_esi-6.0.0.dist-info/METADATA,sha256=0WMNumAVhCwNIO4tWXLmV3RU6ovU1VHRKmbhTlfOGhk,4656
75
- django_esi-6.0.0.dist-info/RECORD,,
73
+ django_esi-7.0.0b1.dist-info/licenses/LICENSE,sha256=WJ7YI-moTFb-uVrFjnzzhGJrnL9P2iqQe8NuED3hutI,35141
74
+ django_esi-7.0.0b1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
75
+ django_esi-7.0.0b1.dist-info/METADATA,sha256=9gZR6sDOlFinhRO_9WFmuFJpiHpPsPM94u54rPY1l5U,4747
76
+ django_esi-7.0.0b1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: flit 3.9.0
2
+ Generator: flit 3.12.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
esi/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """Django app for accessing the EVE Swagger Interface (ESI)."""
2
2
 
3
-
4
- __version__ = '6.0.0'
5
- __title__ = 'django-esi'
3
+ __version__ = '7.0.0b1'
4
+ __title__ = 'Django-ESI'
5
+ __url__ = 'https://gitlab.com/allianceauth/django-esi'
esi/admin.py CHANGED
@@ -23,7 +23,6 @@ class TokenAdmin(admin.ModelAdmin):
23
23
  def get_scopes(self, obj):
24
24
  return ", ".join([x.name for x in obj.scopes.all()])
25
25
 
26
-
27
26
  User = get_user_model()
28
27
  list_display = ('user', 'character_name', 'get_scopes')
29
28
  search_fields = ['user__%s' % User.USERNAME_FIELD, 'character_name', 'scopes__name']
esi/apps.py CHANGED
@@ -6,6 +6,6 @@ class EsiConfig(AppConfig):
6
6
  name = 'esi'
7
7
  verbose_name = 'EVE Swagger Interface (SSO v2)'
8
8
 
9
- def ready(self):
9
+ def ready(self) -> None:
10
10
  super().ready()
11
11
  from esi import checks # noqa
esi/checks.py CHANGED
@@ -4,7 +4,9 @@ from django.conf import settings
4
4
 
5
5
  @register(Tags.security)
6
6
  def check_sso_application_settings(*args, **kwargs):
7
+
7
8
  errors = []
9
+
8
10
  if (
9
11
  not hasattr(settings, "ESI_SSO_CLIENT_ID")
10
12
  or not hasattr(settings, "ESI_SSO_CLIENT_SECRET")
@@ -30,4 +32,25 @@ def check_sso_application_settings(*args, **kwargs):
30
32
  id='esi.E001'
31
33
  )
32
34
  )
35
+
36
+ # Check for ESI_USER_CONTACT_EMAIL
37
+ if hasattr(settings, "ESI_USER_CONTACT_EMAIL"):
38
+ # Check if ESI_USER_CONTACT_EMAIL is empty
39
+ if settings.ESI_USER_CONTACT_EMAIL == "":
40
+ errors.append(
41
+ Error(
42
+ msg="'ESI_USER_CONTACT_EMAIL' is empty. A valid email is required as maintainer contact for CCP.",
43
+ hint="",
44
+ id="esi.E002",
45
+ )
46
+ )
47
+ # ESI_USER_CONTACT_EMAIL not found
48
+ else:
49
+ errors.append(
50
+ Error(
51
+ msg="No 'ESI_USER_CONTACT_EMAIL' found is settings. A valid email is required as maintainer contact for CCP.",
52
+ hint="",
53
+ id="esi.E003",
54
+ )
55
+ )
33
56
  return errors
esi/clients.py CHANGED
@@ -5,6 +5,7 @@ import logging
5
5
  from time import sleep
6
6
  from urllib import parse as urlparse
7
7
  from typing import Any, Union, Tuple
8
+ import warnings
8
9
 
9
10
  from bravado.client import SwaggerClient
10
11
  from bravado import requests_client
@@ -20,7 +21,7 @@ from requests.adapters import HTTPAdapter
20
21
  from django.core.cache import cache
21
22
 
22
23
  from .errors import TokenExpiredError
23
- from . import app_settings, __version__, __title__
24
+ from . import app_settings, __version__, __title__, __url__
24
25
 
25
26
 
26
27
  logger = logging.getLogger(__name__)
@@ -438,13 +439,9 @@ def read_spec(path, http_client=None):
438
439
 
439
440
 
440
441
  def esi_client_factory(
441
- token=None,
442
- datasource: str = None,
443
- spec_file: str = None,
444
- version: str = None,
445
- app_info_text: str = None,
446
- **kwargs
447
- ) -> SwaggerClient:
442
+ token=None, datasource: str = None, spec_file: str = None, version: str = None,
443
+ app_info_text: str = None, # Deprecate in favour of the following variables
444
+ ua_appname: str = None, ua_version: str = None, ua_url: str = None, **kwargs) -> SwaggerClient:
448
445
  """Generate a new ESI client.
449
446
 
450
447
  Args:
@@ -452,10 +449,9 @@ def esi_client_factory(
452
449
  datasource: Name of the ESI datasource to access.
453
450
  spec_file: Absolute path to a swagger spec file to load.
454
451
  version: Base ESI API version. Accepted values are 'legacy', 'latest',
455
- app_info_text: Text identifying the application using ESI which will be \
456
- included in the User-Agent header. Should contain name and version of the \
457
- application using ESI. e.g. `"my-app v1.0.0"`. \
458
- Note that spaces are used as delimiter.
452
+ ua_appname: Name of the App for generating a User-Agent,
453
+ ua_version: Version of the App for generating a User-Agent,
454
+ ua_url: (optional) URL To the Source Code or Documentation for generating a User-Agent,
459
455
  kwargs: Explicit resource versions to build, in the form Character='v4'. \
460
456
  Same values accepted as version.
461
457
 
@@ -466,15 +462,40 @@ def esi_client_factory(
466
462
  Returns:
467
463
  New ESI client
468
464
  """
465
+
466
+ if app_info_text is not None:
467
+ warnings.warn(
468
+ "The 'app_info_text' parameter is deprecated and will be removed in a future release. "
469
+ "Use 'ua_appname', 'ua_version', and `ua_url` to dynamically build a User-Agent instead",
470
+ DeprecationWarning,
471
+ stacklevel=2
472
+ )
473
+
474
+ if ua_appname is None or ua_version is None:
475
+ warnings.warn(
476
+ "Applications must define their own 'ua_appname' and 'ua_version' to generate a User-Agent",
477
+ DeprecationWarning,
478
+ stacklevel=2
479
+ )
480
+
469
481
  if app_settings.ESI_INFO_LOGGING_ENABLED:
470
482
  logger.info('Generating an ESI client...')
471
483
 
472
484
  client = RequestsClientPlus()
473
- user_agent = (
474
- str(app_info_text) if app_info_text else f"{__title__} v{__version__}"
475
- )
476
- if app_settings.ESI_USER_CONTACT_EMAIL:
477
- user_agent += f" {app_settings.ESI_USER_CONTACT_EMAIL}"
485
+
486
+ if app_info_text:
487
+ # app_info_text (email@example) Django-ESI/1.2.3 (+https://gitlab.com/allianceauth/django-esi)
488
+ # Deprecated
489
+ user_agent = f"{app_info_text} ({app_settings.ESI_USER_CONTACT_EMAIL}) {__title__}/{__version__} (+{__url__})"
490
+ elif ua_appname is None or ua_version is None:
491
+ # Django-ESI/1.2.3 () (email@example; +https://gitlab.com/allianceauth/django-esi)
492
+ # Deprecated
493
+ user_agent = f"{__title__}/{__version__} ({app_settings.ESI_USER_CONTACT_EMAIL}; +{__url__})"
494
+ else:
495
+ # AppName/1.2.3 (email@example.com) Django-ESI/1.2.3 (+https://gitlab.com/allianceauth/django-esi)
496
+ # or AppName/1.2.3 (email@example.com; +https://gitlab.com/) Django-ESI/1.2.3 (+https://gitlab.com/allianceauth/django-esi) (+https://gitlab.com/allianceauth/django-esi)
497
+ # Preferred
498
+ user_agent = f"{ua_appname}/{ua_version} ({app_settings.ESI_USER_CONTACT_EMAIL}{f'; +{ua_url})' if ua_url else ')'} {__title__}/{__version__} (+{__url__})"
478
499
 
479
500
  client.user_agent = user_agent
480
501
 
@@ -535,10 +556,9 @@ class EsiClientProvider:
535
556
  datasource: Name of the ESI datasource to access.
536
557
  spec_file: Absolute path to a swagger spec file to load.
537
558
  version: Base ESI API version. Accepted values are 'legacy', 'latest',
538
- app_info_text: Text identifying the application using ESI which will be \
539
- included in the User-Agent header. Should contain name and version of \
540
- the application using ESI. e.g. `"my-app v1.0.0"`. \
541
- Note that spaces are used as delimiter.
559
+ ua_appname: Name of the App for generating a User-Agent,
560
+ ua_version: Version of the App for generating a User-Agent,
561
+ ua_url: (optional) URL To the Source Code or Documentation for generating a User-Agent,
542
562
  kwargs: Explicit resource versions to build, in the form Character='v4'. \
543
563
  Same values accepted as version.
544
564
 
@@ -550,30 +570,40 @@ class EsiClientProvider:
550
570
  _client = None
551
571
 
552
572
  def __init__(
553
- self,
554
- datasource=None,
555
- spec_file=None,
556
- version=None,
557
- app_info_text=None,
558
- **kwargs
559
- ):
573
+ self, datasource=None, spec_file=None, version=None,
574
+ app_info_text=None, # Deprecate in favour of the following variables
575
+ ua_appname: str = None, ua_version: str = None, ua_url: str = None, **kwargs) -> None:
560
576
  self._datasource = datasource
561
577
  self._spec_file = spec_file
562
578
  self._version = version
563
- self._app_text = app_info_text
579
+ self._app_text = app_info_text # Deprecate in favour of the following variables
580
+ self._ua_appname = ua_appname
581
+ self._ua_version = ua_version
582
+ self._ua_url = ua_url
564
583
  self._kwargs = kwargs
565
584
 
585
+ if app_info_text is not None:
586
+ warnings.warn(
587
+ "The 'app_info_text' parameter is deprecated and will be removed in a future release. "
588
+ "Use 'ua_appname', 'ua_version', and `ua_url` to dynamically build a User-Agent instead",
589
+ DeprecationWarning,
590
+ stacklevel=2
591
+ )
592
+
566
593
  @property
567
- def client(self):
594
+ def client(self) -> SwaggerClient:
568
595
  if self._client is None:
569
596
  self._client = esi_client_factory(
570
597
  datasource=self._datasource,
571
598
  spec_file=self._spec_file,
572
599
  version=self._version,
573
- app_info_text=self._app_text,
600
+ app_info_text=self._app_text, # Deprecate in favour of the following variables
601
+ ua_appname=self._ua_appname,
602
+ ua_version=self._ua_version,
603
+ ua_url=self._ua_url,
574
604
  **self._kwargs,
575
605
  )
576
606
  return self._client
577
607
 
578
- def __str__(self):
608
+ def __str__(self) -> str:
579
609
  return 'EsiClientProvider'
esi/decorators.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  from functools import wraps
3
+ from typing import Union
3
4
 
4
5
  from .models import Token, CallbackRedirect
5
6
 
@@ -7,7 +8,7 @@ from .models import Token, CallbackRedirect
7
8
  logger = logging.getLogger(__name__)
8
9
 
9
10
 
10
- def _check_callback(request):
11
+ def _check_callback(request) -> Union[Token, None]:
11
12
  # ensure session installed in database
12
13
  if not request.session.exists(request.session.session_key):
13
14
  logger.debug("Creating new session for %s", request.user)
@@ -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
+ ]
esi/models.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import datetime
2
2
  import re
3
3
  import logging
4
+ from typing import ClassVar
4
5
 
5
6
  from bravado.client import SwaggerClient
6
7
  from requests.auth import HTTPBasicAuth
@@ -54,7 +55,7 @@ class Scope(models.Model):
54
55
  except IndexError:
55
56
  return name
56
57
 
57
- def __str__(self):
58
+ def __str__(self) -> str:
58
59
  return self.name
59
60
 
60
61
 
@@ -121,16 +122,16 @@ class Token(models.Model):
121
122
  default=2
122
123
  )
123
124
 
124
- objects = TokenManager()
125
+ objects: ClassVar[TokenManager] = TokenManager()
125
126
 
126
- def __str__(self):
127
+ def __str__(self) -> str:
127
128
  try:
128
129
  scopes = sorted(s.name for s in self.scopes.all())
129
130
  except ValueError:
130
131
  scopes = []
131
132
  return f'{self.character_name} - {", ".join(scopes)}'
132
133
 
133
- def __repr__(self):
134
+ def __repr__(self) -> str:
134
135
  return "<{}(id={}): {}, {}>".format(
135
136
  self.__class__.__name__,
136
137
  self.pk,
@@ -248,7 +249,7 @@ class Token(models.Model):
248
249
  logger.debug("Not a refreshable token.")
249
250
  raise NotRefreshableTokenError()
250
251
 
251
- def refresh_or_delete(self):
252
+ def refresh_or_delete(self) -> None:
252
253
  """Refresh this token or delete it if it can not be refreshed."""
253
254
  try:
254
255
  self.refresh()
@@ -345,10 +346,10 @@ class CallbackRedirect(models.Model):
345
346
  )
346
347
  )
347
348
 
348
- def __str__(self):
349
+ def __str__(self) -> str:
349
350
  return f"{self.session_key}: {self.url}"
350
351
 
351
- def __repr__(self):
352
+ def __repr__(self) -> str:
352
353
  return "<{}(pk={}): {} to {}>".format(
353
354
  self.__class__.__name__, self.pk,
354
355
  self.session_key, self.url
esi/tasks.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from datetime import timedelta
2
2
  import logging
3
+ from math import ceil
3
4
 
4
5
  from celery import shared_task
5
6
 
@@ -26,14 +27,15 @@ def cleanup_callbackredirect(max_age=300):
26
27
 
27
28
 
28
29
  @shared_task
29
- def cleanup_token():
30
+ def cleanup_token() -> None:
30
31
  """
31
- Delete expired :model:`esi.Token` models.
32
+ Delete Orphaned Tokens, then refresh or delete expired :model:`esi.Token` models.
32
33
  """
33
34
  orphaned_tokens = Token.objects.filter(user__isnull=True)
34
35
  if orphaned_tokens.exists():
35
36
  logger.info("Deleting %d orphaned tokens.", orphaned_tokens.count())
36
37
  orphaned_tokens.delete()
38
+
37
39
  expired_tokens = Token.objects.exclude(user__isnull=True).get_expired()
38
40
  if expired_tokens.exists():
39
41
  logger.info(
@@ -46,6 +48,31 @@ def cleanup_token():
46
48
  refresh_or_delete_token.apply_async(args=[token_pk], priority=8)
47
49
 
48
50
 
51
+ @shared_task
52
+ def cleanup_token_subset(fraction: int = 48) -> None:
53
+ """
54
+ Delete Orphaned Tokens, then refresh or delete a subset of expired :model:`esi.Token` models.
55
+
56
+ This task operates on 1/fraction of the oldest tokens and can be called on a more regular schedule
57
+ """
58
+ orphaned_tokens = Token.objects.filter(user__isnull=True)
59
+ if orphaned_tokens.exists():
60
+ logger.info("Deleting %d orphaned tokens.", orphaned_tokens.count())
61
+ orphaned_tokens.delete()
62
+
63
+ expired_tokens = Token.objects.exclude(user__isnull=True).get_expired()
64
+ expired_tokens_subset = expired_tokens.filter(
65
+ refresh_token__isnull=False
66
+ ).order_by("created")[:ceil(expired_tokens.count() / fraction)]
67
+
68
+ if expired_tokens.exists():
69
+ logger.info(
70
+ f"Triggering bulk refresh of subset/possible {expired_tokens_subset.count()}/{expired_tokens.count()} expired tokens."
71
+ )
72
+ for token_pk in expired_tokens_subset.values_list("pk", flat=True):
73
+ refresh_or_delete_token.apply_async(args=[token_pk], priority=8)
74
+
75
+
49
76
  @shared_task
50
77
  def refresh_or_delete_token(token_pk: int):
51
78
  token = Token.objects.get(pk=token_pk)
@@ -10,7 +10,7 @@
10
10
  <meta name="description" content="">
11
11
  <meta name="author" content="">
12
12
 
13
- <title>{% trans "ESI Token Selection" %}</title>
13
+ <title>{% translate "ESI Token Selection" %}</title>
14
14
 
15
15
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.1/css/bootstrap.min.css" integrity="sha512-T584yQ/tdRR5QwOpfvDfVQUidzfgc2339Lc8uBDtcp/wYu80d7jwBgAxbyMh0a9YM9F8N3tdErpFI8iaGx6x5g==" crossorigin="anonymous" referrerpolicy="no-referrer" />
16
16
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.slim.min.js" integrity="sha512-6ORWJX/LrnSjBzwefdNUyLCMTIsGoNP6NftMy2UAm1JBm6PRZCO1d7OHBStWpVFZLO+RerTvqX/Z9mBFfCJZ4A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
@@ -41,11 +41,11 @@
41
41
  </head>
42
42
  <body class="bg-dark">
43
43
  <div class="container-fluid mt-5">
44
- <h3 class="text-light font-weight-light text-center">{% trans "Select Character" %}</h3><br>
44
+ <h3 class="text-light font-weight-light text-center">{% translate "Select Character" %}</h3><br>
45
45
  <div class="mt-2">
46
46
  <div class="col-lg-10 offset-lg-1 col-md-12" style="padding-left: 0 !important; padding-right: 0 !important;">
47
47
  <div class="card text-white bg-secondary">
48
- <div class="card-header text-center">{% trans "Scopes Requested" %}</div>
48
+ <div class="card-header text-center">{% translate "Scopes Requested" %}</div>
49
49
  <div class="card-body align-middle text-center">
50
50
  {% for scope in scopes %}<span class="badge badge-dark">{{ scope|scope_friendly_name }}</span> {% endfor %}
51
51
  </div>
@@ -58,7 +58,7 @@
58
58
  {% if tokens|length > 14 %}
59
59
  <form method="post">
60
60
  <div class="card text-white bg-secondary m-2 sso-card">
61
- <div class="card-header text-center">{% trans "New Character" %}</div>
61
+ <div class="card-header text-center">{% translate "New Character" %}</div>
62
62
  <div class="card-body text-center d-none d-sm-block">
63
63
  <input type="image" formmethod="post" class="ra-avatar img-responsive character-image rounded-lg"
64
64
  src="https://images.evetech.net/characters/1/portrait?size=256">
@@ -68,7 +68,7 @@
68
68
  <input type="hidden" name="_add" value="True">
69
69
  <a href="#" type="submit" class="m-1">
70
70
  <input type="image" name="go" src="{% static 'esi\img\EVE_SSO_Login_Buttons_Small_Black.png'%}"
71
- alt="{% trans "Add Token" %}">
71
+ alt="{% translate "Add Token" %}">
72
72
  </a>
73
73
  </div>
74
74
  </div>
@@ -85,8 +85,8 @@
85
85
  </div>
86
86
  <div class="card-footer">
87
87
  <input type="hidden" name="_token" value="{{ token.pk }}">
88
- <button class="btn btn-success w-100" type="submit" title="{% trans "Select" %}" formmethod="post">
89
- {% trans "Select" %}
88
+ <button class="btn btn-success w-100" type="submit" title="{% translate "Select" %}" formmethod="post">
89
+ {% translate "Select" %}
90
90
  </button>
91
91
  </div>
92
92
  </div>
@@ -95,7 +95,7 @@
95
95
  <form method="post">
96
96
  {% csrf_token %}
97
97
  <div class="card text-white bg-secondary m-2 sso-card">
98
- <div class="card-header text-center">{% trans "New Character" %}</div>
98
+ <div class="card-header text-center">{% translate "New Character" %}</div>
99
99
  <div class="card-body text-center d-none d-sm-block">
100
100
  <input type="image" formmethod="post" class="ra-avatar img-responsive character-image rounded-lg"
101
101
  src="https://images.evetech.net/characters/1/portrait?size=256">
@@ -104,7 +104,7 @@
104
104
  <input type="hidden" name="_add" value="True">
105
105
  <a href="#" type="submit" class="m-1">
106
106
  <input type="image" name="go" src="{% static 'esi\img\EVE_SSO_Login_Buttons_Small_Black.png'%}"
107
- alt="{% trans "Add Token" %}">
107
+ alt="{% translate "Add Token" %}">
108
108
  </a>
109
109
  </div>
110
110
  </div>
esi/tests/factories.py CHANGED
@@ -43,5 +43,5 @@ class BravadoResponseStub:
43
43
  self.headers = headers if headers else dict()
44
44
  self.raw_bytes = raw_bytes
45
45
 
46
- def __str__(self):
46
+ def __str__(self) -> str:
47
47
  return f"{self.status_code} {self.reason}"
esi/tests/test_clients.py CHANGED
@@ -716,8 +716,8 @@ class TestClientResult2(NoSocketsTestCase):
716
716
  def setUpClass(cls) -> None:
717
717
  super().setUpClass()
718
718
  spec = _load_json_file(SWAGGER_SPEC_PATH_MINIMAL)
719
- with patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", None), patch(
720
- MODULE_PATH + ".__title__", "django-esi"
719
+ with patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", "email@example.com"), patch(
720
+ MODULE_PATH + ".__title__", "Django-ESI"
721
721
  ), patch(MODULE_PATH + ".__version__", "1.0.0"):
722
722
  with requests_mock.Mocker() as requests_mocker:
723
723
  requests_mocker.register_uri(
@@ -733,7 +733,7 @@ class TestClientResult2(NoSocketsTestCase):
733
733
  # then
734
734
  self.assertTrue(requests_mocker.called)
735
735
  request = requests_mocker.last_request
736
- self.assertEqual(request._request.headers["User-Agent"], "django-esi v1.0.0")
736
+ self.assertEqual(request._request.headers["User-Agent"], "Django-ESI/1.0.0 (email@example.com; +https://gitlab.com/allianceauth/django-esi)")
737
737
 
738
738
  def test_existing_headers(self, requests_mocker):
739
739
  # given
@@ -743,7 +743,7 @@ class TestClientResult2(NoSocketsTestCase):
743
743
  # then
744
744
  self.assertTrue(requests_mocker.called)
745
745
  request = requests_mocker.last_request
746
- self.assertEqual(request._request.headers["User-Agent"], "django-esi v1.0.0")
746
+ self.assertEqual(request._request.headers["User-Agent"], "Django-ESI/1.0.0 (email@example.com; +https://gitlab.com/allianceauth/django-esi)")
747
747
 
748
748
 
749
749
  class TestRequestsClientPlus(NoSocketsTestCase):
@@ -764,11 +764,11 @@ class TestRequestsClientPlus(NoSocketsTestCase):
764
764
  {
765
765
  "method": "GET",
766
766
  "url": "https://esi.evetech.net/v1/status/",
767
- "headers": {"From": "dummy@example.com"},
767
+ "headers": {"From": "email@example.com"},
768
768
  }
769
769
  )
770
770
  self.assertEqual(result.future.request.headers["User-Agent"], "abc")
771
- self.assertEqual(result.future.request.headers["From"], "dummy@example.com")
771
+ self.assertEqual(result.future.request.headers["From"], "email@example.com")
772
772
 
773
773
  def test_no_user_agent(self):
774
774
  """When no user agent is defined, leave the existing header intact"""
@@ -777,14 +777,14 @@ class TestRequestsClientPlus(NoSocketsTestCase):
777
777
  {
778
778
  "method": "GET",
779
779
  "url": "https://esi.evetech.net/v1/status/",
780
- "headers": {"From": "dummy@example.com"},
780
+ "headers": {"From": "email@example.com"},
781
781
  }
782
782
  )
783
- self.assertEqual(result.future.request.headers["From"], "dummy@example.com")
783
+ self.assertEqual(result.future.request.headers["From"], "email@example.com")
784
784
  self.assertNotIn("User-Agent", result.future.request.headers)
785
785
 
786
786
 
787
- @patch(MODULE_PATH + ".__title__", "django-esi")
787
+ @patch(MODULE_PATH + ".__title__", "Django-ESI")
788
788
  @patch(MODULE_PATH + ".__version__", "1.0.0")
789
789
  @requests_mock.Mocker()
790
790
  class TestEsiClientFactoryAppText(NoSocketsTestCase):
@@ -799,88 +799,81 @@ class TestEsiClientFactoryAppText(NoSocketsTestCase):
799
799
  }
800
800
 
801
801
  @patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", None)
802
- def test_defaults(self, requests_mocker):
802
+ def test_defaults(self, requests_mocker) -> None:
803
+ # This test is not expected to hit given that ESI_USER_CONTACT_EMAIL must be set
804
+ # But here it is for completeness
805
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec)
806
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec)
807
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/v1/status/", json=self.status_response)
808
+ client = esi_client_factory()
809
+ # when
810
+ operation = client.Status.get_status()
811
+ # then
812
+ self.assertEqual(operation.future.request.headers["User-Agent"], "Django-ESI/1.0.0 (None; +https://gitlab.com/allianceauth/django-esi)")
813
+
814
+ @patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", "email@example.com")
815
+ def test_defaults_email(self, requests_mocker) -> None:
803
816
  # given
804
- requests_mocker.register_uri(
805
- "GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec
806
- )
807
- requests_mocker.register_uri(
808
- "GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec
809
- )
810
- requests_mocker.register_uri(
811
- "GET", url="https://esi.evetech.net/v1/status/", json=self.status_response
812
- )
817
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec)
818
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec)
819
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/v1/status/", json=self.status_response)
813
820
  client = esi_client_factory()
814
821
  # when
815
822
  operation = client.Status.get_status()
816
823
  # then
817
- self.assertEqual(
818
- operation.future.request.headers["User-Agent"], "django-esi v1.0.0"
819
- )
824
+ self.assertEqual(operation.future.request.headers["User-Agent"], "Django-ESI/1.0.0 (email@example.com; +https://gitlab.com/allianceauth/django-esi)")
820
825
 
821
826
  @patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", None)
822
- def test_app_text(self, requests_mocker):
827
+ def test_app_text(self, requests_mocker) -> None:
828
+ # Deprecated
829
+ # This test is not expected to hit given that ESI_USER_CONTACT_EMAIL must be set
823
830
  # given
824
- requests_mocker.register_uri(
825
- "GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec
826
- )
827
- requests_mocker.register_uri(
828
- "GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec
829
- )
830
- requests_mocker.register_uri(
831
- "GET", url="https://esi.evetech.net/v1/status/", json=self.status_response
832
- )
831
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec)
832
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec)
833
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/v1/status/", json=self.status_response)
833
834
  client = esi_client_factory(app_info_text="my-app v1.0.0")
834
835
  # when
835
836
  operation = client.Status.get_status()
836
837
  # then
837
- self.assertEqual(
838
- operation.future.request.headers["User-Agent"], "my-app v1.0.0"
839
- )
838
+ self.assertEqual(operation.future.request.headers["User-Agent"], "my-app v1.0.0 (None) Django-ESI/1.0.0 (+https://gitlab.com/allianceauth/django-esi)",)
840
839
 
841
- @patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", "dummy@example.com")
840
+ @patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", "email@example.com")
842
841
  def test_app_text_with_email(self, requests_mocker):
842
+ # Deprecated
843
843
  # given
844
- requests_mocker.register_uri(
845
- "GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec
846
- )
847
- requests_mocker.register_uri(
848
- "GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec
849
- )
850
- requests_mocker.register_uri(
851
- "GET", url="https://esi.evetech.net/v1/status/", json=self.status_response
852
- )
844
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec)
845
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec)
846
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/v1/status/", json=self.status_response)
853
847
  client = esi_client_factory(app_info_text="my-app v1.0.0")
854
848
  # when
855
849
  operation = client.Status.get_status()
856
850
  # then
857
- self.assertEqual(
858
- operation.future.request.headers["User-Agent"],
859
- "my-app v1.0.0 dummy@example.com",
860
- )
861
-
862
- @patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", "dummy@example.com")
863
- def test_defaults_with_email(self, requests_mocker):
864
- # given
865
- requests_mocker.register_uri(
866
- "GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec
867
- )
868
- requests_mocker.register_uri(
869
- "GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec
870
- )
871
- requests_mocker.register_uri(
872
- "GET", url="https://esi.evetech.net/v1/status/", json=self.status_response
873
- )
874
- client = esi_client_factory()
851
+ self.assertEqual(operation.future.request.headers["User-Agent"], "my-app v1.0.0 (email@example.com) Django-ESI/1.0.0 (+https://gitlab.com/allianceauth/django-esi)",)
852
+
853
+ @patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", "email@example.com")
854
+ def test_ua_generator(self, requests_mocker):
855
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec)
856
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec)
857
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/v1/status/", json=self.status_response)
858
+ client = esi_client_factory(ua_appname="MyApp", ua_version="1.0.0")
875
859
  # when
876
860
  operation = client.Status.get_status()
877
861
  # then
878
- self.assertEqual(
879
- operation.future.request.headers["User-Agent"],
880
- "django-esi v1.0.0 dummy@example.com",
881
- )
882
-
862
+ self.assertEqual(operation.future.request.headers["User-Agent"], "MyApp/1.0.0 (email@example.com) Django-ESI/1.0.0 (+https://gitlab.com/allianceauth/django-esi)")
863
+
864
+ @patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", "email@example.com")
865
+ def test_ua_generator_with_url(self, requests_mocker):
866
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec)
867
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec)
868
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/v1/status/", json=self.status_response)
869
+ client = esi_client_factory(ua_appname="MyApp", ua_version="1.0.0", ua_url="https://example.com")
870
+ # when
871
+ operation = client.Status.get_status()
872
+ # then
873
+ self.assertEqual(operation.future.request.headers["User-Agent"], "MyApp/1.0.0 (email@example.com; +https://example.com) Django-ESI/1.0.0 (+https://gitlab.com/allianceauth/django-esi)")
883
874
 
875
+ @patch(MODULE_PATH + ".__title__", "Django-ESI")
876
+ @patch(MODULE_PATH + ".__version__", "1.0.0")
884
877
  @requests_mock.Mocker()
885
878
  class TestEsiClientProviderAppText(NoSocketsTestCase):
886
879
  @classmethod
@@ -893,45 +886,76 @@ class TestEsiClientProviderAppText(NoSocketsTestCase):
893
886
  "start_time": "2017-01-02T12:34:56Z",
894
887
  }
895
888
 
896
- @patch(MODULE_PATH + ".__title__", "django-esi")
897
- @patch(MODULE_PATH + ".__version__", "1.0.0")
898
889
  @patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", None)
899
- def test_defaults(self, requests_mocker):
890
+ def test_defaults(self, requests_mocker) -> None:
891
+ # This test is not expected to hit given that ESI_USER_CONTACT_EMAIL must be set
892
+ # But here it is for completeness
893
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec)
894
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec)
895
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/v1/status/", json=self.status_response)
896
+ client = EsiClientProvider().client
897
+ # when
898
+ operation = client.Status.get_status()
899
+ # then
900
+ self.assertEqual(operation.future.request.headers["User-Agent"], "Django-ESI/1.0.0 (None; +https://gitlab.com/allianceauth/django-esi)")
901
+
902
+ @patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", "email@example.com")
903
+ def test_defaults_email(self, requests_mocker) -> None:
900
904
  # given
901
- requests_mocker.register_uri(
902
- "GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec
903
- )
904
- requests_mocker.register_uri(
905
- "GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec
906
- )
907
- requests_mocker.register_uri(
908
- "GET", url="https://esi.evetech.net/v1/status/", json=self.status_response
909
- )
905
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec)
906
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec)
907
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/v1/status/", json=self.status_response)
910
908
  client = EsiClientProvider().client
911
- # whem
909
+ # when
912
910
  operation = client.Status.get_status()
913
911
  # then
914
- self.assertEqual(
915
- operation.future.request.headers["User-Agent"], "django-esi v1.0.0"
916
- )
912
+ self.assertEqual(operation.future.request.headers["User-Agent"], "Django-ESI/1.0.0 (email@example.com; +https://gitlab.com/allianceauth/django-esi)")
917
913
 
918
- @patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", "dummy@example.com")
914
+ @patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", None)
915
+ def test_app_text(self, requests_mocker) -> None:
916
+ # Deprecated
917
+ # This test is not expected to hit given that ESI_USER_CONTACT_EMAIL must be set
918
+ # given
919
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec)
920
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec)
921
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/v1/status/", json=self.status_response)
922
+ client = EsiClientProvider(app_info_text="my-app v1.0.0").client
923
+ # when
924
+ operation = client.Status.get_status()
925
+ # then
926
+ self.assertEqual(operation.future.request.headers["User-Agent"], "my-app v1.0.0 (None) Django-ESI/1.0.0 (+https://gitlab.com/allianceauth/django-esi)",)
927
+
928
+ @patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", "email@example.com")
919
929
  def test_app_text_with_email(self, requests_mocker):
930
+ # Deprecated
920
931
  # given
921
- requests_mocker.register_uri(
922
- "GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec
923
- )
924
- requests_mocker.register_uri(
925
- "GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec
926
- )
927
- requests_mocker.register_uri(
928
- "GET", url="https://esi.evetech.net/v1/status/", json=self.status_response
929
- )
932
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec)
933
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec)
934
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/v1/status/", json=self.status_response)
930
935
  client = EsiClientProvider(app_info_text="my-app v1.0.0").client
931
936
  # when
932
937
  operation = client.Status.get_status()
933
938
  # then
934
- self.assertEqual(
935
- operation.future.request.headers["User-Agent"],
936
- "my-app v1.0.0 dummy@example.com",
937
- )
939
+ self.assertEqual(operation.future.request.headers["User-Agent"], "my-app v1.0.0 (email@example.com) Django-ESI/1.0.0 (+https://gitlab.com/allianceauth/django-esi)",)
940
+
941
+ @patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", "email@example.com")
942
+ def test_ua_generator(self, requests_mocker):
943
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec)
944
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec)
945
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/v1/status/", json=self.status_response)
946
+ client = EsiClientProvider(ua_appname="MyApp", ua_version="1.0.0").client
947
+ # when
948
+ operation = client.Status.get_status()
949
+ # then
950
+ self.assertEqual(operation.future.request.headers["User-Agent"], "MyApp/1.0.0 (email@example.com) Django-ESI/1.0.0 (+https://gitlab.com/allianceauth/django-esi)")
951
+
952
+ @patch(MODULE_PATH + ".app_settings.ESI_USER_CONTACT_EMAIL", "email@example.com")
953
+ def test_ua_generator_with_url(self, requests_mocker):
954
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/_latest/swagger.json", json=self.spec)
955
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/latest/swagger.json", json=self.spec)
956
+ requests_mocker.register_uri("GET", url="https://esi.evetech.net/v1/status/", json=self.status_response)
957
+ client = EsiClientProvider(ua_appname="MyApp", ua_version="1.0.0", ua_url="https://example.com").client
958
+ # when
959
+ operation = client.Status.get_status()
960
+ # then
961
+ self.assertEqual(operation.future.request.headers["User-Agent"], "MyApp/1.0.0 (email@example.com; +https://example.com) Django-ESI/1.0.0 (+https://gitlab.com/allianceauth/django-esi)")
esi/tests/test_tasks.py CHANGED
@@ -5,13 +5,16 @@ from celery import current_app as celery_app
5
5
  from django.utils.timezone import now
6
6
 
7
7
  from esi.models import CallbackRedirect, Token
8
- from esi.tasks import cleanup_callbackredirect, cleanup_token
8
+ from esi.tasks import cleanup_callbackredirect, cleanup_token, cleanup_token_subset
9
9
 
10
10
  from . import NoSocketsTestCase
11
11
  from .factories_2 import TokenFactory, CallbackRedirectFactory
12
12
 
13
+ from math import ceil
14
+
13
15
  MANAGERS_PATH = "esi.managers"
14
16
  MODELS_PATH = "esi.models"
17
+ TASKS_PATH = "esi.tasks"
15
18
 
16
19
 
17
20
  class CeleryTestCase(NoSocketsTestCase):
@@ -23,7 +26,7 @@ class CeleryTestCase(NoSocketsTestCase):
23
26
 
24
27
 
25
28
  class TestCleanupCallbackredirect(CeleryTestCase):
26
- def test_should_remove_expired(self):
29
+ def test_should_remove_expired(self) -> None:
27
30
  # given
28
31
  cb_valid = CallbackRedirectFactory()
29
32
  with patch("django.utils.timezone.now") as m:
@@ -39,7 +42,7 @@ class TestCleanupCallbackredirect(CeleryTestCase):
39
42
  @patch(MANAGERS_PATH + '.app_settings.ESI_TOKEN_VALID_DURATION', 120)
40
43
  @patch(MODELS_PATH + '.Token.refresh', spec=True)
41
44
  class TestCleanupToken(CeleryTestCase):
42
- def test_should_delete_orphaned_tokens(self, mock_token_refresh):
45
+ def test_should_delete_orphaned_tokens(self, mock_token_refresh) -> None:
43
46
  # given
44
47
  token_1 = TokenFactory(user=None)
45
48
  token_2 = TokenFactory()
@@ -49,7 +52,7 @@ class TestCleanupToken(CeleryTestCase):
49
52
  self.assertFalse(Token.objects.filter(pk=token_1.pk).exists())
50
53
  self.assertTrue(Token.objects.filter(pk=token_2.pk).exists())
51
54
 
52
- def test_should_refresh_expired_tokens_only(self, mock_token_refresh):
55
+ def test_should_refresh_expired_tokens_only(self, mock_token_refresh) -> None:
53
56
  # given
54
57
  TokenFactory()
55
58
  with patch("django.utils.timezone.now") as m:
@@ -59,3 +62,55 @@ class TestCleanupToken(CeleryTestCase):
59
62
  cleanup_token.delay()
60
63
  # then
61
64
  self.assertEqual(mock_token_refresh.call_count, 1)
65
+
66
+
67
+ class TestCleanupTokenSubset(CeleryTestCase):
68
+ @patch(MANAGERS_PATH + '.app_settings.ESI_TOKEN_VALID_DURATION', 120)
69
+ @patch(TASKS_PATH + ".refresh_or_delete_token.apply_async")
70
+ def test_should_delete_orphaned_tokens(self, mock_token_refresh) -> None:
71
+ # given
72
+ orphaned = TokenFactory(user=None)
73
+ valid = TokenFactory()
74
+ # when
75
+ cleanup_token_subset.delay()
76
+ # then
77
+ self.assertFalse(Token.objects.filter(pk=orphaned.pk).exists())
78
+ self.assertTrue(Token.objects.filter(pk=valid.pk).exists())
79
+
80
+ @patch(MANAGERS_PATH + '.app_settings.ESI_TOKEN_VALID_DURATION', 1)
81
+ @patch(TASKS_PATH + ".refresh_or_delete_token.apply_async")
82
+ def test_should_refresh_fraction_of_expired_tokens(self, mock_token_refresh) -> None:
83
+ # given
84
+ num_expired = 100
85
+ for _ in range(num_expired):
86
+ TokenFactory(refresh_token="some_token")
87
+ # patch time so all tokens are expired
88
+ with patch("django.utils.timezone.now") as m:
89
+ m.return_value = now() + timedelta(minutes=5)
90
+ # when
91
+ cleanup_token_subset.delay(fraction=10)
92
+ # then
93
+ expected_count = ceil(num_expired / 10)
94
+ self.assertEqual(mock_token_refresh.call_count, expected_count)
95
+
96
+ @patch(MANAGERS_PATH + '.app_settings.ESI_TOKEN_VALID_DURATION', 1)
97
+ @patch("esi.tasks.refresh_or_delete_token.apply_async")
98
+ def test_should_refresh_expired_tokens_only(self, mock_token_refresh) -> None:
99
+ # given
100
+ TokenFactory()
101
+ with patch("django.utils.timezone.now") as m:
102
+ m.return_value = now() - timedelta(minutes=3)
103
+ TokenFactory()
104
+ # when
105
+ cleanup_token_subset.delay()
106
+ # then
107
+ self.assertEqual(mock_token_refresh.call_count, 1)
108
+
109
+ @patch(MANAGERS_PATH + '.app_settings.ESI_TOKEN_VALID_DURATION', 1)
110
+ @patch(TASKS_PATH + ".refresh_or_delete_token.apply_async")
111
+ def test_should_log_and_exit_gracefully_with_no_tokens(self, mock_token_refresh) -> None:
112
+ # when
113
+ cleanup_token_subset.delay()
114
+ # then
115
+ self.assertEqual(Token.objects.count(), 0)
116
+ mock_token_refresh.assert_not_called()