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,417 @@
1
+ """
2
+ Enhanced WhatsApp Business API client with SOLID principles.
3
+
4
+ Key Design Decisions:
5
+ - phone_number_id IS the tenant_id (WhatsApp Business Account identifier)
6
+ - Pure dependency injection (no fallback session creation)
7
+ - Single responsibility for HTTP operations
8
+ - Proper error handling and logging
9
+ """
10
+
11
+ from datetime import datetime
12
+ from typing import Any
13
+
14
+ import aiohttp
15
+
16
+ from wappa.core.config.settings import settings
17
+ from wappa.core.logging.logger import get_logger
18
+
19
+
20
+ class WhatsAppUrlBuilder:
21
+ """Builds URLs for WhatsApp Business API endpoints."""
22
+
23
+ def __init__(self, base_url: str, api_version: str, phone_number_id: str):
24
+ """Initialize URL builder with configuration.
25
+
26
+ Args:
27
+ base_url: Facebook Graph API base URL
28
+ api_version: WhatsApp API version
29
+ phone_number_id: WhatsApp Business phone number ID (tenant identifier)
30
+ """
31
+ self.base_url = base_url.rstrip("/") # Ensure no trailing slash
32
+ self.api_version = api_version
33
+ self.phone_number_id = phone_number_id
34
+
35
+ def get_messages_url(self) -> str:
36
+ """Build URL for sending messages."""
37
+ return f"{self.base_url}/{self.api_version}/{self.phone_number_id}/messages"
38
+
39
+ def get_media_url(self, media_id: str | None = None) -> str:
40
+ """Build URL for media operations.
41
+
42
+ Args:
43
+ media_id: Optional media ID for specific media operations
44
+
45
+ Returns:
46
+ URL for media endpoint
47
+ """
48
+ if media_id:
49
+ return f"{self.base_url}/{self.api_version}/{media_id}"
50
+ return f"{self.base_url}/{self.api_version}/{self.phone_number_id}/media"
51
+
52
+ def get_endpoint_url(self, endpoint: str) -> str:
53
+ """Build URL for any custom endpoint.
54
+
55
+ Args:
56
+ endpoint: API endpoint path
57
+
58
+ Returns:
59
+ Complete URL for the endpoint
60
+ """
61
+ return f"{self.base_url}/{self.api_version}/{endpoint}"
62
+
63
+
64
+ class WhatsAppFormDataBuilder:
65
+ """Builds form data for WhatsApp multipart requests."""
66
+
67
+ @staticmethod
68
+ def build_form_data(
69
+ payload: dict[str, Any], files: dict[str, Any]
70
+ ) -> aiohttp.FormData:
71
+ """Build FormData for multipart/form-data requests.
72
+
73
+ Args:
74
+ payload: Data fields to include in the form
75
+ files: Files to upload in format {field_name: (filename, file_handle, content_type)}
76
+
77
+ Returns:
78
+ aiohttp.FormData object ready for request
79
+
80
+ Raises:
81
+ ValueError: If file format is invalid
82
+ """
83
+ form = aiohttp.FormData()
84
+
85
+ # Add data fields first (important for WhatsApp API)
86
+ if payload:
87
+ for key, value in payload.items():
88
+ form.add_field(key, str(value))
89
+
90
+ # Add files - WhatsApp expects specifically a 'file' field
91
+ for field_name, file_info in files.items():
92
+ if isinstance(file_info, tuple) and len(file_info) == 3:
93
+ filename, file_handle, content_type = file_info
94
+
95
+ # Read file content if it's a file-like object
96
+ if hasattr(file_handle, "read"):
97
+ file_content = file_handle.read()
98
+ else:
99
+ file_content = file_handle
100
+
101
+ # Add file to FormData with explicit filename and content_type
102
+ form.add_field(
103
+ field_name,
104
+ file_content,
105
+ filename=filename,
106
+ content_type=content_type,
107
+ )
108
+ else:
109
+ raise ValueError(
110
+ f"Invalid file format for field '{field_name}'. "
111
+ f"Expected tuple (filename, file_handle, content_type)"
112
+ )
113
+
114
+ return form
115
+
116
+
117
+ class WhatsAppClient:
118
+ """
119
+ Enhanced WhatsApp Business API client with proper dependency injection.
120
+
121
+ Key Design Decisions:
122
+ - phone_number_id IS the tenant_id (WhatsApp Business Account identifier)
123
+ - Pure dependency injection (no fallback session creation)
124
+ - Single responsibility for HTTP operations
125
+ - Proper error handling and logging
126
+ """
127
+
128
+ # Class-level activity tracking
129
+ last_activity: datetime | None = None
130
+
131
+ def __init__(
132
+ self,
133
+ session: aiohttp.ClientSession,
134
+ access_token: str,
135
+ phone_number_id: str,
136
+ logger: Any | None = None,
137
+ api_version: str = settings.api_version,
138
+ base_url: str = settings.base_url,
139
+ ):
140
+ """Initialize WhatsApp client with dependency injection.
141
+
142
+ Args:
143
+ session: Persistent aiohttp session (managed by FastAPI lifespan)
144
+ access_token: WhatsApp Business API access token for this tenant
145
+ phone_number_id: WhatsApp Business phone number ID (serves as tenant_id)
146
+ logger: Pre-configured logger instance
147
+ api_version: WhatsApp API version to use
148
+ base_url: Facebook Graph API base URL
149
+ """
150
+ self.session = session
151
+ self.access_token = access_token
152
+ self.phone_number_id = phone_number_id # This IS the tenant identifier
153
+ self.logger = logger or get_logger(__name__)
154
+
155
+ # Initialize URL and form builders
156
+ self.url_builder = WhatsAppUrlBuilder(base_url, api_version, phone_number_id)
157
+ self.form_builder = WhatsAppFormDataBuilder()
158
+
159
+ # Log initialization
160
+ self.logger.info(
161
+ f"WhatsApp client initialized for tenant/phone_id: {self.phone_number_id}, "
162
+ f"api_version: {api_version}"
163
+ )
164
+
165
+ @property
166
+ def tenant_id(self) -> str:
167
+ """Get tenant ID (which is the phone_number_id).
168
+
169
+ Note: In WhatsApp Business API, the phone_number_id IS the tenant identifier.
170
+ """
171
+ return self.phone_number_id
172
+
173
+ def _get_headers(self, include_content_type: bool = True) -> dict[str, str]:
174
+ """Get HTTP headers for WhatsApp API requests.
175
+
176
+ Args:
177
+ include_content_type: Whether to include Content-Type header
178
+
179
+ Returns:
180
+ Dictionary of HTTP headers
181
+ """
182
+ headers = {"Authorization": f"Bearer {self.access_token}"}
183
+ if include_content_type:
184
+ headers["Content-Type"] = "application/json"
185
+ return headers
186
+
187
+ def _update_activity(self) -> None:
188
+ """Update last activity timestamp."""
189
+ self.__class__.last_activity = datetime.utcnow()
190
+
191
+ async def post_request(
192
+ self,
193
+ payload: dict[str, Any],
194
+ custom_url: str | None = None,
195
+ files: dict[str, Any] | None = None,
196
+ ) -> dict[str, Any]:
197
+ """Send POST request to WhatsApp API.
198
+
199
+ Args:
200
+ payload: JSON payload for the request
201
+ custom_url: Optional custom URL (defaults to messages endpoint)
202
+ files: Optional files for multipart upload
203
+
204
+ Returns:
205
+ JSON response from WhatsApp API
206
+
207
+ Raises:
208
+ aiohttp.ClientResponseError: For HTTP errors
209
+ Exception: For other request failures
210
+ """
211
+ self._update_activity()
212
+ url = custom_url or self.url_builder.get_messages_url()
213
+
214
+ try:
215
+ if files:
216
+ # Multipart form-data request
217
+ headers = self._get_headers(
218
+ include_content_type=False
219
+ ) # aiohttp sets Content-Type
220
+ data = self.form_builder.build_form_data(payload, files)
221
+
222
+ self.logger.debug(
223
+ f"Sending multipart request to {url} for tenant {self.tenant_id}"
224
+ )
225
+ self.logger.debug(f"Payload: {payload}")
226
+ self.logger.debug(f"Files: {list(files.keys())}")
227
+
228
+ async with self.session.post(
229
+ url, headers=headers, data=data
230
+ ) as response:
231
+ response.raise_for_status()
232
+ response_data = await response.json()
233
+ self.logger.debug(f"Response: {response_data}")
234
+ return response_data
235
+ else:
236
+ # Standard JSON request
237
+ headers = self._get_headers()
238
+
239
+ self.logger.debug(
240
+ f"Sending JSON request to {url} for tenant {self.tenant_id}"
241
+ )
242
+ self.logger.debug(f"Payload: {payload}")
243
+
244
+ async with self.session.post(
245
+ url, headers=headers, json=payload
246
+ ) as response:
247
+ response.raise_for_status()
248
+ response_data = await response.json()
249
+ self.logger.debug(f"Response: {response_data}")
250
+ return response_data
251
+
252
+ except aiohttp.ClientResponseError as http_err:
253
+ # Enhanced error logging
254
+ try:
255
+ error_text = (
256
+ await response.text() if "response" in locals() else "No response"
257
+ )
258
+ except Exception:
259
+ error_text = "Error reading response"
260
+
261
+ # Special handling for authentication errors
262
+ if http_err.status == 401:
263
+ self.logger.error("🚨" * 10)
264
+ self.logger.error(
265
+ "🚨 CRITICAL: WHATSAPP ACCESS TOKEN EXPIRED OR INVALID! 🚨"
266
+ )
267
+ self.logger.error(
268
+ f"🚨 Tenant {self.tenant_id} authentication FAILED - 401 Unauthorized"
269
+ )
270
+ self.logger.error(f"🚨 Token starts with: {self.access_token[:20]}...")
271
+ self.logger.error(f"🚨 URL: {url}")
272
+ self.logger.error(f"🚨 Response: {error_text}")
273
+ self.logger.error(
274
+ "🚨 ACTION REQUIRED: Update WhatsApp access token in environment variables!"
275
+ )
276
+ self.logger.error("🚨" * 10)
277
+ else:
278
+ self.logger.error(
279
+ f"HTTP error for tenant {self.tenant_id}: {http_err.status} - {error_text}"
280
+ )
281
+ self.logger.debug(f"Failed URL: {url}")
282
+ self.logger.debug(
283
+ f"Failed headers: {headers if 'headers' in locals() else 'N/A'}"
284
+ )
285
+ raise
286
+ except Exception as err:
287
+ self.logger.error(f"Unexpected error for tenant {self.tenant_id}: {err}")
288
+ raise
289
+
290
+ async def get_request(
291
+ self, endpoint: str, params: dict[str, Any] | None = None
292
+ ) -> dict[str, Any]:
293
+ """Send GET request to WhatsApp API.
294
+
295
+ Args:
296
+ endpoint: API endpoint (without base URL)
297
+ params: Optional query parameters
298
+
299
+ Returns:
300
+ JSON response from WhatsApp API
301
+
302
+ Raises:
303
+ aiohttp.ClientResponseError: For HTTP errors
304
+ Exception: For other request failures
305
+ """
306
+ self._update_activity()
307
+ url = self.url_builder.get_endpoint_url(endpoint)
308
+
309
+ try:
310
+ async with self.session.get(
311
+ url, headers=self._get_headers(), params=params
312
+ ) as response:
313
+ response.raise_for_status()
314
+ response_data = await response.json()
315
+ self.logger.debug(
316
+ f"GET request to {url} with params: {params} returned: {response_data}"
317
+ )
318
+ return response_data
319
+
320
+ except aiohttp.ClientResponseError as http_err:
321
+ try:
322
+ error_text = (
323
+ await response.text() if "response" in locals() else "No response"
324
+ )
325
+ except Exception:
326
+ error_text = "Error reading response"
327
+ self.logger.error(
328
+ f"HTTP GET error for tenant {self.tenant_id}: {http_err} - {error_text}"
329
+ )
330
+ raise
331
+ except Exception as err:
332
+ self.logger.error(
333
+ f"Unexpected GET error for tenant {self.tenant_id}: {err}"
334
+ )
335
+ raise
336
+
337
+ async def delete_request(
338
+ self, endpoint: str, params: dict[str, Any] | None = None
339
+ ) -> dict[str, Any]:
340
+ """Send DELETE request to WhatsApp API.
341
+
342
+ Args:
343
+ endpoint: API endpoint (without base URL)
344
+ params: Optional query parameters
345
+
346
+ Returns:
347
+ JSON response from WhatsApp API
348
+
349
+ Raises:
350
+ aiohttp.ClientResponseError: For HTTP errors
351
+ Exception: For other request failures
352
+ """
353
+ self._update_activity()
354
+ url = self.url_builder.get_endpoint_url(endpoint)
355
+
356
+ try:
357
+ async with self.session.delete(
358
+ url, headers=self._get_headers(), params=params
359
+ ) as response:
360
+ response.raise_for_status()
361
+ response_data = await response.json()
362
+ self.logger.debug(
363
+ f"DELETE request to {url} with params: {params} returned: {response_data}"
364
+ )
365
+ return response_data
366
+
367
+ except aiohttp.ClientResponseError as http_err:
368
+ try:
369
+ error_text = (
370
+ await response.text() if "response" in locals() else "No response"
371
+ )
372
+ except Exception:
373
+ error_text = "Error reading response"
374
+ self.logger.error(
375
+ f"HTTP DELETE error for tenant {self.tenant_id}: {http_err} - {error_text}"
376
+ )
377
+ raise
378
+ except Exception as err:
379
+ self.logger.error(
380
+ f"Unexpected DELETE error for tenant {self.tenant_id}: {err}"
381
+ )
382
+ raise
383
+
384
+ async def get_request_stream(
385
+ self, url: str, params: dict[str, Any] | None = None
386
+ ) -> tuple[aiohttp.ClientSession, aiohttp.ClientResponse]:
387
+ """Perform streaming GET request.
388
+
389
+ Returns both session and response for streaming. Caller is responsible
390
+ for managing the response lifecycle.
391
+
392
+ Args:
393
+ url: Full URL to request (e.g., direct media URL)
394
+ params: Optional query parameters
395
+
396
+ Returns:
397
+ Tuple of (session, response) for streaming
398
+
399
+ Raises:
400
+ aiohttp.ClientError: For HTTP request failures
401
+ """
402
+ self._update_activity()
403
+
404
+ try:
405
+ response = await self.session.get(
406
+ url, headers=self._get_headers(), params=params
407
+ )
408
+ self.logger.debug(
409
+ f"Streaming GET request to {url} started. Status: {response.status}"
410
+ )
411
+ return self.session, response
412
+
413
+ except aiohttp.ClientError as e:
414
+ self.logger.error(
415
+ f"Streaming GET request failed for tenant {self.tenant_id}: {e}"
416
+ )
417
+ raise
@@ -0,0 +1,13 @@
1
+ """WhatsApp service handlers."""
2
+
3
+ from .whatsapp_interactive_handler import WhatsAppInteractiveHandler
4
+ from .whatsapp_media_handler import WhatsAppMediaHandler
5
+ from .whatsapp_specialized_handler import WhatsAppSpecializedHandler
6
+ from .whatsapp_template_handler import WhatsAppTemplateHandler
7
+
8
+ __all__ = [
9
+ "WhatsAppInteractiveHandler",
10
+ "WhatsAppMediaHandler",
11
+ "WhatsAppSpecializedHandler",
12
+ "WhatsAppTemplateHandler",
13
+ ]