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,304 @@
1
+ """
2
+ WhatsApp specialized message models.
3
+
4
+ Provides Pydantic v2 validation models for WhatsApp specialized operations:
5
+ - ContactMessage: Contact card sharing with comprehensive contact information
6
+ - LocationMessage: Geographic location sharing with coordinates
7
+ - LocationRequestMessage: Interactive location request from users
8
+
9
+ Based on WhatsApp Cloud API 2025 specialized message specifications.
10
+ """
11
+
12
+ from enum import Enum
13
+
14
+ from pydantic import BaseModel, Field, field_validator
15
+
16
+
17
+ class AddressType(str, Enum):
18
+ """Contact address types."""
19
+
20
+ HOME = "HOME"
21
+ WORK = "WORK"
22
+
23
+
24
+ class EmailType(str, Enum):
25
+ """Contact email types."""
26
+
27
+ HOME = "HOME"
28
+ WORK = "WORK"
29
+
30
+
31
+ class PhoneType(str, Enum):
32
+ """Contact phone number types."""
33
+
34
+ HOME = "HOME"
35
+ WORK = "WORK"
36
+ CELL = "CELL"
37
+ MAIN = "MAIN"
38
+ IPHONE = "IPHONE"
39
+ WHATSAPP = "WHATSAPP"
40
+
41
+
42
+ class UrlType(str, Enum):
43
+ """Contact URL types."""
44
+
45
+ HOME = "HOME"
46
+ WORK = "WORK"
47
+
48
+
49
+ class ContactAddress(BaseModel):
50
+ """Contact address information."""
51
+
52
+ street: str | None = Field(None, max_length=500, description="Street address")
53
+ city: str | None = Field(None, max_length=100, description="City name")
54
+ state: str | None = Field(None, max_length=100, description="State or province")
55
+ zip: str | None = Field(None, max_length=20, description="ZIP or postal code")
56
+ country: str | None = Field(None, max_length=100, description="Country name")
57
+ country_code: str | None = Field(
58
+ None, pattern=r"^[A-Z]{2}$", description="ISO country code"
59
+ )
60
+ type: AddressType = Field(..., description="Address type")
61
+
62
+
63
+ class ContactEmail(BaseModel):
64
+ """Contact email information."""
65
+
66
+ email: str = Field(
67
+ ..., pattern=r"^[^@]+@[^@]+\.[^@]+$", description="Email address"
68
+ )
69
+ type: EmailType = Field(..., description="Email type")
70
+
71
+
72
+ class ContactName(BaseModel):
73
+ """Contact name information."""
74
+
75
+ formatted_name: str = Field(
76
+ ..., min_length=1, max_length=100, description="Full formatted name"
77
+ )
78
+ first_name: str | None = Field(None, max_length=50, description="First name")
79
+ last_name: str | None = Field(None, max_length=50, description="Last name")
80
+ middle_name: str | None = Field(None, max_length=50, description="Middle name")
81
+ suffix: str | None = Field(None, max_length=20, description="Name suffix")
82
+ prefix: str | None = Field(None, max_length=20, description="Name prefix")
83
+
84
+ @field_validator("formatted_name")
85
+ @classmethod
86
+ def validate_formatted_name_required(cls, v):
87
+ """Validate that formatted_name is not empty."""
88
+ if not v or not v.strip():
89
+ raise ValueError("formatted_name is required and cannot be empty")
90
+ return v.strip()
91
+
92
+
93
+ class ContactOrganization(BaseModel):
94
+ """Contact organization information."""
95
+
96
+ company: str | None = Field(None, max_length=100, description="Company name")
97
+ department: str | None = Field(None, max_length=100, description="Department")
98
+ title: str | None = Field(None, max_length=100, description="Job title")
99
+
100
+
101
+ class ContactPhone(BaseModel):
102
+ """Contact phone information."""
103
+
104
+ phone: str = Field(
105
+ ..., pattern=r"^\+?[\d\s\-\(\)]{7,20}$", description="Phone number"
106
+ )
107
+ type: PhoneType = Field(..., description="Phone type")
108
+ wa_id: str | None = Field(None, pattern=r"^\d{10,15}$", description="WhatsApp ID")
109
+
110
+
111
+ class ContactUrl(BaseModel):
112
+ """Contact URL information."""
113
+
114
+ url: str = Field(..., pattern=r"^https?://", description="Website URL")
115
+ type: UrlType = Field(..., description="URL type")
116
+
117
+
118
+ class ContactCard(BaseModel):
119
+ """Complete contact card information."""
120
+
121
+ addresses: list[ContactAddress] | None = Field(
122
+ None, max_length=5, description="Contact addresses"
123
+ )
124
+ birthday: str | None = Field(
125
+ None, pattern=r"^\d{4}-\d{2}-\d{2}$", description="Birthday (YYYY-MM-DD)"
126
+ )
127
+ emails: list[ContactEmail] | None = Field(
128
+ None, max_length=5, description="Email addresses"
129
+ )
130
+ name: ContactName = Field(..., description="Contact name information")
131
+ org: ContactOrganization | None = Field(
132
+ None, description="Organization information"
133
+ )
134
+ phones: list[ContactPhone] = Field(
135
+ ..., min_length=1, max_length=5, description="Phone numbers"
136
+ )
137
+ urls: list[ContactUrl] | None = Field(
138
+ None, max_length=5, description="Website URLs"
139
+ )
140
+
141
+ @field_validator("phones")
142
+ @classmethod
143
+ def validate_at_least_one_phone(cls, v):
144
+ """Validate that at least one phone number is provided."""
145
+ if not v or len(v) == 0:
146
+ raise ValueError("At least one phone number is required")
147
+ return v
148
+
149
+
150
+ class ContactMessage(BaseModel):
151
+ """Contact card message request."""
152
+
153
+ recipient: str = Field(
154
+ ..., pattern=r"^\d{10,15}$", description="Recipient phone number"
155
+ )
156
+ contact: ContactCard = Field(..., description="Contact card information")
157
+ reply_to_message_id: str | None = Field(None, description="Message ID to reply to")
158
+
159
+
160
+ class LocationMessage(BaseModel):
161
+ """Location sharing message request."""
162
+
163
+ recipient: str = Field(
164
+ ..., pattern=r"^\d{10,15}$", description="Recipient phone number"
165
+ )
166
+ latitude: float = Field(
167
+ ..., ge=-90, le=90, description="Location latitude (-90 to 90)"
168
+ )
169
+ longitude: float = Field(
170
+ ..., ge=-180, le=180, description="Location longitude (-180 to 180)"
171
+ )
172
+ name: str | None = Field(None, max_length=100, description="Location name")
173
+ address: str | None = Field(None, max_length=1000, description="Location address")
174
+ reply_to_message_id: str | None = Field(None, description="Message ID to reply to")
175
+
176
+ @field_validator("latitude")
177
+ @classmethod
178
+ def validate_latitude_range(cls, v):
179
+ """Validate latitude is within valid range."""
180
+ if not -90 <= v <= 90:
181
+ raise ValueError("Latitude must be between -90 and 90 degrees")
182
+ return v
183
+
184
+ @field_validator("longitude")
185
+ @classmethod
186
+ def validate_longitude_range(cls, v):
187
+ """Validate longitude is within valid range."""
188
+ if not -180 <= v <= 180:
189
+ raise ValueError("Longitude must be between -180 and 180 degrees")
190
+ return v
191
+
192
+
193
+ class LocationRequestMessage(BaseModel):
194
+ """Location request message (asks user to share their location)."""
195
+
196
+ recipient: str = Field(
197
+ ..., pattern=r"^\d{10,15}$", description="Recipient phone number"
198
+ )
199
+ body: str = Field(
200
+ ..., min_length=1, max_length=1024, description="Request message text"
201
+ )
202
+ reply_to_message_id: str | None = Field(None, description="Message ID to reply to")
203
+
204
+ @field_validator("body")
205
+ @classmethod
206
+ def validate_body_length(cls, v):
207
+ """Validate body text length."""
208
+ if len(v) > 1024:
209
+ raise ValueError("Body text cannot exceed 1024 characters")
210
+ return v
211
+
212
+
213
+ class ContactValidationResult(BaseModel):
214
+ """Contact validation result."""
215
+
216
+ valid: bool = Field(..., description="Whether contact is valid")
217
+ errors: list[str] | None = Field(None, description="Validation errors")
218
+ warnings: list[str] | None = Field(None, description="Validation warnings")
219
+
220
+
221
+ class LocationValidationResult(BaseModel):
222
+ """Location validation result."""
223
+
224
+ valid: bool = Field(..., description="Whether location is valid")
225
+ latitude: float | None = Field(None, description="Validated latitude")
226
+ longitude: float | None = Field(None, description="Validated longitude")
227
+ errors: list[str] | None = Field(None, description="Validation errors")
228
+ address_suggestions: list[str] | None = Field(
229
+ None, description="Address suggestions"
230
+ )
231
+
232
+
233
+ # Example contact structures for common use cases
234
+ class BusinessContact(BaseModel):
235
+ """Simplified business contact model."""
236
+
237
+ business_name: str = Field(..., max_length=100, description="Business name")
238
+ phone: str = Field(
239
+ ..., pattern=r"^\+?[\d\s\-\(\)]{7,20}$", description="Business phone"
240
+ )
241
+ email: str | None = Field(
242
+ None, pattern=r"^[^@]+@[^@]+\.[^@]+$", description="Business email"
243
+ )
244
+ website: str | None = Field(
245
+ None, pattern=r"^https?://", description="Business website"
246
+ )
247
+ address: str | None = Field(None, max_length=500, description="Business address")
248
+
249
+ def to_contact_card(self) -> ContactCard:
250
+ """Convert to full ContactCard format."""
251
+ phones = [ContactPhone(phone=self.phone, type=PhoneType.WORK)]
252
+ emails = (
253
+ [ContactEmail(email=self.email, type=EmailType.WORK)]
254
+ if self.email
255
+ else None
256
+ )
257
+ urls = (
258
+ [ContactUrl(url=self.website, type=UrlType.WORK)] if self.website else None
259
+ )
260
+
261
+ return ContactCard(
262
+ name=ContactName(formatted_name=self.business_name),
263
+ phones=phones,
264
+ emails=emails,
265
+ urls=urls,
266
+ org=ContactOrganization(company=self.business_name),
267
+ )
268
+
269
+
270
+ class PersonalContact(BaseModel):
271
+ """Simplified personal contact model."""
272
+
273
+ first_name: str = Field(..., max_length=50, description="First name")
274
+ last_name: str | None = Field(None, max_length=50, description="Last name")
275
+ phone: str = Field(
276
+ ..., pattern=r"^\+?[\d\s\-\(\)]{7,20}$", description="Phone number"
277
+ )
278
+ email: str | None = Field(
279
+ None, pattern=r"^[^@]+@[^@]+\.[^@]+$", description="Email address"
280
+ )
281
+
282
+ def to_contact_card(self) -> ContactCard:
283
+ """Convert to full ContactCard format."""
284
+ formatted_name = (
285
+ f"{self.first_name} {self.last_name}".strip()
286
+ if self.last_name
287
+ else self.first_name
288
+ )
289
+ phones = [ContactPhone(phone=self.phone, type=PhoneType.CELL)]
290
+ emails = (
291
+ [ContactEmail(email=self.email, type=EmailType.HOME)]
292
+ if self.email
293
+ else None
294
+ )
295
+
296
+ return ContactCard(
297
+ name=ContactName(
298
+ formatted_name=formatted_name,
299
+ first_name=self.first_name,
300
+ last_name=self.last_name,
301
+ ),
302
+ phones=phones,
303
+ emails=emails,
304
+ )
@@ -0,0 +1,261 @@
1
+ """
2
+ WhatsApp template message models.
3
+
4
+ Provides Pydantic v2 validation models for WhatsApp template operations:
5
+ - TextTemplateMessage: Text-only template messages
6
+ - MediaTemplateMessage: Template messages with media headers
7
+ - LocationTemplateMessage: Template messages with location headers
8
+
9
+ Based on WhatsApp Cloud API 2025 template message specifications.
10
+ """
11
+
12
+ from enum import Enum
13
+
14
+ from pydantic import BaseModel, Field, field_validator
15
+
16
+
17
+ class TemplateType(str, Enum):
18
+ """Template message types supported by WhatsApp."""
19
+
20
+ TEXT = "text"
21
+ MEDIA = "media"
22
+ LOCATION = "location"
23
+
24
+
25
+ class MediaType(str, Enum):
26
+ """Media types supported in template headers."""
27
+
28
+ IMAGE = "image"
29
+ VIDEO = "video"
30
+ DOCUMENT = "document"
31
+
32
+
33
+ class TemplateParameterType(str, Enum):
34
+ """Template parameter types."""
35
+
36
+ TEXT = "text"
37
+ CURRENCY = "currency"
38
+ DATE_TIME = "date_time"
39
+ IMAGE = "image"
40
+ VIDEO = "video"
41
+ DOCUMENT = "document"
42
+ LOCATION = "location"
43
+
44
+
45
+ class TemplateParameter(BaseModel):
46
+ """Template parameter for dynamic content replacement."""
47
+
48
+ type: TemplateParameterType = Field(..., description="Parameter type")
49
+ text: str | None = Field(
50
+ None, max_length=1024, description="Text content for text parameters"
51
+ )
52
+
53
+ @field_validator("text")
54
+ @classmethod
55
+ def validate_text_required_for_text_type(cls, v, info):
56
+ """Validate that text is provided for text type parameters."""
57
+ if info.data.get("type") == TemplateParameterType.TEXT and not v:
58
+ raise ValueError("Text content is required for text type parameters")
59
+ return v
60
+
61
+
62
+ class TemplateComponent(BaseModel):
63
+ """Template component (header, body, footer, button)."""
64
+
65
+ type: str = Field(..., description="Component type (header, body, footer, button)")
66
+ parameters: list[TemplateParameter] | None = Field(
67
+ None, description="Component parameters"
68
+ )
69
+
70
+
71
+ class TemplateLanguage(BaseModel):
72
+ """Template language configuration."""
73
+
74
+ code: str = Field(default="es", description="BCP-47 language code")
75
+
76
+ @field_validator("code")
77
+ @classmethod
78
+ def validate_language_code(cls, v):
79
+ """Validate BCP-47 language code format."""
80
+ # Basic validation for common language codes
81
+ common_codes = [
82
+ "es",
83
+ "en",
84
+ "en_US",
85
+ "pt_BR",
86
+ "fr",
87
+ "de",
88
+ "it",
89
+ "ja",
90
+ "ko",
91
+ "zh",
92
+ "ar",
93
+ "hi",
94
+ "ru",
95
+ "tr",
96
+ "nl",
97
+ "sv",
98
+ "da",
99
+ "no",
100
+ "pl",
101
+ "cs",
102
+ "hu",
103
+ ]
104
+
105
+ if v not in common_codes and not v.replace("_", "").replace("-", "").isalpha():
106
+ raise ValueError(f"Invalid language code format: {v}")
107
+ return v
108
+
109
+
110
+ class BaseTemplateMessage(BaseModel):
111
+ """Base template message with common fields."""
112
+
113
+ recipient: str = Field(
114
+ ..., pattern=r"^\d{10,15}$", description="Recipient phone number"
115
+ )
116
+ template_name: str = Field(
117
+ ..., min_length=1, max_length=512, description="Template name"
118
+ )
119
+ language: TemplateLanguage = Field(
120
+ default_factory=lambda: TemplateLanguage(), description="Template language"
121
+ )
122
+ template_type: TemplateType = Field(..., description="Type of template message")
123
+
124
+
125
+ class TextTemplateMessage(BaseTemplateMessage):
126
+ """Text-only template message."""
127
+
128
+ template_type: TemplateType = Field(
129
+ default=TemplateType.TEXT, description="Template type"
130
+ )
131
+ body_parameters: list[TemplateParameter] | None = Field(
132
+ None, max_length=10, description="Body parameters for text replacement"
133
+ )
134
+
135
+ @field_validator("body_parameters")
136
+ @classmethod
137
+ def validate_body_parameters(cls, v):
138
+ """Validate body parameters are text type."""
139
+ if v:
140
+ for param in v:
141
+ if param.type != TemplateParameterType.TEXT:
142
+ raise ValueError(
143
+ "Text template body parameters must be of type 'text'"
144
+ )
145
+ return v
146
+
147
+
148
+ class MediaTemplateMessage(BaseTemplateMessage):
149
+ """Template message with media header."""
150
+
151
+ template_type: TemplateType = Field(
152
+ default=TemplateType.MEDIA, description="Template type"
153
+ )
154
+ media_type: MediaType = Field(..., description="Media type for header")
155
+ media_id: str | None = Field(None, min_length=1, description="Uploaded media ID")
156
+ media_url: str | None = Field(None, pattern=r"^https?://", description="Media URL")
157
+ body_parameters: list[TemplateParameter] | None = Field(
158
+ None, max_length=10, description="Body parameters for text replacement"
159
+ )
160
+
161
+ @field_validator("media_id", "media_url")
162
+ @classmethod
163
+ def validate_media_source(cls, v, info):
164
+ """Validate that either media_id or media_url is provided, but not both."""
165
+ values = info.data
166
+ media_id = values.get("media_id")
167
+ media_url = values.get("media_url")
168
+
169
+ if (media_id and media_url) or (not media_id and not media_url):
170
+ raise ValueError(
171
+ "Either media_id or media_url must be provided, but not both"
172
+ )
173
+ return v
174
+
175
+ @field_validator("body_parameters")
176
+ @classmethod
177
+ def validate_body_parameters(cls, v):
178
+ """Validate body parameters are text type."""
179
+ if v:
180
+ for param in v:
181
+ if param.type != TemplateParameterType.TEXT:
182
+ raise ValueError(
183
+ "Media template body parameters must be of type 'text'"
184
+ )
185
+ return v
186
+
187
+
188
+ class LocationTemplateMessage(BaseTemplateMessage):
189
+ """Template message with location header."""
190
+
191
+ template_type: TemplateType = Field(
192
+ default=TemplateType.LOCATION, description="Template type"
193
+ )
194
+ latitude: str = Field(..., description="Location latitude as string")
195
+ longitude: str = Field(..., description="Location longitude as string")
196
+ name: str = Field(..., min_length=1, max_length=100, description="Location name")
197
+ address: str = Field(
198
+ ..., min_length=1, max_length=1000, description="Location address"
199
+ )
200
+ body_parameters: list[TemplateParameter] | None = Field(
201
+ None, max_length=10, description="Body parameters for text replacement"
202
+ )
203
+
204
+ @field_validator("latitude")
205
+ @classmethod
206
+ def validate_latitude(cls, v):
207
+ """Validate latitude range (-90 to 90)."""
208
+ try:
209
+ lat = float(v)
210
+ if not -90 <= lat <= 90:
211
+ raise ValueError("Latitude must be between -90 and 90 degrees")
212
+ except ValueError as e:
213
+ if "could not convert" in str(e):
214
+ raise ValueError("Latitude must be a valid number")
215
+ raise
216
+ return v
217
+
218
+ @field_validator("longitude")
219
+ @classmethod
220
+ def validate_longitude(cls, v):
221
+ """Validate longitude range (-180 to 180)."""
222
+ try:
223
+ lon = float(v)
224
+ if not -180 <= lon <= 180:
225
+ raise ValueError("Longitude must be between -180 and 180 degrees")
226
+ except ValueError as e:
227
+ if "could not convert" in str(e):
228
+ raise ValueError("Longitude must be a valid number")
229
+ raise
230
+ return v
231
+
232
+ @field_validator("body_parameters")
233
+ @classmethod
234
+ def validate_body_parameters(cls, v):
235
+ """Validate body parameters are text type."""
236
+ if v:
237
+ for param in v:
238
+ if param.type != TemplateParameterType.TEXT:
239
+ raise ValueError(
240
+ "Location template body parameters must be of type 'text'"
241
+ )
242
+ return v
243
+
244
+
245
+ class TemplateMessageStatus(BaseModel):
246
+ """Template message delivery status."""
247
+
248
+ template_name: str = Field(..., description="Template name")
249
+ status: str = Field(..., description="Template status")
250
+ language: str = Field(..., description="Template language")
251
+ category: str | None = Field(None, description="Template category")
252
+ components: list[dict] | None = Field(None, description="Template components")
253
+
254
+
255
+ class TemplateValidationResult(BaseModel):
256
+ """Template validation result."""
257
+
258
+ valid: bool = Field(..., description="Whether template is valid")
259
+ template_name: str = Field(..., description="Template name")
260
+ errors: list[str] | None = Field(None, description="Validation errors")
261
+ warnings: list[str] | None = Field(None, description="Validation warnings")
@@ -0,0 +1,93 @@
1
+ """
2
+ Main cache factory selector for Wappa framework.
3
+
4
+ Provides factory selector based on cache type configuration.
5
+ """
6
+
7
+
8
+ from ..domain.interfaces.cache_factory import ICacheFactory
9
+
10
+
11
+ def create_cache_factory(cache_type: str) -> type[ICacheFactory]:
12
+ """
13
+ Create cache factory class based on cache type.
14
+
15
+ Returns factory classes that can be instantiated with context parameters.
16
+ This supports the new context-aware cache factory pattern where context
17
+ (tenant_id, user_id) is injected at construction time.
18
+
19
+ Args:
20
+ cache_type: Type of cache to create ("redis", "json", "memory")
21
+
22
+ Returns:
23
+ Cache factory class for the specified type
24
+
25
+ Raises:
26
+ ValueError: If cache_type is not supported
27
+ ImportError: If required dependencies are not available
28
+ """
29
+ if cache_type == "redis":
30
+ try:
31
+ from .redis.redis_cache_factory import RedisCacheFactory
32
+
33
+ return RedisCacheFactory
34
+ except ImportError as e:
35
+ raise ImportError(
36
+ f"Redis dependencies not available for cache_type='redis': {e}"
37
+ ) from e
38
+
39
+ elif cache_type == "json":
40
+ try:
41
+ from .json.json_cache_factory import JSONCacheFactory
42
+
43
+ return JSONCacheFactory
44
+ except ImportError as e:
45
+ raise ImportError(
46
+ f"JSON cache dependencies not available for cache_type='json': {e}"
47
+ ) from e
48
+
49
+ elif cache_type == "memory":
50
+ try:
51
+ from .memory.memory_cache_factory import MemoryCacheFactory
52
+
53
+ return MemoryCacheFactory
54
+ except ImportError as e:
55
+ raise ImportError(
56
+ f"Memory cache dependencies not available for cache_type='memory': {e}"
57
+ ) from e
58
+
59
+ else:
60
+ raise ValueError(
61
+ f"Unsupported cache_type: {cache_type}. "
62
+ f"Supported types: 'redis', 'json', 'memory'"
63
+ )
64
+
65
+
66
+ # Convenience function for getting cache factory with validation
67
+ def get_cache_factory(
68
+ cache_type: str, *, validate_redis_url: bool = True
69
+ ) -> type[ICacheFactory]:
70
+ """
71
+ Get cache factory class with validation.
72
+
73
+ Args:
74
+ cache_type: Type of cache to create
75
+ validate_redis_url: Whether to validate Redis URL for redis cache type
76
+
77
+ Returns:
78
+ Configured cache factory class
79
+
80
+ Raises:
81
+ ValueError: If configuration is invalid
82
+ ImportError: If required dependencies are not available
83
+ """
84
+ if cache_type == "redis" and validate_redis_url:
85
+ from ..core.config.settings import settings
86
+
87
+ if not settings.has_redis:
88
+ raise ValueError(
89
+ "Redis URL not configured. Set REDIS_URL environment variable "
90
+ "or use a different cache_type"
91
+ )
92
+
93
+ return create_cache_factory(cache_type)
@@ -0,0 +1,14 @@
1
+ """
2
+ JSON-based cache implementation for Wappa framework.
3
+
4
+ Provides persistent cache storage using JSON files on disk.
5
+ Suitable for development, debugging, and single-process deployments.
6
+
7
+ Usage:
8
+ wappa = Wappa(cache="json")
9
+ # Cache files will be created in {project_root}/cache/
10
+ """
11
+
12
+ from .json_cache_factory import JSONCacheFactory
13
+
14
+ __all__ = ["JSONCacheFactory"]