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,454 @@
1
+ """
2
+ Custom validators for WhatsApp webhook schemas.
3
+
4
+ This module contains reusable validation functions and error handling
5
+ utilities for WhatsApp Business Platform data validation.
6
+ """
7
+
8
+ import re
9
+ from typing import Any
10
+
11
+ from pydantic import ValidationError
12
+
13
+
14
+ class WhatsAppValidationError(Exception):
15
+ """Custom exception for WhatsApp-specific validation errors."""
16
+
17
+ def __init__(self, message: str, field: str | None = None, value: Any = None):
18
+ self.message = message
19
+ self.field = field
20
+ self.value = value
21
+ super().__init__(message)
22
+
23
+
24
+ class WhatsAppValidators:
25
+ """Collection of validation utilities for WhatsApp data."""
26
+
27
+ # WhatsApp phone number regex (international format)
28
+ PHONE_REGEX = re.compile(r"^\+?[1-9]\d{1,14}$")
29
+
30
+ # WhatsApp message ID regex
31
+ MESSAGE_ID_REGEX = re.compile(r"^wamid\.[A-Za-z0-9+/=_-]+$")
32
+
33
+ # Business account ID regex (numeric)
34
+ BUSINESS_ID_REGEX = re.compile(r"^\d{10,}$")
35
+
36
+ # SHA256 hash regex
37
+ SHA256_REGEX = re.compile(r"^[a-fA-F0-9]{64}$")
38
+
39
+ @classmethod
40
+ def validate_phone_number(cls, phone: str, field_name: str = "phone") -> str:
41
+ """
42
+ Validate WhatsApp phone number format.
43
+
44
+ Args:
45
+ phone: Phone number to validate
46
+ field_name: Field name for error messages
47
+
48
+ Returns:
49
+ Cleaned phone number
50
+
51
+ Raises:
52
+ WhatsAppValidationError: If phone number is invalid
53
+ """
54
+ if not phone:
55
+ raise WhatsAppValidationError(
56
+ f"{field_name} cannot be empty", field_name, phone
57
+ )
58
+
59
+ # Remove common formatting
60
+ cleaned = (
61
+ phone.replace(" ", "").replace("-", "").replace("(", "").replace(")", "")
62
+ )
63
+
64
+ # Check minimum length
65
+ if len(cleaned) < 8:
66
+ raise WhatsAppValidationError(
67
+ f"{field_name} must be at least 8 characters", field_name, phone
68
+ )
69
+
70
+ # Check maximum length (international format allows up to 15 digits)
71
+ if len(cleaned.replace("+", "")) > 15:
72
+ raise WhatsAppValidationError(
73
+ f"{field_name} cannot exceed 15 digits", field_name, phone
74
+ )
75
+
76
+ # Basic format validation - should be mostly numeric
77
+ if not cls.PHONE_REGEX.match(cleaned):
78
+ raise WhatsAppValidationError(
79
+ f"{field_name} must be in valid international format", field_name, phone
80
+ )
81
+
82
+ return cleaned
83
+
84
+ @classmethod
85
+ def validate_message_id(
86
+ cls, message_id: str, field_name: str = "message_id"
87
+ ) -> str:
88
+ """
89
+ Validate WhatsApp message ID format.
90
+
91
+ Args:
92
+ message_id: Message ID to validate
93
+ field_name: Field name for error messages
94
+
95
+ Returns:
96
+ Validated message ID
97
+
98
+ Raises:
99
+ WhatsAppValidationError: If message ID is invalid
100
+ """
101
+ if not message_id:
102
+ raise WhatsAppValidationError(
103
+ f"{field_name} cannot be empty", field_name, message_id
104
+ )
105
+
106
+ if len(message_id) < 10:
107
+ raise WhatsAppValidationError(
108
+ f"{field_name} must be at least 10 characters", field_name, message_id
109
+ )
110
+
111
+ if not message_id.startswith("wamid."):
112
+ raise WhatsAppValidationError(
113
+ f"{field_name} must start with 'wamid.'", field_name, message_id
114
+ )
115
+
116
+ if not cls.MESSAGE_ID_REGEX.match(message_id):
117
+ raise WhatsAppValidationError(
118
+ f"{field_name} contains invalid characters", field_name, message_id
119
+ )
120
+
121
+ return message_id
122
+
123
+ @classmethod
124
+ def validate_business_account_id(
125
+ cls, business_id: str, field_name: str = "business_id"
126
+ ) -> str:
127
+ """
128
+ Validate WhatsApp Business Account ID format.
129
+
130
+ Args:
131
+ business_id: Business account ID to validate
132
+ field_name: Field name for error messages
133
+
134
+ Returns:
135
+ Validated business account ID
136
+
137
+ Raises:
138
+ WhatsAppValidationError: If business ID is invalid
139
+ """
140
+ if not business_id:
141
+ raise WhatsAppValidationError(
142
+ f"{field_name} cannot be empty", field_name, business_id
143
+ )
144
+
145
+ if not business_id.isdigit():
146
+ raise WhatsAppValidationError(
147
+ f"{field_name} must be numeric", field_name, business_id
148
+ )
149
+
150
+ if len(business_id) < 10:
151
+ raise WhatsAppValidationError(
152
+ f"{field_name} must be at least 10 digits", field_name, business_id
153
+ )
154
+
155
+ return business_id
156
+
157
+ @classmethod
158
+ def validate_timestamp(cls, timestamp: str, field_name: str = "timestamp") -> str:
159
+ """
160
+ Validate Unix timestamp format.
161
+
162
+ Args:
163
+ timestamp: Timestamp to validate
164
+ field_name: Field name for error messages
165
+
166
+ Returns:
167
+ Validated timestamp
168
+
169
+ Raises:
170
+ WhatsAppValidationError: If timestamp is invalid
171
+ """
172
+ if not timestamp:
173
+ raise WhatsAppValidationError(
174
+ f"{field_name} cannot be empty", field_name, timestamp
175
+ )
176
+
177
+ if not timestamp.isdigit():
178
+ raise WhatsAppValidationError(
179
+ f"{field_name} must be numeric", field_name, timestamp
180
+ )
181
+
182
+ # Check reasonable timestamp range (after 2020, before 2100)
183
+ timestamp_int = int(timestamp)
184
+ if timestamp_int < 1577836800: # 2020-01-01
185
+ raise WhatsAppValidationError(
186
+ f"{field_name} is too old (must be after 2020)", field_name, timestamp
187
+ )
188
+
189
+ if timestamp_int > 4102444800: # 2100-01-01
190
+ raise WhatsAppValidationError(
191
+ f"{field_name} is too far in the future (must be before 2100)",
192
+ field_name,
193
+ timestamp,
194
+ )
195
+
196
+ return timestamp
197
+
198
+ @classmethod
199
+ def validate_sha256_hash(cls, hash_value: str, field_name: str = "hash") -> str:
200
+ """
201
+ Validate SHA256 hash format.
202
+
203
+ Args:
204
+ hash_value: Hash to validate
205
+ field_name: Field name for error messages
206
+
207
+ Returns:
208
+ Validated hash (lowercase)
209
+
210
+ Raises:
211
+ WhatsAppValidationError: If hash is invalid
212
+ """
213
+ if not hash_value:
214
+ raise WhatsAppValidationError(
215
+ f"{field_name} cannot be empty", field_name, hash_value
216
+ )
217
+
218
+ if len(hash_value) != 64:
219
+ raise WhatsAppValidationError(
220
+ f"{field_name} must be exactly 64 characters", field_name, hash_value
221
+ )
222
+
223
+ if not cls.SHA256_REGEX.match(hash_value):
224
+ raise WhatsAppValidationError(
225
+ f"{field_name} must contain only hexadecimal characters",
226
+ field_name,
227
+ hash_value,
228
+ )
229
+
230
+ return hash_value.lower()
231
+
232
+ @classmethod
233
+ def validate_mime_type(
234
+ cls, mime_type: str, allowed_types: list[str], field_name: str = "mime_type"
235
+ ) -> str:
236
+ """
237
+ Validate MIME type against allowed types.
238
+
239
+ Args:
240
+ mime_type: MIME type to validate
241
+ allowed_types: List of allowed MIME types
242
+ field_name: Field name for error messages
243
+
244
+ Returns:
245
+ Validated MIME type (lowercase)
246
+
247
+ Raises:
248
+ WhatsAppValidationError: If MIME type is invalid
249
+ """
250
+ if not mime_type:
251
+ raise WhatsAppValidationError(
252
+ f"{field_name} cannot be empty", field_name, mime_type
253
+ )
254
+
255
+ mime_lower = mime_type.lower().strip()
256
+ if mime_lower not in [t.lower() for t in allowed_types]:
257
+ raise WhatsAppValidationError(
258
+ f"{field_name} must be one of {allowed_types}, got: {mime_type}",
259
+ field_name,
260
+ mime_type,
261
+ )
262
+
263
+ return mime_lower
264
+
265
+ @classmethod
266
+ def validate_text_length(
267
+ cls,
268
+ text: str,
269
+ max_length: int,
270
+ field_name: str = "text",
271
+ allow_empty: bool = False,
272
+ ) -> str:
273
+ """
274
+ Validate text length constraints.
275
+
276
+ Args:
277
+ text: Text to validate
278
+ max_length: Maximum allowed length
279
+ field_name: Field name for error messages
280
+ allow_empty: Whether empty text is allowed
281
+
282
+ Returns:
283
+ Stripped text
284
+
285
+ Raises:
286
+ WhatsAppValidationError: If text length is invalid
287
+ """
288
+ if text is None:
289
+ if allow_empty:
290
+ return ""
291
+ raise WhatsAppValidationError(
292
+ f"{field_name} cannot be None", field_name, text
293
+ )
294
+
295
+ stripped = text.strip()
296
+
297
+ if not stripped and not allow_empty:
298
+ raise WhatsAppValidationError(
299
+ f"{field_name} cannot be empty", field_name, text
300
+ )
301
+
302
+ if len(stripped) > max_length:
303
+ raise WhatsAppValidationError(
304
+ f"{field_name} cannot exceed {max_length} characters (got {len(stripped)})",
305
+ field_name,
306
+ text,
307
+ )
308
+
309
+ return stripped
310
+
311
+ @classmethod
312
+ def validate_url(
313
+ cls, url: str, field_name: str = "url", allow_none: bool = False
314
+ ) -> str | None:
315
+ """
316
+ Validate URL format.
317
+
318
+ Args:
319
+ url: URL to validate
320
+ field_name: Field name for error messages
321
+ allow_none: Whether None values are allowed
322
+
323
+ Returns:
324
+ Validated URL or None
325
+
326
+ Raises:
327
+ WhatsAppValidationError: If URL is invalid
328
+ """
329
+ if url is None:
330
+ if allow_none:
331
+ return None
332
+ raise WhatsAppValidationError(
333
+ f"{field_name} cannot be None", field_name, url
334
+ )
335
+
336
+ if not url.strip():
337
+ if allow_none:
338
+ return None
339
+ raise WhatsAppValidationError(
340
+ f"{field_name} cannot be empty", field_name, url
341
+ )
342
+
343
+ url = url.strip()
344
+ if not (url.startswith("http://") or url.startswith("https://")):
345
+ raise WhatsAppValidationError(
346
+ f"{field_name} must start with http:// or https://", field_name, url
347
+ )
348
+
349
+ # Basic URL validation - check for obvious issues
350
+ if " " in url:
351
+ raise WhatsAppValidationError(
352
+ f"{field_name} cannot contain spaces", field_name, url
353
+ )
354
+
355
+ return url
356
+
357
+
358
+ class WhatsAppErrorHandler:
359
+ """Utility class for handling WhatsApp validation and processing errors."""
360
+
361
+ @staticmethod
362
+ def format_validation_error(error: ValidationError) -> dict[str, Any]:
363
+ """
364
+ Format Pydantic ValidationError for API responses.
365
+
366
+ Args:
367
+ error: Pydantic ValidationError
368
+
369
+ Returns:
370
+ Formatted error dictionary
371
+ """
372
+ errors = []
373
+ for err in error.errors():
374
+ errors.append(
375
+ {
376
+ "field": ".".join(str(loc) for loc in err["loc"]),
377
+ "message": err["msg"],
378
+ "type": err["type"],
379
+ "input": err.get("input", None),
380
+ }
381
+ )
382
+
383
+ return {
384
+ "error": "validation_failed",
385
+ "message": "WhatsApp webhook validation failed",
386
+ "details": errors,
387
+ "error_count": len(errors),
388
+ }
389
+
390
+ @staticmethod
391
+ def format_whatsapp_error(error: WhatsAppValidationError) -> dict[str, Any]:
392
+ """
393
+ Format WhatsAppValidationError for API responses.
394
+
395
+ Args:
396
+ error: WhatsAppValidationError
397
+
398
+ Returns:
399
+ Formatted error dictionary
400
+ """
401
+ return {
402
+ "error": "whatsapp_validation_failed",
403
+ "message": error.message,
404
+ "field": error.field,
405
+ "value": error.value,
406
+ }
407
+
408
+ @staticmethod
409
+ def is_recoverable_error(error: Exception) -> bool:
410
+ """
411
+ Determine if an error is recoverable and the request should be retried.
412
+
413
+ Args:
414
+ error: Exception to check
415
+
416
+ Returns:
417
+ True if error is recoverable, False otherwise
418
+ """
419
+ # Validation errors are not recoverable
420
+ if isinstance(error, (ValidationError, WhatsAppValidationError)):
421
+ return False
422
+
423
+ # Network/timeout errors might be recoverable
424
+ if isinstance(error, (ConnectionError, TimeoutError)):
425
+ return True
426
+
427
+ # Generic exceptions might be recoverable
428
+ return True
429
+
430
+ @staticmethod
431
+ def get_error_priority(error: Exception) -> str:
432
+ """
433
+ Get the priority level for an error for logging and alerting.
434
+
435
+ Args:
436
+ error: Exception to evaluate
437
+
438
+ Returns:
439
+ Priority level: 'low', 'medium', 'high', 'critical'
440
+ """
441
+ if isinstance(error, ValidationError):
442
+ # Validation errors are medium priority - indicate data issues
443
+ return "medium"
444
+
445
+ if isinstance(error, WhatsAppValidationError):
446
+ # WhatsApp-specific validation errors are medium priority
447
+ return "medium"
448
+
449
+ if isinstance(error, (ConnectionError, TimeoutError)):
450
+ # Network issues are high priority - service availability
451
+ return "high"
452
+
453
+ # Unknown errors are critical priority
454
+ return "critical"