mirror of
https://github.com/rommapp/romm.git
synced 2026-10-12 02:21:58 +00:00
chore(deps): bump redis to 8.1 and fakeredis to 2.38, drop types-redis (#4844)
* chore(deps): bump redis to 8.1 and fakeredis to 2.38 redis-py 6.2.0 -> 8.1.0 and fakeredis 2.30.1 -> 2.38.0. RQ 2.12 (redis!=6,>=3.5) and fakeredis (redis>=4.3) both accept redis 8. fakeredis 2.38 types FakeServer's version argument as a tuple, so pass (7,) instead of 7. Behaviour is unchanged since fakeredis normalises an int to a one-element tuple. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_014wzuPg8vDEjsQ8xqvZgFwx * chore(deps): drop types-redis in favour of redis-py's own types types-redis only covers redis 4.6 and shadowed the inline types that redis 8 ships, so mypy was checking calls against a stale API. Removing it also drops its transitive stub packages (types-cffi, types-pyopenssl, types-setuptools), which nothing else uses. redis-py's types differ from the stubs in three ways: - Redis, AsyncRedis and Pipeline are not generic, so drop the [str] and [bytes] parameters. - Responses are typed bytes | str regardless of decode_responses, so cast at the few call sites that rely on the decoded (or raw) type. - watch, unwatch, multi, aclose and execute_command are unannotated, so add redis to untyped_calls_exclude, as is done for rq and apprise. Also replace setex, deprecated in redis 8, with set(..., ex=...). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_014wzuPg8vDEjsQ8xqvZgFwx * fix(device-auth): replace deprecated GETSET with SET ... GET code-review step. GETSET is deprecated since Redis 6.2 in favour of SET with GET. Using it with ex= also sets the poll key's TTL in the same command, instead of a separate EXPIRE that a crash could skip. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_014wzuPg8vDEjsQ8xqvZgFwx --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e06d4a7bd6
commit
6b13fd1698
@@ -3,7 +3,7 @@ strict = True
|
||||
enable_error_code = ignore-without-code
|
||||
# Packages that leave the methods we call unannotated. mypy matches a method by
|
||||
# its receiver's class, so the worker that subclasses rq's is listed too.
|
||||
untyped_calls_exclude = apprise,mutagen,rq,handler.rq_worker.RomMWorker
|
||||
untyped_calls_exclude = apprise,mutagen,redis,rq,handler.rq_worker.RomMWorker
|
||||
# INI configs take a regex here, not a TOML-style list.
|
||||
exclude = (?x)(^|/)alembic/
|
||||
|
||||
|
||||
@@ -141,10 +141,10 @@ def pair_token(request: Request, token_id: int) -> ClientTokenPairSchema:
|
||||
|
||||
code = generate_pair_code()
|
||||
redis_key = f"pair:{code}"
|
||||
sync_cache.setex(
|
||||
sync_cache.set(
|
||||
redis_key,
|
||||
PAIR_CODE_TTL_SECONDS,
|
||||
json.dumps({"token_id": token_id, "user_id": request.user.id}),
|
||||
ex=PAIR_CODE_TTL_SECONDS,
|
||||
)
|
||||
return ClientTokenPairSchema(code=code, expires_in=PAIR_CODE_TTL_SECONDS)
|
||||
|
||||
|
||||
@@ -139,11 +139,11 @@ async def start_log_forwarder() -> None:
|
||||
continue
|
||||
finally:
|
||||
await pubsub.unsubscribe(LOG_CHANNEL)
|
||||
await pubsub.aclose() # type: ignore[attr-defined]
|
||||
await pubsub.aclose()
|
||||
pubsub = None
|
||||
except asyncio.CancelledError:
|
||||
if pubsub is not None:
|
||||
await pubsub.aclose() # type: ignore[attr-defined]
|
||||
await pubsub.aclose()
|
||||
# Release the lock on shutdown so a restart (e.g. uvicorn --reload)
|
||||
# resumes forwarding immediately instead of waiting out the TTL.
|
||||
await _release_lock(lock_id)
|
||||
|
||||
@@ -131,7 +131,7 @@ def _scan_reported_itself(job: Job) -> bool:
|
||||
|
||||
def report_scan_failure(
|
||||
job: Job,
|
||||
connection: Redis[bytes],
|
||||
connection: Redis,
|
||||
exc_type: type,
|
||||
exc_value: BaseException,
|
||||
tb: Any,
|
||||
|
||||
@@ -209,7 +209,7 @@ class ActivityHandler:
|
||||
async def get_active_for_rom(self, rom_id: int) -> list[ActivityEntry]:
|
||||
"""Get all active play sessions for a specific ROM."""
|
||||
rom_key = self._rom_index_key(rom_id)
|
||||
members = await async_cache.smembers(rom_key)
|
||||
members = cast(set[str], await async_cache.smembers(rom_key))
|
||||
entries: list[ActivityEntry] = []
|
||||
stale_members: list[str] = []
|
||||
|
||||
|
||||
@@ -186,8 +186,10 @@ class AuthHandler:
|
||||
to_encode,
|
||||
oct_key,
|
||||
)
|
||||
redis_client.setex(
|
||||
f"reset-jti:{jti}", self.reset_passwd_token_expires_in_minutes * 60, "valid"
|
||||
redis_client.set(
|
||||
f"reset-jti:{jti}",
|
||||
"valid",
|
||||
ex=self.reset_passwd_token_expires_in_minutes * 60,
|
||||
)
|
||||
return token
|
||||
|
||||
@@ -368,7 +370,7 @@ class AuthHandler:
|
||||
log.info(
|
||||
f"Invite link created by {hl(user.username, color=CYAN)} (jti: {hl(jti)})"
|
||||
)
|
||||
redis_client.setex(f"invite-jti:{jti}", expires_in, "valid")
|
||||
redis_client.set(f"invite-jti:{jti}", "valid", ex=expires_in)
|
||||
return token
|
||||
|
||||
def assert_invite_link_token_valid(self, token: str) -> None:
|
||||
@@ -466,10 +468,8 @@ class OAuthHandler:
|
||||
|
||||
token = self._create_oauth_token(to_encode, expires_delta)
|
||||
|
||||
redis_client.setex(
|
||||
f"refresh-jti:{jti}",
|
||||
int(expires_delta.total_seconds()),
|
||||
"valid",
|
||||
redis_client.set(
|
||||
f"refresh-jti:{jti}", "valid", ex=int(expires_delta.total_seconds())
|
||||
)
|
||||
|
||||
return token
|
||||
|
||||
@@ -1053,7 +1053,7 @@ class TwitchAuth(MetadataHandler):
|
||||
return ""
|
||||
|
||||
# Fetch the token cache
|
||||
token = await async_cache.get("romm:twitch_token")
|
||||
token = cast(str | None, await async_cache.get("romm:twitch_token"))
|
||||
if not token:
|
||||
log.info("Twitch token invalid: fetching a new one...")
|
||||
return await self._update_twitch_token()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from typing import Optional, TypedDict
|
||||
from typing import Optional, TypedDict, cast
|
||||
|
||||
from handler.redis_handler import async_cache
|
||||
|
||||
@@ -43,7 +43,7 @@ class NetplayHandler:
|
||||
|
||||
async def get_all(self) -> dict[str, NetplayRoom]:
|
||||
"""Get all rooms from Redis."""
|
||||
rooms = await async_cache.hgetall(self.hash_name)
|
||||
rooms = cast(dict[str, str], await async_cache.hgetall(self.hash_name))
|
||||
return {room_id: json.loads(room_data) for room_id, room_data in rooms.items()}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum
|
||||
from typing import Any, Final, cast
|
||||
from typing import Any, Final
|
||||
|
||||
from redis import Redis
|
||||
from redis.asyncio import Redis as AsyncRedis
|
||||
@@ -50,13 +50,13 @@ def __get_fake_server() -> Any:
|
||||
|
||||
# One keyspace for both caches, as one Redis serves both outside tests, so
|
||||
# a flush between tests clears what either of them wrote.
|
||||
return FakeServer(version=7)
|
||||
return FakeServer(version=(7,))
|
||||
|
||||
|
||||
_fake_server = __get_fake_server() if IS_PYTEST_RUN else None
|
||||
|
||||
|
||||
def __get_sync_cache() -> Redis[str]:
|
||||
def __get_sync_cache() -> Redis:
|
||||
if IS_PYTEST_RUN:
|
||||
from fakeredis import FakeRedis
|
||||
|
||||
@@ -70,7 +70,7 @@ def __get_sync_cache() -> Redis[str]:
|
||||
return client
|
||||
|
||||
|
||||
def __get_async_cache() -> AsyncRedis[str]:
|
||||
def __get_async_cache() -> AsyncRedis:
|
||||
if IS_PYTEST_RUN:
|
||||
from fakeredis import FakeAsyncRedis
|
||||
|
||||
@@ -88,12 +88,12 @@ sync_cache = __get_sync_cache()
|
||||
async_cache = __get_async_cache()
|
||||
|
||||
|
||||
def __get_async_binary_cache() -> AsyncRedis[bytes]:
|
||||
def __get_async_binary_cache() -> AsyncRedis:
|
||||
"""A client that leaves values as bytes, since `async_cache` decodes every
|
||||
response as UTF-8 and a zstd frame is not."""
|
||||
if IS_PYTEST_RUN:
|
||||
# The fake does not decode responses, which is what this client wants.
|
||||
return cast("AsyncRedis[bytes]", async_cache)
|
||||
return async_cache
|
||||
|
||||
return AsyncRedis.from_url(REDIS_URL)
|
||||
|
||||
|
||||
+4
-1
@@ -1,6 +1,7 @@
|
||||
"""Startup script to run tasks before the main application is started."""
|
||||
|
||||
import asyncio
|
||||
from typing import cast
|
||||
|
||||
import sentry_sdk
|
||||
from opentelemetry import trace
|
||||
@@ -141,7 +142,9 @@ def _drop_legacy_scheduler_state() -> None:
|
||||
try:
|
||||
legacy_job_ids = {
|
||||
as_text(job_id)
|
||||
for job_id in redis_client.zrange(LEGACY_SCHEDULED_JOBS_KEY, 0, -1)
|
||||
for job_id in cast(
|
||||
list[bytes], redis_client.zrange(LEGACY_SCHEDULED_JOBS_KEY, 0, -1)
|
||||
)
|
||||
}
|
||||
|
||||
if legacy_job_ids:
|
||||
|
||||
@@ -50,7 +50,7 @@ LAUNCHBOX_TASK_TIMEOUT: Final[int] = max(TASK_TIMEOUT, 30 * 60)
|
||||
class BatchedCacheWriter:
|
||||
"""Queues cache writes on a pipeline, flushing every `batch_size` entries."""
|
||||
|
||||
def __init__(self, pipe: Pipeline[bytes], batch_size: int | None = None) -> None:
|
||||
def __init__(self, pipe: Pipeline, batch_size: int | None = None) -> None:
|
||||
self._pipe = pipe
|
||||
self._batch_size = batch_size or CACHE_WRITE_BATCH_SIZE
|
||||
self._queued = 0
|
||||
|
||||
@@ -558,6 +558,7 @@ class TestToken:
|
||||
second = _poll_token(client, body["device_code"])
|
||||
assert second.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert second.json()["detail"] == "slow_down"
|
||||
assert sync_cache.ttl(df._KEY_POLL_LAST.format(body["device_code"])) > 0
|
||||
|
||||
def test_per_ip_rate_limit(self, client):
|
||||
# Per-IP cap is 60/60s on /token. Use a single device_code with fast
|
||||
|
||||
@@ -16,7 +16,7 @@ import zipfile
|
||||
from collections.abc import Callable, Iterator
|
||||
from compression import zstd
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
from typing import Any, Final, cast
|
||||
|
||||
import redis
|
||||
from defusedxml import ElementTree as ET
|
||||
@@ -249,7 +249,7 @@ def plain(value: Any) -> bytes:
|
||||
|
||||
|
||||
def load(
|
||||
client: redis.Redis[bytes],
|
||||
client: redis.Redis,
|
||||
records: Iterator[Record],
|
||||
serialize: Callable[[Any], bytes],
|
||||
) -> None:
|
||||
@@ -266,7 +266,7 @@ def load(
|
||||
pipe.execute()
|
||||
|
||||
|
||||
def store_stats(client: redis.Redis[bytes]) -> tuple[dict[str, int], dict[str, int]]:
|
||||
def store_stats(client: redis.Redis) -> tuple[dict[str, int], dict[str, int]]:
|
||||
"""Field count and total value bytes per store, read back off the hashes."""
|
||||
# Not tallied while writing: the dump repeats a title on one platform and an
|
||||
# alternate name across platforms, so ~9k records overwrite another's field.
|
||||
@@ -283,13 +283,13 @@ def store_stats(client: redis.Redis[bytes]) -> tuple[dict[str, int], dict[str, i
|
||||
return counts, value_bytes
|
||||
|
||||
|
||||
def foreign_keys(client: redis.Redis[bytes]) -> int:
|
||||
def foreign_keys(client: redis.Redis) -> int:
|
||||
"""How many keys on the server are not this tool's own stores."""
|
||||
owned = {key.encode() for key in DUMP_STORE_KEYS}
|
||||
return sum(1 for key in client.scan_iter(count=1000) if key not in owned)
|
||||
|
||||
|
||||
def clear_stores(client: redis.Redis[bytes]) -> int:
|
||||
def clear_stores(client: redis.Redis) -> int:
|
||||
"""Drop only this tool's stores, returning `used_memory` without them."""
|
||||
# Not `flushall`: the URL can point at a live RomM, whose sessions and RQ
|
||||
# queues share the database with these stores.
|
||||
@@ -298,7 +298,7 @@ def clear_stores(client: redis.Redis[bytes]) -> int:
|
||||
|
||||
|
||||
def store_memory(
|
||||
client: redis.Redis[bytes], keys: list[str]
|
||||
client: redis.Redis, keys: list[str]
|
||||
) -> tuple[dict[str, int], int, int]:
|
||||
sizes = {}
|
||||
for key in keys:
|
||||
@@ -354,10 +354,10 @@ def sweep_threshold(metadata_zip: Path, limit: int) -> int:
|
||||
|
||||
|
||||
def bench_decode(
|
||||
client: redis.Redis[bytes], key: str, rounds: int = 5
|
||||
client: redis.Redis, key: str, rounds: int = 5
|
||||
) -> tuple[float, float] | None:
|
||||
"""Median `decode` latency, and mean stored size, on real stored records."""
|
||||
fields = client.hrandfield(key, 2000)
|
||||
fields = cast(list[bytes], client.hrandfield(key, 2000))
|
||||
if not fields:
|
||||
return None
|
||||
blobs = [b for b in client.hmget(key, fields) if b]
|
||||
|
||||
+5
-11
@@ -10,9 +10,7 @@ from redis.asyncio import Redis as AsyncRedis
|
||||
from logger.logger import log
|
||||
|
||||
|
||||
async def conditionally_set_cache(
|
||||
cache: AsyncRedis[str], key: str, file_path: Path
|
||||
) -> None:
|
||||
async def conditionally_set_cache(cache: AsyncRedis, key: str, file_path: Path) -> None:
|
||||
"""Set the content of a JSON file to the cache, if it does not already exist or is outdated.
|
||||
|
||||
The MD5 hash of the file is stored alongside the data to determine if the content has changed.
|
||||
@@ -67,21 +65,19 @@ class VersionedCacheStore(NamedTuple):
|
||||
|
||||
|
||||
async def is_cache_schema_current(
|
||||
cache: AsyncRedis[str], store: VersionedCacheStore
|
||||
cache: AsyncRedis, store: VersionedCacheStore
|
||||
) -> bool:
|
||||
"""Whether the store holds the shape its readers expect."""
|
||||
return await cache.get(store.schema_key) == str(store.version)
|
||||
|
||||
|
||||
async def stamp_cache_schema(
|
||||
cache: AsyncRedis[str], store: VersionedCacheStore
|
||||
) -> None:
|
||||
async def stamp_cache_schema(cache: AsyncRedis, store: VersionedCacheStore) -> None:
|
||||
"""Record the shape a completed import left the store in."""
|
||||
await cache.set(store.schema_key, str(store.version))
|
||||
|
||||
|
||||
async def is_cache_store_ready(
|
||||
cache: AsyncRedis[str], store: VersionedCacheStore, data_key: str
|
||||
cache: AsyncRedis, store: VersionedCacheStore, data_key: str
|
||||
) -> bool:
|
||||
"""Whether `data_key` holds entries an import wrote under the current shape.
|
||||
|
||||
@@ -93,9 +89,7 @@ async def is_cache_store_ready(
|
||||
return bool(await cache.exists(data_key))
|
||||
|
||||
|
||||
async def drop_stale_cache_store(
|
||||
cache: AsyncRedis[str], store: VersionedCacheStore
|
||||
) -> bool:
|
||||
async def drop_stale_cache_store(cache: AsyncRedis, store: VersionedCacheStore) -> bool:
|
||||
"""Delete a store an older release wrote, returning whether one was there.
|
||||
|
||||
`unlink` frees the hundreds of MB a metadata store holds off the main
|
||||
|
||||
@@ -97,8 +97,9 @@ def polled_too_fast(device_code: str, interval_seconds: int) -> bool:
|
||||
"""
|
||||
key = _KEY_POLL_LAST.format(device_code)
|
||||
now_ms = int(datetime.now(timezone.utc).timestamp() * 1000)
|
||||
prev_raw = sync_cache.getset(key, str(now_ms))
|
||||
sync_cache.expire(key, max(interval_seconds * 4, 30))
|
||||
prev_raw = sync_cache.set(
|
||||
key, str(now_ms), ex=max(interval_seconds * 4, 30), get=True
|
||||
)
|
||||
if prev_raw is None:
|
||||
return False
|
||||
try:
|
||||
@@ -114,16 +115,10 @@ def store_pending(device_code: str, user_code: str, data: dict[str, Any]) -> Non
|
||||
"status": FlowStatus.PENDING,
|
||||
"user_code": user_code,
|
||||
}
|
||||
sync_cache.setex(
|
||||
_KEY_DC.format(device_code),
|
||||
PENDING_TTL_SECONDS,
|
||||
json.dumps(payload),
|
||||
)
|
||||
sync_cache.setex(
|
||||
_KEY_UC.format(user_code),
|
||||
PENDING_TTL_SECONDS,
|
||||
device_code,
|
||||
sync_cache.set(
|
||||
_KEY_DC.format(device_code), json.dumps(payload), ex=PENDING_TTL_SECONDS
|
||||
)
|
||||
sync_cache.set(_KEY_UC.format(user_code), device_code, ex=PENDING_TTL_SECONDS)
|
||||
|
||||
|
||||
def load_pending(device_code: str) -> dict[str, Any] | None:
|
||||
@@ -170,10 +165,10 @@ def mark_approved(
|
||||
remaining = sync_cache.ttl(_KEY_DC.format(device_code))
|
||||
if remaining is None or remaining < 1:
|
||||
remaining = PENDING_TTL_SECONDS
|
||||
sync_cache.setex(
|
||||
sync_cache.set(
|
||||
_KEY_DC.format(device_code),
|
||||
min(remaining, PENDING_TTL_SECONDS),
|
||||
json.dumps(approved),
|
||||
ex=min(remaining, PENDING_TTL_SECONDS),
|
||||
)
|
||||
if user_code:
|
||||
sync_cache.delete(_KEY_UC.format(user_code))
|
||||
@@ -185,10 +180,8 @@ def mark_denied(device_code: str) -> None:
|
||||
return
|
||||
user_code = pending.get("user_code")
|
||||
denied = {"status": FlowStatus.DENIED}
|
||||
sync_cache.setex(
|
||||
_KEY_DC.format(device_code),
|
||||
DENIED_TTL_SECONDS,
|
||||
json.dumps(denied),
|
||||
sync_cache.set(
|
||||
_KEY_DC.format(device_code), json.dumps(denied), ex=DENIED_TTL_SECONDS
|
||||
)
|
||||
if user_code:
|
||||
sync_cache.delete(_KEY_UC.format(user_code))
|
||||
@@ -206,10 +199,8 @@ def consume_approved(device_code: str) -> dict[str, Any] | None:
|
||||
return None
|
||||
data = json.loads(raw)
|
||||
if data.get("status") != FlowStatus.APPROVED:
|
||||
sync_cache.setex(
|
||||
_KEY_DC.format(device_code),
|
||||
PENDING_TTL_SECONDS,
|
||||
json.dumps(data),
|
||||
sync_cache.set(
|
||||
_KEY_DC.format(device_code), json.dumps(data), ex=PENDING_TTL_SECONDS
|
||||
)
|
||||
return None
|
||||
return cast(dict[str, Any], data)
|
||||
|
||||
+2
-3
@@ -46,7 +46,7 @@ dependencies = [
|
||||
"python-dotenv ~= 1.2",
|
||||
"python-magic ~= 0.4",
|
||||
"python-socketio ~= 5.16",
|
||||
"redis ~= 6.2",
|
||||
"redis ~= 8.1",
|
||||
"rq ~= 2.12",
|
||||
"sentry-sdk ~= 2.32",
|
||||
"starlette ~= 1.6.0",
|
||||
@@ -72,10 +72,9 @@ dev = [
|
||||
"types-defusedxml ~= 0.7",
|
||||
"types-passlib ~= 1.7",
|
||||
"types-pyyaml ~= 6.0",
|
||||
"types-redis ~= 4.6",
|
||||
]
|
||||
test = [
|
||||
"fakeredis ~= 2.21",
|
||||
"fakeredis ~= 2.38",
|
||||
"hypothesis ~= 6.0",
|
||||
"pytest ~= 9.0",
|
||||
"pytest-asyncio ~= 1.1",
|
||||
|
||||
@@ -601,15 +601,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fakeredis"
|
||||
version = "2.30.1"
|
||||
version = "2.38.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "redis" },
|
||||
{ name = "sortedcontainers" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/86/3b/eb1d4d0fdc138df1d8e625dfa6b500189030e6c1c265b8dd22b783b2f9ec/fakeredis-2.30.1.tar.gz", hash = "sha256:6489f2926e39815c9bf0fce80751635e0898e333c43a767825adf101180dbc45", size = 167724, upload-time = "2025-06-19T17:55:45.549Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/6b/ad7db311fea3a62f7e359720cc8c064f06f8f0006e65272f50d3a23d37f6/fakeredis-2.38.0.tar.gz", hash = "sha256:d2abfd24652f86501044499bf08c9d639db050f695eefc06b8b8b6f0bb24dbd6", size = 271116, upload-time = "2026-09-08T21:15:04.806Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/ee/acc3de71b8c66029ea4567d83e9c736d79836b2d97aa2cacf1b83f96c678/fakeredis-2.30.1-py3-none-any.whl", hash = "sha256:b594a9c20aef8b94c4d923f489210ef443e4001e62ad3cd73b9a01298dcef743", size = 116215, upload-time = "2025-06-19T17:55:43.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/8e/9a80a8808e8a723aacb3a5d9df07890b46b363a056b372d3cd2e655259fe/fakeredis-2.38.0-py3-none-any.whl", hash = "sha256:d9fb0518c4eaa35f1f2c94df6b4a4c97ff3ca9f43d6cc8112317a9037d244301", size = 167360, upload-time = "2026-09-08T21:15:03.219Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2292,11 +2292,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "6.2.0"
|
||||
version = "8.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ea/9a/0551e01ba52b944f97480721656578c8a7c46b51b99d66814f85fe3a4f3e/redis-6.2.0.tar.gz", hash = "sha256:e821f129b75dde6cb99dd35e5c76e8c49512a5a0d8dfdc560b2fbd44b85ca977", size = 4639129, upload-time = "2025-05-28T05:01:18.91Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/99/604f0b666d4c616d891cf77ebb9db6bb21601344c051aebf1b72b9ff915f/redis-8.1.0.tar.gz", hash = "sha256:6e1a19beef9225c83efd689c7e6b7da2d5215b1f42cd13b7fc3714d0a09c7b25", size = 5254356, upload-time = "2026-07-30T08:51:00.269Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/67/e60968d3b0e077495a8fee89cf3f2373db98e528288a48f1ee44967f6e8c/redis-6.2.0-py3-none-any.whl", hash = "sha256:c8ddf316ee0aab65f04a11229e94a64b2618451dab7a67cb2f77eb799d872d5e", size = 278659, upload-time = "2025-05-28T05:01:16.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/9d/c5731f6e3608663d4d3656fd8d3aecee8b509c3082818f5a13eae925baea/redis-8.1.0-py3-none-any.whl", hash = "sha256:a4fe1aac3d3b3cc791d4b3d5931c5a956045dc951ee74d1c913ee3ac4d2ee9fb", size = 560618, upload-time = "2026-07-30T08:50:58.497Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2419,7 +2419,6 @@ dev = [
|
||||
{ name = "types-defusedxml" },
|
||||
{ name = "types-passlib" },
|
||||
{ name = "types-pyyaml" },
|
||||
{ name = "types-redis" },
|
||||
]
|
||||
test = [
|
||||
{ name = "fakeredis" },
|
||||
@@ -2445,7 +2444,7 @@ requires-dist = [
|
||||
{ name = "colorama", specifier = "~=0.4" },
|
||||
{ name = "cryptography", specifier = ">=49,<51" },
|
||||
{ name = "defusedxml", specifier = "~=0.7" },
|
||||
{ name = "fakeredis", marker = "extra == 'test'", specifier = "~=2.21" },
|
||||
{ name = "fakeredis", marker = "extra == 'test'", specifier = "~=2.38" },
|
||||
{ name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier = "~=0.141.1" },
|
||||
{ name = "gunicorn", specifier = "~=26.0" },
|
||||
{ name = "httpx2", specifier = "~=2.13" },
|
||||
@@ -2481,7 +2480,7 @@ requires-dist = [
|
||||
{ name = "python-magic", specifier = "~=0.4" },
|
||||
{ name = "python-socketio", specifier = "~=5.16" },
|
||||
{ name = "pyyaml", specifier = "~=6.0" },
|
||||
{ name = "redis", specifier = "~=6.2" },
|
||||
{ name = "redis", specifier = "~=8.1" },
|
||||
{ name = "rq", specifier = "~=2.12" },
|
||||
{ name = "sentry-sdk", specifier = "~=2.32" },
|
||||
{ name = "sqlalchemy", extras = ["mariadb-connector", "mysql-connector", "postgresql-psycopg"], specifier = "~=2.0" },
|
||||
@@ -2493,7 +2492,6 @@ requires-dist = [
|
||||
{ name = "types-defusedxml", marker = "extra == 'dev'", specifier = "~=0.7" },
|
||||
{ name = "types-passlib", marker = "extra == 'dev'", specifier = "~=1.7" },
|
||||
{ name = "types-pyyaml", marker = "extra == 'dev'", specifier = "~=6.0" },
|
||||
{ name = "types-redis", marker = "extra == 'dev'", specifier = "~=4.6" },
|
||||
{ name = "ua-parser", specifier = "~=1.0" },
|
||||
{ name = "unidecode", specifier = "~=1.3" },
|
||||
{ name = "uvicorn", specifier = "~=0.53" },
|
||||
@@ -2735,18 +2733,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/65/d9206c38d65602867f15bda7836b1248295b31f3187a09cc1f6195539ebe/types_authlib-1.8.0.20260911-py3-none-any.whl", hash = "sha256:0617c4a2361a605144bae0850f70c18edd5265abec11af66235a02a1b415444e", size = 111525, upload-time = "2026-09-11T06:45:25.033Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-cffi"
|
||||
version = "1.17.0.20250523"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "types-setuptools" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f7/5f/ac80a2f55757019e5d4809d17544569c47a623565258ca1a836ba951d53f/types_cffi-1.17.0.20250523.tar.gz", hash = "sha256:e7110f314c65590533adae1b30763be08ca71ad856a1ae3fe9b9d8664d49ec22", size = 16858, upload-time = "2025-05-23T03:05:40.983Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/86/e26e6ae4dfcbf6031b8422c22cf3a9eb2b6d127770406e7645b6248d8091/types_cffi-1.17.0.20250523-py3-none-any.whl", hash = "sha256:e98c549d8e191f6220e440f9f14315d6775a21a0e588c32c20476be885b2fad9", size = 20010, upload-time = "2025-05-23T03:05:39.136Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-colorama"
|
||||
version = "0.4.15.20240311"
|
||||
@@ -2774,19 +2760,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/fc/530236c21f1a0be84c42b23c91c250ef96404c475b739ac4479430ebd7d4/types_passlib-1.7.7.20250602-py3-none-any.whl", hash = "sha256:ed73a91be9a22484ebd62cc0d127675ded542b892b99776db92dab760bbfe274", size = 40410, upload-time = "2025-06-02T03:14:54.834Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-pyopenssl"
|
||||
version = "24.1.0.20240722"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "types-cffi" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/93/29/47a346550fd2020dac9a7a6d033ea03fccb92fa47c726056618cc889745e/types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39", size = 8458, upload-time = "2024-07-22T02:32:22.558Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/98/05/c868a850b6fbb79c26f5f299b768ee0adc1f9816d3461dcf4287916f655b/types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54", size = 7499, upload-time = "2024-07-22T02:32:21.232Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-pyyaml"
|
||||
version = "6.0.12.20250516"
|
||||
@@ -2796,28 +2769,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/99/5f/e0af6f7f6a260d9af67e1db4f54d732abad514252a7a378a6c4d17dd1036/types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530", size = 20312, upload-time = "2025-05-16T03:08:04.019Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-redis"
|
||||
version = "4.6.0.20241004"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "types-pyopenssl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/95/c054d3ac940e8bac4ca216470c80c26688a0e79e09f520a942bb27da3386/types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e", size = 49679, upload-time = "2024-10-04T02:43:59.224Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/55/82/7d25dce10aad92d2226b269bce2f85cfd843b4477cd50245d7d40ecf8f89/types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed", size = 58737, upload-time = "2024-10-04T02:43:57.968Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-setuptools"
|
||||
version = "80.9.0.20250529"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/79/66/1b276526aad4696a9519919e637801f2c103419d2c248a6feb2729e034d1/types_setuptools-80.9.0.20250529.tar.gz", hash = "sha256:79e088ba0cba2186c8d6499cbd3e143abb142d28a44b042c28d3148b1e353c91", size = 41337, upload-time = "2025-05-29T03:07:34.487Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/d8/83790d67ec771bf029a45ff1bd1aedbb738d8aa58c09dd0cc3033eea0e69/types_setuptools-80.9.0.20250529-py3-none-any.whl", hash = "sha256:00dfcedd73e333a430e10db096e4d46af93faf9314f832f13b6bbe3d6757e95f", size = 63263, upload-time = "2025-05-29T03:07:33.064Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.16.0"
|
||||
|
||||
Reference in New Issue
Block a user