wappa 0.1.7__py3-none-any.whl → 0.1.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wappa might be problematic. Click here for more details.
- wappa/cli/examples/init/.env.example +33 -0
- wappa/cli/examples/init/app/__init__.py +0 -0
- wappa/cli/examples/init/app/main.py +8 -0
- wappa/cli/examples/init/app/master_event.py +8 -0
- wappa/cli/examples/json_cache_example/.env.example +33 -0
- wappa/cli/examples/json_cache_example/app/__init__.py +1 -0
- wappa/cli/examples/json_cache_example/app/main.py +235 -0
- wappa/cli/examples/json_cache_example/app/master_event.py +419 -0
- wappa/cli/examples/json_cache_example/app/models/__init__.py +1 -0
- wappa/cli/examples/json_cache_example/app/models/json_demo_models.py +275 -0
- wappa/cli/examples/json_cache_example/app/scores/__init__.py +35 -0
- wappa/cli/examples/json_cache_example/app/scores/score_base.py +186 -0
- wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +248 -0
- wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +190 -0
- wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +260 -0
- wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +223 -0
- wappa/cli/examples/json_cache_example/app/utils/__init__.py +26 -0
- wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +176 -0
- wappa/cli/examples/json_cache_example/app/utils/message_utils.py +246 -0
- wappa/cli/examples/openai_transcript/.gitignore +63 -4
- wappa/cli/examples/openai_transcript/app/__init__.py +0 -0
- wappa/cli/examples/openai_transcript/app/main.py +8 -0
- wappa/cli/examples/openai_transcript/app/master_event.py +53 -0
- wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +3 -0
- wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +76 -0
- wappa/cli/examples/redis_cache_example/.env.example +33 -0
- wappa/cli/examples/redis_cache_example/app/__init__.py +6 -0
- wappa/cli/examples/redis_cache_example/app/main.py +234 -0
- wappa/cli/examples/redis_cache_example/app/master_event.py +419 -0
- wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +275 -0
- wappa/cli/examples/redis_cache_example/app/scores/__init__.py +35 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_base.py +186 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +248 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +190 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +260 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +223 -0
- wappa/cli/examples/redis_cache_example/app/utils/__init__.py +26 -0
- wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +176 -0
- wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +246 -0
- wappa/cli/examples/simple_echo_example/.env.example +33 -0
- wappa/cli/examples/simple_echo_example/app/__init__.py +7 -0
- wappa/cli/examples/simple_echo_example/app/main.py +183 -0
- wappa/cli/examples/simple_echo_example/app/master_event.py +209 -0
- wappa/cli/examples/wappa_full_example/.env.example +33 -0
- wappa/cli/examples/wappa_full_example/.gitignore +63 -4
- wappa/cli/examples/wappa_full_example/app/__init__.py +6 -0
- wappa/cli/examples/wappa_full_example/app/handlers/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/handlers/command_handlers.py +484 -0
- wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +551 -0
- wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +492 -0
- wappa/cli/examples/wappa_full_example/app/main.py +257 -0
- wappa/cli/examples/wappa_full_example/app/master_event.py +445 -0
- wappa/cli/examples/wappa_full_example/app/media/README.md +54 -0
- wappa/cli/examples/wappa_full_example/app/media/buttons/README.md +62 -0
- wappa/cli/examples/wappa_full_example/app/media/buttons/kitty.png +0 -0
- wappa/cli/examples/wappa_full_example/app/media/buttons/puppy.png +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/README.md +110 -0
- wappa/cli/examples/wappa_full_example/app/media/list/audio.mp3 +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/document.pdf +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/image.png +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/video.mp4 +0 -0
- wappa/cli/examples/wappa_full_example/app/models/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/models/state_models.py +425 -0
- wappa/cli/examples/wappa_full_example/app/models/user_models.py +287 -0
- wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +301 -0
- wappa/cli/examples/wappa_full_example/app/utils/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +483 -0
- wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +473 -0
- wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +298 -0
- wappa/cli/main.py +8 -4
- wappa/core/config/settings.py +34 -2
- wappa/persistence/__init__.py +2 -2
- {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/METADATA +1 -1
- {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/RECORD +77 -13
- wappa/cli/examples/init/pyproject.toml +0 -7
- wappa/cli/examples/simple_echo_example/.python-version +0 -1
- wappa/cli/examples/simple_echo_example/pyproject.toml +0 -9
- {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/WHEEL +0 -0
- {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/entry_points.txt +0 -0
- {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Master Event Handler - WappaEventHandler implementation following SOLID principles.
|
|
3
|
+
|
|
4
|
+
This module defines the main WappaEventHandler that:
|
|
5
|
+
- Extends WappaEventHandler with proper method signatures
|
|
6
|
+
- Coordinates multiple score modules using dependency injection
|
|
7
|
+
- Follows Single Responsibility Principle for event handling
|
|
8
|
+
- Uses Open/Closed Principle for score module extensibility
|
|
9
|
+
- Implements Liskov Substitution for handler compatibility
|
|
10
|
+
- Uses Interface Segregation with focused score interfaces
|
|
11
|
+
- Follows Dependency Inversion with injected dependencies
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Any, Dict
|
|
16
|
+
|
|
17
|
+
from .scores import AVAILABLE_SCORES, ScoreBase, ScoreDependencies
|
|
18
|
+
from .scores.score_base import ScoreRegistry
|
|
19
|
+
from .utils.message_utils import extract_user_data, sanitize_message_text
|
|
20
|
+
from wappa import WappaEventHandler
|
|
21
|
+
from wappa.webhooks import ErrorWebhook, IncomingMessageWebhook, StatusWebhook
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class JSONCacheExampleHandler(WappaEventHandler):
|
|
25
|
+
"""
|
|
26
|
+
Main WappaEventHandler implementation for JSON cache example following SOLID principles.
|
|
27
|
+
|
|
28
|
+
This handler serves as the main entry point for the Wappa framework and demonstrates:
|
|
29
|
+
- Proper WappaEventHandler method implementations
|
|
30
|
+
- SOLID architecture with score module orchestration
|
|
31
|
+
- Dependency injection and lifecycle management
|
|
32
|
+
- Professional error handling and logging
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
"""Initialize the JSON cache example handler."""
|
|
37
|
+
super().__init__()
|
|
38
|
+
|
|
39
|
+
# Score module registry (following Open/Closed Principle)
|
|
40
|
+
self.score_registry = ScoreRegistry()
|
|
41
|
+
|
|
42
|
+
# Processing statistics
|
|
43
|
+
self._total_messages = 0
|
|
44
|
+
self._successful_processing = 0
|
|
45
|
+
self._failed_processing = 0
|
|
46
|
+
|
|
47
|
+
# Master handler state
|
|
48
|
+
self._initialized = False
|
|
49
|
+
|
|
50
|
+
self.logger.info("🎯 JSONCacheExampleHandler initialized - ready for SOLID architecture setup")
|
|
51
|
+
|
|
52
|
+
async def process_message(self, webhook: IncomingMessageWebhook) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Main message processing method required by WappaEventHandler.
|
|
55
|
+
|
|
56
|
+
This method orchestrates score modules following SOLID principles and
|
|
57
|
+
demonstrates proper webhook processing with dependency injection.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
webhook: Incoming message webhook to process
|
|
61
|
+
"""
|
|
62
|
+
self._total_messages += 1
|
|
63
|
+
start_time = self._get_current_timestamp()
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# Initialize SOLID architecture on first message if not already done
|
|
67
|
+
if not self._initialized:
|
|
68
|
+
await self._initialize_solid_architecture()
|
|
69
|
+
|
|
70
|
+
# Extract basic user information for logging
|
|
71
|
+
user_data = extract_user_data(webhook)
|
|
72
|
+
user_id = user_data['user_id']
|
|
73
|
+
message_text = webhook.get_message_text() or "[NON-TEXT MESSAGE]"
|
|
74
|
+
|
|
75
|
+
self.logger.info(
|
|
76
|
+
f"📨 Processing message from {user_id}: "
|
|
77
|
+
f"{sanitize_message_text(message_text)[:50]}..."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Execute score module processing pipeline
|
|
81
|
+
processing_result = await self._execute_score_pipeline(webhook)
|
|
82
|
+
|
|
83
|
+
# Record processing results
|
|
84
|
+
if processing_result['success']:
|
|
85
|
+
self._successful_processing += 1
|
|
86
|
+
processing_time = self._get_current_timestamp() - start_time
|
|
87
|
+
|
|
88
|
+
self.logger.info(
|
|
89
|
+
f"✅ Message processed successfully in {processing_time:.2f}s "
|
|
90
|
+
f"(processed by {processing_result['processed_count']} score modules)"
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
self._failed_processing += 1
|
|
94
|
+
self.logger.warning(
|
|
95
|
+
f"⚠️ Message processing completed with issues: "
|
|
96
|
+
f"{processing_result.get('error', 'Unknown error')}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Send fallback response to user
|
|
100
|
+
await self._send_error_response(webhook, processing_result.get('error', 'Processing error'))
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
self._failed_processing += 1
|
|
104
|
+
self.logger.error(f"❌ Critical error in message processing: {e}", exc_info=True)
|
|
105
|
+
await self._send_error_response(webhook, f"System error: {str(e)}")
|
|
106
|
+
|
|
107
|
+
async def process_status(self, webhook: StatusWebhook) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Process status webhooks from WhatsApp Business API.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
webhook: Status webhook containing delivery status information
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
status_value = webhook.status.value
|
|
116
|
+
recipient = webhook.recipient_id
|
|
117
|
+
|
|
118
|
+
self.logger.info(f"📊 Message status: {status_value.upper()} for {recipient}")
|
|
119
|
+
|
|
120
|
+
# You can add custom status processing logic here
|
|
121
|
+
# For example, updating delivery statistics or handling failed deliveries
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
self.logger.error(f"❌ Error processing status webhook: {e}", exc_info=True)
|
|
125
|
+
|
|
126
|
+
async def process_error(self, webhook: ErrorWebhook) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Process error webhooks from WhatsApp Business API.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
webhook: Error webhook containing error information
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
error_count = webhook.get_error_count()
|
|
135
|
+
primary_error = webhook.get_primary_error()
|
|
136
|
+
|
|
137
|
+
self.logger.error(
|
|
138
|
+
f"🚨 WhatsApp API error: {error_count} errors, "
|
|
139
|
+
f"primary: {primary_error.error_code} - {primary_error.error_title}"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Record error in statistics
|
|
143
|
+
self._failed_processing += 1
|
|
144
|
+
|
|
145
|
+
# You can add custom error handling logic here
|
|
146
|
+
# For example, alerting systems or retry mechanisms
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
self.logger.error(f"❌ Error processing error webhook: {e}", exc_info=True)
|
|
150
|
+
|
|
151
|
+
async def _initialize_solid_architecture(self) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Initialize SOLID architecture with score modules and dependency injection.
|
|
154
|
+
|
|
155
|
+
This method demonstrates Dependency Inversion Principle by injecting
|
|
156
|
+
abstractions and follows Single Responsibility Principle.
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
if not self.validate_dependencies():
|
|
160
|
+
self.logger.error("❌ Dependencies not properly injected - cannot initialize SOLID architecture")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
if not self.cache_factory:
|
|
164
|
+
self.logger.error("❌ Cache factory not available - cannot initialize SOLID architecture")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# Create cache instances from factory (Dependency Inversion)
|
|
168
|
+
user_cache = self.cache_factory.create_user_cache()
|
|
169
|
+
table_cache = self.cache_factory.create_table_cache()
|
|
170
|
+
state_cache = self.cache_factory.create_state_cache()
|
|
171
|
+
|
|
172
|
+
# Create dependencies container
|
|
173
|
+
dependencies = ScoreDependencies(
|
|
174
|
+
messenger=self.messenger,
|
|
175
|
+
user_cache=user_cache,
|
|
176
|
+
table_cache=table_cache,
|
|
177
|
+
state_cache=state_cache,
|
|
178
|
+
logger=self.logger
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Auto-register all available score modules (Open/Closed Principle)
|
|
182
|
+
registered_count = 0
|
|
183
|
+
for score_class in AVAILABLE_SCORES:
|
|
184
|
+
try:
|
|
185
|
+
# Instantiate score with dependency injection
|
|
186
|
+
score_instance = score_class(dependencies)
|
|
187
|
+
self.score_registry.register_score(score_instance)
|
|
188
|
+
registered_count += 1
|
|
189
|
+
|
|
190
|
+
self.logger.info(
|
|
191
|
+
f"✅ Registered score module: {score_instance.score_name}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
except Exception as e:
|
|
195
|
+
self.logger.error(
|
|
196
|
+
f"❌ Failed to register {score_class.__name__}: {e}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
self._initialized = True
|
|
200
|
+
self.logger.info(
|
|
201
|
+
f"🎯 SOLID architecture initialized successfully: {registered_count} score modules registered"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
self.logger.error(f"❌ Critical error initializing SOLID architecture: {e}", exc_info=True)
|
|
206
|
+
raise
|
|
207
|
+
|
|
208
|
+
async def _execute_score_pipeline(self, webhook: IncomingMessageWebhook) -> Dict[str, Any]:
|
|
209
|
+
"""
|
|
210
|
+
Execute the score module processing pipeline.
|
|
211
|
+
|
|
212
|
+
Processes webhook through all applicable score modules following
|
|
213
|
+
the Chain of Responsibility pattern.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
webhook: Webhook to process
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Processing result with success status and metadata
|
|
220
|
+
"""
|
|
221
|
+
try:
|
|
222
|
+
if not self._initialized:
|
|
223
|
+
return {
|
|
224
|
+
'success': False,
|
|
225
|
+
'error': 'SOLID architecture not initialized',
|
|
226
|
+
'processed_count': 0
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
scores = self.score_registry.get_scores()
|
|
230
|
+
processed_count = 0
|
|
231
|
+
processing_errors = []
|
|
232
|
+
|
|
233
|
+
# Process webhook through all applicable score modules
|
|
234
|
+
for score in scores:
|
|
235
|
+
try:
|
|
236
|
+
# Check if score can handle this webhook (Interface Segregation)
|
|
237
|
+
can_handle = await score.can_handle(webhook)
|
|
238
|
+
|
|
239
|
+
if can_handle:
|
|
240
|
+
self.logger.debug(f"🎯 Processing with {score.score_name}")
|
|
241
|
+
|
|
242
|
+
# Process with the score module
|
|
243
|
+
success = await score.process(webhook)
|
|
244
|
+
|
|
245
|
+
if success:
|
|
246
|
+
processed_count += 1
|
|
247
|
+
self.logger.debug(f"✅ {score.score_name} completed successfully")
|
|
248
|
+
else:
|
|
249
|
+
processing_errors.append(f"{score.score_name}: Processing failed")
|
|
250
|
+
self.logger.warning(f"⚠️ {score.score_name} reported processing failure")
|
|
251
|
+
else:
|
|
252
|
+
self.logger.debug(f"⏭️ {score.score_name} skipped (cannot handle this webhook)")
|
|
253
|
+
|
|
254
|
+
except Exception as score_error:
|
|
255
|
+
processing_errors.append(f"{score.score_name}: {str(score_error)}")
|
|
256
|
+
self.logger.error(
|
|
257
|
+
f"❌ Error in {score.score_name}: {score_error}",
|
|
258
|
+
exc_info=True
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Determine overall success
|
|
262
|
+
overall_success = processed_count > 0 and len(processing_errors) == 0
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
'success': overall_success,
|
|
266
|
+
'processed_count': processed_count,
|
|
267
|
+
'total_scores': len(scores),
|
|
268
|
+
'errors': processing_errors if processing_errors else None,
|
|
269
|
+
'message': (
|
|
270
|
+
f"Processed by {processed_count}/{len(scores)} score modules"
|
|
271
|
+
+ (f" with {len(processing_errors)} errors" if processing_errors else "")
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
except Exception as e:
|
|
276
|
+
self.logger.error(f"❌ Critical error in score pipeline: {e}", exc_info=True)
|
|
277
|
+
return {
|
|
278
|
+
'success': False,
|
|
279
|
+
'processed_count': 0,
|
|
280
|
+
'error': f"Pipeline error: {str(e)}"
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async def _send_error_response(self, webhook: IncomingMessageWebhook, error_details: str) -> None:
|
|
284
|
+
"""
|
|
285
|
+
Send user-friendly error response when processing fails.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
webhook: Original webhook that failed to process
|
|
289
|
+
error_details: Details about the error for logging
|
|
290
|
+
"""
|
|
291
|
+
try:
|
|
292
|
+
user_data = extract_user_data(webhook)
|
|
293
|
+
user_id = user_data['user_id']
|
|
294
|
+
|
|
295
|
+
error_message = (
|
|
296
|
+
"🚨 SOLID JSON Cache Example\n\n"
|
|
297
|
+
"❌ An error occurred while processing your message.\n"
|
|
298
|
+
"Our team has been notified and will resolve this issue soon.\n\n"
|
|
299
|
+
"Please try again later or contact support if the problem persists."
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
result = await self.messenger.send_text(
|
|
303
|
+
recipient=user_id,
|
|
304
|
+
text=error_message,
|
|
305
|
+
reply_to_message_id=webhook.message.message_id
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if result.success:
|
|
309
|
+
self.logger.info(f"🚨 Error response sent to {user_id}")
|
|
310
|
+
else:
|
|
311
|
+
self.logger.error(f"❌ Failed to send error response: {result.error}")
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
self.logger.error(f"❌ Error sending error response: {e}")
|
|
315
|
+
|
|
316
|
+
def _get_current_timestamp(self) -> float:
|
|
317
|
+
"""Get current timestamp for performance measurement."""
|
|
318
|
+
import time
|
|
319
|
+
return time.time()
|
|
320
|
+
|
|
321
|
+
async def get_handler_statistics(self) -> Dict[str, Any]:
|
|
322
|
+
"""
|
|
323
|
+
Get comprehensive handler and score module statistics.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Dictionary with processing statistics and score module metrics
|
|
327
|
+
"""
|
|
328
|
+
try:
|
|
329
|
+
# Calculate success rate
|
|
330
|
+
success_rate = (
|
|
331
|
+
(self._successful_processing / self._total_messages)
|
|
332
|
+
if self._total_messages > 0 else 0.0
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Get score-specific statistics if initialized
|
|
336
|
+
score_stats = {}
|
|
337
|
+
if self._initialized:
|
|
338
|
+
score_stats = self.score_registry.get_score_stats()
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
'handler_status': 'initialized' if self._initialized else 'pending_initialization',
|
|
342
|
+
'total_messages': self._total_messages,
|
|
343
|
+
'successful_processing': self._successful_processing,
|
|
344
|
+
'failed_processing': self._failed_processing,
|
|
345
|
+
'success_rate': success_rate,
|
|
346
|
+
'registered_scores': len(self.score_registry.get_scores()) if self._initialized else 0,
|
|
347
|
+
'score_modules': score_stats
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
except Exception as e:
|
|
351
|
+
self.logger.error(f"❌ Error collecting handler statistics: {e}")
|
|
352
|
+
return {
|
|
353
|
+
'error': f"Statistics collection failed: {str(e)}"
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async def validate_system_health(self) -> Dict[str, Any]:
|
|
357
|
+
"""
|
|
358
|
+
Validate system health including all score modules and dependencies.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Health check results for the entire system
|
|
362
|
+
"""
|
|
363
|
+
try:
|
|
364
|
+
health_results = {
|
|
365
|
+
'overall_healthy': True,
|
|
366
|
+
'initialized': self._initialized,
|
|
367
|
+
'components': {},
|
|
368
|
+
'registered_scores': len(self.score_registry.get_scores()) if self._initialized else 0
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# Check core dependencies
|
|
372
|
+
core_components = {
|
|
373
|
+
'messenger': self.messenger,
|
|
374
|
+
'cache_factory': self.cache_factory
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
for component_name, component in core_components.items():
|
|
378
|
+
if component is not None:
|
|
379
|
+
health_results['components'][component_name] = 'Available'
|
|
380
|
+
else:
|
|
381
|
+
health_results['components'][component_name] = 'Missing'
|
|
382
|
+
health_results['overall_healthy'] = False
|
|
383
|
+
|
|
384
|
+
# Check score modules if initialized
|
|
385
|
+
if self._initialized:
|
|
386
|
+
scores = self.score_registry.get_scores()
|
|
387
|
+
for score in scores:
|
|
388
|
+
try:
|
|
389
|
+
# Basic validation check
|
|
390
|
+
is_valid = await score.validate_dependencies()
|
|
391
|
+
health_results['components'][score.score_name] = (
|
|
392
|
+
'Healthy' if is_valid else 'Dependency Issues'
|
|
393
|
+
)
|
|
394
|
+
if not is_valid:
|
|
395
|
+
health_results['overall_healthy'] = False
|
|
396
|
+
|
|
397
|
+
except Exception as e:
|
|
398
|
+
health_results['components'][score.score_name] = f'Error: {str(e)}'
|
|
399
|
+
health_results['overall_healthy'] = False
|
|
400
|
+
|
|
401
|
+
return health_results
|
|
402
|
+
|
|
403
|
+
except Exception as e:
|
|
404
|
+
self.logger.error(f"❌ Error validating system health: {e}")
|
|
405
|
+
return {
|
|
406
|
+
'overall_healthy': False,
|
|
407
|
+
'error': f"Health check failed: {str(e)}"
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
def __str__(self) -> str:
|
|
411
|
+
"""String representation of the handler."""
|
|
412
|
+
return (
|
|
413
|
+
f"JSONCacheExampleHandler("
|
|
414
|
+
f"messages={self._total_messages}, "
|
|
415
|
+
f"success_rate={self._successful_processing/max(1, self._total_messages):.2%}, "
|
|
416
|
+
f"scores={len(self.score_registry.get_scores()) if self._initialized else 'pending'}, "
|
|
417
|
+
f"initialized={self._initialized}"
|
|
418
|
+
f")"
|
|
419
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""JSON cache example models package."""
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic models for JSON cache demonstration.
|
|
3
|
+
|
|
4
|
+
These models demonstrate how to structure data for different cache types
|
|
5
|
+
in the Wappa JSON caching system.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field, field_validator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MessageHistory(BaseModel):
|
|
14
|
+
"""
|
|
15
|
+
Individual message entry for message history storage.
|
|
16
|
+
|
|
17
|
+
Stores a single message with its timestamp.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
message: str = Field(
|
|
21
|
+
...,
|
|
22
|
+
description="The message content or type description",
|
|
23
|
+
max_length=500
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
timestamp: datetime = Field(
|
|
27
|
+
default_factory=datetime.utcnow,
|
|
28
|
+
description="When the message was sent"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
message_type: str = Field(
|
|
32
|
+
default="text",
|
|
33
|
+
description="Type of message (text, image, audio, etc.)",
|
|
34
|
+
max_length=20
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class User(BaseModel):
|
|
39
|
+
"""
|
|
40
|
+
User profile model for user_cache demonstration.
|
|
41
|
+
|
|
42
|
+
Stores user information extracted from WhatsApp webhook data.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
phone_number: str = Field(
|
|
46
|
+
...,
|
|
47
|
+
description="User's phone number (WhatsApp ID)",
|
|
48
|
+
min_length=10,
|
|
49
|
+
max_length=20
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
user_name: str | None = Field(
|
|
53
|
+
None,
|
|
54
|
+
description="User's display name from WhatsApp profile",
|
|
55
|
+
max_length=100
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
first_seen: datetime = Field(
|
|
59
|
+
default_factory=datetime.utcnow,
|
|
60
|
+
description="When the user was first seen"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
last_seen: datetime = Field(
|
|
64
|
+
default_factory=datetime.utcnow,
|
|
65
|
+
description="When the user was last seen"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
message_count: int = Field(
|
|
69
|
+
default=0,
|
|
70
|
+
description="Total number of messages received from this user",
|
|
71
|
+
ge=0
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
is_active: bool = Field(
|
|
75
|
+
default=True,
|
|
76
|
+
description="Whether the user is currently active"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@field_validator('phone_number', mode='before')
|
|
80
|
+
@classmethod
|
|
81
|
+
def validate_phone_number(cls, v) -> str:
|
|
82
|
+
"""Convert phone number to string if it's an integer."""
|
|
83
|
+
if isinstance(v, int):
|
|
84
|
+
return str(v)
|
|
85
|
+
return v
|
|
86
|
+
|
|
87
|
+
def increment_message_count(self) -> None:
|
|
88
|
+
"""Increment the message count and update last_seen timestamp."""
|
|
89
|
+
self.message_count += 1
|
|
90
|
+
self.last_seen = datetime.utcnow()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class MessageLog(BaseModel):
|
|
94
|
+
"""
|
|
95
|
+
Message log model for table_cache demonstration.
|
|
96
|
+
|
|
97
|
+
Stores message history for a user with waid as primary key.
|
|
98
|
+
Contains a list of all messages sent by the user.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
user_id: str = Field(
|
|
102
|
+
...,
|
|
103
|
+
description="User's phone number/ID (primary key)",
|
|
104
|
+
min_length=10,
|
|
105
|
+
max_length=20
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
text_message: list[MessageHistory] = Field(
|
|
109
|
+
default_factory=list,
|
|
110
|
+
description="List of all messages sent by this user"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
tenant_id: str | None = Field(
|
|
114
|
+
None,
|
|
115
|
+
description="Tenant/business ID that received the messages",
|
|
116
|
+
max_length=100
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def get_log_key(self) -> str:
|
|
120
|
+
"""Generate the primary key for this user's message log."""
|
|
121
|
+
return f"msg_history:{self.user_id}"
|
|
122
|
+
|
|
123
|
+
def add_message(self, message: str, message_type: str = "text") -> None:
|
|
124
|
+
"""Add a new message to the user's history."""
|
|
125
|
+
new_message = MessageHistory(
|
|
126
|
+
message=message,
|
|
127
|
+
message_type=message_type,
|
|
128
|
+
timestamp=datetime.utcnow()
|
|
129
|
+
)
|
|
130
|
+
self.text_message.append(new_message)
|
|
131
|
+
|
|
132
|
+
def get_recent_messages(self, count: int = 10) -> list[MessageHistory]:
|
|
133
|
+
"""Get the most recent messages from the user's history."""
|
|
134
|
+
return self.text_message[-count:] if self.text_message else []
|
|
135
|
+
|
|
136
|
+
@field_validator('user_id', 'tenant_id', mode='before')
|
|
137
|
+
@classmethod
|
|
138
|
+
def validate_string_ids(cls, v) -> str:
|
|
139
|
+
"""Convert ID fields to string if they're integers."""
|
|
140
|
+
if isinstance(v, int):
|
|
141
|
+
return str(v)
|
|
142
|
+
return v
|
|
143
|
+
|
|
144
|
+
def get_message_count(self) -> int:
|
|
145
|
+
"""Get the total number of messages in the history."""
|
|
146
|
+
return len(self.text_message)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class StateHandler(BaseModel):
|
|
150
|
+
"""
|
|
151
|
+
State handler model for state_cache demonstration.
|
|
152
|
+
|
|
153
|
+
Manages user state for the /WAPPA command flow.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
is_wappa: bool = Field(
|
|
157
|
+
default=False,
|
|
158
|
+
description="Whether the user is in 'WAPPA' state"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
activated_at: datetime | None = Field(
|
|
162
|
+
None,
|
|
163
|
+
description="When the WAPPA state was activated"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
command_count: int = Field(
|
|
167
|
+
default=0,
|
|
168
|
+
description="Number of commands processed while in WAPPA state",
|
|
169
|
+
ge=0
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
last_command: str | None = Field(
|
|
173
|
+
None,
|
|
174
|
+
description="Last command processed in WAPPA state",
|
|
175
|
+
max_length=100
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def activate_wappa(self) -> None:
|
|
179
|
+
"""Activate WAPPA state."""
|
|
180
|
+
self.is_wappa = True
|
|
181
|
+
self.activated_at = datetime.utcnow()
|
|
182
|
+
self.command_count = 0
|
|
183
|
+
self.last_command = "/WAPPA"
|
|
184
|
+
|
|
185
|
+
def deactivate_wappa(self) -> None:
|
|
186
|
+
"""Deactivate WAPPA state."""
|
|
187
|
+
self.is_wappa = False
|
|
188
|
+
self.last_command = "/EXIT"
|
|
189
|
+
|
|
190
|
+
def process_command(self, command: str) -> None:
|
|
191
|
+
"""Process a command while in WAPPA state."""
|
|
192
|
+
self.command_count += 1
|
|
193
|
+
self.last_command = command
|
|
194
|
+
|
|
195
|
+
def get_state_duration(self) -> int | None:
|
|
196
|
+
"""Get how long the WAPPA state has been active in seconds."""
|
|
197
|
+
if not self.is_wappa or not self.activated_at:
|
|
198
|
+
return None
|
|
199
|
+
return int((datetime.utcnow() - self.activated_at).total_seconds())
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class CacheStats(BaseModel):
|
|
203
|
+
"""
|
|
204
|
+
Cache statistics model for monitoring cache usage.
|
|
205
|
+
|
|
206
|
+
Used to track cache performance and usage statistics.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
# Cache performance metrics
|
|
210
|
+
user_cache_hits: int = Field(default=0, ge=0)
|
|
211
|
+
user_cache_misses: int = Field(default=0, ge=0)
|
|
212
|
+
table_cache_entries: int = Field(default=0, ge=0)
|
|
213
|
+
state_cache_active: int = Field(default=0, ge=0)
|
|
214
|
+
|
|
215
|
+
# Operation tracking
|
|
216
|
+
total_operations: int = Field(default=0, ge=0)
|
|
217
|
+
errors: int = Field(default=0, ge=0)
|
|
218
|
+
|
|
219
|
+
# System information
|
|
220
|
+
cache_type: str = Field(default="JSON")
|
|
221
|
+
connection_status: str = Field(default="File System")
|
|
222
|
+
is_healthy: bool = Field(default=True)
|
|
223
|
+
|
|
224
|
+
# Timing
|
|
225
|
+
last_updated: datetime = Field(default_factory=datetime.utcnow)
|
|
226
|
+
|
|
227
|
+
def record_user_hit(self) -> None:
|
|
228
|
+
"""Record a user cache hit."""
|
|
229
|
+
self.user_cache_hits += 1
|
|
230
|
+
self.total_operations += 1
|
|
231
|
+
self.last_updated = datetime.utcnow()
|
|
232
|
+
|
|
233
|
+
def record_user_miss(self) -> None:
|
|
234
|
+
"""Record a user cache miss."""
|
|
235
|
+
self.user_cache_misses += 1
|
|
236
|
+
self.total_operations += 1
|
|
237
|
+
self.last_updated = datetime.utcnow()
|
|
238
|
+
|
|
239
|
+
def record_table_entry(self) -> None:
|
|
240
|
+
"""Record a new table cache entry."""
|
|
241
|
+
self.table_cache_entries += 1
|
|
242
|
+
self.total_operations += 1
|
|
243
|
+
self.last_updated = datetime.utcnow()
|
|
244
|
+
|
|
245
|
+
def record_state_activation(self) -> None:
|
|
246
|
+
"""Record a state cache activation."""
|
|
247
|
+
self.state_cache_active += 1
|
|
248
|
+
self.total_operations += 1
|
|
249
|
+
self.last_updated = datetime.utcnow()
|
|
250
|
+
|
|
251
|
+
def record_state_deactivation(self) -> None:
|
|
252
|
+
"""Record a state cache deactivation."""
|
|
253
|
+
if self.state_cache_active > 0:
|
|
254
|
+
self.state_cache_active -= 1
|
|
255
|
+
self.total_operations += 1
|
|
256
|
+
self.last_updated = datetime.utcnow()
|
|
257
|
+
|
|
258
|
+
def record_error(self) -> None:
|
|
259
|
+
"""Record an error."""
|
|
260
|
+
self.errors += 1
|
|
261
|
+
self.total_operations += 1
|
|
262
|
+
self.last_updated = datetime.utcnow()
|
|
263
|
+
|
|
264
|
+
def get_user_hit_rate(self) -> float:
|
|
265
|
+
"""Calculate user cache hit rate."""
|
|
266
|
+
total_user_ops = self.user_cache_hits + self.user_cache_misses
|
|
267
|
+
if total_user_ops == 0:
|
|
268
|
+
return 0.0
|
|
269
|
+
return round(self.user_cache_hits / total_user_ops, 3)
|
|
270
|
+
|
|
271
|
+
def get_error_rate(self) -> float:
|
|
272
|
+
"""Calculate overall error rate."""
|
|
273
|
+
if self.total_operations == 0:
|
|
274
|
+
return 0.0
|
|
275
|
+
return round(self.errors / self.total_operations, 3)
|