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,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Messenger factory for creating platform-specific IMessenger implementations.
|
|
3
|
+
|
|
4
|
+
This factory implements the Strategy Pattern to create appropriate IMessenger
|
|
5
|
+
instances based on platform type, handling all dependency injection and
|
|
6
|
+
configuration for each platform-specific messenger.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from wappa.core.logging.logger import get_logger
|
|
12
|
+
from wappa.domain.interfaces.messaging_interface import IMessenger
|
|
13
|
+
from wappa.domain.services.tenant_credentials_service import TenantCredentialsService
|
|
14
|
+
from wappa.messaging.whatsapp.client.whatsapp_client import WhatsAppClient
|
|
15
|
+
from wappa.messaging.whatsapp.handlers.whatsapp_interactive_handler import (
|
|
16
|
+
WhatsAppInteractiveHandler,
|
|
17
|
+
)
|
|
18
|
+
from wappa.messaging.whatsapp.handlers.whatsapp_media_handler import (
|
|
19
|
+
WhatsAppMediaHandler,
|
|
20
|
+
)
|
|
21
|
+
from wappa.messaging.whatsapp.handlers.whatsapp_specialized_handler import (
|
|
22
|
+
WhatsAppSpecializedHandler,
|
|
23
|
+
)
|
|
24
|
+
from wappa.messaging.whatsapp.handlers.whatsapp_template_handler import (
|
|
25
|
+
WhatsAppTemplateHandler,
|
|
26
|
+
)
|
|
27
|
+
from wappa.messaging.whatsapp.messenger.whatsapp_messenger import WhatsAppMessenger
|
|
28
|
+
from wappa.schemas.core.types import PlatformType
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
import aiohttp
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MessengerFactory:
|
|
35
|
+
"""
|
|
36
|
+
Factory for creating platform-specific messenger implementations.
|
|
37
|
+
|
|
38
|
+
Uses Strategy Pattern to provide appropriate IMessenger implementations
|
|
39
|
+
based on platform type. Handles dependency injection and configuration
|
|
40
|
+
for each platform-specific messenger.
|
|
41
|
+
|
|
42
|
+
Currently supported platforms:
|
|
43
|
+
- WhatsApp Business API
|
|
44
|
+
|
|
45
|
+
Future platforms:
|
|
46
|
+
- Telegram Bot API
|
|
47
|
+
- Microsoft Teams
|
|
48
|
+
- Discord
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, http_session: "aiohttp.ClientSession" = None):
|
|
52
|
+
"""
|
|
53
|
+
Initialize the messenger factory.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
http_session: Shared HTTP session for efficient connection pooling
|
|
57
|
+
"""
|
|
58
|
+
self._http_session = http_session
|
|
59
|
+
self.logger = get_logger(__name__)
|
|
60
|
+
|
|
61
|
+
# Cache for created messengers to avoid recreating for same tenant/platform
|
|
62
|
+
self._messenger_cache: dict[str, IMessenger] = {}
|
|
63
|
+
|
|
64
|
+
async def create_messenger(
|
|
65
|
+
self, platform: PlatformType, tenant_id: str, force_recreate: bool = False
|
|
66
|
+
) -> IMessenger:
|
|
67
|
+
"""
|
|
68
|
+
Create platform-specific messenger implementation.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
platform: Platform type (WHATSAPP, TELEGRAM, etc.)
|
|
72
|
+
tenant_id: Tenant-specific identifier
|
|
73
|
+
force_recreate: Force creation of new instance even if cached
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Configured IMessenger implementation for the specified platform
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
ValueError: If platform is not supported
|
|
80
|
+
RuntimeError: If messenger creation fails
|
|
81
|
+
"""
|
|
82
|
+
cache_key = f"{platform.value}:{tenant_id}"
|
|
83
|
+
|
|
84
|
+
# Return cached instance if available and not forcing recreation
|
|
85
|
+
if not force_recreate and cache_key in self._messenger_cache:
|
|
86
|
+
self.logger.debug(f"Using cached messenger for {cache_key}")
|
|
87
|
+
return self._messenger_cache[cache_key]
|
|
88
|
+
|
|
89
|
+
self.logger.debug(
|
|
90
|
+
f"Creating new messenger for platform: {platform.value}, tenant: {tenant_id}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
if platform == PlatformType.WHATSAPP:
|
|
95
|
+
messenger = await self._create_whatsapp_messenger(tenant_id)
|
|
96
|
+
|
|
97
|
+
# Future platform implementations:
|
|
98
|
+
# elif platform == PlatformType.TELEGRAM:
|
|
99
|
+
# messenger = await self._create_telegram_messenger(tenant_id)
|
|
100
|
+
# elif platform == PlatformType.TEAMS:
|
|
101
|
+
# messenger = await self._create_teams_messenger(tenant_id)
|
|
102
|
+
|
|
103
|
+
else:
|
|
104
|
+
raise ValueError(f"Unsupported platform: {platform.value}")
|
|
105
|
+
|
|
106
|
+
# Cache the created messenger
|
|
107
|
+
self._messenger_cache[cache_key] = messenger
|
|
108
|
+
|
|
109
|
+
return messenger
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
self.logger.error(
|
|
113
|
+
f"Failed to create messenger for platform {platform.value}: {e}"
|
|
114
|
+
)
|
|
115
|
+
raise RuntimeError(f"Messenger creation failed: {e}") from e
|
|
116
|
+
|
|
117
|
+
async def _create_whatsapp_messenger(self, tenant_id: str) -> WhatsAppMessenger:
|
|
118
|
+
"""
|
|
119
|
+
Create configured WhatsApp messenger with all dependencies.
|
|
120
|
+
|
|
121
|
+
Reuses the existing dependency injection chain from whatsapp_dependencies.py
|
|
122
|
+
to maintain consistency and avoid code duplication.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
tenant_id: WhatsApp phone_number_id (tenant identifier)
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Fully configured WhatsAppMessenger instance
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
ValueError: If tenant credentials are invalid
|
|
132
|
+
"""
|
|
133
|
+
self.logger.debug(f"Creating WhatsApp messenger for tenant: {tenant_id}")
|
|
134
|
+
|
|
135
|
+
# Validate tenant credentials
|
|
136
|
+
if not TenantCredentialsService.validate_tenant(tenant_id):
|
|
137
|
+
raise ValueError(f"Invalid or inactive tenant: {tenant_id}")
|
|
138
|
+
|
|
139
|
+
# Get tenant-specific access token
|
|
140
|
+
access_token = TenantCredentialsService.get_whatsapp_access_token(tenant_id)
|
|
141
|
+
|
|
142
|
+
# Create WhatsApp client (core dependency)
|
|
143
|
+
client = WhatsAppClient(
|
|
144
|
+
session=self._http_session,
|
|
145
|
+
access_token=access_token,
|
|
146
|
+
phone_number_id=tenant_id,
|
|
147
|
+
logger=self.logger,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Create all WhatsApp handlers (following dependency injection pattern)
|
|
151
|
+
media_handler = WhatsAppMediaHandler(client=client, tenant_id=tenant_id)
|
|
152
|
+
interactive_handler = WhatsAppInteractiveHandler(
|
|
153
|
+
client=client, tenant_id=tenant_id
|
|
154
|
+
)
|
|
155
|
+
template_handler = WhatsAppTemplateHandler(client=client, tenant_id=tenant_id)
|
|
156
|
+
specialized_handler = WhatsAppSpecializedHandler(
|
|
157
|
+
client=client, tenant_id=tenant_id
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Create unified WhatsApp messenger with all handlers
|
|
161
|
+
messenger = WhatsAppMessenger(
|
|
162
|
+
client=client,
|
|
163
|
+
media_handler=media_handler,
|
|
164
|
+
interactive_handler=interactive_handler,
|
|
165
|
+
template_handler=template_handler,
|
|
166
|
+
specialized_handler=specialized_handler,
|
|
167
|
+
tenant_id=tenant_id,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
self.logger.info(
|
|
171
|
+
f"✅ WhatsApp messenger created successfully for tenant: {tenant_id}"
|
|
172
|
+
)
|
|
173
|
+
return messenger
|
|
174
|
+
|
|
175
|
+
def get_supported_platforms(self) -> list[PlatformType]:
|
|
176
|
+
"""
|
|
177
|
+
Get list of currently supported platforms.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List of supported platform types
|
|
181
|
+
"""
|
|
182
|
+
return [PlatformType.WHATSAPP]
|
|
183
|
+
|
|
184
|
+
def is_platform_supported(self, platform: PlatformType) -> bool:
|
|
185
|
+
"""
|
|
186
|
+
Check if platform is supported by this factory.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
platform: Platform type to check
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
True if platform is supported, False otherwise
|
|
193
|
+
"""
|
|
194
|
+
return platform in self.get_supported_platforms()
|
|
195
|
+
|
|
196
|
+
def clear_cache(self, platform: PlatformType = None, tenant_id: str = None):
|
|
197
|
+
"""
|
|
198
|
+
Clear messenger cache.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
platform: Optional platform to clear (clear all if None)
|
|
202
|
+
tenant_id: Optional tenant to clear (clear all if None)
|
|
203
|
+
"""
|
|
204
|
+
if platform and tenant_id:
|
|
205
|
+
# Clear specific platform/tenant combination
|
|
206
|
+
cache_key = f"{platform.value}:{tenant_id}"
|
|
207
|
+
self._messenger_cache.pop(cache_key, None)
|
|
208
|
+
self.logger.debug(f"Cleared messenger cache for {cache_key}")
|
|
209
|
+
elif platform:
|
|
210
|
+
# Clear all messengers for a platform
|
|
211
|
+
to_remove = [
|
|
212
|
+
key
|
|
213
|
+
for key in self._messenger_cache.keys()
|
|
214
|
+
if key.startswith(f"{platform.value}:")
|
|
215
|
+
]
|
|
216
|
+
for key in to_remove:
|
|
217
|
+
del self._messenger_cache[key]
|
|
218
|
+
self.logger.debug(f"Cleared messenger cache for platform: {platform.value}")
|
|
219
|
+
elif tenant_id:
|
|
220
|
+
# Clear all messengers for a tenant
|
|
221
|
+
to_remove = [
|
|
222
|
+
key
|
|
223
|
+
for key in self._messenger_cache.keys()
|
|
224
|
+
if key.endswith(f":{tenant_id}")
|
|
225
|
+
]
|
|
226
|
+
for key in to_remove:
|
|
227
|
+
del self._messenger_cache[key]
|
|
228
|
+
self.logger.debug(f"Cleared messenger cache for tenant: {tenant_id}")
|
|
229
|
+
else:
|
|
230
|
+
# Clear entire cache
|
|
231
|
+
self._messenger_cache.clear()
|
|
232
|
+
self.logger.debug("Cleared entire messenger cache")
|
|
233
|
+
|
|
234
|
+
# Future platform implementations:
|
|
235
|
+
|
|
236
|
+
# async def _create_telegram_messenger(self, tenant_id: str) -> IMessenger:
|
|
237
|
+
# """Create configured Telegram messenger (future implementation)."""
|
|
238
|
+
# # TODO: Implement Telegram Bot API messenger
|
|
239
|
+
# raise NotImplementedError("Telegram messenger not yet implemented")
|
|
240
|
+
|
|
241
|
+
# async def _create_teams_messenger(self, tenant_id: str) -> IMessenger:
|
|
242
|
+
# """Create configured Teams messenger (future implementation)."""
|
|
243
|
+
# # TODO: Implement Microsoft Teams messenger
|
|
244
|
+
# raise NotImplementedError("Teams messenger not yet implemented")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Domain interfaces.
|
|
3
|
+
|
|
4
|
+
Defines the contracts that infrastructure layer must implement.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .base_repository import IBaseRepository
|
|
8
|
+
from .cache_factory import ICacheFactory
|
|
9
|
+
from .cache_interface import ICache
|
|
10
|
+
from .expiry_repository import IExpiryRepository
|
|
11
|
+
from .media_interface import IMediaHandler
|
|
12
|
+
from .messaging_interface import IMessenger
|
|
13
|
+
from .pubsub_repository import IPubSubRepository
|
|
14
|
+
from .repository_factory import IRepositoryFactory
|
|
15
|
+
from .shared_state_repository import ISharedStateRepository
|
|
16
|
+
from .state_repository import IStateRepository
|
|
17
|
+
from .user_repository import IUserRepository
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"IBaseRepository",
|
|
21
|
+
"IUserRepository",
|
|
22
|
+
"IStateRepository",
|
|
23
|
+
"ISharedStateRepository",
|
|
24
|
+
"IExpiryRepository",
|
|
25
|
+
"IPubSubRepository",
|
|
26
|
+
"IRepositoryFactory",
|
|
27
|
+
"IMessenger",
|
|
28
|
+
"IMediaHandler",
|
|
29
|
+
# Cache interfaces
|
|
30
|
+
"ICache",
|
|
31
|
+
"ICacheFactory",
|
|
32
|
+
]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base repository interface.
|
|
3
|
+
|
|
4
|
+
Defines common contract for all Redis-based repositories.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from datetime import timedelta
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class IBaseRepository(ABC):
|
|
15
|
+
"""
|
|
16
|
+
Base interface for all Redis repositories.
|
|
17
|
+
|
|
18
|
+
Provides common contract for key-value operations, TTL management,
|
|
19
|
+
and context-aware operations.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
async def get(self, key: str) -> Any | None:
|
|
24
|
+
"""Get value by key."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
async def set(self, key: str, value: Any, ttl: timedelta | None = None) -> bool:
|
|
29
|
+
"""Set value with optional TTL."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
async def delete(self, key: str) -> bool:
|
|
34
|
+
"""Delete key."""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
async def exists(self, key: str) -> bool:
|
|
39
|
+
"""Check if key exists."""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
async def get_ttl(self, key: str) -> int | None:
|
|
44
|
+
"""Get TTL for key in seconds."""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
async def set_ttl(self, key: str, ttl: timedelta) -> bool:
|
|
49
|
+
"""Set TTL for existing key."""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
async def get_hash(
|
|
54
|
+
self, key: str, models: type[BaseModel] | None = None
|
|
55
|
+
) -> dict[str, Any] | None:
|
|
56
|
+
"""
|
|
57
|
+
Get hash value with optional BaseModel deserialization.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
key: Redis key
|
|
61
|
+
models: Optional BaseModel class for full object reconstruction
|
|
62
|
+
e.g., User (will automatically handle nested UserContact, UserLocation)
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Hash data dictionary or None if not found
|
|
66
|
+
"""
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
async def set_hash(
|
|
71
|
+
self, key: str, data: dict[str, Any], ttl: timedelta | None = None
|
|
72
|
+
) -> bool:
|
|
73
|
+
"""Set hash value with optional TTL."""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
async def get_hash_field(self, key: str, field: str) -> Any | None:
|
|
78
|
+
"""Get single field from hash."""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
async def set_hash_field(self, key: str, field: str, value: Any) -> bool:
|
|
83
|
+
"""Set single field in hash."""
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
async def delete_hash_field(self, key: str, field: str) -> bool:
|
|
88
|
+
"""Delete field from hash."""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
async def get_keys_pattern(self, pattern: str) -> list[str]:
|
|
93
|
+
"""Get keys matching pattern."""
|
|
94
|
+
pass
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cache factory interface definition for Wappa framework.
|
|
3
|
+
|
|
4
|
+
Defines the contract for creating context-aware cache instances.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
|
|
9
|
+
from .cache_interface import ICache
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ICacheFactory(ABC):
|
|
13
|
+
"""
|
|
14
|
+
Interface for creating context-aware cache instances.
|
|
15
|
+
|
|
16
|
+
Cache factories create cache instances bound to specific tenants and users,
|
|
17
|
+
ensuring proper data isolation and context management.
|
|
18
|
+
|
|
19
|
+
Context (tenant_id, user_id) is injected at construction time, eliminating
|
|
20
|
+
the need for manual parameter passing in cache creation methods.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, tenant_id: str, user_id: str):
|
|
24
|
+
"""
|
|
25
|
+
Initialize cache factory with request context.
|
|
26
|
+
|
|
27
|
+
This eliminates the need for manual parameter passing in cache methods.
|
|
28
|
+
Context is injected once at construction and used throughout the factory lifetime.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
tenant_id: Tenant identifier for namespace isolation
|
|
32
|
+
user_id: User identifier for user-specific caches
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ValueError: If tenant_id or user_id is None or empty
|
|
36
|
+
"""
|
|
37
|
+
self.tenant_id = tenant_id
|
|
38
|
+
self.user_id = user_id
|
|
39
|
+
self._validate_context()
|
|
40
|
+
|
|
41
|
+
def _validate_context(self):
|
|
42
|
+
"""Validate that required context is available."""
|
|
43
|
+
if not self.tenant_id or not self.user_id:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"Missing required context: tenant_id={self.tenant_id}, user_id={self.user_id}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def create_state_cache(self) -> ICache:
|
|
50
|
+
"""
|
|
51
|
+
Create state cache instance with context binding.
|
|
52
|
+
|
|
53
|
+
Used for handler state management and conversation state tracking.
|
|
54
|
+
Context (tenant_id, user_id) is automatically injected from constructor.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Context-bound state cache instance
|
|
58
|
+
"""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def create_user_cache(self) -> ICache:
|
|
63
|
+
"""
|
|
64
|
+
Create user cache instance with context binding.
|
|
65
|
+
|
|
66
|
+
Used for user profile data, preferences, and user-specific information.
|
|
67
|
+
Context (tenant_id, user_id) is automatically injected from constructor.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Context-bound user cache instance
|
|
71
|
+
"""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def create_table_cache(self) -> ICache:
|
|
76
|
+
"""
|
|
77
|
+
Create table cache instance with context binding.
|
|
78
|
+
|
|
79
|
+
Used for shared data, lookup tables, and tenant-wide information.
|
|
80
|
+
Context (tenant_id) is automatically injected from constructor.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Context-bound table cache instance
|
|
84
|
+
"""
|
|
85
|
+
pass
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cache interface definition for Wappa framework.
|
|
3
|
+
|
|
4
|
+
Defines the contract for cache implementations (Redis, Memory, JSON).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ICache(ABC):
|
|
14
|
+
"""
|
|
15
|
+
Interface for cache implementations in the Wappa framework.
|
|
16
|
+
|
|
17
|
+
Provides basic cache operations with context awareness for tenant and user isolation.
|
|
18
|
+
All cache implementations must support these core operations.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
async def get(
|
|
23
|
+
self, key: str, models: type[BaseModel] | None = None
|
|
24
|
+
) -> dict[str, Any] | None:
|
|
25
|
+
"""
|
|
26
|
+
Get cached data by key.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
key: Cache key
|
|
30
|
+
models: Optional BaseModel class for deserialization
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Cached data or None if not found
|
|
34
|
+
"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
async def set(self, key: str, data: dict[str, Any], ttl: int | None = None) -> bool:
|
|
39
|
+
"""
|
|
40
|
+
Set cached data with optional TTL.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
key: Cache key
|
|
44
|
+
data: Data to cache
|
|
45
|
+
ttl: Time to live in seconds
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
True if successful, False otherwise
|
|
49
|
+
"""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
async def delete(self, key: str) -> bool:
|
|
54
|
+
"""
|
|
55
|
+
Delete cached data by key.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
key: Cache key to delete
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
True if deleted, False if not found
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
async def exists(self, key: str) -> bool:
|
|
67
|
+
"""
|
|
68
|
+
Check if key exists in cache.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
key: Cache key to check
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True if exists, False otherwise
|
|
75
|
+
"""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
async def get_field(self, key: str, field: str) -> Any | None:
|
|
80
|
+
"""
|
|
81
|
+
Get a specific field from cached hash data.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
key: Cache key
|
|
85
|
+
field: Field name
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Field value or None if not found
|
|
89
|
+
"""
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
async def set_field(
|
|
94
|
+
self, key: str, field: str, value: Any, ttl: int | None = None
|
|
95
|
+
) -> bool:
|
|
96
|
+
"""
|
|
97
|
+
Set a specific field in cached hash data.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
key: Cache key
|
|
101
|
+
field: Field name
|
|
102
|
+
value: Field value
|
|
103
|
+
ttl: Time to live in seconds
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
True if successful, False otherwise
|
|
107
|
+
"""
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
@abstractmethod
|
|
111
|
+
async def increment_field(
|
|
112
|
+
self, key: str, field: str, increment: int = 1, ttl: int | None = None
|
|
113
|
+
) -> int | None:
|
|
114
|
+
"""
|
|
115
|
+
Atomically increment an integer field.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
key: Cache key
|
|
119
|
+
field: Field name
|
|
120
|
+
increment: Amount to increment by
|
|
121
|
+
ttl: Time to live in seconds
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
New value after increment or None on error
|
|
125
|
+
"""
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
async def append_to_list(
|
|
130
|
+
self, key: str, field: str, value: Any, ttl: int | None = None
|
|
131
|
+
) -> bool:
|
|
132
|
+
"""
|
|
133
|
+
Append value to a list field.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
key: Cache key
|
|
137
|
+
field: Field name containing list
|
|
138
|
+
value: Value to append
|
|
139
|
+
ttl: Time to live in seconds
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if successful, False otherwise
|
|
143
|
+
"""
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
@abstractmethod
|
|
147
|
+
async def get_ttl(self, key: str) -> int:
|
|
148
|
+
"""
|
|
149
|
+
Get remaining time to live for a key.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
key: Cache key
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Remaining TTL in seconds, -1 if no expiry, -2 if key doesn't exist
|
|
156
|
+
"""
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
@abstractmethod
|
|
160
|
+
async def set_ttl(self, key: str, ttl: int) -> bool:
|
|
161
|
+
"""
|
|
162
|
+
Set time to live for a key.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
key: Cache key
|
|
166
|
+
ttl: Time to live in seconds
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
True if successful, False otherwise
|
|
170
|
+
"""
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def create_table_key(table_name: str, pkid: str) -> str:
|
|
175
|
+
"""
|
|
176
|
+
Create a properly formatted table cache key.
|
|
177
|
+
|
|
178
|
+
This is a static utility method available on all cache implementations
|
|
179
|
+
to ensure consistent key formatting across cache backends.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
table_name: Name of the table (e.g., "user_profiles", "message_logs")
|
|
183
|
+
pkid: Primary key ID (e.g., user_id, message_id)
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Formatted key string for use with cache methods
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
key = ICache.create_table_key("user_profiles", "12345")
|
|
190
|
+
# Returns: "user_profiles:12345"
|
|
191
|
+
"""
|
|
192
|
+
if not table_name or not pkid:
|
|
193
|
+
raise ValueError("Both table_name and pkid must be provided and non-empty")
|
|
194
|
+
|
|
195
|
+
# Sanitize inputs to avoid conflicts
|
|
196
|
+
safe_table_name = str(table_name).replace(":", "_")
|
|
197
|
+
safe_pkid = str(pkid).replace(":", "_")
|
|
198
|
+
|
|
199
|
+
return f"{safe_table_name}:{safe_pkid}"
|