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
@@ -5,7 +5,7 @@ Provides user-specific cache operations using in-memory storage.
5
5
  """
6
6
 
7
7
  import logging
8
- from typing import Any, Dict, Optional
8
+ from typing import Any
9
9
 
10
10
  from pydantic import BaseModel
11
11
 
@@ -18,196 +18,216 @@ logger = logging.getLogger("MemoryUser")
18
18
  class MemoryUser:
19
19
  """
20
20
  Memory-based user cache handler.
21
-
21
+
22
22
  Mirrors RedisUser functionality using in-memory storage.
23
23
  Maintains the same API for seamless cache backend switching.
24
24
  """
25
-
25
+
26
26
  def __init__(self, tenant: str, user_id: str):
27
27
  """
28
28
  Initialize Memory user handler.
29
-
29
+
30
30
  Args:
31
31
  tenant: Tenant identifier
32
32
  user_id: User identifier
33
33
  """
34
34
  if not tenant or not user_id:
35
- raise ValueError(f"Missing required parameters: tenant={tenant}, user_id={user_id}")
36
-
35
+ raise ValueError(
36
+ f"Missing required parameters: tenant={tenant}, user_id={user_id}"
37
+ )
38
+
37
39
  self.tenant = tenant
38
40
  self.user_id = user_id
39
41
  self.keys = default_key_factory
40
-
42
+
41
43
  def _key(self) -> str:
42
44
  """Build user key using KeyFactory (same as Redis)."""
43
45
  return self.keys.user(self.tenant, self.user_id)
44
-
46
+
45
47
  # ---- Public API matching RedisUser ----
46
48
  async def get(self, models: type[BaseModel] | None = None) -> dict[str, Any] | None:
47
49
  """
48
50
  Get full user data.
49
-
51
+
50
52
  Args:
51
53
  models: Optional BaseModel class for deserialization
52
-
54
+
53
55
  Returns:
54
56
  User data dictionary or BaseModel instance, None if not found
55
57
  """
56
58
  key = self._key()
57
- return await storage_manager.get("users", self.tenant, self.user_id, key, models)
58
-
59
- async def upsert(self, data: dict[str, Any] | BaseModel, ttl: int | None = None) -> bool:
59
+ return await storage_manager.get(
60
+ "users", self.tenant, self.user_id, key, models
61
+ )
62
+
63
+ async def upsert(
64
+ self, data: dict[str, Any] | BaseModel, ttl: int | None = None
65
+ ) -> bool:
60
66
  """
61
67
  Create or update user data.
62
-
68
+
63
69
  Args:
64
70
  data: User data to store
65
71
  ttl: Time to live in seconds
66
-
72
+
67
73
  Returns:
68
74
  True if successful, False otherwise
69
75
  """
70
76
  key = self._key()
71
- return await storage_manager.set("users", self.tenant, self.user_id, key, data, ttl)
72
-
77
+ return await storage_manager.set(
78
+ "users", self.tenant, self.user_id, key, data, ttl
79
+ )
80
+
73
81
  async def delete(self) -> int:
74
82
  """
75
83
  Delete user data.
76
-
84
+
77
85
  Returns:
78
86
  1 if deleted, 0 if didn't exist
79
87
  """
80
88
  key = self._key()
81
89
  success = await storage_manager.delete("users", self.tenant, self.user_id, key)
82
90
  return 1 if success else 0
83
-
91
+
84
92
  async def exists(self) -> bool:
85
93
  """
86
94
  Check if user data exists.
87
-
95
+
88
96
  Returns:
89
97
  True if exists, False otherwise
90
98
  """
91
99
  key = self._key()
92
100
  return await storage_manager.exists("users", self.tenant, self.user_id, key)
93
-
101
+
94
102
  async def get_field(self, field: str) -> Any | None:
95
103
  """
96
104
  Get a specific field from user data.
97
-
105
+
98
106
  Args:
99
107
  field: Field name
100
-
108
+
101
109
  Returns:
102
110
  Field value or None if not found
103
111
  """
104
112
  user_data = await self.get()
105
113
  if user_data is None:
106
114
  return None
107
-
115
+
108
116
  if isinstance(user_data, dict):
109
117
  return user_data.get(field)
110
118
  else:
111
119
  # BaseModel instance
112
120
  return getattr(user_data, field, None)
113
-
114
- async def update_field(self, field: str, value: Any, ttl: int | None = None) -> bool:
121
+
122
+ async def update_field(
123
+ self, field: str, value: Any, ttl: int | None = None
124
+ ) -> bool:
115
125
  """
116
126
  Update a specific field in user data.
117
-
127
+
118
128
  Args:
119
129
  field: Field name
120
130
  value: New value
121
131
  ttl: Time to live in seconds
122
-
132
+
123
133
  Returns:
124
134
  True if successful, False otherwise
125
135
  """
126
136
  user_data = await self.get()
127
137
  if user_data is None:
128
138
  user_data = {}
129
-
139
+
130
140
  if isinstance(user_data, BaseModel):
131
141
  user_data = user_data.model_dump()
132
-
142
+
133
143
  user_data[field] = value
134
144
  return await self.upsert(user_data, ttl)
135
-
136
- async def increment_field(self, field: str, increment: int = 1, ttl: int | None = None) -> int | None:
145
+
146
+ async def increment_field(
147
+ self, field: str, increment: int = 1, ttl: int | None = None
148
+ ) -> int | None:
137
149
  """
138
150
  Atomically increment an integer field.
139
-
151
+
140
152
  Args:
141
153
  field: Field name
142
154
  increment: Amount to increment by
143
155
  ttl: Time to live in seconds
144
-
156
+
145
157
  Returns:
146
158
  New value after increment or None on error
147
159
  """
148
160
  user_data = await self.get()
149
161
  if user_data is None:
150
162
  user_data = {}
151
-
163
+
152
164
  if isinstance(user_data, BaseModel):
153
165
  user_data = user_data.model_dump()
154
-
166
+
155
167
  current_value = user_data.get(field, 0)
156
- if not isinstance(current_value, (int, float)):
157
- logger.warning(f"Cannot increment non-numeric field '{field}': {current_value}")
168
+ if not isinstance(current_value, int | float):
169
+ logger.warning(
170
+ f"Cannot increment non-numeric field '{field}': {current_value}"
171
+ )
158
172
  return None
159
-
173
+
160
174
  new_value = int(current_value) + increment
161
175
  user_data[field] = new_value
162
-
176
+
163
177
  success = await self.upsert(user_data, ttl)
164
178
  return new_value if success else None
165
-
166
- async def append_to_list(self, field: str, value: Any, ttl: int | None = None) -> bool:
179
+
180
+ async def append_to_list(
181
+ self, field: str, value: Any, ttl: int | None = None
182
+ ) -> bool:
167
183
  """
168
184
  Append value to a list field.
169
-
185
+
170
186
  Args:
171
187
  field: Field name containing list
172
188
  value: Value to append
173
189
  ttl: Time to live in seconds
174
-
190
+
175
191
  Returns:
176
192
  True if successful, False otherwise
177
193
  """
178
194
  user_data = await self.get()
179
195
  if user_data is None:
180
196
  user_data = {}
181
-
197
+
182
198
  if isinstance(user_data, BaseModel):
183
199
  user_data = user_data.model_dump()
184
-
200
+
185
201
  current_list = user_data.get(field, [])
186
202
  if not isinstance(current_list, list):
187
203
  current_list = []
188
-
204
+
189
205
  current_list.append(value)
190
206
  user_data[field] = current_list
191
-
207
+
192
208
  return await self.upsert(user_data, ttl)
193
-
209
+
194
210
  async def get_ttl(self, key: str) -> int:
195
211
  """
196
212
  Get remaining time to live.
197
-
213
+
198
214
  Returns:
199
215
  Remaining TTL in seconds, -1 if no expiry, -2 if doesn't exist
200
216
  """
201
- return await storage_manager.get_ttl("users", self.tenant, self.user_id, self._key())
202
-
217
+ return await storage_manager.get_ttl(
218
+ "users", self.tenant, self.user_id, self._key()
219
+ )
220
+
203
221
  async def renew_ttl(self, key: str, ttl: int) -> bool:
204
222
  """
205
223
  Renew time to live.
206
-
224
+
207
225
  Args:
208
226
  ttl: New time to live in seconds
209
-
227
+
210
228
  Returns:
211
229
  True if successful, False otherwise
212
230
  """
213
- return await storage_manager.set_ttl("users", self.tenant, self.user_id, self._key(), ttl)
231
+ return await storage_manager.set_ttl(
232
+ "users", self.tenant, self.user_id, self._key(), ttl
233
+ )
@@ -8,4 +8,4 @@ across all cache implementations.
8
8
  from ....redis.redis_handler.utils.key_factory import KeyFactory, default_key_factory
9
9
 
10
10
  # Export the same key factory used by Redis for consistency
11
- __all__ = ["KeyFactory", "default_key_factory"]
11
+ __all__ = ["KeyFactory", "default_key_factory"]
@@ -8,7 +8,7 @@ and automatic expiration cleanup.
8
8
  import asyncio
9
9
  import logging
10
10
  from datetime import datetime, timedelta
11
- from typing import Any, Dict, Optional, Tuple
11
+ from typing import Any
12
12
 
13
13
  logger = logging.getLogger("MemoryStore")
14
14
 
@@ -16,62 +16,62 @@ logger = logging.getLogger("MemoryStore")
16
16
  class MemoryStore:
17
17
  """
18
18
  Thread-safe in-memory store with TTL support.
19
-
19
+
20
20
  Storage Structure:
21
21
  {
22
22
  "users": {context_key: {key: (data, expires_at)}},
23
23
  "tables": {context_key: {key: (data, expires_at)}},
24
24
  "states": {context_key: {key: (data, expires_at)}}
25
25
  }
26
-
26
+
27
27
  Where context_key is typically "{tenant_id}_{user_id}" for isolation.
28
28
  """
29
-
29
+
30
30
  def __init__(self):
31
- self._store: Dict[str, Dict[str, Dict[str, Tuple[Any, Optional[datetime]]]]] = {
31
+ self._store: dict[str, dict[str, dict[str, tuple[Any, datetime | None]]]] = {
32
32
  "users": {},
33
33
  "tables": {},
34
- "states": {}
34
+ "states": {},
35
35
  }
36
36
  self._locks = {
37
37
  "users": asyncio.Lock(),
38
38
  "tables": asyncio.Lock(),
39
- "states": asyncio.Lock()
39
+ "states": asyncio.Lock(),
40
40
  }
41
- self._cleanup_task: Optional[asyncio.Task] = None
41
+ self._cleanup_task: asyncio.Task | None = None
42
42
  self._cleanup_interval = 300 # 5 minutes
43
-
43
+
44
44
  def start_cleanup_task(self):
45
45
  """Start background TTL cleanup task."""
46
46
  if self._cleanup_task is None or self._cleanup_task.done():
47
47
  self._cleanup_task = asyncio.create_task(self._cleanup_expired_entries())
48
48
  logger.info("Started memory store TTL cleanup task")
49
-
49
+
50
50
  def stop_cleanup_task(self):
51
51
  """Stop background TTL cleanup task."""
52
52
  if self._cleanup_task and not self._cleanup_task.done():
53
53
  self._cleanup_task.cancel()
54
54
  logger.info("Stopped memory store TTL cleanup task")
55
-
55
+
56
56
  async def get(self, namespace: str, context_key: str, key: str) -> Any:
57
57
  """
58
58
  Get value with automatic expiration check.
59
-
59
+
60
60
  Args:
61
61
  namespace: Cache namespace ("users", "tables", "states")
62
62
  context_key: Context identifier (e.g., "{tenant_id}_{user_id}")
63
63
  key: Cache key
64
-
64
+
65
65
  Returns:
66
66
  Cached value or None if not found/expired
67
67
  """
68
68
  if namespace not in self._locks:
69
69
  raise ValueError(f"Invalid namespace: {namespace}")
70
-
70
+
71
71
  async with self._locks[namespace]:
72
72
  store = self._store[namespace]
73
73
  context_store = store.get(context_key, {})
74
-
74
+
75
75
  if key in context_store:
76
76
  data, expires_at = context_store[key]
77
77
  if expires_at and datetime.now() > expires_at:
@@ -80,64 +80,64 @@ class MemoryStore:
80
80
  return None
81
81
  return data
82
82
  return None
83
-
83
+
84
84
  async def set(
85
- self,
86
- namespace: str,
87
- context_key: str,
88
- key: str,
89
- data: Any,
90
- ttl: Optional[int] = None
85
+ self,
86
+ namespace: str,
87
+ context_key: str,
88
+ key: str,
89
+ data: Any,
90
+ ttl: int | None = None,
91
91
  ) -> bool:
92
92
  """
93
93
  Set value with optional TTL.
94
-
94
+
95
95
  Args:
96
96
  namespace: Cache namespace
97
97
  context_key: Context identifier
98
98
  key: Cache key
99
99
  data: Value to store
100
100
  ttl: Time to live in seconds
101
-
101
+
102
102
  Returns:
103
103
  True if successful, False otherwise
104
104
  """
105
105
  if namespace not in self._locks:
106
106
  raise ValueError(f"Invalid namespace: {namespace}")
107
-
107
+
108
108
  expires_at = None
109
109
  if ttl:
110
110
  expires_at = datetime.now() + timedelta(seconds=ttl)
111
-
111
+
112
112
  try:
113
113
  async with self._locks[namespace]:
114
114
  store = self._store[namespace]
115
115
  if context_key not in store:
116
116
  store[context_key] = {}
117
117
  store[context_key][key] = (data, expires_at)
118
-
118
+
119
119
  # Start cleanup task if not running
120
120
  self.start_cleanup_task()
121
121
  return True
122
122
  except Exception as e:
123
123
  logger.error(f"Failed to set key '{key}' in {namespace}: {e}")
124
124
  return False
125
-
125
+
126
126
  async def delete(self, namespace: str, context_key: str, key: str) -> bool:
127
127
  """
128
128
  Delete key from store.
129
-
129
+
130
130
  Args:
131
131
  namespace: Cache namespace
132
132
  context_key: Context identifier
133
133
  key: Cache key
134
-
134
+
135
135
  Returns:
136
136
  True if deleted or didn't exist, False on error
137
137
  """
138
138
  if namespace not in self._locks:
139
139
  raise ValueError(f"Invalid namespace: {namespace}")
140
-
140
+
141
141
  try:
142
142
  async with self._locks[namespace]:
143
143
  store = self._store[namespace]
@@ -151,73 +151,75 @@ class MemoryStore:
151
151
  except Exception as e:
152
152
  logger.error(f"Failed to delete key '{key}' from {namespace}: {e}")
153
153
  return False
154
-
154
+
155
155
  async def exists(self, namespace: str, context_key: str, key: str) -> bool:
156
156
  """
157
157
  Check if key exists and is not expired.
158
-
158
+
159
159
  Args:
160
160
  namespace: Cache namespace
161
161
  context_key: Context identifier
162
162
  key: Cache key
163
-
163
+
164
164
  Returns:
165
165
  True if exists and not expired, False otherwise
166
166
  """
167
167
  value = await self.get(namespace, context_key, key)
168
168
  return value is not None
169
-
169
+
170
170
  async def get_ttl(self, namespace: str, context_key: str, key: str) -> int:
171
171
  """
172
172
  Get remaining TTL for key.
173
-
173
+
174
174
  Returns:
175
175
  Remaining TTL in seconds, -1 if no expiry, -2 if doesn't exist
176
176
  """
177
177
  if namespace not in self._locks:
178
178
  return -2
179
-
179
+
180
180
  async with self._locks[namespace]:
181
181
  store = self._store[namespace]
182
182
  context_store = store.get(context_key, {})
183
-
183
+
184
184
  if key not in context_store:
185
185
  return -2 # Doesn't exist
186
-
186
+
187
187
  data, expires_at = context_store[key]
188
-
188
+
189
189
  if expires_at is None:
190
190
  return -1 # No expiry
191
-
191
+
192
192
  now = datetime.now()
193
193
  if now >= expires_at:
194
194
  # Already expired, clean up
195
195
  del context_store[key]
196
196
  return -2
197
-
197
+
198
198
  return int((expires_at - now).total_seconds())
199
-
200
- async def set_ttl(self, namespace: str, context_key: str, key: str, ttl: int) -> bool:
199
+
200
+ async def set_ttl(
201
+ self, namespace: str, context_key: str, key: str, ttl: int
202
+ ) -> bool:
201
203
  """
202
204
  Set TTL for existing key.
203
-
205
+
204
206
  Args:
205
207
  ttl: Time to live in seconds
206
-
208
+
207
209
  Returns:
208
210
  True if successful, False if key doesn't exist or error
209
211
  """
210
212
  if namespace not in self._locks:
211
213
  return False
212
-
214
+
213
215
  try:
214
216
  async with self._locks[namespace]:
215
217
  store = self._store[namespace]
216
218
  context_store = store.get(context_key, {})
217
-
219
+
218
220
  if key not in context_store:
219
221
  return False # Key doesn't exist
220
-
222
+
221
223
  data, _ = context_store[key] # Get existing data, ignore old TTL
222
224
  expires_at = datetime.now() + timedelta(seconds=ttl)
223
225
  context_store[key] = (data, expires_at)
@@ -225,78 +227,80 @@ class MemoryStore:
225
227
  except Exception as e:
226
228
  logger.error(f"Failed to set TTL for key '{key}' in {namespace}: {e}")
227
229
  return False
228
-
229
- async def get_all_keys(self, namespace: str, context_key: str) -> Dict[str, Any]:
230
+
231
+ async def get_all_keys(self, namespace: str, context_key: str) -> dict[str, Any]:
230
232
  """
231
233
  Get all non-expired keys for a context.
232
-
234
+
233
235
  Args:
234
236
  namespace: Cache namespace
235
237
  context_key: Context identifier
236
-
238
+
237
239
  Returns:
238
240
  Dictionary of all non-expired key-value pairs
239
241
  """
240
242
  if namespace not in self._locks:
241
243
  return {}
242
-
244
+
243
245
  async with self._locks[namespace]:
244
246
  store = self._store[namespace]
245
247
  context_store = store.get(context_key, {})
246
-
248
+
247
249
  result = {}
248
250
  now = datetime.now()
249
251
  expired_keys = []
250
-
252
+
251
253
  for key, (data, expires_at) in context_store.items():
252
254
  if expires_at and now > expires_at:
253
255
  expired_keys.append(key)
254
256
  else:
255
257
  result[key] = data
256
-
258
+
257
259
  # Clean up expired keys
258
260
  for key in expired_keys:
259
261
  del context_store[key]
260
-
262
+
261
263
  return result
262
-
264
+
263
265
  async def _cleanup_expired_entries(self):
264
266
  """Background task to clean up expired entries."""
265
267
  while True:
266
268
  try:
267
269
  await asyncio.sleep(self._cleanup_interval)
268
-
270
+
269
271
  now = datetime.now()
270
272
  total_cleaned = 0
271
-
273
+
272
274
  for namespace in ["users", "tables", "states"]:
273
275
  async with self._locks[namespace]:
274
276
  store = self._store[namespace]
275
277
  empty_contexts = []
276
-
278
+
277
279
  for context_key, context_store in store.items():
278
280
  expired_keys = []
279
-
281
+
280
282
  for key, (_, expires_at) in context_store.items():
281
283
  if expires_at and now > expires_at:
282
284
  expired_keys.append(key)
283
-
285
+
284
286
  # Remove expired keys
285
287
  for key in expired_keys:
286
288
  del context_store[key]
287
289
  total_cleaned += 1
288
-
290
+
289
291
  # Mark empty contexts for cleanup
290
292
  if not context_store:
291
293
  empty_contexts.append(context_key)
292
-
294
+
293
295
  # Remove empty contexts
294
296
  for context_key in empty_contexts:
295
297
  del store[context_key]
296
-
298
+
297
299
  if total_cleaned > 0:
298
- logger.debug(f"Cleaned up {total_cleaned} expired entries from memory store")
299
-
300
+ logger.debug(
301
+ f"Cleaned up {total_cleaned} expired entries from memory store"
302
+ )
303
+
300
304
  except asyncio.CancelledError:
301
305
  logger.info("Memory store cleanup task cancelled")
302
306
  break
@@ -306,7 +310,7 @@ class MemoryStore:
306
310
 
307
311
 
308
312
  # Global singleton memory store instance
309
- _global_memory_store: Optional[MemoryStore] = None
313
+ _global_memory_store: MemoryStore | None = None
310
314
 
311
315
 
312
316
  def get_memory_store() -> MemoryStore:
@@ -314,4 +318,4 @@ def get_memory_store() -> MemoryStore:
314
318
  global _global_memory_store
315
319
  if _global_memory_store is None:
316
320
  _global_memory_store = MemoryStore()
317
- return _global_memory_store
321
+ return _global_memory_store