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,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory Table handler - mirrors Redis table handler functionality.
|
|
3
|
+
|
|
4
|
+
Provides table cache operations using in-memory storage.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from ..storage_manager import storage_manager
|
|
13
|
+
from .utils.key_factory import default_key_factory
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("MemoryTable")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MemoryTable:
|
|
19
|
+
"""
|
|
20
|
+
Memory-based table cache handler.
|
|
21
|
+
|
|
22
|
+
Mirrors RedisTable functionality using in-memory storage.
|
|
23
|
+
Maintains the same API for seamless cache backend switching.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, tenant: str):
|
|
27
|
+
"""
|
|
28
|
+
Initialize Memory table handler.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
tenant: Tenant identifier
|
|
32
|
+
"""
|
|
33
|
+
if not tenant:
|
|
34
|
+
raise ValueError(f"Missing required parameter: tenant={tenant}")
|
|
35
|
+
|
|
36
|
+
self.tenant = tenant
|
|
37
|
+
self.keys = default_key_factory
|
|
38
|
+
|
|
39
|
+
def _key(self, table_name: str, pkid: str) -> str:
|
|
40
|
+
"""Build table key using KeyFactory (same as Redis)."""
|
|
41
|
+
return self.keys.table(self.tenant, table_name, pkid)
|
|
42
|
+
|
|
43
|
+
# ---- Public API matching RedisTable ----
|
|
44
|
+
async def get(
|
|
45
|
+
self,
|
|
46
|
+
table_name: str,
|
|
47
|
+
pkid: str,
|
|
48
|
+
models: type[BaseModel] | None = None,
|
|
49
|
+
) -> dict[str, Any] | None:
|
|
50
|
+
"""
|
|
51
|
+
Get table row data.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
table_name: Table name
|
|
55
|
+
pkid: Primary key ID
|
|
56
|
+
models: Optional BaseModel class for deserialization
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Table row data or None if not found
|
|
60
|
+
"""
|
|
61
|
+
key = self._key(table_name, pkid)
|
|
62
|
+
return await storage_manager.get("tables", self.tenant, None, key, models)
|
|
63
|
+
|
|
64
|
+
async def upsert(
|
|
65
|
+
self,
|
|
66
|
+
table_name: str,
|
|
67
|
+
pkid: str,
|
|
68
|
+
data: dict[str, Any] | BaseModel,
|
|
69
|
+
ttl: int | None = None,
|
|
70
|
+
) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Create or update table row data.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
table_name: Table name
|
|
76
|
+
pkid: Primary key ID
|
|
77
|
+
data: Data to store
|
|
78
|
+
ttl: Time to live in seconds
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
True if successful, False otherwise
|
|
82
|
+
"""
|
|
83
|
+
key = self._key(table_name, pkid)
|
|
84
|
+
return await storage_manager.set("tables", self.tenant, None, key, data, ttl)
|
|
85
|
+
|
|
86
|
+
async def delete(self, table_name: str, pkid: str) -> int:
|
|
87
|
+
"""
|
|
88
|
+
Delete table row data.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
table_name: Table name
|
|
92
|
+
pkid: Primary key ID
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
1 if deleted, 0 if didn't exist
|
|
96
|
+
"""
|
|
97
|
+
key = self._key(table_name, pkid)
|
|
98
|
+
success = await storage_manager.delete("tables", self.tenant, None, key)
|
|
99
|
+
return 1 if success else 0
|
|
100
|
+
|
|
101
|
+
async def exists(self, table_name: str, pkid: str) -> bool:
|
|
102
|
+
"""
|
|
103
|
+
Check if table row exists.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
table_name: Table name
|
|
107
|
+
pkid: Primary key ID
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
True if exists, False otherwise
|
|
111
|
+
"""
|
|
112
|
+
key = self._key(table_name, pkid)
|
|
113
|
+
return await storage_manager.exists("tables", self.tenant, None, key)
|
|
114
|
+
|
|
115
|
+
async def get_field(self, table_name: str, pkid: str, field: str) -> Any | None:
|
|
116
|
+
"""
|
|
117
|
+
Get a specific field from table row.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
table_name: Table name
|
|
121
|
+
pkid: Primary key ID
|
|
122
|
+
field: Field name
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Field value or None if not found
|
|
126
|
+
"""
|
|
127
|
+
row_data = await self.get(table_name, pkid)
|
|
128
|
+
if row_data is None:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
if isinstance(row_data, dict):
|
|
132
|
+
return row_data.get(field)
|
|
133
|
+
else:
|
|
134
|
+
# BaseModel instance
|
|
135
|
+
return getattr(row_data, field, None)
|
|
136
|
+
|
|
137
|
+
async def update_field(
|
|
138
|
+
self,
|
|
139
|
+
table_name: str,
|
|
140
|
+
pkid: str,
|
|
141
|
+
field: str,
|
|
142
|
+
value: Any,
|
|
143
|
+
ttl: int | None = None,
|
|
144
|
+
) -> bool:
|
|
145
|
+
"""
|
|
146
|
+
Update a specific field in table row.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
table_name: Table name
|
|
150
|
+
pkid: Primary key ID
|
|
151
|
+
field: Field name
|
|
152
|
+
value: New value
|
|
153
|
+
ttl: Time to live in seconds
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
True if successful, False otherwise
|
|
157
|
+
"""
|
|
158
|
+
row_data = await self.get(table_name, pkid)
|
|
159
|
+
if row_data is None:
|
|
160
|
+
row_data = {}
|
|
161
|
+
|
|
162
|
+
if isinstance(row_data, BaseModel):
|
|
163
|
+
row_data = row_data.model_dump()
|
|
164
|
+
|
|
165
|
+
row_data[field] = value
|
|
166
|
+
return await self.upsert(table_name, pkid, row_data, ttl)
|
|
167
|
+
|
|
168
|
+
async def increment_field(
|
|
169
|
+
self,
|
|
170
|
+
table_name: str,
|
|
171
|
+
pkid: str,
|
|
172
|
+
field: str,
|
|
173
|
+
increment: int = 1,
|
|
174
|
+
ttl: int | None = None,
|
|
175
|
+
) -> int | None:
|
|
176
|
+
"""
|
|
177
|
+
Atomically increment an integer field in table row.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
table_name: Table name
|
|
181
|
+
pkid: Primary key ID
|
|
182
|
+
field: Field name
|
|
183
|
+
increment: Amount to increment by
|
|
184
|
+
ttl: Time to live in seconds
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
New value after increment or None on error
|
|
188
|
+
"""
|
|
189
|
+
row_data = await self.get(table_name, pkid)
|
|
190
|
+
if row_data is None:
|
|
191
|
+
row_data = {}
|
|
192
|
+
|
|
193
|
+
if isinstance(row_data, BaseModel):
|
|
194
|
+
row_data = row_data.model_dump()
|
|
195
|
+
|
|
196
|
+
current_value = row_data.get(field, 0)
|
|
197
|
+
if not isinstance(current_value, (int, float)):
|
|
198
|
+
logger.warning(f"Cannot increment non-numeric field '{field}': {current_value}")
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
new_value = int(current_value) + increment
|
|
202
|
+
row_data[field] = new_value
|
|
203
|
+
|
|
204
|
+
success = await self.upsert(table_name, pkid, row_data, ttl)
|
|
205
|
+
return new_value if success else None
|
|
206
|
+
|
|
207
|
+
async def append_to_list(
|
|
208
|
+
self,
|
|
209
|
+
table_name: str,
|
|
210
|
+
pkid: str,
|
|
211
|
+
field: str,
|
|
212
|
+
value: Any,
|
|
213
|
+
ttl: int | None = None,
|
|
214
|
+
) -> bool:
|
|
215
|
+
"""
|
|
216
|
+
Append value to a list field in table row.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
table_name: Table name
|
|
220
|
+
pkid: Primary key ID
|
|
221
|
+
field: Field name containing list
|
|
222
|
+
value: Value to append
|
|
223
|
+
ttl: Time to live in seconds
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
True if successful, False otherwise
|
|
227
|
+
"""
|
|
228
|
+
row_data = await self.get(table_name, pkid)
|
|
229
|
+
if row_data is None:
|
|
230
|
+
row_data = {}
|
|
231
|
+
|
|
232
|
+
if isinstance(row_data, BaseModel):
|
|
233
|
+
row_data = row_data.model_dump()
|
|
234
|
+
|
|
235
|
+
current_list = row_data.get(field, [])
|
|
236
|
+
if not isinstance(current_list, list):
|
|
237
|
+
current_list = []
|
|
238
|
+
|
|
239
|
+
current_list.append(value)
|
|
240
|
+
row_data[field] = current_list
|
|
241
|
+
|
|
242
|
+
return await self.upsert(table_name, pkid, row_data, ttl)
|
|
243
|
+
|
|
244
|
+
async def get_ttl(self, key: str) -> int:
|
|
245
|
+
"""
|
|
246
|
+
Get remaining time to live for table cache.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Remaining TTL in seconds, -1 if no expiry, -2 if doesn't exist
|
|
250
|
+
"""
|
|
251
|
+
# For table cache, TTL is per key, not per cache file
|
|
252
|
+
# Extract table_name and pkid from key to build proper key
|
|
253
|
+
if ":" in key:
|
|
254
|
+
parts = key.split(":", 1)
|
|
255
|
+
if len(parts) == 2:
|
|
256
|
+
table_name, pkid = parts
|
|
257
|
+
actual_key = self._key(table_name, pkid)
|
|
258
|
+
return await storage_manager.get_ttl("tables", self.tenant, None, actual_key)
|
|
259
|
+
|
|
260
|
+
return await storage_manager.get_ttl("tables", self.tenant, None, key)
|
|
261
|
+
|
|
262
|
+
async def renew_ttl(self, key: str, ttl: int) -> bool:
|
|
263
|
+
"""
|
|
264
|
+
Renew time to live for table cache.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
ttl: New time to live in seconds
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
True if successful, False otherwise
|
|
271
|
+
"""
|
|
272
|
+
# For table cache, TTL is per key, not per cache file
|
|
273
|
+
if ":" in key:
|
|
274
|
+
parts = key.split(":", 1)
|
|
275
|
+
if len(parts) == 2:
|
|
276
|
+
table_name, pkid = parts
|
|
277
|
+
actual_key = self._key(table_name, pkid)
|
|
278
|
+
return await storage_manager.set_ttl("tables", self.tenant, None, actual_key, ttl)
|
|
279
|
+
|
|
280
|
+
return await storage_manager.set_ttl("tables", self.tenant, None, key, ttl)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory User handler - mirrors Redis user handler functionality.
|
|
3
|
+
|
|
4
|
+
Provides user-specific cache operations using in-memory storage.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from ..storage_manager import storage_manager
|
|
13
|
+
from .utils.key_factory import default_key_factory
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("MemoryUser")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MemoryUser:
|
|
19
|
+
"""
|
|
20
|
+
Memory-based user cache handler.
|
|
21
|
+
|
|
22
|
+
Mirrors RedisUser functionality using in-memory storage.
|
|
23
|
+
Maintains the same API for seamless cache backend switching.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, tenant: str, user_id: str):
|
|
27
|
+
"""
|
|
28
|
+
Initialize Memory user handler.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
tenant: Tenant identifier
|
|
32
|
+
user_id: User identifier
|
|
33
|
+
"""
|
|
34
|
+
if not tenant or not user_id:
|
|
35
|
+
raise ValueError(f"Missing required parameters: tenant={tenant}, user_id={user_id}")
|
|
36
|
+
|
|
37
|
+
self.tenant = tenant
|
|
38
|
+
self.user_id = user_id
|
|
39
|
+
self.keys = default_key_factory
|
|
40
|
+
|
|
41
|
+
def _key(self) -> str:
|
|
42
|
+
"""Build user key using KeyFactory (same as Redis)."""
|
|
43
|
+
return self.keys.user(self.tenant, self.user_id)
|
|
44
|
+
|
|
45
|
+
# ---- Public API matching RedisUser ----
|
|
46
|
+
async def get(self, models: type[BaseModel] | None = None) -> dict[str, Any] | None:
|
|
47
|
+
"""
|
|
48
|
+
Get full user data.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
models: Optional BaseModel class for deserialization
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
User data dictionary or BaseModel instance, None if not found
|
|
55
|
+
"""
|
|
56
|
+
key = self._key()
|
|
57
|
+
return await storage_manager.get("users", self.tenant, self.user_id, key, models)
|
|
58
|
+
|
|
59
|
+
async def upsert(self, data: dict[str, Any] | BaseModel, ttl: int | None = None) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Create or update user data.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
data: User data to store
|
|
65
|
+
ttl: Time to live in seconds
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
True if successful, False otherwise
|
|
69
|
+
"""
|
|
70
|
+
key = self._key()
|
|
71
|
+
return await storage_manager.set("users", self.tenant, self.user_id, key, data, ttl)
|
|
72
|
+
|
|
73
|
+
async def delete(self) -> int:
|
|
74
|
+
"""
|
|
75
|
+
Delete user data.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
1 if deleted, 0 if didn't exist
|
|
79
|
+
"""
|
|
80
|
+
key = self._key()
|
|
81
|
+
success = await storage_manager.delete("users", self.tenant, self.user_id, key)
|
|
82
|
+
return 1 if success else 0
|
|
83
|
+
|
|
84
|
+
async def exists(self) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Check if user data exists.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if exists, False otherwise
|
|
90
|
+
"""
|
|
91
|
+
key = self._key()
|
|
92
|
+
return await storage_manager.exists("users", self.tenant, self.user_id, key)
|
|
93
|
+
|
|
94
|
+
async def get_field(self, field: str) -> Any | None:
|
|
95
|
+
"""
|
|
96
|
+
Get a specific field from user data.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
field: Field name
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Field value or None if not found
|
|
103
|
+
"""
|
|
104
|
+
user_data = await self.get()
|
|
105
|
+
if user_data is None:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
if isinstance(user_data, dict):
|
|
109
|
+
return user_data.get(field)
|
|
110
|
+
else:
|
|
111
|
+
# BaseModel instance
|
|
112
|
+
return getattr(user_data, field, None)
|
|
113
|
+
|
|
114
|
+
async def update_field(self, field: str, value: Any, ttl: int | None = None) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Update a specific field in user data.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
field: Field name
|
|
120
|
+
value: New value
|
|
121
|
+
ttl: Time to live in seconds
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
True if successful, False otherwise
|
|
125
|
+
"""
|
|
126
|
+
user_data = await self.get()
|
|
127
|
+
if user_data is None:
|
|
128
|
+
user_data = {}
|
|
129
|
+
|
|
130
|
+
if isinstance(user_data, BaseModel):
|
|
131
|
+
user_data = user_data.model_dump()
|
|
132
|
+
|
|
133
|
+
user_data[field] = value
|
|
134
|
+
return await self.upsert(user_data, ttl)
|
|
135
|
+
|
|
136
|
+
async def increment_field(self, field: str, increment: int = 1, ttl: int | None = None) -> int | None:
|
|
137
|
+
"""
|
|
138
|
+
Atomically increment an integer field.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
field: Field name
|
|
142
|
+
increment: Amount to increment by
|
|
143
|
+
ttl: Time to live in seconds
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
New value after increment or None on error
|
|
147
|
+
"""
|
|
148
|
+
user_data = await self.get()
|
|
149
|
+
if user_data is None:
|
|
150
|
+
user_data = {}
|
|
151
|
+
|
|
152
|
+
if isinstance(user_data, BaseModel):
|
|
153
|
+
user_data = user_data.model_dump()
|
|
154
|
+
|
|
155
|
+
current_value = user_data.get(field, 0)
|
|
156
|
+
if not isinstance(current_value, (int, float)):
|
|
157
|
+
logger.warning(f"Cannot increment non-numeric field '{field}': {current_value}")
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
new_value = int(current_value) + increment
|
|
161
|
+
user_data[field] = new_value
|
|
162
|
+
|
|
163
|
+
success = await self.upsert(user_data, ttl)
|
|
164
|
+
return new_value if success else None
|
|
165
|
+
|
|
166
|
+
async def append_to_list(self, field: str, value: Any, ttl: int | None = None) -> bool:
|
|
167
|
+
"""
|
|
168
|
+
Append value to a list field.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
field: Field name containing list
|
|
172
|
+
value: Value to append
|
|
173
|
+
ttl: Time to live in seconds
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
True if successful, False otherwise
|
|
177
|
+
"""
|
|
178
|
+
user_data = await self.get()
|
|
179
|
+
if user_data is None:
|
|
180
|
+
user_data = {}
|
|
181
|
+
|
|
182
|
+
if isinstance(user_data, BaseModel):
|
|
183
|
+
user_data = user_data.model_dump()
|
|
184
|
+
|
|
185
|
+
current_list = user_data.get(field, [])
|
|
186
|
+
if not isinstance(current_list, list):
|
|
187
|
+
current_list = []
|
|
188
|
+
|
|
189
|
+
current_list.append(value)
|
|
190
|
+
user_data[field] = current_list
|
|
191
|
+
|
|
192
|
+
return await self.upsert(user_data, ttl)
|
|
193
|
+
|
|
194
|
+
async def get_ttl(self, key: str) -> int:
|
|
195
|
+
"""
|
|
196
|
+
Get remaining time to live.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Remaining TTL in seconds, -1 if no expiry, -2 if doesn't exist
|
|
200
|
+
"""
|
|
201
|
+
return await storage_manager.get_ttl("users", self.tenant, self.user_id, self._key())
|
|
202
|
+
|
|
203
|
+
async def renew_ttl(self, key: str, ttl: int) -> bool:
|
|
204
|
+
"""
|
|
205
|
+
Renew time to live.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
ttl: New time to live in seconds
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
True if successful, False otherwise
|
|
212
|
+
"""
|
|
213
|
+
return await storage_manager.set_ttl("users", self.tenant, self.user_id, self._key(), ttl)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Memory cache utilities package."""
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Key factory for Memory cache using Redis patterns.
|
|
3
|
+
|
|
4
|
+
Reuses the existing KeyFactory from Redis to maintain consistency
|
|
5
|
+
across all cache implementations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ....redis.redis_handler.utils.key_factory import KeyFactory, default_key_factory
|
|
9
|
+
|
|
10
|
+
# Export the same key factory used by Redis for consistency
|
|
11
|
+
__all__ = ["KeyFactory", "default_key_factory"]
|