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,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Redis Manager for Wappa application lifecycle management.
|
|
3
|
+
|
|
4
|
+
Wraps the Wappa RedisClient with application-specific initialization and cleanup.
|
|
5
|
+
Provides a clean interface for applications to manage Redis connections.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ...core.config.settings import settings
|
|
12
|
+
from .redis_client import POOL_DB_MAPPING, PoolAlias, RedisClient
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RedisManager:
|
|
18
|
+
"""
|
|
19
|
+
Application-level wrapper around the Wappa RedisClient.
|
|
20
|
+
|
|
21
|
+
Handles lifecycle management, health monitoring, and provides
|
|
22
|
+
a clean interface for applications to manage Redis connections.
|
|
23
|
+
|
|
24
|
+
This manager follows the same patterns as the reference implementation
|
|
25
|
+
but uses Wappa's own RedisClient instead of external dependencies.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
_initialized: bool = False
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
async def initialize(
|
|
32
|
+
cls, redis_url: str | None = None, max_connections: int | None = None
|
|
33
|
+
) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Initialize Redis pools with enhanced logging.
|
|
36
|
+
|
|
37
|
+
Uses the existing multi-pool setup with proper configuration
|
|
38
|
+
from application settings or provided parameters.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
redis_url: Redis connection URL (defaults to settings.redis_url)
|
|
42
|
+
max_connections: Max connections per pool (defaults to settings.redis_max_connections)
|
|
43
|
+
"""
|
|
44
|
+
if cls._initialized:
|
|
45
|
+
logger.info("Redis pools already initialized - skipping")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
# Enhanced initialization logging
|
|
50
|
+
url = redis_url or settings.redis_url
|
|
51
|
+
connections = max_connections or settings.redis_max_connections
|
|
52
|
+
|
|
53
|
+
logger.info(f"Setting up Redis pools from {url} (max_connections: {connections})")
|
|
54
|
+
|
|
55
|
+
# Use Wappa's RedisClient.setup_single_url
|
|
56
|
+
RedisClient.setup_single_url(
|
|
57
|
+
base_url=url,
|
|
58
|
+
max_connections=connections,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Verify all pools with detailed logging
|
|
62
|
+
logger.info("Verifying Redis pool health...")
|
|
63
|
+
await cls._verify_pools()
|
|
64
|
+
|
|
65
|
+
cls._initialized = True
|
|
66
|
+
|
|
67
|
+
# Success confirmation with pool details
|
|
68
|
+
pool_count = len(POOL_DB_MAPPING)
|
|
69
|
+
pool_details = ", ".join(f"{alias}:db{db}" for alias, db in POOL_DB_MAPPING.items())
|
|
70
|
+
logger.info(f"✅ Redis pools ready: {pool_count} pools ({pool_details})")
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"❌ Redis pool initialization failed: {e}", exc_info=True)
|
|
74
|
+
raise
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
async def _verify_pools(cls) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Health check all Redis pools with detailed logging.
|
|
80
|
+
|
|
81
|
+
Ensures all expected pools are accessible and responding.
|
|
82
|
+
"""
|
|
83
|
+
failed_pools = []
|
|
84
|
+
successful_pools = []
|
|
85
|
+
|
|
86
|
+
for alias in POOL_DB_MAPPING:
|
|
87
|
+
pool_alias: PoolAlias = alias # type: ignore
|
|
88
|
+
try:
|
|
89
|
+
redis = await RedisClient.get(pool_alias)
|
|
90
|
+
await redis.ping()
|
|
91
|
+
successful_pools.append(f"{alias}:db{POOL_DB_MAPPING[alias]}")
|
|
92
|
+
logger.debug(f"✅ Redis pool '{alias}' (db{POOL_DB_MAPPING[alias]}) health check passed")
|
|
93
|
+
except Exception as e:
|
|
94
|
+
failed_pools.append(f"{alias}:db{POOL_DB_MAPPING[alias]}")
|
|
95
|
+
logger.error(f"❌ Redis pool '{alias}' (db{POOL_DB_MAPPING[alias]}) health check failed: {e}")
|
|
96
|
+
|
|
97
|
+
if failed_pools:
|
|
98
|
+
raise ConnectionError(f"Failed Redis pools: {', '.join(failed_pools)}")
|
|
99
|
+
|
|
100
|
+
logger.info(f"✅ All Redis pools healthy: {', '.join(successful_pools)}")
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
async def get_health_status(cls) -> dict[str, Any]:
|
|
104
|
+
"""
|
|
105
|
+
Get health status of all Redis pools.
|
|
106
|
+
|
|
107
|
+
Returns detailed health information for monitoring.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Dictionary containing initialization status and per-pool health info
|
|
111
|
+
"""
|
|
112
|
+
health_status = {"initialized": cls._initialized, "pools": {}}
|
|
113
|
+
|
|
114
|
+
if not cls._initialized:
|
|
115
|
+
health_status["message"] = "Redis not initialized"
|
|
116
|
+
return health_status
|
|
117
|
+
|
|
118
|
+
for alias in POOL_DB_MAPPING:
|
|
119
|
+
pool_alias: PoolAlias = alias # type: ignore
|
|
120
|
+
try:
|
|
121
|
+
redis = await RedisClient.get(pool_alias)
|
|
122
|
+
await redis.ping()
|
|
123
|
+
health_status["pools"][alias] = {
|
|
124
|
+
"status": "healthy",
|
|
125
|
+
"database": POOL_DB_MAPPING[alias],
|
|
126
|
+
"error": None,
|
|
127
|
+
}
|
|
128
|
+
except Exception as e:
|
|
129
|
+
health_status["pools"][alias] = {
|
|
130
|
+
"status": "unhealthy",
|
|
131
|
+
"database": POOL_DB_MAPPING[alias],
|
|
132
|
+
"error": str(e),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return health_status
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
async def get_client(cls, alias: PoolAlias = "users"):
|
|
139
|
+
"""
|
|
140
|
+
Get Redis client for specified pool.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
alias: Pool alias ("users", "state_handler", "table")
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Redis client instance
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
RuntimeError: If Redis not initialized
|
|
150
|
+
"""
|
|
151
|
+
if not cls._initialized:
|
|
152
|
+
raise RuntimeError("RedisManager not initialized. Call initialize() first.")
|
|
153
|
+
|
|
154
|
+
return await RedisClient.get(alias)
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
async def cleanup(cls) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Clean shutdown of all Redis pools.
|
|
160
|
+
|
|
161
|
+
Should be called during application shutdown.
|
|
162
|
+
"""
|
|
163
|
+
if not cls._initialized:
|
|
164
|
+
logger.info("Redis pools not initialized, skipping cleanup")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
logger.info("Shutting down Redis pools...")
|
|
169
|
+
await RedisClient.close()
|
|
170
|
+
cls._initialized = False
|
|
171
|
+
logger.info("Redis pools shut down successfully")
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.error(f"Error during Redis cleanup: {e}", exc_info=True)
|
|
174
|
+
raise
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def is_initialized(cls) -> bool:
|
|
178
|
+
"""Check if Redis pools are initialized."""
|
|
179
|
+
return cls._initialized
|
|
180
|
+
|
|
181
|
+
@classmethod
|
|
182
|
+
def get_pool_info(cls) -> dict[str, int]:
|
|
183
|
+
"""
|
|
184
|
+
Get information about available Redis pools.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Dictionary mapping pool aliases to database numbers
|
|
188
|
+
"""
|
|
189
|
+
return POOL_DB_MAPPING.copy()
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base processor abstraction for platform-agnostic webhook processing.
|
|
3
|
+
|
|
4
|
+
This module defines the abstract base classes that all platform-specific
|
|
5
|
+
webhook processors must inherit from to ensure consistent interfaces.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from wappa.core.logging.logger import get_logger
|
|
13
|
+
from wappa.schemas.core.base_message import BaseMessage
|
|
14
|
+
from wappa.schemas.core.base_status import BaseMessageStatus
|
|
15
|
+
from wappa.schemas.core.base_webhook import BaseWebhook
|
|
16
|
+
from wappa.schemas.core.types import ErrorCode, MessageType, PlatformType
|
|
17
|
+
|
|
18
|
+
# Legacy ProcessingResult class removed - Universal Webhook Interface is now the ONLY way
|
|
19
|
+
# Use processor.create_universal_webhook() method instead for type-safe webhook handling
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProcessorCapabilities:
|
|
23
|
+
"""
|
|
24
|
+
Represents the capabilities of a platform processor.
|
|
25
|
+
|
|
26
|
+
This allows the system to understand what each processor can handle
|
|
27
|
+
and route webhooks appropriately.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
platform: PlatformType,
|
|
33
|
+
supported_message_types: set[MessageType],
|
|
34
|
+
supports_status_updates: bool = True,
|
|
35
|
+
supports_signature_validation: bool = True,
|
|
36
|
+
supports_error_webhooks: bool = True,
|
|
37
|
+
max_payload_size: int | None = None,
|
|
38
|
+
rate_limit_per_minute: int | None = None,
|
|
39
|
+
):
|
|
40
|
+
self.platform = platform
|
|
41
|
+
self.supported_message_types = supported_message_types
|
|
42
|
+
self.supports_status_updates = supports_status_updates
|
|
43
|
+
self.supports_signature_validation = supports_signature_validation
|
|
44
|
+
self.supports_error_webhooks = supports_error_webhooks
|
|
45
|
+
self.max_payload_size = max_payload_size
|
|
46
|
+
self.rate_limit_per_minute = rate_limit_per_minute
|
|
47
|
+
|
|
48
|
+
def can_handle_message_type(self, message_type: MessageType) -> bool:
|
|
49
|
+
"""Check if processor can handle a specific message type."""
|
|
50
|
+
return message_type in self.supported_message_types
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict[str, Any]:
|
|
53
|
+
"""Convert capabilities to dictionary."""
|
|
54
|
+
return {
|
|
55
|
+
"platform": self.platform.value,
|
|
56
|
+
"supported_message_types": [
|
|
57
|
+
mt.value for mt in self.supported_message_types
|
|
58
|
+
],
|
|
59
|
+
"supports_status_updates": self.supports_status_updates,
|
|
60
|
+
"supports_signature_validation": self.supports_signature_validation,
|
|
61
|
+
"supports_error_webhooks": self.supports_error_webhooks,
|
|
62
|
+
"max_payload_size": self.max_payload_size,
|
|
63
|
+
"rate_limit_per_minute": self.rate_limit_per_minute,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class BaseWebhookProcessor(ABC):
|
|
68
|
+
"""
|
|
69
|
+
Platform-agnostic webhook processor base class.
|
|
70
|
+
|
|
71
|
+
All platform-specific processors must inherit from this class
|
|
72
|
+
to provide a consistent interface for webhook processing.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self):
|
|
76
|
+
self.logger = get_logger(__name__)
|
|
77
|
+
|
|
78
|
+
# Message type handlers - subclasses should populate this
|
|
79
|
+
self._message_type_handlers: dict[str, callable] = {}
|
|
80
|
+
|
|
81
|
+
# Processing statistics
|
|
82
|
+
self._processed_count = 0
|
|
83
|
+
self._error_count = 0
|
|
84
|
+
self._last_processed = None
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def platform(self) -> PlatformType:
|
|
89
|
+
"""Get the platform this processor handles."""
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
@abstractmethod
|
|
94
|
+
def capabilities(self) -> ProcessorCapabilities:
|
|
95
|
+
"""Get the capabilities of this processor."""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
# Legacy process_webhook abstract method removed - Universal Webhook Interface is the ONLY way
|
|
99
|
+
# All processors must implement create_universal_webhook() method instead
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def validate_webhook_signature(
|
|
103
|
+
self, payload: bytes, signature: str, **kwargs
|
|
104
|
+
) -> bool:
|
|
105
|
+
"""
|
|
106
|
+
Validate webhook signature for security.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
payload: Raw webhook payload bytes
|
|
110
|
+
signature: Platform-specific signature header
|
|
111
|
+
**kwargs: Additional validation parameters
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
True if signature is valid, False otherwise
|
|
115
|
+
"""
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
@abstractmethod
|
|
119
|
+
def parse_webhook_container(self, payload: dict[str, Any], **kwargs) -> BaseWebhook:
|
|
120
|
+
"""
|
|
121
|
+
Parse the top-level webhook structure.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
payload: Raw webhook payload
|
|
125
|
+
**kwargs: Additional parsing parameters
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Parsed webhook container with universal interface
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
ValidationError: If webhook structure is invalid
|
|
132
|
+
"""
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
@abstractmethod
|
|
136
|
+
def get_supported_message_types(self) -> set[MessageType]:
|
|
137
|
+
"""Get the set of message types this processor supports."""
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
def create_message_from_data(
|
|
142
|
+
self, message_data: dict[str, Any], message_type: MessageType, **kwargs
|
|
143
|
+
) -> BaseMessage:
|
|
144
|
+
"""
|
|
145
|
+
Create a message instance from raw data.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
message_data: Raw message data from webhook
|
|
149
|
+
message_type: The type of message to create
|
|
150
|
+
**kwargs: Additional creation parameters
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Parsed message with universal interface
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ValidationError: If message data is invalid
|
|
157
|
+
UnsupportedMessageType: If message type is not supported
|
|
158
|
+
"""
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
@abstractmethod
|
|
162
|
+
def create_status_from_data(
|
|
163
|
+
self, status_data: dict[str, Any], **kwargs
|
|
164
|
+
) -> BaseMessageStatus:
|
|
165
|
+
"""
|
|
166
|
+
Create a status instance from raw data.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
status_data: Raw status data from webhook
|
|
170
|
+
**kwargs: Additional creation parameters
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Parsed status with universal interface
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
ValidationError: If status data is invalid
|
|
177
|
+
"""
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
def is_supported_platform(self, platform: str) -> bool:
|
|
181
|
+
"""Check if the platform string matches this processor."""
|
|
182
|
+
return platform.lower() == self.platform.value.lower()
|
|
183
|
+
|
|
184
|
+
def register_message_handler(self, message_type: str, handler: Callable) -> None:
|
|
185
|
+
"""Register a handler for a specific message type."""
|
|
186
|
+
self._message_type_handlers[message_type] = handler
|
|
187
|
+
|
|
188
|
+
def get_message_handler(self, message_type: str) -> Callable | None:
|
|
189
|
+
"""Get the handler for a specific message type."""
|
|
190
|
+
return self._message_type_handlers.get(message_type)
|
|
191
|
+
|
|
192
|
+
# Legacy _process_incoming_messages method removed - Universal Webhook Interface handles this via IncomingMessageWebhook
|
|
193
|
+
|
|
194
|
+
# Legacy _process_status_updates method removed - Universal Webhook Interface handles this via StatusWebhook
|
|
195
|
+
|
|
196
|
+
def get_processing_stats(self) -> dict[str, Any]:
|
|
197
|
+
"""
|
|
198
|
+
Get processing statistics for monitoring and analysis.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Dictionary with processing statistics
|
|
202
|
+
"""
|
|
203
|
+
return {
|
|
204
|
+
"platform": self.platform.value,
|
|
205
|
+
"capabilities": self.capabilities.to_dict(),
|
|
206
|
+
"processed_count": self._processed_count,
|
|
207
|
+
"error_count": self._error_count,
|
|
208
|
+
"error_rate": self._error_count / max(self._processed_count, 1),
|
|
209
|
+
"last_processed": self._last_processed.isoformat()
|
|
210
|
+
if self._last_processed
|
|
211
|
+
else None,
|
|
212
|
+
"supported_message_types": [
|
|
213
|
+
mt.value for mt in self.get_supported_message_types()
|
|
214
|
+
],
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# Legacy create_error_result method removed - Universal Webhook Interface handles errors via ErrorWebhook
|
|
218
|
+
|
|
219
|
+
def __str__(self) -> str:
|
|
220
|
+
"""String representation of the processor."""
|
|
221
|
+
return f"{self.__class__.__name__}(platform={self.platform.value})"
|
|
222
|
+
|
|
223
|
+
def __repr__(self) -> str:
|
|
224
|
+
"""Detailed string representation of the processor."""
|
|
225
|
+
return (
|
|
226
|
+
f"{self.__class__.__name__}("
|
|
227
|
+
f"platform={self.platform.value}, "
|
|
228
|
+
f"processed={self._processed_count}, "
|
|
229
|
+
f"errors={self._error_count})"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class ProcessorError(Exception):
|
|
234
|
+
"""Base exception for processor-related errors."""
|
|
235
|
+
|
|
236
|
+
def __init__(self, message: str, error_code: ErrorCode, platform: PlatformType):
|
|
237
|
+
self.message = message
|
|
238
|
+
self.error_code = error_code
|
|
239
|
+
self.platform = platform
|
|
240
|
+
super().__init__(message)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class UnsupportedMessageTypeError(ProcessorError):
|
|
244
|
+
"""Raised when a processor encounters an unsupported message type."""
|
|
245
|
+
|
|
246
|
+
def __init__(self, message_type: str, platform: PlatformType):
|
|
247
|
+
super().__init__(
|
|
248
|
+
f"Message type '{message_type}' not supported by {platform.value} processor",
|
|
249
|
+
ErrorCode.UNKNOWN_MESSAGE_TYPE,
|
|
250
|
+
platform,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class SignatureValidationError(ProcessorError):
|
|
255
|
+
"""Raised when webhook signature validation fails."""
|
|
256
|
+
|
|
257
|
+
def __init__(self, platform: PlatformType):
|
|
258
|
+
super().__init__(
|
|
259
|
+
f"Webhook signature validation failed for {platform.value}",
|
|
260
|
+
ErrorCode.SIGNATURE_VALIDATION_FAILED,
|
|
261
|
+
platform,
|
|
262
|
+
)
|