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,754 @@
1
+ """
2
+ Schema factory for platform-agnostic message and webhook schema creation.
3
+
4
+ This module provides factory classes for dynamically selecting and creating
5
+ the appropriate schema classes based on platform and message type combinations.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from wappa.core.logging.logger import get_logger
11
+ from wappa.webhooks.core.base_message import BaseMessage
12
+ from wappa.webhooks.core.base_webhook import BaseWebhook
13
+ from wappa.webhooks.core.types import MessageType, PlatformType
14
+
15
+
16
+ class SchemaRegistryError(Exception):
17
+ """Raised when schema registry operations fail."""
18
+
19
+ pass
20
+
21
+
22
+ class MessageSchemaRegistry:
23
+ """
24
+ Registry for message schema classes organized by platform and message type.
25
+
26
+ Provides centralized registration and lookup of message schema classes
27
+ to enable dynamic schema selection based on incoming webhook data.
28
+ """
29
+
30
+ def __init__(self):
31
+ """Initialize the schema registry."""
32
+ self.logger = get_logger(__name__)
33
+
34
+ # Registry structure: {platform: {message_type: schema_class}}
35
+ self._message_schemas: dict[
36
+ PlatformType, dict[MessageType, type[BaseMessage]]
37
+ ] = {}
38
+
39
+ # Initialize with WhatsApp schemas
40
+ self._register_whatsapp_schemas()
41
+
42
+ def _register_whatsapp_schemas(self) -> None:
43
+ """Register all WhatsApp message schema classes."""
44
+ try:
45
+ # Import WhatsApp message schemas
46
+ from .whatsapp.message_types.audio import WhatsAppAudioMessage
47
+ from .whatsapp.message_types.button import WhatsAppButtonMessage
48
+ from .whatsapp.message_types.contact import WhatsAppContactMessage
49
+ from .whatsapp.message_types.document import WhatsAppDocumentMessage
50
+ from .whatsapp.message_types.image import WhatsAppImageMessage
51
+ from .whatsapp.message_types.interactive import WhatsAppInteractiveMessage
52
+ from .whatsapp.message_types.location import WhatsAppLocationMessage
53
+ from .whatsapp.message_types.order import WhatsAppOrderMessage
54
+ from .whatsapp.message_types.reaction import WhatsAppReactionMessage
55
+ from .whatsapp.message_types.sticker import WhatsAppStickerMessage
56
+ from .whatsapp.message_types.system import WhatsAppSystemMessage
57
+ from .whatsapp.message_types.text import WhatsAppTextMessage
58
+ from .whatsapp.message_types.unsupported import WhatsAppUnsupportedMessage
59
+ from .whatsapp.message_types.video import WhatsAppVideoMessage
60
+
61
+ # Register WhatsApp schemas
62
+ whatsapp_schemas = {
63
+ MessageType.TEXT: WhatsAppTextMessage,
64
+ MessageType.INTERACTIVE: WhatsAppInteractiveMessage,
65
+ MessageType.IMAGE: WhatsAppImageMessage,
66
+ MessageType.AUDIO: WhatsAppAudioMessage,
67
+ MessageType.VIDEO: WhatsAppVideoMessage,
68
+ MessageType.DOCUMENT: WhatsAppDocumentMessage,
69
+ MessageType.CONTACT: WhatsAppContactMessage,
70
+ MessageType.LOCATION: WhatsAppLocationMessage,
71
+ MessageType.STICKER: WhatsAppStickerMessage,
72
+ MessageType.SYSTEM: WhatsAppSystemMessage,
73
+ MessageType.REACTION: WhatsAppReactionMessage,
74
+ # Additional WhatsApp-specific types (now with proper enum values)
75
+ MessageType.BUTTON: WhatsAppButtonMessage,
76
+ MessageType.ORDER: WhatsAppOrderMessage,
77
+ MessageType.UNSUPPORTED: WhatsAppUnsupportedMessage,
78
+ }
79
+
80
+ self._message_schemas[PlatformType.WHATSAPP] = whatsapp_schemas
81
+
82
+ self.logger.info(
83
+ f"Registered {len(whatsapp_schemas)} WhatsApp message schemas"
84
+ )
85
+
86
+ except ImportError as e:
87
+ self.logger.error(f"Failed to import WhatsApp schemas: {e}")
88
+ raise SchemaRegistryError(f"Failed to register WhatsApp schemas: {e}")
89
+
90
+ def register_message_schema(
91
+ self,
92
+ platform: PlatformType,
93
+ message_type: MessageType,
94
+ schema_class: type[BaseMessage],
95
+ ) -> None:
96
+ """
97
+ Register a message schema class for a platform and message type.
98
+
99
+ Args:
100
+ platform: The platform this schema belongs to
101
+ message_type: The message type this schema handles
102
+ schema_class: The Pydantic model class for this message type
103
+
104
+ Raises:
105
+ SchemaRegistryError: If registration fails
106
+ """
107
+ if not issubclass(schema_class, BaseMessage):
108
+ raise SchemaRegistryError(
109
+ f"Schema class must inherit from BaseMessage: {schema_class}"
110
+ )
111
+
112
+ if platform not in self._message_schemas:
113
+ self._message_schemas[platform] = {}
114
+
115
+ self._message_schemas[platform][message_type] = schema_class
116
+
117
+ self.logger.debug(
118
+ f"Registered schema: {platform.value}.{message_type.value} -> {schema_class.__name__}"
119
+ )
120
+
121
+ def get_message_schema(
122
+ self, platform: PlatformType, message_type: MessageType
123
+ ) -> type[BaseMessage] | None:
124
+ """
125
+ Get the message schema class for a platform and message type.
126
+
127
+ Args:
128
+ platform: The platform to get schema for
129
+ message_type: The message type to get schema for
130
+
131
+ Returns:
132
+ Message schema class or None if not found
133
+ """
134
+ platform_schemas = self._message_schemas.get(platform, {})
135
+ return platform_schemas.get(message_type)
136
+
137
+ def is_message_type_supported(
138
+ self, platform: PlatformType, message_type: MessageType
139
+ ) -> bool:
140
+ """
141
+ Check if a message type is supported for a platform.
142
+
143
+ Args:
144
+ platform: The platform to check
145
+ message_type: The message type to check
146
+
147
+ Returns:
148
+ True if supported, False otherwise
149
+ """
150
+ return self.get_message_schema(platform, message_type) is not None
151
+
152
+ def get_supported_message_types(self, platform: PlatformType) -> set[MessageType]:
153
+ """
154
+ Get all supported message types for a platform.
155
+
156
+ Args:
157
+ platform: The platform to get supported types for
158
+
159
+ Returns:
160
+ Set of supported message types
161
+ """
162
+ platform_schemas = self._message_schemas.get(platform, {})
163
+ return set(platform_schemas.keys())
164
+
165
+ def get_supported_platforms(self) -> set[PlatformType]:
166
+ """
167
+ Get all platforms with registered schemas.
168
+
169
+ Returns:
170
+ Set of platforms with registered schemas
171
+ """
172
+ return set(self._message_schemas.keys())
173
+
174
+ def get_registry_stats(self) -> dict[str, Any]:
175
+ """
176
+ Get registry statistics for monitoring.
177
+
178
+ Returns:
179
+ Dictionary with registry statistics
180
+ """
181
+ stats = {"total_platforms": len(self._message_schemas), "platforms": {}}
182
+
183
+ for platform, schemas in self._message_schemas.items():
184
+ stats["platforms"][platform.value] = {
185
+ "message_types": len(schemas),
186
+ "supported_types": [mt.value for mt in schemas.keys()],
187
+ }
188
+
189
+ return stats
190
+
191
+
192
+ class WebhookSchemaRegistry:
193
+ """
194
+ Registry for webhook container schema classes organized by platform.
195
+
196
+ Provides centralized registration and lookup of webhook container schemas
197
+ for parsing platform-specific webhook structures.
198
+ """
199
+
200
+ def __init__(self):
201
+ """Initialize the webhook schema registry."""
202
+ self.logger = get_logger(__name__)
203
+
204
+ # Registry structure: {platform: webhook_schema_class}
205
+ self._webhook_schemas: dict[PlatformType, type[BaseWebhook]] = {}
206
+
207
+ # Initialize with WhatsApp webhook schema
208
+ self._register_whatsapp_webhook_schema()
209
+
210
+ def _register_whatsapp_webhook_schema(self) -> None:
211
+ """Register WhatsApp webhook container schema."""
212
+ try:
213
+ from .whatsapp.webhook_container import WhatsAppWebhook
214
+
215
+ self._webhook_schemas[PlatformType.WHATSAPP] = WhatsAppWebhook
216
+
217
+ self.logger.info("Registered WhatsApp webhook schema")
218
+
219
+ except ImportError as e:
220
+ self.logger.error(f"Failed to import WhatsApp webhook schema: {e}")
221
+ raise SchemaRegistryError(
222
+ f"Failed to register WhatsApp webhook schema: {e}"
223
+ )
224
+
225
+ def register_webhook_schema(
226
+ self, platform: PlatformType, schema_class: type[BaseWebhook]
227
+ ) -> None:
228
+ """
229
+ Register a webhook schema class for a platform.
230
+
231
+ Args:
232
+ platform: The platform this schema belongs to
233
+ schema_class: The Pydantic model class for webhook containers
234
+
235
+ Raises:
236
+ SchemaRegistryError: If registration fails
237
+ """
238
+ if not issubclass(schema_class, BaseWebhook):
239
+ raise SchemaRegistryError(
240
+ f"Schema class must inherit from BaseWebhook: {schema_class}"
241
+ )
242
+
243
+ self._webhook_schemas[platform] = schema_class
244
+
245
+ self.logger.debug(
246
+ f"Registered webhook schema: {platform.value} -> {schema_class.__name__}"
247
+ )
248
+
249
+ def get_webhook_schema(self, platform: PlatformType) -> type[BaseWebhook] | None:
250
+ """
251
+ Get the webhook schema class for a platform.
252
+
253
+ Args:
254
+ platform: The platform to get schema for
255
+
256
+ Returns:
257
+ Webhook schema class or None if not found
258
+ """
259
+ return self._webhook_schemas.get(platform)
260
+
261
+ def is_platform_supported(self, platform: PlatformType) -> bool:
262
+ """
263
+ Check if a platform has a registered webhook schema.
264
+
265
+ Args:
266
+ platform: The platform to check
267
+
268
+ Returns:
269
+ True if supported, False otherwise
270
+ """
271
+ return platform in self._webhook_schemas
272
+
273
+ def get_supported_platforms(self) -> set[PlatformType]:
274
+ """
275
+ Get all platforms with registered webhook schemas.
276
+
277
+ Returns:
278
+ Set of supported platforms
279
+ """
280
+ return set(self._webhook_schemas.keys())
281
+
282
+
283
+ class SchemaFactory:
284
+ """
285
+ Factory for creating platform-specific message and webhook schema instances.
286
+
287
+ Provides centralized schema selection and instantiation based on platform
288
+ and message type, with comprehensive error handling and validation.
289
+ """
290
+
291
+ def __init__(self):
292
+ """Initialize the schema factory with registries."""
293
+ self.logger = get_logger(__name__)
294
+
295
+ # Initialize registries
296
+ self.message_registry = MessageSchemaRegistry()
297
+ self.webhook_registry = WebhookSchemaRegistry()
298
+
299
+ def create_message_instance(
300
+ self,
301
+ platform: PlatformType,
302
+ message_type: MessageType,
303
+ message_data: dict[str, Any],
304
+ **kwargs,
305
+ ) -> BaseMessage:
306
+ """
307
+ Create a message instance from raw data.
308
+
309
+ Args:
310
+ platform: The platform this message came from
311
+ message_type: The type of message to create
312
+ message_data: Raw message data from webhook
313
+ **kwargs: Additional parameters for message creation
314
+
315
+ Returns:
316
+ Parsed message instance with universal interface
317
+
318
+ Raises:
319
+ SchemaRegistryError: If schema not found for platform/message type
320
+ ValidationError: If message data is invalid
321
+ """
322
+ # Get appropriate schema class
323
+ schema_class = self.message_registry.get_message_schema(platform, message_type)
324
+
325
+ if schema_class is None:
326
+ raise SchemaRegistryError(
327
+ f"No schema registered for {platform.value}.{message_type.value}"
328
+ )
329
+
330
+ try:
331
+ # Create and validate message instance
332
+ message_instance = schema_class.model_validate(message_data)
333
+
334
+ self.logger.debug(
335
+ f"Created {schema_class.__name__} instance: {message_instance.message_id}"
336
+ )
337
+
338
+ return message_instance
339
+
340
+ except Exception as e:
341
+ self.logger.error(
342
+ f"Failed to create {schema_class.__name__} instance: {e}", exc_info=True
343
+ )
344
+ raise
345
+
346
+ def create_webhook_instance(
347
+ self, platform: PlatformType, webhook_data: dict[str, Any], **kwargs
348
+ ) -> BaseWebhook:
349
+ """
350
+ Create a webhook container instance from raw data.
351
+
352
+ Args:
353
+ platform: The platform this webhook came from
354
+ webhook_data: Raw webhook payload data
355
+ **kwargs: Additional parameters for webhook creation
356
+
357
+ Returns:
358
+ Parsed webhook instance with universal interface
359
+
360
+ Raises:
361
+ SchemaRegistryError: If schema not found for platform
362
+ ValidationError: If webhook data is invalid
363
+ """
364
+ # Get appropriate webhook schema class
365
+ schema_class = self.webhook_registry.get_webhook_schema(platform)
366
+
367
+ if schema_class is None:
368
+ raise SchemaRegistryError(
369
+ f"No webhook schema registered for {platform.value}"
370
+ )
371
+
372
+ try:
373
+ # Create and validate webhook instance
374
+ webhook_instance = schema_class.model_validate(webhook_data)
375
+
376
+ self.logger.debug(
377
+ f"Created {schema_class.__name__} instance: {webhook_instance.get_webhook_id()}"
378
+ )
379
+
380
+ return webhook_instance
381
+
382
+ except Exception as e:
383
+ self.logger.error(
384
+ f"Failed to create {schema_class.__name__} instance: {e}", exc_info=True
385
+ )
386
+ raise
387
+
388
+ def detect_message_type(
389
+ self, platform: PlatformType, message_data: dict[str, Any]
390
+ ) -> MessageType | None:
391
+ """
392
+ Detect message type from raw message data.
393
+
394
+ Args:
395
+ platform: The platform this message came from
396
+ message_data: Raw message data
397
+
398
+ Returns:
399
+ Detected message type or None if cannot determine
400
+ """
401
+ if not isinstance(message_data, dict):
402
+ return None
403
+
404
+ # Get type from message data (common pattern across platforms)
405
+ message_type_str = message_data.get("type")
406
+ if not message_type_str:
407
+ return None
408
+
409
+ try:
410
+ # Try to convert to MessageType enum
411
+ message_type = MessageType(message_type_str)
412
+
413
+ # Verify this message type is supported for the platform
414
+ if self.message_registry.is_message_type_supported(platform, message_type):
415
+ return message_type
416
+
417
+ except ValueError:
418
+ # Handle platform-specific message types not in the enum
419
+ if platform == PlatformType.WHATSAPP:
420
+ # WhatsApp-specific message types
421
+ whatsapp_types = {
422
+ "button": MessageType.INTERACTIVE, # Map to closest standard type
423
+ "order": MessageType.INTERACTIVE, # Map to closest standard type
424
+ "unsupported": MessageType.SYSTEM, # Map to closest standard type
425
+ }
426
+ mapped_type = whatsapp_types.get(message_type_str)
427
+ if mapped_type and self.message_registry.is_message_type_supported(
428
+ platform, mapped_type
429
+ ):
430
+ return mapped_type
431
+
432
+ return None
433
+
434
+ def get_supported_combinations(self) -> dict[str, list[str]]:
435
+ """
436
+ Get all supported platform and message type combinations.
437
+
438
+ Returns:
439
+ Dictionary mapping platform names to lists of supported message types
440
+ """
441
+ combinations = {}
442
+
443
+ for platform in self.message_registry.get_supported_platforms():
444
+ message_types = self.message_registry.get_supported_message_types(platform)
445
+ combinations[platform.value] = [mt.value for mt in message_types]
446
+
447
+ return combinations
448
+
449
+ def validate_platform_message_compatibility(
450
+ self, platform: PlatformType, message_type: MessageType
451
+ ) -> tuple[bool, str | None]:
452
+ """
453
+ Validate if a platform supports a specific message type.
454
+
455
+ Args:
456
+ platform: The platform to check
457
+ message_type: The message type to check
458
+
459
+ Returns:
460
+ Tuple of (is_supported, error_message)
461
+ """
462
+ if not self.message_registry.is_message_type_supported(platform, message_type):
463
+ supported_types = self.message_registry.get_supported_message_types(
464
+ platform
465
+ )
466
+ return False, (
467
+ f"Message type '{message_type.value}' not supported for platform '{platform.value}'. "
468
+ f"Supported types: {[mt.value for mt in supported_types]}"
469
+ )
470
+
471
+ return True, None
472
+
473
+ def get_factory_stats(self) -> dict[str, Any]:
474
+ """
475
+ Get factory statistics for monitoring.
476
+
477
+ Returns:
478
+ Dictionary with factory statistics
479
+ """
480
+ return {
481
+ "message_registry": self.message_registry.get_registry_stats(),
482
+ "webhook_registry": {
483
+ "supported_platforms": [
484
+ p.value for p in self.webhook_registry.get_supported_platforms()
485
+ ]
486
+ },
487
+ "total_combinations": sum(
488
+ len(types) for types in self.get_supported_combinations().values()
489
+ ),
490
+ }
491
+
492
+ # ===== Universal Webhook Interface Support =====
493
+
494
+ def detect_webhook_type(self, payload: dict[str, Any]) -> str | None:
495
+ """
496
+ Detect webhook type from raw payload structure.
497
+
498
+ Based on WhatsApp webhook structure but designed to be universal:
499
+ - IncomingMessage: Contains 'messages' array
500
+ - Status: Contains 'statuses' array
501
+ - Error: Contains 'errors' array at value level
502
+ - OutgoingMessage: Future - currently not supported
503
+
504
+ Args:
505
+ payload: Raw webhook payload dictionary
506
+
507
+ Returns:
508
+ Webhook type string or None if cannot determine
509
+ """
510
+ if not isinstance(payload, dict):
511
+ return None
512
+
513
+ # Navigate to WhatsApp webhook structure: entry[0].changes[0].value
514
+ try:
515
+ entry = payload.get("entry", [])
516
+ if not entry or not isinstance(entry, list):
517
+ return None
518
+
519
+ changes = entry[0].get("changes", [])
520
+ if not changes or not isinstance(changes, list):
521
+ return None
522
+
523
+ value = changes[0].get("value", {})
524
+ if not isinstance(value, dict):
525
+ return None
526
+
527
+ # Check for webhook type indicators
528
+ if (
529
+ "messages" in value
530
+ and isinstance(value["messages"], list)
531
+ and len(value["messages"]) > 0
532
+ ):
533
+ return "incoming_message"
534
+ elif (
535
+ "statuses" in value
536
+ and isinstance(value["statuses"], list)
537
+ and len(value["statuses"]) > 0
538
+ ):
539
+ return "status"
540
+ elif (
541
+ "errors" in value
542
+ and isinstance(value["errors"], list)
543
+ and len(value["errors"]) > 0
544
+ ):
545
+ return "error"
546
+
547
+ return None
548
+
549
+ except (KeyError, IndexError, TypeError):
550
+ return None
551
+
552
+ async def create_universal_webhook(
553
+ self,
554
+ platform: PlatformType,
555
+ payload: dict[str, Any],
556
+ webhook_type: str | None = None,
557
+ **kwargs,
558
+ ) -> "UniversalWebhook":
559
+ """
560
+ Create a Universal Webhook Interface from platform-specific payload.
561
+
562
+ This method acts as the main factory for converting platform webhooks
563
+ into universal interfaces. It delegates to platform-specific processors
564
+ to handle the transformation.
565
+
566
+ Args:
567
+ platform: The platform this webhook came from
568
+ payload: Raw webhook payload dictionary
569
+ webhook_type: Optional webhook type hint (will auto-detect if None)
570
+ **kwargs: Additional parameters for webhook creation
571
+
572
+ Returns:
573
+ Universal webhook interface instance
574
+
575
+ Raises:
576
+ SchemaRegistryError: If platform not supported or conversion fails
577
+ """
578
+ from wappa.processors.factory import processor_factory
579
+
580
+ try:
581
+ # Get platform-specific processor
582
+ processor = processor_factory.get_processor(platform)
583
+
584
+ # Use processor to create universal webhook
585
+ if hasattr(processor, "create_universal_webhook"):
586
+ return await processor.create_universal_webhook(payload, **kwargs)
587
+ else:
588
+ raise SchemaRegistryError(
589
+ f"Processor for {platform.value} does not support universal webhook creation"
590
+ )
591
+
592
+ except Exception as e:
593
+ self.logger.error(
594
+ f"Failed to create universal webhook for {platform.value}: {e}",
595
+ exc_info=True,
596
+ )
597
+ raise SchemaRegistryError(f"Failed to create universal webhook: {e}")
598
+
599
+ def create_universal_webhook_from_payload(
600
+ self,
601
+ payload: dict[str, Any],
602
+ url_path: str | None = None,
603
+ headers: dict[str, str] | None = None,
604
+ **kwargs,
605
+ ) -> "UniversalWebhook":
606
+ """
607
+ Create Universal Webhook Interface with automatic platform detection.
608
+
609
+ Args:
610
+ payload: Raw webhook payload dictionary
611
+ url_path: Optional URL path for platform detection
612
+ headers: Optional HTTP headers for platform detection
613
+ **kwargs: Additional parameters for webhook creation
614
+
615
+ Returns:
616
+ Universal webhook interface instance
617
+
618
+ Raises:
619
+ SchemaRegistryError: If platform cannot be detected or conversion fails
620
+ """
621
+ from wappa.processors.factory import PlatformDetector
622
+
623
+ try:
624
+ # Detect platform from payload
625
+ platform = PlatformDetector.detect_platform(payload, url_path, headers)
626
+
627
+ # Create universal webhook for detected platform
628
+ return self.create_universal_webhook(platform, payload, **kwargs)
629
+
630
+ except Exception as e:
631
+ self.logger.error(
632
+ f"Failed to create universal webhook from payload: {e}", exc_info=True
633
+ )
634
+ raise SchemaRegistryError(f"Failed to create universal webhook: {e}")
635
+
636
+ def validate_universal_webhook_payload(
637
+ self, payload: dict[str, Any], expected_type: str | None = None
638
+ ) -> tuple[bool, str | None, str | None]:
639
+ """
640
+ Validate payload for universal webhook creation.
641
+
642
+ Args:
643
+ payload: Raw webhook payload dictionary
644
+ expected_type: Optional expected webhook type for validation
645
+
646
+ Returns:
647
+ Tuple of (is_valid, error_message, detected_type)
648
+ """
649
+ try:
650
+ # Detect webhook type
651
+ detected_type = self.detect_webhook_type(payload)
652
+
653
+ if not detected_type:
654
+ return (
655
+ False,
656
+ "Unable to determine webhook type from payload structure",
657
+ None,
658
+ )
659
+
660
+ # Check if detected type matches expected
661
+ if expected_type and detected_type != expected_type:
662
+ return (
663
+ False,
664
+ (
665
+ f"Webhook type mismatch: expected {expected_type}, "
666
+ f"detected {detected_type}"
667
+ ),
668
+ detected_type,
669
+ )
670
+
671
+ # Basic structure validation
672
+ if not self._validate_basic_webhook_structure(payload):
673
+ return (
674
+ False,
675
+ "Payload does not match expected webhook structure",
676
+ detected_type,
677
+ )
678
+
679
+ return True, None, detected_type
680
+
681
+ except Exception as e:
682
+ return False, f"Validation error: {e}", None
683
+
684
+ def _validate_basic_webhook_structure(self, payload: dict[str, Any]) -> bool:
685
+ """
686
+ Validate basic webhook structure (based on WhatsApp format).
687
+
688
+ Args:
689
+ payload: Raw webhook payload dictionary
690
+
691
+ Returns:
692
+ True if structure is valid, False otherwise
693
+ """
694
+ try:
695
+ # Check for required top-level fields
696
+ if "object" not in payload or "entry" not in payload:
697
+ return False
698
+
699
+ # Check entry structure
700
+ entry = payload["entry"]
701
+ if not isinstance(entry, list) or len(entry) == 0:
702
+ return False
703
+
704
+ # Check changes structure
705
+ first_entry = entry[0]
706
+ if "changes" not in first_entry:
707
+ return False
708
+
709
+ changes = first_entry["changes"]
710
+ if not isinstance(changes, list) or len(changes) == 0:
711
+ return False
712
+
713
+ # Check value structure
714
+ first_change = changes[0]
715
+ if "value" not in first_change:
716
+ return False
717
+
718
+ value = first_change["value"]
719
+ if not isinstance(value, dict):
720
+ return False
721
+
722
+ # Should have metadata at minimum
723
+ if "metadata" not in value:
724
+ return False
725
+
726
+ return True
727
+
728
+ except (KeyError, IndexError, TypeError):
729
+ return False
730
+
731
+ def get_supported_webhook_types(self) -> list[str]:
732
+ """
733
+ Get list of supported universal webhook types.
734
+
735
+ Returns:
736
+ List of supported webhook type strings
737
+ """
738
+ return ["incoming_message", "status", "error", "outgoing_message"]
739
+
740
+ def is_webhook_type_supported(self, webhook_type: str) -> bool:
741
+ """
742
+ Check if a webhook type is supported.
743
+
744
+ Args:
745
+ webhook_type: Webhook type string to check
746
+
747
+ Returns:
748
+ True if supported, False otherwise
749
+ """
750
+ return webhook_type in self.get_supported_webhook_types()
751
+
752
+
753
+ # Singleton instance for global access
754
+ schema_factory = SchemaFactory()