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