-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathconfig.py
More file actions
268 lines (225 loc) · 8.84 KB
/
Copy pathconfig.py
File metadata and controls
268 lines (225 loc) · 8.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
"""Configuration for fastapi-redis-sdk using Pydantic Settings.
Following FastAPI's recommended pattern for settings:
https://fastapi.tiangolo.com/advanced/settings
"""
from __future__ import annotations
import warnings
from functools import lru_cache
from importlib.metadata import PackageNotFoundError, version
from typing import Any
from pydantic import Field, SecretStr, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from redis.driver_info import DriverInfo
LIB_NAME: str = "fastapi-redis-sdk"
try:
LIB_VERSION: str = version("fastapi-redis-sdk")
except PackageNotFoundError:
# Package not installed (e.g. running from source via sys.path).
from redis_fastapi import __version__ as LIB_VERSION
DRIVER_INFO: DriverInfo = DriverInfo().add_upstream_driver(LIB_NAME, LIB_VERSION)
CACHE_STATUS_HEADER: str = "X-Redis-Cache"
class RedisSettings(BaseSettings):
"""Central configuration for the FastAPI Redis integration.
Supports two connection modes:
1. **URL mode** (default): set ``url`` to a full Redis URL.
2. **KV mode**: set ``host``, ``port``, ``db``, ``password``, etc.
When ``url`` is provided it takes precedence over KV fields.
All settings can be configured via environment variables with the ``REDIS_`` prefix.
For example: ``REDIS_URL``, ``REDIS_HOST``, ``REDIS_PORT``, etc.
Supports reading from ``.env`` files automatically.
"""
# -- Connection: URL mode --------------------------------------------------
url: str | None = Field(
default=None,
description="Full Redis connection URL (redis://...)",
)
# -- Connection: KV mode ---------------------------------------------------
host: str = Field(
default="localhost",
description="Redis server hostname",
)
port: int = Field(
default=6379,
ge=1,
le=65535,
description="Redis server port (1-65535)",
)
db: int = Field(
default=0,
ge=0,
description="Redis database number (0-15)",
)
username: str | None = Field(
default=None,
description="Redis username (Redis 6+)",
)
password: SecretStr | None = Field(
default=None,
description="Redis password (stored securely)",
)
# -- TLS -------------------------------------------------------------------
ssl: bool = Field(
default=False,
description="Enable TLS/SSL encryption",
)
ssl_certfile: str | None = Field(
default=None,
description="Path to client certificate file",
)
ssl_keyfile: str | None = Field(
default=None,
description="Path to client private key file",
)
ssl_ca_certs: str | None = Field(
default=None,
description="Path to CA certificate bundle",
)
ssl_check_hostname: bool = Field(
default=True,
description="Verify hostname in TLS certificate",
)
# -- Pool ------------------------------------------------------------------
max_connections: int | None = Field(
default=None,
ge=1,
description="Maximum connections in pool (None = unbounded)",
)
socket_timeout: float | None = Field(
default=None,
ge=0,
description="Socket read/write timeout in seconds",
)
socket_connect_timeout: float | None = Field(
default=None,
ge=0,
description="Socket connect timeout in seconds",
)
# -- Cluster ---------------------------------------------------------------
cluster: bool = Field(
default=False,
description="Enable Redis Cluster mode",
)
# -- Prefix ----------------------------------------------------------------
prefix: str = Field(
default="redis:fastapi",
description="Global prefix for all Redis keys",
)
# -- Cache defaults --------------------------------------------------------
default_ttl: int = Field(
default=0,
ge=0,
description=(
"Default cache TTL in seconds. "
"0 means no automatic expiration (cache entries persist until "
"explicitly evicted or removed by Redis eviction policy). "
"Set a positive value to enable automatic expiry."
),
)
# -- Telemetry -------------------------------------------------------------
otel_enabled: bool = Field(
default=False,
description="Enable OpenTelemetry instrumentation for cache operations",
)
otel_redis_enabled: bool = Field(
default=False,
description="Also initialize redis-py native OTel (connection/command metrics)",
)
# -- KV fields that are silently ignored when url is set -----------------
_KV_FIELDS: frozenset[str] = frozenset(
{"host", "port", "db", "username", "password"}
)
@model_validator(mode="after")
def _warn_url_with_kv(self) -> RedisSettings:
"""Emit a warning when ``url`` is set alongside KV fields."""
if self.url is not None:
overlap = self._KV_FIELDS & self.model_fields_set
if overlap:
warnings.warn(
f"Both 'url' and {sorted(overlap)} are set. "
"When 'url' is provided the KV fields are ignored.",
UserWarning,
stacklevel=2,
)
return self
# Pydantic Settings configuration
model_config = SettingsConfigDict(
env_prefix="REDIS_", # All env vars start with REDIS_
env_file=".env", # Read from .env file if present
env_file_encoding="utf-8",
case_sensitive=False, # REDIS_URL = redis_url = REDIS_url
extra="ignore", # Ignore extra env vars
)
def _tls_kwargs(self) -> dict[str, Any]:
"""Build SSL-related kwargs for ``ConnectionPool`` / ``from_url``."""
if not self.ssl:
return {}
kw: dict[str, Any] = {"ssl": True}
if self.ssl_certfile:
kw["ssl_certfile"] = self.ssl_certfile
if self.ssl_keyfile:
kw["ssl_keyfile"] = self.ssl_keyfile
if self.ssl_ca_certs:
kw["ssl_ca_certs"] = self.ssl_ca_certs
kw["ssl_check_hostname"] = self.ssl_check_hostname
return kw
def _pool_kwargs(self) -> dict[str, Any]:
"""Build pool-related kwargs shared by all pool constructors."""
kw: dict[str, Any] = {"driver_info": DRIVER_INFO}
if self.max_connections is not None:
kw["max_connections"] = self.max_connections
if self.socket_timeout is not None:
kw["socket_timeout"] = self.socket_timeout
if self.socket_connect_timeout is not None:
kw["socket_connect_timeout"] = self.socket_connect_timeout
kw.update(self._tls_kwargs())
return kw
def connection_kwargs(self) -> dict[str, Any]:
"""Return the full set of kwargs for pool/client construction.
If ``url`` is set the dict contains ``{"url": ..., **pool_kwargs}``.
Otherwise, it contains ``{"host": ..., "port": ..., **pool_kwargs}``.
"""
kw = self._pool_kwargs()
if self.url is not None:
kw["url"] = self.url
else:
kw["host"] = self.host
kw["port"] = self.port
kw["db"] = self.db
if self.username is not None:
kw["username"] = self.username
if self.password is not None:
# Extract the secret value from SecretStr
kw["password"] = self.password.get_secret_value()
return kw
def pattern_prefix(self, pattern: str) -> str:
"""Return the full prefix for a given pattern name.
Example: ``settings.pattern_prefix("cache")`` → ``"redis:fastapi:cache"``
"""
return f"{self.prefix}:{pattern}"
def reset_settings() -> None:
"""Clear the cached settings instance.
Useful in tests to force a fresh reload from environment variables
between test cases.
"""
get_settings.cache_clear()
@lru_cache
def get_settings() -> RedisSettings:
"""Get cached RedisSettings instance.
This function uses ``@lru_cache`` to return the same Settings object
on every call, preventing reading from ``.env`` file multiple times.
Following FastAPI's recommended pattern for settings:
https://fastapi.tiangolo.com/advanced/settings
Usage as a dependency in FastAPI endpoints:
from redis_fastapi import get_settings
from fastapi import Depends
@app.get("/config")
async def show_config(settings: Annotated[RedisSettings, Depends(get_settings)]):
return {"host": settings.host}
Usage in non-endpoint code:
from redis_fastapi import get_settings
settings = get_settings()
print(settings.host)
Returns:
Cached settings instance loaded from environment variables and .env file.
"""
return RedisSettings()