smartinno 1.0.0__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.
- smartinno-1.0.0/MANIFEST.in +1 -0
- smartinno-1.0.0/PKG-INFO +275 -0
- smartinno-1.0.0/redis_sdk/DEVELOPER_GUIDE.md +253 -0
- smartinno-1.0.0/redis_sdk/README.md +254 -0
- smartinno-1.0.0/redis_sdk/__init__.py +15 -0
- smartinno-1.0.0/redis_sdk/clients/__init__.py +0 -0
- smartinno-1.0.0/redis_sdk/clients/base.py +405 -0
- smartinno-1.0.0/redis_sdk/clients/cluster.py +65 -0
- smartinno-1.0.0/redis_sdk/clients/hybrid.py +51 -0
- smartinno-1.0.0/redis_sdk/clients/sentinel.py +59 -0
- smartinno-1.0.0/redis_sdk/clients/smart_router.py +119 -0
- smartinno-1.0.0/redis_sdk/communication_service.py +512 -0
- smartinno-1.0.0/redis_sdk/config.py +50 -0
- smartinno-1.0.0/redis_sdk/factory.py +104 -0
- smartinno-1.0.0/redis_sdk/interfaces.py +124 -0
- smartinno-1.0.0/redis_sdk/test_sdk.py +321 -0
- smartinno-1.0.0/setup.cfg +4 -0
- smartinno-1.0.0/setup.py +28 -0
- smartinno-1.0.0/smartinno.egg-info/PKG-INFO +275 -0
- smartinno-1.0.0/smartinno.egg-info/SOURCES.txt +21 -0
- smartinno-1.0.0/smartinno.egg-info/dependency_links.txt +1 -0
- smartinno-1.0.0/smartinno.egg-info/requires.txt +2 -0
- smartinno-1.0.0/smartinno.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include redis_sdk/*.md
|
smartinno-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smartinno
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Unified Redis Python SDK for the Safari Pro architecture.
|
|
5
|
+
Author: Safari Pro Engineering
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: redis>=5.0.0
|
|
13
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
14
|
+
Dynamic: author
|
|
15
|
+
Dynamic: classifier
|
|
16
|
+
Dynamic: description
|
|
17
|
+
Dynamic: description-content-type
|
|
18
|
+
Dynamic: requires-dist
|
|
19
|
+
Dynamic: requires-python
|
|
20
|
+
Dynamic: summary
|
|
21
|
+
|
|
22
|
+
# Unified Redis Python SDK
|
|
23
|
+
|
|
24
|
+
A unified, robust Python wrapper for interacting with multiple Redis architectures seamlessly. This SDK uses the Factory Design Pattern to abstract away the complexity of connecting to different Redis setups. It natively enforces data standardization (JSON enveloping) and integrates enterprise-grade features out of the box.
|
|
25
|
+
|
|
26
|
+
## Core Features
|
|
27
|
+
|
|
28
|
+
- **Automatic Data Standardization**: All payloads sent to Redis are automatically wrapped in a standard JSON envelope containing `request_id` (a UUID), `event`, `type`, `service`, `payload`, and `timestamp`. The SDK transparently unwraps this when reading data so your application only ever sees the raw data it passed.
|
|
29
|
+
- **Key Namespacing**: All keys are prefixed with `{service_name}:{action}:{key}` to prevent collisions across microservices.
|
|
30
|
+
- **Intelligent Routing**: Use `UseCase.AUTO` to automatically route cache commands to a Native Cluster, Pub/Sub to Sentinel, and Streams to a Hybrid proxy.
|
|
31
|
+
- **Enterprise-Grade Configurations**: Built-in support for Connection Pooling, SSL/TLS, Exponential Backoff Retries, and Socket Keepalives.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Supported Architectures & Use Cases
|
|
36
|
+
|
|
37
|
+
| Architecture | `UseCase` Enum | Underlying Driver | Best For |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| **Intelligent Router** | `AUTO` | `IntelligentRouterAdapter` | Hands-off optimal routing based on command type. |
|
|
40
|
+
| **Native Redis Cluster** | `HIGH_CONCURRENCY` | `redis.cluster.RedisCluster` | Horizontal scaling, massive reads/writes. |
|
|
41
|
+
| **Sentinel + HAProxy** | `HEAVY_TRANSACTIONS` | `redis.Redis` (HAProxy) | Pub/Sub, robust transactions. |
|
|
42
|
+
| **Hybrid (Predixy Proxy)** | `MICROSERVICES` | `redis.Redis` (Standard) | Easy multi-tenant connections, stateless clients. |
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Usage Guide
|
|
47
|
+
|
|
48
|
+
### 1. Basic Import
|
|
49
|
+
```python
|
|
50
|
+
from redis_sdk.config import RedisConfig, UseCase
|
|
51
|
+
from redis_sdk.factory import RedisClientFactory
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Configuration & Enterprise Settings
|
|
55
|
+
Initialize your configuration and pass `service_name` to define your application's namespace. You can also pass enterprise features like retries and connection limits.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
cluster_config = RedisConfig(
|
|
59
|
+
host="redis-node-1",
|
|
60
|
+
port=6379,
|
|
61
|
+
password="your_password",
|
|
62
|
+
startup_nodes=[{"host": "redis-node-1", "port": 6379}],
|
|
63
|
+
|
|
64
|
+
# Core SDK configs
|
|
65
|
+
service_name="my_billing_app",
|
|
66
|
+
|
|
67
|
+
# Enterprise configs
|
|
68
|
+
ssl=False,
|
|
69
|
+
max_connections=100,
|
|
70
|
+
socket_timeout=5.0,
|
|
71
|
+
retry_on_timeout=True,
|
|
72
|
+
retry_backoff=True, # Enables Exponential Backoff
|
|
73
|
+
retry_backoff_retries=3
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. Native Cluster (`HIGH_CONCURRENCY`)
|
|
78
|
+
```python
|
|
79
|
+
client = RedisClientFactory.get_client(cluster_config, UseCase.HIGH_CONCURRENCY)
|
|
80
|
+
|
|
81
|
+
# Automatically formats key to 'my_billing_app:cache:foo'
|
|
82
|
+
# Automatically wraps data in a JSON envelope.
|
|
83
|
+
client.set("foo", {"some": "data"})
|
|
84
|
+
|
|
85
|
+
# Transparently unwraps the envelope and returns {"some": "data"}
|
|
86
|
+
print(client.get("foo"))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 4. Hybrid Proxy (`MICROSERVICES`)
|
|
90
|
+
Connects to the Native Cluster through a Predixy proxy. The client patches pipeline transactions automatically because Predixy handles clustering efficiently.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
hybrid_config = RedisConfig(
|
|
94
|
+
host="localhost",
|
|
95
|
+
port=6381,
|
|
96
|
+
password="your_password",
|
|
97
|
+
service_name="my_billing_app"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
client = RedisClientFactory.get_client(hybrid_config, UseCase.MICROSERVICES)
|
|
101
|
+
|
|
102
|
+
# Pipelines are patched automatically!
|
|
103
|
+
pipe = client.pipeline(transaction=True)
|
|
104
|
+
pipe.set("a", 1)
|
|
105
|
+
pipe.set("b", 2)
|
|
106
|
+
pipe.execute()
|
|
107
|
+
```
|
|
108
|
+
> **Note on Predixy Pipelines:** Predixy Proxy does not support cross-node `MULTI/EXEC` efficiently via python pipelines. If you pass `transaction=True`, the Hybrid adapter catches it, prints a notice, and gracefully overrides it to `transaction=False`.
|
|
109
|
+
|
|
110
|
+
### 5. Intelligent Router (`AUTO`)
|
|
111
|
+
The absolute easiest way to consume Redis. Provide the configurations for all environments to the factory, request `UseCase.AUTO`, and the router will send your queries to the optimal architecture.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
router_client = RedisClientFactory.get_client(
|
|
115
|
+
config=global_config,
|
|
116
|
+
use_case=UseCase.AUTO
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Routes to Native Cluster
|
|
120
|
+
router_client.set("cache_key", "fast_data")
|
|
121
|
+
|
|
122
|
+
# Routes to Sentinel
|
|
123
|
+
router_client.publish("global_events", "system_ready")
|
|
124
|
+
|
|
125
|
+
# Routes to Hybrid Proxy
|
|
126
|
+
router_client.xadd("audit_trail", {"action": "login"})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Running the Tests
|
|
132
|
+
|
|
133
|
+
To verify that the SDK routes correctly to all three architectures:
|
|
134
|
+
|
|
135
|
+
1. **Inside Docker (Recommended)**: Use `run_tests_in_docker.ps1` to run the tests securely inside the same docker network as the Redis instances.
|
|
136
|
+
2. **Local Windows**: Run `python test_sdk.py`. (Note: Native Cluster tests may fail gracefully if it requires docker DNS resolution).
|
|
137
|
+
# Redis SDK Developer Guide
|
|
138
|
+
|
|
139
|
+
This developer guide provides step-by-step instructions on how to use the Safari Pro Redis SDK, including how to connect to Redis, read data (both pending and new) from streams, and utilize the built-in Communication Service for sending SMS, emails, and in-app notifications.
|
|
140
|
+
|
|
141
|
+
## 1. Connecting to Redis
|
|
142
|
+
|
|
143
|
+
The SDK abstracts the underlying Redis architecture (Cluster, Sentinel, or Hybrid/Predixy) using the `RedisClientFactory` and the `UseCase` enum.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from redis_sdk.config import RedisConfig, UseCase
|
|
147
|
+
from redis_sdk.factory import RedisClientFactory
|
|
148
|
+
|
|
149
|
+
# Initialize the configuration with your service namespace
|
|
150
|
+
config = RedisConfig(
|
|
151
|
+
host="redis-node-1",
|
|
152
|
+
port=6379,
|
|
153
|
+
password="your_password",
|
|
154
|
+
startup_nodes=[{"host": "redis-node-1", "port": 6379}],
|
|
155
|
+
service_name="my_service"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Request a client tailored for high concurrency (e.g., Native Cluster)
|
|
159
|
+
client = RedisClientFactory.get_client(config, UseCase.HIGH_CONCURRENCY)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## 2. Reading Data from Redis Streams
|
|
163
|
+
|
|
164
|
+
Redis Streams is the backbone of the SDK's messaging architecture. You can use the unified `xread` method to consume data from streams. The SDK handles JSON envelop unwrapping automatically.
|
|
165
|
+
|
|
166
|
+
### Reading Pending Messages
|
|
167
|
+
To read all existing/pending messages in a stream from the beginning, pass `0` (or `"0-0"`) as the stream ID.
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
# Read pending messages from the beginning (ID 0)
|
|
171
|
+
messages = client.xread({"communication:events": 0}, count=10)
|
|
172
|
+
|
|
173
|
+
if messages:
|
|
174
|
+
for stream_name, events in messages:
|
|
175
|
+
print(f"Found {len(events)} pending messages in {stream_name}")
|
|
176
|
+
for msg_id, msg_data in events:
|
|
177
|
+
print(f"Message ID: {msg_id}, Data: {msg_data}")
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Reading New Messages
|
|
181
|
+
To listen for new messages, pass `$` as the stream ID and use the `block` parameter (in milliseconds). The client will block and wait until a new message arrives.
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
# Block and wait for up to 5000ms for new messages
|
|
185
|
+
new_messages = client.xread({"communication:events": "$"}, count=1, block=5000)
|
|
186
|
+
|
|
187
|
+
if new_messages:
|
|
188
|
+
for stream_name, events in new_messages:
|
|
189
|
+
for msg_id, msg_data in events:
|
|
190
|
+
print(f"New event received: {msg_id}")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
> [!TIP]
|
|
194
|
+
> The Intelligent Router (`UseCase.AUTO`) will automatically route stream commands like `XADD` and `XREAD` to the Hybrid architecture!
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## 3. Using the Communication Service
|
|
199
|
+
|
|
200
|
+
The `CommunicationService` is a robust module integrated into the SDK that queues messages onto Redis Streams for external communication workers to process.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from redis_sdk.communication_service import communication_service
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Sending SMS
|
|
207
|
+
|
|
208
|
+
You can send SMS messages to one or multiple recipients.
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
success = communication_service.send_sms(
|
|
212
|
+
recipients=["+254700123456", "+254711654321"],
|
|
213
|
+
message="Your Safari Pro booking is confirmed!",
|
|
214
|
+
template="booking_confirmation",
|
|
215
|
+
metadata={"booking_id": "BK-001"}
|
|
216
|
+
)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Sending Emails
|
|
220
|
+
|
|
221
|
+
The email functionality supports plain text, HTML, and templates.
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
email_result = communication_service.send_email(
|
|
225
|
+
recipients=["guest@example.com"],
|
|
226
|
+
subject="Booking Confirmation",
|
|
227
|
+
body="Your safari booking BK-001 has been confirmed.",
|
|
228
|
+
html_content="<h1>Booking Confirmed</h1>",
|
|
229
|
+
template_name="booking_email",
|
|
230
|
+
context_data={"guest_name": "Alice"},
|
|
231
|
+
metadata={"booking_id": "BK-001"}
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
print(email_result["message_id"]) # Message ID tracking
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Sending In-App Notifications
|
|
238
|
+
|
|
239
|
+
Queue an in-app notification that will be pushed to the user's notification center in real-time.
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
inapp_result = communication_service.send_inapp_notification(
|
|
243
|
+
user_id="user_123",
|
|
244
|
+
title="Booking Confirmed",
|
|
245
|
+
message="Your booking #BK-001 has been confirmed",
|
|
246
|
+
notification_type="booking_confirmed",
|
|
247
|
+
action_url="/bookings/BK-001",
|
|
248
|
+
priority="high"
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Multi-Channel Notifications (All-in-One)
|
|
253
|
+
|
|
254
|
+
Send to multiple channels simultaneously using `send_notification()`.
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
results = communication_service.send_notification(
|
|
258
|
+
notification_type="payment_received",
|
|
259
|
+
recipients={
|
|
260
|
+
"email": ["finance@safari.co"],
|
|
261
|
+
"sms": ["+254700999888"],
|
|
262
|
+
"user_id": "user_456",
|
|
263
|
+
},
|
|
264
|
+
content={
|
|
265
|
+
"email_subject": "Payment Received",
|
|
266
|
+
"email_body": "Payment of $500 received for booking BK-002.",
|
|
267
|
+
"sms_message": "Payment of $500 received. Ref: BK-002",
|
|
268
|
+
"inapp_title": "Payment Received",
|
|
269
|
+
"inapp_message": "We received your $500 payment for booking BK-002",
|
|
270
|
+
},
|
|
271
|
+
metadata={"booking_id": "BK-002", "amount": "500.00", "priority": "high"}
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
print(results) # {"email": {...}, "sms": True, "inapp": True}
|
|
275
|
+
```
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# Safari Pro: Redis & Communication Developer Guide
|
|
2
|
+
|
|
3
|
+
This developer guide details how to read data from Redis Streams (including handling **New** and **Pending** messages) and how to leverage the **Communication Service** for sending Emails, SMS, and In-App notifications.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 📚 1. Redis Streams: Reading Data (New vs. Pending)
|
|
8
|
+
|
|
9
|
+
Redis Streams (`XADD`, `XREAD`, `XREADGROUP`) act as a high-performance, append-only log suited for event-driven architectures. When consuming from a stream via **Consumer Groups**, messages fall into two categories:
|
|
10
|
+
1. **New Messages**: Messages added to the stream that have never been delivered to any consumer in the consumer group.
|
|
11
|
+
2. **Pending Messages**: Messages that were delivered to a consumer but have **not yet been acknowledged** (`XACK`). These live in the group's **Pending Entries List (PEL)**.
|
|
12
|
+
|
|
13
|
+
### How the SDK Wraps Stream Data
|
|
14
|
+
The unified SDK automatically prefixes stream keys with `{service_name}:stream:{key}` and encodes fields into a JSON envelope:
|
|
15
|
+
- **Writing**: `xadd("audit_trail", {"user": "alice"})` writes to `safari_pro:stream:audit_trail`.
|
|
16
|
+
- **Reading**: `xread` automatically extracts the `payload` block and decodes it back to a Python dictionary.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
### 📥 Reading New Messages
|
|
21
|
+
To read new messages using a consumer group, pass the special ID `>` to `XREADGROUP`. This indicates that you only want messages that have never been delivered to any other consumer in the group.
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from redis_sdk.config import RedisConfig, UseCase
|
|
25
|
+
from redis_sdk.factory import RedisClientFactory
|
|
26
|
+
|
|
27
|
+
# 1. Initialize Client
|
|
28
|
+
config = RedisConfig(
|
|
29
|
+
host="redis-node-1",
|
|
30
|
+
port=6379,
|
|
31
|
+
password="my_super_secret_cluster_pass_2026",
|
|
32
|
+
service_name="safari_pro"
|
|
33
|
+
)
|
|
34
|
+
client = RedisClientFactory.get_client(config, UseCase.AUTO)
|
|
35
|
+
|
|
36
|
+
# Note: Since 'xgroup_create' is not explicitly wrapped,
|
|
37
|
+
# it transparently proxies to the native redis-py driver with Circuit Breaker protection.
|
|
38
|
+
stream_key = "safari_pro:stream:booking_events"
|
|
39
|
+
group_name = "booking_processors"
|
|
40
|
+
|
|
41
|
+
# 2. Create the consumer group if it doesn't exist
|
|
42
|
+
try:
|
|
43
|
+
client.xgroup_create(name=stream_key, groupname=group_name, id="0", mkstream=True)
|
|
44
|
+
except Exception:
|
|
45
|
+
# Group already exists
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
# 3. Read NEW messages (using '>')
|
|
49
|
+
# This block blocks for up to 2 seconds (2000ms) waiting for new messages
|
|
50
|
+
response = client.xreadgroup(
|
|
51
|
+
groupname=group_name,
|
|
52
|
+
consumername="worker-1",
|
|
53
|
+
streams={stream_key: ">"},
|
|
54
|
+
count=10,
|
|
55
|
+
block=2000
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if response:
|
|
59
|
+
for stream, messages in response:
|
|
60
|
+
for msg_id, fields in messages:
|
|
61
|
+
print(f"New Message Received [{msg_id}]: {fields}")
|
|
62
|
+
|
|
63
|
+
# 4. Acknowledge the message once processed successfully
|
|
64
|
+
client.xack(stream_key, group_name, msg_id)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### 🔄 Reading & Resolving Pending Messages
|
|
70
|
+
If a consumer crashes after reading a message but before acknowledging it with `XACK`, that message remains in the **Pending Entries List (PEL)**. If the worker restarts, it should first read and process its pending messages before requesting new ones.
|
|
71
|
+
|
|
72
|
+
To retrieve pending messages, read from the stream using the **start ID (e.g. `0-0` or `0`)** instead of `>`. This tells Redis to return messages assigned to this consumer that have not been acknowledged.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
# 1. Read PENDING messages assigned to "worker-1"
|
|
76
|
+
pending_response = client.xreadgroup(
|
|
77
|
+
groupname=group_name,
|
|
78
|
+
consumername="worker-1",
|
|
79
|
+
streams={stream_key: "0"}, # "0" fetches pending messages for this consumer
|
|
80
|
+
count=10
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if pending_response:
|
|
84
|
+
for stream, messages in pending_response:
|
|
85
|
+
for msg_id, fields in messages:
|
|
86
|
+
print(f"Reprocessing Pending Message [{msg_id}]: {fields}")
|
|
87
|
+
|
|
88
|
+
# Process and ACK
|
|
89
|
+
client.xack(stream_key, group_name, msg_id)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 🚨 Dead-Letter & Orphaned Message Recovery (`XPENDING` and `XCLAIM`)
|
|
93
|
+
If a worker dies permanently, its pending messages will be stuck forever unless claimed by another worker. You can inspect stuck messages and transfer ownership using `xpending` and `xclaim`:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import time
|
|
97
|
+
|
|
98
|
+
# Find messages pending for more than 30 seconds
|
|
99
|
+
# xpending_range(name, group, idle_min_time_ms, start, end, count)
|
|
100
|
+
pending_info = client.xpending_range(
|
|
101
|
+
name=stream_key,
|
|
102
|
+
group=group_name,
|
|
103
|
+
min_idle_time=30000, # 30 seconds idle
|
|
104
|
+
start="-",
|
|
105
|
+
end="+",
|
|
106
|
+
count=10
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
for item in pending_info:
|
|
110
|
+
msg_id = item["message_id"]
|
|
111
|
+
consumer = item["consumer"]
|
|
112
|
+
idle_time = item["elapsed_time"]
|
|
113
|
+
|
|
114
|
+
print(f"Message {msg_id} is dead/idle for {idle_time}ms on consumer {consumer}.")
|
|
115
|
+
|
|
116
|
+
# Claim ownership of the message for "worker-2"
|
|
117
|
+
client.xclaim(
|
|
118
|
+
name=stream_key,
|
|
119
|
+
group=group_name,
|
|
120
|
+
consumername="worker-2",
|
|
121
|
+
min_idle_time=30000,
|
|
122
|
+
message_ids=[msg_id]
|
|
123
|
+
)
|
|
124
|
+
print(f"Successfully claimed {msg_id} to worker-2")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## ✉️ 2. Using the Communication Service
|
|
130
|
+
|
|
131
|
+
The `CommunicationService` provides a unified API for dispatching SMS, Email, and In-App notifications. Under the hood, it pushes these events to a unified Redis stream (`communication:events`), decoupling the frontend/API from delivery workers.
|
|
132
|
+
|
|
133
|
+
### Import and Access
|
|
134
|
+
Always use the pre-configured singleton instance:
|
|
135
|
+
```python
|
|
136
|
+
from redis_sdk.communication_service import communication_service
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### 📱 A. Sending SMS
|
|
142
|
+
Sends short text messages. The phone number is validated automatically before being queued.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
success = communication_service.send_sms(
|
|
146
|
+
recipients=["+254700123456", "+254711654321"],
|
|
147
|
+
message="Your Safari Pro booking is confirmed! Details: safari.pro/b/BK-902",
|
|
148
|
+
template="booking_confirmation",
|
|
149
|
+
metadata={"booking_id": "BK-902"}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if success:
|
|
153
|
+
print("SMS queued successfully!")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Parameters:**
|
|
157
|
+
- `recipients`: A phone number string or list of phone numbers.
|
|
158
|
+
- `message`: Plain text content.
|
|
159
|
+
- `template`: Optional template identifier (useful for transactional SMS providers).
|
|
160
|
+
- `metadata`: Optional dict containing analytics fields or tracking tags.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
### 📧 B. Sending Emails
|
|
165
|
+
Supports plain text, custom HTML structures, template context injection, and document attachments.
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
result = communication_service.send_email(
|
|
169
|
+
recipients=["guest@example.com"],
|
|
170
|
+
subject="Welcome to Safari Pro!",
|
|
171
|
+
body="Thank you for signing up for Safari Pro.",
|
|
172
|
+
html_content="<h1>Welcome!</h1><p>We are thrilled to have you join us.</p>",
|
|
173
|
+
template_name="welcome_user",
|
|
174
|
+
context_data={"username": "Alice"},
|
|
175
|
+
attachments=[
|
|
176
|
+
{
|
|
177
|
+
"filename": "itinerary.pdf",
|
|
178
|
+
"content": "BASE64_ENCODED_STRING...",
|
|
179
|
+
"content_type": "application/pdf"
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
metadata={"user_id": "usr_9021"}
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Returns a dictionary: {"success": bool, "message_id": str, "queued": bool}
|
|
186
|
+
if result.get("success"):
|
|
187
|
+
print(f"Email queued. Msg ID: {result['message_id']}")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### 🔔 C. Sending In-App Notifications
|
|
193
|
+
Queues real-time alerts that appear directly inside the user's notification center within the Safari Pro app.
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
result = communication_service.send_inapp_notification(
|
|
197
|
+
user_id="user_123",
|
|
198
|
+
title="New Booking Request",
|
|
199
|
+
message="A client requested a booking on 'Maasai Mara Express'.",
|
|
200
|
+
notification_type="booking_request",
|
|
201
|
+
action_url="/partner/bookings/BK-902",
|
|
202
|
+
metadata={"booking_id": "BK-902"},
|
|
203
|
+
priority="high", # 'low', 'normal', 'high', 'critical'
|
|
204
|
+
requires_action=True # Forces the user to explicitly dismiss or act on it
|
|
205
|
+
)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
### 📣 D. Multi-Channel Notifications
|
|
211
|
+
When you need to alert a user across multiple channels simultaneously (e.g. notify a host of a payment via Email + SMS + In-App notification), use the combined `send_notification` method.
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
channels_status = communication_service.send_notification(
|
|
215
|
+
notification_type="payment_received",
|
|
216
|
+
recipients={
|
|
217
|
+
"email": ["host@example.com"],
|
|
218
|
+
"sms": ["+254700999888"],
|
|
219
|
+
"user_id": "host_456"
|
|
220
|
+
},
|
|
221
|
+
content={
|
|
222
|
+
"email_subject": "Payment Received for Booking BK-002",
|
|
223
|
+
"email_body": "Payment of $1,500 has been successfully captured.",
|
|
224
|
+
"email_html": "<h2>Payment Captured</h2><p>Payment of $1,500 has been captured.</p>",
|
|
225
|
+
"sms_message": "Safari Pro: $1,500 payment received. Ref: BK-002",
|
|
226
|
+
"inapp_title": "Payment Captured",
|
|
227
|
+
"inapp_message": "$1,500 payment received from guest."
|
|
228
|
+
},
|
|
229
|
+
metadata={"booking_id": "BK-002", "amount": "1500.00"}
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Returns a dictionary showing success per channel:
|
|
233
|
+
# Example: {"email": {"success": True, ...}, "sms": True, "inapp": True}
|
|
234
|
+
print("Notification results:", channels_status)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
### ⚙️ E. Checking Communication Service Health
|
|
240
|
+
To monitor the communication pipeline:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
status = communication_service.get_service_status()
|
|
244
|
+
print(status)
|
|
245
|
+
# Output:
|
|
246
|
+
# {
|
|
247
|
+
# "redis_connected": True,
|
|
248
|
+
# "redis_healthy": True,
|
|
249
|
+
# "sms_stream": "communication:events",
|
|
250
|
+
# "email_stream": "communication:events",
|
|
251
|
+
# "inapp_stream": "communication:events"
|
|
252
|
+
# }
|
|
253
|
+
```
|