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.
- wappa/__init__.py +85 -0
- wappa/api/__init__.py +1 -0
- wappa/api/controllers/__init__.py +10 -0
- wappa/api/controllers/webhook_controller.py +441 -0
- wappa/api/dependencies/__init__.py +15 -0
- wappa/api/dependencies/whatsapp_dependencies.py +220 -0
- wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
- wappa/api/middleware/__init__.py +7 -0
- wappa/api/middleware/error_handler.py +158 -0
- wappa/api/middleware/owner.py +99 -0
- wappa/api/middleware/request_logging.py +184 -0
- wappa/api/routes/__init__.py +6 -0
- wappa/api/routes/health.py +102 -0
- wappa/api/routes/webhooks.py +211 -0
- wappa/api/routes/whatsapp/__init__.py +15 -0
- wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
- wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
- wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
- wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
- wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
- wappa/api/routes/whatsapp_combined.py +35 -0
- wappa/cli/__init__.py +9 -0
- wappa/cli/main.py +199 -0
- wappa/core/__init__.py +6 -0
- wappa/core/config/__init__.py +5 -0
- wappa/core/config/settings.py +161 -0
- wappa/core/events/__init__.py +41 -0
- wappa/core/events/default_handlers.py +642 -0
- wappa/core/events/event_dispatcher.py +244 -0
- wappa/core/events/event_handler.py +247 -0
- wappa/core/events/webhook_factory.py +219 -0
- wappa/core/factory/__init__.py +15 -0
- wappa/core/factory/plugin.py +68 -0
- wappa/core/factory/wappa_builder.py +326 -0
- wappa/core/logging/__init__.py +5 -0
- wappa/core/logging/context.py +100 -0
- wappa/core/logging/logger.py +343 -0
- wappa/core/plugins/__init__.py +34 -0
- wappa/core/plugins/auth_plugin.py +169 -0
- wappa/core/plugins/cors_plugin.py +128 -0
- wappa/core/plugins/custom_middleware_plugin.py +182 -0
- wappa/core/plugins/database_plugin.py +235 -0
- wappa/core/plugins/rate_limit_plugin.py +183 -0
- wappa/core/plugins/redis_plugin.py +224 -0
- wappa/core/plugins/wappa_core_plugin.py +261 -0
- wappa/core/plugins/webhook_plugin.py +253 -0
- wappa/core/types.py +108 -0
- wappa/core/wappa_app.py +546 -0
- wappa/database/__init__.py +18 -0
- wappa/database/adapter.py +107 -0
- wappa/database/adapters/__init__.py +17 -0
- wappa/database/adapters/mysql_adapter.py +187 -0
- wappa/database/adapters/postgresql_adapter.py +169 -0
- wappa/database/adapters/sqlite_adapter.py +174 -0
- wappa/domain/__init__.py +28 -0
- wappa/domain/builders/__init__.py +5 -0
- wappa/domain/builders/message_builder.py +189 -0
- wappa/domain/entities/__init__.py +5 -0
- wappa/domain/enums/messenger_platform.py +123 -0
- wappa/domain/factories/__init__.py +6 -0
- wappa/domain/factories/media_factory.py +450 -0
- wappa/domain/factories/message_factory.py +497 -0
- wappa/domain/factories/messenger_factory.py +244 -0
- wappa/domain/interfaces/__init__.py +32 -0
- wappa/domain/interfaces/base_repository.py +94 -0
- wappa/domain/interfaces/cache_factory.py +85 -0
- wappa/domain/interfaces/cache_interface.py +199 -0
- wappa/domain/interfaces/expiry_repository.py +68 -0
- wappa/domain/interfaces/media_interface.py +311 -0
- wappa/domain/interfaces/messaging_interface.py +523 -0
- wappa/domain/interfaces/pubsub_repository.py +151 -0
- wappa/domain/interfaces/repository_factory.py +108 -0
- wappa/domain/interfaces/shared_state_repository.py +122 -0
- wappa/domain/interfaces/state_repository.py +123 -0
- wappa/domain/interfaces/tables_repository.py +215 -0
- wappa/domain/interfaces/user_repository.py +114 -0
- wappa/domain/interfaces/webhooks/__init__.py +1 -0
- wappa/domain/models/media_result.py +110 -0
- wappa/domain/models/platforms/__init__.py +15 -0
- wappa/domain/models/platforms/platform_config.py +104 -0
- wappa/domain/services/__init__.py +11 -0
- wappa/domain/services/tenant_credentials_service.py +56 -0
- wappa/messaging/__init__.py +7 -0
- wappa/messaging/whatsapp/__init__.py +1 -0
- wappa/messaging/whatsapp/client/__init__.py +5 -0
- wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
- wappa/messaging/whatsapp/handlers/__init__.py +13 -0
- wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
- wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
- wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
- wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
- wappa/messaging/whatsapp/messenger/__init__.py +5 -0
- wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
- wappa/messaging/whatsapp/models/__init__.py +61 -0
- wappa/messaging/whatsapp/models/basic_models.py +65 -0
- wappa/messaging/whatsapp/models/interactive_models.py +287 -0
- wappa/messaging/whatsapp/models/media_models.py +215 -0
- wappa/messaging/whatsapp/models/specialized_models.py +304 -0
- wappa/messaging/whatsapp/models/template_models.py +261 -0
- wappa/persistence/cache_factory.py +93 -0
- wappa/persistence/json/__init__.py +14 -0
- wappa/persistence/json/cache_adapters.py +271 -0
- wappa/persistence/json/handlers/__init__.py +1 -0
- wappa/persistence/json/handlers/state_handler.py +250 -0
- wappa/persistence/json/handlers/table_handler.py +263 -0
- wappa/persistence/json/handlers/user_handler.py +213 -0
- wappa/persistence/json/handlers/utils/__init__.py +1 -0
- wappa/persistence/json/handlers/utils/file_manager.py +153 -0
- wappa/persistence/json/handlers/utils/key_factory.py +11 -0
- wappa/persistence/json/handlers/utils/serialization.py +121 -0
- wappa/persistence/json/json_cache_factory.py +76 -0
- wappa/persistence/json/storage_manager.py +285 -0
- wappa/persistence/memory/__init__.py +14 -0
- wappa/persistence/memory/cache_adapters.py +271 -0
- wappa/persistence/memory/handlers/__init__.py +1 -0
- wappa/persistence/memory/handlers/state_handler.py +250 -0
- wappa/persistence/memory/handlers/table_handler.py +280 -0
- wappa/persistence/memory/handlers/user_handler.py +213 -0
- wappa/persistence/memory/handlers/utils/__init__.py +1 -0
- wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
- wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
- wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
- wappa/persistence/memory/memory_cache_factory.py +76 -0
- wappa/persistence/memory/storage_manager.py +235 -0
- wappa/persistence/redis/README.md +699 -0
- wappa/persistence/redis/__init__.py +11 -0
- wappa/persistence/redis/cache_adapters.py +285 -0
- wappa/persistence/redis/ops.py +880 -0
- wappa/persistence/redis/redis_cache_factory.py +71 -0
- wappa/persistence/redis/redis_client.py +231 -0
- wappa/persistence/redis/redis_handler/__init__.py +26 -0
- wappa/persistence/redis/redis_handler/state_handler.py +176 -0
- wappa/persistence/redis/redis_handler/table.py +158 -0
- wappa/persistence/redis/redis_handler/user.py +138 -0
- wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
- wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
- wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
- wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
- wappa/persistence/redis/redis_manager.py +189 -0
- wappa/processors/__init__.py +6 -0
- wappa/processors/base_processor.py +262 -0
- wappa/processors/factory.py +550 -0
- wappa/processors/whatsapp_processor.py +810 -0
- wappa/schemas/__init__.py +6 -0
- wappa/schemas/core/__init__.py +71 -0
- wappa/schemas/core/base_message.py +499 -0
- wappa/schemas/core/base_status.py +322 -0
- wappa/schemas/core/base_webhook.py +312 -0
- wappa/schemas/core/types.py +253 -0
- wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
- wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
- wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
- wappa/schemas/factory.py +754 -0
- wappa/schemas/webhooks/__init__.py +3 -0
- wappa/schemas/whatsapp/__init__.py +6 -0
- wappa/schemas/whatsapp/base_models.py +285 -0
- wappa/schemas/whatsapp/message_types/__init__.py +93 -0
- wappa/schemas/whatsapp/message_types/audio.py +350 -0
- wappa/schemas/whatsapp/message_types/button.py +267 -0
- wappa/schemas/whatsapp/message_types/contact.py +464 -0
- wappa/schemas/whatsapp/message_types/document.py +421 -0
- wappa/schemas/whatsapp/message_types/errors.py +195 -0
- wappa/schemas/whatsapp/message_types/image.py +424 -0
- wappa/schemas/whatsapp/message_types/interactive.py +430 -0
- wappa/schemas/whatsapp/message_types/location.py +416 -0
- wappa/schemas/whatsapp/message_types/order.py +372 -0
- wappa/schemas/whatsapp/message_types/reaction.py +271 -0
- wappa/schemas/whatsapp/message_types/sticker.py +328 -0
- wappa/schemas/whatsapp/message_types/system.py +317 -0
- wappa/schemas/whatsapp/message_types/text.py +411 -0
- wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
- wappa/schemas/whatsapp/message_types/video.py +344 -0
- wappa/schemas/whatsapp/status_models.py +479 -0
- wappa/schemas/whatsapp/validators.py +454 -0
- wappa/schemas/whatsapp/webhook_container.py +438 -0
- wappa/webhooks/__init__.py +17 -0
- wappa/webhooks/core/__init__.py +71 -0
- wappa/webhooks/core/base_message.py +499 -0
- wappa/webhooks/core/base_status.py +322 -0
- wappa/webhooks/core/base_webhook.py +312 -0
- wappa/webhooks/core/types.py +253 -0
- wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
- wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
- wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
- wappa/webhooks/factory.py +754 -0
- wappa/webhooks/whatsapp/__init__.py +6 -0
- wappa/webhooks/whatsapp/base_models.py +285 -0
- wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
- wappa/webhooks/whatsapp/message_types/audio.py +350 -0
- wappa/webhooks/whatsapp/message_types/button.py +267 -0
- wappa/webhooks/whatsapp/message_types/contact.py +464 -0
- wappa/webhooks/whatsapp/message_types/document.py +421 -0
- wappa/webhooks/whatsapp/message_types/errors.py +195 -0
- wappa/webhooks/whatsapp/message_types/image.py +424 -0
- wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
- wappa/webhooks/whatsapp/message_types/location.py +416 -0
- wappa/webhooks/whatsapp/message_types/order.py +372 -0
- wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
- wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
- wappa/webhooks/whatsapp/message_types/system.py +317 -0
- wappa/webhooks/whatsapp/message_types/text.py +411 -0
- wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
- wappa/webhooks/whatsapp/message_types/video.py +344 -0
- wappa/webhooks/whatsapp/status_models.py +479 -0
- wappa/webhooks/whatsapp/validators.py +454 -0
- wappa/webhooks/whatsapp/webhook_container.py +438 -0
- wappa-0.1.0.dist-info/METADATA +269 -0
- wappa-0.1.0.dist-info/RECORD +211 -0
- wappa-0.1.0.dist-info/WHEEL +4 -0
- wappa-0.1.0.dist-info/entry_points.txt +2 -0
- 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)
|