wappa 0.1.0__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 (211) hide show
  1. wappa/__init__.py +85 -0
  2. wappa/api/__init__.py +1 -0
  3. wappa/api/controllers/__init__.py +10 -0
  4. wappa/api/controllers/webhook_controller.py +441 -0
  5. wappa/api/dependencies/__init__.py +15 -0
  6. wappa/api/dependencies/whatsapp_dependencies.py +220 -0
  7. wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
  8. wappa/api/middleware/__init__.py +7 -0
  9. wappa/api/middleware/error_handler.py +158 -0
  10. wappa/api/middleware/owner.py +99 -0
  11. wappa/api/middleware/request_logging.py +184 -0
  12. wappa/api/routes/__init__.py +6 -0
  13. wappa/api/routes/health.py +102 -0
  14. wappa/api/routes/webhooks.py +211 -0
  15. wappa/api/routes/whatsapp/__init__.py +15 -0
  16. wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
  17. wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
  18. wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
  19. wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
  20. wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
  21. wappa/api/routes/whatsapp_combined.py +35 -0
  22. wappa/cli/__init__.py +9 -0
  23. wappa/cli/main.py +199 -0
  24. wappa/core/__init__.py +6 -0
  25. wappa/core/config/__init__.py +5 -0
  26. wappa/core/config/settings.py +161 -0
  27. wappa/core/events/__init__.py +41 -0
  28. wappa/core/events/default_handlers.py +642 -0
  29. wappa/core/events/event_dispatcher.py +244 -0
  30. wappa/core/events/event_handler.py +247 -0
  31. wappa/core/events/webhook_factory.py +219 -0
  32. wappa/core/factory/__init__.py +15 -0
  33. wappa/core/factory/plugin.py +68 -0
  34. wappa/core/factory/wappa_builder.py +326 -0
  35. wappa/core/logging/__init__.py +5 -0
  36. wappa/core/logging/context.py +100 -0
  37. wappa/core/logging/logger.py +343 -0
  38. wappa/core/plugins/__init__.py +34 -0
  39. wappa/core/plugins/auth_plugin.py +169 -0
  40. wappa/core/plugins/cors_plugin.py +128 -0
  41. wappa/core/plugins/custom_middleware_plugin.py +182 -0
  42. wappa/core/plugins/database_plugin.py +235 -0
  43. wappa/core/plugins/rate_limit_plugin.py +183 -0
  44. wappa/core/plugins/redis_plugin.py +224 -0
  45. wappa/core/plugins/wappa_core_plugin.py +261 -0
  46. wappa/core/plugins/webhook_plugin.py +253 -0
  47. wappa/core/types.py +108 -0
  48. wappa/core/wappa_app.py +546 -0
  49. wappa/database/__init__.py +18 -0
  50. wappa/database/adapter.py +107 -0
  51. wappa/database/adapters/__init__.py +17 -0
  52. wappa/database/adapters/mysql_adapter.py +187 -0
  53. wappa/database/adapters/postgresql_adapter.py +169 -0
  54. wappa/database/adapters/sqlite_adapter.py +174 -0
  55. wappa/domain/__init__.py +28 -0
  56. wappa/domain/builders/__init__.py +5 -0
  57. wappa/domain/builders/message_builder.py +189 -0
  58. wappa/domain/entities/__init__.py +5 -0
  59. wappa/domain/enums/messenger_platform.py +123 -0
  60. wappa/domain/factories/__init__.py +6 -0
  61. wappa/domain/factories/media_factory.py +450 -0
  62. wappa/domain/factories/message_factory.py +497 -0
  63. wappa/domain/factories/messenger_factory.py +244 -0
  64. wappa/domain/interfaces/__init__.py +32 -0
  65. wappa/domain/interfaces/base_repository.py +94 -0
  66. wappa/domain/interfaces/cache_factory.py +85 -0
  67. wappa/domain/interfaces/cache_interface.py +199 -0
  68. wappa/domain/interfaces/expiry_repository.py +68 -0
  69. wappa/domain/interfaces/media_interface.py +311 -0
  70. wappa/domain/interfaces/messaging_interface.py +523 -0
  71. wappa/domain/interfaces/pubsub_repository.py +151 -0
  72. wappa/domain/interfaces/repository_factory.py +108 -0
  73. wappa/domain/interfaces/shared_state_repository.py +122 -0
  74. wappa/domain/interfaces/state_repository.py +123 -0
  75. wappa/domain/interfaces/tables_repository.py +215 -0
  76. wappa/domain/interfaces/user_repository.py +114 -0
  77. wappa/domain/interfaces/webhooks/__init__.py +1 -0
  78. wappa/domain/models/media_result.py +110 -0
  79. wappa/domain/models/platforms/__init__.py +15 -0
  80. wappa/domain/models/platforms/platform_config.py +104 -0
  81. wappa/domain/services/__init__.py +11 -0
  82. wappa/domain/services/tenant_credentials_service.py +56 -0
  83. wappa/messaging/__init__.py +7 -0
  84. wappa/messaging/whatsapp/__init__.py +1 -0
  85. wappa/messaging/whatsapp/client/__init__.py +5 -0
  86. wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
  87. wappa/messaging/whatsapp/handlers/__init__.py +13 -0
  88. wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
  89. wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
  90. wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
  91. wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
  92. wappa/messaging/whatsapp/messenger/__init__.py +5 -0
  93. wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
  94. wappa/messaging/whatsapp/models/__init__.py +61 -0
  95. wappa/messaging/whatsapp/models/basic_models.py +65 -0
  96. wappa/messaging/whatsapp/models/interactive_models.py +287 -0
  97. wappa/messaging/whatsapp/models/media_models.py +215 -0
  98. wappa/messaging/whatsapp/models/specialized_models.py +304 -0
  99. wappa/messaging/whatsapp/models/template_models.py +261 -0
  100. wappa/persistence/cache_factory.py +93 -0
  101. wappa/persistence/json/__init__.py +14 -0
  102. wappa/persistence/json/cache_adapters.py +271 -0
  103. wappa/persistence/json/handlers/__init__.py +1 -0
  104. wappa/persistence/json/handlers/state_handler.py +250 -0
  105. wappa/persistence/json/handlers/table_handler.py +263 -0
  106. wappa/persistence/json/handlers/user_handler.py +213 -0
  107. wappa/persistence/json/handlers/utils/__init__.py +1 -0
  108. wappa/persistence/json/handlers/utils/file_manager.py +153 -0
  109. wappa/persistence/json/handlers/utils/key_factory.py +11 -0
  110. wappa/persistence/json/handlers/utils/serialization.py +121 -0
  111. wappa/persistence/json/json_cache_factory.py +76 -0
  112. wappa/persistence/json/storage_manager.py +285 -0
  113. wappa/persistence/memory/__init__.py +14 -0
  114. wappa/persistence/memory/cache_adapters.py +271 -0
  115. wappa/persistence/memory/handlers/__init__.py +1 -0
  116. wappa/persistence/memory/handlers/state_handler.py +250 -0
  117. wappa/persistence/memory/handlers/table_handler.py +280 -0
  118. wappa/persistence/memory/handlers/user_handler.py +213 -0
  119. wappa/persistence/memory/handlers/utils/__init__.py +1 -0
  120. wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
  121. wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
  122. wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
  123. wappa/persistence/memory/memory_cache_factory.py +76 -0
  124. wappa/persistence/memory/storage_manager.py +235 -0
  125. wappa/persistence/redis/README.md +699 -0
  126. wappa/persistence/redis/__init__.py +11 -0
  127. wappa/persistence/redis/cache_adapters.py +285 -0
  128. wappa/persistence/redis/ops.py +880 -0
  129. wappa/persistence/redis/redis_cache_factory.py +71 -0
  130. wappa/persistence/redis/redis_client.py +231 -0
  131. wappa/persistence/redis/redis_handler/__init__.py +26 -0
  132. wappa/persistence/redis/redis_handler/state_handler.py +176 -0
  133. wappa/persistence/redis/redis_handler/table.py +158 -0
  134. wappa/persistence/redis/redis_handler/user.py +138 -0
  135. wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
  136. wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
  137. wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
  138. wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
  139. wappa/persistence/redis/redis_manager.py +189 -0
  140. wappa/processors/__init__.py +6 -0
  141. wappa/processors/base_processor.py +262 -0
  142. wappa/processors/factory.py +550 -0
  143. wappa/processors/whatsapp_processor.py +810 -0
  144. wappa/schemas/__init__.py +6 -0
  145. wappa/schemas/core/__init__.py +71 -0
  146. wappa/schemas/core/base_message.py +499 -0
  147. wappa/schemas/core/base_status.py +322 -0
  148. wappa/schemas/core/base_webhook.py +312 -0
  149. wappa/schemas/core/types.py +253 -0
  150. wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
  151. wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
  152. wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
  153. wappa/schemas/factory.py +754 -0
  154. wappa/schemas/webhooks/__init__.py +3 -0
  155. wappa/schemas/whatsapp/__init__.py +6 -0
  156. wappa/schemas/whatsapp/base_models.py +285 -0
  157. wappa/schemas/whatsapp/message_types/__init__.py +93 -0
  158. wappa/schemas/whatsapp/message_types/audio.py +350 -0
  159. wappa/schemas/whatsapp/message_types/button.py +267 -0
  160. wappa/schemas/whatsapp/message_types/contact.py +464 -0
  161. wappa/schemas/whatsapp/message_types/document.py +421 -0
  162. wappa/schemas/whatsapp/message_types/errors.py +195 -0
  163. wappa/schemas/whatsapp/message_types/image.py +424 -0
  164. wappa/schemas/whatsapp/message_types/interactive.py +430 -0
  165. wappa/schemas/whatsapp/message_types/location.py +416 -0
  166. wappa/schemas/whatsapp/message_types/order.py +372 -0
  167. wappa/schemas/whatsapp/message_types/reaction.py +271 -0
  168. wappa/schemas/whatsapp/message_types/sticker.py +328 -0
  169. wappa/schemas/whatsapp/message_types/system.py +317 -0
  170. wappa/schemas/whatsapp/message_types/text.py +411 -0
  171. wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
  172. wappa/schemas/whatsapp/message_types/video.py +344 -0
  173. wappa/schemas/whatsapp/status_models.py +479 -0
  174. wappa/schemas/whatsapp/validators.py +454 -0
  175. wappa/schemas/whatsapp/webhook_container.py +438 -0
  176. wappa/webhooks/__init__.py +17 -0
  177. wappa/webhooks/core/__init__.py +71 -0
  178. wappa/webhooks/core/base_message.py +499 -0
  179. wappa/webhooks/core/base_status.py +322 -0
  180. wappa/webhooks/core/base_webhook.py +312 -0
  181. wappa/webhooks/core/types.py +253 -0
  182. wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
  183. wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
  184. wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
  185. wappa/webhooks/factory.py +754 -0
  186. wappa/webhooks/whatsapp/__init__.py +6 -0
  187. wappa/webhooks/whatsapp/base_models.py +285 -0
  188. wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
  189. wappa/webhooks/whatsapp/message_types/audio.py +350 -0
  190. wappa/webhooks/whatsapp/message_types/button.py +267 -0
  191. wappa/webhooks/whatsapp/message_types/contact.py +464 -0
  192. wappa/webhooks/whatsapp/message_types/document.py +421 -0
  193. wappa/webhooks/whatsapp/message_types/errors.py +195 -0
  194. wappa/webhooks/whatsapp/message_types/image.py +424 -0
  195. wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
  196. wappa/webhooks/whatsapp/message_types/location.py +416 -0
  197. wappa/webhooks/whatsapp/message_types/order.py +372 -0
  198. wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
  199. wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
  200. wappa/webhooks/whatsapp/message_types/system.py +317 -0
  201. wappa/webhooks/whatsapp/message_types/text.py +411 -0
  202. wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
  203. wappa/webhooks/whatsapp/message_types/video.py +344 -0
  204. wappa/webhooks/whatsapp/status_models.py +479 -0
  205. wappa/webhooks/whatsapp/validators.py +454 -0
  206. wappa/webhooks/whatsapp/webhook_container.py +438 -0
  207. wappa-0.1.0.dist-info/METADATA +269 -0
  208. wappa-0.1.0.dist-info/RECORD +211 -0
  209. wappa-0.1.0.dist-info/WHEEL +4 -0
  210. wappa-0.1.0.dist-info/entry_points.txt +2 -0
  211. wappa-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,11 @@
1
+ """
2
+ Redis Module
3
+
4
+ Provides Redis client functionality, operations, and lifecycle management.
5
+ """
6
+
7
+ from . import ops, redis_handler
8
+ from .redis_client import RedisClient
9
+ from .redis_manager import RedisManager
10
+
11
+ __all__ = ["RedisClient", "RedisManager", "ops", "redis_handler"]
@@ -0,0 +1,285 @@
1
+ """
2
+ Cache adapters that make Redis handlers implement the ICache interface.
3
+
4
+ These adapters wrap the existing Redis handlers to provide a uniform ICache interface
5
+ while preserving all the existing functionality.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from pydantic import BaseModel
11
+
12
+ from ...domain.interfaces.cache_interface import ICache
13
+ from .redis_handler.state_handler import RedisStateHandler
14
+ from .redis_handler.table import RedisTable
15
+ from .redis_handler.user import RedisUser
16
+
17
+
18
+ class RedisStateCacheAdapter(ICache):
19
+ """Adapter that makes RedisStateHandler implement ICache interface."""
20
+
21
+ def __init__(
22
+ self, tenant_id: str, user_id: str, redis_alias: str = "state_handler"
23
+ ):
24
+ self._handler = RedisStateHandler(
25
+ tenant=tenant_id, user_id=user_id, redis_alias=redis_alias
26
+ )
27
+ self._default_handler_name = "cache"
28
+
29
+ async def get(
30
+ self, key: str, models: type[BaseModel] | None = None
31
+ ) -> dict[str, Any] | None:
32
+ """Get cached data by key."""
33
+ return await self._handler.get(key, models=models)
34
+
35
+ async def set(self, key: str, data: dict[str, Any], ttl: int | None = None) -> bool:
36
+ """Set cached data with optional TTL."""
37
+ return await self._handler.upsert(key, data, ttl=ttl)
38
+
39
+ async def delete(self, key: str) -> bool:
40
+ """Delete cached data by key."""
41
+ result = await self._handler.delete(key)
42
+ return result > 0
43
+
44
+ async def exists(self, key: str) -> bool:
45
+ """Check if key exists in cache."""
46
+ return await self._handler.exists(key)
47
+
48
+ async def get_field(self, key: str, field: str) -> Any | None:
49
+ """Get a specific field from cached hash data."""
50
+ return await self._handler.get_field(key, field)
51
+
52
+ async def set_field(
53
+ self, key: str, field: str, value: Any, ttl: int | None = None
54
+ ) -> bool:
55
+ """Set a specific field in cached hash data."""
56
+ return await self._handler.update_field(key, field, value, ttl=ttl)
57
+
58
+ async def increment_field(
59
+ self, key: str, field: str, increment: int = 1, ttl: int | None = None
60
+ ) -> int | None:
61
+ """Atomically increment an integer field."""
62
+ return await self._handler.increment_field(key, field, increment, ttl=ttl)
63
+
64
+ async def append_to_list(
65
+ self, key: str, field: str, value: Any, ttl: int | None = None
66
+ ) -> bool:
67
+ """Append value to a list field."""
68
+ return await self._handler.append_to_list(key, field, value, ttl=ttl)
69
+
70
+ async def get_ttl(self, key: str) -> int:
71
+ """Get remaining time to live for a key."""
72
+ handler_key = self._handler._key(key)
73
+ return await self._handler.get_ttl(handler_key)
74
+
75
+ async def set_ttl(self, key: str, ttl: int) -> bool:
76
+ """Set time to live for a key."""
77
+ handler_key = self._handler._key(key)
78
+ return await self._handler.renew_ttl(handler_key, ttl=ttl)
79
+
80
+
81
+ class RedisUserCacheAdapter(ICache):
82
+ """Adapter that makes RedisUser implement ICache interface."""
83
+
84
+ def __init__(self, tenant_id: str, user_id: str, redis_alias: str = "users"):
85
+ self._handler = RedisUser(
86
+ tenant=tenant_id, user_id=user_id, redis_alias=redis_alias
87
+ )
88
+
89
+ async def get(
90
+ self, key: str, models: type[BaseModel] | None = None
91
+ ) -> dict[str, Any] | None:
92
+ """Get cached data by key. For user cache, key is ignored as it uses user_id."""
93
+ return await self._handler.get(models=models)
94
+
95
+ async def set(self, key: str, data: dict[str, Any], ttl: int | None = None) -> bool:
96
+ """Set cached data with optional TTL."""
97
+ return await self._handler.upsert(data, ttl=ttl)
98
+
99
+ async def delete(self, key: str) -> bool:
100
+ """Delete cached data by key."""
101
+ result = await self._handler.delete()
102
+ return result > 0
103
+
104
+ async def exists(self, key: str) -> bool:
105
+ """Check if key exists in cache."""
106
+ return await self._handler.exists()
107
+
108
+ async def get_field(self, key: str, field: str) -> Any | None:
109
+ """Get a specific field from cached hash data."""
110
+ return await self._handler.get_field(field)
111
+
112
+ async def set_field(
113
+ self, key: str, field: str, value: Any, ttl: int | None = None
114
+ ) -> bool:
115
+ """Set a specific field in cached hash data."""
116
+ return await self._handler.update_field(field, value, ttl=ttl)
117
+
118
+ async def increment_field(
119
+ self, key: str, field: str, increment: int = 1, ttl: int | None = None
120
+ ) -> int | None:
121
+ """Atomically increment an integer field."""
122
+ return await self._handler.increment_field(field, increment, ttl=ttl)
123
+
124
+ async def append_to_list(
125
+ self, key: str, field: str, value: Any, ttl: int | None = None
126
+ ) -> bool:
127
+ """Append value to a list field."""
128
+ return await self._handler.append_to_list(field, value, ttl=ttl)
129
+
130
+ async def get_ttl(self, key: str) -> int:
131
+ """Get remaining time to live for a key."""
132
+ handler_key = self._handler._key()
133
+ return await self._handler.get_ttl(handler_key)
134
+
135
+ async def set_ttl(self, key: str, ttl: int) -> bool:
136
+ """Set time to live for a key."""
137
+ handler_key = self._handler._key()
138
+ return await self._handler.renew_ttl(handler_key, ttl=ttl)
139
+
140
+
141
+ class RedisTableCacheAdapter(ICache):
142
+ """
143
+ Adapter that makes RedisTable implement ICache interface.
144
+
145
+ Table Key Format Guide:
146
+ Use create_table_key(table_name, pkid) to generate proper keys.
147
+
148
+ Examples:
149
+ # Good - using helper method
150
+ key = cache.create_table_key("user_profiles", "12345")
151
+ await cache.set(key, user_data)
152
+
153
+ # Also supported - manual format
154
+ key = "user_profiles:12345"
155
+ await cache.set(key, user_data)
156
+ """
157
+
158
+ def __init__(self, tenant_id: str, redis_alias: str = "table"):
159
+ self._handler = RedisTable(tenant=tenant_id, redis_alias=redis_alias)
160
+
161
+ def create_table_key(self, table_name: str, pkid: str) -> str:
162
+ """
163
+ Create a properly formatted table cache key.
164
+
165
+ Args:
166
+ table_name: Name of the table (e.g., "user_profiles", "message_logs")
167
+ pkid: Primary key ID (e.g., user_id, message_id)
168
+
169
+ Returns:
170
+ Formatted key string for use with cache methods
171
+
172
+ Example:
173
+ key = cache.create_table_key("user_profiles", "12345")
174
+ # Returns: "user_profiles:12345"
175
+ """
176
+ if not table_name or not pkid:
177
+ raise ValueError("Both table_name and pkid must be provided and non-empty")
178
+
179
+ # Sanitize inputs to avoid conflicts
180
+ safe_table_name = str(table_name).replace(":", "_")
181
+ safe_pkid = str(pkid).replace(":", "_")
182
+
183
+ return f"{safe_table_name}:{safe_pkid}"
184
+
185
+ async def get(
186
+ self, key: str, models: type[BaseModel] | None = None
187
+ ) -> dict[str, Any] | None:
188
+ """Get cached data by key. Key should be in format 'table_name:pkid'."""
189
+ table_name, pkid = self._parse_key(key)
190
+ return await self._handler.get(table_name, pkid, models=models)
191
+
192
+ async def set(self, key: str, data: dict[str, Any], ttl: int | None = None) -> bool:
193
+ """Set cached data with optional TTL."""
194
+ table_name, pkid = self._parse_key(key)
195
+ return await self._handler.upsert(table_name, pkid, data, ttl=ttl)
196
+
197
+ async def delete(self, key: str) -> bool:
198
+ """Delete cached data by key."""
199
+ table_name, pkid = self._parse_key(key)
200
+ result = await self._handler.delete(table_name, pkid)
201
+ return result > 0
202
+
203
+ async def exists(self, key: str) -> bool:
204
+ """Check if key exists in cache."""
205
+ table_name, pkid = self._parse_key(key)
206
+ return await self._handler.exists(table_name, pkid)
207
+
208
+ async def get_field(self, key: str, field: str) -> Any | None:
209
+ """Get a specific field from cached hash data."""
210
+ table_name, pkid = self._parse_key(key)
211
+ return await self._handler.get_field(table_name, pkid, field)
212
+
213
+ async def set_field(
214
+ self, key: str, field: str, value: Any, ttl: int | None = None
215
+ ) -> bool:
216
+ """Set a specific field in cached hash data."""
217
+ table_name, pkid = self._parse_key(key)
218
+ return await self._handler.update_field(table_name, pkid, field, value, ttl=ttl)
219
+
220
+ async def increment_field(
221
+ self, key: str, field: str, increment: int = 1, ttl: int | None = None
222
+ ) -> int | None:
223
+ """Atomically increment an integer field."""
224
+ table_name, pkid = self._parse_key(key)
225
+ return await self._handler.increment_field(
226
+ table_name, pkid, field, increment, ttl=ttl
227
+ )
228
+
229
+ async def append_to_list(
230
+ self, key: str, field: str, value: Any, ttl: int | None = None
231
+ ) -> bool:
232
+ """Append value to a list field."""
233
+ table_name, pkid = self._parse_key(key)
234
+ return await self._handler.append_to_list(
235
+ table_name, pkid, field, value, ttl=ttl
236
+ )
237
+
238
+ async def get_ttl(self, key: str) -> int:
239
+ """Get remaining time to live for a key."""
240
+ table_name, pkid = self._parse_key(key)
241
+ handler_key = self._handler._key(table_name, pkid)
242
+ return await self._handler.get_ttl(handler_key)
243
+
244
+ async def set_ttl(self, key: str, ttl: int) -> bool:
245
+ """Set time to live for a key."""
246
+ table_name, pkid = self._parse_key(key)
247
+ handler_key = self._handler._key(table_name, pkid)
248
+ return await self._handler.renew_ttl(handler_key, ttl=ttl)
249
+
250
+ def _parse_key(self, key: str) -> tuple[str, str]:
251
+ """
252
+ Parse key into table_name and pkid with validation.
253
+
254
+ Args:
255
+ key: Cache key in format "table_name:pkid"
256
+
257
+ Returns:
258
+ Tuple of (table_name, pkid)
259
+
260
+ Raises:
261
+ ValueError: If key format is invalid
262
+ """
263
+ if not key:
264
+ raise ValueError("Key cannot be empty")
265
+
266
+ if ":" not in key:
267
+ raise ValueError(
268
+ f"Invalid table cache key format: '{key}'. "
269
+ f"Expected format: 'table_name:pkid'. "
270
+ f"Use create_table_key(table_name, pkid) to generate proper keys."
271
+ )
272
+
273
+ parts = key.split(":", 1)
274
+ if len(parts) != 2:
275
+ raise ValueError(f"Invalid table cache key format: '{key}'. Expected exactly one ':' separator.")
276
+
277
+ table_name, pkid = parts
278
+
279
+ if not table_name.strip():
280
+ raise ValueError(f"Invalid table cache key: '{key}'. Table name cannot be empty.")
281
+
282
+ if not pkid.strip():
283
+ raise ValueError(f"Invalid table cache key: '{key}'. Primary key ID cannot be empty.")
284
+
285
+ return table_name.strip(), pkid.strip()