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,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codex_platform.notifications.channels.registry
|
|
3
|
+
=============================================
|
|
4
|
+
Auto-discovers available delivery channels based on configuration.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
registry = ChannelRegistry()
|
|
8
|
+
registry.register("smtp", lambda cfg: SmtpChannel(cfg) if cfg.SMTP_HOST else None)
|
|
9
|
+
registry.register("sendgrid", lambda cfg: SendGridChannel(cfg) if cfg.SENDGRID_API_KEY else None)
|
|
10
|
+
channels = registry.build_channels(settings)
|
|
11
|
+
# → [SmtpChannel, SendGridChannel] (только те, чей конфиг заполнен)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from .orchestrator import DeliveryChannel
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChannelRegistry:
|
|
24
|
+
"""
|
|
25
|
+
Registry for delivery channels.
|
|
26
|
+
Channels register with a factory function that returns a channel or None.
|
|
27
|
+
build_channels() creates only the channels whose config is available.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
self._factories: list[tuple[str, Callable[[Any], DeliveryChannel | None]]] = []
|
|
32
|
+
|
|
33
|
+
def register(
|
|
34
|
+
self,
|
|
35
|
+
name: str,
|
|
36
|
+
factory: Callable[[Any], DeliveryChannel | None],
|
|
37
|
+
) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Register a channel factory.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
name: Human-readable channel name (for logging).
|
|
43
|
+
factory: Callable that takes config and returns DeliveryChannel or None.
|
|
44
|
+
Return None if the channel's config is missing/incomplete.
|
|
45
|
+
"""
|
|
46
|
+
self._factories.append((name, factory))
|
|
47
|
+
|
|
48
|
+
def build_channels(self, config: Any) -> list[DeliveryChannel]:
|
|
49
|
+
"""Build the list of available channels from all registered factories.
|
|
50
|
+
|
|
51
|
+
Calls each factory with ``config``. A channel is included only when the
|
|
52
|
+
factory returns a non-``None`` instance that also reports ``is_available() == True``.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
config: Application settings object passed verbatim to every factory.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Ordered list of ready-to-use :class:`DeliveryChannel` instances.
|
|
59
|
+
"""
|
|
60
|
+
channels: list[DeliveryChannel] = []
|
|
61
|
+
for name, factory in self._factories:
|
|
62
|
+
try:
|
|
63
|
+
channel = factory(config)
|
|
64
|
+
if channel is not None and channel.is_available():
|
|
65
|
+
channels.append(channel)
|
|
66
|
+
log.info("ChannelRegistry | %s enabled", name)
|
|
67
|
+
else:
|
|
68
|
+
log.debug("ChannelRegistry | %s skipped (not configured)", name)
|
|
69
|
+
except Exception:
|
|
70
|
+
log.exception("ChannelRegistry | %s factory failed", name)
|
|
71
|
+
return channels
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codex_platform.notifications.renderer
|
|
3
|
+
======================================
|
|
4
|
+
Jinja2-based HTML template renderer for notification workers.
|
|
5
|
+
|
|
6
|
+
OPTIONAL DEPENDENCY
|
|
7
|
+
-------------------
|
|
8
|
+
This module requires ``jinja2`` which is NOT included in the default
|
|
9
|
+
``codex-platform`` installation.
|
|
10
|
+
|
|
11
|
+
There are two delivery modes:
|
|
12
|
+
|
|
13
|
+
Mode 1 — Worker renders templates itself (needs Jinja2):
|
|
14
|
+
Worker receives ``template_name`` + ``context_data``,
|
|
15
|
+
renders HTML internally, then sends via SMTP.
|
|
16
|
+
|
|
17
|
+
Mode 2 — Pre-rendered HTML (no Jinja2 needed):
|
|
18
|
+
Django (or any other layer) renders the template and passes
|
|
19
|
+
ready ``html_content`` directly to the workers for delivery only.
|
|
20
|
+
|
|
21
|
+
If you are using Mode 1, add the following to your project dependencies:
|
|
22
|
+
|
|
23
|
+
# pyproject.toml
|
|
24
|
+
dependencies = [
|
|
25
|
+
"jinja2>=3.1",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
# or via pip
|
|
29
|
+
pip install jinja2
|
|
30
|
+
|
|
31
|
+
Usage
|
|
32
|
+
-----
|
|
33
|
+
from codex_platform.notifications.renderer import TemplateRenderer
|
|
34
|
+
|
|
35
|
+
renderer = TemplateRenderer(templates_dir="/path/to/workers/templates")
|
|
36
|
+
html = renderer.render("booking/bk_confirmation.html", context={...})
|
|
37
|
+
|
|
38
|
+
Template directory structure (recommended)
|
|
39
|
+
------------------------------------------
|
|
40
|
+
templates/
|
|
41
|
+
├── base_email.html
|
|
42
|
+
├── booking/
|
|
43
|
+
│ ├── bk_confirmation.html
|
|
44
|
+
│ └── bk_cancellation.html
|
|
45
|
+
├── contacts/
|
|
46
|
+
│ └── ct_receipt.html
|
|
47
|
+
└── marketing/
|
|
48
|
+
└── mk_reengagement.html
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
import logging
|
|
52
|
+
import os
|
|
53
|
+
|
|
54
|
+
log = logging.getLogger(__name__)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
58
|
+
|
|
59
|
+
_JINJA2_AVAILABLE = True
|
|
60
|
+
except ImportError:
|
|
61
|
+
_JINJA2_AVAILABLE = False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TemplateRenderer:
|
|
65
|
+
"""
|
|
66
|
+
Jinja2 HTML template renderer.
|
|
67
|
+
|
|
68
|
+
Requires ``jinja2`` — install via ``pip install codex-platform[jinja2]``.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
RuntimeError: If jinja2 is not installed.
|
|
72
|
+
FileNotFoundError: If the templates directory does not exist.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, templates_dir: str) -> None:
|
|
76
|
+
if not _JINJA2_AVAILABLE:
|
|
77
|
+
raise RuntimeError(
|
|
78
|
+
"jinja2 is not installed. "
|
|
79
|
+
"Run: pip install codex-platform[jinja2]\n"
|
|
80
|
+
"If you are passing pre-rendered html_content to the workers, "
|
|
81
|
+
"you do not need TemplateRenderer at all."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if not os.path.exists(templates_dir):
|
|
85
|
+
raise FileNotFoundError(f"Templates directory not found: {templates_dir}")
|
|
86
|
+
|
|
87
|
+
self.env = Environment(
|
|
88
|
+
loader=FileSystemLoader(templates_dir),
|
|
89
|
+
autoescape=select_autoescape(["html", "xml"]),
|
|
90
|
+
)
|
|
91
|
+
self.templates_dir = templates_dir
|
|
92
|
+
log.info("TemplateRenderer | initialized with dir='%s'", templates_dir)
|
|
93
|
+
|
|
94
|
+
def render(self, template_name: str, context: dict[str, object]) -> str:
|
|
95
|
+
"""
|
|
96
|
+
Render a template with the given context.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
template_name: Relative path to template (e.g. 'booking/bk_confirmation.html').
|
|
100
|
+
context: Dictionary of variables passed to the template.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Rendered HTML string.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
jinja2.TemplateNotFound: If the template file does not exist.
|
|
107
|
+
jinja2.TemplateError: On rendering errors.
|
|
108
|
+
"""
|
|
109
|
+
log.debug("TemplateRenderer | rendering template='%s'", template_name)
|
|
110
|
+
template = self.env.get_template(template_name)
|
|
111
|
+
return template.render(context)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codex_platform.redis_service
|
|
3
|
+
==========================
|
|
4
|
+
Async Redis service with mixin architecture.
|
|
5
|
+
|
|
6
|
+
Requires ``redis.asyncio.Redis`` client. All methods are async-only.
|
|
7
|
+
|
|
8
|
+
Quick start::
|
|
9
|
+
|
|
10
|
+
from codex_platform.redis_service import RedisService
|
|
11
|
+
|
|
12
|
+
service = RedisService(client=redis_client)
|
|
13
|
+
await service.set_hash_json("my_key", "field", {"a": 1})
|
|
14
|
+
|
|
15
|
+
Custom composition::
|
|
16
|
+
|
|
17
|
+
from codex_platform.redis_service import BaseRedisService, HashMixin, StreamMixin
|
|
18
|
+
|
|
19
|
+
class MyRedisService(BaseRedisService, HashMixin, StreamMixin):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
Key Registry::
|
|
23
|
+
|
|
24
|
+
from codex_platform.redis_service import UserKey
|
|
25
|
+
|
|
26
|
+
await service.get_hash_json(UserKey(), "profile", user_id=42)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from .base import BaseRedisService
|
|
30
|
+
from .keys import BaseRedisKey, SessionKey, UserKey
|
|
31
|
+
from .operations import (
|
|
32
|
+
HashOperations,
|
|
33
|
+
JsonModuleOperations,
|
|
34
|
+
JsonStringOperations,
|
|
35
|
+
ListOperations,
|
|
36
|
+
PipelineOperations,
|
|
37
|
+
SetOperations,
|
|
38
|
+
StringOperations,
|
|
39
|
+
ZSetOperations,
|
|
40
|
+
)
|
|
41
|
+
from .service import RedisService
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"BaseRedisService",
|
|
45
|
+
"RedisService",
|
|
46
|
+
# Operations
|
|
47
|
+
"HashOperations",
|
|
48
|
+
"SetOperations",
|
|
49
|
+
"ListOperations",
|
|
50
|
+
"StringOperations",
|
|
51
|
+
"ZSetOperations",
|
|
52
|
+
"JsonStringOperations",
|
|
53
|
+
"JsonModuleOperations",
|
|
54
|
+
"PipelineOperations",
|
|
55
|
+
# Key Registry
|
|
56
|
+
"BaseRedisKey",
|
|
57
|
+
"UserKey",
|
|
58
|
+
"SessionKey",
|
|
59
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codex_platform.redis_service.base
|
|
3
|
+
=================================
|
|
4
|
+
Core Redis infrastructure components.
|
|
5
|
+
|
|
6
|
+
Contains:
|
|
7
|
+
- catch_redis_errors — decorator that converts redis-py errors to domain exceptions
|
|
8
|
+
- BaseRedisService — base class holding a Redis connection
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from functools import wraps
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from redis.asyncio import Redis
|
|
17
|
+
from redis.exceptions import ConnectionError, RedisError, TimeoutError
|
|
18
|
+
|
|
19
|
+
from codex_platform.redis_service.exceptions import RedisConnectionError, RedisServiceError
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def catch_redis_errors(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
25
|
+
"""Decorator that converts redis-py exceptions into typed domain exceptions.
|
|
26
|
+
|
|
27
|
+
Never suppresses errors — always re-raises as a domain exception.
|
|
28
|
+
Preserves the original traceback via ``raise ... from e``.
|
|
29
|
+
|
|
30
|
+
Catches:
|
|
31
|
+
- ``ConnectionError``, ``TimeoutError`` → :exc:`RedisConnectionError`
|
|
32
|
+
- ``RedisError`` → :exc:`RedisServiceError`
|
|
33
|
+
|
|
34
|
+
Does NOT catch:
|
|
35
|
+
``JSONDecodeError``, ``TypeError`` — data-specific errors are caught
|
|
36
|
+
in individual operations and re-raised as :exc:`RedisDataError`.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
@wraps(func)
|
|
40
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
41
|
+
try:
|
|
42
|
+
return await func(*args, **kwargs)
|
|
43
|
+
except (ConnectionError, TimeoutError) as e:
|
|
44
|
+
msg = f"Network failure in {func.__name__}: {e}"
|
|
45
|
+
log.exception("Redis | error=connection_failed fn='%s'", func.__name__)
|
|
46
|
+
raise RedisConnectionError(msg) from e
|
|
47
|
+
except RedisError as e:
|
|
48
|
+
msg = f"Operation failed in {func.__name__}: {e}"
|
|
49
|
+
log.exception("Redis | error=operation_failed fn='%s'", func.__name__)
|
|
50
|
+
raise RedisServiceError(msg) from e
|
|
51
|
+
|
|
52
|
+
return wrapper
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class BaseRedisService:
|
|
56
|
+
"""Base class that holds a Redis connection.
|
|
57
|
+
|
|
58
|
+
Inherit from this class when building a service that wraps a Redis client
|
|
59
|
+
but does not need the full :class:`~codex_platform.redis_service.service.RedisService`
|
|
60
|
+
composition.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, client: Redis) -> None:
|
|
64
|
+
"""Initialize the service with an existing async Redis client.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
client: An already-constructed ``redis.asyncio.Redis`` instance.
|
|
68
|
+
"""
|
|
69
|
+
self.redis_client = client
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codex_platform.redis_service.exceptions
|
|
3
|
+
=======================================
|
|
4
|
+
Кастомные исключения для работы с Redis.
|
|
5
|
+
|
|
6
|
+
Используй эти классы в бизнес-логике вместо redis.exceptions.*
|
|
7
|
+
чтобы не зависеть от внутренностей redis-py.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RedisServiceError(Exception):
|
|
12
|
+
"""Базовый класс для всех ошибок слоя Redis."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RedisConnectionError(RedisServiceError):
|
|
16
|
+
"""Ошибки сети: таймауты, недоступность сервера, обрывы связи."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RedisDataError(RedisServiceError):
|
|
20
|
+
"""Ошибки контента: битый JSON, несовпадение типов."""
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codex_platform.redis_service.keys
|
|
3
|
+
================================
|
|
4
|
+
Redis Key Registry — typed, centralized key definitions.
|
|
5
|
+
|
|
6
|
+
Eliminates "magic strings" and prevents key format mismatches
|
|
7
|
+
across multiple containers (bot, workers, API) that share the same Redis.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
class UserKey(BaseRedisKey):
|
|
12
|
+
template = "u:{user_id}"
|
|
13
|
+
|
|
14
|
+
key = UserKey().build(user_id=42) # → "u:42"
|
|
15
|
+
|
|
16
|
+
# Or via resolve_key:
|
|
17
|
+
from codex_platform.redis_service.keys import resolve_key
|
|
18
|
+
resolve_key(UserKey(), user_id=42) # → "u:42"
|
|
19
|
+
resolve_key("u:42") # → "u:42"
|
|
20
|
+
|
|
21
|
+
Extending for project-specific keys::
|
|
22
|
+
|
|
23
|
+
from codex_platform.redis_service.keys import BaseRedisKey
|
|
24
|
+
|
|
25
|
+
class JobStatusKey(BaseRedisKey):
|
|
26
|
+
template = "arq:job:{job_id}:status"
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from abc import ABC, abstractmethod
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BaseRedisKey(ABC):
|
|
34
|
+
"""
|
|
35
|
+
Abstract base for all Redis key definitions.
|
|
36
|
+
|
|
37
|
+
Subclass and define `template` with ``{placeholder}`` syntax.
|
|
38
|
+
Call ``.build(**kwargs)`` to construct the final key string.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def template(self) -> str:
|
|
44
|
+
"""Key template string with ``{placeholder}`` syntax."""
|
|
45
|
+
|
|
46
|
+
def build(self, **kwargs: Any) -> str:
|
|
47
|
+
"""Build the final key string by formatting the template with keyword arguments.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
**kwargs: Values for each placeholder defined in ``template``.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Fully resolved Redis key string.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ValueError: If a required placeholder argument is missing.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
return self.template.format(**kwargs)
|
|
60
|
+
except KeyError as e:
|
|
61
|
+
raise ValueError(f"Missing key argument: {e} for template '{self.template}'") from e
|
|
62
|
+
|
|
63
|
+
def __repr__(self) -> str:
|
|
64
|
+
return f"{self.__class__.__name__}(template={self.template!r})"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def resolve_key(key: "str | BaseRedisKey", **kwargs: Any) -> str:
|
|
68
|
+
"""Resolve a key that is either a plain string or a ``BaseRedisKey`` instance.
|
|
69
|
+
|
|
70
|
+
If ``key`` is a :class:`BaseRedisKey`, calls ``key.build(**kwargs)``.
|
|
71
|
+
If ``key`` is a plain string, returns it unchanged.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
key: Redis key string or a ``BaseRedisKey`` instance.
|
|
75
|
+
**kwargs: Placeholder values forwarded to ``BaseRedisKey.build``.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Resolved Redis key string.
|
|
79
|
+
|
|
80
|
+
Example::
|
|
81
|
+
|
|
82
|
+
resolve_key(UserKey(), user_id=42) # → "u:42"
|
|
83
|
+
resolve_key("u:42") # → "u:42"
|
|
84
|
+
"""
|
|
85
|
+
return key.build(**kwargs) if isinstance(key, BaseRedisKey) else key
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Shared keys — common across all services
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class UserKey(BaseRedisKey):
|
|
94
|
+
"""User data hash. Args: user_id."""
|
|
95
|
+
|
|
96
|
+
template = "u:{user_id}"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SessionKey(BaseRedisKey):
|
|
100
|
+
"""Session data. Args: token."""
|
|
101
|
+
|
|
102
|
+
template = "sess:{token}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = [
|
|
106
|
+
"BaseRedisKey",
|
|
107
|
+
"resolve_key",
|
|
108
|
+
"UserKey",
|
|
109
|
+
"SessionKey",
|
|
110
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codex_platform.redis_service.managers
|
|
3
|
+
=======================================
|
|
4
|
+
Business-logic managers built on top of Operations.
|
|
5
|
+
|
|
6
|
+
Create project-specific managers here::
|
|
7
|
+
|
|
8
|
+
from codex_platform.redis_service.managers import BaseRedisManager
|
|
9
|
+
from codex_platform.redis_service.operations import HashOperations
|
|
10
|
+
|
|
11
|
+
class SiteSettingsManager(BaseRedisManager):
|
|
12
|
+
async def get_settings(self) -> dict:
|
|
13
|
+
return await self.hash.get_all("site:settings") or {}
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .base_manager import BaseRedisManager
|
|
17
|
+
|
|
18
|
+
__all__ = ["BaseRedisManager"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codex_platform.redis_service.managers.base_manager
|
|
3
|
+
====================================================
|
|
4
|
+
Base class for Redis business-logic managers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from redis.asyncio import Redis
|
|
8
|
+
|
|
9
|
+
from codex_platform.redis_service.operations import HashOperations, StringOperations
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseRedisManager:
|
|
13
|
+
"""Base manager that initialises common Redis operation helpers.
|
|
14
|
+
|
|
15
|
+
Subclass and add only the operation attributes your manager actually needs.
|
|
16
|
+
|
|
17
|
+
Example::
|
|
18
|
+
|
|
19
|
+
class SiteSettingsManager(BaseRedisManager):
|
|
20
|
+
async def get_settings(self) -> dict:
|
|
21
|
+
return await self.hash.get_all("site:settings") or {}
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, redis_client: Redis) -> None:
|
|
25
|
+
"""Initialize the manager with an existing async Redis client.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
redis_client: An already-constructed ``redis.asyncio.Redis`` instance.
|
|
29
|
+
"""
|
|
30
|
+
self.hash = HashOperations(redis_client)
|
|
31
|
+
self.string = StringOperations(redis_client)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .hash import HashOperations
|
|
2
|
+
from .json_module import JsonModuleOperations
|
|
3
|
+
from .json_string import JsonStringOperations
|
|
4
|
+
from .list_ import ListOperations
|
|
5
|
+
from .pipeline import PipelineOperations
|
|
6
|
+
from .set_ import SetOperations
|
|
7
|
+
from .string import StringOperations
|
|
8
|
+
from .zset import ZSetOperations
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"HashOperations",
|
|
12
|
+
"StringOperations",
|
|
13
|
+
"ListOperations",
|
|
14
|
+
"SetOperations",
|
|
15
|
+
"ZSetOperations",
|
|
16
|
+
"JsonStringOperations",
|
|
17
|
+
"JsonModuleOperations",
|
|
18
|
+
"PipelineOperations",
|
|
19
|
+
]
|