Beta/venv/lib/python3.12/site-packages/redis/driver_info.py

185 lines
5.8 KiB
Python
Raw Normal View History

2026-06-16 17:09:34 +00:00
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List, Optional
from redis.utils import SENTINEL
_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. If omitted, defaults to "redis-py". If None,
LIB-NAME will not be sent.
lib_version : str, optional
The redis-py library version. If omitted, the version will be determined
automatically from the installed package. If None, LIB-VER will not be sent.
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: Optional[str] | object = SENTINEL
lib_version: Optional[str] | object = SENTINEL
_upstream: List[str] = field(default_factory=list)
def __post_init__(self):
"""Initialize default metadata if not explicitly provided."""
if self.name is SENTINEL:
self.name = "redis-py"
if self.lib_version is SENTINEL:
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) -> Optional[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)
"""
name = self.name
if not isinstance(name, str) or not name:
return None
if not self._upstream:
return name
return f"{name}({';'.join(self._upstream)})"
def resolve_driver_info(
driver_info: Optional[DriverInfo] | object = SENTINEL,
lib_name: Optional[str] | object = SENTINEL,
lib_version: Optional[str] | object = SENTINEL,
) -> Optional[DriverInfo]:
"""Resolve driver_info from parameters.
If driver_info is provided, use it. Otherwise, create DriverInfo from
lib_name and lib_version (using defaults only for sentinel values).
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, optional
The resolved DriverInfo instance
"""
if driver_info is SENTINEL:
if lib_name is None and lib_version is None:
return None
return DriverInfo(name=lib_name, lib_version=lib_version)
if driver_info is None or isinstance(driver_info, DriverInfo):
return driver_info
raise TypeError("driver_info must be a DriverInfo instance or None")