Skip to content

Commit 793e80b

Browse files
committed
fix backends, more parameters
1 parent 09608a7 commit 793e80b

File tree

3 files changed

+365
-20
lines changed

3 files changed

+365
-20
lines changed

crudadmin/admin_interface/crud_admin.py

Lines changed: 270 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,50 +1231,184 @@ async def _create_initial_admin(self, admin_data: Union[dict, BaseModel]) -> Non
12311231
raise
12321232

12331233
def use_redis_sessions(
1234-
self, redis_url: str = "redis://localhost:6379", **kwargs: Any
1234+
self,
1235+
redis_url: Optional[str] = None,
1236+
host: Optional[str] = None,
1237+
port: Optional[int] = None,
1238+
db: Optional[int] = None,
1239+
username: Optional[str] = None,
1240+
password: Optional[str] = None,
1241+
**kwargs: Any,
12351242
) -> "CRUDAdmin":
12361243
"""Configure Redis session backend.
12371244
1245+
You can configure Redis connection using either a URL or individual parameters,
1246+
but not both.
1247+
12381248
Args:
1239-
redis_url: Redis connection URL
1240-
**kwargs: Additional Redis configuration options
1249+
redis_url: Redis connection URL (e.g., "redis://user:pass@localhost:6379/0")
1250+
host: Redis host (default: "localhost")
1251+
port: Redis port (default: 6379)
1252+
db: Redis database number (default: 0)
1253+
username: Redis username for ACL authentication (Redis 6.0+, default: None)
1254+
password: Redis password (default: None)
1255+
**kwargs: Additional Redis configuration options (pool_size, connect_timeout, etc.)
12411256
12421257
Returns:
12431258
Self for method chaining
1259+
1260+
Raises:
1261+
ValueError: If both redis_url and individual parameters are provided
1262+
1263+
Examples:
1264+
Using URL:
1265+
```python
1266+
admin.use_redis_sessions(redis_url="redis://myuser:mypass@localhost:6379/1")
1267+
```
1268+
1269+
Using individual parameters:
1270+
```python
1271+
admin.use_redis_sessions(host="localhost", port=6379, db=1, username="myuser", password="secret")
1272+
```
12441273
"""
12451274
self._session_backend = "redis"
1246-
self._session_backend_kwargs = {"redis_url": redis_url, **kwargs}
1275+
1276+
individual_params = [host, port, db, username, password]
1277+
has_individual_params = any(param is not None for param in individual_params)
1278+
1279+
if redis_url is not None and has_individual_params:
1280+
raise ValueError(
1281+
"Cannot specify both redis_url and individual parameters (host, port, db, username, password). "
1282+
"Use either redis_url='redis://...' OR individual parameters like host='localhost'."
1283+
)
1284+
1285+
if redis_url is not None:
1286+
parsed_redis_kwargs = self._parse_redis_url(redis_url)
1287+
elif has_individual_params:
1288+
parsed_redis_kwargs = {
1289+
"host": host or "localhost",
1290+
"port": port or 6379,
1291+
"db": db or 0,
1292+
}
1293+
if username is not None:
1294+
parsed_redis_kwargs["username"] = username
1295+
if password is not None:
1296+
parsed_redis_kwargs["password"] = password
1297+
else:
1298+
parsed_redis_kwargs = {
1299+
"host": "localhost",
1300+
"port": 6379,
1301+
"db": 0,
1302+
}
1303+
1304+
if "track_sessions_in_db" in kwargs:
1305+
self.track_sessions_in_db = kwargs.pop("track_sessions_in_db")
1306+
else:
1307+
self.track_sessions_in_db = False
1308+
1309+
parsed_redis_kwargs.update(kwargs)
1310+
self._session_backend_kwargs = parsed_redis_kwargs
1311+
1312+
if hasattr(self, "session_manager"):
1313+
self._recreate_session_manager()
1314+
12471315
return self
12481316

12491317
def use_memcached_sessions(
1250-
self, servers: Optional[List[str]] = None, **kwargs: Any
1318+
self,
1319+
servers: Optional[List[str]] = None,
1320+
host: Optional[str] = None,
1321+
port: Optional[int] = None,
1322+
**kwargs: Any,
12511323
) -> "CRUDAdmin":
12521324
"""Configure Memcached session backend.
12531325
1326+
You can configure Memcached connection using either a servers list or individual parameters,
1327+
but not both.
1328+
12541329
Args:
1255-
servers: List of memcached server addresses
1256-
**kwargs: Additional Memcached configuration options
1330+
servers: List of memcached server addresses (e.g., ["localhost:11211", "server2:11211"])
1331+
host: Memcached host (default: "localhost")
1332+
port: Memcached port (default: 11211)
1333+
**kwargs: Additional Memcached configuration options (pool_size, etc.)
12571334
12581335
Returns:
12591336
Self for method chaining
1337+
1338+
Raises:
1339+
ValueError: If both servers and individual parameters are provided
1340+
1341+
Examples:
1342+
Using servers list:
1343+
```python
1344+
admin.use_memcached_sessions(servers=["localhost:11211", "server2:11211"])
1345+
```
1346+
1347+
Using individual parameters:
1348+
```python
1349+
admin.use_memcached_sessions(host="localhost", port=11211)
1350+
```
1351+
1352+
Note:
1353+
aiomcache currently supports only single server connections,
1354+
so only the first server in the list will be used.
12601355
"""
1261-
if servers is None:
1262-
servers = ["localhost:11211"]
1356+
individual_params = [host, port]
1357+
has_individual_params = any(param is not None for param in individual_params)
1358+
1359+
if servers is not None and has_individual_params:
1360+
raise ValueError(
1361+
"Cannot specify both servers and individual parameters (host, port). "
1362+
"Use either servers=['host:port', ...] OR individual parameters like host='localhost'."
1363+
)
1364+
1365+
if servers is not None:
1366+
parsed_memcached_kwargs = self._parse_memcached_servers(servers)
1367+
elif has_individual_params:
1368+
parsed_memcached_kwargs = {
1369+
"host": host or "localhost",
1370+
"port": port or 11211,
1371+
}
1372+
else:
1373+
parsed_memcached_kwargs = {
1374+
"host": "localhost",
1375+
"port": 11211,
1376+
}
1377+
1378+
if "track_sessions_in_db" in kwargs:
1379+
self.track_sessions_in_db = kwargs.pop("track_sessions_in_db")
1380+
else:
1381+
self.track_sessions_in_db = False
1382+
1383+
parsed_memcached_kwargs.update(kwargs)
1384+
12631385
self._session_backend = "memcached"
1264-
self._session_backend_kwargs = {"servers": servers, **kwargs}
1386+
self._session_backend_kwargs = parsed_memcached_kwargs
1387+
1388+
if hasattr(self, "session_manager"):
1389+
self._recreate_session_manager()
1390+
12651391
return self
12661392

12671393
def use_memory_sessions(self, **kwargs: Any) -> "CRUDAdmin":
12681394
"""Configure in-memory session backend.
12691395
12701396
Args:
12711397
**kwargs: Additional memory storage configuration options
1398+
Note: track_sessions_in_db is ignored for memory sessions
12721399
12731400
Returns:
12741401
Self for method chaining
12751402
"""
1403+
kwargs.pop("track_sessions_in_db", None)
1404+
12761405
self._session_backend = "memory"
12771406
self._session_backend_kwargs = kwargs
1407+
self.track_sessions_in_db = False
1408+
1409+
if hasattr(self, "session_manager"):
1410+
self._recreate_session_manager()
1411+
12781412
return self
12791413

12801414
def use_database_sessions(self, **kwargs: Any) -> "CRUDAdmin":
@@ -1292,4 +1426,130 @@ def use_database_sessions(self, **kwargs: Any) -> "CRUDAdmin":
12921426
self._session_backend = "database"
12931427
self._session_backend_kwargs = kwargs
12941428
self.track_sessions_in_db = True
1429+
1430+
if hasattr(self, "session_manager"):
1431+
self._recreate_session_manager()
1432+
12951433
return self
1434+
1435+
def _parse_redis_url(self, redis_url: str) -> Dict[str, Any]:
1436+
"""Parse Redis URL to extract connection parameters.
1437+
1438+
Args:
1439+
redis_url: Redis connection URL (e.g., redis://user:pass@localhost:6379/0)
1440+
1441+
Returns:
1442+
Dictionary of Redis connection parameters
1443+
"""
1444+
from urllib.parse import urlparse
1445+
1446+
parsed = urlparse(redis_url)
1447+
1448+
redis_kwargs = {
1449+
"host": parsed.hostname or "localhost",
1450+
"port": parsed.port or 6379,
1451+
"db": int(parsed.path.lstrip("/"))
1452+
if parsed.path and parsed.path != "/"
1453+
else 0,
1454+
}
1455+
1456+
if parsed.username:
1457+
redis_kwargs["username"] = parsed.username
1458+
1459+
if parsed.password:
1460+
redis_kwargs["password"] = parsed.password
1461+
1462+
return redis_kwargs
1463+
1464+
def _parse_memcached_servers(self, servers: List[str]) -> Dict[str, Any]:
1465+
"""Parse Memcached servers list to extract connection parameters.
1466+
1467+
Args:
1468+
servers: List of server addresses (e.g., ["localhost:11211", "server2:11211"])
1469+
1470+
Returns:
1471+
Dictionary of Memcached connection parameters
1472+
1473+
Note:
1474+
aiomcache currently supports only single server connections,
1475+
so we use the first server in the list.
1476+
"""
1477+
if not servers:
1478+
return {"host": "localhost", "port": 11211}
1479+
1480+
server = servers[0]
1481+
1482+
if ":" in server:
1483+
host, port_str = server.split(":", 1)
1484+
try:
1485+
port = int(port_str)
1486+
except ValueError:
1487+
port = 11211
1488+
else:
1489+
host = server
1490+
port = 11211
1491+
1492+
return {"host": host, "port": port}
1493+
1494+
def _recreate_session_manager(self) -> None:
1495+
"""Recreate session manager with current backend configuration."""
1496+
session_backend = getattr(self, "_session_backend", "memory")
1497+
backend_kwargs = getattr(self, "_session_backend_kwargs", {})
1498+
1499+
if self.track_sessions_in_db:
1500+
if session_backend in ["redis", "memcached"]:
1501+
actual_backend = "hybrid"
1502+
backend_kwargs["db_config"] = self.db_config
1503+
backend_kwargs["_cache_backend"] = session_backend
1504+
else:
1505+
actual_backend = "database"
1506+
backend_kwargs["db_config"] = self.db_config
1507+
else:
1508+
actual_backend = session_backend
1509+
1510+
session_timeout_minutes = 30 # default
1511+
if hasattr(self, "session_manager"):
1512+
session_timeout_minutes = int(
1513+
self.session_manager.session_timeout.total_seconds() / 60
1514+
)
1515+
1516+
storage: AbstractSessionStorage[SessionData] = get_session_storage(
1517+
backend=actual_backend,
1518+
model_type=SessionData,
1519+
prefix="session:",
1520+
expiration=session_timeout_minutes * 60,
1521+
**backend_kwargs,
1522+
)
1523+
1524+
if hasattr(self, "session_manager"):
1525+
max_sessions = self.session_manager.max_sessions
1526+
cleanup_interval_minutes = int(
1527+
self.session_manager.cleanup_interval.total_seconds() / 60
1528+
)
1529+
csrf_token_bytes = self.session_manager.csrf_token_bytes
1530+
rate_limiter = self.session_manager.rate_limiter
1531+
login_max_attempts = self.session_manager.login_max_attempts
1532+
login_window_minutes = int(
1533+
self.session_manager.login_window.total_seconds() / 60
1534+
)
1535+
else:
1536+
max_sessions = 5
1537+
cleanup_interval_minutes = 15
1538+
csrf_token_bytes = 32
1539+
rate_limiter = None
1540+
login_max_attempts = 5
1541+
login_window_minutes = 15
1542+
1543+
self.session_manager = SessionManager(
1544+
session_storage=storage,
1545+
max_sessions_per_user=max_sessions,
1546+
session_timeout_minutes=session_timeout_minutes,
1547+
cleanup_interval_minutes=cleanup_interval_minutes,
1548+
csrf_token_bytes=csrf_token_bytes,
1549+
rate_limiter=rate_limiter,
1550+
login_max_attempts=login_max_attempts,
1551+
login_window_minutes=login_window_minutes,
1552+
)
1553+
1554+
if hasattr(self, "admin_authentication"):
1555+
self.admin_authentication.session_manager = self.session_manager

crudadmin/session/backends/redis.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __init__(
2828
host: str = "localhost",
2929
port: int = 6379,
3030
db: int = 0,
31+
username: Optional[str] = None,
3132
password: Optional[str] = None,
3233
pool_size: int = 10,
3334
connect_timeout: int = 10,
@@ -40,6 +41,7 @@ def __init__(
4041
host: Redis host
4142
port: Redis port
4243
db: Redis database number
44+
username: Redis username (for ACL authentication, Redis 6.0+)
4345
password: Redis password
4446
pool_size: Redis connection pool size
4547
connect_timeout: Redis connection timeout
@@ -50,6 +52,7 @@ def __init__(
5052
host=host,
5153
port=port,
5254
db=db,
55+
username=username,
5356
password=password,
5457
socket_timeout=connect_timeout,
5558
socket_connect_timeout=connect_timeout,

0 commit comments

Comments
 (0)