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,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"]