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,1017 @@
1
+ import json
2
+ import os
3
+ from unittest.mock import MagicMock, patch
4
+ from django.contrib.auth.models import User
5
+ from django.test import TestCase
6
+ from datetime import date, timedelta
7
+
8
+ from esi.openapi_clients import ESIClientProvider
9
+ from django.core.cache import cache
10
+ from django.core.management import call_command
11
+ from django.utils import timezone
12
+ from esi.tests import NoSocketsTestCase
13
+ from httpx import RequestError, HTTPStatusError
14
+ from esi.exceptions import ESIErrorLimitException, HTTPClientError, HTTPNotModified, ESIBucketLimitException, HTTPServerError
15
+ from esi.rate_limiting import ESIRateLimits
16
+ from esi import app_settings
17
+ from esi import __title__, __url__, __version__
18
+ import httpx
19
+
20
+ from . import _generate_token, _store_as_Token
21
+ from .. import openapi_clients as oc
22
+
23
+ SPEC_PATH = os.path.join(
24
+ os.path.dirname(os.path.abspath(__file__)), "test_openapi.json"
25
+ )
26
+ MODULE_PATH_PLUGINS = 'esi.aiopenapi3.plugins'
27
+
28
+
29
+ class TestClientFunctions(TestCase):
30
+ def test_time_to_expiry_valid(self):
31
+ expires = (
32
+ timezone.now() + timedelta(seconds=120)
33
+ ).strftime('%a, %d %b %Y %H:%M:%S %Z')
34
+ ttl = oc._time_to_expiry(expires)
35
+
36
+ # this shouldnt take more that 10 seconds
37
+ self.assertGreater(ttl, 110)
38
+
39
+ def test_time_to_expiry_invalid(self):
40
+ # invalid format returns 0
41
+ self.assertEqual(oc._time_to_expiry("not-a-date"), 0)
42
+
43
+ def test_httpx_exceptions_valids(self):
44
+ self.assertTrue(
45
+ oc._httpx_exceptions(
46
+ RequestError("Bad Request")
47
+ )
48
+ )
49
+
50
+ response = MagicMock(status_code=502)
51
+ exc = HTTPStatusError("msg", request=None, response=response)
52
+
53
+ self.assertTrue(
54
+ oc._httpx_exceptions(exc)
55
+ )
56
+
57
+ response.status_code = 400
58
+ exc = HTTPStatusError("msg", request=None, response=response)
59
+
60
+ self.assertFalse(
61
+ oc._httpx_exceptions(exc)
62
+ )
63
+
64
+ self.assertFalse(
65
+ oc._httpx_exceptions(
66
+ ESIErrorLimitException(reset=10)
67
+ )
68
+ )
69
+
70
+ def test_httpx_exceptions_invalid(self):
71
+ self.assertFalse(
72
+ oc._httpx_exceptions(
73
+ "this is not an exception!"
74
+ )
75
+ )
76
+
77
+
78
+ class BuildUserAgentTests(TestCase):
79
+ app_name = "TestApp"
80
+ app_ver = "1.2.3"
81
+ app_url = "https://tests.pass"
82
+
83
+ def test_build_user_agent_with_url(self):
84
+ ua = oc._build_user_agent(self.app_name, self.app_ver, self.app_url)
85
+
86
+ expected_app_name = "TestApp"
87
+ expected_title = "DjangoEsi"
88
+
89
+ self.assertEqual(
90
+ (
91
+ f"{expected_app_name}/{self.app_ver} "
92
+ f"({app_settings.ESI_USER_CONTACT_EMAIL}{f'; +{self.app_url})'} "
93
+ f"{expected_title}/{__version__} (+{__url__})"
94
+ ),
95
+ ua
96
+ )
97
+
98
+ def test_enforce_pascal_case_for_ua_appname_with_space(self):
99
+ """
100
+ Test that the application name is converted to PascalCase in the User-Agent string when it contains spaces.
101
+
102
+ :return:
103
+ :rtype:
104
+ """
105
+
106
+ ua = oc._build_user_agent("test app", self.app_ver, self.app_url)
107
+
108
+ expected_app_name = "TestApp"
109
+ expected_title = "DjangoEsi"
110
+
111
+ self.assertEqual(
112
+ (
113
+ f"{expected_app_name}/{self.app_ver} "
114
+ f"({app_settings.ESI_USER_CONTACT_EMAIL}{f'; +{self.app_url})'} "
115
+ f"{expected_title}/{__version__} (+{__url__})"
116
+ ),
117
+ ua
118
+ )
119
+
120
+ def test_enforce_pascal_case_for_ua_appname_with_hyphen(self):
121
+ """
122
+ Test that the application name is converted to PascalCase in the User-Agent string when it contains hyphens.
123
+
124
+ :return:
125
+ :rtype:
126
+ """
127
+
128
+ ua = oc._build_user_agent("test-app", self.app_ver, self.app_url)
129
+
130
+ expected_app_name = "TestApp"
131
+ expected_title = "DjangoEsi"
132
+
133
+ self.assertEqual(
134
+ (
135
+ f"{expected_app_name}/{self.app_ver} "
136
+ f"({app_settings.ESI_USER_CONTACT_EMAIL}{f'; +{self.app_url})'} "
137
+ f"{expected_title}/{__version__} (+{__url__})"
138
+ ),
139
+ ua
140
+ )
141
+
142
+ def test_build_user_agent_without_url(self):
143
+ ua = oc._build_user_agent(self.app_name, self.app_ver)
144
+
145
+ expected_app_name = "TestApp"
146
+ expected_title = "DjangoEsi"
147
+
148
+ self.assertEqual(
149
+ (
150
+ f"{expected_app_name}/{self.app_ver} "
151
+ f"({app_settings.ESI_USER_CONTACT_EMAIL}) "
152
+ f"{expected_title}/{__version__} (+{__url__})"
153
+ ),
154
+ ua
155
+ )
156
+
157
+
158
+ class BaseEsiOperationTests(TestCase):
159
+ def setUp(self):
160
+ self.page_param = MagicMock()
161
+ self.page_param.name = "page"
162
+
163
+ self.after_param = MagicMock()
164
+ self.after_param.name = "after"
165
+
166
+ self.before_param = MagicMock()
167
+ self.before_param.name = "before"
168
+
169
+ self.data_param = MagicMock()
170
+ self.data_param.name = "data"
171
+
172
+ self.lang_param = MagicMock()
173
+ self.lang_param.name = "Accept-Language"
174
+
175
+ self.body_param = MagicMock()
176
+ self.body_param.name = "body"
177
+
178
+ self.fake_op = MagicMock()
179
+ self.fake_op.parameters = [
180
+ self.data_param,
181
+ self.lang_param
182
+ ]
183
+ self.fake_op.tags = ["test"]
184
+ self.fake_op.operationId = "fake_op"
185
+ self.fake_op.extensions = {}
186
+ self.api = MagicMock(app_name="TestApp")
187
+ self.op = oc.BaseEsiOperation(
188
+ ("GET", "/fake_op", self.fake_op, {}),
189
+ self.api
190
+ )
191
+
192
+ def test_non_unique_kwargs(self):
193
+ op_1 = self.op(data="bar")
194
+ key_1 = op_1._cache_key()
195
+ op_2 = self.op(data="foo")
196
+ key_2 = op_2._cache_key()
197
+ self.assertNotEqual(key_1, key_2)
198
+
199
+ def test_unique_kwargs(self):
200
+ op_1 = self.op(data="foo")
201
+ key_1 = op_1._cache_key()
202
+ op_2 = self.op(data="foo")
203
+ key_2 = op_2._cache_key()
204
+ self.assertEqual(key_1, key_2)
205
+
206
+ def test_extract_body(self):
207
+ test_body = "something somethng something..."
208
+ op = self.op(body=test_body)
209
+ body = op._extract_body_param()
210
+ self.assertEqual(test_body, body)
211
+
212
+ def test_extract_body_exception(self):
213
+ test_body = "something somethng something..."
214
+ self.fake_op.requestBody = False
215
+ op = self.op(body=test_body)
216
+ with self.assertRaises(ValueError):
217
+ op._extract_body_param()
218
+
219
+ def test_extract_token(self):
220
+ test_tkn = {"token": "token model goes here"}
221
+ op = self.op(token=test_tkn)
222
+ token = op._extract_token_param()
223
+ self.assertEqual(test_tkn, token)
224
+
225
+ def test_extract_token_exception_no_token_needed(self):
226
+ self.op._kwargs = {"token": "token"}
227
+ self.fake_op.security = None
228
+ with self.assertRaises(ValueError):
229
+ self.op._extract_token_param()
230
+
231
+ def test_not_page_or_cursor_param(self):
232
+ self.assertFalse(self.op._has_page_param())
233
+ self.assertFalse(self.op._has_cursor_param())
234
+
235
+ def test_has_page_param(self):
236
+ self.fake_op.parameters += [self.page_param]
237
+ op = self.op()
238
+ self.assertTrue(op._has_page_param())
239
+
240
+ def test_has_cursor_params(self):
241
+ self.fake_op.parameters = [self.after_param]
242
+ op = self.op()
243
+ self.assertTrue(op._has_cursor_param())
244
+
245
+ self.fake_op.parameters = [self.before_param]
246
+ op = self.op()
247
+ self.assertTrue(op._has_cursor_param())
248
+
249
+
250
+ class EsiOperationTests(TestCase):
251
+ def setUp(self):
252
+ self.op_mock = MagicMock()
253
+ self.op_mock.parameters = []
254
+ self.op_mock.tags = ["tag"]
255
+ self.op_mock.operationId = "opid"
256
+ self.op_mock.extensions = {}
257
+
258
+ self.op_mock_rate = MagicMock()
259
+ self.op_mock_rate.parameters = []
260
+ self.op_mock_rate.tags = ["tag"]
261
+ self.op_mock_rate.operationId = "opidRated"
262
+ self.op_mock_rate.extensions = {
263
+ "rate-limit": {
264
+ "group": "test-group",
265
+ "max-tokens": 100,
266
+ "window-size": "5m"
267
+ }
268
+ }
269
+
270
+ self.api_mock = MagicMock()
271
+ self.api_mock.app_name = "TestApp"
272
+
273
+ self.op = oc.EsiOperation(
274
+ (
275
+ "GET",
276
+ "/url",
277
+ self.op_mock,
278
+ {}
279
+ ),
280
+ self.api_mock
281
+ )
282
+
283
+ self.op_rate_rated = oc.EsiOperation(
284
+ (
285
+ "GET",
286
+ "/url",
287
+ self.op_mock_rate,
288
+ {}
289
+ ),
290
+ self.api_mock
291
+ )
292
+
293
+ @patch.object(oc.EsiOperation, "_make_request")
294
+ def test_result_and_results(self, mock_make_request):
295
+ data = {"data": "stuff"}
296
+ mock_resp = MagicMock(status_code=200, headers={"Expires": "Wed, 1 July 2099 11:00:00 GMT"})
297
+ mock_make_request.return_value = ({"Expires": "date"}, data, mock_resp)
298
+ data_resp = self.op(foo="bar").result()
299
+ self.assertEqual(data, data_resp)
300
+
301
+ def test_esi_bucket_public(self):
302
+ op = self.op_rate_rated(foo="bar")
303
+ self.assertEqual(op.bucket.window, 300)
304
+ self.assertEqual(op.bucket.slug, "test-group")
305
+ self.assertEqual(op.bucket.limit, 100)
306
+
307
+ def test_esi_bucket_limit(self):
308
+ op = self.op_rate_rated(foo="bar")
309
+ ESIRateLimits.set_bucket(op.bucket, 0)
310
+
311
+ with self.assertRaises(ESIBucketLimitException):
312
+ op.result()
313
+
314
+
315
+ class TestOpenapiClientProvider(NoSocketsTestCase):
316
+ def setUp(self):
317
+ self.app_name = "TestsApp"
318
+ self.app_ver = "1.2.3"
319
+ self.app_url = "https://tests.pass"
320
+ self.esi = ESIClientProvider(
321
+ ua_appname=self.app_name,
322
+ ua_url=self.app_url,
323
+ ua_version=self.app_ver,
324
+ compatibility_date="2020-01-01",
325
+ tags=["Status"],
326
+ spec_file=SPEC_PATH
327
+ )
328
+ cache.clear()
329
+
330
+ def test_str(self):
331
+ self.assertIn(self.esi._ua_appname, str(self.esi))
332
+ self.assertIn(self.esi._ua_version, str(self.esi))
333
+
334
+ def test_compatibilitydate_date_to_string(self):
335
+ testdate_1 = date(2024, 1, 1)
336
+ testdate_2 = date(2025, 8, 26)
337
+
338
+ self.assertEqual("2024-01-01", ESIClientProvider._date_to_string(testdate_1))
339
+ self.assertEqual("2025-08-26", ESIClientProvider._date_to_string(testdate_2))
340
+
341
+ def test_compatibility_date_as_date(self):
342
+ testdate = date(2024, 1, 1)
343
+ app_name = "TestsApp"
344
+ app_ver = "1.2.3"
345
+ app_url = "https://tests.pass"
346
+
347
+ esi = ESIClientProvider(
348
+ ua_appname=app_name,
349
+ ua_url=app_url,
350
+ ua_version=app_ver,
351
+ compatibility_date=testdate,
352
+ spec_file=SPEC_PATH
353
+ )
354
+ self.assertEqual(esi._compatibility_date, ESIClientProvider._date_to_string(testdate))
355
+
356
+
357
+ @patch.object(httpx.Client, "send")
358
+ def test_ua(self, send: MagicMock):
359
+ send.return_value = httpx.Response(
360
+ 200,
361
+ json={
362
+ "players": 1234,
363
+ "server_version": "1234",
364
+ "start_time": "2029-09-19T11:02:08Z"
365
+ },
366
+ request=httpx.Request("GET", "test"),
367
+ )
368
+
369
+ status = self.esi.client.Status.GetStatus().result()
370
+ call_args, call_kwargs = send.call_args
371
+
372
+ expected_app_name = "TestsApp"
373
+ expected_title = 'DjangoEsi'
374
+
375
+ self.assertEqual(
376
+ call_args[0].headers["user-agent"],
377
+ (
378
+ f"{expected_app_name}/{self.app_ver} "
379
+ f"({app_settings.ESI_USER_CONTACT_EMAIL}{f'; +{self.app_url})'} "
380
+ f"{expected_title}/{__version__} (+{__url__})"
381
+ )
382
+ )
383
+ self.assertEqual(status.players, 1234)
384
+
385
+ @patch(MODULE_PATH_PLUGINS + '.settings.DEBUG', True)
386
+ def test_no_tag_no_op_debug(self):
387
+ app_name = "TestsApp"
388
+ app_ver = "1.2.3"
389
+ app_url = "https://tests.pass"
390
+
391
+ esi = ESIClientProvider(
392
+ ua_appname=app_name,
393
+ ua_url=app_url,
394
+ ua_version=app_ver,
395
+ compatibility_date="2020-01-01",
396
+ spec_file=SPEC_PATH
397
+ )
398
+ self.assertIsNotNone(esi.client.Status)
399
+ self.assertIsNotNone(esi.client.Status.GetStatus)
400
+
401
+ @patch(MODULE_PATH_PLUGINS + '.settings.DEBUG', True)
402
+ def test_tag_no_op_debug(self):
403
+ app_name = "TestsApp"
404
+ app_ver = "1.2.3"
405
+ app_url = "https://tests.pass"
406
+
407
+ esi = ESIClientProvider(
408
+ ua_appname=app_name,
409
+ ua_url=app_url,
410
+ ua_version=app_ver,
411
+ compatibility_date="2020-01-01",
412
+ tags=["Status"],
413
+ spec_file=SPEC_PATH
414
+ )
415
+ self.assertIsNotNone(esi.client.Status)
416
+ self.assertIsNotNone(esi.client.Status.GetStatus)
417
+
418
+ @patch(MODULE_PATH_PLUGINS + '.settings.DEBUG', True)
419
+ def test_no_tag_op_debug(self):
420
+ app_name = "TestsApp"
421
+ app_ver = "1.2.3"
422
+ app_url = "https://tests.pass"
423
+
424
+ esi = ESIClientProvider(
425
+ ua_appname=app_name,
426
+ ua_url=app_url,
427
+ ua_version=app_ver,
428
+ compatibility_date="2020-01-01",
429
+ operations=["GetStatus"],
430
+ spec_file=SPEC_PATH
431
+ )
432
+ self.assertIsNotNone(esi.client.Status)
433
+ self.assertIsNotNone(esi.client.Status.GetStatus)
434
+
435
+ @patch(MODULE_PATH_PLUGINS + '.settings.DEBUG', False)
436
+ def test_no_tag_no_op_no_debug(self):
437
+ app_name = "TestsApp"
438
+ app_ver = "1.2.3"
439
+ app_url = "https://tests.pass"
440
+
441
+ with self.assertRaises(AttributeError):
442
+ esi = ESIClientProvider(
443
+ ua_appname=app_name,
444
+ ua_url=app_url,
445
+ ua_version=app_ver,
446
+ compatibility_date="2020-01-01",
447
+ spec_file=SPEC_PATH
448
+ )
449
+ esi.client
450
+
451
+ @patch.object(httpx.Client, "send")
452
+ def test_no_bucket(self, send: MagicMock):
453
+ self.esi = ESIClientProvider(
454
+ ua_appname=self.app_name,
455
+ ua_url=self.app_url,
456
+ ua_version=self.app_ver,
457
+ compatibility_date="2020-01-01",
458
+ tags=["Universe"],
459
+ spec_file=SPEC_PATH
460
+ )
461
+
462
+ send.return_value = httpx.Response(
463
+ 200,
464
+ json=[1, 2, 3, 4],
465
+ request=httpx.Request("GET", "test"),
466
+ )
467
+
468
+ types = self.esi.client.Universe.GetUniverseTypes().result()
469
+ self.assertEqual(len(types), 4)
470
+
471
+ @patch.object(httpx.Client, "send")
472
+ def test_etag_hit_cached(self, send: MagicMock):
473
+ etag = "'123456789abcdef123456789abcdef'"
474
+
475
+ expires = (
476
+ timezone.now() + timedelta(minutes=5)
477
+ ).strftime('%a, %d %b %Y %H:%M:%S %Z')
478
+
479
+ send.return_value = httpx.Response(
480
+ 200,
481
+ json={
482
+ "players": 1234,
483
+ "server_version": "1234",
484
+ "start_time": "2029-09-19T11:02:08Z"
485
+ },
486
+ headers={
487
+ "etag": etag,
488
+ "expires": expires
489
+ },
490
+ request=httpx.Request(
491
+ "GET",
492
+ "test",
493
+ ),
494
+ )
495
+
496
+ self.esi.client.Status.GetStatus().result()
497
+
498
+ with self.assertRaises(HTTPNotModified):
499
+ self.esi.client.Status.GetStatus().result()
500
+
501
+ @patch.object(httpx.Client, "send")
502
+ def test_etag_not_hit_cached(self, send: MagicMock):
503
+ etag = "'123456789abcdef123456789abcdef'"
504
+
505
+ expires = (
506
+ timezone.now() + timedelta(minutes=5)
507
+ ).strftime('%a, %d %b %Y %H:%M:%S %Z')
508
+
509
+ send.return_value = httpx.Response(
510
+ 200,
511
+ json={
512
+ "players": 1234,
513
+ "server_version": "1234",
514
+ "start_time": "2029-09-19T11:02:08Z"
515
+ },
516
+ headers={
517
+ "etag": etag,
518
+ "expires": expires
519
+ },
520
+ request=httpx.Request(
521
+ "GET",
522
+ "test",
523
+ ),
524
+ )
525
+
526
+ self.esi.client.Status.GetStatus().result()
527
+
528
+ result = self.esi.client.Status.GetStatus().result(use_etag=False)
529
+ self.assertEqual(result.players, 1234)
530
+
531
+ @patch.object(httpx.Client, "send")
532
+ def test_force_refresh(self, send: MagicMock):
533
+ etag = "'123456789abcdef123456789abcdef'"
534
+
535
+ expires = (
536
+ timezone.now() + timedelta(minutes=5)
537
+ ).strftime('%a, %d %b %Y %H:%M:%S %Z')
538
+
539
+ send.return_value = httpx.Response(
540
+ 200,
541
+ json={
542
+ "players": 1234,
543
+ "server_version": "1234",
544
+ "start_time": "2029-09-19T11:02:08Z"
545
+ },
546
+ headers={
547
+ "etag": etag,
548
+ "expires": expires
549
+ },
550
+ request=httpx.Request(
551
+ "GET",
552
+ "test",
553
+ ),
554
+ )
555
+
556
+ self.esi.client.Status.GetStatus().result()
557
+
558
+ result = self.esi.client.Status.GetStatus().result(force_refresh=True)
559
+ self.assertEqual(result.players, 1234)
560
+ self.assertEqual(send.call_count, 2)
561
+
562
+ @patch.object(httpx.Client, "send")
563
+ def test_404(self, send: MagicMock):
564
+ self.esi = ESIClientProvider(
565
+ ua_appname=self.app_name,
566
+ ua_url=self.app_url,
567
+ ua_version=self.app_ver,
568
+ compatibility_date="2020-01-01",
569
+ tags=["Universe"],
570
+ spec_file=SPEC_PATH
571
+ )
572
+
573
+ send.return_value = httpx.Response(
574
+ 404,
575
+ json={
576
+ "error": "error"
577
+ },
578
+ headers={
579
+ "X-RateLimit-Reset": "15",
580
+ "X-RateLimit-Remaining": "0"
581
+ },
582
+ request=httpx.Request(
583
+ "GET",
584
+ "/universe/types"
585
+ ),
586
+ )
587
+
588
+ with self.assertRaises(HTTPClientError):
589
+ self.esi.client.Universe.GetUniverseTypes().result()
590
+
591
+ @patch.object(httpx.Client, "send")
592
+ def test_420(self, send: MagicMock):
593
+ self.esi = ESIClientProvider(
594
+ ua_appname=self.app_name,
595
+ ua_url=self.app_url,
596
+ ua_version=self.app_ver,
597
+ compatibility_date="2020-01-01",
598
+ tags=["Universe"],
599
+ spec_file=SPEC_PATH
600
+ )
601
+
602
+ send.return_value = httpx.Response(
603
+ 420,
604
+ json={
605
+ "error": "error"
606
+ },
607
+ headers={
608
+ "X-RateLimit-Reset": "15",
609
+ "X-RateLimit-Remaining": "0"
610
+ },
611
+ request=httpx.Request(
612
+ "GET",
613
+ "/universe/types"
614
+ ),
615
+ )
616
+
617
+ with self.assertRaises(ESIErrorLimitException):
618
+ self.esi.client.Universe.GetUniverseTypes().result()
619
+
620
+ self.assertGreater(cache.get("esi_error_limit_reset"), 10)
621
+
622
+ @patch.object(httpx.Client, "send")
623
+ def test_420_past(self, send: MagicMock):
624
+ self.esi = ESIClientProvider(
625
+ ua_appname=self.app_name,
626
+ ua_url=self.app_url,
627
+ ua_version=self.app_ver,
628
+ compatibility_date="2020-01-01",
629
+ tags=["Universe"],
630
+ spec_file=SPEC_PATH
631
+ )
632
+
633
+ send.return_value = httpx.Response(
634
+ 420,
635
+ json={
636
+ "error": "error"
637
+ },
638
+ headers={
639
+ "X-RateLimit-Remaining": "0"
640
+ },
641
+ request=httpx.Request(
642
+ "GET",
643
+ "/universe/types"
644
+ ),
645
+ )
646
+
647
+ with self.assertRaises(ESIErrorLimitException):
648
+ self.esi.client.Universe.GetUniverseTypes().result()
649
+
650
+ self.assertIsNone(cache.get("esi_error_limit_reset"))
651
+
652
+ @patch.object(httpx.Client, "send")
653
+ def test_rate_bucket(self, send: MagicMock):
654
+ send.return_value = httpx.Response(
655
+ 200,
656
+ json={
657
+ "players": 1234,
658
+ "server_version": "1234",
659
+ "start_time": "2029-09-19T11:02:08Z"
660
+ },
661
+ headers={
662
+ "x-ratelimit-group": "status",
663
+ "x-ratelimit-used": "2",
664
+ "x-ratelimit-remaining": "598",
665
+ "x-ratelimit-limit": "600/15m",
666
+ },
667
+ request=httpx.Request(
668
+ "GET",
669
+ "/status"
670
+ ),
671
+ )
672
+ self.esi.client.Status.GetStatus().result()
673
+ self.assertEqual(
674
+ ESIRateLimits.get_bucket(self.esi.client.Status.GetStatus().bucket),
675
+ 598
676
+ )
677
+
678
+ @patch.object(httpx.Client, "send")
679
+ def test_server_error(self, send: MagicMock):
680
+ send.return_value = httpx.Response(
681
+ 520,
682
+ json={
683
+ "error": "error"
684
+ },
685
+ headers={
686
+ "x-ratelimit-group": "status",
687
+ "x-ratelimit-used": "5",
688
+ "x-ratelimit-remaining": "595",
689
+ "x-ratelimit-limit": "600/15m",
690
+ },
691
+ request=httpx.Request(
692
+ "GET",
693
+ "/status"
694
+ ),
695
+ )
696
+ with self.assertRaises(HTTPServerError):
697
+ self.esi.client.Status.GetStatus().result()
698
+
699
+ def test_minified_op_not_found(self):
700
+ with self.assertRaises(AttributeError):
701
+ self.esi.client.Universe.GetUniverseTypes()
702
+
703
+ def test_minified_op_not_found(self):
704
+ self.esi = ESIClientProvider(
705
+ ua_appname=self.app_name,
706
+ ua_url=self.app_url,
707
+ ua_version=self.app_ver,
708
+ compatibility_date="2020-01-01",
709
+ tags=["Universe"],
710
+ spec_file=SPEC_PATH
711
+ )
712
+
713
+ with self.assertRaises(AttributeError):
714
+ self.esi.client.Universe.GetUniverseAncestries()
715
+
716
+ def test_rate_bucket_found_in_spec(self):
717
+
718
+ op = self.esi.client.Status.GetStatus()
719
+
720
+ self.assertIsNotNone(op.bucket)
721
+ self.assertEqual(op.bucket.limit, 600)
722
+ self.assertEqual(op.bucket.slug, "status")
723
+ self.assertEqual(op.bucket.window, 900)
724
+
725
+ def test_rate_bucket_hit(self):
726
+ op = self.esi.client.Status.GetStatus()
727
+
728
+ ESIRateLimits.set_bucket(op.bucket, 0)
729
+
730
+ with self.assertRaises(ESIBucketLimitException):
731
+ op.result()
732
+
733
+ def test_global_limit_hit(self):
734
+ op = self.esi.client.Status.GetStatus()
735
+
736
+ cache.set("esi_error_limit_reset", 15, 15)
737
+
738
+ with self.assertRaises(ESIErrorLimitException):
739
+ op.result()
740
+
741
+ @patch.object(httpx.Client, "send")
742
+ def test_load_sync(self, send: MagicMock):
743
+ esi = ESIClientProvider(
744
+ ua_appname=self.app_name,
745
+ ua_url=self.app_url,
746
+ ua_version=self.app_ver,
747
+ compatibility_date="2020-01-01",
748
+ tags=["Status"]
749
+ )
750
+ cache.clear()
751
+ expires = (
752
+ timezone.now() + timedelta(minutes=5)
753
+ ).strftime('%a, %d %b %Y %H:%M:%S %Z')
754
+
755
+ spec = None
756
+ with open(SPEC_PATH) as f:
757
+ spec = json.load(f)
758
+
759
+ send.return_value = httpx.Response(
760
+ 200,
761
+ json=spec,
762
+ headers={
763
+ "expires": expires
764
+ },
765
+ request=httpx.Request(
766
+ "GET",
767
+ "test",
768
+ ),
769
+ )
770
+
771
+ esi.client
772
+ self.assertIsNotNone(esi.client.Status)
773
+
774
+
775
+ class TestTokenisedEndpoints(TestCase):
776
+ def setUp(self):
777
+ self.app_name = "TestsApp"
778
+ self.app_ver = "1.2.3"
779
+ self.app_url = "https://tests.pass"
780
+ self.esi = ESIClientProvider(
781
+ ua_appname=self.app_name,
782
+ ua_url=self.app_url,
783
+ ua_version=self.app_ver,
784
+ compatibility_date="2020-01-01",
785
+ tags=["Character", "Assets"],
786
+ spec_file=SPEC_PATH
787
+ )
788
+ cache.clear()
789
+
790
+ self.character_id = 1000
791
+ character_name = 'Bruce Wayne'
792
+
793
+ self.user = User.objects.create_user(
794
+ character_name,
795
+ 'abc@example.com',
796
+ 'password'
797
+ )
798
+
799
+ self.token_str = _store_as_Token(
800
+ _generate_token(
801
+ character_id=self.character_id,
802
+ character_name=character_name,
803
+ scopes=['esi-assets.read_assets.v1']
804
+ ),
805
+ self.user
806
+ )
807
+
808
+ self.token_note = _store_as_Token(
809
+ _generate_token(
810
+ character_id=self.character_id,
811
+ character_name=character_name,
812
+ scopes=['esi-characters.read_notifications.v1']
813
+ ),
814
+ self.user
815
+ )
816
+
817
+ self.resp_note = httpx.Response(
818
+ 200,
819
+ json=[{
820
+ "is_read": None,
821
+ "notification_id": 123456,
822
+ "sender_id": 12345,
823
+ "sender_type": "corporation",
824
+ "text": "text...",
825
+ "timestamp": "2024-11-25T05:37:00Z",
826
+ "type": "CorpAllBillMsg"
827
+ },
828
+ {
829
+ "is_read": None,
830
+ "notification_id": 123457,
831
+ "sender_id": 123456,
832
+ "sender_type": "corporation",
833
+ "text": "text...",
834
+ "timestamp": "2024-11-25T01:43:00Z",
835
+ "type": "MoonminingExtractionStarted"
836
+ },
837
+ ],
838
+ headers={
839
+ "x-ratelimit-group": "char-notification",
840
+ "x-ratelimit-used": "5",
841
+ "x-ratelimit-remaining": "595",
842
+ "x-ratelimit-limit": "600/15m",
843
+ },
844
+ request=httpx.Request(
845
+ "GET",
846
+ "/characters/{character_id}/notifications"
847
+ ),
848
+ )
849
+
850
+ self.resp_asset = httpx.Response(
851
+ 200,
852
+ json=[
853
+ {
854
+ "is_blueprint_copy": None,
855
+ "is_singleton": False,
856
+ "item_id": 12346,
857
+ "location_flag": "Hangar",
858
+ "location_id": 60003760,
859
+ "location_type": "station",
860
+ "quantity": 999,
861
+ "type_id": 3764
862
+ },
863
+ {
864
+ "is_blueprint_copy": None,
865
+ "is_singleton": False,
866
+ "item_id": 12345,
867
+ "location_flag": "Hangar",
868
+ "location_id": 60003760 ,
869
+ "location_type": "station",
870
+ "quantity": 999,
871
+ "type_id": 11567
872
+ }
873
+ ],
874
+ headers={
875
+ "x-esi-error-limit-remain": "100",
876
+ "x-esi-error-limit-reset": "60",
877
+ "x-pages": "5"
878
+ },
879
+ request=httpx.Request(
880
+ "GET",
881
+ "/characters/{character_id}/notifications"
882
+ ),
883
+ )
884
+
885
+ @patch.object(httpx.Client, "send")
886
+ def test_no_token_provided(self, send: MagicMock):
887
+ send.return_value = self.resp_note
888
+ with self.assertRaises(ValueError):
889
+ self.esi.client.Character.GetCharactersCharacterIdNotifications(
890
+ character_id=self.character_id
891
+ ).result()
892
+ # Didn't hit ESI
893
+ self.assertEqual(send.call_count, 0)
894
+
895
+ @patch.object(httpx.Client, "send")
896
+ def test_good_token_provided(self, send: MagicMock):
897
+ send.return_value = self.resp_note
898
+ results = self.esi.client.Character.GetCharactersCharacterIdNotifications(
899
+ character_id=self.character_id,
900
+ token=self.token_note
901
+ ).result()
902
+ self.assertEqual(send.call_count, 1)
903
+ self.assertEqual(len(results), 2)
904
+
905
+ @patch.object(httpx.Client, "send")
906
+ def test_bad_token_provided(self, send: MagicMock):
907
+ send.return_value = self.resp_note
908
+ with self.assertRaises(ValueError):
909
+ self.esi.client.Character.GetCharactersCharacterIdNotifications(
910
+ character_id=self.character_id,
911
+ token=self.token_str
912
+ ).result()
913
+ # Didn't hit ESI
914
+ self.assertEqual(send.call_count, 0)
915
+
916
+ @patch.object(httpx.Client, "send")
917
+ def test_token_pages(self, send: MagicMock):
918
+ send.return_value = self.resp_asset
919
+ results = self.esi.client.Assets.GetCharactersCharacterIdAssets(
920
+ character_id=self.character_id,
921
+ token=self.token_str
922
+ ).results()
923
+ self.assertEqual(send.call_count, 5)
924
+ self.assertEqual(len(results), 10)
925
+
926
+ class TestTokenisedEndpoints(TestCase):
927
+ def setUp(self):
928
+ cache.clear()
929
+ self.app_name = "TestsApp"
930
+ self.app_ver = "1.2.3"
931
+ self.app_url = "https://tests.pass"
932
+ spec = {}
933
+ with open(SPEC_PATH) as f:
934
+ spec = json.load(f)
935
+
936
+ self.resp = httpx.Response(
937
+ 200,
938
+ json=spec,
939
+ headers={
940
+ "x-esi-error-limit-remain": "100",
941
+ "x-esi-error-limit-reset": "60"
942
+ },
943
+ request=httpx.Request(
944
+ "GET",
945
+ "https://esi.evetech.net/meta/openapi.json"
946
+ ),
947
+ )
948
+
949
+ @patch.object(httpx.Client, "get")
950
+ def test_load_spec(self, get: MagicMock):
951
+ get.return_value = self.resp
952
+
953
+ esi = ESIClientProvider(
954
+ ua_appname=self.app_name,
955
+ ua_url=self.app_url,
956
+ ua_version=self.app_ver,
957
+ compatibility_date="2020-01-01",
958
+ tags=["Character", "Assets"],
959
+ )
960
+ esi.client
961
+ self.assertEqual(get.call_count, 1)
962
+
963
+ esi2 = ESIClientProvider(
964
+ ua_appname=self.app_name,
965
+ ua_url=self.app_url,
966
+ ua_version=self.app_ver,
967
+ compatibility_date="2020-10-10",
968
+ tags=["status"],
969
+ )
970
+ esi2.client
971
+ self.assertEqual(get.call_count, 2)
972
+
973
+ esi3 = ESIClientProvider(
974
+ ua_appname=self.app_name,
975
+ ua_url=self.app_url,
976
+ ua_version=self.app_ver,
977
+ compatibility_date="2020-01-01",
978
+ tags=["Character"],
979
+ )
980
+ esi3.client
981
+ self.assertEqual(get.call_count, 2)
982
+
983
+ esi4 = ESIClientProvider(
984
+ ua_appname=self.app_name,
985
+ ua_url=self.app_url,
986
+ ua_version=self.app_ver,
987
+ compatibility_date="2020-10-10",
988
+ tags=["Assets"],
989
+ )
990
+ esi4.client
991
+ self.assertEqual(get.call_count, 2)
992
+
993
+ @patch.object(httpx.Client, "get")
994
+ def test_purge_cache_load_spec(self, get: MagicMock):
995
+ get.return_value = self.resp
996
+
997
+ esi = ESIClientProvider(
998
+ ua_appname=self.app_name,
999
+ ua_url=self.app_url,
1000
+ ua_version=self.app_ver,
1001
+ compatibility_date="2020-01-01",
1002
+ tags=["Character", "Assets"],
1003
+ )
1004
+ esi.client
1005
+ self.assertEqual(get.call_count, 1)
1006
+
1007
+ call_command("esi_clear_spec_cache")
1008
+
1009
+ esi2 = ESIClientProvider(
1010
+ ua_appname=self.app_name,
1011
+ ua_url=self.app_url,
1012
+ ua_version=self.app_ver,
1013
+ compatibility_date="2020-01-01",
1014
+ tags=["Character", "Assets"],
1015
+ )
1016
+ esi2.client
1017
+ self.assertEqual(get.call_count, 2)