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.
@@ -0,0 +1 @@
1
+ include redis_sdk/*.md
@@ -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
+ ```