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,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Webhook URL Factory for generating platform-specific webhook URLs.
|
|
3
|
+
|
|
4
|
+
Implements the Factory pattern to provide clean, consistent webhook URL generation
|
|
5
|
+
for different messaging platforms with tenant-aware routing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
from wappa.core.config.settings import settings
|
|
11
|
+
from wappa.schemas.core.types import PlatformType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WebhookEndpointType(Enum):
|
|
15
|
+
"""Types of webhook endpoints that can be generated."""
|
|
16
|
+
|
|
17
|
+
WEBHOOK = "webhook" # Main webhook processing endpoint
|
|
18
|
+
VERIFY = "verify" # Webhook verification endpoint
|
|
19
|
+
STATUS = "status" # Webhook status check endpoint
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WebhookURLFactory:
|
|
23
|
+
"""
|
|
24
|
+
Factory for generating platform-specific webhook URLs.
|
|
25
|
+
|
|
26
|
+
Provides consistent URL generation for different messaging platforms
|
|
27
|
+
with support for tenant-aware routing and different endpoint types.
|
|
28
|
+
|
|
29
|
+
Implements the Factory pattern with Builder pattern elements for
|
|
30
|
+
flexible URL construction.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, base_url: str | None = None):
|
|
34
|
+
"""
|
|
35
|
+
Initialize the webhook URL factory.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
base_url: Base URL for webhook generation. If None, will be determined
|
|
39
|
+
from settings or environment.
|
|
40
|
+
"""
|
|
41
|
+
self.base_url = base_url or self._determine_base_url()
|
|
42
|
+
|
|
43
|
+
def _determine_base_url(self) -> str:
|
|
44
|
+
"""
|
|
45
|
+
Determine the base URL for webhook generation.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Base URL for webhook endpoints
|
|
49
|
+
"""
|
|
50
|
+
# In development, use localhost
|
|
51
|
+
if settings.is_development:
|
|
52
|
+
return f"http://localhost:{settings.port}"
|
|
53
|
+
|
|
54
|
+
# In production, this would be configured via environment
|
|
55
|
+
# For now, use a placeholder that should be configured
|
|
56
|
+
webhook_base_url = getattr(settings, "webhook_base_url", None)
|
|
57
|
+
if webhook_base_url:
|
|
58
|
+
return webhook_base_url
|
|
59
|
+
|
|
60
|
+
# Default fallback (should be configured in production)
|
|
61
|
+
return "https://your-domain.com"
|
|
62
|
+
|
|
63
|
+
def generate_webhook_url(
|
|
64
|
+
self,
|
|
65
|
+
platform: PlatformType,
|
|
66
|
+
tenant_id: str,
|
|
67
|
+
endpoint_type: WebhookEndpointType = WebhookEndpointType.WEBHOOK,
|
|
68
|
+
) -> str:
|
|
69
|
+
"""
|
|
70
|
+
Generate a webhook URL for a specific platform and tenant.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
platform: The messaging platform (WhatsApp, Telegram, etc.)
|
|
74
|
+
tenant_id: The tenant identifier (phone_number_id for WhatsApp)
|
|
75
|
+
endpoint_type: Type of webhook endpoint to generate
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Complete webhook URL for the platform and tenant
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> factory = WebhookURLFactory()
|
|
82
|
+
>>> factory.generate_webhook_url(PlatformType.WHATSAPP, "123456789")
|
|
83
|
+
"https://your-domain.com/webhook/messenger/123456789/whatsapp"
|
|
84
|
+
"""
|
|
85
|
+
platform_name = platform.value.lower()
|
|
86
|
+
|
|
87
|
+
if endpoint_type == WebhookEndpointType.VERIFY:
|
|
88
|
+
return f"{self.base_url}/webhook/messenger/{platform_name}/verify"
|
|
89
|
+
elif endpoint_type == WebhookEndpointType.STATUS:
|
|
90
|
+
return (
|
|
91
|
+
f"{self.base_url}/webhook/messenger/{tenant_id}/{platform_name}/status"
|
|
92
|
+
)
|
|
93
|
+
else: # WEBHOOK (default)
|
|
94
|
+
return f"{self.base_url}/webhook/messenger/{tenant_id}/{platform_name}"
|
|
95
|
+
|
|
96
|
+
def generate_whatsapp_webhook_url(self) -> str:
|
|
97
|
+
"""
|
|
98
|
+
Generate a WhatsApp-specific webhook URL.
|
|
99
|
+
|
|
100
|
+
Convenience method for WhatsApp webhook generation that automatically
|
|
101
|
+
uses the tenant_id from settings (WP_PHONE_ID from .env).
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Complete WhatsApp webhook URL using configured phone number ID
|
|
105
|
+
"""
|
|
106
|
+
return self.generate_webhook_url(PlatformType.WHATSAPP, settings.owner_id)
|
|
107
|
+
|
|
108
|
+
def generate_whatsapp_verify_url(self) -> str:
|
|
109
|
+
"""
|
|
110
|
+
Generate a WhatsApp webhook verification URL.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
WhatsApp webhook verification URL
|
|
114
|
+
"""
|
|
115
|
+
return self.generate_webhook_url(
|
|
116
|
+
PlatformType.WHATSAPP, "", WebhookEndpointType.VERIFY
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def get_supported_platforms(self) -> dict[str, dict[str, str]]:
|
|
120
|
+
"""
|
|
121
|
+
Get all supported platforms and their webhook URL patterns.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Dictionary mapping platform names to their URL patterns
|
|
125
|
+
"""
|
|
126
|
+
patterns = {}
|
|
127
|
+
|
|
128
|
+
for platform in PlatformType:
|
|
129
|
+
platform_name = platform.value.lower()
|
|
130
|
+
patterns[platform_name] = {
|
|
131
|
+
"webhook_pattern": f"/webhook/messenger/{{tenant_id}}/{platform_name}",
|
|
132
|
+
"verify_pattern": f"/webhook/messenger/{platform_name}/verify",
|
|
133
|
+
"status_pattern": f"/webhook/messenger/{{tenant_id}}/{platform_name}/status",
|
|
134
|
+
"example_webhook": self.generate_webhook_url(platform, "TENANT_ID"),
|
|
135
|
+
"example_verify": self.generate_webhook_url(
|
|
136
|
+
platform, "", WebhookEndpointType.VERIFY
|
|
137
|
+
),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return patterns
|
|
141
|
+
|
|
142
|
+
def validate_webhook_url(
|
|
143
|
+
self, url: str, platform: PlatformType, tenant_id: str
|
|
144
|
+
) -> bool:
|
|
145
|
+
"""
|
|
146
|
+
Validate if a URL matches the expected webhook pattern for a platform.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
url: URL to validate
|
|
150
|
+
platform: Expected platform
|
|
151
|
+
tenant_id: Expected tenant ID
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
True if URL matches the expected pattern
|
|
155
|
+
"""
|
|
156
|
+
expected_url = self.generate_webhook_url(platform, tenant_id)
|
|
157
|
+
return url == expected_url
|
|
158
|
+
|
|
159
|
+
def extract_platform_from_url(self, webhook_path: str) -> PlatformType | None:
|
|
160
|
+
"""
|
|
161
|
+
Extract platform type from a webhook URL path.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
webhook_path: Webhook URL path (e.g., "/webhook/messenger/123/whatsapp")
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
PlatformType if found, None otherwise
|
|
168
|
+
"""
|
|
169
|
+
# Expected pattern: /webhook/messenger/{tenant_id}/{platform}
|
|
170
|
+
path_parts = webhook_path.strip("/").split("/")
|
|
171
|
+
|
|
172
|
+
if (
|
|
173
|
+
len(path_parts) >= 4
|
|
174
|
+
and path_parts[0] == "webhook"
|
|
175
|
+
and path_parts[1] == "messenger"
|
|
176
|
+
):
|
|
177
|
+
platform_name = path_parts[3] # platform is at index 3
|
|
178
|
+
try:
|
|
179
|
+
return PlatformType(platform_name.lower())
|
|
180
|
+
except ValueError:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
def extract_tenant_from_url(self, webhook_path: str) -> str | None:
|
|
186
|
+
"""
|
|
187
|
+
Extract tenant ID from a webhook URL path.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
webhook_path: Webhook URL path (e.g., "/webhook/messenger/123/whatsapp")
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Tenant ID if found, None otherwise
|
|
194
|
+
"""
|
|
195
|
+
# Expected pattern: /webhook/messenger/{tenant_id}/{platform}
|
|
196
|
+
path_parts = webhook_path.strip("/").split("/")
|
|
197
|
+
|
|
198
|
+
if (
|
|
199
|
+
len(path_parts) >= 4
|
|
200
|
+
and path_parts[0] == "webhook"
|
|
201
|
+
and path_parts[1] == "messenger"
|
|
202
|
+
):
|
|
203
|
+
return path_parts[2] # tenant_id is at index 2
|
|
204
|
+
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# Global factory instance
|
|
209
|
+
webhook_url_factory = WebhookURLFactory()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def get_webhook_url_factory() -> WebhookURLFactory:
|
|
213
|
+
"""
|
|
214
|
+
Get the global webhook URL factory instance.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
WebhookURLFactory instance
|
|
218
|
+
"""
|
|
219
|
+
return webhook_url_factory
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wappa Factory Module
|
|
3
|
+
|
|
4
|
+
This module provides the plugin-based factory system for building extensible
|
|
5
|
+
Wappa applications. It includes the WappaBuilder class and plugin interfaces
|
|
6
|
+
that allow users to extend FastAPI functionality without modifying core code.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .plugin import WappaPlugin
|
|
10
|
+
from .wappa_builder import WappaBuilder
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"WappaBuilder",
|
|
14
|
+
"WappaPlugin",
|
|
15
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wappa Plugin Protocol
|
|
3
|
+
|
|
4
|
+
Defines the interface that all Wappa plugins must implement for consistent
|
|
5
|
+
integration with the WappaBuilder factory system.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, Protocol
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from fastapi import FastAPI
|
|
12
|
+
|
|
13
|
+
from .wappa_builder import WappaBuilder
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WappaPlugin(Protocol):
|
|
17
|
+
"""
|
|
18
|
+
Universal plugin interface for extending Wappa functionality.
|
|
19
|
+
|
|
20
|
+
All plugins must implement these three lifecycle methods:
|
|
21
|
+
1. configure: Called during WappaBuilder setup to register middleware/routes
|
|
22
|
+
2. startup: Called during FastAPI application startup
|
|
23
|
+
3. shutdown: Called during FastAPI application shutdown
|
|
24
|
+
|
|
25
|
+
This protocol ensures consistent plugin behavior and proper resource management.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def configure(self, builder: "WappaBuilder") -> None:
|
|
29
|
+
"""
|
|
30
|
+
Configure the plugin with the WappaBuilder.
|
|
31
|
+
|
|
32
|
+
This method is called during the build phase, before the FastAPI app
|
|
33
|
+
is created. Use this to register middleware, routes, and other components
|
|
34
|
+
that need to be set up before the app starts.
|
|
35
|
+
|
|
36
|
+
This method is synchronous because it only registers components with the builder.
|
|
37
|
+
Actual async initialization should be done in the startup() method.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
builder: WappaBuilder instance to configure
|
|
41
|
+
"""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
async def startup(self, app: "FastAPI") -> None:
|
|
45
|
+
"""
|
|
46
|
+
Execute plugin startup logic.
|
|
47
|
+
|
|
48
|
+
This method is called during FastAPI application startup. Use this
|
|
49
|
+
for initializing connections, setting up resources, health checks,
|
|
50
|
+
and any other startup tasks.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
app: FastAPI application instance
|
|
54
|
+
"""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
async def shutdown(self, app: "FastAPI") -> None:
|
|
58
|
+
"""
|
|
59
|
+
Execute plugin cleanup logic.
|
|
60
|
+
|
|
61
|
+
This method is called during FastAPI application shutdown. Use this
|
|
62
|
+
for closing connections, cleaning up resources, and any other
|
|
63
|
+
shutdown tasks. Should be the reverse of startup operations.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
app: FastAPI application instance
|
|
67
|
+
"""
|
|
68
|
+
...
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WappaBuilder - Extensible FastAPI Application Factory
|
|
3
|
+
|
|
4
|
+
This module provides the WappaBuilder class that enables users to create
|
|
5
|
+
highly customized FastAPI applications using a plugin-based architecture.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from contextlib import asynccontextmanager
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from fastapi import FastAPI
|
|
13
|
+
|
|
14
|
+
from ..logging.logger import get_app_logger
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .plugin import WappaPlugin
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WappaBuilder:
|
|
21
|
+
"""
|
|
22
|
+
Fluent builder for creating extensible Wappa applications.
|
|
23
|
+
|
|
24
|
+
The WappaBuilder provides a plugin-based architecture that allows users
|
|
25
|
+
to extend FastAPI functionality without modifying core code. It supports:
|
|
26
|
+
|
|
27
|
+
- Plugin system with lifecycle management
|
|
28
|
+
- Priority-based middleware ordering
|
|
29
|
+
- Configurable startup/shutdown hooks
|
|
30
|
+
- Seamless integration with existing Wappa class
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
builder = WappaBuilder()
|
|
34
|
+
app = await (builder
|
|
35
|
+
.add_plugin(DatabasePlugin("postgresql://...", PostgreSQLAdapter()))
|
|
36
|
+
.add_plugin(RedisPlugin())
|
|
37
|
+
.add_middleware(CORSMiddleware, allow_origins=["*"])
|
|
38
|
+
.configure(title="My Wappa App")
|
|
39
|
+
.build())
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self):
|
|
43
|
+
"""Initialize WappaBuilder with empty configuration."""
|
|
44
|
+
self.plugins: list[WappaPlugin] = []
|
|
45
|
+
self.middlewares: list[tuple[type, dict, int]] = [] # (class, kwargs, priority)
|
|
46
|
+
self.routers: list[tuple[Any, dict]] = [] # (router, include_kwargs)
|
|
47
|
+
self.startup_hooks: list[tuple[Callable, int]] = [] # (hook, priority)
|
|
48
|
+
self.shutdown_hooks: list[tuple[Callable, int]] = [] # (hook, priority)
|
|
49
|
+
self.config_overrides: dict[str, Any] = {}
|
|
50
|
+
|
|
51
|
+
def add_plugin(self, plugin: "WappaPlugin") -> "WappaBuilder":
|
|
52
|
+
"""
|
|
53
|
+
Add a plugin to extend functionality.
|
|
54
|
+
|
|
55
|
+
Plugins provide a clean way to add complex functionality like database
|
|
56
|
+
connections, Redis caching, authentication, etc.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
plugin: WappaPlugin instance to add
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Self for method chaining
|
|
63
|
+
"""
|
|
64
|
+
self.plugins.append(plugin)
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def add_middleware(
|
|
68
|
+
self, middleware_class: type, priority: int = 50, **kwargs: Any
|
|
69
|
+
) -> "WappaBuilder":
|
|
70
|
+
"""
|
|
71
|
+
Add middleware to the application with priority ordering.
|
|
72
|
+
|
|
73
|
+
Priority determines execution order:
|
|
74
|
+
- Lower numbers run first (outer middleware)
|
|
75
|
+
- Higher numbers run last (inner middleware)
|
|
76
|
+
- Default priority is 50
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
middleware_class: Middleware class to add
|
|
80
|
+
priority: Execution priority (lower = outer, higher = inner)
|
|
81
|
+
**kwargs: Middleware configuration parameters
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Self for method chaining
|
|
85
|
+
"""
|
|
86
|
+
self.middlewares.append((middleware_class, kwargs, priority))
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def add_router(self, router: Any, **kwargs: Any) -> "WappaBuilder":
|
|
90
|
+
"""
|
|
91
|
+
Add a router to the application.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
router: FastAPI router to include
|
|
95
|
+
**kwargs: Arguments for app.include_router()
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Self for method chaining
|
|
99
|
+
"""
|
|
100
|
+
self.routers.append((router, kwargs))
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def add_startup_hook(self, hook: Callable, priority: int = 50) -> "WappaBuilder":
|
|
104
|
+
"""
|
|
105
|
+
Add a startup hook to unified lifespan management.
|
|
106
|
+
|
|
107
|
+
Hooks are executed during application startup in priority order.
|
|
108
|
+
Lower priority numbers execute first.
|
|
109
|
+
|
|
110
|
+
Priority Guidelines:
|
|
111
|
+
- 10: Core system initialization (logging, sessions)
|
|
112
|
+
- 20: Infrastructure (databases, caches, external services)
|
|
113
|
+
- 30: Application services
|
|
114
|
+
- 50: User hooks (default)
|
|
115
|
+
- 70+: Late initialization hooks
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
hook: Async callable that takes (app: FastAPI) -> None
|
|
119
|
+
priority: Execution priority (lower = runs first)
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Self for method chaining
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
async def my_startup(app: FastAPI):
|
|
126
|
+
print("Starting my service")
|
|
127
|
+
|
|
128
|
+
builder.add_startup_hook(my_startup, priority=30)
|
|
129
|
+
"""
|
|
130
|
+
self.startup_hooks.append((hook, priority))
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
def add_shutdown_hook(self, hook: Callable, priority: int = 50) -> "WappaBuilder":
|
|
134
|
+
"""
|
|
135
|
+
Add a shutdown hook to unified lifespan management.
|
|
136
|
+
|
|
137
|
+
Hooks are executed during application shutdown in reverse priority order.
|
|
138
|
+
Higher priority numbers execute first during shutdown.
|
|
139
|
+
|
|
140
|
+
Priority Guidelines:
|
|
141
|
+
- 90: Core system cleanup (sessions, logging) - runs last
|
|
142
|
+
- 70: Application services cleanup
|
|
143
|
+
- 50: User hooks (default)
|
|
144
|
+
- 30: Application services
|
|
145
|
+
- 20: Infrastructure cleanup (databases, caches)
|
|
146
|
+
- 10: Early cleanup
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
hook: Async callable that takes (app: FastAPI) -> None
|
|
150
|
+
priority: Execution priority (higher = runs first in shutdown)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Self for method chaining
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
async def my_shutdown(app: FastAPI):
|
|
157
|
+
print("Cleaning up my service")
|
|
158
|
+
|
|
159
|
+
builder.add_shutdown_hook(my_shutdown, priority=30)
|
|
160
|
+
"""
|
|
161
|
+
self.shutdown_hooks.append((hook, priority))
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def configure(self, **overrides: Any) -> "WappaBuilder":
|
|
165
|
+
"""
|
|
166
|
+
Override default FastAPI configuration.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
**overrides: FastAPI constructor arguments to override
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Self for method chaining
|
|
173
|
+
"""
|
|
174
|
+
self.config_overrides.update(overrides)
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
def build(self) -> FastAPI:
|
|
178
|
+
"""
|
|
179
|
+
Build the configured FastAPI application following proper FastAPI initialization pattern.
|
|
180
|
+
|
|
181
|
+
This method follows the exact pattern you specified:
|
|
182
|
+
1. Configure plugins (sync setup only)
|
|
183
|
+
2. Create FastAPI app with lifespan and config
|
|
184
|
+
3. Add all middleware via app.add_middleware()
|
|
185
|
+
4. Include all routers via app.include_router()
|
|
186
|
+
|
|
187
|
+
Only async operations happen in the lifespan (startup/shutdown hooks).
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
FastAPI application with properly configured plugins
|
|
191
|
+
"""
|
|
192
|
+
logger = get_app_logger()
|
|
193
|
+
logger.debug(f"🏗️ Building FastAPI app with {len(self.plugins)} plugins")
|
|
194
|
+
|
|
195
|
+
# Step 1: Configure plugins (sync setup only - middleware/router registration)
|
|
196
|
+
if self.plugins:
|
|
197
|
+
logger.debug(f"⚙️ Configuring {len(self.plugins)} plugins synchronously...")
|
|
198
|
+
for plugin in self.plugins:
|
|
199
|
+
plugin.configure(self) # Synchronous configuration
|
|
200
|
+
|
|
201
|
+
logger.info(
|
|
202
|
+
f"✅ Plugin configuration complete - registered {len(self.middlewares)} middlewares, "
|
|
203
|
+
f"{len(self.routers)} routers, {len(self.startup_hooks)} startup hooks, "
|
|
204
|
+
f"{len(self.shutdown_hooks)} shutdown hooks"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Create unified lifespan (only for async startup/shutdown hooks)
|
|
208
|
+
from contextlib import asynccontextmanager
|
|
209
|
+
|
|
210
|
+
@asynccontextmanager
|
|
211
|
+
async def unified_lifespan(app: FastAPI):
|
|
212
|
+
try:
|
|
213
|
+
# Startup phase - execute async startup hooks only
|
|
214
|
+
logger.debug("🚀 Starting unified lifespan startup phase...")
|
|
215
|
+
await self._execute_all_startup_hooks(app)
|
|
216
|
+
logger.info("✅ All startup hooks completed successfully")
|
|
217
|
+
yield
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.error(f"❌ Error during startup phase: {e}", exc_info=True)
|
|
220
|
+
raise
|
|
221
|
+
finally:
|
|
222
|
+
# Shutdown phase - execute async shutdown hooks
|
|
223
|
+
logger.debug("🛑 Starting unified lifespan shutdown phase...")
|
|
224
|
+
await self._execute_all_shutdown_hooks(app)
|
|
225
|
+
logger.info("✅ All shutdown hooks completed")
|
|
226
|
+
|
|
227
|
+
# Step 2: Create FastAPI app with lifespan and config
|
|
228
|
+
default_config = {
|
|
229
|
+
"title": "Wappa Application",
|
|
230
|
+
"description": "WhatsApp Business application built with Wappa framework",
|
|
231
|
+
"version": "1.0.0",
|
|
232
|
+
"lifespan": unified_lifespan,
|
|
233
|
+
}
|
|
234
|
+
default_config.update(self.config_overrides)
|
|
235
|
+
|
|
236
|
+
app = FastAPI(**default_config)
|
|
237
|
+
logger.debug(f"Created FastAPI app: {default_config['title']}")
|
|
238
|
+
|
|
239
|
+
# Step 3: Add all middleware via app.add_middleware()
|
|
240
|
+
# Sort by priority (reverse order because FastAPI adds middleware in reverse)
|
|
241
|
+
sorted_middlewares = sorted(self.middlewares, key=lambda x: x[2], reverse=True)
|
|
242
|
+
for middleware_class, kwargs, priority in sorted_middlewares:
|
|
243
|
+
app.add_middleware(middleware_class, **kwargs)
|
|
244
|
+
logger.debug(f"Added middleware {middleware_class.__name__} (priority: {priority})")
|
|
245
|
+
|
|
246
|
+
# Step 4: Include all routers via app.include_router()
|
|
247
|
+
for router, kwargs in self.routers:
|
|
248
|
+
app.include_router(router, **kwargs)
|
|
249
|
+
logger.debug(f"Included router with config: {kwargs}")
|
|
250
|
+
|
|
251
|
+
logger.info(
|
|
252
|
+
f"🎉 WappaBuilder created FastAPI app: {len(self.plugins)} plugins, "
|
|
253
|
+
f"{len(self.middlewares)} middlewares, {len(self.routers)} routers"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return app
|
|
257
|
+
|
|
258
|
+
async def _execute_all_startup_hooks(self, app: FastAPI) -> None:
|
|
259
|
+
"""
|
|
260
|
+
Execute all startup hooks in unified priority order.
|
|
261
|
+
|
|
262
|
+
This replaces the old plugin-specific startup execution with a unified
|
|
263
|
+
approach where all hooks (from plugins and user code) are executed
|
|
264
|
+
in a single priority-ordered sequence.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
app: FastAPI application instance
|
|
268
|
+
"""
|
|
269
|
+
logger = get_app_logger()
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
# Execute all startup hooks in priority order (10, 20, 30, ...)
|
|
273
|
+
sorted_hooks = sorted(self.startup_hooks, key=lambda x: x[1])
|
|
274
|
+
|
|
275
|
+
if not sorted_hooks:
|
|
276
|
+
logger.debug("No startup hooks registered")
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
logger.debug(f"Executing {len(sorted_hooks)} startup hooks in priority order...")
|
|
280
|
+
|
|
281
|
+
for hook, priority in sorted_hooks:
|
|
282
|
+
hook_name = getattr(hook, "__name__", "anonymous_hook")
|
|
283
|
+
logger.debug(f"⚡ Executing startup hook: {hook_name} (priority: {priority})")
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
await hook(app)
|
|
287
|
+
logger.debug(f"✅ Startup hook {hook_name} completed")
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.error(f"❌ Startup hook {hook_name} failed: {e}", exc_info=True)
|
|
290
|
+
raise # Re-raise to fail fast
|
|
291
|
+
|
|
292
|
+
except Exception as e:
|
|
293
|
+
logger.error(f"❌ Startup sequence failed: {e}", exc_info=True)
|
|
294
|
+
raise
|
|
295
|
+
|
|
296
|
+
async def _execute_all_shutdown_hooks(self, app: FastAPI) -> None:
|
|
297
|
+
"""
|
|
298
|
+
Execute all shutdown hooks in reverse priority order.
|
|
299
|
+
|
|
300
|
+
This replaces the old plugin-specific shutdown execution with a unified
|
|
301
|
+
approach where all hooks are executed in reverse priority order,
|
|
302
|
+
with error isolation to prevent shutdown cascading failures.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
app: FastAPI application instance
|
|
306
|
+
"""
|
|
307
|
+
logger = get_app_logger()
|
|
308
|
+
|
|
309
|
+
# Execute shutdown hooks in reverse priority order (90, 70, 50, 30, 20, 10)
|
|
310
|
+
sorted_hooks = sorted(self.shutdown_hooks, key=lambda x: x[1], reverse=True)
|
|
311
|
+
|
|
312
|
+
if not sorted_hooks:
|
|
313
|
+
logger.debug("No shutdown hooks registered")
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
logger.debug(f"Executing {len(sorted_hooks)} shutdown hooks in reverse priority order...")
|
|
317
|
+
|
|
318
|
+
for hook, priority in sorted_hooks:
|
|
319
|
+
hook_name = getattr(hook, "__name__", "anonymous_hook")
|
|
320
|
+
try:
|
|
321
|
+
logger.debug(f"🛑 Executing shutdown hook: {hook_name} (priority: {priority})")
|
|
322
|
+
await hook(app)
|
|
323
|
+
logger.debug(f"✅ Shutdown hook {hook_name} completed")
|
|
324
|
+
except Exception as e:
|
|
325
|
+
# Don't re-raise in shutdown - log and continue with other hooks
|
|
326
|
+
logger.error(f"❌ Error in shutdown hook {hook_name}: {e}", exc_info=True)
|