wappa 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wappa might be problematic. Click here for more details.
- wappa/__init__.py +85 -0
- wappa/api/__init__.py +1 -0
- wappa/api/controllers/__init__.py +10 -0
- wappa/api/controllers/webhook_controller.py +441 -0
- wappa/api/dependencies/__init__.py +15 -0
- wappa/api/dependencies/whatsapp_dependencies.py +220 -0
- wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
- wappa/api/middleware/__init__.py +7 -0
- wappa/api/middleware/error_handler.py +158 -0
- wappa/api/middleware/owner.py +99 -0
- wappa/api/middleware/request_logging.py +184 -0
- wappa/api/routes/__init__.py +6 -0
- wappa/api/routes/health.py +102 -0
- wappa/api/routes/webhooks.py +211 -0
- wappa/api/routes/whatsapp/__init__.py +15 -0
- wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
- wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
- wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
- wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
- wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
- wappa/api/routes/whatsapp_combined.py +35 -0
- wappa/cli/__init__.py +9 -0
- wappa/cli/main.py +199 -0
- wappa/core/__init__.py +6 -0
- wappa/core/config/__init__.py +5 -0
- wappa/core/config/settings.py +161 -0
- wappa/core/events/__init__.py +41 -0
- wappa/core/events/default_handlers.py +642 -0
- wappa/core/events/event_dispatcher.py +244 -0
- wappa/core/events/event_handler.py +247 -0
- wappa/core/events/webhook_factory.py +219 -0
- wappa/core/factory/__init__.py +15 -0
- wappa/core/factory/plugin.py +68 -0
- wappa/core/factory/wappa_builder.py +326 -0
- wappa/core/logging/__init__.py +5 -0
- wappa/core/logging/context.py +100 -0
- wappa/core/logging/logger.py +343 -0
- wappa/core/plugins/__init__.py +34 -0
- wappa/core/plugins/auth_plugin.py +169 -0
- wappa/core/plugins/cors_plugin.py +128 -0
- wappa/core/plugins/custom_middleware_plugin.py +182 -0
- wappa/core/plugins/database_plugin.py +235 -0
- wappa/core/plugins/rate_limit_plugin.py +183 -0
- wappa/core/plugins/redis_plugin.py +224 -0
- wappa/core/plugins/wappa_core_plugin.py +261 -0
- wappa/core/plugins/webhook_plugin.py +253 -0
- wappa/core/types.py +108 -0
- wappa/core/wappa_app.py +546 -0
- wappa/database/__init__.py +18 -0
- wappa/database/adapter.py +107 -0
- wappa/database/adapters/__init__.py +17 -0
- wappa/database/adapters/mysql_adapter.py +187 -0
- wappa/database/adapters/postgresql_adapter.py +169 -0
- wappa/database/adapters/sqlite_adapter.py +174 -0
- wappa/domain/__init__.py +28 -0
- wappa/domain/builders/__init__.py +5 -0
- wappa/domain/builders/message_builder.py +189 -0
- wappa/domain/entities/__init__.py +5 -0
- wappa/domain/enums/messenger_platform.py +123 -0
- wappa/domain/factories/__init__.py +6 -0
- wappa/domain/factories/media_factory.py +450 -0
- wappa/domain/factories/message_factory.py +497 -0
- wappa/domain/factories/messenger_factory.py +244 -0
- wappa/domain/interfaces/__init__.py +32 -0
- wappa/domain/interfaces/base_repository.py +94 -0
- wappa/domain/interfaces/cache_factory.py +85 -0
- wappa/domain/interfaces/cache_interface.py +199 -0
- wappa/domain/interfaces/expiry_repository.py +68 -0
- wappa/domain/interfaces/media_interface.py +311 -0
- wappa/domain/interfaces/messaging_interface.py +523 -0
- wappa/domain/interfaces/pubsub_repository.py +151 -0
- wappa/domain/interfaces/repository_factory.py +108 -0
- wappa/domain/interfaces/shared_state_repository.py +122 -0
- wappa/domain/interfaces/state_repository.py +123 -0
- wappa/domain/interfaces/tables_repository.py +215 -0
- wappa/domain/interfaces/user_repository.py +114 -0
- wappa/domain/interfaces/webhooks/__init__.py +1 -0
- wappa/domain/models/media_result.py +110 -0
- wappa/domain/models/platforms/__init__.py +15 -0
- wappa/domain/models/platforms/platform_config.py +104 -0
- wappa/domain/services/__init__.py +11 -0
- wappa/domain/services/tenant_credentials_service.py +56 -0
- wappa/messaging/__init__.py +7 -0
- wappa/messaging/whatsapp/__init__.py +1 -0
- wappa/messaging/whatsapp/client/__init__.py +5 -0
- wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
- wappa/messaging/whatsapp/handlers/__init__.py +13 -0
- wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
- wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
- wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
- wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
- wappa/messaging/whatsapp/messenger/__init__.py +5 -0
- wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
- wappa/messaging/whatsapp/models/__init__.py +61 -0
- wappa/messaging/whatsapp/models/basic_models.py +65 -0
- wappa/messaging/whatsapp/models/interactive_models.py +287 -0
- wappa/messaging/whatsapp/models/media_models.py +215 -0
- wappa/messaging/whatsapp/models/specialized_models.py +304 -0
- wappa/messaging/whatsapp/models/template_models.py +261 -0
- wappa/persistence/cache_factory.py +93 -0
- wappa/persistence/json/__init__.py +14 -0
- wappa/persistence/json/cache_adapters.py +271 -0
- wappa/persistence/json/handlers/__init__.py +1 -0
- wappa/persistence/json/handlers/state_handler.py +250 -0
- wappa/persistence/json/handlers/table_handler.py +263 -0
- wappa/persistence/json/handlers/user_handler.py +213 -0
- wappa/persistence/json/handlers/utils/__init__.py +1 -0
- wappa/persistence/json/handlers/utils/file_manager.py +153 -0
- wappa/persistence/json/handlers/utils/key_factory.py +11 -0
- wappa/persistence/json/handlers/utils/serialization.py +121 -0
- wappa/persistence/json/json_cache_factory.py +76 -0
- wappa/persistence/json/storage_manager.py +285 -0
- wappa/persistence/memory/__init__.py +14 -0
- wappa/persistence/memory/cache_adapters.py +271 -0
- wappa/persistence/memory/handlers/__init__.py +1 -0
- wappa/persistence/memory/handlers/state_handler.py +250 -0
- wappa/persistence/memory/handlers/table_handler.py +280 -0
- wappa/persistence/memory/handlers/user_handler.py +213 -0
- wappa/persistence/memory/handlers/utils/__init__.py +1 -0
- wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
- wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
- wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
- wappa/persistence/memory/memory_cache_factory.py +76 -0
- wappa/persistence/memory/storage_manager.py +235 -0
- wappa/persistence/redis/README.md +699 -0
- wappa/persistence/redis/__init__.py +11 -0
- wappa/persistence/redis/cache_adapters.py +285 -0
- wappa/persistence/redis/ops.py +880 -0
- wappa/persistence/redis/redis_cache_factory.py +71 -0
- wappa/persistence/redis/redis_client.py +231 -0
- wappa/persistence/redis/redis_handler/__init__.py +26 -0
- wappa/persistence/redis/redis_handler/state_handler.py +176 -0
- wappa/persistence/redis/redis_handler/table.py +158 -0
- wappa/persistence/redis/redis_handler/user.py +138 -0
- wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
- wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
- wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
- wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
- wappa/persistence/redis/redis_manager.py +189 -0
- wappa/processors/__init__.py +6 -0
- wappa/processors/base_processor.py +262 -0
- wappa/processors/factory.py +550 -0
- wappa/processors/whatsapp_processor.py +810 -0
- wappa/schemas/__init__.py +6 -0
- wappa/schemas/core/__init__.py +71 -0
- wappa/schemas/core/base_message.py +499 -0
- wappa/schemas/core/base_status.py +322 -0
- wappa/schemas/core/base_webhook.py +312 -0
- wappa/schemas/core/types.py +253 -0
- wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
- wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
- wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
- wappa/schemas/factory.py +754 -0
- wappa/schemas/webhooks/__init__.py +3 -0
- wappa/schemas/whatsapp/__init__.py +6 -0
- wappa/schemas/whatsapp/base_models.py +285 -0
- wappa/schemas/whatsapp/message_types/__init__.py +93 -0
- wappa/schemas/whatsapp/message_types/audio.py +350 -0
- wappa/schemas/whatsapp/message_types/button.py +267 -0
- wappa/schemas/whatsapp/message_types/contact.py +464 -0
- wappa/schemas/whatsapp/message_types/document.py +421 -0
- wappa/schemas/whatsapp/message_types/errors.py +195 -0
- wappa/schemas/whatsapp/message_types/image.py +424 -0
- wappa/schemas/whatsapp/message_types/interactive.py +430 -0
- wappa/schemas/whatsapp/message_types/location.py +416 -0
- wappa/schemas/whatsapp/message_types/order.py +372 -0
- wappa/schemas/whatsapp/message_types/reaction.py +271 -0
- wappa/schemas/whatsapp/message_types/sticker.py +328 -0
- wappa/schemas/whatsapp/message_types/system.py +317 -0
- wappa/schemas/whatsapp/message_types/text.py +411 -0
- wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
- wappa/schemas/whatsapp/message_types/video.py +344 -0
- wappa/schemas/whatsapp/status_models.py +479 -0
- wappa/schemas/whatsapp/validators.py +454 -0
- wappa/schemas/whatsapp/webhook_container.py +438 -0
- wappa/webhooks/__init__.py +17 -0
- wappa/webhooks/core/__init__.py +71 -0
- wappa/webhooks/core/base_message.py +499 -0
- wappa/webhooks/core/base_status.py +322 -0
- wappa/webhooks/core/base_webhook.py +312 -0
- wappa/webhooks/core/types.py +253 -0
- wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
- wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
- wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
- wappa/webhooks/factory.py +754 -0
- wappa/webhooks/whatsapp/__init__.py +6 -0
- wappa/webhooks/whatsapp/base_models.py +285 -0
- wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
- wappa/webhooks/whatsapp/message_types/audio.py +350 -0
- wappa/webhooks/whatsapp/message_types/button.py +267 -0
- wappa/webhooks/whatsapp/message_types/contact.py +464 -0
- wappa/webhooks/whatsapp/message_types/document.py +421 -0
- wappa/webhooks/whatsapp/message_types/errors.py +195 -0
- wappa/webhooks/whatsapp/message_types/image.py +424 -0
- wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
- wappa/webhooks/whatsapp/message_types/location.py +416 -0
- wappa/webhooks/whatsapp/message_types/order.py +372 -0
- wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
- wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
- wappa/webhooks/whatsapp/message_types/system.py +317 -0
- wappa/webhooks/whatsapp/message_types/text.py +411 -0
- wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
- wappa/webhooks/whatsapp/message_types/video.py +344 -0
- wappa/webhooks/whatsapp/status_models.py +479 -0
- wappa/webhooks/whatsapp/validators.py +454 -0
- wappa/webhooks/whatsapp/webhook_container.py +438 -0
- wappa-0.1.0.dist-info/METADATA +269 -0
- wappa-0.1.0.dist-info/RECORD +211 -0
- wappa-0.1.0.dist-info/WHEEL +4 -0
- wappa-0.1.0.dist-info/entry_points.txt +2 -0
- wappa-0.1.0.dist-info/licenses/LICENSE +201 -0
wappa/core/wappa_app.py
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main Wappa application class.
|
|
3
|
+
|
|
4
|
+
This is the core class that developers use to create and run their WhatsApp applications.
|
|
5
|
+
The class now uses WappaBuilder internally for unified plugin-based architecture.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
import uvicorn
|
|
14
|
+
from fastapi import FastAPI
|
|
15
|
+
|
|
16
|
+
from wappa.api.routes.webhooks import create_webhook_router
|
|
17
|
+
|
|
18
|
+
from .config.settings import settings
|
|
19
|
+
from .events import WappaEventDispatcher
|
|
20
|
+
from .factory.wappa_builder import WappaBuilder
|
|
21
|
+
from .logging.logger import get_app_logger
|
|
22
|
+
from .plugins.wappa_core_plugin import WappaCorePlugin
|
|
23
|
+
from .types import CacheType, CacheTypeOptions, validate_cache_type
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from .events import WappaEventHandler
|
|
27
|
+
from .factory.plugin import WappaPlugin
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Traditional lifespan management has been moved to WappaCorePlugin
|
|
31
|
+
# This enables unified plugin-based architecture for all Wappa applications
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Wappa:
|
|
35
|
+
"""
|
|
36
|
+
Main Wappa application class with unified plugin-based architecture.
|
|
37
|
+
|
|
38
|
+
This class now uses WappaBuilder internally, providing both simple usage
|
|
39
|
+
for beginners and advanced extensibility for power users. All functionality
|
|
40
|
+
is implemented through plugins, ensuring consistency and maintainability.
|
|
41
|
+
|
|
42
|
+
Simple Usage:
|
|
43
|
+
app = Wappa()
|
|
44
|
+
app.set_event_handler(MyEventHandler())
|
|
45
|
+
app.run()
|
|
46
|
+
|
|
47
|
+
Advanced Usage:
|
|
48
|
+
app = Wappa(cache="redis")
|
|
49
|
+
app.add_plugin(DatabasePlugin(...))
|
|
50
|
+
app.add_startup_hook(my_startup_func)
|
|
51
|
+
app.set_event_handler(MyEventHandler())
|
|
52
|
+
app.run()
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, cache: CacheTypeOptions = "memory", config: dict | None = None):
|
|
56
|
+
"""
|
|
57
|
+
Initialize Wappa application with plugin-based architecture.
|
|
58
|
+
|
|
59
|
+
Automatically creates WappaBuilder with WappaCorePlugin and adds
|
|
60
|
+
cache-specific plugins based on the cache type.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
cache: Cache type ('memory', 'redis', 'json')
|
|
64
|
+
config: Optional configuration overrides for FastAPI app
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: If cache type is not supported
|
|
68
|
+
"""
|
|
69
|
+
# Validate and convert cache type
|
|
70
|
+
self.cache_type = validate_cache_type(cache)
|
|
71
|
+
self.config = config or {}
|
|
72
|
+
self._event_handler: WappaEventHandler | None = None
|
|
73
|
+
self._app: FastAPI | None = None
|
|
74
|
+
self._asgi: FastAPI | None = None
|
|
75
|
+
|
|
76
|
+
# Initialize WappaBuilder with core plugin
|
|
77
|
+
self._builder = WappaBuilder()
|
|
78
|
+
self._core_plugin = WappaCorePlugin(cache_type=self.cache_type)
|
|
79
|
+
self._builder.add_plugin(self._core_plugin)
|
|
80
|
+
|
|
81
|
+
# Automatically add cache-specific plugins
|
|
82
|
+
self._auto_add_cache_plugins()
|
|
83
|
+
|
|
84
|
+
# Apply any FastAPI configuration overrides
|
|
85
|
+
if self.config:
|
|
86
|
+
self._builder.configure(**self.config)
|
|
87
|
+
|
|
88
|
+
logger = get_app_logger()
|
|
89
|
+
logger.debug(
|
|
90
|
+
f"🏗️ Wappa initialized with cache_type={self.cache_type.value}, "
|
|
91
|
+
f"plugins={len(self._builder.plugins)}, config_overrides={bool(self.config)}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def _auto_add_cache_plugins(self) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Automatically add cache-specific plugins based on cache type.
|
|
97
|
+
|
|
98
|
+
This method ensures users get the appropriate cache infrastructure
|
|
99
|
+
automatically without manual plugin management.
|
|
100
|
+
"""
|
|
101
|
+
logger = get_app_logger()
|
|
102
|
+
|
|
103
|
+
if self.cache_type == CacheType.REDIS:
|
|
104
|
+
from .plugins.redis_plugin import RedisPlugin
|
|
105
|
+
|
|
106
|
+
redis_plugin = RedisPlugin()
|
|
107
|
+
self._builder.add_plugin(redis_plugin)
|
|
108
|
+
logger.debug("🔴 Auto-added RedisPlugin for Redis cache type")
|
|
109
|
+
elif self.cache_type == CacheType.JSON:
|
|
110
|
+
# Future: JSONCachePlugin implementation
|
|
111
|
+
logger.debug("📄 JSON cache type selected (plugin not yet implemented)")
|
|
112
|
+
else: # CacheType.MEMORY
|
|
113
|
+
logger.debug("💾 Memory cache type selected (no additional plugin needed)")
|
|
114
|
+
|
|
115
|
+
def set_event_handler(self, handler: "WappaEventHandler") -> None:
|
|
116
|
+
"""
|
|
117
|
+
Set the event handler for this application.
|
|
118
|
+
|
|
119
|
+
Dependencies are now injected per-request by the WebhookController,
|
|
120
|
+
ensuring proper multi-tenant support and correct tenant isolation.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
handler: WappaEventHandler instance to handle webhooks
|
|
124
|
+
"""
|
|
125
|
+
# Store handler reference - dependencies will be injected per request
|
|
126
|
+
self._event_handler = handler
|
|
127
|
+
|
|
128
|
+
logger = get_app_logger()
|
|
129
|
+
logger.debug(f"Event handler set: {handler.__class__.__name__}")
|
|
130
|
+
|
|
131
|
+
def set_app(self, app: FastAPI) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Set a pre-built FastAPI application instance.
|
|
134
|
+
|
|
135
|
+
This method allows users to provide a FastAPI app that was created
|
|
136
|
+
using WappaBuilder or other advanced configuration methods, while
|
|
137
|
+
still using the Wappa class for event handler integration and running.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
app: Pre-configured FastAPI application instance
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
# Create advanced app with WappaBuilder
|
|
144
|
+
builder = WappaBuilder()
|
|
145
|
+
app = await (builder
|
|
146
|
+
.add_plugin(DatabasePlugin(...))
|
|
147
|
+
.add_plugin(RedisPlugin())
|
|
148
|
+
.build())
|
|
149
|
+
|
|
150
|
+
# Use with Wappa for event handling
|
|
151
|
+
wappa = Wappa()
|
|
152
|
+
wappa.set_app(app)
|
|
153
|
+
wappa.set_event_handler(MyHandler())
|
|
154
|
+
wappa.run()
|
|
155
|
+
"""
|
|
156
|
+
self._app = app
|
|
157
|
+
|
|
158
|
+
logger = get_app_logger()
|
|
159
|
+
logger.debug(
|
|
160
|
+
f"Pre-built FastAPI app set: {app.title} v{app.version} "
|
|
161
|
+
f"(middleware: {len(app.user_middleware)}, routes: {len(app.routes)})"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def asgi(self) -> FastAPI:
|
|
166
|
+
"""
|
|
167
|
+
Return a FastAPI ASGI application, building synchronously if needed.
|
|
168
|
+
|
|
169
|
+
This property enables uvicorn reload compatibility by providing a synchronous
|
|
170
|
+
way to access the FastAPI app. Plugin configuration is deferred to lifespan
|
|
171
|
+
hooks to maintain async initialization while keeping this property sync.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
FastAPI ASGI application instance
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
ValueError: If no event handler has been set
|
|
178
|
+
"""
|
|
179
|
+
if self._asgi is None:
|
|
180
|
+
self._asgi = self._build_asgi_sync()
|
|
181
|
+
return self._asgi
|
|
182
|
+
|
|
183
|
+
def _build_asgi_sync(self) -> FastAPI:
|
|
184
|
+
"""
|
|
185
|
+
Build FastAPI application synchronously using WappaBuilder.
|
|
186
|
+
|
|
187
|
+
Creates the FastAPI app with WappaBuilder's unified lifespan management.
|
|
188
|
+
Plugin configuration is deferred to lifespan startup hooks to maintain
|
|
189
|
+
proper async initialization.
|
|
190
|
+
"""
|
|
191
|
+
if not self._event_handler:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
"Must set event handler with set_event_handler() before accessing .asgi property"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
logger = get_app_logger()
|
|
197
|
+
logger.debug("Building FastAPI ASGI app synchronously using WappaBuilder")
|
|
198
|
+
|
|
199
|
+
# Configure FastAPI settings for builder
|
|
200
|
+
self._builder.configure(
|
|
201
|
+
title="Wappa Application",
|
|
202
|
+
description="WhatsApp Business application built with Wappa framework",
|
|
203
|
+
version=settings.version,
|
|
204
|
+
docs_url="/docs" if settings.is_development else None,
|
|
205
|
+
redoc_url="/redoc" if settings.is_development else None,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Use WappaBuilder.build() - creates app with lifespan,
|
|
209
|
+
# defers plugin configuration to startup hooks
|
|
210
|
+
app = self._builder.build()
|
|
211
|
+
|
|
212
|
+
# Add webhook routes to the built app
|
|
213
|
+
dispatcher = WappaEventDispatcher(self._event_handler)
|
|
214
|
+
webhook_router = create_webhook_router(dispatcher)
|
|
215
|
+
app.include_router(webhook_router)
|
|
216
|
+
|
|
217
|
+
logger.info(
|
|
218
|
+
f"✅ Wappa ASGI app built synchronously - cache: {self.cache_type.value}, "
|
|
219
|
+
f"plugins: {len(self._builder.plugins)}, "
|
|
220
|
+
f"event_handler: {self._event_handler.__class__.__name__}"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return app
|
|
224
|
+
|
|
225
|
+
def create_app(self) -> FastAPI:
|
|
226
|
+
"""
|
|
227
|
+
Create FastAPI application.
|
|
228
|
+
|
|
229
|
+
This method directly uses WappaBuilder.build() which creates the app
|
|
230
|
+
with unified lifespan management.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Configured FastAPI application instance
|
|
234
|
+
"""
|
|
235
|
+
if self._asgi is None:
|
|
236
|
+
self._asgi = self._build_asgi_sync()
|
|
237
|
+
return self._asgi
|
|
238
|
+
|
|
239
|
+
def add_plugin(self, plugin: "WappaPlugin") -> "Wappa":
|
|
240
|
+
"""
|
|
241
|
+
Add a plugin to extend Wappa functionality.
|
|
242
|
+
|
|
243
|
+
This method provides access to the underlying WappaBuilder's plugin system
|
|
244
|
+
while maintaining the simple Wappa interface. Plugins should be added
|
|
245
|
+
before calling create_app() or run().
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
plugin: WappaPlugin instance to add
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Self for method chaining
|
|
252
|
+
|
|
253
|
+
Example:
|
|
254
|
+
from wappa.plugins import DatabasePlugin
|
|
255
|
+
|
|
256
|
+
app = Wappa(cache="redis")
|
|
257
|
+
app.add_plugin(DatabasePlugin("postgresql://...", PostgreSQLAdapter()))
|
|
258
|
+
app.set_event_handler(MyEventHandler())
|
|
259
|
+
app.run()
|
|
260
|
+
"""
|
|
261
|
+
self._builder.add_plugin(plugin)
|
|
262
|
+
|
|
263
|
+
logger = get_app_logger()
|
|
264
|
+
logger.debug(f"Plugin added to Wappa: {plugin.__class__.__name__}")
|
|
265
|
+
|
|
266
|
+
return self
|
|
267
|
+
|
|
268
|
+
def add_startup_hook(self, hook: Callable, priority: int = 50) -> "Wappa":
|
|
269
|
+
"""
|
|
270
|
+
Add a startup hook to be executed during application startup.
|
|
271
|
+
|
|
272
|
+
This provides access to the underlying WappaBuilder's hook system.
|
|
273
|
+
Hooks are executed in priority order during app startup.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
hook: Async callable that takes (app: FastAPI) -> None
|
|
277
|
+
priority: Execution priority (lower numbers run first)
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Self for method chaining
|
|
281
|
+
|
|
282
|
+
Example:
|
|
283
|
+
async def my_startup(app: FastAPI):
|
|
284
|
+
print("My service is starting!")
|
|
285
|
+
|
|
286
|
+
app = Wappa()
|
|
287
|
+
app.add_startup_hook(my_startup, priority=30)
|
|
288
|
+
app.set_event_handler(MyEventHandler())
|
|
289
|
+
app.run()
|
|
290
|
+
"""
|
|
291
|
+
self._builder.add_startup_hook(hook, priority)
|
|
292
|
+
|
|
293
|
+
logger = get_app_logger()
|
|
294
|
+
hook_name = getattr(hook, "__name__", "anonymous_hook")
|
|
295
|
+
logger.debug(f"Startup hook added to Wappa: {hook_name} (priority: {priority})")
|
|
296
|
+
|
|
297
|
+
return self
|
|
298
|
+
|
|
299
|
+
def add_shutdown_hook(self, hook: Callable, priority: int = 50) -> "Wappa":
|
|
300
|
+
"""
|
|
301
|
+
Add a shutdown hook to be executed during application shutdown.
|
|
302
|
+
|
|
303
|
+
This provides access to the underlying WappaBuilder's hook system.
|
|
304
|
+
Hooks are executed in reverse priority order during app shutdown.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
hook: Async callable that takes (app: FastAPI) -> None
|
|
308
|
+
priority: Execution priority (higher numbers run first in shutdown)
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Self for method chaining
|
|
312
|
+
|
|
313
|
+
Example:
|
|
314
|
+
async def my_shutdown(app: FastAPI):
|
|
315
|
+
print("Cleaning up my service!")
|
|
316
|
+
|
|
317
|
+
app = Wappa()
|
|
318
|
+
app.add_shutdown_hook(my_shutdown, priority=30)
|
|
319
|
+
app.set_event_handler(MyEventHandler())
|
|
320
|
+
app.run()
|
|
321
|
+
"""
|
|
322
|
+
self._builder.add_shutdown_hook(hook, priority)
|
|
323
|
+
|
|
324
|
+
logger = get_app_logger()
|
|
325
|
+
hook_name = getattr(hook, "__name__", "anonymous_hook")
|
|
326
|
+
logger.debug(
|
|
327
|
+
f"Shutdown hook added to Wappa: {hook_name} (priority: {priority})"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
return self
|
|
331
|
+
|
|
332
|
+
def add_middleware(self, middleware_class: type, priority: int = 50, **kwargs) -> "Wappa":
|
|
333
|
+
"""
|
|
334
|
+
Add middleware to the application with priority ordering.
|
|
335
|
+
|
|
336
|
+
This provides access to the underlying WappaBuilder's middleware system.
|
|
337
|
+
Priority determines execution order:
|
|
338
|
+
- Lower numbers run first (outer middleware)
|
|
339
|
+
- Higher numbers run last (inner middleware)
|
|
340
|
+
- Default priority is 50
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
middleware_class: Middleware class to add
|
|
344
|
+
priority: Execution priority (lower = outer, higher = inner)
|
|
345
|
+
**kwargs: Middleware configuration parameters
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Self for method chaining
|
|
349
|
+
|
|
350
|
+
Example:
|
|
351
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
352
|
+
|
|
353
|
+
app = Wappa(cache="redis")
|
|
354
|
+
app.add_middleware(CORSMiddleware, allow_origins=["*"], priority=30)
|
|
355
|
+
app.set_event_handler(MyHandler())
|
|
356
|
+
app.run()
|
|
357
|
+
"""
|
|
358
|
+
self._builder.add_middleware(middleware_class, priority, **kwargs)
|
|
359
|
+
|
|
360
|
+
logger = get_app_logger()
|
|
361
|
+
logger.debug(
|
|
362
|
+
f"Middleware added to Wappa: {middleware_class.__name__} (priority: {priority})"
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return self
|
|
366
|
+
|
|
367
|
+
def add_router(self, router, **kwargs) -> "Wappa":
|
|
368
|
+
"""
|
|
369
|
+
Add a router to the application.
|
|
370
|
+
|
|
371
|
+
This provides access to the underlying WappaBuilder's router system.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
router: FastAPI router to include
|
|
375
|
+
**kwargs: Arguments for app.include_router()
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Self for method chaining
|
|
379
|
+
|
|
380
|
+
Example:
|
|
381
|
+
from fastapi import APIRouter
|
|
382
|
+
|
|
383
|
+
custom_router = APIRouter()
|
|
384
|
+
|
|
385
|
+
app = Wappa(cache="redis")
|
|
386
|
+
app.add_router(custom_router, prefix="/api/v1", tags=["custom"])
|
|
387
|
+
app.set_event_handler(MyHandler())
|
|
388
|
+
app.run()
|
|
389
|
+
"""
|
|
390
|
+
self._builder.add_router(router, **kwargs)
|
|
391
|
+
|
|
392
|
+
logger = get_app_logger()
|
|
393
|
+
router_name = getattr(router, "prefix", "router")
|
|
394
|
+
logger.debug(f"Router added to Wappa: {router_name} with config: {kwargs}")
|
|
395
|
+
|
|
396
|
+
return self
|
|
397
|
+
|
|
398
|
+
def configure(self, **overrides) -> "Wappa":
|
|
399
|
+
"""
|
|
400
|
+
Override default FastAPI configuration.
|
|
401
|
+
|
|
402
|
+
This provides access to the underlying WappaBuilder's configuration system.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
**overrides: FastAPI constructor arguments to override
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
Self for method chaining
|
|
409
|
+
|
|
410
|
+
Example:
|
|
411
|
+
app = Wappa(cache="redis")
|
|
412
|
+
app.configure(
|
|
413
|
+
title="My WhatsApp Bot",
|
|
414
|
+
version="2.0.0",
|
|
415
|
+
description="Custom bot with advanced features"
|
|
416
|
+
)
|
|
417
|
+
app.set_event_handler(MyHandler())
|
|
418
|
+
app.run()
|
|
419
|
+
"""
|
|
420
|
+
self._builder.configure(**overrides)
|
|
421
|
+
|
|
422
|
+
logger = get_app_logger()
|
|
423
|
+
logger.debug(f"FastAPI configuration overrides added: {list(overrides.keys())}")
|
|
424
|
+
|
|
425
|
+
return self
|
|
426
|
+
|
|
427
|
+
def run(self, host: str = "0.0.0.0", port: int | None = None, **kwargs) -> None:
|
|
428
|
+
"""
|
|
429
|
+
Run the Wappa application using uvicorn.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
host: Host to bind to
|
|
433
|
+
port: Port to bind to (defaults to settings.port)
|
|
434
|
+
**kwargs: Additional uvicorn configuration
|
|
435
|
+
"""
|
|
436
|
+
# Use port from settings if not provided
|
|
437
|
+
if port is None:
|
|
438
|
+
port = settings.port
|
|
439
|
+
|
|
440
|
+
# Use settings.is_development to determine mode
|
|
441
|
+
dev_mode = settings.is_development
|
|
442
|
+
|
|
443
|
+
logger = get_app_logger()
|
|
444
|
+
logger.info(f"Starting Wappa v{settings.version} server on {host}:{port}")
|
|
445
|
+
logger.info(f"Mode: {'development' if dev_mode else 'production'}")
|
|
446
|
+
|
|
447
|
+
if dev_mode:
|
|
448
|
+
logger.info("🔄 Development mode: auto-reload enabled")
|
|
449
|
+
self._run_dev_mode(host, port, **kwargs)
|
|
450
|
+
else:
|
|
451
|
+
logger.info("🚀 Production mode: running directly")
|
|
452
|
+
self._run_production(host, port, **kwargs)
|
|
453
|
+
|
|
454
|
+
def _run_production(self, host: str, port: int, **kwargs) -> None:
|
|
455
|
+
"""Run in production mode without auto-reload."""
|
|
456
|
+
logger = get_app_logger()
|
|
457
|
+
|
|
458
|
+
uvicorn_config = {
|
|
459
|
+
"host": host,
|
|
460
|
+
"port": port,
|
|
461
|
+
"reload": False,
|
|
462
|
+
"log_level": settings.log_level.lower(),
|
|
463
|
+
**kwargs,
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
logger.info("Starting production server with .asgi property...")
|
|
467
|
+
# Use the .asgi property which handles sync build + lifespan initialization
|
|
468
|
+
uvicorn.run(self.asgi, **uvicorn_config)
|
|
469
|
+
|
|
470
|
+
def _run_dev_mode(self, host: str, port: int, **kwargs) -> None:
|
|
471
|
+
"""
|
|
472
|
+
Run in development mode with uvicorn auto-reload.
|
|
473
|
+
|
|
474
|
+
Uses uvicorn with import string for proper reload functionality.
|
|
475
|
+
Requires module-level 'app' variable containing Wappa instance.
|
|
476
|
+
"""
|
|
477
|
+
import inspect
|
|
478
|
+
|
|
479
|
+
logger = get_app_logger()
|
|
480
|
+
|
|
481
|
+
# Get the calling module to build import string
|
|
482
|
+
frame = inspect.currentframe()
|
|
483
|
+
while frame and frame.f_globals.get("__name__") == __name__:
|
|
484
|
+
frame = frame.f_back
|
|
485
|
+
|
|
486
|
+
if not frame:
|
|
487
|
+
logger.error("❌ Cannot detect calling script for dev mode")
|
|
488
|
+
raise RuntimeError(
|
|
489
|
+
"Cannot locate calling script for dev mode.\n"
|
|
490
|
+
"Make sure you're running from a Python file, not a REPL."
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
module_name = frame.f_globals.get("__name__")
|
|
494
|
+
if module_name in (None, "__main__"):
|
|
495
|
+
# Try to guess from file path
|
|
496
|
+
file_path = frame.f_globals.get("__file__")
|
|
497
|
+
if not file_path:
|
|
498
|
+
raise RuntimeError("Dev mode requires running from a file, not a REPL.")
|
|
499
|
+
|
|
500
|
+
# Convert file path to module name (e.g., examples/main.py -> examples.main)
|
|
501
|
+
import os.path
|
|
502
|
+
|
|
503
|
+
module_name = (
|
|
504
|
+
os.path.splitext(os.path.relpath(file_path))[0]
|
|
505
|
+
.replace(os.sep, ".")
|
|
506
|
+
.lstrip(".")
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Build uvicorn command with import string
|
|
510
|
+
import_string = f"{module_name}:app.asgi"
|
|
511
|
+
cmd = [
|
|
512
|
+
sys.executable,
|
|
513
|
+
"-m",
|
|
514
|
+
"uvicorn",
|
|
515
|
+
import_string,
|
|
516
|
+
"--reload",
|
|
517
|
+
"--host",
|
|
518
|
+
host,
|
|
519
|
+
"--port",
|
|
520
|
+
str(port),
|
|
521
|
+
"--log-level",
|
|
522
|
+
settings.log_level.lower(),
|
|
523
|
+
]
|
|
524
|
+
|
|
525
|
+
# Add additional kwargs
|
|
526
|
+
for key, value in kwargs.items():
|
|
527
|
+
if key != "reload": # Skip reload, we're handling it
|
|
528
|
+
cmd.extend([f"--{key.replace('_', '-')}", str(value)])
|
|
529
|
+
|
|
530
|
+
logger.info(f"🚀 Starting dev server: {' '.join(cmd)}")
|
|
531
|
+
logger.info(f"📡 Import string: {import_string}")
|
|
532
|
+
|
|
533
|
+
try:
|
|
534
|
+
subprocess.run(cmd, check=True)
|
|
535
|
+
except subprocess.CalledProcessError as e:
|
|
536
|
+
logger.error(f"❌ Uvicorn failed to start (exit code: {e.returncode})")
|
|
537
|
+
raise RuntimeError(
|
|
538
|
+
f"Development server failed to start.\n\n"
|
|
539
|
+
f"Common causes:\n"
|
|
540
|
+
f"• No module-level 'app' variable found in {module_name}\n"
|
|
541
|
+
f"• Port {port} already in use\n"
|
|
542
|
+
f"• Import errors in your script\n\n"
|
|
543
|
+
f"Make sure you have: app = Wappa(...) at module level."
|
|
544
|
+
) from e
|
|
545
|
+
except KeyboardInterrupt:
|
|
546
|
+
logger.info("👋 Development server stopped by user")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wappa Database Module
|
|
3
|
+
|
|
4
|
+
This module provides database abstraction and adapters for SQLModel/SQLAlchemy
|
|
5
|
+
async connections. It supports multiple database engines with a unified interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .adapter import DatabaseAdapter
|
|
9
|
+
from .adapters.mysql_adapter import MySQLAdapter
|
|
10
|
+
from .adapters.postgresql_adapter import PostgreSQLAdapter
|
|
11
|
+
from .adapters.sqlite_adapter import SQLiteAdapter
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"DatabaseAdapter",
|
|
15
|
+
"PostgreSQLAdapter",
|
|
16
|
+
"SQLiteAdapter",
|
|
17
|
+
"MySQLAdapter",
|
|
18
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database Adapter Protocol
|
|
3
|
+
|
|
4
|
+
Defines the interface for database adapters that work with SQLModel and
|
|
5
|
+
SQLAlchemy async engines. Each adapter handles database-specific connection
|
|
6
|
+
patterns, configuration, and schema management.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from typing import TYPE_CHECKING, Any, AsyncContextManager, Protocol
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
|
|
14
|
+
from sqlmodel import SQLModel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DatabaseAdapter(Protocol):
|
|
18
|
+
"""
|
|
19
|
+
Universal database adapter interface for SQLModel/SQLAlchemy async connections.
|
|
20
|
+
|
|
21
|
+
All database adapters must implement these methods to provide consistent
|
|
22
|
+
database integration across different engines (PostgreSQL, SQLite, MySQL).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
async def create_engine(
|
|
26
|
+
self, connection_string: str, **kwargs: Any
|
|
27
|
+
) -> "AsyncEngine":
|
|
28
|
+
"""
|
|
29
|
+
Create an async SQLAlchemy engine for the database.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
connection_string: Database connection URL
|
|
33
|
+
**kwargs: Engine-specific configuration options
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Configured AsyncEngine instance
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ConnectionError: If unable to create engine
|
|
40
|
+
"""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
async def create_session_factory(
|
|
44
|
+
self, engine: "AsyncEngine"
|
|
45
|
+
) -> Callable[[], AsyncContextManager["AsyncSession"]]:
|
|
46
|
+
"""
|
|
47
|
+
Create a session factory for the database engine.
|
|
48
|
+
|
|
49
|
+
The returned factory creates async context managers that yield
|
|
50
|
+
AsyncSession instances for database operations.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
engine: AsyncEngine instance
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Callable that returns AsyncSession context manager
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
session_factory = await adapter.create_session_factory(engine)
|
|
60
|
+
async with session_factory() as session:
|
|
61
|
+
# Use session for database operations
|
|
62
|
+
pass
|
|
63
|
+
"""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
async def initialize_schema(
|
|
67
|
+
self, engine: "AsyncEngine", models: list[type["SQLModel"]] = None
|
|
68
|
+
) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Initialize database schema from SQLModel definitions.
|
|
71
|
+
|
|
72
|
+
Creates all tables defined by the provided SQLModel classes.
|
|
73
|
+
Handles database-specific schema creation patterns.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
engine: AsyncEngine instance
|
|
77
|
+
models: List of SQLModel classes to create tables for
|
|
78
|
+
If None, creates all tables from metadata
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
DatabaseError: If schema creation fails
|
|
82
|
+
"""
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
async def health_check(self, engine: "AsyncEngine") -> bool:
|
|
86
|
+
"""
|
|
87
|
+
Perform a health check on the database connection.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
engine: AsyncEngine instance to check
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if database is healthy and responsive
|
|
94
|
+
"""
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
async def get_connection_info(self, engine: "AsyncEngine") -> dict[str, Any]:
|
|
98
|
+
"""
|
|
99
|
+
Get information about the database connection.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
engine: AsyncEngine instance
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Dictionary with connection information (driver, version, etc.)
|
|
106
|
+
"""
|
|
107
|
+
...
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database Adapters Module
|
|
3
|
+
|
|
4
|
+
Contains specific database adapter implementations for SQLModel/SQLAlchemy
|
|
5
|
+
async engines. Each adapter handles database-specific connection patterns,
|
|
6
|
+
configuration, and optimization.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .mysql_adapter import MySQLAdapter
|
|
10
|
+
from .postgresql_adapter import PostgreSQLAdapter
|
|
11
|
+
from .sqlite_adapter import SQLiteAdapter
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"PostgreSQLAdapter",
|
|
15
|
+
"SQLiteAdapter",
|
|
16
|
+
"MySQLAdapter",
|
|
17
|
+
]
|