diff --git a/const.py b/const.py new file mode 100644 index 0000000..79da748 --- /dev/null +++ b/const.py @@ -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' diff --git a/main.py b/main.py index db19a48..f427531 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,6 @@ import base64 import datetime import logging import os -import random import re import traceback from asyncio import sleep @@ -20,12 +19,13 @@ from PIL import Image import httpx 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.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, BufferedInputFile, \ CallbackQuery 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 dotenv.load_dotenv('.env') @@ -127,7 +127,7 @@ async def send_post(post: E621Post, tag_list: Iterable[Iterable[str]]): caption=caption, parse_mode=ParseMode.HTML, reply_markup=markup) - await redis.sadd('e621:sent', post.id) + await redis.sadd(REDIS_SENT_KEY, post.id) except Exception as e: logging.exception(e) except Exception as e: @@ -136,12 +136,12 @@ async def send_post(post: E621Post, tag_list: Iterable[Iterable[str]]): async def check_updates(): logging.warning('Waiting for lock...') - async with redis.lock('e621:update'): + async with redis.lock(REDIS_LOCK_KEY): logging.warning('Lock acquired...') 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, ())) - 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] = [] logging.warning(f'Getting post versions from id {last_post_version}') for page in count(1): @@ -167,7 +167,7 @@ async def check_updates(): break matched_posts.sort() 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] logging.warning(f'Found {len(posts_to_send)} posts') 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): logging.warning(f'Sending post {post_chunk_idx + i + 1}/{len(posts_to_send)}') 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 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) @@ -190,8 +190,8 @@ async def resend_after(msg: Message): await msg.reply('Invalid timestamp or not provided') return - async with redis.lock('e621:update'): - tag_list = [tuple(t.decode().split()) for t in await redis.smembers('e621:subs')] + async with redis.lock(REDIS_LOCK_KEY): + tag_list = [tuple(t.decode().split()) for t in await redis.smembers(REDIS_SUBS_KEY)] for i, tag in enumerate(tag_list): await msg.reply(f'Checking tag {tag} ({i+1}/{len(tag_list)})', parse_mode=ParseMode.HTML) posts = [] @@ -218,8 +218,8 @@ async def add_tag(msg: Message): return for tag in args.split(): posts = await e621.get_posts(tag) - await redis.sadd('e621:sent', *[post.id for post in posts]) - await redis.sadd('e621:subs', tag) + await redis.sadd(REDIS_SENT_KEY, *[post.id for post in posts]) + await redis.sadd(REDIS_SUBS_KEY, tag) await msg.reply(f'Tags {args} added') @@ -233,20 +233,20 @@ async def add_tags(msg: Message): tags.sort() tags = ' '.join(tags) posts = await e621.get_posts(tags) - await redis.sadd('e621:sent', *[post.id for post in posts]) - await redis.sadd('e621:subs', tags) + await redis.sadd(REDIS_SENT_KEY, *[post.id for post in posts]) + await redis.sadd(REDIS_SUBS_KEY, tags) await msg.reply(f'Tag group {tags} added', parse_mode=ParseMode.HTML) @dp.message(filters.Command('mark_old_as_sent'), ChatFilter) async def mark_old_as_sent(msg: Message): logging.warning('Waiting for lock...') - async with redis.lock('e621:update'): - tag_list = [t.decode() for t in await redis.smembers('e621:subs')] + async with redis.lock(REDIS_LOCK_KEY): + 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') for i, tag in enumerate(tag_list, 1): 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 sleep(1) 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: await msg.reply('Tag should not contain spaces') return - if not await redis.sismember('e621:subs', args): + if not await redis.sismember(REDIS_SUBS_KEY, args): await msg.reply('Tag not found') return - await redis.srem('e621:subs', args) + await redis.srem(REDIS_SUBS_KEY, args) 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') return 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') @dp.message(filters.Command('list'), ChatFilter) 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() lines = [] for tag in tags: @@ -313,7 +313,7 @@ async def test(msg: Message): if not post: await msg.reply('Post not found') 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) @@ -345,7 +345,7 @@ async def send_callback(cq: CallbackQuery): async def background_on_start(): - await redis.delete('e621:update') + await redis.delete(REDIS_LOCK_KEY) while True: logging.warning('Checking updates...') try: diff --git a/pyproject.toml b/pyproject.toml index f8a18fc..5e7c31e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,5 +13,8 @@ dependencies = [ "pillow>=11.2.1", "python-dotenv>=1.1.1", "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", ] diff --git a/server.py b/server.py new file mode 100644 index 0000000..cb84bb7 --- /dev/null +++ b/server.py @@ -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, + ) + diff --git a/uv.lock b/uv.lock index 23cc69f..be5030a 100644 --- a/uv.lock +++ b/uv.lock @@ -190,10 +190,13 @@ dependencies = [ { name = "dataclasses-json" }, { name = "ffmpeg-python" }, { name = "httpx" }, + { name = "passlib" }, { name = "pillow" }, + { name = "pyjwt" }, { name = "python-dotenv" }, { name = "redis" }, - { name = "sanic" }, + { name = "sanic", extra = ["ext"] }, + { name = "sanic-redis" }, ] [package.metadata] @@ -203,10 +206,13 @@ requires-dist = [ { name = "dataclasses-json", specifier = ">=0.6.7" }, { name = "ffmpeg-python", specifier = ">=0.2.0" }, { name = "httpx", specifier = ">=0.28.1" }, + { name = "passlib", specifier = ">=1.7.4" }, { name = "pillow", specifier = ">=11.2.1" }, + { name = "pyjwt", specifier = ">=2.10.1" }, { name = "python-dotenv", specifier = ">=1.1.1" }, { 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]] @@ -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" }, ] +[[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]] name = "html5tagger" 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" }, ] +[[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]] name = "pillow" 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" }, ] +[[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]] name = "python-dateutil" 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" }, ] +[[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]] name = "redis" 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" }, ] +[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]] name = "sanic-routing" version = "23.12.0"