feat(bot): 🎉 Create a redis client from scratch.
This commit is contained in:
@@ -6,15 +6,6 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
|
|
||||||
serverless-redis-http:
|
|
||||||
ports:
|
|
||||||
- '8079:80'
|
|
||||||
image: hiett/serverless-redis-http:latest
|
|
||||||
environment:
|
|
||||||
SRH_MODE: env
|
|
||||||
SRH_TOKEN: payphone
|
|
||||||
SRH_CONNECTION_STRING: 'redis://redis:6379'
|
|
||||||
|
|
||||||
postgresql:
|
postgresql:
|
||||||
image: postgres:latest
|
image: postgres:latest
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -33,46 +33,3 @@ spec:
|
|||||||
port: 6379
|
port: 6379
|
||||||
targetPort: 6379
|
targetPort: 6379
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
|
|
||||||
---
|
|
||||||
# serverless-redis-http Pod
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Pod
|
|
||||||
metadata:
|
|
||||||
name: serverless-redis
|
|
||||||
labels:
|
|
||||||
app: serverless-redis
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: serverless-redis
|
|
||||||
image: hiett/serverless-redis-http:latest
|
|
||||||
ports:
|
|
||||||
- containerPort: 80 # Internal container port
|
|
||||||
env:
|
|
||||||
- name: SRH_MODE
|
|
||||||
value: "env"
|
|
||||||
- name: SRH_TOKEN
|
|
||||||
value: "payphone"
|
|
||||||
- name: SRH_CONNECTION_STRING
|
|
||||||
value: "redis://redis:6379"
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
memory: "1Gi"
|
|
||||||
cpu: "1"
|
|
||||||
limits:
|
|
||||||
memory: "2Gi"
|
|
||||||
cpu: "1"
|
|
||||||
|
|
||||||
---
|
|
||||||
# serverless-redis-http Service
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: serverless-redis
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
ports:
|
|
||||||
- port: 8079 # Exposes port 8079 for internal cluster access
|
|
||||||
targetPort: 80 # Routes to container port 80
|
|
||||||
selector:
|
|
||||||
app: serverless-redis
|
|
||||||
|
|||||||
+3
-4
@@ -11,7 +11,7 @@ from discord.ext.commands import errors
|
|||||||
|
|
||||||
from tortoise import expressions, transactions, exceptions as tortoise_exc
|
from tortoise import expressions, transactions, exceptions as tortoise_exc
|
||||||
|
|
||||||
import upstash_redis.asyncio as aredis
|
from redis import Redis
|
||||||
|
|
||||||
import asyncpg.exceptions
|
import asyncpg.exceptions
|
||||||
|
|
||||||
@@ -29,9 +29,8 @@ from payphone.core import (
|
|||||||
class Payphone(commands.AutoShardedBot):
|
class Payphone(commands.AutoShardedBot):
|
||||||
log = logging.logger
|
log = logging.logger
|
||||||
|
|
||||||
redis: aredis.Redis = aredis.Redis(
|
redis: Redis = Redis(
|
||||||
url="http://localhost:8079" if config.IS_DEBUG else "http://serverless-redis:8079",
|
url="redis://localhost" if config.IS_DEBUG else "redis://redis",
|
||||||
token="payphone"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
|||||||
+35
-29
@@ -103,8 +103,8 @@ class Calls(base.Cog):
|
|||||||
if last_calls_rss_channels_map[call_rs.call_id] != call_rs.channel_id:
|
if last_calls_rss_channels_map[call_rs.call_id] != call_rs.channel_id:
|
||||||
last_calls_other_channels_ids.add(call_rs.channel_id)
|
last_calls_other_channels_ids.add(call_rs.channel_id)
|
||||||
|
|
||||||
other_channel_id: int = await self.bot.redis.spop(
|
other_channel_id: int = next(
|
||||||
constants.CHANNELS_SEARCH_QUEUE
|
iter(await self.bot.redis.spop(constants.CHANNELS_SEARCH_QUEUE)), None
|
||||||
)
|
)
|
||||||
i = 0
|
i = 0
|
||||||
|
|
||||||
@@ -127,8 +127,9 @@ class Calls(base.Cog):
|
|||||||
constants.CHANNELS_SEARCH_QUEUE, str(other_channel_id)
|
constants.CHANNELS_SEARCH_QUEUE, str(other_channel_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
other_channel_id = await self.bot.redis.spop(
|
other_channel_id = next(
|
||||||
constants.CHANNELS_SEARCH_QUEUE
|
iter(await self.bot.redis.spop(constants.CHANNELS_SEARCH_QUEUE)),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if other_channel_id is None:
|
if other_channel_id is None:
|
||||||
@@ -293,8 +294,8 @@ class Calls(base.Cog):
|
|||||||
if last_calls_rss_channels_map[call_rs.call_id] != call_rs.channel_id:
|
if last_calls_rss_channels_map[call_rs.call_id] != call_rs.channel_id:
|
||||||
last_calls_other_channels_ids.add(call_rs.channel_id)
|
last_calls_other_channels_ids.add(call_rs.channel_id)
|
||||||
|
|
||||||
other_channel_id: int = await self.bot.redis.spop(
|
other_channel_id: int = next(
|
||||||
constants.CHANNELS_SEARCH_QUEUE
|
iter(await self.bot.redis.spop(constants.CHANNELS_SEARCH_QUEUE)), None
|
||||||
)
|
)
|
||||||
i = 0
|
i = 0
|
||||||
|
|
||||||
@@ -317,8 +318,9 @@ class Calls(base.Cog):
|
|||||||
constants.CHANNELS_SEARCH_QUEUE, str(other_channel_id)
|
constants.CHANNELS_SEARCH_QUEUE, str(other_channel_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
other_channel_id = await self.bot.redis.spop(
|
other_channel_id = next(
|
||||||
constants.CHANNELS_SEARCH_QUEUE
|
iter(await self.bot.redis.spop(constants.CHANNELS_SEARCH_QUEUE)),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if other_channel_id is None:
|
if other_channel_id is None:
|
||||||
@@ -762,8 +764,8 @@ class Calls(base.Cog):
|
|||||||
|
|
||||||
pipe.hset(
|
pipe.hset(
|
||||||
constants.CALL_MESSAGES_TEMPLATE % call.id,
|
constants.CALL_MESSAGES_TEMPLATE % call.id,
|
||||||
message.id,
|
{
|
||||||
orjson.dumps(
|
message.id: orjson.dumps(
|
||||||
{
|
{
|
||||||
"id": message.id,
|
"id": message.id,
|
||||||
"call_id": call.id,
|
"call_id": call.id,
|
||||||
@@ -771,36 +773,41 @@ class Calls(base.Cog):
|
|||||||
"user_id": user.id,
|
"user_id": user.id,
|
||||||
"profile_id": profile.id,
|
"profile_id": profile.id,
|
||||||
"mask": call_rs.mask if call_rs.is_masked else None,
|
"mask": call_rs.mask if call_rs.is_masked else None,
|
||||||
"propagation_id": webhook_message.id if webhook_message else None,
|
"propagation_id": (
|
||||||
|
webhook_message.id if webhook_message else None
|
||||||
|
),
|
||||||
"propagation_channel_id": (
|
"propagation_channel_id": (
|
||||||
other_channel.id if webhook_message else None
|
other_channel.id if webhook_message else None
|
||||||
),
|
),
|
||||||
"replying_to_id": replying_to_id,
|
"replying_to_id": replying_to_id,
|
||||||
"created_at": datetime.now(timezone.utc),
|
"created_at": datetime.now(timezone.utc),
|
||||||
}
|
}
|
||||||
).decode(),
|
).decode()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
pipe.hset(
|
pipe.hset(
|
||||||
constants.CALL_MESSAGE_CONTENTS_TEMPLATE % call.id,
|
constants.CALL_MESSAGE_CONTENTS_TEMPLATE % call.id,
|
||||||
constants.CALL_MESSAGE_CONTENTS_HASH_TEMPLATE % (message.id, 1),
|
{
|
||||||
orjson.dumps(
|
constants.CALL_MESSAGE_CONTENTS_HASH_TEMPLATE
|
||||||
|
% (message.id, 1): orjson.dumps(
|
||||||
{
|
{
|
||||||
"message_id": message.id,
|
"message_id": message.id,
|
||||||
"content": cleaned_content,
|
"content": cleaned_content,
|
||||||
"was_propagated": not bool(blocked_word),
|
"was_propagated": not bool(blocked_word),
|
||||||
"created_at": datetime.now(timezone.utc),
|
"created_at": datetime.now(timezone.utc),
|
||||||
}
|
}
|
||||||
).decode(),
|
).decode()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
pipe.hset(
|
pipe.hset(
|
||||||
constants.CALL_MESSAGE_CONTENT_ITERATIONS_TEMPLATE % call.id, message.id, 1
|
constants.CALL_MESSAGE_CONTENT_ITERATIONS_TEMPLATE % call.id,
|
||||||
|
{message.id: 1},
|
||||||
)
|
)
|
||||||
|
|
||||||
if webhook_message is not None:
|
if webhook_message is not None:
|
||||||
pipe.hset(
|
pipe.hset(
|
||||||
constants.CALL_MESSAGE_FROM_PROPAGATION_TEMPLATE % call.id,
|
constants.CALL_MESSAGE_FROM_PROPAGATION_TEMPLATE % call.id,
|
||||||
webhook_message.id,
|
{webhook_message.id: message.id},
|
||||||
message.id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if replying_to_id is not None:
|
if replying_to_id is not None:
|
||||||
@@ -811,18 +818,16 @@ class Calls(base.Cog):
|
|||||||
if other_replies is None:
|
if other_replies is None:
|
||||||
pipe.hset(
|
pipe.hset(
|
||||||
constants.CALL_MESSAGE_REPLIES_TEMPLATE % call.id,
|
constants.CALL_MESSAGE_REPLIES_TEMPLATE % call.id,
|
||||||
replying_to_id,
|
{replying_to_id: message.id},
|
||||||
message.id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
pipe.hset(
|
pipe.hset(
|
||||||
constants.CALL_MESSAGE_REPLIES_TEMPLATE % call.id,
|
constants.CALL_MESSAGE_REPLIES_TEMPLATE % call.id,
|
||||||
replying_to_id,
|
{replying_to_id: other_replies + f",{message.id}"},
|
||||||
other_replies + f",{message.id}",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await pipe.exec()
|
await pipe.execute()
|
||||||
|
|
||||||
@commands.Cog.listener()
|
@commands.Cog.listener()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@@ -862,8 +867,7 @@ class Calls(base.Cog):
|
|||||||
|
|
||||||
await self.bot.redis.hset(
|
await self.bot.redis.hset(
|
||||||
constants.CALL_MESSAGES_TEMPLATE % channel.call_id,
|
constants.CALL_MESSAGES_TEMPLATE % channel.call_id,
|
||||||
message.id,
|
{message.id: orjson.dumps(message_dict).decode()},
|
||||||
orjson.dumps(message_dict).decode(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
other_channel = await models.Channel.get_or_none(
|
other_channel = await models.Channel.get_or_none(
|
||||||
@@ -975,18 +979,20 @@ class Calls(base.Cog):
|
|||||||
)
|
)
|
||||||
pipe.hset(
|
pipe.hset(
|
||||||
constants.CALL_MESSAGE_CONTENTS_TEMPLATE % channel.call_id,
|
constants.CALL_MESSAGE_CONTENTS_TEMPLATE % channel.call_id,
|
||||||
constants.CALL_MESSAGE_CONTENTS_HASH_TEMPLATE % (message.id, iteration + 1),
|
{
|
||||||
orjson.dumps(
|
constants.CALL_MESSAGE_CONTENTS_HASH_TEMPLATE
|
||||||
|
% (message.id, iteration + 1): orjson.dumps(
|
||||||
{
|
{
|
||||||
"message_id": message.id,
|
"message_id": message.id,
|
||||||
"content": cleaned_content,
|
"content": cleaned_content,
|
||||||
"was_propagated": not bool(blocked_word),
|
"was_propagated": not bool(blocked_word),
|
||||||
"created_at": datetime.now(timezone.utc),
|
"created_at": datetime.now(timezone.utc),
|
||||||
}
|
}
|
||||||
).decode(),
|
).decode()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
await pipe.exec()
|
await pipe.execute()
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
webhook = other_channel.webhook(session)
|
webhook = other_channel.webhook(session)
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ import discord
|
|||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
|
|
||||||
import upstash_redis.asyncio as aredis
|
from redis import Redis
|
||||||
|
|
||||||
from payphone.db import models
|
from payphone.db import models
|
||||||
from payphone.core import constants, formatting
|
from payphone.core import constants, formatting
|
||||||
|
|
||||||
|
|
||||||
async def ReplyEmbed(
|
async def ReplyEmbed(
|
||||||
redis: aredis.Redis, reference: discord.MessageReference, call_id: int
|
redis: Redis, reference: discord.MessageReference, call_id: int
|
||||||
) -> tuple[discord.Embed | None, models.Message | None]:
|
) -> tuple[discord.Embed | None, models.Message | None]:
|
||||||
"""Generates the embed sent on calls to represent replies.
|
"""Generates the embed sent on calls to represent replies.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
redis (redis.asyncio.Redis): An instance of a Redis connection handler.
|
redis (redis.Redis): An instance of a Redis connection handler.
|
||||||
reference (discord.MessageReference): The message reference.
|
reference (discord.MessageReference): The message reference.
|
||||||
call_id (int): The ID of the call the message was sent in.
|
call_id (int): The ID of the call the message was sent in.
|
||||||
|
|
||||||
|
|||||||
@@ -9,20 +9,20 @@ async def flush():
|
|||||||
|
|
||||||
import asyncpg.exceptions
|
import asyncpg.exceptions
|
||||||
|
|
||||||
import upstash_redis.asyncio as aredis
|
import redis
|
||||||
|
|
||||||
from payphone.db import models
|
from payphone.db import models
|
||||||
|
|
||||||
from payphone.core import constants, utils, config
|
from payphone.core import constants, utils, config
|
||||||
|
|
||||||
redis = aredis.Redis(host="localhost" if config.IS_DEBUG else "redis")
|
redis = redis.Redis("localhost" if config.IS_DEBUG else "redis")
|
||||||
|
|
||||||
pipe = redis.pipeline()
|
pipe = redis.pipeline()
|
||||||
|
|
||||||
pipe.smembers(constants.CALL_IDS_TO_FLUSH)
|
pipe.smembers(constants.CALL_IDS_TO_FLUSH)
|
||||||
pipe.delete(constants.CALL_IDS_TO_FLUSH)
|
pipe.delete(constants.CALL_IDS_TO_FLUSH)
|
||||||
|
|
||||||
call_ids, _ = await pipe.exec()
|
call_ids, _ = await pipe.execute()
|
||||||
|
|
||||||
# Redis returns bytes.
|
# Redis returns bytes.
|
||||||
call_ids = [int(call_id) for call_id in call_ids]
|
call_ids = [int(call_id) for call_id in call_ids]
|
||||||
@@ -40,7 +40,7 @@ async def flush():
|
|||||||
pipe.hgetall(constants.CALL_MESSAGE_CONTENTS_TEMPLATE % call_id)
|
pipe.hgetall(constants.CALL_MESSAGE_CONTENTS_TEMPLATE % call_id)
|
||||||
|
|
||||||
for call_id, (messages, message_contents) in zip(
|
for call_id, (messages, message_contents) in zip(
|
||||||
call_ids, utils.chunks(await pipe.exec(), 2)
|
call_ids, utils.chunks(await pipe.execute(), 2)
|
||||||
):
|
):
|
||||||
message_rows: list[models.Message] = []
|
message_rows: list[models.Message] = []
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ async def flush():
|
|||||||
constants.CALL_IS_ACTIVE % call_id,
|
constants.CALL_IS_ACTIVE % call_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
await pipe.exec()
|
await pipe.execute()
|
||||||
|
|
||||||
for _ in range(5):
|
for _ in range(5):
|
||||||
try:
|
try:
|
||||||
|
|||||||
+14
-2
@@ -1,10 +1,22 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "payphone"
|
name = "payphone"
|
||||||
version = "0.12.0"
|
version = "0.13.0"
|
||||||
description = "Add your description here"
|
description = "Add your description here"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [ "aerich==0.7.1", "aiohttp>=3.10.10", "click>=8.1.7", "colorthief>=0.2.1", "httpx>=0.27.2", "orjson>=3.10.7", "py-cord>=2.6.1", "python-dotenv>=1.0.1", "rich>=13.9.3", "tortoise-orm[asyncpg]>=0.21.7", "upstash-redis>=1.2.0",]
|
dependencies = [
|
||||||
|
"aerich==0.7.1",
|
||||||
|
"aiohttp>=3.10.10",
|
||||||
|
"click>=8.1.7",
|
||||||
|
"colorthief>=0.2.1",
|
||||||
|
"hiredis>=3.0.0",
|
||||||
|
"httpx>=0.27.2",
|
||||||
|
"orjson>=3.10.7",
|
||||||
|
"py-cord>=2.6.1",
|
||||||
|
"python-dotenv>=1.0.1",
|
||||||
|
"rich>=13.9.3",
|
||||||
|
"tortoise-orm[asyncpg]>=0.21.7",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.aerich]
|
[tool.aerich]
|
||||||
tortoise_orm = "payphone.core.config.TORTOISE_ORM"
|
tortoise_orm = "payphone.core.config.TORTOISE_ORM"
|
||||||
|
|||||||
@@ -0,0 +1,826 @@
|
|||||||
|
from typing import TypeAlias, Awaitable, AsyncGenerator
|
||||||
|
|
||||||
|
from urllib import parse
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import hiredis
|
||||||
|
|
||||||
|
|
||||||
|
Resp3Repr: TypeAlias = str | int | bytes | bytearray | float | bool | None
|
||||||
|
|
||||||
|
|
||||||
|
class _Ellipsis:
|
||||||
|
"""Represents an incomplete response of Redis."""
|
||||||
|
|
||||||
|
|
||||||
|
def _encode(
|
||||||
|
arg: (
|
||||||
|
Resp3Repr
|
||||||
|
| list[Resp3Repr]
|
||||||
|
| tuple[Resp3Repr]
|
||||||
|
| dict[Resp3Repr, Resp3Repr]
|
||||||
|
| set[Resp3Repr]
|
||||||
|
)
|
||||||
|
) -> bytes:
|
||||||
|
"""Encode a value into its RESP3 representation.
|
||||||
|
|
||||||
|
This function supports str (converted to bytes), int, bytes, bytearray, float, bool, and None
|
||||||
|
as basic types. It also supports lists (arrays), dicts (maps), and sets.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arg (Resp3Repr | list[Resp3Repr] | tuple[Resp3Repr] | dict[Resp3Repr, Resp3Repr] | set[Resp3Repr]):
|
||||||
|
The argument to encode.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: The value provided is invalid or not encodable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bytes: The encoded representation of the value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(arg, (str, int)):
|
||||||
|
arg = str(arg).encode()
|
||||||
|
|
||||||
|
if isinstance(arg, (bytes, bytearray)):
|
||||||
|
return f"${len(arg)}\r\n".encode() + bytes(arg) + b"\r\n"
|
||||||
|
|
||||||
|
elif isinstance(arg, int):
|
||||||
|
return f":{arg}\r\n".encode()
|
||||||
|
|
||||||
|
elif isinstance(arg, type(None)):
|
||||||
|
return b"_\r\n"
|
||||||
|
|
||||||
|
elif isinstance(arg, float):
|
||||||
|
result = f",{arg}\r\n".encode()
|
||||||
|
|
||||||
|
elif isinstance(arg, bool):
|
||||||
|
if arg:
|
||||||
|
return b"#t\r\n"
|
||||||
|
|
||||||
|
return b"#f\r\n"
|
||||||
|
|
||||||
|
elif isinstance(arg, (list, tuple)):
|
||||||
|
return f"*{len(arg)}\r\n".encode() + b"".join([_encode(i) for i in arg])
|
||||||
|
|
||||||
|
elif isinstance(arg, dict):
|
||||||
|
result = f"%{len(arg.keys())}\r\n".encode()
|
||||||
|
|
||||||
|
for key, value in arg.items():
|
||||||
|
result += _encode(key) + _encode(value)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
elif isinstance(arg, set):
|
||||||
|
result = f"~{len(arg)}\r\n".encode()
|
||||||
|
|
||||||
|
for item in arg:
|
||||||
|
result += _encode(item)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
raise ValueError(f"Value of type {type(arg)} is not encodable into RESP3.")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_secs(time: int | timedelta) -> int:
|
||||||
|
"""Converts an integer or timedelta into an amount of seconds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time (int | timedelta): The number or timedelta to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The amount of seconds.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(time, timedelta):
|
||||||
|
return int(time.total_seconds())
|
||||||
|
|
||||||
|
return time
|
||||||
|
|
||||||
|
|
||||||
|
def _get_millis(time: int | timedelta) -> int:
|
||||||
|
"""Converts an integer or timedelta into an amount of milliseconds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time (int | timedelta): The number or timedelta to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The amount of milliseconds.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(time, timedelta):
|
||||||
|
return int(time.total_seconds() * 1000)
|
||||||
|
|
||||||
|
return time
|
||||||
|
|
||||||
|
|
||||||
|
def _get_timestamp_secs(dtime: datetime | int) -> int:
|
||||||
|
"""Converts a datetime or timestamp into it's seconds timestamp.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dtime (datetime | int): The datetime object to convert or an integer value which will be
|
||||||
|
returned as-is.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The seconds timestamp.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(dtime, datetime):
|
||||||
|
return int(dtime.timestamp())
|
||||||
|
|
||||||
|
return dtime
|
||||||
|
|
||||||
|
|
||||||
|
def _get_timestamp_millis(dtime: datetime | int) -> int:
|
||||||
|
"""Converts a datetime or timestamp into it's milliseconds timestamp.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dtime (datetime | int): The datetime object to convert or an integer value which will be
|
||||||
|
returned as-is.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The milliseconds timestamp.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(dtime, datetime):
|
||||||
|
return int(dtime.timestamp() * 1000)
|
||||||
|
|
||||||
|
return dtime
|
||||||
|
|
||||||
|
|
||||||
|
class RedisError(Exception):
|
||||||
|
"""Raised whenever an error is raised from this library."""
|
||||||
|
|
||||||
|
|
||||||
|
class FatalRedisError(RedisError):
|
||||||
|
"""Raised whenever an error occurs that does not allow the connection to stay alive."""
|
||||||
|
|
||||||
|
|
||||||
|
class Runnable:
|
||||||
|
"""The base class for all Redis commands.
|
||||||
|
|
||||||
|
This class is inherited by Connection, Pipeline, and Transaction to implement their own
|
||||||
|
functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_client: "Redis"
|
||||||
|
|
||||||
|
def __init__(self, client: "Redis") -> None:
|
||||||
|
"""Initializes the runner with the client's configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client (Redis): The client that initialized the runner.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._client = client
|
||||||
|
|
||||||
|
def command(self, *args: tuple[Resp3Repr]) -> Resp3Repr | Awaitable[Resp3Repr]:
|
||||||
|
"""Runs a command taking it as a list of arguments.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
```
|
||||||
|
await Runnable.command("SET", "key", "value")
|
||||||
|
```
|
||||||
|
|
||||||
|
This method is not implemented by default, so you need a Connection, a Pipeline, or a
|
||||||
|
Transaction to use it.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Resp3Repr | Awaitable[Resp3Repr]: The value(s) returned by the execution of the
|
||||||
|
command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise NotImplementedError(
|
||||||
|
"This method needs to be implemented by a subclass of Runner."
|
||||||
|
)
|
||||||
|
|
||||||
|
def ping(self, arg: str = "PONG") -> Resp3Repr:
|
||||||
|
"""Returns PONG if no argument is provided, otherwise return a copy of the argument as a
|
||||||
|
bulk.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arg (str): The argument to return by the command.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Resp3Repr: The response of the command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.command("PING", str(arg))
|
||||||
|
|
||||||
|
|
||||||
|
class RunnableGenericMixin:
|
||||||
|
"""Mixin containing all Redis generic commands."""
|
||||||
|
|
||||||
|
def delete(self, *keys: tuple[str]) -> None:
|
||||||
|
"""Deletes one or more keys."""
|
||||||
|
|
||||||
|
return self.command("DEL", *[str(key) for key in keys])
|
||||||
|
|
||||||
|
def exists(self, *keys: tuple[str]) -> int:
|
||||||
|
"""Returns if key exists.
|
||||||
|
|
||||||
|
The user should be aware that if the same existing key is mentioned in the arguments
|
||||||
|
multiple times, it will be counted multiple times. So if somekey exists, `EXISTS somekey
|
||||||
|
somekey` will return 2.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*keys (tuple[str]): The keys to check for existance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The amount of keys that exist.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.command("EXISTS", *[str(key) for key in keys])
|
||||||
|
|
||||||
|
|
||||||
|
class RunnableHashMixin:
|
||||||
|
"""Mixin containing all Redis hash commands."""
|
||||||
|
|
||||||
|
def hget(self, key: str, field: str) -> str | None:
|
||||||
|
"""Returns the value associated with field in the hash stored at key.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The key of the hash.
|
||||||
|
field (str): The field of the hash to get.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str | None: The value of the field, or None if it does not exist.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.command("HGET", str(key), str(field))
|
||||||
|
|
||||||
|
def hgetall(self, key: str) -> dict[Resp3Repr, Resp3Repr]:
|
||||||
|
"""Returns all fields and values of the hash stored at key.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The key to get the fields and values from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[Resp3Repr, Resp3Repr]: A dict containing all the fields mapped to their respective
|
||||||
|
values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.command("HGETALL", str(key))
|
||||||
|
|
||||||
|
def hincrby(self, key: str, field: str, increment: int) -> int:
|
||||||
|
"""Increments the number stored at field in the hash stored at key by increment.
|
||||||
|
|
||||||
|
If key does not exist, a new key holding a hash is created. If field does not exist the
|
||||||
|
value is set to 0 before the operation is performed. The range of values supported by
|
||||||
|
HINCRBY is limited to 64 bit signed integers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The key of the hash whose field to increment.
|
||||||
|
field (str): The field to increment.
|
||||||
|
increment (int): The amount to increment by.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The amount returned
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.command("HINCRBY", str(key), str(field), str(increment))
|
||||||
|
|
||||||
|
def hset(self, key: str, mapping: dict[Resp3Repr, Resp3Repr]) -> int:
|
||||||
|
"""Sets the specified fields to their respective values in the hash stored at key.
|
||||||
|
|
||||||
|
This command overwrites the values of specified fields that exist in the hash. If key
|
||||||
|
doesn't exist, a new key holding a hash is created.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The key to set the values for.
|
||||||
|
mapping (dict[Resp3Repr, Resp3Repr]): The fields mapped to their values.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The number of fields that were added.
|
||||||
|
"""
|
||||||
|
|
||||||
|
items = []
|
||||||
|
|
||||||
|
for k, v in mapping.items():
|
||||||
|
items.append(k)
|
||||||
|
items.append(v)
|
||||||
|
|
||||||
|
return self.command("HSET", str(key), *items)
|
||||||
|
|
||||||
|
|
||||||
|
class RunnableSetMixin:
|
||||||
|
"""Mixin containing all Redis set commands."""
|
||||||
|
|
||||||
|
def sadd(self, key: str, *values: tuple[str]) -> int:
|
||||||
|
"""Adds the specified members to the set stored at key.
|
||||||
|
|
||||||
|
Specified members that are already a member of this set are ignored. If key does not exist,
|
||||||
|
a new set is created before adding the specified members. An error is raised when the value
|
||||||
|
stored at key is not a set.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The key to add the values to.
|
||||||
|
*values (tuple[str]): The values to add to the set.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The amount of new values that were added.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.command("SADD", str(key), *values)
|
||||||
|
|
||||||
|
def scard(self, key: str) -> int:
|
||||||
|
"""Returns the set cardinality (number of elements) of the set stored at key.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The key of the set to get the cardinality from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The number of elements, or 0 if it does not exist.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.command("SCARD", str(key))
|
||||||
|
|
||||||
|
def smembers(self, key: str) -> set[str]:
|
||||||
|
"""Returns all the members of the set value stored at key.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The key of the set to get the members from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
set[str]: The members of the set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.command("SMEMBERS", str(key))
|
||||||
|
|
||||||
|
def sismember(self, key: str, value: str) -> bool:
|
||||||
|
"""Returns whether the value is a member of the set.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The key of the set.
|
||||||
|
value (str): The value to check whether it is a member of the set.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Whether the value is a member of the set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.command("SISMEMBER", str(key), str(value))
|
||||||
|
|
||||||
|
def spop(self, key: str, count: int = 1) -> str | list[str]:
|
||||||
|
"""Removes and returns one or more random members from the set value store at key."""
|
||||||
|
|
||||||
|
return self.command("SPOP", str(key), count)
|
||||||
|
|
||||||
|
def srem(self, key: str, *values: tuple[str]) -> int:
|
||||||
|
"""Removes the specified members from the set stored at key.
|
||||||
|
|
||||||
|
Specified members that are not a member of this set are ignored. If key does not exist,
|
||||||
|
it is treated as an empty set and this command returns 0. An error is returned when the
|
||||||
|
value stored at key is not a set.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The key of the set.
|
||||||
|
*values (tuple[str]): The values to remove from the set.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The amount of values that were removed from the set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.command("SREM", str(key), *values)
|
||||||
|
|
||||||
|
|
||||||
|
class RunnableStringMixin:
|
||||||
|
"""Mixin containing all Redis string commands."""
|
||||||
|
|
||||||
|
def append(self, key: str, value: str) -> int:
|
||||||
|
"""Appends a string to an existing key, or creates a new one if it does not exist.
|
||||||
|
|
||||||
|
If key already exists and is a string, this command appends the value at the end of the
|
||||||
|
string. If key does not exist it is created and set as an empty string, so APPEND will be
|
||||||
|
similar to SET in this special case.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The key to append to.
|
||||||
|
value (str): The string to append.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The new length of the value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.command("APPEND", str(key), str(value))
|
||||||
|
|
||||||
|
def decr(self, key: str) -> int:
|
||||||
|
"""Decrements the number stored at key by one.
|
||||||
|
|
||||||
|
If the key does not exist, it is set to 0 before performing the operation. An error is
|
||||||
|
returned if the key contains a value of the wrong type or contains a string that can not be
|
||||||
|
represented as integer. This operation is limited to 64 bit signed integers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The key to decrement.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The decremented value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.command("DECR", str(key))
|
||||||
|
|
||||||
|
def decrby(self, key: str, decrement: int) -> int:
|
||||||
|
"""Reduces the value stored at the specified key by the specified decrement.
|
||||||
|
|
||||||
|
If the key does not exist, it is initialized with a value of 0 before performing the
|
||||||
|
operation. If the key's value is not of the correct type or cannot be represented as an
|
||||||
|
integer, an error is returned. This operation is limited to 64-bit signed integers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The key to decrement.
|
||||||
|
decrement (int): The amount to decrement by.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The decremented value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.command("DECRBY", str(key), decrement)
|
||||||
|
|
||||||
|
def get(self, key: str) -> str | None:
|
||||||
|
"""Returns the string value of a key.
|
||||||
|
|
||||||
|
If the key does not exist None is returned. An error is returned if the value stored at key
|
||||||
|
is not a string, because GET only handles string values.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The key to get the value from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str | None: The value of the key, of None if it does not exist.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.command("GET", str(key))
|
||||||
|
|
||||||
|
def getdel(self, key: str) -> str | None:
|
||||||
|
"""Returns the value of key and delete the key.
|
||||||
|
|
||||||
|
This command is similar to GET, except for the fact that it also deletes the key on success
|
||||||
|
(if and only if the key's value type is a string).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The key to get and delete.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str | None: The value of the key.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.command("GETDEL", str(key))
|
||||||
|
|
||||||
|
def set(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
value: str,
|
||||||
|
nx: bool = False,
|
||||||
|
xx: bool = False,
|
||||||
|
get: bool = False,
|
||||||
|
ex: timedelta | int | None = None,
|
||||||
|
px: timedelta | int | None = None,
|
||||||
|
exat: datetime | int | None = None,
|
||||||
|
pxat: datetime | int | None = None,
|
||||||
|
keepttl: bool = False,
|
||||||
|
) -> str | None:
|
||||||
|
"""Set key to hold the string value.
|
||||||
|
|
||||||
|
If key already holds a value, it is overwritten, regardless of its type. Any previous time
|
||||||
|
to live associated with the key is discarded on successful SET operation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The key to set.
|
||||||
|
value (str): The value to set to the specified key.
|
||||||
|
nx (bool, optional): Only set the key if it does not already exist. Defaults to False.
|
||||||
|
xx (bool, optional): Only set the key if it already exists. Defaults to False.
|
||||||
|
get (bool, optional): Return the old string stored at key, or None if key did not
|
||||||
|
exist. An error is returned and SET aborted if the value stored at key is not a
|
||||||
|
string. Defaults to False.
|
||||||
|
ex (timedelta | int | None, optional): Set the specified expire time, in seconds (a positive
|
||||||
|
integer). Defaults to None.
|
||||||
|
px (timedelta | int | None, optional): Set the specified expire time, in milliseconds (a positive
|
||||||
|
integer). Defaults to None.
|
||||||
|
exat (datetime | int | None, optional): Set the specified time at which the key will
|
||||||
|
expire, in seconds (a positive integer) or a datetime object. Defaults to None.
|
||||||
|
pxat (datetime | int | None, optional): Set the specified time at which the key will
|
||||||
|
expire, in milliseconds (a positive integer) or a datetime object. Defaults to None.
|
||||||
|
keepttl (bool, optional): Retain the time to live associated with the key. Defaults to
|
||||||
|
False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str | None: The old key in case `get` was set, or None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if nx and xx:
|
||||||
|
raise ValueError("Only one of XX or NX can be set, but not both.")
|
||||||
|
|
||||||
|
if len([i for i in (ex, px, exat, pxat, keepttl) if i]) > 1:
|
||||||
|
raise ValueError(
|
||||||
|
"Only one of EX, PX, EXAT, PXAT, or KEEPTTL can be set, but not multiple of them."
|
||||||
|
)
|
||||||
|
|
||||||
|
command = ["SET", str(key), str(value)]
|
||||||
|
|
||||||
|
if nx:
|
||||||
|
command += ["NX"]
|
||||||
|
|
||||||
|
elif xx:
|
||||||
|
command += ["XX"]
|
||||||
|
|
||||||
|
if get:
|
||||||
|
command += ["GET"]
|
||||||
|
|
||||||
|
if ex:
|
||||||
|
command += ["EX", str(_get_secs(ex))]
|
||||||
|
|
||||||
|
elif px:
|
||||||
|
command += ["PX", str(_get_millis(px))]
|
||||||
|
|
||||||
|
elif exat:
|
||||||
|
command += ["EXAT", str(_get_timestamp_secs(exat))]
|
||||||
|
|
||||||
|
elif pxat:
|
||||||
|
command += ["PXAT", str(_get_timestamp_millis(pxat))]
|
||||||
|
|
||||||
|
elif keepttl:
|
||||||
|
command += ["KEEPTTL"]
|
||||||
|
|
||||||
|
return self.command(*command)
|
||||||
|
|
||||||
|
|
||||||
|
class FullRunnable(
|
||||||
|
Runnable,
|
||||||
|
RunnableGenericMixin,
|
||||||
|
RunnableHashMixin,
|
||||||
|
RunnableSetMixin,
|
||||||
|
RunnableStringMixin,
|
||||||
|
):
|
||||||
|
"""A runnable containing all the command mixins."""
|
||||||
|
|
||||||
|
|
||||||
|
class Connection(FullRunnable):
|
||||||
|
_reader: asyncio.StreamReader
|
||||||
|
_writer: asyncio.StreamWriter
|
||||||
|
_parser: hiredis.Reader
|
||||||
|
_lock: asyncio.Lock
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
client: "Redis",
|
||||||
|
reader: asyncio.StreamReader,
|
||||||
|
writer: asyncio.StreamWriter,
|
||||||
|
parser: hiredis.Reader = hiredis.Reader(notEnoughData=_Ellipsis),
|
||||||
|
) -> None:
|
||||||
|
"""Initializes the connection object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client (Redis): The client that initialized the connection.
|
||||||
|
reader (asyncio.StreamReader): The socket stream reader.
|
||||||
|
writer (asyncio.StreamWriter): The socket stream writer.
|
||||||
|
parser (hiredis.Reader, optional): The RESP3 parser. Defaults to a basic implementation
|
||||||
|
if not set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._client = client
|
||||||
|
self._reader = reader
|
||||||
|
self._writer = writer
|
||||||
|
self._parser = parser
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
|
|
||||||
|
super().__init__(client)
|
||||||
|
|
||||||
|
def release(self) -> None:
|
||||||
|
"""Returns the connection back to the pool."""
|
||||||
|
|
||||||
|
self._client._pool.append(self)
|
||||||
|
|
||||||
|
def destroy(self) -> None:
|
||||||
|
"""Closes the connection and frees it's space in the client's pool."""
|
||||||
|
|
||||||
|
self._client._open_connections -= 1
|
||||||
|
|
||||||
|
self._writer.close()
|
||||||
|
|
||||||
|
async def __aenter__(self) -> "Connection":
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, *args) -> None:
|
||||||
|
self.release()
|
||||||
|
|
||||||
|
async def command(self, *args: list[Resp3Repr]) -> Resp3Repr:
|
||||||
|
"""Executes a command and returns it's response.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
hiredis.ReplyError: Any errors returned by Redis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
args = list(args)
|
||||||
|
|
||||||
|
command = _encode(args)
|
||||||
|
|
||||||
|
async def run():
|
||||||
|
async with self._lock:
|
||||||
|
self._writer.write(command)
|
||||||
|
await self._writer.drain()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
self._parser.feed(await self._reader.readuntil(b"\r\n"))
|
||||||
|
|
||||||
|
result = self._parser.gets()
|
||||||
|
|
||||||
|
if result == _Ellipsis:
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif isinstance(result, hiredis.ReplyError):
|
||||||
|
raise result
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await asyncio.wait_for(run(), self._client._command_timeout)
|
||||||
|
|
||||||
|
except (
|
||||||
|
hiredis.ProtocolError,
|
||||||
|
asyncio.TimeoutError,
|
||||||
|
ConnectionResetError,
|
||||||
|
) as exc:
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
raise FatalRedisError() from exc
|
||||||
|
|
||||||
|
async def hello(self, username: str = None, password: str = None) -> dict:
|
||||||
|
"""Sends the initial HELLO command to the server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username (str, optional): The username to authenticate with. Defaults to None, or
|
||||||
|
"default" if `password` is set.
|
||||||
|
password (str, optional): The password to authenticate with. Defaults to None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: The metadata map returned by the server.
|
||||||
|
"""
|
||||||
|
|
||||||
|
command = ["HELLO", "3"]
|
||||||
|
|
||||||
|
if password is not None:
|
||||||
|
command += ["AUTH", username or "default", password]
|
||||||
|
|
||||||
|
return await self.command(*command)
|
||||||
|
|
||||||
|
|
||||||
|
class Pipeline(FullRunnable):
|
||||||
|
"""A Redis pipeline."""
|
||||||
|
|
||||||
|
_commands: list[tuple[Resp3Repr]]
|
||||||
|
|
||||||
|
def __init__(self, client: "Redis") -> None:
|
||||||
|
self._commands = []
|
||||||
|
|
||||||
|
super().__init__(client)
|
||||||
|
|
||||||
|
def command(self, *args: tuple[Resp3Repr]) -> None:
|
||||||
|
self._commands.append(args)
|
||||||
|
|
||||||
|
async def execute(self):
|
||||||
|
"""Executes the pipeline.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[Resp3Repr]: The results returned by the execution of the commands.
|
||||||
|
"""
|
||||||
|
|
||||||
|
command = b"".join([_encode(comm) for comm in self._commands])
|
||||||
|
|
||||||
|
async with await self._client.connection() as conn:
|
||||||
|
|
||||||
|
async def run():
|
||||||
|
async with conn._lock:
|
||||||
|
conn._writer.write(command)
|
||||||
|
await conn._writer.drain()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
conn._parser.feed(await conn._reader.readuntil(b"\r\n"))
|
||||||
|
|
||||||
|
result = conn._parser.gets()
|
||||||
|
|
||||||
|
if result == _Ellipsis:
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif isinstance(result, hiredis.ReplyError):
|
||||||
|
raise RedisError(
|
||||||
|
"An error was returned from the Redis server."
|
||||||
|
) from result
|
||||||
|
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
if len(results) == len(self._commands):
|
||||||
|
return results
|
||||||
|
|
||||||
|
try:
|
||||||
|
return tuple(
|
||||||
|
await asyncio.wait_for(run(), self._client._command_timeout)
|
||||||
|
)
|
||||||
|
|
||||||
|
except (
|
||||||
|
hiredis.ProtocolError,
|
||||||
|
asyncio.TimeoutError,
|
||||||
|
ConnectionResetError,
|
||||||
|
) as exc:
|
||||||
|
conn.destroy()
|
||||||
|
|
||||||
|
raise FatalRedisError() from exc
|
||||||
|
|
||||||
|
|
||||||
|
class Redis(FullRunnable):
|
||||||
|
_pool: list[Connection] = []
|
||||||
|
_open_connections: int = 0
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
url: str = "",
|
||||||
|
max_connections: int = 100,
|
||||||
|
connection_timeout: int = 3,
|
||||||
|
command_timeout: int = 3,
|
||||||
|
encoding: str = "utf-8",
|
||||||
|
) -> None:
|
||||||
|
"""Initializes the client class.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url (str): The redis URL to connect to. Defaults to "redis://localhost:6379"
|
||||||
|
max_connections (int, optional): The maximum amount of connections that can be opened.
|
||||||
|
Defaults to 100.
|
||||||
|
connection_timeout (int, optional): The amount of seconds to wait for a connection to
|
||||||
|
establish before raising a TimeoutError. Defaults to 3.
|
||||||
|
command_timeout (int, optional): The amount of seconds to wait for a command to execute
|
||||||
|
before raising a TimeoutError. Defaults to 3.
|
||||||
|
encoding (str, optional): The encoding with which to decode Redis response values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._client = self
|
||||||
|
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
|
self._max_connections = max_connections
|
||||||
|
self._connection_timeout = connection_timeout
|
||||||
|
self._command_timeout = command_timeout
|
||||||
|
self._encoding = encoding
|
||||||
|
|
||||||
|
parsed_url = parse.urlparse(url)
|
||||||
|
|
||||||
|
self._host = parsed_url.hostname or "localhost"
|
||||||
|
self._port = parsed_url.port or 6379
|
||||||
|
self._username = parsed_url.username
|
||||||
|
self._password = parsed_url.password
|
||||||
|
|
||||||
|
async def connection(self) -> Connection:
|
||||||
|
"""Returns an available connection or creates a new one if none are available and the pool
|
||||||
|
has space remaining.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConnectionError: If the pool has reached it's max size and no connections are available.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
try:
|
||||||
|
connection = self._pool.pop()
|
||||||
|
return connection
|
||||||
|
|
||||||
|
except IndexError as exc:
|
||||||
|
if self._open_connections >= self._max_connections:
|
||||||
|
raise ConnectionError(
|
||||||
|
"The pool has reached it's limit of connections and no connections are "
|
||||||
|
"available at the moment."
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
reader, writer = await asyncio.wait_for(
|
||||||
|
asyncio.open_connection(host=self._host, port=self._port),
|
||||||
|
self._connection_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
connection = Connection(
|
||||||
|
client=self,
|
||||||
|
reader=reader,
|
||||||
|
writer=writer,
|
||||||
|
parser=hiredis.Reader(
|
||||||
|
notEnoughData=_Ellipsis, encoding=self._encoding
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await connection.hello(username=self._username, password=self._password)
|
||||||
|
|
||||||
|
self._open_connections += 1
|
||||||
|
|
||||||
|
return connection
|
||||||
|
|
||||||
|
async def command(self, *args: tuple[Resp3Repr]) -> Awaitable[Resp3Repr]:
|
||||||
|
"""Executes a command with a free connection from the pool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Awaitable[Resp3Repr]: The command's response.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async with await self.connection() as conn:
|
||||||
|
return await conn.command(*args)
|
||||||
|
|
||||||
|
def pipeline(self) -> Pipeline:
|
||||||
|
"""Creates a new pipeline."""
|
||||||
|
|
||||||
|
return Pipeline(self)
|
||||||
@@ -164,45 +164,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
|
{ url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "charset-normalizer"
|
|
||||||
version = "3.4.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.1.7"
|
version = "8.1.7"
|
||||||
@@ -293,6 +254,29 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
|
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hiredis"
|
||||||
|
version = "3.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8b/80/740fb0dfa7a42416ce8376490f41dcdb1e5deed9c3739dfe4200fad865a9/hiredis-3.0.0.tar.gz", hash = "sha256:fed8581ae26345dea1f1e0d1a96e05041a727a45e7d8d459164583e23c6ac441", size = 87581 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/09/0a3eace00115d8c82a8e7d8e58e60aacec10334f4f1512f09ffbac3252e3/hiredis-3.0.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:484025d2eb8f6348f7876fc5a2ee742f568915039fcb31b478fd5c242bb0fe3a", size = 81540 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/e8/1a7a5ded4fb11e91aafc5ba5518392f22883d54e79c4b47f188fb712ea46/hiredis-3.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:fcdb552ffd97151dab8e7bc3ab556dfa1512556b48a367db94b5c20253a35ee1", size = 44814 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/f5/4e055dc9b55484644afb18063f28649cdbd19be4f15bc152bd633dccd6f7/hiredis-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bb6f9fd92f147ba11d338ef5c68af4fd2908739c09e51f186e1d90958c68cc1", size = 42478 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/7b/e06f55b9dcdf10cb6b3f08d7917d3080096cd83deaef1bd4927720fbb280/hiredis-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa86bf9a0ed339ec9e8a9a9d0ae4dccd8671625c83f9f9f2640729b15e07fbfd", size = 168303 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/16/081e90137bb896acd9dc2e1e68480cc84d652af4d959e75e52d6ce9dd602/hiredis-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e194a0d5df9456995d8f510eab9f529213e7326af6b94770abf8f8b7952ddcaa", size = 179151 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/0f/f5aba1c82977f4b639e5b450c0d8685333f1200cd1972647eb3f4d972e55/hiredis-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a1df39d74ec507d79c7a82c8063eee60bf80537cdeee652f576059b9cdd15c", size = 168580 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/86/aa24c20f6d3038bf244bc60a2fe8cde61fb3c0d6a82e2bed30b08d55f96c/hiredis-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f91456507427ba36fd81b2ca11053a8e112c775325acc74e993201ea912d63e9", size = 169147 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/03/a4c7a28b6320ef3e36062c1c51e9d66e889c9e09ee7d7ae38b8a2ffdb365/hiredis-3.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9862db92ef67a8a02e0d5370f07d380e14577ecb281b79720e0d7a89aedb9ee5", size = 164722 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/66/d60106b56ba0ddd9789656d204a577591ff0cd91ab94178bb96c84d0d918/hiredis-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d10fcd9e0eeab835f492832b2a6edb5940e2f1230155f33006a8dfd3bd2c94e4", size = 162561 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/30/f33f2b782096efe9fe6b24c67a4df13b5055d9c859f615a74fb4f18cce41/hiredis-3.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:48727d7d405d03977d01885f317328dc21d639096308de126c2c4e9950cbd3c9", size = 161388 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/02/34d9b151f9ea4655bfe00e0230f7db8fd8a52c7b7bd728efdf1c17655860/hiredis-3.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e0bb6102ebe2efecf8a3292c6660a0e6fac98176af6de67f020bea1c2343717", size = 173561 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/54/68285d208918b6d83e32d872d8dcbf8d479ed2c74b863b836e48a2702a3f/hiredis-3.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:df274e3abb4df40f4c7274dd3e587dfbb25691826c948bc98d5fead019dfb001", size = 165914 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/4f/5f36865f9f032caf00d603ff9cbde21506d2b1e0e0ce0b5d2ce2851411c9/hiredis-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:034925b5fb514f7b11aac38cd55b3fd7e9d3af23bd6497f3f20aa5b8ba58e232", size = 163968 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/ee/c38693bd1dbce34806ecc3536dc425e87e420030de7018194865511860c2/hiredis-3.0.0-cp312-cp312-win32.whl", hash = "sha256:120f2dda469b28d12ccff7c2230225162e174657b49cf4cd119db525414ae281", size = 20189 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/67/f50b45071bb8652fa9a28a84ee470a02042fb7a096a16f3c08842f2a5c2b/hiredis-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e584fe5f4e6681d8762982be055f1534e0170f6308a7a90f58d737bab12ff6a8", size = 21971 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpcore"
|
name = "httpcore"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -427,20 +411,20 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "payphone"
|
name = "payphone"
|
||||||
version = "0.10.0"
|
version = "0.13.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aerich" },
|
{ name = "aerich" },
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "colorthief" },
|
{ name = "colorthief" },
|
||||||
|
{ name = "hiredis" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "orjson" },
|
{ name = "orjson" },
|
||||||
{ name = "py-cord" },
|
{ name = "py-cord" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
{ name = "tortoise-orm", extra = ["asyncpg"] },
|
{ name = "tortoise-orm", extra = ["asyncpg"] },
|
||||||
{ name = "upstash-redis" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
@@ -454,13 +438,13 @@ requires-dist = [
|
|||||||
{ name = "aiohttp", specifier = ">=3.10.10" },
|
{ name = "aiohttp", specifier = ">=3.10.10" },
|
||||||
{ name = "click", specifier = ">=8.1.7" },
|
{ name = "click", specifier = ">=8.1.7" },
|
||||||
{ name = "colorthief", specifier = ">=0.2.1" },
|
{ name = "colorthief", specifier = ">=0.2.1" },
|
||||||
|
{ name = "hiredis", specifier = ">=3.0.0" },
|
||||||
{ name = "httpx", specifier = ">=0.27.2" },
|
{ name = "httpx", specifier = ">=0.27.2" },
|
||||||
{ name = "orjson", specifier = ">=3.10.7" },
|
{ name = "orjson", specifier = ">=3.10.7" },
|
||||||
{ name = "py-cord", specifier = ">=2.6.1" },
|
{ name = "py-cord", specifier = ">=2.6.1" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.0.1" },
|
{ name = "python-dotenv", specifier = ">=1.0.1" },
|
||||||
{ name = "rich", specifier = ">=13.9.3" },
|
{ name = "rich", specifier = ">=13.9.3" },
|
||||||
{ name = "tortoise-orm", extras = ["asyncpg"], specifier = ">=0.21.7" },
|
{ name = "tortoise-orm", extras = ["asyncpg"], specifier = ">=0.21.7" },
|
||||||
{ name = "upstash-redis", specifier = ">=1.2.0" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
@@ -642,21 +626,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 },
|
{ url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "requests"
|
|
||||||
version = "2.32.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "certifi" },
|
|
||||||
{ name = "charset-normalizer" },
|
|
||||||
{ name = "idna" },
|
|
||||||
{ name = "urllib3" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rich"
|
name = "rich"
|
||||||
version = "13.9.4"
|
version = "13.9.4"
|
||||||
@@ -727,28 +696,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
|
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "upstash-redis"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "aiohttp" },
|
|
||||||
{ name = "requests" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/de/ec/4d137a714a3e1a621fbf39eac88489291bbc43912f9c2f8f4c8258eda5be/upstash_redis-1.2.0.tar.gz", hash = "sha256:8708ef56241832df1ed0dd3c27c19c14ca38dc34df9c5d3fc2c1ba8c2586edc6", size = 32898 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c5/97/2eaef1e7ffcf846733256aba60cee69ffc86daeaf039877eae6d4ee7416c/upstash_redis-1.2.0-py3-none-any.whl", hash = "sha256:a23db26bafb3706e80dbdcbdbb50bb419ca229247cb47eebfff986c59cfcaabf", size = 35351 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "urllib3"
|
|
||||||
version = "2.2.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yarl"
|
name = "yarl"
|
||||||
version = "1.17.1"
|
version = "1.17.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user