fdc-shared-kernel 0.0.3__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 (33) hide show
  1. fdc_shared_kernel-0.0.3.dist-info/METADATA +159 -0
  2. fdc_shared_kernel-0.0.3.dist-info/RECORD +33 -0
  3. fdc_shared_kernel-0.0.3.dist-info/WHEEL +5 -0
  4. fdc_shared_kernel-0.0.3.dist-info/top_level.txt +1 -0
  5. shared_kernel/__init__.py +0 -0
  6. shared_kernel/config/__init__.py +49 -0
  7. shared_kernel/database/__init__.py +43 -0
  8. shared_kernel/exceptions/__init__.py +7 -0
  9. shared_kernel/exceptions/configuration_exceptions.py +20 -0
  10. shared_kernel/exceptions/custom_exceptions.py +14 -0
  11. shared_kernel/exceptions/data_validation_exceptions.py +26 -0
  12. shared_kernel/exceptions/http_exceptions.py +60 -0
  13. shared_kernel/exceptions/infrastructure_exceptions.py +26 -0
  14. shared_kernel/exceptions/operational_exceptions.py +26 -0
  15. shared_kernel/exceptions/security_exceptions.py +26 -0
  16. shared_kernel/interfaces/__init__.py +0 -0
  17. shared_kernel/interfaces/databus.py +73 -0
  18. shared_kernel/logger/__init__.py +75 -0
  19. shared_kernel/messaging/__init__.py +1 -0
  20. shared_kernel/messaging/nats_databus.py +171 -0
  21. shared_kernel/models/__init__.py +0 -0
  22. shared_kernel/tests/__init__.py +0 -0
  23. shared_kernel/tests/config/test_config.py +35 -0
  24. shared_kernel/tests/logger/test_logger.py +48 -0
  25. shared_kernel/tests/messaging/test_nats_interface.py +36 -0
  26. shared_kernel/tests/utils/test_data_validators.py +18 -0
  27. shared_kernel/tests/utils/test_date_format_utils.py +20 -0
  28. shared_kernel/tests/utils/test_string_utils.py +28 -0
  29. shared_kernel/tests/utils/utils.py +618 -0
  30. shared_kernel/utils/__init__.py +3 -0
  31. shared_kernel/utils/data_validators_utils.py +15 -0
  32. shared_kernel/utils/date_format_utils.py +13 -0
  33. shared_kernel/utils/string_utils.py +28 -0
@@ -0,0 +1,171 @@
1
+ import json
2
+ import logging
3
+ from nats.aio.client import Client as NATS
4
+ from nats.js.api import ConsumerConfig, DeliverPolicy, StreamConfig
5
+ from typing import Callable, Any, List, Union
6
+
7
+
8
+ class NATSDataBus:
9
+ """
10
+ A NATS Interface class to handle both standard NATS and JetStream operations.
11
+ """
12
+
13
+ _instance = None
14
+
15
+ def __new__(cls, *args, **kwargs):
16
+ if cls._instance is None:
17
+ cls._instance = super(NATSDataBus, cls).__new__(cls)
18
+ return cls._instance
19
+
20
+ def __init__(self, servers: str = None):
21
+ """
22
+ Initialize the NATSInterface.
23
+
24
+ Args:
25
+ servers (str): A string containing the NATS server URLs.
26
+ """
27
+ if not hasattr(self, "initialized"): # to prevent reinitialization
28
+ super().__init__()
29
+ self.nc = NATS()
30
+ self.servers = servers
31
+ self.connected = False
32
+ self.js = None # JetStream context
33
+ self.initialized = True
34
+
35
+ async def make_connection(self):
36
+ """
37
+ Connect to the NATS server.
38
+ """
39
+ if not self.connected:
40
+ await self.nc.connect(servers=self.servers)
41
+ self.js = self.nc.jetstream(timeout=10)
42
+ self.connected = True
43
+
44
+ async def close_connection(self):
45
+ """
46
+ Close the connection to the NATS server.
47
+ """
48
+ try:
49
+ if self.connected:
50
+ await self.nc.close()
51
+ self.connected = False
52
+ except Exception as e:
53
+ raise e
54
+
55
+ async def create_stream(self, topics: List[Any]):
56
+ """
57
+ Create a stream for topics to persist the messages
58
+
59
+ Args:
60
+ topics (List): The messages in this topic with be persisted.
61
+ """
62
+ try:
63
+ stream_name = "sample-stream-1"
64
+ stream_config = StreamConfig(
65
+ name=stream_name,
66
+ subjects=topics,
67
+ max_age=600, # retain messages for 10 mins
68
+ )
69
+ await self.js.add_stream(stream_config)
70
+ except Exception as e:
71
+ raise e
72
+
73
+ async def publish_event(
74
+ self, topic: str, event_payload: dict
75
+ ) -> Union[bool, Exception]:
76
+ """
77
+ Publish a message to a JetStream subject.
78
+
79
+ Args:
80
+ topic (str): The topic to publish the message to.
81
+ event_payload (dict): The message to be published.
82
+
83
+ Returns:
84
+ bool: True if the event was published successfully.
85
+ """
86
+ try:
87
+
88
+ ack = await self.js.publish(
89
+ topic, json.dumps(event_payload).encode("utf-8")
90
+ )
91
+ logging.info(
92
+ f"Published event '{event_payload.get('event_name')}' to topic '{topic}', ack: {ack}"
93
+ )
94
+ return True
95
+ except Exception as e:
96
+ logging.error(
97
+ f"Failed to publish event '{event_payload.get('event_name')}': {str(e)}",
98
+ exc_info=True,
99
+ )
100
+ raise e
101
+
102
+ async def request_event(
103
+ self, topic: str, event_payload: str, timeout: float = 10.0
104
+ ) -> Union[dict, Exception]:
105
+ """
106
+ Send a request and wait for a response.
107
+
108
+ Args:
109
+ topic (str): The topic to publish the message to.
110
+ event_payload (dict): The message to be published.
111
+ timeout (float): The timeout for the request.
112
+
113
+ Returns:
114
+ dict: The response message.
115
+ """
116
+ try:
117
+ response = await self.nc.request(
118
+ topic, json.dumps(event_payload).encode("utf-8"), timeout=timeout
119
+ )
120
+ return json.loads(response.data.decode("utf-8"))
121
+ except Exception as e:
122
+ logging.error(f"Failed to request topic '{topic}': {e}", exc_info=True)
123
+ raise e
124
+
125
+ async def subscribe_async_event(
126
+ self, topic: str, callback: Callable[[Any], None], durable_name: str
127
+ ):
128
+ """
129
+ Subscribe to a JetStream subject with a durable consumer and process messages asynchronously.
130
+
131
+ Args:
132
+ topic: The topic to subscribe to.
133
+ callback: A callback function to handle received messages.
134
+ durable_name: The durable name for the subscription/consumer. (give a unique name for each consumer)
135
+ """
136
+ try:
137
+ await self.create_stream(topics=[topic])
138
+ consumer_config = ConsumerConfig(
139
+ name=durable_name,
140
+ deliver_policy=DeliverPolicy.ALL,
141
+ durable_name=durable_name,
142
+ )
143
+
144
+ await self.js.subscribe(topic, cb=callback, config=consumer_config)
145
+ logging.info(f"Subscribed to async event on topic '{topic}'")
146
+
147
+ except Exception as e:
148
+ logging.error(
149
+ f"Failed to subscribe to async event on topic '{topic}': {e}",
150
+ exc_info=True,
151
+ )
152
+ raise e
153
+
154
+ async def subscribe_sync_event(self, topic: str, callback: Callable[[Any], None]):
155
+ """
156
+ Subscribe to a NATS subject and return a response after processing the message.
157
+
158
+ Args:
159
+ topic: The topic to subscribe to.
160
+ callback: A callback function to handle received messages.
161
+ """
162
+ try:
163
+ await self.nc.subscribe(topic, cb=callback)
164
+ logging.info(f"Subscribed to sync event on topic '{topic}'")
165
+
166
+ except Exception as e:
167
+ logging.error(
168
+ f"Failed to subscribe to sync event on topic '{topic}': {e}",
169
+ exc_info=True,
170
+ )
171
+ raise e
File without changes
File without changes
@@ -0,0 +1,35 @@
1
+ # import unittest
2
+ # import os
3
+ # from unittest.mock import patch, mock_open
4
+ # from shared_kernel.config import Config
5
+ # from shared_kernel.exceptions import InvalidConfiguration, MissingConfiguration
6
+ #
7
+ #
8
+ # class TestConfig(unittest.TestCase):
9
+ #
10
+ # @patch('shared_kernel.config.find_dotenv', return_value='.env')
11
+ # def test_init_with_env_file_found(self, mock_find_dotenv):
12
+ # with patch('builtins.open', mock_open(read_data='KEY=value')):
13
+ # config=Config()
14
+ # self.assertEqual(config.get('KEY'), 'value')
15
+ #
16
+ # @patch('shared_kernel.config.find_dotenv', return_value='')
17
+ # def test_init_with_env_file_not_found(self, mock_find_dotenv):
18
+ # with self.assertRaises(InvalidConfiguration):
19
+ # Config()
20
+ #
21
+ # @patch('shared_kernel.config.find_dotenv', return_value='.env')
22
+ # def test_get_existing_variable(self, mock_find_dotenv):
23
+ # with patch.dict(os.environ, {'EXISTING_KEY': 'existing_value'}):
24
+ # config=Config()
25
+ # self.assertEqual(config.get('EXISTING_KEY'), 'existing_value')
26
+ #
27
+ # @patch('shared_kernel.config.find_dotenv', return_value='/mocked/path/to/.env')
28
+ # def test_get_non_existing_variable(self, mock_find_dotenv):
29
+ # with self.assertRaises(MissingConfiguration):
30
+ # config=Config()
31
+ # config.get('NON_EXISTING_KEY')
32
+ #
33
+ #
34
+ # if __name__ == '__main__':
35
+ # unittest.main()
@@ -0,0 +1,48 @@
1
+ # import unittest
2
+ # from unittest.mock import patch, MagicMock
3
+ # import logging
4
+ # from shared_kernel.logger import Logger
5
+ #
6
+ #
7
+ # class TestLogger(unittest.TestCase):
8
+ #
9
+ # @patch('logging.getLogger')
10
+ # def setUp(self, mock_get_logger):
11
+ # # Setup runs before each test method
12
+ # self.mock_logger = MagicMock(spec=logging.Logger)
13
+ # self.mock_logger.handlers = [] # Initialize handlers attribute
14
+ # self.mock_logger.level = logging.NOTSET # Set the level attribute
15
+ # mock_get_logger.return_value = self.mock_logger
16
+ # self.test_logger = Logger(name='test_logger', log_file='test_log.log')
17
+ #
18
+ # def test_init(self):
19
+ # # Test initialization parameters
20
+ # self.assertEqual(self.test_logger.name, 'test_logger')
21
+ # self.assertEqual(self.test_logger.log_file, 'test_log.log')
22
+ #
23
+ # def test_configure_logger(self):
24
+ # # Test logger configuration
25
+ # self.test_logger.configure_logger()
26
+ # self.mock_logger.addHandler.assert_called()
27
+ #
28
+ # def test_info_logging(self):
29
+ # # Test info logging
30
+ # message = 'Test info message'
31
+ # self.test_logger.info(message)
32
+ # self.mock_logger.info.assert_called_with(message)
33
+ #
34
+ # def test_error_logging(self):
35
+ # # Test error logging
36
+ # message = 'Test error message'
37
+ # self.test_logger.error(message)
38
+ # self.mock_logger.error.assert_called_with(message)
39
+ #
40
+ # def test_debug_logging(self):
41
+ # # Test debug logging
42
+ # message = 'Test debug message'
43
+ # self.test_logger.debug(message)
44
+ # self.mock_logger.debug.assert_called_with(message)
45
+ #
46
+ #
47
+ # if __name__ == '__main__':
48
+ # unittest.main()
@@ -0,0 +1,36 @@
1
+ import asyncio
2
+ from unittest.mock import patch
3
+ from nats.aio.client import Client as NATS
4
+ import pytest
5
+
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_nats_subscriber():
9
+ # Start the patch
10
+ mock_client = patch('nats.aio.client.Client').start()
11
+
12
+ # Get the mock instance
13
+ instance = mock_client.return_value
14
+
15
+ # Configure the mock to return True for is_connected and None for publish
16
+ instance.is_connected.return_value = True
17
+ instance.publish.return_value = None
18
+
19
+ try:
20
+ # Initialize the NATS client
21
+ nc = NATS()
22
+
23
+ # Connect to the mock NATS server
24
+ await nc.connect('nats://localhost:4222')
25
+
26
+ # Subscribe to a topic asynchronously
27
+ await nc.subscribe('foo', cb=lambda m: print(f'Received: {m.data}'))
28
+
29
+ # Publish a message to the subscribed topic
30
+ await nc.publish('foo', b'bar')
31
+
32
+ # Assert that the callback was called with the published message
33
+ instance.publish.assert_called_once_with('foo', b'bar')
34
+ finally:
35
+ # Stop the patch after the test
36
+ mock_client.stop()
@@ -0,0 +1,18 @@
1
+ import unittest
2
+ from shared_kernel.utils import DataValidators
3
+
4
+
5
+ class TestDataValidators(unittest.TestCase):
6
+
7
+ def test_validate_email(self):
8
+ self.assertTrue(DataValidators.validate_email("test@example.com"))
9
+ self.assertFalse(DataValidators.validate_email("not_an_email"))
10
+
11
+ def test_validate_phone(self):
12
+ self.assertTrue(DataValidators.validate_phone("1234567890"))
13
+ self.assertFalse(DataValidators.validate_phone("12345")) # Not exactly 10 digits
14
+ self.assertFalse(DataValidators.validate_phone("123-456-7890")) # Contains dashes
15
+
16
+
17
+ if __name__ == '__main__':
18
+ unittest.main()
@@ -0,0 +1,20 @@
1
+ from datetime import datetime
2
+ from shared_kernel.utils import DateFormatUtils
3
+ import unittest
4
+
5
+
6
+ class TestDateFormatUtils(unittest.TestCase):
7
+
8
+ def test_format_date(self):
9
+ now = datetime.now()
10
+ formatted_now = DateFormatUtils.format_date(now)
11
+ self.assertEqual(formatted_now, now.strftime("%Y-%m-%d"))
12
+
13
+ def test_parse_date(self):
14
+ date_str = "2024-07-17"
15
+ parsed_date = DateFormatUtils.parse_date(date_str)
16
+ self.assertEqual(parsed_date.strftime("%Y-%m-%d"), date_str)
17
+
18
+
19
+ if __name__ == '__main__':
20
+ unittest.main()
@@ -0,0 +1,28 @@
1
+ import unittest
2
+ from shared_kernel.utils import StringUtils
3
+
4
+
5
+ class TestStringUtils(unittest.TestCase):
6
+
7
+ def test_no_truncation_needed(self):
8
+ self.assertEqual(StringUtils.truncate_string("Hello, world!", 20), "Hello, world!")
9
+
10
+ def test_truncation_with_default_suffix(self):
11
+ self.assertEqual(StringUtils.truncate_string("Hello, world!", 8), "Hello...")
12
+
13
+ def test_truncation_with_custom_suffix(self):
14
+ self.assertEqual(StringUtils.truncate_string("Hello, world!", 10, suffix='***'), "Hello, ***")
15
+
16
+ def test_empty_string(self):
17
+ self.assertEqual(StringUtils.truncate_string("", 5), "")
18
+
19
+ def test_suffix_longer_than_max_length(self):
20
+ self.assertEqual(StringUtils.truncate_string("Hello", 3, suffix='***'), '***')
21
+
22
+ def test_remove_html_tags(self):
23
+ self.assertEqual(StringUtils.remove_html_tags("<p>Hello, <strong>world</strong>!</p>"),
24
+ "Hello, world!")
25
+
26
+
27
+ if __name__ == '__main__':
28
+ unittest.main()