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,271 @@
1
+ """
2
+ JSON cache adapters that implement the ICache interface.
3
+
4
+ These adapters wrap JSON handlers to provide a uniform ICache interface
5
+ while preserving all functionality and maintaining API compatibility with Redis.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from pydantic import BaseModel
11
+
12
+ from ..domain.interfaces.cache_interface import ICache
13
+ from .handlers.state_handler import JSONStateHandler
14
+ from .handlers.table_handler import JSONTable
15
+ from .handlers.user_handler import JSONUser
16
+
17
+
18
+ class JSONStateCacheAdapter(ICache):
19
+ """Adapter that makes JSONStateHandler implement ICache interface."""
20
+
21
+ def __init__(self, tenant_id: str, user_id: str):
22
+ self._handler = JSONStateHandler(tenant=tenant_id, user_id=user_id)
23
+ self._default_handler_name = "cache"
24
+
25
+ async def get(
26
+ self, key: str, models: type[BaseModel] | None = None
27
+ ) -> dict[str, Any] | None:
28
+ """Get cached data by key."""
29
+ return await self._handler.get(key, models=models)
30
+
31
+ async def set(self, key: str, data: dict[str, Any] | BaseModel, ttl: int | None = None) -> bool:
32
+ """Set cached data with optional TTL."""
33
+ return await self._handler.upsert(key, data, ttl=ttl)
34
+
35
+ async def delete(self, key: str) -> bool:
36
+ """Delete cached data by key."""
37
+ result = await self._handler.delete(key)
38
+ return result > 0
39
+
40
+ async def exists(self, key: str) -> bool:
41
+ """Check if key exists in cache."""
42
+ return await self._handler.exists(key)
43
+
44
+ async def get_field(self, key: str, field: str) -> Any | None:
45
+ """Get a specific field from cached hash data."""
46
+ return await self._handler.get_field(key, field)
47
+
48
+ async def set_field(
49
+ self, key: str, field: str, value: Any, ttl: int | None = None
50
+ ) -> bool:
51
+ """Set a specific field in cached hash data."""
52
+ return await self._handler.update_field(key, field, value, ttl=ttl)
53
+
54
+ async def increment_field(
55
+ self, key: str, field: str, increment: int = 1, ttl: int | None = None
56
+ ) -> int | None:
57
+ """Atomically increment an integer field."""
58
+ return await self._handler.increment_field(key, field, increment, ttl=ttl)
59
+
60
+ async def append_to_list(
61
+ self, key: str, field: str, value: Any, ttl: int | None = None
62
+ ) -> bool:
63
+ """Append value to a list field."""
64
+ return await self._handler.append_to_list(key, field, value, ttl=ttl)
65
+
66
+ async def get_ttl(self, key: str) -> int:
67
+ """Get remaining time to live for a key."""
68
+ return await self._handler.get_ttl(key)
69
+
70
+ async def set_ttl(self, key: str, ttl: int) -> bool:
71
+ """Set time to live for a key."""
72
+ return await self._handler.renew_ttl(key, ttl=ttl)
73
+
74
+
75
+ class JSONUserCacheAdapter(ICache):
76
+ """Adapter that makes JSONUser implement ICache interface."""
77
+
78
+ def __init__(self, tenant_id: str, user_id: str):
79
+ self._handler = JSONUser(tenant=tenant_id, user_id=user_id)
80
+
81
+ async def get(
82
+ self, key: str, models: type[BaseModel] | None = None
83
+ ) -> dict[str, Any] | None:
84
+ """Get cached data by key. For user cache, key is ignored as it uses user_id."""
85
+ return await self._handler.get(models=models)
86
+
87
+ async def set(self, key: str, data: dict[str, Any] | BaseModel, ttl: int | None = None) -> bool:
88
+ """Set cached data with optional TTL."""
89
+ return await self._handler.upsert(data, ttl=ttl)
90
+
91
+ async def delete(self, key: str) -> bool:
92
+ """Delete cached data by key."""
93
+ result = await self._handler.delete()
94
+ return result > 0
95
+
96
+ async def exists(self, key: str) -> bool:
97
+ """Check if key exists in cache."""
98
+ return await self._handler.exists()
99
+
100
+ async def get_field(self, key: str, field: str) -> Any | None:
101
+ """Get a specific field from cached hash data."""
102
+ return await self._handler.get_field(field)
103
+
104
+ async def set_field(
105
+ self, key: str, field: str, value: Any, ttl: int | None = None
106
+ ) -> bool:
107
+ """Set a specific field in cached hash data."""
108
+ return await self._handler.update_field(field, value, ttl=ttl)
109
+
110
+ async def increment_field(
111
+ self, key: str, field: str, increment: int = 1, ttl: int | None = None
112
+ ) -> int | None:
113
+ """Atomically increment an integer field."""
114
+ return await self._handler.increment_field(field, increment, ttl=ttl)
115
+
116
+ async def append_to_list(
117
+ self, key: str, field: str, value: Any, ttl: int | None = None
118
+ ) -> bool:
119
+ """Append value to a list field."""
120
+ return await self._handler.append_to_list(field, value, ttl=ttl)
121
+
122
+ async def get_ttl(self, key: str) -> int:
123
+ """Get remaining time to live for a key."""
124
+ return await self._handler.get_ttl(key)
125
+
126
+ async def set_ttl(self, key: str, ttl: int) -> bool:
127
+ """Set time to live for a key."""
128
+ return await self._handler.renew_ttl(key, ttl=ttl)
129
+
130
+
131
+ class JSONTableCacheAdapter(ICache):
132
+ """
133
+ Adapter that makes JSONTable implement ICache interface.
134
+
135
+ Table Key Format Guide:
136
+ Use create_table_key(table_name, pkid) to generate proper keys.
137
+
138
+ Examples:
139
+ # Good - using helper method
140
+ key = cache.create_table_key("user_profiles", "12345")
141
+ await cache.set(key, user_data)
142
+
143
+ # Also supported - manual format
144
+ key = "user_profiles:12345"
145
+ await cache.set(key, user_data)
146
+ """
147
+
148
+ def __init__(self, tenant_id: str):
149
+ self._handler = JSONTable(tenant=tenant_id)
150
+
151
+ def create_table_key(self, table_name: str, pkid: str) -> str:
152
+ """
153
+ Create a properly formatted table cache key.
154
+
155
+ Args:
156
+ table_name: Name of the table (e.g., "user_profiles", "message_logs")
157
+ pkid: Primary key ID (e.g., user_id, message_id)
158
+
159
+ Returns:
160
+ Formatted key string for use with cache methods
161
+
162
+ Example:
163
+ key = cache.create_table_key("user_profiles", "12345")
164
+ # Returns: "user_profiles:12345"
165
+ """
166
+ if not table_name or not pkid:
167
+ raise ValueError("Both table_name and pkid must be provided and non-empty")
168
+
169
+ # Sanitize inputs to avoid conflicts
170
+ safe_table_name = str(table_name).replace(":", "_")
171
+ safe_pkid = str(pkid).replace(":", "_")
172
+
173
+ return f"{safe_table_name}:{safe_pkid}"
174
+
175
+ async def get(
176
+ self, key: str, models: type[BaseModel] | None = None
177
+ ) -> dict[str, Any] | None:
178
+ """Get cached data by key. Key should be in format 'table_name:pkid'."""
179
+ table_name, pkid = self._parse_key(key)
180
+ return await self._handler.get(table_name, pkid, models=models)
181
+
182
+ async def set(self, key: str, data: dict[str, Any] | BaseModel, ttl: int | None = None) -> bool:
183
+ """Set cached data with optional TTL."""
184
+ table_name, pkid = self._parse_key(key)
185
+ return await self._handler.upsert(table_name, pkid, data, ttl=ttl)
186
+
187
+ async def delete(self, key: str) -> bool:
188
+ """Delete cached data by key."""
189
+ table_name, pkid = self._parse_key(key)
190
+ result = await self._handler.delete(table_name, pkid)
191
+ return result > 0
192
+
193
+ async def exists(self, key: str) -> bool:
194
+ """Check if key exists in cache."""
195
+ table_name, pkid = self._parse_key(key)
196
+ return await self._handler.exists(table_name, pkid)
197
+
198
+ async def get_field(self, key: str, field: str) -> Any | None:
199
+ """Get a specific field from cached hash data."""
200
+ table_name, pkid = self._parse_key(key)
201
+ return await self._handler.get_field(table_name, pkid, field)
202
+
203
+ async def set_field(
204
+ self, key: str, field: str, value: Any, ttl: int | None = None
205
+ ) -> bool:
206
+ """Set a specific field in cached hash data."""
207
+ table_name, pkid = self._parse_key(key)
208
+ return await self._handler.update_field(table_name, pkid, field, value, ttl=ttl)
209
+
210
+ async def increment_field(
211
+ self, key: str, field: str, increment: int = 1, ttl: int | None = None
212
+ ) -> int | None:
213
+ """Atomically increment an integer field."""
214
+ table_name, pkid = self._parse_key(key)
215
+ return await self._handler.increment_field(
216
+ table_name, pkid, field, increment, ttl=ttl
217
+ )
218
+
219
+ async def append_to_list(
220
+ self, key: str, field: str, value: Any, ttl: int | None = None
221
+ ) -> bool:
222
+ """Append value to a list field."""
223
+ table_name, pkid = self._parse_key(key)
224
+ return await self._handler.append_to_list(
225
+ table_name, pkid, field, value, ttl=ttl
226
+ )
227
+
228
+ async def get_ttl(self, key: str) -> int:
229
+ """Get remaining time to live for a key."""
230
+ return await self._handler.get_ttl(key)
231
+
232
+ async def set_ttl(self, key: str, ttl: int) -> bool:
233
+ """Set time to live for a key."""
234
+ return await self._handler.renew_ttl(key, ttl=ttl)
235
+
236
+ def _parse_key(self, key: str) -> tuple[str, str]:
237
+ """
238
+ Parse key into table_name and pkid with validation.
239
+
240
+ Args:
241
+ key: Cache key in format "table_name:pkid"
242
+
243
+ Returns:
244
+ Tuple of (table_name, pkid)
245
+
246
+ Raises:
247
+ ValueError: If key format is invalid
248
+ """
249
+ if not key:
250
+ raise ValueError("Key cannot be empty")
251
+
252
+ if ":" not in key:
253
+ raise ValueError(
254
+ f"Invalid table cache key format: '{key}'. "
255
+ f"Expected format: 'table_name:pkid'. "
256
+ f"Use create_table_key(table_name, pkid) to generate proper keys."
257
+ )
258
+
259
+ parts = key.split(":", 1)
260
+ if len(parts) != 2:
261
+ raise ValueError(f"Invalid table cache key format: '{key}'. Expected exactly one ':' separator.")
262
+
263
+ table_name, pkid = parts
264
+
265
+ if not table_name.strip():
266
+ raise ValueError(f"Invalid table cache key: '{key}'. Table name cannot be empty.")
267
+
268
+ if not pkid.strip():
269
+ raise ValueError(f"Invalid table cache key: '{key}'. Primary key ID cannot be empty.")
270
+
271
+ return table_name.strip(), pkid.strip()
@@ -0,0 +1 @@
1
+ """JSON cache handlers package."""
@@ -0,0 +1,250 @@
1
+ """
2
+ JSON State handler - mirrors Redis state handler functionality.
3
+
4
+ Provides state cache operations using JSON file 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("JSONStateHandler")
16
+
17
+
18
+ class JSONStateHandler:
19
+ """
20
+ JSON-based state cache handler.
21
+
22
+ Mirrors RedisStateHandler functionality using file-based JSON 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 JSON state 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, handler_name: str) -> str:
42
+ """Build handler key using KeyFactory (same as Redis)."""
43
+ return self.keys.handler(self.tenant, handler_name, self.user_id)
44
+
45
+ # ---- Public API matching RedisStateHandler ----
46
+ async def get(
47
+ self, handler_name: str, models: type[BaseModel] | None = None
48
+ ) -> dict[str, Any] | None:
49
+ """
50
+ Get handler state data.
51
+
52
+ Args:
53
+ handler_name: Handler name
54
+ models: Optional BaseModel class for deserialization
55
+
56
+ Returns:
57
+ Handler state data or None if not found
58
+ """
59
+ key = self._key(handler_name)
60
+ return await storage_manager.get("states", self.tenant, self.user_id, key, models)
61
+
62
+ async def upsert(
63
+ self,
64
+ handler_name: str,
65
+ data: dict[str, Any] | BaseModel,
66
+ ttl: int | None = None,
67
+ ) -> bool:
68
+ """
69
+ Create or update handler state data.
70
+
71
+ Args:
72
+ handler_name: Handler name
73
+ data: State data to store
74
+ ttl: Time to live in seconds
75
+
76
+ Returns:
77
+ True if successful, False otherwise
78
+ """
79
+ key = self._key(handler_name)
80
+ return await storage_manager.set("states", self.tenant, self.user_id, key, data, ttl)
81
+
82
+ async def delete(self, handler_name: str) -> int:
83
+ """
84
+ Delete handler state data.
85
+
86
+ Args:
87
+ handler_name: Handler name
88
+
89
+ Returns:
90
+ 1 if deleted, 0 if didn't exist
91
+ """
92
+ key = self._key(handler_name)
93
+ success = await storage_manager.delete("states", self.tenant, self.user_id, key)
94
+ return 1 if success else 0
95
+
96
+ async def exists(self, handler_name: str) -> bool:
97
+ """
98
+ Check if handler state exists.
99
+
100
+ Args:
101
+ handler_name: Handler name
102
+
103
+ Returns:
104
+ True if exists, False otherwise
105
+ """
106
+ key = self._key(handler_name)
107
+ return await storage_manager.exists("states", self.tenant, self.user_id, key)
108
+
109
+ async def get_field(self, handler_name: str, field: str) -> Any | None:
110
+ """
111
+ Get a specific field from handler state.
112
+
113
+ Args:
114
+ handler_name: Handler name
115
+ field: Field name
116
+
117
+ Returns:
118
+ Field value or None if not found
119
+ """
120
+ state_data = await self.get(handler_name)
121
+ if state_data is None:
122
+ return None
123
+
124
+ if isinstance(state_data, dict):
125
+ return state_data.get(field)
126
+ else:
127
+ # BaseModel instance
128
+ return getattr(state_data, field, None)
129
+
130
+ async def update_field(
131
+ self,
132
+ handler_name: str,
133
+ field: str,
134
+ value: Any,
135
+ ttl: int | None = None,
136
+ ) -> bool:
137
+ """
138
+ Update a specific field in handler state.
139
+
140
+ Args:
141
+ handler_name: Handler name
142
+ field: Field name
143
+ value: New value
144
+ ttl: Time to live in seconds
145
+
146
+ Returns:
147
+ True if successful, False otherwise
148
+ """
149
+ state_data = await self.get(handler_name)
150
+ if state_data is None:
151
+ state_data = {}
152
+
153
+ if isinstance(state_data, BaseModel):
154
+ state_data = state_data.model_dump()
155
+
156
+ state_data[field] = value
157
+ return await self.upsert(handler_name, state_data, ttl)
158
+
159
+ async def increment_field(
160
+ self,
161
+ handler_name: str,
162
+ field: str,
163
+ increment: int = 1,
164
+ ttl: int | None = None,
165
+ ) -> int | None:
166
+ """
167
+ Atomically increment an integer field in handler state.
168
+
169
+ Args:
170
+ handler_name: Handler name
171
+ field: Field name
172
+ increment: Amount to increment by
173
+ ttl: Time to live in seconds
174
+
175
+ Returns:
176
+ New value after increment or None on error
177
+ """
178
+ state_data = await self.get(handler_name)
179
+ if state_data is None:
180
+ state_data = {}
181
+
182
+ if isinstance(state_data, BaseModel):
183
+ state_data = state_data.model_dump()
184
+
185
+ current_value = state_data.get(field, 0)
186
+ if not isinstance(current_value, (int, float)):
187
+ logger.warning(f"Cannot increment non-numeric field '{field}': {current_value}")
188
+ return None
189
+
190
+ new_value = int(current_value) + increment
191
+ state_data[field] = new_value
192
+
193
+ success = await self.upsert(handler_name, state_data, ttl)
194
+ return new_value if success else None
195
+
196
+ async def append_to_list(
197
+ self,
198
+ handler_name: str,
199
+ field: str,
200
+ value: Any,
201
+ ttl: int | None = None,
202
+ ) -> bool:
203
+ """
204
+ Append value to a list field in handler state.
205
+
206
+ Args:
207
+ handler_name: Handler name
208
+ field: Field name containing list
209
+ value: Value to append
210
+ ttl: Time to live in seconds
211
+
212
+ Returns:
213
+ True if successful, False otherwise
214
+ """
215
+ state_data = await self.get(handler_name)
216
+ if state_data is None:
217
+ state_data = {}
218
+
219
+ if isinstance(state_data, BaseModel):
220
+ state_data = state_data.model_dump()
221
+
222
+ current_list = state_data.get(field, [])
223
+ if not isinstance(current_list, list):
224
+ current_list = []
225
+
226
+ current_list.append(value)
227
+ state_data[field] = current_list
228
+
229
+ return await self.upsert(handler_name, state_data, ttl)
230
+
231
+ async def get_ttl(self, key: str) -> int:
232
+ """
233
+ Get remaining time to live for state cache.
234
+
235
+ Returns:
236
+ Remaining TTL in seconds, -1 if no expiry, -2 if doesn't exist
237
+ """
238
+ return await storage_manager.get_ttl("states", self.tenant, self.user_id)
239
+
240
+ async def renew_ttl(self, key: str, ttl: int) -> bool:
241
+ """
242
+ Renew time to live for state cache.
243
+
244
+ Args:
245
+ ttl: New time to live in seconds
246
+
247
+ Returns:
248
+ True if successful, False otherwise
249
+ """
250
+ return await storage_manager.set_ttl("states", self.tenant, self.user_id, ttl)