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:
Sam Dornan
2026-09-27 13:28:09 -05:00
committed by GitHub
co-authored by Claude Opus 5.5
parent e06d4a7bd6
commit 6b13fd1698
17 changed files with 64 additions and 125 deletions
+1 -1
View File
@@ -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/
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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] = []
+7 -7
View File
@@ -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
+1 -1
View File
@@ -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()
+2 -2
View File
@@ -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()}
+6 -6
View File
@@ -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
View File
@@ -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
+8 -8
View File
@@ -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
View File
@@ -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
+12 -21
View File
@@ -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
View File
@@ -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",
Generated
+8 -57
View File
@@ -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"