This commit is contained in:
bakatrouble 2025-07-14 17:12:42 +03:00
parent 6029e1b16d
commit a289d24698
5 changed files with 296 additions and 27 deletions

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'

48
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')
@ -127,7 +127,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 +136,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 +167,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,9 +176,9 @@ 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)
@ -190,8 +190,8 @@ async def resend_after(msg: Message):
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')] tag_list = [tuple(t.decode().split()) for t in await redis.smembers(REDIS_SUBS_KEY)]
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) await msg.reply(f'Checking tag <b>{tag}</b> ({i+1}/{len(tag_list)})', parse_mode=ParseMode.HTML)
posts = [] posts = []
@ -218,8 +218,8 @@ 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]) await redis.sadd(REDIS_SENT_KEY, *[post.id for post in posts])
await redis.sadd('e621:subs', tag) await redis.sadd(REDIS_SUBS_KEY, tag)
await msg.reply(f'Tags {args} added') await msg.reply(f'Tags {args} added')
@ -233,20 +233,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 +261,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 +275,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:
@ -313,7 +313,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)
@ -345,7 +345,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",
] ]

143
server.py Normal file
View File

@ -0,0 +1,143 @@
import json
import os
from dataclasses import dataclass
from functools import wraps
import dotenv
import jwt
from sanic import Sanic, Unauthorized
from sanic_ext import validate
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.update({
'REDIS': 'redis://localhost',
})
redis = SanicRedis()
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(request):
if pbkdf2_sha256(request.json['password']) != api_auth.get(request.json['username']):
return {'status': 'error', 'message': 'Invalid username or password'}
return {
'token': jwt.encode({}, api_secret, algorithm='HS256'),
}
@app.get('/api/subscriptions')
@protected
async def get_subscriptions(request):
async with redis.conn as r:
return {
'subscriptions': await r.smembers(REDIS_SUBS_KEY),
}
@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(request):
data = request.json
requested_subs = {' '.join(sorted(sub.lower().split())) for sub in data['subs']}
async with redis.conn as r:
subs = await get_subs(r)
skipped = requested_subs - subs
if skipped:
return {'status': 'error', 'message': 'Some subscriptions were not found', 'skipped': sorted(skipped)}
await r.srem(REDIS_SUBS_KEY, *requested_subs)
return {'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(request):
data = request.json
requested_subs = {' '.join(sorted(sub.lower().split())) for sub in data['subs']}
async with redis.conn as r:
subs = await get_subs(r)
conflicts = requested_subs & subs
if conflicts:
return {'status': 'error', 'message': 'Some subscriptions already exist', 'conflicts': sorted(conflicts)}
await r.sadd(REDIS_SUBS_KEY, *data['subs'])
return {'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
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"