wappa 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wappa might be problematic. Click here for more details.
- wappa/__init__.py +85 -0
- wappa/api/__init__.py +1 -0
- wappa/api/controllers/__init__.py +10 -0
- wappa/api/controllers/webhook_controller.py +441 -0
- wappa/api/dependencies/__init__.py +15 -0
- wappa/api/dependencies/whatsapp_dependencies.py +220 -0
- wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
- wappa/api/middleware/__init__.py +7 -0
- wappa/api/middleware/error_handler.py +158 -0
- wappa/api/middleware/owner.py +99 -0
- wappa/api/middleware/request_logging.py +184 -0
- wappa/api/routes/__init__.py +6 -0
- wappa/api/routes/health.py +102 -0
- wappa/api/routes/webhooks.py +211 -0
- wappa/api/routes/whatsapp/__init__.py +15 -0
- wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
- wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
- wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
- wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
- wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
- wappa/api/routes/whatsapp_combined.py +35 -0
- wappa/cli/__init__.py +9 -0
- wappa/cli/main.py +199 -0
- wappa/core/__init__.py +6 -0
- wappa/core/config/__init__.py +5 -0
- wappa/core/config/settings.py +161 -0
- wappa/core/events/__init__.py +41 -0
- wappa/core/events/default_handlers.py +642 -0
- wappa/core/events/event_dispatcher.py +244 -0
- wappa/core/events/event_handler.py +247 -0
- wappa/core/events/webhook_factory.py +219 -0
- wappa/core/factory/__init__.py +15 -0
- wappa/core/factory/plugin.py +68 -0
- wappa/core/factory/wappa_builder.py +326 -0
- wappa/core/logging/__init__.py +5 -0
- wappa/core/logging/context.py +100 -0
- wappa/core/logging/logger.py +343 -0
- wappa/core/plugins/__init__.py +34 -0
- wappa/core/plugins/auth_plugin.py +169 -0
- wappa/core/plugins/cors_plugin.py +128 -0
- wappa/core/plugins/custom_middleware_plugin.py +182 -0
- wappa/core/plugins/database_plugin.py +235 -0
- wappa/core/plugins/rate_limit_plugin.py +183 -0
- wappa/core/plugins/redis_plugin.py +224 -0
- wappa/core/plugins/wappa_core_plugin.py +261 -0
- wappa/core/plugins/webhook_plugin.py +253 -0
- wappa/core/types.py +108 -0
- wappa/core/wappa_app.py +546 -0
- wappa/database/__init__.py +18 -0
- wappa/database/adapter.py +107 -0
- wappa/database/adapters/__init__.py +17 -0
- wappa/database/adapters/mysql_adapter.py +187 -0
- wappa/database/adapters/postgresql_adapter.py +169 -0
- wappa/database/adapters/sqlite_adapter.py +174 -0
- wappa/domain/__init__.py +28 -0
- wappa/domain/builders/__init__.py +5 -0
- wappa/domain/builders/message_builder.py +189 -0
- wappa/domain/entities/__init__.py +5 -0
- wappa/domain/enums/messenger_platform.py +123 -0
- wappa/domain/factories/__init__.py +6 -0
- wappa/domain/factories/media_factory.py +450 -0
- wappa/domain/factories/message_factory.py +497 -0
- wappa/domain/factories/messenger_factory.py +244 -0
- wappa/domain/interfaces/__init__.py +32 -0
- wappa/domain/interfaces/base_repository.py +94 -0
- wappa/domain/interfaces/cache_factory.py +85 -0
- wappa/domain/interfaces/cache_interface.py +199 -0
- wappa/domain/interfaces/expiry_repository.py +68 -0
- wappa/domain/interfaces/media_interface.py +311 -0
- wappa/domain/interfaces/messaging_interface.py +523 -0
- wappa/domain/interfaces/pubsub_repository.py +151 -0
- wappa/domain/interfaces/repository_factory.py +108 -0
- wappa/domain/interfaces/shared_state_repository.py +122 -0
- wappa/domain/interfaces/state_repository.py +123 -0
- wappa/domain/interfaces/tables_repository.py +215 -0
- wappa/domain/interfaces/user_repository.py +114 -0
- wappa/domain/interfaces/webhooks/__init__.py +1 -0
- wappa/domain/models/media_result.py +110 -0
- wappa/domain/models/platforms/__init__.py +15 -0
- wappa/domain/models/platforms/platform_config.py +104 -0
- wappa/domain/services/__init__.py +11 -0
- wappa/domain/services/tenant_credentials_service.py +56 -0
- wappa/messaging/__init__.py +7 -0
- wappa/messaging/whatsapp/__init__.py +1 -0
- wappa/messaging/whatsapp/client/__init__.py +5 -0
- wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
- wappa/messaging/whatsapp/handlers/__init__.py +13 -0
- wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
- wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
- wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
- wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
- wappa/messaging/whatsapp/messenger/__init__.py +5 -0
- wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
- wappa/messaging/whatsapp/models/__init__.py +61 -0
- wappa/messaging/whatsapp/models/basic_models.py +65 -0
- wappa/messaging/whatsapp/models/interactive_models.py +287 -0
- wappa/messaging/whatsapp/models/media_models.py +215 -0
- wappa/messaging/whatsapp/models/specialized_models.py +304 -0
- wappa/messaging/whatsapp/models/template_models.py +261 -0
- wappa/persistence/cache_factory.py +93 -0
- wappa/persistence/json/__init__.py +14 -0
- wappa/persistence/json/cache_adapters.py +271 -0
- wappa/persistence/json/handlers/__init__.py +1 -0
- wappa/persistence/json/handlers/state_handler.py +250 -0
- wappa/persistence/json/handlers/table_handler.py +263 -0
- wappa/persistence/json/handlers/user_handler.py +213 -0
- wappa/persistence/json/handlers/utils/__init__.py +1 -0
- wappa/persistence/json/handlers/utils/file_manager.py +153 -0
- wappa/persistence/json/handlers/utils/key_factory.py +11 -0
- wappa/persistence/json/handlers/utils/serialization.py +121 -0
- wappa/persistence/json/json_cache_factory.py +76 -0
- wappa/persistence/json/storage_manager.py +285 -0
- wappa/persistence/memory/__init__.py +14 -0
- wappa/persistence/memory/cache_adapters.py +271 -0
- wappa/persistence/memory/handlers/__init__.py +1 -0
- wappa/persistence/memory/handlers/state_handler.py +250 -0
- wappa/persistence/memory/handlers/table_handler.py +280 -0
- wappa/persistence/memory/handlers/user_handler.py +213 -0
- wappa/persistence/memory/handlers/utils/__init__.py +1 -0
- wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
- wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
- wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
- wappa/persistence/memory/memory_cache_factory.py +76 -0
- wappa/persistence/memory/storage_manager.py +235 -0
- wappa/persistence/redis/README.md +699 -0
- wappa/persistence/redis/__init__.py +11 -0
- wappa/persistence/redis/cache_adapters.py +285 -0
- wappa/persistence/redis/ops.py +880 -0
- wappa/persistence/redis/redis_cache_factory.py +71 -0
- wappa/persistence/redis/redis_client.py +231 -0
- wappa/persistence/redis/redis_handler/__init__.py +26 -0
- wappa/persistence/redis/redis_handler/state_handler.py +176 -0
- wappa/persistence/redis/redis_handler/table.py +158 -0
- wappa/persistence/redis/redis_handler/user.py +138 -0
- wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
- wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
- wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
- wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
- wappa/persistence/redis/redis_manager.py +189 -0
- wappa/processors/__init__.py +6 -0
- wappa/processors/base_processor.py +262 -0
- wappa/processors/factory.py +550 -0
- wappa/processors/whatsapp_processor.py +810 -0
- wappa/schemas/__init__.py +6 -0
- wappa/schemas/core/__init__.py +71 -0
- wappa/schemas/core/base_message.py +499 -0
- wappa/schemas/core/base_status.py +322 -0
- wappa/schemas/core/base_webhook.py +312 -0
- wappa/schemas/core/types.py +253 -0
- wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
- wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
- wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
- wappa/schemas/factory.py +754 -0
- wappa/schemas/webhooks/__init__.py +3 -0
- wappa/schemas/whatsapp/__init__.py +6 -0
- wappa/schemas/whatsapp/base_models.py +285 -0
- wappa/schemas/whatsapp/message_types/__init__.py +93 -0
- wappa/schemas/whatsapp/message_types/audio.py +350 -0
- wappa/schemas/whatsapp/message_types/button.py +267 -0
- wappa/schemas/whatsapp/message_types/contact.py +464 -0
- wappa/schemas/whatsapp/message_types/document.py +421 -0
- wappa/schemas/whatsapp/message_types/errors.py +195 -0
- wappa/schemas/whatsapp/message_types/image.py +424 -0
- wappa/schemas/whatsapp/message_types/interactive.py +430 -0
- wappa/schemas/whatsapp/message_types/location.py +416 -0
- wappa/schemas/whatsapp/message_types/order.py +372 -0
- wappa/schemas/whatsapp/message_types/reaction.py +271 -0
- wappa/schemas/whatsapp/message_types/sticker.py +328 -0
- wappa/schemas/whatsapp/message_types/system.py +317 -0
- wappa/schemas/whatsapp/message_types/text.py +411 -0
- wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
- wappa/schemas/whatsapp/message_types/video.py +344 -0
- wappa/schemas/whatsapp/status_models.py +479 -0
- wappa/schemas/whatsapp/validators.py +454 -0
- wappa/schemas/whatsapp/webhook_container.py +438 -0
- wappa/webhooks/__init__.py +17 -0
- wappa/webhooks/core/__init__.py +71 -0
- wappa/webhooks/core/base_message.py +499 -0
- wappa/webhooks/core/base_status.py +322 -0
- wappa/webhooks/core/base_webhook.py +312 -0
- wappa/webhooks/core/types.py +253 -0
- wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
- wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
- wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
- wappa/webhooks/factory.py +754 -0
- wappa/webhooks/whatsapp/__init__.py +6 -0
- wappa/webhooks/whatsapp/base_models.py +285 -0
- wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
- wappa/webhooks/whatsapp/message_types/audio.py +350 -0
- wappa/webhooks/whatsapp/message_types/button.py +267 -0
- wappa/webhooks/whatsapp/message_types/contact.py +464 -0
- wappa/webhooks/whatsapp/message_types/document.py +421 -0
- wappa/webhooks/whatsapp/message_types/errors.py +195 -0
- wappa/webhooks/whatsapp/message_types/image.py +424 -0
- wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
- wappa/webhooks/whatsapp/message_types/location.py +416 -0
- wappa/webhooks/whatsapp/message_types/order.py +372 -0
- wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
- wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
- wappa/webhooks/whatsapp/message_types/system.py +317 -0
- wappa/webhooks/whatsapp/message_types/text.py +411 -0
- wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
- wappa/webhooks/whatsapp/message_types/video.py +344 -0
- wappa/webhooks/whatsapp/status_models.py +479 -0
- wappa/webhooks/whatsapp/validators.py +454 -0
- wappa/webhooks/whatsapp/webhook_container.py +438 -0
- wappa-0.1.0.dist-info/METADATA +269 -0
- wappa-0.1.0.dist-info/RECORD +211 -0
- wappa-0.1.0.dist-info/WHEEL +4 -0
- wappa-0.1.0.dist-info/entry_points.txt +2 -0
- wappa-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,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"
|