wappa 0.1.9__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 (126) 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/app/main.py +2 -1
  9. wappa/cli/examples/init/app/master_event.py +5 -3
  10. wappa/cli/examples/json_cache_example/app/__init__.py +1 -1
  11. wappa/cli/examples/json_cache_example/app/main.py +56 -44
  12. wappa/cli/examples/json_cache_example/app/master_event.py +181 -145
  13. wappa/cli/examples/json_cache_example/app/models/__init__.py +1 -1
  14. wappa/cli/examples/json_cache_example/app/models/json_demo_models.py +32 -51
  15. wappa/cli/examples/json_cache_example/app/scores/__init__.py +2 -2
  16. wappa/cli/examples/json_cache_example/app/scores/score_base.py +52 -46
  17. wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +70 -62
  18. wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +41 -44
  19. wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +83 -71
  20. wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +73 -57
  21. wappa/cli/examples/json_cache_example/app/utils/__init__.py +2 -2
  22. wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +54 -56
  23. wappa/cli/examples/json_cache_example/app/utils/message_utils.py +85 -80
  24. wappa/cli/examples/openai_transcript/app/main.py +2 -1
  25. wappa/cli/examples/openai_transcript/app/master_event.py +31 -22
  26. wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +1 -1
  27. wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +37 -24
  28. wappa/cli/examples/redis_cache_example/app/__init__.py +1 -1
  29. wappa/cli/examples/redis_cache_example/app/main.py +56 -44
  30. wappa/cli/examples/redis_cache_example/app/master_event.py +181 -145
  31. wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +31 -50
  32. wappa/cli/examples/redis_cache_example/app/scores/__init__.py +2 -2
  33. wappa/cli/examples/redis_cache_example/app/scores/score_base.py +52 -46
  34. wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +70 -62
  35. wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +41 -44
  36. wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +83 -71
  37. wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +73 -57
  38. wappa/cli/examples/redis_cache_example/app/utils/__init__.py +2 -2
  39. wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +54 -56
  40. wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +85 -80
  41. wappa/cli/examples/simple_echo_example/app/__init__.py +1 -1
  42. wappa/cli/examples/simple_echo_example/app/main.py +41 -33
  43. wappa/cli/examples/simple_echo_example/app/master_event.py +78 -57
  44. wappa/cli/examples/wappa_full_example/app/__init__.py +1 -1
  45. wappa/cli/examples/wappa_full_example/app/handlers/__init__.py +1 -1
  46. wappa/cli/examples/wappa_full_example/app/handlers/command_handlers.py +134 -126
  47. wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +237 -229
  48. wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +170 -148
  49. wappa/cli/examples/wappa_full_example/app/main.py +51 -39
  50. wappa/cli/examples/wappa_full_example/app/master_event.py +179 -120
  51. wappa/cli/examples/wappa_full_example/app/models/__init__.py +1 -1
  52. wappa/cli/examples/wappa_full_example/app/models/state_models.py +113 -104
  53. wappa/cli/examples/wappa_full_example/app/models/user_models.py +92 -76
  54. wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +109 -83
  55. wappa/cli/examples/wappa_full_example/app/utils/__init__.py +1 -1
  56. wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +132 -113
  57. wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +175 -132
  58. wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +126 -87
  59. wappa/cli/main.py +9 -4
  60. wappa/core/__init__.py +18 -23
  61. wappa/core/config/settings.py +7 -5
  62. wappa/core/events/default_handlers.py +1 -1
  63. wappa/core/factory/wappa_builder.py +38 -25
  64. wappa/core/plugins/redis_plugin.py +1 -3
  65. wappa/core/plugins/wappa_core_plugin.py +7 -6
  66. wappa/core/types.py +12 -12
  67. wappa/core/wappa_app.py +10 -8
  68. wappa/database/__init__.py +3 -4
  69. wappa/domain/enums/messenger_platform.py +1 -2
  70. wappa/domain/factories/media_factory.py +5 -20
  71. wappa/domain/factories/message_factory.py +5 -20
  72. wappa/domain/factories/messenger_factory.py +2 -4
  73. wappa/domain/interfaces/cache_interface.py +7 -7
  74. wappa/domain/interfaces/media_interface.py +2 -5
  75. wappa/domain/models/media_result.py +1 -3
  76. wappa/domain/models/platforms/platform_config.py +1 -3
  77. wappa/messaging/__init__.py +9 -12
  78. wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +20 -22
  79. wappa/models/__init__.py +27 -35
  80. wappa/persistence/__init__.py +12 -15
  81. wappa/persistence/cache_factory.py +0 -1
  82. wappa/persistence/json/__init__.py +1 -1
  83. wappa/persistence/json/cache_adapters.py +37 -25
  84. wappa/persistence/json/handlers/state_handler.py +60 -52
  85. wappa/persistence/json/handlers/table_handler.py +51 -49
  86. wappa/persistence/json/handlers/user_handler.py +71 -55
  87. wappa/persistence/json/handlers/utils/file_manager.py +42 -39
  88. wappa/persistence/json/handlers/utils/key_factory.py +1 -1
  89. wappa/persistence/json/handlers/utils/serialization.py +13 -11
  90. wappa/persistence/json/json_cache_factory.py +4 -8
  91. wappa/persistence/json/storage_manager.py +66 -79
  92. wappa/persistence/memory/__init__.py +1 -1
  93. wappa/persistence/memory/cache_adapters.py +37 -25
  94. wappa/persistence/memory/handlers/state_handler.py +62 -52
  95. wappa/persistence/memory/handlers/table_handler.py +59 -53
  96. wappa/persistence/memory/handlers/user_handler.py +75 -55
  97. wappa/persistence/memory/handlers/utils/key_factory.py +1 -1
  98. wappa/persistence/memory/handlers/utils/memory_store.py +75 -71
  99. wappa/persistence/memory/handlers/utils/ttl_manager.py +59 -67
  100. wappa/persistence/memory/memory_cache_factory.py +3 -7
  101. wappa/persistence/memory/storage_manager.py +52 -62
  102. wappa/persistence/redis/cache_adapters.py +27 -21
  103. wappa/persistence/redis/ops.py +11 -11
  104. wappa/persistence/redis/redis_client.py +4 -6
  105. wappa/persistence/redis/redis_manager.py +12 -4
  106. wappa/processors/factory.py +5 -5
  107. wappa/schemas/factory.py +2 -5
  108. wappa/schemas/whatsapp/message_types/errors.py +3 -12
  109. wappa/schemas/whatsapp/validators.py +3 -3
  110. wappa/webhooks/__init__.py +17 -18
  111. wappa/webhooks/factory.py +3 -5
  112. wappa/webhooks/whatsapp/__init__.py +10 -13
  113. wappa/webhooks/whatsapp/message_types/audio.py +0 -4
  114. wappa/webhooks/whatsapp/message_types/document.py +1 -9
  115. wappa/webhooks/whatsapp/message_types/errors.py +3 -12
  116. wappa/webhooks/whatsapp/message_types/location.py +1 -21
  117. wappa/webhooks/whatsapp/message_types/sticker.py +1 -5
  118. wappa/webhooks/whatsapp/message_types/text.py +0 -6
  119. wappa/webhooks/whatsapp/message_types/video.py +1 -20
  120. wappa/webhooks/whatsapp/status_models.py +2 -2
  121. wappa/webhooks/whatsapp/validators.py +3 -3
  122. {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/METADATA +362 -8
  123. {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/RECORD +126 -126
  124. {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/WHEEL +0 -0
  125. {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/entry_points.txt +0 -0
  126. {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/licenses/LICENSE +0 -0
@@ -7,13 +7,14 @@ objects to provide comprehensive information about each message type.
7
7
 
8
8
  from datetime import datetime
9
9
  from enum import Enum
10
- from typing import Any, Dict, List, Optional
10
+ from typing import Any
11
11
 
12
12
  from pydantic import BaseModel, Field
13
13
 
14
14
 
15
15
  class MessageType(str, Enum):
16
16
  """Supported message types for metadata extraction."""
17
+
17
18
  TEXT = "text"
18
19
  IMAGE = "image"
19
20
  VIDEO = "video"
@@ -30,33 +31,37 @@ class MessageType(str, Enum):
30
31
 
31
32
  class BaseMessageMetadata(BaseModel):
32
33
  """Base metadata common to all message types."""
34
+
33
35
  message_id: str
34
36
  message_type: MessageType
35
37
  timestamp: datetime
36
38
  user_id: str
37
- user_name: Optional[str] = None
39
+ user_name: str | None = None
38
40
  tenant_id: str
39
41
  platform: str
40
-
42
+
41
43
  # Processing metadata
42
- processing_time_ms: Optional[int] = None
44
+ processing_time_ms: int | None = None
43
45
  cache_hit: bool = False
44
-
46
+
45
47
  class Config:
46
48
  use_enum_values = True
47
49
 
48
50
 
49
51
  class TextMessageMetadata(BaseMessageMetadata):
50
52
  """Metadata specific to text messages."""
53
+
51
54
  message_type: MessageType = MessageType.TEXT
52
55
  text_content: str
53
56
  text_length: int
54
57
  has_urls: bool = False
55
58
  has_mentions: bool = False
56
59
  is_forwarded: bool = False
57
-
60
+
58
61
  @classmethod
59
- def from_webhook(cls, webhook, processing_time_ms: int = None) -> "TextMessageMetadata":
62
+ def from_webhook(
63
+ cls, webhook, processing_time_ms: int = None
64
+ ) -> "TextMessageMetadata":
60
65
  """Create TextMessageMetadata from IncomingMessageWebhook."""
61
66
  text_content = webhook.get_message_text() or ""
62
67
  return cls(
@@ -71,43 +76,48 @@ class TextMessageMetadata(BaseMessageMetadata):
71
76
  has_urls="http" in text_content.lower(),
72
77
  has_mentions="@" in text_content,
73
78
  is_forwarded=webhook.was_forwarded(),
74
- processing_time_ms=processing_time_ms
79
+ processing_time_ms=processing_time_ms,
75
80
  )
76
81
 
77
82
 
78
83
  class MediaMessageMetadata(BaseMessageMetadata):
79
84
  """Metadata specific to media messages (image, video, audio, document)."""
85
+
80
86
  media_id: str
81
87
  media_type: str
82
- file_size: Optional[int] = None
83
- mime_type: Optional[str] = None
84
- caption: Optional[str] = None
88
+ file_size: int | None = None
89
+ mime_type: str | None = None
90
+ caption: str | None = None
85
91
  caption_length: int = 0
86
92
  is_forwarded: bool = False
87
-
93
+
88
94
  # Media-specific fields
89
- width: Optional[int] = None
90
- height: Optional[int] = None
91
- duration: Optional[int] = None # For video/audio
92
- filename: Optional[str] = None # For documents
93
-
95
+ width: int | None = None
96
+ height: int | None = None
97
+ duration: int | None = None # For video/audio
98
+ filename: str | None = None # For documents
99
+
94
100
  @classmethod
95
- def from_webhook(cls, webhook, message_type: MessageType, processing_time_ms: int = None) -> "MediaMessageMetadata":
101
+ def from_webhook(
102
+ cls, webhook, message_type: MessageType, processing_time_ms: int = None
103
+ ) -> "MediaMessageMetadata":
96
104
  """Create MediaMessageMetadata from IncomingMessageWebhook."""
97
105
  # Extract media information from webhook
98
- media_id = getattr(webhook.message, 'media_id', '') or getattr(webhook.message, 'id', '')
99
- caption = getattr(webhook.message, 'caption', '') or ''
100
-
106
+ media_id = getattr(webhook.message, "media_id", "") or getattr(
107
+ webhook.message, "id", ""
108
+ )
109
+ caption = getattr(webhook.message, "caption", "") or ""
110
+
101
111
  # Try to get additional media properties
102
- mime_type = getattr(webhook.message, 'mime_type', None)
103
- file_size = getattr(webhook.message, 'file_size', None)
104
- filename = getattr(webhook.message, 'filename', None)
105
-
112
+ mime_type = getattr(webhook.message, "mime_type", None)
113
+ file_size = getattr(webhook.message, "file_size", None)
114
+ filename = getattr(webhook.message, "filename", None)
115
+
106
116
  # For images/videos
107
- width = getattr(webhook.message, 'width', None)
108
- height = getattr(webhook.message, 'height', None)
109
- duration = getattr(webhook.message, 'duration', None)
110
-
117
+ width = getattr(webhook.message, "width", None)
118
+ height = getattr(webhook.message, "height", None)
119
+ duration = getattr(webhook.message, "duration", None)
120
+
111
121
  return cls(
112
122
  message_id=webhook.message.message_id,
113
123
  message_type=message_type,
@@ -127,28 +137,31 @@ class MediaMessageMetadata(BaseMessageMetadata):
127
137
  height=height,
128
138
  duration=duration,
129
139
  is_forwarded=webhook.was_forwarded(),
130
- processing_time_ms=processing_time_ms
140
+ processing_time_ms=processing_time_ms,
131
141
  )
132
142
 
133
143
 
134
144
  class LocationMessageMetadata(BaseMessageMetadata):
135
145
  """Metadata specific to location messages."""
146
+
136
147
  message_type: MessageType = MessageType.LOCATION
137
148
  latitude: float
138
149
  longitude: float
139
- location_name: Optional[str] = None
140
- location_address: Optional[str] = None
150
+ location_name: str | None = None
151
+ location_address: str | None = None
141
152
  is_forwarded: bool = False
142
-
153
+
143
154
  @classmethod
144
- def from_webhook(cls, webhook, processing_time_ms: int = None) -> "LocationMessageMetadata":
155
+ def from_webhook(
156
+ cls, webhook, processing_time_ms: int = None
157
+ ) -> "LocationMessageMetadata":
145
158
  """Create LocationMessageMetadata from IncomingMessageWebhook."""
146
159
  # Extract location data from webhook
147
- latitude = getattr(webhook.message, 'latitude', 0.0)
148
- longitude = getattr(webhook.message, 'longitude', 0.0)
149
- location_name = getattr(webhook.message, 'name', None)
150
- location_address = getattr(webhook.message, 'address', None)
151
-
160
+ latitude = getattr(webhook.message, "latitude", 0.0)
161
+ longitude = getattr(webhook.message, "longitude", 0.0)
162
+ location_name = getattr(webhook.message, "name", None)
163
+ location_address = getattr(webhook.message, "address", None)
164
+
152
165
  return cls(
153
166
  message_id=webhook.message.message_id,
154
167
  timestamp=webhook.timestamp,
@@ -161,47 +174,50 @@ class LocationMessageMetadata(BaseMessageMetadata):
161
174
  location_name=location_name,
162
175
  location_address=location_address,
163
176
  is_forwarded=webhook.was_forwarded(),
164
- processing_time_ms=processing_time_ms
177
+ processing_time_ms=processing_time_ms,
165
178
  )
166
179
 
167
180
 
168
181
  class ContactMessageMetadata(BaseMessageMetadata):
169
182
  """Metadata specific to contact messages."""
183
+
170
184
  message_type: MessageType = MessageType.CONTACT
171
185
  contacts_count: int
172
- contact_names: List[str] = Field(default_factory=list)
186
+ contact_names: list[str] = Field(default_factory=list)
173
187
  has_phone_numbers: bool = False
174
188
  has_emails: bool = False
175
189
  is_forwarded: bool = False
176
-
190
+
177
191
  @classmethod
178
- def from_webhook(cls, webhook, processing_time_ms: int = None) -> "ContactMessageMetadata":
192
+ def from_webhook(
193
+ cls, webhook, processing_time_ms: int = None
194
+ ) -> "ContactMessageMetadata":
179
195
  """Create ContactMessageMetadata from IncomingMessageWebhook."""
180
196
  # Extract contact data from webhook
181
- contacts = getattr(webhook.message, 'contacts', [])
197
+ contacts = getattr(webhook.message, "contacts", [])
182
198
  if not isinstance(contacts, list):
183
199
  contacts = [contacts] if contacts else []
184
-
200
+
185
201
  contact_names = []
186
202
  has_phone_numbers = False
187
203
  has_emails = False
188
-
204
+
189
205
  for contact in contacts:
190
206
  # Extract contact name
191
- if hasattr(contact, 'name') and contact.name:
192
- if hasattr(contact.name, 'formatted_name'):
207
+ if hasattr(contact, "name") and contact.name:
208
+ if hasattr(contact.name, "formatted_name"):
193
209
  contact_names.append(contact.name.formatted_name)
194
210
  else:
195
211
  contact_names.append(str(contact.name))
196
-
212
+
197
213
  # Check for phone numbers
198
- if hasattr(contact, 'phones') and contact.phones:
214
+ if hasattr(contact, "phones") and contact.phones:
199
215
  has_phone_numbers = True
200
-
216
+
201
217
  # Check for emails
202
- if hasattr(contact, 'emails') and contact.emails:
218
+ if hasattr(contact, "emails") and contact.emails:
203
219
  has_emails = True
204
-
220
+
205
221
  return cls(
206
222
  message_id=webhook.message.message_id,
207
223
  timestamp=webhook.timestamp,
@@ -214,43 +230,50 @@ class ContactMessageMetadata(BaseMessageMetadata):
214
230
  has_phone_numbers=has_phone_numbers,
215
231
  has_emails=has_emails,
216
232
  is_forwarded=webhook.was_forwarded(),
217
- processing_time_ms=processing_time_ms
233
+ processing_time_ms=processing_time_ms,
218
234
  )
219
235
 
220
236
 
221
237
  class InteractiveMessageMetadata(BaseMessageMetadata):
222
238
  """Metadata specific to interactive messages (button/list selections)."""
239
+
223
240
  message_type: MessageType = MessageType.INTERACTIVE
224
241
  interaction_type: str # button_reply, list_reply
225
242
  selection_id: str
226
- selection_title: Optional[str] = None
227
- context_message_id: Optional[str] = None # Original message that triggered this
228
-
243
+ selection_title: str | None = None
244
+ context_message_id: str | None = None # Original message that triggered this
245
+
229
246
  @classmethod
230
- def from_webhook(cls, webhook, processing_time_ms: int = None) -> "InteractiveMessageMetadata":
247
+ def from_webhook(
248
+ cls, webhook, processing_time_ms: int = None
249
+ ) -> "InteractiveMessageMetadata":
231
250
  """Create InteractiveMessageMetadata from IncomingMessageWebhook."""
232
251
  # Extract interactive data
233
252
  selection_id = webhook.get_interactive_selection() or ""
234
253
  interaction_type = "unknown"
235
254
  selection_title = None
236
-
255
+
237
256
  # Try to determine interaction type and get more details
238
- if hasattr(webhook.message, 'interactive') and webhook.message.interactive:
257
+ if hasattr(webhook.message, "interactive") and webhook.message.interactive:
239
258
  interactive_data = webhook.message.interactive
240
-
241
- if hasattr(interactive_data, 'type'):
259
+
260
+ if hasattr(interactive_data, "type"):
242
261
  interaction_type = interactive_data.type
243
-
262
+
244
263
  # Get button reply details
245
- if interaction_type == "button_reply" and hasattr(interactive_data, 'button_reply'):
264
+ if interaction_type == "button_reply" and hasattr(
265
+ interactive_data, "button_reply"
266
+ ):
246
267
  button_reply = interactive_data.button_reply
247
- selection_title = getattr(button_reply, 'title', None)
248
-
268
+ selection_title = getattr(button_reply, "title", None)
269
+
249
270
  # Get list reply details
250
- elif interaction_type == "list_reply" and hasattr(interactive_data, 'list_reply'):
271
+ elif interaction_type == "list_reply" and hasattr(
272
+ interactive_data, "list_reply"
273
+ ):
251
274
  list_reply = interactive_data.list_reply
252
- selection_title = getattr(list_reply, 'title', None)
253
-
275
+ selection_title = getattr(list_reply, "title", None)
276
+
254
277
  return cls(
255
278
  message_id=webhook.message.message_id,
256
279
  timestamp=webhook.timestamp,
@@ -261,23 +284,26 @@ class InteractiveMessageMetadata(BaseMessageMetadata):
261
284
  interaction_type=interaction_type,
262
285
  selection_id=selection_id,
263
286
  selection_title=selection_title,
264
- processing_time_ms=processing_time_ms
287
+ processing_time_ms=processing_time_ms,
265
288
  )
266
289
 
267
290
 
268
291
  class UnknownMessageMetadata(BaseMessageMetadata):
269
292
  """Metadata for unsupported or unknown message types."""
293
+
270
294
  message_type: MessageType = MessageType.UNKNOWN
271
- raw_message_data: Dict[str, Any] = Field(default_factory=dict)
272
-
295
+ raw_message_data: dict[str, Any] = Field(default_factory=dict)
296
+
273
297
  @classmethod
274
- def from_webhook(cls, webhook, processing_time_ms: int = None) -> "UnknownMessageMetadata":
298
+ def from_webhook(
299
+ cls, webhook, processing_time_ms: int = None
300
+ ) -> "UnknownMessageMetadata":
275
301
  """Create UnknownMessageMetadata from IncomingMessageWebhook."""
276
302
  # Capture raw message data for debugging
277
303
  raw_data = {}
278
- if hasattr(webhook.message, '__dict__'):
304
+ if hasattr(webhook.message, "__dict__"):
279
305
  raw_data = {k: str(v)[:200] for k, v in webhook.message.__dict__.items()}
280
-
306
+
281
307
  return cls(
282
308
  message_id=webhook.message.message_id,
283
309
  timestamp=webhook.timestamp,
@@ -286,16 +312,16 @@ class UnknownMessageMetadata(BaseMessageMetadata):
286
312
  tenant_id=webhook.tenant.get_tenant_key(),
287
313
  platform=webhook.platform.value,
288
314
  raw_message_data=raw_data,
289
- processing_time_ms=processing_time_ms
315
+ processing_time_ms=processing_time_ms,
290
316
  )
291
317
 
292
318
 
293
319
  # Union type for all metadata models
294
320
  WebhookMetadata = (
295
- TextMessageMetadata |
296
- MediaMessageMetadata |
297
- LocationMessageMetadata |
298
- ContactMessageMetadata |
299
- InteractiveMessageMetadata |
300
- UnknownMessageMetadata
301
- )
321
+ TextMessageMetadata
322
+ | MediaMessageMetadata
323
+ | LocationMessageMetadata
324
+ | ContactMessageMetadata
325
+ | InteractiveMessageMetadata
326
+ | UnknownMessageMetadata
327
+ )
@@ -2,4 +2,4 @@
2
2
  Utility modules for the Wappa Full Example application.
3
3
 
4
4
  Contains utilities for metadata extraction, media handling, and cache operations.
5
- """
5
+ """