Compare commits

...

39 Commits

Author SHA1 Message Date
b8b13616a8 fix broken api 2025-08-27 04:17:30 +03:00
8f5772b5dc fix broken api 2025-08-27 04:16:36 +03:00
5d11a1a3de fix broken api 2025-08-27 04:14:15 +03:00
abb4570958 update for new channel-helper 2025-08-09 23:06:18 +03:00
1b88ad2ac8 update for new channel-helper 2025-08-09 23:03:48 +03:00
f1684edef8 update for new channel-helper 2025-08-09 22:17:02 +03:00
3e275f54c4 update for new channel-helper 2025-08-09 22:08:58 +03:00
a9518259fb handle empty tags 2025-08-09 18:07:27 +03:00
2c2088d993 fix get subscriptions endpoint 2025-08-01 10:03:28 +03:00
69d0950037 fix get subscriptions endpoint 2025-08-01 09:59:57 +03:00
baea50eb73 init sanic-redis 2025-08-01 09:57:46 +03:00
a9c022b992 allow authorization header in cors 2025-08-01 09:51:25 +03:00
40644bcb4b add error http statuses 2025-08-01 09:44:23 +03:00
3723280e3d fix invalid auth response 2025-08-01 09:42:53 +03:00
cf9aad9aee fix invalid auth response 2025-08-01 09:34:54 +03:00
94184f8635 add cors to server 2025-08-01 05:09:25 +03:00
b52afa59cd fix resend after 2025-07-31 12:39:08 +03:00
15d4853402 fix resend after 2025-07-31 12:37:25 +03:00
ca194a8a2c fix resend after 2025-07-31 12:31:50 +03:00
687dabc354 fix resend after 2025-07-31 12:30:52 +03:00
af1479bc75 fix resend after 2025-07-31 12:29:41 +03:00
7bdebae54a fix resend after 2025-07-31 12:28:56 +03:00
c88e5e7ab1 fix resend after 2025-07-31 12:26:37 +03:00
973f432b53 fix server 2025-07-31 03:14:05 +03:00
1b3af70082 fix server auth 2025-07-31 03:05:05 +03:00
9b9197be0e fix handle send method returning image hash 2025-07-26 13:55:14 +03:00
fed68c0dfb update rpc response format 2025-07-26 02:45:36 +03:00
60c6448f72 handle send method returning image hash 2025-07-25 17:53:22 +03:00
01ba70c0ce fix tag alias filter 2025-07-15 03:34:57 +03:00
4c3b80fc5a do not update alias check progress on each iteration 2025-07-15 01:52:07 +03:00
2b6e9bd2df do not update alias check progress on each iteration 2025-07-14 17:39:16 +03:00
fab1798f7e validate tag alias status 2025-07-14 17:36:03 +03:00
92599a335f validate tag alias status 2025-07-14 17:35:49 +03:00
5c35f8cda5 fix tag aliases request 2025-07-14 17:31:16 +03:00
d1c35ace8d fix tag aliases request 2025-07-14 17:29:15 +03:00
c6d323f3f9 fix 2025-07-14 17:23:54 +03:00
4ea3039c01 ignore python-version file 2025-07-14 17:21:22 +03:00
b41f303d3c add check_aliases command 2025-07-14 17:20:17 +03:00
a289d24698 add api 2025-07-14 17:12:42 +03:00
8 changed files with 363 additions and 43 deletions

View File

@@ -1 +0,0 @@
3.12

4
const.py Normal file
View File

@@ -0,0 +1,4 @@
REDIS_SUBS_KEY = 'e621:subs'
REDIS_LAST_VERSION_KEY = 'e621:last_version'
REDIS_SENT_KEY = 'e621:sent'
REDIS_LOCK_KEY = 'e621:update'

View File

@@ -177,4 +177,11 @@ class E621:
})).json() })).json()
if 'success' in r: if 'success' in r:
return [] return []
return [E621PostVersion.from_dict(p) for p in r] return [E621PostVersion.from_dict(p) for p in r if p.get('tags') is not None]
async def get_tag_aliases(self, name: str) -> List[str]:
data = (await self.client.get('/tag_aliases.json', params={'search[antecedent_name]': name})).json()
logging.warning(f'{name}: {data}')
if 'tag_aliases' in data:
return []
return [alias['consequent_name'] for alias in data if alias['status'] == 'active']

View File

@@ -4,3 +4,4 @@ USERS=9893249151
AWS_ACCESS_KEY=AKIAUIXZQT AWS_ACCESS_KEY=AKIAUIXZQT
AWS_SECRET_KEY=QyBnXOhmlc AWS_SECRET_KEY=QyBnXOhmlc
AWS_S3_BUCKET=bucket AWS_S3_BUCKET=bucket
UPLOAD_KEY=123

116
main.py
View File

@@ -3,7 +3,6 @@ import base64
import datetime import datetime
import logging import logging
import os import os
import random
import re import re
import traceback import traceback
from asyncio import sleep from asyncio import sleep
@@ -20,12 +19,13 @@ from PIL import Image
import httpx import httpx
import redis.asyncio as aioredis import redis.asyncio as aioredis
from aiogram import Bot, Dispatcher, filters, exceptions, F from aiogram import Bot, Dispatcher, filters, F
from aiogram.enums import ChatAction, ParseMode from aiogram.enums import ChatAction, ParseMode
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, BufferedInputFile, \ from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, BufferedInputFile, \
CallbackQuery CallbackQuery
import dotenv import dotenv
from const import REDIS_SUBS_KEY, REDIS_LAST_VERSION_KEY, REDIS_SENT_KEY, REDIS_LOCK_KEY
from e621 import E621, E621Post, E621PostFile, E621PostVersion from e621 import E621, E621Post, E621PostFile, E621PostVersion
dotenv.load_dotenv('.env') dotenv.load_dotenv('.env')
@@ -36,6 +36,7 @@ e621 = E621()
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
bot = Bot(token=os.environ['BOT_TOKEN']) bot = Bot(token=os.environ['BOT_TOKEN'])
dp = Dispatcher() dp = Dispatcher()
upload_key = os.environ['UPLOAD_KEY']
ChatFilter = F.chat.id.in_(set(map(int, os.environ['USERS'].split(',')))) ChatFilter = F.chat.id.in_(set(map(int, os.environ['USERS'].split(','))))
@@ -127,7 +128,7 @@ async def send_post(post: E621Post, tag_list: Iterable[Iterable[str]]):
caption=caption, caption=caption,
parse_mode=ParseMode.HTML, parse_mode=ParseMode.HTML,
reply_markup=markup) reply_markup=markup)
await redis.sadd('e621:sent', post.id) await redis.sadd(REDIS_SENT_KEY, post.id)
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
except Exception as e: except Exception as e:
@@ -136,12 +137,12 @@ async def send_post(post: E621Post, tag_list: Iterable[Iterable[str]]):
async def check_updates(): async def check_updates():
logging.warning('Waiting for lock...') logging.warning('Waiting for lock...')
async with redis.lock('e621:update'): async with redis.lock(REDIS_LOCK_KEY):
logging.warning('Lock acquired...') logging.warning('Lock acquired...')
matched_posts = [] matched_posts = []
tag_list = set(tuple(t.decode().split()) for t in await redis.smembers('e621:subs')) tag_list = set(tuple(t.decode().split()) for t in await redis.smembers(REDIS_SUBS_KEY))
tag_list_flat = set(sum(tag_list, ())) tag_list_flat = set(sum(tag_list, ()))
last_post_version = int((await redis.get('e621:last_version') or b'0').decode()) last_post_version = int((await redis.get(REDIS_LAST_VERSION_KEY) or b'0').decode())
post_versions: List[E621PostVersion] = [] post_versions: List[E621PostVersion] = []
logging.warning(f'Getting post versions from id {last_post_version}') logging.warning(f'Getting post versions from id {last_post_version}')
for page in count(1): for page in count(1):
@@ -167,7 +168,7 @@ async def check_updates():
break break
matched_posts.sort() matched_posts.sort()
if matched_posts: if matched_posts:
already_sent: List = await redis.smismember('e621:sent', matched_posts) already_sent: List = await redis.smismember(REDIS_SENT_KEY, matched_posts)
posts_to_send = [post_id for post_id, sent in zip(matched_posts, already_sent) if not sent] posts_to_send = [post_id for post_id, sent in zip(matched_posts, already_sent) if not sent]
logging.warning(f'Found {len(posts_to_send)} posts') logging.warning(f'Found {len(posts_to_send)} posts')
for post_chunk_idx in range(0, len(posts_to_send), PAGE_SIZE): for post_chunk_idx in range(0, len(posts_to_send), PAGE_SIZE):
@@ -176,34 +177,45 @@ async def check_updates():
for i, post in enumerate(posts): for i, post in enumerate(posts):
logging.warning(f'Sending post {post_chunk_idx + i + 1}/{len(posts_to_send)}') logging.warning(f'Sending post {post_chunk_idx + i + 1}/{len(posts_to_send)}')
await send_post(post, tag_list) await send_post(post, tag_list)
await redis.sadd('e621:sent', post.id) await redis.sadd(REDIS_SENT_KEY, post.id)
await sleep(1) await sleep(1)
await redis.set('e621:last_version', last_post_version) await redis.set(REDIS_LAST_VERSION_KEY, last_post_version)
@dp.message(filters.Command('resend_after'), ChatFilter) @dp.message(filters.Command('resend_after'), ChatFilter)
async def resend_after(msg: Message): async def resend_after(msg: Message):
args = msg.text.split()[1:]
try: try:
timestamp = int(msg.text.split()[1]) timestamp = int(args[0])
skip_to_sub = args[1] if len(args) > 1 else None
except: except:
traceback.print_exc() traceback.print_exc()
await msg.reply('Invalid timestamp or not provided') await msg.reply('Invalid timestamp or not provided')
return return
async with redis.lock('e621:update'): async with redis.lock(REDIS_LOCK_KEY):
tag_list = [tuple(t.decode().split()) for t in await redis.smembers('e621:subs')] tags = sorted(await redis.smembers(REDIS_SUBS_KEY))
if skip_to_sub is not None and skip_to_sub in tags:
tags = tags[tags.index(skip_to_sub):]
tag_list = [tuple(t.decode().split()) for t in tags]
for i, tag in enumerate(tag_list): for i, tag in enumerate(tag_list):
await msg.reply(f'Checking tag <b>{tag}</b> ({i+1}/{len(tag_list)})', parse_mode=ParseMode.HTML)
posts = [] posts = []
page = 1 page = 1
while True: while True:
page_posts = await e621.get_posts(tag, page) break_loop = False
if page > 10:
break
page_posts = await e621.get_posts(' '.join(tag), page)
if not page_posts: if not page_posts:
break break
for post in page_posts: for post in page_posts:
if datetime.datetime.fromisoformat(post.created_at).timestamp() < timestamp: post_created_at = datetime.datetime.fromisoformat(post.created_at)
if post_created_at.timestamp() < timestamp:
break_loop = True
break break
posts.append(post) posts.append(post)
if break_loop:
break
page += 1 page += 1
for post in posts[::-1]: for post in posts[::-1]:
await send_post(post, tag_list) await send_post(post, tag_list)
@@ -218,8 +230,11 @@ async def add_tag(msg: Message):
return return
for tag in args.split(): for tag in args.split():
posts = await e621.get_posts(tag) posts = await e621.get_posts(tag)
await redis.sadd('e621:sent', *[post.id for post in posts]) if posts:
await redis.sadd('e621:subs', tag) await redis.sadd(REDIS_SENT_KEY, *[post.id for post in posts])
else:
logging.warning(f'No posts found for tag {tag}')
await redis.sadd(REDIS_SUBS_KEY, tag)
await msg.reply(f'Tags {args} added') await msg.reply(f'Tags {args} added')
@@ -233,20 +248,20 @@ async def add_tags(msg: Message):
tags.sort() tags.sort()
tags = ' '.join(tags) tags = ' '.join(tags)
posts = await e621.get_posts(tags) posts = await e621.get_posts(tags)
await redis.sadd('e621:sent', *[post.id for post in posts]) await redis.sadd(REDIS_SENT_KEY, *[post.id for post in posts])
await redis.sadd('e621:subs', tags) await redis.sadd(REDIS_SUBS_KEY, tags)
await msg.reply(f'Tag group <code>{tags}</code> added', parse_mode=ParseMode.HTML) await msg.reply(f'Tag group <code>{tags}</code> added', parse_mode=ParseMode.HTML)
@dp.message(filters.Command('mark_old_as_sent'), ChatFilter) @dp.message(filters.Command('mark_old_as_sent'), ChatFilter)
async def mark_old_as_sent(msg: Message): async def mark_old_as_sent(msg: Message):
logging.warning('Waiting for lock...') logging.warning('Waiting for lock...')
async with redis.lock('e621:update'): async with redis.lock(REDIS_LOCK_KEY):
tag_list = [t.decode() for t in await redis.smembers('e621:subs')] tag_list = [t.decode() for t in await redis.smembers(REDIS_SUBS_KEY)]
m = await msg.reply(f'0/{len(tag_list)} tags have old posts marked as sent') m = await msg.reply(f'0/{len(tag_list)} tags have old posts marked as sent')
for i, tag in enumerate(tag_list, 1): for i, tag in enumerate(tag_list, 1):
posts = await e621.get_posts(tag) posts = await e621.get_posts(tag)
await redis.sadd('e621:sent', *[post.id for post in posts]) await redis.sadd(REDIS_SENT_KEY, *[post.id for post in posts])
await m.edit_text(f'{i}/{len(tag_list)} tags have old posts marked as sent') await m.edit_text(f'{i}/{len(tag_list)} tags have old posts marked as sent')
await sleep(1) await sleep(1)
await m.edit_text(f'Done marking old posts as sent for {len(tag_list)} tags') await m.edit_text(f'Done marking old posts as sent for {len(tag_list)} tags')
@@ -261,10 +276,10 @@ async def del_tag(msg: Message):
if ' ' in args: if ' ' in args:
await msg.reply('Tag should not contain spaces') await msg.reply('Tag should not contain spaces')
return return
if not await redis.sismember('e621:subs', args): if not await redis.sismember(REDIS_SUBS_KEY, args):
await msg.reply('Tag not found') await msg.reply('Tag not found')
return return
await redis.srem('e621:subs', args) await redis.srem(REDIS_SUBS_KEY, args)
await msg.reply(f'Tag {args} removed') await msg.reply(f'Tag {args} removed')
@@ -275,13 +290,13 @@ async def del_command(msg: Message):
await msg.reply('Please provide tag to subscribe to') await msg.reply('Please provide tag to subscribe to')
return return
for tag in args.split(): for tag in args.split():
await redis.srem('e621:subs', tag) await redis.srem(REDIS_SUBS_KEY, tag)
await msg.reply(f'Tags {args} removed') await msg.reply(f'Tags {args} removed')
@dp.message(filters.Command('list'), ChatFilter) @dp.message(filters.Command('list'), ChatFilter)
async def list_tags(msg: Message): async def list_tags(msg: Message):
tags = [t.decode() for t in await redis.smembers('e621:subs')] tags = [t.decode() for t in await redis.smembers(REDIS_SUBS_KEY)]
tags.sort() tags.sort()
lines = [] lines = []
for tag in tags: for tag in tags:
@@ -297,6 +312,32 @@ async def list_tags(msg: Message):
await msg.reply(f'Monitored tags:\n\n{lines}') await msg.reply(f'Monitored tags:\n\n{lines}')
@dp.message(filters.Command('check_aliases'), ChatFilter)
async def check_aliases(msg: Message):
tags = [t.decode() for t in await redis.smembers(REDIS_SUBS_KEY)]
tags.sort()
progress = 0
lines = []
resp = await msg.reply('tmp')
async def send_progress():
l = "\n".join(lines)
await resp.edit_text(f'Checking aliases {progress}/{len(tags)}\n\n{l}', parse_mode=ParseMode.HTML)
for sub in tags:
replaced_tags = False
for subtag in sub.split():
if replacements := await e621.get_tag_aliases(subtag):
lines.append(f'- {subtag} -> {replacements[0]}, (<code>{sub}</code>)')
replaced_tags = False
progress += 1
if replaced_tags:
await send_progress()
await send_progress()
@dp.message(filters.Command('update'), ChatFilter) @dp.message(filters.Command('update'), ChatFilter)
async def update(msg: Message): async def update(msg: Message):
await check_updates() await check_updates()
@@ -313,7 +354,7 @@ async def test(msg: Message):
if not post: if not post:
await msg.reply('Post not found') await msg.reply('Post not found')
return return
tag_list = [tuple(t.decode().split()) for t in await redis.smembers('e621:subs')] tag_list = [tuple(t.decode().split()) for t in await redis.smembers(REDIS_SUBS_KEY)]
await send_post(post[0], tag_list) await send_post(post[0], tag_list)
@@ -323,20 +364,19 @@ async def send_callback(cq: CallbackQuery):
img_bytes = BytesIO() img_bytes = BytesIO()
await bot.download(cq.message.photo[-1], img_bytes) await bot.download(cq.message.photo[-1], img_bytes)
img_bytes.seek(0) img_bytes.seek(0)
data = base64.b64encode(img_bytes.read()).decode()
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:
r = await client.post(f'https://bots.bakatrouble.me/bots_rpc/{destination}/', json={ subdomain = 'ch' + ('sfw' if destination == 'pics' else 'nsfw')
"method": "post_photo", r = await client.post(f'https://{subdomain}.bakatrouble.me/{upload_key}/photo', files={'upload': img_bytes})
"params": [data, True], logging.info(r.text)
"jsonrpc": "2.0",
"id": 0,
})
resp = r.json() resp = r.json()
if 'result' in resp and resp['result'] == True: status = resp.get('status')
await cq.answer('Sent') if not status:
elif 'result' in resp and resp['result'] == 'duplicate': raise Exception(f'No result in response: {resp}')
elif status == 'duplicate':
await cq.answer('Duplicate') await cq.answer('Duplicate')
elif status == 'ok':
await cq.answer('Sent')
else: else:
raise Exception(resp) raise Exception(resp)
except: except:
@@ -345,7 +385,7 @@ async def send_callback(cq: CallbackQuery):
async def background_on_start(): async def background_on_start():
await redis.delete('e621:update') await redis.delete(REDIS_LOCK_KEY)
while True: while True:
logging.warning('Checking updates...') logging.warning('Checking updates...')
try: try:

View File

@@ -13,5 +13,8 @@ dependencies = [
"pillow>=11.2.1", "pillow>=11.2.1",
"python-dotenv>=1.1.1", "python-dotenv>=1.1.1",
"redis>=6.2.0", "redis>=6.2.0",
"sanic>=25.3.0", "sanic[ext]>=25.3.0",
"sanic-redis>=0.6.0",
"passlib>=1.7.4",
"pyjwt>=2.10.1",
] ]

147
server.py Normal file
View File

@@ -0,0 +1,147 @@
import json
import os
from dataclasses import dataclass
from functools import wraps
import dotenv
import jwt
from sanic import Sanic, Unauthorized, json as jsonr
from sanic_ext import validate, Extend
from sanic_ext.extensions.openapi import openapi
from sanic_ext.extensions.openapi.definitions import RequestBody
from sanic_redis import SanicRedis
from passlib.hash import pbkdf2_sha256
from const import REDIS_SUBS_KEY
dotenv.load_dotenv('.env')
api_secret = os.environ['API_SECRET']
api_auth = json.loads(os.environ['API_AUTH'])
app = Sanic('e621_bot_api')
app.config.CORS_ORIGINS = '*'
app.config.CORS_HEADERS = 'Authorization, *'
app.config.update({
'REDIS': 'redis://localhost',
})
Extend(app)
redis = SanicRedis()
redis.init_app(app)
async def get_subs(r):
subs = await r.smembers(REDIS_SUBS_KEY)
return {s.decode() for s in subs}
@dataclass
class LoginRequest:
username: str
password: str
def protected(wrapped):
def decorator(f):
@wraps(f)
async def decorated(request, *args, **kwargs):
token = request.headers.get('Authorization')
if not token:
raise Unauthorized('Authorization header is missing')
try:
jwt.decode(token, api_secret, algorithms=['HS256'])
except jwt.ExpiredSignatureError:
raise Unauthorized('Token has expired')
except jwt.InvalidTokenError:
raise Unauthorized('Invalid token')
return await f(request, *args, **kwargs)
return decorated
return decorator(wrapped)
@app.post('/api/login')
@openapi.definition(
body=RequestBody({
'application/json': LoginRequest,
})
)
@validate(json=LoginRequest)
async def login(_, body: LoginRequest):
hash = api_auth.get(body.username)
if not hash or not pbkdf2_sha256(10000, salt=b'salt').verify(body.password, hash):
return jsonr({'status': 'error', 'message': 'Invalid username or password'}, 401)
return jsonr({
'token': jwt.encode({}, api_secret, algorithm='HS256'),
})
@app.get('/api/subscriptions')
@protected
async def get_subscriptions(_):
async with redis.conn as r:
return jsonr({
'subscriptions': sorted(list(await get_subs(r))),
})
@dataclass
class UpdateSubscriptionRequest:
subs: list[str]
@app.delete('/api/subscriptions')
@openapi.definition(
body=RequestBody({
'application/json': UpdateSubscriptionRequest,
})
)
@validate(json=UpdateSubscriptionRequest)
@protected
async def delete_subscriptions(_, body: UpdateSubscriptionRequest):
requested_subs = {' '.join(sorted(sub.lower().split())) for sub in body.subs}
async with redis.conn as r:
subs = await get_subs(r)
skipped = requested_subs - subs
if skipped:
return jsonr({'status': 'error', 'message': 'Some subscriptions were not found', 'skipped': sorted(skipped)}, 404)
await r.srem(REDIS_SUBS_KEY, *requested_subs)
return jsonr({'status': 'ok', 'removed': sorted(requested_subs)})
@app.post('/api/subscriptions')
@openapi.definition(
body=RequestBody({
'application/json': UpdateSubscriptionRequest,
})
)
@validate(json=UpdateSubscriptionRequest)
@protected
async def add_subscriptions(_, body: UpdateSubscriptionRequest):
requested_subs = {' '.join(sorted(sub.lower().split())) for sub in body.subs}
async with redis.conn as r:
subs = await get_subs(r)
conflicts = requested_subs & subs
if conflicts:
return jsonr({'status': 'error', 'message': 'Some subscriptions already exist', 'conflicts': sorted(conflicts)}, 409)
await r.sadd(REDIS_SUBS_KEY, *body.subs)
return jsonr({'status': 'ok', 'added': sorted(requested_subs)})
if __name__ == '__main__':
is_debug = os.path.exists('.debug')
app.run(
host=os.environ.get('API_HOST', '0.0.0.0'),
port=int(os.environ.get('API_PORT', 8000)),
debug=is_debug,
access_log=is_debug,
auto_reload=is_debug,
)

123
uv.lock generated
View File

@@ -190,10 +190,13 @@ dependencies = [
{ name = "dataclasses-json" }, { name = "dataclasses-json" },
{ name = "ffmpeg-python" }, { name = "ffmpeg-python" },
{ name = "httpx" }, { name = "httpx" },
{ name = "passlib" },
{ name = "pillow" }, { name = "pillow" },
{ name = "pyjwt" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "redis" }, { name = "redis" },
{ name = "sanic" }, { name = "sanic", extra = ["ext"] },
{ name = "sanic-redis" },
] ]
[package.metadata] [package.metadata]
@@ -203,10 +206,13 @@ requires-dist = [
{ name = "dataclasses-json", specifier = ">=0.6.7" }, { name = "dataclasses-json", specifier = ">=0.6.7" },
{ name = "ffmpeg-python", specifier = ">=0.2.0" }, { name = "ffmpeg-python", specifier = ">=0.2.0" },
{ name = "httpx", specifier = ">=0.28.1" }, { name = "httpx", specifier = ">=0.28.1" },
{ name = "passlib", specifier = ">=1.7.4" },
{ name = "pillow", specifier = ">=11.2.1" }, { name = "pillow", specifier = ">=11.2.1" },
{ name = "pyjwt", specifier = ">=2.10.1" },
{ name = "python-dotenv", specifier = ">=1.1.1" }, { name = "python-dotenv", specifier = ">=1.1.1" },
{ name = "redis", specifier = ">=6.2.0" }, { name = "redis", specifier = ">=6.2.0" },
{ name = "sanic", specifier = ">=25.3.0" }, { name = "sanic", extras = ["ext"], specifier = ">=25.3.0" },
{ name = "sanic-redis", specifier = ">=0.6.0" },
] ]
[[package]] [[package]]
@@ -299,6 +305,44 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
] ]
[[package]]
name = "hiredis"
version = "3.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f7/08/24b72f425b75e1de7442fb1740f69ca66d5820b9f9c0e2511ff9aadab3b7/hiredis-3.2.1.tar.gz", hash = "sha256:5a5f64479bf04dd829fe7029fad0ea043eac4023abc6e946668cbbec3493a78d", size = 89096, upload-time = "2025-05-23T11:41:57.227Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/a1/6da1578a22df1926497f7a3f6a3d2408fe1d1559f762c1640af5762a8eb6/hiredis-3.2.1-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:3742d8b17e73c198cabeab11da35f2e2a81999d406f52c6275234592256bf8e8", size = 82627, upload-time = "2025-05-23T11:40:15.362Z" },
{ url = "https://files.pythonhosted.org/packages/6c/b1/1056558ca8dc330be5bb25162fe5f268fee71571c9a535153df9f871a073/hiredis-3.2.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9c2f3176fb617a79f6cccf22cb7d2715e590acb534af6a82b41f8196ad59375d", size = 45404, upload-time = "2025-05-23T11:40:16.72Z" },
{ url = "https://files.pythonhosted.org/packages/58/4f/13d1fa1a6b02a99e9fed8f546396f2d598c3613c98e6c399a3284fa65361/hiredis-3.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a8bd46189c7fa46174e02670dc44dfecb60f5bd4b67ed88cb050d8f1fd842f09", size = 43299, upload-time = "2025-05-23T11:40:17.697Z" },
{ url = "https://files.pythonhosted.org/packages/c0/25/ddfac123ba5a32eb1f0b40ba1b2ec98a599287f7439def8856c3c7e5dd0d/hiredis-3.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f86ee4488c8575b58139cdfdddeae17f91e9a893ffee20260822add443592e2f", size = 172194, upload-time = "2025-05-23T11:40:19.143Z" },
{ url = "https://files.pythonhosted.org/packages/2c/1e/443a3703ce570b631ca43494094fbaeb051578a0ebe4bfcefde351e1ba25/hiredis-3.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3717832f4a557b2fe7060b9d4a7900e5de287a15595e398c3f04df69019ca69d", size = 168429, upload-time = "2025-05-23T11:40:20.329Z" },
{ url = "https://files.pythonhosted.org/packages/3b/d6/0d8c6c706ed79b2298c001b5458c055615e3166533dcee3900e821a18a3e/hiredis-3.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5cb12c21fb9e2403d28c4e6a38120164973342d34d08120f2d7009b66785644", size = 182967, upload-time = "2025-05-23T11:40:21.921Z" },
{ url = "https://files.pythonhosted.org/packages/da/68/da8dd231fbce858b5a20ab7d7bf558912cd125f08bac4c778865ef5fe2c2/hiredis-3.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:080fda1510bbd389af91f919c11a4f2aa4d92f0684afa4709236faa084a42cac", size = 172495, upload-time = "2025-05-23T11:40:23.105Z" },
{ url = "https://files.pythonhosted.org/packages/65/25/83a31420535e2778662caa95533d5c997011fa6a88331f0cdb22afea9ec3/hiredis-3.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1252e10a1f3273d1c6bf2021e461652c2e11b05b83e0915d6eb540ec7539afe2", size = 173142, upload-time = "2025-05-23T11:40:24.24Z" },
{ url = "https://files.pythonhosted.org/packages/41/d7/cb907348889eb75e2aa2e6b63e065b611459e0f21fe1e371a968e13f0d55/hiredis-3.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d9e320e99ab7d2a30dc91ff6f745ba38d39b23f43d345cdee9881329d7b511d6", size = 166433, upload-time = "2025-05-23T11:40:25.287Z" },
{ url = "https://files.pythonhosted.org/packages/01/5d/7cbc69d82af7b29a95723d50f5261555ba3d024bfbdc414bdc3d23c0defb/hiredis-3.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:641668f385f16550fdd6fdc109b0af6988b94ba2acc06770a5e06a16e88f320c", size = 164883, upload-time = "2025-05-23T11:40:26.454Z" },
{ url = "https://files.pythonhosted.org/packages/f9/00/f995b1296b1d7e0247651347aa230f3225a9800e504fdf553cf7cd001cf7/hiredis-3.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e1f44208c39d6c345ff451f82f21e9eeda6fe9af4ac65972cc3eeb58d41f7cb", size = 177262, upload-time = "2025-05-23T11:40:27.576Z" },
{ url = "https://files.pythonhosted.org/packages/c5/f3/723a67d729e94764ce9e0d73fa5f72a0f87d3ce3c98c9a0b27cbf001cc79/hiredis-3.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f882a0d6415fffe1ffcb09e6281d0ba8b1ece470e866612bbb24425bf76cf397", size = 169619, upload-time = "2025-05-23T11:40:29.671Z" },
{ url = "https://files.pythonhosted.org/packages/45/58/f69028df00fb1b223e221403f3be2059ae86031e7885f955d26236bdfc17/hiredis-3.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4e78719a0730ebffe335528531d154bc8867a246418f74ecd88adbc4d938c49", size = 167303, upload-time = "2025-05-23T11:40:30.902Z" },
{ url = "https://files.pythonhosted.org/packages/2b/7d/567411e65cce76cf265a9a4f837fd2ebc564bef6368dd42ac03f7a517c0a/hiredis-3.2.1-cp312-cp312-win32.whl", hash = "sha256:33c4604d9f79a13b84da79950a8255433fca7edaf292bbd3364fd620864ed7b2", size = 20551, upload-time = "2025-05-23T11:40:32.69Z" },
{ url = "https://files.pythonhosted.org/packages/90/74/b4c291eb4a4a874b3690ff9fc311a65d5292072556421b11b1d786e3e1d0/hiredis-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7b9749375bf9d171aab8813694f379f2cff0330d7424000f5e92890ad4932dc9", size = 22128, upload-time = "2025-05-23T11:40:33.686Z" },
{ url = "https://files.pythonhosted.org/packages/47/91/c07e737288e891c974277b9fa090f0a43c72ab6ccb5182117588f1c01269/hiredis-3.2.1-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:7cabf7f1f06be221e1cbed1f34f00891a7bdfad05b23e4d315007dd42148f3d4", size = 82636, upload-time = "2025-05-23T11:40:35.035Z" },
{ url = "https://files.pythonhosted.org/packages/92/20/02cb1820360eda419bc17eb835eca976079e2b3e48aecc5de0666b79a54c/hiredis-3.2.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:db85cb86f8114c314d0ec6d8de25b060a2590b4713135240d568da4f7dea97ac", size = 45404, upload-time = "2025-05-23T11:40:36.113Z" },
{ url = "https://files.pythonhosted.org/packages/87/51/d30a4aadab8670ed9d40df4982bc06c891ee1da5cdd88d16a74e1ecbd520/hiredis-3.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c9a592a49b7b8497e4e62c3ff40700d0c7f1a42d145b71e3e23c385df573c964", size = 43301, upload-time = "2025-05-23T11:40:37.557Z" },
{ url = "https://files.pythonhosted.org/packages/f7/7b/2c613e1bb5c2e2bac36e8befeefdd58b42816befb17e26ab600adfe337fb/hiredis-3.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0079ef1e03930b364556b78548e67236ab3def4e07e674f6adfc52944aa972dd", size = 172486, upload-time = "2025-05-23T11:40:38.659Z" },
{ url = "https://files.pythonhosted.org/packages/1e/df/8f2c4fcc28d6f5178b25ee1ba2157cc473f9908c16ce4b8e0bdd79e38b05/hiredis-3.2.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d6a290ed45d9c14f4c50b6bda07afb60f270c69b5cb626fd23a4c2fde9e3da1", size = 168532, upload-time = "2025-05-23T11:40:39.843Z" },
{ url = "https://files.pythonhosted.org/packages/88/ae/d0864ffaa0461e29a6940a11c858daf78c99476c06ed531b41ad2255ec25/hiredis-3.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79dd5fe8c0892769f82949adeb021342ca46871af26e26945eb55d044fcdf0d0", size = 183216, upload-time = "2025-05-23T11:40:41.005Z" },
{ url = "https://files.pythonhosted.org/packages/75/17/558e831b77692d73f5bcf8b493ab3eace9f11b0aa08839cdbb87995152c7/hiredis-3.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:998a82281a159f4aebbfd4fb45cfe24eb111145206df2951d95bc75327983b58", size = 172689, upload-time = "2025-05-23T11:40:42.153Z" },
{ url = "https://files.pythonhosted.org/packages/35/b9/4fccda21f930f08c5072ad51e825d85d457748138443d7b510afe77b8264/hiredis-3.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41fc3cd52368ffe7c8e489fb83af5e99f86008ed7f9d9ba33b35fec54f215c0a", size = 173319, upload-time = "2025-05-23T11:40:43.328Z" },
{ url = "https://files.pythonhosted.org/packages/3d/8b/596d613588b0a3c58dfcf9a17edc6a886c4de6a3096e27c7142a94e2304d/hiredis-3.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8d10df3575ce09b0fa54b8582f57039dcbdafde5de698923a33f601d2e2a246c", size = 166695, upload-time = "2025-05-23T11:40:44.453Z" },
{ url = "https://files.pythonhosted.org/packages/e7/5b/6a1c266e9f6627a8be1fa0d8622e35e35c76ae40cce6d1c78a7e6021184a/hiredis-3.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1ab010d04be33735ad8e643a40af0d68a21d70a57b1d0bff9b6a66b28cca9dbf", size = 165181, upload-time = "2025-05-23T11:40:45.697Z" },
{ url = "https://files.pythonhosted.org/packages/6c/70/a9b91fa70d21763d9dfd1c27ddd378f130749a0ae4a0645552f754b3d1fc/hiredis-3.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ec3b5f9ea34f70aaba3e061cbe1fa3556fea401d41f5af321b13e326792f3017", size = 177589, upload-time = "2025-05-23T11:40:46.903Z" },
{ url = "https://files.pythonhosted.org/packages/1a/c7/31bbb015156dc4441f6e19daa9598266a61445bf3f6e14c44292764638f6/hiredis-3.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:158dfb505fff6bffd17f823a56effc0c2a7a8bc4fb659d79a52782f22eefc697", size = 169883, upload-time = "2025-05-23T11:40:48.111Z" },
{ url = "https://files.pythonhosted.org/packages/89/44/cddc23379e0ce20ad7514b2adb2aa2c9b470ffb1ca0a2d8c020748962a22/hiredis-3.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d632cd0ddd7895081be76748e6fb9286f81d2a51c371b516541c6324f2fdac9", size = 167585, upload-time = "2025-05-23T11:40:49.208Z" },
{ url = "https://files.pythonhosted.org/packages/48/92/8fc9b981ed01fc2bbac463a203455cd493482b749801bb555ebac72923f1/hiredis-3.2.1-cp313-cp313-win32.whl", hash = "sha256:e9726d03e7df068bf755f6d1ecc61f7fc35c6b20363c7b1b96f39a14083df940", size = 20554, upload-time = "2025-05-23T11:40:50.314Z" },
{ url = "https://files.pythonhosted.org/packages/e1/6e/e76341d68aa717a705a2ee3be6da9f4122a0d1e3f3ad93a7104ed7a81bea/hiredis-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:b5b1653ad7263a001f2e907e81a957d6087625f9700fa404f1a2268c0a4f9059", size = 22136, upload-time = "2025-05-23T11:40:51.497Z" },
]
[[package]] [[package]]
name = "html5tagger" name = "html5tagger"
version = "1.3.0" version = "1.3.0"
@@ -472,6 +516,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
] ]
[[package]]
name = "passlib"
version = "1.7.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" },
]
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "11.2.1" version = "11.2.1"
@@ -627,6 +680,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
] ]
[[package]]
name = "pyjwt"
version = "2.10.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"
@@ -648,6 +710,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
] ]
[[package]]
name = "pyyaml"
version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
]
[[package]] [[package]]
name = "redis" name = "redis"
version = "6.2.0" version = "6.2.0"
@@ -691,6 +779,37 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/e1/b36ddc16862d63d22986ae21b04a79c8fb7ec48d5d664acdfd1c2acf78ac/sanic-25.3.0-py3-none-any.whl", hash = "sha256:fb519b38b4c220569b0e2e868583ffeaffaab96a78b2e42ae78bc56a644a4cd7", size = 246416, upload-time = "2025-03-31T21:22:27.946Z" }, { url = "https://files.pythonhosted.org/packages/a6/e1/b36ddc16862d63d22986ae21b04a79c8fb7ec48d5d664acdfd1c2acf78ac/sanic-25.3.0-py3-none-any.whl", hash = "sha256:fb519b38b4c220569b0e2e868583ffeaffaab96a78b2e42ae78bc56a644a4cd7", size = 246416, upload-time = "2025-03-31T21:22:27.946Z" },
] ]
[package.optional-dependencies]
ext = [
{ name = "sanic-ext" },
]
[[package]]
name = "sanic-ext"
version = "24.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/c6/f5f87268e72825e3cd39c5b833996a2ac47f98b888f4253c5830afebd057/sanic_ext-24.12.0.tar.gz", hash = "sha256:8f912f4c29f242bc638346d09b79f0c8896ff64e79bd0e7fa09eac4b6c0e23c8", size = 66209, upload-time = "2025-03-05T07:24:39.795Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/3f/4c23be085bce45defd3863cbc707227fc82f49e7d9a5e1bb2656e2e1a2ed/sanic_ext-24.12.0-py3-none-any.whl", hash = "sha256:861f809f071770cf28acd5f13e97ed59985e07361b13b4b4540da1333730c83e", size = 96445, upload-time = "2025-03-05T07:24:38.059Z" },
]
[[package]]
name = "sanic-redis"
version = "0.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "hiredis" },
{ name = "redis" },
{ name = "sanic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5d/f1/10f56fbd817574058e09b2453474264afad5a203e85cd189f9d5513e0c2e/sanic_redis-0.6.0.tar.gz", hash = "sha256:e801be810b6ecc51f22a0a6f5543d3a684ab23ecb3decef0af8464679d47acc7", size = 9995, upload-time = "2025-06-20T06:57:57.582Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/28/c9/c06be46ee3390a9eafe0a878da9f4f5dcf14f681ad04a5cc78a996e0d7bc/sanic_redis-0.6.0-py3-none-any.whl", hash = "sha256:e8cce06d08e46bd19ebe7f89fb8717cbd5db3818238248bb77692640277f48c8", size = 4995, upload-time = "2025-06-20T06:57:56.585Z" },
]
[[package]] [[package]]
name = "sanic-routing" name = "sanic-routing"
version = "23.12.0" version = "23.12.0"