You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

177 lines
5.4 KiB

from __future__ import annotations
from dataclasses import dataclass, field
from typing import List, Optional
_BRACES = {"(", ")", "[", "]", "{", "}"}
def _validate_no_invalid_chars(value: str, field_name: str) -> None:
"""Ensure value contains only printable ASCII without spaces or braces.
This mirrors the constraints enforced by other Redis clients for values that
will appear in CLIENT LIST / CLIENT INFO output.
"""
for ch in value:
# printable ASCII without space: '!' (0x21) to '~' (0x7E)
if ord(ch) < 0x21 or ord(ch) > 0x7E or ch in _BRACES:
raise ValueError(
f"{field_name} must not contain spaces, newlines, non-printable characters, or braces"
)
def _validate_driver_name(name: str) -> None:
"""Validate an upstream driver name.
The name should look like a typical Python distribution or package name,
following a simplified form of PEP 503 normalisation rules:
* start with a lowercase ASCII letter
* contain only lowercase letters, digits, hyphens and underscores
Examples of valid names: ``"django-redis"``, ``"celery"``, ``"rq"``.
"""
import re
_validate_no_invalid_chars(name, "Driver name")
if not re.match(r"^[a-z][a-z0-9_-]*$", name):
raise ValueError(
"Upstream driver name must use a Python package-style name: "
"start with a lowercase letter and contain only lowercase letters, "
"digits, hyphens, and underscores (e.g., 'django-redis')."
)
def _validate_driver_version(version: str) -> None:
_validate_no_invalid_chars(version, "Driver version")
def _format_driver_entry(driver_name: str, driver_version: str) -> str:
return f"{driver_name}_v{driver_version}"
@dataclass
class DriverInfo:
"""Driver information used to build the CLIENT SETINFO LIB-NAME and LIB-VER values.
This class consolidates all driver metadata (redis-py version and upstream drivers)
into a single object that is propagated through connection pools and connections.
The formatted name follows the pattern::
name(driver1_vVersion1;driver2_vVersion2)
Parameters
----------
name : str, optional
The base library name (default: "redis-py")
lib_version : str, optional
The redis-py library version. If None, the version will be determined
automatically from the installed package.
Examples
--------
>>> info = DriverInfo()
>>> info.formatted_name
'redis-py'
>>> info = DriverInfo().add_upstream_driver("django-redis", "5.4.0")
>>> info.formatted_name
'redis-py(django-redis_v5.4.0)'
>>> info = DriverInfo(lib_version="5.0.0")
>>> info.lib_version
'5.0.0'
"""
name: str = "redis-py"
lib_version: Optional[str] = None
_upstream: List[str] = field(default_factory=list)
def __post_init__(self):
"""Initialize lib_version if not provided."""
if self.lib_version is None:
from redis.utils import get_lib_version
self.lib_version = get_lib_version()
@property
def upstream_drivers(self) -> List[str]:
"""Return a copy of the upstream driver entries.
Each entry is in the form ``"driver-name_vversion"``.
"""
return list(self._upstream)
def add_upstream_driver(
self, driver_name: str, driver_version: str
) -> "DriverInfo":
"""Add an upstream driver to this instance and return self.
The most recently added driver appears first in :pyattr:`formatted_name`.
"""
if driver_name is None:
raise ValueError("Driver name must not be None")
if driver_version is None:
raise ValueError("Driver version must not be None")
_validate_driver_name(driver_name)
_validate_driver_version(driver_version)
entry = _format_driver_entry(driver_name, driver_version)
# insert at the beginning so latest is first
self._upstream.insert(0, entry)
return self
@property
def formatted_name(self) -> str:
"""Return the base name with upstream drivers encoded, if any.
With no upstream drivers, this is just :pyattr:`name`. Otherwise::
name(driver1_vX;driver2_vY)
"""
if not self._upstream:
return self.name
return f"{self.name}({';'.join(self._upstream)})"
def resolve_driver_info(
driver_info: Optional[DriverInfo],
lib_name: Optional[str],
lib_version: Optional[str],
) -> DriverInfo:
"""Resolve driver_info from parameters.
If driver_info is provided, use it. Otherwise, create DriverInfo from
lib_name and lib_version (using defaults if not provided).
Parameters
----------
driver_info : DriverInfo, optional
The DriverInfo instance to use
lib_name : str, optional
The library name (default: "redis-py")
lib_version : str, optional
The library version (default: auto-detected)
Returns
-------
DriverInfo
The resolved DriverInfo instance
"""
if driver_info is not None:
return driver_info
# Fallback: create DriverInfo from lib_name and lib_version
from redis.utils import get_lib_version
name = lib_name if lib_name is not None else "redis-py"
version = lib_version if lib_version is not None else get_lib_version()
return DriverInfo(name=name, lib_version=version)