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.

Files changed (211) hide show
  1. wappa/__init__.py +85 -0
  2. wappa/api/__init__.py +1 -0
  3. wappa/api/controllers/__init__.py +10 -0
  4. wappa/api/controllers/webhook_controller.py +441 -0
  5. wappa/api/dependencies/__init__.py +15 -0
  6. wappa/api/dependencies/whatsapp_dependencies.py +220 -0
  7. wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
  8. wappa/api/middleware/__init__.py +7 -0
  9. wappa/api/middleware/error_handler.py +158 -0
  10. wappa/api/middleware/owner.py +99 -0
  11. wappa/api/middleware/request_logging.py +184 -0
  12. wappa/api/routes/__init__.py +6 -0
  13. wappa/api/routes/health.py +102 -0
  14. wappa/api/routes/webhooks.py +211 -0
  15. wappa/api/routes/whatsapp/__init__.py +15 -0
  16. wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
  17. wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
  18. wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
  19. wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
  20. wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
  21. wappa/api/routes/whatsapp_combined.py +35 -0
  22. wappa/cli/__init__.py +9 -0
  23. wappa/cli/main.py +199 -0
  24. wappa/core/__init__.py +6 -0
  25. wappa/core/config/__init__.py +5 -0
  26. wappa/core/config/settings.py +161 -0
  27. wappa/core/events/__init__.py +41 -0
  28. wappa/core/events/default_handlers.py +642 -0
  29. wappa/core/events/event_dispatcher.py +244 -0
  30. wappa/core/events/event_handler.py +247 -0
  31. wappa/core/events/webhook_factory.py +219 -0
  32. wappa/core/factory/__init__.py +15 -0
  33. wappa/core/factory/plugin.py +68 -0
  34. wappa/core/factory/wappa_builder.py +326 -0
  35. wappa/core/logging/__init__.py +5 -0
  36. wappa/core/logging/context.py +100 -0
  37. wappa/core/logging/logger.py +343 -0
  38. wappa/core/plugins/__init__.py +34 -0
  39. wappa/core/plugins/auth_plugin.py +169 -0
  40. wappa/core/plugins/cors_plugin.py +128 -0
  41. wappa/core/plugins/custom_middleware_plugin.py +182 -0
  42. wappa/core/plugins/database_plugin.py +235 -0
  43. wappa/core/plugins/rate_limit_plugin.py +183 -0
  44. wappa/core/plugins/redis_plugin.py +224 -0
  45. wappa/core/plugins/wappa_core_plugin.py +261 -0
  46. wappa/core/plugins/webhook_plugin.py +253 -0
  47. wappa/core/types.py +108 -0
  48. wappa/core/wappa_app.py +546 -0
  49. wappa/database/__init__.py +18 -0
  50. wappa/database/adapter.py +107 -0
  51. wappa/database/adapters/__init__.py +17 -0
  52. wappa/database/adapters/mysql_adapter.py +187 -0
  53. wappa/database/adapters/postgresql_adapter.py +169 -0
  54. wappa/database/adapters/sqlite_adapter.py +174 -0
  55. wappa/domain/__init__.py +28 -0
  56. wappa/domain/builders/__init__.py +5 -0
  57. wappa/domain/builders/message_builder.py +189 -0
  58. wappa/domain/entities/__init__.py +5 -0
  59. wappa/domain/enums/messenger_platform.py +123 -0
  60. wappa/domain/factories/__init__.py +6 -0
  61. wappa/domain/factories/media_factory.py +450 -0
  62. wappa/domain/factories/message_factory.py +497 -0
  63. wappa/domain/factories/messenger_factory.py +244 -0
  64. wappa/domain/interfaces/__init__.py +32 -0
  65. wappa/domain/interfaces/base_repository.py +94 -0
  66. wappa/domain/interfaces/cache_factory.py +85 -0
  67. wappa/domain/interfaces/cache_interface.py +199 -0
  68. wappa/domain/interfaces/expiry_repository.py +68 -0
  69. wappa/domain/interfaces/media_interface.py +311 -0
  70. wappa/domain/interfaces/messaging_interface.py +523 -0
  71. wappa/domain/interfaces/pubsub_repository.py +151 -0
  72. wappa/domain/interfaces/repository_factory.py +108 -0
  73. wappa/domain/interfaces/shared_state_repository.py +122 -0
  74. wappa/domain/interfaces/state_repository.py +123 -0
  75. wappa/domain/interfaces/tables_repository.py +215 -0
  76. wappa/domain/interfaces/user_repository.py +114 -0
  77. wappa/domain/interfaces/webhooks/__init__.py +1 -0
  78. wappa/domain/models/media_result.py +110 -0
  79. wappa/domain/models/platforms/__init__.py +15 -0
  80. wappa/domain/models/platforms/platform_config.py +104 -0
  81. wappa/domain/services/__init__.py +11 -0
  82. wappa/domain/services/tenant_credentials_service.py +56 -0
  83. wappa/messaging/__init__.py +7 -0
  84. wappa/messaging/whatsapp/__init__.py +1 -0
  85. wappa/messaging/whatsapp/client/__init__.py +5 -0
  86. wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
  87. wappa/messaging/whatsapp/handlers/__init__.py +13 -0
  88. wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
  89. wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
  90. wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
  91. wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
  92. wappa/messaging/whatsapp/messenger/__init__.py +5 -0
  93. wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
  94. wappa/messaging/whatsapp/models/__init__.py +61 -0
  95. wappa/messaging/whatsapp/models/basic_models.py +65 -0
  96. wappa/messaging/whatsapp/models/interactive_models.py +287 -0
  97. wappa/messaging/whatsapp/models/media_models.py +215 -0
  98. wappa/messaging/whatsapp/models/specialized_models.py +304 -0
  99. wappa/messaging/whatsapp/models/template_models.py +261 -0
  100. wappa/persistence/cache_factory.py +93 -0
  101. wappa/persistence/json/__init__.py +14 -0
  102. wappa/persistence/json/cache_adapters.py +271 -0
  103. wappa/persistence/json/handlers/__init__.py +1 -0
  104. wappa/persistence/json/handlers/state_handler.py +250 -0
  105. wappa/persistence/json/handlers/table_handler.py +263 -0
  106. wappa/persistence/json/handlers/user_handler.py +213 -0
  107. wappa/persistence/json/handlers/utils/__init__.py +1 -0
  108. wappa/persistence/json/handlers/utils/file_manager.py +153 -0
  109. wappa/persistence/json/handlers/utils/key_factory.py +11 -0
  110. wappa/persistence/json/handlers/utils/serialization.py +121 -0
  111. wappa/persistence/json/json_cache_factory.py +76 -0
  112. wappa/persistence/json/storage_manager.py +285 -0
  113. wappa/persistence/memory/__init__.py +14 -0
  114. wappa/persistence/memory/cache_adapters.py +271 -0
  115. wappa/persistence/memory/handlers/__init__.py +1 -0
  116. wappa/persistence/memory/handlers/state_handler.py +250 -0
  117. wappa/persistence/memory/handlers/table_handler.py +280 -0
  118. wappa/persistence/memory/handlers/user_handler.py +213 -0
  119. wappa/persistence/memory/handlers/utils/__init__.py +1 -0
  120. wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
  121. wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
  122. wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
  123. wappa/persistence/memory/memory_cache_factory.py +76 -0
  124. wappa/persistence/memory/storage_manager.py +235 -0
  125. wappa/persistence/redis/README.md +699 -0
  126. wappa/persistence/redis/__init__.py +11 -0
  127. wappa/persistence/redis/cache_adapters.py +285 -0
  128. wappa/persistence/redis/ops.py +880 -0
  129. wappa/persistence/redis/redis_cache_factory.py +71 -0
  130. wappa/persistence/redis/redis_client.py +231 -0
  131. wappa/persistence/redis/redis_handler/__init__.py +26 -0
  132. wappa/persistence/redis/redis_handler/state_handler.py +176 -0
  133. wappa/persistence/redis/redis_handler/table.py +158 -0
  134. wappa/persistence/redis/redis_handler/user.py +138 -0
  135. wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
  136. wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
  137. wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
  138. wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
  139. wappa/persistence/redis/redis_manager.py +189 -0
  140. wappa/processors/__init__.py +6 -0
  141. wappa/processors/base_processor.py +262 -0
  142. wappa/processors/factory.py +550 -0
  143. wappa/processors/whatsapp_processor.py +810 -0
  144. wappa/schemas/__init__.py +6 -0
  145. wappa/schemas/core/__init__.py +71 -0
  146. wappa/schemas/core/base_message.py +499 -0
  147. wappa/schemas/core/base_status.py +322 -0
  148. wappa/schemas/core/base_webhook.py +312 -0
  149. wappa/schemas/core/types.py +253 -0
  150. wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
  151. wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
  152. wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
  153. wappa/schemas/factory.py +754 -0
  154. wappa/schemas/webhooks/__init__.py +3 -0
  155. wappa/schemas/whatsapp/__init__.py +6 -0
  156. wappa/schemas/whatsapp/base_models.py +285 -0
  157. wappa/schemas/whatsapp/message_types/__init__.py +93 -0
  158. wappa/schemas/whatsapp/message_types/audio.py +350 -0
  159. wappa/schemas/whatsapp/message_types/button.py +267 -0
  160. wappa/schemas/whatsapp/message_types/contact.py +464 -0
  161. wappa/schemas/whatsapp/message_types/document.py +421 -0
  162. wappa/schemas/whatsapp/message_types/errors.py +195 -0
  163. wappa/schemas/whatsapp/message_types/image.py +424 -0
  164. wappa/schemas/whatsapp/message_types/interactive.py +430 -0
  165. wappa/schemas/whatsapp/message_types/location.py +416 -0
  166. wappa/schemas/whatsapp/message_types/order.py +372 -0
  167. wappa/schemas/whatsapp/message_types/reaction.py +271 -0
  168. wappa/schemas/whatsapp/message_types/sticker.py +328 -0
  169. wappa/schemas/whatsapp/message_types/system.py +317 -0
  170. wappa/schemas/whatsapp/message_types/text.py +411 -0
  171. wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
  172. wappa/schemas/whatsapp/message_types/video.py +344 -0
  173. wappa/schemas/whatsapp/status_models.py +479 -0
  174. wappa/schemas/whatsapp/validators.py +454 -0
  175. wappa/schemas/whatsapp/webhook_container.py +438 -0
  176. wappa/webhooks/__init__.py +17 -0
  177. wappa/webhooks/core/__init__.py +71 -0
  178. wappa/webhooks/core/base_message.py +499 -0
  179. wappa/webhooks/core/base_status.py +322 -0
  180. wappa/webhooks/core/base_webhook.py +312 -0
  181. wappa/webhooks/core/types.py +253 -0
  182. wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
  183. wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
  184. wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
  185. wappa/webhooks/factory.py +754 -0
  186. wappa/webhooks/whatsapp/__init__.py +6 -0
  187. wappa/webhooks/whatsapp/base_models.py +285 -0
  188. wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
  189. wappa/webhooks/whatsapp/message_types/audio.py +350 -0
  190. wappa/webhooks/whatsapp/message_types/button.py +267 -0
  191. wappa/webhooks/whatsapp/message_types/contact.py +464 -0
  192. wappa/webhooks/whatsapp/message_types/document.py +421 -0
  193. wappa/webhooks/whatsapp/message_types/errors.py +195 -0
  194. wappa/webhooks/whatsapp/message_types/image.py +424 -0
  195. wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
  196. wappa/webhooks/whatsapp/message_types/location.py +416 -0
  197. wappa/webhooks/whatsapp/message_types/order.py +372 -0
  198. wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
  199. wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
  200. wappa/webhooks/whatsapp/message_types/system.py +317 -0
  201. wappa/webhooks/whatsapp/message_types/text.py +411 -0
  202. wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
  203. wappa/webhooks/whatsapp/message_types/video.py +344 -0
  204. wappa/webhooks/whatsapp/status_models.py +479 -0
  205. wappa/webhooks/whatsapp/validators.py +454 -0
  206. wappa/webhooks/whatsapp/webhook_container.py +438 -0
  207. wappa-0.1.0.dist-info/METADATA +269 -0
  208. wappa-0.1.0.dist-info/RECORD +211 -0
  209. wappa-0.1.0.dist-info/WHEEL +4 -0
  210. wappa-0.1.0.dist-info/entry_points.txt +2 -0
  211. wappa-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,220 @@
1
+ """
2
+ WhatsApp messaging dependency injection.
3
+
4
+ Provides dependency injection for WhatsApp messaging services including
5
+ factory pattern, client management, and messenger implementations.
6
+ """
7
+
8
+ from fastapi import Depends, Request
9
+
10
+ # Note: Using context-based tenant access instead of request-based
11
+ from wappa.core.logging.logger import get_logger
12
+ from wappa.domain.builders.message_builder import MessageBuilder
13
+ from wappa.domain.factories.message_factory import (
14
+ MessageFactory,
15
+ WhatsAppMessageFactory,
16
+ )
17
+ from wappa.domain.interfaces.messaging_interface import IMessenger
18
+ from wappa.domain.services.tenant_credentials_service import TenantCredentialsService
19
+ from wappa.messaging.whatsapp.client.whatsapp_client import WhatsAppClient
20
+ from wappa.messaging.whatsapp.handlers.whatsapp_interactive_handler import (
21
+ WhatsAppInteractiveHandler,
22
+ )
23
+ from wappa.messaging.whatsapp.handlers.whatsapp_media_handler import (
24
+ WhatsAppMediaHandler,
25
+ )
26
+ from wappa.messaging.whatsapp.handlers.whatsapp_specialized_handler import (
27
+ WhatsAppSpecializedHandler,
28
+ )
29
+ from wappa.messaging.whatsapp.handlers.whatsapp_template_handler import (
30
+ WhatsAppTemplateHandler,
31
+ )
32
+ from wappa.messaging.whatsapp.messenger.whatsapp_messenger import WhatsAppMessenger
33
+
34
+
35
+ async def get_whatsapp_message_factory() -> MessageFactory:
36
+ """Get WhatsApp message factory.
37
+
38
+ Returns:
39
+ MessageFactory implementation for WhatsApp platform
40
+ """
41
+ return WhatsAppMessageFactory()
42
+
43
+
44
+ async def get_whatsapp_client(request: Request) -> WhatsAppClient:
45
+ """Get configured WhatsApp client with tenant-specific credentials.
46
+
47
+ Args:
48
+ request: FastAPI request object containing HTTP session
49
+
50
+ Returns:
51
+ Configured WhatsApp client with persistent session and tenant credentials
52
+
53
+ Raises:
54
+ ValueError: If tenant credentials are invalid
55
+ """
56
+ from wappa.core.logging.context import get_current_tenant_context
57
+
58
+ # Get persistent HTTP session from app state (created in main.py lifespan)
59
+ session = request.app.state.http_session
60
+
61
+ # Get tenant ID from context (set by webhook processing)
62
+ tenant_id = get_current_tenant_context()
63
+ if not tenant_id:
64
+ raise ValueError("No tenant context available - webhook processing required")
65
+
66
+ # Get tenant-specific access token (future: from database)
67
+ access_token = TenantCredentialsService.get_whatsapp_access_token(tenant_id)
68
+
69
+ # Validate tenant
70
+ if not TenantCredentialsService.validate_tenant(tenant_id):
71
+ raise ValueError(f"Invalid or inactive tenant: {tenant_id}")
72
+
73
+ # Create tenant-aware logger
74
+ logger = get_logger(__name__)
75
+
76
+ # Create WhatsApp client with dependency injection
77
+ client = WhatsAppClient(
78
+ session=session,
79
+ access_token=access_token,
80
+ phone_number_id=tenant_id, # tenant_id IS the phone_number_id
81
+ logger=logger,
82
+ )
83
+
84
+ return client
85
+
86
+
87
+ async def get_whatsapp_media_handler(
88
+ client: WhatsAppClient = Depends(get_whatsapp_client),
89
+ ) -> WhatsAppMediaHandler:
90
+ """Get configured WhatsApp media handler with tenant-specific context.
91
+
92
+ Args:
93
+ client: Configured WhatsApp client with persistent session
94
+
95
+ Returns:
96
+ Configured WhatsApp media handler for upload/download operations
97
+ """
98
+ from wappa.core.logging.context import get_current_tenant_context
99
+
100
+ tenant_id = get_current_tenant_context()
101
+ if not tenant_id:
102
+ raise ValueError("No tenant context available")
103
+ return WhatsAppMediaHandler(client=client, tenant_id=tenant_id)
104
+
105
+
106
+ async def get_whatsapp_interactive_handler(
107
+ client: WhatsAppClient = Depends(get_whatsapp_client),
108
+ ) -> WhatsAppInteractiveHandler:
109
+ """Get configured WhatsApp interactive handler with tenant-specific context.
110
+
111
+ Args:
112
+ client: Configured WhatsApp client with persistent session
113
+
114
+ Returns:
115
+ Configured WhatsApp interactive handler for button/list/CTA operations
116
+ """
117
+ from wappa.core.logging.context import get_current_tenant_context
118
+
119
+ tenant_id = get_current_tenant_context()
120
+ if not tenant_id:
121
+ raise ValueError("No tenant context available")
122
+ return WhatsAppInteractiveHandler(client=client, tenant_id=tenant_id)
123
+
124
+
125
+ async def get_whatsapp_template_handler(
126
+ client: WhatsAppClient = Depends(get_whatsapp_client),
127
+ ) -> WhatsAppTemplateHandler:
128
+ """Get configured WhatsApp template handler with tenant-specific context.
129
+
130
+ Args:
131
+ client: Configured WhatsApp client with persistent session
132
+
133
+ Returns:
134
+ Configured WhatsApp template handler for business template operations
135
+ """
136
+ from wappa.core.logging.context import get_current_tenant_context
137
+
138
+ tenant_id = get_current_tenant_context()
139
+ if not tenant_id:
140
+ raise ValueError("No tenant context available")
141
+ return WhatsAppTemplateHandler(client=client, tenant_id=tenant_id)
142
+
143
+
144
+ async def get_whatsapp_specialized_handler(
145
+ client: WhatsAppClient = Depends(get_whatsapp_client),
146
+ ) -> WhatsAppSpecializedHandler:
147
+ """Get configured WhatsApp specialized handler with tenant-specific context.
148
+
149
+ Args:
150
+ client: Configured WhatsApp client with persistent session
151
+
152
+ Returns:
153
+ Configured WhatsApp specialized handler for contact and location operations
154
+ """
155
+ from wappa.core.logging.context import get_current_tenant_context
156
+
157
+ tenant_id = get_current_tenant_context()
158
+ if not tenant_id:
159
+ raise ValueError("No tenant context available")
160
+ return WhatsAppSpecializedHandler(client=client, tenant_id=tenant_id)
161
+
162
+
163
+ async def get_whatsapp_messenger(
164
+ client: WhatsAppClient = Depends(get_whatsapp_client),
165
+ media_handler: WhatsAppMediaHandler = Depends(get_whatsapp_media_handler),
166
+ interactive_handler: WhatsAppInteractiveHandler = Depends(
167
+ get_whatsapp_interactive_handler
168
+ ),
169
+ template_handler: WhatsAppTemplateHandler = Depends(get_whatsapp_template_handler),
170
+ specialized_handler: WhatsAppSpecializedHandler = Depends(
171
+ get_whatsapp_specialized_handler
172
+ ),
173
+ ) -> IMessenger:
174
+ """Get unified WhatsApp messenger implementation with complete functionality.
175
+
176
+ Args:
177
+ client: Configured WhatsApp client
178
+ media_handler: Configured media handler for upload operations
179
+ interactive_handler: Configured interactive handler for button/list/CTA operations
180
+ template_handler: Configured template handler for business template operations
181
+ specialized_handler: Configured specialized handler for contact/location operations
182
+
183
+ Returns:
184
+ Complete IMessenger implementation for WhatsApp messaging (text + media + interactive + template + specialized)
185
+ """
186
+ from wappa.core.logging.context import get_current_tenant_context
187
+
188
+ tenant_id = get_current_tenant_context()
189
+ if not tenant_id:
190
+ raise ValueError("No tenant context available")
191
+
192
+ return WhatsAppMessenger(
193
+ client=client,
194
+ media_handler=media_handler,
195
+ interactive_handler=interactive_handler,
196
+ template_handler=template_handler,
197
+ specialized_handler=specialized_handler,
198
+ tenant_id=tenant_id,
199
+ )
200
+
201
+
202
+ async def get_message_builder(
203
+ factory: MessageFactory = Depends(get_whatsapp_message_factory),
204
+ ) -> MessageBuilder:
205
+ """Get message builder for fluent message construction.
206
+
207
+ Args:
208
+ factory: Message factory for creating platform-specific payloads
209
+
210
+ Returns:
211
+ MessageBuilder instance for fluent message construction
212
+
213
+ Note: The recipient should be set when using the builder
214
+ """
215
+
216
+ # Return a builder factory function since recipient is set per message
217
+ def create_builder(recipient: str) -> MessageBuilder:
218
+ return MessageBuilder(factory, recipient)
219
+
220
+ return create_builder
@@ -0,0 +1,26 @@
1
+ """
2
+ WhatsApp media messaging dependency injection.
3
+
4
+ Provides dependency injection for WhatsApp media services including
5
+ media handlers, media messengers, and media factories.
6
+ """
7
+
8
+ from wappa.domain.factories.media_factory import MediaFactory, WhatsAppMediaFactory
9
+
10
+ # WhatsAppMediaMessenger removed - using unified WhatsAppMessenger instead
11
+
12
+
13
+ async def get_whatsapp_media_factory() -> MediaFactory:
14
+ """Get WhatsApp media factory.
15
+
16
+ Returns:
17
+ MediaFactory implementation for WhatsApp platform
18
+ """
19
+ return WhatsAppMediaFactory()
20
+
21
+
22
+ # get_whatsapp_media_handler moved to whatsapp_dependencies.py to eliminate duplication
23
+
24
+
25
+ # WhatsAppMediaMessenger dependency removed - using unified WhatsAppMessenger from whatsapp_dependencies.py instead
26
+ # This eliminates DRY violation and architectural redundancy
@@ -0,0 +1,7 @@
1
+ """Middleware module for Wappa API."""
2
+
3
+ from .error_handler import ErrorHandlerMiddleware
4
+ from .owner import OwnerMiddleware
5
+ from .request_logging import RequestLoggingMiddleware
6
+
7
+ __all__ = ["ErrorHandlerMiddleware", "RequestLoggingMiddleware", "OwnerMiddleware"]
@@ -0,0 +1,158 @@
1
+ """
2
+ Global error handling middleware with tenant and context awareness.
3
+
4
+ Provides structured error responses and comprehensive logging for Wappa framework.
5
+ """
6
+
7
+ import traceback
8
+ from typing import Any
9
+
10
+ from fastapi import HTTPException, Request
11
+ from fastapi.responses import JSONResponse
12
+ from starlette.middleware.base import BaseHTTPMiddleware
13
+ from starlette.responses import Response
14
+
15
+ from wappa.core.logging.logger import get_logger
16
+
17
+
18
+ class ErrorHandlerMiddleware(BaseHTTPMiddleware):
19
+ """
20
+ Global error handling middleware with tenant-aware logging.
21
+
22
+ Catches all unhandled exceptions and provides structured error responses
23
+ while maintaining security by not exposing internal details in production.
24
+ """
25
+
26
+ async def dispatch(self, request: Request, call_next) -> Response:
27
+ """Process request with comprehensive error handling."""
28
+ try:
29
+ response = await call_next(request)
30
+ return response
31
+
32
+ except HTTPException as http_exc:
33
+ # HTTP exceptions are handled by FastAPI, but we log them with context
34
+ await self._log_http_exception(request, http_exc)
35
+ raise # Re-raise to let FastAPI handle the response
36
+
37
+ except Exception as exc:
38
+ # Handle unexpected exceptions
39
+ return await self._handle_unexpected_exception(request, exc)
40
+
41
+ async def _log_http_exception(self, request: Request, exc: HTTPException) -> None:
42
+ """Log HTTP exceptions with tenant context."""
43
+ tenant_id = getattr(request.state, "tenant_id", "unknown")
44
+ logger = get_logger(__name__)
45
+
46
+ # Extract user context from request if available
47
+ user_id = getattr(request.state, "user_id", "unknown")
48
+
49
+ logger.warning(
50
+ f"HTTP {exc.status_code} - {request.method} {request.url.path} - "
51
+ f"Detail: {exc.detail}"
52
+ )
53
+
54
+ async def _handle_unexpected_exception(
55
+ self, request: Request, exc: Exception
56
+ ) -> JSONResponse:
57
+ """Handle unexpected exceptions with proper logging and response."""
58
+ tenant_id = getattr(request.state, "tenant_id", "unknown")
59
+ user_id = getattr(request.state, "user_id", "unknown")
60
+
61
+ # Get context-aware logger
62
+ logger = get_logger(__name__)
63
+
64
+ # Log the full exception with context
65
+ logger.error(
66
+ f"Unhandled exception in {request.method} {request.url.path}: {exc}",
67
+ exc_info=True,
68
+ )
69
+
70
+ # Determine error response based on environment
71
+ if self._is_webhook_endpoint(request.url.path):
72
+ # Webhook endpoints need specific error handling
73
+ return await self._create_webhook_error_response(exc)
74
+ else:
75
+ # Regular API endpoints
76
+ return await self._create_api_error_response(exc)
77
+
78
+ def _is_webhook_endpoint(self, path: str) -> bool:
79
+ """Check if the request is to a webhook endpoint."""
80
+ return path.startswith("/webhook/")
81
+
82
+ async def _create_webhook_error_response(self, exc: Exception) -> JSONResponse:
83
+ """
84
+ Create error response for webhook endpoints.
85
+
86
+ Webhook providers expect specific response formats and status codes.
87
+ """
88
+ error_response = {
89
+ "status": "error",
90
+ "message": "Webhook processing failed",
91
+ "type": "webhook_error",
92
+ }
93
+
94
+ # In development, add more details
95
+ from wappa.core.config.settings import settings
96
+
97
+ if settings.is_development:
98
+ error_response["debug"] = {
99
+ "exception_type": type(exc).__name__,
100
+ "exception_message": str(exc),
101
+ }
102
+
103
+ return JSONResponse(status_code=500, content=error_response)
104
+
105
+ async def _create_api_error_response(self, exc: Exception) -> JSONResponse:
106
+ """Create error response for regular API endpoints."""
107
+ from wappa.core.config.settings import settings
108
+
109
+ # Base error response
110
+ error_response: dict[str, Any] = {
111
+ "detail": "Internal server error",
112
+ "type": "internal_error",
113
+ "timestamp": self._get_current_timestamp(),
114
+ }
115
+
116
+ # Add development-specific debugging information
117
+ if settings.is_development:
118
+ error_response["debug"] = {
119
+ "exception_type": type(exc).__name__,
120
+ "exception_message": str(exc),
121
+ "traceback": traceback.format_exc().split("\n"),
122
+ }
123
+
124
+ return JSONResponse(status_code=500, content=error_response)
125
+
126
+ def _get_current_timestamp(self) -> float:
127
+ """Get current timestamp for error responses."""
128
+ import time
129
+
130
+ return time.time()
131
+
132
+
133
+ class ValidationErrorHandler:
134
+ """
135
+ Custom handler for Pydantic validation errors.
136
+
137
+ Provides more user-friendly validation error messages.
138
+ """
139
+
140
+ @staticmethod
141
+ def format_validation_error(exc: any) -> dict[str, Any]:
142
+ """Format Pydantic validation errors for API responses."""
143
+ errors = []
144
+
145
+ for error in exc.errors():
146
+ errors.append(
147
+ {
148
+ "field": " -> ".join(str(loc) for loc in error["loc"]),
149
+ "message": error["msg"],
150
+ "type": error["type"],
151
+ }
152
+ )
153
+
154
+ return {
155
+ "detail": "Validation failed",
156
+ "type": "validation_error",
157
+ "errors": errors,
158
+ }
@@ -0,0 +1,99 @@
1
+ """
2
+ Owner middleware for extracting owner ID from webhook URLs.
3
+
4
+ Simple middleware that extracts the owner_id from webhook URL paths and sets it in context.
5
+ This replaces the over-complicated tenant middleware with a focused single-purpose solution.
6
+ """
7
+
8
+ from fastapi import HTTPException, Request
9
+ from starlette.middleware.base import BaseHTTPMiddleware
10
+ from starlette.responses import Response
11
+
12
+ from wappa.core.config.settings import settings
13
+ from wappa.core.logging.context import set_request_context
14
+ from wappa.core.logging.logger import get_logger
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ class OwnerMiddleware(BaseHTTPMiddleware):
20
+ """
21
+ Middleware to extract owner_id from webhook URLs and set in context.
22
+
23
+ URL Pattern: /webhook/messenger/{owner_id}/whatsapp
24
+ Purpose: Extract owner_id and set it in the context system.
25
+
26
+ That's it. Nothing more.
27
+ """
28
+
29
+ async def dispatch(self, request: Request, call_next) -> Response:
30
+ """Extract owner_id from URL path and set in context."""
31
+ owner_id = None
32
+
33
+ try:
34
+ # ENHANCED DEBUGGING: Log all request details
35
+ logger.debug(f"🔍 OwnerMiddleware processing: {request.method} {request.url.path}")
36
+
37
+ # Extract owner_id from webhook URL pattern: /webhook/messenger/{owner_id}/{platform}
38
+ if request.url.path.startswith("/webhook/"):
39
+ logger.debug(f"🎯 Webhook request detected: {request.url.path}")
40
+ path_parts = request.url.path.strip("/").split("/")
41
+ logger.debug(f"📋 Path parts: {path_parts} (length: {len(path_parts)})")
42
+
43
+ if len(path_parts) >= 4:
44
+ # path_parts = ["webhook", "messenger", "owner_id", "platform"]
45
+ owner_id = path_parts[2]
46
+ logger.debug(f"🔑 Extracted owner_id from URL: '{owner_id}'")
47
+
48
+ # Validate basic format
49
+ if self._is_valid_owner_id(owner_id):
50
+ # Set owner_id context from URL
51
+ set_request_context(owner_id=owner_id)
52
+ logger.debug(f"✅ Owner ID context set successfully: {owner_id}")
53
+ else:
54
+ logger.error(f"❌ Invalid owner ID format: {owner_id}")
55
+ raise HTTPException(
56
+ status_code=400, detail=f"Invalid owner ID: {owner_id}"
57
+ )
58
+ else:
59
+ logger.warning(f"⚠️ Webhook URL does not have enough parts: {path_parts}")
60
+
61
+ # For non-webhook endpoints, use default owner from settings
62
+ elif not self._is_public_endpoint(request.url.path):
63
+ default_owner = settings.owner_id
64
+ set_request_context(owner_id=default_owner)
65
+ logger.debug(f"Using default owner ID: {default_owner}")
66
+
67
+ # Process request
68
+ response = await call_next(request)
69
+ return response
70
+
71
+ except HTTPException:
72
+ raise
73
+ except Exception as e:
74
+ logger.error(f"Error in owner middleware: {e}", exc_info=True)
75
+ raise HTTPException(status_code=500, detail="Internal server error") from e
76
+
77
+ def _is_valid_owner_id(self, owner_id: str) -> bool:
78
+ """Validate owner ID format."""
79
+ if not owner_id or not isinstance(owner_id, str):
80
+ return False
81
+
82
+ # Basic format validation - alphanumeric and underscores only
83
+ if not owner_id.replace("_", "").replace("-", "").isalnum():
84
+ return False
85
+
86
+ # Length validation
87
+ return not (len(owner_id) < 3 or len(owner_id) > 50)
88
+
89
+ def _is_public_endpoint(self, path: str) -> bool:
90
+ """Check if endpoint is public and doesn't require owner context."""
91
+ public_paths = [
92
+ "/",
93
+ "/health",
94
+ "/health/detailed",
95
+ "/docs",
96
+ "/redoc",
97
+ "/openapi.json",
98
+ ]
99
+ return any(path.startswith(p) for p in public_paths)
@@ -0,0 +1,184 @@
1
+ """
2
+ Request and response logging middleware with tenant and user context.
3
+
4
+ Provides comprehensive logging for monitoring, debugging, and audit trails for Wappa framework.
5
+ """
6
+
7
+ import time
8
+ from typing import Any
9
+
10
+ from fastapi import Request
11
+ from starlette.middleware.base import BaseHTTPMiddleware
12
+ from starlette.responses import Response
13
+
14
+
15
+ class RequestLoggingMiddleware(BaseHTTPMiddleware):
16
+ """
17
+ Middleware for logging HTTP requests and responses with context.
18
+
19
+ Features:
20
+ - Tenant-aware logging
21
+ - Request/response timing
22
+ - Privacy-conscious logging (excludes sensitive data)
23
+ - Structured log format for monitoring
24
+ """
25
+
26
+ def __init__(self, app, log_requests: bool = True, log_responses: bool = True):
27
+ super().__init__(app)
28
+ self.log_requests = log_requests
29
+ self.log_responses = log_responses
30
+ # Sensitive headers to exclude from logs
31
+ self.sensitive_headers = {
32
+ "authorization",
33
+ "x-api-key",
34
+ "cookie",
35
+ "set-cookie",
36
+ "x-access-token",
37
+ "x-auth-token",
38
+ "x-whatsapp-hub-signature",
39
+ }
40
+
41
+ async def dispatch(self, request: Request, call_next) -> Response:
42
+ """Process request with comprehensive logging."""
43
+ # Start timing
44
+ start_time = time.time()
45
+
46
+ # Get logger with context (context is set by OwnerMiddleware and processors)
47
+ from wappa.core.logging.logger import get_logger
48
+
49
+ logger = get_logger(__name__)
50
+
51
+ # Log incoming request
52
+ if self.log_requests and not self._should_skip_logging(request.url.path):
53
+ await self._log_request(request, logger)
54
+
55
+ # Process request
56
+ response = await call_next(request)
57
+
58
+ # Calculate processing time
59
+ process_time = time.time() - start_time
60
+
61
+ # Add processing time to response headers (in development)
62
+ from wappa.core.config.settings import settings
63
+
64
+ if settings.is_development:
65
+ response.headers["X-Process-Time"] = str(round(process_time * 1000, 2))
66
+
67
+ # Log response
68
+ if self.log_responses and not self._should_skip_logging(request.url.path):
69
+ await self._log_response(request, response, process_time, logger)
70
+
71
+ return response
72
+
73
+ def _extract_user_id(self, request: Request) -> str:
74
+ """
75
+ Extract user ID from request context.
76
+
77
+ This will be populated by webhook processing or API authentication.
78
+ For now, returns 'unknown' as we haven't implemented user extraction yet.
79
+ """
80
+ return getattr(request.state, "user_id", "unknown")
81
+
82
+ def _should_skip_logging(self, path: str) -> bool:
83
+ """Check if we should skip logging for this path."""
84
+ # Skip logging for health checks and static assets to reduce noise
85
+ skip_paths = ["/health", "/docs", "/redoc", "/openapi.json", "/favicon.ico"]
86
+ return any(path.startswith(skip_path) for skip_path in skip_paths)
87
+
88
+ async def _log_request(self, request: Request, logger) -> None:
89
+ """Log incoming request with sanitized information."""
90
+ # Safely read body for logging (only for small payloads)
91
+ body_info = await self._get_request_body_info(request)
92
+
93
+ # Sanitized headers (exclude sensitive ones)
94
+ safe_headers = {
95
+ k: v
96
+ for k, v in request.headers.items()
97
+ if k.lower() not in self.sensitive_headers
98
+ }
99
+
100
+ log_data = {
101
+ "method": request.method,
102
+ "url": str(request.url),
103
+ "path": request.url.path,
104
+ "query_params": dict(request.query_params),
105
+ "headers": safe_headers,
106
+ "client_host": request.client.host if request.client else "unknown",
107
+ "user_agent": request.headers.get("user-agent", "unknown"),
108
+ **body_info,
109
+ }
110
+
111
+ logger.info(
112
+ f"Incoming {request.method} {request.url.path}", extra={"request": log_data}
113
+ )
114
+
115
+ async def _log_response(
116
+ self, request: Request, response: Response, process_time: float, logger
117
+ ) -> None:
118
+ """Log response with timing and status information."""
119
+ # Determine log level based on status code
120
+ status_code = response.status_code
121
+
122
+ if status_code >= 500:
123
+ log_level = "error"
124
+ elif status_code >= 400:
125
+ log_level = "warning"
126
+ else:
127
+ log_level = "info"
128
+
129
+ log_data = {
130
+ "status_code": status_code,
131
+ "process_time_ms": round(process_time * 1000, 2),
132
+ "content_length": response.headers.get("content-length", "unknown"),
133
+ "content_type": response.headers.get("content-type", "unknown"),
134
+ }
135
+
136
+ message = (
137
+ f"Response {status_code} for {request.method} {request.url.path} "
138
+ f"({log_data['process_time_ms']}ms)"
139
+ )
140
+
141
+ # Log with appropriate level
142
+ getattr(logger, log_level)(message, extra={"response": log_data})
143
+
144
+ async def _get_request_body_info(self, request: Request) -> dict[str, Any]:
145
+ """
146
+ Get safe information about request body.
147
+
148
+ Returns body size and type information without logging sensitive content.
149
+ """
150
+ try:
151
+ # Only read body for non-streaming requests and if small enough
152
+ content_length = request.headers.get("content-length")
153
+ content_type = request.headers.get("content-type", "unknown")
154
+
155
+ body_info = {"content_type": content_type, "content_length": content_length}
156
+
157
+ # For webhook endpoints, we might want to log structure but not content
158
+ if request.url.path.startswith("/webhook/"):
159
+ body_info["is_webhook"] = True
160
+ # Don't log webhook payload content for privacy/security
161
+ body_info["body_logged"] = False
162
+ else:
163
+ # For API endpoints, we could log small bodies in development
164
+ from wappa.core.config.settings import settings
165
+
166
+ if (
167
+ settings.is_development
168
+ and content_length
169
+ and int(content_length) < 1000
170
+ ): # Only log small payloads
171
+ # Read body (this consumes the stream, so we need to be careful)
172
+ # For now, just log that we could log it
173
+ body_info["body_loggable"] = True
174
+ else:
175
+ body_info["body_logged"] = False
176
+
177
+ return body_info
178
+
179
+ except Exception as e:
180
+ # If we can't read body info, just return basic info
181
+ return {
182
+ "content_type": request.headers.get("content-type", "unknown"),
183
+ "body_read_error": str(e),
184
+ }