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.
- wappa/__init__.py +85 -0
- wappa/api/__init__.py +1 -0
- wappa/api/controllers/__init__.py +10 -0
- wappa/api/controllers/webhook_controller.py +441 -0
- wappa/api/dependencies/__init__.py +15 -0
- wappa/api/dependencies/whatsapp_dependencies.py +220 -0
- wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
- wappa/api/middleware/__init__.py +7 -0
- wappa/api/middleware/error_handler.py +158 -0
- wappa/api/middleware/owner.py +99 -0
- wappa/api/middleware/request_logging.py +184 -0
- wappa/api/routes/__init__.py +6 -0
- wappa/api/routes/health.py +102 -0
- wappa/api/routes/webhooks.py +211 -0
- wappa/api/routes/whatsapp/__init__.py +15 -0
- wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
- wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
- wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
- wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
- wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
- wappa/api/routes/whatsapp_combined.py +35 -0
- wappa/cli/__init__.py +9 -0
- wappa/cli/main.py +199 -0
- wappa/core/__init__.py +6 -0
- wappa/core/config/__init__.py +5 -0
- wappa/core/config/settings.py +161 -0
- wappa/core/events/__init__.py +41 -0
- wappa/core/events/default_handlers.py +642 -0
- wappa/core/events/event_dispatcher.py +244 -0
- wappa/core/events/event_handler.py +247 -0
- wappa/core/events/webhook_factory.py +219 -0
- wappa/core/factory/__init__.py +15 -0
- wappa/core/factory/plugin.py +68 -0
- wappa/core/factory/wappa_builder.py +326 -0
- wappa/core/logging/__init__.py +5 -0
- wappa/core/logging/context.py +100 -0
- wappa/core/logging/logger.py +343 -0
- wappa/core/plugins/__init__.py +34 -0
- wappa/core/plugins/auth_plugin.py +169 -0
- wappa/core/plugins/cors_plugin.py +128 -0
- wappa/core/plugins/custom_middleware_plugin.py +182 -0
- wappa/core/plugins/database_plugin.py +235 -0
- wappa/core/plugins/rate_limit_plugin.py +183 -0
- wappa/core/plugins/redis_plugin.py +224 -0
- wappa/core/plugins/wappa_core_plugin.py +261 -0
- wappa/core/plugins/webhook_plugin.py +253 -0
- wappa/core/types.py +108 -0
- wappa/core/wappa_app.py +546 -0
- wappa/database/__init__.py +18 -0
- wappa/database/adapter.py +107 -0
- wappa/database/adapters/__init__.py +17 -0
- wappa/database/adapters/mysql_adapter.py +187 -0
- wappa/database/adapters/postgresql_adapter.py +169 -0
- wappa/database/adapters/sqlite_adapter.py +174 -0
- wappa/domain/__init__.py +28 -0
- wappa/domain/builders/__init__.py +5 -0
- wappa/domain/builders/message_builder.py +189 -0
- wappa/domain/entities/__init__.py +5 -0
- wappa/domain/enums/messenger_platform.py +123 -0
- wappa/domain/factories/__init__.py +6 -0
- wappa/domain/factories/media_factory.py +450 -0
- wappa/domain/factories/message_factory.py +497 -0
- wappa/domain/factories/messenger_factory.py +244 -0
- wappa/domain/interfaces/__init__.py +32 -0
- wappa/domain/interfaces/base_repository.py +94 -0
- wappa/domain/interfaces/cache_factory.py +85 -0
- wappa/domain/interfaces/cache_interface.py +199 -0
- wappa/domain/interfaces/expiry_repository.py +68 -0
- wappa/domain/interfaces/media_interface.py +311 -0
- wappa/domain/interfaces/messaging_interface.py +523 -0
- wappa/domain/interfaces/pubsub_repository.py +151 -0
- wappa/domain/interfaces/repository_factory.py +108 -0
- wappa/domain/interfaces/shared_state_repository.py +122 -0
- wappa/domain/interfaces/state_repository.py +123 -0
- wappa/domain/interfaces/tables_repository.py +215 -0
- wappa/domain/interfaces/user_repository.py +114 -0
- wappa/domain/interfaces/webhooks/__init__.py +1 -0
- wappa/domain/models/media_result.py +110 -0
- wappa/domain/models/platforms/__init__.py +15 -0
- wappa/domain/models/platforms/platform_config.py +104 -0
- wappa/domain/services/__init__.py +11 -0
- wappa/domain/services/tenant_credentials_service.py +56 -0
- wappa/messaging/__init__.py +7 -0
- wappa/messaging/whatsapp/__init__.py +1 -0
- wappa/messaging/whatsapp/client/__init__.py +5 -0
- wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
- wappa/messaging/whatsapp/handlers/__init__.py +13 -0
- wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
- wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
- wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
- wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
- wappa/messaging/whatsapp/messenger/__init__.py +5 -0
- wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
- wappa/messaging/whatsapp/models/__init__.py +61 -0
- wappa/messaging/whatsapp/models/basic_models.py +65 -0
- wappa/messaging/whatsapp/models/interactive_models.py +287 -0
- wappa/messaging/whatsapp/models/media_models.py +215 -0
- wappa/messaging/whatsapp/models/specialized_models.py +304 -0
- wappa/messaging/whatsapp/models/template_models.py +261 -0
- wappa/persistence/cache_factory.py +93 -0
- wappa/persistence/json/__init__.py +14 -0
- wappa/persistence/json/cache_adapters.py +271 -0
- wappa/persistence/json/handlers/__init__.py +1 -0
- wappa/persistence/json/handlers/state_handler.py +250 -0
- wappa/persistence/json/handlers/table_handler.py +263 -0
- wappa/persistence/json/handlers/user_handler.py +213 -0
- wappa/persistence/json/handlers/utils/__init__.py +1 -0
- wappa/persistence/json/handlers/utils/file_manager.py +153 -0
- wappa/persistence/json/handlers/utils/key_factory.py +11 -0
- wappa/persistence/json/handlers/utils/serialization.py +121 -0
- wappa/persistence/json/json_cache_factory.py +76 -0
- wappa/persistence/json/storage_manager.py +285 -0
- wappa/persistence/memory/__init__.py +14 -0
- wappa/persistence/memory/cache_adapters.py +271 -0
- wappa/persistence/memory/handlers/__init__.py +1 -0
- wappa/persistence/memory/handlers/state_handler.py +250 -0
- wappa/persistence/memory/handlers/table_handler.py +280 -0
- wappa/persistence/memory/handlers/user_handler.py +213 -0
- wappa/persistence/memory/handlers/utils/__init__.py +1 -0
- wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
- wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
- wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
- wappa/persistence/memory/memory_cache_factory.py +76 -0
- wappa/persistence/memory/storage_manager.py +235 -0
- wappa/persistence/redis/README.md +699 -0
- wappa/persistence/redis/__init__.py +11 -0
- wappa/persistence/redis/cache_adapters.py +285 -0
- wappa/persistence/redis/ops.py +880 -0
- wappa/persistence/redis/redis_cache_factory.py +71 -0
- wappa/persistence/redis/redis_client.py +231 -0
- wappa/persistence/redis/redis_handler/__init__.py +26 -0
- wappa/persistence/redis/redis_handler/state_handler.py +176 -0
- wappa/persistence/redis/redis_handler/table.py +158 -0
- wappa/persistence/redis/redis_handler/user.py +138 -0
- wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
- wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
- wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
- wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
- wappa/persistence/redis/redis_manager.py +189 -0
- wappa/processors/__init__.py +6 -0
- wappa/processors/base_processor.py +262 -0
- wappa/processors/factory.py +550 -0
- wappa/processors/whatsapp_processor.py +810 -0
- wappa/schemas/__init__.py +6 -0
- wappa/schemas/core/__init__.py +71 -0
- wappa/schemas/core/base_message.py +499 -0
- wappa/schemas/core/base_status.py +322 -0
- wappa/schemas/core/base_webhook.py +312 -0
- wappa/schemas/core/types.py +253 -0
- wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
- wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
- wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
- wappa/schemas/factory.py +754 -0
- wappa/schemas/webhooks/__init__.py +3 -0
- wappa/schemas/whatsapp/__init__.py +6 -0
- wappa/schemas/whatsapp/base_models.py +285 -0
- wappa/schemas/whatsapp/message_types/__init__.py +93 -0
- wappa/schemas/whatsapp/message_types/audio.py +350 -0
- wappa/schemas/whatsapp/message_types/button.py +267 -0
- wappa/schemas/whatsapp/message_types/contact.py +464 -0
- wappa/schemas/whatsapp/message_types/document.py +421 -0
- wappa/schemas/whatsapp/message_types/errors.py +195 -0
- wappa/schemas/whatsapp/message_types/image.py +424 -0
- wappa/schemas/whatsapp/message_types/interactive.py +430 -0
- wappa/schemas/whatsapp/message_types/location.py +416 -0
- wappa/schemas/whatsapp/message_types/order.py +372 -0
- wappa/schemas/whatsapp/message_types/reaction.py +271 -0
- wappa/schemas/whatsapp/message_types/sticker.py +328 -0
- wappa/schemas/whatsapp/message_types/system.py +317 -0
- wappa/schemas/whatsapp/message_types/text.py +411 -0
- wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
- wappa/schemas/whatsapp/message_types/video.py +344 -0
- wappa/schemas/whatsapp/status_models.py +479 -0
- wappa/schemas/whatsapp/validators.py +454 -0
- wappa/schemas/whatsapp/webhook_container.py +438 -0
- wappa/webhooks/__init__.py +17 -0
- wappa/webhooks/core/__init__.py +71 -0
- wappa/webhooks/core/base_message.py +499 -0
- wappa/webhooks/core/base_status.py +322 -0
- wappa/webhooks/core/base_webhook.py +312 -0
- wappa/webhooks/core/types.py +253 -0
- wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
- wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
- wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
- wappa/webhooks/factory.py +754 -0
- wappa/webhooks/whatsapp/__init__.py +6 -0
- wappa/webhooks/whatsapp/base_models.py +285 -0
- wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
- wappa/webhooks/whatsapp/message_types/audio.py +350 -0
- wappa/webhooks/whatsapp/message_types/button.py +267 -0
- wappa/webhooks/whatsapp/message_types/contact.py +464 -0
- wappa/webhooks/whatsapp/message_types/document.py +421 -0
- wappa/webhooks/whatsapp/message_types/errors.py +195 -0
- wappa/webhooks/whatsapp/message_types/image.py +424 -0
- wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
- wappa/webhooks/whatsapp/message_types/location.py +416 -0
- wappa/webhooks/whatsapp/message_types/order.py +372 -0
- wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
- wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
- wappa/webhooks/whatsapp/message_types/system.py +317 -0
- wappa/webhooks/whatsapp/message_types/text.py +411 -0
- wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
- wappa/webhooks/whatsapp/message_types/video.py +344 -0
- wappa/webhooks/whatsapp/status_models.py +479 -0
- wappa/webhooks/whatsapp/validators.py +454 -0
- wappa/webhooks/whatsapp/webhook_container.py +438 -0
- wappa-0.1.0.dist-info/METADATA +269 -0
- wappa-0.1.0.dist-info/RECORD +211 -0
- wappa-0.1.0.dist-info/WHEEL +4 -0
- wappa-0.1.0.dist-info/entry_points.txt +2 -0
- wappa-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
# mimeiapify/symphony_ai/redis/ops.py
|
|
2
|
+
|
|
3
|
+
import builtins
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
|
|
7
|
+
from .redis_client import PoolAlias, RedisClient
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(
|
|
10
|
+
"RedisCoreMethods"
|
|
11
|
+
) # Use __name__ for standard logging practice
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# =========================================================================
|
|
15
|
+
# SECTION: Basic Key-Value Operations
|
|
16
|
+
# =========================================================================
|
|
17
|
+
async def set(
|
|
18
|
+
key: str, value: str, ex: int | None = None, *, alias: PoolAlias = "default"
|
|
19
|
+
) -> bool:
|
|
20
|
+
"""
|
|
21
|
+
Sets the string value of a key, with optional expiration.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
key: The full Redis key.
|
|
25
|
+
value: The string value to store.
|
|
26
|
+
ex: Optional expiration time in seconds.
|
|
27
|
+
alias: Redis pool alias to use (default: "default").
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
True if the operation was successful, False otherwise.
|
|
31
|
+
"""
|
|
32
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
33
|
+
try:
|
|
34
|
+
# Ensure value is a type redis-py can handle directly (str, bytes, int, float)
|
|
35
|
+
# Assuming decode_responses=True, strings are preferred.
|
|
36
|
+
if not isinstance(value, (str, bytes, int, float)):
|
|
37
|
+
# Log a warning if a complex type is passed unexpectedly
|
|
38
|
+
logger.warning(
|
|
39
|
+
f"RedisCoreMethods.set received non-primitive type for key '{key}'. Attempting str conversion. Type: {type(value)}"
|
|
40
|
+
)
|
|
41
|
+
value = str(value)
|
|
42
|
+
return await redis.set(key, value, ex=ex)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.error(f"Redis SET error for key '{key}': {e}", exc_info=True)
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def setex(
|
|
49
|
+
key: str, seconds: int, value: str, *, alias: PoolAlias = "default"
|
|
50
|
+
) -> bool:
|
|
51
|
+
"""
|
|
52
|
+
Set key to hold string value and set key to timeout after given number of seconds.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
key: The full Redis key.
|
|
56
|
+
seconds: Expiration time in seconds.
|
|
57
|
+
value: The string value to store.
|
|
58
|
+
alias: Redis pool alias to use (default: "default").
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
True if the operation was successful, False otherwise.
|
|
62
|
+
"""
|
|
63
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
64
|
+
try:
|
|
65
|
+
if not isinstance(value, (str, bytes, int, float)):
|
|
66
|
+
logger.warning(
|
|
67
|
+
f"RedisCoreMethods.setex received non-primitive type for key '{key}'. Attempting str conversion. Type: {type(value)}"
|
|
68
|
+
)
|
|
69
|
+
value = str(value)
|
|
70
|
+
return await redis.setex(key, seconds, value)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error(f"Redis SETEX error for key '{key}': {e}", exc_info=True)
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def exists(*keys: str, alias: PoolAlias = "default") -> int:
|
|
77
|
+
"""
|
|
78
|
+
Returns the number of keys that exist from the list of keys.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
*keys: One or more full Redis keys.
|
|
82
|
+
alias: Redis pool alias to use (default: "default").
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Integer reply: The number of keys that exist.
|
|
86
|
+
"""
|
|
87
|
+
if not keys:
|
|
88
|
+
return 0
|
|
89
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
90
|
+
try:
|
|
91
|
+
return await redis.exists(*keys)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error(
|
|
94
|
+
f"Redis EXISTS error for keys starting with '{keys[0]}...': {e}",
|
|
95
|
+
exc_info=True,
|
|
96
|
+
)
|
|
97
|
+
return 0 # Return 0 on error
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def get(key: str, *, alias: PoolAlias = "default") -> str | None:
|
|
101
|
+
"""
|
|
102
|
+
Retrieve the string value of a key.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
key: The full Redis key.
|
|
106
|
+
alias: Redis pool alias to use (default: "default").
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The string value if the key exists, otherwise None.
|
|
110
|
+
Returns None on error.
|
|
111
|
+
"""
|
|
112
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
113
|
+
try:
|
|
114
|
+
# Assumes decode_responses=True in RedisClient config
|
|
115
|
+
return await redis.get(key)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.error(f"Redis GET error for key '{key}': {e}", exc_info=True)
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def delete(*keys: str, alias: PoolAlias = "default") -> int:
|
|
122
|
+
"""
|
|
123
|
+
Delete one or more keys.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
*keys: One or more full Redis keys to delete.
|
|
127
|
+
alias: Redis pool alias to use (default: "default").
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
The number of keys deleted. Returns 0 on error.
|
|
131
|
+
"""
|
|
132
|
+
if not keys:
|
|
133
|
+
return 0
|
|
134
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
135
|
+
try:
|
|
136
|
+
return await redis.delete(*keys)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(
|
|
139
|
+
f"Redis DELETE error for keys starting with '{keys[0]}...': {e}",
|
|
140
|
+
exc_info=True,
|
|
141
|
+
)
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
# =========================================================================
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# SECTION: Atomic Combined Operations (Using Pipelines internally)
|
|
148
|
+
# =========================================================================
|
|
149
|
+
async def hset_with_expire(
|
|
150
|
+
key: str, mapping: Mapping[str, str], ttl: int, *, alias: PoolAlias = "default"
|
|
151
|
+
) -> tuple[int | None, bool]:
|
|
152
|
+
"""
|
|
153
|
+
Atomically sets hash fields using HSET and sets key expiration using EXPIRE.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
key: The full Redis key of the hash.
|
|
157
|
+
mapping: A dictionary of field-value pairs (str:str).
|
|
158
|
+
ttl: Time To Live in seconds.
|
|
159
|
+
alias: Redis pool alias to use (default: "default").
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
A tuple: (result_of_hset, result_of_expire).
|
|
163
|
+
- result_of_hset: Integer reply from HSET (number of fields added), or None on pipeline error.
|
|
164
|
+
- result_of_expire: Boolean indicating if EXPIRE was successful, or False on pipeline error.
|
|
165
|
+
Returns (None, False) on general exception.
|
|
166
|
+
"""
|
|
167
|
+
if not mapping:
|
|
168
|
+
logger.warning(
|
|
169
|
+
f"hset_with_expire called with empty mapping for key '{key}'. Skipping HSET, attempting EXPIRE."
|
|
170
|
+
)
|
|
171
|
+
try:
|
|
172
|
+
# Still attempt expire if key might exist
|
|
173
|
+
expire_success = await expire(key, ttl, alias=alias)
|
|
174
|
+
return 0, expire_success # HSET result is 0 as nothing was set
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(
|
|
177
|
+
f"Error attempting EXPIRE in hset_with_expire for key '{key}' with empty mapping: {e}",
|
|
178
|
+
exc_info=True,
|
|
179
|
+
)
|
|
180
|
+
return None, False
|
|
181
|
+
|
|
182
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
183
|
+
try:
|
|
184
|
+
async with redis.pipeline(transaction=True) as pipe:
|
|
185
|
+
# Ensure mapping values are suitable primitive types
|
|
186
|
+
checked_mapping = {}
|
|
187
|
+
for k, v in mapping.items():
|
|
188
|
+
if not isinstance(v, (str, bytes, int, float)):
|
|
189
|
+
logger.warning(
|
|
190
|
+
f"RedisCoreMethods.hset_with_expire received non-primitive type for field '{k}' in mapping for key '{key}'. Attempting str conversion. Type: {type(v)}"
|
|
191
|
+
)
|
|
192
|
+
checked_mapping[k] = str(v)
|
|
193
|
+
else:
|
|
194
|
+
checked_mapping[k] = v
|
|
195
|
+
pipe.hset(key, mapping=checked_mapping)
|
|
196
|
+
pipe.expire(key, ttl)
|
|
197
|
+
results = await pipe.execute()
|
|
198
|
+
# results is a list [hset_result, expire_result]
|
|
199
|
+
hset_res = results[0] if results and len(results) > 0 else None
|
|
200
|
+
expire_res = bool(results[1]) if results and len(results) > 1 else False
|
|
201
|
+
return hset_res, expire_res
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.error(
|
|
204
|
+
f"Redis HSET+EXPIRE pipeline error for key '{key}': {e}", exc_info=True
|
|
205
|
+
)
|
|
206
|
+
return None, False # Indicate pipeline execution failure
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
async def hincrby_with_expire(
|
|
210
|
+
key: str, field: str, increment: int, ttl: int, *, alias: PoolAlias = "default"
|
|
211
|
+
) -> tuple[int | None, bool]:
|
|
212
|
+
"""
|
|
213
|
+
Atomically increments a hash field using HINCRBY and sets key expiration using EXPIRE.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
key: The full Redis key of the hash.
|
|
217
|
+
field: The field name.
|
|
218
|
+
increment: The amount to increment by.
|
|
219
|
+
ttl: Time To Live in seconds.
|
|
220
|
+
alias: Redis pool alias to use (default: "default").
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
A tuple: (new_value, result_of_expire).
|
|
224
|
+
- new_value: The integer value after increment, or None if HINCRBY failed (e.g., WRONGTYPE) or pipeline error.
|
|
225
|
+
- result_of_expire: Boolean indicating if EXPIRE was successful, or False on pipeline error.
|
|
226
|
+
Returns (None, False) on general exception.
|
|
227
|
+
"""
|
|
228
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
229
|
+
try:
|
|
230
|
+
async with redis.pipeline(transaction=True) as pipe:
|
|
231
|
+
pipe.hincrby(key, field, increment)
|
|
232
|
+
pipe.expire(key, ttl)
|
|
233
|
+
results = await pipe.execute()
|
|
234
|
+
|
|
235
|
+
# results is a list [hincrby_result, expire_result]
|
|
236
|
+
# Check for specific redis error within pipeline result for hincrby if needed
|
|
237
|
+
# For now, assume if results[0] is None or exception, it failed.
|
|
238
|
+
incr_res = (
|
|
239
|
+
int(results[0])
|
|
240
|
+
if results and len(results) > 0 and results[0] is not None
|
|
241
|
+
else None
|
|
242
|
+
)
|
|
243
|
+
expire_res = bool(results[1]) if results and len(results) > 1 else False
|
|
244
|
+
return incr_res, expire_res
|
|
245
|
+
except (
|
|
246
|
+
Exception
|
|
247
|
+
) as e: # Catches WRONGTYPE from HINCRBY within execute or connection errors
|
|
248
|
+
logger.error(
|
|
249
|
+
f"Redis HINCRBY+EXPIRE pipeline error for key '{key}', field '{field}': {e}",
|
|
250
|
+
exc_info=True,
|
|
251
|
+
)
|
|
252
|
+
return None, False # Indicate pipeline execution failure
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
async def rpush_and_sadd(
|
|
256
|
+
list_key: str,
|
|
257
|
+
list_values: Sequence[str],
|
|
258
|
+
set_key: str,
|
|
259
|
+
set_members: Sequence[str],
|
|
260
|
+
*,
|
|
261
|
+
alias: PoolAlias = "default",
|
|
262
|
+
) -> tuple[int | None, int | None]:
|
|
263
|
+
"""
|
|
264
|
+
Atomically pushes values to a list using RPUSH and adds members to a set using SADD.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
list_key: The full Redis key for the list.
|
|
268
|
+
list_values: Sequence of string values to push to the list.
|
|
269
|
+
set_key: The full Redis key for the set.
|
|
270
|
+
set_members: Sequence of string members to add to the set.
|
|
271
|
+
alias: Redis pool alias to use (default: "default").
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
A tuple: (result_of_rpush, result_of_sadd).
|
|
275
|
+
- result_of_rpush: New length of the list, or None on pipeline error.
|
|
276
|
+
- result_of_sadd: Number of members added to the set, or None on pipeline error.
|
|
277
|
+
Returns (None, None) on general exception.
|
|
278
|
+
"""
|
|
279
|
+
if not list_values or not set_members:
|
|
280
|
+
logger.warning(
|
|
281
|
+
f"rpush_and_sadd called with empty list_values or set_members. ListKey: '{list_key}', SetKey: '{set_key}'. Skipping pipeline."
|
|
282
|
+
)
|
|
283
|
+
# Decide desired behavior: maybe still run the non-empty part? For now, return failure.
|
|
284
|
+
return None, None # Indicate nothing was done due to empty input
|
|
285
|
+
|
|
286
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
287
|
+
try:
|
|
288
|
+
# Ensure values/members are primitive types
|
|
289
|
+
checked_list_values = []
|
|
290
|
+
for v in list_values:
|
|
291
|
+
if not isinstance(v, (str, bytes, int, float)):
|
|
292
|
+
logger.warning(
|
|
293
|
+
f"RedisCoreMethods.rpush_and_sadd received non-primitive type in list_values for key '{list_key}'. Attempting str conversion. Type: {type(v)}"
|
|
294
|
+
)
|
|
295
|
+
checked_list_values.append(str(v))
|
|
296
|
+
else:
|
|
297
|
+
checked_list_values.append(v)
|
|
298
|
+
|
|
299
|
+
checked_set_members = []
|
|
300
|
+
for m in set_members:
|
|
301
|
+
if not isinstance(m, (str, bytes, int, float)):
|
|
302
|
+
logger.warning(
|
|
303
|
+
f"RedisCoreMethods.rpush_and_sadd received non-primitive type in set_members for key '{set_key}'. Attempting str conversion. Type: {type(m)}"
|
|
304
|
+
)
|
|
305
|
+
checked_set_members.append(str(m))
|
|
306
|
+
else:
|
|
307
|
+
checked_set_members.append(m)
|
|
308
|
+
|
|
309
|
+
async with redis.pipeline(transaction=True) as pipe:
|
|
310
|
+
pipe.rpush(list_key, *checked_list_values)
|
|
311
|
+
pipe.sadd(set_key, *checked_set_members)
|
|
312
|
+
results = await pipe.execute()
|
|
313
|
+
|
|
314
|
+
# results is a list [rpush_result, sadd_result]
|
|
315
|
+
rpush_res = results[0] if results and len(results) > 0 else None
|
|
316
|
+
sadd_res = results[1] if results and len(results) > 1 else None
|
|
317
|
+
return rpush_res, sadd_res
|
|
318
|
+
except Exception as e:
|
|
319
|
+
logger.error(
|
|
320
|
+
f"Redis RPUSH+SADD pipeline error for list '{list_key}', set '{set_key}': {e}",
|
|
321
|
+
exc_info=True,
|
|
322
|
+
)
|
|
323
|
+
return None, None # Indicate pipeline execution failure
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# =========================================================================
|
|
327
|
+
# SECTION: Scan Operations
|
|
328
|
+
# =========================================================================
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
async def scan_keys(
|
|
332
|
+
match_pattern: str,
|
|
333
|
+
cursor: str | bytes | int = 0,
|
|
334
|
+
count: int | None = None,
|
|
335
|
+
*,
|
|
336
|
+
alias: PoolAlias = "default",
|
|
337
|
+
) -> tuple[str | int, list[str]]:
|
|
338
|
+
"""
|
|
339
|
+
Iterates the key space using the SCAN command.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
match_pattern: Glob-style pattern to match keys.
|
|
343
|
+
cursor: The cursor to start iteration from (0 for the first call).
|
|
344
|
+
Can be int or string representation of int. Bytes cursor is also possible if decode_responses=False.
|
|
345
|
+
count: Hint for the number of keys to return per iteration.
|
|
346
|
+
alias: Redis pool alias to use (default: "default").
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
A tuple containing:
|
|
350
|
+
- The cursor for the next iteration (string or int, depending on redis-py version and connection settings). 0 indicates iteration is complete.
|
|
351
|
+
- A list of matching key strings found in this iteration.
|
|
352
|
+
Returns (0, []) on error.
|
|
353
|
+
"""
|
|
354
|
+
# Ensure cursor is suitable for redis-py call
|
|
355
|
+
# redis-py >= 4.2 prefers int cursor, older versions might use bytes/str
|
|
356
|
+
# Let's try to stick to int/str representation for broader compatibility
|
|
357
|
+
current_cursor: str | int = (
|
|
358
|
+
cursor if isinstance(cursor, (str, int)) else str(int(cursor))
|
|
359
|
+
) # Prefer string '0' if bytes 'b0' is passed. Assume int 0 is start.
|
|
360
|
+
|
|
361
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
362
|
+
try:
|
|
363
|
+
# redis-py's scan returns (new_cursor, keys_list)
|
|
364
|
+
# new_cursor type might be int or bytes depending on version/config
|
|
365
|
+
# keys_list should be List[str] if decode_responses=True
|
|
366
|
+
next_cursor, keys_batch = await redis.scan(
|
|
367
|
+
cursor=current_cursor, match=match_pattern, count=count
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Normalize cursor for return - prefer string representation if bytes returned
|
|
371
|
+
if isinstance(next_cursor, bytes):
|
|
372
|
+
next_cursor = next_cursor.decode("utf-8")
|
|
373
|
+
elif isinstance(next_cursor, int): # Common return type
|
|
374
|
+
next_cursor = str(
|
|
375
|
+
next_cursor
|
|
376
|
+
) # Return as string for consistency with potential byte cursor
|
|
377
|
+
|
|
378
|
+
# Ensure keys are strings (should be if decode_responses=True)
|
|
379
|
+
keys_batch_str = [
|
|
380
|
+
k if isinstance(k, str) else k.decode("utf-8") for k in keys_batch
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
return next_cursor, keys_batch_str
|
|
384
|
+
except Exception as e:
|
|
385
|
+
logger.error(
|
|
386
|
+
f"Redis SCAN error for pattern '{match_pattern}' with cursor '{current_cursor}': {e}",
|
|
387
|
+
exc_info=True,
|
|
388
|
+
)
|
|
389
|
+
return (
|
|
390
|
+
"0",
|
|
391
|
+
[],
|
|
392
|
+
) # Return '0' cursor and empty list to signal completion/error
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# =========================================================================
|
|
396
|
+
# SECTION: TTL Management
|
|
397
|
+
# =========================================================================
|
|
398
|
+
async def get_ttl(key: str, *, alias: PoolAlias = "default") -> int:
|
|
399
|
+
"""
|
|
400
|
+
Get remaining Time To Live (TTL) for a key in seconds.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
key: The full Redis key.
|
|
404
|
+
alias: Redis pool alias to use (default: "default").
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
- TTL in seconds
|
|
408
|
+
- -1 if the key exists but has no associated expire time
|
|
409
|
+
- -2 if the key does not exist or an error occurred.
|
|
410
|
+
"""
|
|
411
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
412
|
+
try:
|
|
413
|
+
return await redis.ttl(key)
|
|
414
|
+
except Exception as e:
|
|
415
|
+
logger.error(f"Error getting TTL for key '{key}': {e}")
|
|
416
|
+
return -2 # Consistent with redis-py for errors/non-existent key
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
async def expire(key: str, ttl: int, *, alias: PoolAlias = "default") -> bool:
|
|
420
|
+
"""
|
|
421
|
+
Set a timeout on key in seconds.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
key: The full Redis key.
|
|
425
|
+
ttl: Time To Live in seconds.
|
|
426
|
+
alias: Redis pool alias to use (default: "default").
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
True if the timeout was set, False if key does not exist or timeout could not be set (or on error).
|
|
430
|
+
"""
|
|
431
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
432
|
+
try:
|
|
433
|
+
# EXPIRE returns 1 if timeout was set, 0 if key doesn't exist or timeout wasn't set
|
|
434
|
+
result = await redis.expire(key, ttl)
|
|
435
|
+
return bool(result) # Convert 1/0 to True/False
|
|
436
|
+
except Exception as e:
|
|
437
|
+
logger.error(f"Error setting EXPIRE for key '{key}': {e}")
|
|
438
|
+
return False
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# =========================================================================
|
|
442
|
+
# SECTION: Hash Operations
|
|
443
|
+
# =========================================================================
|
|
444
|
+
async def hset(
|
|
445
|
+
key: str,
|
|
446
|
+
field: str | None = None,
|
|
447
|
+
value: str | None = None,
|
|
448
|
+
mapping: Mapping[str, str] | None = None,
|
|
449
|
+
*,
|
|
450
|
+
alias: PoolAlias = "default",
|
|
451
|
+
) -> int:
|
|
452
|
+
"""
|
|
453
|
+
Sets field in the hash stored at key to value.
|
|
454
|
+
If mapping is provided, sets multiple fields and values.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
key: The full Redis key of the hash.
|
|
458
|
+
field: The field name (required if mapping is None).
|
|
459
|
+
value: The string value for the field (required if mapping is None).
|
|
460
|
+
mapping: A dictionary of field-value pairs (must be str:str).
|
|
461
|
+
alias: Redis pool alias to use (default: "default").
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Integer reply: The number of fields that were added.
|
|
465
|
+
Returns -1 on error or invalid arguments.
|
|
466
|
+
"""
|
|
467
|
+
if mapping is None and (field is None or value is None):
|
|
468
|
+
logger.error(
|
|
469
|
+
f"HSET requires either 'field' and 'value', or 'mapping' for key '{key}'"
|
|
470
|
+
)
|
|
471
|
+
return -1 # Indicate error due to invalid arguments
|
|
472
|
+
if mapping is not None and (field is not None or value is not None):
|
|
473
|
+
logger.warning(
|
|
474
|
+
f"HSET called with both ('field', 'value') and 'mapping' for key '{key}'. Using 'mapping'."
|
|
475
|
+
)
|
|
476
|
+
# Prioritize mapping if both are provided, clear field/value
|
|
477
|
+
|
|
478
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
479
|
+
try:
|
|
480
|
+
# redis-py's hset handles both single field/value and mapping
|
|
481
|
+
if mapping:
|
|
482
|
+
# Ensure mapping values are strings (or bytes/int/float)
|
|
483
|
+
checked_mapping = {}
|
|
484
|
+
for k, v in mapping.items():
|
|
485
|
+
if not isinstance(v, (str, bytes, int, float)):
|
|
486
|
+
logger.warning(
|
|
487
|
+
f"RedisCoreMethods.hset received non-primitive type for field '{k}' in mapping for key '{key}'. Attempting str conversion. Type: {type(v)}"
|
|
488
|
+
)
|
|
489
|
+
checked_mapping[k] = str(v)
|
|
490
|
+
else:
|
|
491
|
+
checked_mapping[k] = v # Keep original primitive type
|
|
492
|
+
return await redis.hset(key, mapping=checked_mapping)
|
|
493
|
+
else:
|
|
494
|
+
# Handle single field/value
|
|
495
|
+
if not isinstance(value, (str, bytes, int, float)):
|
|
496
|
+
logger.warning(
|
|
497
|
+
f"RedisCoreMethods.hset received non-primitive type for field '{field}' for key '{key}'. Attempting str conversion. Type: {type(value)}"
|
|
498
|
+
)
|
|
499
|
+
value_to_set = str(value)
|
|
500
|
+
else:
|
|
501
|
+
value_to_set = value
|
|
502
|
+
# Use the non-mapping version of hset call
|
|
503
|
+
# Note: redis-py hset changed signature; mapping is preferred, but single key/value works
|
|
504
|
+
# Forcing mapping approach for consistency:
|
|
505
|
+
return await redis.hset(key, mapping={field: value_to_set})
|
|
506
|
+
# Older way (might vary by redis-py version): return await redis.hset(key, field, value_to_set)
|
|
507
|
+
except Exception as e:
|
|
508
|
+
logger.error(f"Redis HSET error for key '{key}': {e}", exc_info=True)
|
|
509
|
+
return -1 # Indicate error
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
async def hget(key: str, field: str, *, alias: PoolAlias = "default") -> str | None:
|
|
513
|
+
"""
|
|
514
|
+
Gets the string value of a hash field.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
key: The full Redis key of the hash.
|
|
518
|
+
field: The field name.
|
|
519
|
+
alias: Redis pool alias to use (default: "default").
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
The string value of the field, or None if the field or key doesn't exist, or on error.
|
|
523
|
+
"""
|
|
524
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
525
|
+
try:
|
|
526
|
+
# Assumes decode_responses=True
|
|
527
|
+
return await redis.hget(key, field)
|
|
528
|
+
except Exception as e:
|
|
529
|
+
logger.error(f"Redis HGET error for key '{key}', field '{field}': {e}")
|
|
530
|
+
return None
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
async def hgetall(key: str, *, alias: PoolAlias = "default") -> dict[str, str]:
|
|
534
|
+
"""
|
|
535
|
+
Gets all fields and values stored in a hash.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
key: The full Redis key of the hash.
|
|
539
|
+
alias: Redis pool alias to use (default: "default").
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
A dictionary mapping field names to string values. Returns an empty dict
|
|
543
|
+
if the key doesn't exist or on error.
|
|
544
|
+
"""
|
|
545
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
546
|
+
try:
|
|
547
|
+
# Assumes decode_responses=True, returns Dict[str, str]
|
|
548
|
+
return await redis.hgetall(key)
|
|
549
|
+
except Exception as e:
|
|
550
|
+
logger.error(f"Redis HGETALL error for key '{key}': {e}", exc_info=True)
|
|
551
|
+
return {}
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
async def hexists(key: str, field: str, *, alias: PoolAlias = "default") -> bool:
|
|
555
|
+
"""
|
|
556
|
+
Checks if a field exists in a hash.
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
key: The full Redis key of the hash.
|
|
560
|
+
field: The field name.
|
|
561
|
+
alias: Redis pool alias to use (default: "default").
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
True if the field exists, False otherwise (including key not existing or error).
|
|
565
|
+
"""
|
|
566
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
567
|
+
try:
|
|
568
|
+
return await redis.hexists(key, field)
|
|
569
|
+
except Exception as e:
|
|
570
|
+
logger.error(f"Redis HEXISTS error for key '{key}', field '{field}': {e}")
|
|
571
|
+
return False
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
async def hdel(key: str, *fields: str, alias: PoolAlias = "default") -> int:
|
|
575
|
+
"""
|
|
576
|
+
Deletes one or more hash fields.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
key: The full Redis key of the hash.
|
|
580
|
+
*fields: One or more field names to delete.
|
|
581
|
+
alias: Redis pool alias to use (default: "default").
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
The number of fields that were removed from the hash (0 if the key doesn't exist or on error).
|
|
585
|
+
"""
|
|
586
|
+
if not fields:
|
|
587
|
+
return 0
|
|
588
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
589
|
+
try:
|
|
590
|
+
return await redis.hdel(key, *fields)
|
|
591
|
+
except Exception as e:
|
|
592
|
+
logger.error(f"Redis HDEL error for key '{key}', fields '{fields}': {e}")
|
|
593
|
+
return 0
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
async def hincrby(
|
|
597
|
+
key: str, field: str, increment: int = 1, *, alias: PoolAlias = "default"
|
|
598
|
+
) -> int | None:
|
|
599
|
+
"""
|
|
600
|
+
Atomically increments the integer value of a hash field by the given amount.
|
|
601
|
+
Sets the field to `increment` if the field does not exist.
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
key: The full Redis key of the hash.
|
|
605
|
+
field: The field name.
|
|
606
|
+
increment: The amount to increment by (default: 1).
|
|
607
|
+
alias: Redis pool alias to use (default: "default").
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
The new integer value of the field after the increment.
|
|
611
|
+
Returns None if the key exists but the field contains a value of the wrong type,
|
|
612
|
+
or if an error occurs.
|
|
613
|
+
"""
|
|
614
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
615
|
+
try:
|
|
616
|
+
result = await redis.hincrby(key, field, increment)
|
|
617
|
+
# HINCRBY returns the new value as an integer
|
|
618
|
+
return int(result)
|
|
619
|
+
except Exception as e: # Catches redis errors like WRONGTYPE
|
|
620
|
+
logger.error(f"Redis HINCRBY error for key '{key}', field '{field}': {e}")
|
|
621
|
+
return None
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
# =========================================================================
|
|
625
|
+
# SECTION: List Operations
|
|
626
|
+
# =========================================================================
|
|
627
|
+
async def rpush(key: str, *values: str, alias: PoolAlias = "default") -> int:
|
|
628
|
+
"""
|
|
629
|
+
Pushes one or more string values onto the right end of a list.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
key: The full Redis key for the list.
|
|
633
|
+
*values: One or more string values to push.
|
|
634
|
+
alias: Redis pool alias to use (default: "default").
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
The length of the list after the push operation, or 0 on error.
|
|
638
|
+
"""
|
|
639
|
+
if not values:
|
|
640
|
+
return await llen(
|
|
641
|
+
key, alias=alias
|
|
642
|
+
) # RPUSH with no values is a no-op, return current length
|
|
643
|
+
|
|
644
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
645
|
+
try:
|
|
646
|
+
# Ensure values are primitive types suitable for redis-py
|
|
647
|
+
checked_values = []
|
|
648
|
+
for v in values:
|
|
649
|
+
if not isinstance(v, (str, bytes, int, float)):
|
|
650
|
+
logger.warning(
|
|
651
|
+
f"RedisCoreMethods.rpush received non-primitive type in values for key '{key}'. Attempting str conversion. Type: {type(v)}"
|
|
652
|
+
)
|
|
653
|
+
checked_values.append(str(v))
|
|
654
|
+
else:
|
|
655
|
+
checked_values.append(v)
|
|
656
|
+
return await redis.rpush(key, *checked_values)
|
|
657
|
+
except Exception as e:
|
|
658
|
+
logger.error(f"Redis RPUSH error for key '{key}': {e}", exc_info=True)
|
|
659
|
+
return 0
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
async def lpop(
|
|
663
|
+
key: str, count: int | None = None, *, alias: PoolAlias = "default"
|
|
664
|
+
) -> str | list[str] | None:
|
|
665
|
+
"""
|
|
666
|
+
Removes and returns elements from the left end of a list.
|
|
667
|
+
|
|
668
|
+
Args:
|
|
669
|
+
key: The full Redis key for the list.
|
|
670
|
+
count: The number of elements to pop. If None (default), pops one element.
|
|
671
|
+
If count > 0, pops up to 'count' elements (Redis >= 6.2).
|
|
672
|
+
alias: Redis pool alias to use (default: "default").
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
A single string, a list of strings, or None if the list is empty or an error occurs.
|
|
676
|
+
(Assumes decode_responses=True).
|
|
677
|
+
"""
|
|
678
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
679
|
+
try:
|
|
680
|
+
# aioredis-py handles the 'count' argument.
|
|
681
|
+
# Assumes decode_responses=True returns str or List[str].
|
|
682
|
+
result = await redis.lpop(key, count=count)
|
|
683
|
+
return result # Should be str, List[str], or None
|
|
684
|
+
except Exception as e:
|
|
685
|
+
logger.error(
|
|
686
|
+
f"Redis LPOP error for key '{key}' with count {count}: {e}",
|
|
687
|
+
exc_info=True,
|
|
688
|
+
)
|
|
689
|
+
return None
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
async def lrange(
|
|
693
|
+
key: str, start: int, end: int, *, alias: PoolAlias = "default"
|
|
694
|
+
) -> list[str]:
|
|
695
|
+
"""
|
|
696
|
+
Gets a range of elements from a list.
|
|
697
|
+
|
|
698
|
+
Args:
|
|
699
|
+
key: The full Redis key for the list.
|
|
700
|
+
start: Start index (0-based).
|
|
701
|
+
end: End index (inclusive, -1 for last element).
|
|
702
|
+
alias: Redis pool alias to use (default: "default").
|
|
703
|
+
|
|
704
|
+
Returns:
|
|
705
|
+
A list of string elements (assumes decode_responses=True),
|
|
706
|
+
or an empty list if key doesn't exist or on error.
|
|
707
|
+
"""
|
|
708
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
709
|
+
try:
|
|
710
|
+
# Assumes decode_responses=True returns List[str]
|
|
711
|
+
return await redis.lrange(key, start, end)
|
|
712
|
+
except Exception as e:
|
|
713
|
+
logger.error(f"Redis LRANGE error for key '{key}': {e}", exc_info=True)
|
|
714
|
+
return []
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
async def ltrim(
|
|
718
|
+
key: str, start: int, end: int, *, alias: PoolAlias = "default"
|
|
719
|
+
) -> bool:
|
|
720
|
+
"""
|
|
721
|
+
Trims a list so that it will contain only the specified range of elements.
|
|
722
|
+
|
|
723
|
+
Args:
|
|
724
|
+
key: The full Redis key for the list.
|
|
725
|
+
start: Start index (0-based).
|
|
726
|
+
end: End index (inclusive, -1 for last element).
|
|
727
|
+
alias: Redis pool alias to use (default: "default").
|
|
728
|
+
|
|
729
|
+
Returns:
|
|
730
|
+
True if the operation was successful, False otherwise.
|
|
731
|
+
"""
|
|
732
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
733
|
+
try:
|
|
734
|
+
return await redis.ltrim(key, start, end)
|
|
735
|
+
except Exception as e:
|
|
736
|
+
logger.error(f"Redis LTRIM error for key '{key}': {e}", exc_info=True)
|
|
737
|
+
return False
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
async def llen(key: str, *, alias: PoolAlias = "default") -> int:
|
|
741
|
+
"""
|
|
742
|
+
Gets the length of a list.
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
key: The full Redis key for the list.
|
|
746
|
+
alias: Redis pool alias to use (default: "default").
|
|
747
|
+
|
|
748
|
+
Returns:
|
|
749
|
+
The length of the list, or 0 if the key doesn't exist or on error.
|
|
750
|
+
"""
|
|
751
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
752
|
+
try:
|
|
753
|
+
return await redis.llen(key)
|
|
754
|
+
except Exception as e:
|
|
755
|
+
logger.error(f"Redis LLEN error for key '{key}': {e}", exc_info=True)
|
|
756
|
+
return 0
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
# =========================================================================
|
|
760
|
+
# SECTION: Set Operations
|
|
761
|
+
# =========================================================================
|
|
762
|
+
async def sadd(key: str, *members: str, alias: PoolAlias = "default") -> int:
|
|
763
|
+
"""
|
|
764
|
+
Adds one or more members to a set.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
key: The full key name for the set.
|
|
768
|
+
*members: One or more string members to add.
|
|
769
|
+
alias: Redis pool alias to use (default: "default").
|
|
770
|
+
|
|
771
|
+
Returns:
|
|
772
|
+
The number of members that were added to the set (not including members already present),
|
|
773
|
+
or 0 on error.
|
|
774
|
+
"""
|
|
775
|
+
if not members:
|
|
776
|
+
return 0 # SADD with no members is a no-op
|
|
777
|
+
|
|
778
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
779
|
+
try:
|
|
780
|
+
# Ensure members are primitive types suitable for redis-py
|
|
781
|
+
checked_members = []
|
|
782
|
+
for m in members:
|
|
783
|
+
if not isinstance(m, (str, bytes, int, float)):
|
|
784
|
+
logger.warning(
|
|
785
|
+
f"RedisCoreMethods.sadd received non-primitive type in members for key '{key}'. Attempting str conversion. Type: {type(m)}"
|
|
786
|
+
)
|
|
787
|
+
checked_members.append(str(m))
|
|
788
|
+
else:
|
|
789
|
+
checked_members.append(m)
|
|
790
|
+
return await redis.sadd(key, *checked_members)
|
|
791
|
+
except Exception as e:
|
|
792
|
+
logger.error(f"Redis SADD error for key '{key}': {e}", exc_info=True)
|
|
793
|
+
return 0
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
async def smembers(key: str, *, alias: PoolAlias = "default") -> builtins.set[str]:
|
|
797
|
+
"""
|
|
798
|
+
Gets all members of a set.
|
|
799
|
+
|
|
800
|
+
Args:
|
|
801
|
+
key: The full key name for the set.
|
|
802
|
+
alias: Redis pool alias to use (default: "default").
|
|
803
|
+
|
|
804
|
+
Returns:
|
|
805
|
+
A set of string members (assumes decode_responses=True),
|
|
806
|
+
or an empty set if the key doesn't exist or on error.
|
|
807
|
+
"""
|
|
808
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
809
|
+
try:
|
|
810
|
+
# Assumes decode_responses=True returns Set[str]
|
|
811
|
+
return await redis.smembers(key)
|
|
812
|
+
except Exception as e:
|
|
813
|
+
logger.error(f"Redis SMEMBERS error for key '{key}': {e}", exc_info=True)
|
|
814
|
+
return set()
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
async def srem(key: str, *members: str, alias: PoolAlias = "default") -> int:
|
|
818
|
+
"""
|
|
819
|
+
Removes one or more members from a set.
|
|
820
|
+
|
|
821
|
+
Args:
|
|
822
|
+
key: The full key name for the set.
|
|
823
|
+
*members: One or more string members to remove.
|
|
824
|
+
alias: Redis pool alias to use (default: "default").
|
|
825
|
+
|
|
826
|
+
Returns:
|
|
827
|
+
The number of members that were removed from the set, or 0 on error.
|
|
828
|
+
"""
|
|
829
|
+
if not members:
|
|
830
|
+
return 0 # SREM with no members is a no-op
|
|
831
|
+
|
|
832
|
+
async with RedisClient.connection(alias=alias) as redis:
|
|
833
|
+
try:
|
|
834
|
+
# Ensure members are primitive types suitable for redis-py
|
|
835
|
+
checked_members = []
|
|
836
|
+
for m in members:
|
|
837
|
+
if not isinstance(m, (str, bytes, int, float)):
|
|
838
|
+
logger.warning(
|
|
839
|
+
f"RedisCoreMethods.srem received non-primitive type in members for key '{key}'. Attempting str conversion. Type: {type(m)}"
|
|
840
|
+
)
|
|
841
|
+
checked_members.append(str(m))
|
|
842
|
+
else:
|
|
843
|
+
checked_members.append(m)
|
|
844
|
+
return await redis.srem(key, *checked_members)
|
|
845
|
+
except Exception as e:
|
|
846
|
+
logger.error(f"Redis SREM error for key '{key}': {e}", exc_info=True)
|
|
847
|
+
return 0
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
"""
|
|
851
|
+
Tiny, opinionated helpers on top of `redis.asyncio`.
|
|
852
|
+
|
|
853
|
+
```python
|
|
854
|
+
from symphony_concurrency.redis import ops as r
|
|
855
|
+
|
|
856
|
+
await r.set("foo", "bar", ex=60)
|
|
857
|
+
val = await r.get("foo")
|
|
858
|
+
await r.hset_with_expire("profile:42", {"name": "alice"}, ttl=3600)
|
|
859
|
+
|
|
860
|
+
# Use different Redis pools/databases via alias
|
|
861
|
+
await r.set("key", "value", alias="expiry") # expiry pool
|
|
862
|
+
await r.hget("hash", "field", alias="pubsub") # pubsub pool
|
|
863
|
+
```
|
|
864
|
+
Each call grabs a connection from RedisClient.connection(alias) – the pool is
|
|
865
|
+
reused, so there's no socket churn.
|
|
866
|
+
|
|
867
|
+
All functions accept an optional `alias` parameter (default: "default") to target
|
|
868
|
+
specific Redis pools configured via GlobalSymphonyConfig.
|
|
869
|
+
|
|
870
|
+
Values are automatically coerced to str for safety.
|
|
871
|
+
|
|
872
|
+
All functions return a sane fallback (False / None / 0) on error and
|
|
873
|
+
emit a Rich-style log entry via logger = logging.getLogger("RedisOps").
|
|
874
|
+
|
|
875
|
+
If you need more exotic commands, import the raw client:
|
|
876
|
+
```python
|
|
877
|
+
redis = await RedisClient.get("handlers") # specific pool
|
|
878
|
+
await redis.zadd("scores", {"bob": 123})
|
|
879
|
+
```
|
|
880
|
+
"""
|