wappa 0.1.6__py3-none-any.whl → 0.1.8__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 (51) hide show
  1. wappa/__init__.py +13 -69
  2. wappa/cli/examples/init/.gitignore +69 -0
  3. wappa/cli/examples/init/pyproject.toml +7 -0
  4. wappa/cli/examples/json_cache_example/.gitignore +69 -0
  5. wappa/cli/examples/json_cache_example/README.md +190 -0
  6. wappa/cli/examples/openai_transcript/.gitignore +10 -0
  7. wappa/cli/examples/openai_transcript/README.md +0 -0
  8. wappa/cli/examples/redis_cache_example/.gitignore +69 -0
  9. wappa/cli/examples/redis_cache_example/README.md +0 -0
  10. wappa/cli/examples/simple_echo_example/.gitignore +69 -0
  11. wappa/cli/examples/simple_echo_example/.python-version +1 -0
  12. wappa/cli/examples/simple_echo_example/README.md +0 -0
  13. wappa/cli/examples/simple_echo_example/pyproject.toml +9 -0
  14. wappa/cli/examples/wappa_full_example/.dockerignore +171 -0
  15. wappa/cli/examples/wappa_full_example/.gitignore +10 -0
  16. wappa/cli/examples/wappa_full_example/Dockerfile +85 -0
  17. wappa/cli/examples/wappa_full_example/RAILWAY_DEPLOYMENT.md +366 -0
  18. wappa/cli/examples/wappa_full_example/README.md +322 -0
  19. wappa/cli/examples/wappa_full_example/docker-compose.yml +170 -0
  20. wappa/cli/examples/wappa_full_example/nginx.conf +177 -0
  21. wappa/cli/examples/wappa_full_example/railway.toml +30 -0
  22. wappa/cli/main.py +346 -22
  23. wappa/cli/templates/__init__.py.template +0 -0
  24. wappa/cli/templates/env.template +37 -0
  25. wappa/cli/templates/gitignore.template +165 -0
  26. wappa/cli/templates/main.py.template +8 -0
  27. wappa/cli/templates/master_event.py.template +8 -0
  28. wappa/core/__init__.py +86 -3
  29. wappa/core/config/settings.py +34 -2
  30. wappa/core/plugins/wappa_core_plugin.py +15 -5
  31. wappa/database/__init__.py +16 -4
  32. wappa/domain/interfaces/media_interface.py +57 -3
  33. wappa/domain/models/media_result.py +43 -0
  34. wappa/messaging/__init__.py +53 -3
  35. wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +112 -4
  36. wappa/models/__init__.py +103 -0
  37. wappa/persistence/__init__.py +55 -0
  38. wappa/webhooks/__init__.py +53 -4
  39. wappa/webhooks/whatsapp/__init__.py +57 -3
  40. wappa/webhooks/whatsapp/status_models.py +10 -0
  41. {wappa-0.1.6.dist-info → wappa-0.1.8.dist-info}/METADATA +1 -1
  42. {wappa-0.1.6.dist-info → wappa-0.1.8.dist-info}/RECORD +45 -24
  43. wappa/domain/interfaces/webhooks/__init__.py +0 -1
  44. wappa/persistence/json/handlers/__init__.py +0 -1
  45. wappa/persistence/json/handlers/utils/__init__.py +0 -1
  46. wappa/persistence/memory/handlers/__init__.py +0 -1
  47. wappa/persistence/memory/handlers/utils/__init__.py +0 -1
  48. wappa/schemas/webhooks/__init__.py +0 -3
  49. {wappa-0.1.6.dist-info → wappa-0.1.8.dist-info}/WHEEL +0 -0
  50. {wappa-0.1.6.dist-info → wappa-0.1.8.dist-info}/entry_points.txt +0 -0
  51. {wappa-0.1.6.dist-info → wappa-0.1.8.dist-info}/licenses/LICENSE +0 -0
wappa/core/__init__.py CHANGED
@@ -1,6 +1,89 @@
1
- """Core module for Wappa framework."""
1
+ """
2
+ Wappa Core Framework Components
2
3
 
3
- from .events import WappaEventHandler
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__ = ["Wappa", "WappaEventHandler"]
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
+ ]
@@ -5,6 +5,7 @@ Simple, reliable environment variable configuration focused on core WhatsApp fun
5
5
  """
6
6
 
7
7
  import os
8
+ import sys
8
9
  import tomllib
9
10
  from pathlib import Path
10
11
 
@@ -41,6 +42,34 @@ def _get_version_from_pyproject() -> str:
41
42
  return "0.1.0"
42
43
 
43
44
 
45
+ def _is_cli_context() -> bool:
46
+ """
47
+ Detect if we're running in CLI context (help, init, examples) vs server context (dev, prod).
48
+
49
+ Returns:
50
+ True if running CLI commands that don't need WhatsApp credentials
51
+ """
52
+ # Check command line arguments
53
+ if len(sys.argv) > 1:
54
+ # Direct CLI commands that don't need credentials
55
+ cli_only_commands = {"--help", "-h", "init", "examples"}
56
+
57
+ # Check for help flag or CLI-only commands
58
+ for arg in sys.argv[1:]:
59
+ if arg in cli_only_commands:
60
+ return True
61
+
62
+ # Check if we're running wappa command directly (not through uvicorn)
63
+ if any("wappa" in arg for arg in sys.argv):
64
+ # If no server commands (dev/prod) are present, assume CLI context
65
+ server_commands = {"dev", "prod"}
66
+ has_server_command = any(cmd in sys.argv for cmd in server_commands)
67
+ if not has_server_command:
68
+ return True
69
+
70
+ return False
71
+
72
+
44
73
  class Settings:
45
74
  """Application settings with environment-based configuration."""
46
75
 
@@ -98,9 +127,12 @@ class Settings:
98
127
  # Development/Production detection
99
128
  self.environment: str = os.getenv("ENVIRONMENT", "DEV")
100
129
 
101
- # Apply validation
130
+ # Apply validation (skip WhatsApp validation for CLI-only commands)
102
131
  self._validate_settings()
103
- self._validate_whatsapp_credentials()
132
+
133
+ # Only validate WhatsApp credentials for server operations
134
+ if not _is_cli_context():
135
+ self._validate_whatsapp_credentials()
104
136
 
105
137
  def _validate_settings(self):
106
138
  """Validate settings values."""
@@ -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
- # Initialize HTTP session for connection pooling (correct scope for app lifecycle)
153
- app.state.http_session = aiohttp.ClientSession()
154
- logger.debug("🌐 HTTP session initialized for connection pooling")
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.debug("🌐 HTTP session closed")
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"):
@@ -1,8 +1,17 @@
1
1
  """
2
- Wappa Database Module
2
+ Wappa Database Components
3
3
 
4
- This module provides database abstraction and adapters for SQLModel/SQLAlchemy
5
- async connections. It supports multiple database engines with a unified interface.
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 not provided, returns file data in memory.
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.
@@ -1,7 +1,57 @@
1
- """Messaging interfaces and implementations for Wappa framework."""
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
- from .whatsapp.messenger.whatsapp_messenger import WhatsAppMessenger
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__ = ["IMessenger", "WhatsAppMessenger"]
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
- if destination_path:
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
- return MediaDownloadResult(
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]: