wappa 0.1.0__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 (211) hide show
  1. wappa/__init__.py +85 -0
  2. wappa/api/__init__.py +1 -0
  3. wappa/api/controllers/__init__.py +10 -0
  4. wappa/api/controllers/webhook_controller.py +441 -0
  5. wappa/api/dependencies/__init__.py +15 -0
  6. wappa/api/dependencies/whatsapp_dependencies.py +220 -0
  7. wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
  8. wappa/api/middleware/__init__.py +7 -0
  9. wappa/api/middleware/error_handler.py +158 -0
  10. wappa/api/middleware/owner.py +99 -0
  11. wappa/api/middleware/request_logging.py +184 -0
  12. wappa/api/routes/__init__.py +6 -0
  13. wappa/api/routes/health.py +102 -0
  14. wappa/api/routes/webhooks.py +211 -0
  15. wappa/api/routes/whatsapp/__init__.py +15 -0
  16. wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
  17. wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
  18. wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
  19. wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
  20. wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
  21. wappa/api/routes/whatsapp_combined.py +35 -0
  22. wappa/cli/__init__.py +9 -0
  23. wappa/cli/main.py +199 -0
  24. wappa/core/__init__.py +6 -0
  25. wappa/core/config/__init__.py +5 -0
  26. wappa/core/config/settings.py +161 -0
  27. wappa/core/events/__init__.py +41 -0
  28. wappa/core/events/default_handlers.py +642 -0
  29. wappa/core/events/event_dispatcher.py +244 -0
  30. wappa/core/events/event_handler.py +247 -0
  31. wappa/core/events/webhook_factory.py +219 -0
  32. wappa/core/factory/__init__.py +15 -0
  33. wappa/core/factory/plugin.py +68 -0
  34. wappa/core/factory/wappa_builder.py +326 -0
  35. wappa/core/logging/__init__.py +5 -0
  36. wappa/core/logging/context.py +100 -0
  37. wappa/core/logging/logger.py +343 -0
  38. wappa/core/plugins/__init__.py +34 -0
  39. wappa/core/plugins/auth_plugin.py +169 -0
  40. wappa/core/plugins/cors_plugin.py +128 -0
  41. wappa/core/plugins/custom_middleware_plugin.py +182 -0
  42. wappa/core/plugins/database_plugin.py +235 -0
  43. wappa/core/plugins/rate_limit_plugin.py +183 -0
  44. wappa/core/plugins/redis_plugin.py +224 -0
  45. wappa/core/plugins/wappa_core_plugin.py +261 -0
  46. wappa/core/plugins/webhook_plugin.py +253 -0
  47. wappa/core/types.py +108 -0
  48. wappa/core/wappa_app.py +546 -0
  49. wappa/database/__init__.py +18 -0
  50. wappa/database/adapter.py +107 -0
  51. wappa/database/adapters/__init__.py +17 -0
  52. wappa/database/adapters/mysql_adapter.py +187 -0
  53. wappa/database/adapters/postgresql_adapter.py +169 -0
  54. wappa/database/adapters/sqlite_adapter.py +174 -0
  55. wappa/domain/__init__.py +28 -0
  56. wappa/domain/builders/__init__.py +5 -0
  57. wappa/domain/builders/message_builder.py +189 -0
  58. wappa/domain/entities/__init__.py +5 -0
  59. wappa/domain/enums/messenger_platform.py +123 -0
  60. wappa/domain/factories/__init__.py +6 -0
  61. wappa/domain/factories/media_factory.py +450 -0
  62. wappa/domain/factories/message_factory.py +497 -0
  63. wappa/domain/factories/messenger_factory.py +244 -0
  64. wappa/domain/interfaces/__init__.py +32 -0
  65. wappa/domain/interfaces/base_repository.py +94 -0
  66. wappa/domain/interfaces/cache_factory.py +85 -0
  67. wappa/domain/interfaces/cache_interface.py +199 -0
  68. wappa/domain/interfaces/expiry_repository.py +68 -0
  69. wappa/domain/interfaces/media_interface.py +311 -0
  70. wappa/domain/interfaces/messaging_interface.py +523 -0
  71. wappa/domain/interfaces/pubsub_repository.py +151 -0
  72. wappa/domain/interfaces/repository_factory.py +108 -0
  73. wappa/domain/interfaces/shared_state_repository.py +122 -0
  74. wappa/domain/interfaces/state_repository.py +123 -0
  75. wappa/domain/interfaces/tables_repository.py +215 -0
  76. wappa/domain/interfaces/user_repository.py +114 -0
  77. wappa/domain/interfaces/webhooks/__init__.py +1 -0
  78. wappa/domain/models/media_result.py +110 -0
  79. wappa/domain/models/platforms/__init__.py +15 -0
  80. wappa/domain/models/platforms/platform_config.py +104 -0
  81. wappa/domain/services/__init__.py +11 -0
  82. wappa/domain/services/tenant_credentials_service.py +56 -0
  83. wappa/messaging/__init__.py +7 -0
  84. wappa/messaging/whatsapp/__init__.py +1 -0
  85. wappa/messaging/whatsapp/client/__init__.py +5 -0
  86. wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
  87. wappa/messaging/whatsapp/handlers/__init__.py +13 -0
  88. wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
  89. wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
  90. wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
  91. wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
  92. wappa/messaging/whatsapp/messenger/__init__.py +5 -0
  93. wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
  94. wappa/messaging/whatsapp/models/__init__.py +61 -0
  95. wappa/messaging/whatsapp/models/basic_models.py +65 -0
  96. wappa/messaging/whatsapp/models/interactive_models.py +287 -0
  97. wappa/messaging/whatsapp/models/media_models.py +215 -0
  98. wappa/messaging/whatsapp/models/specialized_models.py +304 -0
  99. wappa/messaging/whatsapp/models/template_models.py +261 -0
  100. wappa/persistence/cache_factory.py +93 -0
  101. wappa/persistence/json/__init__.py +14 -0
  102. wappa/persistence/json/cache_adapters.py +271 -0
  103. wappa/persistence/json/handlers/__init__.py +1 -0
  104. wappa/persistence/json/handlers/state_handler.py +250 -0
  105. wappa/persistence/json/handlers/table_handler.py +263 -0
  106. wappa/persistence/json/handlers/user_handler.py +213 -0
  107. wappa/persistence/json/handlers/utils/__init__.py +1 -0
  108. wappa/persistence/json/handlers/utils/file_manager.py +153 -0
  109. wappa/persistence/json/handlers/utils/key_factory.py +11 -0
  110. wappa/persistence/json/handlers/utils/serialization.py +121 -0
  111. wappa/persistence/json/json_cache_factory.py +76 -0
  112. wappa/persistence/json/storage_manager.py +285 -0
  113. wappa/persistence/memory/__init__.py +14 -0
  114. wappa/persistence/memory/cache_adapters.py +271 -0
  115. wappa/persistence/memory/handlers/__init__.py +1 -0
  116. wappa/persistence/memory/handlers/state_handler.py +250 -0
  117. wappa/persistence/memory/handlers/table_handler.py +280 -0
  118. wappa/persistence/memory/handlers/user_handler.py +213 -0
  119. wappa/persistence/memory/handlers/utils/__init__.py +1 -0
  120. wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
  121. wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
  122. wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
  123. wappa/persistence/memory/memory_cache_factory.py +76 -0
  124. wappa/persistence/memory/storage_manager.py +235 -0
  125. wappa/persistence/redis/README.md +699 -0
  126. wappa/persistence/redis/__init__.py +11 -0
  127. wappa/persistence/redis/cache_adapters.py +285 -0
  128. wappa/persistence/redis/ops.py +880 -0
  129. wappa/persistence/redis/redis_cache_factory.py +71 -0
  130. wappa/persistence/redis/redis_client.py +231 -0
  131. wappa/persistence/redis/redis_handler/__init__.py +26 -0
  132. wappa/persistence/redis/redis_handler/state_handler.py +176 -0
  133. wappa/persistence/redis/redis_handler/table.py +158 -0
  134. wappa/persistence/redis/redis_handler/user.py +138 -0
  135. wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
  136. wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
  137. wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
  138. wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
  139. wappa/persistence/redis/redis_manager.py +189 -0
  140. wappa/processors/__init__.py +6 -0
  141. wappa/processors/base_processor.py +262 -0
  142. wappa/processors/factory.py +550 -0
  143. wappa/processors/whatsapp_processor.py +810 -0
  144. wappa/schemas/__init__.py +6 -0
  145. wappa/schemas/core/__init__.py +71 -0
  146. wappa/schemas/core/base_message.py +499 -0
  147. wappa/schemas/core/base_status.py +322 -0
  148. wappa/schemas/core/base_webhook.py +312 -0
  149. wappa/schemas/core/types.py +253 -0
  150. wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
  151. wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
  152. wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
  153. wappa/schemas/factory.py +754 -0
  154. wappa/schemas/webhooks/__init__.py +3 -0
  155. wappa/schemas/whatsapp/__init__.py +6 -0
  156. wappa/schemas/whatsapp/base_models.py +285 -0
  157. wappa/schemas/whatsapp/message_types/__init__.py +93 -0
  158. wappa/schemas/whatsapp/message_types/audio.py +350 -0
  159. wappa/schemas/whatsapp/message_types/button.py +267 -0
  160. wappa/schemas/whatsapp/message_types/contact.py +464 -0
  161. wappa/schemas/whatsapp/message_types/document.py +421 -0
  162. wappa/schemas/whatsapp/message_types/errors.py +195 -0
  163. wappa/schemas/whatsapp/message_types/image.py +424 -0
  164. wappa/schemas/whatsapp/message_types/interactive.py +430 -0
  165. wappa/schemas/whatsapp/message_types/location.py +416 -0
  166. wappa/schemas/whatsapp/message_types/order.py +372 -0
  167. wappa/schemas/whatsapp/message_types/reaction.py +271 -0
  168. wappa/schemas/whatsapp/message_types/sticker.py +328 -0
  169. wappa/schemas/whatsapp/message_types/system.py +317 -0
  170. wappa/schemas/whatsapp/message_types/text.py +411 -0
  171. wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
  172. wappa/schemas/whatsapp/message_types/video.py +344 -0
  173. wappa/schemas/whatsapp/status_models.py +479 -0
  174. wappa/schemas/whatsapp/validators.py +454 -0
  175. wappa/schemas/whatsapp/webhook_container.py +438 -0
  176. wappa/webhooks/__init__.py +17 -0
  177. wappa/webhooks/core/__init__.py +71 -0
  178. wappa/webhooks/core/base_message.py +499 -0
  179. wappa/webhooks/core/base_status.py +322 -0
  180. wappa/webhooks/core/base_webhook.py +312 -0
  181. wappa/webhooks/core/types.py +253 -0
  182. wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
  183. wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
  184. wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
  185. wappa/webhooks/factory.py +754 -0
  186. wappa/webhooks/whatsapp/__init__.py +6 -0
  187. wappa/webhooks/whatsapp/base_models.py +285 -0
  188. wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
  189. wappa/webhooks/whatsapp/message_types/audio.py +350 -0
  190. wappa/webhooks/whatsapp/message_types/button.py +267 -0
  191. wappa/webhooks/whatsapp/message_types/contact.py +464 -0
  192. wappa/webhooks/whatsapp/message_types/document.py +421 -0
  193. wappa/webhooks/whatsapp/message_types/errors.py +195 -0
  194. wappa/webhooks/whatsapp/message_types/image.py +424 -0
  195. wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
  196. wappa/webhooks/whatsapp/message_types/location.py +416 -0
  197. wappa/webhooks/whatsapp/message_types/order.py +372 -0
  198. wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
  199. wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
  200. wappa/webhooks/whatsapp/message_types/system.py +317 -0
  201. wappa/webhooks/whatsapp/message_types/text.py +411 -0
  202. wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
  203. wappa/webhooks/whatsapp/message_types/video.py +344 -0
  204. wappa/webhooks/whatsapp/status_models.py +479 -0
  205. wappa/webhooks/whatsapp/validators.py +454 -0
  206. wappa/webhooks/whatsapp/webhook_container.py +438 -0
  207. wappa-0.1.0.dist-info/METADATA +269 -0
  208. wappa-0.1.0.dist-info/RECORD +211 -0
  209. wappa-0.1.0.dist-info/WHEEL +4 -0
  210. wappa-0.1.0.dist-info/entry_points.txt +2 -0
  211. wappa-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,317 @@
1
+ """
2
+ Thread-safe in-memory storage with TTL support.
3
+
4
+ Provides global singleton memory store with namespace isolation
5
+ and automatic expiration cleanup.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ from datetime import datetime, timedelta
11
+ from typing import Any, Dict, Optional, Tuple
12
+
13
+ logger = logging.getLogger("MemoryStore")
14
+
15
+
16
+ class MemoryStore:
17
+ """
18
+ Thread-safe in-memory store with TTL support.
19
+
20
+ Storage Structure:
21
+ {
22
+ "users": {context_key: {key: (data, expires_at)}},
23
+ "tables": {context_key: {key: (data, expires_at)}},
24
+ "states": {context_key: {key: (data, expires_at)}}
25
+ }
26
+
27
+ Where context_key is typically "{tenant_id}_{user_id}" for isolation.
28
+ """
29
+
30
+ def __init__(self):
31
+ self._store: Dict[str, Dict[str, Dict[str, Tuple[Any, Optional[datetime]]]]] = {
32
+ "users": {},
33
+ "tables": {},
34
+ "states": {}
35
+ }
36
+ self._locks = {
37
+ "users": asyncio.Lock(),
38
+ "tables": asyncio.Lock(),
39
+ "states": asyncio.Lock()
40
+ }
41
+ self._cleanup_task: Optional[asyncio.Task] = None
42
+ self._cleanup_interval = 300 # 5 minutes
43
+
44
+ def start_cleanup_task(self):
45
+ """Start background TTL cleanup task."""
46
+ if self._cleanup_task is None or self._cleanup_task.done():
47
+ self._cleanup_task = asyncio.create_task(self._cleanup_expired_entries())
48
+ logger.info("Started memory store TTL cleanup task")
49
+
50
+ def stop_cleanup_task(self):
51
+ """Stop background TTL cleanup task."""
52
+ if self._cleanup_task and not self._cleanup_task.done():
53
+ self._cleanup_task.cancel()
54
+ logger.info("Stopped memory store TTL cleanup task")
55
+
56
+ async def get(self, namespace: str, context_key: str, key: str) -> Any:
57
+ """
58
+ Get value with automatic expiration check.
59
+
60
+ Args:
61
+ namespace: Cache namespace ("users", "tables", "states")
62
+ context_key: Context identifier (e.g., "{tenant_id}_{user_id}")
63
+ key: Cache key
64
+
65
+ Returns:
66
+ Cached value or None if not found/expired
67
+ """
68
+ if namespace not in self._locks:
69
+ raise ValueError(f"Invalid namespace: {namespace}")
70
+
71
+ async with self._locks[namespace]:
72
+ store = self._store[namespace]
73
+ context_store = store.get(context_key, {})
74
+
75
+ if key in context_store:
76
+ data, expires_at = context_store[key]
77
+ if expires_at and datetime.now() > expires_at:
78
+ # Expired, remove and return None
79
+ del context_store[key]
80
+ return None
81
+ return data
82
+ return None
83
+
84
+ async def set(
85
+ self,
86
+ namespace: str,
87
+ context_key: str,
88
+ key: str,
89
+ data: Any,
90
+ ttl: Optional[int] = None
91
+ ) -> bool:
92
+ """
93
+ Set value with optional TTL.
94
+
95
+ Args:
96
+ namespace: Cache namespace
97
+ context_key: Context identifier
98
+ key: Cache key
99
+ data: Value to store
100
+ ttl: Time to live in seconds
101
+
102
+ Returns:
103
+ True if successful, False otherwise
104
+ """
105
+ if namespace not in self._locks:
106
+ raise ValueError(f"Invalid namespace: {namespace}")
107
+
108
+ expires_at = None
109
+ if ttl:
110
+ expires_at = datetime.now() + timedelta(seconds=ttl)
111
+
112
+ try:
113
+ async with self._locks[namespace]:
114
+ store = self._store[namespace]
115
+ if context_key not in store:
116
+ store[context_key] = {}
117
+ store[context_key][key] = (data, expires_at)
118
+
119
+ # Start cleanup task if not running
120
+ self.start_cleanup_task()
121
+ return True
122
+ except Exception as e:
123
+ logger.error(f"Failed to set key '{key}' in {namespace}: {e}")
124
+ return False
125
+
126
+ async def delete(self, namespace: str, context_key: str, key: str) -> bool:
127
+ """
128
+ Delete key from store.
129
+
130
+ Args:
131
+ namespace: Cache namespace
132
+ context_key: Context identifier
133
+ key: Cache key
134
+
135
+ Returns:
136
+ True if deleted or didn't exist, False on error
137
+ """
138
+ if namespace not in self._locks:
139
+ raise ValueError(f"Invalid namespace: {namespace}")
140
+
141
+ try:
142
+ async with self._locks[namespace]:
143
+ store = self._store[namespace]
144
+ context_store = store.get(context_key, {})
145
+ if key in context_store:
146
+ del context_store[key]
147
+ # Clean up empty context store
148
+ if not context_store:
149
+ del store[context_key]
150
+ return True
151
+ except Exception as e:
152
+ logger.error(f"Failed to delete key '{key}' from {namespace}: {e}")
153
+ return False
154
+
155
+ async def exists(self, namespace: str, context_key: str, key: str) -> bool:
156
+ """
157
+ Check if key exists and is not expired.
158
+
159
+ Args:
160
+ namespace: Cache namespace
161
+ context_key: Context identifier
162
+ key: Cache key
163
+
164
+ Returns:
165
+ True if exists and not expired, False otherwise
166
+ """
167
+ value = await self.get(namespace, context_key, key)
168
+ return value is not None
169
+
170
+ async def get_ttl(self, namespace: str, context_key: str, key: str) -> int:
171
+ """
172
+ Get remaining TTL for key.
173
+
174
+ Returns:
175
+ Remaining TTL in seconds, -1 if no expiry, -2 if doesn't exist
176
+ """
177
+ if namespace not in self._locks:
178
+ return -2
179
+
180
+ async with self._locks[namespace]:
181
+ store = self._store[namespace]
182
+ context_store = store.get(context_key, {})
183
+
184
+ if key not in context_store:
185
+ return -2 # Doesn't exist
186
+
187
+ data, expires_at = context_store[key]
188
+
189
+ if expires_at is None:
190
+ return -1 # No expiry
191
+
192
+ now = datetime.now()
193
+ if now >= expires_at:
194
+ # Already expired, clean up
195
+ del context_store[key]
196
+ return -2
197
+
198
+ return int((expires_at - now).total_seconds())
199
+
200
+ async def set_ttl(self, namespace: str, context_key: str, key: str, ttl: int) -> bool:
201
+ """
202
+ Set TTL for existing key.
203
+
204
+ Args:
205
+ ttl: Time to live in seconds
206
+
207
+ Returns:
208
+ True if successful, False if key doesn't exist or error
209
+ """
210
+ if namespace not in self._locks:
211
+ return False
212
+
213
+ try:
214
+ async with self._locks[namespace]:
215
+ store = self._store[namespace]
216
+ context_store = store.get(context_key, {})
217
+
218
+ if key not in context_store:
219
+ return False # Key doesn't exist
220
+
221
+ data, _ = context_store[key] # Get existing data, ignore old TTL
222
+ expires_at = datetime.now() + timedelta(seconds=ttl)
223
+ context_store[key] = (data, expires_at)
224
+ return True
225
+ except Exception as e:
226
+ logger.error(f"Failed to set TTL for key '{key}' in {namespace}: {e}")
227
+ return False
228
+
229
+ async def get_all_keys(self, namespace: str, context_key: str) -> Dict[str, Any]:
230
+ """
231
+ Get all non-expired keys for a context.
232
+
233
+ Args:
234
+ namespace: Cache namespace
235
+ context_key: Context identifier
236
+
237
+ Returns:
238
+ Dictionary of all non-expired key-value pairs
239
+ """
240
+ if namespace not in self._locks:
241
+ return {}
242
+
243
+ async with self._locks[namespace]:
244
+ store = self._store[namespace]
245
+ context_store = store.get(context_key, {})
246
+
247
+ result = {}
248
+ now = datetime.now()
249
+ expired_keys = []
250
+
251
+ for key, (data, expires_at) in context_store.items():
252
+ if expires_at and now > expires_at:
253
+ expired_keys.append(key)
254
+ else:
255
+ result[key] = data
256
+
257
+ # Clean up expired keys
258
+ for key in expired_keys:
259
+ del context_store[key]
260
+
261
+ return result
262
+
263
+ async def _cleanup_expired_entries(self):
264
+ """Background task to clean up expired entries."""
265
+ while True:
266
+ try:
267
+ await asyncio.sleep(self._cleanup_interval)
268
+
269
+ now = datetime.now()
270
+ total_cleaned = 0
271
+
272
+ for namespace in ["users", "tables", "states"]:
273
+ async with self._locks[namespace]:
274
+ store = self._store[namespace]
275
+ empty_contexts = []
276
+
277
+ for context_key, context_store in store.items():
278
+ expired_keys = []
279
+
280
+ for key, (_, expires_at) in context_store.items():
281
+ if expires_at and now > expires_at:
282
+ expired_keys.append(key)
283
+
284
+ # Remove expired keys
285
+ for key in expired_keys:
286
+ del context_store[key]
287
+ total_cleaned += 1
288
+
289
+ # Mark empty contexts for cleanup
290
+ if not context_store:
291
+ empty_contexts.append(context_key)
292
+
293
+ # Remove empty contexts
294
+ for context_key in empty_contexts:
295
+ del store[context_key]
296
+
297
+ if total_cleaned > 0:
298
+ logger.debug(f"Cleaned up {total_cleaned} expired entries from memory store")
299
+
300
+ except asyncio.CancelledError:
301
+ logger.info("Memory store cleanup task cancelled")
302
+ break
303
+ except Exception as e:
304
+ logger.error(f"Error in memory store cleanup task: {e}")
305
+ # Continue running despite errors
306
+
307
+
308
+ # Global singleton memory store instance
309
+ _global_memory_store: Optional[MemoryStore] = None
310
+
311
+
312
+ def get_memory_store() -> MemoryStore:
313
+ """Get or create the global memory store singleton."""
314
+ global _global_memory_store
315
+ if _global_memory_store is None:
316
+ _global_memory_store = MemoryStore()
317
+ return _global_memory_store
@@ -0,0 +1,235 @@
1
+ """
2
+ TTL management utilities for memory cache.
3
+
4
+ Provides additional TTL management functionality beyond the basic
5
+ memory store implementation.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ from datetime import datetime, timedelta
11
+ from typing import Optional
12
+
13
+ from .memory_store import get_memory_store
14
+
15
+ logger = logging.getLogger("TTLManager")
16
+
17
+
18
+ class TTLManager:
19
+ """
20
+ Advanced TTL management for memory cache.
21
+
22
+ Provides utilities for TTL monitoring, batch operations,
23
+ and advanced expiration handling.
24
+ """
25
+
26
+ def __init__(self):
27
+ self.memory_store = get_memory_store()
28
+
29
+ async def get_ttl_info(self, namespace: str, context_key: str, key: str) -> dict:
30
+ """
31
+ Get detailed TTL information for a key.
32
+
33
+ Args:
34
+ namespace: Cache namespace
35
+ context_key: Context identifier
36
+ key: Cache key
37
+
38
+ Returns:
39
+ Dictionary with TTL details
40
+ """
41
+ ttl_seconds = await self.memory_store.get_ttl(namespace, context_key, key)
42
+
43
+ info = {
44
+ "key": key,
45
+ "namespace": namespace,
46
+ "context_key": context_key,
47
+ "ttl_seconds": ttl_seconds,
48
+ "status": "unknown"
49
+ }
50
+
51
+ if ttl_seconds == -2:
52
+ info["status"] = "not_found"
53
+ info["message"] = "Key does not exist"
54
+ elif ttl_seconds == -1:
55
+ info["status"] = "no_expiry"
56
+ info["message"] = "Key exists with no expiration"
57
+ else:
58
+ info["status"] = "expires"
59
+ info["expires_at"] = datetime.now() + timedelta(seconds=ttl_seconds)
60
+ info["message"] = f"Key expires in {ttl_seconds} seconds"
61
+
62
+ return info
63
+
64
+ async def extend_ttl(
65
+ self,
66
+ namespace: str,
67
+ context_key: str,
68
+ key: str,
69
+ additional_seconds: int
70
+ ) -> bool:
71
+ """
72
+ Extend TTL by adding additional seconds to current TTL.
73
+
74
+ Args:
75
+ namespace: Cache namespace
76
+ context_key: Context identifier
77
+ key: Cache key
78
+ additional_seconds: Seconds to add to current TTL
79
+
80
+ Returns:
81
+ True if successful, False otherwise
82
+ """
83
+ current_ttl = await self.memory_store.get_ttl(namespace, context_key, key)
84
+
85
+ if current_ttl == -2:
86
+ # Key doesn't exist
87
+ return False
88
+ elif current_ttl == -1:
89
+ # No current expiry, set new TTL
90
+ return await self.memory_store.set_ttl(namespace, context_key, key, additional_seconds)
91
+ else:
92
+ # Extend current TTL
93
+ new_ttl = current_ttl + additional_seconds
94
+ return await self.memory_store.set_ttl(namespace, context_key, key, new_ttl)
95
+
96
+ async def refresh_ttl(
97
+ self,
98
+ namespace: str,
99
+ context_key: str,
100
+ key: str,
101
+ ttl_seconds: int
102
+ ) -> bool:
103
+ """
104
+ Refresh TTL to a new value (reset expiration timer).
105
+
106
+ Args:
107
+ namespace: Cache namespace
108
+ context_key: Context identifier
109
+ key: Cache key
110
+ ttl_seconds: New TTL in seconds
111
+
112
+ Returns:
113
+ True if successful, False otherwise
114
+ """
115
+ return await self.memory_store.set_ttl(namespace, context_key, key, ttl_seconds)
116
+
117
+ async def clear_ttl(self, namespace: str, context_key: str, key: str) -> bool:
118
+ """
119
+ Remove TTL from key (make it persistent).
120
+
121
+ Note: This is achieved by setting a very long TTL (100 years).
122
+
123
+ Args:
124
+ namespace: Cache namespace
125
+ context_key: Context identifier
126
+ key: Cache key
127
+
128
+ Returns:
129
+ True if successful, False otherwise
130
+ """
131
+ # Set TTL to 100 years (effectively no expiry)
132
+ very_long_ttl = 100 * 365 * 24 * 3600 # 100 years in seconds
133
+ return await self.memory_store.set_ttl(namespace, context_key, key, very_long_ttl)
134
+
135
+ async def get_expiring_keys(
136
+ self,
137
+ namespace: str,
138
+ context_key: str,
139
+ within_seconds: int = 300
140
+ ) -> list[dict]:
141
+ """
142
+ Get keys that will expire within specified seconds.
143
+
144
+ Args:
145
+ namespace: Cache namespace
146
+ context_key: Context identifier
147
+ within_seconds: Time window in seconds (default: 5 minutes)
148
+
149
+ Returns:
150
+ List of dictionaries with key info for expiring keys
151
+ """
152
+ expiring_keys = []
153
+
154
+ # Get all keys for the context
155
+ all_keys = await self.memory_store.get_all_keys(namespace, context_key)
156
+
157
+ for key in all_keys.keys():
158
+ ttl_info = await self.get_ttl_info(namespace, context_key, key)
159
+ if ttl_info["status"] == "expires" and ttl_info["ttl_seconds"] <= within_seconds:
160
+ expiring_keys.append(ttl_info)
161
+
162
+ return expiring_keys
163
+
164
+ async def batch_refresh_ttl(
165
+ self,
166
+ namespace: str,
167
+ context_key: str,
168
+ keys: list[str],
169
+ ttl_seconds: int
170
+ ) -> dict[str, bool]:
171
+ """
172
+ Refresh TTL for multiple keys in batch.
173
+
174
+ Args:
175
+ namespace: Cache namespace
176
+ context_key: Context identifier
177
+ keys: List of cache keys
178
+ ttl_seconds: New TTL in seconds
179
+
180
+ Returns:
181
+ Dictionary mapping key -> success status
182
+ """
183
+ results = {}
184
+ for key in keys:
185
+ results[key] = await self.refresh_ttl(namespace, context_key, key, ttl_seconds)
186
+ return results
187
+
188
+ async def get_namespace_stats(self, namespace: str) -> dict:
189
+ """
190
+ Get statistics for a namespace.
191
+
192
+ Args:
193
+ namespace: Cache namespace
194
+
195
+ Returns:
196
+ Dictionary with namespace statistics
197
+ """
198
+ stats = {
199
+ "namespace": namespace,
200
+ "total_contexts": 0,
201
+ "total_keys": 0,
202
+ "keys_with_ttl": 0,
203
+ "keys_persistent": 0,
204
+ "estimated_cleanup_needed": 0
205
+ }
206
+
207
+ # This would require access to the internal store structure
208
+ # For now, we'll provide basic stats that can be calculated
209
+ # without breaking encapsulation
210
+
211
+ try:
212
+ # Access the store directly for stats (this is a utility function)
213
+ store = self.memory_store._store[namespace]
214
+ stats["total_contexts"] = len(store)
215
+
216
+ for context_key, context_store in store.items():
217
+ stats["total_keys"] += len(context_store)
218
+
219
+ for key in context_store.keys():
220
+ ttl = await self.memory_store.get_ttl(namespace, context_key, key)
221
+ if ttl == -1:
222
+ stats["keys_persistent"] += 1
223
+ elif ttl >= 0:
224
+ stats["keys_with_ttl"] += 1
225
+ else:
226
+ stats["estimated_cleanup_needed"] += 1
227
+
228
+ except Exception as e:
229
+ logger.warning(f"Failed to calculate namespace stats for {namespace}: {e}")
230
+
231
+ return stats
232
+
233
+
234
+ # Global TTL manager instance
235
+ ttl_manager = TTLManager()
@@ -0,0 +1,76 @@
1
+ """
2
+ Memory cache factory implementation for Wappa framework.
3
+
4
+ Creates memory-backed cache instances using in-memory storage
5
+ with ICache adapters for uniform interface.
6
+ """
7
+
8
+ from ..domain.interfaces.cache_factory import ICacheFactory
9
+ from ..domain.interfaces.cache_interface import ICache
10
+ from .cache_adapters import (
11
+ MemoryStateCacheAdapter,
12
+ MemoryTableCacheAdapter,
13
+ MemoryUserCacheAdapter,
14
+ )
15
+
16
+
17
+ class MemoryCacheFactory(ICacheFactory):
18
+ """
19
+ Factory for creating memory-backed cache instances.
20
+
21
+ Uses thread-safe in-memory storage with TTL support:
22
+ - State cache: Uses states namespace with automatic TTL cleanup
23
+ - User cache: Uses users namespace with context isolation
24
+ - Table cache: Uses tables namespace with tenant isolation
25
+
26
+ All instances implement the ICache interface through adapters.
27
+
28
+ Context (tenant_id, user_id) is injected at construction time, eliminating
29
+ manual parameter passing.
30
+
31
+ Cache data is stored in memory with automatic background cleanup of expired entries.
32
+ """
33
+
34
+ def __init__(self, tenant_id: str, user_id: str):
35
+ """Initialize Memory cache factory with context injection."""
36
+ super().__init__(tenant_id, user_id)
37
+
38
+ def create_state_cache(self) -> ICache:
39
+ """
40
+ Create Memory state cache instance.
41
+
42
+ Uses context (tenant_id, user_id) injected at construction time.
43
+ Stores data in memory with namespace isolation and automatic TTL cleanup.
44
+
45
+ Returns:
46
+ ICache adapter wrapping MemoryStateHandler
47
+ """
48
+ return MemoryStateCacheAdapter(
49
+ tenant_id=self.tenant_id, user_id=self.user_id
50
+ )
51
+
52
+ def create_user_cache(self) -> ICache:
53
+ """
54
+ Create Memory user cache instance.
55
+
56
+ Uses context (tenant_id, user_id) injected at construction time.
57
+ Stores data in memory with namespace isolation and automatic TTL cleanup.
58
+
59
+ Returns:
60
+ ICache adapter wrapping MemoryUser
61
+ """
62
+ return MemoryUserCacheAdapter(
63
+ tenant_id=self.tenant_id, user_id=self.user_id
64
+ )
65
+
66
+ def create_table_cache(self) -> ICache:
67
+ """
68
+ Create Memory table cache instance.
69
+
70
+ Uses context (tenant_id) injected at construction time.
71
+ Stores data in memory with namespace isolation and automatic TTL cleanup.
72
+
73
+ Returns:
74
+ ICache adapter wrapping MemoryTable
75
+ """
76
+ return MemoryTableCacheAdapter(tenant_id=self.tenant_id)