wappa 0.1.7__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.

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