wappa 0.1.5__py3-none-any.whl → 0.1.7__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.
- wappa/__init__.py +13 -69
- wappa/cli/examples/init/.gitignore +69 -0
- wappa/cli/examples/init/pyproject.toml +7 -0
- wappa/cli/examples/json_cache_example/.gitignore +69 -0
- wappa/cli/examples/json_cache_example/README.md +190 -0
- wappa/cli/examples/openai_transcript/.gitignore +10 -0
- wappa/cli/examples/openai_transcript/README.md +0 -0
- wappa/cli/examples/redis_cache_example/.gitignore +69 -0
- wappa/cli/examples/redis_cache_example/README.md +0 -0
- wappa/cli/examples/simple_echo_example/.gitignore +69 -0
- wappa/cli/examples/simple_echo_example/.python-version +1 -0
- wappa/cli/examples/simple_echo_example/README.md +0 -0
- wappa/cli/examples/simple_echo_example/pyproject.toml +9 -0
- wappa/cli/examples/wappa_full_example/.dockerignore +171 -0
- wappa/cli/examples/wappa_full_example/.gitignore +10 -0
- wappa/cli/examples/wappa_full_example/Dockerfile +85 -0
- wappa/cli/examples/wappa_full_example/RAILWAY_DEPLOYMENT.md +366 -0
- wappa/cli/examples/wappa_full_example/README.md +322 -0
- wappa/cli/examples/wappa_full_example/docker-compose.yml +170 -0
- wappa/cli/examples/wappa_full_example/nginx.conf +177 -0
- wappa/cli/examples/wappa_full_example/railway.toml +30 -0
- wappa/cli/main.py +346 -22
- wappa/cli/templates/__init__.py.template +0 -0
- wappa/cli/templates/env.template +37 -0
- wappa/cli/templates/gitignore.template +165 -0
- wappa/cli/templates/main.py.template +8 -0
- wappa/cli/templates/master_event.py.template +8 -0
- wappa/core/__init__.py +86 -3
- wappa/core/plugins/wappa_core_plugin.py +15 -5
- wappa/database/__init__.py +16 -4
- wappa/domain/interfaces/media_interface.py +57 -3
- wappa/domain/models/media_result.py +43 -0
- wappa/messaging/__init__.py +53 -3
- wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +112 -4
- wappa/models/__init__.py +103 -0
- wappa/persistence/__init__.py +55 -0
- wappa/webhooks/__init__.py +53 -4
- wappa/webhooks/whatsapp/__init__.py +57 -3
- wappa/webhooks/whatsapp/status_models.py +10 -0
- {wappa-0.1.5.dist-info → wappa-0.1.7.dist-info}/METADATA +7 -25
- {wappa-0.1.5.dist-info → wappa-0.1.7.dist-info}/RECORD +44 -23
- wappa/domain/interfaces/webhooks/__init__.py +0 -1
- wappa/persistence/json/handlers/__init__.py +0 -1
- wappa/persistence/json/handlers/utils/__init__.py +0 -1
- wappa/persistence/memory/handlers/__init__.py +0 -1
- wappa/persistence/memory/handlers/utils/__init__.py +0 -1
- wappa/schemas/webhooks/__init__.py +0 -3
- {wappa-0.1.5.dist-info → wappa-0.1.7.dist-info}/WHEEL +0 -0
- {wappa-0.1.5.dist-info → wappa-0.1.7.dist-info}/entry_points.txt +0 -0
- {wappa-0.1.5.dist-info → wappa-0.1.7.dist-info}/licenses/LICENSE +0 -0
wappa/core/__init__.py
CHANGED
|
@@ -1,6 +1,89 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Wappa Core Framework Components
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
Provides access to core framework functionality including configuration,
|
|
5
|
+
logging, events, plugins, and factory system.
|
|
6
|
+
|
|
7
|
+
Clean Architecture: Core application logic and framework components.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Configuration & Settings
|
|
11
|
+
from .config.settings import settings
|
|
12
|
+
|
|
13
|
+
# Logging System
|
|
14
|
+
from .logging import get_logger, get_app_logger, setup_app_logging
|
|
15
|
+
|
|
16
|
+
# Event System
|
|
17
|
+
from .events import (
|
|
18
|
+
WappaEventHandler,
|
|
19
|
+
WappaEventDispatcher,
|
|
20
|
+
DefaultMessageHandler,
|
|
21
|
+
DefaultStatusHandler,
|
|
22
|
+
DefaultErrorHandler,
|
|
23
|
+
DefaultHandlerFactory,
|
|
24
|
+
MessageLogStrategy,
|
|
25
|
+
StatusLogStrategy,
|
|
26
|
+
ErrorLogStrategy,
|
|
27
|
+
WebhookURLFactory,
|
|
28
|
+
WebhookEndpointType,
|
|
29
|
+
webhook_url_factory,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Factory System
|
|
33
|
+
from .factory import WappaBuilder, WappaPlugin
|
|
34
|
+
|
|
35
|
+
# Plugin System
|
|
36
|
+
from .plugins import (
|
|
37
|
+
WappaCorePlugin,
|
|
38
|
+
AuthPlugin,
|
|
39
|
+
CORSPlugin,
|
|
40
|
+
DatabasePlugin,
|
|
41
|
+
RedisPlugin,
|
|
42
|
+
RateLimitPlugin,
|
|
43
|
+
CustomMiddlewarePlugin,
|
|
44
|
+
WebhookPlugin,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Core Application
|
|
4
48
|
from .wappa_app import Wappa
|
|
5
49
|
|
|
6
|
-
__all__ = [
|
|
50
|
+
__all__ = [
|
|
51
|
+
# Configuration
|
|
52
|
+
"settings",
|
|
53
|
+
|
|
54
|
+
# Logging
|
|
55
|
+
"get_logger",
|
|
56
|
+
"get_app_logger",
|
|
57
|
+
"setup_app_logging",
|
|
58
|
+
|
|
59
|
+
# Event System
|
|
60
|
+
"WappaEventHandler",
|
|
61
|
+
"WappaEventDispatcher",
|
|
62
|
+
"DefaultMessageHandler",
|
|
63
|
+
"DefaultStatusHandler",
|
|
64
|
+
"DefaultErrorHandler",
|
|
65
|
+
"DefaultHandlerFactory",
|
|
66
|
+
"MessageLogStrategy",
|
|
67
|
+
"StatusLogStrategy",
|
|
68
|
+
"ErrorLogStrategy",
|
|
69
|
+
"WebhookURLFactory",
|
|
70
|
+
"WebhookEndpointType",
|
|
71
|
+
"webhook_url_factory",
|
|
72
|
+
|
|
73
|
+
# Factory System
|
|
74
|
+
"WappaBuilder",
|
|
75
|
+
"WappaPlugin",
|
|
76
|
+
|
|
77
|
+
# Plugin System
|
|
78
|
+
"WappaCorePlugin",
|
|
79
|
+
"AuthPlugin",
|
|
80
|
+
"CORSPlugin",
|
|
81
|
+
"DatabasePlugin",
|
|
82
|
+
"RedisPlugin",
|
|
83
|
+
"RateLimitPlugin",
|
|
84
|
+
"CustomMiddlewarePlugin",
|
|
85
|
+
"WebhookPlugin",
|
|
86
|
+
|
|
87
|
+
# Core Application
|
|
88
|
+
"Wappa",
|
|
89
|
+
]
|
|
@@ -149,9 +149,19 @@ class WappaCorePlugin:
|
|
|
149
149
|
app.state.wappa_cache_type = self.cache_type.value
|
|
150
150
|
logger.debug(f"💾 Set app.state.wappa_cache_type = {self.cache_type.value}")
|
|
151
151
|
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
# Create persistent HTTP session with optimized connection pooling
|
|
153
|
+
logger.info("🌐 Creating persistent HTTP session...")
|
|
154
|
+
connector = aiohttp.TCPConnector(
|
|
155
|
+
limit=100, # Max connections
|
|
156
|
+
keepalive_timeout=30, # Keep alive timeout
|
|
157
|
+
enable_cleanup_closed=True # Auto cleanup closed connections
|
|
158
|
+
)
|
|
159
|
+
session = aiohttp.ClientSession(
|
|
160
|
+
connector=connector,
|
|
161
|
+
timeout=aiohttp.ClientTimeout(total=30)
|
|
162
|
+
)
|
|
163
|
+
app.state.http_session = session
|
|
164
|
+
logger.info("✅ Persistent HTTP session created - connections: 100, keepalive: 30s")
|
|
155
165
|
|
|
156
166
|
# Log available endpoints
|
|
157
167
|
base_url = (
|
|
@@ -198,10 +208,10 @@ class WappaCorePlugin:
|
|
|
198
208
|
logger.info("🛑 Starting Wappa core shutdown...")
|
|
199
209
|
|
|
200
210
|
try:
|
|
201
|
-
# Close HTTP session if it exists
|
|
211
|
+
# Close HTTP session and connector if it exists
|
|
202
212
|
if hasattr(app.state, "http_session"):
|
|
203
213
|
await app.state.http_session.close()
|
|
204
|
-
logger.
|
|
214
|
+
logger.info("🌐 Persistent HTTP session closed cleanly")
|
|
205
215
|
|
|
206
216
|
# Clear cache type from app state
|
|
207
217
|
if hasattr(app.state, "wappa_cache_type"):
|
wappa/database/__init__.py
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Wappa Database
|
|
2
|
+
Wappa Database Components
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Provides database abstraction and adapters for SQLModel/SQLAlchemy async
|
|
5
|
+
connections. Supports multiple database engines with a unified interface.
|
|
6
|
+
|
|
7
|
+
Clean Architecture: Infrastructure layer database adapters.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
# Base adapter
|
|
11
|
+
from wappa.database import DatabaseAdapter
|
|
12
|
+
|
|
13
|
+
# Specific database adapters
|
|
14
|
+
from wappa.database import PostgreSQLAdapter, MySQLAdapter, SQLiteAdapter
|
|
6
15
|
"""
|
|
7
16
|
|
|
8
17
|
from .adapter import DatabaseAdapter
|
|
@@ -11,8 +20,11 @@ from .adapters.postgresql_adapter import PostgreSQLAdapter
|
|
|
11
20
|
from .adapters.sqlite_adapter import SQLiteAdapter
|
|
12
21
|
|
|
13
22
|
__all__ = [
|
|
23
|
+
# Base Adapter
|
|
14
24
|
"DatabaseAdapter",
|
|
25
|
+
|
|
26
|
+
# Database Adapters (Clean Architecture: Infrastructure implementations)
|
|
15
27
|
"PostgreSQLAdapter",
|
|
28
|
+
"MySQLAdapter",
|
|
16
29
|
"SQLiteAdapter",
|
|
17
|
-
"MySQLAdapter",
|
|
18
30
|
]
|
|
@@ -14,8 +14,9 @@ and WhatsApp Cloud API 2025 specifications for the 4 core endpoints:
|
|
|
14
14
|
|
|
15
15
|
from abc import ABC, abstractmethod
|
|
16
16
|
from collections.abc import AsyncIterator
|
|
17
|
+
from contextlib import asynccontextmanager
|
|
17
18
|
from pathlib import Path
|
|
18
|
-
from typing import BinaryIO
|
|
19
|
+
from typing import BinaryIO, AsyncContextManager
|
|
19
20
|
|
|
20
21
|
from wappa.domain.models.media_result import (
|
|
21
22
|
MediaDeleteResult,
|
|
@@ -197,6 +198,9 @@ class IMediaHandler(ABC):
|
|
|
197
198
|
media_id: str,
|
|
198
199
|
destination_path: str | Path | None = None,
|
|
199
200
|
sender_id: str | None = None,
|
|
201
|
+
use_tempfile: bool = False,
|
|
202
|
+
temp_suffix: str | None = None,
|
|
203
|
+
auto_cleanup: bool = True,
|
|
200
204
|
) -> MediaDownloadResult:
|
|
201
205
|
"""
|
|
202
206
|
Download media by ID.
|
|
@@ -206,19 +210,69 @@ class IMediaHandler(ABC):
|
|
|
206
210
|
|
|
207
211
|
Args:
|
|
208
212
|
media_id: Platform-specific media identifier
|
|
209
|
-
destination_path: Optional path to save file
|
|
213
|
+
destination_path: Optional path to save file (ignored if use_tempfile=True)
|
|
210
214
|
sender_id: Optional sender ID for filename generation
|
|
215
|
+
use_tempfile: If True, creates a temporary file with automatic cleanup
|
|
216
|
+
temp_suffix: Custom suffix for temporary file (e.g., '.mp3', '.jpg')
|
|
217
|
+
auto_cleanup: If True, temp files are cleaned up automatically
|
|
211
218
|
|
|
212
219
|
Returns:
|
|
213
220
|
MediaDownloadResult with file data and metadata
|
|
214
221
|
|
|
215
222
|
Note:
|
|
216
223
|
If destination_path provided, saves file to disk.
|
|
217
|
-
If
|
|
224
|
+
If use_tempfile=True, creates temporary file that can be auto-cleaned.
|
|
225
|
+
If neither provided, returns file data in memory.
|
|
218
226
|
Handles URL expiration by re-fetching URL if needed.
|
|
219
227
|
"""
|
|
220
228
|
pass
|
|
221
229
|
|
|
230
|
+
@abstractmethod
|
|
231
|
+
async def download_media_tempfile(
|
|
232
|
+
self,
|
|
233
|
+
media_id: str,
|
|
234
|
+
temp_suffix: str | None = None,
|
|
235
|
+
sender_id: str | None = None,
|
|
236
|
+
) -> AsyncContextManager[MediaDownloadResult]:
|
|
237
|
+
"""
|
|
238
|
+
Download media to a temporary file with automatic cleanup.
|
|
239
|
+
|
|
240
|
+
Convenience method that provides a context manager for temporary file handling.
|
|
241
|
+
The temporary file is automatically deleted when the context exits.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
media_id: Platform-specific media identifier
|
|
245
|
+
temp_suffix: Custom suffix for temporary file (e.g., '.mp3', '.jpg')
|
|
246
|
+
sender_id: Optional sender ID for logging/debugging
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Async context manager yielding MediaDownloadResult with temp file path
|
|
250
|
+
|
|
251
|
+
Example:
|
|
252
|
+
async with handler.download_media_tempfile(media_id, '.mp3') as result:
|
|
253
|
+
if result.success:
|
|
254
|
+
# Use result.file_path - file auto-deleted on exit
|
|
255
|
+
process_audio(result.file_path)
|
|
256
|
+
"""
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
@abstractmethod
|
|
260
|
+
async def get_media_as_bytes(
|
|
261
|
+
self, media_id: str
|
|
262
|
+
) -> MediaDownloadResult:
|
|
263
|
+
"""
|
|
264
|
+
Download media as bytes without creating any files.
|
|
265
|
+
|
|
266
|
+
Memory-only download for processing that doesn't require file system access.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
media_id: Platform-specific media identifier
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
MediaDownloadResult with file_data bytes (file_path will be None)
|
|
273
|
+
"""
|
|
274
|
+
pass
|
|
275
|
+
|
|
222
276
|
@abstractmethod
|
|
223
277
|
async def stream_media(
|
|
224
278
|
self, media_id: str, chunk_size: int = 8192
|
|
@@ -6,7 +6,10 @@ messaging platforms, providing consistent response structures while
|
|
|
6
6
|
maintaining compatibility with platform-specific response formats.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import os
|
|
9
10
|
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import AsyncContextManager, Optional
|
|
10
13
|
|
|
11
14
|
from pydantic import BaseModel, Field
|
|
12
15
|
|
|
@@ -71,6 +74,7 @@ class MediaDownloadResult(BaseModel):
|
|
|
71
74
|
|
|
72
75
|
Standard response model for media download operations.
|
|
73
76
|
Compatible with existing handle_media.py download patterns.
|
|
77
|
+
Supports context manager for automatic temporary file cleanup.
|
|
74
78
|
"""
|
|
75
79
|
|
|
76
80
|
success: bool
|
|
@@ -84,12 +88,51 @@ class MediaDownloadResult(BaseModel):
|
|
|
84
88
|
error_code: str | None = None
|
|
85
89
|
downloaded_at: datetime = Field(default_factory=datetime.utcnow)
|
|
86
90
|
tenant_id: str | None = None
|
|
91
|
+
_is_temp_file: bool = False
|
|
92
|
+
_cleanup_on_exit: bool = False
|
|
87
93
|
|
|
88
94
|
class Config:
|
|
89
95
|
use_enum_values = True
|
|
90
96
|
# Allow bytes in file_data field
|
|
91
97
|
arbitrary_types_allowed = True
|
|
92
98
|
|
|
99
|
+
def __enter__(self):
|
|
100
|
+
"""Synchronous context manager entry."""
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
104
|
+
"""Synchronous context manager exit with cleanup."""
|
|
105
|
+
self._cleanup_temp_file()
|
|
106
|
+
|
|
107
|
+
async def __aenter__(self):
|
|
108
|
+
"""Async context manager entry."""
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
112
|
+
"""Async context manager exit with cleanup."""
|
|
113
|
+
self._cleanup_temp_file()
|
|
114
|
+
|
|
115
|
+
def _cleanup_temp_file(self):
|
|
116
|
+
"""Clean up temporary file if configured for auto-cleanup."""
|
|
117
|
+
if self._cleanup_on_exit and self._is_temp_file and self.file_path:
|
|
118
|
+
try:
|
|
119
|
+
file_path = Path(self.file_path)
|
|
120
|
+
if file_path.exists():
|
|
121
|
+
file_path.unlink()
|
|
122
|
+
except Exception:
|
|
123
|
+
# Silently ignore cleanup errors - temp files will be cleaned by OS eventually
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
def mark_as_temp_file(self, cleanup_on_exit: bool = True):
|
|
127
|
+
"""Mark this result as containing a temporary file for cleanup.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
cleanup_on_exit: Whether to automatically delete the temp file when context exits
|
|
131
|
+
"""
|
|
132
|
+
self._is_temp_file = True
|
|
133
|
+
self._cleanup_on_exit = cleanup_on_exit
|
|
134
|
+
return self
|
|
135
|
+
|
|
93
136
|
|
|
94
137
|
class MediaDeleteResult(BaseModel):
|
|
95
138
|
"""Result of a media delete operation.
|
wappa/messaging/__init__.py
CHANGED
|
@@ -1,7 +1,57 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Wappa Messaging Components
|
|
2
3
|
|
|
4
|
+
Provides access to messaging interfaces and platform-specific implementations
|
|
5
|
+
including WhatsApp client, messenger, and specialized handlers.
|
|
6
|
+
|
|
7
|
+
Clean Architecture: Application services and infrastructure implementations.
|
|
8
|
+
|
|
9
|
+
Usage (User Request: Quick access to WhatsApp messaging components):
|
|
10
|
+
# Core messaging interface
|
|
11
|
+
from wappa.messaging import IMessenger
|
|
12
|
+
|
|
13
|
+
# WhatsApp client and messenger
|
|
14
|
+
from wappa.messaging.whatsapp import WhatsAppClient, WhatsAppMessenger
|
|
15
|
+
|
|
16
|
+
# WhatsApp specialized handlers
|
|
17
|
+
from wappa.messaging.whatsapp import (
|
|
18
|
+
WhatsAppMediaHandler,
|
|
19
|
+
WhatsAppInteractiveHandler,
|
|
20
|
+
WhatsAppTemplateHandler,
|
|
21
|
+
WhatsAppSpecializedHandler
|
|
22
|
+
)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# Core Messaging Interface
|
|
3
26
|
from wappa.domain.interfaces.messaging_interface import IMessenger
|
|
4
27
|
|
|
5
|
-
|
|
28
|
+
# WhatsApp Client & Messenger (User Request: Quick access)
|
|
29
|
+
from .whatsapp.client import WhatsAppClient, WhatsAppUrlBuilder, WhatsAppFormDataBuilder
|
|
30
|
+
from .whatsapp.messenger import WhatsAppMessenger
|
|
31
|
+
|
|
32
|
+
# WhatsApp Specialized Handlers (User Request: Quick access)
|
|
33
|
+
from .whatsapp.handlers import (
|
|
34
|
+
WhatsAppMediaHandler,
|
|
35
|
+
WhatsAppInteractiveHandler,
|
|
36
|
+
WhatsAppTemplateHandler,
|
|
37
|
+
WhatsAppSpecializedHandler,
|
|
38
|
+
)
|
|
6
39
|
|
|
7
|
-
__all__ = [
|
|
40
|
+
__all__ = [
|
|
41
|
+
# Core Interface
|
|
42
|
+
"IMessenger",
|
|
43
|
+
|
|
44
|
+
# WhatsApp Client & Utilities
|
|
45
|
+
"WhatsAppClient",
|
|
46
|
+
"WhatsAppUrlBuilder",
|
|
47
|
+
"WhatsAppFormDataBuilder",
|
|
48
|
+
|
|
49
|
+
# WhatsApp Messenger
|
|
50
|
+
"WhatsAppMessenger",
|
|
51
|
+
|
|
52
|
+
# WhatsApp Handlers (User Request: Clean access to all handlers)
|
|
53
|
+
"WhatsAppMediaHandler",
|
|
54
|
+
"WhatsAppInteractiveHandler",
|
|
55
|
+
"WhatsAppTemplateHandler",
|
|
56
|
+
"WhatsAppSpecializedHandler",
|
|
57
|
+
]
|
|
@@ -6,10 +6,13 @@ to follow SOLID principles with dependency injection and proper separation of co
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import mimetypes
|
|
9
|
+
import os
|
|
10
|
+
import tempfile
|
|
9
11
|
import time
|
|
10
12
|
from collections.abc import AsyncIterator
|
|
13
|
+
from contextlib import asynccontextmanager
|
|
11
14
|
from pathlib import Path
|
|
12
|
-
from typing import Any, BinaryIO
|
|
15
|
+
from typing import Any, BinaryIO, AsyncContextManager
|
|
13
16
|
|
|
14
17
|
from wappa.core.logging.logger import get_logger
|
|
15
18
|
from wappa.domain.interfaces.media_interface import IMediaHandler
|
|
@@ -319,12 +322,23 @@ class WhatsAppMediaHandler(IMediaHandler):
|
|
|
319
322
|
media_id: str,
|
|
320
323
|
destination_path: str | Path | None = None,
|
|
321
324
|
sender_id: str | None = None,
|
|
325
|
+
use_tempfile: bool = False,
|
|
326
|
+
temp_suffix: str | None = None,
|
|
327
|
+
auto_cleanup: bool = True,
|
|
322
328
|
) -> MediaDownloadResult:
|
|
323
329
|
"""
|
|
324
330
|
Download WhatsApp media using its media ID.
|
|
325
331
|
|
|
326
332
|
Based on existing WhatsAppServiceMedia.download_media() method.
|
|
327
333
|
Implements workflow: GET /MEDIA_ID -> GET /MEDIA_URL
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
media_id: Platform-specific media identifier
|
|
337
|
+
destination_path: Optional path to save file (ignored if use_tempfile=True)
|
|
338
|
+
sender_id: Optional sender ID for filename generation
|
|
339
|
+
use_tempfile: If True, creates a temporary file with automatic cleanup
|
|
340
|
+
temp_suffix: Custom suffix for temporary file (e.g., '.mp3', '.jpg')
|
|
341
|
+
auto_cleanup: If True, temp files are cleaned up automatically
|
|
328
342
|
"""
|
|
329
343
|
try:
|
|
330
344
|
# Get media info first
|
|
@@ -395,9 +409,35 @@ class WhatsAppMediaHandler(IMediaHandler):
|
|
|
395
409
|
)
|
|
396
410
|
data.extend(chunk)
|
|
397
411
|
|
|
398
|
-
# Save to file if destination_path provided
|
|
412
|
+
# Save to file if destination_path provided or tempfile requested
|
|
399
413
|
final_path = None
|
|
400
|
-
|
|
414
|
+
is_temp_file = False
|
|
415
|
+
|
|
416
|
+
if use_tempfile:
|
|
417
|
+
# Create temporary file
|
|
418
|
+
extension_map = self._get_extension_map()
|
|
419
|
+
extension = temp_suffix or extension_map.get(response_content_type, "")
|
|
420
|
+
|
|
421
|
+
# Create named temporary file
|
|
422
|
+
temp_fd, temp_path = tempfile.mkstemp(suffix=extension, prefix="wappa_media_")
|
|
423
|
+
try:
|
|
424
|
+
with os.fdopen(temp_fd, 'wb') as temp_file:
|
|
425
|
+
temp_file.write(data)
|
|
426
|
+
final_path = Path(temp_path)
|
|
427
|
+
is_temp_file = True
|
|
428
|
+
self.logger.info(
|
|
429
|
+
f"Media downloaded to temp file {final_path} ({downloaded_size} bytes)"
|
|
430
|
+
)
|
|
431
|
+
except Exception:
|
|
432
|
+
# Clean up on error
|
|
433
|
+
try:
|
|
434
|
+
os.unlink(temp_path)
|
|
435
|
+
except Exception:
|
|
436
|
+
pass
|
|
437
|
+
raise
|
|
438
|
+
|
|
439
|
+
elif destination_path:
|
|
440
|
+
# Original destination path logic
|
|
401
441
|
extension_map = self._get_extension_map()
|
|
402
442
|
extension = extension_map.get(response_content_type, "")
|
|
403
443
|
media_type_base = response_content_type.split("/")[0]
|
|
@@ -415,7 +455,8 @@ class WhatsAppMediaHandler(IMediaHandler):
|
|
|
415
455
|
f"Media successfully downloaded to {final_path} ({downloaded_size} bytes)"
|
|
416
456
|
)
|
|
417
457
|
|
|
418
|
-
|
|
458
|
+
# Create result with temp file handling
|
|
459
|
+
result = MediaDownloadResult(
|
|
419
460
|
success=True,
|
|
420
461
|
file_data=bytes(data),
|
|
421
462
|
file_path=str(final_path) if final_path else None,
|
|
@@ -425,6 +466,12 @@ class WhatsAppMediaHandler(IMediaHandler):
|
|
|
425
466
|
platform=PlatformType.WHATSAPP,
|
|
426
467
|
tenant_id=self._tenant_id,
|
|
427
468
|
)
|
|
469
|
+
|
|
470
|
+
# Mark as temp file if needed
|
|
471
|
+
if is_temp_file:
|
|
472
|
+
result.mark_as_temp_file(cleanup_on_exit=auto_cleanup)
|
|
473
|
+
|
|
474
|
+
return result
|
|
428
475
|
|
|
429
476
|
finally:
|
|
430
477
|
# Ensure response is closed
|
|
@@ -440,6 +487,67 @@ class WhatsAppMediaHandler(IMediaHandler):
|
|
|
440
487
|
tenant_id=self._tenant_id,
|
|
441
488
|
)
|
|
442
489
|
|
|
490
|
+
@asynccontextmanager
|
|
491
|
+
async def download_media_tempfile(
|
|
492
|
+
self,
|
|
493
|
+
media_id: str,
|
|
494
|
+
temp_suffix: str | None = None,
|
|
495
|
+
sender_id: str | None = None,
|
|
496
|
+
) -> AsyncContextManager[MediaDownloadResult]:
|
|
497
|
+
"""
|
|
498
|
+
Download media to a temporary file with automatic cleanup.
|
|
499
|
+
|
|
500
|
+
Convenience method that provides a context manager for temporary file handling.
|
|
501
|
+
The temporary file is automatically deleted when the context exits.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
media_id: Platform-specific media identifier
|
|
505
|
+
temp_suffix: Custom suffix for temporary file (e.g., '.mp3', '.jpg')
|
|
506
|
+
sender_id: Optional sender ID for logging/debugging
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
Async context manager yielding MediaDownloadResult with temp file path
|
|
510
|
+
|
|
511
|
+
Example:
|
|
512
|
+
async with handler.download_media_tempfile(media_id, '.mp3') as result:
|
|
513
|
+
if result.success:
|
|
514
|
+
# Use result.file_path - file auto-deleted on exit
|
|
515
|
+
process_audio(result.file_path)
|
|
516
|
+
"""
|
|
517
|
+
result = await self.download_media(
|
|
518
|
+
media_id=media_id,
|
|
519
|
+
use_tempfile=True,
|
|
520
|
+
temp_suffix=temp_suffix,
|
|
521
|
+
sender_id=sender_id,
|
|
522
|
+
auto_cleanup=True
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
try:
|
|
526
|
+
yield result
|
|
527
|
+
finally:
|
|
528
|
+
# Context manager cleanup handled by MediaDownloadResult
|
|
529
|
+
result._cleanup_temp_file()
|
|
530
|
+
|
|
531
|
+
async def get_media_as_bytes(
|
|
532
|
+
self, media_id: str
|
|
533
|
+
) -> MediaDownloadResult:
|
|
534
|
+
"""
|
|
535
|
+
Download media as bytes without creating any files.
|
|
536
|
+
|
|
537
|
+
Memory-only download for processing that doesn't require file system access.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
media_id: Platform-specific media identifier
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
MediaDownloadResult with file_data bytes (file_path will be None)
|
|
544
|
+
"""
|
|
545
|
+
return await self.download_media(
|
|
546
|
+
media_id=media_id,
|
|
547
|
+
destination_path=None,
|
|
548
|
+
use_tempfile=False
|
|
549
|
+
)
|
|
550
|
+
|
|
443
551
|
async def stream_media(
|
|
444
552
|
self, media_id: str, chunk_size: int = 8192
|
|
445
553
|
) -> AsyncIterator[bytes]:
|
wappa/models/__init__.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wappa Data Models for API Endpoints
|
|
3
|
+
|
|
4
|
+
Re-exports WhatsApp models with cleaner import paths for creating API endpoint schemas.
|
|
5
|
+
This provides easy access to message models for FastAPI endpoint typing.
|
|
6
|
+
|
|
7
|
+
Clean Architecture: Domain entities and data transfer objects.
|
|
8
|
+
|
|
9
|
+
Usage (User Request: Quick access to WhatsApp models for endpoint schemas):
|
|
10
|
+
# Basic message models
|
|
11
|
+
from wappa.models import BasicTextMessage, MessageResult
|
|
12
|
+
|
|
13
|
+
# Interactive models
|
|
14
|
+
from wappa.models import ButtonMessage, ListMessage, CTAMessage
|
|
15
|
+
|
|
16
|
+
# Media models
|
|
17
|
+
from wappa.models import ImageMessage, VideoMessage, AudioMessage
|
|
18
|
+
|
|
19
|
+
# Specialized models
|
|
20
|
+
from wappa.models import ContactMessage, LocationMessage
|
|
21
|
+
|
|
22
|
+
# Template models
|
|
23
|
+
from wappa.models import TextTemplateMessage, MediaTemplateMessage
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Re-export WhatsApp models with cleaner path (User Request: Quick access)
|
|
27
|
+
from ..messaging.whatsapp.models import (
|
|
28
|
+
# Basic Models
|
|
29
|
+
BasicTextMessage,
|
|
30
|
+
MessageResult,
|
|
31
|
+
ReadStatusMessage,
|
|
32
|
+
|
|
33
|
+
# Interactive Models
|
|
34
|
+
ButtonMessage,
|
|
35
|
+
CTAMessage,
|
|
36
|
+
ListMessage,
|
|
37
|
+
|
|
38
|
+
# Media Models
|
|
39
|
+
MediaType,
|
|
40
|
+
ImageMessage,
|
|
41
|
+
VideoMessage,
|
|
42
|
+
AudioMessage,
|
|
43
|
+
DocumentMessage,
|
|
44
|
+
StickerMessage,
|
|
45
|
+
|
|
46
|
+
# Specialized Models
|
|
47
|
+
ContactCard,
|
|
48
|
+
ContactMessage,
|
|
49
|
+
ContactValidationResult,
|
|
50
|
+
LocationMessage,
|
|
51
|
+
LocationRequestMessage,
|
|
52
|
+
LocationValidationResult,
|
|
53
|
+
BusinessContact,
|
|
54
|
+
PersonalContact,
|
|
55
|
+
|
|
56
|
+
# Template Models
|
|
57
|
+
TemplateType,
|
|
58
|
+
TemplateParameter,
|
|
59
|
+
TextTemplateMessage,
|
|
60
|
+
MediaTemplateMessage,
|
|
61
|
+
LocationTemplateMessage,
|
|
62
|
+
TemplateMessageStatus,
|
|
63
|
+
TemplateValidationResult,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
# Basic Models (User Request: Clean access for endpoint schemas)
|
|
68
|
+
"BasicTextMessage",
|
|
69
|
+
"MessageResult",
|
|
70
|
+
"ReadStatusMessage",
|
|
71
|
+
|
|
72
|
+
# Interactive Models
|
|
73
|
+
"ButtonMessage",
|
|
74
|
+
"CTAMessage",
|
|
75
|
+
"ListMessage",
|
|
76
|
+
|
|
77
|
+
# Media Models
|
|
78
|
+
"MediaType",
|
|
79
|
+
"ImageMessage",
|
|
80
|
+
"VideoMessage",
|
|
81
|
+
"AudioMessage",
|
|
82
|
+
"DocumentMessage",
|
|
83
|
+
"StickerMessage",
|
|
84
|
+
|
|
85
|
+
# Specialized Models
|
|
86
|
+
"ContactCard",
|
|
87
|
+
"ContactMessage",
|
|
88
|
+
"ContactValidationResult",
|
|
89
|
+
"LocationMessage",
|
|
90
|
+
"LocationRequestMessage",
|
|
91
|
+
"LocationValidationResult",
|
|
92
|
+
"BusinessContact",
|
|
93
|
+
"PersonalContact",
|
|
94
|
+
|
|
95
|
+
# Template Models
|
|
96
|
+
"TemplateType",
|
|
97
|
+
"TemplateParameter",
|
|
98
|
+
"TextTemplateMessage",
|
|
99
|
+
"MediaTemplateMessage",
|
|
100
|
+
"LocationTemplateMessage",
|
|
101
|
+
"TemplateMessageStatus",
|
|
102
|
+
"TemplateValidationResult",
|
|
103
|
+
]
|