added redis adapter
This commit is contained in:
@@ -1,3 +1,17 @@
|
|||||||
# python-repositories
|
# python-repositories
|
||||||
|
|
||||||
Various python repository interfaces exposed as a python package.
|
Various python repository interfaces exposed as a python package.
|
||||||
|
|
||||||
|
## Optional dependencies
|
||||||
|
|
||||||
|
This package supports interacting with multiple different backends:
|
||||||
|
|
||||||
|
- Redis
|
||||||
|
- Mongo
|
||||||
|
- MinIO
|
||||||
|
|
||||||
|
To add support for a specific backend install this package with one or more of these optional packages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv add python-repositories[redis, mongo, minio]
|
||||||
|
```
|
||||||
|
|||||||
+18
-2
@@ -13,13 +13,29 @@ classifiers = [
|
|||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
]
|
]
|
||||||
dependencies = []
|
dependencies = [
|
||||||
|
"python-utils>=0.1.0",
|
||||||
|
"structlog>=25.4.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
redis = [
|
||||||
|
"redis>=6.4.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
python-utils = { index = "gitea" }
|
||||||
|
|
||||||
[[tool.uv.index]]
|
[[tool.uv.index]]
|
||||||
name = "private-cache"
|
name = "threadripper-proxpi-cache"
|
||||||
url = "http://10.0.0.2:5001/index/"
|
url = "http://10.0.0.2:5001/index/"
|
||||||
default = true
|
default = true
|
||||||
|
|
||||||
|
[[tool.uv.index]]
|
||||||
|
name = "gitea"
|
||||||
|
url = "https://gitea.gt-proj.com/api/packages/brian/pypi/simple/"
|
||||||
|
explicit = true
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
python_version = "3.10"
|
python_version = "3.10"
|
||||||
warn_return_any = true # nudge to use stricter types
|
warn_return_any = true # nudge to use stricter types
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""python_repositories: Unified repository interfaces and adapters."""
|
||||||
|
from . import adapters
|
||||||
|
from . import interfaces
|
||||||
|
|
||||||
|
__all__ = ['adapters', 'interfaces']
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from .redis_adapter import RedisAdapter as RedisAdapter
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"RedisAdapter",
|
||||||
|
]
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
"""Definition of RedisAdapter class."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import structlog
|
||||||
|
|
||||||
|
import redis
|
||||||
|
from redis.commands.json.path import Path as RedisPath
|
||||||
|
|
||||||
|
from python_utils import check_env
|
||||||
|
|
||||||
|
from python_repositories.interfaces import (
|
||||||
|
ContextAwareInterface,
|
||||||
|
ConnectionAwareInterface,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RedisAdapter(
|
||||||
|
ContextAwareInterface,
|
||||||
|
ConnectionAwareInterface,
|
||||||
|
):
|
||||||
|
"""Redis adapter exposing basic CRUD functionality."""
|
||||||
|
|
||||||
|
uri_env_var_name: str = "REDIS_URI"
|
||||||
|
path: str = RedisPath.root_path()
|
||||||
|
encoding: str = "UTF-8"
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
# Setup logger
|
||||||
|
self.logger = structlog.get_logger(
|
||||||
|
self.__class__.__name__,
|
||||||
|
)
|
||||||
|
# Check environment variables
|
||||||
|
check_env(self.uri_env_var_name)
|
||||||
|
# Prepare internal variables
|
||||||
|
self._client: redis.Redis | None = None
|
||||||
|
|
||||||
|
def __enter__(self) -> RedisAdapter:
|
||||||
|
"""Enter the context."""
|
||||||
|
self.connect()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: type | None,
|
||||||
|
exc_val: object | None,
|
||||||
|
exc_tb: object | None
|
||||||
|
) -> None:
|
||||||
|
"""Exit the context."""
|
||||||
|
ctx_info = {
|
||||||
|
'exc_type': exc_type,
|
||||||
|
'exc_val': exc_val,
|
||||||
|
'exc_tb': exc_tb
|
||||||
|
}
|
||||||
|
if any(
|
||||||
|
(
|
||||||
|
exc_type is not None,
|
||||||
|
exc_val is not None,
|
||||||
|
exc_tb is not None,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
self.logger.error("Error while exiting context", **ctx_info)
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
|
def connect(self) -> None:
|
||||||
|
"""Connect to the Redis server."""
|
||||||
|
# Prepare arguments
|
||||||
|
uri = str(os.getenv(self.uri_env_var_name))
|
||||||
|
# Connect client
|
||||||
|
client = redis.Redis.from_url(
|
||||||
|
url=uri,
|
||||||
|
socket_connect_timeout=10,
|
||||||
|
)
|
||||||
|
if not client.ping():
|
||||||
|
raise ConnectionError(f"Could not connect to Redis at {uri}")
|
||||||
|
# Persist client
|
||||||
|
self._client = client
|
||||||
|
|
||||||
|
def disconnect(self) -> None:
|
||||||
|
"""Disconnect from the Redis server."""
|
||||||
|
# Close connection
|
||||||
|
if self._client is not None:
|
||||||
|
self._client.close()
|
||||||
|
# Reset client
|
||||||
|
self._client = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""Check if connected to Redis server."""
|
||||||
|
res = bool(isinstance(self._client, redis.Redis))
|
||||||
|
self.logger.debug(res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _set(self, key: str, data: dict) -> None:
|
||||||
|
"""Set a JSON object in Redis."""
|
||||||
|
# Check input
|
||||||
|
if not isinstance(key, str) or len(key) == 0:
|
||||||
|
raise ValueError("Key must be a non-empty string")
|
||||||
|
if not isinstance(data, dict) or len(data) == 0:
|
||||||
|
raise ValueError("Data must be a non-empty dictionary")
|
||||||
|
# Check connection
|
||||||
|
if not self.is_connected:
|
||||||
|
raise ConnectionError("Not connected to Redis")
|
||||||
|
# Set data
|
||||||
|
self._client.json().set(key, self.path, data)
|
||||||
|
self.logger.debug(f"Set {key} to {data}")
|
||||||
|
|
||||||
|
def _get(self, key: str) -> dict | None:
|
||||||
|
"""Get a JSON object from Redis."""
|
||||||
|
# Check input
|
||||||
|
if not isinstance(key, str) or len(key) == 0:
|
||||||
|
raise ValueError("Key must be a non-empty string")
|
||||||
|
# Check connection
|
||||||
|
if not self.is_connected:
|
||||||
|
raise ConnectionError("Not connected to Redis")
|
||||||
|
# Get data
|
||||||
|
data = self._client.json().get(key)
|
||||||
|
self.logger.debug(f"Got {data} from {key}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _delete(self, key: str) -> None:
|
||||||
|
"""Delete data from Redis."""
|
||||||
|
# Check input
|
||||||
|
if not isinstance(key, str) or len(key) == 0:
|
||||||
|
raise ValueError("Key must be a non-empty string")
|
||||||
|
# Check connection
|
||||||
|
if not self.is_connected:
|
||||||
|
raise ConnectionError("Not connected to Redis")
|
||||||
|
# Delete data
|
||||||
|
self._client.json().delete(key)
|
||||||
|
self.logger.debug(f"Deleted {key}")
|
||||||
|
|
||||||
|
def _list_keys(self, pattern: str) -> list[str]:
|
||||||
|
"""List keys in Redis matching a pattern."""
|
||||||
|
# Check input
|
||||||
|
if not isinstance(pattern, str) or len(pattern) == 0:
|
||||||
|
raise ValueError("Pattern must be a non-empty string")
|
||||||
|
# Check connection
|
||||||
|
if not self.is_connected:
|
||||||
|
raise ConnectionError("Not connected to Redis")
|
||||||
|
# List keys
|
||||||
|
keys: list[str] = [
|
||||||
|
key.decode(self.encoding)
|
||||||
|
for key
|
||||||
|
in self._client.keys(pattern)
|
||||||
|
]
|
||||||
|
self.logger.debug(f"Got {keys} matching {pattern}")
|
||||||
|
return keys
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from .connection_aware_interface import ConnectionAwareInterface as ConnectionAwareInterface
|
||||||
|
from .context_aware_interface import ContextAwareInterface as ContextAwareInterface
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ConnectionAwareInterface",
|
||||||
|
"ContextAwareInterface",
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"""Definition of ConnectionAwareInterface abstract base class."""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionAwareInterface(ABC):
|
||||||
|
"""Interface that defines connection-related methods."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def connect(self) -> None:
|
||||||
|
"""Connect to resource."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def disconnect(self) -> None:
|
||||||
|
"""Disconnect from resource."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""Check if connected to resource."""
|
||||||
|
raise NotImplementedError()
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""Definition of ConnectionAwareInterface abstract base class."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class ContextAwareInterface(ABC):
|
||||||
|
"""Interface that defined context-related methods."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __enter__(self) -> ContextAwareInterface:
|
||||||
|
"""Enter the context."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: type | None,
|
||||||
|
exc_val: object | None,
|
||||||
|
exc_tb: object | None
|
||||||
|
) -> None:
|
||||||
|
"""Exit the context."""
|
||||||
|
raise NotImplementedError()
|
||||||
@@ -2,7 +2,8 @@ version = 1
|
|||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
"python_full_version >= '3.13'",
|
"python_full_version >= '3.14'",
|
||||||
|
"python_full_version == '3.13.*'",
|
||||||
"python_full_version < '3.13'",
|
"python_full_version < '3.13'",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -592,6 +593,15 @@ wheels = [
|
|||||||
name = "python-repositories"
|
name = "python-repositories"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "python-utils" },
|
||||||
|
{ name = "structlog" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
redis = [
|
||||||
|
{ name = "redis" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
@@ -604,6 +614,12 @@ dev = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "python-utils", specifier = ">=0.1.0", index = "https://gitea.gt-proj.com/api/packages/brian/pypi/simple/" },
|
||||||
|
{ name = "redis", marker = "extra == 'redis'", specifier = ">=6.4.0" },
|
||||||
|
{ name = "structlog", specifier = ">=25.4.0" },
|
||||||
|
]
|
||||||
|
provides-extras = ["redis"]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
@@ -615,6 +631,15 @@ dev = [
|
|||||||
{ name = "safety", specifier = ">=3.6.0" },
|
{ name = "safety", specifier = ">=3.6.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { registry = "https://gitea.gt-proj.com/api/packages/brian/pypi/simple/" }
|
||||||
|
sdist = { url = "https://gitea.gt-proj.com/api/packages/brian/pypi/files/python-utils/0.1.0/python_utils-0.1.0.tar.gz", hash = "sha256:1c1c6a885a47d65bfc4542b4be98412f7444d832d1171b02d135e7eacb7989c6" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://gitea.gt-proj.com/api/packages/brian/pypi/files/python-utils/0.1.0/python_utils-0.1.0-py3-none-any.whl", hash = "sha256:a2b7c55bb308a0ff39e1ef8fa28c71f32afc59f6d865cd0ce6da1a1a7a507d3d" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyupgrade"
|
name = "pyupgrade"
|
||||||
version = "3.20.0"
|
version = "3.20.0"
|
||||||
@@ -653,6 +678,15 @@ wheels = [
|
|||||||
{ url = "http://10.0.0.2:5001/index/pyyaml/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563" },
|
{ url = "http://10.0.0.2:5001/index/pyyaml/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redis"
|
||||||
|
version = "6.4.0"
|
||||||
|
source = { registry = "http://10.0.0.2:5001/index/" }
|
||||||
|
sdist = { url = "http://10.0.0.2:5001/index/redis/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "http://10.0.0.2:5001/index/redis/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "2025.8.29"
|
version = "2025.8.29"
|
||||||
@@ -868,6 +902,15 @@ wheels = [
|
|||||||
{ url = "http://10.0.0.2:5001/index/sniffio/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" },
|
{ url = "http://10.0.0.2:5001/index/sniffio/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "structlog"
|
||||||
|
version = "25.4.0"
|
||||||
|
source = { registry = "http://10.0.0.2:5001/index/" }
|
||||||
|
sdist = { url = "http://10.0.0.2:5001/index/structlog/structlog-25.4.0.tar.gz", hash = "sha256:186cd1b0a8ae762e29417095664adf1d6a31702160a46dacb7796ea82f7409e4" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "http://10.0.0.2:5001/index/structlog/structlog-25.4.0-py3-none-any.whl", hash = "sha256:fe809ff5c27e557d14e613f45ca441aabda051d119ee5a0102aaba6ce40eed2c" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tenacity"
|
name = "tenacity"
|
||||||
version = "9.1.2"
|
version = "9.1.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user