aa-killtracker 0.18.0a2__py3-none-any.whl → 1.0.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 (37) hide show
  1. {aa_killtracker-0.18.0a2.dist-info → aa_killtracker-1.0.0.dist-info}/METADATA +7 -8
  2. {aa_killtracker-0.18.0a2.dist-info → aa_killtracker-1.0.0.dist-info}/RECORD +33 -30
  3. killtracker/__init__.py +1 -1
  4. killtracker/admin.py +1 -1
  5. killtracker/app_settings.py +14 -7
  6. killtracker/core/discord.py +162 -0
  7. killtracker/core/helpers.py +13 -0
  8. killtracker/core/{discord_messages.py → trackers.py} +16 -75
  9. killtracker/core/{worker_shutdown.py → workers.py} +3 -4
  10. killtracker/core/{killmails.py → zkb.py} +32 -40
  11. killtracker/managers.py +1 -1
  12. killtracker/models/trackers.py +4 -5
  13. killtracker/models/webhooks.py +4 -53
  14. killtracker/signals.py +4 -4
  15. killtracker/tasks.py +47 -49
  16. killtracker/tests/core/test_discord.py +184 -0
  17. killtracker/tests/core/test_helpers.py +23 -0
  18. killtracker/tests/core/{test_discord_messages_1.py → test_tracker_1.py} +12 -39
  19. killtracker/tests/core/{test_discord_messages_2.py → test_tracker_2.py} +3 -3
  20. killtracker/tests/core/test_workers.py +49 -0
  21. killtracker/tests/core/{test_killmails.py → test_zkb.py} +58 -46
  22. killtracker/tests/models/test_killmails.py +0 -2
  23. killtracker/tests/models/test_trackers_1.py +1 -1
  24. killtracker/tests/models/test_trackers_2.py +2 -2
  25. killtracker/tests/models/test_webhooks.py +63 -0
  26. killtracker/tests/test_integration.py +26 -13
  27. killtracker/tests/test_tasks.py +68 -58
  28. killtracker/tests/test_utils.py +39 -0
  29. killtracker/tests/testdata/factories.py +1 -1
  30. killtracker/tests/testdata/helpers.py +1 -1
  31. killtracker/tests/utils.py +28 -0
  32. killtracker/exceptions.py +0 -32
  33. killtracker/tests/core/test_worker_shutdown.py +0 -34
  34. killtracker/tests/models/test_webhook.py +0 -164
  35. killtracker/tests/test_exceptions.py +0 -12
  36. {aa_killtracker-0.18.0a2.dist-info → aa_killtracker-1.0.0.dist-info}/WHEEL +0 -0
  37. {aa_killtracker-0.18.0a2.dist-info → aa_killtracker-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
1
- """Fetching killmails from ZKB."""
1
+ """Fetch killmails from zKillboard."""
2
2
 
3
3
  # pylint: disable = redefined-builtin
4
4
 
@@ -32,19 +32,22 @@ from killtracker.app_settings import (
32
32
  KILLTRACKER_STORAGE_KILLMAILS_LIFETIME,
33
33
  KILLTRACKER_ZKB_REQUEST_DELAY,
34
34
  )
35
- from killtracker.exceptions import KillmailDoesNotExist
35
+ from killtracker.core.helpers import datetime_or_none
36
36
  from killtracker.providers import esi
37
37
 
38
- logger = LoggerAddTag(get_extension_logger(__name__), __title__)
39
-
40
- ZKB_REDISQ_URL = "https://zkillredisq.stream/listen.php"
41
- ZKB_API_URL = "https://zkillboard.com/api/"
42
38
  ZKB_KILLMAIL_BASEURL = "https://zkillboard.com/kill/"
43
- REQUESTS_TIMEOUT = (5, 30)
44
- DEFAULT_429_TIMEOUT = 10
45
39
 
46
- MAIN_MINIMUM_COUNT = 2
47
- MAIN_MINIMUM_SHARE = 0.25
40
+ _KEY_RETRY_AT = "killtracker-zkb-retry-at"
41
+ _KEY_LAST_REQUEST = "killtracker-zkb-last-request"
42
+ _MAIN_MINIMUM_COUNT = 2
43
+ _MAIN_MINIMUM_SHARE = 0.25
44
+ _REQUESTS_TIMEOUT = (5, 30)
45
+ _ZKB_429_DEFAULT_TIMEOUT = 10
46
+ _ZKB_API_URL = "https://zkillboard.com/api/"
47
+ _ZKB_REDISQ_URL = "https://zkillredisq.stream/listen.php"
48
+
49
+ logger = LoggerAddTag(get_extension_logger(__name__), __title__)
50
+
48
51
 
49
52
  # TODO: Factor out logic for accessing the API to another module
50
53
 
@@ -57,6 +60,10 @@ class ZKBTooManyRequestsError(Exception):
57
60
  self.is_original = is_original
58
61
 
59
62
 
63
+ class KillmailDoesNotExist(Exception):
64
+ """Killmail does not exist in storage."""
65
+
66
+
60
67
  @dataclass
61
68
  class _KillmailBase:
62
69
  """Base class for all Killmail."""
@@ -264,8 +271,8 @@ class Killmail(_KillmailBase):
264
271
  jumps: Optional[int] = None,
265
272
  distance: Optional[float] = None,
266
273
  matching_ship_type_ids: Optional[List[int]] = None,
267
- minimum_count: int = MAIN_MINIMUM_COUNT,
268
- minimum_share: float = MAIN_MINIMUM_SHARE,
274
+ minimum_count: int = _MAIN_MINIMUM_COUNT,
275
+ minimum_share: float = _MAIN_MINIMUM_SHARE,
269
276
  ) -> "Killmail":
270
277
  """Clone this killmail and add tracker info."""
271
278
  main_ship_group = self._calc_main_attacker_ship_group(
@@ -419,45 +426,30 @@ class Killmail(_KillmailBase):
419
426
  if "," in KILLTRACKER_QUEUE_ID:
420
427
  raise ImproperlyConfigured("A queue ID must not contains commas.")
421
428
 
422
- key_retry_at = "killtracker-retry-at"
423
- if (v := cache.get(key_retry_at)) is not None:
424
- try:
425
- retry_at = dt.datetime.fromisoformat(v)
426
- except (TypeError, ValueError):
427
- cache.delete(key_retry_at)
428
- logger.warning("unable to parse timestamp of retry at")
429
- return None
430
-
431
- if retry_at > now():
432
- raise ZKBTooManyRequestsError(retry_at=retry_at, is_original=False)
433
-
434
- key_last_request = "killtracker-last-request"
435
- if (v := cache.get(key_last_request)) is not None:
436
- try:
437
- last_request = dt.datetime.fromisoformat(v)
438
- except (TypeError, ValueError):
439
- cache.delete(key_last_request)
440
- logger.warning("unable to parse timestamp of last request")
441
- last_request = now()
429
+ retry_at = datetime_or_none(cache.get(_KEY_RETRY_AT))
430
+ if retry_at is not None and retry_at > now():
431
+ raise ZKBTooManyRequestsError(retry_at=retry_at, is_original=False)
442
432
 
433
+ last_request = datetime_or_none(cache.get(_KEY_LAST_REQUEST))
434
+ if last_request is not None:
443
435
  next_slot = last_request + dt.timedelta(
444
436
  milliseconds=KILLTRACKER_ZKB_REQUEST_DELAY
445
437
  )
446
438
  seconds = (next_slot - now()).total_seconds()
447
439
  if seconds > 0:
448
- logger.debug("Waiting %f seconds to for next free slot", seconds)
440
+ logger.debug("ZKB API: Waiting %f seconds for next free slot", seconds)
449
441
  sleep(seconds)
450
442
 
451
443
  response = requests.get(
452
- ZKB_REDISQ_URL,
444
+ _ZKB_REDISQ_URL,
453
445
  params={
454
446
  "queueID": quote_plus(KILLTRACKER_QUEUE_ID),
455
447
  "ttw": KILLTRACKER_REDISQ_TTW,
456
448
  },
457
- timeout=REQUESTS_TIMEOUT,
449
+ timeout=_REQUESTS_TIMEOUT,
458
450
  headers={"User-Agent": USER_AGENT_TEXT},
459
451
  )
460
- cache.set(key_last_request, dt.datetime.isoformat(now()), timeout=30)
452
+ cache.set(_KEY_LAST_REQUEST, now(), timeout=KILLTRACKER_ZKB_REQUEST_DELAY + 30)
461
453
  logger.debug(
462
454
  "Response from ZKB API: %d %s %s",
463
455
  response.status_code,
@@ -473,9 +465,9 @@ class Killmail(_KillmailBase):
473
465
  try:
474
466
  retry_after = int(response.headers["Retry-After"])
475
467
  except KeyError:
476
- retry_after = DEFAULT_429_TIMEOUT
468
+ retry_after = _ZKB_429_DEFAULT_TIMEOUT
477
469
  retry_at = now() + dt.timedelta(seconds=retry_after)
478
- cache.set(key_retry_at, retry_at.isoformat(), timeout=retry_after + 60)
470
+ cache.set(_KEY_RETRY_AT, retry_at, timeout=retry_after + 60)
479
471
  raise ZKBTooManyRequestsError(retry_at=retry_at, is_original=True)
480
472
 
481
473
  return None
@@ -514,9 +506,9 @@ class Killmail(_KillmailBase):
514
506
  "Trying to fetch killmail from ZKB API with killmail ID %d ...",
515
507
  killmail_id,
516
508
  )
517
- url = f"{ZKB_API_URL}killID/{killmail_id}/"
509
+ url = f"{_ZKB_API_URL}killID/{killmail_id}/"
518
510
  response = requests.get(
519
- url, timeout=REQUESTS_TIMEOUT, headers={"User-Agent": USER_AGENT_TEXT}
511
+ url, timeout=_REQUESTS_TIMEOUT, headers={"User-Agent": USER_AGENT_TEXT}
520
512
  )
521
513
  response.raise_for_status()
522
514
  zkb_data = response.json()
killtracker/managers.py CHANGED
@@ -15,7 +15,7 @@ from app_utils.logging import LoggerAddTag
15
15
 
16
16
  from killtracker import __title__
17
17
  from killtracker.app_settings import KILLTRACKER_PURGE_KILLMAILS_AFTER_DAYS
18
- from killtracker.core.killmails import Killmail, _KillmailCharacter
18
+ from killtracker.core.zkb import Killmail, _KillmailCharacter
19
19
 
20
20
  logger = LoggerAddTag(get_extension_logger(__name__), __title__)
21
21
 
@@ -28,11 +28,10 @@ from app_utils.logging import LoggerAddTag
28
28
  from killtracker import __title__
29
29
  from killtracker.app_settings import KILLTRACKER_KILLMAIL_MAX_AGE_FOR_TRACKER
30
30
  from killtracker.constants import EveCategoryId, EveGroupId
31
- from killtracker.core.discord_messages import DiscordMessage
32
- from killtracker.core.killmails import Killmail
31
+ from killtracker.core.trackers import create_discord_message_from_killmail
32
+ from killtracker.core.zkb import Killmail
33
33
  from killtracker.managers import TrackerManager
34
-
35
- from .webhooks import Webhook
34
+ from killtracker.models.webhooks import Webhook
36
35
 
37
36
  logger = LoggerAddTag(get_extension_logger(__name__), __title__)
38
37
 
@@ -886,5 +885,5 @@ class Tracker(models.Model):
886
885
 
887
886
  Returns the new queue size.
888
887
  """
889
- message = DiscordMessage.from_killmail(self, killmail, intro_text)
888
+ message = create_discord_message_from_killmail(self, killmail, intro_text)
890
889
  return self.webhook.enqueue_message(message)
@@ -2,10 +2,8 @@
2
2
 
3
3
  from typing import Optional
4
4
 
5
- import dhooks_lite
6
5
  from simple_mq import SimpleMQ
7
6
 
8
- from django.core.cache import cache
9
7
  from django.db import models
10
8
  from django.utils.translation import gettext_lazy as _
11
9
 
@@ -14,10 +12,9 @@ from app_utils.allianceauth import get_redis_client
14
12
  from app_utils.logging import LoggerAddTag
15
13
  from app_utils.urls import static_file_absolute_url
16
14
 
17
- from killtracker import APP_NAME, HOMEPAGE_URL, __title__, __version__
15
+ from killtracker import __title__
18
16
  from killtracker.app_settings import KILLTRACKER_WEBHOOK_SET_AVATAR
19
- from killtracker.core.discord_messages import DiscordMessage
20
- from killtracker.exceptions import WebhookTooManyRequests
17
+ from killtracker.core.discord import DiscordMessage, send_message_to_webhook
21
18
  from killtracker.managers import WebhookManager
22
19
 
23
20
  logger = LoggerAddTag(get_extension_logger(__name__), __title__)
@@ -26,8 +23,6 @@ logger = LoggerAddTag(get_extension_logger(__name__), __title__)
26
23
  class Webhook(models.Model):
27
24
  """A webhook to receive messages"""
28
25
 
29
- HTTP_TOO_MANY_REQUESTS = 429
30
-
31
26
  class WebhookType(models.IntegerChoices):
32
27
  """A webhook type."""
33
28
 
@@ -167,53 +162,9 @@ class Webhook(models.Model):
167
162
 
168
163
  return q.clear()
169
164
 
170
- def send_message(self, message: DiscordMessage) -> dhooks_lite.WebhookResponse:
165
+ def send_message(self, message: DiscordMessage) -> int:
171
166
  """Send a message to the webhook."""
172
- timeout = cache.ttl(self._blocked_cache_key()) # type: ignore
173
- if timeout:
174
- raise WebhookTooManyRequests(timeout)
175
-
176
- hook = dhooks_lite.Webhook(
177
- url=self.url,
178
- user_agent=dhooks_lite.UserAgent(
179
- name=APP_NAME, url=HOMEPAGE_URL, version=__version__
180
- ),
181
- )
182
- response = hook.execute(
183
- content=message.content,
184
- embeds=message.embeds,
185
- username=message.username,
186
- avatar_url=message.avatar_url,
187
- wait_for_response=True,
188
- max_retries=0, # we will handle retries ourselves
189
- )
190
- logger.debug(
191
- "%s: Response from Discord for creating message from killmail %d: %s %s %s",
192
- self,
193
- message.killmail_id,
194
- response.status_code,
195
- response.headers,
196
- response.content,
197
- )
198
- if response.status_code == self.HTTP_TOO_MANY_REQUESTS:
199
- logger.error(
200
- "%s: Received too many requests error from API: %s",
201
- self,
202
- response.content,
203
- )
204
- try:
205
- retry_after = int(response.headers["Retry-After"]) + 2
206
- except (ValueError, KeyError):
207
- retry_after = WebhookTooManyRequests.DEFAULT_RESET_AFTER
208
- cache.set(
209
- key=self._blocked_cache_key(), value="BLOCKED", timeout=retry_after
210
- )
211
- raise WebhookTooManyRequests(retry_after)
212
-
213
- return response
214
-
215
- def _blocked_cache_key(self) -> str:
216
- return f"{__title__}_webhook_{self.pk}_blocked"
167
+ return send_message_to_webhook(name=self.name, url=self.url, message=message)
217
168
 
218
169
  @staticmethod
219
170
  def create_message_link(name: str, url: str) -> str:
killtracker/signals.py CHANGED
@@ -8,24 +8,24 @@ from allianceauth.services.hooks import get_extension_logger
8
8
  from app_utils.logging import LoggerAddTag
9
9
 
10
10
  from killtracker import __title__
11
- from killtracker.core import worker_shutdown
11
+ from killtracker.core import workers
12
12
 
13
13
  logger = LoggerAddTag(get_extension_logger(__name__), __title__)
14
14
 
15
15
 
16
16
  @signals.worker_ready.connect
17
17
  def worker_ready_handler(sender, **kwargs):
18
- worker_shutdown.reset(sender.hostname)
18
+ workers.state_reset(sender.hostname)
19
19
  logger.debug("worker_ready: %s", sender.hostname)
20
20
 
21
21
 
22
22
  @signals.worker_shutting_down.connect
23
23
  def worker_shutting_down_handler(sender, **kwargs):
24
- worker_shutdown.set(sender)
24
+ workers.state_set(sender)
25
25
  logger.debug("worker_shutting_down: %s", sender)
26
26
 
27
27
 
28
28
  @signals.worker_shutdown.connect
29
29
  def worker_shutdown_handler(sender, **kwargs):
30
- worker_shutdown.reset(sender.hostname)
30
+ workers.state_reset(sender.hostname)
31
31
  logger.debug("worker_shutdown: %s", sender.hostname)
killtracker/tasks.py CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  import time
4
4
 
5
- import dhooks_lite
6
5
  from celery import Task, chain, shared_task
7
6
 
8
7
  from django.db import IntegrityError
@@ -21,20 +20,20 @@ from killtracker.app_settings import (
21
20
  KILLTRACKER_GENERATE_MESSAGE_MAX_RETRIES,
22
21
  KILLTRACKER_GENERATE_MESSAGE_RETRY_COUNTDOWN,
23
22
  KILLTRACKER_MAX_KILLMAILS_PER_RUN,
23
+ KILLTRACKER_MAX_MESSAGES_SENT_PER_RUN,
24
24
  KILLTRACKER_PURGE_KILLMAILS_AFTER_DAYS,
25
25
  KILLTRACKER_RUN_TIMEOUT,
26
26
  KILLTRACKER_STORING_KILLMAILS_ENABLED,
27
27
  KILLTRACKER_TASK_OBJECTS_CACHE_TIMEOUT,
28
28
  KILLTRACKER_TASKS_TIMEOUT,
29
29
  )
30
- from killtracker.core import worker_shutdown
31
- from killtracker.core.discord_messages import DiscordMessage
32
- from killtracker.core.killmails import (
33
- Killmail,
34
- KillmailDoesNotExist,
35
- ZKBTooManyRequestsError,
30
+ from killtracker.core import workers
31
+ from killtracker.core.discord import (
32
+ DiscordMessage,
33
+ HTTPError,
34
+ WebhookRateLimitExhausted,
36
35
  )
37
- from killtracker.exceptions import WebhookTooManyRequests
36
+ from killtracker.core.zkb import Killmail, KillmailDoesNotExist, ZKBTooManyRequestsError
38
37
  from killtracker.models import EveKillmail, Tracker, Webhook
39
38
 
40
39
  logger = LoggerAddTag(get_extension_logger(__name__), __title__)
@@ -42,7 +41,8 @@ logger = LoggerAddTag(get_extension_logger(__name__), __title__)
42
41
 
43
42
  @shared_task(bind=True, base=QueueOnce, timeout=KILLTRACKER_TASKS_TIMEOUT)
44
43
  def run_killtracker(self: Task) -> int:
45
- """Try to fetch new killmails from ZKB API and start trackers.
44
+ """Fetches and processes new killmails from ZKB API
45
+ and returns how many killmails were processed.
46
46
 
47
47
  This is the main periodic task for running Killtracker.
48
48
  """
@@ -64,7 +64,7 @@ def run_killtracker(self: Task) -> int:
64
64
  if is_timed_out():
65
65
  break
66
66
 
67
- if worker_shutdown.is_shutting_down(self):
67
+ if workers.is_shutting_down(self):
68
68
  logger.debug("Aborting due to worker shutdown")
69
69
  break
70
70
 
@@ -203,54 +203,51 @@ def delete_stale_killmails() -> None:
203
203
 
204
204
 
205
205
  @shared_task(
206
- bind=True,
207
- base=QueueOnce, # celery_once locks stay intact during retries
208
- timeout=KILLTRACKER_TASKS_TIMEOUT,
209
- retry_backoff=False,
210
- max_retries=None,
206
+ bind=True, base=QueueOnce, timeout=KILLTRACKER_TASKS_TIMEOUT, max_retries=None
211
207
  )
212
208
  def send_messages_to_webhook(self: Task, webhook_pk: int) -> None:
213
- """Sends all queued messages to given Webhook."""
209
+ """Sends queued messages to a webhook.
214
210
 
215
- webhook: Webhook = Webhook.objects.get_cached(
216
- pk=webhook_pk,
217
- timeout=KILLTRACKER_TASK_OBJECTS_CACHE_TIMEOUT,
218
- )
211
+ Note: This task will retry after processing a set number of messages
212
+ to avoid running potentially forever.
213
+ """
214
+
215
+ webhook: Webhook = Webhook.objects.get(pk=webhook_pk)
219
216
  if not webhook.is_enabled:
220
217
  logger.info("%s: Webhook is disabled - aborting", webhook)
221
218
  return
222
219
 
223
- message = webhook.dequeue_message()
224
- if not message:
225
- logger.debug("%s: No more messages to send for webhook", webhook)
226
- return
220
+ for _ in range(KILLTRACKER_MAX_MESSAGES_SENT_PER_RUN):
221
+ if workers.is_shutting_down(self):
222
+ logger.debug("Aborting due to worker shutdown")
223
+ return
227
224
 
228
- try:
229
- response: dhooks_lite.WebhookResponse = webhook.send_message(message)
230
- except WebhookTooManyRequests as ex:
231
- webhook.enqueue_message(message)
232
- logger.warning(
233
- "%s: Too many requests for webhook. Blocked for %s seconds. Aborting.",
234
- webhook,
235
- ex.retry_after,
236
- )
237
- return
225
+ message = webhook.dequeue_message()
226
+ if not message:
227
+ logger.debug("%s: No more messages to send for webhook", webhook)
228
+ break
238
229
 
239
- if not response.status_ok:
240
- webhook.enqueue_message(message, is_error=True)
241
- logger.warning(
242
- "%s: Failed to send message for Killmail %d to webhook, will retry. "
243
- "HTTP status code: %d, response: %s",
244
- webhook,
245
- message.killmail_id,
246
- response.status_code,
247
- response.content,
248
- )
249
- else:
250
230
  try:
251
- message_id = response.content.get("id")
252
- except AttributeError:
253
- message_id = "?"
231
+ message_id = webhook.send_message(message)
232
+
233
+ except WebhookRateLimitExhausted as ex:
234
+ webhook.enqueue_message(message)
235
+ logger.warning(
236
+ "%s: Webhook temporarily blocked. Retrying at %s.", webhook, ex.retry_at
237
+ )
238
+ raise self.retry(eta=ex.retry_at)
239
+
240
+ except HTTPError as ex:
241
+ webhook.enqueue_message(message, is_error=True)
242
+ logger.warning(
243
+ "%s: Failed to send message for Killmail %d to webhook, will retry. "
244
+ "HTTP status code: %d",
245
+ webhook,
246
+ message.killmail_id,
247
+ ex.status_code,
248
+ )
249
+ continue
250
+
254
251
  logger.info(
255
252
  "%s: Discord message %s created for killmail %d",
256
253
  webhook,
@@ -258,7 +255,8 @@ def send_messages_to_webhook(self: Task, webhook_pk: int) -> None:
258
255
  message.killmail_id,
259
256
  )
260
257
 
261
- raise self.retry(countdown=KILLTRACKER_DISCORD_SEND_DELAY)
258
+ if webhook.messages_queued() > 0:
259
+ raise self.retry(countdown=KILLTRACKER_DISCORD_SEND_DELAY)
262
260
 
263
261
 
264
262
  @shared_task(timeout=KILLTRACKER_TASKS_TIMEOUT)
@@ -0,0 +1,184 @@
1
+ import datetime as dt
2
+ from unittest.mock import patch
3
+
4
+ import dhooks_lite
5
+ import requests_mock
6
+
7
+ from django.utils.timezone import now
8
+
9
+ from app_utils.testing import NoSocketsTestCase
10
+
11
+ from killtracker.core.discord import (
12
+ DiscordMessage,
13
+ HTTPError,
14
+ WebhookRateLimitExhausted,
15
+ _make_key_last_request,
16
+ _make_key_retry_at,
17
+ send_message_to_webhook,
18
+ )
19
+ from killtracker.tests.utils import CacheFake
20
+
21
+ MODULE_PATH = "killtracker.core.discord"
22
+
23
+
24
+ class TestDiscordMessage(NoSocketsTestCase):
25
+ def test_can_create(self):
26
+ o = DiscordMessage(content="content")
27
+ self.assertEqual(o.content, "content")
28
+
29
+ def test_should_raise_exception_when_invalid(self):
30
+ with self.assertRaises(ValueError):
31
+ DiscordMessage(username="user")
32
+
33
+ def test_can_convert_to_and_from_json_1(self):
34
+ o1 = DiscordMessage(
35
+ content="content",
36
+ )
37
+ s = o1.to_json()
38
+ o2 = DiscordMessage.from_json(s)
39
+ self.assertEqual(o1, o2)
40
+
41
+ def test_can_convert_to_and_from_json_2(self):
42
+ o1 = DiscordMessage(
43
+ avatar_url="avatar_url",
44
+ content="content",
45
+ embeds=[dhooks_lite.Embed(description="description")],
46
+ killmail_id=42,
47
+ username="username",
48
+ )
49
+ s = o1.to_json()
50
+ o2 = DiscordMessage.from_json(s)
51
+ self.assertEqual(o1, o2)
52
+
53
+
54
+ @requests_mock.Mocker()
55
+ @patch(MODULE_PATH + ".cache", new_callable=CacheFake)
56
+ class TestWebhookSendMessage(NoSocketsTestCase):
57
+ def setUp(self) -> None:
58
+ self.name = "webhook"
59
+ self.message = DiscordMessage(content="Test message")
60
+ self.url = "https://webhook.example.com/1234"
61
+ self.message_api = {
62
+ "name": "test webhook",
63
+ "type": 1,
64
+ "channel_id": "199737254929760256",
65
+ "token": "3d89bb7572e0fb30d8128367b3b1b44fecd1726de135cbe28a41f8b2f777c372ba2939e72279b94526ff5d1bd4358d65cf11",
66
+ "avatar": None,
67
+ "guild_id": "199737254929760256",
68
+ "id": "223704706495545344",
69
+ "application_id": None,
70
+ "user": {
71
+ "username": "test",
72
+ "discriminator": "7479",
73
+ "id": "190320984123768832",
74
+ "avatar": "b004ec1740a63ca06ae2e14c5cee11f3",
75
+ "public_flags": 131328,
76
+ },
77
+ }
78
+
79
+ def test_when_send_ok_returns_true(self, requests_mocker, mock_cache):
80
+ # given
81
+ requests_mocker.register_uri(
82
+ "POST", self.url, status_code=200, json=self.message_api
83
+ )
84
+ # when
85
+ got = send_message_to_webhook(
86
+ name=self.name, url=self.url, message=self.message
87
+ )
88
+ # then
89
+ self.assertEqual(got, 223704706495545344)
90
+ self.assertTrue(requests_mocker.called)
91
+
92
+ def test_should_ignore_invalid_key_for_last_request(
93
+ self, requests_mocker, mock_cache
94
+ ):
95
+ # given
96
+ mock_cache.set(_make_key_last_request(self.url), "invalid")
97
+ requests_mocker.register_uri(
98
+ "POST", self.url, status_code=200, json=self.message_api
99
+ )
100
+ # when
101
+ got = send_message_to_webhook(
102
+ name=self.name, url=self.url, message=self.message
103
+ )
104
+ # then
105
+ self.assertEqual(got, 223704706495545344)
106
+ self.assertTrue(requests_mocker.called)
107
+
108
+ def test_should_ignore_invalid_key_for_retry_at(self, requests_mocker, mock_cache):
109
+ # given
110
+ mock_cache.set(_make_key_retry_at(self.url), "invalid")
111
+ requests_mocker.register_uri(
112
+ "POST", self.url, status_code=200, json=self.message_api
113
+ )
114
+ # when
115
+ got = send_message_to_webhook(
116
+ name=self.name, url=self.url, message=self.message
117
+ )
118
+ # then
119
+ self.assertEqual(got, 223704706495545344)
120
+ self.assertTrue(requests_mocker.called)
121
+
122
+ def test_when_send_not_ok_raise_error(self, requests_mocker, mock_cache):
123
+ # given
124
+ requests_mocker.register_uri("POST", self.url, status_code=404)
125
+ # when
126
+ with self.assertRaises(HTTPError) as ctx:
127
+ send_message_to_webhook(name=self.name, url=self.url, message=self.message)
128
+ # then
129
+ self.assertEqual(ctx.exception.status_code, 404)
130
+ self.assertTrue(requests_mocker.called)
131
+
132
+ def test_raise_too_many_requests_when_received_from_api(
133
+ self, requests_mocker, mock_cache
134
+ ):
135
+ # given
136
+ requests_mocker.register_uri(
137
+ "POST",
138
+ self.url,
139
+ status_code=429,
140
+ json={
141
+ "global": False,
142
+ "message": "You are being rate limited.",
143
+ "retry_after": 2000,
144
+ },
145
+ headers={
146
+ "x-ratelimit-remaining": "5",
147
+ "x-ratelimit-reset-after": "60",
148
+ "Retry-After": "2000",
149
+ },
150
+ )
151
+ # when/then
152
+ with self.assertRaises(WebhookRateLimitExhausted) as ctx:
153
+ send_message_to_webhook(name=self.name, url=self.url, message=self.message)
154
+
155
+ self.assertTrue(ctx.exception.retry_at)
156
+
157
+ def test_too_many_requests_no_retry_value(self, requests_mocker, mock_cache):
158
+ # given
159
+ requests_mocker.register_uri(
160
+ "POST",
161
+ self.url,
162
+ status_code=429,
163
+ headers={
164
+ "x-ratelimit-remaining": "5",
165
+ "x-ratelimit-reset-after": "60",
166
+ },
167
+ )
168
+ # when/then
169
+ with self.assertRaises(WebhookRateLimitExhausted) as ctx:
170
+ send_message_to_webhook(name=self.name, url=self.url, message=self.message)
171
+
172
+ self.assertTrue(ctx.exception.retry_at)
173
+
174
+ def test_should_reraise_exception_when_not_expired(
175
+ self, requests_mocker, mock_cache
176
+ ):
177
+ # given
178
+ key = _make_key_retry_at(self.url)
179
+ mock_cache.set(key, now() + dt.timedelta(hours=1))
180
+ # when
181
+ with self.assertRaises(WebhookRateLimitExhausted) as ctx:
182
+ send_message_to_webhook(name=self.name, url=self.url, message=self.message)
183
+ # then
184
+ self.assertTrue(ctx.exception.retry_at)
@@ -0,0 +1,23 @@
1
+ import datetime as dt
2
+ from typing import Any, NamedTuple, Optional
3
+ from unittest import TestCase
4
+
5
+ from killtracker.core.helpers import datetime_or_none
6
+
7
+
8
+ class TestDatetimeOrNone(TestCase):
9
+ def test_should_return_value(self):
10
+ class Case(NamedTuple):
11
+ value: Any
12
+ want: Optional[dt.datetime]
13
+
14
+ now = dt.datetime.now()
15
+ cases = [
16
+ Case(now, now),
17
+ Case("abc", None),
18
+ Case(42, None),
19
+ Case(None, None),
20
+ ]
21
+ for tc in cases:
22
+ got = datetime_or_none(tc.value)
23
+ self.assertIs(got, tc.want)