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,431 @@
1
+ """
2
+ WhatsApp template messaging API endpoints.
3
+
4
+ Provides REST API endpoints for WhatsApp Business template operations:
5
+ - POST /api/whatsapp/templates/send-text: Send text-only templates
6
+ - POST /api/whatsapp/templates/send-media: Send templates with media headers
7
+ - POST /api/whatsapp/templates/send-location: Send templates with location headers
8
+ - GET /api/whatsapp/templates/health: Service health check
9
+
10
+ Router configuration:
11
+ - Prefix: /whatsapp/templates
12
+ - Tags: ["WhatsApp - Templates"]
13
+ - Full URL: /api/whatsapp/templates/ (when included with /api prefix)
14
+ """
15
+
16
+ from fastapi import APIRouter, Depends, HTTPException
17
+
18
+ from wappa.api.dependencies.whatsapp_dependencies import get_whatsapp_messenger
19
+ from wappa.domain.interfaces.messaging_interface import IMessenger
20
+ from wappa.messaging.whatsapp.models.basic_models import MessageResult
21
+ from wappa.messaging.whatsapp.models.template_models import (
22
+ LocationTemplateMessage,
23
+ MediaTemplateMessage,
24
+ TemplateMessageStatus,
25
+ TextTemplateMessage,
26
+ )
27
+
28
+ # Create router with WhatsApp Templates configuration
29
+ router = APIRouter(
30
+ prefix="/whatsapp/templates",
31
+ tags=["WhatsApp - Templates"],
32
+ responses={
33
+ 400: {"description": "Bad Request - Invalid template format or parameters"},
34
+ 401: {"description": "Unauthorized - Invalid tenant credentials"},
35
+ 403: {"description": "Forbidden - Template not approved or access denied"},
36
+ 404: {"description": "Not Found - Template not found"},
37
+ 413: {"description": "Payload Too Large - Template content too large"},
38
+ 429: {"description": "Rate Limited - Too many requests"},
39
+ 500: {"description": "Internal Server Error"},
40
+ },
41
+ )
42
+
43
+
44
+ @router.post(
45
+ "/send-text",
46
+ response_model=MessageResult,
47
+ summary="Send Text Template Message",
48
+ description="Send a text-only template message with parameter substitution",
49
+ )
50
+ async def send_text_template(
51
+ request: TextTemplateMessage,
52
+ messenger: IMessenger = Depends(get_whatsapp_messenger),
53
+ ) -> MessageResult:
54
+ """Send text-only template message via WhatsApp.
55
+
56
+ Sends pre-approved business templates with dynamic parameter substitution.
57
+ Templates must be approved by WhatsApp before use.
58
+ """
59
+ try:
60
+ # Convert Pydantic model to dict format expected by messenger
61
+ body_parameters = None
62
+ if request.body_parameters:
63
+ body_parameters = []
64
+ for param in request.body_parameters:
65
+ body_parameters.append({"type": param.type.value, "text": param.text})
66
+
67
+ result = await messenger.send_text_template(
68
+ template_name=request.template_name,
69
+ recipient=request.recipient,
70
+ body_parameters=body_parameters,
71
+ language_code=request.language.code,
72
+ )
73
+
74
+ if not result.success:
75
+ # Map specific template error codes to HTTP status codes
76
+ if result.error_code in ["TEMPLATE_NOT_FOUND", "TEMPLATE_NOT_APPROVED"]:
77
+ raise HTTPException(status_code=403, detail=result.error)
78
+ elif result.error_code in ["INVALID_PARAMETERS", "MISSING_PARAMETERS"]:
79
+ raise HTTPException(status_code=400, detail=result.error)
80
+ elif result.error_code in ["TEMPLATE_SEND_FAILED"]:
81
+ raise HTTPException(status_code=500, detail=result.error)
82
+ else:
83
+ raise HTTPException(status_code=400, detail=result.error)
84
+
85
+ return result
86
+
87
+ except HTTPException:
88
+ raise
89
+ except Exception as e:
90
+ raise HTTPException(
91
+ status_code=500, detail=f"Failed to send text template: {str(e)}"
92
+ )
93
+
94
+
95
+ @router.post(
96
+ "/send-media",
97
+ response_model=MessageResult,
98
+ summary="Send Media Template Message",
99
+ description="Send a template message with media header (image, video, or document)",
100
+ )
101
+ async def send_media_template(
102
+ request: MediaTemplateMessage,
103
+ messenger: IMessenger = Depends(get_whatsapp_messenger),
104
+ ) -> MessageResult:
105
+ """Send template message with media header via WhatsApp.
106
+
107
+ Supports templates with image, video, or document headers.
108
+ Either media_id (uploaded media) or media_url (external media) must be provided.
109
+ """
110
+ try:
111
+ # Convert Pydantic model to dict format expected by messenger
112
+ body_parameters = None
113
+ if request.body_parameters:
114
+ body_parameters = []
115
+ for param in request.body_parameters:
116
+ body_parameters.append({"type": param.type.value, "text": param.text})
117
+
118
+ result = await messenger.send_media_template(
119
+ template_name=request.template_name,
120
+ recipient=request.recipient,
121
+ media_type=request.media_type.value,
122
+ media_id=request.media_id,
123
+ media_url=request.media_url,
124
+ body_parameters=body_parameters,
125
+ language_code=request.language.code,
126
+ )
127
+
128
+ if not result.success:
129
+ # Map specific template error codes to HTTP status codes
130
+ if result.error_code in ["TEMPLATE_NOT_FOUND", "TEMPLATE_NOT_APPROVED"]:
131
+ raise HTTPException(status_code=403, detail=result.error)
132
+ elif result.error_code in [
133
+ "INVALID_MEDIA_TYPE",
134
+ "MEDIA_NOT_FOUND",
135
+ "INVALID_PARAMETERS",
136
+ ]:
137
+ raise HTTPException(status_code=400, detail=result.error)
138
+ elif result.error_code in ["MEDIA_TEMPLATE_SEND_FAILED"]:
139
+ raise HTTPException(status_code=500, detail=result.error)
140
+ else:
141
+ raise HTTPException(status_code=400, detail=result.error)
142
+
143
+ return result
144
+
145
+ except HTTPException:
146
+ raise
147
+ except Exception as e:
148
+ raise HTTPException(
149
+ status_code=500, detail=f"Failed to send media template: {str(e)}"
150
+ )
151
+
152
+
153
+ @router.post(
154
+ "/send-location",
155
+ response_model=MessageResult,
156
+ summary="Send Location Template Message",
157
+ description="Send a template message with location header and map preview",
158
+ )
159
+ async def send_location_template(
160
+ request: LocationTemplateMessage,
161
+ messenger: IMessenger = Depends(get_whatsapp_messenger),
162
+ ) -> MessageResult:
163
+ """Send template message with location header via WhatsApp.
164
+
165
+ Supports templates with geographic location headers showing a map preview.
166
+ Coordinates must be valid latitude (-90 to 90) and longitude (-180 to 180).
167
+ """
168
+ try:
169
+ # Convert Pydantic model to dict format expected by messenger
170
+ body_parameters = None
171
+ if request.body_parameters:
172
+ body_parameters = []
173
+ for param in request.body_parameters:
174
+ body_parameters.append({"type": param.type.value, "text": param.text})
175
+
176
+ result = await messenger.send_location_template(
177
+ template_name=request.template_name,
178
+ recipient=request.recipient,
179
+ latitude=request.latitude,
180
+ longitude=request.longitude,
181
+ name=request.name,
182
+ address=request.address,
183
+ body_parameters=body_parameters,
184
+ language_code=request.language.code,
185
+ )
186
+
187
+ if not result.success:
188
+ # Map specific template error codes to HTTP status codes
189
+ if result.error_code in ["TEMPLATE_NOT_FOUND", "TEMPLATE_NOT_APPROVED"]:
190
+ raise HTTPException(status_code=403, detail=result.error)
191
+ elif result.error_code in ["INVALID_COORDINATES", "INVALID_PARAMETERS"]:
192
+ raise HTTPException(status_code=400, detail=result.error)
193
+ elif result.error_code in ["LOCATION_TEMPLATE_SEND_FAILED"]:
194
+ raise HTTPException(status_code=500, detail=result.error)
195
+ else:
196
+ raise HTTPException(status_code=400, detail=result.error)
197
+
198
+ return result
199
+
200
+ except HTTPException:
201
+ raise
202
+ except Exception as e:
203
+ raise HTTPException(
204
+ status_code=500, detail=f"Failed to send location template: {str(e)}"
205
+ )
206
+
207
+
208
+ @router.get(
209
+ "/limits",
210
+ summary="Get Template Message Limits",
211
+ description="Get platform-specific template message limits and constraints",
212
+ )
213
+ async def get_template_limits() -> dict:
214
+ """Get WhatsApp template message limits and constraints.
215
+
216
+ Returns supported template types, parameter limits, and platform constraints.
217
+ """
218
+ return {
219
+ "text_templates": {
220
+ "max_body_parameters": 10,
221
+ "max_parameter_length": 1024,
222
+ "supported_parameter_types": ["text", "currency", "date_time"],
223
+ "supported_languages": [
224
+ "es",
225
+ "en",
226
+ "en_US",
227
+ "pt_BR",
228
+ "fr",
229
+ "de",
230
+ "it",
231
+ "ja",
232
+ "ko",
233
+ "zh",
234
+ ],
235
+ },
236
+ "media_templates": {
237
+ "supported_media_types": ["image", "video", "document"],
238
+ "max_body_parameters": 10,
239
+ "media_requirements": {
240
+ "image": {"formats": ["JPEG", "PNG"], "max_size": "5MB"},
241
+ "video": {"formats": ["MP4", "3GP"], "max_size": "16MB"},
242
+ "document": {
243
+ "formats": ["PDF", "DOC", "DOCX", "XLS", "XLSX"],
244
+ "max_size": "100MB",
245
+ },
246
+ },
247
+ },
248
+ "location_templates": {
249
+ "coordinate_ranges": {
250
+ "latitude": {"min": -90, "max": 90},
251
+ "longitude": {"min": -180, "max": 180},
252
+ },
253
+ "max_name_length": 100,
254
+ "max_address_length": 1000,
255
+ "max_body_parameters": 10,
256
+ },
257
+ "general": {
258
+ "requires_approval": True,
259
+ "approval_process": "WhatsApp Business Account Manager",
260
+ "rate_limits": "Per WhatsApp Business API terms",
261
+ "supported_platforms": ["whatsapp"],
262
+ "requires_authentication": True,
263
+ },
264
+ }
265
+
266
+
267
+ @router.get(
268
+ "/status/{template_name}",
269
+ response_model=TemplateMessageStatus,
270
+ summary="Get Template Status",
271
+ description="Get the approval status and configuration of a specific template",
272
+ )
273
+ async def get_template_status(
274
+ template_name: str,
275
+ language: str = "es",
276
+ messenger: IMessenger = Depends(get_whatsapp_messenger),
277
+ ) -> TemplateMessageStatus:
278
+ """Get template status and configuration.
279
+
280
+ Returns the approval status, category, and components of a WhatsApp template.
281
+ """
282
+ try:
283
+ # This would typically call the template handler's get_template_info method
284
+ # For now, return a mock status structure
285
+ return TemplateMessageStatus(
286
+ template_name=template_name,
287
+ status="APPROVED", # This should come from actual API
288
+ language=language,
289
+ category="MARKETING", # This should come from actual API
290
+ components=[
291
+ {"type": "HEADER", "format": "TEXT"},
292
+ {"type": "BODY", "text": "Template body with parameters"},
293
+ {"type": "FOOTER", "text": "Optional footer text"},
294
+ ],
295
+ )
296
+
297
+ except Exception as e:
298
+ raise HTTPException(
299
+ status_code=500, detail=f"Failed to get template status: {str(e)}"
300
+ )
301
+
302
+
303
+ @router.get(
304
+ "/health",
305
+ summary="Template Service Health Check",
306
+ description="Check health status of template messaging services",
307
+ )
308
+ async def health_check(messenger: IMessenger = Depends(get_whatsapp_messenger)) -> dict:
309
+ """Health check for template messaging services.
310
+
311
+ Returns service status and configuration information.
312
+ """
313
+ return {
314
+ "status": "healthy",
315
+ "service": "whatsapp-templates",
316
+ "platform": messenger.platform.value,
317
+ "tenant_id": messenger.tenant_id,
318
+ "template_types": ["text", "media", "location"],
319
+ "message_types_supported": [
320
+ "text",
321
+ "image",
322
+ "video",
323
+ "audio",
324
+ "document",
325
+ "sticker",
326
+ "button",
327
+ "list",
328
+ "cta_url",
329
+ "text_template",
330
+ "media_template",
331
+ "location_template",
332
+ ],
333
+ "features": [
334
+ "Parameter substitution",
335
+ "Multi-language support",
336
+ "Media header templates",
337
+ "Location header templates",
338
+ "Template approval status",
339
+ ],
340
+ }
341
+
342
+
343
+ # Example endpoint demonstrating complex template usage
344
+ @router.post(
345
+ "/send-welcome-template",
346
+ response_model=MessageResult,
347
+ summary="Send Welcome Template (Example)",
348
+ description="Example endpoint showing complex template message with parameters",
349
+ )
350
+ async def send_welcome_template(
351
+ recipient: str,
352
+ customer_name: str,
353
+ business_name: str = "Mimeia Hotel",
354
+ messenger: IMessenger = Depends(get_whatsapp_messenger),
355
+ ) -> MessageResult:
356
+ """Example endpoint demonstrating welcome template with parameters.
357
+
358
+ This endpoint shows how to create a complex template message with
359
+ multiple parameters and proper error handling.
360
+ """
361
+ try:
362
+ # Example welcome template parameters
363
+ body_parameters = [
364
+ {"type": "text", "text": customer_name},
365
+ {"type": "text", "text": business_name},
366
+ ]
367
+
368
+ result = await messenger.send_text_template(
369
+ template_name="welcome_customer",
370
+ recipient=recipient,
371
+ body_parameters=body_parameters,
372
+ language_code="es",
373
+ )
374
+
375
+ if not result.success:
376
+ raise HTTPException(status_code=400, detail=result.error)
377
+
378
+ return result
379
+
380
+ except HTTPException:
381
+ raise
382
+ except Exception as e:
383
+ raise HTTPException(
384
+ status_code=500, detail=f"Failed to send welcome template: {str(e)}"
385
+ )
386
+
387
+
388
+ # Example endpoint for media template with location
389
+ @router.post(
390
+ "/send-store-location-template",
391
+ response_model=MessageResult,
392
+ summary="Send Store Location Template (Example)",
393
+ description="Example endpoint showing location template for business use",
394
+ )
395
+ async def send_store_location_template(
396
+ recipient: str,
397
+ store_name: str = "Main Store",
398
+ messenger: IMessenger = Depends(get_whatsapp_messenger),
399
+ ) -> MessageResult:
400
+ """Example endpoint demonstrating store location template.
401
+
402
+ This endpoint shows how to create a location-based template for
403
+ business location sharing with customers.
404
+ """
405
+ try:
406
+ # Example store location (replace with actual coordinates)
407
+ result = await messenger.send_location_template(
408
+ template_name="store_location",
409
+ recipient=recipient,
410
+ latitude="37.483307",
411
+ longitude="-122.148981",
412
+ name=store_name,
413
+ address="123 Business Ave, Business City, BC 12345",
414
+ body_parameters=[
415
+ {"type": "text", "text": store_name},
416
+ {"type": "text", "text": "Mon-Fri 9AM-6PM"},
417
+ ],
418
+ language_code="es",
419
+ )
420
+
421
+ if not result.success:
422
+ raise HTTPException(status_code=400, detail=result.error)
423
+
424
+ return result
425
+
426
+ except HTTPException:
427
+ raise
428
+ except Exception as e:
429
+ raise HTTPException(
430
+ status_code=500, detail=f"Failed to send store location template: {str(e)}"
431
+ )
@@ -0,0 +1,35 @@
1
+ """
2
+ Combined WhatsApp API router that includes all WhatsApp endpoints.
3
+
4
+ This module combines all WhatsApp API endpoints into a single router for easy inclusion
5
+ in the main Wappa application.
6
+ """
7
+
8
+ from fastapi import APIRouter
9
+
10
+ from .whatsapp import (
11
+ whatsapp_interactive_router,
12
+ whatsapp_media_router,
13
+ whatsapp_messages_router,
14
+ whatsapp_specialized_router,
15
+ whatsapp_templates_router,
16
+ )
17
+
18
+ # Create a combined WhatsApp router
19
+ whatsapp_router = APIRouter(
20
+ prefix="/api/whatsapp",
21
+ tags=["WhatsApp API"],
22
+ responses={
23
+ 400: {"description": "Bad Request - Invalid message format"},
24
+ 401: {"description": "Unauthorized - Invalid tenant credentials"},
25
+ 429: {"description": "Rate Limited - Too many requests"},
26
+ 500: {"description": "Internal Server Error"},
27
+ },
28
+ )
29
+
30
+ # Include all WhatsApp sub-routers
31
+ whatsapp_router.include_router(whatsapp_messages_router)
32
+ whatsapp_router.include_router(whatsapp_media_router)
33
+ whatsapp_router.include_router(whatsapp_interactive_router)
34
+ whatsapp_router.include_router(whatsapp_templates_router)
35
+ whatsapp_router.include_router(whatsapp_specialized_router)
wappa/cli/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """
2
+ Wappa CLI module.
3
+
4
+ Provides command-line interface for Wappa development and deployment workflows.
5
+ """
6
+
7
+ from .main import app
8
+
9
+ __all__ = ["app"]
wappa/cli/main.py ADDED
@@ -0,0 +1,199 @@
1
+ """
2
+ Wappa CLI main module.
3
+
4
+ Provides clean command-line interface for development and production workflows.
5
+ """
6
+
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import typer
13
+
14
+ app = typer.Typer(help="Wappa WhatsApp Business Framework CLI")
15
+
16
+
17
+ def _resolve_module_name(file_path: str) -> tuple[str, Path]:
18
+ """
19
+ Convert a file path to a Python module name and working directory.
20
+
21
+ Examples:
22
+ main.py -> ("main", Path("."))
23
+ examples/redis_demo/main.py -> ("main", Path("examples/redis_demo"))
24
+
25
+ Returns:
26
+ tuple[str, Path]: (module_name, working_directory)
27
+ """
28
+ # Convert to Path object for better handling
29
+ path = Path(file_path)
30
+
31
+ # Get the directory and filename
32
+ working_dir = path.parent if path.parent != Path(".") else Path(".")
33
+ module_name = path.stem # Just the filename without extension
34
+
35
+ return module_name, working_dir
36
+
37
+
38
+ @app.command()
39
+ def dev(
40
+ file_path: str = typer.Argument(
41
+ ..., help="Path to your Python file (e.g., main.py)"
42
+ ),
43
+ app_var: str = typer.Option(
44
+ "app", "--app", "-a", help="Wappa instance variable name"
45
+ ),
46
+ host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind to"),
47
+ port: int = typer.Option(8000, "--port", "-p", help="Port to bind to"),
48
+ ):
49
+ """
50
+ Run development server with auto-reload.
51
+
52
+ Examples:
53
+ wappa dev main.py
54
+ wappa dev examples/redis_demo/main.py --port 8080
55
+ wappa dev src/app.py --app my_wappa_app
56
+ """
57
+ # Validate file exists
58
+ if not Path(file_path).exists():
59
+ typer.echo(f"❌ File not found: {file_path}", err=True)
60
+ raise typer.Exit(1)
61
+
62
+ # Convert file path to module name and working directory
63
+ module_name, working_dir = _resolve_module_name(file_path)
64
+ import_string = f"{module_name}:{app_var}.asgi"
65
+
66
+ # Build uvicorn command
67
+ cmd = [
68
+ sys.executable,
69
+ "-m",
70
+ "uvicorn",
71
+ import_string,
72
+ "--reload",
73
+ "--host",
74
+ host,
75
+ "--port",
76
+ str(port),
77
+ ]
78
+
79
+ typer.echo(f"🚀 Starting Wappa development server...")
80
+ typer.echo(f"📡 Import: {working_dir / module_name}:{app_var}.asgi")
81
+ typer.echo(f"🌐 Server: http://{host}:{port}")
82
+ typer.echo(f"📝 Docs: http://{host}:{port}/docs")
83
+ typer.echo("💡 Press CTRL+C to stop")
84
+ typer.echo()
85
+
86
+ try:
87
+ subprocess.run(cmd, check=True, cwd=working_dir)
88
+ except subprocess.CalledProcessError as e:
89
+ typer.echo(
90
+ f"❌ Development server failed to start (exit code: {e.returncode})",
91
+ err=True,
92
+ )
93
+ typer.echo("", err=True)
94
+ typer.echo("Common issues:", err=True)
95
+ typer.echo(f"• No module-level '{app_var}' variable in {file_path}", err=True)
96
+ typer.echo(
97
+ f"• Port {port} already in use (try --port with different number)", err=True
98
+ )
99
+ typer.echo(f"• Import errors in {file_path} or its dependencies", err=True)
100
+ typer.echo("", err=True)
101
+ typer.echo(
102
+ f"Make sure your file has: {app_var} = Wappa(...) at module level", err=True
103
+ )
104
+ raise typer.Exit(1)
105
+ except KeyboardInterrupt:
106
+ typer.echo("👋 Development server stopped")
107
+
108
+
109
+ @app.command()
110
+ def run(
111
+ file_path: str = typer.Argument(
112
+ ..., help="Path to your Python file (e.g., main.py)"
113
+ ),
114
+ app_var: str = typer.Option(
115
+ "app", "--app", "-a", help="Wappa instance variable name"
116
+ ),
117
+ host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind to"),
118
+ port: int = typer.Option(8000, "--port", "-p", help="Port to bind to"),
119
+ workers: int = typer.Option(
120
+ 1, "--workers", "-w", help="Number of worker processes"
121
+ ),
122
+ ):
123
+ """
124
+ Run production server (no auto-reload).
125
+
126
+ Examples:
127
+ wappa run main.py
128
+ wappa run main.py --workers 4 --port 8080
129
+ """
130
+ # Validate file exists
131
+ if not Path(file_path).exists():
132
+ typer.echo(f"❌ File not found: {file_path}", err=True)
133
+ raise typer.Exit(1)
134
+
135
+ # Convert file path to module name and working directory
136
+ module_name, working_dir = _resolve_module_name(file_path)
137
+ import_string = f"{module_name}:{app_var}.asgi"
138
+
139
+ # Build uvicorn command (no reload for production)
140
+ cmd = [
141
+ sys.executable,
142
+ "-m",
143
+ "uvicorn",
144
+ import_string,
145
+ "--host",
146
+ host,
147
+ "--port",
148
+ str(port),
149
+ "--workers",
150
+ str(workers),
151
+ ]
152
+
153
+ typer.echo(f"🚀 Starting Wappa production server...")
154
+ typer.echo(f"📡 Import: {working_dir / module_name}:{app_var}.asgi")
155
+ typer.echo(f"🌐 Server: http://{host}:{port}")
156
+ typer.echo(f"👥 Workers: {workers}")
157
+ typer.echo("💡 Press CTRL+C to stop")
158
+ typer.echo()
159
+
160
+ try:
161
+ subprocess.run(cmd, check=True, cwd=working_dir)
162
+ except subprocess.CalledProcessError as e:
163
+ typer.echo(
164
+ f"❌ Production server failed to start (exit code: {e.returncode})",
165
+ err=True,
166
+ )
167
+ typer.echo("", err=True)
168
+ typer.echo("Common issues:", err=True)
169
+ typer.echo(f"• No module-level '{app_var}' variable in {file_path}", err=True)
170
+ typer.echo(f"• Port {port} already in use", err=True)
171
+ typer.echo(f"• Import errors in {file_path} or its dependencies", err=True)
172
+ raise typer.Exit(1)
173
+ except KeyboardInterrupt:
174
+ typer.echo("👋 Production server stopped")
175
+
176
+
177
+ @app.command()
178
+ def init(
179
+ project_name: str = typer.Argument(..., help="Project name"),
180
+ template: str = typer.Option(
181
+ "basic", "--template", "-t", help="Project template (basic, redis)"
182
+ ),
183
+ ):
184
+ """
185
+ Initialize a new Wappa project (coming soon).
186
+
187
+ Examples:
188
+ wappa init my-whatsapp-bot
189
+ wappa init my-bot --template redis
190
+ """
191
+ typer.echo("🚧 Project initialization coming soon!")
192
+ typer.echo(f"Project: {project_name}")
193
+ typer.echo(f"Template: {template}")
194
+ typer.echo()
195
+ typer.echo("For now, check out the examples directory for project templates.")
196
+
197
+
198
+ if __name__ == "__main__":
199
+ app()
wappa/core/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Core module for Wappa framework."""
2
+
3
+ from .events import WappaEventHandler
4
+ from .wappa_app import Wappa
5
+
6
+ __all__ = ["Wappa", "WappaEventHandler"]
@@ -0,0 +1,5 @@
1
+ """Configuration module for Wappa framework."""
2
+
3
+ from .settings import settings
4
+
5
+ __all__ = ["settings"]