wappa 0.1.9__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/app/main.py +2 -1
- wappa/cli/examples/init/app/master_event.py +5 -3
- wappa/cli/examples/json_cache_example/app/__init__.py +1 -1
- wappa/cli/examples/json_cache_example/app/main.py +56 -44
- wappa/cli/examples/json_cache_example/app/master_event.py +181 -145
- wappa/cli/examples/json_cache_example/app/models/__init__.py +1 -1
- wappa/cli/examples/json_cache_example/app/models/json_demo_models.py +32 -51
- wappa/cli/examples/json_cache_example/app/scores/__init__.py +2 -2
- wappa/cli/examples/json_cache_example/app/scores/score_base.py +52 -46
- wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +70 -62
- wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +41 -44
- wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +83 -71
- wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +73 -57
- wappa/cli/examples/json_cache_example/app/utils/__init__.py +2 -2
- wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +54 -56
- wappa/cli/examples/json_cache_example/app/utils/message_utils.py +85 -80
- wappa/cli/examples/openai_transcript/app/main.py +2 -1
- wappa/cli/examples/openai_transcript/app/master_event.py +31 -22
- wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +1 -1
- wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +37 -24
- wappa/cli/examples/redis_cache_example/app/__init__.py +1 -1
- wappa/cli/examples/redis_cache_example/app/main.py +56 -44
- wappa/cli/examples/redis_cache_example/app/master_event.py +181 -145
- wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +31 -50
- wappa/cli/examples/redis_cache_example/app/scores/__init__.py +2 -2
- wappa/cli/examples/redis_cache_example/app/scores/score_base.py +52 -46
- wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +70 -62
- wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +41 -44
- wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +83 -71
- wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +73 -57
- wappa/cli/examples/redis_cache_example/app/utils/__init__.py +2 -2
- wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +54 -56
- wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +85 -80
- wappa/cli/examples/simple_echo_example/app/__init__.py +1 -1
- wappa/cli/examples/simple_echo_example/app/main.py +41 -33
- wappa/cli/examples/simple_echo_example/app/master_event.py +78 -57
- wappa/cli/examples/wappa_full_example/app/__init__.py +1 -1
- wappa/cli/examples/wappa_full_example/app/handlers/__init__.py +1 -1
- wappa/cli/examples/wappa_full_example/app/handlers/command_handlers.py +134 -126
- wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +237 -229
- wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +170 -148
- wappa/cli/examples/wappa_full_example/app/main.py +51 -39
- wappa/cli/examples/wappa_full_example/app/master_event.py +179 -120
- wappa/cli/examples/wappa_full_example/app/models/__init__.py +1 -1
- wappa/cli/examples/wappa_full_example/app/models/state_models.py +113 -104
- wappa/cli/examples/wappa_full_example/app/models/user_models.py +92 -76
- wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +109 -83
- wappa/cli/examples/wappa_full_example/app/utils/__init__.py +1 -1
- wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +132 -113
- wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +175 -132
- wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +126 -87
- wappa/cli/main.py +9 -4
- 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.9.dist-info → wappa-0.1.10.dist-info}/METADATA +362 -8
- {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/RECORD +126 -126
- {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/WHEEL +0 -0
- {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/entry_points.txt +0 -0
- {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,9 +5,9 @@ Provides user-specific 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
|
-
from pydantic import BaseModel
|
|
10
|
+
from pydantic import BaseModel
|
|
11
11
|
|
|
12
12
|
from ..storage_manager import storage_manager
|
|
13
13
|
from .utils.key_factory import default_key_factory
|
|
@@ -18,196 +18,212 @@ logger = logging.getLogger("JSONUser")
|
|
|
18
18
|
class JSONUser:
|
|
19
19
|
"""
|
|
20
20
|
JSON-based user cache handler.
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
Mirrors RedisUser 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 user 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) -> str:
|
|
42
44
|
"""Build user key using KeyFactory (same as Redis)."""
|
|
43
45
|
return self.keys.user(self.tenant, self.user_id)
|
|
44
|
-
|
|
46
|
+
|
|
45
47
|
# ---- Public API matching RedisUser ----
|
|
46
48
|
async def get(self, models: type[BaseModel] | None = None) -> dict[str, Any] | None:
|
|
47
49
|
"""
|
|
48
50
|
Get full user data.
|
|
49
|
-
|
|
51
|
+
|
|
50
52
|
Args:
|
|
51
53
|
models: Optional BaseModel class for deserialization
|
|
52
|
-
|
|
54
|
+
|
|
53
55
|
Returns:
|
|
54
56
|
User data dictionary or BaseModel instance, None if not found
|
|
55
57
|
"""
|
|
56
58
|
key = self._key()
|
|
57
|
-
return await storage_manager.get(
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
return await storage_manager.get(
|
|
60
|
+
"users", self.tenant, self.user_id, key, models
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def upsert(
|
|
64
|
+
self, data: dict[str, Any] | BaseModel, ttl: int | None = None
|
|
65
|
+
) -> bool:
|
|
60
66
|
"""
|
|
61
67
|
Create or update user data.
|
|
62
|
-
|
|
68
|
+
|
|
63
69
|
Args:
|
|
64
70
|
data: User data to store
|
|
65
71
|
ttl: Time to live in seconds
|
|
66
|
-
|
|
72
|
+
|
|
67
73
|
Returns:
|
|
68
74
|
True if successful, False otherwise
|
|
69
75
|
"""
|
|
70
76
|
key = self._key()
|
|
71
|
-
return await storage_manager.set(
|
|
72
|
-
|
|
77
|
+
return await storage_manager.set(
|
|
78
|
+
"users", self.tenant, self.user_id, key, data, ttl
|
|
79
|
+
)
|
|
80
|
+
|
|
73
81
|
async def delete(self) -> int:
|
|
74
82
|
"""
|
|
75
83
|
Delete user data.
|
|
76
|
-
|
|
84
|
+
|
|
77
85
|
Returns:
|
|
78
86
|
1 if deleted, 0 if didn't exist
|
|
79
87
|
"""
|
|
80
88
|
key = self._key()
|
|
81
89
|
success = await storage_manager.delete("users", self.tenant, self.user_id, key)
|
|
82
90
|
return 1 if success else 0
|
|
83
|
-
|
|
91
|
+
|
|
84
92
|
async def exists(self) -> bool:
|
|
85
93
|
"""
|
|
86
94
|
Check if user data exists.
|
|
87
|
-
|
|
95
|
+
|
|
88
96
|
Returns:
|
|
89
97
|
True if exists, False otherwise
|
|
90
98
|
"""
|
|
91
99
|
key = self._key()
|
|
92
100
|
return await storage_manager.exists("users", self.tenant, self.user_id, key)
|
|
93
|
-
|
|
101
|
+
|
|
94
102
|
async def get_field(self, field: str) -> Any | None:
|
|
95
103
|
"""
|
|
96
104
|
Get a specific field from user data.
|
|
97
|
-
|
|
105
|
+
|
|
98
106
|
Args:
|
|
99
107
|
field: Field name
|
|
100
|
-
|
|
108
|
+
|
|
101
109
|
Returns:
|
|
102
110
|
Field value or None if not found
|
|
103
111
|
"""
|
|
104
112
|
user_data = await self.get()
|
|
105
113
|
if user_data is None:
|
|
106
114
|
return None
|
|
107
|
-
|
|
115
|
+
|
|
108
116
|
if isinstance(user_data, dict):
|
|
109
117
|
return user_data.get(field)
|
|
110
118
|
else:
|
|
111
119
|
# BaseModel instance
|
|
112
120
|
return getattr(user_data, field, None)
|
|
113
|
-
|
|
114
|
-
async def update_field(
|
|
121
|
+
|
|
122
|
+
async def update_field(
|
|
123
|
+
self, field: str, value: Any, ttl: int | None = None
|
|
124
|
+
) -> bool:
|
|
115
125
|
"""
|
|
116
126
|
Update a specific field in user data.
|
|
117
|
-
|
|
127
|
+
|
|
118
128
|
Args:
|
|
119
129
|
field: Field name
|
|
120
130
|
value: New value
|
|
121
131
|
ttl: Time to live in seconds
|
|
122
|
-
|
|
132
|
+
|
|
123
133
|
Returns:
|
|
124
134
|
True if successful, False otherwise
|
|
125
135
|
"""
|
|
126
136
|
user_data = await self.get()
|
|
127
137
|
if user_data is None:
|
|
128
138
|
user_data = {}
|
|
129
|
-
|
|
139
|
+
|
|
130
140
|
if isinstance(user_data, BaseModel):
|
|
131
141
|
user_data = user_data.model_dump()
|
|
132
|
-
|
|
142
|
+
|
|
133
143
|
user_data[field] = value
|
|
134
144
|
return await self.upsert(user_data, ttl)
|
|
135
|
-
|
|
136
|
-
async def increment_field(
|
|
145
|
+
|
|
146
|
+
async def increment_field(
|
|
147
|
+
self, field: str, increment: int = 1, ttl: int | None = None
|
|
148
|
+
) -> int | None:
|
|
137
149
|
"""
|
|
138
150
|
Atomically increment an integer field.
|
|
139
|
-
|
|
151
|
+
|
|
140
152
|
Args:
|
|
141
153
|
field: Field name
|
|
142
154
|
increment: Amount to increment by
|
|
143
155
|
ttl: Time to live in seconds
|
|
144
|
-
|
|
156
|
+
|
|
145
157
|
Returns:
|
|
146
158
|
New value after increment or None on error
|
|
147
159
|
"""
|
|
148
160
|
user_data = await self.get()
|
|
149
161
|
if user_data is None:
|
|
150
162
|
user_data = {}
|
|
151
|
-
|
|
163
|
+
|
|
152
164
|
if isinstance(user_data, BaseModel):
|
|
153
165
|
user_data = user_data.model_dump()
|
|
154
|
-
|
|
166
|
+
|
|
155
167
|
current_value = user_data.get(field, 0)
|
|
156
|
-
if not isinstance(current_value,
|
|
157
|
-
logger.warning(
|
|
168
|
+
if not isinstance(current_value, int | float):
|
|
169
|
+
logger.warning(
|
|
170
|
+
f"Cannot increment non-numeric field '{field}': {current_value}"
|
|
171
|
+
)
|
|
158
172
|
return None
|
|
159
|
-
|
|
173
|
+
|
|
160
174
|
new_value = int(current_value) + increment
|
|
161
175
|
user_data[field] = new_value
|
|
162
|
-
|
|
176
|
+
|
|
163
177
|
success = await self.upsert(user_data, ttl)
|
|
164
178
|
return new_value if success else None
|
|
165
|
-
|
|
166
|
-
async def append_to_list(
|
|
179
|
+
|
|
180
|
+
async def append_to_list(
|
|
181
|
+
self, field: str, value: Any, ttl: int | None = None
|
|
182
|
+
) -> bool:
|
|
167
183
|
"""
|
|
168
184
|
Append value to a list field.
|
|
169
|
-
|
|
185
|
+
|
|
170
186
|
Args:
|
|
171
187
|
field: Field name containing list
|
|
172
188
|
value: Value to append
|
|
173
189
|
ttl: Time to live in seconds
|
|
174
|
-
|
|
190
|
+
|
|
175
191
|
Returns:
|
|
176
192
|
True if successful, False otherwise
|
|
177
193
|
"""
|
|
178
194
|
user_data = await self.get()
|
|
179
195
|
if user_data is None:
|
|
180
196
|
user_data = {}
|
|
181
|
-
|
|
197
|
+
|
|
182
198
|
if isinstance(user_data, BaseModel):
|
|
183
199
|
user_data = user_data.model_dump()
|
|
184
|
-
|
|
200
|
+
|
|
185
201
|
current_list = user_data.get(field, [])
|
|
186
202
|
if not isinstance(current_list, list):
|
|
187
203
|
current_list = []
|
|
188
|
-
|
|
204
|
+
|
|
189
205
|
current_list.append(value)
|
|
190
206
|
user_data[field] = current_list
|
|
191
|
-
|
|
207
|
+
|
|
192
208
|
return await self.upsert(user_data, ttl)
|
|
193
|
-
|
|
209
|
+
|
|
194
210
|
async def get_ttl(self, key: str) -> int:
|
|
195
211
|
"""
|
|
196
212
|
Get remaining time to live.
|
|
197
|
-
|
|
213
|
+
|
|
198
214
|
Returns:
|
|
199
215
|
Remaining TTL in seconds, -1 if no expiry, -2 if doesn't exist
|
|
200
216
|
"""
|
|
201
217
|
return await storage_manager.get_ttl("users", self.tenant, self.user_id)
|
|
202
|
-
|
|
218
|
+
|
|
203
219
|
async def renew_ttl(self, key: str, ttl: int) -> bool:
|
|
204
220
|
"""
|
|
205
221
|
Renew time to live.
|
|
206
|
-
|
|
222
|
+
|
|
207
223
|
Args:
|
|
208
224
|
ttl: New time to live in seconds
|
|
209
|
-
|
|
225
|
+
|
|
210
226
|
Returns:
|
|
211
227
|
True if successful, False otherwise
|
|
212
228
|
"""
|
|
213
|
-
return await storage_manager.set_ttl("users", self.tenant, self.user_id, ttl)
|
|
229
|
+
return await storage_manager.set_ttl("users", self.tenant, self.user_id, ttl)
|
|
@@ -7,9 +7,8 @@ Handles cache directory creation, file I/O, and project root detection.
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import json
|
|
9
9
|
import logging
|
|
10
|
-
import os
|
|
11
10
|
from pathlib import Path
|
|
12
|
-
from typing import Any
|
|
11
|
+
from typing import Any
|
|
13
12
|
|
|
14
13
|
from .serialization import from_json_string, to_json_string
|
|
15
14
|
|
|
@@ -18,76 +17,80 @@ logger = logging.getLogger("JSONFileManager")
|
|
|
18
17
|
|
|
19
18
|
class FileManager:
|
|
20
19
|
"""Manages file operations for JSON cache."""
|
|
21
|
-
|
|
20
|
+
|
|
22
21
|
def __init__(self):
|
|
23
|
-
self._cache_root:
|
|
24
|
-
self._file_locks:
|
|
25
|
-
|
|
22
|
+
self._cache_root: Path | None = None
|
|
23
|
+
self._file_locks: dict[str, asyncio.Lock] = {}
|
|
24
|
+
|
|
26
25
|
def _get_file_lock(self, file_path: str) -> asyncio.Lock:
|
|
27
26
|
"""Get or create a lock for a specific file path."""
|
|
28
27
|
if file_path not in self._file_locks:
|
|
29
28
|
self._file_locks[file_path] = asyncio.Lock()
|
|
30
29
|
return self._file_locks[file_path]
|
|
31
|
-
|
|
30
|
+
|
|
32
31
|
def get_cache_root(self) -> Path:
|
|
33
32
|
"""Get or detect the cache root directory."""
|
|
34
33
|
if self._cache_root is None:
|
|
35
34
|
self._cache_root = self._detect_project_root()
|
|
36
35
|
return self._cache_root
|
|
37
|
-
|
|
36
|
+
|
|
38
37
|
def _detect_project_root(self) -> Path:
|
|
39
38
|
"""
|
|
40
39
|
Detect project root by looking for main.py with Wappa.run().
|
|
41
|
-
|
|
40
|
+
|
|
42
41
|
Searches from current working directory upwards.
|
|
43
42
|
Falls back to current directory if not found.
|
|
44
43
|
"""
|
|
45
44
|
current_dir = Path.cwd()
|
|
46
|
-
|
|
45
|
+
|
|
47
46
|
# Search upwards for main.py containing Wappa.run()
|
|
48
47
|
for directory in [current_dir] + list(current_dir.parents):
|
|
49
48
|
main_py = directory / "main.py"
|
|
50
49
|
if main_py.exists():
|
|
51
50
|
try:
|
|
52
|
-
content = main_py.read_text(encoding=
|
|
53
|
-
if "Wappa" in content and (
|
|
51
|
+
content = main_py.read_text(encoding="utf-8")
|
|
52
|
+
if "Wappa" in content and (
|
|
53
|
+
".run()" in content or "app.run()" in content
|
|
54
|
+
):
|
|
54
55
|
cache_dir = directory / "cache"
|
|
55
56
|
logger.info(f"Detected project root: {directory}")
|
|
56
57
|
return cache_dir
|
|
57
|
-
except (
|
|
58
|
+
except (OSError, UnicodeDecodeError):
|
|
58
59
|
continue
|
|
59
|
-
|
|
60
|
+
|
|
60
61
|
# Fallback to current directory + cache
|
|
61
62
|
fallback_cache = current_dir / "cache"
|
|
62
63
|
logger.info(f"Project root not detected, using fallback: {fallback_cache}")
|
|
63
64
|
return fallback_cache
|
|
64
|
-
|
|
65
|
+
|
|
65
66
|
def ensure_cache_directories(self) -> None:
|
|
66
67
|
"""Create cache directory structure if it doesn't exist."""
|
|
67
68
|
cache_root = self.get_cache_root()
|
|
68
69
|
cache_root.mkdir(exist_ok=True)
|
|
69
|
-
|
|
70
|
+
|
|
70
71
|
# Create subdirectories
|
|
71
72
|
(cache_root / "users").mkdir(exist_ok=True)
|
|
72
73
|
(cache_root / "tables").mkdir(exist_ok=True)
|
|
73
74
|
(cache_root / "states").mkdir(exist_ok=True)
|
|
74
|
-
|
|
75
|
+
|
|
75
76
|
logger.debug(f"Cache directories ensured at: {cache_root}")
|
|
76
|
-
|
|
77
|
-
def get_cache_file_path(
|
|
77
|
+
|
|
78
|
+
def get_cache_file_path(
|
|
79
|
+
self, cache_type: str, tenant_id: str, user_id: str = None
|
|
80
|
+
) -> Path:
|
|
78
81
|
"""
|
|
79
82
|
Get the file path for a cache file.
|
|
80
|
-
|
|
83
|
+
|
|
81
84
|
Args:
|
|
82
85
|
cache_type: "users", "tables", or "states"
|
|
83
86
|
tenant_id: Tenant identifier
|
|
84
87
|
user_id: User identifier (required for users and states)
|
|
85
|
-
|
|
88
|
+
|
|
86
89
|
Returns:
|
|
87
90
|
Path to cache file
|
|
88
91
|
"""
|
|
89
92
|
cache_root = self.get_cache_root()
|
|
90
|
-
|
|
93
|
+
|
|
91
94
|
if cache_type == "users":
|
|
92
95
|
if not user_id:
|
|
93
96
|
raise ValueError("user_id is required for users cache")
|
|
@@ -100,39 +103,39 @@ class FileManager:
|
|
|
100
103
|
return cache_root / "states" / f"{tenant_id}_{user_id}_state.json"
|
|
101
104
|
else:
|
|
102
105
|
raise ValueError(f"Invalid cache_type: {cache_type}")
|
|
103
|
-
|
|
104
|
-
async def read_file(self, file_path: Path) ->
|
|
106
|
+
|
|
107
|
+
async def read_file(self, file_path: Path) -> dict[str, Any]:
|
|
105
108
|
"""Read and parse JSON file with file locking."""
|
|
106
109
|
async with self._get_file_lock(str(file_path)):
|
|
107
110
|
if not file_path.exists():
|
|
108
111
|
return {}
|
|
109
|
-
|
|
112
|
+
|
|
110
113
|
try:
|
|
111
|
-
content = await asyncio.to_thread(file_path.read_text, encoding=
|
|
114
|
+
content = await asyncio.to_thread(file_path.read_text, encoding="utf-8")
|
|
112
115
|
return from_json_string(content)
|
|
113
|
-
except (
|
|
116
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
114
117
|
logger.error(f"Failed to read file {file_path}: {e}")
|
|
115
118
|
return {}
|
|
116
|
-
|
|
117
|
-
async def write_file(self, file_path: Path, data:
|
|
119
|
+
|
|
120
|
+
async def write_file(self, file_path: Path, data: dict[str, Any]) -> bool:
|
|
118
121
|
"""Write data to JSON file with file locking."""
|
|
119
122
|
async with self._get_file_lock(str(file_path)):
|
|
120
123
|
try:
|
|
121
124
|
# Ensure parent directory exists
|
|
122
125
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
-
|
|
126
|
+
|
|
124
127
|
# Write to temporary file first, then rename (atomic operation)
|
|
125
|
-
temp_file = file_path.with_suffix(file_path.suffix +
|
|
128
|
+
temp_file = file_path.with_suffix(file_path.suffix + ".tmp")
|
|
126
129
|
content = to_json_string(data)
|
|
127
|
-
|
|
128
|
-
await asyncio.to_thread(temp_file.write_text, content, encoding=
|
|
130
|
+
|
|
131
|
+
await asyncio.to_thread(temp_file.write_text, content, encoding="utf-8")
|
|
129
132
|
await asyncio.to_thread(temp_file.replace, file_path)
|
|
130
|
-
|
|
133
|
+
|
|
131
134
|
return True
|
|
132
|
-
except
|
|
135
|
+
except OSError as e:
|
|
133
136
|
logger.error(f"Failed to write file {file_path}: {e}")
|
|
134
137
|
return False
|
|
135
|
-
|
|
138
|
+
|
|
136
139
|
async def delete_file(self, file_path: Path) -> bool:
|
|
137
140
|
"""Delete file with file locking."""
|
|
138
141
|
async with self._get_file_lock(str(file_path)):
|
|
@@ -140,14 +143,14 @@ class FileManager:
|
|
|
140
143
|
if file_path.exists():
|
|
141
144
|
await asyncio.to_thread(file_path.unlink)
|
|
142
145
|
return True
|
|
143
|
-
except
|
|
146
|
+
except OSError as e:
|
|
144
147
|
logger.error(f"Failed to delete file {file_path}: {e}")
|
|
145
148
|
return False
|
|
146
|
-
|
|
149
|
+
|
|
147
150
|
async def file_exists(self, file_path: Path) -> bool:
|
|
148
151
|
"""Check if file exists."""
|
|
149
152
|
return await asyncio.to_thread(file_path.exists)
|
|
150
153
|
|
|
151
154
|
|
|
152
155
|
# Global file manager instance
|
|
153
|
-
file_manager = FileManager()
|
|
156
|
+
file_manager = FileManager()
|
|
@@ -8,4 +8,4 @@ across all cache implementations.
|
|
|
8
8
|
from ....redis.redis_handler.utils.key_factory import KeyFactory, default_key_factory
|
|
9
9
|
|
|
10
10
|
# Export the same key factory used by Redis for consistency
|
|
11
|
-
__all__ = ["KeyFactory", "default_key_factory"]
|
|
11
|
+
__all__ = ["KeyFactory", "default_key_factory"]
|
|
@@ -60,30 +60,32 @@ def deserialize_from_json(data: Any, model: type[BaseModel] | None = None) -> An
|
|
|
60
60
|
"""Deserialize data from JSON storage."""
|
|
61
61
|
if data is None:
|
|
62
62
|
return None
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
# Convert datetime strings back to datetime objects
|
|
65
65
|
data = _convert_iso_strings_to_datetime(data)
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
if model is not None:
|
|
68
68
|
return model.model_validate(data)
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
return data
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
def create_cache_file_data(
|
|
73
|
+
def create_cache_file_data(
|
|
74
|
+
data: dict[str, Any], ttl: int | None = None
|
|
75
|
+
) -> dict[str, Any]:
|
|
74
76
|
"""Create JSON cache file structure with metadata."""
|
|
75
77
|
now = datetime.now()
|
|
76
78
|
expires_at = None
|
|
77
79
|
if ttl:
|
|
78
80
|
expires_at = datetime.fromtimestamp(now.timestamp() + ttl)
|
|
79
|
-
|
|
81
|
+
|
|
80
82
|
return {
|
|
81
83
|
"_metadata": {
|
|
82
84
|
"created_at": now.isoformat(),
|
|
83
85
|
"expires_at": expires_at.isoformat() if expires_at else None,
|
|
84
|
-
"version": "1.0"
|
|
86
|
+
"version": "1.0",
|
|
85
87
|
},
|
|
86
|
-
"data": serialize_for_json(data)
|
|
88
|
+
"data": serialize_for_json(data),
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
|
|
@@ -91,10 +93,10 @@ def extract_cache_file_data(file_data: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
91
93
|
"""Extract data from JSON cache file, checking expiration."""
|
|
92
94
|
if not isinstance(file_data, dict):
|
|
93
95
|
return None
|
|
94
|
-
|
|
96
|
+
|
|
95
97
|
metadata = file_data.get("_metadata", {})
|
|
96
98
|
expires_at_str = metadata.get("expires_at")
|
|
97
|
-
|
|
99
|
+
|
|
98
100
|
# Check expiration
|
|
99
101
|
if expires_at_str:
|
|
100
102
|
try:
|
|
@@ -103,7 +105,7 @@ def extract_cache_file_data(file_data: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
103
105
|
return None # Expired
|
|
104
106
|
except ValueError:
|
|
105
107
|
logger.warning(f"Invalid expires_at format: {expires_at_str}")
|
|
106
|
-
|
|
108
|
+
|
|
107
109
|
return file_data.get("data", {})
|
|
108
110
|
|
|
109
111
|
|
|
@@ -118,4 +120,4 @@ def from_json_string(json_str: str) -> Any:
|
|
|
118
120
|
return json.loads(json_str)
|
|
119
121
|
except json.JSONDecodeError as e:
|
|
120
122
|
logger.error(f"Failed to parse JSON: {e}")
|
|
121
|
-
raise
|
|
123
|
+
raise
|
|
@@ -20,7 +20,7 @@ class JSONCacheFactory(ICacheFactory):
|
|
|
20
20
|
|
|
21
21
|
Uses file-based JSON storage with proper file management:
|
|
22
22
|
- State cache: Uses states subdirectory
|
|
23
|
-
- User cache: Uses users subdirectory
|
|
23
|
+
- User cache: Uses users subdirectory
|
|
24
24
|
- Table cache: Uses tables subdirectory
|
|
25
25
|
|
|
26
26
|
All instances implement the ICache interface through adapters.
|
|
@@ -45,9 +45,7 @@ class JSONCacheFactory(ICacheFactory):
|
|
|
45
45
|
Returns:
|
|
46
46
|
ICache adapter wrapping JSONStateHandler
|
|
47
47
|
"""
|
|
48
|
-
return JSONStateCacheAdapter(
|
|
49
|
-
tenant_id=self.tenant_id, user_id=self.user_id
|
|
50
|
-
)
|
|
48
|
+
return JSONStateCacheAdapter(tenant_id=self.tenant_id, user_id=self.user_id)
|
|
51
49
|
|
|
52
50
|
def create_user_cache(self) -> ICache:
|
|
53
51
|
"""
|
|
@@ -59,9 +57,7 @@ class JSONCacheFactory(ICacheFactory):
|
|
|
59
57
|
Returns:
|
|
60
58
|
ICache adapter wrapping JSONUser
|
|
61
59
|
"""
|
|
62
|
-
return JSONUserCacheAdapter(
|
|
63
|
-
tenant_id=self.tenant_id, user_id=self.user_id
|
|
64
|
-
)
|
|
60
|
+
return JSONUserCacheAdapter(tenant_id=self.tenant_id, user_id=self.user_id)
|
|
65
61
|
|
|
66
62
|
def create_table_cache(self) -> ICache:
|
|
67
63
|
"""
|
|
@@ -73,4 +69,4 @@ class JSONCacheFactory(ICacheFactory):
|
|
|
73
69
|
Returns:
|
|
74
70
|
ICache adapter wrapping JSONTable
|
|
75
71
|
"""
|
|
76
|
-
return JSONTableCacheAdapter(tenant_id=self.tenant_id)
|
|
72
|
+
return JSONTableCacheAdapter(tenant_id=self.tenant_id)
|