feat(redis): add native JSON session storage for 4.1.0
Build & Publish Package / publish (push) Successful in 49s

- Add opt-in SESSION_REDIS_JSON storage for asyncio Redis backends.
- Check JSON command support at startup and reject JSON mode on Trio.
- Read legacy strings and migrate sessions on their next save.
- Validate JSON objects before transactional replacement and expiry.
- Preserve tagged serialization for datetime, tuple, and other types.
- Cover command contracts, error handling, and session round-trips.
- Document server requirements, permissions, and migration limits.
- Normalize indentation and whitespace across code, tests, and examples.
- Bump the package and lockfile to 4.1.0 and update the changelog.
- Refresh the locked pycparser dependency to 3.12.
This commit is contained in:
2026-10-23 20:26:24 +02:00
parent 5dc4a8f6cb
commit 4939bbe987
17 changed files with 1024 additions and 843 deletions
+37
View File
@@ -0,0 +1,37 @@
"""Native JSON storage for the asyncio Redis session backend."""
import json
async def check_support(backend):
"""Fail at startup rather than silently falling back to string storage."""
commands = await backend.execute_command("COMMAND", "INFO", "JSON.GET", "JSON.SET")
entries = list(commands.values()) if isinstance(commands, dict) else commands
if len(entries) != 2 or not all(entries):
raise RuntimeError("SESSION_REDIS_JSON requires JSON.GET and JSON.SET support")
async def get(backend, key):
"""Read legacy strings as well as native JSON documents."""
from redis.exceptions import ResponseError
try:
return await backend.get(key)
except ResponseError as error:
if not str(error).startswith("WRONGTYPE"):
raise
# A single root path returns the object, not a JSONPath result array.
return await backend.execute_command("JSON.GET", key, ".")
async def set(backend, key, value, expiry):
"""Replace a string or JSON key and apply expiry in one transaction."""
# Validate before deleting an existing key. Preserve TaggedJSONSerializer's
# representation so tuples, datetimes, etc. still round-trip normally.
document = json.loads(value)
if not isinstance(document, dict):
raise ValueError("Native Redis JSON sessions require a JSON object")
async with backend.pipeline(transaction=True) as pipeline:
pipeline.delete(key)
pipeline.execute_command("JSON.SET", key, "$", value)
if expiry is not None:
pipeline.expire(key, expiry)
results = await pipeline.execute()
return results[1]