wappa 0.1.8__py3-none-any.whl → 0.1.10__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 +4 -5
- wappa/api/controllers/webhook_controller.py +5 -2
- wappa/api/dependencies/__init__.py +0 -5
- wappa/api/middleware/error_handler.py +4 -4
- wappa/api/middleware/owner.py +11 -5
- wappa/api/routes/webhooks.py +2 -2
- wappa/cli/__init__.py +1 -1
- wappa/cli/examples/init/.env.example +33 -0
- wappa/cli/examples/init/app/__init__.py +0 -0
- wappa/cli/examples/init/app/main.py +9 -0
- wappa/cli/examples/init/app/master_event.py +10 -0
- wappa/cli/examples/json_cache_example/.env.example +33 -0
- wappa/cli/examples/json_cache_example/app/__init__.py +1 -0
- wappa/cli/examples/json_cache_example/app/main.py +247 -0
- wappa/cli/examples/json_cache_example/app/master_event.py +455 -0
- wappa/cli/examples/json_cache_example/app/models/__init__.py +1 -0
- wappa/cli/examples/json_cache_example/app/models/json_demo_models.py +256 -0
- wappa/cli/examples/json_cache_example/app/scores/__init__.py +35 -0
- wappa/cli/examples/json_cache_example/app/scores/score_base.py +192 -0
- wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +256 -0
- wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +187 -0
- wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +272 -0
- wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +239 -0
- wappa/cli/examples/json_cache_example/app/utils/__init__.py +26 -0
- wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +174 -0
- wappa/cli/examples/json_cache_example/app/utils/message_utils.py +251 -0
- wappa/cli/examples/openai_transcript/.gitignore +63 -4
- wappa/cli/examples/openai_transcript/app/__init__.py +0 -0
- wappa/cli/examples/openai_transcript/app/main.py +9 -0
- wappa/cli/examples/openai_transcript/app/master_event.py +62 -0
- wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +3 -0
- wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +89 -0
- wappa/cli/examples/redis_cache_example/.env.example +33 -0
- wappa/cli/examples/redis_cache_example/app/__init__.py +6 -0
- wappa/cli/examples/redis_cache_example/app/main.py +246 -0
- wappa/cli/examples/redis_cache_example/app/master_event.py +455 -0
- wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +256 -0
- wappa/cli/examples/redis_cache_example/app/scores/__init__.py +35 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_base.py +192 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +256 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +187 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +272 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +239 -0
- wappa/cli/examples/redis_cache_example/app/utils/__init__.py +26 -0
- wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +174 -0
- wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +251 -0
- wappa/cli/examples/simple_echo_example/.env.example +33 -0
- wappa/cli/examples/simple_echo_example/app/__init__.py +7 -0
- wappa/cli/examples/simple_echo_example/app/main.py +191 -0
- wappa/cli/examples/simple_echo_example/app/master_event.py +230 -0
- wappa/cli/examples/wappa_full_example/.env.example +33 -0
- wappa/cli/examples/wappa_full_example/.gitignore +63 -4
- wappa/cli/examples/wappa_full_example/app/__init__.py +6 -0
- wappa/cli/examples/wappa_full_example/app/handlers/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/handlers/command_handlers.py +492 -0
- wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +559 -0
- wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +514 -0
- wappa/cli/examples/wappa_full_example/app/main.py +269 -0
- wappa/cli/examples/wappa_full_example/app/master_event.py +504 -0
- wappa/cli/examples/wappa_full_example/app/media/README.md +54 -0
- wappa/cli/examples/wappa_full_example/app/media/buttons/README.md +62 -0
- wappa/cli/examples/wappa_full_example/app/media/buttons/kitty.png +0 -0
- wappa/cli/examples/wappa_full_example/app/media/buttons/puppy.png +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/README.md +110 -0
- wappa/cli/examples/wappa_full_example/app/media/list/audio.mp3 +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/document.pdf +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/image.png +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/video.mp4 +0 -0
- wappa/cli/examples/wappa_full_example/app/models/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/models/state_models.py +434 -0
- wappa/cli/examples/wappa_full_example/app/models/user_models.py +303 -0
- wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +327 -0
- wappa/cli/examples/wappa_full_example/app/utils/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +502 -0
- wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +516 -0
- wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +337 -0
- wappa/cli/main.py +14 -5
- wappa/core/__init__.py +18 -23
- wappa/core/config/settings.py +7 -5
- wappa/core/events/default_handlers.py +1 -1
- wappa/core/factory/wappa_builder.py +38 -25
- wappa/core/plugins/redis_plugin.py +1 -3
- wappa/core/plugins/wappa_core_plugin.py +7 -6
- wappa/core/types.py +12 -12
- wappa/core/wappa_app.py +10 -8
- wappa/database/__init__.py +3 -4
- wappa/domain/enums/messenger_platform.py +1 -2
- wappa/domain/factories/media_factory.py +5 -20
- wappa/domain/factories/message_factory.py +5 -20
- wappa/domain/factories/messenger_factory.py +2 -4
- wappa/domain/interfaces/cache_interface.py +7 -7
- wappa/domain/interfaces/media_interface.py +2 -5
- wappa/domain/models/media_result.py +1 -3
- wappa/domain/models/platforms/platform_config.py +1 -3
- wappa/messaging/__init__.py +9 -12
- wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +20 -22
- wappa/models/__init__.py +27 -35
- wappa/persistence/__init__.py +12 -15
- wappa/persistence/cache_factory.py +0 -1
- wappa/persistence/json/__init__.py +1 -1
- wappa/persistence/json/cache_adapters.py +37 -25
- wappa/persistence/json/handlers/state_handler.py +60 -52
- wappa/persistence/json/handlers/table_handler.py +51 -49
- wappa/persistence/json/handlers/user_handler.py +71 -55
- wappa/persistence/json/handlers/utils/file_manager.py +42 -39
- wappa/persistence/json/handlers/utils/key_factory.py +1 -1
- wappa/persistence/json/handlers/utils/serialization.py +13 -11
- wappa/persistence/json/json_cache_factory.py +4 -8
- wappa/persistence/json/storage_manager.py +66 -79
- wappa/persistence/memory/__init__.py +1 -1
- wappa/persistence/memory/cache_adapters.py +37 -25
- wappa/persistence/memory/handlers/state_handler.py +62 -52
- wappa/persistence/memory/handlers/table_handler.py +59 -53
- wappa/persistence/memory/handlers/user_handler.py +75 -55
- wappa/persistence/memory/handlers/utils/key_factory.py +1 -1
- wappa/persistence/memory/handlers/utils/memory_store.py +75 -71
- wappa/persistence/memory/handlers/utils/ttl_manager.py +59 -67
- wappa/persistence/memory/memory_cache_factory.py +3 -7
- wappa/persistence/memory/storage_manager.py +52 -62
- wappa/persistence/redis/cache_adapters.py +27 -21
- wappa/persistence/redis/ops.py +11 -11
- wappa/persistence/redis/redis_client.py +4 -6
- wappa/persistence/redis/redis_manager.py +12 -4
- wappa/processors/factory.py +5 -5
- wappa/schemas/factory.py +2 -5
- wappa/schemas/whatsapp/message_types/errors.py +3 -12
- wappa/schemas/whatsapp/validators.py +3 -3
- wappa/webhooks/__init__.py +17 -18
- wappa/webhooks/factory.py +3 -5
- wappa/webhooks/whatsapp/__init__.py +10 -13
- wappa/webhooks/whatsapp/message_types/audio.py +0 -4
- wappa/webhooks/whatsapp/message_types/document.py +1 -9
- wappa/webhooks/whatsapp/message_types/errors.py +3 -12
- wappa/webhooks/whatsapp/message_types/location.py +1 -21
- wappa/webhooks/whatsapp/message_types/sticker.py +1 -5
- wappa/webhooks/whatsapp/message_types/text.py +0 -6
- wappa/webhooks/whatsapp/message_types/video.py +1 -20
- wappa/webhooks/whatsapp/status_models.py +2 -2
- wappa/webhooks/whatsapp/validators.py +3 -3
- {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/METADATA +362 -8
- {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/RECORD +144 -80
- wappa/cli/examples/init/pyproject.toml +0 -7
- wappa/cli/examples/simple_echo_example/.python-version +0 -1
- wappa/cli/examples/simple_echo_example/pyproject.toml +0 -9
- {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/WHEEL +0 -0
- {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/entry_points.txt +0 -0
- {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,7 +5,7 @@ Provides state cache operations using JSON file storage.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
from pydantic import BaseModel
|
|
11
11
|
|
|
@@ -18,47 +18,51 @@ logger = logging.getLogger("JSONStateHandler")
|
|
|
18
18
|
class JSONStateHandler:
|
|
19
19
|
"""
|
|
20
20
|
JSON-based state cache handler.
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
Mirrors RedisStateHandler functionality using file-based JSON storage.
|
|
23
23
|
Maintains the same API for seamless cache backend switching.
|
|
24
24
|
"""
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
def __init__(self, tenant: str, user_id: str):
|
|
27
27
|
"""
|
|
28
28
|
Initialize JSON state handler.
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
Args:
|
|
31
31
|
tenant: Tenant identifier
|
|
32
32
|
user_id: User identifier
|
|
33
33
|
"""
|
|
34
34
|
if not tenant or not user_id:
|
|
35
|
-
raise ValueError(
|
|
36
|
-
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"Missing required parameters: tenant={tenant}, user_id={user_id}"
|
|
37
|
+
)
|
|
38
|
+
|
|
37
39
|
self.tenant = tenant
|
|
38
40
|
self.user_id = user_id
|
|
39
41
|
self.keys = default_key_factory
|
|
40
|
-
|
|
42
|
+
|
|
41
43
|
def _key(self, handler_name: str) -> str:
|
|
42
44
|
"""Build handler key using KeyFactory (same as Redis)."""
|
|
43
45
|
return self.keys.handler(self.tenant, handler_name, self.user_id)
|
|
44
|
-
|
|
46
|
+
|
|
45
47
|
# ---- Public API matching RedisStateHandler ----
|
|
46
48
|
async def get(
|
|
47
49
|
self, handler_name: str, models: type[BaseModel] | None = None
|
|
48
50
|
) -> dict[str, Any] | None:
|
|
49
51
|
"""
|
|
50
52
|
Get handler state data.
|
|
51
|
-
|
|
53
|
+
|
|
52
54
|
Args:
|
|
53
55
|
handler_name: Handler name
|
|
54
56
|
models: Optional BaseModel class for deserialization
|
|
55
|
-
|
|
57
|
+
|
|
56
58
|
Returns:
|
|
57
59
|
Handler state data or None if not found
|
|
58
60
|
"""
|
|
59
61
|
key = self._key(handler_name)
|
|
60
|
-
return await storage_manager.get(
|
|
61
|
-
|
|
62
|
+
return await storage_manager.get(
|
|
63
|
+
"states", self.tenant, self.user_id, key, models
|
|
64
|
+
)
|
|
65
|
+
|
|
62
66
|
async def upsert(
|
|
63
67
|
self,
|
|
64
68
|
handler_name: str,
|
|
@@ -67,66 +71,68 @@ class JSONStateHandler:
|
|
|
67
71
|
) -> bool:
|
|
68
72
|
"""
|
|
69
73
|
Create or update handler state data.
|
|
70
|
-
|
|
74
|
+
|
|
71
75
|
Args:
|
|
72
76
|
handler_name: Handler name
|
|
73
77
|
data: State data to store
|
|
74
78
|
ttl: Time to live in seconds
|
|
75
|
-
|
|
79
|
+
|
|
76
80
|
Returns:
|
|
77
81
|
True if successful, False otherwise
|
|
78
82
|
"""
|
|
79
83
|
key = self._key(handler_name)
|
|
80
|
-
return await storage_manager.set(
|
|
81
|
-
|
|
84
|
+
return await storage_manager.set(
|
|
85
|
+
"states", self.tenant, self.user_id, key, data, ttl
|
|
86
|
+
)
|
|
87
|
+
|
|
82
88
|
async def delete(self, handler_name: str) -> int:
|
|
83
89
|
"""
|
|
84
90
|
Delete handler state data.
|
|
85
|
-
|
|
91
|
+
|
|
86
92
|
Args:
|
|
87
93
|
handler_name: Handler name
|
|
88
|
-
|
|
94
|
+
|
|
89
95
|
Returns:
|
|
90
96
|
1 if deleted, 0 if didn't exist
|
|
91
97
|
"""
|
|
92
98
|
key = self._key(handler_name)
|
|
93
99
|
success = await storage_manager.delete("states", self.tenant, self.user_id, key)
|
|
94
100
|
return 1 if success else 0
|
|
95
|
-
|
|
101
|
+
|
|
96
102
|
async def exists(self, handler_name: str) -> bool:
|
|
97
103
|
"""
|
|
98
104
|
Check if handler state exists.
|
|
99
|
-
|
|
105
|
+
|
|
100
106
|
Args:
|
|
101
107
|
handler_name: Handler name
|
|
102
|
-
|
|
108
|
+
|
|
103
109
|
Returns:
|
|
104
110
|
True if exists, False otherwise
|
|
105
111
|
"""
|
|
106
112
|
key = self._key(handler_name)
|
|
107
113
|
return await storage_manager.exists("states", self.tenant, self.user_id, key)
|
|
108
|
-
|
|
114
|
+
|
|
109
115
|
async def get_field(self, handler_name: str, field: str) -> Any | None:
|
|
110
116
|
"""
|
|
111
117
|
Get a specific field from handler state.
|
|
112
|
-
|
|
118
|
+
|
|
113
119
|
Args:
|
|
114
120
|
handler_name: Handler name
|
|
115
121
|
field: Field name
|
|
116
|
-
|
|
122
|
+
|
|
117
123
|
Returns:
|
|
118
124
|
Field value or None if not found
|
|
119
125
|
"""
|
|
120
126
|
state_data = await self.get(handler_name)
|
|
121
127
|
if state_data is None:
|
|
122
128
|
return None
|
|
123
|
-
|
|
129
|
+
|
|
124
130
|
if isinstance(state_data, dict):
|
|
125
131
|
return state_data.get(field)
|
|
126
132
|
else:
|
|
127
133
|
# BaseModel instance
|
|
128
134
|
return getattr(state_data, field, None)
|
|
129
|
-
|
|
135
|
+
|
|
130
136
|
async def update_field(
|
|
131
137
|
self,
|
|
132
138
|
handler_name: str,
|
|
@@ -136,26 +142,26 @@ class JSONStateHandler:
|
|
|
136
142
|
) -> bool:
|
|
137
143
|
"""
|
|
138
144
|
Update a specific field in handler state.
|
|
139
|
-
|
|
145
|
+
|
|
140
146
|
Args:
|
|
141
147
|
handler_name: Handler name
|
|
142
148
|
field: Field name
|
|
143
149
|
value: New value
|
|
144
150
|
ttl: Time to live in seconds
|
|
145
|
-
|
|
151
|
+
|
|
146
152
|
Returns:
|
|
147
153
|
True if successful, False otherwise
|
|
148
154
|
"""
|
|
149
155
|
state_data = await self.get(handler_name)
|
|
150
156
|
if state_data is None:
|
|
151
157
|
state_data = {}
|
|
152
|
-
|
|
158
|
+
|
|
153
159
|
if isinstance(state_data, BaseModel):
|
|
154
160
|
state_data = state_data.model_dump()
|
|
155
|
-
|
|
161
|
+
|
|
156
162
|
state_data[field] = value
|
|
157
163
|
return await self.upsert(handler_name, state_data, ttl)
|
|
158
|
-
|
|
164
|
+
|
|
159
165
|
async def increment_field(
|
|
160
166
|
self,
|
|
161
167
|
handler_name: str,
|
|
@@ -165,34 +171,36 @@ class JSONStateHandler:
|
|
|
165
171
|
) -> int | None:
|
|
166
172
|
"""
|
|
167
173
|
Atomically increment an integer field in handler state.
|
|
168
|
-
|
|
174
|
+
|
|
169
175
|
Args:
|
|
170
176
|
handler_name: Handler name
|
|
171
177
|
field: Field name
|
|
172
178
|
increment: Amount to increment by
|
|
173
179
|
ttl: Time to live in seconds
|
|
174
|
-
|
|
180
|
+
|
|
175
181
|
Returns:
|
|
176
182
|
New value after increment or None on error
|
|
177
183
|
"""
|
|
178
184
|
state_data = await self.get(handler_name)
|
|
179
185
|
if state_data is None:
|
|
180
186
|
state_data = {}
|
|
181
|
-
|
|
187
|
+
|
|
182
188
|
if isinstance(state_data, BaseModel):
|
|
183
189
|
state_data = state_data.model_dump()
|
|
184
|
-
|
|
190
|
+
|
|
185
191
|
current_value = state_data.get(field, 0)
|
|
186
|
-
if not isinstance(current_value,
|
|
187
|
-
logger.warning(
|
|
192
|
+
if not isinstance(current_value, int | float):
|
|
193
|
+
logger.warning(
|
|
194
|
+
f"Cannot increment non-numeric field '{field}': {current_value}"
|
|
195
|
+
)
|
|
188
196
|
return None
|
|
189
|
-
|
|
197
|
+
|
|
190
198
|
new_value = int(current_value) + increment
|
|
191
199
|
state_data[field] = new_value
|
|
192
|
-
|
|
200
|
+
|
|
193
201
|
success = await self.upsert(handler_name, state_data, ttl)
|
|
194
202
|
return new_value if success else None
|
|
195
|
-
|
|
203
|
+
|
|
196
204
|
async def append_to_list(
|
|
197
205
|
self,
|
|
198
206
|
handler_name: str,
|
|
@@ -202,49 +210,49 @@ class JSONStateHandler:
|
|
|
202
210
|
) -> bool:
|
|
203
211
|
"""
|
|
204
212
|
Append value to a list field in handler state.
|
|
205
|
-
|
|
213
|
+
|
|
206
214
|
Args:
|
|
207
215
|
handler_name: Handler name
|
|
208
216
|
field: Field name containing list
|
|
209
217
|
value: Value to append
|
|
210
218
|
ttl: Time to live in seconds
|
|
211
|
-
|
|
219
|
+
|
|
212
220
|
Returns:
|
|
213
221
|
True if successful, False otherwise
|
|
214
222
|
"""
|
|
215
223
|
state_data = await self.get(handler_name)
|
|
216
224
|
if state_data is None:
|
|
217
225
|
state_data = {}
|
|
218
|
-
|
|
226
|
+
|
|
219
227
|
if isinstance(state_data, BaseModel):
|
|
220
228
|
state_data = state_data.model_dump()
|
|
221
|
-
|
|
229
|
+
|
|
222
230
|
current_list = state_data.get(field, [])
|
|
223
231
|
if not isinstance(current_list, list):
|
|
224
232
|
current_list = []
|
|
225
|
-
|
|
233
|
+
|
|
226
234
|
current_list.append(value)
|
|
227
235
|
state_data[field] = current_list
|
|
228
|
-
|
|
236
|
+
|
|
229
237
|
return await self.upsert(handler_name, state_data, ttl)
|
|
230
|
-
|
|
238
|
+
|
|
231
239
|
async def get_ttl(self, key: str) -> int:
|
|
232
240
|
"""
|
|
233
241
|
Get remaining time to live for state cache.
|
|
234
|
-
|
|
242
|
+
|
|
235
243
|
Returns:
|
|
236
244
|
Remaining TTL in seconds, -1 if no expiry, -2 if doesn't exist
|
|
237
245
|
"""
|
|
238
246
|
return await storage_manager.get_ttl("states", self.tenant, self.user_id)
|
|
239
|
-
|
|
247
|
+
|
|
240
248
|
async def renew_ttl(self, key: str, ttl: int) -> bool:
|
|
241
249
|
"""
|
|
242
250
|
Renew time to live for state cache.
|
|
243
|
-
|
|
251
|
+
|
|
244
252
|
Args:
|
|
245
253
|
ttl: New time to live in seconds
|
|
246
|
-
|
|
254
|
+
|
|
247
255
|
Returns:
|
|
248
256
|
True if successful, False otherwise
|
|
249
257
|
"""
|
|
250
|
-
return await storage_manager.set_ttl("states", self.tenant, self.user_id, ttl)
|
|
258
|
+
return await storage_manager.set_ttl("states", self.tenant, self.user_id, ttl)
|
|
@@ -5,7 +5,7 @@ Provides table cache operations using JSON file storage.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
from pydantic import BaseModel
|
|
11
11
|
|
|
@@ -18,28 +18,28 @@ logger = logging.getLogger("JSONTable")
|
|
|
18
18
|
class JSONTable:
|
|
19
19
|
"""
|
|
20
20
|
JSON-based table cache handler.
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
Mirrors RedisTable functionality using file-based JSON storage.
|
|
23
23
|
Maintains the same API for seamless cache backend switching.
|
|
24
24
|
"""
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
def __init__(self, tenant: str):
|
|
27
27
|
"""
|
|
28
28
|
Initialize JSON table handler.
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
Args:
|
|
31
31
|
tenant: Tenant identifier
|
|
32
32
|
"""
|
|
33
33
|
if not tenant:
|
|
34
34
|
raise ValueError(f"Missing required parameter: tenant={tenant}")
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
self.tenant = tenant
|
|
37
37
|
self.keys = default_key_factory
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
def _key(self, table_name: str, pkid: str) -> str:
|
|
40
40
|
"""Build table key using KeyFactory (same as Redis)."""
|
|
41
41
|
return self.keys.table(self.tenant, table_name, pkid)
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
# ---- Public API matching RedisTable ----
|
|
44
44
|
async def get(
|
|
45
45
|
self,
|
|
@@ -49,18 +49,18 @@ class JSONTable:
|
|
|
49
49
|
) -> dict[str, Any] | None:
|
|
50
50
|
"""
|
|
51
51
|
Get table row data.
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
Args:
|
|
54
54
|
table_name: Table name
|
|
55
55
|
pkid: Primary key ID
|
|
56
56
|
models: Optional BaseModel class for deserialization
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
Returns:
|
|
59
59
|
Table row data or None if not found
|
|
60
60
|
"""
|
|
61
61
|
key = self._key(table_name, pkid)
|
|
62
62
|
return await storage_manager.get("tables", self.tenant, None, key, models)
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
async def upsert(
|
|
65
65
|
self,
|
|
66
66
|
table_name: str,
|
|
@@ -70,70 +70,70 @@ class JSONTable:
|
|
|
70
70
|
) -> bool:
|
|
71
71
|
"""
|
|
72
72
|
Create or update table row data.
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
Args:
|
|
75
75
|
table_name: Table name
|
|
76
76
|
pkid: Primary key ID
|
|
77
77
|
data: Data to store
|
|
78
78
|
ttl: Time to live in seconds
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
Returns:
|
|
81
81
|
True if successful, False otherwise
|
|
82
82
|
"""
|
|
83
83
|
key = self._key(table_name, pkid)
|
|
84
84
|
return await storage_manager.set("tables", self.tenant, None, key, data, ttl)
|
|
85
|
-
|
|
85
|
+
|
|
86
86
|
async def delete(self, table_name: str, pkid: str) -> int:
|
|
87
87
|
"""
|
|
88
88
|
Delete table row data.
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
Args:
|
|
91
91
|
table_name: Table name
|
|
92
92
|
pkid: Primary key ID
|
|
93
|
-
|
|
93
|
+
|
|
94
94
|
Returns:
|
|
95
95
|
1 if deleted, 0 if didn't exist
|
|
96
96
|
"""
|
|
97
97
|
key = self._key(table_name, pkid)
|
|
98
98
|
success = await storage_manager.delete("tables", self.tenant, None, key)
|
|
99
99
|
return 1 if success else 0
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
async def exists(self, table_name: str, pkid: str) -> bool:
|
|
102
102
|
"""
|
|
103
103
|
Check if table row exists.
|
|
104
|
-
|
|
104
|
+
|
|
105
105
|
Args:
|
|
106
106
|
table_name: Table name
|
|
107
107
|
pkid: Primary key ID
|
|
108
|
-
|
|
108
|
+
|
|
109
109
|
Returns:
|
|
110
110
|
True if exists, False otherwise
|
|
111
111
|
"""
|
|
112
112
|
key = self._key(table_name, pkid)
|
|
113
113
|
return await storage_manager.exists("tables", self.tenant, None, key)
|
|
114
|
-
|
|
114
|
+
|
|
115
115
|
async def get_field(self, table_name: str, pkid: str, field: str) -> Any | None:
|
|
116
116
|
"""
|
|
117
117
|
Get a specific field from table row.
|
|
118
|
-
|
|
118
|
+
|
|
119
119
|
Args:
|
|
120
120
|
table_name: Table name
|
|
121
121
|
pkid: Primary key ID
|
|
122
122
|
field: Field name
|
|
123
|
-
|
|
123
|
+
|
|
124
124
|
Returns:
|
|
125
125
|
Field value or None if not found
|
|
126
126
|
"""
|
|
127
127
|
row_data = await self.get(table_name, pkid)
|
|
128
128
|
if row_data is None:
|
|
129
129
|
return None
|
|
130
|
-
|
|
130
|
+
|
|
131
131
|
if isinstance(row_data, dict):
|
|
132
132
|
return row_data.get(field)
|
|
133
133
|
else:
|
|
134
134
|
# BaseModel instance
|
|
135
135
|
return getattr(row_data, field, None)
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
async def update_field(
|
|
138
138
|
self,
|
|
139
139
|
table_name: str,
|
|
@@ -144,27 +144,27 @@ class JSONTable:
|
|
|
144
144
|
) -> bool:
|
|
145
145
|
"""
|
|
146
146
|
Update a specific field in table row.
|
|
147
|
-
|
|
147
|
+
|
|
148
148
|
Args:
|
|
149
149
|
table_name: Table name
|
|
150
150
|
pkid: Primary key ID
|
|
151
151
|
field: Field name
|
|
152
152
|
value: New value
|
|
153
153
|
ttl: Time to live in seconds
|
|
154
|
-
|
|
154
|
+
|
|
155
155
|
Returns:
|
|
156
156
|
True if successful, False otherwise
|
|
157
157
|
"""
|
|
158
158
|
row_data = await self.get(table_name, pkid)
|
|
159
159
|
if row_data is None:
|
|
160
160
|
row_data = {}
|
|
161
|
-
|
|
161
|
+
|
|
162
162
|
if isinstance(row_data, BaseModel):
|
|
163
163
|
row_data = row_data.model_dump()
|
|
164
|
-
|
|
164
|
+
|
|
165
165
|
row_data[field] = value
|
|
166
166
|
return await self.upsert(table_name, pkid, row_data, ttl)
|
|
167
|
-
|
|
167
|
+
|
|
168
168
|
async def increment_field(
|
|
169
169
|
self,
|
|
170
170
|
table_name: str,
|
|
@@ -175,35 +175,37 @@ class JSONTable:
|
|
|
175
175
|
) -> int | None:
|
|
176
176
|
"""
|
|
177
177
|
Atomically increment an integer field in table row.
|
|
178
|
-
|
|
178
|
+
|
|
179
179
|
Args:
|
|
180
180
|
table_name: Table name
|
|
181
181
|
pkid: Primary key ID
|
|
182
182
|
field: Field name
|
|
183
183
|
increment: Amount to increment by
|
|
184
184
|
ttl: Time to live in seconds
|
|
185
|
-
|
|
185
|
+
|
|
186
186
|
Returns:
|
|
187
187
|
New value after increment or None on error
|
|
188
188
|
"""
|
|
189
189
|
row_data = await self.get(table_name, pkid)
|
|
190
190
|
if row_data is None:
|
|
191
191
|
row_data = {}
|
|
192
|
-
|
|
192
|
+
|
|
193
193
|
if isinstance(row_data, BaseModel):
|
|
194
194
|
row_data = row_data.model_dump()
|
|
195
|
-
|
|
195
|
+
|
|
196
196
|
current_value = row_data.get(field, 0)
|
|
197
|
-
if not isinstance(current_value,
|
|
198
|
-
logger.warning(
|
|
197
|
+
if not isinstance(current_value, int | float):
|
|
198
|
+
logger.warning(
|
|
199
|
+
f"Cannot increment non-numeric field '{field}': {current_value}"
|
|
200
|
+
)
|
|
199
201
|
return None
|
|
200
|
-
|
|
202
|
+
|
|
201
203
|
new_value = int(current_value) + increment
|
|
202
204
|
row_data[field] = new_value
|
|
203
|
-
|
|
205
|
+
|
|
204
206
|
success = await self.upsert(table_name, pkid, row_data, ttl)
|
|
205
207
|
return new_value if success else None
|
|
206
|
-
|
|
208
|
+
|
|
207
209
|
async def append_to_list(
|
|
208
210
|
self,
|
|
209
211
|
table_name: str,
|
|
@@ -214,50 +216,50 @@ class JSONTable:
|
|
|
214
216
|
) -> bool:
|
|
215
217
|
"""
|
|
216
218
|
Append value to a list field in table row.
|
|
217
|
-
|
|
219
|
+
|
|
218
220
|
Args:
|
|
219
221
|
table_name: Table name
|
|
220
222
|
pkid: Primary key ID
|
|
221
223
|
field: Field name containing list
|
|
222
224
|
value: Value to append
|
|
223
225
|
ttl: Time to live in seconds
|
|
224
|
-
|
|
226
|
+
|
|
225
227
|
Returns:
|
|
226
228
|
True if successful, False otherwise
|
|
227
229
|
"""
|
|
228
230
|
row_data = await self.get(table_name, pkid)
|
|
229
231
|
if row_data is None:
|
|
230
232
|
row_data = {}
|
|
231
|
-
|
|
233
|
+
|
|
232
234
|
if isinstance(row_data, BaseModel):
|
|
233
235
|
row_data = row_data.model_dump()
|
|
234
|
-
|
|
236
|
+
|
|
235
237
|
current_list = row_data.get(field, [])
|
|
236
238
|
if not isinstance(current_list, list):
|
|
237
239
|
current_list = []
|
|
238
|
-
|
|
240
|
+
|
|
239
241
|
current_list.append(value)
|
|
240
242
|
row_data[field] = current_list
|
|
241
|
-
|
|
243
|
+
|
|
242
244
|
return await self.upsert(table_name, pkid, row_data, ttl)
|
|
243
|
-
|
|
245
|
+
|
|
244
246
|
async def get_ttl(self, key: str) -> int:
|
|
245
247
|
"""
|
|
246
248
|
Get remaining time to live for table cache.
|
|
247
|
-
|
|
249
|
+
|
|
248
250
|
Returns:
|
|
249
251
|
Remaining TTL in seconds, -1 if no expiry, -2 if doesn't exist
|
|
250
252
|
"""
|
|
251
253
|
return await storage_manager.get_ttl("tables", self.tenant, None)
|
|
252
|
-
|
|
254
|
+
|
|
253
255
|
async def renew_ttl(self, key: str, ttl: int) -> bool:
|
|
254
256
|
"""
|
|
255
257
|
Renew time to live for table cache.
|
|
256
|
-
|
|
258
|
+
|
|
257
259
|
Args:
|
|
258
260
|
ttl: New time to live in seconds
|
|
259
|
-
|
|
261
|
+
|
|
260
262
|
Returns:
|
|
261
263
|
True if successful, False otherwise
|
|
262
264
|
"""
|
|
263
|
-
return await storage_manager.set_ttl("tables", self.tenant, None, ttl)
|
|
265
|
+
return await storage_manager.set_ttl("tables", self.tenant, None, ttl)
|