wappa 0.1.8__py3-none-any.whl → 0.1.10__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 (147) hide show
  1. wappa/__init__.py +4 -5
  2. wappa/api/controllers/webhook_controller.py +5 -2
  3. wappa/api/dependencies/__init__.py +0 -5
  4. wappa/api/middleware/error_handler.py +4 -4
  5. wappa/api/middleware/owner.py +11 -5
  6. wappa/api/routes/webhooks.py +2 -2
  7. wappa/cli/__init__.py +1 -1
  8. wappa/cli/examples/init/.env.example +33 -0
  9. wappa/cli/examples/init/app/__init__.py +0 -0
  10. wappa/cli/examples/init/app/main.py +9 -0
  11. wappa/cli/examples/init/app/master_event.py +10 -0
  12. wappa/cli/examples/json_cache_example/.env.example +33 -0
  13. wappa/cli/examples/json_cache_example/app/__init__.py +1 -0
  14. wappa/cli/examples/json_cache_example/app/main.py +247 -0
  15. wappa/cli/examples/json_cache_example/app/master_event.py +455 -0
  16. wappa/cli/examples/json_cache_example/app/models/__init__.py +1 -0
  17. wappa/cli/examples/json_cache_example/app/models/json_demo_models.py +256 -0
  18. wappa/cli/examples/json_cache_example/app/scores/__init__.py +35 -0
  19. wappa/cli/examples/json_cache_example/app/scores/score_base.py +192 -0
  20. wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +256 -0
  21. wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +187 -0
  22. wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +272 -0
  23. wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +239 -0
  24. wappa/cli/examples/json_cache_example/app/utils/__init__.py +26 -0
  25. wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +174 -0
  26. wappa/cli/examples/json_cache_example/app/utils/message_utils.py +251 -0
  27. wappa/cli/examples/openai_transcript/.gitignore +63 -4
  28. wappa/cli/examples/openai_transcript/app/__init__.py +0 -0
  29. wappa/cli/examples/openai_transcript/app/main.py +9 -0
  30. wappa/cli/examples/openai_transcript/app/master_event.py +62 -0
  31. wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +3 -0
  32. wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +89 -0
  33. wappa/cli/examples/redis_cache_example/.env.example +33 -0
  34. wappa/cli/examples/redis_cache_example/app/__init__.py +6 -0
  35. wappa/cli/examples/redis_cache_example/app/main.py +246 -0
  36. wappa/cli/examples/redis_cache_example/app/master_event.py +455 -0
  37. wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +256 -0
  38. wappa/cli/examples/redis_cache_example/app/scores/__init__.py +35 -0
  39. wappa/cli/examples/redis_cache_example/app/scores/score_base.py +192 -0
  40. wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +256 -0
  41. wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +187 -0
  42. wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +272 -0
  43. wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +239 -0
  44. wappa/cli/examples/redis_cache_example/app/utils/__init__.py +26 -0
  45. wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +174 -0
  46. wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +251 -0
  47. wappa/cli/examples/simple_echo_example/.env.example +33 -0
  48. wappa/cli/examples/simple_echo_example/app/__init__.py +7 -0
  49. wappa/cli/examples/simple_echo_example/app/main.py +191 -0
  50. wappa/cli/examples/simple_echo_example/app/master_event.py +230 -0
  51. wappa/cli/examples/wappa_full_example/.env.example +33 -0
  52. wappa/cli/examples/wappa_full_example/.gitignore +63 -4
  53. wappa/cli/examples/wappa_full_example/app/__init__.py +6 -0
  54. wappa/cli/examples/wappa_full_example/app/handlers/__init__.py +5 -0
  55. wappa/cli/examples/wappa_full_example/app/handlers/command_handlers.py +492 -0
  56. wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +559 -0
  57. wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +514 -0
  58. wappa/cli/examples/wappa_full_example/app/main.py +269 -0
  59. wappa/cli/examples/wappa_full_example/app/master_event.py +504 -0
  60. wappa/cli/examples/wappa_full_example/app/media/README.md +54 -0
  61. wappa/cli/examples/wappa_full_example/app/media/buttons/README.md +62 -0
  62. wappa/cli/examples/wappa_full_example/app/media/buttons/kitty.png +0 -0
  63. wappa/cli/examples/wappa_full_example/app/media/buttons/puppy.png +0 -0
  64. wappa/cli/examples/wappa_full_example/app/media/list/README.md +110 -0
  65. wappa/cli/examples/wappa_full_example/app/media/list/audio.mp3 +0 -0
  66. wappa/cli/examples/wappa_full_example/app/media/list/document.pdf +0 -0
  67. wappa/cli/examples/wappa_full_example/app/media/list/image.png +0 -0
  68. wappa/cli/examples/wappa_full_example/app/media/list/video.mp4 +0 -0
  69. wappa/cli/examples/wappa_full_example/app/models/__init__.py +5 -0
  70. wappa/cli/examples/wappa_full_example/app/models/state_models.py +434 -0
  71. wappa/cli/examples/wappa_full_example/app/models/user_models.py +303 -0
  72. wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +327 -0
  73. wappa/cli/examples/wappa_full_example/app/utils/__init__.py +5 -0
  74. wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +502 -0
  75. wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +516 -0
  76. wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +337 -0
  77. wappa/cli/main.py +14 -5
  78. wappa/core/__init__.py +18 -23
  79. wappa/core/config/settings.py +7 -5
  80. wappa/core/events/default_handlers.py +1 -1
  81. wappa/core/factory/wappa_builder.py +38 -25
  82. wappa/core/plugins/redis_plugin.py +1 -3
  83. wappa/core/plugins/wappa_core_plugin.py +7 -6
  84. wappa/core/types.py +12 -12
  85. wappa/core/wappa_app.py +10 -8
  86. wappa/database/__init__.py +3 -4
  87. wappa/domain/enums/messenger_platform.py +1 -2
  88. wappa/domain/factories/media_factory.py +5 -20
  89. wappa/domain/factories/message_factory.py +5 -20
  90. wappa/domain/factories/messenger_factory.py +2 -4
  91. wappa/domain/interfaces/cache_interface.py +7 -7
  92. wappa/domain/interfaces/media_interface.py +2 -5
  93. wappa/domain/models/media_result.py +1 -3
  94. wappa/domain/models/platforms/platform_config.py +1 -3
  95. wappa/messaging/__init__.py +9 -12
  96. wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +20 -22
  97. wappa/models/__init__.py +27 -35
  98. wappa/persistence/__init__.py +12 -15
  99. wappa/persistence/cache_factory.py +0 -1
  100. wappa/persistence/json/__init__.py +1 -1
  101. wappa/persistence/json/cache_adapters.py +37 -25
  102. wappa/persistence/json/handlers/state_handler.py +60 -52
  103. wappa/persistence/json/handlers/table_handler.py +51 -49
  104. wappa/persistence/json/handlers/user_handler.py +71 -55
  105. wappa/persistence/json/handlers/utils/file_manager.py +42 -39
  106. wappa/persistence/json/handlers/utils/key_factory.py +1 -1
  107. wappa/persistence/json/handlers/utils/serialization.py +13 -11
  108. wappa/persistence/json/json_cache_factory.py +4 -8
  109. wappa/persistence/json/storage_manager.py +66 -79
  110. wappa/persistence/memory/__init__.py +1 -1
  111. wappa/persistence/memory/cache_adapters.py +37 -25
  112. wappa/persistence/memory/handlers/state_handler.py +62 -52
  113. wappa/persistence/memory/handlers/table_handler.py +59 -53
  114. wappa/persistence/memory/handlers/user_handler.py +75 -55
  115. wappa/persistence/memory/handlers/utils/key_factory.py +1 -1
  116. wappa/persistence/memory/handlers/utils/memory_store.py +75 -71
  117. wappa/persistence/memory/handlers/utils/ttl_manager.py +59 -67
  118. wappa/persistence/memory/memory_cache_factory.py +3 -7
  119. wappa/persistence/memory/storage_manager.py +52 -62
  120. wappa/persistence/redis/cache_adapters.py +27 -21
  121. wappa/persistence/redis/ops.py +11 -11
  122. wappa/persistence/redis/redis_client.py +4 -6
  123. wappa/persistence/redis/redis_manager.py +12 -4
  124. wappa/processors/factory.py +5 -5
  125. wappa/schemas/factory.py +2 -5
  126. wappa/schemas/whatsapp/message_types/errors.py +3 -12
  127. wappa/schemas/whatsapp/validators.py +3 -3
  128. wappa/webhooks/__init__.py +17 -18
  129. wappa/webhooks/factory.py +3 -5
  130. wappa/webhooks/whatsapp/__init__.py +10 -13
  131. wappa/webhooks/whatsapp/message_types/audio.py +0 -4
  132. wappa/webhooks/whatsapp/message_types/document.py +1 -9
  133. wappa/webhooks/whatsapp/message_types/errors.py +3 -12
  134. wappa/webhooks/whatsapp/message_types/location.py +1 -21
  135. wappa/webhooks/whatsapp/message_types/sticker.py +1 -5
  136. wappa/webhooks/whatsapp/message_types/text.py +0 -6
  137. wappa/webhooks/whatsapp/message_types/video.py +1 -20
  138. wappa/webhooks/whatsapp/status_models.py +2 -2
  139. wappa/webhooks/whatsapp/validators.py +3 -3
  140. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/METADATA +362 -8
  141. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/RECORD +144 -80
  142. wappa/cli/examples/init/pyproject.toml +0 -7
  143. wappa/cli/examples/simple_echo_example/.python-version +0 -1
  144. wappa/cli/examples/simple_echo_example/pyproject.toml +0 -9
  145. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/WHEEL +0 -0
  146. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/entry_points.txt +0 -0
  147. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,337 @@
1
+ """
2
+ Metadata extraction utilities for webhook processing.
3
+
4
+ This module provides functions to extract comprehensive metadata from
5
+ IncomingMessageWebhook objects based on message type.
6
+ """
7
+
8
+ import time
9
+
10
+ from wappa.webhooks import IncomingMessageWebhook
11
+
12
+ from ..models.webhook_metadata import (
13
+ ContactMessageMetadata,
14
+ InteractiveMessageMetadata,
15
+ LocationMessageMetadata,
16
+ MediaMessageMetadata,
17
+ MessageType,
18
+ TextMessageMetadata,
19
+ UnknownMessageMetadata,
20
+ WebhookMetadata,
21
+ )
22
+
23
+
24
+ class MetadataExtractor:
25
+ """Utility class for extracting metadata from webhooks."""
26
+
27
+ @staticmethod
28
+ def extract_metadata(
29
+ webhook: IncomingMessageWebhook, start_time: float = None
30
+ ) -> WebhookMetadata:
31
+ """
32
+ Extract appropriate metadata from webhook based on message type.
33
+
34
+ Args:
35
+ webhook: IncomingMessageWebhook to extract metadata from
36
+ start_time: Optional start time for processing time calculation
37
+
38
+ Returns:
39
+ WebhookMetadata object with extracted information
40
+ """
41
+ # Calculate processing time if start_time provided
42
+ processing_time_ms = None
43
+ if start_time is not None:
44
+ processing_time_ms = int((time.time() - start_time) * 1000)
45
+
46
+ # Get message type from webhook
47
+ message_type_name = webhook.get_message_type_name().lower()
48
+
49
+ try:
50
+ # Map message type string to enum
51
+ message_type = MessageType(message_type_name)
52
+ except ValueError:
53
+ # Handle unknown message types
54
+ return UnknownMessageMetadata.from_webhook(webhook, processing_time_ms)
55
+
56
+ # Extract metadata based on message type
57
+ if message_type == MessageType.TEXT:
58
+ return TextMessageMetadata.from_webhook(webhook, processing_time_ms)
59
+
60
+ elif message_type in [
61
+ MessageType.IMAGE,
62
+ MessageType.VIDEO,
63
+ MessageType.AUDIO,
64
+ MessageType.VOICE,
65
+ MessageType.DOCUMENT,
66
+ MessageType.STICKER,
67
+ ]:
68
+ return MediaMessageMetadata.from_webhook(
69
+ webhook, message_type, processing_time_ms
70
+ )
71
+
72
+ elif message_type == MessageType.LOCATION:
73
+ return LocationMessageMetadata.from_webhook(webhook, processing_time_ms)
74
+
75
+ elif message_type in [MessageType.CONTACT, MessageType.CONTACTS]:
76
+ return ContactMessageMetadata.from_webhook(webhook, processing_time_ms)
77
+
78
+ elif message_type == MessageType.INTERACTIVE:
79
+ return InteractiveMessageMetadata.from_webhook(webhook, processing_time_ms)
80
+
81
+ else:
82
+ # Fallback for unsupported types
83
+ return UnknownMessageMetadata.from_webhook(webhook, processing_time_ms)
84
+
85
+ @staticmethod
86
+ def format_metadata_for_user(metadata: WebhookMetadata) -> str:
87
+ """
88
+ Format metadata for display to user.
89
+
90
+ Args:
91
+ metadata: WebhookMetadata object to format
92
+
93
+ Returns:
94
+ Formatted string for user display
95
+ """
96
+ lines = []
97
+ lines.append("📊 *Message Metadata*")
98
+ lines.append("─" * 30)
99
+
100
+ # Basic information
101
+ lines.append(f"🆔 Message ID: `{metadata.message_id[:20]}...`")
102
+ # Handle both enum and string values for message_type
103
+ message_type_str = (
104
+ metadata.message_type.value
105
+ if hasattr(metadata.message_type, "value")
106
+ else str(metadata.message_type)
107
+ )
108
+ lines.append(f"📱 Message Type: `{message_type_str}`")
109
+ lines.append(f"👤 User: {metadata.user_name or metadata.user_id}")
110
+ lines.append(
111
+ f"🕐 Timestamp: {metadata.timestamp.strftime('%Y-%m-%d %H:%M:%S')}"
112
+ )
113
+ lines.append(f"🏢 Tenant: `{metadata.tenant_id}`")
114
+ lines.append(f"🌐 Platform: `{metadata.platform}`")
115
+
116
+ # Processing metadata
117
+ if metadata.processing_time_ms:
118
+ lines.append(f"⚡ Processing Time: {metadata.processing_time_ms}ms")
119
+
120
+ if metadata.cache_hit:
121
+ lines.append("💾 Cache: Hit ✅")
122
+
123
+ # Type-specific metadata
124
+ if isinstance(metadata, TextMessageMetadata):
125
+ lines.append("")
126
+ lines.append("📝 *Text Message Details:*")
127
+ lines.append(f"📏 Length: {metadata.text_length} characters")
128
+ if metadata.has_urls:
129
+ lines.append("🔗 Contains URLs: Yes")
130
+ if metadata.has_mentions:
131
+ lines.append("👥 Contains Mentions: Yes")
132
+ if metadata.is_forwarded:
133
+ lines.append("↪️ Forwarded: Yes")
134
+
135
+ elif isinstance(metadata, MediaMessageMetadata):
136
+ lines.append("")
137
+ lines.append(f"🎬 *{metadata.media_type.title()} Media Details:*")
138
+ lines.append(f"🆔 Media ID: `{metadata.media_id[:20]}...`")
139
+ if metadata.mime_type:
140
+ lines.append(f"📄 MIME Type: `{metadata.mime_type}`")
141
+ if metadata.file_size:
142
+ lines.append(
143
+ f"📦 File Size: {MetadataExtractor._format_file_size(metadata.file_size)}"
144
+ )
145
+ if metadata.caption:
146
+ lines.append(
147
+ f"💬 Caption: {metadata.caption[:50]}{'...' if len(metadata.caption) > 50 else ''}"
148
+ )
149
+ if metadata.width and metadata.height:
150
+ lines.append(f"📐 Dimensions: {metadata.width}x{metadata.height}")
151
+ if metadata.duration:
152
+ lines.append(f"⏱️ Duration: {metadata.duration}s")
153
+ if metadata.filename:
154
+ lines.append(f"📎 Filename: `{metadata.filename}`")
155
+ if metadata.is_forwarded:
156
+ lines.append("↪️ Forwarded: Yes")
157
+
158
+ elif isinstance(metadata, LocationMessageMetadata):
159
+ lines.append("")
160
+ lines.append("📍 *Location Details:*")
161
+ lines.append(f"🌍 Coordinates: {metadata.latitude}, {metadata.longitude}")
162
+ if metadata.location_name:
163
+ lines.append(f"🏷️ Name: {metadata.location_name}")
164
+ if metadata.location_address:
165
+ lines.append(f"🏠 Address: {metadata.location_address}")
166
+ if metadata.is_forwarded:
167
+ lines.append("↪️ Forwarded: Yes")
168
+
169
+ elif isinstance(metadata, ContactMessageMetadata):
170
+ lines.append("")
171
+ lines.append("👥 *Contact Details:*")
172
+ lines.append(f"📇 Contact Count: {metadata.contacts_count}")
173
+ if metadata.contact_names:
174
+ lines.append(
175
+ f"👤 Names: {', '.join(metadata.contact_names[:3])}{'...' if len(metadata.contact_names) > 3 else ''}"
176
+ )
177
+ if metadata.has_phone_numbers:
178
+ lines.append("📞 Has Phone Numbers: Yes")
179
+ if metadata.has_emails:
180
+ lines.append("✉️ Has Emails: Yes")
181
+ if metadata.is_forwarded:
182
+ lines.append("↪️ Forwarded: Yes")
183
+
184
+ elif isinstance(metadata, InteractiveMessageMetadata):
185
+ lines.append("")
186
+ lines.append("🔘 *Interactive Details:*")
187
+ lines.append(f"⚡ Type: {metadata.interaction_type}")
188
+ lines.append(f"🆔 Selection ID: `{metadata.selection_id}`")
189
+ if metadata.selection_title:
190
+ lines.append(f"🏷️ Selection: {metadata.selection_title}")
191
+ if metadata.context_message_id:
192
+ lines.append(
193
+ f"💬 Context Message: `{metadata.context_message_id[:20]}...`"
194
+ )
195
+
196
+ elif isinstance(metadata, UnknownMessageMetadata):
197
+ lines.append("")
198
+ lines.append("❓ *Unknown Message Type*")
199
+ lines.append("🔍 Raw data captured for debugging")
200
+
201
+ return "\n".join(lines)
202
+
203
+ @staticmethod
204
+ def get_metadata_summary(metadata: WebhookMetadata) -> dict:
205
+ """
206
+ Get a summary of metadata for logging/analytics.
207
+
208
+ Args:
209
+ metadata: WebhookMetadata object to summarize
210
+
211
+ Returns:
212
+ Dictionary with metadata summary
213
+ """
214
+ # Handle both enum and string values for message_type
215
+ message_type_value = (
216
+ metadata.message_type.value
217
+ if hasattr(metadata.message_type, "value")
218
+ else str(metadata.message_type)
219
+ )
220
+
221
+ summary = {
222
+ "message_id": metadata.message_id,
223
+ "message_type": message_type_value,
224
+ "user_id": metadata.user_id,
225
+ "timestamp": metadata.timestamp.isoformat(),
226
+ "processing_time_ms": metadata.processing_time_ms,
227
+ "cache_hit": metadata.cache_hit,
228
+ }
229
+
230
+ # Add type-specific summary data
231
+ if isinstance(metadata, TextMessageMetadata):
232
+ summary.update(
233
+ {
234
+ "text_length": metadata.text_length,
235
+ "has_urls": metadata.has_urls,
236
+ "has_mentions": metadata.has_mentions,
237
+ "is_forwarded": metadata.is_forwarded,
238
+ }
239
+ )
240
+
241
+ elif isinstance(metadata, MediaMessageMetadata):
242
+ summary.update(
243
+ {
244
+ "media_id": metadata.media_id,
245
+ "media_type": metadata.media_type,
246
+ "file_size": metadata.file_size,
247
+ "has_caption": bool(metadata.caption),
248
+ "is_forwarded": metadata.is_forwarded,
249
+ }
250
+ )
251
+
252
+ elif isinstance(metadata, LocationMessageMetadata):
253
+ summary.update(
254
+ {
255
+ "latitude": metadata.latitude,
256
+ "longitude": metadata.longitude,
257
+ "has_name": bool(metadata.location_name),
258
+ "has_address": bool(metadata.location_address),
259
+ "is_forwarded": metadata.is_forwarded,
260
+ }
261
+ )
262
+
263
+ elif isinstance(metadata, ContactMessageMetadata):
264
+ summary.update(
265
+ {
266
+ "contacts_count": metadata.contacts_count,
267
+ "has_phone_numbers": metadata.has_phone_numbers,
268
+ "has_emails": metadata.has_emails,
269
+ "is_forwarded": metadata.is_forwarded,
270
+ }
271
+ )
272
+
273
+ elif isinstance(metadata, InteractiveMessageMetadata):
274
+ summary.update(
275
+ {
276
+ "interaction_type": metadata.interaction_type,
277
+ "selection_id": metadata.selection_id,
278
+ "has_title": bool(metadata.selection_title),
279
+ }
280
+ )
281
+
282
+ return summary
283
+
284
+ @staticmethod
285
+ def _format_file_size(size_bytes: int) -> str:
286
+ """Format file size in human-readable format."""
287
+ if size_bytes < 1024:
288
+ return f"{size_bytes} B"
289
+ elif size_bytes < 1024 * 1024:
290
+ return f"{size_bytes / 1024:.1f} KB"
291
+ elif size_bytes < 1024 * 1024 * 1024:
292
+ return f"{size_bytes / (1024 * 1024):.1f} MB"
293
+ else:
294
+ return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
295
+
296
+
297
+ # Convenience functions for direct use
298
+ def extract_webhook_metadata(
299
+ webhook: IncomingMessageWebhook, start_time: float = None
300
+ ) -> WebhookMetadata:
301
+ """
302
+ Extract metadata from webhook (convenience function).
303
+
304
+ Args:
305
+ webhook: IncomingMessageWebhook to process
306
+ start_time: Optional start time for processing measurement
307
+
308
+ Returns:
309
+ WebhookMetadata object
310
+ """
311
+ return MetadataExtractor.extract_metadata(webhook, start_time)
312
+
313
+
314
+ def format_metadata_message(metadata: WebhookMetadata) -> str:
315
+ """
316
+ Format metadata for user display (convenience function).
317
+
318
+ Args:
319
+ metadata: WebhookMetadata to format
320
+
321
+ Returns:
322
+ Formatted string for user
323
+ """
324
+ return MetadataExtractor.format_metadata_for_user(metadata)
325
+
326
+
327
+ def get_processing_summary(metadata: WebhookMetadata) -> dict:
328
+ """
329
+ Get processing summary (convenience function).
330
+
331
+ Args:
332
+ metadata: WebhookMetadata to summarize
333
+
334
+ Returns:
335
+ Summary dictionary
336
+ """
337
+ return MetadataExtractor.get_metadata_summary(metadata)
wappa/cli/main.py CHANGED
@@ -127,7 +127,7 @@ def _initialize_project(directory: str) -> None:
127
127
  try:
128
128
  # Create directory structure
129
129
  (project_path / "app").mkdir(exist_ok=True)
130
- (project_path / "scores").mkdir(exist_ok=True)
130
+ (project_path / "app" / "scores").mkdir(exist_ok=True)
131
131
 
132
132
  typer.echo("📁 Created directory structure")
133
133
 
@@ -136,7 +136,7 @@ def _initialize_project(directory: str) -> None:
136
136
  "app/__init__.py": "__init__.py.template",
137
137
  "app/main.py": "main.py.template",
138
138
  "app/master_event.py": "master_event.py.template",
139
- "scores/__init__.py": "__init__.py.template",
139
+ "app/scores/__init__.py": "__init__.py.template",
140
140
  ".gitignore": "gitignore.template",
141
141
  ".env": "env.template",
142
142
  }
@@ -398,7 +398,7 @@ def _show_examples_menu(target_directory: str) -> None:
398
398
  table.add_column("Key Features", style="yellow")
399
399
 
400
400
  example_keys = list(EXAMPLES.keys())
401
- for i, (key, example) in enumerate(EXAMPLES.items(), 1):
401
+ for i, (_key, example) in enumerate(EXAMPLES.items(), 1):
402
402
  features_text = ", ".join(example["features"][:3]) # Show first 3 features
403
403
  if len(example["features"]) > 3:
404
404
  features_text += "..."
@@ -493,13 +493,22 @@ def _copy_example(example_key: str, target_directory: str) -> None:
493
493
 
494
494
  console.print(f"🚀 Copying {EXAMPLES[example_key]['name']} to {target_path}")
495
495
 
496
- # Copy all files from the example
496
+ # Copy all files from the example (including hidden files, excluding .git and __pycache__)
497
497
  for item in source_path.iterdir():
498
+ # Skip .git and __pycache__ directories
499
+ if item.name in {".git", "__pycache__"}:
500
+ continue
501
+
498
502
  if item.is_file():
499
503
  shutil.copy2(item, target_path / item.name)
500
504
  console.print(f"📝 Copied: {item.name}")
501
505
  elif item.is_dir():
502
- shutil.copytree(item, target_path / item.name, dirs_exist_ok=True)
506
+ shutil.copytree(
507
+ item,
508
+ target_path / item.name,
509
+ dirs_exist_ok=True,
510
+ ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
511
+ )
503
512
  console.print(f"📁 Copied: {item.name}/")
504
513
 
505
514
  console.print("\n✅ Example copied successfully!")
wappa/core/__init__.py CHANGED
@@ -10,37 +10,37 @@ Clean Architecture: Core application logic and framework components.
10
10
  # Configuration & Settings
11
11
  from .config.settings import settings
12
12
 
13
- # Logging System
14
- from .logging import get_logger, get_app_logger, setup_app_logging
15
-
16
13
  # Event System
17
14
  from .events import (
18
- WappaEventHandler,
19
- WappaEventDispatcher,
20
- DefaultMessageHandler,
21
- DefaultStatusHandler,
22
15
  DefaultErrorHandler,
23
16
  DefaultHandlerFactory,
24
- MessageLogStrategy,
25
- StatusLogStrategy,
17
+ DefaultMessageHandler,
18
+ DefaultStatusHandler,
26
19
  ErrorLogStrategy,
27
- WebhookURLFactory,
20
+ MessageLogStrategy,
21
+ StatusLogStrategy,
22
+ WappaEventDispatcher,
23
+ WappaEventHandler,
28
24
  WebhookEndpointType,
25
+ WebhookURLFactory,
29
26
  webhook_url_factory,
30
27
  )
31
28
 
32
29
  # Factory System
33
30
  from .factory import WappaBuilder, WappaPlugin
34
31
 
32
+ # Logging System
33
+ from .logging import get_app_logger, get_logger, setup_app_logging
34
+
35
35
  # Plugin System
36
36
  from .plugins import (
37
- WappaCorePlugin,
38
37
  AuthPlugin,
39
38
  CORSPlugin,
39
+ CustomMiddlewarePlugin,
40
40
  DatabasePlugin,
41
- RedisPlugin,
42
41
  RateLimitPlugin,
43
- CustomMiddlewarePlugin,
42
+ RedisPlugin,
43
+ WappaCorePlugin,
44
44
  WebhookPlugin,
45
45
  )
46
46
 
@@ -50,16 +50,14 @@ from .wappa_app import Wappa
50
50
  __all__ = [
51
51
  # Configuration
52
52
  "settings",
53
-
54
53
  # Logging
55
- "get_logger",
54
+ "get_logger",
56
55
  "get_app_logger",
57
56
  "setup_app_logging",
58
-
59
57
  # Event System
60
58
  "WappaEventHandler",
61
59
  "WappaEventDispatcher",
62
- "DefaultMessageHandler",
60
+ "DefaultMessageHandler",
63
61
  "DefaultStatusHandler",
64
62
  "DefaultErrorHandler",
65
63
  "DefaultHandlerFactory",
@@ -67,23 +65,20 @@ __all__ = [
67
65
  "StatusLogStrategy",
68
66
  "ErrorLogStrategy",
69
67
  "WebhookURLFactory",
70
- "WebhookEndpointType",
68
+ "WebhookEndpointType",
71
69
  "webhook_url_factory",
72
-
73
70
  # Factory System
74
71
  "WappaBuilder",
75
- "WappaPlugin",
76
-
72
+ "WappaPlugin",
77
73
  # Plugin System
78
74
  "WappaCorePlugin",
79
75
  "AuthPlugin",
80
- "CORSPlugin",
76
+ "CORSPlugin",
81
77
  "DatabasePlugin",
82
78
  "RedisPlugin",
83
79
  "RateLimitPlugin",
84
80
  "CustomMiddlewarePlugin",
85
81
  "WebhookPlugin",
86
-
87
82
  # Core Application
88
83
  "Wappa",
89
84
  ]
@@ -45,7 +45,7 @@ def _get_version_from_pyproject() -> str:
45
45
  def _is_cli_context() -> bool:
46
46
  """
47
47
  Detect if we're running in CLI context (help, init, examples) vs server context (dev, prod).
48
-
48
+
49
49
  Returns:
50
50
  True if running CLI commands that don't need WhatsApp credentials
51
51
  """
@@ -53,12 +53,12 @@ def _is_cli_context() -> bool:
53
53
  if len(sys.argv) > 1:
54
54
  # Direct CLI commands that don't need credentials
55
55
  cli_only_commands = {"--help", "-h", "init", "examples"}
56
-
56
+
57
57
  # Check for help flag or CLI-only commands
58
58
  for arg in sys.argv[1:]:
59
59
  if arg in cli_only_commands:
60
60
  return True
61
-
61
+
62
62
  # Check if we're running wappa command directly (not through uvicorn)
63
63
  if any("wappa" in arg for arg in sys.argv):
64
64
  # If no server commands (dev/prod) are present, assume CLI context
@@ -66,7 +66,7 @@ def _is_cli_context() -> bool:
66
66
  has_server_command = any(cmd in sys.argv for cmd in server_commands)
67
67
  if not has_server_command:
68
68
  return True
69
-
69
+
70
70
  return False
71
71
 
72
72
 
@@ -129,7 +129,7 @@ class Settings:
129
129
 
130
130
  # Apply validation (skip WhatsApp validation for CLI-only commands)
131
131
  self._validate_settings()
132
-
132
+
133
133
  # Only validate WhatsApp credentials for server operations
134
134
  if not _is_cli_context():
135
135
  self._validate_whatsapp_credentials()
@@ -156,6 +156,8 @@ class Settings:
156
156
  raise ValueError("WP_PHONE_ID is required")
157
157
  if not self.wp_bid:
158
158
  raise ValueError("WP_BID is required")
159
+ if not self.whatsapp_webhook_verify_token:
160
+ raise ValueError("WHATSAPP_WEBHOOK_VERIFY_TOKEN is required")
159
161
 
160
162
  @property
161
163
  def owner_id(self) -> str:
@@ -116,7 +116,7 @@ class DefaultMessageHandler:
116
116
  self._update_stats(webhook)
117
117
 
118
118
  # Get logger with tenant context
119
- tenant_id = webhook.tenant.get_tenant_key() if webhook.tenant else "unknown"
119
+ webhook.tenant.get_tenant_key() if webhook.tenant else "unknown"
120
120
  logger = get_logger(__name__)
121
121
 
122
122
  # Log based on strategy