feat(bot): 🎉 Create a redis client from scratch.

This commit is contained in:
Nyeki.py
2024-11-04 22:11:22 -03:00
parent eaaea73ab1
commit cd84781640
9 changed files with 937 additions and 199 deletions
-9
View File
@@ -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:
-43
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+3 -3
View File
@@ -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.
+5 -5
View File
@@ -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
View File
@@ -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"
+826
View File
@@ -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)
Generated
+26 -79
View File
@@ -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"