fdc-shared-kernel 0.0.40__tar.gz → 0.0.41__tar.gz

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 (64) hide show
  1. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/PKG-INFO +1 -1
  2. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/fdc_shared_kernel.egg-info/PKG-INFO +1 -1
  3. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/pyproject.toml +1 -1
  4. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/messaging/__init__.py +2 -2
  5. fdc_shared_kernel-0.0.41/shared_kernel/messaging/nats_databus.py +337 -0
  6. fdc_shared_kernel-0.0.40/shared_kernel/messaging/nats_databus.py +0 -177
  7. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/README.md +0 -0
  8. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/README_pypi.md +0 -0
  9. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/fdc_shared_kernel.egg-info/SOURCES.txt +0 -0
  10. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/fdc_shared_kernel.egg-info/dependency_links.txt +0 -0
  11. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/fdc_shared_kernel.egg-info/requires.txt +0 -0
  12. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/fdc_shared_kernel.egg-info/top_level.txt +0 -0
  13. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/requirements.txt +0 -0
  14. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/setup.cfg +0 -0
  15. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/__init__.py +0 -0
  16. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/async_task_executor/__init__.py +0 -0
  17. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/async_task_executor/async_task_executor.py +0 -0
  18. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/auth/__init__.py +0 -0
  19. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/auth/jwt_helper.py +0 -0
  20. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/auth/token_handler.py +0 -0
  21. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/config/__init__.py +0 -0
  22. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/database/__init__.py +0 -0
  23. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/enums/__init__.py +0 -0
  24. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/enums/async_task_status.py +0 -0
  25. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/exceptions/__init__.py +0 -0
  26. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/exceptions/configuration_exceptions.py +0 -0
  27. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/exceptions/custom_exceptions.py +0 -0
  28. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/exceptions/data_validation_exceptions.py +0 -0
  29. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/exceptions/http_exceptions.py +0 -0
  30. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/exceptions/infrastructure_exceptions.py +0 -0
  31. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/exceptions/operational_exceptions.py +0 -0
  32. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/exceptions/security_exceptions.py +0 -0
  33. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/http/__init__.py +0 -0
  34. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/http/httpx_http_client.py +0 -0
  35. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/http/request_http_client.py +0 -0
  36. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/interfaces/__init__.py +0 -0
  37. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/interfaces/databus.py +0 -0
  38. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/interfaces/http.py +0 -0
  39. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/interfaces/keyvault.py +0 -0
  40. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/logger/__init__.py +0 -0
  41. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/messaging/aws_databus.py +0 -0
  42. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/messaging/http_databus.py +0 -0
  43. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/metrics/__init__.py +0 -0
  44. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/metrics/status_tracker.py +0 -0
  45. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/models/__init__.py +0 -0
  46. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/security/__init__.py +0 -0
  47. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/security/key_vault/__init__.py +0 -0
  48. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/security/key_vault/aws_secret_manager.py +0 -0
  49. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/security/key_vault/azure_keyvault.py +0 -0
  50. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/tests/__init__.py +0 -0
  51. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/tests/config/__init__.py +0 -0
  52. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/tests/config/test_config.py +0 -0
  53. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/tests/logger/__init__.py +0 -0
  54. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/tests/logger/test_logger.py +0 -0
  55. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/tests/messaging/__init__.py +0 -0
  56. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/tests/messaging/test_nats_interface.py +0 -0
  57. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/tests/utils/__init__.py +0 -0
  58. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/tests/utils/test_data_validators.py +0 -0
  59. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/tests/utils/test_date_format_utils.py +0 -0
  60. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/tests/utils/test_string_utils.py +0 -0
  61. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/utils/__init__.py +0 -0
  62. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/utils/data_validators_utils.py +0 -0
  63. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/utils/date_format_utils.py +0 -0
  64. {fdc_shared_kernel-0.0.40 → fdc_shared_kernel-0.0.41}/shared_kernel/utils/string_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fdc_shared_kernel
3
- Version: 0.0.40
3
+ Version: 0.0.41
4
4
  Summary: Shared library for microservice
5
5
  Author-email: Shikhil S <shikhil.s@dbizsolution.com>, Ahammed Akdham N <ahammedakdham.n@dbizsolution.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fdc_shared_kernel
3
- Version: 0.0.40
3
+ Version: 0.0.41
4
4
  Summary: Shared library for microservice
5
5
  Author-email: Shikhil S <shikhil.s@dbizsolution.com>, Ahammed Akdham N <ahammedakdham.n@dbizsolution.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fdc_shared_kernel"
7
- version = "0.0.40"
7
+ version = "0.0.41"
8
8
  requires-python = ">=3.7"
9
9
  readme = "README_pypi.md"
10
10
  description = "Shared library for microservice"
@@ -4,8 +4,8 @@ from shared_kernel.messaging.aws_databus import AWSDataBus
4
4
  from shared_kernel.interfaces import DataBus
5
5
 
6
6
 
7
- # create an enum for the buses available
8
- class DataBus:
7
+ # create an enum for the buses available
8
+ class DataBusFactory:
9
9
  data_bus_classes = {
10
10
  "NATS": NATSDataBus,
11
11
  "HTTP": HTTPDataBus,
@@ -0,0 +1,337 @@
1
+ import json
2
+ from nats.aio.client import Client as NATS
3
+ from nats.js.api import StreamConfig, ConsumerConfig, DeliverPolicy, AckPolicy
4
+ from typing import Callable, Any, Dict, List, Optional, Union
5
+ from nats.js.errors import (
6
+ BadRequestError as JetstreamBadRequestError,
7
+ NotFoundError as JetstreamNotFoundError,
8
+ )
9
+ from nats.errors import TimeoutError as NATSTimeoutError
10
+ from nats.js.client import JetStreamContext
11
+ from nats.aio.msg import Msg
12
+
13
+ from shared_kernel.config import Config
14
+ from shared_kernel.interfaces.databus import DataBus
15
+ from shared_kernel.logger import Logger
16
+
17
+ app_config = Config()
18
+ logger = Logger(app_config.get("APP_NAME"))
19
+
20
+
21
+ class NATSDataBus(DataBus):
22
+ """
23
+ A NATS interface class to handle both standard NATS and JetStream operations.
24
+ """
25
+
26
+ _instance = None
27
+
28
+ def __new__(cls, *args, **kwargs):
29
+ if cls._instance is None:
30
+ cls._instance = super(NATSDataBus, cls).__new__(cls)
31
+ return cls._instance
32
+
33
+ def __init__(self, config: Dict = None):
34
+ """
35
+ Initialize the NATSDataBus.
36
+
37
+ Args:
38
+ config (Dict): A dictionary containing the NATS configuration.
39
+ """
40
+ if not hasattr(self, "initialized"): # Prevent reinitialization
41
+ super().__init__()
42
+ self.nats_client = NATS()
43
+ self.servers = config.get("servers")
44
+ self.user = config.get("user")
45
+ self.password = config.get("password")
46
+ self.connected = False
47
+ self.nats_jet_stream_context = None # JetStream context
48
+ self.initialized = True
49
+ self.max_delivery_count = app_config.get("MAX_DELIVERY_COUNT")
50
+
51
+ async def _connection_error_callback(self, e: Exception) -> None:
52
+ """
53
+ Callback function for connection errors.
54
+
55
+ Args:
56
+ e (Exception): The exception that occurred during connection.
57
+ """
58
+ logger.error(f"Unable to connect to NATS server - {str(e)}")
59
+
60
+ async def _create_consumer(self, consumer_name: str, subject: str):
61
+ try:
62
+ # check if the consumer already exists
63
+ try:
64
+ await self.nats_jet_stream_context.consumer_info(
65
+ app_config.get("NATS_STREAM_NAME"), consumer_name
66
+ )
67
+ logger.info(f"Consumer {consumer_name} already exists")
68
+ return
69
+ except JetstreamNotFoundError as e:
70
+ # case where the consumer doesn't exists (error code 10014)
71
+ if e.err_code == 10014:
72
+ consumer_config = ConsumerConfig(
73
+ durable_name=consumer_name,
74
+ ack_policy=AckPolicy.EXPLICIT,
75
+ deliver_policy=DeliverPolicy.ALL,
76
+ filter_subject=subject,
77
+ max_ack_pending=5,
78
+ )
79
+ await self.nats_jet_stream_context.add_consumer(
80
+ stream=app_config.get("NATS_STREAM_NAME"),
81
+ config=consumer_config,
82
+ )
83
+ logger.info(f"Consumer {consumer_name} created")
84
+ except Exception as e:
85
+ logger.error(f"Error creating consumer: {e}")
86
+ raise
87
+
88
+ async def _create_dlq_stream(self) -> None:
89
+ """
90
+ Create a Dead Letter Queue stream if it doesn't exist.
91
+
92
+ Raises:
93
+ Exception: If an error occurs while creating the DLQ stream.
94
+ """
95
+ try:
96
+ dlq_config = StreamConfig(
97
+ name=app_config.get("NATS_DLQ_STREAM_NAME"),
98
+ subjects=[f"{app_config.get('NATS_DLQ_STREAM_NAME')}.>"],
99
+ max_age=86400 * 7, # Keep messages for 7 days
100
+ )
101
+ await self.nats_jet_stream_context.add_stream(dlq_config)
102
+ logger.info(
103
+ f"DLQ Stream '{app_config.get('NATS_DLQ_STREAM_NAME')}' created"
104
+ )
105
+ except JetstreamBadRequestError as e:
106
+ if e.err_code == 10058: # Stream already exists
107
+ logger.info(
108
+ f"DLQ Stream '{app_config.get('NATS_DLQ_STREAM_NAME')}' already exists"
109
+ )
110
+ else:
111
+ raise
112
+
113
+ async def _create_update_stream(self, topic: Optional[str]):
114
+ """
115
+ Create or update a JetStream stream.
116
+ The function attempts to create a JetStream stream with the provided topic.
117
+ If a stream with the same name already exists but does not include the topic,
118
+ the stream is updated by adding the new topic.
119
+
120
+ Args:
121
+ topic (Optional[str]): The topic to add to the stream.
122
+
123
+ Raises:
124
+ RuntimeError: If unable to create or update the stream.
125
+ """
126
+ try:
127
+ # ensure that the topic is a list (empty if None)
128
+ topic = [] if topic is None else [topic]
129
+ stream_config = StreamConfig(
130
+ name=app_config.get("NATS_STREAM_NAME"),
131
+ subjects=topic,
132
+ max_age=600,
133
+ )
134
+ await self.nats_jet_stream_context.add_stream(stream_config)
135
+ logger.info(f"Stream {app_config.get("NATS_STREAM_NAME")} created")
136
+ except JetstreamBadRequestError as e:
137
+ # case where the stream already exists (error code 10058)
138
+ if e.err_code == 10058:
139
+ logger.info(f"Stream {app_config.get("NATS_STREAM_NAME")} already exists with other topics")
140
+ # retrieve the existing stream's information
141
+ stream_info = await self.nats_jet_stream_context.stream_info(
142
+ app_config.get("NATS_STREAM_NAME")
143
+ )
144
+ # get the current list of topics associated with the stream
145
+ topics: list = stream_info.config.subjects
146
+ # if the new topic is not already in the stream's subjects, add it
147
+ if topic[0] not in topics:
148
+ topics.append(topic[0])
149
+ # update the stream configuration to include the new topic
150
+ stream_config = StreamConfig(
151
+ name=app_config.get("NATS_STREAM_NAME"),
152
+ subjects=topics,
153
+ max_age=600,
154
+ )
155
+ # apply the updated configuration to the existing stream
156
+ await self.nats_jet_stream_context.update_stream(stream_config)
157
+ logger.info(f"Added topic: {topic[0]} to the stream {app_config.get("NATS_STREAM_NAME")}")
158
+ except Exception as e:
159
+ logger.error(f"Error creating/updating stream: {e}")
160
+ raise e
161
+
162
+ async def _move_to_dlq(
163
+ self, original_subject: str, message: Dict[str, Any]
164
+ ) -> None:
165
+ """
166
+ Move a message to the Dead Letter Queue.
167
+
168
+ Args:
169
+ original_subject (str): The original subject of the message.
170
+ message (Dict[str, Any]): The message to be moved to DLQ.
171
+ """
172
+ dlq_subject = f"{app_config.get('NATS_DLQ_STREAM_NAME')}.{original_subject}"
173
+ await self.nats_jet_stream_context.publish(dlq_subject, json.dumps(message).encode())
174
+ logger.info(f"Moved message to DLQ: {dlq_subject}")
175
+
176
+ async def _register_topic_and_consumer(
177
+ self, consumer_name: str, event_name: str
178
+ ) -> None:
179
+ """
180
+ Register a topic and create a consumer for it.
181
+
182
+ Args:
183
+ consumer_name (str): The name of the consumer to create.
184
+ event_name (str): The event name (subject) to register.
185
+ """
186
+ await self._create_update_stream(topic=event_name)
187
+ await self._create_consumer(consumer_name=consumer_name, subject=event_name)
188
+
189
+ async def _fetch_messages(
190
+ self, pull_subscription_object: JetStreamContext.PullSubscription
191
+ ) -> Msg:
192
+ # fetch only one message
193
+ msgs: List[Msg] = await pull_subscription_object.fetch(batch=1, timeout=3600)
194
+ # get the message object from the list
195
+ message_object = msgs.pop()
196
+ # process the message
197
+ return message_object
198
+
199
+ async def make_connection(self):
200
+ """
201
+ Connect to the NATS server.
202
+ """
203
+ if not self.connected:
204
+ await self.nats_client.connect(
205
+ servers=self.servers,
206
+ user=self.user,
207
+ password=self.password,
208
+ error_cb=self._connection_error_callback,
209
+ )
210
+ self.nats_jet_stream_context: JetStreamContext = self.nats_client.jetstream(timeout=10)
211
+ self.connected = True
212
+
213
+ async def close_connection(self):
214
+ """
215
+ Close the connection to the NATS server.
216
+ """
217
+ if self.connected:
218
+ try:
219
+ await self.nats_client.close()
220
+ self.connected = False
221
+ except Exception as e:
222
+ raise e
223
+
224
+ async def publish_event(
225
+ self, event_name: str, event_payload: dict
226
+ ) -> Union[bool, Exception]:
227
+ """
228
+ Publish a message to a JetStream subject.
229
+
230
+ Args:
231
+ event_name (str): The subject to publish the message to.
232
+ event_payload (dict): The message to be published.
233
+
234
+ Returns:
235
+ bool: True if the event was published successfully.
236
+ """
237
+ # acknowledgement from the jetstream after successful publish
238
+ ack = await self.nats_jet_stream_context.publish(
239
+ event_name, json.dumps(event_payload).encode("utf-8")
240
+ )
241
+ logger.info(
242
+ f"Published event '{event_payload.get('event_name')}' to subject '{event_name}', ack: {ack}"
243
+ )
244
+ return ack
245
+
246
+ async def request_event(
247
+ self, event_name: str, event_payload: dict, timeout: float = 10.0
248
+ ) -> Union[dict, Exception]:
249
+ """
250
+ Send a request and wait for a response.
251
+
252
+ Args:
253
+ event_name (str): The subject to publish the message to.
254
+ event_payload (dict): The message to be published.
255
+ timeout (float): The timeout for the request.
256
+
257
+ Returns:
258
+ dict: The response message.
259
+ """
260
+ response = await self.nats_client.request(
261
+ event_name, json.dumps(event_payload).encode("utf-8"), timeout=timeout
262
+ )
263
+ return json.loads(response.data.decode("utf-8"))
264
+
265
+ async def subscribe_async_event(
266
+ self, event_name: str, callback: Callable[[Any], None]
267
+ ):
268
+ """
269
+ Subscribe to a JetStream subject with a durable consumer and process messages asynchronously.
270
+
271
+ Args:
272
+ event_name (str): The subject to subscribe to.
273
+ callback (Callable[[Any], None]): A callback function to handle received messages.
274
+
275
+ Raises:
276
+ RuntimeError: If unable to subscribe to the event.
277
+ """
278
+ # consumer_name is a persistent identifier for a consumer that ensures message
279
+ # consumption state is retained even if the client disconnects or the server restarts.
280
+ consumer_name = app_config.get("APP_NAME") + f"-{event_name}"
281
+
282
+ await self._register_topic_and_consumer(
283
+ consumer_name=consumer_name, event_name=event_name
284
+ )
285
+
286
+ pull_subscription_object: JetStreamContext.PullSubscription = (
287
+ await self.nats_jet_stream_context.pull_subscribe(subject=event_name, durable=consumer_name)
288
+ )
289
+ logger.info(f"Subscribed to async event on subject '{event_name}'")
290
+ while True:
291
+ try:
292
+ message_object: Msg = await self._fetch_messages(
293
+ pull_subscription_object
294
+ )
295
+ # process the message
296
+ event_data: dict = json.loads(message_object.data.decode())
297
+ has_job_failed = False
298
+ try:
299
+ callback(event_data)
300
+ # acknowledge the message after processing and finishing the job
301
+ await message_object.ack()
302
+ logger.info(f"Acknowledged job: {event_data}")
303
+ except Exception as e:
304
+ logger.error(
305
+ f"Invoking callback during execution of {event_name} failed due to {str(e)}"
306
+ )
307
+ has_job_failed = True
308
+
309
+ if (
310
+ has_job_failed
311
+ and message_object.metadata.num_delivered == self.max_delivery_count
312
+ ):
313
+ logger.warning("Moving event to the dead letter topic after max retries")
314
+ await self._create_dlq_stream()
315
+ await self._move_to_dlq(event_name, event_data)
316
+ await message_object.ack() # acknowledge to remove from original queue
317
+
318
+ # TODO - What to do failed jobs ?
319
+ # failed jobs are moved to dead letter topic after max retries for now
320
+ except NATSTimeoutError:
321
+ logger.info("No more messages, will pull again...")
322
+
323
+ async def subscribe_sync_event(
324
+ self, event_name: str, callback: Callable[[Any], None]
325
+ ):
326
+ """
327
+ Subscribe to a NATS subject and process the message synchronously.
328
+
329
+ Args:
330
+ event_name (str): The subject to subscribe to.
331
+ callback (Callable[[Any], None]): A callback function to handle received messages.
332
+ """
333
+ await self.nats_client.subscribe(event_name, cb=callback)
334
+ logger.info(f"Subscribed to sync event on subject '{event_name}'")
335
+
336
+ def delete_message(self, receipt_handle: str):
337
+ pass
@@ -1,177 +0,0 @@
1
- import json
2
- import logging
3
- from nats.aio.client import Client as NATS
4
- from nats.js.api import StreamConfig, ConsumerConfig, DeliverPolicy
5
- from typing import Callable, Any, List, Union, Dict
6
- from shared_kernel.interfaces import DataBus
7
-
8
- logging.getLogger().setLevel(logging.INFO)
9
-
10
-
11
- class NATSDataBus(DataBus):
12
- """
13
- A NATS interface class to handle both standard NATS and JetStream operations.
14
- """
15
-
16
- _instance = None
17
-
18
- def __new__(cls, *args, **kwargs):
19
- if cls._instance is None:
20
- cls._instance = super(NATSDataBus, cls).__new__(cls)
21
- return cls._instance
22
-
23
- def __init__(self, config: Dict = None):
24
- """
25
- Initialize the NATSDataBus.
26
-
27
- Args:
28
- config (Dict): A dictionary containing the NATS configuration.
29
- """
30
- if not hasattr(self, "initialized"): # Prevent reinitialization
31
- super().__init__()
32
- self.nc = NATS()
33
- self.servers = config.get('servers')
34
- self.user = config.get('user')
35
- self.password = config.get('password')
36
- self.connected = False
37
- self.js = None # JetStream context
38
- self.initialized = True
39
- self.stream_name = config.get('stream_name', '')
40
- self.event_names = config.get('event_names', [])
41
-
42
- if self.stream_name and self.event_names:
43
- self.create_stream(self.stream_name, self.event_names)
44
-
45
- async def make_connection(self):
46
- """
47
- Connect to the NATS server.
48
- """
49
- if not self.connected:
50
- await self.nc.connect(
51
- servers=self.servers,
52
- user=self.user,
53
- password=self.password
54
- )
55
- self.js = self.nc.jetstream(timeout=10)
56
- self.connected = True
57
-
58
- async def close_connection(self):
59
- """
60
- Close the connection to the NATS server.
61
- """
62
- if self.connected:
63
- try:
64
- await self.nc.close()
65
- self.connected = False
66
- except Exception as e:
67
- raise e
68
-
69
- async def create_stream(self, stream_name: str, event_names: List[Any]):
70
- """
71
- Create a stream for event names to persist the messages.
72
-
73
- Args:
74
- stream_name (str): The name of the stream.
75
- event_names (List[Any]): The subjects whose messages will be persisted.
76
- """
77
- try:
78
- self.stream_name = stream_name
79
- # Check if the stream already exists
80
- await self.js.stream_info(stream_name)
81
- logging.info(f"Stream '{stream_name}' already exists.")
82
- except Exception:
83
- # Stream does not exist, so create it
84
- stream_config = StreamConfig(
85
- name=stream_name,
86
- subjects=event_names,
87
- max_age=600 # Retain messages for 10 minutes
88
- )
89
- await self.js.add_stream(stream_config)
90
- logging.info(f"Stream '{stream_name}' created.")
91
-
92
- async def publish_event(
93
- self, event_name: str, event_payload: dict
94
- ) -> Union[bool, Exception]:
95
- """
96
- Publish a message to a JetStream subject.
97
-
98
- Args:
99
- event_name (str): The subject to publish the message to.
100
- event_payload (dict): The message to be published.
101
-
102
- Returns:
103
- bool: True if the event was published successfully.
104
- """
105
- ack = await self.js.publish(
106
- event_name, json.dumps(event_payload).encode("utf-8")
107
- )
108
- logging.info(
109
- f"Published event '{event_payload.get('event_name')}' to subject '{event_name}', ack: {ack}"
110
- )
111
- return True
112
-
113
- async def request_event(
114
- self, event_name: str, event_payload: dict, timeout: float = 10.0
115
- ) -> Union[dict, Exception]:
116
- """
117
- Send a request and wait for a response.
118
-
119
- Args:
120
- event_name (str): The subject to publish the message to.
121
- event_payload (dict): The message to be published.
122
- timeout (float): The timeout for the request.
123
-
124
- Returns:
125
- dict: The response message.
126
- """
127
- response = await self.nc.request(
128
- event_name, json.dumps(event_payload).encode("utf-8"), timeout=timeout
129
- )
130
- return json.loads(response.data.decode("utf-8"))
131
-
132
- async def subscribe_async_event(
133
- self, event_name: str, callback: Callable[[Any], None], durable_name: str
134
- ):
135
- """
136
- Subscribe to a JetStream subject with a durable consumer and process messages asynchronously.
137
-
138
- Args:
139
- event_name (str): The subject to subscribe to.
140
- callback (Callable[[Any], None]): A callback function to handle received messages.
141
- durable_name (str): The name of the durable consumer.
142
- """
143
- try:
144
- # Check if the consumer already exists
145
- await self.js.consumer_info(stream=self.stream_name, name=durable_name)
146
- logging.info(f"Consumer '{durable_name}' already exists.")
147
- except Exception:
148
- # Consumer does not exist, so create it
149
- self.consumer_config = ConsumerConfig(
150
- name=durable_name,
151
- durable_name=durable_name,
152
- deliver_policy=DeliverPolicy.ALL,
153
- deliver_subject=durable_name,
154
- max_deliver=1
155
- )
156
- await self.js.add_consumer(stream=self.stream_name, config=self.consumer_config)
157
- logging.info(f"Consumer '{durable_name}' created.")
158
-
159
- await self.js.subscribe_bind(
160
- stream=self.stream_name, cb=callback, config=self.consumer_config,
161
- consumer=durable_name
162
- )
163
- logging.info(f"Subscribed to async event on subject '{event_name}'")
164
-
165
- async def subscribe_sync_event(self, event_name: str, callback: Callable[[Any], None]):
166
- """
167
- Subscribe to a NATS subject and process the message synchronously.
168
-
169
- Args:
170
- event_name (str): The subject to subscribe to.
171
- callback (Callable[[Any], None]): A callback function to handle received messages.
172
- """
173
- await self.nc.subscribe(event_name, cb=callback)
174
- logging.info(f"Subscribed to sync event on subject '{event_name}'")
175
-
176
- def delete_message(self, receipt_handle: str):
177
- pass