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.
Files changed (45) hide show
  1. codex_platform/__init__.py +1 -0
  2. codex_platform/notifications/__init__.py +48 -0
  3. codex_platform/notifications/channels.py +24 -0
  4. codex_platform/notifications/clients/__init__.py +3 -0
  5. codex_platform/notifications/clients/smtp.py +134 -0
  6. codex_platform/notifications/delivery/__init__.py +18 -0
  7. codex_platform/notifications/delivery/arq.py +69 -0
  8. codex_platform/notifications/delivery/base.py +39 -0
  9. codex_platform/notifications/delivery/direct.py +89 -0
  10. codex_platform/notifications/dto.py +63 -0
  11. codex_platform/notifications/interfaces.py +30 -0
  12. codex_platform/notifications/orchestrator.py +116 -0
  13. codex_platform/notifications/registry.py +71 -0
  14. codex_platform/notifications/renderer.py +111 -0
  15. codex_platform/redis_service/__init__.py +59 -0
  16. codex_platform/redis_service/base.py +69 -0
  17. codex_platform/redis_service/exceptions.py +20 -0
  18. codex_platform/redis_service/keys.py +110 -0
  19. codex_platform/redis_service/managers/__init__.py +18 -0
  20. codex_platform/redis_service/managers/base_manager.py +31 -0
  21. codex_platform/redis_service/operations/__init__.py +19 -0
  22. codex_platform/redis_service/operations/hash.py +302 -0
  23. codex_platform/redis_service/operations/json_module.py +129 -0
  24. codex_platform/redis_service/operations/json_string.py +113 -0
  25. codex_platform/redis_service/operations/list_.py +219 -0
  26. codex_platform/redis_service/operations/pipeline.py +197 -0
  27. codex_platform/redis_service/operations/set_.py +212 -0
  28. codex_platform/redis_service/operations/string.py +318 -0
  29. codex_platform/redis_service/operations/zset.py +277 -0
  30. codex_platform/redis_service/service.py +80 -0
  31. codex_platform/streams/__init__.py +59 -0
  32. codex_platform/streams/consumer.py +106 -0
  33. codex_platform/streams/dispatcher.py +175 -0
  34. codex_platform/streams/processor.py +217 -0
  35. codex_platform/streams/producer.py +60 -0
  36. codex_platform/streams/router.py +78 -0
  37. codex_platform/workers/__init__.py +1 -0
  38. codex_platform/workers/arq/__init__.py +27 -0
  39. codex_platform/workers/arq/base.py +170 -0
  40. codex_platform/workers/arq/config.py +67 -0
  41. codex_platform/workers/arq/task_utils.py +72 -0
  42. codex_platform/workers/arq/types.py +11 -0
  43. codex_platform-0.1.0.dist-info/METADATA +120 -0
  44. codex_platform-0.1.0.dist-info/RECORD +45 -0
  45. codex_platform-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,219 @@
1
+ """
2
+ codex_platform.redis_service.operations.list_
3
+ ==============================================
4
+ Redis List operations.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any
9
+
10
+ from redis.asyncio import Redis
11
+
12
+ from codex_platform.redis_service.base import catch_redis_errors
13
+ from codex_platform.redis_service.keys import BaseRedisKey, resolve_key
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+
18
+ class ListOperations:
19
+ """Redis List operations (RPUSH / LPUSH / LPOP / LRANGE and more).
20
+
21
+ Accepts an already-constructed ``redis.asyncio.Redis`` client.
22
+ All methods wrap Redis errors in typed exceptions.
23
+
24
+ Example::
25
+
26
+ ops = ListOperations(client)
27
+ await ops.push("queue:jobs", "job-1", "job-2")
28
+ job = await ops.pop("queue:jobs")
29
+ """
30
+
31
+ def __init__(self, client: Redis) -> None:
32
+ self.client = client
33
+
34
+ @catch_redis_errors
35
+ async def push(self, key: "str | BaseRedisKey", *values: str, **kwargs: Any) -> int:
36
+ """Append one or more values to the end of a list (RPUSH).
37
+
38
+ Args:
39
+ key: Redis key or a ``BaseRedisKey`` instance.
40
+ *values: String values to append.
41
+ **kwargs: Extra parameters forwarded to ``resolve_key``.
42
+
43
+ Returns:
44
+ New length of the list after the operation.
45
+
46
+ Raises:
47
+ RedisConnectionError: Redis connection failure.
48
+ RedisServiceError: Redis operation failure.
49
+ """
50
+ real_key = resolve_key(key, **kwargs)
51
+ return int(await self.client.rpush(real_key, *values))
52
+
53
+ @catch_redis_errors
54
+ async def lpush(self, key: "str | BaseRedisKey", *values: str, **kwargs: Any) -> int:
55
+ """Prepend one or more values to the beginning of a list (LPUSH).
56
+
57
+ Args:
58
+ key: Redis key or a ``BaseRedisKey`` instance.
59
+ *values: String values to prepend.
60
+ **kwargs: Extra parameters forwarded to ``resolve_key``.
61
+
62
+ Returns:
63
+ New length of the list after the operation.
64
+
65
+ Raises:
66
+ RedisConnectionError: Redis connection failure.
67
+ RedisServiceError: Redis operation failure.
68
+ """
69
+ real_key = resolve_key(key, **kwargs)
70
+ return int(await self.client.lpush(real_key, *values))
71
+
72
+ @catch_redis_errors
73
+ async def pop(self, key: "str | BaseRedisKey", **kwargs: Any) -> str | None:
74
+ """Remove and return the first element of a list (LPOP).
75
+
76
+ Args:
77
+ key: Redis key or a ``BaseRedisKey`` instance.
78
+ **kwargs: Extra parameters forwarded to ``resolve_key``.
79
+
80
+ Returns:
81
+ The first element, or ``None`` if the list is empty or does not exist.
82
+
83
+ Raises:
84
+ RedisConnectionError: Redis connection failure.
85
+ RedisServiceError: Redis operation failure.
86
+ """
87
+ real_key = resolve_key(key, **kwargs)
88
+ value = await self.client.lpop(real_key)
89
+ return str(value) if value is not None else None
90
+
91
+ @catch_redis_errors
92
+ async def rpop(self, key: "str | BaseRedisKey", **kwargs: Any) -> str | None:
93
+ """Remove and return the last element of a list (RPOP).
94
+
95
+ Args:
96
+ key: Redis key or a ``BaseRedisKey`` instance.
97
+ **kwargs: Extra parameters forwarded to ``resolve_key``.
98
+
99
+ Returns:
100
+ The last element, or ``None`` if the list is empty or does not exist.
101
+
102
+ Raises:
103
+ RedisConnectionError: Redis connection failure.
104
+ RedisServiceError: Redis operation failure.
105
+ """
106
+ real_key = resolve_key(key, **kwargs)
107
+ value = await self.client.rpop(real_key)
108
+ return str(value) if value is not None else None
109
+
110
+ @catch_redis_errors
111
+ async def brpop(self, *keys: str, timeout: int = 0) -> tuple[str, str] | None:
112
+ """Blocking pop of the last element from the first non-empty list (BRPOP).
113
+
114
+ Args:
115
+ *keys: One or more Redis key strings to check in order.
116
+ timeout: Maximum seconds to block. ``0`` blocks indefinitely.
117
+
118
+ Returns:
119
+ ``(key, value)`` tuple when an element is available,
120
+ or ``None`` if the timeout expires.
121
+
122
+ Raises:
123
+ RedisConnectionError: Redis connection failure.
124
+ RedisServiceError: Redis operation failure.
125
+ """
126
+ result = await self.client.brpop(list(keys), timeout=timeout)
127
+ if result:
128
+ return str(result[0]), str(result[1])
129
+ return None
130
+
131
+ @catch_redis_errors
132
+ async def range(self, key: "str | BaseRedisKey", start: int = 0, end: int = -1, **kwargs: Any) -> list[str]:
133
+ """Return a slice of list elements by index range (LRANGE).
134
+
135
+ Args:
136
+ key: Redis key or a ``BaseRedisKey`` instance.
137
+ start: Start index (0-based). Negative values count from the end.
138
+ end: End index inclusive. ``-1`` means the last element.
139
+ **kwargs: Extra parameters forwarded to ``resolve_key``.
140
+
141
+ Returns:
142
+ List of elements in the requested range.
143
+
144
+ Raises:
145
+ RedisConnectionError: Redis connection failure.
146
+ RedisServiceError: Redis operation failure.
147
+ """
148
+ real_key = resolve_key(key, **kwargs)
149
+ return await self.client.lrange(real_key, start, end)
150
+
151
+ @catch_redis_errors
152
+ async def length(self, key: "str | BaseRedisKey", **kwargs: Any) -> int:
153
+ """Return the number of elements in a list (LLEN).
154
+
155
+ Args:
156
+ key: Redis key or a ``BaseRedisKey`` instance.
157
+ **kwargs: Extra parameters forwarded to ``resolve_key``.
158
+
159
+ Returns:
160
+ List length. ``0`` if the key does not exist.
161
+
162
+ Raises:
163
+ RedisConnectionError: Redis connection failure.
164
+ RedisServiceError: Redis operation failure.
165
+ """
166
+ real_key = resolve_key(key, **kwargs)
167
+ return int(await self.client.llen(real_key))
168
+
169
+ @catch_redis_errors
170
+ async def set_index(self, key: "str | BaseRedisKey", index: int, value: str, **kwargs: Any) -> None:
171
+ """Set the element at a specific list index (LSET).
172
+
173
+ Args:
174
+ key: Redis key or a ``BaseRedisKey`` instance.
175
+ index: Zero-based index. Negative values count from the end.
176
+ value: New value to set at the given index.
177
+ **kwargs: Extra parameters forwarded to ``resolve_key``.
178
+
179
+ Raises:
180
+ RedisConnectionError: Redis connection failure.
181
+ RedisServiceError: Redis operation failure.
182
+ """
183
+ real_key = resolve_key(key, **kwargs)
184
+ await self.client.lset(real_key, index, value)
185
+
186
+ @catch_redis_errors
187
+ async def trim(self, key: "str | BaseRedisKey", start: int, end: int, **kwargs: Any) -> None:
188
+ """Trim a list to the specified index range, discarding all other elements (LTRIM).
189
+
190
+ Args:
191
+ key: Redis key or a ``BaseRedisKey`` instance.
192
+ start: Start index (inclusive).
193
+ end: End index (inclusive).
194
+ **kwargs: Extra parameters forwarded to ``resolve_key``.
195
+
196
+ Raises:
197
+ RedisConnectionError: Redis connection failure.
198
+ RedisServiceError: Redis operation failure.
199
+ """
200
+ real_key = resolve_key(key, **kwargs)
201
+ await self.client.ltrim(real_key, start, end)
202
+
203
+ @catch_redis_errors
204
+ async def move(self, src: str, dst: str) -> str | None:
205
+ """Atomically move the last element of ``src`` to the head of ``dst`` (RPOPLPUSH).
206
+
207
+ Args:
208
+ src: Source list key.
209
+ dst: Destination list key.
210
+
211
+ Returns:
212
+ The moved element, or ``None`` if ``src`` is empty.
213
+
214
+ Raises:
215
+ RedisConnectionError: Redis connection failure.
216
+ RedisServiceError: Redis operation failure.
217
+ """
218
+ value = await self.client.rpoplpush(src, dst)
219
+ return str(value) if value is not None else None
@@ -0,0 +1,197 @@
1
+ """
2
+ codex_platform.redis_service.operations.pipeline
3
+ =================================================
4
+ Redis Pipeline & Atomic operations.
5
+
6
+ Two modes:
7
+ - ``execute`` — pipeline without MULTI/EXEC (batching, not atomic).
8
+ - ``transact`` — pipeline with MULTI/EXEC (atomic transaction).
9
+ - ``atomic`` — async context manager that yields a Pipeline in MULTI/EXEC mode.
10
+ - ``eval_script`` — Lua script execution (always atomic in Redis).
11
+
12
+ Usage::
13
+
14
+ # Batching (faster, not atomic)
15
+ results = await ops.pipeline.execute(build_fn)
16
+
17
+ # Atomic transaction (MULTI/EXEC)
18
+ results = await ops.pipeline.transact(build_fn)
19
+
20
+ # Atomic context manager
21
+ async with ops.pipeline.atomic() as pipe:
22
+ await pipe.set("k1", "v1")
23
+ await pipe.incr("counter")
24
+ # committed on exit
25
+
26
+ # Lua script
27
+ await ops.pipeline.eval_script(
28
+ "return redis.call('SET', KEYS[1], ARGV[1])",
29
+ keys=["mykey"], args=["value"],
30
+ )
31
+ """
32
+
33
+ import logging
34
+ from collections.abc import AsyncIterator, Awaitable, Callable
35
+ from contextlib import asynccontextmanager
36
+ from typing import Any
37
+
38
+ from redis.asyncio import Redis
39
+ from redis.asyncio.client import Pipeline
40
+
41
+ from codex_platform.redis_service.base import catch_redis_errors
42
+ from codex_platform.redis_service.exceptions import RedisConnectionError, RedisServiceError
43
+
44
+ log = logging.getLogger(__name__)
45
+
46
+
47
+ class PipelineOperations:
48
+ """Redis pipeline and atomic transaction operations.
49
+
50
+ Provides three execution modes:
51
+
52
+ - **Batching** (``execute``) — commands are sent in one round-trip, not atomic.
53
+ - **Transaction** (``transact``) — commands are wrapped in MULTI/EXEC, atomic.
54
+ - **Context manager** (``atomic``) — atomic block with automatic EXEC on exit.
55
+
56
+ Also exposes Lua script execution via ``eval_script``.
57
+ """
58
+
59
+ def __init__(self, client: Redis) -> None:
60
+ self.client = client
61
+
62
+ @catch_redis_errors
63
+ async def execute(self, builder_func: Callable[[Pipeline], Awaitable[None]]) -> list[Any]:
64
+ """Execute a sequence of commands in a pipeline without MULTI/EXEC (batching).
65
+
66
+ Commands are sent in a single round-trip but are **not** atomic.
67
+ Use ``transact`` or ``atomic`` when atomicity is required.
68
+
69
+ Args:
70
+ builder_func: Async function that receives a ``Pipeline`` and queues commands.
71
+
72
+ Returns:
73
+ List of results for each queued command, in order.
74
+
75
+ Raises:
76
+ RedisConnectionError: Redis connection failure.
77
+ RedisServiceError: Redis operation failure.
78
+
79
+ Example::
80
+
81
+ async def build(pipe):
82
+ await pipe.set("k1", "v1")
83
+ await pipe.set("k2", "v2")
84
+
85
+ results = await ops.execute(build)
86
+ """
87
+ async with self.client.pipeline(transaction=False) as pipe:
88
+ await builder_func(pipe)
89
+ results = await pipe.execute()
90
+ log.debug("PipelineOps | execute commands=%d", len(results))
91
+ return results
92
+
93
+ @catch_redis_errors
94
+ async def transact(self, builder_func: Callable[[Pipeline], Awaitable[None]]) -> list[Any]:
95
+ """Execute commands in an atomic transaction (MULTI/EXEC).
96
+
97
+ All commands inside ``builder_func`` are executed atomically.
98
+ If the server crashes before EXEC, none of the commands are applied.
99
+
100
+ Args:
101
+ builder_func: Async function that receives a ``Pipeline`` and queues commands.
102
+
103
+ Returns:
104
+ List of results for each queued command, in order.
105
+
106
+ Raises:
107
+ RedisConnectionError: Redis connection failure.
108
+ RedisServiceError: Redis operation failure.
109
+
110
+ Example::
111
+
112
+ async def transfer(pipe):
113
+ await pipe.decrby("wallet:from", 100)
114
+ await pipe.incrby("wallet:to", 100)
115
+
116
+ results = await ops.transact(transfer)
117
+ """
118
+ async with self.client.pipeline(transaction=True) as pipe:
119
+ await builder_func(pipe)
120
+ results = await pipe.execute()
121
+ log.debug("PipelineOps | transact commands=%d", len(results))
122
+ return results
123
+
124
+ @asynccontextmanager
125
+ async def atomic(self) -> AsyncIterator[Pipeline]:
126
+ """Async context manager for an atomic block (MULTI/EXEC).
127
+
128
+ Opens a Pipeline in transaction mode. EXEC is called automatically on exit.
129
+
130
+ Yields:
131
+ ``Pipeline`` instance in MULTI/EXEC mode.
132
+
133
+ Raises:
134
+ RedisConnectionError: Redis connection failure.
135
+ RedisServiceError: Redis operation failure.
136
+
137
+ Example::
138
+
139
+ async with service.pipeline.atomic() as pipe:
140
+ await pipe.set("session:abc", "token")
141
+ await pipe.expire("session:abc", 3600)
142
+ # EXEC is called automatically on context exit
143
+ """
144
+ from redis.exceptions import ConnectionError, RedisError, TimeoutError
145
+
146
+ pipe: Pipeline = self.client.pipeline(transaction=True)
147
+ try:
148
+ async with pipe as p:
149
+ yield p
150
+ await p.execute()
151
+ log.debug("PipelineOps | atomic block committed")
152
+ except (ConnectionError, TimeoutError) as e:
153
+ raise RedisConnectionError(f"Atomic block connection failed: {e}") from e
154
+ except RedisError as e:
155
+ raise RedisServiceError(f"Atomic block error: {e}") from e
156
+
157
+ @catch_redis_errors
158
+ async def eval_script(
159
+ self,
160
+ script: str,
161
+ keys: list[str] | None = None,
162
+ args: list[Any] | None = None,
163
+ ) -> Any:
164
+ """Execute a Lua script (EVAL). Lua scripts are always atomic in Redis.
165
+
166
+ Use for complex read-modify-write operations that must not be interrupted.
167
+
168
+ Args:
169
+ script: Lua script source code.
170
+ keys: List of Redis keys accessible as ``KEYS[1]``, ``KEYS[2]``, etc.
171
+ args: Script arguments accessible as ``ARGV[1]``, ``ARGV[2]``, etc.
172
+
173
+ Returns:
174
+ Value returned by the Lua script.
175
+
176
+ Raises:
177
+ RedisConnectionError: Redis connection failure.
178
+ RedisServiceError: Redis operation failure.
179
+
180
+ Example::
181
+
182
+ result = await ops.eval_script(
183
+ \"\"\"
184
+ local val = redis.call('GET', KEYS[1])
185
+ if val == ARGV[1] then
186
+ redis.call('SET', KEYS[1], ARGV[2])
187
+ return 1
188
+ end
189
+ return 0
190
+ \"\"\",
191
+ keys=["mykey"],
192
+ args=["expected", "new_value"],
193
+ )
194
+ """
195
+ result = await self.client.eval(script, len(keys or []), *(keys or []), *(args or []))
196
+ log.debug("PipelineOps | eval_script keys=%d args=%d", len(keys or []), len(args or []))
197
+ return result
@@ -0,0 +1,212 @@
1
+ """
2
+ codex_platform.redis_service.operations.set_
3
+ =============================================
4
+ Redis Set operations.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any
9
+
10
+ from redis.asyncio import Redis
11
+
12
+ from codex_platform.redis_service.base import catch_redis_errors
13
+ from codex_platform.redis_service.keys import BaseRedisKey, resolve_key
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+
18
+ class SetOperations:
19
+ """Redis Set operations (SADD / SREM / SMEMBERS / SISMEMBER and more).
20
+
21
+ Accepts an already-constructed ``redis.asyncio.Redis`` client.
22
+ All methods wrap Redis errors in typed exceptions.
23
+
24
+ Example::
25
+
26
+ ops = SetOperations(client)
27
+ await ops.add("tags:post:42", "python", "redis")
28
+ has_tag = await ops.is_member("tags:post:42", "python")
29
+ """
30
+
31
+ def __init__(self, client: Redis) -> None:
32
+ self.client = client
33
+
34
+ @catch_redis_errors
35
+ async def add(self, key: "str | BaseRedisKey", *values: str | int, **kwargs: Any) -> int:
36
+ """Add one or more members to a set (SADD).
37
+
38
+ Args:
39
+ key: Redis key or a ``BaseRedisKey`` instance.
40
+ *values: Values to add. Integers are coerced to strings.
41
+ **kwargs: Extra parameters forwarded to ``resolve_key``.
42
+
43
+ Returns:
44
+ Number of members actually added (already-existing members are not counted).
45
+
46
+ Raises:
47
+ RedisConnectionError: Redis connection failure.
48
+ RedisServiceError: Redis operation failure.
49
+ """
50
+ real_key = resolve_key(key, **kwargs)
51
+ return int(await self.client.sadd(real_key, *[str(v) for v in values]))
52
+
53
+ @catch_redis_errors
54
+ async def remove(self, key: "str | BaseRedisKey", *values: str | int, **kwargs: Any) -> int:
55
+ """Remove one or more members from a set (SREM).
56
+
57
+ Args:
58
+ key: Redis key or a ``BaseRedisKey`` instance.
59
+ *values: Values to remove. Integers are coerced to strings.
60
+ **kwargs: Extra parameters forwarded to ``resolve_key``.
61
+
62
+ Returns:
63
+ Number of members actually removed.
64
+
65
+ Raises:
66
+ RedisConnectionError: Redis connection failure.
67
+ RedisServiceError: Redis operation failure.
68
+ """
69
+ real_key = resolve_key(key, **kwargs)
70
+ return int(await self.client.srem(real_key, *[str(v) for v in values]))
71
+
72
+ @catch_redis_errors
73
+ async def members(self, key: "str | BaseRedisKey", **kwargs: Any) -> set[str]:
74
+ """Return all members of a set (SMEMBERS).
75
+
76
+ Args:
77
+ key: Redis key or a ``BaseRedisKey`` instance.
78
+ **kwargs: Extra parameters forwarded to ``resolve_key``.
79
+
80
+ Returns:
81
+ Set of all member strings. Empty set if the key does not exist.
82
+
83
+ Raises:
84
+ RedisConnectionError: Redis connection failure.
85
+ RedisServiceError: Redis operation failure.
86
+ """
87
+ real_key = resolve_key(key, **kwargs)
88
+ return await self.client.smembers(real_key)
89
+
90
+ @catch_redis_errors
91
+ async def is_member(self, key: "str | BaseRedisKey", value: str | int, **kwargs: Any) -> bool:
92
+ """Check whether a value is a member of a set (SISMEMBER).
93
+
94
+ Args:
95
+ key: Redis key or a ``BaseRedisKey`` instance.
96
+ value: Value to check. Integers are coerced to strings.
97
+ **kwargs: Extra parameters forwarded to ``resolve_key``.
98
+
99
+ Returns:
100
+ ``True`` if the value is in the set, ``False`` otherwise.
101
+
102
+ Raises:
103
+ RedisConnectionError: Redis connection failure.
104
+ RedisServiceError: Redis operation failure.
105
+ """
106
+ real_key = resolve_key(key, **kwargs)
107
+ return bool(await self.client.sismember(real_key, str(value)))
108
+
109
+ @catch_redis_errors
110
+ async def card(self, key: "str | BaseRedisKey", **kwargs: Any) -> int:
111
+ """Return the number of members in a set (SCARD).
112
+
113
+ Args:
114
+ key: Redis key or a ``BaseRedisKey`` instance.
115
+ **kwargs: Extra parameters forwarded to ``resolve_key``.
116
+
117
+ Returns:
118
+ Set cardinality. ``0`` if the key does not exist.
119
+
120
+ Raises:
121
+ RedisConnectionError: Redis connection failure.
122
+ RedisServiceError: Redis operation failure.
123
+ """
124
+ real_key = resolve_key(key, **kwargs)
125
+ return int(await self.client.scard(real_key))
126
+
127
+ @catch_redis_errors
128
+ async def pop(self, key: "str | BaseRedisKey", count: int = 1, **kwargs: Any) -> set[str]:
129
+ """Remove and return random members from a set (SPOP).
130
+
131
+ Args:
132
+ key: Redis key or a ``BaseRedisKey`` instance.
133
+ count: Number of members to pop. Defaults to ``1``.
134
+ **kwargs: Extra parameters forwarded to ``resolve_key``.
135
+
136
+ Returns:
137
+ Set of popped members. Empty set if the key does not exist.
138
+
139
+ Raises:
140
+ RedisConnectionError: Redis connection failure.
141
+ RedisServiceError: Redis operation failure.
142
+ """
143
+ real_key = resolve_key(key, **kwargs)
144
+ result = await self.client.spop(real_key, count)
145
+ return set(result) if result else set()
146
+
147
+ @catch_redis_errors
148
+ async def random(self, key: "str | BaseRedisKey", count: int = 1, **kwargs: Any) -> list[str]:
149
+ """Return random members without removing them (SRANDMEMBER).
150
+
151
+ Args:
152
+ key: Redis key or a ``BaseRedisKey`` instance.
153
+ count: Number of random members to return.
154
+ **kwargs: Extra parameters forwarded to ``resolve_key``.
155
+
156
+ Returns:
157
+ List of random members (may contain duplicates if ``count > cardinality``).
158
+
159
+ Raises:
160
+ RedisConnectionError: Redis connection failure.
161
+ RedisServiceError: Redis operation failure.
162
+ """
163
+ real_key = resolve_key(key, **kwargs)
164
+ return await self.client.srandmember(real_key, count)
165
+
166
+ @catch_redis_errors
167
+ async def union(self, *keys: str) -> set[str]:
168
+ """Return the union of multiple sets (SUNION).
169
+
170
+ Args:
171
+ *keys: Redis key strings.
172
+
173
+ Returns:
174
+ Set containing all members from all given sets.
175
+
176
+ Raises:
177
+ RedisConnectionError: Redis connection failure.
178
+ RedisServiceError: Redis operation failure.
179
+ """
180
+ return await self.client.sunion(list(keys))
181
+
182
+ @catch_redis_errors
183
+ async def inter(self, *keys: str) -> set[str]:
184
+ """Return the intersection of multiple sets (SINTER).
185
+
186
+ Args:
187
+ *keys: Redis key strings.
188
+
189
+ Returns:
190
+ Set containing only members present in all given sets.
191
+
192
+ Raises:
193
+ RedisConnectionError: Redis connection failure.
194
+ RedisServiceError: Redis operation failure.
195
+ """
196
+ return await self.client.sinter(list(keys))
197
+
198
+ @catch_redis_errors
199
+ async def diff(self, *keys: str) -> set[str]:
200
+ """Return the difference between the first set and all subsequent sets (SDIFF).
201
+
202
+ Args:
203
+ *keys: Redis key strings. The first key is the base set.
204
+
205
+ Returns:
206
+ Set of members present in the first set but not in any of the others.
207
+
208
+ Raises:
209
+ RedisConnectionError: Redis connection failure.
210
+ RedisServiceError: Redis operation failure.
211
+ """
212
+ return await self.client.sdiff(list(keys))