wappa 0.1.8__py3-none-any.whl → 0.1.9__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/cli/examples/init/.env.example +33 -0
- wappa/cli/examples/init/app/__init__.py +0 -0
- wappa/cli/examples/init/app/main.py +8 -0
- wappa/cli/examples/init/app/master_event.py +8 -0
- wappa/cli/examples/json_cache_example/.env.example +33 -0
- wappa/cli/examples/json_cache_example/app/__init__.py +1 -0
- wappa/cli/examples/json_cache_example/app/main.py +235 -0
- wappa/cli/examples/json_cache_example/app/master_event.py +419 -0
- wappa/cli/examples/json_cache_example/app/models/__init__.py +1 -0
- wappa/cli/examples/json_cache_example/app/models/json_demo_models.py +275 -0
- wappa/cli/examples/json_cache_example/app/scores/__init__.py +35 -0
- wappa/cli/examples/json_cache_example/app/scores/score_base.py +186 -0
- wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +248 -0
- wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +190 -0
- wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +260 -0
- wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +223 -0
- wappa/cli/examples/json_cache_example/app/utils/__init__.py +26 -0
- wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +176 -0
- wappa/cli/examples/json_cache_example/app/utils/message_utils.py +246 -0
- wappa/cli/examples/openai_transcript/.gitignore +63 -4
- wappa/cli/examples/openai_transcript/app/__init__.py +0 -0
- wappa/cli/examples/openai_transcript/app/main.py +8 -0
- wappa/cli/examples/openai_transcript/app/master_event.py +53 -0
- wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +3 -0
- wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +76 -0
- wappa/cli/examples/redis_cache_example/.env.example +33 -0
- wappa/cli/examples/redis_cache_example/app/__init__.py +6 -0
- wappa/cli/examples/redis_cache_example/app/main.py +234 -0
- wappa/cli/examples/redis_cache_example/app/master_event.py +419 -0
- wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +275 -0
- wappa/cli/examples/redis_cache_example/app/scores/__init__.py +35 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_base.py +186 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +248 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +190 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +260 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +223 -0
- wappa/cli/examples/redis_cache_example/app/utils/__init__.py +26 -0
- wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +176 -0
- wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +246 -0
- wappa/cli/examples/simple_echo_example/.env.example +33 -0
- wappa/cli/examples/simple_echo_example/app/__init__.py +7 -0
- wappa/cli/examples/simple_echo_example/app/main.py +183 -0
- wappa/cli/examples/simple_echo_example/app/master_event.py +209 -0
- wappa/cli/examples/wappa_full_example/.env.example +33 -0
- wappa/cli/examples/wappa_full_example/.gitignore +63 -4
- wappa/cli/examples/wappa_full_example/app/__init__.py +6 -0
- wappa/cli/examples/wappa_full_example/app/handlers/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/handlers/command_handlers.py +484 -0
- wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +551 -0
- wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +492 -0
- wappa/cli/examples/wappa_full_example/app/main.py +257 -0
- wappa/cli/examples/wappa_full_example/app/master_event.py +445 -0
- wappa/cli/examples/wappa_full_example/app/media/README.md +54 -0
- wappa/cli/examples/wappa_full_example/app/media/buttons/README.md +62 -0
- wappa/cli/examples/wappa_full_example/app/media/buttons/kitty.png +0 -0
- wappa/cli/examples/wappa_full_example/app/media/buttons/puppy.png +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/README.md +110 -0
- wappa/cli/examples/wappa_full_example/app/media/list/audio.mp3 +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/document.pdf +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/image.png +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/video.mp4 +0 -0
- wappa/cli/examples/wappa_full_example/app/models/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/models/state_models.py +425 -0
- wappa/cli/examples/wappa_full_example/app/models/user_models.py +287 -0
- wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +301 -0
- wappa/cli/examples/wappa_full_example/app/utils/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +483 -0
- wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +473 -0
- wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +298 -0
- wappa/cli/main.py +8 -4
- {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/METADATA +1 -1
- {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/RECORD +75 -11
- wappa/cli/examples/init/pyproject.toml +0 -7
- wappa/cli/examples/simple_echo_example/.python-version +0 -1
- wappa/cli/examples/simple_echo_example/pyproject.toml +0 -9
- {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/WHEEL +0 -0
- {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/entry_points.txt +0 -0
- {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic models for Redis cache demonstration.
|
|
3
|
+
|
|
4
|
+
These models demonstrate how to structure data for different cache types
|
|
5
|
+
in the Wappa Redis caching system.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field, field_validator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MessageHistory(BaseModel):
|
|
14
|
+
"""
|
|
15
|
+
Individual message entry for message history storage.
|
|
16
|
+
|
|
17
|
+
Stores a single message with its timestamp.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
message: str = Field(
|
|
21
|
+
...,
|
|
22
|
+
description="The message content or type description",
|
|
23
|
+
max_length=500
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
timestamp: datetime = Field(
|
|
27
|
+
default_factory=datetime.utcnow,
|
|
28
|
+
description="When the message was sent"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
message_type: str = Field(
|
|
32
|
+
default="text",
|
|
33
|
+
description="Type of message (text, image, audio, etc.)",
|
|
34
|
+
max_length=20
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class User(BaseModel):
|
|
39
|
+
"""
|
|
40
|
+
User profile model for user_cache demonstration.
|
|
41
|
+
|
|
42
|
+
Stores user information extracted from WhatsApp webhook data.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
phone_number: str = Field(
|
|
46
|
+
...,
|
|
47
|
+
description="User's phone number (WhatsApp ID)",
|
|
48
|
+
min_length=10,
|
|
49
|
+
max_length=20
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
user_name: str | None = Field(
|
|
53
|
+
None,
|
|
54
|
+
description="User's display name from WhatsApp profile",
|
|
55
|
+
max_length=100
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
first_seen: datetime = Field(
|
|
59
|
+
default_factory=datetime.utcnow,
|
|
60
|
+
description="When the user was first seen"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
last_seen: datetime = Field(
|
|
64
|
+
default_factory=datetime.utcnow,
|
|
65
|
+
description="When the user was last seen"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
message_count: int = Field(
|
|
69
|
+
default=0,
|
|
70
|
+
description="Total number of messages received from this user",
|
|
71
|
+
ge=0
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
is_active: bool = Field(
|
|
75
|
+
default=True,
|
|
76
|
+
description="Whether the user is currently active"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@field_validator('phone_number', mode='before')
|
|
80
|
+
@classmethod
|
|
81
|
+
def validate_phone_number(cls, v) -> str:
|
|
82
|
+
"""Convert phone number to string if it's an integer."""
|
|
83
|
+
if isinstance(v, int):
|
|
84
|
+
return str(v)
|
|
85
|
+
return v
|
|
86
|
+
|
|
87
|
+
def increment_message_count(self) -> None:
|
|
88
|
+
"""Increment the message count and update last_seen timestamp."""
|
|
89
|
+
self.message_count += 1
|
|
90
|
+
self.last_seen = datetime.utcnow()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class MessageLog(BaseModel):
|
|
94
|
+
"""
|
|
95
|
+
Message log model for table_cache demonstration.
|
|
96
|
+
|
|
97
|
+
Stores message history for a user with waid as primary key.
|
|
98
|
+
Contains a list of all messages sent by the user.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
user_id: str = Field(
|
|
102
|
+
...,
|
|
103
|
+
description="User's phone number/ID (primary key)",
|
|
104
|
+
min_length=10,
|
|
105
|
+
max_length=20
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
text_message: list[MessageHistory] = Field(
|
|
109
|
+
default_factory=list,
|
|
110
|
+
description="List of all messages sent by this user"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
tenant_id: str | None = Field(
|
|
114
|
+
None,
|
|
115
|
+
description="Tenant/business ID that received the messages",
|
|
116
|
+
max_length=100
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def get_log_key(self) -> str:
|
|
120
|
+
"""Generate the primary key for this user's message log."""
|
|
121
|
+
return f"msg_history:{self.user_id}"
|
|
122
|
+
|
|
123
|
+
def add_message(self, message: str, message_type: str = "text") -> None:
|
|
124
|
+
"""Add a new message to the user's history."""
|
|
125
|
+
new_message = MessageHistory(
|
|
126
|
+
message=message,
|
|
127
|
+
message_type=message_type,
|
|
128
|
+
timestamp=datetime.utcnow()
|
|
129
|
+
)
|
|
130
|
+
self.text_message.append(new_message)
|
|
131
|
+
|
|
132
|
+
def get_recent_messages(self, count: int = 10) -> list[MessageHistory]:
|
|
133
|
+
"""Get the most recent messages from the user's history."""
|
|
134
|
+
return self.text_message[-count:] if self.text_message else []
|
|
135
|
+
|
|
136
|
+
@field_validator('user_id', 'tenant_id', mode='before')
|
|
137
|
+
@classmethod
|
|
138
|
+
def validate_string_ids(cls, v) -> str:
|
|
139
|
+
"""Convert ID fields to string if they're integers."""
|
|
140
|
+
if isinstance(v, int):
|
|
141
|
+
return str(v)
|
|
142
|
+
return v
|
|
143
|
+
|
|
144
|
+
def get_message_count(self) -> int:
|
|
145
|
+
"""Get the total number of messages in the history."""
|
|
146
|
+
return len(self.text_message)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class StateHandler(BaseModel):
|
|
150
|
+
"""
|
|
151
|
+
State handler model for state_cache demonstration.
|
|
152
|
+
|
|
153
|
+
Manages user state for the /WAPPA command flow.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
is_wappa: bool = Field(
|
|
157
|
+
default=False,
|
|
158
|
+
description="Whether the user is in 'WAPPA' state"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
activated_at: datetime | None = Field(
|
|
162
|
+
None,
|
|
163
|
+
description="When the WAPPA state was activated"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
command_count: int = Field(
|
|
167
|
+
default=0,
|
|
168
|
+
description="Number of commands processed while in WAPPA state",
|
|
169
|
+
ge=0
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
last_command: str | None = Field(
|
|
173
|
+
None,
|
|
174
|
+
description="Last command processed in WAPPA state",
|
|
175
|
+
max_length=100
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def activate_wappa(self) -> None:
|
|
179
|
+
"""Activate WAPPA state."""
|
|
180
|
+
self.is_wappa = True
|
|
181
|
+
self.activated_at = datetime.utcnow()
|
|
182
|
+
self.command_count = 0
|
|
183
|
+
self.last_command = "/WAPPA"
|
|
184
|
+
|
|
185
|
+
def deactivate_wappa(self) -> None:
|
|
186
|
+
"""Deactivate WAPPA state."""
|
|
187
|
+
self.is_wappa = False
|
|
188
|
+
self.last_command = "/EXIT"
|
|
189
|
+
|
|
190
|
+
def process_command(self, command: str) -> None:
|
|
191
|
+
"""Process a command while in WAPPA state."""
|
|
192
|
+
self.command_count += 1
|
|
193
|
+
self.last_command = command
|
|
194
|
+
|
|
195
|
+
def get_state_duration(self) -> int | None:
|
|
196
|
+
"""Get how long the WAPPA state has been active in seconds."""
|
|
197
|
+
if not self.is_wappa or not self.activated_at:
|
|
198
|
+
return None
|
|
199
|
+
return int((datetime.utcnow() - self.activated_at).total_seconds())
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class CacheStats(BaseModel):
|
|
203
|
+
"""
|
|
204
|
+
Cache statistics model for monitoring cache usage.
|
|
205
|
+
|
|
206
|
+
Used to track cache performance and usage statistics.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
# Cache performance metrics
|
|
210
|
+
user_cache_hits: int = Field(default=0, ge=0)
|
|
211
|
+
user_cache_misses: int = Field(default=0, ge=0)
|
|
212
|
+
table_cache_entries: int = Field(default=0, ge=0)
|
|
213
|
+
state_cache_active: int = Field(default=0, ge=0)
|
|
214
|
+
|
|
215
|
+
# Operation tracking
|
|
216
|
+
total_operations: int = Field(default=0, ge=0)
|
|
217
|
+
errors: int = Field(default=0, ge=0)
|
|
218
|
+
|
|
219
|
+
# System information
|
|
220
|
+
cache_type: str = Field(default="Unknown")
|
|
221
|
+
connection_status: str = Field(default="Unknown")
|
|
222
|
+
is_healthy: bool = Field(default=True)
|
|
223
|
+
|
|
224
|
+
# Timing
|
|
225
|
+
last_updated: datetime = Field(default_factory=datetime.utcnow)
|
|
226
|
+
|
|
227
|
+
def record_user_hit(self) -> None:
|
|
228
|
+
"""Record a user cache hit."""
|
|
229
|
+
self.user_cache_hits += 1
|
|
230
|
+
self.total_operations += 1
|
|
231
|
+
self.last_updated = datetime.utcnow()
|
|
232
|
+
|
|
233
|
+
def record_user_miss(self) -> None:
|
|
234
|
+
"""Record a user cache miss."""
|
|
235
|
+
self.user_cache_misses += 1
|
|
236
|
+
self.total_operations += 1
|
|
237
|
+
self.last_updated = datetime.utcnow()
|
|
238
|
+
|
|
239
|
+
def record_table_entry(self) -> None:
|
|
240
|
+
"""Record a new table cache entry."""
|
|
241
|
+
self.table_cache_entries += 1
|
|
242
|
+
self.total_operations += 1
|
|
243
|
+
self.last_updated = datetime.utcnow()
|
|
244
|
+
|
|
245
|
+
def record_state_activation(self) -> None:
|
|
246
|
+
"""Record a state cache activation."""
|
|
247
|
+
self.state_cache_active += 1
|
|
248
|
+
self.total_operations += 1
|
|
249
|
+
self.last_updated = datetime.utcnow()
|
|
250
|
+
|
|
251
|
+
def record_state_deactivation(self) -> None:
|
|
252
|
+
"""Record a state cache deactivation."""
|
|
253
|
+
if self.state_cache_active > 0:
|
|
254
|
+
self.state_cache_active -= 1
|
|
255
|
+
self.total_operations += 1
|
|
256
|
+
self.last_updated = datetime.utcnow()
|
|
257
|
+
|
|
258
|
+
def record_error(self) -> None:
|
|
259
|
+
"""Record an error."""
|
|
260
|
+
self.errors += 1
|
|
261
|
+
self.total_operations += 1
|
|
262
|
+
self.last_updated = datetime.utcnow()
|
|
263
|
+
|
|
264
|
+
def get_user_hit_rate(self) -> float:
|
|
265
|
+
"""Calculate user cache hit rate."""
|
|
266
|
+
total_user_ops = self.user_cache_hits + self.user_cache_misses
|
|
267
|
+
if total_user_ops == 0:
|
|
268
|
+
return 0.0
|
|
269
|
+
return round(self.user_cache_hits / total_user_ops, 3)
|
|
270
|
+
|
|
271
|
+
def get_error_rate(self) -> float:
|
|
272
|
+
"""Calculate overall error rate."""
|
|
273
|
+
if self.total_operations == 0:
|
|
274
|
+
return 0.0
|
|
275
|
+
return round(self.errors / self.total_operations, 3)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Score modules for Redis Cache Example following SOLID principles.
|
|
3
|
+
|
|
4
|
+
Each score module handles a specific business concern:
|
|
5
|
+
- score_user_management: User profile and caching logic
|
|
6
|
+
- score_message_history: Message logging and history retrieval
|
|
7
|
+
- score_state_commands: /WAPPA, /EXIT command processing
|
|
8
|
+
- score_cache_statistics: Cache monitoring and statistics
|
|
9
|
+
|
|
10
|
+
This architecture follows the Single Responsibility and Open/Closed principles.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .score_base import ScoreBase, ScoreDependencies
|
|
14
|
+
from .score_cache_statistics import CacheStatisticsScore
|
|
15
|
+
from .score_message_history import MessageHistoryScore
|
|
16
|
+
from .score_state_commands import StateCommandsScore
|
|
17
|
+
from .score_user_management import UserManagementScore
|
|
18
|
+
|
|
19
|
+
# Available score modules for automatic discovery
|
|
20
|
+
AVAILABLE_SCORES = [
|
|
21
|
+
UserManagementScore,
|
|
22
|
+
MessageHistoryScore,
|
|
23
|
+
StateCommandsScore,
|
|
24
|
+
CacheStatisticsScore,
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ScoreBase",
|
|
29
|
+
"ScoreDependencies",
|
|
30
|
+
"UserManagementScore",
|
|
31
|
+
"MessageHistoryScore",
|
|
32
|
+
"StateCommandsScore",
|
|
33
|
+
"CacheStatisticsScore",
|
|
34
|
+
"AVAILABLE_SCORES",
|
|
35
|
+
]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base interface for score modules following Interface Segregation Principle.
|
|
3
|
+
|
|
4
|
+
This module defines the common interface that all score modules must implement,
|
|
5
|
+
ensuring consistent behavior across different business logic handlers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from logging import Logger
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from wappa.domain.interfaces.cache_interface import ICache
|
|
14
|
+
from wappa.messaging.whatsapp.messenger.whatsapp_messenger import WhatsAppMessenger
|
|
15
|
+
from wappa.webhooks import IncomingMessageWebhook
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ScoreDependencies:
|
|
20
|
+
"""
|
|
21
|
+
Dependencies required by score modules.
|
|
22
|
+
|
|
23
|
+
This follows Dependency Inversion Principle by providing
|
|
24
|
+
abstractions that score modules depend on.
|
|
25
|
+
"""
|
|
26
|
+
messenger: WhatsAppMessenger
|
|
27
|
+
user_cache: ICache
|
|
28
|
+
table_cache: ICache
|
|
29
|
+
state_cache: ICache
|
|
30
|
+
logger: Logger
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ScoreBase(ABC):
|
|
34
|
+
"""
|
|
35
|
+
Base class for all score modules.
|
|
36
|
+
|
|
37
|
+
Implements Interface Segregation Principle by providing
|
|
38
|
+
only the methods that score modules actually need.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, dependencies: ScoreDependencies):
|
|
42
|
+
"""
|
|
43
|
+
Initialize score with injected dependencies.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
dependencies: Required dependencies for the score
|
|
47
|
+
"""
|
|
48
|
+
self.messenger = dependencies.messenger
|
|
49
|
+
self.user_cache = dependencies.user_cache
|
|
50
|
+
self.table_cache = dependencies.table_cache
|
|
51
|
+
self.state_cache = dependencies.state_cache
|
|
52
|
+
self.logger = dependencies.logger
|
|
53
|
+
|
|
54
|
+
# Track processing statistics
|
|
55
|
+
self._processing_count = 0
|
|
56
|
+
self._error_count = 0
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def score_name(self) -> str:
|
|
60
|
+
"""Return the name of this score module."""
|
|
61
|
+
return self.__class__.__name__
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def processing_stats(self) -> dict:
|
|
65
|
+
"""Return processing statistics for this score."""
|
|
66
|
+
return {
|
|
67
|
+
'processed': self._processing_count,
|
|
68
|
+
'errors': self._error_count,
|
|
69
|
+
'success_rate': (
|
|
70
|
+
(self._processing_count - self._error_count) / self._processing_count
|
|
71
|
+
if self._processing_count > 0 else 0.0
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
async def can_handle(self, webhook: IncomingMessageWebhook) -> bool:
|
|
77
|
+
"""
|
|
78
|
+
Determine if this score can handle the given webhook.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
webhook: Incoming message webhook to evaluate
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if this score should process the webhook
|
|
85
|
+
"""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
async def process(self, webhook: IncomingMessageWebhook) -> bool:
|
|
90
|
+
"""
|
|
91
|
+
Process the webhook with this score's business logic.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
webhook: Incoming message webhook to process
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if processing was successful and complete
|
|
98
|
+
"""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
async def validate_dependencies(self) -> bool:
|
|
102
|
+
"""
|
|
103
|
+
Validate that all required dependencies are available.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
True if all dependencies are valid
|
|
107
|
+
"""
|
|
108
|
+
if not all([self.messenger, self.user_cache, self.table_cache,
|
|
109
|
+
self.state_cache, self.logger]):
|
|
110
|
+
self.logger.error(f"{self.score_name}: Missing required dependencies")
|
|
111
|
+
return False
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
def _record_processing(self, success: bool = True) -> None:
|
|
115
|
+
"""Record processing statistics."""
|
|
116
|
+
self._processing_count += 1
|
|
117
|
+
if not success:
|
|
118
|
+
self._error_count += 1
|
|
119
|
+
|
|
120
|
+
async def _handle_error(self, error: Exception, context: str) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Handle errors consistently across score modules.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
error: Exception that occurred
|
|
126
|
+
context: Context where error occurred
|
|
127
|
+
"""
|
|
128
|
+
self._record_processing(success=False)
|
|
129
|
+
self.logger.error(
|
|
130
|
+
f"{self.score_name} error in {context}: {str(error)}",
|
|
131
|
+
exc_info=True
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def __str__(self) -> str:
|
|
135
|
+
"""String representation of the score."""
|
|
136
|
+
return f"{self.score_name}(processed={self._processing_count}, errors={self._error_count})"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class ScoreRegistry:
|
|
140
|
+
"""
|
|
141
|
+
Registry for managing score modules.
|
|
142
|
+
|
|
143
|
+
Implements Open/Closed Principle by allowing new scores
|
|
144
|
+
to be registered without modifying existing code.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def __init__(self):
|
|
148
|
+
self._scores: list[ScoreBase] = []
|
|
149
|
+
|
|
150
|
+
def register_score(self, score: ScoreBase) -> None:
|
|
151
|
+
"""Register a score module."""
|
|
152
|
+
if not isinstance(score, ScoreBase):
|
|
153
|
+
raise ValueError("Score must inherit from ScoreBase")
|
|
154
|
+
|
|
155
|
+
self._scores.append(score)
|
|
156
|
+
|
|
157
|
+
def get_scores(self) -> list[ScoreBase]:
|
|
158
|
+
"""Get all registered scores."""
|
|
159
|
+
return self._scores.copy()
|
|
160
|
+
|
|
161
|
+
async def find_handler(self, webhook: IncomingMessageWebhook) -> Optional[ScoreBase]:
|
|
162
|
+
"""
|
|
163
|
+
Find the first score that can handle the webhook.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
webhook: Webhook to find handler for
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Score that can handle the webhook, or None
|
|
170
|
+
"""
|
|
171
|
+
for score in self._scores:
|
|
172
|
+
try:
|
|
173
|
+
if await score.can_handle(webhook):
|
|
174
|
+
return score
|
|
175
|
+
except Exception as e:
|
|
176
|
+
# Log error but continue to next score
|
|
177
|
+
score.logger.error(f"Error checking if {score.score_name} can handle webhook: {e}")
|
|
178
|
+
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def get_score_stats(self) -> dict:
|
|
182
|
+
"""Get statistics for all registered scores."""
|
|
183
|
+
return {
|
|
184
|
+
score.score_name: score.processing_stats
|
|
185
|
+
for score in self._scores
|
|
186
|
+
}
|