codex-platform 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.
- codex_platform/__init__.py +1 -0
- codex_platform/notifications/__init__.py +48 -0
- codex_platform/notifications/channels.py +24 -0
- codex_platform/notifications/clients/__init__.py +3 -0
- codex_platform/notifications/clients/smtp.py +134 -0
- codex_platform/notifications/delivery/__init__.py +18 -0
- codex_platform/notifications/delivery/arq.py +69 -0
- codex_platform/notifications/delivery/base.py +39 -0
- codex_platform/notifications/delivery/direct.py +89 -0
- codex_platform/notifications/dto.py +63 -0
- codex_platform/notifications/interfaces.py +30 -0
- codex_platform/notifications/orchestrator.py +116 -0
- codex_platform/notifications/registry.py +71 -0
- codex_platform/notifications/renderer.py +111 -0
- codex_platform/redis_service/__init__.py +59 -0
- codex_platform/redis_service/base.py +69 -0
- codex_platform/redis_service/exceptions.py +20 -0
- codex_platform/redis_service/keys.py +110 -0
- codex_platform/redis_service/managers/__init__.py +18 -0
- codex_platform/redis_service/managers/base_manager.py +31 -0
- codex_platform/redis_service/operations/__init__.py +19 -0
- codex_platform/redis_service/operations/hash.py +302 -0
- codex_platform/redis_service/operations/json_module.py +129 -0
- codex_platform/redis_service/operations/json_string.py +113 -0
- codex_platform/redis_service/operations/list_.py +219 -0
- codex_platform/redis_service/operations/pipeline.py +197 -0
- codex_platform/redis_service/operations/set_.py +212 -0
- codex_platform/redis_service/operations/string.py +318 -0
- codex_platform/redis_service/operations/zset.py +277 -0
- codex_platform/redis_service/service.py +80 -0
- codex_platform/streams/__init__.py +59 -0
- codex_platform/streams/consumer.py +106 -0
- codex_platform/streams/dispatcher.py +175 -0
- codex_platform/streams/processor.py +217 -0
- codex_platform/streams/producer.py +60 -0
- codex_platform/streams/router.py +78 -0
- codex_platform/workers/__init__.py +1 -0
- codex_platform/workers/arq/__init__.py +27 -0
- codex_platform/workers/arq/base.py +170 -0
- codex_platform/workers/arq/config.py +67 -0
- codex_platform/workers/arq/task_utils.py +72 -0
- codex_platform/workers/arq/types.py +11 -0
- codex_platform-0.1.0.dist-info/METADATA +120 -0
- codex_platform-0.1.0.dist-info/RECORD +45 -0
- codex_platform-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codex_platform.redis_service.operations.hash
|
|
3
|
+
=============================================
|
|
4
|
+
Redis Hash operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from redis.asyncio import Redis
|
|
12
|
+
|
|
13
|
+
from codex_platform.redis_service.base import catch_redis_errors
|
|
14
|
+
from codex_platform.redis_service.exceptions import RedisDataError
|
|
15
|
+
from codex_platform.redis_service.keys import BaseRedisKey, resolve_key
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HashOperations:
|
|
21
|
+
"""Redis Hash operations (HSET / HGET / HGETALL / HDEL and more).
|
|
22
|
+
|
|
23
|
+
Accepts an already-constructed ``redis.asyncio.Redis`` client.
|
|
24
|
+
All methods wrap Redis errors in typed exceptions.
|
|
25
|
+
|
|
26
|
+
Example::
|
|
27
|
+
|
|
28
|
+
ops = HashOperations(client)
|
|
29
|
+
await ops.set_json("user:42", "profile", {"name": "Alice"})
|
|
30
|
+
profile = await ops.get_json("user:42", "profile")
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, client: Redis) -> None:
|
|
34
|
+
self.client = client
|
|
35
|
+
|
|
36
|
+
@catch_redis_errors
|
|
37
|
+
async def set_json(self, key: "str | BaseRedisKey", field: str, data: dict[str, Any], **kwargs: Any) -> None:
|
|
38
|
+
"""Serialize a dict to JSON and store it in a hash field (HSET).
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
key: Redis key or a ``BaseRedisKey`` instance.
|
|
42
|
+
field: Hash field name.
|
|
43
|
+
data: Dictionary to serialize as JSON.
|
|
44
|
+
**kwargs: Extra parameters forwarded to ``resolve_key`` (e.g. ``user_id=42``).
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
RedisDataError: If ``data`` cannot be serialized to JSON.
|
|
48
|
+
RedisConnectionError: Redis connection failure.
|
|
49
|
+
RedisServiceError: Redis operation failure.
|
|
50
|
+
"""
|
|
51
|
+
real_key = resolve_key(key, **kwargs)
|
|
52
|
+
try:
|
|
53
|
+
data_json = json.dumps(data)
|
|
54
|
+
except TypeError as e:
|
|
55
|
+
raise RedisDataError(f"JSON serialization error for key='{real_key}': {e}") from e
|
|
56
|
+
await self.client.hset(real_key, field, data_json)
|
|
57
|
+
|
|
58
|
+
@catch_redis_errors
|
|
59
|
+
async def get_json(self, key: "str | BaseRedisKey", field: str, **kwargs: Any) -> dict[str, Any] | None:
|
|
60
|
+
"""Retrieve a hash field and deserialize it from JSON (HGET).
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
key: Redis key or a ``BaseRedisKey`` instance.
|
|
64
|
+
field: Hash field name.
|
|
65
|
+
**kwargs: Extra parameters forwarded to ``resolve_key``.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Deserialized dictionary, or ``None`` if the field does not exist.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
RedisDataError: If the field value is not valid JSON.
|
|
72
|
+
RedisConnectionError: Redis connection failure.
|
|
73
|
+
RedisServiceError: Redis operation failure.
|
|
74
|
+
"""
|
|
75
|
+
real_key = resolve_key(key, **kwargs)
|
|
76
|
+
raw = await self.client.hget(real_key, field)
|
|
77
|
+
if raw is None:
|
|
78
|
+
return None
|
|
79
|
+
try:
|
|
80
|
+
return json.loads(raw)
|
|
81
|
+
except json.JSONDecodeError as e:
|
|
82
|
+
raise RedisDataError(f"Invalid JSON in key='{real_key}' field='{field}': {e}") from e
|
|
83
|
+
|
|
84
|
+
@catch_redis_errors
|
|
85
|
+
async def set_field(self, key: "str | BaseRedisKey", field: str, value: str, **kwargs: Any) -> None:
|
|
86
|
+
"""Set a single string field in a hash (HSET).
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
key: Redis key or a ``BaseRedisKey`` instance.
|
|
90
|
+
field: Hash field name.
|
|
91
|
+
value: String value to store.
|
|
92
|
+
**kwargs: Extra parameters forwarded to ``resolve_key``.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
RedisConnectionError: Redis connection failure.
|
|
96
|
+
RedisServiceError: Redis operation failure.
|
|
97
|
+
"""
|
|
98
|
+
real_key = resolve_key(key, **kwargs)
|
|
99
|
+
await self.client.hset(real_key, field, value)
|
|
100
|
+
|
|
101
|
+
@catch_redis_errors
|
|
102
|
+
async def set_fields(self, key: "str | BaseRedisKey", data: dict[str, Any], **kwargs: Any) -> None:
|
|
103
|
+
"""Set multiple hash fields in a single call (HSET mapping).
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
key: Redis key or a ``BaseRedisKey`` instance.
|
|
107
|
+
data: Mapping of ``{field: value}`` pairs to write.
|
|
108
|
+
**kwargs: Extra parameters forwarded to ``resolve_key``.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
RedisConnectionError: Redis connection failure.
|
|
112
|
+
RedisServiceError: Redis operation failure.
|
|
113
|
+
"""
|
|
114
|
+
real_key = resolve_key(key, **kwargs)
|
|
115
|
+
await self.client.hset(real_key, mapping=data)
|
|
116
|
+
|
|
117
|
+
@catch_redis_errors
|
|
118
|
+
async def get_field(self, key: "str | BaseRedisKey", field: str, **kwargs: Any) -> str | None:
|
|
119
|
+
"""Retrieve a single hash field as a string (HGET).
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
key: Redis key or a ``BaseRedisKey`` instance.
|
|
123
|
+
field: Hash field name.
|
|
124
|
+
**kwargs: Extra parameters forwarded to ``resolve_key``.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
String value of the field, or ``None`` if it does not exist.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
RedisConnectionError: Redis connection failure.
|
|
131
|
+
RedisServiceError: Redis operation failure.
|
|
132
|
+
"""
|
|
133
|
+
real_key = resolve_key(key, **kwargs)
|
|
134
|
+
value = await self.client.hget(real_key, field)
|
|
135
|
+
return str(value) if value is not None else None
|
|
136
|
+
|
|
137
|
+
@catch_redis_errors
|
|
138
|
+
async def get_fields(self, key: "str | BaseRedisKey", *fields: str, **kwargs: Any) -> list[str | None]:
|
|
139
|
+
"""Retrieve multiple hash fields in a single request (HMGET).
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
key: Redis key or a ``BaseRedisKey`` instance.
|
|
143
|
+
*fields: Field names to retrieve.
|
|
144
|
+
**kwargs: Extra parameters forwarded to ``resolve_key``.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
List of values in the same order as ``fields``.
|
|
148
|
+
``None`` for fields that do not exist.
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
RedisConnectionError: Redis connection failure.
|
|
152
|
+
RedisServiceError: Redis operation failure.
|
|
153
|
+
"""
|
|
154
|
+
real_key = resolve_key(key, **kwargs)
|
|
155
|
+
values = await self.client.hmget(real_key, list(fields))
|
|
156
|
+
return [str(v) if v is not None else None for v in values]
|
|
157
|
+
|
|
158
|
+
@catch_redis_errors
|
|
159
|
+
async def get_all(self, key: "str | BaseRedisKey", **kwargs: Any) -> dict[str, str] | None:
|
|
160
|
+
"""Retrieve all fields and values from a hash (HGETALL).
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
key: Redis key or a ``BaseRedisKey`` instance.
|
|
164
|
+
**kwargs: Extra parameters forwarded to ``resolve_key``.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Dict of ``{field: value}``, or ``None`` if the hash does not exist.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
RedisConnectionError: Redis connection failure.
|
|
171
|
+
RedisServiceError: Redis operation failure.
|
|
172
|
+
"""
|
|
173
|
+
real_key = resolve_key(key, **kwargs)
|
|
174
|
+
data = await self.client.hgetall(real_key)
|
|
175
|
+
return data if data else None
|
|
176
|
+
|
|
177
|
+
@catch_redis_errors
|
|
178
|
+
async def delete_field(self, key: "str | BaseRedisKey", *fields: str, **kwargs: Any) -> int:
|
|
179
|
+
"""Delete one or more fields from a hash (HDEL).
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
key: Redis key or a ``BaseRedisKey`` instance.
|
|
183
|
+
*fields: Field names to delete.
|
|
184
|
+
**kwargs: Extra parameters forwarded to ``resolve_key``.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Number of fields actually deleted.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
RedisConnectionError: Redis connection failure.
|
|
191
|
+
RedisServiceError: Redis operation failure.
|
|
192
|
+
"""
|
|
193
|
+
real_key = resolve_key(key, **kwargs)
|
|
194
|
+
return int(await self.client.hdel(real_key, *fields))
|
|
195
|
+
|
|
196
|
+
@catch_redis_errors
|
|
197
|
+
async def delete(self, key: "str | BaseRedisKey", **kwargs: Any) -> None:
|
|
198
|
+
"""Delete the entire hash key (DEL).
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
key: Redis key or a ``BaseRedisKey`` instance.
|
|
202
|
+
**kwargs: Extra parameters forwarded to ``resolve_key``.
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
RedisConnectionError: Redis connection failure.
|
|
206
|
+
RedisServiceError: Redis operation failure.
|
|
207
|
+
"""
|
|
208
|
+
real_key = resolve_key(key, **kwargs)
|
|
209
|
+
await self.client.delete(real_key)
|
|
210
|
+
|
|
211
|
+
@catch_redis_errors
|
|
212
|
+
async def exists_field(self, key: "str | BaseRedisKey", field: str, **kwargs: Any) -> bool:
|
|
213
|
+
"""Check whether a field exists in a hash (HEXISTS).
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
key: Redis key or a ``BaseRedisKey`` instance.
|
|
217
|
+
field: Hash field name to check.
|
|
218
|
+
**kwargs: Extra parameters forwarded to ``resolve_key``.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
``True`` if the field exists, ``False`` otherwise.
|
|
222
|
+
|
|
223
|
+
Raises:
|
|
224
|
+
RedisConnectionError: Redis connection failure.
|
|
225
|
+
RedisServiceError: Redis operation failure.
|
|
226
|
+
"""
|
|
227
|
+
real_key = resolve_key(key, **kwargs)
|
|
228
|
+
return bool(await self.client.hexists(real_key, field))
|
|
229
|
+
|
|
230
|
+
@catch_redis_errors
|
|
231
|
+
async def keys(self, key: "str | BaseRedisKey", **kwargs: Any) -> list[str]:
|
|
232
|
+
"""Return all field names of a hash (HKEYS).
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
key: Redis key or a ``BaseRedisKey`` instance.
|
|
236
|
+
**kwargs: Extra parameters forwarded to ``resolve_key``.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
List of field names. Empty list if the hash does not exist.
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
RedisConnectionError: Redis connection failure.
|
|
243
|
+
RedisServiceError: Redis operation failure.
|
|
244
|
+
"""
|
|
245
|
+
real_key = resolve_key(key, **kwargs)
|
|
246
|
+
return await self.client.hkeys(real_key)
|
|
247
|
+
|
|
248
|
+
@catch_redis_errors
|
|
249
|
+
async def values(self, key: "str | BaseRedisKey", **kwargs: Any) -> list[str]:
|
|
250
|
+
"""Return all field values of a hash (HVALS).
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
key: Redis key or a ``BaseRedisKey`` instance.
|
|
254
|
+
**kwargs: Extra parameters forwarded to ``resolve_key``.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
List of field values. Empty list if the hash does not exist.
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
RedisConnectionError: Redis connection failure.
|
|
261
|
+
RedisServiceError: Redis operation failure.
|
|
262
|
+
"""
|
|
263
|
+
real_key = resolve_key(key, **kwargs)
|
|
264
|
+
return await self.client.hvals(real_key)
|
|
265
|
+
|
|
266
|
+
@catch_redis_errors
|
|
267
|
+
async def length(self, key: "str | BaseRedisKey", **kwargs: Any) -> int:
|
|
268
|
+
"""Return the number of fields in a hash (HLEN).
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
key: Redis key or a ``BaseRedisKey`` instance.
|
|
272
|
+
**kwargs: Extra parameters forwarded to ``resolve_key``.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Number of fields. ``0`` if the hash does not exist.
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
RedisConnectionError: Redis connection failure.
|
|
279
|
+
RedisServiceError: Redis operation failure.
|
|
280
|
+
"""
|
|
281
|
+
real_key = resolve_key(key, **kwargs)
|
|
282
|
+
return int(await self.client.hlen(real_key))
|
|
283
|
+
|
|
284
|
+
@catch_redis_errors
|
|
285
|
+
async def increment(self, key: "str | BaseRedisKey", field: str, amount: int = 1, **kwargs: Any) -> int:
|
|
286
|
+
"""Increment a numeric hash field by the given amount (HINCRBY).
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
key: Redis key or a ``BaseRedisKey`` instance.
|
|
290
|
+
field: Numeric field name.
|
|
291
|
+
amount: Increment step. Defaults to ``1``.
|
|
292
|
+
**kwargs: Extra parameters forwarded to ``resolve_key``.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
New value of the field after incrementing.
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
RedisConnectionError: Redis connection failure.
|
|
299
|
+
RedisServiceError: Redis operation failure.
|
|
300
|
+
"""
|
|
301
|
+
real_key = resolve_key(key, **kwargs)
|
|
302
|
+
return int(await self.client.hincrby(real_key, field, amount))
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codex_platform.redis_service.operations.json_module
|
|
3
|
+
====================================================
|
|
4
|
+
RedisJSON module operations (JSON.SET / JSON.GET / ...).
|
|
5
|
+
|
|
6
|
+
Requires the RedisJSON server module to be loaded on the Redis instance.
|
|
7
|
+
Raises ``RuntimeError`` on first use if the module is not available in redis-py.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from redis.asyncio import Redis
|
|
14
|
+
|
|
15
|
+
from codex_platform.redis_service.base import catch_redis_errors
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from redis.commands.json.path import Path as JsonPath # noqa: F401
|
|
21
|
+
|
|
22
|
+
_JSON_AVAILABLE = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
_JSON_AVAILABLE = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class JsonModuleOperations:
|
|
28
|
+
"""Redis JSON operations using the server-side RedisJSON module (JSON.* commands).
|
|
29
|
+
|
|
30
|
+
Requires the RedisJSON module on the server and redis-py with JSON support.
|
|
31
|
+
Supports path-based access, atomic updates, array append, and more.
|
|
32
|
+
|
|
33
|
+
Example::
|
|
34
|
+
|
|
35
|
+
svc = JsonModuleOperations(client)
|
|
36
|
+
await svc.set("user:42", "$", {"name": "Alice", "age": 30})
|
|
37
|
+
name = await svc.get("user:42", "$.name") # [["Alice"]]
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, client: Redis) -> None:
|
|
41
|
+
self.client = client
|
|
42
|
+
|
|
43
|
+
def _require_json(self) -> None:
|
|
44
|
+
if not _JSON_AVAILABLE:
|
|
45
|
+
raise RuntimeError("RedisJSON module not available in redis-py. Install: pip install redis[hiredis]")
|
|
46
|
+
|
|
47
|
+
@catch_redis_errors
|
|
48
|
+
async def set(self, key: str, path: str, obj: Any, nx: bool = False, xx: bool = False) -> bool:
|
|
49
|
+
"""Set a JSON value at the given path (JSON.SET).
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
key: Redis key string.
|
|
53
|
+
path: JSONPath expression, e.g. ``$`` or ``$.field``.
|
|
54
|
+
obj: Any JSON-serializable object.
|
|
55
|
+
nx: Set only if the key does not exist.
|
|
56
|
+
xx: Set only if the key already exists.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
``True`` if the value was successfully set.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
RuntimeError: If the RedisJSON module is not available.
|
|
63
|
+
RedisConnectionError: Redis connection failure.
|
|
64
|
+
RedisServiceError: Redis operation failure.
|
|
65
|
+
"""
|
|
66
|
+
self._require_json()
|
|
67
|
+
result = await self.client.json().set(key, path, obj, nx=nx, xx=xx) # type: ignore[no-untyped-call]
|
|
68
|
+
log.debug("JsonModuleOps | set key='%s' path='%s'", key, path)
|
|
69
|
+
return bool(result)
|
|
70
|
+
|
|
71
|
+
@catch_redis_errors
|
|
72
|
+
async def get(self, key: str, path: str = "$") -> Any:
|
|
73
|
+
"""Retrieve a JSON value at the given path (JSON.GET).
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
key: Redis key string.
|
|
77
|
+
path: JSONPath expression. Defaults to ``$`` (the whole document).
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Parsed value, or ``None`` if the key does not exist.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
RuntimeError: If the RedisJSON module is not available.
|
|
84
|
+
RedisConnectionError: Redis connection failure.
|
|
85
|
+
RedisServiceError: Redis operation failure.
|
|
86
|
+
"""
|
|
87
|
+
self._require_json()
|
|
88
|
+
return await self.client.json().get(key, path) # type: ignore[no-untyped-call]
|
|
89
|
+
|
|
90
|
+
@catch_redis_errors
|
|
91
|
+
async def arrappend(self, key: str, path: str, *args: Any) -> int:
|
|
92
|
+
"""Append elements to a JSON array at the given path (JSON.ARRAPPEND).
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
key: Redis key string.
|
|
96
|
+
path: JSONPath expression pointing to an array.
|
|
97
|
+
*args: Values to append to the array.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
New length of the array after appending.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
RuntimeError: If the RedisJSON module is not available.
|
|
104
|
+
RedisConnectionError: Redis connection failure.
|
|
105
|
+
RedisServiceError: Redis operation failure.
|
|
106
|
+
"""
|
|
107
|
+
self._require_json()
|
|
108
|
+
count = await self.client.json().arrappend(key, path, *args) # type: ignore[no-untyped-call]
|
|
109
|
+
return int(count) if count else 0
|
|
110
|
+
|
|
111
|
+
@catch_redis_errors
|
|
112
|
+
async def delete(self, key: str, path: str = "$") -> int:
|
|
113
|
+
"""Delete a JSON value at the given path (JSON.DEL).
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
key: Redis key string.
|
|
117
|
+
path: JSONPath expression. Defaults to ``$`` (the whole document).
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Number of elements deleted.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
RuntimeError: If the RedisJSON module is not available.
|
|
124
|
+
RedisConnectionError: Redis connection failure.
|
|
125
|
+
RedisServiceError: Redis operation failure.
|
|
126
|
+
"""
|
|
127
|
+
self._require_json()
|
|
128
|
+
result = await self.client.json().delete(key, path) # type: ignore[no-untyped-call]
|
|
129
|
+
return int(result) if result else 0
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codex_platform.redis_service.operations.json_string
|
|
3
|
+
====================================================
|
|
4
|
+
JSON operations via standard Redis string commands (SET / GET).
|
|
5
|
+
|
|
6
|
+
Works with any Redis instance — no server modules required.
|
|
7
|
+
Values are serialized with ``json.dumps`` and deserialized with ``json.loads``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from redis.asyncio import Redis
|
|
15
|
+
|
|
16
|
+
from codex_platform.redis_service.base import catch_redis_errors
|
|
17
|
+
|
|
18
|
+
log = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class JsonStringOperations:
|
|
22
|
+
"""Store JSON objects as Redis strings using SET/GET with json.dumps/loads.
|
|
23
|
+
|
|
24
|
+
Works with any standard Redis instance — no server modules required.
|
|
25
|
+
Does not support path-based access; for that use
|
|
26
|
+
:class:`~codex_platform.redis_service.operations.json_module.JsonModuleOperations`.
|
|
27
|
+
|
|
28
|
+
Example::
|
|
29
|
+
|
|
30
|
+
svc = JsonStringOperations(client)
|
|
31
|
+
await svc.set("user:42", {"name": "Alice", "age": 30}, ttl=3600)
|
|
32
|
+
user = await svc.get("user:42") # {"name": "Alice", "age": 30}
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, client: Redis) -> None:
|
|
36
|
+
self.client = client
|
|
37
|
+
|
|
38
|
+
@catch_redis_errors
|
|
39
|
+
async def set(self, key: str, obj: Any, ttl: int | None = None) -> None:
|
|
40
|
+
"""Serialize an object to JSON and store it in Redis (SET).
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
key: Redis key string.
|
|
44
|
+
obj: Any JSON-serializable object (dict, list, str, int, …).
|
|
45
|
+
ttl: Expiry in seconds. ``None`` means no expiry.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
RedisConnectionError: Redis connection failure.
|
|
49
|
+
RedisServiceError: Redis operation failure.
|
|
50
|
+
"""
|
|
51
|
+
value = json.dumps(obj, ensure_ascii=False)
|
|
52
|
+
await self.client.set(key, value, ex=ttl)
|
|
53
|
+
log.debug("JsonStringOps | set key='%s' ttl=%s", key, ttl)
|
|
54
|
+
|
|
55
|
+
@catch_redis_errors
|
|
56
|
+
async def get(self, key: str) -> Any:
|
|
57
|
+
"""Read a Redis string and deserialize it from JSON (GET).
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
key: Redis key string.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Deserialized object, or ``None`` if the key does not exist.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
RedisConnectionError: Redis connection failure.
|
|
67
|
+
RedisServiceError: Redis operation failure.
|
|
68
|
+
"""
|
|
69
|
+
raw = await self.client.get(key)
|
|
70
|
+
if raw is None:
|
|
71
|
+
return None
|
|
72
|
+
return json.loads(raw)
|
|
73
|
+
|
|
74
|
+
@catch_redis_errors
|
|
75
|
+
async def set_nx(self, key: str, obj: Any, ttl: int | None = None) -> bool:
|
|
76
|
+
"""Set a JSON value only if the key does not exist (SET NX).
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
key: Redis key string.
|
|
80
|
+
obj: Any JSON-serializable object.
|
|
81
|
+
ttl: Expiry in seconds. ``None`` means no expiry.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
``True`` if the value was set, ``False`` if the key already existed.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
RedisConnectionError: Redis connection failure.
|
|
88
|
+
RedisServiceError: Redis operation failure.
|
|
89
|
+
"""
|
|
90
|
+
value = json.dumps(obj, ensure_ascii=False)
|
|
91
|
+
result = await self.client.set(key, value, ex=ttl, nx=True)
|
|
92
|
+
log.debug("JsonStringOps | set_nx key='%s' result=%s", key, bool(result))
|
|
93
|
+
return bool(result)
|
|
94
|
+
|
|
95
|
+
@catch_redis_errors
|
|
96
|
+
async def mget(self, *keys: str) -> list[Any]:
|
|
97
|
+
"""Retrieve multiple JSON values in a single request (MGET).
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
*keys: Redis key strings.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
List of deserialized objects in the same order as ``keys``.
|
|
104
|
+
``None`` for keys that do not exist.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
RedisConnectionError: Redis connection failure.
|
|
108
|
+
RedisServiceError: Redis operation failure.
|
|
109
|
+
"""
|
|
110
|
+
if not keys:
|
|
111
|
+
return []
|
|
112
|
+
raws = await self.client.mget(*keys)
|
|
113
|
+
return [json.loads(r) if r is not None else None for r in raws]
|