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,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]