wappa 0.1.8__py3-none-any.whl → 0.1.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wappa might be problematic. Click here for more details.

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