├── .dockerignore ├── .github └── workflows │ └── publish-docker.yml ├── .gitignore ├── Dockerfile ├── config.json ├── picobot ├── __init__.py ├── __main__.py ├── config.py ├── fonts │ ├── OpenSans-Bold.ttf │ ├── OpenSans-Regular.ttf │ ├── OpenSans-SemiBold.ttf │ └── Symbola.ttf ├── geometry.py ├── handlers.py ├── media │ ├── caravela.png │ ├── caravela.webm │ └── circle_mask.png ├── msg_type.py ├── painter.py ├── repository │ ├── __init__.py │ ├── repo.py │ └── user_entity.py ├── responses.py ├── video_editor.py └── videos │ └── caravela.webm ├── poetry.lock ├── pyproject.toml ├── readme.md └── tests ├── __init__.py ├── test_painter.py └── test_picobot.py /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | -------------------------------------------------------------------------------- /.github/workflows/publish-docker.yml: -------------------------------------------------------------------------------- 1 | name: Create and publish a Docker image 2 | 3 | on: 4 | push: 5 | branches: ['release'] 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | jobs: 12 | build-and-push-image: 13 | runs-on: ubuntu-latest 14 | 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Log in to the Container registry 24 | uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 25 | with: 26 | registry: ${{ env.REGISTRY }} 27 | username: ${{ github.actor }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Extract metadata (tags, labels) for Docker 31 | id: meta 32 | uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 33 | with: 34 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 35 | 36 | - name: Build and push Docker image 37 | uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 38 | with: 39 | context: . 40 | push: true 41 | tags: ${{ steps.meta.outputs.tags }} 42 | labels: ${{ steps.meta.outputs.labels }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | config.json 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | # VSCode 108 | .vscode/ 109 | 110 | sketches/ 111 | 112 | # other trash 113 | backup* 114 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine AS builder 2 | 3 | RUN apk add poetry 4 | 5 | COPY . /build 6 | WORKDIR /build 7 | 8 | RUN poetry build --format wheel 9 | 10 | FROM python:3.11-alpine 11 | 12 | COPY --from=builder /build/dist/*.whl /root 13 | RUN pip install /root/*.whl 14 | 15 | CMD python -m picobot 16 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "Token you received from @BotFather", 3 | "creator_id": "Your Telegram user id (optional)", 4 | "db_path": "Path to database. Default: .config/picobot/bot.db" 5 | } 6 | -------------------------------------------------------------------------------- /picobot/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.0' 2 | -------------------------------------------------------------------------------- /picobot/__main__.py: -------------------------------------------------------------------------------- 1 | from telegram.ext import CommandHandler, Filters, MessageHandler, Updater 2 | 3 | from picobot import handlers 4 | 5 | from picobot.config import DB_PATH, TOKEN 6 | from picobot.repository.repo import repository 7 | 8 | 9 | def main(): 10 | """Start the bot.""" 11 | 12 | updater = Updater(TOKEN) 13 | 14 | dp = updater.dispatcher 15 | 16 | dp.add_error_handler(handlers.error) 17 | 18 | dp.add_handler(CommandHandler('start', handlers.start)) 19 | dp.add_handler(CommandHandler('addsticker', handlers.add_sticker)) 20 | dp.add_handler(CommandHandler('newpack', handlers.create_pack)) 21 | dp.add_handler(CommandHandler('newvideopack', handlers.create_video_pack)) 22 | dp.add_handler(CommandHandler('delsticker', handlers.del_sticker)) 23 | dp.add_handler(CommandHandler('help', handlers.handler_help)) 24 | dp.add_handler(CommandHandler('setdefaultpack', handlers.set_default_pack)) 25 | dp.add_handler(CommandHandler('setpublic', handlers.handler_pack_public)) 26 | dp.add_handler(CommandHandler('setprivate', handlers.handler_pack_private)) 27 | media_filter = (Filters.photo | Filters.document) & (~Filters.reply) 28 | dp.add_handler(MessageHandler(filters=media_filter, callback=handlers.caption_handler)) 29 | 30 | dp.add_handler(CommandHandler('add_pack_to_user', handlers.add_pack_to_user)) 31 | 32 | repository(DB_PATH) # create or load persistence repository 33 | updater.start_polling() 34 | updater.idle() 35 | 36 | 37 | if __name__ == "__main__": 38 | main() 39 | -------------------------------------------------------------------------------- /picobot/config.py: -------------------------------------------------------------------------------- 1 | import json 2 | from functools import lru_cache 3 | from os import environ 4 | from pathlib import Path 5 | 6 | ROOT_DIR = Path(__file__).absolute().parent 7 | 8 | @lru_cache(1) 9 | def get_config_dir() -> Path: 10 | if 'PICOBOT_CONFIG_DIR' in environ: 11 | return Path(environ['PICOBOT_CONFIG_DIR']) 12 | 13 | if 'XDG_CONFIG_HOME' in environ: 14 | return Path(environ['XDG_CONFIG_HOME']) / 'picobot' 15 | 16 | return Path.home() / '.config' / 'picobot' 17 | 18 | CONFIG_DIR = get_config_dir() 19 | 20 | with open(CONFIG_DIR / 'config.json') as f: 21 | config = json.load(f) 22 | 23 | TOKEN = config['token'] 24 | DB_PATH = config.get('db_path', CONFIG_DIR / 'bot.db') 25 | CREATOR_ID = config['creator_id'] 26 | -------------------------------------------------------------------------------- /picobot/fonts/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caravelahc/pico-bot/38f269f8965add712011f4a50561e477c71f2172/picobot/fonts/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /picobot/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caravelahc/pico-bot/38f269f8965add712011f4a50561e477c71f2172/picobot/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /picobot/fonts/OpenSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caravelahc/pico-bot/38f269f8965add712011f4a50561e477c71f2172/picobot/fonts/OpenSans-SemiBold.ttf -------------------------------------------------------------------------------- /picobot/fonts/Symbola.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caravelahc/pico-bot/38f269f8965add712011f4a50561e477c71f2172/picobot/fonts/Symbola.ttf -------------------------------------------------------------------------------- /picobot/geometry.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | AVATAR_SIZE = 42 4 | MARGIN = 4 5 | PADDING = 10 6 | MSG_PADDING_H = 8 7 | MSG_PADDING_V = 6 8 | TIME_PADDING_H = 8 9 | TIME_PADDING_BOTTOM = 6 10 | BOX_HEIGHT = 54 11 | BOX_MIN_WIDTH = 160 12 | BOX_MAX_WIDTH = 264 13 | BOX_RADIUS = 10 14 | FONT_SIZE = 14 15 | LINE_SPACE = 4 16 | LINE_WIDTH_LIMIT = 26 17 | MAX_NUMBER_OF_LINES = 20 18 | 19 | 20 | @dataclass 21 | class Point: 22 | x: int 23 | y: int 24 | 25 | def to_tuple(self): 26 | return (self.x, self.y) 27 | 28 | 29 | @dataclass 30 | class Box: 31 | top_left: Point 32 | bottom_right: Point 33 | 34 | def __init__(self, x0, y0, x1, y1): 35 | self.top_left = Point(x0, y0) 36 | self.bottom_right = Point(x1, y1) 37 | 38 | def center(self): 39 | return Point( 40 | (self.top_left.x + self.bottom_right.x) / 2, 41 | (self.top_left.y + self.bottom_right.y) / 2, 42 | ) 43 | 44 | def to_list(self): 45 | return [ 46 | self.top_left.x, 47 | self.top_left.y, 48 | self.bottom_right.x, 49 | self.bottom_right.y, 50 | ] 51 | -------------------------------------------------------------------------------- /picobot/handlers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import shlex 4 | from functools import wraps 5 | from pathlib import Path 6 | 7 | from slugify import slugify 8 | from telegram import Bot, Message, Update 9 | from telegram.ext import CallbackContext 10 | import telegram 11 | 12 | from picobot import responses 13 | 14 | from .config import CREATOR_ID, ROOT_DIR 15 | from .msg_type import MsgType 16 | from .painter import sticker_from_image, sticker_from_text 17 | from .video_editor import sticker_from_video, VideoTooLongError 18 | from .repository.repo import repository 19 | 20 | MEDIA_DIR = ROOT_DIR / 'media' 21 | IMG_PREFIX = 'img' 22 | VIDEO_PREFIX = 'vid' 23 | AVATAR_PREFIX = 'avatar' 24 | 25 | logging.basicConfig( 26 | filename='log_picobot.log', 27 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 28 | level=logging.INFO, 29 | ) 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | DEFAULT_EMOJI = '😁' 34 | 35 | 36 | class FileTooLargeError(Exception): 37 | pass 38 | 39 | 40 | def creator_only(func): 41 | @wraps(func) 42 | def new_func(bot, update, *args, **kwargs): 43 | if update.message.from_user.id == CREATOR_ID: 44 | return func(bot, update, *args, **kwargs) 45 | else: 46 | update.message.reply_text(responses.CREATOR_ACCESS_DENIED) 47 | 48 | return new_func 49 | 50 | 51 | def build_pack_name(title: str, bot: Bot) -> str: 52 | slug = slugify(title, separator="_", lowercase=False) 53 | return f'{slug}_by_{bot.username}' 54 | 55 | 56 | def start(update: Update, context: CallbackContext) -> None: 57 | """Send a message when the command /start is issued.""" 58 | update.message.reply_text(responses.GREETING) 59 | 60 | 61 | def create_pack(update: Update, context: CallbackContext) -> None: 62 | bot = context.bot 63 | user = update.message.from_user 64 | 65 | if not check_msg_format(update.message.text): 66 | update.message.reply_text(responses.INVALID_MSG) 67 | return 68 | 69 | splittext = shlex.split(update.message.text) 70 | 71 | title = splittext[1] 72 | name = build_pack_name(title, bot) 73 | png_sticker = open(MEDIA_DIR / 'caravela.png', 'rb') 74 | emoji = splittext[2] if len(splittext) > 2 else DEFAULT_EMOJI 75 | 76 | # Create Pack 77 | try: 78 | bot.create_new_sticker_set( 79 | user_id=user.id, 80 | name=name, 81 | title=title, 82 | png_sticker=png_sticker, 83 | emojis=emoji, 84 | ) 85 | sticker = bot.get_sticker_set(name).stickers[0] 86 | update.message.reply_sticker(sticker) 87 | repository().add_pack_to_user(user, name) 88 | except Exception as exc: 89 | logger.error( 90 | "Exception on Create Pack. User %s (id %d) Pack %s", 91 | user.first_name, 92 | user.id, 93 | name, 94 | ) 95 | 96 | logger.error(exc) 97 | update.message.reply_text(responses.ERROR_MSG) 98 | png_sticker.close() 99 | 100 | 101 | def create_video_pack(update: Update, context: CallbackContext) -> None: 102 | bot = context.bot 103 | user = update.message.from_user 104 | 105 | if not check_msg_format(update.message.text): 106 | update.message.reply_text(responses.INVALID_MSG) 107 | return 108 | 109 | splittext = shlex.split(update.message.text) 110 | 111 | title = splittext[1] 112 | name = build_pack_name(title, bot) 113 | with open(MEDIA_DIR / 'caravela.webm', 'rb') as webm_sticker: 114 | emoji = splittext[2] if len(splittext) > 2 else DEFAULT_EMOJI 115 | 116 | # Create Video Pack 117 | try: 118 | bot.create_new_sticker_set( 119 | user_id=user.id, 120 | name=name, 121 | title=title, 122 | webm_sticker=webm_sticker, 123 | emojis=emoji, 124 | ) 125 | sticker = bot.get_sticker_set(name).stickers[0] 126 | update.message.reply_sticker(sticker) 127 | repository().add_pack_to_user(user, name) 128 | except Exception as exc: 129 | logger.error( 130 | "Exception on Create Pack. User %s (id %d) Pack %s", 131 | user.first_name, 132 | user.id, 133 | name, 134 | ) 135 | 136 | logger.error(exc) 137 | update.message.reply_text(responses.ERROR_MSG) 138 | 139 | 140 | def add_sticker(update: Update, context: CallbackContext) -> None: 141 | bot = context.bot 142 | msg = update.message 143 | 144 | msg_type = get_msg_type(msg) 145 | response = responses.ERROR_MSG 146 | user_id = msg.from_user.id 147 | splittext = shlex.split(msg.text) 148 | 149 | if check_msg_format(msg.text): 150 | title = splittext[1] 151 | pack_name = build_pack_name(title, bot) 152 | 153 | # check if user is pack's owner 154 | if not repository().check_permission(user_id, pack_name): 155 | msg.reply_text(responses.NO_PERMISSION) 156 | return 157 | 158 | else: # if pack name not informed check if user has default pack 159 | user = repository().users().get(user_id) 160 | 161 | if user is not None and user.def_pack is not None: 162 | pack_name = user.def_pack 163 | else: 164 | msg.reply_text(responses.INVALID_MSG) 165 | return 166 | 167 | if len(splittext) > 2: 168 | emoji = splittext[2] 169 | else: 170 | emoji = DEFAULT_EMOJI 171 | 172 | # check if it's image, file, text, or sticker 173 | if msg_type == MsgType.REP_TEXT: 174 | if add_text(bot, msg, user_id, pack_name, emoji): 175 | return 176 | elif msg_type == MsgType.PHOTO: 177 | if add_photo(bot, msg, user_id, pack_name, emoji, False): 178 | return 179 | elif msg_type == MsgType.REP_PHOTO: 180 | if add_photo(bot, msg, user_id, pack_name, emoji, True): 181 | return 182 | elif msg_type == MsgType.VIDEO: 183 | media = msg.video[-1] 184 | if add_video(bot, msg, media, user_id, pack_name, emoji, circle=False): 185 | return 186 | elif msg_type == MsgType.REP_VIDEO: 187 | media = msg.reply_to_message.video 188 | if add_video(bot, msg, media, user_id, pack_name, emoji, circle=False): 189 | return 190 | elif msg_type == MsgType.VIDEO_NOTE: 191 | media = msg.video_note[-1] 192 | if add_video(bot, msg, media, user_id, pack_name, emoji, circle=True): 193 | return 194 | elif msg_type == MsgType.REP_VIDEO_NOTE: 195 | media = msg.reply_to_message.video_note 196 | if add_video(bot, msg, media, user_id, pack_name, emoji, circle=True): 197 | return 198 | elif msg_type == MsgType.DOCUMENT_VIDEO: 199 | media = msg.document[-1] 200 | if add_video(bot, msg, media, user_id, pack_name, emoji, circle=False): 201 | return 202 | elif msg_type == MsgType.REP_DOCUMENT_VIDEO: 203 | media = msg.reply_to_message.document 204 | if add_video(bot, msg, media, user_id, pack_name, emoji, circle=False): 205 | return 206 | elif msg_type == MsgType.DOCUMENT: 207 | if add_document(bot, msg, user_id, pack_name, emoji, False): 208 | return 209 | elif msg_type == MsgType.REP_DOCUMENT: 210 | if add_document(bot, msg, user_id, pack_name, emoji, True): 211 | return 212 | elif msg_type in [MsgType.STICKER, MsgType.REP_STICKER]: 213 | if insert_sticker_in_pack(bot, msg, user_id, pack_name, emoji): 214 | return 215 | elif msg_type in [MsgType.VIDEO_STICKER, MsgType.REP_VIDEO_STICKER]: 216 | media = msg.reply_to_message.document 217 | if insert_video_sticker_in_pack(bot, msg, user_id, pack_name, emoji): 218 | return 219 | 220 | # check for errors 221 | 222 | update.message.reply_text(response) 223 | 224 | 225 | def add_text(bot: Bot, msg: Message, user_id: int, pack_name: str, emoji: str): 226 | forward = msg.reply_to_message.forward_from 227 | if forward is not None: 228 | username = forward.first_name 229 | other_user_id = forward.id 230 | msg_time = msg.reply_to_message.forward_date.strftime('%H:%M') 231 | else: 232 | username = msg.reply_to_message.from_user.first_name 233 | other_user_id = msg.reply_to_message.from_user.id 234 | msg_time = msg.reply_to_message.date.strftime('%H:%M') 235 | photos = bot.get_user_profile_photos(other_user_id, limit=1).photos 236 | avatar_path = Path('') 237 | try: 238 | photo = photos[0][0] 239 | avatar_path = MEDIA_DIR / f'{AVATAR_PREFIX}{other_user_id}.jpg' 240 | bot.get_file(photo.file_id).download(custom_path=avatar_path) 241 | except Exception: 242 | msg.reply_text(responses.ERROR_DOWNLOAD_PHOTO) 243 | 244 | text = msg.reply_to_message.text 245 | # save as png 246 | img_path = sticker_from_text(user_id, username, text, avatar_path, msg_time, other_user_id) 247 | try: 248 | with open(img_path, 'rb') as png_sticker: 249 | bot.add_sticker_to_set( 250 | user_id=user_id, name=pack_name, png_sticker=png_sticker, emojis=emoji 251 | ) 252 | sticker = bot.get_sticker_set(pack_name).stickers[-1] 253 | msg.reply_sticker(sticker) 254 | img_path.unlink(missing_ok=True) 255 | except Exception as exc: 256 | if isinstance(exc, telegram.error.BadRequest): 257 | exception_msg = exc.message.lower() 258 | if exception_msg in responses.TELEGRAM_ERROR_CODES: 259 | msg.reply_text(responses.TELEGRAM_ERROR_CODES[exception_msg]) 260 | return True 261 | logger.error( 262 | "Exception on add_text. User %s (id %d) Pack %s", 263 | username, 264 | user_id, 265 | pack_name, 266 | ) 267 | logger.error(exc) 268 | return False 269 | finally: 270 | img_path.unlink(missing_ok=True) 271 | avatar_path.unlink(missing_ok=True) 272 | return True 273 | 274 | 275 | def caption_handler(update: Update, context: CallbackContext): 276 | text = update.message.caption 277 | if text is None or text == '': 278 | return 279 | if text.split()[0] == '/addsticker': 280 | update.message.text = text 281 | add_sticker(context.bot, update) 282 | 283 | 284 | def add_photo(bot: Bot, msg: Message, user_id: int, pack_name: str, emoji: str, replied: bool): 285 | if replied: 286 | photo = msg.reply_to_message.photo[-1] 287 | else: 288 | photo = msg.photo[-1] 289 | img_path = MEDIA_DIR / f'{IMG_PREFIX}{user_id}.jpg' 290 | try: 291 | bot.get_file(photo.file_id).download(custom_path=img_path) 292 | # resize and save as png 293 | png_path = sticker_from_image(img_path) 294 | with open(png_path, 'rb') as png_sticker: 295 | bot.add_sticker_to_set( 296 | user_id=user_id, name=pack_name, png_sticker=png_sticker, emojis=emoji 297 | ) 298 | sticker = bot.get_sticker_set(pack_name).stickers[-1] 299 | msg.reply_sticker(sticker) 300 | img_path.unlink(missing_ok=True) 301 | png_path.unlink(missing_ok=True) 302 | except Exception as exc: 303 | if isinstance(exc, telegram.error.BadRequest): 304 | exception_msg = exc.message.lower() 305 | if exception_msg in responses.TELEGRAM_ERROR_CODES: 306 | msg.reply_text(responses.TELEGRAM_ERROR_CODES[exception_msg]) 307 | return True 308 | logger.error( 309 | "Exception on add_photo. User id %d Pack %s", 310 | user_id, 311 | pack_name, 312 | ) 313 | logger.error(exc) 314 | return False 315 | 316 | return True 317 | 318 | 319 | def add_video( 320 | bot: Bot, msg: Message, video, user_id: int, pack_name: str, emoji: str, *, circle: bool 321 | ): 322 | video_path = MEDIA_DIR / f'{VIDEO_PREFIX}{user_id}.mp4' 323 | try: 324 | if bot.get_file(video.file_id).file_size > 2172859: 325 | msg.reply_text(responses.FILE_TOO_LARGE) 326 | raise FileTooLargeError("File size exceeds limits") 327 | bot.get_file(video.file_id).download(custom_path=video_path) 328 | # resize and save as webm 329 | if not circle: 330 | webm_path = sticker_from_video(video_path) 331 | else: 332 | webm_path = sticker_from_video(video_path, MEDIA_DIR / "circle_mask.png") 333 | with open(webm_path, 'rb') as webm_sticker: 334 | bot.add_sticker_to_set( 335 | user_id=user_id, name=pack_name, webm_sticker=webm_sticker, emojis=emoji 336 | ) 337 | sticker = bot.get_sticker_set(pack_name).stickers[-1] 338 | msg.reply_sticker(sticker) 339 | video_path.unlink(missing_ok=True) 340 | webm_path.unlink(missing_ok=True) 341 | except Exception as exc: 342 | if isinstance(exc, VideoTooLongError): 343 | msg.reply_text(responses.VIDEO_TOO_LONG) 344 | return True 345 | if isinstance(exc, telegram.error.BadRequest): 346 | exception_msg = exc.message.lower() 347 | if exception_msg in responses.TELEGRAM_ERROR_CODES: 348 | msg.reply_text(responses.TELEGRAM_ERROR_CODES[exception_msg]) 349 | return True 350 | logger.error( 351 | "Exception on add_video. User id %d Pack %s", 352 | user_id, 353 | pack_name, 354 | ) 355 | logger.error(exc) 356 | return False 357 | 358 | return True 359 | 360 | 361 | def add_document(bot: Bot, msg: Message, user_id: int, pack_name: str, emoji: str, replied: bool): 362 | if replied: 363 | doc = msg.reply_to_message.document 364 | else: 365 | doc = msg.document 366 | 367 | try: 368 | bot.add_sticker_to_set( 369 | user_id=user_id, name=pack_name, png_sticker=doc.file_id, emojis=emoji 370 | ) 371 | sticker = bot.get_sticker_set(pack_name).stickers[-1] 372 | msg.reply_sticker(sticker) 373 | except telegram.error.BadRequest as exc: 374 | exception_msg = exc.message.lower() 375 | if exception_msg in responses.TELEGRAM_ERROR_CODES: 376 | msg.reply_text(responses.TELEGRAM_ERROR_CODES[exception_msg]) 377 | except Exception: 378 | msg.reply_text(responses.INVALID_DOC) 379 | return False 380 | return True 381 | 382 | 383 | def insert_sticker_in_pack(bot: Bot, msg: Message, user_id: int, pack_name: str, emoji: str): 384 | sticker_id = msg.reply_to_message.sticker.file_id 385 | 386 | img_path = MEDIA_DIR / f'{IMG_PREFIX}{user_id}.jpg' 387 | try: 388 | sticker_file = bot.get_file(sticker_id) 389 | sticker_file.download(custom_path=str(img_path)) 390 | # resize and save as png 391 | png_path = sticker_from_image(img_path) 392 | with open(png_path, 'rb') as png_sticker: 393 | bot.add_sticker_to_set( 394 | user_id=user_id, name=pack_name, png_sticker=png_sticker, emojis=emoji 395 | ) 396 | sticker = bot.get_sticker_set(pack_name).stickers[-1] 397 | msg.reply_sticker(sticker) 398 | img_path.unlink(missing_ok=True) 399 | png_path.unlink(missing_ok=True) 400 | except Exception as exc: 401 | if isinstance(exc, telegram.error.BadRequest): 402 | exception_msg = exc.message.lower() 403 | if exception_msg in responses.TELEGRAM_ERROR_CODES: 404 | msg.reply_text(responses.TELEGRAM_ERROR_CODES[exception_msg]) 405 | return True 406 | logger.error( 407 | "Exception inserting sticker in pack. User id %d Pack %s", 408 | user_id, 409 | pack_name, 410 | ) 411 | logger.error(exc) 412 | return False 413 | return True 414 | 415 | 416 | def insert_video_sticker_in_pack(bot: Bot, msg: Message, user_id: int, pack_name: str, emoji: str): 417 | sticker_id = msg.reply_to_message.sticker.file_id 418 | 419 | video_path = MEDIA_DIR / f'{VIDEO_PREFIX}{user_id}.webm' 420 | try: 421 | sticker_file = bot.get_file(sticker_id) 422 | sticker_file.download(custom_path=str(video_path)) 423 | with open(video_path, 'rb') as webm_sticker: 424 | bot.add_sticker_to_set( 425 | user_id=user_id, name=pack_name, webm_sticker=webm_sticker, emojis=emoji 426 | ) 427 | sticker = bot.get_sticker_set(pack_name).stickers[-1] 428 | msg.reply_sticker(sticker) 429 | except Exception as exc: 430 | if isinstance(exc, telegram.error.BadRequest): 431 | exception_msg = exc.message.lower() 432 | if exception_msg in responses.TELEGRAM_ERROR_CODES: 433 | msg.reply_text(responses.TELEGRAM_ERROR_CODES[exception_msg]) 434 | return True 435 | logger.error( 436 | "Exception inserting sticker in pack. User id %d Pack %s", 437 | user_id, 438 | pack_name, 439 | ) 440 | logger.error(exc) 441 | return False 442 | return True 443 | 444 | 445 | def del_sticker(update: Update, context: CallbackContext): 446 | bot = context.bot 447 | msg: Message = update.message 448 | msg_type = get_msg_type(msg) 449 | user_id = msg.from_user.id 450 | 451 | try: 452 | if msg_type == MsgType.TEXT: 453 | splittext = shlex.split(msg.text) 454 | title = splittext[1] 455 | pos = int(splittext[2]) 456 | 457 | pack_name = build_pack_name(title, bot) 458 | sticker_id = bot.get_sticker_set(pack_name).stickers[pos].file_id 459 | elif msg_type == MsgType.REP_STICKER or msg_type == MsgType.REP_VIDEO_STICKER: 460 | pack_name = msg.reply_to_message.sticker.set_name 461 | sticker_id = msg.reply_to_message.sticker.file_id 462 | 463 | if pack_name is None: 464 | msg.reply_text('Não é possível remover o sticker de um pack inexistente.') 465 | return 466 | 467 | if not repository().check_permission(user_id, pack_name): 468 | msg.reply_text(responses.NO_PERMISSION) 469 | return 470 | 471 | bot.delete_sticker_from_set(sticker_id) 472 | msg.reply_text(responses.REMOVED_STICKER) 473 | 474 | except Exception: 475 | msg.reply_text(responses.REMOVE_STICKER_HELP) 476 | 477 | 478 | def set_default_pack(update: Update, context: CallbackContext): 479 | bot = context.bot 480 | msg: Message = update.message 481 | user_id = msg.from_user.id 482 | 483 | if check_msg_format(msg.text): 484 | splittext = shlex.split(msg.text) 485 | title = splittext[1] 486 | pack_name = build_pack_name(title, bot) 487 | 488 | # check if user is pack's owner 489 | if repository().check_permission(user_id, pack_name): 490 | repository().users().get(user_id).def_pack = pack_name 491 | else: 492 | msg.reply_text(responses.NO_PERMISSION) 493 | return 494 | else: 495 | update.message.reply_text(responses.INVALID_MSG) 496 | 497 | 498 | def handler_pack_public(update: Update, context: CallbackContext): 499 | _set_pack_public(update, context, True) 500 | 501 | 502 | def handler_pack_private(update: Update, context: CallbackContext): 503 | _set_pack_public(update, context, False) 504 | 505 | 506 | def _set_pack_public(update: Update, context: CallbackContext, is_public: bool): 507 | msg: Message = update.message 508 | user_id = msg.from_user.id 509 | 510 | if check_msg_format(msg.text): 511 | splittext = shlex.split(msg.text) 512 | title = splittext[1] 513 | pack_name = build_pack_name(title, context.bot) 514 | 515 | # check if user is pack's owner 516 | if repository().check_permission(user_id, pack_name): 517 | repository().set_pack_public(pack_name, is_public) 518 | msg.reply_text(responses.PACK_PRIVACY_UPDATED) 519 | else: 520 | msg.reply_text(responses.NO_PERMISSION) 521 | return 522 | else: 523 | msg.reply_text(responses.INVALID_MSG) 524 | 525 | 526 | @creator_only 527 | def add_pack_to_user(update: Update, context: CallbackContext): 528 | msg: Message = update.message 529 | try: 530 | user = msg.reply_to_message.forward_from 531 | if user is None: 532 | user = msg.reply_to_message.from_user 533 | 534 | if check_msg_format(msg.text): 535 | splittext = shlex.split(msg.text) 536 | title = splittext[1] 537 | pack_name = build_pack_name(title, context.bot) 538 | 539 | repository().add_pack_to_user(user, pack_name) 540 | else: 541 | msg.reply_text(responses.INVALID_MSG) 542 | except Exception: 543 | msg.reply_text(responses.ERROR_MSG) 544 | 545 | 546 | def check_msg_format(text: str): 547 | return text is not None and len(text.split()) > 1 548 | 549 | 550 | def get_msg_type(message: Message): 551 | replied = False 552 | if message.reply_to_message is not None: 553 | replied = True 554 | message = message.reply_to_message 555 | 556 | if message.photo is not None and len(message.photo) > 0: 557 | msg_type = MsgType.PHOTO 558 | elif message.video is not None: 559 | msg_type = MsgType.VIDEO 560 | elif message.sticker is not None: 561 | if message.sticker.is_video: 562 | msg_type = MsgType.VIDEO_STICKER 563 | else: 564 | msg_type = MsgType.STICKER 565 | elif message.document is not None: 566 | if message.document.mime_type == 'video/mp4': 567 | msg_type = MsgType.DOCUMENT_VIDEO 568 | else: 569 | msg_type = MsgType.DOCUMENT 570 | elif message.video_note is not None: 571 | msg_type = MsgType.VIDEO_NOTE 572 | elif message.text is not None: 573 | msg_type = MsgType.TEXT 574 | 575 | if replied: 576 | return MsgType(msg_type * 10) 577 | else: 578 | return msg_type 579 | 580 | 581 | def handler_help(update: Update, context: CallbackContext): 582 | """Send a message when the command /help is issued.""" 583 | update.message.reply_text(responses.HELP_MSG) 584 | 585 | 586 | def error(update: Update, context: CallbackContext): 587 | """Log Errors caused by Updates.""" 588 | logger.warning('Update "%s" caused error "%s"', update, context.error) 589 | -------------------------------------------------------------------------------- /picobot/media/caravela.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caravelahc/pico-bot/38f269f8965add712011f4a50561e477c71f2172/picobot/media/caravela.png -------------------------------------------------------------------------------- /picobot/media/caravela.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caravelahc/pico-bot/38f269f8965add712011f4a50561e477c71f2172/picobot/media/caravela.webm -------------------------------------------------------------------------------- /picobot/media/circle_mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caravelahc/pico-bot/38f269f8965add712011f4a50561e477c71f2172/picobot/media/circle_mask.png -------------------------------------------------------------------------------- /picobot/msg_type.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class MsgType(IntEnum): 5 | TEXT = 1 6 | PHOTO = 2 7 | DOCUMENT = 3 8 | DOCUMENT_VIDEO = 4 9 | STICKER = 5 10 | VIDEO = 6 11 | VIDEO_NOTE = 7 12 | VIDEO_STICKER = 8 13 | REP_TEXT = 10 14 | REP_PHOTO = 20 15 | REP_DOCUMENT = 30 16 | REP_DOCUMENT_VIDEO = 40 17 | REP_STICKER = 50 18 | REP_VIDEO = 60 19 | REP_VIDEO_NOTE = 70 20 | REP_VIDEO_STICKER = 80 21 | -------------------------------------------------------------------------------- /picobot/painter.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | from pathlib import Path 3 | 4 | from emoji import emoji_lis, emoji_count 5 | from PIL import Image, ImageDraw, ImageFont 6 | 7 | from .config import ROOT_DIR 8 | from .geometry import ( 9 | AVATAR_SIZE, 10 | MARGIN, 11 | PADDING, 12 | MSG_PADDING_H, 13 | MSG_PADDING_V, 14 | TIME_PADDING_H, 15 | TIME_PADDING_BOTTOM, 16 | BOX_MIN_WIDTH, 17 | BOX_RADIUS, 18 | FONT_SIZE, 19 | LINE_SPACE, 20 | LINE_WIDTH_LIMIT, 21 | MAX_NUMBER_OF_LINES, 22 | Point, 23 | Box, 24 | ) 25 | 26 | MEDIA_DIR = ROOT_DIR / 'media' 27 | FONT_DIR = ROOT_DIR / 'fonts' 28 | IMG_PREFIX = 'img' 29 | AVATAR_MASK_NAME = 'avatar_mask.png' 30 | 31 | BOX_COLOR = "#182533" 32 | TITLE_COLOR = "#338cf3" 33 | TEXT_COLOR = "#dddddd" 34 | TIME_COLOR = "#6A7B8C" 35 | FOREGROUND_COLORS = ["#1D9BF9","#FFCC00","#F91880","#7856FF","#FF7A00","#00BA7C"] 36 | EMOJI_JOINER = chr(0xFE0F) 37 | 38 | FONTS = { 39 | 'bold': ImageFont.truetype(font=str(FONT_DIR / 'OpenSans-Bold.ttf'), size=FONT_SIZE), 40 | 'normal': ImageFont.truetype(font=str(FONT_DIR / 'OpenSans-SemiBold.ttf'), size=FONT_SIZE), 41 | 'time': ImageFont.truetype(font=str(FONT_DIR / 'OpenSans-Regular.ttf'), size=13), 42 | 'emoji': ImageFont.truetype(font=str(FONT_DIR / 'Symbola.ttf'), size=16), 43 | 'avatar': ImageFont.truetype(font=str(FONT_DIR / 'OpenSans-SemiBold.ttf'), size=20), 44 | } 45 | 46 | 47 | def draw_balloon(img_draw: ImageDraw.Draw, points: Box, fill=None, width=0): 48 | r_x0 = points.top_left.x 49 | r_y0 = points.top_left.y + BOX_RADIUS 50 | r_x1 = points.bottom_right.x 51 | r_y1 = points.bottom_right.y - BOX_RADIUS 52 | img_draw.rectangle([r_x0, r_y0, r_x1, r_y1], fill=fill, width=width) 53 | r_x0 = points.top_left.x + BOX_RADIUS 54 | r_y0 = points.top_left.y 55 | r_x1 = points.bottom_right.x - BOX_RADIUS 56 | r_y1 = points.bottom_right.y 57 | img_draw.rectangle([r_x0, r_y0, r_x1, r_y1], fill=fill, width=width) 58 | diam = 2 * BOX_RADIUS 59 | c_x0 = points.top_left.x 60 | c_y0 = points.top_left.y 61 | c_x1 = c_x0 + diam 62 | c_y1 = c_y0 + diam 63 | img_draw.ellipse([c_x0, c_y0, c_x1, c_y1], fill=fill, width=width) 64 | c_x0 = points.bottom_right.x - diam 65 | c_x1 = c_x0 + diam 66 | img_draw.ellipse([c_x0, c_y0, c_x1, c_y1], fill=fill, width=width) 67 | c_y0 = points.bottom_right.y - diam 68 | c_y1 = c_y0 + diam 69 | img_draw.ellipse([c_x0, c_y0, c_x1, c_y1], fill=fill, width=width) 70 | 71 | arrow_x = 10 72 | arrow_y = 10 if BOX_RADIUS < 10 else BOX_RADIUS 73 | arrow = [ 74 | (points.top_left.x - arrow_x, points.bottom_right.y), 75 | (points.top_left.x, points.bottom_right.y - arrow_y), 76 | (points.top_left.x + diam, points.bottom_right.y - arrow_y), 77 | (points.top_left.x + diam, points.bottom_right.y), 78 | ] 79 | img_draw.polygon(arrow, fill=fill) 80 | 81 | 82 | def draw_username( 83 | txt_draw: ImageDraw.ImageDraw, position: Point, username="Caravela", fill=TITLE_COLOR, 84 | ): 85 | x0 = position.x + MSG_PADDING_H 86 | y0 = position.y + MSG_PADDING_V 87 | txt_draw.text((x0, y0), username, font=FONTS['bold'], fill=fill) 88 | 89 | 90 | def draw_message( 91 | txt_draw: ImageDraw.ImageDraw, points: Box, text=' ', user_size=[0, 0], 92 | ): 93 | current_position = Point( 94 | points.top_left.x + MSG_PADDING_H, 95 | points.top_left.y + MSG_PADDING_V + user_size[1] + LINE_SPACE, 96 | ) 97 | emoji_locations = emoji_lis(text) 98 | indexes = [0] 99 | 100 | # Some complex emojis use \u200d as a zero-width character to join two emojis into one 101 | # Pillow doesn't work with this and use chr(0xFE0F) instead 102 | text = text.replace('\u200d', EMOJI_JOINER) 103 | 104 | def draw_emoji(em: dict): 105 | current_text = em['emoji'] 106 | txt_draw.text( 107 | (current_position.x, current_position.y + 4), 108 | text=em['emoji'], 109 | font=FONTS['emoji'], 110 | fill=TEXT_COLOR, 111 | ) 112 | incr = 1 113 | if len(text) > em['location'] + 1 and text[em['location'] + 1] == EMOJI_JOINER: 114 | incr = 2 115 | indexes.append(em['location'] + incr) 116 | current_position.x += txt_draw.textsize(current_text, font=FONTS['emoji'])[0] 117 | 118 | def draw_text(text: str): 119 | lines = text.split('\n') 120 | y_displacement = txt_draw.textsize(' ', font=FONTS['normal'])[1] + LINE_SPACE 121 | for line in lines[:-1]: 122 | txt_draw.text( 123 | current_position.to_tuple(), text=line, font=FONTS['normal'], fill=TEXT_COLOR, 124 | ) 125 | current_position.x = points.top_left.x + MSG_PADDING_H 126 | current_position.y += y_displacement 127 | text = lines[-1] 128 | txt_draw.text( 129 | current_position.to_tuple(), text=text, font=FONTS['normal'], fill=TEXT_COLOR, 130 | ) 131 | displacement = txt_draw.textsize(text, font=FONTS['normal']) 132 | current_position.x += displacement[0] 133 | 134 | for em in emoji_locations: 135 | last_index = indexes[-1] 136 | if em['location'] - 1 < last_index: # last drawn symbol was an emoji 137 | draw_emoji(em) 138 | else: 139 | current_text = text[last_index : em['location']] 140 | draw_text(current_text) 141 | draw_emoji(em) 142 | if indexes[-1] < len(text): 143 | current_text = text[indexes[-1] :] 144 | draw_text(current_text) 145 | 146 | 147 | def draw_time(txt_draw: ImageDraw.ImageDraw, points: Box, text="04:20"): 148 | tw = txt_draw.textsize(text, font=FONTS['time']) 149 | x0 = points.bottom_right.x - TIME_PADDING_H - tw[0] 150 | y0 = points.bottom_right.y - (TIME_PADDING_BOTTOM + tw[1]) 151 | txt_draw.text((x0, y0), text, font=FONTS['time'], fill=TIME_COLOR) 152 | 153 | 154 | def draw_avatar( 155 | img: Image, draw: ImageDraw.ImageDraw, username: str, points_balloon: Box, avatar_path: str, background_color: str 156 | ): 157 | y0 = points_balloon.bottom_right.y - AVATAR_SIZE 158 | y1 = points_balloon.bottom_right.y 159 | points = Box(MARGIN, y0, MARGIN + AVATAR_SIZE, y1) 160 | box_position = tuple(a - 2 for a in points.top_left.to_tuple()) 161 | size = AVATAR_SIZE + 4 162 | if avatar_path == '': 163 | draw.ellipse(points.to_list(), fill=background_color) 164 | avatar_center = points.center().to_tuple() 165 | draw.text( 166 | avatar_center, username[0], anchor='mm', font=FONTS['avatar'], fill='#FFFFFF', 167 | ) 168 | return 169 | 170 | avatar = Image.open(avatar_path).convert(mode='RGBA') 171 | if avatar.width == avatar.height: 172 | avatar = avatar.resize((size, size), resample=Image.ANTIALIAS) 173 | elif avatar.width > avatar.height: 174 | ratio = size / avatar.width 175 | avatar = avatar.resize((size, int(ratio * avatar.height)), resample=Image.ANTIALIAS) 176 | else: 177 | ratio = size / avatar.height 178 | avatar = avatar.resize((int(ratio * avatar.width), size), resample=Image.ANTIALIAS) 179 | 180 | avatar_mask = generate_avatar_mask(img.size, points) 181 | tmp = Image.new('RGBA', img.size) 182 | tmp.paste(avatar, box=box_position) 183 | img.paste(tmp, mask=avatar_mask) 184 | 185 | 186 | def sticker_from_text(user_id: int, username: str, text: str, avatar_path: str, msg_time: str, other_user_id: int): 187 | ''' 188 | Creates an image from a text message, emulating Telegram's message layout/design. 189 | ''' 190 | size = (512, 256) 191 | transparent = (0, 0, 0, 0) 192 | 193 | username = username if (len(username) < 26) else f'{username[0:25]}...' 194 | limit_is_user = len(username) >= len(text) 195 | aux_img = ImageDraw.Draw(Image.new('RGBA', size, transparent)) 196 | title_size = aux_img.textsize(username, font=FONTS['bold']) 197 | text_size = aux_img.textsize(text, font=FONTS['normal']) 198 | additional_space_for_emojis = 5 * emoji_count(text) 199 | text_size = (text_size[0] + additional_space_for_emojis, text_size[1]) 200 | time_size = aux_img.textsize('04:20', font=FONTS['time']) 201 | final_text = text 202 | 203 | if limit_is_user: 204 | bigger_size = title_size 205 | else: 206 | if len(text) >= LINE_WIDTH_LIMIT: 207 | aux_text = wrapped_text(text, line_width=LINE_WIDTH_LIMIT) 208 | final_text, text_size = try_better_aspect_ratio( 209 | aux_img, original_text=text, modified_text=aux_text 210 | ) 211 | bigger_size = text_size 212 | box_size = ( 213 | bigger_size[0] + 2 * MSG_PADDING_H, 214 | title_size[1] + bigger_size[1] + 2 * MSG_PADDING_V + time_size[1] + 2 * LINE_SPACE, 215 | ) 216 | 217 | b_width = max(BOX_MIN_WIDTH, box_size[0]) 218 | b_height = box_size[1] 219 | img_width = min(512, 2 * MARGIN + AVATAR_SIZE + PADDING + b_width) 220 | size = (img_width, 4 * PADDING + b_height) 221 | 222 | x0 = MARGIN + PADDING + AVATAR_SIZE 223 | x1 = x0 + b_width 224 | y1 = PADDING + b_height 225 | points_balloon = Box(x0, PADDING, x1, y1) 226 | 227 | img = Image.new("RGBA", size, transparent) 228 | dr = ImageDraw.Draw(img) 229 | user_color = get_user_color(other_user_id) 230 | draw_avatar(img, dr, username, points_balloon=points_balloon, avatar_path=avatar_path, background_color=user_color) 231 | 232 | draw_balloon(dr, points=points_balloon, fill=BOX_COLOR) 233 | 234 | draw_username(dr, position=points_balloon.top_left, username=username, fill=user_color) 235 | draw_message(dr, points=points_balloon, text=final_text, user_size=title_size) 236 | draw_time(dr, text=msg_time, points=points_balloon) 237 | 238 | img = resize_to_sticker_limits(img) 239 | img_path = MEDIA_DIR / f'{IMG_PREFIX}{user_id}.png' 240 | img.save(img_path) 241 | img.close() 242 | return img_path 243 | 244 | 245 | def wrapped_text(text: str, line_width=25, max_lines=None): 246 | return '\n'.join(textwrap.wrap(text, width=line_width, max_lines=max_lines)) 247 | 248 | 249 | def try_better_aspect_ratio(img: ImageDraw, original_text: str, modified_text: str): 250 | ''' 251 | If the message text is too long, wrapping it in 25 character lines will result in an image with a big height and small width, making it difficult to read when resized to Telegram's limit of 512px. 252 | So if the wrapped text has a height more than two times its width, we increase the line width limit and re-wrap it until we get an aspect ratio closer to 1:1. 253 | ''' 254 | line_width = LINE_WIDTH_LIMIT 255 | text_size = img.multiline_textsize(modified_text, font=FONTS['normal']) 256 | for _ in range(3): 257 | if text_size[1] > 2 * text_size[0]: 258 | line_width *= 2 259 | modified_text = wrapped_text( 260 | original_text, line_width=line_width, max_lines=MAX_NUMBER_OF_LINES 261 | ) 262 | text_size = img.multiline_textsize(modified_text, font=FONTS['normal']) 263 | else: 264 | break 265 | return modified_text, text_size 266 | 267 | 268 | def sticker_from_image(jpg_path: Path): 269 | ''' 270 | Converts the given image to a proper format that can be uploaded as a sticker. 271 | Telegram accepts PNG images with a maximum size of 512x512 pixels. 272 | ''' 273 | with Image.open(jpg_path) as img: 274 | img = resize_to_sticker_limits(img) 275 | img_path = jpg_path.with_suffix('.png') 276 | img.save(img_path) 277 | return img_path 278 | 279 | 280 | def resize_to_sticker_limits(img: Image): 281 | ''' 282 | Resizes the image to fit Telegram restrictions (maximum size of 512x512 pixels), keeping its aspect ratio. 283 | ''' 284 | if img.width >= img.height: 285 | ratio = 512 / img.width 286 | img = img.resize((512, int(ratio * img.height)), resample=Image.ANTIALIAS) 287 | else: 288 | ratio = 512 / img.height 289 | img = img.resize((int(ratio * img.width), 512), resample=Image.ANTIALIAS) 290 | return img 291 | 292 | 293 | def generate_avatar_mask(img_size: tuple, points: Box): 294 | img = Image.new("RGBA", img_size, (0, 0, 0, 0)) 295 | maskdraw = ImageDraw.Draw(img) 296 | maskdraw.ellipse(points.to_list(), fill='#FFFFFF') 297 | del maskdraw 298 | return img 299 | 300 | 301 | def get_user_color(user_id: int) -> str: 302 | return FOREGROUND_COLORS[user_id % len(FOREGROUND_COLORS)] 303 | -------------------------------------------------------------------------------- /picobot/repository/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caravelahc/pico-bot/38f269f8965add712011f4a50561e477c71f2172/picobot/repository/__init__.py -------------------------------------------------------------------------------- /picobot/repository/repo.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import time 4 | 5 | from .user_entity import UserEntity 6 | 7 | 8 | def repository(database_path: str = None): 9 | if Repo.instance is None: 10 | Repo.instance = Repo(database_path) 11 | return Repo.instance 12 | 13 | 14 | class Repo(object): 15 | instance = None 16 | 17 | def __init__(self, database_path: str = None): 18 | self._db = '' 19 | self._users = {} 20 | self._public_packs = set() 21 | 22 | if database_path is not None: 23 | self._load_db(database_path) 24 | 25 | def users(self): 26 | return self._users 27 | 28 | def packs(self): 29 | return self._public_packs 30 | 31 | def add_pack_to_user(self, user, pack_name: str): 32 | if user.id not in self._users: 33 | self._users[user.id] = UserEntity(user.to_dict()) 34 | 35 | self._users[user.id].packs.add(pack_name) 36 | self._update_db() 37 | 38 | def check_permission(self, user_id: int, pack_name: str): 39 | if pack_name in self._public_packs: 40 | return True 41 | return user_id in self._users and pack_name in self._users[user_id].packs 42 | 43 | def set_pack_public(self, pack_name: str, is_public: bool): 44 | if is_public: 45 | self._public_packs.add(pack_name) 46 | elif pack_name in self._public_packs: 47 | self._public_packs.remove(pack_name) 48 | self._update_db() 49 | 50 | def _load_db(self, db_path: str): 51 | self._db = db_path 52 | if os.path.exists(db_path): 53 | fp = open(db_path, 'rb') 54 | data = pickle.load(fp) 55 | self._users = data['users'] 56 | self._public_packs = data['packs'] 57 | fp.close() 58 | 59 | def _update_db(self): 60 | data = {'users': self._users, 'packs': self._public_packs} 61 | fp = open(self._db, 'wb') 62 | pickle.dump(data, fp) 63 | fp.close() 64 | -------------------------------------------------------------------------------- /picobot/repository/user_entity.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import Optional, Union 3 | 4 | 5 | @dataclass 6 | class UserEntity: 7 | t_user: dict[str, Union[str, int, bool]] 8 | state: str = field(default_factory=str) 9 | packs: set[str] = field(default_factory=set[str]) 10 | def_pack: Optional[str] = None 11 | -------------------------------------------------------------------------------- /picobot/responses.py: -------------------------------------------------------------------------------- 1 | ERROR_MSG = "Algo deu errado. Utilize o comando /help." 2 | 3 | TELEGRAM_ERROR_CODES = { 4 | 'stickerpack_stickers_too_much': 'Este pack atingiu o tamanho limite permitido pelo Telegram.\nCrie um novo pack.', 5 | 'stickers_too_much': 'Este pack atingiu o tamanho limite permitido pelo Telegram.\nCrie um novo pack.', 6 | 'sticker_png_nopng': 'Este pack é de imagens e não suporta este tipo de arquivo.\nPara criar pack de vídeo use\nnewvideopack [nome_do_pack]', 7 | 'sticker_video_nowebm': 'Este pack é de vídeos e não suporta este tipo de arquivo.\nPara criar pack de de imagens use\nnewpack [nome_do_pack]', 8 | } 9 | 10 | INVALID_MSG = "Mensagem inválida!" 11 | 12 | INVALID_DOC = "O arquivo de imagem deve estar em formato PNG \ 13 | com uma camada transparente e caber em um quadrado 512x512 \ 14 | (um dos lados deve ter 512px e o outro 512px ou menos)." 15 | 16 | ERROR_DOWNLOAD_PHOTO = "Não foi possível baixar a foto do usuário." 17 | 18 | FILE_TOO_LARGE = "O arquivo é grande demais" 19 | 20 | VIDEO_TOO_LONG = "O vídeo não pode ter mais de 3 segundos." 21 | 22 | USER_NO_PACK = "Você ainda não tem nenhum pacote de sticker. \ 23 | Por favor, primeiro crie um utilizando o comando /newpack para criar um novo pacote de stickers." 24 | 25 | ADDED_STICKER = 'Sticker adicionado!' 26 | 27 | REMOVED_STICKER = 'Sticker excluído!' 28 | 29 | PACK_PRIVACY_UPDATED = 'Configuração de privacidade do pack atualizada.' 30 | 31 | REMOVE_STICKER_HELP = """Comando inválido. 32 | - Responda a um sticker que você possui com o comando /delsticker 33 | - Ou use /delsticker 34 | X: posição do sticker no pack, sendo 0 (zero) o primeiro 35 | """ 36 | 37 | GREETING = """Olá! Bot para automatizar a criação de stickers \ 38 | a partir de imagens ou mensagens. 39 | Utilize o comando /help para ver os comandos disponíveis. 40 | 41 | Código disponível no repositório: 42 | http://github.com/caravelahc/pico-bot 43 | 44 | Feito por @diogojs e @caravelahc""" 45 | 46 | CREATOR_ACCESS_DENIED = """Username is not in the sudoers file. 47 | This incident will be reported.""" 48 | 49 | NO_PERMISSION = """Você não tem permissão para editar este pack. 50 | Certifique-se de que você é dono do pack, \ 51 | ou que o mesmo é público.""" 52 | 53 | HELP_MSG = """Comandos: 54 | /newpack - cria um novo pack/pacote 55 | /addsticker [NomeDoPack] : 56 | Adiciona sticker a um pack, com o respectivo emoji. \ 57 | Envie esse comando como legenda de uma imagem, \ 58 | em resposta a uma imagem/mensagem para criar um novo sticker, \ 59 | ou como resposta a um sticker existente para apenas adicioná-lo ao pack. 60 | /delsticker - remove o sticker do pack (não recuperável) 61 | /setdefaultpack - configura seu pack padrão 62 | /setpublic - torna seu pack público (qualquer pessoa pode editá-lo, adicionar e remover stickers) 63 | /setprivate - torna seu pack privado para edição (qualquer pessoa ainda pode visualizá-lo e utilizá-lo) 64 | 65 | Para utilizar espaços no nome do pacote, escreva-o entre aspas simples ou duplas.""" 66 | -------------------------------------------------------------------------------- /picobot/video_editor.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import NamedTuple 3 | import ffmpeg 4 | 5 | class NoVideoStreamError(Exception): 6 | pass 7 | 8 | class VideoTooLongError(Exception): 9 | pass 10 | 11 | 12 | def sticker_from_video(video_path: Path, circle_mask: Path = None) -> Path: 13 | ''' 14 | Converts the given video to a proper format that can be uploaded as a sticker. 15 | Telegram accepts WEBM VP9 with a maximum size of 512x512 pixels with maximum 3 seconds duration. 16 | ''' 17 | probe = ffmpeg.probe(video_path) 18 | video_stream = next( 19 | (stream for stream in probe['streams'] if stream['codec_type'] == 'video'), None 20 | ) 21 | if video_stream is None: 22 | raise NoVideoStreamError('Video stream not found') 23 | 24 | width = int(video_stream['width']) 25 | height = int(video_stream['height']) 26 | duration = float(probe['format']['duration']) 27 | 28 | if duration > 3.0: 29 | raise VideoTooLongError('Video duration exceeds limits') 30 | 31 | width, height = estimate_video_sticker_size(VideoSize(width, height)) 32 | vid_output_path = video_path.with_suffix('.webm') 33 | in_file = ffmpeg.input(video_path).filter('scale', width, height) 34 | if circle_mask is None: 35 | in_file.output( 36 | filename=vid_output_path.as_posix(), 37 | format='webm', 38 | vcodec='libvpx-vp9', 39 | pix_fmt='yuva420p', 40 | ).run(overwrite_output=True) 41 | else: 42 | mask = ffmpeg.input(circle_mask).filter('alphaextract') 43 | ffmpeg.filter((in_file, mask), 'alphamerge',).output( 44 | filename=vid_output_path.as_posix(), 45 | format='webm', 46 | vcodec='libvpx-vp9', 47 | pix_fmt='yuva420p', 48 | ).run(overwrite_output=True) 49 | return vid_output_path 50 | 51 | class VideoSize(NamedTuple): 52 | width: int 53 | height: int 54 | 55 | def estimate_video_sticker_size(candidate_size: VideoSize) -> VideoSize: 56 | ''' 57 | Estimates the frame size to fit Telegram restrictions (maximum size of 512x512 pixels), keeping its aspect ratio. 58 | ''' 59 | if candidate_size.width >= candidate_size.height: 60 | ratio = 512 / candidate_size.width 61 | return VideoSize(512, int(ratio * candidate_size.height)) 62 | else: 63 | ratio = 512 / candidate_size.height 64 | return VideoSize(int(ratio * candidate_size.width), 512) 65 | -------------------------------------------------------------------------------- /picobot/videos/caravela.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caravelahc/pico-bot/38f269f8965add712011f4a50561e477c71f2172/picobot/videos/caravela.webm -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "alembic" 5 | version = "1.13.2" 6 | description = "A database migration tool for SQLAlchemy." 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "alembic-1.13.2-py3-none-any.whl", hash = "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953"}, 11 | {file = "alembic-1.13.2.tar.gz", hash = "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef"}, 12 | ] 13 | 14 | [package.dependencies] 15 | Mako = "*" 16 | SQLAlchemy = ">=1.3.0" 17 | typing-extensions = ">=4" 18 | 19 | [package.extras] 20 | tz = ["backports.zoneinfo"] 21 | 22 | [[package]] 23 | name = "apscheduler" 24 | version = "3.6.3" 25 | description = "In-process task scheduler with Cron-like capabilities" 26 | optional = false 27 | python-versions = "*" 28 | files = [ 29 | {file = "APScheduler-3.6.3-py2.py3-none-any.whl", hash = "sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526"}, 30 | {file = "APScheduler-3.6.3.tar.gz", hash = "sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244"}, 31 | ] 32 | 33 | [package.dependencies] 34 | pytz = "*" 35 | setuptools = ">=0.7" 36 | six = ">=1.4.0" 37 | tzlocal = ">=1.2" 38 | 39 | [package.extras] 40 | asyncio = ["trollius"] 41 | doc = ["sphinx", "sphinx-rtd-theme"] 42 | gevent = ["gevent"] 43 | mongodb = ["pymongo (>=2.8)"] 44 | redis = ["redis (>=3.0)"] 45 | rethinkdb = ["rethinkdb (>=2.4.0)"] 46 | sqlalchemy = ["sqlalchemy (>=0.8)"] 47 | testing = ["mock", "pytest", "pytest-asyncio", "pytest-asyncio (<0.6)", "pytest-cov", "pytest-tornado5"] 48 | tornado = ["tornado (>=4.3)"] 49 | twisted = ["twisted"] 50 | zookeeper = ["kazoo"] 51 | 52 | [[package]] 53 | name = "banal" 54 | version = "1.0.6" 55 | description = "Commons of banal micro-functions for Python." 56 | optional = false 57 | python-versions = "*" 58 | files = [ 59 | {file = "banal-1.0.6-py2.py3-none-any.whl", hash = "sha256:877aacb16b17f8fa4fd29a7c44515c5a23dc1a7b26078bc41dd34829117d85e1"}, 60 | {file = "banal-1.0.6.tar.gz", hash = "sha256:2fe02c9305f53168441948f4a03dfbfa2eacc73db30db4a93309083cb0e250a5"}, 61 | ] 62 | 63 | [package.extras] 64 | dev = ["mypy", "wheel"] 65 | 66 | [[package]] 67 | name = "black" 68 | version = "24.4.2" 69 | description = "The uncompromising code formatter." 70 | optional = false 71 | python-versions = ">=3.8" 72 | files = [ 73 | {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, 74 | {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, 75 | {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, 76 | {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, 77 | {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, 78 | {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, 79 | {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, 80 | {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, 81 | {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, 82 | {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, 83 | {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, 84 | {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, 85 | {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, 86 | {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, 87 | {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, 88 | {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, 89 | {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, 90 | {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, 91 | {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, 92 | {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, 93 | {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, 94 | {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, 95 | ] 96 | 97 | [package.dependencies] 98 | click = ">=8.0.0" 99 | mypy-extensions = ">=0.4.3" 100 | packaging = ">=22.0" 101 | pathspec = ">=0.9.0" 102 | platformdirs = ">=2" 103 | 104 | [package.extras] 105 | colorama = ["colorama (>=0.4.3)"] 106 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 107 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 108 | uvloop = ["uvloop (>=0.15.2)"] 109 | 110 | [[package]] 111 | name = "cachetools" 112 | version = "4.2.2" 113 | description = "Extensible memoizing collections and decorators" 114 | optional = false 115 | python-versions = "~=3.5" 116 | files = [ 117 | {file = "cachetools-4.2.2-py3-none-any.whl", hash = "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001"}, 118 | {file = "cachetools-4.2.2.tar.gz", hash = "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"}, 119 | ] 120 | 121 | [[package]] 122 | name = "certifi" 123 | version = "2024.7.4" 124 | description = "Python package for providing Mozilla's CA Bundle." 125 | optional = false 126 | python-versions = ">=3.6" 127 | files = [ 128 | {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, 129 | {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, 130 | ] 131 | 132 | [[package]] 133 | name = "click" 134 | version = "8.1.7" 135 | description = "Composable command line interface toolkit" 136 | optional = false 137 | python-versions = ">=3.7" 138 | files = [ 139 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 140 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 141 | ] 142 | 143 | [package.dependencies] 144 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 145 | 146 | [[package]] 147 | name = "colorama" 148 | version = "0.4.6" 149 | description = "Cross-platform colored terminal text." 150 | optional = false 151 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 152 | files = [ 153 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 154 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 155 | ] 156 | 157 | [[package]] 158 | name = "dataset" 159 | version = "1.6.2" 160 | description = "Toolkit for Python-based database access." 161 | optional = false 162 | python-versions = "*" 163 | files = [ 164 | {file = "dataset-1.6.2-py2.py3-none-any.whl", hash = "sha256:dcca9ba7658473d3082b1adf87a650252a1cd665705b73fa7d4ee32116a107b9"}, 165 | {file = "dataset-1.6.2.tar.gz", hash = "sha256:77d362118f67a8cbb4848dbd30ab362b9fa7cfebdbfaf426c9c500cb38969a99"}, 166 | ] 167 | 168 | [package.dependencies] 169 | alembic = ">=0.6.2" 170 | banal = ">=1.0.1" 171 | sqlalchemy = ">=1.3.2,<2.0.0" 172 | 173 | [package.extras] 174 | dev = ["PyMySQL", "coverage", "cryptography", "flake8", "pip", "psycopg2-binary", "pytest", "wheel"] 175 | 176 | [[package]] 177 | name = "emoji" 178 | version = "0.6.0" 179 | description = "Emoji for Python" 180 | optional = false 181 | python-versions = "*" 182 | files = [ 183 | {file = "emoji-0.6.0.tar.gz", hash = "sha256:e42da4f8d648f8ef10691bc246f682a1ec6b18373abfd9be10ec0b398823bd11"}, 184 | ] 185 | 186 | [package.extras] 187 | dev = ["coverage", "coveralls", "pytest"] 188 | 189 | [[package]] 190 | name = "ffmpeg-python" 191 | version = "0.2.0" 192 | description = "Python bindings for FFmpeg - with complex filtering support" 193 | optional = false 194 | python-versions = "*" 195 | files = [ 196 | {file = "ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127"}, 197 | {file = "ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5"}, 198 | ] 199 | 200 | [package.dependencies] 201 | future = "*" 202 | 203 | [package.extras] 204 | dev = ["Sphinx (==2.1.0)", "future (==0.17.1)", "numpy (==1.16.4)", "pytest (==4.6.1)", "pytest-mock (==1.10.4)", "tox (==3.12.1)"] 205 | 206 | [[package]] 207 | name = "flake8" 208 | version = "7.1.0" 209 | description = "the modular source code checker: pep8 pyflakes and co" 210 | optional = false 211 | python-versions = ">=3.8.1" 212 | files = [ 213 | {file = "flake8-7.1.0-py2.py3-none-any.whl", hash = "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a"}, 214 | {file = "flake8-7.1.0.tar.gz", hash = "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5"}, 215 | ] 216 | 217 | [package.dependencies] 218 | mccabe = ">=0.7.0,<0.8.0" 219 | pycodestyle = ">=2.12.0,<2.13.0" 220 | pyflakes = ">=3.2.0,<3.3.0" 221 | 222 | [[package]] 223 | name = "future" 224 | version = "1.0.0" 225 | description = "Clean single-source support for Python 3 and 2" 226 | optional = false 227 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 228 | files = [ 229 | {file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"}, 230 | {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"}, 231 | ] 232 | 233 | [[package]] 234 | name = "greenlet" 235 | version = "3.0.3" 236 | description = "Lightweight in-process concurrent programming" 237 | optional = false 238 | python-versions = ">=3.7" 239 | files = [ 240 | {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, 241 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, 242 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, 243 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, 244 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, 245 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, 246 | {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, 247 | {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, 248 | {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, 249 | {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, 250 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, 251 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, 252 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, 253 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, 254 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, 255 | {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, 256 | {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, 257 | {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, 258 | {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, 259 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, 260 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, 261 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, 262 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, 263 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, 264 | {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, 265 | {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, 266 | {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, 267 | {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, 268 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, 269 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, 270 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, 271 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, 272 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, 273 | {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, 274 | {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, 275 | {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, 276 | {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, 277 | {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, 278 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, 279 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, 280 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, 281 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, 282 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, 283 | {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, 284 | {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, 285 | {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, 286 | {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, 287 | {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, 288 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, 289 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, 290 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, 291 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, 292 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, 293 | {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, 294 | {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, 295 | {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, 296 | {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, 297 | {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, 298 | ] 299 | 300 | [package.extras] 301 | docs = ["Sphinx", "furo"] 302 | test = ["objgraph", "psutil"] 303 | 304 | [[package]] 305 | name = "iniconfig" 306 | version = "2.0.0" 307 | description = "brain-dead simple config-ini parsing" 308 | optional = false 309 | python-versions = ">=3.7" 310 | files = [ 311 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 312 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 313 | ] 314 | 315 | [[package]] 316 | name = "isort" 317 | version = "5.13.2" 318 | description = "A Python utility / library to sort Python imports." 319 | optional = false 320 | python-versions = ">=3.8.0" 321 | files = [ 322 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 323 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 324 | ] 325 | 326 | [package.extras] 327 | colors = ["colorama (>=0.4.6)"] 328 | 329 | [[package]] 330 | name = "mako" 331 | version = "1.3.5" 332 | description = "A super-fast templating language that borrows the best ideas from the existing templating languages." 333 | optional = false 334 | python-versions = ">=3.8" 335 | files = [ 336 | {file = "Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a"}, 337 | {file = "Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"}, 338 | ] 339 | 340 | [package.dependencies] 341 | MarkupSafe = ">=0.9.2" 342 | 343 | [package.extras] 344 | babel = ["Babel"] 345 | lingua = ["lingua"] 346 | testing = ["pytest"] 347 | 348 | [[package]] 349 | name = "markupsafe" 350 | version = "2.1.5" 351 | description = "Safely add untrusted strings to HTML/XML markup." 352 | optional = false 353 | python-versions = ">=3.7" 354 | files = [ 355 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, 356 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, 357 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, 358 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, 359 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, 360 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, 361 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, 362 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, 363 | {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, 364 | {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, 365 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, 366 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, 367 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, 368 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, 369 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, 370 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, 371 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, 372 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, 373 | {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, 374 | {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, 375 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, 376 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, 377 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, 378 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, 379 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, 380 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, 381 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, 382 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, 383 | {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, 384 | {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, 385 | {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, 386 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, 387 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, 388 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, 389 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, 390 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, 391 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, 392 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, 393 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, 394 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, 395 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, 396 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, 397 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, 398 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, 399 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, 400 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, 401 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, 402 | {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, 403 | {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, 404 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, 405 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, 406 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, 407 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, 408 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, 409 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, 410 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, 411 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, 412 | {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, 413 | {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, 414 | {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, 415 | ] 416 | 417 | [[package]] 418 | name = "mccabe" 419 | version = "0.7.0" 420 | description = "McCabe checker, plugin for flake8" 421 | optional = false 422 | python-versions = ">=3.6" 423 | files = [ 424 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 425 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 426 | ] 427 | 428 | [[package]] 429 | name = "mypy-extensions" 430 | version = "1.0.0" 431 | description = "Type system extensions for programs checked with the mypy type checker." 432 | optional = false 433 | python-versions = ">=3.5" 434 | files = [ 435 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 436 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 437 | ] 438 | 439 | [[package]] 440 | name = "packaging" 441 | version = "24.1" 442 | description = "Core utilities for Python packages" 443 | optional = false 444 | python-versions = ">=3.8" 445 | files = [ 446 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 447 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 448 | ] 449 | 450 | [[package]] 451 | name = "pathspec" 452 | version = "0.12.1" 453 | description = "Utility library for gitignore style pattern matching of file paths." 454 | optional = false 455 | python-versions = ">=3.8" 456 | files = [ 457 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 458 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 459 | ] 460 | 461 | [[package]] 462 | name = "pillow" 463 | version = "10.3.0" 464 | description = "Python Imaging Library (Fork)" 465 | optional = false 466 | python-versions = ">=3.8" 467 | files = [ 468 | {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, 469 | {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, 470 | {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, 471 | {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, 472 | {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, 473 | {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, 474 | {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, 475 | {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, 476 | {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, 477 | {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, 478 | {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, 479 | {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, 480 | {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, 481 | {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, 482 | {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, 483 | {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, 484 | {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, 485 | {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, 486 | {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, 487 | {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, 488 | {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, 489 | {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, 490 | {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, 491 | {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, 492 | {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, 493 | {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, 494 | {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, 495 | {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, 496 | {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, 497 | {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, 498 | {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, 499 | {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, 500 | {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, 501 | {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, 502 | {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, 503 | {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, 504 | {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, 505 | {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, 506 | {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, 507 | {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, 508 | {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, 509 | {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, 510 | {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, 511 | {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, 512 | {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, 513 | {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, 514 | {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, 515 | {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, 516 | {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, 517 | {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, 518 | {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, 519 | {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, 520 | {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, 521 | {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, 522 | {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, 523 | {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, 524 | {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, 525 | {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, 526 | {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, 527 | {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, 528 | {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, 529 | {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, 530 | {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, 531 | {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, 532 | {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, 533 | {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, 534 | {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, 535 | {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, 536 | {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, 537 | ] 538 | 539 | [package.extras] 540 | docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] 541 | fpx = ["olefile"] 542 | mic = ["olefile"] 543 | tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] 544 | typing = ["typing-extensions"] 545 | xmp = ["defusedxml"] 546 | 547 | [[package]] 548 | name = "platformdirs" 549 | version = "4.2.2" 550 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 551 | optional = false 552 | python-versions = ">=3.8" 553 | files = [ 554 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 555 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 556 | ] 557 | 558 | [package.extras] 559 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 560 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 561 | type = ["mypy (>=1.8)"] 562 | 563 | [[package]] 564 | name = "pluggy" 565 | version = "1.5.0" 566 | description = "plugin and hook calling mechanisms for python" 567 | optional = false 568 | python-versions = ">=3.8" 569 | files = [ 570 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 571 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 572 | ] 573 | 574 | [package.extras] 575 | dev = ["pre-commit", "tox"] 576 | testing = ["pytest", "pytest-benchmark"] 577 | 578 | [[package]] 579 | name = "pycodestyle" 580 | version = "2.12.0" 581 | description = "Python style guide checker" 582 | optional = false 583 | python-versions = ">=3.8" 584 | files = [ 585 | {file = "pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"}, 586 | {file = "pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c"}, 587 | ] 588 | 589 | [[package]] 590 | name = "pyflakes" 591 | version = "3.2.0" 592 | description = "passive checker of Python programs" 593 | optional = false 594 | python-versions = ">=3.8" 595 | files = [ 596 | {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, 597 | {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, 598 | ] 599 | 600 | [[package]] 601 | name = "pytest" 602 | version = "8.3.1" 603 | description = "pytest: simple powerful testing with Python" 604 | optional = false 605 | python-versions = ">=3.8" 606 | files = [ 607 | {file = "pytest-8.3.1-py3-none-any.whl", hash = "sha256:e9600ccf4f563976e2c99fa02c7624ab938296551f280835ee6516df8bc4ae8c"}, 608 | {file = "pytest-8.3.1.tar.gz", hash = "sha256:7e8e5c5abd6e93cb1cc151f23e57adc31fcf8cfd2a3ff2da63e23f732de35db6"}, 609 | ] 610 | 611 | [package.dependencies] 612 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 613 | iniconfig = "*" 614 | packaging = "*" 615 | pluggy = ">=1.5,<2" 616 | 617 | [package.extras] 618 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 619 | 620 | [[package]] 621 | name = "python-slugify" 622 | version = "4.0.1" 623 | description = "A Python Slugify application that handles Unicode" 624 | optional = false 625 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 626 | files = [ 627 | {file = "python-slugify-4.0.1.tar.gz", hash = "sha256:69a517766e00c1268e5bbfc0d010a0a8508de0b18d30ad5a1ff357f8ae724270"}, 628 | ] 629 | 630 | [package.dependencies] 631 | text-unidecode = ">=1.3" 632 | 633 | [package.extras] 634 | unidecode = ["Unidecode (>=1.1.1)"] 635 | 636 | [[package]] 637 | name = "python-telegram-bot" 638 | version = "13.15" 639 | description = "We have made you a wrapper you can't refuse" 640 | optional = false 641 | python-versions = ">=3.7" 642 | files = [ 643 | {file = "python-telegram-bot-13.15.tar.gz", hash = "sha256:b4047606b8081b62bbd6aa361f7ca1efe87fa8f1881ec9d932d35844bf57a154"}, 644 | {file = "python_telegram_bot-13.15-py3-none-any.whl", hash = "sha256:06780c258d3f2a3c6c79a7aeb45714f4cd1dd6275941b7dc4628bba64fddd465"}, 645 | ] 646 | 647 | [package.dependencies] 648 | APScheduler = "3.6.3" 649 | cachetools = "4.2.2" 650 | certifi = "*" 651 | pytz = ">=2018.6" 652 | tornado = "6.1" 653 | 654 | [package.extras] 655 | json = ["ujson"] 656 | passport = ["cryptography (!=3.4,!=3.4.1,!=3.4.2,!=3.4.3)"] 657 | socks = ["PySocks"] 658 | 659 | [[package]] 660 | name = "pytz" 661 | version = "2024.1" 662 | description = "World timezone definitions, modern and historical" 663 | optional = false 664 | python-versions = "*" 665 | files = [ 666 | {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, 667 | {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, 668 | ] 669 | 670 | [[package]] 671 | name = "setuptools" 672 | version = "71.0.4" 673 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 674 | optional = false 675 | python-versions = ">=3.8" 676 | files = [ 677 | {file = "setuptools-71.0.4-py3-none-any.whl", hash = "sha256:ed2feca703be3bdbd94e6bb17365d91c6935c6b2a8d0bb09b66a2c435ba0b1a5"}, 678 | {file = "setuptools-71.0.4.tar.gz", hash = "sha256:48297e5d393a62b7cb2a10b8f76c63a73af933bd809c9e0d0d6352a1a0135dd8"}, 679 | ] 680 | 681 | [package.extras] 682 | core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] 683 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 684 | test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 685 | 686 | [[package]] 687 | name = "six" 688 | version = "1.16.0" 689 | description = "Python 2 and 3 compatibility utilities" 690 | optional = false 691 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 692 | files = [ 693 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 694 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 695 | ] 696 | 697 | [[package]] 698 | name = "sqlalchemy" 699 | version = "1.4.52" 700 | description = "Database Abstraction Library" 701 | optional = false 702 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" 703 | files = [ 704 | {file = "SQLAlchemy-1.4.52-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f68016f9a5713684c1507cc37133c28035f29925c75c0df2f9d0f7571e23720a"}, 705 | {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24bb0f81fbbb13d737b7f76d1821ec0b117ce8cbb8ee5e8641ad2de41aa916d3"}, 706 | {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e93983cc0d2edae253b3f2141b0a3fb07e41c76cd79c2ad743fc27eb79c3f6db"}, 707 | {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:84e10772cfc333eb08d0b7ef808cd76e4a9a30a725fb62a0495877a57ee41d81"}, 708 | {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:427988398d2902de042093d17f2b9619a5ebc605bf6372f7d70e29bde6736842"}, 709 | {file = "SQLAlchemy-1.4.52-cp310-cp310-win32.whl", hash = "sha256:1296f2cdd6db09b98ceb3c93025f0da4835303b8ac46c15c2136e27ee4d18d94"}, 710 | {file = "SQLAlchemy-1.4.52-cp310-cp310-win_amd64.whl", hash = "sha256:80e7f697bccc56ac6eac9e2df5c98b47de57e7006d2e46e1a3c17c546254f6ef"}, 711 | {file = "SQLAlchemy-1.4.52-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2f251af4c75a675ea42766880ff430ac33291c8d0057acca79710f9e5a77383d"}, 712 | {file = "SQLAlchemy-1.4.52-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb8f9e4c4718f111d7b530c4e6fb4d28f9f110eb82e7961412955b3875b66de0"}, 713 | {file = "SQLAlchemy-1.4.52-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afb1672b57f58c0318ad2cff80b384e816735ffc7e848d8aa51e0b0fc2f4b7bb"}, 714 | {file = "SQLAlchemy-1.4.52-cp311-cp311-win32.whl", hash = "sha256:6e41cb5cda641f3754568d2ed8962f772a7f2b59403b95c60c89f3e0bd25f15e"}, 715 | {file = "SQLAlchemy-1.4.52-cp311-cp311-win_amd64.whl", hash = "sha256:5bed4f8c3b69779de9d99eb03fd9ab67a850d74ab0243d1be9d4080e77b6af12"}, 716 | {file = "SQLAlchemy-1.4.52-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:49e3772eb3380ac88d35495843daf3c03f094b713e66c7d017e322144a5c6b7c"}, 717 | {file = "SQLAlchemy-1.4.52-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:618827c1a1c243d2540314c6e100aee7af09a709bd005bae971686fab6723554"}, 718 | {file = "SQLAlchemy-1.4.52-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de9acf369aaadb71a725b7e83a5ef40ca3de1cf4cdc93fa847df6b12d3cd924b"}, 719 | {file = "SQLAlchemy-1.4.52-cp312-cp312-win32.whl", hash = "sha256:763bd97c4ebc74136ecf3526b34808c58945023a59927b416acebcd68d1fc126"}, 720 | {file = "SQLAlchemy-1.4.52-cp312-cp312-win_amd64.whl", hash = "sha256:f12aaf94f4d9679ca475975578739e12cc5b461172e04d66f7a3c39dd14ffc64"}, 721 | {file = "SQLAlchemy-1.4.52-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:853fcfd1f54224ea7aabcf34b227d2b64a08cbac116ecf376907968b29b8e763"}, 722 | {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f98dbb8fcc6d1c03ae8ec735d3c62110949a3b8bc6e215053aa27096857afb45"}, 723 | {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e135fff2e84103bc15c07edd8569612ce317d64bdb391f49ce57124a73f45c5"}, 724 | {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b5de6af8852500d01398f5047d62ca3431d1e29a331d0b56c3e14cb03f8094c"}, 725 | {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3491c85df263a5c2157c594f54a1a9c72265b75d3777e61ee13c556d9e43ffc9"}, 726 | {file = "SQLAlchemy-1.4.52-cp36-cp36m-win32.whl", hash = "sha256:427c282dd0deba1f07bcbf499cbcc9fe9a626743f5d4989bfdfd3ed3513003dd"}, 727 | {file = "SQLAlchemy-1.4.52-cp36-cp36m-win_amd64.whl", hash = "sha256:ca5ce82b11731492204cff8845c5e8ca1a4bd1ade85e3b8fcf86e7601bfc6a39"}, 728 | {file = "SQLAlchemy-1.4.52-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:29d4247313abb2015f8979137fe65f4eaceead5247d39603cc4b4a610936cd2b"}, 729 | {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a752bff4796bf22803d052d4841ebc3c55c26fb65551f2c96e90ac7c62be763a"}, 730 | {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7ea11727feb2861deaa293c7971a4df57ef1c90e42cb53f0da40c3468388000"}, 731 | {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d913f8953e098ca931ad7f58797f91deed26b435ec3756478b75c608aa80d139"}, 732 | {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a251146b921725547ea1735b060a11e1be705017b568c9f8067ca61e6ef85f20"}, 733 | {file = "SQLAlchemy-1.4.52-cp37-cp37m-win32.whl", hash = "sha256:1f8e1c6a6b7f8e9407ad9afc0ea41c1f65225ce505b79bc0342159de9c890782"}, 734 | {file = "SQLAlchemy-1.4.52-cp37-cp37m-win_amd64.whl", hash = "sha256:346ed50cb2c30f5d7a03d888e25744154ceac6f0e6e1ab3bc7b5b77138d37710"}, 735 | {file = "SQLAlchemy-1.4.52-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:4dae6001457d4497736e3bc422165f107ecdd70b0d651fab7f731276e8b9e12d"}, 736 | {file = "SQLAlchemy-1.4.52-cp38-cp38-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5d2e08d79f5bf250afb4a61426b41026e448da446b55e4770c2afdc1e200fce"}, 737 | {file = "SQLAlchemy-1.4.52-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bbce5dd7c7735e01d24f5a60177f3e589078f83c8a29e124a6521b76d825b85"}, 738 | {file = "SQLAlchemy-1.4.52-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bdb7b4d889631a3b2a81a3347c4c3f031812eb4adeaa3ee4e6b0d028ad1852b5"}, 739 | {file = "SQLAlchemy-1.4.52-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c294ae4e6bbd060dd79e2bd5bba8b6274d08ffd65b58d106394cb6abbf35cf45"}, 740 | {file = "SQLAlchemy-1.4.52-cp38-cp38-win32.whl", hash = "sha256:bcdfb4b47fe04967669874fb1ce782a006756fdbebe7263f6a000e1db969120e"}, 741 | {file = "SQLAlchemy-1.4.52-cp38-cp38-win_amd64.whl", hash = "sha256:7d0dbc56cb6af5088f3658982d3d8c1d6a82691f31f7b0da682c7b98fa914e91"}, 742 | {file = "SQLAlchemy-1.4.52-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:a551d5f3dc63f096ed41775ceec72fdf91462bb95abdc179010dc95a93957800"}, 743 | {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab773f9ad848118df7a9bbabca53e3f1002387cdbb6ee81693db808b82aaab0"}, 744 | {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2de46f5d5396d5331127cfa71f837cca945f9a2b04f7cb5a01949cf676db7d1"}, 745 | {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7027be7930a90d18a386b25ee8af30514c61f3852c7268899f23fdfbd3107181"}, 746 | {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99224d621affbb3c1a4f72b631f8393045f4ce647dd3262f12fe3576918f8bf3"}, 747 | {file = "SQLAlchemy-1.4.52-cp39-cp39-win32.whl", hash = "sha256:c124912fd4e1bb9d1e7dc193ed482a9f812769cb1e69363ab68e01801e859821"}, 748 | {file = "SQLAlchemy-1.4.52-cp39-cp39-win_amd64.whl", hash = "sha256:2c286fab42e49db23c46ab02479f328b8bdb837d3e281cae546cc4085c83b680"}, 749 | {file = "SQLAlchemy-1.4.52.tar.gz", hash = "sha256:80e63bbdc5217dad3485059bdf6f65a7d43f33c8bde619df5c220edf03d87296"}, 750 | ] 751 | 752 | [package.dependencies] 753 | greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} 754 | 755 | [package.extras] 756 | aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] 757 | aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] 758 | asyncio = ["greenlet (!=0.4.17)"] 759 | asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] 760 | mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] 761 | mssql = ["pyodbc"] 762 | mssql-pymssql = ["pymssql"] 763 | mssql-pyodbc = ["pyodbc"] 764 | mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] 765 | mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] 766 | mysql-connector = ["mysql-connector-python"] 767 | oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"] 768 | postgresql = ["psycopg2 (>=2.7)"] 769 | postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] 770 | postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] 771 | postgresql-psycopg2binary = ["psycopg2-binary"] 772 | postgresql-psycopg2cffi = ["psycopg2cffi"] 773 | pymysql = ["pymysql", "pymysql (<1)"] 774 | sqlcipher = ["sqlcipher3_binary"] 775 | 776 | [[package]] 777 | name = "text-unidecode" 778 | version = "1.3" 779 | description = "The most basic Text::Unidecode port" 780 | optional = false 781 | python-versions = "*" 782 | files = [ 783 | {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, 784 | {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, 785 | ] 786 | 787 | [[package]] 788 | name = "tornado" 789 | version = "6.1" 790 | description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." 791 | optional = false 792 | python-versions = ">= 3.5" 793 | files = [ 794 | {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, 795 | {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, 796 | {file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"}, 797 | {file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"}, 798 | {file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"}, 799 | {file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"}, 800 | {file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"}, 801 | {file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"}, 802 | {file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"}, 803 | {file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"}, 804 | {file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"}, 805 | {file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"}, 806 | {file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"}, 807 | {file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"}, 808 | {file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"}, 809 | {file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"}, 810 | {file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"}, 811 | {file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"}, 812 | {file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"}, 813 | {file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"}, 814 | {file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"}, 815 | {file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"}, 816 | {file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"}, 817 | {file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"}, 818 | {file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"}, 819 | {file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"}, 820 | {file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"}, 821 | {file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"}, 822 | {file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"}, 823 | {file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"}, 824 | {file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"}, 825 | {file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"}, 826 | {file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"}, 827 | {file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"}, 828 | {file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"}, 829 | {file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"}, 830 | {file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"}, 831 | {file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"}, 832 | {file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"}, 833 | {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"}, 834 | {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, 835 | ] 836 | 837 | [[package]] 838 | name = "typed-ast" 839 | version = "1.5.5" 840 | description = "a fork of Python 2 and 3 ast modules with type comment support" 841 | optional = false 842 | python-versions = ">=3.6" 843 | files = [ 844 | {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, 845 | {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, 846 | {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, 847 | {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"}, 848 | {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"}, 849 | {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"}, 850 | {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"}, 851 | {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"}, 852 | {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"}, 853 | {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"}, 854 | {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"}, 855 | {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"}, 856 | {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"}, 857 | {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"}, 858 | {file = "typed_ast-1.5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5"}, 859 | {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c"}, 860 | {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa"}, 861 | {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f"}, 862 | {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d"}, 863 | {file = "typed_ast-1.5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5"}, 864 | {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"}, 865 | {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"}, 866 | {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"}, 867 | {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"}, 868 | {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"}, 869 | {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"}, 870 | {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"}, 871 | {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"}, 872 | {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"}, 873 | {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"}, 874 | {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"}, 875 | {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"}, 876 | {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"}, 877 | {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"}, 878 | {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"}, 879 | {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"}, 880 | {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"}, 881 | {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"}, 882 | {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"}, 883 | {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, 884 | {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, 885 | ] 886 | 887 | [[package]] 888 | name = "typing-extensions" 889 | version = "4.12.2" 890 | description = "Backported and Experimental Type Hints for Python 3.8+" 891 | optional = false 892 | python-versions = ">=3.8" 893 | files = [ 894 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 895 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 896 | ] 897 | 898 | [[package]] 899 | name = "tzdata" 900 | version = "2024.1" 901 | description = "Provider of IANA time zone data" 902 | optional = false 903 | python-versions = ">=2" 904 | files = [ 905 | {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, 906 | {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, 907 | ] 908 | 909 | [[package]] 910 | name = "tzlocal" 911 | version = "5.2" 912 | description = "tzinfo object for the local timezone" 913 | optional = false 914 | python-versions = ">=3.8" 915 | files = [ 916 | {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, 917 | {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, 918 | ] 919 | 920 | [package.dependencies] 921 | tzdata = {version = "*", markers = "platform_system == \"Windows\""} 922 | 923 | [package.extras] 924 | devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] 925 | 926 | [metadata] 927 | lock-version = "2.0" 928 | python-versions = "^3.11" 929 | content-hash = "7b01d1ed0a135a4ae390c8939beaabad803e9960f36303db8bdb78248d0eeb91" 930 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "picobot" 3 | version = "0.2.0" 4 | description = "" 5 | authors = ["Arthur Mesquita Pickcius "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.11" 9 | python-telegram-bot = "^13.11" 10 | dataset = "^1.1" 11 | pillow = "^10.3" 12 | python-slugify = "^4.0" 13 | emoji = "^0.6.0" 14 | typed-ast = "^1.5.2" 15 | ffmpeg-python = "^0.2.0" 16 | 17 | [tool.poetry.group.dev.dependencies] 18 | pytest = "^8.1.1" 19 | flake8 = "^7.0.0" 20 | isort = "^5.13.2" 21 | black = "^24.3.0" 22 | 23 | [tool.black] 24 | skip_string_normalization = true 25 | line_length = 100 26 | 27 | [tool.isort] 28 | profile = "hug" 29 | 30 | [build-system] 31 | requires = ["poetry>=0.12"] 32 | build-backend = "poetry.masonry.api" 33 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Pico-Bot 2 | 3 | Bot para automatizar a criação de stickers a partir de imagens ou mensagens. 4 | 5 | Código disponível no repositório: 6 | [http://github.com/caravelahc/pico-bot]() 7 | 8 | Feito por [@diogojs](https://t.me/diogojs) e [@caravelahc](https://t.me/caravelahc). 9 | 10 | 11 | ## A) Como Instalar 12 | 13 | ### A.1) Python 14 | 15 | Tenha certeza que possui o Python com versão maior ou igual à 3.7.0. 16 | 17 | ### A.2) Gerenciador de pacotes `poetry` 18 | 19 | Primeiro de tudo, instale o `poetry`, que é um gerenciador de pacotes e dependências do Python. 20 | 21 | ### A.3) Dependências 22 | 23 | Para instalar as dependências do projeto, utilizamos o gerenciador de dependências `poetry`, executando o comando: 24 | ``` 25 | poetry install 26 | ``` 27 | 28 | Caso possua problemas com a instalação através do `poetry`, é possível criar um ambiente local manualmente, para que possamos instalar os pacotes sem afetar o sistema: 29 | ``` 30 | python3.7 -m venv .venv 31 | source .venv/bin/activate 32 | ``` 33 | Estando dentro do VENV (virtual env), tente rodar novamente `poetry install`, ou instale as dependências listadas no arquivo `pyproject.toml` manualmente, com o comando: 34 | ``` 35 | python3.7 -m pip install 36 | ``` 37 | 38 | 39 | ## B) Como Configurar 40 | 41 | Todas as variáveis que precisam ser configuradas são localizadas no arquivo `config.json.copy`, que deve ser renomeado para `config.json` com o seguinte comando: 42 | ``` 43 | cp config.json.copy config.json 44 | ``` 45 | 46 | ### B.1) "Token" 47 | 48 | Vamos criar o nosso Bot junto ao Telegram para obter o `token` e poder configurar o arquivo `config.json` com o valor retornado. Isso pode ser feito com o usuário do Telegram chamado [@BotFather](https://t.me/BotFather). Para criar e configurar o seu primeiro Bot, [clique aqui](https://telegram.me/BotFather) e siga as instruções dadas pelo Bot ao longo do processo (em inglês). 49 | 50 | ### B.2) "Creator_ID" 51 | 52 | Consulte o seu ID de usuário no Telegram através de um bot como o [@userinfobot](https://t.me/userinfobot), que lhe fornece facilmente o ID (identificador único) da sua conta. 53 | 54 | ### B.3) "DB_Path" 55 | 56 | Pode deixar o valor apenas como `picobot/bot.db`. Não nos é muito interessante agora. 57 | 58 | 59 | ## C) Como Usar 60 | 61 | ### Criar novo pacote de Stickers: 62 | 63 | Cria um novo pack/pacote de stickers, que lhe permitirá adicionar stickers nele e compartilhar para uso/edição dos demais usuários do Telegram. 64 | ``` 65 | /newpack@ 66 | ``` 67 | 68 | ### Adicionar sticker no Pack: 69 | 70 | Adiciona sticker a um pack, com o respectivo emoji. Envie esse comando como legenda de uma imagem, em resposta a uma imagem/mensagem para criar um novo sticker, ou como resposta a um sticker existente para apenas adicioná-lo ao pack. 71 | ``` 72 | /addsticker@ [NomeDoPack] 73 | ``` 74 | 75 | ### Remover sticker do Pack: 76 | 77 | Remove o sticker do pack (não recuperável), sendo que `` é a posição do sticker dentro do pack. Caso `` seja igual à 0 (zero), removerá o primeiro sticker, caso o seu valor seja **1 (um)** removerá o segundo sticker, e assim por diante. 78 | ``` 79 | /delsticker@ [NomeDoPack] 80 | ``` 81 | 82 | ### Configurar pack de stickers padrão: 83 | 84 | Configura seu pack padrão. 85 | ``` 86 | /setdefaultpack@ [NomeDoPack] 87 | ``` 88 | 89 | ### Tornar pack público: 90 | 91 | Torna seu pack público, de forma que qualquer pessoa pode editá-lo (adicionar e remover stickers). 92 | ``` 93 | /setpublic 94 | ``` 95 | 96 | ### Tornar pack privado: 97 | 98 | Torna seu pack privado para edição, de forma que qualquer pessoa possa apenas visualizá-lo e utilizá-lo, sem opção de adicionar ou remover stickers. 99 | ``` 100 | /setprivate 101 | ``` 102 | 103 | 104 | ## D) Outros Comandos Úteis 105 | 106 | ### Logar na instância do GCloud via SSH: 107 | ``` 108 | gcloud compute ssh 109 | ``` 110 | 111 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caravelahc/pico-bot/38f269f8965add712011f4a50561e477c71f2172/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_painter.py: -------------------------------------------------------------------------------- 1 | from picobot.painter import wrapped_text 2 | 3 | 4 | def test_wrapped_text(): 5 | samples_and_expected = [ 6 | ('short message', 'short message'), 7 | ('msg that should have two lines', 'msg that should have two\nlines'), 8 | ( 9 | 'msg with over 50 characters that should have three lines', 10 | 'msg with over 50\ncharacters that should\nhave three lines', 11 | ), 12 | ('ABigWordThatDoesntFitInJustOneLine', 'ABigWordThatDoesntFitInJu\nstOneLine'), 13 | ( 14 | 'msg with four lines that also have ABigWordThatDoesntFitInJustOneLine in the 2nd line', 15 | 'msg with four lines that\nalso have ABigWordThatDoe\nsntFitInJustOneLine in\nthe 2nd line', 16 | ), 17 | ] 18 | for sample, expected in samples_and_expected: 19 | result = wrapped_text(sample) 20 | assert result == expected 21 | -------------------------------------------------------------------------------- /tests/test_picobot.py: -------------------------------------------------------------------------------- 1 | from picobot import __version__ 2 | 3 | 4 | def test_version(): 5 | assert __version__ == '0.1.0' 6 | --------------------------------------------------------------------------------