wappa 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wappa might be problematic. Click here for more details.
- wappa/__init__.py +85 -0
- wappa/api/__init__.py +1 -0
- wappa/api/controllers/__init__.py +10 -0
- wappa/api/controllers/webhook_controller.py +441 -0
- wappa/api/dependencies/__init__.py +15 -0
- wappa/api/dependencies/whatsapp_dependencies.py +220 -0
- wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
- wappa/api/middleware/__init__.py +7 -0
- wappa/api/middleware/error_handler.py +158 -0
- wappa/api/middleware/owner.py +99 -0
- wappa/api/middleware/request_logging.py +184 -0
- wappa/api/routes/__init__.py +6 -0
- wappa/api/routes/health.py +102 -0
- wappa/api/routes/webhooks.py +211 -0
- wappa/api/routes/whatsapp/__init__.py +15 -0
- wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
- wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
- wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
- wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
- wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
- wappa/api/routes/whatsapp_combined.py +35 -0
- wappa/cli/__init__.py +9 -0
- wappa/cli/main.py +199 -0
- wappa/core/__init__.py +6 -0
- wappa/core/config/__init__.py +5 -0
- wappa/core/config/settings.py +161 -0
- wappa/core/events/__init__.py +41 -0
- wappa/core/events/default_handlers.py +642 -0
- wappa/core/events/event_dispatcher.py +244 -0
- wappa/core/events/event_handler.py +247 -0
- wappa/core/events/webhook_factory.py +219 -0
- wappa/core/factory/__init__.py +15 -0
- wappa/core/factory/plugin.py +68 -0
- wappa/core/factory/wappa_builder.py +326 -0
- wappa/core/logging/__init__.py +5 -0
- wappa/core/logging/context.py +100 -0
- wappa/core/logging/logger.py +343 -0
- wappa/core/plugins/__init__.py +34 -0
- wappa/core/plugins/auth_plugin.py +169 -0
- wappa/core/plugins/cors_plugin.py +128 -0
- wappa/core/plugins/custom_middleware_plugin.py +182 -0
- wappa/core/plugins/database_plugin.py +235 -0
- wappa/core/plugins/rate_limit_plugin.py +183 -0
- wappa/core/plugins/redis_plugin.py +224 -0
- wappa/core/plugins/wappa_core_plugin.py +261 -0
- wappa/core/plugins/webhook_plugin.py +253 -0
- wappa/core/types.py +108 -0
- wappa/core/wappa_app.py +546 -0
- wappa/database/__init__.py +18 -0
- wappa/database/adapter.py +107 -0
- wappa/database/adapters/__init__.py +17 -0
- wappa/database/adapters/mysql_adapter.py +187 -0
- wappa/database/adapters/postgresql_adapter.py +169 -0
- wappa/database/adapters/sqlite_adapter.py +174 -0
- wappa/domain/__init__.py +28 -0
- wappa/domain/builders/__init__.py +5 -0
- wappa/domain/builders/message_builder.py +189 -0
- wappa/domain/entities/__init__.py +5 -0
- wappa/domain/enums/messenger_platform.py +123 -0
- wappa/domain/factories/__init__.py +6 -0
- wappa/domain/factories/media_factory.py +450 -0
- wappa/domain/factories/message_factory.py +497 -0
- wappa/domain/factories/messenger_factory.py +244 -0
- wappa/domain/interfaces/__init__.py +32 -0
- wappa/domain/interfaces/base_repository.py +94 -0
- wappa/domain/interfaces/cache_factory.py +85 -0
- wappa/domain/interfaces/cache_interface.py +199 -0
- wappa/domain/interfaces/expiry_repository.py +68 -0
- wappa/domain/interfaces/media_interface.py +311 -0
- wappa/domain/interfaces/messaging_interface.py +523 -0
- wappa/domain/interfaces/pubsub_repository.py +151 -0
- wappa/domain/interfaces/repository_factory.py +108 -0
- wappa/domain/interfaces/shared_state_repository.py +122 -0
- wappa/domain/interfaces/state_repository.py +123 -0
- wappa/domain/interfaces/tables_repository.py +215 -0
- wappa/domain/interfaces/user_repository.py +114 -0
- wappa/domain/interfaces/webhooks/__init__.py +1 -0
- wappa/domain/models/media_result.py +110 -0
- wappa/domain/models/platforms/__init__.py +15 -0
- wappa/domain/models/platforms/platform_config.py +104 -0
- wappa/domain/services/__init__.py +11 -0
- wappa/domain/services/tenant_credentials_service.py +56 -0
- wappa/messaging/__init__.py +7 -0
- wappa/messaging/whatsapp/__init__.py +1 -0
- wappa/messaging/whatsapp/client/__init__.py +5 -0
- wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
- wappa/messaging/whatsapp/handlers/__init__.py +13 -0
- wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
- wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
- wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
- wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
- wappa/messaging/whatsapp/messenger/__init__.py +5 -0
- wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
- wappa/messaging/whatsapp/models/__init__.py +61 -0
- wappa/messaging/whatsapp/models/basic_models.py +65 -0
- wappa/messaging/whatsapp/models/interactive_models.py +287 -0
- wappa/messaging/whatsapp/models/media_models.py +215 -0
- wappa/messaging/whatsapp/models/specialized_models.py +304 -0
- wappa/messaging/whatsapp/models/template_models.py +261 -0
- wappa/persistence/cache_factory.py +93 -0
- wappa/persistence/json/__init__.py +14 -0
- wappa/persistence/json/cache_adapters.py +271 -0
- wappa/persistence/json/handlers/__init__.py +1 -0
- wappa/persistence/json/handlers/state_handler.py +250 -0
- wappa/persistence/json/handlers/table_handler.py +263 -0
- wappa/persistence/json/handlers/user_handler.py +213 -0
- wappa/persistence/json/handlers/utils/__init__.py +1 -0
- wappa/persistence/json/handlers/utils/file_manager.py +153 -0
- wappa/persistence/json/handlers/utils/key_factory.py +11 -0
- wappa/persistence/json/handlers/utils/serialization.py +121 -0
- wappa/persistence/json/json_cache_factory.py +76 -0
- wappa/persistence/json/storage_manager.py +285 -0
- wappa/persistence/memory/__init__.py +14 -0
- wappa/persistence/memory/cache_adapters.py +271 -0
- wappa/persistence/memory/handlers/__init__.py +1 -0
- wappa/persistence/memory/handlers/state_handler.py +250 -0
- wappa/persistence/memory/handlers/table_handler.py +280 -0
- wappa/persistence/memory/handlers/user_handler.py +213 -0
- wappa/persistence/memory/handlers/utils/__init__.py +1 -0
- wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
- wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
- wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
- wappa/persistence/memory/memory_cache_factory.py +76 -0
- wappa/persistence/memory/storage_manager.py +235 -0
- wappa/persistence/redis/README.md +699 -0
- wappa/persistence/redis/__init__.py +11 -0
- wappa/persistence/redis/cache_adapters.py +285 -0
- wappa/persistence/redis/ops.py +880 -0
- wappa/persistence/redis/redis_cache_factory.py +71 -0
- wappa/persistence/redis/redis_client.py +231 -0
- wappa/persistence/redis/redis_handler/__init__.py +26 -0
- wappa/persistence/redis/redis_handler/state_handler.py +176 -0
- wappa/persistence/redis/redis_handler/table.py +158 -0
- wappa/persistence/redis/redis_handler/user.py +138 -0
- wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
- wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
- wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
- wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
- wappa/persistence/redis/redis_manager.py +189 -0
- wappa/processors/__init__.py +6 -0
- wappa/processors/base_processor.py +262 -0
- wappa/processors/factory.py +550 -0
- wappa/processors/whatsapp_processor.py +810 -0
- wappa/schemas/__init__.py +6 -0
- wappa/schemas/core/__init__.py +71 -0
- wappa/schemas/core/base_message.py +499 -0
- wappa/schemas/core/base_status.py +322 -0
- wappa/schemas/core/base_webhook.py +312 -0
- wappa/schemas/core/types.py +253 -0
- wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
- wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
- wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
- wappa/schemas/factory.py +754 -0
- wappa/schemas/webhooks/__init__.py +3 -0
- wappa/schemas/whatsapp/__init__.py +6 -0
- wappa/schemas/whatsapp/base_models.py +285 -0
- wappa/schemas/whatsapp/message_types/__init__.py +93 -0
- wappa/schemas/whatsapp/message_types/audio.py +350 -0
- wappa/schemas/whatsapp/message_types/button.py +267 -0
- wappa/schemas/whatsapp/message_types/contact.py +464 -0
- wappa/schemas/whatsapp/message_types/document.py +421 -0
- wappa/schemas/whatsapp/message_types/errors.py +195 -0
- wappa/schemas/whatsapp/message_types/image.py +424 -0
- wappa/schemas/whatsapp/message_types/interactive.py +430 -0
- wappa/schemas/whatsapp/message_types/location.py +416 -0
- wappa/schemas/whatsapp/message_types/order.py +372 -0
- wappa/schemas/whatsapp/message_types/reaction.py +271 -0
- wappa/schemas/whatsapp/message_types/sticker.py +328 -0
- wappa/schemas/whatsapp/message_types/system.py +317 -0
- wappa/schemas/whatsapp/message_types/text.py +411 -0
- wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
- wappa/schemas/whatsapp/message_types/video.py +344 -0
- wappa/schemas/whatsapp/status_models.py +479 -0
- wappa/schemas/whatsapp/validators.py +454 -0
- wappa/schemas/whatsapp/webhook_container.py +438 -0
- wappa/webhooks/__init__.py +17 -0
- wappa/webhooks/core/__init__.py +71 -0
- wappa/webhooks/core/base_message.py +499 -0
- wappa/webhooks/core/base_status.py +322 -0
- wappa/webhooks/core/base_webhook.py +312 -0
- wappa/webhooks/core/types.py +253 -0
- wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
- wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
- wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
- wappa/webhooks/factory.py +754 -0
- wappa/webhooks/whatsapp/__init__.py +6 -0
- wappa/webhooks/whatsapp/base_models.py +285 -0
- wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
- wappa/webhooks/whatsapp/message_types/audio.py +350 -0
- wappa/webhooks/whatsapp/message_types/button.py +267 -0
- wappa/webhooks/whatsapp/message_types/contact.py +464 -0
- wappa/webhooks/whatsapp/message_types/document.py +421 -0
- wappa/webhooks/whatsapp/message_types/errors.py +195 -0
- wappa/webhooks/whatsapp/message_types/image.py +424 -0
- wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
- wappa/webhooks/whatsapp/message_types/location.py +416 -0
- wappa/webhooks/whatsapp/message_types/order.py +372 -0
- wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
- wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
- wappa/webhooks/whatsapp/message_types/system.py +317 -0
- wappa/webhooks/whatsapp/message_types/text.py +411 -0
- wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
- wappa/webhooks/whatsapp/message_types/video.py +344 -0
- wappa/webhooks/whatsapp/status_models.py +479 -0
- wappa/webhooks/whatsapp/validators.py +454 -0
- wappa/webhooks/whatsapp/webhook_container.py +438 -0
- wappa-0.1.0.dist-info/METADATA +269 -0
- wappa-0.1.0.dist-info/RECORD +211 -0
- wappa-0.1.0.dist-info/WHEEL +4 -0
- wappa-0.1.0.dist-info/entry_points.txt +2 -0
- wappa-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,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)
|