aa-rss-to-discord 2.3.32.3.3__py3-none-any.whl → 2.5.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 (34) hide show
  1. aa_rss_to_discord/__init__.py +3 -2
  2. aa_rss_to_discord/apps.py +5 -2
  3. aa_rss_to_discord/constants.py +1 -1
  4. aa_rss_to_discord/locale/cs_CZ/LC_MESSAGES/django.po +2 -2
  5. aa_rss_to_discord/locale/de/LC_MESSAGES/django.po +2 -2
  6. aa_rss_to_discord/locale/django.pot +3 -3
  7. aa_rss_to_discord/locale/es/LC_MESSAGES/django.mo +0 -0
  8. aa_rss_to_discord/locale/es/LC_MESSAGES/django.po +8 -6
  9. aa_rss_to_discord/locale/fr_FR/LC_MESSAGES/django.po +2 -2
  10. aa_rss_to_discord/locale/it_IT/LC_MESSAGES/django.po +2 -2
  11. aa_rss_to_discord/locale/ja/LC_MESSAGES/django.po +2 -2
  12. aa_rss_to_discord/locale/ko_KR/LC_MESSAGES/django.po +2 -2
  13. aa_rss_to_discord/locale/nl_NL/LC_MESSAGES/django.po +2 -2
  14. aa_rss_to_discord/locale/pl_PL/LC_MESSAGES/django.po +2 -2
  15. aa_rss_to_discord/locale/ru/LC_MESSAGES/django.po +2 -2
  16. aa_rss_to_discord/locale/sk/LC_MESSAGES/django.po +2 -2
  17. aa_rss_to_discord/locale/uk/LC_MESSAGES/django.mo +0 -0
  18. aa_rss_to_discord/locale/uk/LC_MESSAGES/django.po +9 -8
  19. aa_rss_to_discord/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  20. aa_rss_to_discord/locale/zh_Hans/LC_MESSAGES/django.po +12 -13
  21. aa_rss_to_discord/managers.py +1 -1
  22. aa_rss_to_discord/providers.py +43 -0
  23. aa_rss_to_discord/tasks.py +94 -147
  24. aa_rss_to_discord/tests/__init__.py +41 -0
  25. aa_rss_to_discord/tests/test_auth_hooks.py +49 -0
  26. aa_rss_to_discord/tests/test_managers.py +32 -0
  27. aa_rss_to_discord/tests/test_models.py +165 -0
  28. aa_rss_to_discord/tests/test_providers.py +99 -0
  29. aa_rss_to_discord/tests/test_tasks.py +236 -0
  30. {aa_rss_to_discord-2.3.32.3.3.dist-info → aa_rss_to_discord-2.5.0.dist-info}/METADATA +11 -9
  31. aa_rss_to_discord-2.5.0.dist-info/RECORD +51 -0
  32. {aa_rss_to_discord-2.3.32.3.3.dist-info → aa_rss_to_discord-2.5.0.dist-info}/WHEEL +1 -1
  33. aa_rss_to_discord-2.3.32.3.3.dist-info/RECORD +0 -44
  34. {aa_rss_to_discord-2.3.32.3.3.dist-info → aa_rss_to_discord-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,39 +3,35 @@ AA RSS To Discord Tasks
3
3
  """
4
4
 
5
5
  # Standard Library
6
- import logging
7
6
  import re
8
7
 
9
8
  # Third Party
10
9
  import feedparser
11
- from celery import shared_task
12
-
13
- # Django
14
- from django.apps import apps
10
+ from aadiscordbot.tasks import send_message
11
+ from celery import group, shared_task
15
12
 
16
13
  # Alliance Auth
17
14
  from allianceauth.services.hooks import get_extension_logger
18
15
  from allianceauth.services.tasks import QueueOnce
19
16
 
20
- # Alliance Auth (External Libs)
21
- from app_utils.logging import LoggerAddTag
22
-
23
17
  # AA RSS to Discord
24
18
  from aa_rss_to_discord import __title__
19
+ from aa_rss_to_discord.constants import USER_AGENT
25
20
  from aa_rss_to_discord.models import LastItem, RssFeeds
21
+ from aa_rss_to_discord.providers import AppLogger
26
22
 
27
- logger = LoggerAddTag(get_extension_logger(__name__), __title__)
23
+ logger = AppLogger(get_extension_logger(__name__), __title__)
28
24
 
29
25
 
30
- def remove_emoji(string):
26
+ def remove_emoji(string: str) -> str:
31
27
  """
32
28
  Removing these dumb as fuck emojis from the title string.
33
29
  Like honestly, who in the hell needs that shit?
34
30
 
35
- :param string:
36
- :type string:
37
- :return:
38
- :rtype:
31
+ :param string: Input string
32
+ :type string: str
33
+ :return: String without emojis
34
+ :rtype: str
39
35
  """
40
36
 
41
37
  emoji_pattern = re.compile(
@@ -66,142 +62,93 @@ def remove_emoji(string):
66
62
 
67
63
 
68
64
  @shared_task(**{"base": QueueOnce})
69
- def fetch_rss() -> None: # pylint: disable=too-many-statements, too-many-branches
65
+ def fetch_rss() -> None:
70
66
  """
71
- Fetch RSS feeds and post to Discord
67
+ Fetch all enabled RSS feeds and dispatch processing tasks.
72
68
 
73
- :return:
74
- :rtype:
69
+ :return: None
70
+ :rtype: None
75
71
  """
76
72
 
77
- # pylint: disable=too-many-nested-blocks
78
- if apps.is_installed(app_name="aadiscordbot"):
79
- # Third Party
80
- from aadiscordbot.tasks import ( # pylint: disable=import-outside-toplevel
81
- send_message,
82
- )
73
+ rss_feeds = RssFeeds.objects.select_enabled()
74
+ if not rss_feeds:
75
+ logger.debug("No RSS feeds found to parse.")
76
+ return
77
+
78
+ feed_ids = [f.id for f in rss_feeds]
79
+ if not feed_ids:
80
+ logger.debug("No RSS feed ids to dispatch.")
81
+ return
82
+
83
+ group(_process_feed.s(fid) for fid in feed_ids).apply_async()
84
+
85
+
86
+ @shared_task
87
+ def _process_feed(rss_feed_id: int) -> None:
88
+ """
89
+ Process a single RSS feed by fetching its latest entry and posting to Discord if it's new.
90
+
91
+ :param rss_feed_id: ID of the RSS feed to process
92
+ :type rss_feed_id: int
93
+ :return: None
94
+ :rtype: None
95
+ """
83
96
 
84
- rss_feeds = RssFeeds.objects.select_enabled()
85
-
86
- if rss_feeds:
87
- for rss_feed in rss_feeds:
88
- logger.info(msg=f'Fetching RSS Feed "{rss_feed.name}"')
89
-
90
- feed = feedparser.parse(url_file_stream_or_string=rss_feed.url)
91
-
92
- feed_entry_title = "No title"
93
- feed_entry_link = None
94
- feed_entry_time = None
95
- feed_entry_guid = None
96
- has_last_item = False
97
- last_item = None
98
- post_entry = False
99
-
100
- try:
101
- latest_entry = feed.entries[0]
102
-
103
- feed_entry_title = remove_emoji(
104
- string=latest_entry.get("title", "No title")
105
- )
106
- feed_entry_link = latest_entry.get("link", None)
107
- feed_entry_time = latest_entry.get(
108
- "published", latest_entry.updated
109
- )
110
- feed_entry_guid = latest_entry.get("id", None)
111
- except AttributeError as exc:
112
- logger.debug(
113
- msg=f'Malformed RSS feed item in feed "{rss_feed.name}". Error: {exc}'
114
- )
115
- except IndexError as exc:
116
- logger.debug(
117
- msg=f'Could not index the RSS feed "{rss_feed.name}". Error: {exc}'
118
- )
119
- else:
120
- post_entry = True
121
- has_last_item = True
122
-
123
- try:
124
- last_item = LastItem.objects.get(rss_feed=rss_feed)
125
-
126
- if (
127
- last_item
128
- and last_item.rss_item_time == feed_entry_time
129
- and last_item.rss_item_title == feed_entry_title
130
- and last_item.rss_item_link == feed_entry_link
131
- and last_item.rss_item_guid == feed_entry_guid
132
- ):
133
- logger.debug(
134
- msg=(
135
- f'News item "{feed_entry_title}" for RSS Feed '
136
- f'"{rss_feed.name}" has already been posted to your Discord'
137
- )
138
- )
139
- post_entry = False
140
- except LastItem.DoesNotExist:
141
- logger.debug(msg="This seems to be a completely new RSS feed.")
142
-
143
- has_last_item = False
144
-
145
- logger.debug(
146
- msg=(
147
- "RSS Information gathered: "
148
- f"post_entry => {post_entry}, "
149
- f'feed_entry_link => "{feed_entry_link}", '
150
- f'feed_entry_title => "{feed_entry_title}", '
151
- f"feed_entry_time => {feed_entry_time}, "
152
- f"feed_entry_guid => {feed_entry_guid}"
153
- )
154
- )
155
-
156
- if (
157
- post_entry is True
158
- and feed_entry_link is not None
159
- and feed_entry_guid is not None
160
- ):
161
- logger.info(
162
- msg=(
163
- "New entry found, posting to Discord channel "
164
- f"{rss_feed.discord_channel}"
165
- )
166
- )
167
-
168
- if has_last_item is True:
169
- # Update the last item ...
170
- last_item.rss_item_time = feed_entry_time
171
- last_item.rss_item_title = feed_entry_title
172
- last_item.rss_item_link = feed_entry_link
173
- last_item.rss_item_guid = feed_entry_guid
174
- last_item.save()
175
- else:
176
- # Set the last item ...
177
- LastItem(
178
- rss_feed=rss_feed,
179
- rss_item_time=feed_entry_time,
180
- rss_item_title=feed_entry_title,
181
- rss_item_link=feed_entry_link,
182
- rss_item_guid=feed_entry_guid,
183
- ).save()
184
-
185
- discord_message = f"**{rss_feed.name}**\n{feed_entry_link}"
186
-
187
- send_message(
188
- channel_id=rss_feed.discord_channel.channel,
189
- message=discord_message,
190
- )
191
- else:
192
- logger.debug(
193
- msg=(
194
- f'No item for feed "{rss_feed.name}" to post. '
195
- 'Missing either "post_entry" to be "True" or '
196
- 'either "feed_entry_link" or "feed_entry_guid" is "None".'
197
- )
198
- )
199
- else:
200
- logger.debug(msg="No RSS feeds found to parse.")
201
- else:
202
- logging.info(
203
- msg=(
204
- "AA Discordbot (https://github.com/pvyParts/allianceauth-discordbot) "
205
- "needs to be installed and configured."
206
- )
97
+ try:
98
+ rss_feed = RssFeeds.objects.select_related("discord_channel").get(
99
+ id=rss_feed_id
100
+ )
101
+ except RssFeeds.DoesNotExist:
102
+ logger.debug("RSS feed %s not found", rss_feed_id)
103
+ return
104
+
105
+ logger.info(f'Fetching RSS Feed "{rss_feed.name}"')
106
+ feed = feedparser.parse(rss_feed.url, agent=USER_AGENT)
107
+
108
+ latest_entry = next(iter(getattr(feed, "entries", [])), None)
109
+ if not latest_entry:
110
+ logger.debug(f'No entries found for feed "{rss_feed.name}".')
111
+ return
112
+
113
+ feed_entry_title = remove_emoji(latest_entry.get("title", "No title"))
114
+ feed_entry_link = latest_entry.get("link")
115
+ feed_entry_time = latest_entry.get("published", latest_entry.get("updated"))
116
+ feed_entry_guid = latest_entry.get("id")
117
+
118
+ last_item = LastItem.objects.filter(rss_feed=rss_feed).first()
119
+ if last_item and (
120
+ last_item.rss_item_time,
121
+ last_item.rss_item_title,
122
+ last_item.rss_item_link,
123
+ last_item.rss_item_guid,
124
+ ) == (feed_entry_time, feed_entry_title, feed_entry_link, feed_entry_guid):
125
+ logger.debug(
126
+ 'News item "%s" for RSS Feed "%s" has already been posted to your Discord',
127
+ feed_entry_title,
128
+ rss_feed.name,
207
129
  )
130
+ return
131
+
132
+ if not last_item:
133
+ logger.debug("This seems to be a completely new RSS feed: %s", rss_feed.name)
134
+
135
+ logger.info(
136
+ 'New entry found for RSS feed "%s", posting to Discord channel %s',
137
+ rss_feed.name,
138
+ rss_feed.discord_channel,
139
+ )
140
+
141
+ LastItem.objects.update_or_create(
142
+ rss_feed=rss_feed,
143
+ defaults={
144
+ "rss_item_time": feed_entry_time,
145
+ "rss_item_title": feed_entry_title,
146
+ "rss_item_link": feed_entry_link,
147
+ "rss_item_guid": feed_entry_guid,
148
+ },
149
+ )
150
+
151
+ send_message(
152
+ channel_id=rss_feed.discord_channel.channel,
153
+ message=f"**{rss_feed.name}**\n{feed_entry_link}",
154
+ )
@@ -0,0 +1,41 @@
1
+ """
2
+ Initialize the tests
3
+ """
4
+
5
+ # Standard Library
6
+ import socket
7
+
8
+ # Django
9
+ from django.test import TestCase
10
+
11
+
12
+ class SocketAccessError(Exception):
13
+ """Error raised when a test script accesses the network"""
14
+
15
+
16
+ class BaseTestCase(TestCase):
17
+ """Variation of Django's TestCase class that prevents any network use.
18
+
19
+ Example:
20
+
21
+ .. code-block:: python
22
+
23
+ class TestMyStuff(BaseTestCase):
24
+ def test_should_do_what_i_need(self): ...
25
+
26
+ """
27
+
28
+ @classmethod
29
+ def setUpClass(cls):
30
+ cls.socket_original = socket.socket
31
+ socket.socket = cls.guard
32
+ return super().setUpClass()
33
+
34
+ @classmethod
35
+ def tearDownClass(cls):
36
+ socket.socket = cls.socket_original
37
+ return super().tearDownClass()
38
+
39
+ @staticmethod
40
+ def guard(*args, **kwargs):
41
+ raise SocketAccessError("Attempted to access network")
@@ -0,0 +1,49 @@
1
+ # Standard Library
2
+ from unittest import mock
3
+
4
+ # AA RSS to Discord
5
+ from aa_rss_to_discord.auth_hooks import register_cogs
6
+ from aa_rss_to_discord.tests import BaseTestCase
7
+
8
+
9
+ class TestAuthHooks(BaseTestCase):
10
+ """
11
+ Test the auth hooks
12
+ """
13
+
14
+ def test_registers_discord_cogs_hook_correctly(self):
15
+ """
16
+ Tests that the discord cogs hook is registered correctly.
17
+
18
+ :return:
19
+ :rtype:
20
+ """
21
+
22
+ result = register_cogs()
23
+
24
+ self.assertIn("aa_rss_to_discord.discordbot.cogs.rss", result)
25
+
26
+ def test_returns_list_of_cogs(self):
27
+ """
28
+ Tests that the register_cogs function returns a list.
29
+
30
+ :return:
31
+ :rtype:
32
+ """
33
+
34
+ result = register_cogs()
35
+
36
+ self.assertIsInstance(result, list)
37
+
38
+ def test_handles_empty_hook_registration_gracefully(self):
39
+ """
40
+ Tests that the register_cogs function handles an empty hook registration gracefully.
41
+
42
+ :return:
43
+ :rtype:
44
+ """
45
+
46
+ with mock.patch("allianceauth.hooks.register", return_value=None):
47
+ result = register_cogs()
48
+
49
+ self.assertIsNotNone(result)
@@ -0,0 +1,32 @@
1
+ # AA RSS to Discord
2
+ from aa_rss_to_discord.models import RssFeeds
3
+ from aa_rss_to_discord.tests import BaseTestCase
4
+
5
+
6
+ class TestRssFeedManager(BaseTestCase):
7
+ """
8
+ Test the RSS Feed Manager
9
+ """
10
+
11
+ def test_returns_only_enabled_feeds(self):
12
+ RssFeeds.objects.create(name="Feed 1", enabled=True)
13
+ RssFeeds.objects.create(name="Feed 2", enabled=False)
14
+ RssFeeds.objects.create(name="Feed 3", enabled=True)
15
+
16
+ result = RssFeeds.objects.select_enabled()
17
+
18
+ self.assertEqual(result.count(), 2)
19
+ self.assertTrue(all(feed.enabled for feed in result))
20
+
21
+ def test_handles_no_enabled_feeds_gracefully(self):
22
+ RssFeeds.objects.create(name="Feed 1", enabled=False)
23
+ RssFeeds.objects.create(name="Feed 2", enabled=False)
24
+
25
+ result = RssFeeds.objects.select_enabled()
26
+
27
+ self.assertEqual(result.count(), 0)
28
+
29
+ def test_handles_empty_database_gracefully(self):
30
+ result = RssFeeds.objects.select_enabled()
31
+
32
+ self.assertEqual(result.count(), 0)
@@ -0,0 +1,165 @@
1
+ # Third Party
2
+ from aadiscordbot.models import Channels, Servers
3
+
4
+ # AA RSS to Discord
5
+ from aa_rss_to_discord.models import LastItem, RssFeeds
6
+ from aa_rss_to_discord.tests import BaseTestCase
7
+
8
+
9
+ class TestRssFeeds(BaseTestCase):
10
+ """
11
+ Test the RssFeeds model.
12
+ """
13
+
14
+ def test_saves_and_retrieves_rss_feed_correctly(self):
15
+ """
16
+ Tests that an RSS feed can be saved and retrieved correctly.
17
+
18
+ :return:
19
+ :rtype:
20
+ """
21
+
22
+ feed = RssFeeds.objects.create(
23
+ name="Feed 1", url="http://example.com/rss", enabled=True
24
+ )
25
+ retrieved_feed = RssFeeds.objects.get(id=feed.id)
26
+
27
+ self.assertEqual(retrieved_feed.name, "Feed 1")
28
+ self.assertEqual(retrieved_feed.url, "http://example.com/rss")
29
+ self.assertTrue(retrieved_feed.enabled)
30
+
31
+ def test_handles_null_discord_channel_gracefully(self):
32
+ """
33
+ Tests that the model handles a null discord_channel gracefully.
34
+
35
+ :return:
36
+ :rtype:
37
+ """
38
+
39
+ feed = RssFeeds.objects.create(
40
+ name="Feed 2", url="http://example.com/rss", discord_channel=None
41
+ )
42
+
43
+ self.assertIsNone(feed.discord_channel)
44
+
45
+ def test_string_representation_includes_name_and_channel(self):
46
+ """
47
+ Tests that the string representation of the RssFeeds model
48
+
49
+ :return:
50
+ :rtype:
51
+ """
52
+ server = Servers.objects.create(server=1, name="Test Server")
53
+ channel = Channels.objects.create(name="General", server=server, channel=1)
54
+ feed = RssFeeds.objects.create(name="Feed 3", discord_channel=channel)
55
+
56
+ self.assertEqual(
57
+ str(feed), 'RSS Feed "Feed 3" for channel "General" On "Test Server"'
58
+ )
59
+
60
+ def test_allows_disabling_rss_feed(self):
61
+ """
62
+ Tests that an RSS feed can be disabled.
63
+
64
+ :return:
65
+ :rtype:
66
+ """
67
+ feed = RssFeeds.objects.create(name="Feed 4", enabled=True)
68
+ feed.enabled = False
69
+ feed.save()
70
+
71
+ updated_feed = RssFeeds.objects.get(id=feed.id)
72
+
73
+ self.assertFalse(updated_feed.enabled)
74
+
75
+ def test_handles_empty_name_and_url_gracefully(self):
76
+ """
77
+ Tests that the model handles empty name and url fields gracefully.
78
+
79
+ :return:
80
+ :rtype:
81
+ """
82
+
83
+ feed = RssFeeds.objects.create(name="", url="")
84
+
85
+ self.assertEqual(feed.name, "")
86
+ self.assertEqual(feed.url, "")
87
+
88
+
89
+ class TestLastItem(BaseTestCase):
90
+ """
91
+ Test the LastItem model.
92
+ """
93
+
94
+ def test_string_representation_includes_item_title(self):
95
+ """
96
+ Tests that the string representation of the LastItem model includes the item title.
97
+
98
+ :return:
99
+ :rtype:
100
+ """
101
+
102
+ feed = RssFeeds.objects.create(name="Feed 1", url="http://example.com/rss")
103
+ item = LastItem.objects.create(
104
+ rss_feed=feed,
105
+ rss_item_title="Sample Item",
106
+ rss_item_link="http://example.com/item",
107
+ )
108
+
109
+ self.assertEqual(str(item), 'RSS Entry "Sample Item"')
110
+
111
+ def test_handles_empty_item_title_gracefully(self):
112
+ """
113
+ Tests that the model handles an empty item title gracefully.
114
+
115
+ :return:
116
+ :rtype:
117
+ """
118
+
119
+ feed = RssFeeds.objects.create(name="Feed 2", url="http://example.com/rss")
120
+ item = LastItem.objects.create(
121
+ rss_feed=feed,
122
+ rss_item_title="",
123
+ rss_item_link="http://example.com/item",
124
+ )
125
+
126
+ self.assertEqual(str(item), 'RSS Entry ""')
127
+
128
+ def test_saves_and_retrieves_last_item_correctly(self):
129
+ """
130
+ Tests that a LastItem can be saved and retrieved correctly.
131
+
132
+ :return:
133
+ :rtype:
134
+ """
135
+
136
+ feed = RssFeeds.objects.create(name="Feed 3", url="http://example.com/rss")
137
+ item = LastItem.objects.create(
138
+ rss_feed=feed,
139
+ rss_item_time="2023-10-01T12:00:00Z",
140
+ rss_item_title="Item Title",
141
+ rss_item_link="http://example.com/item",
142
+ rss_item_guid="12345",
143
+ )
144
+ retrieved_item = LastItem.objects.get(id=item.id)
145
+
146
+ self.assertEqual(retrieved_item.rss_item_time, "2023-10-01T12:00:00Z")
147
+ self.assertEqual(retrieved_item.rss_item_title, "Item Title")
148
+ self.assertEqual(retrieved_item.rss_item_link, "http://example.com/item")
149
+ self.assertEqual(retrieved_item.rss_item_guid, "12345")
150
+
151
+ def test_handles_null_guid_gracefully(self):
152
+ """
153
+ Tests that the model handles a null guid gracefully.
154
+
155
+ :return:
156
+ :rtype:
157
+ """
158
+
159
+ feed = RssFeeds.objects.create(name="Feed 4", url="http://example.com/rss")
160
+ item = LastItem.objects.create(
161
+ rss_feed=feed,
162
+ rss_item_guid="",
163
+ )
164
+
165
+ self.assertEqual(item.rss_item_guid, "")
@@ -0,0 +1,99 @@
1
+ """
2
+ Test for the providers module.
3
+ """
4
+
5
+ # Standard Library
6
+ import logging
7
+
8
+ # AA RSS to Discord
9
+ # AA Intel Tool
10
+ from aa_rss_to_discord.providers import AppLogger
11
+ from aa_rss_to_discord.tests import BaseTestCase
12
+
13
+
14
+ class TestAppLogger(BaseTestCase):
15
+ """
16
+ Test the AppLogger provider.
17
+ """
18
+
19
+ def test_adds_prefix_to_log_message(self):
20
+ """
21
+ Tests that the AppLogger correctly adds a prefix to log messages.
22
+
23
+ :return:
24
+ :rtype:
25
+ """
26
+
27
+ logger = logging.getLogger("test_logger")
28
+ app_logger = AppLogger(logger, "PREFIX")
29
+
30
+ with self.assertLogs("test_logger", level="INFO") as log:
31
+ app_logger.info("This is a test message")
32
+
33
+ self.assertIn("[PREFIX] This is a test message", log.output[0])
34
+
35
+ def test_handles_empty_prefix(self):
36
+ """
37
+ Tests that the AppLogger handles an empty prefix correctly.
38
+
39
+ :return:
40
+ :rtype:
41
+ """
42
+
43
+ logger = logging.getLogger("test_logger")
44
+ app_logger = AppLogger(logger, "")
45
+
46
+ with self.assertLogs("test_logger", level="INFO") as log:
47
+ app_logger.info("Message without prefix")
48
+
49
+ self.assertIn("Message without prefix", log.output[0])
50
+
51
+ def test_handles_non_string_prefix(self):
52
+ """
53
+ Tests that the AppLogger handles a non-string prefix correctly.
54
+
55
+ :return:
56
+ :rtype:
57
+ """
58
+
59
+ logger = logging.getLogger("test_logger")
60
+ app_logger = AppLogger(logger, 123)
61
+
62
+ with self.assertLogs("test_logger", level="INFO") as log:
63
+ app_logger.info("Message with numeric prefix")
64
+
65
+ self.assertIn("[123] Message with numeric prefix", log.output[0])
66
+
67
+ def test_handles_special_characters_in_prefix(self):
68
+ """
69
+ Tests that the AppLogger handles special characters in the prefix correctly.
70
+
71
+ :return:
72
+ :rtype:
73
+ """
74
+
75
+ logger = logging.getLogger("test_logger")
76
+ app_logger = AppLogger(logger, "!@#$%^&*()")
77
+
78
+ with self.assertLogs("test_logger", level="INFO") as log:
79
+ app_logger.info("Message with special characters in prefix")
80
+
81
+ self.assertIn(
82
+ "[!@#$%^&*()] Message with special characters in prefix", log.output[0]
83
+ )
84
+
85
+ def test_handles_empty_message(self):
86
+ """
87
+ Tests that the AppLogger handles an empty log message correctly.
88
+
89
+ :return:
90
+ :rtype:
91
+ """
92
+
93
+ logger = logging.getLogger("test_logger")
94
+ app_logger = AppLogger(logger, "PREFIX")
95
+
96
+ with self.assertLogs("test_logger", level="INFO") as log:
97
+ app_logger.info("")
98
+
99
+ self.assertIn("[PREFIX] ", log.output[0])