driver_info.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. from __future__ import annotations
  2. from dataclasses import dataclass, field
  3. from typing import List, Optional
  4. _BRACES = {"(", ")", "[", "]", "{", "}"}
  5. def _validate_no_invalid_chars(value: str, field_name: str) -> None:
  6. """Ensure value contains only printable ASCII without spaces or braces.
  7. This mirrors the constraints enforced by other Redis clients for values that
  8. will appear in CLIENT LIST / CLIENT INFO output.
  9. """
  10. for ch in value:
  11. # printable ASCII without space: '!' (0x21) to '~' (0x7E)
  12. if ord(ch) < 0x21 or ord(ch) > 0x7E or ch in _BRACES:
  13. raise ValueError(
  14. f"{field_name} must not contain spaces, newlines, non-printable characters, or braces"
  15. )
  16. def _validate_driver_name(name: str) -> None:
  17. """Validate an upstream driver name.
  18. The name should look like a typical Python distribution or package name,
  19. following a simplified form of PEP 503 normalisation rules:
  20. * start with a lowercase ASCII letter
  21. * contain only lowercase letters, digits, hyphens and underscores
  22. Examples of valid names: ``"django-redis"``, ``"celery"``, ``"rq"``.
  23. """
  24. import re
  25. _validate_no_invalid_chars(name, "Driver name")
  26. if not re.match(r"^[a-z][a-z0-9_-]*$", name):
  27. raise ValueError(
  28. "Upstream driver name must use a Python package-style name: "
  29. "start with a lowercase letter and contain only lowercase letters, "
  30. "digits, hyphens, and underscores (e.g., 'django-redis')."
  31. )
  32. def _validate_driver_version(version: str) -> None:
  33. _validate_no_invalid_chars(version, "Driver version")
  34. def _format_driver_entry(driver_name: str, driver_version: str) -> str:
  35. return f"{driver_name}_v{driver_version}"
  36. @dataclass
  37. class DriverInfo:
  38. """Driver information used to build the CLIENT SETINFO LIB-NAME and LIB-VER values.
  39. This class consolidates all driver metadata (redis-py version and upstream drivers)
  40. into a single object that is propagated through connection pools and connections.
  41. The formatted name follows the pattern::
  42. name(driver1_vVersion1;driver2_vVersion2)
  43. Parameters
  44. ----------
  45. name : str, optional
  46. The base library name (default: "redis-py")
  47. lib_version : str, optional
  48. The redis-py library version. If None, the version will be determined
  49. automatically from the installed package.
  50. Examples
  51. --------
  52. >>> info = DriverInfo()
  53. >>> info.formatted_name
  54. 'redis-py'
  55. >>> info = DriverInfo().add_upstream_driver("django-redis", "5.4.0")
  56. >>> info.formatted_name
  57. 'redis-py(django-redis_v5.4.0)'
  58. >>> info = DriverInfo(lib_version="5.0.0")
  59. >>> info.lib_version
  60. '5.0.0'
  61. """
  62. name: str = "redis-py"
  63. lib_version: Optional[str] = None
  64. _upstream: List[str] = field(default_factory=list)
  65. def __post_init__(self):
  66. """Initialize lib_version if not provided."""
  67. if self.lib_version is None:
  68. from redis.utils import get_lib_version
  69. self.lib_version = get_lib_version()
  70. @property
  71. def upstream_drivers(self) -> List[str]:
  72. """Return a copy of the upstream driver entries.
  73. Each entry is in the form ``"driver-name_vversion"``.
  74. """
  75. return list(self._upstream)
  76. def add_upstream_driver(
  77. self, driver_name: str, driver_version: str
  78. ) -> "DriverInfo":
  79. """Add an upstream driver to this instance and return self.
  80. The most recently added driver appears first in :pyattr:`formatted_name`.
  81. """
  82. if driver_name is None:
  83. raise ValueError("Driver name must not be None")
  84. if driver_version is None:
  85. raise ValueError("Driver version must not be None")
  86. _validate_driver_name(driver_name)
  87. _validate_driver_version(driver_version)
  88. entry = _format_driver_entry(driver_name, driver_version)
  89. # insert at the beginning so latest is first
  90. self._upstream.insert(0, entry)
  91. return self
  92. @property
  93. def formatted_name(self) -> str:
  94. """Return the base name with upstream drivers encoded, if any.
  95. With no upstream drivers, this is just :pyattr:`name`. Otherwise::
  96. name(driver1_vX;driver2_vY)
  97. """
  98. if not self._upstream:
  99. return self.name
  100. return f"{self.name}({';'.join(self._upstream)})"
  101. def resolve_driver_info(
  102. driver_info: Optional[DriverInfo],
  103. lib_name: Optional[str],
  104. lib_version: Optional[str],
  105. ) -> DriverInfo:
  106. """Resolve driver_info from parameters.
  107. If driver_info is provided, use it. Otherwise, create DriverInfo from
  108. lib_name and lib_version (using defaults if not provided).
  109. Parameters
  110. ----------
  111. driver_info : DriverInfo, optional
  112. The DriverInfo instance to use
  113. lib_name : str, optional
  114. The library name (default: "redis-py")
  115. lib_version : str, optional
  116. The library version (default: auto-detected)
  117. Returns
  118. -------
  119. DriverInfo
  120. The resolved DriverInfo instance
  121. """
  122. if driver_info is not None:
  123. return driver_info
  124. # Fallback: create DriverInfo from lib_name and lib_version
  125. from redis.utils import get_lib_version
  126. name = lib_name if lib_name is not None else "redis-py"
  127. version = lib_version if lib_version is not None else get_lib_version()
  128. return DriverInfo(name=name, lib_version=version)