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
esi/clients.py ADDED
@@ -0,0 +1,657 @@
1
+ import json
2
+ import logging
3
+ from timeit import default_timer
4
+ import warnings
5
+ import datetime as dt
6
+ from hashlib import md5
7
+ from time import sleep
8
+ from typing import Any
9
+ from urllib import parse as urlparse
10
+
11
+ from bravado import requests_client
12
+ from bravado.client import SwaggerClient
13
+ from bravado.exception import (
14
+ HTTPBadGateway, HTTPGatewayTimeout, HTTPServiceUnavailable, HTTPError,
15
+ )
16
+ from bravado.http_future import HttpFuture
17
+ from bravado.swagger_model import Loader
18
+ from bravado_core.response import IncomingResponse
19
+ from bravado_core.spec import CONFIG_DEFAULTS, Spec
20
+ from requests.adapters import HTTPAdapter
21
+
22
+ from django.core.cache import cache
23
+
24
+ from . import __title__, __url__, __version__, app_settings
25
+ from .errors import TokenExpiredError
26
+ from .signals import esi_request_statistics
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ _LIBRARIES_LOG_LEVEL = logging.getLevelName(app_settings.ESI_LOG_LEVEL_LIBRARIES)
31
+ logging.getLogger('swagger_spec_validator').setLevel(_LIBRARIES_LOG_LEVEL)
32
+ logging.getLogger('bravado_core').setLevel(_LIBRARIES_LOG_LEVEL)
33
+ logging.getLogger('urllib3').setLevel(_LIBRARIES_LOG_LEVEL)
34
+ logging.getLogger('bravado').setLevel(_LIBRARIES_LOG_LEVEL)
35
+
36
+ SPEC_CONFIG = {'use_models': False}
37
+ RETRY_SLEEP_SECS = 1
38
+
39
+
40
+ class CachingHttpFuture(HttpFuture):
41
+ """Extended wrapper for a FutureAdapter that returns a HTTP response
42
+ and also supports caching.
43
+
44
+ This class contains the response for an ESI request with an ESI client.
45
+ """
46
+ def _cache_key(self) -> str:
47
+ """Generate the key name used to cache responses."""
48
+ request = self.future.request
49
+ data = (
50
+ request.method
51
+ + request.url
52
+ + str(request.params)
53
+ + str(request.data)
54
+ + str(request.json)
55
+ ).encode('utf-8')
56
+ # The following hash is not used in any security context. It is only used
57
+ # to generate unique values, collisions are acceptable and "data" is not
58
+ # coming from user-generated input
59
+ str_hash = md5(data).hexdigest() # nosec B303, B303-1
60
+ return f'esi_{str_hash}'
61
+
62
+ @staticmethod
63
+ def _time_to_expiry(expires):
64
+ """Determine the seconds until a HTTP header "Expires" timestamp.
65
+
66
+ Args:
67
+ expires: HTTP response "Expires" header
68
+
69
+ Returns:
70
+ seconds until "Expires" time
71
+ """
72
+ try:
73
+ expires_dt = dt.datetime.strptime(str(expires), '%a, %d %b %Y %H:%M:%S %Z')
74
+ if expires_dt.tzinfo is None:
75
+ expires_dt = expires_dt.replace(tzinfo=dt.timezone.utc)
76
+ delta = expires_dt - dt.datetime.now(dt.timezone.utc)
77
+ return delta.total_seconds()
78
+ except ValueError:
79
+ return 0
80
+
81
+ def results(self, **kwargs) -> Any | tuple[Any, IncomingResponse]:
82
+ """Executes the request and returns the response from ESI for the current
83
+ route. Response will include all pages if there are more available.
84
+
85
+ Accepts same parameters in ``kwargs`` as :meth:`result`
86
+
87
+ Returns:
88
+ same as :meth:`result`, but for multiple pages
89
+ """
90
+ results = list()
91
+ headers = None
92
+ # preserve original value
93
+ _also_return_response = self.request_config.also_return_response
94
+ # override to always get the raw response for expiry header
95
+ self.request_config.also_return_response = True
96
+
97
+ if "page" in self.operation.params:
98
+ current_page = 1
99
+ total_pages = 1
100
+
101
+ # loop all pages and add data to output array
102
+ while current_page <= total_pages:
103
+ self.future.request.params["page"] = current_page
104
+ # will use cache if applicable
105
+ result, headers = self.result(**kwargs)
106
+ total_pages = int(headers.headers['X-Pages'])
107
+ # append to results list to be seamless to the client
108
+ results += result
109
+ current_page += 1
110
+ else: # it doesn't so just return
111
+ results, headers = self.result(**kwargs)
112
+
113
+ # restore original value
114
+ self.request_config.also_return_response = _also_return_response
115
+
116
+ # obey the output
117
+ if self.request_config.also_return_response:
118
+ return results, headers
119
+ else:
120
+ return results
121
+
122
+ def results_localized(self, languages: list = None, **kwargs) -> dict:
123
+ """Executes the request and returns the response from ESI for all default
124
+ languages and pages (if any).
125
+
126
+ Accepts same parameters in ``kwargs`` as :meth:`result` plus ``languages``
127
+
128
+ Args:
129
+ languages: (optional) list of languages to return \
130
+ instead of default languages
131
+
132
+ Returns:
133
+ Dict of all responses with the language code as keys.
134
+ """
135
+ if not languages:
136
+ my_languages = list(app_settings.ESI_LANGUAGES)
137
+ else:
138
+ my_languages = []
139
+ for lang in dict.fromkeys(languages):
140
+ if lang not in app_settings.ESI_LANGUAGES:
141
+ raise ValueError('Invalid language code: %s' % lang)
142
+ my_languages.append(lang)
143
+
144
+ return {
145
+ language: self.results(language=language, **kwargs)
146
+ for language in my_languages
147
+ }
148
+
149
+ def _send_signal(self, status_code: int, headers: dict = {}, latency: float = 0) -> None:
150
+ """
151
+ Dispatch the esi request statistics signal
152
+ """
153
+ esi_request_statistics.send(
154
+ sender=self.__class__,
155
+ operation=self.operation.path_name,
156
+ status_code=status_code,
157
+ headers=headers,
158
+ latency=latency,
159
+ bucket=""
160
+ )
161
+
162
+ def result(self, **kwargs) -> Any | tuple[Any, IncomingResponse]:
163
+ """Executes the request and returns the response from ESI. Response will
164
+ include the requested / first page only if there are more pages available.
165
+
166
+ Args:
167
+ timeout: (optional) timeout for ESI request in seconds, overwrites default
168
+ retries: (optional) max number of retries, overwrites default
169
+ language: (optional) retrieve result for specific language
170
+ ignore_cache: (optional) set to ``True`` to ignore response caching
171
+
172
+ Returns:
173
+ Response from endpoint or a tuple with response from endpoint \
174
+ and an incoming response object containing additional meta data \
175
+ including the HTTP response headers
176
+ """
177
+ if 'language' in kwargs.keys():
178
+ # this parameter is not supported by bravado, so we can't pass it on
179
+ self.future.request.params['language'] = str(kwargs.pop('language'))
180
+
181
+ if 'timeout' not in kwargs:
182
+ kwargs['timeout'] = (
183
+ app_settings.ESI_REQUESTS_CONNECT_TIMEOUT,
184
+ app_settings.ESI_REQUESTS_READ_TIMEOUT
185
+ )
186
+
187
+ ignore_cache = (
188
+ kwargs.pop('ignore_cache') if 'ignore_cache' in kwargs.keys() else False
189
+ )
190
+
191
+ if (
192
+ app_settings.ESI_CACHE_RESPONSE
193
+ and not ignore_cache
194
+ and self.future.request.method == 'GET'
195
+ and self.operation is not None
196
+ ):
197
+ result = None
198
+ response = None
199
+ cache_key = self._cache_key()
200
+ try:
201
+ cached = cache.get(cache_key)
202
+ except Exception:
203
+ cached = None
204
+ logger.warning(
205
+ "Attempt to read ESI results from cache failed", exc_info=True
206
+ )
207
+
208
+ if cached:
209
+ self._send_signal(
210
+ status_code=0
211
+ )
212
+ result, response = cached
213
+ expiry = self._time_to_expiry(str(response.headers.get('Expires')))
214
+ if expiry < 0:
215
+ logger.warning(
216
+ "cache expired by %d seconds, Forcing expiry", expiry
217
+ )
218
+ cached = False
219
+
220
+ if not cached:
221
+ result, response = self._result_with_retries(**kwargs)
222
+ if response and 'Expires' in response.headers:
223
+ expires = self._time_to_expiry(response.headers['Expires'])
224
+ if expires > 0:
225
+ try:
226
+ cache.set(cache_key, (result, response), expires)
227
+ except Exception:
228
+ logger.warning(
229
+ "Failed to write ESI result to cache", exc_info=True
230
+ )
231
+
232
+ if self.request_config.also_return_response:
233
+ return result, response
234
+ return result
235
+
236
+ elif self.operation is not None:
237
+ result, response = self._result_with_retries(**kwargs)
238
+ if self.request_config.also_return_response:
239
+ return result, response
240
+ return result
241
+
242
+ return super().result(**kwargs)
243
+
244
+ def _result_with_retries(self, **kwargs) -> tuple[Any, IncomingResponse]:
245
+ """Execute request and retry on certain HTTP errors.
246
+
247
+ ``kwargs`` are passed through to super().result()
248
+
249
+ Returns:
250
+ Tuple with response from endpoint and an incoming response object \
251
+ containing additional meta data including the HTTP response headers
252
+ """
253
+ # preserve original value
254
+ _also_return_response = self.request_config.also_return_response
255
+ # override to always get the raw response for expiry header
256
+ self.request_config.also_return_response = True
257
+
258
+ if 'retries' in kwargs.keys():
259
+ max_retries = int(kwargs.pop('retries'))
260
+ else:
261
+ max_retries = int(app_settings.ESI_SERVER_ERROR_MAX_RETRIES)
262
+ max_retries = max(0, max_retries)
263
+
264
+ retries = 0
265
+ while retries <= max_retries:
266
+ _t = default_timer()
267
+ try:
268
+ if app_settings.ESI_INFO_LOGGING_ENABLED:
269
+ params = self.future.request.params
270
+ logger.info(
271
+ 'Fetching from ESI: %s%s%s',
272
+ self.future.request.url,
273
+ f' - language {params["language"]}'
274
+ if 'language' in params else '',
275
+ f' - page {params["page"]}'
276
+ if 'page' in params else ''
277
+ )
278
+ logger.debug(
279
+ 'ESI request: %s - %s',
280
+ self.future.request.url,
281
+ self.future.request.params
282
+ )
283
+ logger.debug('ESI request headers: %s', self.future.request.headers)
284
+ result, response = super().result(**kwargs)
285
+ logger.debug('ESI response status code: %s', response.status_code)
286
+ logger.debug('ESI response headers: %s', response.headers)
287
+ if app_settings.ESI_DEBUG_RESPONSE_CONTENT_LOGGING:
288
+ logger.debug('ESI response content: %s', response.text)
289
+ break
290
+ except (HTTPBadGateway, HTTPGatewayTimeout, HTTPServiceUnavailable) as ex:
291
+ self._send_signal(
292
+ status_code=ex.status_code,
293
+ headers=ex.response.headers,
294
+ latency=default_timer() - _t
295
+ )
296
+ if retries < max_retries:
297
+ retries += 1
298
+ logger.warning(
299
+ "ESI error - %s %s - Retry: %d/%d",
300
+ self.future.request.url,
301
+ ex.status_code,
302
+ retries,
303
+ max_retries
304
+ )
305
+ wait_secs = (
306
+ app_settings.ESI_SERVER_ERROR_BACKOFF_FACTOR
307
+ * (2 ** (retries - 1))
308
+ )
309
+ sleep(wait_secs)
310
+ else:
311
+ raise ex
312
+ except HTTPError as ex:
313
+ """
314
+ Throw any other error into the signal
315
+ then just re-raise
316
+ """
317
+ self._send_signal(
318
+ status_code=ex.status_code,
319
+ headers=ex.response.headers,
320
+ latency=default_timer() - _t
321
+ )
322
+ raise ex
323
+
324
+ self._send_signal(
325
+ status_code=response.status_code,
326
+ headers=response.headers,
327
+ latency=default_timer() - _t
328
+ )
329
+ # restore original value
330
+ self.request_config.also_return_response = _also_return_response
331
+ return result, response
332
+
333
+
334
+ requests_client.HttpFuture = CachingHttpFuture
335
+
336
+
337
+ class TokenAuthenticator(requests_client.Authenticator):
338
+ """
339
+ Adds the authorization header containing access token, if specified.
340
+ Sets ESI datasource to tranquility or singularity.
341
+ """
342
+
343
+ def __init__(self, token=None, datasource=None):
344
+ host = urlparse.urlsplit(app_settings.ESI_API_URL).hostname
345
+ super().__init__(host)
346
+ self.token = token
347
+ self.datasource = datasource
348
+
349
+ def apply(self, request):
350
+ if self.token and self.token.expired:
351
+ if self.token.can_refresh:
352
+ self.token.refresh()
353
+ else:
354
+ raise TokenExpiredError()
355
+ request.headers['Authorization'] = \
356
+ 'Bearer ' + self.token.access_token if self.token else None
357
+ request.params['datasource'] = \
358
+ self.datasource or app_settings.ESI_API_DATASOURCE
359
+ return request
360
+
361
+
362
+ class RequestsClientPlus(requests_client.RequestsClient):
363
+ """RequestsClient with ability to set the user agent header for all requests"""
364
+
365
+ def __init__(
366
+ self,
367
+ ssl_verify=True,
368
+ ssl_cert=None,
369
+ future_adapter_class=requests_client.RequestsFutureAdapter,
370
+ response_adapter_class=requests_client.RequestsResponseAdapter,
371
+ ):
372
+ super().__init__(
373
+ ssl_verify, ssl_cert, future_adapter_class, response_adapter_class
374
+ )
375
+ self.user_agent = None
376
+
377
+ def request(
378
+ self, request_params, operation=None, request_config=None
379
+ ) -> HttpFuture:
380
+ if self.user_agent:
381
+ current_headers = request_params.get("headers", dict())
382
+ new_header = {"User-Agent": str(self.user_agent)}
383
+ request_params["headers"] = {**current_headers, **new_header}
384
+
385
+ return super().request(request_params, operation, request_config)
386
+
387
+
388
+ def build_cache_name(name):
389
+ """
390
+ Cache key name formatter
391
+ :param name: Name of the spec dict to cache, usually version
392
+ :return: String name for cache key
393
+ :rtype: str
394
+ """
395
+ return 'esi_swaggerspec_%s' % name
396
+
397
+
398
+ def cache_spec(name, spec):
399
+ """
400
+ Cache the spec dict
401
+ :param name: Version name
402
+ :param spec: Spec dict
403
+ :return: True if cached
404
+ """
405
+ return cache.set(
406
+ build_cache_name(name), spec, app_settings.ESI_SPEC_CACHE_DURATION
407
+ )
408
+
409
+
410
+ def build_spec_url(spec_version):
411
+ """
412
+ Generates the URL to swagger.json for the ESI version
413
+ :param spec_version: Name of the swagger spec version, like latest or v4
414
+ :return: URL to swagger.json for the requested spec version
415
+ """
416
+ return urlparse.urljoin(app_settings.ESI_API_URL, spec_version + '/swagger.json')
417
+
418
+
419
+ def get_spec(name, http_client=None, config=None):
420
+ """
421
+ :param name: Name of the revision of spec, eg latest or v4
422
+ :param http_client: Requests client used for retrieving specs
423
+ :param config: Spec configuration - see Spec.CONFIG_DEFAULTS
424
+ :return: :class:`bravado_core.spec.Spec`
425
+ """
426
+ http_client = http_client or requests_client.RequestsClient()
427
+
428
+ def load_spec():
429
+ loader = Loader(http_client)
430
+ return loader.load_spec(build_spec_url(name))
431
+
432
+ spec_dict = cache.get_or_set(
433
+ build_cache_name(name), load_spec, app_settings.ESI_SPEC_CACHE_DURATION
434
+ )
435
+ config = dict(CONFIG_DEFAULTS, **(config or {}))
436
+ return Spec.from_dict(spec_dict, build_spec_url(name), http_client, config)
437
+
438
+
439
+ def build_spec(base_version, http_client=None, **kwargs):
440
+ """
441
+ Generates the Spec used to initialize a SwaggerClient,
442
+ supporting mixed resource versions
443
+ :param http_client: :class:`bravado.requests_client.RequestsClient`
444
+ :param base_version: Version to base the spec on.
445
+ Any resource without an explicit version will be this.
446
+ :param kwargs: Explicit resource versions, by name (eg Character='v4')
447
+ :return: :class:`bravado_core.spec.Spec`
448
+ """
449
+ base_spec = get_spec(base_version, http_client=http_client, config=SPEC_CONFIG)
450
+ if kwargs:
451
+ for resource, resource_version in kwargs.items():
452
+ versioned_spec = get_spec(
453
+ resource_version, http_client=http_client, config=SPEC_CONFIG
454
+ )
455
+ try:
456
+ spec_resource = versioned_spec.resources[resource.capitalize()]
457
+ except KeyError:
458
+ raise AttributeError(
459
+ 'Resource {} not found on API revision {}'.format(
460
+ resource, resource_version
461
+ )
462
+ )
463
+ base_spec.resources[resource.capitalize()] = spec_resource
464
+ return base_spec
465
+
466
+
467
+ def read_spec(path, http_client=None):
468
+ """
469
+ Reads in a swagger spec file used to initialize a SwaggerClient
470
+ :param path: String path to local swagger spec file.
471
+ :param http_client: :class:`bravado.requests_client.RequestsClient`
472
+ :return: :class:`bravado_core.spec.Spec`
473
+ """
474
+ with open(path, encoding='utf-8') as f:
475
+ spec_dict = json.loads(f.read())
476
+
477
+ return SwaggerClient.from_spec(
478
+ spec_dict, http_client=http_client, config=SPEC_CONFIG
479
+ )
480
+
481
+
482
+ def esi_client_factory(
483
+ token=None, datasource: str = None, spec_file: str = None, version: str = None,
484
+ app_info_text: str = None, # Deprecate in favour of the following variables
485
+ ua_appname: str = None, ua_version: str = None, ua_url: str = None, **kwargs) -> SwaggerClient:
486
+ """Generate a new ESI client.
487
+
488
+ Args:
489
+ token(esi.models.Token): used to access authenticated endpoints.
490
+ datasource: Name of the ESI datasource to access.
491
+ spec_file: Absolute path to a swagger spec file to load.
492
+ version: Base ESI API version. Accepted values are 'legacy', 'latest',
493
+ ua_appname: Name of the App for generating a User-Agent,
494
+ ua_version: Version of the App for generating a User-Agent,
495
+ ua_url: (optional) URL To the Source Code or Documentation for generating a User-Agent,
496
+ kwargs: Explicit resource versions to build, in the form Character='v4'. \
497
+ Same values accepted as version.
498
+
499
+ If a spec_file is specified, specific versioning is not available.
500
+ Meaning the version and resource version kwargs are ignored in favour of the
501
+ versions available in the spec_file.
502
+
503
+ Returns:
504
+ New ESI client
505
+ """
506
+
507
+ if app_info_text is not None:
508
+ warnings.warn(
509
+ "The 'app_info_text' parameter is deprecated and will be removed in a future release. "
510
+ "Use 'ua_appname', 'ua_version', and `ua_url` to dynamically build a User-Agent instead",
511
+ DeprecationWarning,
512
+ stacklevel=2
513
+ )
514
+
515
+ if ua_appname is None or ua_version is None:
516
+ warnings.warn(
517
+ "Applications must define their own 'ua_appname' and 'ua_version' to generate a User-Agent",
518
+ DeprecationWarning,
519
+ stacklevel=2
520
+ )
521
+
522
+ if app_settings.ESI_INFO_LOGGING_ENABLED:
523
+ logger.info('Generating an ESI client...')
524
+
525
+ client = RequestsClientPlus()
526
+
527
+ from esi.helpers import pascal_case_string
528
+ sanitized_appname = pascal_case_string(__title__)
529
+
530
+ if app_info_text:
531
+ # app_info_text (email@example) Django-ESI/1.2.3 (+https://gitlab.com/allianceauth/django-esi)
532
+ # Deprecated
533
+ user_agent = f"{app_info_text} ({app_settings.ESI_USER_CONTACT_EMAIL}) {sanitized_appname}/{__version__} (+{__url__})"
534
+ elif ua_appname is None or ua_version is None:
535
+ # Django-ESI/1.2.3 () (email@example; +https://gitlab.com/allianceauth/django-esi)
536
+ # Deprecated
537
+ user_agent = f"{sanitized_appname}/{__version__} ({app_settings.ESI_USER_CONTACT_EMAIL}; +{__url__})"
538
+ else:
539
+ # AppName/1.2.3 (email@example.com) Django-ESI/1.2.3 (+https://gitlab.com/allianceauth/django-esi)
540
+ # 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)
541
+ # Preferred
542
+
543
+ # Enforce PascalCase for `ua_appname` and strip whitespace
544
+ sanitized_ua_appname = pascal_case_string(ua_appname)
545
+
546
+ user_agent = f"{sanitized_ua_appname}/{ua_version} ({app_settings.ESI_USER_CONTACT_EMAIL}{f'; +{ua_url})' if ua_url else ')'} {sanitized_appname}/{__version__} (+{__url__})"
547
+
548
+ client.user_agent = user_agent
549
+
550
+ my_http_adapter = HTTPAdapter(
551
+ pool_maxsize=app_settings.ESI_CONNECTION_POOL_MAXSIZE,
552
+ max_retries=app_settings.ESI_CONNECTION_ERROR_MAX_RETRIES
553
+ )
554
+ client.session.mount('https://', my_http_adapter)
555
+
556
+ if token or datasource:
557
+ client.authenticator = TokenAuthenticator(token=token, datasource=datasource)
558
+
559
+ api_version = version or app_settings.ESI_API_VERSION
560
+
561
+ if spec_file:
562
+ return read_spec(spec_file, http_client=client)
563
+ else:
564
+ spec = build_spec(api_version, http_client=client, **kwargs)
565
+ return SwaggerClient(spec)
566
+
567
+
568
+ def minimize_spec(spec_dict, operations=None, resources=None):
569
+ """
570
+ Trims down a source spec dict to only the operations or resources indicated.
571
+ :param spec_dict: The source spec dict to minimize.
572
+ :type spec_dict: dict
573
+ :param operations: A list of operation IDs to retain.
574
+ :type operations: list of str
575
+ :param resources: A list of resource names to retain.
576
+ :type resources: list of str
577
+ :return: Minimized swagger spec dict
578
+ :rtype: dict
579
+ """
580
+ operations = operations or []
581
+ resources = resources or []
582
+
583
+ # keep the ugly overhead for now but only add paths we need
584
+ minimized = {key: value for key, value in spec_dict.items() if key != 'paths'}
585
+ minimized['paths'] = {}
586
+
587
+ for path_name, path in spec_dict['paths'].items():
588
+ for method, data in path.items():
589
+ if (
590
+ data['operationId'] in operations
591
+ or any(tag in resources for tag in data['tags'])
592
+ ):
593
+ if path_name not in minimized['paths']:
594
+ minimized['paths'][path_name] = {}
595
+ minimized['paths'][path_name][method] = data
596
+
597
+ return minimized
598
+
599
+
600
+ class EsiClientProvider:
601
+ """Class for providing a single ESI client instance for the whole app
602
+
603
+ Args:
604
+ datasource: Name of the ESI datasource to access.
605
+ spec_file: Absolute path to a swagger spec file to load.
606
+ version: Base ESI API version. Accepted values are 'legacy', 'latest',
607
+ ua_appname: Name of the App for generating a User-Agent,
608
+ ua_version: Version of the App for generating a User-Agent,
609
+ ua_url: (optional) URL To the Source Code or Documentation for generating a User-Agent,
610
+ kwargs: Explicit resource versions to build, in the form Character='v4'. \
611
+ Same values accepted as version.
612
+
613
+ If a spec_file is specified, specific versioning is not available.
614
+ Meaning the version and resource version kwargs are ignored in favour of the
615
+ versions available in the spec_file.
616
+ """
617
+
618
+ _client = None
619
+
620
+ def __init__(
621
+ self, datasource=None, spec_file=None, version=None,
622
+ app_info_text=None, # Deprecate in favour of the following variables
623
+ ua_appname: str = None, ua_version: str = None, ua_url: str = None, **kwargs) -> None:
624
+ self._datasource = datasource
625
+ self._spec_file = spec_file
626
+ self._version = version
627
+ self._app_text = app_info_text # Deprecate in favour of the following variables
628
+ self._ua_appname = ua_appname
629
+ self._ua_version = ua_version
630
+ self._ua_url = ua_url
631
+ self._kwargs = kwargs
632
+
633
+ if app_info_text is not None:
634
+ warnings.warn(
635
+ "The 'app_info_text' parameter is deprecated and will be removed in a future release. "
636
+ "Use 'ua_appname', 'ua_version', and `ua_url` to dynamically build a User-Agent instead",
637
+ DeprecationWarning,
638
+ stacklevel=2
639
+ )
640
+
641
+ @property
642
+ def client(self) -> SwaggerClient:
643
+ if self._client is None:
644
+ self._client = esi_client_factory(
645
+ datasource=self._datasource,
646
+ spec_file=self._spec_file,
647
+ version=self._version,
648
+ app_info_text=self._app_text, # Deprecate in favour of the following variables
649
+ ua_appname=self._ua_appname,
650
+ ua_version=self._ua_version,
651
+ ua_url=self._ua_url,
652
+ **self._kwargs,
653
+ )
654
+ return self._client
655
+
656
+ def __str__(self) -> str:
657
+ return 'EsiClientProvider'