sify-queue-kafka 0.1.3__tar.gz → 0.1.4__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 (31) hide show
  1. {sify_queue_kafka-0.1.3 → sify_queue_kafka-0.1.4}/PKG-INFO +14 -13
  2. {sify_queue_kafka-0.1.3 → sify_queue_kafka-0.1.4}/README.md +13 -12
  3. {sify_queue_kafka-0.1.3 → sify_queue_kafka-0.1.4}/pyproject.toml +1 -1
  4. sify_queue_kafka-0.1.4/queue_kafka/__init__.py +40 -0
  5. sify_queue_kafka-0.1.4/queue_kafka/config.py +15 -0
  6. sify_queue_kafka-0.1.4/queue_kafka/consumer.py +101 -0
  7. sify_queue_kafka-0.1.4/queue_kafka/decorators.py +10 -0
  8. sify_queue_kafka-0.1.4/queue_kafka/dispatcher.py +69 -0
  9. sify_queue_kafka-0.1.4/queue_kafka/exceptions.py +33 -0
  10. sify_queue_kafka-0.1.4/queue_kafka/idempotency.py +17 -0
  11. sify_queue_kafka-0.1.4/queue_kafka/models.py +23 -0
  12. sify_queue_kafka-0.1.4/queue_kafka/producer.py +74 -0
  13. sify_queue_kafka-0.1.4/queue_kafka/registry.py +13 -0
  14. sify_queue_kafka-0.1.4/queue_kafka/retry.py +45 -0
  15. sify_queue_kafka-0.1.4/queue_kafka/topic_resolver.py +6 -0
  16. sify_queue_kafka-0.1.4/queue_kafka/utils.py +22 -0
  17. {sify_queue_kafka-0.1.3 → sify_queue_kafka-0.1.4}/setup.cfg +1 -1
  18. {sify_queue_kafka-0.1.3 → sify_queue_kafka-0.1.4}/sify_queue_kafka.egg-info/PKG-INFO +14 -13
  19. sify_queue_kafka-0.1.4/sify_queue_kafka.egg-info/SOURCES.txt +21 -0
  20. sify_queue_kafka-0.1.4/sify_queue_kafka.egg-info/top_level.txt +1 -0
  21. sify_queue_kafka-0.1.3/sify_queue_kafka.egg-info/SOURCES.txt +0 -15
  22. sify_queue_kafka-0.1.3/sify_queue_kafka.egg-info/top_level.txt +0 -1
  23. sify_queue_kafka-0.1.3/tests/test_email_signup.py +0 -101
  24. sify_queue_kafka-0.1.3/tests/test_image_pipeline.py +0 -226
  25. sify_queue_kafka-0.1.3/tests/test_notify_service.py +0 -172
  26. sify_queue_kafka-0.1.3/tests/test_order_pipeline.py +0 -99
  27. sify_queue_kafka-0.1.3/tests/test_order_service.py +0 -160
  28. sify_queue_kafka-0.1.3/tests/test_payment_service.py +0 -193
  29. sify_queue_kafka-0.1.3/tests/test_vehicle_service.py +0 -164
  30. {sify_queue_kafka-0.1.3 → sify_queue_kafka-0.1.4}/sify_queue_kafka.egg-info/dependency_links.txt +0 -0
  31. {sify_queue_kafka-0.1.3 → sify_queue_kafka-0.1.4}/sify_queue_kafka.egg-info/requires.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sify-queue-kafka
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Enterprise Queue management Framework
5
5
  Author: sifymodernization
6
6
  Author-email: sifymodernization <sifymodernization.dev@sifycorp.com>
@@ -192,32 +192,33 @@ if __name__ == "__main__":
192
192
 
193
193
  | Option | Type | Default | Description |
194
194
  |--------|------|---------|-------------|
195
- | `bootstrap_servers` | str | Required | Kafka bootstrap servers |
196
- | `group_id` | str | None | Consumer group ID |
195
+ | `bootstrap_servers` | str | "localhost:9092" | Kafka bootstrap servers |
196
+ | `group_id` | str | "" | Consumer group ID. Is generated with the tenant_id and source from the consumer config |
197
197
  | `enable_auto_commit` | bool | False | Enable auto commit of offsets |
198
198
  | `auto_offset_reset` | str | "earliest" | Offset reset policy |
199
199
  | `enable_idempotency` | bool | True | Enable duplicate detection |
200
200
  | `enable_retry` | bool | True | Enable retry with backoff |
201
201
  | `dead_letter_topic` | str | None | Topic for failed events |
202
- | `stage` | str | None | Processing stage name |
202
+ | `stage` | str | "" | Processing stage name |
203
+ | `enable_idempotent_producer` | bool | False | Enable idempotent producer |
203
204
 
204
205
  ## Event Model
205
206
 
206
207
  ```python
207
208
  class KafkaEvent(BaseModel):
208
- eventId: str
209
+ eventId: str = Field(default_factory=lambda: str(uuid.uuid4()))
209
210
  eventType: str
210
211
  eventVersion: str
211
212
  source: str
212
213
  tenantId: str
213
- timestamp: datetime
214
- correlationId: Optional[str]
215
- traceId: Optional[str]
216
- priority: Optional[str]
217
- retryCount: int
218
- maxRetries: int
214
+ timestamp: datetime = Field(default_factory=datetime.utcnow)
215
+ correlationId: Optional[str] = None
216
+ traceId: Optional[str] = None
217
+ priority: Optional[str] = "MEDIUM"
218
+ retryCount: int = 0
219
+ maxRetries: int = 5
219
220
  payload: Dict[str, Any]
220
- metadata: Optional[Dict[str, Any]]
221
+ metadata: Optional[Dict[str, Any]] = None
221
222
  ```
222
223
 
223
224
  ## Exception Types
@@ -260,7 +261,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
260
261
 
261
262
  ## Changelog
262
263
 
263
- ### v0.1.3
264
+ ### v0.1.4
264
265
  - Initial release
265
266
  - Basic producer/consumer functionality
266
267
  - Event validation with Pydantic
@@ -159,32 +159,33 @@ if __name__ == "__main__":
159
159
 
160
160
  | Option | Type | Default | Description |
161
161
  |--------|------|---------|-------------|
162
- | `bootstrap_servers` | str | Required | Kafka bootstrap servers |
163
- | `group_id` | str | None | Consumer group ID |
162
+ | `bootstrap_servers` | str | "localhost:9092" | Kafka bootstrap servers |
163
+ | `group_id` | str | "" | Consumer group ID. Is generated with the tenant_id and source from the consumer config |
164
164
  | `enable_auto_commit` | bool | False | Enable auto commit of offsets |
165
165
  | `auto_offset_reset` | str | "earliest" | Offset reset policy |
166
166
  | `enable_idempotency` | bool | True | Enable duplicate detection |
167
167
  | `enable_retry` | bool | True | Enable retry with backoff |
168
168
  | `dead_letter_topic` | str | None | Topic for failed events |
169
- | `stage` | str | None | Processing stage name |
169
+ | `stage` | str | "" | Processing stage name |
170
+ | `enable_idempotent_producer` | bool | False | Enable idempotent producer |
170
171
 
171
172
  ## Event Model
172
173
 
173
174
  ```python
174
175
  class KafkaEvent(BaseModel):
175
- eventId: str
176
+ eventId: str = Field(default_factory=lambda: str(uuid.uuid4()))
176
177
  eventType: str
177
178
  eventVersion: str
178
179
  source: str
179
180
  tenantId: str
180
- timestamp: datetime
181
- correlationId: Optional[str]
182
- traceId: Optional[str]
183
- priority: Optional[str]
184
- retryCount: int
185
- maxRetries: int
181
+ timestamp: datetime = Field(default_factory=datetime.utcnow)
182
+ correlationId: Optional[str] = None
183
+ traceId: Optional[str] = None
184
+ priority: Optional[str] = "MEDIUM"
185
+ retryCount: int = 0
186
+ maxRetries: int = 5
186
187
  payload: Dict[str, Any]
187
- metadata: Optional[Dict[str, Any]]
188
+ metadata: Optional[Dict[str, Any]] = None
188
189
  ```
189
190
 
190
191
  ## Exception Types
@@ -227,7 +228,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
227
228
 
228
229
  ## Changelog
229
230
 
230
- ### v0.1.3
231
+ ### v0.1.4
231
232
  - Initial release
232
233
  - Basic producer/consumer functionality
233
234
  - Event validation with Pydantic
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sify-queue-kafka"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  description = "Enterprise Queue management Framework"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -0,0 +1,40 @@
1
+ """
2
+ Sify Queue Kafka
3
+ """
4
+
5
+ __version__ = "0.1.4"
6
+
7
+ from .config import QueueConfig
8
+ from .models import KafkaEvent
9
+ from .producer import Producer
10
+ from .consumer import Consumer
11
+ from .dispatcher import Dispatcher
12
+ from .decorators import event_handler
13
+ from .registry import EventRegistry, registry
14
+ from .exceptions import (
15
+ QueueSDKError,
16
+ ConfigurationError,
17
+ HandlerNotFoundError,
18
+ EventProcessingError,
19
+ ProducerError,
20
+ ConsumerError,
21
+ SerializationError,
22
+ )
23
+
24
+ __all__ = [
25
+ "QueueConfig",
26
+ "KafkaEvent",
27
+ "Producer",
28
+ "Consumer",
29
+ "Dispatcher",
30
+ "event_handler",
31
+ "registry",
32
+ "EventRegistry",
33
+ "QueueSDKError",
34
+ "ConfigurationError",
35
+ "HandlerNotFoundError",
36
+ "EventProcessingError",
37
+ "ProducerError",
38
+ "ConsumerError",
39
+ "SerializationError",
40
+ ]
@@ -0,0 +1,15 @@
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass
4
+ class QueueConfig:
5
+ bootstrap_servers: str = "localhost:9092"
6
+ group_id: str = ""
7
+ enable_auto_commit: bool = False
8
+ auto_offset_reset: str = "earliest"
9
+
10
+ enable_idempotency: bool = True
11
+ enable_retry: bool = True
12
+ dead_letter_topic: str = None
13
+ stage: str = ""
14
+
15
+ enable_idempotent_producer: bool = False
@@ -0,0 +1,101 @@
1
+ import json
2
+ from kafka import KafkaConsumer
3
+ from typing import Optional, Dict, Union, List
4
+
5
+ from .models import KafkaEvent
6
+ from .utils import get_logger
7
+ from .dispatcher import Dispatcher
8
+ from .config import QueueConfig
9
+ from .decorators import registry
10
+
11
+
12
+ BOOTSTRAP = "localhost:9092"
13
+
14
+ class Consumer:
15
+ def __init__( self, topics: Union[str, List[str]], config: Optional[Dict] = None, ):
16
+ # Convert topics to list
17
+ if isinstance(topics, str):
18
+ topics = [topics]
19
+
20
+ # Extract client overrides (only allowed fields)
21
+ stage = None
22
+ dead_letter_topic = None
23
+ tenant_id = None
24
+ source = None
25
+
26
+ if config:
27
+ stage = config.get("stage")
28
+ dead_letter_topic = config.get("dead_letter_topic")
29
+ tenant_id = config.get("tenant_id")
30
+ source = config.get("source")
31
+
32
+
33
+ # SDK-controlled configuration
34
+ self.config = QueueConfig(
35
+ bootstrap_servers=BOOTSTRAP,
36
+ group_id=f"{tenant_id}.{source}.{stage}.group",
37
+ enable_auto_commit=True,
38
+ auto_offset_reset="earliest",
39
+ enable_retry=True,
40
+ enable_idempotency=True,
41
+ dead_letter_topic=dead_letter_topic,
42
+ stage=stage,
43
+ )
44
+
45
+ self.running = True
46
+ self.logger = get_logger(f"Consumer.{self.config.stage or 'default'}")
47
+
48
+ # Initialize Kafka Consumer
49
+ self.consumer = KafkaConsumer(
50
+ bootstrap_servers=self.config.bootstrap_servers,
51
+ group_id=self.config.group_id,
52
+ enable_auto_commit=self.config.enable_auto_commit,
53
+ auto_offset_reset=self.config.auto_offset_reset,
54
+ value_deserializer=lambda m: json.loads(m.decode("utf-8")),
55
+ )
56
+
57
+ # Subscribe once
58
+ self.consumer.subscribe(topics)
59
+
60
+ self.dispatcher = Dispatcher(registry, self.config)
61
+
62
+ self.logger.info("Consumer initialized successfully")
63
+
64
+ def start(self):
65
+ self.logger.info(f"Consumer started for stage: {self.config.stage}")
66
+
67
+ try:
68
+ while self.running:
69
+ records = self.consumer.poll(timeout_ms=1000)
70
+
71
+ for tp, messages in records.items():
72
+ for message in messages:
73
+ try:
74
+ event = KafkaEvent(**message.value)
75
+ self.logger.debug(f"Processing event: {event.eventId}")
76
+
77
+ self.dispatcher.dispatch(event)
78
+
79
+ if not self.config.enable_auto_commit:
80
+ self.consumer.commit()
81
+
82
+ except Exception as e:
83
+ event_id = message.value.get("eventId", "unknown")
84
+ self.logger.error(
85
+ f"Error processing event {event_id}: {e}"
86
+ )
87
+ continue
88
+
89
+ except KeyboardInterrupt:
90
+ self.logger.info("Keyboard interrupt received. Shutting down...")
91
+
92
+ except Exception as e:
93
+ self.logger.error(f"Unexpected error in consumer: {e}")
94
+
95
+ finally:
96
+ self.consumer.close()
97
+ self.logger.info("Consumer closed.")
98
+
99
+ def stop(self):
100
+ self.running = False
101
+ self.logger.info("Stop signal received.")
@@ -0,0 +1,10 @@
1
+ from .registry import EventRegistry
2
+
3
+ registry = EventRegistry()
4
+
5
+
6
+ def event_handler(event_type: str, stage: str = None):
7
+ def decorator(func):
8
+ registry.register(event_type, func, stage)
9
+ return func
10
+ return decorator
@@ -0,0 +1,69 @@
1
+ from .idempotency import IdempotencyStore
2
+ from .retry import retry_with_backoff
3
+ from .utils import get_logger
4
+ from .exceptions import HandlerNotFoundError
5
+
6
+
7
+ class Dispatcher:
8
+
9
+ def __init__(self, registry, config):
10
+ self.registry = registry
11
+ self.config = config
12
+ self.logger = get_logger(f"Dispatcher.{config.stage or 'default'}")
13
+ self.idempotency_store = IdempotencyStore()
14
+
15
+ def dispatch(self, event):
16
+ """Dispatch event to appropriate handler with retry and idempotency."""
17
+ # Check for duplicate events
18
+ if self.config.enable_idempotency and self.idempotency_store.is_duplicate( self.config.group_id, event.eventId ):
19
+ self.logger.debug(f"Duplicate event detected: {event.eventId}")
20
+ return
21
+
22
+ handler = self.registry.get_handler(
23
+ event.eventType,
24
+ self.config.stage
25
+ )
26
+
27
+ if not handler:
28
+ error_msg = f"No handler found for {event.eventType} @ {self.config.stage}"
29
+ self.logger.warning(error_msg)
30
+ raise HandlerNotFoundError(error_msg)
31
+
32
+ try:
33
+ if self.config.enable_retry:
34
+ retry_with_backoff(handler, event, event.maxRetries)
35
+ else:
36
+ handler(event)
37
+
38
+ self.logger.info(f"Successfully processed event: {event.eventId}")
39
+
40
+ except Exception as e:
41
+ self.logger.error(f"Failed to process event {event.eventId}: {e}")
42
+
43
+ # Send to dead letter queue if configured
44
+ if self.config.dead_letter_topic:
45
+ self._send_to_dlq(event, str(e))
46
+ else:
47
+ raise
48
+
49
+ def _send_to_dlq(self, event, error_message):
50
+ """Send failed event to dead letter queue."""
51
+ try:
52
+ from .producer import Producer
53
+
54
+ # Add error information to event metadata
55
+ if event.metadata is None:
56
+ event.metadata = {}
57
+ event.metadata['error'] = error_message
58
+ event.metadata['failed_stage'] = self.config.stage
59
+ event.metadata['failure_time'] = str(event.timestamp)
60
+
61
+ producer = Producer(self.config)
62
+ producer.send(self.config.dead_letter_topic, event)
63
+ producer.close()
64
+
65
+ self.logger.info(f"Sent event {event.eventId} to DLQ: {self.config.dead_letter_topic}")
66
+
67
+ except Exception as dlq_error:
68
+ self.logger.error(f"Failed to send event {event.eventId} to DLQ: {dlq_error}")
69
+ raise
@@ -0,0 +1,33 @@
1
+ class QueueSDKError(Exception):
2
+ """Base exception for all Queue SDK errors."""
3
+ pass
4
+
5
+
6
+ class ConfigurationError(QueueSDKError):
7
+ """Raised when there's a configuration issue."""
8
+ pass
9
+
10
+
11
+ class HandlerNotFoundError(QueueSDKError):
12
+ """Raised when no handler is found for an event type/stage combination."""
13
+ pass
14
+
15
+
16
+ class EventProcessingError(QueueSDKError):
17
+ """Raised when event processing fails."""
18
+ pass
19
+
20
+
21
+ class ProducerError(QueueSDKError):
22
+ """Raised when producer operations fail."""
23
+ pass
24
+
25
+
26
+ class ConsumerError(QueueSDKError):
27
+ """Raised when consumer operations fail."""
28
+ pass
29
+
30
+
31
+ class SerializationError(QueueSDKError):
32
+ """Raised when event serialization/deserialization fails."""
33
+ pass
@@ -0,0 +1,17 @@
1
+ import threading
2
+
3
+
4
+ class IdempotencyStore:
5
+ def __init__(self):
6
+ self.processed_ids = set()
7
+ self.lock = threading.Lock()
8
+
9
+ def is_duplicate(self, group_id: str, event_id: str) -> bool:
10
+ key = f"{group_id}:{event_id}"
11
+
12
+ with self.lock:
13
+ if key in self.processed_ids:
14
+ return True
15
+
16
+ self.processed_ids.add(key)
17
+ return False
@@ -0,0 +1,23 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import Any, Dict, Optional
3
+ from datetime import datetime
4
+ import uuid
5
+
6
+
7
+ class KafkaEvent(BaseModel):
8
+ eventId: str = Field(default_factory=lambda: str(uuid.uuid4()))
9
+ eventType: str
10
+ eventVersion: str
11
+ source: str
12
+ tenantId: str
13
+ timestamp: datetime = Field(default_factory=datetime.utcnow)
14
+
15
+ correlationId: Optional[str] = None
16
+ traceId: Optional[str] = None
17
+
18
+ priority: Optional[str] = "MEDIUM"
19
+ retryCount: int = 0
20
+ maxRetries: int = 5
21
+
22
+ payload: Dict[str, Any]
23
+ metadata: Optional[Dict[str, Any]] = None
@@ -0,0 +1,74 @@
1
+ import json
2
+ import uuid
3
+ from datetime import datetime, timezone
4
+ from kafka import KafkaProducer
5
+ from .models import KafkaEvent
6
+ from .utils import get_logger
7
+ from .exceptions import ProducerError
8
+ from typing import Union, List
9
+
10
+
11
+ class Producer:
12
+ def __init__(self):
13
+ self.logger = get_logger("Producer")
14
+
15
+ try:
16
+ self.producer = KafkaProducer(
17
+ bootstrap_servers="localhost:9092",
18
+ value_serializer=lambda v: json.dumps(v, default=str).encode("utf-8"),
19
+ key_serializer=lambda k: k.encode("utf-8") if k else None,
20
+ acks="all",
21
+ retries=5,
22
+ enable_idempotence=True
23
+ )
24
+ self.logger.info("Producer initialized successfully")
25
+ except Exception as e:
26
+ self.logger.error(f"Failed to initialize producer: {e}")
27
+ raise ProducerError(f"Producer initialization failed: {e}")
28
+
29
+ def send(self, topic: Union[str, List[str]], message: dict) -> None:
30
+ try:
31
+ # Create KafkaEvent internally
32
+ event = KafkaEvent(
33
+ eventId=str(uuid.uuid4()),
34
+ eventType=message.get("eventType"),
35
+ eventVersion=message.get("eventVersion", "1.0"),
36
+ source=message.get("source"),
37
+ tenantId=message.get("tenantId"),
38
+ timestamp=datetime.now(timezone.utc),
39
+ correlationId=str(uuid.uuid4()),
40
+ traceId=str(uuid.uuid4()),
41
+ priority=message.get("priority", "MEDIUM"),
42
+ payload=message.get("payload", {}),
43
+ metadata=message.get("metadata", {})
44
+ )
45
+
46
+ event_dict = event.model_dump(mode="json")
47
+
48
+ topics = [topic] if isinstance(topic, str) else topic
49
+
50
+ for t in topics:
51
+ self.producer.send(t, value=event_dict, key=event.eventId)
52
+
53
+ except Exception as e:
54
+ self.logger.error(f"Failed to send event: {e}")
55
+ raise ProducerError(f"Failed to send event: {e}")
56
+
57
+ def flush(self):
58
+ """Flush pending messages."""
59
+ try:
60
+ self.producer.flush()
61
+ self.logger.debug("Producer flushed successfully")
62
+ except Exception as e:
63
+ self.logger.error(f"Failed to flush producer: {e}")
64
+ raise ProducerError(f"Failed to flush producer: {e}")
65
+
66
+ def close(self):
67
+ """Close producer and clean up resources."""
68
+ try:
69
+ self.producer.flush()
70
+ self.producer.close()
71
+ self.logger.info("Producer closed successfully")
72
+ except Exception as e:
73
+ self.logger.error(f"Error closing producer: {e}")
74
+ raise ProducerError(f"Error closing producer: {e}")
@@ -0,0 +1,13 @@
1
+ class EventRegistry:
2
+ def __init__(self):
3
+ self._handlers = {}
4
+
5
+ def register(self, event_type, handler, stage=None):
6
+ key = (event_type, stage)
7
+ self._handlers[key] = handler
8
+
9
+ def get_handler(self, event_type, stage=None):
10
+ return self._handlers.get((event_type, stage))
11
+
12
+
13
+ registry = EventRegistry()
@@ -0,0 +1,45 @@
1
+ import time
2
+ from typing import Callable, Any
3
+ from .utils import get_logger
4
+
5
+
6
+ def retry_with_backoff(handler: Callable[[Any], Any], event: Any, max_retries: int,
7
+ base_delay: float = 1.0, max_delay: float = 60.0) -> Any:
8
+ """
9
+ Retry handler with exponential backoff.
10
+
11
+ Args:
12
+ handler: The function to retry
13
+ event: The event to pass to the handler
14
+ max_retries: Maximum number of retry attempts
15
+ base_delay: Initial delay between retries (seconds)
16
+ max_delay: Maximum delay between retries (seconds)
17
+
18
+ Returns:
19
+ Result from handler function
20
+
21
+ Raises:
22
+ MaxRetriesExceededError: If all retries are exhausted
23
+ """
24
+ logger = get_logger("Retry")
25
+
26
+ while event.retryCount < max_retries:
27
+ try:
28
+ return handler(event)
29
+ except Exception as e:
30
+ event.retryCount += 1
31
+
32
+ if event.retryCount >= max_retries:
33
+ logger.error(f"Max retries ({max_retries}) exceeded for event {event.eventId}")
34
+ raise MaxRetriesExceededError(f"Failed after {max_retries} retries: {e}")
35
+
36
+ # Calculate exponential backoff with jitter
37
+ delay = min(base_delay * (2 ** (event.retryCount - 1)), max_delay)
38
+ jitter = delay * 0.1 * (0.5 - time.time() % 1) # ±10% jitter
39
+ final_delay = delay + jitter
40
+
41
+ logger.warning(f"Retry {event.retryCount}/{max_retries} for event {event.eventId} "
42
+ f"after {final_delay:.2f}s. Error: {e}")
43
+ time.sleep(final_delay)
44
+
45
+ raise MaxRetriesExceededError(f"Failed after {max_retries} retries")
@@ -0,0 +1,6 @@
1
+ class TopicResolver:
2
+ def __init__(self, environment: str):
3
+ self.environment = environment
4
+
5
+ def resolve(self, logical_topic: str) -> str:
6
+ return f"{self.environment}.{logical_topic}"
@@ -0,0 +1,22 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+
5
+ def get_logger(name: str, level: Optional[int] = None) -> logging.Logger:
6
+ """Get a configured logger with consistent formatting."""
7
+ logger = logging.getLogger(name)
8
+
9
+ if not logger.handlers:
10
+ handler = logging.StreamHandler()
11
+ formatter = logging.Formatter(
12
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
13
+ )
14
+ handler.setFormatter(formatter)
15
+ logger.addHandler(handler)
16
+
17
+ if level:
18
+ logger.setLevel(level)
19
+ elif logger.level == logging.NOTSET:
20
+ logger.setLevel(logging.INFO)
21
+
22
+ return logger
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = sify-queue-kafka
3
- version = 0.1.3
3
+ version = 0.1.4
4
4
  description = Enterprise Queue management Framework
5
5
  long_description = file: README.md
6
6
  long_description_content_type = text/markdown
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sify-queue-kafka
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Enterprise Queue management Framework
5
5
  Author: sifymodernization
6
6
  Author-email: sifymodernization <sifymodernization.dev@sifycorp.com>
@@ -192,32 +192,33 @@ if __name__ == "__main__":
192
192
 
193
193
  | Option | Type | Default | Description |
194
194
  |--------|------|---------|-------------|
195
- | `bootstrap_servers` | str | Required | Kafka bootstrap servers |
196
- | `group_id` | str | None | Consumer group ID |
195
+ | `bootstrap_servers` | str | "localhost:9092" | Kafka bootstrap servers |
196
+ | `group_id` | str | "" | Consumer group ID. Is generated with the tenant_id and source from the consumer config |
197
197
  | `enable_auto_commit` | bool | False | Enable auto commit of offsets |
198
198
  | `auto_offset_reset` | str | "earliest" | Offset reset policy |
199
199
  | `enable_idempotency` | bool | True | Enable duplicate detection |
200
200
  | `enable_retry` | bool | True | Enable retry with backoff |
201
201
  | `dead_letter_topic` | str | None | Topic for failed events |
202
- | `stage` | str | None | Processing stage name |
202
+ | `stage` | str | "" | Processing stage name |
203
+ | `enable_idempotent_producer` | bool | False | Enable idempotent producer |
203
204
 
204
205
  ## Event Model
205
206
 
206
207
  ```python
207
208
  class KafkaEvent(BaseModel):
208
- eventId: str
209
+ eventId: str = Field(default_factory=lambda: str(uuid.uuid4()))
209
210
  eventType: str
210
211
  eventVersion: str
211
212
  source: str
212
213
  tenantId: str
213
- timestamp: datetime
214
- correlationId: Optional[str]
215
- traceId: Optional[str]
216
- priority: Optional[str]
217
- retryCount: int
218
- maxRetries: int
214
+ timestamp: datetime = Field(default_factory=datetime.utcnow)
215
+ correlationId: Optional[str] = None
216
+ traceId: Optional[str] = None
217
+ priority: Optional[str] = "MEDIUM"
218
+ retryCount: int = 0
219
+ maxRetries: int = 5
219
220
  payload: Dict[str, Any]
220
- metadata: Optional[Dict[str, Any]]
221
+ metadata: Optional[Dict[str, Any]] = None
221
222
  ```
222
223
 
223
224
  ## Exception Types
@@ -260,7 +261,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
260
261
 
261
262
  ## Changelog
262
263
 
263
- ### v0.1.3
264
+ ### v0.1.4
264
265
  - Initial release
265
266
  - Basic producer/consumer functionality
266
267
  - Event validation with Pydantic