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.

Files changed (80) hide show
  1. wappa/cli/examples/init/.env.example +33 -0
  2. wappa/cli/examples/init/app/__init__.py +0 -0
  3. wappa/cli/examples/init/app/main.py +8 -0
  4. wappa/cli/examples/init/app/master_event.py +8 -0
  5. wappa/cli/examples/json_cache_example/.env.example +33 -0
  6. wappa/cli/examples/json_cache_example/app/__init__.py +1 -0
  7. wappa/cli/examples/json_cache_example/app/main.py +235 -0
  8. wappa/cli/examples/json_cache_example/app/master_event.py +419 -0
  9. wappa/cli/examples/json_cache_example/app/models/__init__.py +1 -0
  10. wappa/cli/examples/json_cache_example/app/models/json_demo_models.py +275 -0
  11. wappa/cli/examples/json_cache_example/app/scores/__init__.py +35 -0
  12. wappa/cli/examples/json_cache_example/app/scores/score_base.py +186 -0
  13. wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +248 -0
  14. wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +190 -0
  15. wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +260 -0
  16. wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +223 -0
  17. wappa/cli/examples/json_cache_example/app/utils/__init__.py +26 -0
  18. wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +176 -0
  19. wappa/cli/examples/json_cache_example/app/utils/message_utils.py +246 -0
  20. wappa/cli/examples/openai_transcript/.gitignore +63 -4
  21. wappa/cli/examples/openai_transcript/app/__init__.py +0 -0
  22. wappa/cli/examples/openai_transcript/app/main.py +8 -0
  23. wappa/cli/examples/openai_transcript/app/master_event.py +53 -0
  24. wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +3 -0
  25. wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +76 -0
  26. wappa/cli/examples/redis_cache_example/.env.example +33 -0
  27. wappa/cli/examples/redis_cache_example/app/__init__.py +6 -0
  28. wappa/cli/examples/redis_cache_example/app/main.py +234 -0
  29. wappa/cli/examples/redis_cache_example/app/master_event.py +419 -0
  30. wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +275 -0
  31. wappa/cli/examples/redis_cache_example/app/scores/__init__.py +35 -0
  32. wappa/cli/examples/redis_cache_example/app/scores/score_base.py +186 -0
  33. wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +248 -0
  34. wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +190 -0
  35. wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +260 -0
  36. wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +223 -0
  37. wappa/cli/examples/redis_cache_example/app/utils/__init__.py +26 -0
  38. wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +176 -0
  39. wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +246 -0
  40. wappa/cli/examples/simple_echo_example/.env.example +33 -0
  41. wappa/cli/examples/simple_echo_example/app/__init__.py +7 -0
  42. wappa/cli/examples/simple_echo_example/app/main.py +183 -0
  43. wappa/cli/examples/simple_echo_example/app/master_event.py +209 -0
  44. wappa/cli/examples/wappa_full_example/.env.example +33 -0
  45. wappa/cli/examples/wappa_full_example/.gitignore +63 -4
  46. wappa/cli/examples/wappa_full_example/app/__init__.py +6 -0
  47. wappa/cli/examples/wappa_full_example/app/handlers/__init__.py +5 -0
  48. wappa/cli/examples/wappa_full_example/app/handlers/command_handlers.py +484 -0
  49. wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +551 -0
  50. wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +492 -0
  51. wappa/cli/examples/wappa_full_example/app/main.py +257 -0
  52. wappa/cli/examples/wappa_full_example/app/master_event.py +445 -0
  53. wappa/cli/examples/wappa_full_example/app/media/README.md +54 -0
  54. wappa/cli/examples/wappa_full_example/app/media/buttons/README.md +62 -0
  55. wappa/cli/examples/wappa_full_example/app/media/buttons/kitty.png +0 -0
  56. wappa/cli/examples/wappa_full_example/app/media/buttons/puppy.png +0 -0
  57. wappa/cli/examples/wappa_full_example/app/media/list/README.md +110 -0
  58. wappa/cli/examples/wappa_full_example/app/media/list/audio.mp3 +0 -0
  59. wappa/cli/examples/wappa_full_example/app/media/list/document.pdf +0 -0
  60. wappa/cli/examples/wappa_full_example/app/media/list/image.png +0 -0
  61. wappa/cli/examples/wappa_full_example/app/media/list/video.mp4 +0 -0
  62. wappa/cli/examples/wappa_full_example/app/models/__init__.py +5 -0
  63. wappa/cli/examples/wappa_full_example/app/models/state_models.py +425 -0
  64. wappa/cli/examples/wappa_full_example/app/models/user_models.py +287 -0
  65. wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +301 -0
  66. wappa/cli/examples/wappa_full_example/app/utils/__init__.py +5 -0
  67. wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +483 -0
  68. wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +473 -0
  69. wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +298 -0
  70. wappa/cli/main.py +8 -4
  71. wappa/core/config/settings.py +34 -2
  72. wappa/persistence/__init__.py +2 -2
  73. {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/METADATA +1 -1
  74. {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/RECORD +77 -13
  75. wappa/cli/examples/init/pyproject.toml +0 -7
  76. wappa/cli/examples/simple_echo_example/.python-version +0 -1
  77. wappa/cli/examples/simple_echo_example/pyproject.toml +0 -9
  78. {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/WHEEL +0 -0
  79. {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/entry_points.txt +0 -0
  80. {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,483 @@
1
+ """
2
+ Cache utilities for Redis operations in the Wappa Full Example application.
3
+
4
+ This module provides helper functions and classes for working with Redis cache,
5
+ including user management, state management, and statistics tracking.
6
+ """
7
+
8
+ import json
9
+ from datetime import datetime, timedelta
10
+ from typing import Any, Dict, List, Optional, Type, TypeVar
11
+
12
+ from pydantic import BaseModel
13
+
14
+ from ..models.state_models import InteractiveState, StateType
15
+ from ..models.user_models import UserProfile
16
+
17
+ T = TypeVar('T', bound=BaseModel)
18
+
19
+
20
+ class CacheHelper:
21
+ """Helper class for common cache operations."""
22
+
23
+ def __init__(self, cache_factory):
24
+ """
25
+ Initialize CacheHelper with cache factory.
26
+
27
+ Args:
28
+ cache_factory: Wappa cache factory instance
29
+ """
30
+ self.cache_factory = cache_factory
31
+ self._user_cache = None
32
+ self._state_cache = None
33
+ self._table_cache = None
34
+
35
+ @property
36
+ def user_cache(self):
37
+ """Get user cache instance."""
38
+ if not self._user_cache:
39
+ self._user_cache = self.cache_factory.create_user_cache()
40
+ return self._user_cache
41
+
42
+ @property
43
+ def state_cache(self):
44
+ """Get state cache instance."""
45
+ if not self._state_cache:
46
+ self._state_cache = self.cache_factory.create_state_cache()
47
+ return self._state_cache
48
+
49
+ @property
50
+ def table_cache(self):
51
+ """Get table cache instance."""
52
+ if not self._table_cache:
53
+ self._table_cache = self.cache_factory.create_table_cache()
54
+ return self._table_cache
55
+
56
+ async def get_user_profile(self, user_id: str) -> Optional[UserProfile]:
57
+ """
58
+ Get user profile from cache.
59
+
60
+ Args:
61
+ user_id: User phone number/ID
62
+
63
+ Returns:
64
+ UserProfile object or None if not found
65
+ """
66
+ try:
67
+ profile_data = await self.user_cache.get(f"user_profile_{user_id}", models=UserProfile)
68
+ return profile_data
69
+ except Exception as e:
70
+ print(f"Error getting user profile {user_id}: {e}")
71
+ return None
72
+
73
+ async def save_user_profile(self, user_profile: UserProfile, ttl_seconds: int = 86400) -> bool:
74
+ """
75
+ Save user profile to cache.
76
+
77
+ Args:
78
+ user_profile: UserProfile object to save
79
+ ttl_seconds: Time to live in seconds (default 24 hours)
80
+
81
+ Returns:
82
+ True if successful, False otherwise
83
+ """
84
+ try:
85
+ cache_key = f"user_profile_{user_profile.phone_number}"
86
+ await self.user_cache.set(cache_key, user_profile, ttl=ttl_seconds)
87
+ return True
88
+ except Exception as e:
89
+ print(f"Error saving user profile {user_profile.phone_number}: {e}")
90
+ return False
91
+
92
+ async def get_or_create_user_profile(self, user_id: str, user_name: str|None = None,
93
+ profile_name: str|None = None) -> UserProfile:
94
+ """
95
+ Get existing user profile or create a new one.
96
+
97
+ Args:
98
+ user_id: User phone number/ID
99
+ user_name: Optional user name
100
+ profile_name: Optional profile name
101
+
102
+ Returns:
103
+ UserProfile object (existing or new)
104
+ """
105
+ # Try to get existing profile
106
+ profile = await self.get_user_profile(user_id)
107
+
108
+ if profile:
109
+ # Update profile information if provided
110
+ profile.update_profile_info(user_name, profile_name)
111
+ return profile
112
+ else:
113
+ # Create new profile
114
+ profile = UserProfile(
115
+ phone_number=user_id,
116
+ user_name=user_name,
117
+ profile_name=profile_name,
118
+ is_first_time_user=True,
119
+ has_received_welcome=False
120
+ )
121
+
122
+ # Save new profile
123
+ await self.save_user_profile(profile)
124
+ return profile
125
+
126
+ async def update_user_activity(self, user_id: str, message_type: str = "text",
127
+ command: str|None = None, interaction_type: str|None = None) -> Optional[UserProfile]:
128
+ """
129
+ Update user activity statistics.
130
+
131
+ Args:
132
+ user_id: User phone number/ID
133
+ message_type: Type of message
134
+ command: Optional command used
135
+ interaction_type: Optional interaction type
136
+
137
+ Returns:
138
+ Updated UserProfile or None if error
139
+ """
140
+ try:
141
+ profile = await self.get_or_create_user_profile(user_id)
142
+
143
+ # Update message count
144
+ profile.increment_message_count(message_type)
145
+
146
+ # Update command usage
147
+ if command:
148
+ profile.increment_command_usage(command)
149
+
150
+ # Update interactions
151
+ if interaction_type:
152
+ profile.increment_interactions(interaction_type)
153
+
154
+ # Save updated profile
155
+ await self.save_user_profile(profile)
156
+ return profile
157
+
158
+ except Exception as e:
159
+ print(f"Error updating user activity {user_id}: {e}")
160
+ return None
161
+
162
+ async def get_user_state(self, user_id: str, state_type: StateType) -> Optional[InteractiveState]:
163
+ """
164
+ Get user interactive state.
165
+
166
+ Args:
167
+ user_id: User phone number/ID
168
+ state_type: Type of state to get
169
+
170
+ Returns:
171
+ InteractiveState object or None if not found/expired
172
+ """
173
+ try:
174
+ # Use simpler cache key format: state_{state_type}
175
+ cache_key = f"state_{state_type.value}"
176
+
177
+ # Import the specific state classes for proper type casting
178
+ from ..models.state_models import ButtonState, ListState
179
+
180
+ # Use the appropriate model class based on state type
181
+ model_class = InteractiveState
182
+ if state_type == StateType.BUTTON:
183
+ model_class = ButtonState
184
+ elif state_type == StateType.LIST:
185
+ model_class = ListState
186
+
187
+ state_data = await self.state_cache.get(cache_key, models=model_class)
188
+
189
+ if state_data and state_data.is_expired():
190
+ # Remove expired state
191
+ await self.state_cache.delete(cache_key)
192
+ return None
193
+
194
+ return state_data
195
+ except Exception as e:
196
+ print(f"Error getting user state {state_type.value}: {e}")
197
+ return None
198
+
199
+ async def save_user_state(self, state: InteractiveState) -> bool:
200
+ """
201
+ Save user interactive state.
202
+
203
+ Args:
204
+ state: InteractiveState object to save
205
+
206
+ Returns:
207
+ True if successful, False otherwise
208
+ """
209
+ try:
210
+ # Use simpler cache key format: state_{state_type}
211
+ cache_key = f"state_{state.state_type.value}"
212
+ ttl = state.time_remaining_seconds()
213
+
214
+ if ttl <= 0:
215
+ # Don't save expired states
216
+ return False
217
+
218
+ await self.state_cache.set(cache_key, state, ttl=ttl)
219
+ return True
220
+ except Exception as e:
221
+ print(f"Error saving user state {state.state_type.value}: {e}")
222
+ return False
223
+
224
+ async def remove_user_state(self, user_id: str, state_type: StateType) -> bool:
225
+ """
226
+ Remove user interactive state.
227
+
228
+ Args:
229
+ user_id: User phone number/ID
230
+ state_type: Type of state to remove
231
+
232
+ Returns:
233
+ True if successful, False otherwise
234
+ """
235
+ try:
236
+ # Use simpler cache key format: state_{state_type}
237
+ cache_key = f"state_{state_type.value}"
238
+ result = await self.state_cache.delete(cache_key)
239
+ return result
240
+ except Exception as e:
241
+ print(f"Error removing user state {state_type.value}: {e}")
242
+ return False
243
+
244
+ async def cleanup_expired_states(self, batch_size: int = 100) -> int:
245
+ """
246
+ Cleanup expired states from cache.
247
+
248
+ Args:
249
+ batch_size: Number of states to check in each batch
250
+
251
+ Returns:
252
+ Number of expired states cleaned up
253
+ """
254
+ cleanup_count = 0
255
+ try:
256
+ # This is a simplified implementation
257
+ # In a real implementation, you would need to scan Redis keys
258
+ # and check expiration status
259
+
260
+ # For now, return 0 as this requires Redis-specific commands
261
+ return cleanup_count
262
+ except Exception as e:
263
+ print(f"Error during cleanup: {e}")
264
+ return 0
265
+
266
+ async def get_cache_statistics(self) -> Dict[str, Any]:
267
+ """
268
+ Get cache usage statistics.
269
+
270
+ Returns:
271
+ Dictionary with cache statistics
272
+ """
273
+ try:
274
+ stats = {
275
+ "timestamp": datetime.now().isoformat(),
276
+ "user_cache": {
277
+ "type": "user_profiles",
278
+ "description": "User profile and activity data"
279
+ },
280
+ "state_cache": {
281
+ "type": "interactive_states",
282
+ "description": "Active interactive command states"
283
+ },
284
+ "table_cache": {
285
+ "type": "structured_data",
286
+ "description": "Structured application data"
287
+ }
288
+ }
289
+
290
+ # Add cache-specific info if available
291
+ try:
292
+ # Try to get cache info from Redis if available
293
+ # This would require implementing Redis-specific info commands
294
+ pass
295
+ except:
296
+ pass
297
+
298
+ return stats
299
+ except Exception as e:
300
+ print(f"Error getting cache statistics: {e}")
301
+ return {"error": str(e)}
302
+
303
+ async def store_message_history(self, user_id: str, message_data: Dict[str, Any],
304
+ max_history: int = 50) -> bool:
305
+ """
306
+ Store message in user's history.
307
+
308
+ Args:
309
+ user_id: User phone number/ID
310
+ message_data: Dictionary with message information
311
+ max_history: Maximum number of messages to keep
312
+
313
+ Returns:
314
+ True if successful, False otherwise
315
+ """
316
+ try:
317
+ history_key = f"history_{user_id}"
318
+
319
+ # Get existing history
320
+ history = await self.table_cache.get(history_key, models=list) or []
321
+
322
+ # Add new message with timestamp
323
+ message_entry = {
324
+ **message_data,
325
+ "stored_at": datetime.now().isoformat()
326
+ }
327
+
328
+ history.append(message_entry)
329
+
330
+ # Keep only recent messages
331
+ if len(history) > max_history:
332
+ history = history[-max_history:]
333
+
334
+ # Save updated history
335
+ await self.table_cache.set(history_key, history, ttl=604800) # 7 days
336
+ return True
337
+
338
+ except Exception as e:
339
+ print(f"Error storing message history {user_id}: {e}")
340
+ return False
341
+
342
+ async def get_message_history(self, user_id: str, limit: int = 20) -> List[Dict[str, Any]]:
343
+ """
344
+ Get user's message history.
345
+
346
+ Args:
347
+ user_id: User phone number/ID
348
+ limit: Maximum number of messages to return
349
+
350
+ Returns:
351
+ List of message history entries
352
+ """
353
+ try:
354
+ history_key = f"history_{user_id}"
355
+ history = await self.table_cache.get(history_key, models=list) or []
356
+
357
+ # Return recent messages
358
+ return history[-limit:] if history else []
359
+
360
+ except Exception as e:
361
+ print(f"Error getting message history {user_id}: {e}")
362
+ return []
363
+
364
+ async def store_application_data(self, key: str, data: Any, ttl_seconds: int|None = None) -> bool:
365
+ """
366
+ Store application-specific data.
367
+
368
+ Args:
369
+ key: Cache key
370
+ data: Data to store
371
+ ttl_seconds: Optional TTL in seconds
372
+
373
+ Returns:
374
+ True if successful, False otherwise
375
+ """
376
+ try:
377
+ await self.table_cache.set(key, data, ttl=ttl_seconds)
378
+ return True
379
+ except Exception as e:
380
+ print(f"Error storing application data {key}: {e}")
381
+ return False
382
+
383
+ async def get_application_data(self, key: str, model_class: Type[T]|None = None) -> Any:
384
+ """
385
+ Get application-specific data.
386
+
387
+ Args:
388
+ key: Cache key
389
+ model_class: Optional Pydantic model class for validation
390
+
391
+ Returns:
392
+ Stored data or None if not found
393
+ """
394
+ try:
395
+ return await self.table_cache.get(key, models=model_class)
396
+ except Exception as e:
397
+ print(f"Error getting application data {key}: {e}")
398
+ return None
399
+
400
+
401
+ class CacheKeys:
402
+ """Centralized cache key management."""
403
+
404
+ @staticmethod
405
+ def user_profile(user_id: str) -> str:
406
+ """Get cache key for user profile."""
407
+ return f"user_profile_{user_id}"
408
+
409
+ @staticmethod
410
+ def user_state(user_id: str, state_type: str) -> str:
411
+ """Get cache key for user state."""
412
+ return f"state_{state_type}"
413
+
414
+ @staticmethod
415
+ def message_history(user_id: str) -> str:
416
+ """Get cache key for message history."""
417
+ return f"history_{user_id}"
418
+
419
+ @staticmethod
420
+ def user_session(user_id: str) -> str:
421
+ """Get cache key for user session."""
422
+ return f"session_{user_id}"
423
+
424
+ @staticmethod
425
+ def application_stats() -> str:
426
+ """Get cache key for application statistics."""
427
+ return "app_stats"
428
+
429
+ @staticmethod
430
+ def daily_stats(date_str: str|None = None) -> str:
431
+ """Get cache key for daily statistics."""
432
+ if not date_str:
433
+ date_str = datetime.now().strftime("%Y-%m-%d")
434
+ return f"daily_stats_{date_str}"
435
+
436
+
437
+ # Convenience functions for direct use
438
+ async def get_user_from_cache(cache_factory, user_id: str) -> Optional[UserProfile]:
439
+ """
440
+ Get user profile from cache (convenience function).
441
+
442
+ Args:
443
+ cache_factory: Wappa cache factory
444
+ user_id: User phone number/ID
445
+
446
+ Returns:
447
+ UserProfile or None
448
+ """
449
+ helper = CacheHelper(cache_factory)
450
+ return await helper.get_user_profile(user_id)
451
+
452
+
453
+ async def save_user_to_cache(cache_factory, user_profile: UserProfile) -> bool:
454
+ """
455
+ Save user profile to cache (convenience function).
456
+
457
+ Args:
458
+ cache_factory: Wappa cache factory
459
+ user_profile: UserProfile to save
460
+
461
+ Returns:
462
+ True if successful, False otherwise
463
+ """
464
+ helper = CacheHelper(cache_factory)
465
+ return await helper.save_user_profile(user_profile)
466
+
467
+
468
+ async def update_user_stats(cache_factory, user_id: str, message_type: str = "text",
469
+ command: str|None = None) -> Optional[UserProfile]:
470
+ """
471
+ Update user statistics (convenience function).
472
+
473
+ Args:
474
+ cache_factory: Wappa cache factory
475
+ user_id: User phone number/ID
476
+ message_type: Message type
477
+ command: Optional command
478
+
479
+ Returns:
480
+ Updated UserProfile or None
481
+ """
482
+ helper = CacheHelper(cache_factory)
483
+ return await helper.update_user_activity(user_id, message_type, command)