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,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JSON serialization utilities with BaseModel support.
|
|
3
|
+
|
|
4
|
+
Provides JSON-based serialization compatible with Redis patterns
|
|
5
|
+
while optimizing for file storage.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("JSONSerde")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _datetime_handler(obj: Any) -> str:
|
|
19
|
+
"""Handle datetime objects during JSON serialization."""
|
|
20
|
+
if isinstance(obj, datetime):
|
|
21
|
+
return obj.isoformat()
|
|
22
|
+
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _convert_iso_strings_to_datetime(obj: Any) -> Any:
|
|
26
|
+
"""Recursively convert ISO datetime strings back to datetime objects."""
|
|
27
|
+
if isinstance(obj, str):
|
|
28
|
+
# Try to parse as ISO datetime
|
|
29
|
+
try:
|
|
30
|
+
# Check if it looks like an ISO datetime (basic heuristic)
|
|
31
|
+
if "T" in obj and len(obj) >= 19: # YYYY-MM-DDTHH:MM:SS minimum
|
|
32
|
+
return datetime.fromisoformat(obj.replace("Z", "+00:00"))
|
|
33
|
+
except (ValueError, AttributeError):
|
|
34
|
+
pass
|
|
35
|
+
return obj
|
|
36
|
+
elif isinstance(obj, dict):
|
|
37
|
+
return {k: _convert_iso_strings_to_datetime(v) for k, v in obj.items()}
|
|
38
|
+
elif isinstance(obj, list):
|
|
39
|
+
return [_convert_iso_strings_to_datetime(item) for item in obj]
|
|
40
|
+
else:
|
|
41
|
+
return obj
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def serialize_for_json(obj: Any) -> Any:
|
|
45
|
+
"""Serialize Python object for JSON storage."""
|
|
46
|
+
if obj is None:
|
|
47
|
+
return None
|
|
48
|
+
if isinstance(obj, BaseModel):
|
|
49
|
+
return obj.model_dump()
|
|
50
|
+
if isinstance(obj, datetime):
|
|
51
|
+
return obj.isoformat()
|
|
52
|
+
if isinstance(obj, dict):
|
|
53
|
+
return {k: serialize_for_json(v) for k, v in obj.items()}
|
|
54
|
+
if isinstance(obj, list):
|
|
55
|
+
return [serialize_for_json(item) for item in obj]
|
|
56
|
+
return obj
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def deserialize_from_json(data: Any, model: type[BaseModel] | None = None) -> Any:
|
|
60
|
+
"""Deserialize data from JSON storage."""
|
|
61
|
+
if data is None:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
# Convert datetime strings back to datetime objects
|
|
65
|
+
data = _convert_iso_strings_to_datetime(data)
|
|
66
|
+
|
|
67
|
+
if model is not None:
|
|
68
|
+
return model.model_validate(data)
|
|
69
|
+
|
|
70
|
+
return data
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def create_cache_file_data(data: dict[str, Any], ttl: int | None = None) -> dict[str, Any]:
|
|
74
|
+
"""Create JSON cache file structure with metadata."""
|
|
75
|
+
now = datetime.now()
|
|
76
|
+
expires_at = None
|
|
77
|
+
if ttl:
|
|
78
|
+
expires_at = datetime.fromtimestamp(now.timestamp() + ttl)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
"_metadata": {
|
|
82
|
+
"created_at": now.isoformat(),
|
|
83
|
+
"expires_at": expires_at.isoformat() if expires_at else None,
|
|
84
|
+
"version": "1.0"
|
|
85
|
+
},
|
|
86
|
+
"data": serialize_for_json(data)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def extract_cache_file_data(file_data: dict[str, Any]) -> dict[str, Any] | None:
|
|
91
|
+
"""Extract data from JSON cache file, checking expiration."""
|
|
92
|
+
if not isinstance(file_data, dict):
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
metadata = file_data.get("_metadata", {})
|
|
96
|
+
expires_at_str = metadata.get("expires_at")
|
|
97
|
+
|
|
98
|
+
# Check expiration
|
|
99
|
+
if expires_at_str:
|
|
100
|
+
try:
|
|
101
|
+
expires_at = datetime.fromisoformat(expires_at_str)
|
|
102
|
+
if datetime.now() > expires_at:
|
|
103
|
+
return None # Expired
|
|
104
|
+
except ValueError:
|
|
105
|
+
logger.warning(f"Invalid expires_at format: {expires_at_str}")
|
|
106
|
+
|
|
107
|
+
return file_data.get("data", {})
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def to_json_string(data: Any) -> str:
|
|
111
|
+
"""Convert data to JSON string."""
|
|
112
|
+
return json.dumps(data, ensure_ascii=False, indent=2, default=_datetime_handler)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def from_json_string(json_str: str) -> Any:
|
|
116
|
+
"""Convert JSON string to data."""
|
|
117
|
+
try:
|
|
118
|
+
return json.loads(json_str)
|
|
119
|
+
except json.JSONDecodeError as e:
|
|
120
|
+
logger.error(f"Failed to parse JSON: {e}")
|
|
121
|
+
raise
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JSON cache factory implementation for Wappa framework.
|
|
3
|
+
|
|
4
|
+
Creates JSON-backed cache instances using file-based storage
|
|
5
|
+
with ICache adapters for uniform interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ..domain.interfaces.cache_factory import ICacheFactory
|
|
9
|
+
from ..domain.interfaces.cache_interface import ICache
|
|
10
|
+
from .cache_adapters import (
|
|
11
|
+
JSONStateCacheAdapter,
|
|
12
|
+
JSONTableCacheAdapter,
|
|
13
|
+
JSONUserCacheAdapter,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class JSONCacheFactory(ICacheFactory):
|
|
18
|
+
"""
|
|
19
|
+
Factory for creating JSON-backed cache instances.
|
|
20
|
+
|
|
21
|
+
Uses file-based JSON storage with proper file management:
|
|
22
|
+
- State cache: Uses states subdirectory
|
|
23
|
+
- User cache: Uses users subdirectory
|
|
24
|
+
- Table cache: Uses tables subdirectory
|
|
25
|
+
|
|
26
|
+
All instances implement the ICache interface through adapters.
|
|
27
|
+
|
|
28
|
+
Context (tenant_id, user_id) is injected at construction time, eliminating
|
|
29
|
+
manual parameter passing.
|
|
30
|
+
|
|
31
|
+
Cache files are automatically created in {project_root}/cache/ directory structure.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, tenant_id: str, user_id: str):
|
|
35
|
+
"""Initialize JSON cache factory with context injection."""
|
|
36
|
+
super().__init__(tenant_id, user_id)
|
|
37
|
+
|
|
38
|
+
def create_state_cache(self) -> ICache:
|
|
39
|
+
"""
|
|
40
|
+
Create JSON state cache instance.
|
|
41
|
+
|
|
42
|
+
Uses context (tenant_id, user_id) injected at construction time.
|
|
43
|
+
Stores data in {project_root}/cache/states/ directory.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
ICache adapter wrapping JSONStateHandler
|
|
47
|
+
"""
|
|
48
|
+
return JSONStateCacheAdapter(
|
|
49
|
+
tenant_id=self.tenant_id, user_id=self.user_id
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def create_user_cache(self) -> ICache:
|
|
53
|
+
"""
|
|
54
|
+
Create JSON user cache instance.
|
|
55
|
+
|
|
56
|
+
Uses context (tenant_id, user_id) injected at construction time.
|
|
57
|
+
Stores data in {project_root}/cache/users/ directory.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
ICache adapter wrapping JSONUser
|
|
61
|
+
"""
|
|
62
|
+
return JSONUserCacheAdapter(
|
|
63
|
+
tenant_id=self.tenant_id, user_id=self.user_id
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def create_table_cache(self) -> ICache:
|
|
67
|
+
"""
|
|
68
|
+
Create JSON table cache instance.
|
|
69
|
+
|
|
70
|
+
Uses context (tenant_id) injected at construction time.
|
|
71
|
+
Stores data in {project_root}/cache/tables/ directory.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
ICache adapter wrapping JSONTable
|
|
75
|
+
"""
|
|
76
|
+
return JSONTableCacheAdapter(tenant_id=self.tenant_id)
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JSON storage manager for coordinating cache operations.
|
|
3
|
+
|
|
4
|
+
Provides high-level interface for JSON cache operations with TTL support,
|
|
5
|
+
BaseModel serialization, and atomic file operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from .handlers.utils.file_manager import file_manager
|
|
15
|
+
from .handlers.utils.serialization import (
|
|
16
|
+
create_cache_file_data,
|
|
17
|
+
deserialize_from_json,
|
|
18
|
+
extract_cache_file_data,
|
|
19
|
+
serialize_for_json,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("JSONStorageManager")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class JSONStorageManager:
|
|
26
|
+
"""High-level JSON storage operations manager."""
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
# Ensure cache directories exist on initialization
|
|
30
|
+
file_manager.ensure_cache_directories()
|
|
31
|
+
|
|
32
|
+
async def get(
|
|
33
|
+
self,
|
|
34
|
+
cache_type: str,
|
|
35
|
+
tenant_id: str,
|
|
36
|
+
user_id: Optional[str],
|
|
37
|
+
key: str,
|
|
38
|
+
model: type[BaseModel] | None = None
|
|
39
|
+
) -> Any:
|
|
40
|
+
"""
|
|
41
|
+
Get value from JSON cache.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
cache_type: "users", "tables", or "states"
|
|
45
|
+
tenant_id: Tenant identifier
|
|
46
|
+
user_id: User identifier (required for users/states)
|
|
47
|
+
key: Cache key
|
|
48
|
+
model: Optional BaseModel for deserialization
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Cached value or None if not found/expired
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
file_path = file_manager.get_cache_file_path(cache_type, tenant_id, user_id)
|
|
55
|
+
file_data = await file_manager.read_file(file_path)
|
|
56
|
+
|
|
57
|
+
if not file_data:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
# Extract data and check expiration
|
|
61
|
+
cache_data = extract_cache_file_data(file_data)
|
|
62
|
+
if cache_data is None:
|
|
63
|
+
# Expired - delete the file
|
|
64
|
+
await file_manager.delete_file(file_path)
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
# Get specific key data
|
|
68
|
+
if key not in cache_data:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
value_data = cache_data[key]
|
|
72
|
+
return deserialize_from_json(value_data, model)
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error(f"Failed to get key '{key}' from {cache_type} cache: {e}")
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
async def set(
|
|
79
|
+
self,
|
|
80
|
+
cache_type: str,
|
|
81
|
+
tenant_id: str,
|
|
82
|
+
user_id: Optional[str],
|
|
83
|
+
key: str,
|
|
84
|
+
value: Any,
|
|
85
|
+
ttl: Optional[int] = None
|
|
86
|
+
) -> bool:
|
|
87
|
+
"""
|
|
88
|
+
Set value in JSON cache.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
cache_type: "users", "tables", or "states"
|
|
92
|
+
tenant_id: Tenant identifier
|
|
93
|
+
user_id: User identifier (required for users/states)
|
|
94
|
+
key: Cache key
|
|
95
|
+
value: Value to cache
|
|
96
|
+
ttl: Time to live in seconds
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
True if successful, False otherwise
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
file_path = file_manager.get_cache_file_path(cache_type, tenant_id, user_id)
|
|
103
|
+
|
|
104
|
+
# Read existing data
|
|
105
|
+
file_data = await file_manager.read_file(file_path)
|
|
106
|
+
cache_data = extract_cache_file_data(file_data) if file_data else {}
|
|
107
|
+
if cache_data is None:
|
|
108
|
+
cache_data = {}
|
|
109
|
+
|
|
110
|
+
# Update key
|
|
111
|
+
cache_data[key] = serialize_for_json(value)
|
|
112
|
+
|
|
113
|
+
# Create new file data with TTL
|
|
114
|
+
new_file_data = create_cache_file_data(cache_data, ttl)
|
|
115
|
+
|
|
116
|
+
# Write file
|
|
117
|
+
return await file_manager.write_file(file_path, new_file_data)
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.error(f"Failed to set key '{key}' in {cache_type} cache: {e}")
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
async def delete(
|
|
124
|
+
self,
|
|
125
|
+
cache_type: str,
|
|
126
|
+
tenant_id: str,
|
|
127
|
+
user_id: Optional[str],
|
|
128
|
+
key: str
|
|
129
|
+
) -> bool:
|
|
130
|
+
"""
|
|
131
|
+
Delete key from JSON cache.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
cache_type: "users", "tables", or "states"
|
|
135
|
+
tenant_id: Tenant identifier
|
|
136
|
+
user_id: User identifier (required for users/states)
|
|
137
|
+
key: Cache key to delete
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True if deleted or didn't exist, False on error
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
file_path = file_manager.get_cache_file_path(cache_type, tenant_id, user_id)
|
|
144
|
+
|
|
145
|
+
# Read existing data
|
|
146
|
+
file_data = await file_manager.read_file(file_path)
|
|
147
|
+
if not file_data:
|
|
148
|
+
return True # Already doesn't exist
|
|
149
|
+
|
|
150
|
+
cache_data = extract_cache_file_data(file_data)
|
|
151
|
+
if cache_data is None or key not in cache_data:
|
|
152
|
+
return True # Already doesn't exist
|
|
153
|
+
|
|
154
|
+
# Remove key
|
|
155
|
+
del cache_data[key]
|
|
156
|
+
|
|
157
|
+
# If no keys left, delete the file
|
|
158
|
+
if not cache_data:
|
|
159
|
+
return await file_manager.delete_file(file_path)
|
|
160
|
+
|
|
161
|
+
# Otherwise update file
|
|
162
|
+
new_file_data = create_cache_file_data(cache_data)
|
|
163
|
+
return await file_manager.write_file(file_path, new_file_data)
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.error(f"Failed to delete key '{key}' from {cache_type} cache: {e}")
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
async def exists(
|
|
170
|
+
self,
|
|
171
|
+
cache_type: str,
|
|
172
|
+
tenant_id: str,
|
|
173
|
+
user_id: Optional[str],
|
|
174
|
+
key: str
|
|
175
|
+
) -> bool:
|
|
176
|
+
"""
|
|
177
|
+
Check if key exists in JSON cache.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
cache_type: "users", "tables", or "states"
|
|
181
|
+
tenant_id: Tenant identifier
|
|
182
|
+
user_id: User identifier (required for users/states)
|
|
183
|
+
key: Cache key to check
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
True if exists and not expired, False otherwise
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
file_path = file_manager.get_cache_file_path(cache_type, tenant_id, user_id)
|
|
190
|
+
file_data = await file_manager.read_file(file_path)
|
|
191
|
+
|
|
192
|
+
if not file_data:
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
cache_data = extract_cache_file_data(file_data)
|
|
196
|
+
if cache_data is None:
|
|
197
|
+
# Expired - delete the file
|
|
198
|
+
await file_manager.delete_file(file_path)
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
return key in cache_data
|
|
202
|
+
|
|
203
|
+
except Exception as e:
|
|
204
|
+
logger.error(f"Failed to check existence of key '{key}' in {cache_type} cache: {e}")
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
async def get_ttl(
|
|
208
|
+
self,
|
|
209
|
+
cache_type: str,
|
|
210
|
+
tenant_id: str,
|
|
211
|
+
user_id: Optional[str]
|
|
212
|
+
) -> int:
|
|
213
|
+
"""
|
|
214
|
+
Get remaining TTL for cache file.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Remaining TTL in seconds, -1 if no expiry, -2 if doesn't exist
|
|
218
|
+
"""
|
|
219
|
+
try:
|
|
220
|
+
file_path = file_manager.get_cache_file_path(cache_type, tenant_id, user_id)
|
|
221
|
+
file_data = await file_manager.read_file(file_path)
|
|
222
|
+
|
|
223
|
+
if not file_data:
|
|
224
|
+
return -2 # Doesn't exist
|
|
225
|
+
|
|
226
|
+
metadata = file_data.get("_metadata", {})
|
|
227
|
+
expires_at_str = metadata.get("expires_at")
|
|
228
|
+
|
|
229
|
+
if not expires_at_str:
|
|
230
|
+
return -1 # No expiry
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
expires_at = datetime.fromisoformat(expires_at_str)
|
|
234
|
+
now = datetime.now()
|
|
235
|
+
|
|
236
|
+
if now >= expires_at:
|
|
237
|
+
return -2 # Already expired
|
|
238
|
+
|
|
239
|
+
return int((expires_at - now).total_seconds())
|
|
240
|
+
|
|
241
|
+
except ValueError:
|
|
242
|
+
return -1 # Invalid expiry format, treat as no expiry
|
|
243
|
+
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logger.error(f"Failed to get TTL for {cache_type} cache: {e}")
|
|
246
|
+
return -2
|
|
247
|
+
|
|
248
|
+
async def set_ttl(
|
|
249
|
+
self,
|
|
250
|
+
cache_type: str,
|
|
251
|
+
tenant_id: str,
|
|
252
|
+
user_id: Optional[str],
|
|
253
|
+
ttl: int
|
|
254
|
+
) -> bool:
|
|
255
|
+
"""
|
|
256
|
+
Set TTL for cache file.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
ttl: Time to live in seconds
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
True if successful, False otherwise
|
|
263
|
+
"""
|
|
264
|
+
try:
|
|
265
|
+
file_path = file_manager.get_cache_file_path(cache_type, tenant_id, user_id)
|
|
266
|
+
file_data = await file_manager.read_file(file_path)
|
|
267
|
+
|
|
268
|
+
if not file_data:
|
|
269
|
+
return False # File doesn't exist
|
|
270
|
+
|
|
271
|
+
cache_data = extract_cache_file_data(file_data)
|
|
272
|
+
if cache_data is None:
|
|
273
|
+
return False # Already expired
|
|
274
|
+
|
|
275
|
+
# Create new file data with updated TTL
|
|
276
|
+
new_file_data = create_cache_file_data(cache_data, ttl)
|
|
277
|
+
return await file_manager.write_file(file_path, new_file_data)
|
|
278
|
+
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logger.error(f"Failed to set TTL for {cache_type} cache: {e}")
|
|
281
|
+
return False
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# Global storage manager instance
|
|
285
|
+
storage_manager = JSONStorageManager()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory-based cache implementation for Wappa framework.
|
|
3
|
+
|
|
4
|
+
Provides high-speed in-memory cache storage with TTL support.
|
|
5
|
+
Suitable for development, testing, and single-process deployments.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
wappa = Wappa(cache="memory")
|
|
9
|
+
# Data will be stored in memory with automatic TTL cleanup
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .memory_cache_factory import MemoryCacheFactory
|
|
13
|
+
|
|
14
|
+
__all__ = ["MemoryCacheFactory"]
|