wappa 0.1.9__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 (126) 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/app/main.py +2 -1
  9. wappa/cli/examples/init/app/master_event.py +5 -3
  10. wappa/cli/examples/json_cache_example/app/__init__.py +1 -1
  11. wappa/cli/examples/json_cache_example/app/main.py +56 -44
  12. wappa/cli/examples/json_cache_example/app/master_event.py +181 -145
  13. wappa/cli/examples/json_cache_example/app/models/__init__.py +1 -1
  14. wappa/cli/examples/json_cache_example/app/models/json_demo_models.py +32 -51
  15. wappa/cli/examples/json_cache_example/app/scores/__init__.py +2 -2
  16. wappa/cli/examples/json_cache_example/app/scores/score_base.py +52 -46
  17. wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +70 -62
  18. wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +41 -44
  19. wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +83 -71
  20. wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +73 -57
  21. wappa/cli/examples/json_cache_example/app/utils/__init__.py +2 -2
  22. wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +54 -56
  23. wappa/cli/examples/json_cache_example/app/utils/message_utils.py +85 -80
  24. wappa/cli/examples/openai_transcript/app/main.py +2 -1
  25. wappa/cli/examples/openai_transcript/app/master_event.py +31 -22
  26. wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +1 -1
  27. wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +37 -24
  28. wappa/cli/examples/redis_cache_example/app/__init__.py +1 -1
  29. wappa/cli/examples/redis_cache_example/app/main.py +56 -44
  30. wappa/cli/examples/redis_cache_example/app/master_event.py +181 -145
  31. wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +31 -50
  32. wappa/cli/examples/redis_cache_example/app/scores/__init__.py +2 -2
  33. wappa/cli/examples/redis_cache_example/app/scores/score_base.py +52 -46
  34. wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +70 -62
  35. wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +41 -44
  36. wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +83 -71
  37. wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +73 -57
  38. wappa/cli/examples/redis_cache_example/app/utils/__init__.py +2 -2
  39. wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +54 -56
  40. wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +85 -80
  41. wappa/cli/examples/simple_echo_example/app/__init__.py +1 -1
  42. wappa/cli/examples/simple_echo_example/app/main.py +41 -33
  43. wappa/cli/examples/simple_echo_example/app/master_event.py +78 -57
  44. wappa/cli/examples/wappa_full_example/app/__init__.py +1 -1
  45. wappa/cli/examples/wappa_full_example/app/handlers/__init__.py +1 -1
  46. wappa/cli/examples/wappa_full_example/app/handlers/command_handlers.py +134 -126
  47. wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +237 -229
  48. wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +170 -148
  49. wappa/cli/examples/wappa_full_example/app/main.py +51 -39
  50. wappa/cli/examples/wappa_full_example/app/master_event.py +179 -120
  51. wappa/cli/examples/wappa_full_example/app/models/__init__.py +1 -1
  52. wappa/cli/examples/wappa_full_example/app/models/state_models.py +113 -104
  53. wappa/cli/examples/wappa_full_example/app/models/user_models.py +92 -76
  54. wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +109 -83
  55. wappa/cli/examples/wappa_full_example/app/utils/__init__.py +1 -1
  56. wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +132 -113
  57. wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +175 -132
  58. wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +126 -87
  59. wappa/cli/main.py +9 -4
  60. wappa/core/__init__.py +18 -23
  61. wappa/core/config/settings.py +7 -5
  62. wappa/core/events/default_handlers.py +1 -1
  63. wappa/core/factory/wappa_builder.py +38 -25
  64. wappa/core/plugins/redis_plugin.py +1 -3
  65. wappa/core/plugins/wappa_core_plugin.py +7 -6
  66. wappa/core/types.py +12 -12
  67. wappa/core/wappa_app.py +10 -8
  68. wappa/database/__init__.py +3 -4
  69. wappa/domain/enums/messenger_platform.py +1 -2
  70. wappa/domain/factories/media_factory.py +5 -20
  71. wappa/domain/factories/message_factory.py +5 -20
  72. wappa/domain/factories/messenger_factory.py +2 -4
  73. wappa/domain/interfaces/cache_interface.py +7 -7
  74. wappa/domain/interfaces/media_interface.py +2 -5
  75. wappa/domain/models/media_result.py +1 -3
  76. wappa/domain/models/platforms/platform_config.py +1 -3
  77. wappa/messaging/__init__.py +9 -12
  78. wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +20 -22
  79. wappa/models/__init__.py +27 -35
  80. wappa/persistence/__init__.py +12 -15
  81. wappa/persistence/cache_factory.py +0 -1
  82. wappa/persistence/json/__init__.py +1 -1
  83. wappa/persistence/json/cache_adapters.py +37 -25
  84. wappa/persistence/json/handlers/state_handler.py +60 -52
  85. wappa/persistence/json/handlers/table_handler.py +51 -49
  86. wappa/persistence/json/handlers/user_handler.py +71 -55
  87. wappa/persistence/json/handlers/utils/file_manager.py +42 -39
  88. wappa/persistence/json/handlers/utils/key_factory.py +1 -1
  89. wappa/persistence/json/handlers/utils/serialization.py +13 -11
  90. wappa/persistence/json/json_cache_factory.py +4 -8
  91. wappa/persistence/json/storage_manager.py +66 -79
  92. wappa/persistence/memory/__init__.py +1 -1
  93. wappa/persistence/memory/cache_adapters.py +37 -25
  94. wappa/persistence/memory/handlers/state_handler.py +62 -52
  95. wappa/persistence/memory/handlers/table_handler.py +59 -53
  96. wappa/persistence/memory/handlers/user_handler.py +75 -55
  97. wappa/persistence/memory/handlers/utils/key_factory.py +1 -1
  98. wappa/persistence/memory/handlers/utils/memory_store.py +75 -71
  99. wappa/persistence/memory/handlers/utils/ttl_manager.py +59 -67
  100. wappa/persistence/memory/memory_cache_factory.py +3 -7
  101. wappa/persistence/memory/storage_manager.py +52 -62
  102. wappa/persistence/redis/cache_adapters.py +27 -21
  103. wappa/persistence/redis/ops.py +11 -11
  104. wappa/persistence/redis/redis_client.py +4 -6
  105. wappa/persistence/redis/redis_manager.py +12 -4
  106. wappa/processors/factory.py +5 -5
  107. wappa/schemas/factory.py +2 -5
  108. wappa/schemas/whatsapp/message_types/errors.py +3 -12
  109. wappa/schemas/whatsapp/validators.py +3 -3
  110. wappa/webhooks/__init__.py +17 -18
  111. wappa/webhooks/factory.py +3 -5
  112. wappa/webhooks/whatsapp/__init__.py +10 -13
  113. wappa/webhooks/whatsapp/message_types/audio.py +0 -4
  114. wappa/webhooks/whatsapp/message_types/document.py +1 -9
  115. wappa/webhooks/whatsapp/message_types/errors.py +3 -12
  116. wappa/webhooks/whatsapp/message_types/location.py +1 -21
  117. wappa/webhooks/whatsapp/message_types/sticker.py +1 -5
  118. wappa/webhooks/whatsapp/message_types/text.py +0 -6
  119. wappa/webhooks/whatsapp/message_types/video.py +1 -20
  120. wappa/webhooks/whatsapp/status_models.py +2 -2
  121. wappa/webhooks/whatsapp/validators.py +3 -3
  122. {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/METADATA +362 -8
  123. {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/RECORD +126 -126
  124. {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/WHEEL +0 -0
  125. {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/entry_points.txt +0 -0
  126. {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/licenses/LICENSE +0 -0
@@ -6,13 +6,14 @@ Contains models for managing button and list interactive states with TTL.
6
6
 
7
7
  from datetime import datetime, timedelta
8
8
  from enum import Enum
9
- from typing import Any, Dict, List, Optional
9
+ from typing import Any
10
10
 
11
11
  from pydantic import BaseModel, Field, field_validator
12
12
 
13
13
 
14
14
  class StateType(str, Enum):
15
15
  """Types of interactive states."""
16
+
16
17
  BUTTON = "button"
17
18
  LIST = "list"
18
19
  CTA = "cta"
@@ -22,6 +23,7 @@ class StateType(str, Enum):
22
23
 
23
24
  class StateStatus(str, Enum):
24
25
  """Status of interactive states."""
26
+
25
27
  ACTIVE = "active"
26
28
  EXPIRED = "expired"
27
29
  COMPLETED = "completed"
@@ -30,105 +32,106 @@ class StateStatus(str, Enum):
30
32
 
31
33
  class InteractiveState(BaseModel):
32
34
  """Base model for interactive session states (buttons/lists)."""
33
-
35
+
34
36
  # State identification
35
- state_id: str = Field(default_factory=lambda: f"state_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}")
37
+ state_id: str = Field(
38
+ default_factory=lambda: f"state_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}"
39
+ )
36
40
  user_id: str
37
41
  state_type: StateType
38
-
42
+
39
43
  # State timing
40
44
  created_at: datetime = Field(default_factory=datetime.now)
41
45
  expires_at: datetime
42
46
  last_activity: datetime = Field(default_factory=datetime.now)
43
-
47
+
44
48
  # State data
45
- context: Dict[str, Any] = Field(default_factory=dict)
46
- original_message_id: Optional[str] = None
47
- interactive_message_id: Optional[str] = None
48
-
49
+ context: dict[str, Any] = Field(default_factory=dict)
50
+ original_message_id: str | None = None
51
+ interactive_message_id: str | None = None
52
+
49
53
  # State management
50
54
  status: StateStatus = StateStatus.ACTIVE
51
55
  attempts: int = 0
52
56
  max_attempts: int = 5
53
-
57
+
54
58
  # Metadata
55
- creation_metadata: Dict[str, Any] = Field(default_factory=dict)
56
-
57
- @field_validator('user_id', mode='before')
59
+ creation_metadata: dict[str, Any] = Field(default_factory=dict)
60
+
61
+ @field_validator("user_id", mode="before")
58
62
  @classmethod
59
63
  def validate_user_id(cls, v):
60
64
  """Convert user ID to string if it's an integer."""
61
65
  return str(v) if v is not None else v
62
-
66
+
63
67
  @classmethod
64
- def create_with_ttl(cls, user_id: str, state_type: StateType, ttl_seconds: int = 600, **kwargs) -> "InteractiveState":
68
+ def create_with_ttl(
69
+ cls, user_id: str, state_type: StateType, ttl_seconds: int = 600, **kwargs
70
+ ) -> "InteractiveState":
65
71
  """Create a new interactive state with TTL."""
66
72
  expires_at = datetime.now() + timedelta(seconds=ttl_seconds)
67
73
  return cls(
68
- user_id=user_id,
69
- state_type=state_type,
70
- expires_at=expires_at,
71
- **kwargs
74
+ user_id=user_id, state_type=state_type, expires_at=expires_at, **kwargs
72
75
  )
73
-
76
+
74
77
  def is_expired(self) -> bool:
75
78
  """Check if the state has expired."""
76
79
  return datetime.now() > self.expires_at or self.status == StateStatus.EXPIRED
77
-
80
+
78
81
  def is_active(self) -> bool:
79
82
  """Check if the state is currently active."""
80
83
  return not self.is_expired() and self.status == StateStatus.ACTIVE
81
-
84
+
82
85
  def time_remaining_seconds(self) -> int:
83
86
  """Get remaining time in seconds."""
84
87
  if self.is_expired():
85
88
  return 0
86
89
  remaining = self.expires_at - datetime.now()
87
90
  return max(0, int(remaining.total_seconds()))
88
-
91
+
89
92
  def time_remaining_minutes(self) -> int:
90
93
  """Get remaining time in minutes."""
91
94
  return max(0, self.time_remaining_seconds() // 60)
92
-
95
+
93
96
  def increment_attempts(self) -> None:
94
97
  """Increment the attempts counter."""
95
98
  self.attempts += 1
96
99
  self.last_activity = datetime.now()
97
-
100
+
98
101
  if self.attempts >= self.max_attempts:
99
102
  self.status = StateStatus.CANCELLED
100
-
103
+
101
104
  def mark_completed(self) -> None:
102
105
  """Mark the state as completed."""
103
106
  self.status = StateStatus.COMPLETED
104
107
  self.last_activity = datetime.now()
105
-
108
+
106
109
  def mark_expired(self) -> None:
107
110
  """Mark the state as expired."""
108
111
  self.status = StateStatus.EXPIRED
109
112
  self.last_activity = datetime.now()
110
-
113
+
111
114
  def mark_cancelled(self) -> None:
112
115
  """Mark the state as cancelled."""
113
116
  self.status = StateStatus.CANCELLED
114
117
  self.last_activity = datetime.now()
115
-
118
+
116
119
  def update_context(self, key: str, value: Any) -> None:
117
120
  """Update a context value."""
118
121
  self.context[key] = value
119
122
  self.last_activity = datetime.now()
120
-
123
+
121
124
  def get_context_value(self, key: str, default: Any = None) -> Any:
122
125
  """Get a context value."""
123
126
  return self.context.get(key, default)
124
-
127
+
125
128
  def extend_ttl(self, additional_seconds: int) -> None:
126
129
  """Extend the TTL of this state."""
127
130
  if self.is_active():
128
131
  self.expires_at = self.expires_at + timedelta(seconds=additional_seconds)
129
132
  self.last_activity = datetime.now()
130
-
131
- def get_summary(self) -> Dict[str, Any]:
133
+
134
+ def get_summary(self) -> dict[str, Any]:
132
135
  """Get a summary of this state."""
133
136
  return {
134
137
  "state_id": self.state_id,
@@ -141,28 +144,28 @@ class InteractiveState(BaseModel):
141
144
  "max_attempts": self.max_attempts,
142
145
  "created_at": self.created_at.isoformat(),
143
146
  "expires_at": self.expires_at.isoformat(),
144
- "context": self.context
147
+ "context": self.context,
145
148
  }
146
149
 
147
150
 
148
151
  class ButtonState(InteractiveState):
149
152
  """State model specifically for button interactions."""
150
-
153
+
151
154
  state_type: StateType = StateType.BUTTON
152
-
155
+
153
156
  # Button-specific data
154
- button_options: List[Dict[str, str]] = Field(default_factory=list)
155
- selected_button_id: Optional[str] = None
156
- button_message_text: Optional[str] = None
157
-
157
+ button_options: list[dict[str, str]] = Field(default_factory=list)
158
+ selected_button_id: str | None = None
159
+ button_message_text: str | None = None
160
+
158
161
  @classmethod
159
162
  def create_button_state(
160
- cls,
161
- user_id: str,
162
- buttons: List[Dict[str, str]],
163
+ cls,
164
+ user_id: str,
165
+ buttons: list[dict[str, str]],
163
166
  message_text: str,
164
167
  ttl_seconds: int = 600,
165
- original_message_id: str = None
168
+ original_message_id: str = None,
166
169
  ) -> "ButtonState":
167
170
  """Create a new button state."""
168
171
  return cls.create_with_ttl(
@@ -175,15 +178,15 @@ class ButtonState(InteractiveState):
175
178
  context={
176
179
  "button_count": len(buttons),
177
180
  "message_text": message_text,
178
- "expected_selections": [btn.get("id", "") for btn in buttons]
179
- }
181
+ "expected_selections": [btn.get("id", "") for btn in buttons],
182
+ },
180
183
  )
181
-
184
+
182
185
  def is_valid_selection(self, button_id: str) -> bool:
183
186
  """Check if the button selection is valid."""
184
187
  valid_ids = [btn.get("id", "") for btn in self.button_options]
185
188
  return button_id in valid_ids
186
-
189
+
187
190
  def handle_selection(self, button_id: str) -> bool:
188
191
  """Handle button selection."""
189
192
  if self.is_valid_selection(button_id) and self.is_active():
@@ -193,12 +196,12 @@ class ButtonState(InteractiveState):
193
196
  self.update_context("selected_at", datetime.now().isoformat())
194
197
  return True
195
198
  return False
196
-
197
- def get_selected_button(self) -> Optional[Dict[str, str]]:
199
+
200
+ def get_selected_button(self) -> dict[str, str] | None:
198
201
  """Get the selected button information."""
199
202
  if not self.selected_button_id:
200
203
  return None
201
-
204
+
202
205
  for button in self.button_options:
203
206
  if button.get("id") == self.selected_button_id:
204
207
  return button
@@ -207,24 +210,24 @@ class ButtonState(InteractiveState):
207
210
 
208
211
  class ListState(InteractiveState):
209
212
  """State model specifically for list interactions."""
210
-
213
+
211
214
  state_type: StateType = StateType.LIST
212
-
215
+
213
216
  # List-specific data
214
- list_sections: List[Dict[str, Any]] = Field(default_factory=list)
215
- selected_item_id: Optional[str] = None
217
+ list_sections: list[dict[str, Any]] = Field(default_factory=list)
218
+ selected_item_id: str | None = None
216
219
  list_button_text: str = "Choose an option"
217
- list_message_text: Optional[str] = None
218
-
220
+ list_message_text: str | None = None
221
+
219
222
  @classmethod
220
223
  def create_list_state(
221
224
  cls,
222
225
  user_id: str,
223
- sections: List[Dict[str, Any]],
226
+ sections: list[dict[str, Any]],
224
227
  message_text: str,
225
228
  button_text: str = "Choose an option",
226
229
  ttl_seconds: int = 600,
227
- original_message_id: str = None
230
+ original_message_id: str = None,
228
231
  ) -> "ListState":
229
232
  """Create a new list state."""
230
233
  # Extract all possible item IDs for validation
@@ -234,7 +237,7 @@ class ListState(InteractiveState):
234
237
  for row in rows:
235
238
  if "id" in row:
236
239
  valid_ids.append(row["id"])
237
-
240
+
238
241
  return cls.create_with_ttl(
239
242
  user_id=user_id,
240
243
  state_type=StateType.LIST,
@@ -248,15 +251,15 @@ class ListState(InteractiveState):
248
251
  "total_items": len(valid_ids),
249
252
  "message_text": message_text,
250
253
  "button_text": button_text,
251
- "expected_selections": valid_ids
252
- }
254
+ "expected_selections": valid_ids,
255
+ },
253
256
  )
254
-
257
+
255
258
  def is_valid_selection(self, item_id: str) -> bool:
256
259
  """Check if the list item selection is valid."""
257
260
  valid_ids = self.get_context_value("expected_selections", [])
258
261
  return item_id in valid_ids
259
-
262
+
260
263
  def handle_selection(self, item_id: str) -> bool:
261
264
  """Handle list item selection."""
262
265
  if self.is_valid_selection(item_id) and self.is_active():
@@ -266,12 +269,12 @@ class ListState(InteractiveState):
266
269
  self.update_context("selected_at", datetime.now().isoformat())
267
270
  return True
268
271
  return False
269
-
270
- def get_selected_item(self) -> Optional[Dict[str, Any]]:
272
+
273
+ def get_selected_item(self) -> dict[str, Any] | None:
271
274
  """Get the selected list item information."""
272
275
  if not self.selected_item_id:
273
276
  return None
274
-
277
+
275
278
  for section in self.list_sections:
276
279
  rows = section.get("rows", [])
277
280
  for row in rows:
@@ -282,24 +285,24 @@ class ListState(InteractiveState):
282
285
 
283
286
  class CommandState(InteractiveState):
284
287
  """State model for command-based interactions."""
285
-
288
+
286
289
  state_type: StateType = StateType.CUSTOM
287
-
290
+
288
291
  # Command-specific data
289
292
  command_name: str
290
- expected_responses: List[str] = Field(default_factory=list)
293
+ expected_responses: list[str] = Field(default_factory=list)
291
294
  current_step: int = 0
292
295
  total_steps: int = 1
293
-
296
+
294
297
  @classmethod
295
298
  def create_command_state(
296
299
  cls,
297
300
  user_id: str,
298
301
  command_name: str,
299
- expected_responses: List[str] = None,
302
+ expected_responses: list[str] = None,
300
303
  ttl_seconds: int = 600,
301
304
  total_steps: int = 1,
302
- original_message_id: str = None
305
+ original_message_id: str = None,
303
306
  ) -> "CommandState":
304
307
  """Create a new command state."""
305
308
  return cls.create_with_ttl(
@@ -314,10 +317,10 @@ class CommandState(InteractiveState):
314
317
  "command_name": command_name,
315
318
  "expected_responses": expected_responses or [],
316
319
  "total_steps": total_steps,
317
- "current_step": 0
318
- }
320
+ "current_step": 0,
321
+ },
319
322
  )
320
-
323
+
321
324
  def advance_step(self) -> bool:
322
325
  """Advance to the next step."""
323
326
  if self.current_step < self.total_steps - 1:
@@ -327,11 +330,11 @@ class CommandState(InteractiveState):
327
330
  else:
328
331
  self.mark_completed()
329
332
  return False
330
-
333
+
331
334
  def is_final_step(self) -> bool:
332
335
  """Check if this is the final step."""
333
336
  return self.current_step >= self.total_steps - 1
334
-
337
+
335
338
  def get_progress_percentage(self) -> float:
336
339
  """Get the progress percentage."""
337
340
  if self.total_steps <= 1:
@@ -341,85 +344,91 @@ class CommandState(InteractiveState):
341
344
 
342
345
  class StateManager(BaseModel):
343
346
  """Manager for handling multiple interactive states."""
344
-
345
- active_states: Dict[str, InteractiveState] = Field(default_factory=dict)
346
- completed_states: Dict[str, InteractiveState] = Field(default_factory=dict)
347
-
347
+
348
+ active_states: dict[str, InteractiveState] = Field(default_factory=dict)
349
+ completed_states: dict[str, InteractiveState] = Field(default_factory=dict)
350
+
348
351
  def add_state(self, state: InteractiveState) -> None:
349
352
  """Add a new state to the manager."""
350
353
  state_key = f"{state.user_id}_{state.state_type.value}"
351
-
354
+
352
355
  # Remove any existing state of the same type for this user
353
356
  if state_key in self.active_states:
354
357
  old_state = self.active_states[state_key]
355
358
  old_state.mark_cancelled()
356
359
  self.completed_states[f"{state_key}_{old_state.state_id}"] = old_state
357
-
360
+
358
361
  self.active_states[state_key] = state
359
-
360
- def get_user_state(self, user_id: str, state_type: StateType) -> Optional[InteractiveState]:
362
+
363
+ def get_user_state(
364
+ self, user_id: str, state_type: StateType
365
+ ) -> InteractiveState | None:
361
366
  """Get the active state for a user and state type."""
362
367
  state_key = f"{user_id}_{state_type.value}"
363
368
  state = self.active_states.get(state_key)
364
-
369
+
365
370
  if state and state.is_expired():
366
371
  state.mark_expired()
367
372
  self.completed_states[f"{state_key}_{state.state_id}"] = state
368
373
  del self.active_states[state_key]
369
374
  return None
370
-
375
+
371
376
  return state if state and state.is_active() else None
372
-
373
- def remove_state(self, user_id: str, state_type: StateType) -> Optional[InteractiveState]:
377
+
378
+ def remove_state(
379
+ self, user_id: str, state_type: StateType
380
+ ) -> InteractiveState | None:
374
381
  """Remove a state from active states."""
375
382
  state_key = f"{user_id}_{state_type.value}"
376
383
  state = self.active_states.pop(state_key, None)
377
-
384
+
378
385
  if state:
379
386
  self.completed_states[f"{state_key}_{state.state_id}"] = state
380
-
387
+
381
388
  return state
382
-
389
+
383
390
  def cleanup_expired_states(self) -> int:
384
391
  """Clean up expired states and return count of cleaned states."""
385
392
  expired_count = 0
386
393
  expired_keys = []
387
-
394
+
388
395
  for state_key, state in self.active_states.items():
389
396
  if state.is_expired():
390
397
  state.mark_expired()
391
398
  self.completed_states[f"{state_key}_{state.state_id}"] = state
392
399
  expired_keys.append(state_key)
393
400
  expired_count += 1
394
-
401
+
395
402
  for key in expired_keys:
396
403
  del self.active_states[key]
397
-
404
+
398
405
  return expired_count
399
-
400
- def get_user_states(self, user_id: str) -> List[InteractiveState]:
406
+
407
+ def get_user_states(self, user_id: str) -> list[InteractiveState]:
401
408
  """Get all active states for a user."""
402
409
  user_states = []
403
410
  for state in self.active_states.values():
404
411
  if state.user_id == user_id and state.is_active():
405
412
  user_states.append(state)
406
413
  return user_states
407
-
408
- def get_statistics(self) -> Dict[str, Any]:
414
+
415
+ def get_statistics(self) -> dict[str, Any]:
409
416
  """Get manager statistics."""
410
417
  active_count = len(self.active_states)
411
418
  completed_count = len(self.completed_states)
412
-
419
+
413
420
  # Count by state type
414
421
  type_counts = {}
415
422
  for state in self.active_states.values():
416
423
  state_type = state.state_type.value
417
424
  type_counts[state_type] = type_counts.get(state_type, 0) + 1
418
-
425
+
419
426
  return {
420
427
  "active_states": active_count,
421
428
  "completed_states": completed_count,
422
429
  "total_states": active_count + completed_count,
423
430
  "state_type_distribution": type_counts,
424
- "cleanup_needed": sum(1 for state in self.active_states.values() if state.is_expired())
425
- }
431
+ "cleanup_needed": sum(
432
+ 1 for state in self.active_states.values() if state.is_expired()
433
+ ),
434
+ }