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.
- fdc_shared_kernel-0.0.3.dist-info/METADATA +159 -0
- fdc_shared_kernel-0.0.3.dist-info/RECORD +33 -0
- fdc_shared_kernel-0.0.3.dist-info/WHEEL +5 -0
- fdc_shared_kernel-0.0.3.dist-info/top_level.txt +1 -0
- shared_kernel/__init__.py +0 -0
- shared_kernel/config/__init__.py +49 -0
- shared_kernel/database/__init__.py +43 -0
- shared_kernel/exceptions/__init__.py +7 -0
- shared_kernel/exceptions/configuration_exceptions.py +20 -0
- shared_kernel/exceptions/custom_exceptions.py +14 -0
- shared_kernel/exceptions/data_validation_exceptions.py +26 -0
- shared_kernel/exceptions/http_exceptions.py +60 -0
- shared_kernel/exceptions/infrastructure_exceptions.py +26 -0
- shared_kernel/exceptions/operational_exceptions.py +26 -0
- shared_kernel/exceptions/security_exceptions.py +26 -0
- shared_kernel/interfaces/__init__.py +0 -0
- shared_kernel/interfaces/databus.py +73 -0
- shared_kernel/logger/__init__.py +75 -0
- shared_kernel/messaging/__init__.py +1 -0
- shared_kernel/messaging/nats_databus.py +171 -0
- shared_kernel/models/__init__.py +0 -0
- shared_kernel/tests/__init__.py +0 -0
- shared_kernel/tests/config/test_config.py +35 -0
- shared_kernel/tests/logger/test_logger.py +48 -0
- shared_kernel/tests/messaging/test_nats_interface.py +36 -0
- shared_kernel/tests/utils/test_data_validators.py +18 -0
- shared_kernel/tests/utils/test_date_format_utils.py +20 -0
- shared_kernel/tests/utils/test_string_utils.py +28 -0
- shared_kernel/tests/utils/utils.py +618 -0
- shared_kernel/utils/__init__.py +3 -0
- shared_kernel/utils/data_validators_utils.py +15 -0
- shared_kernel/utils/date_format_utils.py +13 -0
- 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()
|