├── telegram_to_rss ├── models │ ├── __init__.py │ ├── feed.py │ └── feed_entry.py ├── consts.py ├── __main__.py ├── templates │ ├── base.html │ ├── qr_code.html │ └── feeds.html ├── qr_code.py ├── db.py ├── config.py ├── __init__.py ├── client.py ├── server.py ├── generate_feed.py └── poll_telegram.py ├── .pre-commit-config.yaml ├── docker ├── compose.dev.yml ├── compose.yml └── Dockerfile ├── pyproject.toml ├── .github └── workflows │ ├── build-dev.yml │ └── build-tag.yml ├── LICENSE ├── requirements.txt ├── .gitignore ├── README.md └── poetry.lock /telegram_to_rss/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .feed import * 2 | from .feed_entry import * 3 | -------------------------------------------------------------------------------- /telegram_to_rss/consts.py: -------------------------------------------------------------------------------- 1 | TELEGRAM_NOTIFICATIONS_DIALOG_ID = 777000 2 | MESSAGE_FETCH_HARD_LIMIT = 1000 3 | -------------------------------------------------------------------------------- /telegram_to_rss/__main__.py: -------------------------------------------------------------------------------- 1 | from telegram_to_rss import main 2 | 3 | 4 | if __name__ == "__main__": 5 | main() 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/python-poetry/poetry 3 | rev: "1.8.3" 4 | hooks: 5 | - id: poetry-export 6 | args: ["--without-hashes", "-f", "requirements.txt"] 7 | - id: poetry-lock 8 | -------------------------------------------------------------------------------- /telegram_to_rss/models/feed.py: -------------------------------------------------------------------------------- 1 | from tortoise.models import Model 2 | from tortoise import fields 3 | from .feed_entry import FeedEntry 4 | 5 | 6 | class Feed(Model): 7 | id = fields.IntField(primary_key=True) 8 | name = fields.TextField() 9 | last_update = fields.DatetimeField(auto_now=True) 10 | entries: fields.ReverseRelation[FeedEntry] 11 | -------------------------------------------------------------------------------- /telegram_to_rss/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | Telegram to RSS 9 | {% block head %}{% endblock %} 10 | 11 | 12 | {% block content %}{% endblock %} 13 | 14 | 15 | -------------------------------------------------------------------------------- /telegram_to_rss/qr_code.py: -------------------------------------------------------------------------------- 1 | import qrcode 2 | from io import BytesIO 3 | from base64 import b64encode 4 | 5 | 6 | def get_qr_code_image(qr_code: str): 7 | buffer = BytesIO() 8 | img = qrcode.make(qr_code) 9 | img.save(buffer) 10 | encoded_img = b64encode(buffer.getvalue()).decode() 11 | 12 | data_uri = "data:image/png;base64,{}".format(encoded_img) 13 | return data_uri 14 | -------------------------------------------------------------------------------- /docker/compose.dev.yml: -------------------------------------------------------------------------------- 1 | services: 2 | telegram-to-rss: 3 | build: 4 | context: ../ 5 | dockerfile: ./docker/Dockerfile 6 | tags: 7 | - "telegram-to-rss:dev" 8 | container_name: telegram-to-rss 9 | restart: always 10 | env_file: ../.env 11 | ports: 12 | - 3042:3042 13 | volumes: 14 | - data:/data 15 | 16 | volumes: 17 | data: null 18 | 19 | networks: {} -------------------------------------------------------------------------------- /telegram_to_rss/db.py: -------------------------------------------------------------------------------- 1 | from tortoise import Tortoise 2 | 3 | 4 | async def init_feeds_db(db_path: str): 5 | await Tortoise.init( 6 | db_url="sqlite://{}".format(db_path), 7 | modules={"models": ["telegram_to_rss.models"]}, 8 | ) 9 | # Generate the schema 10 | await Tortoise.generate_schemas(safe=True) 11 | 12 | 13 | async def close_feeds_db(): 14 | await Tortoise.close_connections() 15 | -------------------------------------------------------------------------------- /docker/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | telegram-to-rss: 3 | image: aigoncharov/telegram-to-rss:latest 4 | container_name: telegram-to-rss 5 | restart: always 6 | environment: 7 | - TG_API_ID=REPLACE_ME 8 | - TG_API_HASH=REPLACE_ME 9 | - TG_PASSWORD=REPLACE_ME 10 | - BASE_URL=REPLACE_ME 11 | ports: 12 | - 3042:3042 13 | volumes: 14 | - data:/data 15 | 16 | volumes: 17 | data: null 18 | 19 | networks: {} -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim AS builder 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY requirements.txt ./ 6 | 7 | RUN pip install --no-cache-dir --target=/usr/src/app/dependencies -r requirements.txt 8 | 9 | FROM python:3.12-slim 10 | 11 | WORKDIR /usr/src/app 12 | 13 | COPY --from=builder /usr/src/app/dependencies /usr/local/lib/python3.12/site-packages 14 | 15 | COPY telegram_to_rss ./telegram_to_rss 16 | 17 | ENV DATA_DIR=/data 18 | ENV BIND=0.0.0.0:3042 19 | 20 | CMD [ "python", "-m", "telegram_to_rss" ] -------------------------------------------------------------------------------- /telegram_to_rss/templates/qr_code.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block content %} 2 |
3 |

1. In Telegram on your phone go to Settings -> Devices.

4 |

2. Click on "Link Desktop Device" and scan this QR code.

5 |

3. Give it a few seconds before you panic and think it does not work.

6 |

4. If it does not work indeed, check the logs.

7 |

8 | 9 |

10 |
11 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /telegram_to_rss/templates/feeds.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block head %} {% for feed in feeds %} 2 | 8 | {% endfor %} {% endblock %} {% block content %} 9 |
10 |

11 | Logged in as {{ user.first_name }} {{ user.last_name }} ({{ user.username 12 | }}). 13 |

14 |

Give it a few minutes on first start to fetch the data.

15 |

Available feeds:

16 | 28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "telegram-to-rss" 3 | version = "0.3.0" 4 | description = "" 5 | authors = ["aigoncharov"] 6 | license = "MIT" 7 | readme = "README.md" 8 | packages = [{include = "telegram_to_rss"}] 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.12" 12 | Telethon = "^1.36.0" 13 | quart = "^0.19.6" 14 | qrcode = "^7.4.2" 15 | platformdirs = "^4.2.2" 16 | tortoise-orm = "^0.21.3" 17 | anyio = "^4.4.0" 18 | hypercorn = "^0.17.3" 19 | 20 | cryptg = "^0.4.0" 21 | [tool.poetry.group.dev.dependencies] 22 | flake8 = "^7.1.0" 23 | flake8-bugbear = "^24.4.26" 24 | flake8-pyproject = "^1.2.3" 25 | 26 | pre-commit = "^3.7.1" 27 | [tool.flake8] 28 | max-line-length = 88 29 | extend-select = "B950" 30 | extend-ignore = "E203,E501,E701" 31 | 32 | [build-system] 33 | requires = ["poetry-core"] 34 | build-backend = "poetry.core.masonry.api" 35 | 36 | [tool.poetry.scripts] 37 | telegram_to_rss = "telegram_to_rss:main" 38 | -------------------------------------------------------------------------------- /.github/workflows/build-dev.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker image dev 2 | on: 3 | push: 4 | # Run pipeline for commits on branch main 5 | branches: 6 | - "main" 7 | - "!testing/**" 8 | - "!feature/**" 9 | - "!hotfix/**" 10 | 11 | jobs: 12 | build-dev: 13 | runs-on: [ubuntu-22.04] 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Set up QEMU 18 | uses: docker/setup-qemu-action@v3 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v3 21 | - name: Login to Docker Hub 22 | uses: docker/login-action@v3 23 | with: 24 | username: ${{ secrets.DOCKERHUB_USERNAME }} 25 | password: ${{ secrets.DOCKERHUB_TOKEN }} 26 | - name: Build and push dev 27 | uses: docker/build-push-action@v5 28 | with: 29 | context: . 30 | file: docker/Dockerfile 31 | push: true 32 | tags: aigoncharov/telegram-to-rss:dev 33 | platforms: linux/amd64,linux/arm64 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Andrey Goncharov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /telegram_to_rss/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from platformdirs import user_data_dir 4 | 5 | api_id = int(os.environ.get("TG_API_ID")) 6 | api_hash = os.environ.get("TG_API_HASH") 7 | password = os.environ.get("TG_PASSWORD") 8 | 9 | update_interval_seconds = int(os.environ.get("UPDATE_INTERVAL") or 3600) 10 | feed_size_limit = int(os.environ.get("FEED_SIZE") or 200) 11 | initial_feed_size = int(os.environ.get("INITIAL_FEED_SIZE") or 50) 12 | base_url = os.environ.get("BASE_URL") 13 | bind = os.environ.get("BIND") or "127.0.0.1:3042" 14 | max_video_size_mb = int(os.environ.get("MAX_VIDEO_SIZE_MB", 10)) 15 | max_video_size = max_video_size_mb * 1024 * 1024 16 | 17 | loglevel = os.environ.get("LOGLEVEL", "INFO").upper() 18 | 19 | data_dir = ( 20 | Path(os.environ.get("DATA_DIR")) 21 | if os.environ.get("DATA_DIR") 22 | else Path(user_data_dir()).joinpath("telegram_to_rss") 23 | ) 24 | session_path = data_dir.joinpath("telegram_to-rss.session") 25 | static_path = data_dir.joinpath("static") 26 | db_path = data_dir.joinpath("feeds.db") 27 | 28 | data_dir.mkdir(mode=0o700, parents=True, exist_ok=True) 29 | static_path.mkdir(mode=0o700, exist_ok=True) 30 | -------------------------------------------------------------------------------- /telegram_to_rss/models/feed_entry.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from tortoise.models import Model 3 | from tortoise import fields 4 | from tortoise.signals import post_delete 5 | from typing import Type 6 | from anyio import Path 7 | from telegram_to_rss.config import static_path 8 | 9 | 10 | class FeedEntry(Model): 11 | id = fields.TextField(primary_key=True) 12 | feed = fields.ForeignKeyField( 13 | "models.Feed", on_delete=fields.CASCADE, related_name="entries" 14 | ) 15 | message = fields.TextField() 16 | date = fields.DatetimeField() 17 | media = fields.JSONField(default=[]) 18 | has_unsupported_media = fields.BooleanField(default=False) 19 | 20 | 21 | @post_delete(FeedEntry) 22 | async def remove_associated_file( 23 | sender: Type[FeedEntry], 24 | instance: FeedEntry, 25 | using_db 26 | ) -> None: 27 | try: 28 | for media_relative_path in instance.media: 29 | file_path = Path(static_path).joinpath(media_relative_path) 30 | await file_path.unlink(missing_ok=True) 31 | logging.debug(f"File removed: {file_path}") 32 | 33 | except Exception as e: 34 | logging.error(f"Error while removing FeedEntry id {instance.id}: {e}") 35 | -------------------------------------------------------------------------------- /.github/workflows/build-tag.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker images tag 2 | on: 3 | push: 4 | # Run pipeline for release tags 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | build-tag: 10 | runs-on: [ubuntu-22.04] 11 | # Only run on pushed tags (and explicitely ignore scheduled runs) 12 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') && github.event_name != 'schedule' 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Set up QEMU 17 | uses: docker/setup-qemu-action@v3 18 | - name: Set up Docker Buildx 19 | uses: docker/setup-buildx-action@v3 20 | - name: Login to Docker Hub 21 | uses: docker/login-action@v3 22 | with: 23 | username: ${{ secrets.DOCKERHUB_USERNAME }} 24 | password: ${{ secrets.DOCKERHUB_TOKEN }} 25 | - name: Build tag and latest 26 | uses: docker/build-push-action@v5 27 | with: 28 | context: . 29 | file: docker/Dockerfile 30 | push: true 31 | tags: | 32 | aigoncharov/telegram-to-rss:latest 33 | aigoncharov/telegram-to-rss:${{github.ref_name}} 34 | platforms: linux/amd64,linux/arm64 35 | -------------------------------------------------------------------------------- /telegram_to_rss/__init__.py: -------------------------------------------------------------------------------- 1 | from telegram_to_rss.server import app 2 | from telegram_to_rss.config import bind 3 | import asyncio 4 | from hypercorn.config import Config 5 | from hypercorn.asyncio import serve 6 | import argparse 7 | 8 | 9 | # https://stackoverflow.com/a/46877092 10 | def parse_hostport(bind_str: str | None) -> tuple[str | None, int | None]: 11 | if bind_str is None: 12 | return (None, None) 13 | 14 | out = bind_str.rsplit(":", 1) 15 | try: 16 | out[1] = int(out[1]) 17 | except (IndexError, ValueError): 18 | # couldn't parse the last component as a port, so let's 19 | # assume there isn't a port. 20 | out = (bind_str, None) 21 | return out 22 | 23 | 24 | def main(): 25 | parser = argparse.ArgumentParser( 26 | prog="Telegram to RSS", 27 | description="Generate an RSS feed from your Telegram chats", 28 | ) 29 | parser.add_argument("-d", "--dev", action="store_true") 30 | args = parser.parse_args() 31 | 32 | if args.dev: 33 | [host, port] = parse_hostport(bind) 34 | app.run(debug=True, host=host, port=port) 35 | else: 36 | config = Config() 37 | if bind: 38 | config.bind = bind 39 | asyncio.run(serve(app, config)) 40 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==24.1.0 ; python_version >= "3.12" and python_version < "4.0" 2 | aiosqlite==0.17.0 ; python_version >= "3.12" and python_version < "4.0" 3 | annotated-types==0.7.0 ; python_version >= "3.12" and python_version < "4.0" 4 | anyio==4.4.0 ; python_version >= "3.12" and python_version < "4.0" 5 | blinker==1.8.2 ; python_version >= "3.12" and python_version < "4.0" 6 | click==8.1.7 ; python_version >= "3.12" and python_version < "4.0" 7 | colorama==0.4.6 ; python_version >= "3.12" and python_version < "4.0" and platform_system == "Windows" 8 | cryptg==0.4.0 ; python_version >= "3.12" and python_version < "4.0" 9 | flask==3.0.3 ; python_version >= "3.12" and python_version < "4.0" 10 | h11==0.14.0 ; python_version >= "3.12" and python_version < "4.0" 11 | h2==4.1.0 ; python_version >= "3.12" and python_version < "4.0" 12 | hpack==4.0.0 ; python_version >= "3.12" and python_version < "4.0" 13 | hypercorn==0.17.3 ; python_version >= "3.12" and python_version < "4.0" 14 | hyperframe==6.0.1 ; python_version >= "3.12" and python_version < "4.0" 15 | idna==3.7 ; python_version >= "3.12" and python_version < "4.0" 16 | iso8601==1.1.0 ; python_version >= "3.12" and python_version < "4.0" 17 | itsdangerous==2.2.0 ; python_version >= "3.12" and python_version < "4.0" 18 | jinja2==3.1.4 ; python_version >= "3.12" and python_version < "4.0" 19 | markupsafe==2.1.5 ; python_version >= "3.12" and python_version < "4.0" 20 | platformdirs==4.2.2 ; python_version >= "3.12" and python_version < "4.0" 21 | priority==2.0.0 ; python_version >= "3.12" and python_version < "4.0" 22 | pyaes==1.6.1 ; python_version >= "3.12" and python_version < "4.0" 23 | pyasn1==0.6.0 ; python_version >= "3.12" and python_version < "4" 24 | pydantic-core==2.20.1 ; python_version >= "3.12" and python_version < "4.0" 25 | pydantic==2.8.2 ; python_version >= "3.12" and python_version < "4.0" 26 | pypika-tortoise==0.1.6 ; python_version >= "3.12" and python_version < "4.0" 27 | pypng==0.20220715.0 ; python_version >= "3.12" and python_version < "4.0" 28 | pytz==2024.1 ; python_version >= "3.12" and python_version < "4.0" 29 | qrcode==7.4.2 ; python_version >= "3.12" and python_version < "4.0" 30 | quart==0.19.6 ; python_version >= "3.12" and python_version < "4.0" 31 | rsa==4.9 ; python_version >= "3.12" and python_version < "4" 32 | sniffio==1.3.1 ; python_version >= "3.12" and python_version < "4.0" 33 | telethon==1.36.0 ; python_version >= "3.12" and python_version < "4.0" 34 | tortoise-orm==0.21.4 ; python_version >= "3.12" and python_version < "4.0" 35 | typing-extensions==4.12.2 ; python_version >= "3.12" and python_version < "4.0" 36 | werkzeug==3.0.3 ; python_version >= "3.12" and python_version < "4.0" 37 | wsproto==1.2.0 ; python_version >= "3.12" and python_version < "4.0" 38 | -------------------------------------------------------------------------------- /telegram_to_rss/client.py: -------------------------------------------------------------------------------- 1 | from telethon import TelegramClient, types, errors, custom 2 | from telegram_to_rss.consts import TELEGRAM_NOTIFICATIONS_DIALOG_ID 3 | from telethon.utils import resolve_id 4 | from telegram_to_rss.consts import MESSAGE_FETCH_HARD_LIMIT 5 | import logging 6 | 7 | 8 | class TelegramToRssClient: 9 | _telethon: TelegramClient 10 | _qr_code_url: str | None = None 11 | _user: types.User = None 12 | _password: str | None = None 13 | 14 | def __init__( 15 | self, session_path: str, api_id: int, api_hash: str, password: str | None = None 16 | ): 17 | self._telethon = TelegramClient( 18 | session=session_path, api_id=api_id, api_hash=api_hash 19 | ) 20 | self._telethon.parse_mode = "html" 21 | self._password = password 22 | 23 | async def start(self): 24 | await self._telethon.connect() 25 | is_authorized = await self._telethon.is_user_authorized() 26 | 27 | if not is_authorized: 28 | try: 29 | qr_login_req = await self._telethon.qr_login() 30 | self._qr_code_url = qr_login_req.url 31 | await qr_login_req.wait() 32 | except errors.SessionPasswordNeededError: 33 | if self._password is None: 34 | raise Exception( 35 | "2FA enabled and requires a password, but no password is provided." 36 | ) 37 | await self._telethon.sign_in(password=self._password) 38 | 39 | self._qr_code_url = None 40 | self._user = await self._telethon.get_me() 41 | 42 | async def stop(self): 43 | if self._telethon.is_connected(): 44 | await self._telethon.disconnect() 45 | 46 | async def list_dialogs(self) -> list[custom.Dialog]: 47 | all_dialogs = await self._telethon.get_dialogs() 48 | filtered_dialogs = [ 49 | dialog 50 | for dialog in all_dialogs 51 | if ( 52 | dialog.id != TELEGRAM_NOTIFICATIONS_DIALOG_ID 53 | and dialog.entity.id != self._user.id 54 | ) 55 | ] 56 | return filtered_dialogs 57 | 58 | async def get_dialog_messages( 59 | self, 60 | dialog: custom.Dialog, 61 | limit: int = MESSAGE_FETCH_HARD_LIMIT, 62 | min_message_id: int = 0, 63 | ) -> list[custom.Message]: 64 | limit = min(MESSAGE_FETCH_HARD_LIMIT, limit) 65 | 66 | logging.debug( 67 | "TelegramToRssClient.get_dialog_messages %s (%s) %s %s", 68 | dialog.name, 69 | dialog.id, 70 | limit, 71 | min_message_id, 72 | ) 73 | 74 | messages: list[custom.Message] = await self._telethon.iter_messages( 75 | dialog, limit=limit, min_id=min_message_id 76 | ).collect() 77 | return messages 78 | 79 | @property 80 | def qr_code_url(self): 81 | return self._qr_code_url 82 | 83 | @property 84 | def user(self): 85 | return self._user 86 | 87 | 88 | def telethon_dialog_id_to_tg_id(id: int): 89 | return resolve_id(id)[0] 90 | -------------------------------------------------------------------------------- /telegram_to_rss/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from quart import Quart, render_template 3 | from telegram_to_rss.client import TelegramToRssClient 4 | from telegram_to_rss.config import ( 5 | api_hash, 6 | api_id, 7 | session_path, 8 | password, 9 | static_path, 10 | feed_size_limit, 11 | initial_feed_size, 12 | update_interval_seconds, 13 | db_path, 14 | loglevel, 15 | max_video_size, 16 | ) 17 | from telegram_to_rss.qr_code import get_qr_code_image 18 | from telegram_to_rss.db import init_feeds_db, close_feeds_db 19 | from telegram_to_rss.generate_feed import update_feeds_cache 20 | from telegram_to_rss.poll_telegram import ( 21 | TelegramPoller, 22 | update_feeds_in_db, 23 | reset_feeds_in_db, 24 | ) 25 | from telegram_to_rss.models import Feed 26 | import logging 27 | 28 | logging.basicConfig( 29 | level=loglevel, 30 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 31 | datefmt='%Y-%m-%d %H:%M:%S' 32 | ) 33 | 34 | app = Quart(__name__, static_folder=static_path, static_url_path="/static") 35 | client = TelegramToRssClient( 36 | session_path=session_path, api_id=api_id, api_hash=api_hash, password=password 37 | ) 38 | telegram_poller = TelegramPoller( 39 | client=client, 40 | message_limit=feed_size_limit, 41 | new_feed_limit=initial_feed_size, 42 | static_path=static_path, 43 | max_video_size=max_video_size, 44 | ) 45 | rss_task: asyncio.Task | None = None 46 | 47 | 48 | async def start_rss_generation(): 49 | global rss_task 50 | 51 | logging.info("start_rss_generation") 52 | 53 | async def update_rss(): 54 | global rss_task 55 | 56 | should_reschedule = True 57 | try: 58 | logging.info("update_rss -> db") 59 | await update_feeds_in_db(telegram_poller=telegram_poller) 60 | 61 | logging.info("update_rss -> cache") 62 | await update_feeds_cache(feed_render_dir=static_path) 63 | 64 | logging.info("update_rss -> sleep") 65 | await asyncio.sleep(update_interval_seconds) 66 | except asyncio.CancelledError: 67 | should_reschedule = False 68 | except Exception as e: 69 | logging.error(f"update_rss -> error: {e}") 70 | logging.warning("update_rss -> rebuilding feeds from scratch") 71 | await reset_feeds_in_db(telegram_poller=telegram_poller) 72 | raise e 73 | finally: 74 | if should_reschedule: 75 | logging.info("update_rss -> scheduling a new run") 76 | loop = asyncio.get_event_loop() 77 | rss_task = loop.create_task(update_rss()) 78 | 79 | await client.start() 80 | 81 | loop = asyncio.get_event_loop() 82 | rss_task = loop.create_task(update_rss()) 83 | 84 | logging.info("start_rss_generation -> done") 85 | 86 | 87 | @app.before_serving 88 | async def startup(): 89 | global rss_task 90 | 91 | logging.info("startup") 92 | 93 | await init_feeds_db(db_path=db_path) 94 | loop = asyncio.get_event_loop() 95 | rss_task = loop.create_task(start_rss_generation()) 96 | 97 | logging.info("startup -> done") 98 | 99 | 100 | @app.after_serving 101 | async def cleanup(): 102 | logging.info("cleanup") 103 | 104 | if rss_task is not None: 105 | rss_task.cancel() 106 | await client.stop() 107 | await close_feeds_db() 108 | 109 | logging.info("cleanup -> done") 110 | 111 | 112 | @app.route("/") 113 | async def root(): 114 | logging.debug("GET /root %s", bool(client.qr_code_url)) 115 | 116 | if client.qr_code_url is not None: 117 | qr_code_image = get_qr_code_image(client.qr_code_url) 118 | return await render_template("qr_code.html", qr_code=qr_code_image) 119 | 120 | feeds = await Feed.all() 121 | logging.debug("GET /root -> feeds %s", len(feeds)) 122 | 123 | return await render_template("feeds.html", user=client.user, feeds=feeds) 124 | -------------------------------------------------------------------------------- /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | 164 | # MacOS 165 | .DS_Store 166 | 167 | # App specific artifacts 168 | *.session 169 | *.session-journal 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # telegram-to-rss 2 | 3 | Generate an RSS feed from your Telegram chats. You digital minimalism friend. 4 | 5 | ## How to get the most of it 6 | 7 | > Digital minimalism is a strategy to help people optimize their use of technology and keep from being overwhelmed by it. 8 | 9 | 1. Create a separate Telegram account to subscribe to various channels available only as Telegram feeds (yep, they exist!) 10 | 2. Convert all of them to RSS feeds using this app 11 | 3. Be in power of your information consumption with a single place to get it - your RSS reader! 12 | 13 | ## Quick start 14 | 15 | ### Docker 16 | 17 | 1. Get `api_id` an `api_hash` at https://my.telegram.org 18 | 2. Create a docker compose file and replace the environment variables (see [Configuration](#configuration) for details) 19 | ```yaml 20 | services: 21 | telegram-to-rss: 22 | image: aigoncharov/telegram-to-rss:latest 23 | container_name: telegram-to-rss 24 | restart: always 25 | environment: 26 | - TG_API_ID=REPLACE_ME 27 | - TG_API_HASH=REPLACE_ME 28 | - TG_PASSWORD=REPLACE_ME 29 | - BASE_URL=REPLACE_ME 30 | ports: 31 | - 3042:3042 32 | volumes: 33 | - data:/data 34 | 35 | volumes: 36 | data: null 37 | 38 | networks: {} 39 | ``` 40 | 3. Run `docker compose up` 41 | 4. Go to `http://127.0.0.1:3042` 42 | 5. Scan the QR code with your Telegram app 43 | 1. If there is an AUTH_ERROR, restart the docker compose stack 44 | 6. Give it a few seconds to log in 45 | 7. Get redirected to a page with a list of all your chats available as RSS feeds. 46 | 1. If the list is incomplete, give it a few minutes on the first start to generate the RSS feeds. 47 | 2. Subsequent updates should be much faster! 48 | 49 | ### Bare bone Python 50 | 51 | 1. `pip install telegram-to-rss` 52 | 2. Get `api_id` an `api_hash` at https://my.telegram.org 53 | 3. Run in your terminal `TG_API_ID=api_id TG_API_HASH=api_hash BASE_URL=server_url python telegram_to_rss` 54 | 1. Example: `TG_API_ID=00000000 TG_API_HASH=7w8sdsd3g334r333refdwd3qqrwe BASE_URL=example.myserver.com python telegram_to_rss` 55 | 4. If you have 2FA enabled on your Telegram account set `TG_PASSWORD` with your account password as well: `TG_API_ID=api_id TG_API_HASH=api_hash TG_PASSWORD=your_password BASE_URL=server_url python telegram_to_rss` 56 | 5. Go to `http://127.0.0.1:5000` 57 | 6. Scan the QR code with your Telegram app 58 | 1. If there is an AUTH_ERROR, restart `telegram-to-rss` 59 | 7. Give it a few seconds to log in 60 | 8. Get redirected to a page with a list of all your chats available as RSS feeds. 61 | 1. If the list is incomplete, give it a few minutes on the first start to generate the RSS feeds. 62 | 2. Subsequent updates should be much faster! 63 | 64 | ## Configuration 65 | 66 | Available environment variables (\* marks required ones): 67 | - \* `TG_API_ID` - api_id from https://my.telegram.org 68 | - \* `TG_API_HASH` - api_hash from https://my.telegram.org 69 | - \* `BASE_URL` - address of your server that hosts this app, used in RSS feeds to correctly set image addresses 70 | - `TG_PASSWORD` - your telegram password, required if 2fa is enabled on the account 71 | - `BIND` - `host:port` to bind to on the server. Default: `127.0.0.1:3042` 72 | - `LOGLEVEL` - log level for the app ([supported values](https://docs.python.org/3/library/logging.html#logging-levels)). Default: `INFO` 73 | - `DATA_DIR` - path to store the database, RSS feeds and other static files. Default: `user_data_dir` from [platformdirs](https://github.com/platformdirs/platformdirs?tab=readme-ov-file#platformdirs-to-the-rescue) 74 | - `FEED_SIZE` - size of the RSS feed. When your RSS feed grows larger than the limit, older entries are going to be discarded. Default: 200. 75 | - `INITIAL_FEED_SIZE` - number of messages we fetch for any new feed on the first run. Default value: 50. 76 | - `UPDATE_INTERVAL` - how often the app should fetch new messages from Telegram and regenerate RSS feeds (in seconds). Default: 3600. 77 | - `MAX_VIDEO_SIZE_MB` - the maximum allowed size (in megabytes) for video files to be downloaded from Telegram. Default value: 10. -------------------------------------------------------------------------------- /telegram_to_rss/generate_feed.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from quart import utils 3 | from telegram_to_rss.models import Feed, FeedEntry 4 | from tortoise.query_utils import Prefetch 5 | from telegram_to_rss.config import base_url 6 | from telegram_to_rss.poll_telegram import parse_feed_entry_id 7 | import re 8 | from telegram_to_rss.client import telethon_dialog_id_to_tg_id 9 | import xml.etree.ElementTree as ET 10 | import logging 11 | 12 | CLEAN_TITLE = re.compile("<.*?>") 13 | 14 | 15 | def clean_title(raw_html): 16 | cleantext = re.sub(CLEAN_TITLE, "", raw_html).replace("\n", " ").strip() 17 | return cleantext 18 | 19 | 20 | def generate_feed(feed_render_dir: Path, feed: Feed): 21 | logging.info("generate_feed %s %s", feed.name, feed.id) 22 | 23 | feed_url = "https://t.me/c/{}".format(telethon_dialog_id_to_tg_id(feed.id)) 24 | 25 | rss_root_el = ET.Element("rss", {"version": "2.0"}) 26 | 27 | rss_feed_el = ET.SubElement(rss_root_el, "channel") 28 | 29 | ET.SubElement(rss_feed_el, "title").text = feed.name 30 | ET.SubElement(rss_feed_el, "pubDate").text = feed.last_update.isoformat() 31 | ET.SubElement( 32 | rss_feed_el, 33 | "link", 34 | {"href": feed_url}, 35 | ) 36 | ET.SubElement(rss_feed_el, "description").text = feed.name 37 | 38 | for feed_entry in feed.entries: 39 | [feed_id, entry_id] = parse_feed_entry_id(feed_entry.id) 40 | feed_entry_url = "https://t.me/c/{}/{}".format( 41 | telethon_dialog_id_to_tg_id(feed_id), entry_id 42 | ) 43 | 44 | rss_item_el = ET.SubElement(rss_feed_el, "item") 45 | 46 | ET.SubElement(rss_item_el, "guid").text = feed_entry_url 47 | 48 | message_text = clean_title(feed_entry.message) 49 | title = message_text[:100] 50 | ET.SubElement(rss_item_el, "title").text = title 51 | 52 | media_content = "" 53 | media_download_failure = False 54 | media_too_large = False 55 | 56 | # processing mediafiles 57 | for media_path in feed_entry.media: 58 | if media_path == "FAIL": 59 | media_download_failure = True 60 | elif media_path == "TOO_LARGE": 61 | media_too_large = True 62 | else: 63 | media_url = "{}/static/{}".format(base_url, media_path) 64 | 65 | # checking file type 66 | if media_path.endswith(('.jpg', '.png', '.gif')): 67 | media_content += '
media'.format(media_url) 68 | elif media_path.endswith('.mp4'): 69 | media_content += ( 70 | '
' 73 | ).format(media_url, media_url) 74 | 75 | # creating feed with text and media 76 | content = feed_entry.message.replace("\n", "
") + media_content 77 | if feed_entry.has_unsupported_media: 78 | content += "
This message has unsupported attachment. Open Telegram to view it." 79 | if media_download_failure: 80 | content += "
Downloading some of the media for this message failed. Open Telegram to view it." 81 | if media_too_large: 82 | content += "
The video is too large." 83 | 84 | ET.SubElement(rss_item_el, "description").text = content 85 | ET.SubElement(rss_item_el, "pubDate").text = feed_entry.date.isoformat() 86 | ET.SubElement(rss_item_el, "link", {"href": feed_entry_url}).text = feed_entry_url 87 | 88 | final_feed_file = feed_render_dir.joinpath("{}.xml".format(feed.id)) 89 | 90 | rss_xml_tree = ET.ElementTree(rss_root_el) 91 | rss_xml_tree.write( 92 | file_or_filename=final_feed_file, encoding="UTF-8", short_empty_elements=True 93 | ) 94 | 95 | logging.info("generate_feed -> done %s %s", feed.name, feed.id) 96 | 97 | 98 | async def update_feeds_cache(feed_render_dir: str): 99 | feeds = await Feed.all().prefetch_related( 100 | Prefetch("entries", queryset=FeedEntry.all().order_by("-date")) 101 | ) 102 | 103 | for feed in feeds: 104 | await utils.run_sync(generate_feed)(feed_render_dir, feed) 105 | -------------------------------------------------------------------------------- /telegram_to_rss/poll_telegram.py: -------------------------------------------------------------------------------- 1 | from telegram_to_rss.client import TelegramToRssClient, custom, types 2 | from telegram_to_rss.models import Feed, FeedEntry 3 | from tortoise.expressions import Q 4 | from tortoise.transactions import atomic 5 | from pathlib import Path 6 | import logging 7 | 8 | 9 | class TelegramPoller: 10 | _client: TelegramToRssClient 11 | _message_limit: int 12 | _new_feed_limit: int 13 | _static_path: Path 14 | _max_video_size: int 15 | 16 | def __init__( 17 | self, 18 | client: TelegramToRssClient, 19 | message_limit: int, 20 | new_feed_limit: int, 21 | static_path: Path, 22 | max_video_size: int, 23 | ) -> None: 24 | self._client = client 25 | self._message_limit = message_limit 26 | self._new_feed_limit = new_feed_limit 27 | self._static_path = static_path 28 | self._max_video_size = max_video_size 29 | 30 | 31 | async def fetch_dialogs(self): 32 | tg_dialogs = await self._client.list_dialogs() 33 | db_feeds = await Feed.all() 34 | 35 | tg_dialogs_ids = set([dialog.id for dialog in tg_dialogs]) 36 | db_feeds_ids = set([feed.id for feed in db_feeds]) 37 | 38 | feed_ids_to_delete = db_feeds_ids - tg_dialogs_ids 39 | feed_ids_to_create = tg_dialogs_ids - db_feeds_ids 40 | feed_ids_to_update = db_feeds_ids.intersection(tg_dialogs_ids) 41 | 42 | feeds_to_create = [ 43 | dialog for dialog in tg_dialogs if dialog.id in feed_ids_to_create 44 | ] 45 | feeds_to_update = [ 46 | dialog for dialog in tg_dialogs if dialog.id in feed_ids_to_update 47 | ] 48 | 49 | return (list(feed_ids_to_delete), feeds_to_create, feeds_to_update) 50 | 51 | async def bulk_delete_feeds(self, ids: list[int] | None): 52 | if ids is None: 53 | await Feed.all().delete() 54 | return 55 | if len(ids) != 0: 56 | await Feed.filter(Q(id__in=list(ids))).delete() 57 | 58 | @atomic() 59 | async def create_feed(self, dialog: custom.Dialog): 60 | logging.debug("TelegramPoller.create_feed %s %s", dialog.name, dialog.id) 61 | 62 | feed = await Feed.create(id=dialog.id, name=dialog.name) 63 | 64 | logging.debug("TelegramPoller.create_feed -> get_dialog_messages") 65 | dialog_messages = await self._client.get_dialog_messages( 66 | dialog=dialog, limit=self._new_feed_limit 67 | ) 68 | logging.debug("TelegramPoller.create_feed -> _process_new_dialog_messages") 69 | feed_entries = await self._process_new_dialog_messages(feed, dialog_messages) 70 | 71 | logging.debug("TelegramPoller.create_feed -> bulk_create") 72 | await FeedEntry.bulk_create(feed_entries) 73 | 74 | @atomic() 75 | async def update_feed(self, dialog: custom.Dialog): 76 | feed = await Feed.get(id=dialog.id) 77 | last_feed_entry = await FeedEntry.filter(feed=feed).order_by("-date").first() 78 | logging.debug( 79 | f"TelegramPoller.update_feed -> last feed entry {last_feed_entry}", 80 | ) 81 | 82 | get_dialog_messages_args = {} 83 | if last_feed_entry: 84 | [_, tg_message_id] = parse_feed_entry_id(last_feed_entry.id) 85 | get_dialog_messages_args["min_message_id"] = tg_message_id 86 | else: 87 | get_dialog_messages_args["limit"] = self._new_feed_limit 88 | logging.warning( 89 | f"TelegramPoller.update_feed -> feed {feed.name} ({feed.id}) does not have associated feed entries" 90 | ) 91 | 92 | new_dialog_messages = await self._client.get_dialog_messages( 93 | dialog=dialog, **get_dialog_messages_args 94 | ) 95 | 96 | for new_message in new_dialog_messages: 97 | if new_message.date is None: 98 | logging.warning( 99 | f"TelegramPoller.update_feed {feed.name} ({feed.id}) -> message without a date! WTF? {new_message.id} {new_message.message}" 100 | ) 101 | continue 102 | if last_feed_entry and new_message.date <= last_feed_entry.date: 103 | logging.warning( 104 | f"TelegramPoller.update_feed {feed.name} ({feed.id}) -> TG sent a message older than we requested! WTF? TG sent ut {new_message.date} {new_message.message}, our last known message {last_feed_entry.date} {last_feed_entry.message}" 105 | ) 106 | continue 107 | 108 | feed_entries = await self._process_new_dialog_messages( 109 | feed, new_dialog_messages 110 | ) 111 | 112 | await FeedEntry.bulk_create(feed_entries) 113 | # Save even if unchanged to update date 114 | await feed.save() 115 | 116 | old_feed_entries = ( 117 | await FeedEntry.filter(feed=feed) 118 | .order_by("-date") 119 | .limit(self._message_limit) 120 | .offset(self._message_limit) 121 | ) 122 | 123 | for entry in old_feed_entries: 124 | logging.debug(f"Deleting FeedEntry with id: {entry.id}") 125 | await entry.delete() 126 | 127 | async def _process_new_dialog_messages( 128 | self, feed: Feed, dialog_messages: list[custom.Message] 129 | ): 130 | filtered_dialog_messages: list[custom.Message] = [] 131 | logging.info(f"Processing {len(dialog_messages)} messages from {feed.name}") 132 | 133 | for dialog_message in dialog_messages: 134 | try: 135 | logging.debug( 136 | "Processing message ID: %s, grouped_id: %s, has photo: %s, has media: %s, text: %s", 137 | dialog_message.id, 138 | dialog_message.grouped_id, 139 | dialog_message.photo is not None, 140 | dialog_message.media is not None, 141 | dialog_message.text, 142 | ) 143 | 144 | if dialog_message.text is None: 145 | continue 146 | 147 | dialog_message.downloaded_media = [] 148 | 149 | if ( 150 | dialog_message.grouped_id is None 151 | or len(filtered_dialog_messages) == 0 152 | or dialog_message.grouped_id != filtered_dialog_messages[-1].grouped_id 153 | ): 154 | filtered_dialog_messages.append(dialog_message) 155 | else: 156 | if len(dialog_message.text) > len(filtered_dialog_messages[-1].text): 157 | filtered_dialog_messages[-1].text = dialog_message.text 158 | 159 | last_processed_message = filtered_dialog_messages[-1] 160 | 161 | if dialog_message.photo: 162 | await self._download_media(dialog_message, last_processed_message, feed, 'photo') 163 | 164 | if isinstance(dialog_message.media, types.MessageMediaDocument): 165 | document = dialog_message.media.document 166 | mime_type = getattr(document, 'mime_type', None) 167 | if mime_type: 168 | if mime_type.startswith("video/"): 169 | video_size = document.size 170 | if video_size > self._max_video_size: 171 | logging.info( 172 | f"Video in message {dialog_message.id} is too large ({video_size} bytes). Skipping download." 173 | ) 174 | last_processed_message.downloaded_media.append("TOO_LARGE") 175 | continue 176 | await self._download_media(dialog_message, last_processed_message, feed, 'video') 177 | elif mime_type.startswith("image/"): 178 | await self._download_media(dialog_message, last_processed_message, feed, 'image') 179 | else: 180 | logging.debug( 181 | f"Unsupported media type '{mime_type}' in message {dialog_message.id}" 182 | ) 183 | last_processed_message.has_unsupported_media = True 184 | 185 | except Exception as e: 186 | logging.error(f"Error processing message {dialog_message.id}: {e}", exc_info=True) 187 | continue 188 | 189 | feed_entries: list[FeedEntry] = [] 190 | for dialog_message in filtered_dialog_messages: 191 | feed_entry_id = to_feed_entry_id(feed, dialog_message) 192 | feed_entries.append( 193 | FeedEntry( 194 | id=feed_entry_id, 195 | feed=feed, 196 | message=dialog_message.text, 197 | date=dialog_message.date, 198 | media=dialog_message.downloaded_media, 199 | has_unsupported_media=getattr(dialog_message, 'has_unsupported_media', False), 200 | ) 201 | ) 202 | return feed_entries 203 | 204 | async def _download_media(self, dialog_message, last_processed_message, feed, media_type): 205 | try: 206 | feed_entry_media_id = "{}-{}".format( 207 | to_feed_entry_id(feed, dialog_message), 208 | len(last_processed_message.downloaded_media), 209 | ) 210 | media_path = self._static_path.joinpath(feed_entry_media_id) 211 | 212 | def progress_callback(current, total, media_path=media_path): 213 | logging.debug( 214 | "Downloading %s %s: %s out of %s", 215 | media_type, 216 | media_path, 217 | current, 218 | total, 219 | ) 220 | 221 | res_path = await dialog_message.download_media( 222 | file=media_path, progress_callback=progress_callback 223 | ) 224 | last_processed_message.downloaded_media.append(Path(res_path).name) 225 | logging.debug(f"Downloaded {media_type} to {res_path}") 226 | except Exception as e: 227 | logging.warning( 228 | f"Downloading {media_type} failed with {e} for message {dialog_message.id} {dialog_message.date} {dialog_message.text}", 229 | ) 230 | last_processed_message.downloaded_media.append("FAIL") 231 | 232 | 233 | def to_feed_entry_id(feed: Feed, dialog_message: custom.Message): 234 | return "{}--{}".format(feed.id, dialog_message.id) 235 | 236 | 237 | def parse_feed_entry_id(id: str): 238 | [channel_id, message_id] = id.split("--") 239 | return int(channel_id), int(message_id) 240 | 241 | 242 | async def reset_feeds_in_db(telegram_poller: TelegramPoller): 243 | logging.debug("reset_feeds_in_db") 244 | 245 | await telegram_poller.bulk_delete_feeds(ids=None) 246 | 247 | logging.debug("reset_feeds_in_db -> done") 248 | 249 | 250 | async def update_feeds_in_db(telegram_poller: TelegramPoller): 251 | logging.debug("update_feeds_in_db") 252 | 253 | [feed_ids_to_delete, feeds_to_create, feeds_to_update] = ( 254 | await telegram_poller.fetch_dialogs() 255 | ) 256 | logging.debug( 257 | "update_feeds_in_db -> fetched dialogs %s %s %s", 258 | feed_ids_to_delete, 259 | [dialog.id for dialog in feeds_to_create], 260 | [dialog.id for dialog in feeds_to_update], 261 | ) 262 | 263 | await telegram_poller.bulk_delete_feeds(feed_ids_to_delete) 264 | logging.debug("update_feeds_in_db -> deleted feeds %s", feed_ids_to_delete) 265 | 266 | for feed_to_create in feeds_to_create: 267 | logging.debug( 268 | "update_feeds_in_db.create_feed %s %s", 269 | feed_to_create.id, 270 | feed_to_create.name, 271 | ) 272 | await telegram_poller.create_feed(feed_to_create) 273 | logging.debug("update_feeds_in_db.create_feed -> done") 274 | 275 | for feed_to_update in feeds_to_update: 276 | logging.debug( 277 | "update_feeds_in_db.update_feed %s %s", 278 | feed_to_update.id, 279 | feed_to_update.name, 280 | ) 281 | await telegram_poller.update_feed(feed_to_update) 282 | logging.debug("update_feeds_in_db.update_feed -> done") 283 | -------------------------------------------------------------------------------- /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 = "aiofiles" 5 | version = "24.1.0" 6 | description = "File support for asyncio." 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, 11 | {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, 12 | ] 13 | 14 | [[package]] 15 | name = "aiosqlite" 16 | version = "0.17.0" 17 | description = "asyncio bridge to the standard sqlite3 module" 18 | optional = false 19 | python-versions = ">=3.6" 20 | files = [ 21 | {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"}, 22 | {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, 23 | ] 24 | 25 | [package.dependencies] 26 | typing_extensions = ">=3.7.2" 27 | 28 | [[package]] 29 | name = "annotated-types" 30 | version = "0.7.0" 31 | description = "Reusable constraint types to use with typing.Annotated" 32 | optional = false 33 | python-versions = ">=3.8" 34 | files = [ 35 | {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, 36 | {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, 37 | ] 38 | 39 | [[package]] 40 | name = "anyio" 41 | version = "4.4.0" 42 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 43 | optional = false 44 | python-versions = ">=3.8" 45 | files = [ 46 | {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, 47 | {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, 48 | ] 49 | 50 | [package.dependencies] 51 | idna = ">=2.8" 52 | sniffio = ">=1.1" 53 | 54 | [package.extras] 55 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 56 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 57 | trio = ["trio (>=0.23)"] 58 | 59 | [[package]] 60 | name = "attrs" 61 | version = "23.2.0" 62 | description = "Classes Without Boilerplate" 63 | optional = false 64 | python-versions = ">=3.7" 65 | files = [ 66 | {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, 67 | {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, 68 | ] 69 | 70 | [package.extras] 71 | cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] 72 | dev = ["attrs[tests]", "pre-commit"] 73 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] 74 | tests = ["attrs[tests-no-zope]", "zope-interface"] 75 | tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] 76 | tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] 77 | 78 | [[package]] 79 | name = "blinker" 80 | version = "1.8.2" 81 | description = "Fast, simple object-to-object and broadcast signaling" 82 | optional = false 83 | python-versions = ">=3.8" 84 | files = [ 85 | {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"}, 86 | {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, 87 | ] 88 | 89 | [[package]] 90 | name = "cfgv" 91 | version = "3.4.0" 92 | description = "Validate configuration and produce human readable error messages." 93 | optional = false 94 | python-versions = ">=3.8" 95 | files = [ 96 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 97 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 98 | ] 99 | 100 | [[package]] 101 | name = "click" 102 | version = "8.1.7" 103 | description = "Composable command line interface toolkit" 104 | optional = false 105 | python-versions = ">=3.7" 106 | files = [ 107 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 108 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 109 | ] 110 | 111 | [package.dependencies] 112 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 113 | 114 | [[package]] 115 | name = "colorama" 116 | version = "0.4.6" 117 | description = "Cross-platform colored terminal text." 118 | optional = false 119 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 120 | files = [ 121 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 122 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 123 | ] 124 | 125 | [[package]] 126 | name = "cryptg" 127 | version = "0.4.0" 128 | description = "Cryptographic utilities for Telegram." 129 | optional = false 130 | python-versions = ">=3.7" 131 | files = [ 132 | {file = "cryptg-0.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59a6c881535bd3ff406855122484daf46a3f7b105a3c9e0cde294ff72e68f4e8"}, 133 | {file = "cryptg-0.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a045a8af59814a50787cce9965743fa67e6c4b948305139aa3c216ecfb45b7f"}, 134 | {file = "cryptg-0.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cabdd52e7f3e24a800b4769d9e4b9da45aeb7065b986c16fc946e6798ed09525"}, 135 | {file = "cryptg-0.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67d408d335f99dd850f69fb2aed99e6469e6e046d5d4b870271bc932d7f102d4"}, 136 | {file = "cryptg-0.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cbf215e88f07656ac96bb59efa04d27134be85788e14d4f3898ee0b2a7f7d70"}, 137 | {file = "cryptg-0.4.0-cp310-cp310-win32.whl", hash = "sha256:9f2f63aa12965e99824a05147b4cef26c4988630181f8e55f13050d7ac86bbc5"}, 138 | {file = "cryptg-0.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:adcf175908a93557ef2e53ffba62706ae4afb8bb3489cecc6672c7c9d99585ef"}, 139 | {file = "cryptg-0.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88df8cd6f2222570f34ee054a7a92d3c44816acc689bbbebe9a95f94f328c1a3"}, 140 | {file = "cryptg-0.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad2c2a615dbd64b35f42ceca7f1f3ccc7c3f1275d833ae7eac3e4672678a8e96"}, 141 | {file = "cryptg-0.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a0fbaf9de1166ace65a3c589ef9db5b42d88728ae5f6b3ebe6f42846efc72d7"}, 142 | {file = "cryptg-0.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96f4415910dec41671a422d7fc29cef434b62e8c84908bf8e585a9dac66caa71"}, 143 | {file = "cryptg-0.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16d33f8213f480e895eab0bad6faaedd8d54da51b066373ccd5840a7a951dd37"}, 144 | {file = "cryptg-0.4.0-cp311-cp311-win32.whl", hash = "sha256:72875d7129cdb7f9a4be68854c77cb569857d736f0cbc7753cadc577f13360bd"}, 145 | {file = "cryptg-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:47a249a242497ba0fffe87067e0d5ef99b21c4081fe490a08c596b6184dda2dd"}, 146 | {file = "cryptg-0.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2da433484653470cba6b3e8c4c874ea9ba142c66d0214968c58ec8bed1fb5981"}, 147 | {file = "cryptg-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:951ebab677abfa661356eff578988eb3e9d5e6e6b46c876731051a01616cfb18"}, 148 | {file = "cryptg-0.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c720ebec7c5fe65d6843a24495a4012b02fbca98f93a687f143864fa1333949a"}, 149 | {file = "cryptg-0.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb564dbc370476e299c56d35a33aa8b8ec3df00184d5c78a0f73eebc2a3f057c"}, 150 | {file = "cryptg-0.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1e547e5ca4665d413397a43453ad867a78b0959362a056460ee3f4936da6728"}, 151 | {file = "cryptg-0.4.0-cp312-cp312-win32.whl", hash = "sha256:975e53f1d713ef5733bf160ef1dce473519c93e5d27ec19013766f1f81224b0f"}, 152 | {file = "cryptg-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:6a914b1b7199b9bb0bb20a11572f160bdc23b50575895112ca37395f2ad598db"}, 153 | {file = "cryptg-0.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:70c93ffc05f3b9e47aa30d6606e527da07532c0b6f78b6d23482fecd44881f57"}, 154 | {file = "cryptg-0.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f42587a71a9142f1abfe412ce677035abd9604e0333e5d1d188443349e327d"}, 155 | {file = "cryptg-0.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:719810a1cf0b8f8ba159943e95010ac66f9777cc3b72b49098817e260aa0753b"}, 156 | {file = "cryptg-0.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80acd4051fd122d3d921e0119d6225a7a40c648e690c451037cbe0ced654bb49"}, 157 | {file = "cryptg-0.4.0-cp37-cp37m-win32.whl", hash = "sha256:67d1dcf3f215acb22f5a73f9ffca9c0821e06e37f75652dcae7ca2327639ab1b"}, 158 | {file = "cryptg-0.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:152801d5738af4c99751ae22ec086862f949dc763a0874c9d35cf19ad07321cd"}, 159 | {file = "cryptg-0.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fac561c738a567a3fcce2a1ed5365ebd3268670454e051898e802972f50b700a"}, 160 | {file = "cryptg-0.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:de2879166cc9fa86f166f6c17bad40af4ab7ded69aeed1e644d92f0a16112a2d"}, 161 | {file = "cryptg-0.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cde6bf5d351498ef1dc6bbcf5b0285ccb2282ca7d85bffc9d3c39af08494dfe1"}, 162 | {file = "cryptg-0.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5f9f4a565699719b39fdff349fe76c748f9f2d1af5a8c11beea93bcadd802e7"}, 163 | {file = "cryptg-0.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7bf7511eb36f1a25acec916f0a3c8039422fb8b915a13122c82986382b2c9431"}, 164 | {file = "cryptg-0.4.0-cp38-cp38-win32.whl", hash = "sha256:3a8cbf459d49fe461e09375c5383bf74b6f4d2c7df9328d068beaff2289a3232"}, 165 | {file = "cryptg-0.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:f0c8e18d8aac544f3c525fbeb011fdbb05fc6cf6a33fe953132de5b2d8a1199d"}, 166 | {file = "cryptg-0.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c781f20b30a0831c9d1e7cc55526f854c57ece147a2ccc9d290c7e230cd6c7a1"}, 167 | {file = "cryptg-0.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c1ca745b5e90513a7a58d1dc982d6fd96e3a3e23065c0147c2ed192aa2e1e88"}, 168 | {file = "cryptg-0.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8eaeeb065b7a2ef8d5e8006e64821dae73890e9fba95749804babfefcc7e29b7"}, 169 | {file = "cryptg-0.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05df4812d592410c3e738207368cc6ee3d757401f979eb76281343e1f28f4b8a"}, 170 | {file = "cryptg-0.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:932e131a73bc1bd01457c8eaecb04b91f714051d0ba549f5301e4545d294628a"}, 171 | {file = "cryptg-0.4.0-cp39-cp39-win32.whl", hash = "sha256:717c65ca5c753d89b111938329379df59ffd43b80678c07e4338ff46be554a72"}, 172 | {file = "cryptg-0.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:64f205d02fbadfe5415e3050388727c9de65eaea20948d8fd444b73a391f54fa"}, 173 | {file = "cryptg-0.4.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b29dc778f811db9a1ca40cae9f187e9413c0592c8ea404f63356583a54f81fcd"}, 174 | {file = "cryptg-0.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5658eb76f46315e2539e831a9f53117cd908ccc95faa053df77fadf7e7a35e16"}, 175 | {file = "cryptg-0.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7459b9d4a79bccba6e31d3e955245659ee813466ab048881958757bb799602e7"}, 176 | {file = "cryptg-0.4.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a03b9a11ddb133dabbd642386a8d6f66af8e691faf5a18a99a39c4658df7aa9e"}, 177 | {file = "cryptg-0.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:551c8452fce4601b01d5a6490738683bc46e67a9201626267309369672d0e3f3"}, 178 | {file = "cryptg-0.4.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:39e89cca35430569d79db118da9112efc7d4834b5bb5db1d1674afaca6d78e8f"}, 179 | {file = "cryptg-0.4.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb0e7dd994d6547e24fdec41facd373fbe31f9551edc3258da17a82838a6442"}, 180 | {file = "cryptg-0.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c7a5052c81e27cb13e4d4354caf3619d27bea8cf2da2dcad2a1e225119054a7"}, 181 | {file = "cryptg-0.4.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a05e20b992b46413204df78349c70a41753ee98d487e8e681f060f87bf813c51"}, 182 | {file = "cryptg-0.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:52e05ccaafed67d827690303e745facd7a16576d7e06bf3a83d2d94349c16b97"}, 183 | {file = "cryptg-0.4.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7c4c34377bbf69b1124953c38a14fc18e28e7871006ae7d0686a2d84b600ef83"}, 184 | {file = "cryptg-0.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8514d9300bc846825391bfebf7148d163456a882ecf32d2cd7347c5234745927"}, 185 | {file = "cryptg-0.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94573f1eeec18871c54b866eed942f85011f093eac7dd0ed387e18f3e2ae4568"}, 186 | {file = "cryptg-0.4.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c60cf65e9cb0fcdf67353b003b8459e06a987933f957d47f578492527be522e5"}, 187 | {file = "cryptg-0.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4d5f4021285ec0784019a1d52e435af2f1ecfd3f05a44681410efc84f1e413b4"}, 188 | {file = "cryptg-0.4.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2a40dc8167d7a00d8ebc08784e022dcfa8919d0540c0956cf6a3b067a0233314"}, 189 | {file = "cryptg-0.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3940c533af77128ea56fa7420ee652f1d061d0de09dd8a38ac493228bf170b64"}, 190 | {file = "cryptg-0.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa16e8691d83d5c665e4bdf82a2a66257fecc5d32b60d888c95a7f5caa154d0d"}, 191 | {file = "cryptg-0.4.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f3200c8c783c11198373a1e614654df88617b4ca260700e155d7be8c3144e7b"}, 192 | {file = "cryptg-0.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:19b61547eead81a3a8aca68e30673d439fc577dedd068d86d970da923035471d"}, 193 | {file = "cryptg-0.4.0.tar.gz", hash = "sha256:38f918c685c305569d7cee3795a932e28f61e633eeac452032a76f242ae7eb69"}, 194 | ] 195 | 196 | [[package]] 197 | name = "distlib" 198 | version = "0.3.8" 199 | description = "Distribution utilities" 200 | optional = false 201 | python-versions = "*" 202 | files = [ 203 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 204 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 205 | ] 206 | 207 | [[package]] 208 | name = "filelock" 209 | version = "3.15.4" 210 | description = "A platform independent file lock." 211 | optional = false 212 | python-versions = ">=3.8" 213 | files = [ 214 | {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, 215 | {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, 216 | ] 217 | 218 | [package.extras] 219 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 220 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] 221 | typing = ["typing-extensions (>=4.8)"] 222 | 223 | [[package]] 224 | name = "flake8" 225 | version = "7.1.0" 226 | description = "the modular source code checker: pep8 pyflakes and co" 227 | optional = false 228 | python-versions = ">=3.8.1" 229 | files = [ 230 | {file = "flake8-7.1.0-py2.py3-none-any.whl", hash = "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a"}, 231 | {file = "flake8-7.1.0.tar.gz", hash = "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5"}, 232 | ] 233 | 234 | [package.dependencies] 235 | mccabe = ">=0.7.0,<0.8.0" 236 | pycodestyle = ">=2.12.0,<2.13.0" 237 | pyflakes = ">=3.2.0,<3.3.0" 238 | 239 | [[package]] 240 | name = "flake8-bugbear" 241 | version = "24.4.26" 242 | description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." 243 | optional = false 244 | python-versions = ">=3.8.1" 245 | files = [ 246 | {file = "flake8_bugbear-24.4.26-py3-none-any.whl", hash = "sha256:cb430dd86bc821d79ccc0b030789a9c87a47a369667f12ba06e80f11305e8258"}, 247 | {file = "flake8_bugbear-24.4.26.tar.gz", hash = "sha256:ff8d4ba5719019ebf98e754624c30c05cef0dadcf18a65d91c7567300e52a130"}, 248 | ] 249 | 250 | [package.dependencies] 251 | attrs = ">=19.2.0" 252 | flake8 = ">=6.0.0" 253 | 254 | [package.extras] 255 | dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "pytest", "tox"] 256 | 257 | [[package]] 258 | name = "flake8-pyproject" 259 | version = "1.2.3" 260 | description = "Flake8 plug-in loading the configuration from pyproject.toml" 261 | optional = false 262 | python-versions = ">= 3.6" 263 | files = [ 264 | {file = "flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a"}, 265 | ] 266 | 267 | [package.dependencies] 268 | Flake8 = ">=5" 269 | 270 | [package.extras] 271 | dev = ["pyTest", "pyTest-cov"] 272 | 273 | [[package]] 274 | name = "flask" 275 | version = "3.0.3" 276 | description = "A simple framework for building complex web applications." 277 | optional = false 278 | python-versions = ">=3.8" 279 | files = [ 280 | {file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"}, 281 | {file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"}, 282 | ] 283 | 284 | [package.dependencies] 285 | blinker = ">=1.6.2" 286 | click = ">=8.1.3" 287 | itsdangerous = ">=2.1.2" 288 | Jinja2 = ">=3.1.2" 289 | Werkzeug = ">=3.0.0" 290 | 291 | [package.extras] 292 | async = ["asgiref (>=3.2)"] 293 | dotenv = ["python-dotenv"] 294 | 295 | [[package]] 296 | name = "h11" 297 | version = "0.14.0" 298 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 299 | optional = false 300 | python-versions = ">=3.7" 301 | files = [ 302 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 303 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 304 | ] 305 | 306 | [[package]] 307 | name = "h2" 308 | version = "4.1.0" 309 | description = "HTTP/2 State-Machine based protocol implementation" 310 | optional = false 311 | python-versions = ">=3.6.1" 312 | files = [ 313 | {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, 314 | {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, 315 | ] 316 | 317 | [package.dependencies] 318 | hpack = ">=4.0,<5" 319 | hyperframe = ">=6.0,<7" 320 | 321 | [[package]] 322 | name = "hpack" 323 | version = "4.0.0" 324 | description = "Pure-Python HPACK header compression" 325 | optional = false 326 | python-versions = ">=3.6.1" 327 | files = [ 328 | {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, 329 | {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, 330 | ] 331 | 332 | [[package]] 333 | name = "hypercorn" 334 | version = "0.17.3" 335 | description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn" 336 | optional = false 337 | python-versions = ">=3.8" 338 | files = [ 339 | {file = "hypercorn-0.17.3-py3-none-any.whl", hash = "sha256:059215dec34537f9d40a69258d323f56344805efb462959e727152b0aa504547"}, 340 | {file = "hypercorn-0.17.3.tar.gz", hash = "sha256:1b37802ee3ac52d2d85270700d565787ab16cf19e1462ccfa9f089ca17574165"}, 341 | ] 342 | 343 | [package.dependencies] 344 | h11 = "*" 345 | h2 = ">=3.1.0" 346 | priority = "*" 347 | wsproto = ">=0.14.0" 348 | 349 | [package.extras] 350 | docs = ["pydata_sphinx_theme", "sphinxcontrib_mermaid"] 351 | h3 = ["aioquic (>=0.9.0,<1.0)"] 352 | trio = ["trio (>=0.22.0)"] 353 | uvloop = ["uvloop (>=0.18)"] 354 | 355 | [[package]] 356 | name = "hyperframe" 357 | version = "6.0.1" 358 | description = "HTTP/2 framing layer for Python" 359 | optional = false 360 | python-versions = ">=3.6.1" 361 | files = [ 362 | {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, 363 | {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, 364 | ] 365 | 366 | [[package]] 367 | name = "identify" 368 | version = "2.5.36" 369 | description = "File identification library for Python" 370 | optional = false 371 | python-versions = ">=3.8" 372 | files = [ 373 | {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, 374 | {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, 375 | ] 376 | 377 | [package.extras] 378 | license = ["ukkonen"] 379 | 380 | [[package]] 381 | name = "idna" 382 | version = "3.7" 383 | description = "Internationalized Domain Names in Applications (IDNA)" 384 | optional = false 385 | python-versions = ">=3.5" 386 | files = [ 387 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 388 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 389 | ] 390 | 391 | [[package]] 392 | name = "iso8601" 393 | version = "1.1.0" 394 | description = "Simple module to parse ISO 8601 dates" 395 | optional = false 396 | python-versions = ">=3.6.2,<4.0" 397 | files = [ 398 | {file = "iso8601-1.1.0-py3-none-any.whl", hash = "sha256:8400e90141bf792bce2634df533dc57e3bee19ea120a87bebcd3da89a58ad73f"}, 399 | {file = "iso8601-1.1.0.tar.gz", hash = "sha256:32811e7b81deee2063ea6d2e94f8819a86d1f3811e49d23623a41fa832bef03f"}, 400 | ] 401 | 402 | [[package]] 403 | name = "itsdangerous" 404 | version = "2.2.0" 405 | description = "Safely pass data to untrusted environments and back." 406 | optional = false 407 | python-versions = ">=3.8" 408 | files = [ 409 | {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, 410 | {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, 411 | ] 412 | 413 | [[package]] 414 | name = "jinja2" 415 | version = "3.1.4" 416 | description = "A very fast and expressive template engine." 417 | optional = false 418 | python-versions = ">=3.7" 419 | files = [ 420 | {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, 421 | {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, 422 | ] 423 | 424 | [package.dependencies] 425 | MarkupSafe = ">=2.0" 426 | 427 | [package.extras] 428 | i18n = ["Babel (>=2.7)"] 429 | 430 | [[package]] 431 | name = "markupsafe" 432 | version = "2.1.5" 433 | description = "Safely add untrusted strings to HTML/XML markup." 434 | optional = false 435 | python-versions = ">=3.7" 436 | files = [ 437 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, 438 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, 439 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, 440 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, 441 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, 442 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, 443 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, 444 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, 445 | {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, 446 | {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, 447 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, 448 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, 449 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, 450 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, 451 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, 452 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, 453 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, 454 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, 455 | {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, 456 | {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, 457 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, 458 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, 459 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, 460 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, 461 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, 462 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, 463 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, 464 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, 465 | {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, 466 | {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, 467 | {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, 468 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, 469 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, 470 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, 471 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, 472 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, 473 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, 474 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, 475 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, 476 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, 477 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, 478 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, 479 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, 480 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, 481 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, 482 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, 483 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, 484 | {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, 485 | {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, 486 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, 487 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, 488 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, 489 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, 490 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, 491 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, 492 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, 493 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, 494 | {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, 495 | {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, 496 | {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, 497 | ] 498 | 499 | [[package]] 500 | name = "mccabe" 501 | version = "0.7.0" 502 | description = "McCabe checker, plugin for flake8" 503 | optional = false 504 | python-versions = ">=3.6" 505 | files = [ 506 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 507 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 508 | ] 509 | 510 | [[package]] 511 | name = "nodeenv" 512 | version = "1.9.1" 513 | description = "Node.js virtual environment builder" 514 | optional = false 515 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 516 | files = [ 517 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 518 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 519 | ] 520 | 521 | [[package]] 522 | name = "platformdirs" 523 | version = "4.2.2" 524 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 525 | optional = false 526 | python-versions = ">=3.8" 527 | files = [ 528 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 529 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 530 | ] 531 | 532 | [package.extras] 533 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 534 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 535 | type = ["mypy (>=1.8)"] 536 | 537 | [[package]] 538 | name = "pre-commit" 539 | version = "3.7.1" 540 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 541 | optional = false 542 | python-versions = ">=3.9" 543 | files = [ 544 | {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, 545 | {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, 546 | ] 547 | 548 | [package.dependencies] 549 | cfgv = ">=2.0.0" 550 | identify = ">=1.0.0" 551 | nodeenv = ">=0.11.1" 552 | pyyaml = ">=5.1" 553 | virtualenv = ">=20.10.0" 554 | 555 | [[package]] 556 | name = "priority" 557 | version = "2.0.0" 558 | description = "A pure-Python implementation of the HTTP/2 priority tree" 559 | optional = false 560 | python-versions = ">=3.6.1" 561 | files = [ 562 | {file = "priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa"}, 563 | {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, 564 | ] 565 | 566 | [[package]] 567 | name = "pyaes" 568 | version = "1.6.1" 569 | description = "Pure-Python Implementation of the AES block-cipher and common modes of operation" 570 | optional = false 571 | python-versions = "*" 572 | files = [ 573 | {file = "pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"}, 574 | ] 575 | 576 | [[package]] 577 | name = "pyasn1" 578 | version = "0.6.0" 579 | description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" 580 | optional = false 581 | python-versions = ">=3.8" 582 | files = [ 583 | {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"}, 584 | {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, 585 | ] 586 | 587 | [[package]] 588 | name = "pycodestyle" 589 | version = "2.12.0" 590 | description = "Python style guide checker" 591 | optional = false 592 | python-versions = ">=3.8" 593 | files = [ 594 | {file = "pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"}, 595 | {file = "pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c"}, 596 | ] 597 | 598 | [[package]] 599 | name = "pydantic" 600 | version = "2.8.2" 601 | description = "Data validation using Python type hints" 602 | optional = false 603 | python-versions = ">=3.8" 604 | files = [ 605 | {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, 606 | {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, 607 | ] 608 | 609 | [package.dependencies] 610 | annotated-types = ">=0.4.0" 611 | pydantic-core = "2.20.1" 612 | typing-extensions = [ 613 | {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, 614 | {version = ">=4.6.1", markers = "python_version < \"3.13\""}, 615 | ] 616 | 617 | [package.extras] 618 | email = ["email-validator (>=2.0.0)"] 619 | 620 | [[package]] 621 | name = "pydantic-core" 622 | version = "2.20.1" 623 | description = "Core functionality for Pydantic validation and serialization" 624 | optional = false 625 | python-versions = ">=3.8" 626 | files = [ 627 | {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, 628 | {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, 629 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, 630 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, 631 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, 632 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, 633 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, 634 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, 635 | {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, 636 | {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, 637 | {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, 638 | {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, 639 | {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, 640 | {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, 641 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, 642 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, 643 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, 644 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, 645 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, 646 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, 647 | {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, 648 | {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, 649 | {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, 650 | {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, 651 | {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, 652 | {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, 653 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, 654 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, 655 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, 656 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, 657 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, 658 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, 659 | {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, 660 | {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, 661 | {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, 662 | {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, 663 | {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, 664 | {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, 665 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, 666 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, 667 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, 668 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, 669 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, 670 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, 671 | {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, 672 | {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, 673 | {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, 674 | {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, 675 | {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, 676 | {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, 677 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, 678 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, 679 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, 680 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, 681 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, 682 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, 683 | {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, 684 | {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, 685 | {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, 686 | {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, 687 | {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, 688 | {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, 689 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, 690 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, 691 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, 692 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, 693 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, 694 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, 695 | {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, 696 | {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, 697 | {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, 698 | {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, 699 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, 700 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, 701 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, 702 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, 703 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, 704 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, 705 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, 706 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, 707 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, 708 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, 709 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, 710 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, 711 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, 712 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, 713 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, 714 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, 715 | {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, 716 | ] 717 | 718 | [package.dependencies] 719 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 720 | 721 | [[package]] 722 | name = "pyflakes" 723 | version = "3.2.0" 724 | description = "passive checker of Python programs" 725 | optional = false 726 | python-versions = ">=3.8" 727 | files = [ 728 | {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, 729 | {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, 730 | ] 731 | 732 | [[package]] 733 | name = "pypika-tortoise" 734 | version = "0.1.6" 735 | description = "Forked from pypika and streamline just for tortoise-orm" 736 | optional = false 737 | python-versions = ">=3.7,<4.0" 738 | files = [ 739 | {file = "pypika-tortoise-0.1.6.tar.gz", hash = "sha256:d802868f479a708e3263724c7b5719a26ad79399b2a70cea065f4a4cadbebf36"}, 740 | {file = "pypika_tortoise-0.1.6-py3-none-any.whl", hash = "sha256:2d68bbb7e377673743cff42aa1059f3a80228d411fbcae591e4465e173109fd8"}, 741 | ] 742 | 743 | [[package]] 744 | name = "pypng" 745 | version = "0.20220715.0" 746 | description = "Pure Python library for saving and loading PNG images" 747 | optional = false 748 | python-versions = "*" 749 | files = [ 750 | {file = "pypng-0.20220715.0-py3-none-any.whl", hash = "sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c"}, 751 | {file = "pypng-0.20220715.0.tar.gz", hash = "sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1"}, 752 | ] 753 | 754 | [[package]] 755 | name = "pytz" 756 | version = "2024.1" 757 | description = "World timezone definitions, modern and historical" 758 | optional = false 759 | python-versions = "*" 760 | files = [ 761 | {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, 762 | {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, 763 | ] 764 | 765 | [[package]] 766 | name = "pyyaml" 767 | version = "6.0.1" 768 | description = "YAML parser and emitter for Python" 769 | optional = false 770 | python-versions = ">=3.6" 771 | files = [ 772 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 773 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 774 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 775 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 776 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 777 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 778 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 779 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 780 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 781 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 782 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 783 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 784 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 785 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 786 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 787 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 788 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 789 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 790 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, 791 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 792 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 793 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 794 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 795 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 796 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 797 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 798 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 799 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 800 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 801 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 802 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 803 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 804 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 805 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 806 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 807 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 808 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 809 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 810 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 811 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 812 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 813 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 814 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 815 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 816 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 817 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 818 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 819 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 820 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 821 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 822 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 823 | ] 824 | 825 | [[package]] 826 | name = "qrcode" 827 | version = "7.4.2" 828 | description = "QR Code image generator" 829 | optional = false 830 | python-versions = ">=3.7" 831 | files = [ 832 | {file = "qrcode-7.4.2-py3-none-any.whl", hash = "sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a"}, 833 | {file = "qrcode-7.4.2.tar.gz", hash = "sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845"}, 834 | ] 835 | 836 | [package.dependencies] 837 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 838 | pypng = "*" 839 | typing-extensions = "*" 840 | 841 | [package.extras] 842 | all = ["pillow (>=9.1.0)", "pytest", "pytest-cov", "tox", "zest.releaser[recommended]"] 843 | dev = ["pytest", "pytest-cov", "tox"] 844 | maintainer = ["zest.releaser[recommended]"] 845 | pil = ["pillow (>=9.1.0)"] 846 | test = ["coverage", "pytest"] 847 | 848 | [[package]] 849 | name = "quart" 850 | version = "0.19.6" 851 | description = "A Python ASGI web microframework with the same API as Flask" 852 | optional = false 853 | python-versions = ">=3.8" 854 | files = [ 855 | {file = "quart-0.19.6-py3-none-any.whl", hash = "sha256:f9092310f4eb120903da692a5e4354f05d48c28ca7ec3054d3d94dd862412c58"}, 856 | {file = "quart-0.19.6.tar.gz", hash = "sha256:89ddda6da24300a5ea4f21e4582d5e89bc8ea678e724e0b747767143401e4558"}, 857 | ] 858 | 859 | [package.dependencies] 860 | aiofiles = "*" 861 | blinker = ">=1.6" 862 | click = ">=8.0.0" 863 | flask = ">=3.0.0" 864 | hypercorn = ">=0.11.2" 865 | itsdangerous = "*" 866 | jinja2 = "*" 867 | markupsafe = "*" 868 | werkzeug = ">=3.0.0" 869 | 870 | [package.extras] 871 | docs = ["pydata_sphinx_theme"] 872 | dotenv = ["python-dotenv"] 873 | 874 | [[package]] 875 | name = "rsa" 876 | version = "4.9" 877 | description = "Pure-Python RSA implementation" 878 | optional = false 879 | python-versions = ">=3.6,<4" 880 | files = [ 881 | {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, 882 | {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, 883 | ] 884 | 885 | [package.dependencies] 886 | pyasn1 = ">=0.1.3" 887 | 888 | [[package]] 889 | name = "sniffio" 890 | version = "1.3.1" 891 | description = "Sniff out which async library your code is running under" 892 | optional = false 893 | python-versions = ">=3.7" 894 | files = [ 895 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 896 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 897 | ] 898 | 899 | [[package]] 900 | name = "telethon" 901 | version = "1.36.0" 902 | description = "Full-featured Telegram client library for Python 3" 903 | optional = false 904 | python-versions = ">=3.5" 905 | files = [ 906 | {file = "Telethon-1.36.0.tar.gz", hash = "sha256:11db5c7ed7e37f1272d443fb7eea0f1db580d56c6949165233946fb323aaf3a7"}, 907 | ] 908 | 909 | [package.dependencies] 910 | pyaes = "*" 911 | rsa = "*" 912 | 913 | [package.extras] 914 | cryptg = ["cryptg"] 915 | 916 | [[package]] 917 | name = "tortoise-orm" 918 | version = "0.21.4" 919 | description = "Easy async ORM for python, built with relations in mind" 920 | optional = false 921 | python-versions = "<4.0,>=3.8" 922 | files = [ 923 | {file = "tortoise_orm-0.21.4-py3-none-any.whl", hash = "sha256:2ade41ae1d9c7279adb9c69199357cb1dce9380c4c880c32713f99c1e057ce5f"}, 924 | {file = "tortoise_orm-0.21.4.tar.gz", hash = "sha256:1d6a4ed450ac831e5f520c369c6ba8e0ff947c8e34b4edd40ba8a94c4efcd0aa"}, 925 | ] 926 | 927 | [package.dependencies] 928 | aiosqlite = ">=0.16.0,<0.18.0" 929 | iso8601 = ">=1.0.2,<2.0.0" 930 | pydantic = ">=2.0,<2.7.0 || >2.7.0,<3.0" 931 | pypika-tortoise = ">=0.1.6,<0.2.0" 932 | pytz = "*" 933 | 934 | [package.extras] 935 | accel = ["ciso8601", "orjson", "uvloop"] 936 | aiomysql = ["aiomysql"] 937 | asyncmy = ["asyncmy (>=0.2.8,<0.3.0)"] 938 | asyncodbc = ["asyncodbc (>=0.1.1,<0.2.0)"] 939 | asyncpg = ["asyncpg"] 940 | psycopg = ["psycopg[binary,pool] (>=3.0.12,<4.0.0)"] 941 | 942 | [[package]] 943 | name = "typing-extensions" 944 | version = "4.12.2" 945 | description = "Backported and Experimental Type Hints for Python 3.8+" 946 | optional = false 947 | python-versions = ">=3.8" 948 | files = [ 949 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 950 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 951 | ] 952 | 953 | [[package]] 954 | name = "virtualenv" 955 | version = "20.26.3" 956 | description = "Virtual Python Environment builder" 957 | optional = false 958 | python-versions = ">=3.7" 959 | files = [ 960 | {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, 961 | {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, 962 | ] 963 | 964 | [package.dependencies] 965 | distlib = ">=0.3.7,<1" 966 | filelock = ">=3.12.2,<4" 967 | platformdirs = ">=3.9.1,<5" 968 | 969 | [package.extras] 970 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 971 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 972 | 973 | [[package]] 974 | name = "werkzeug" 975 | version = "3.0.3" 976 | description = "The comprehensive WSGI web application library." 977 | optional = false 978 | python-versions = ">=3.8" 979 | files = [ 980 | {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"}, 981 | {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"}, 982 | ] 983 | 984 | [package.dependencies] 985 | MarkupSafe = ">=2.1.1" 986 | 987 | [package.extras] 988 | watchdog = ["watchdog (>=2.3)"] 989 | 990 | [[package]] 991 | name = "wsproto" 992 | version = "1.2.0" 993 | description = "WebSockets state-machine based protocol implementation" 994 | optional = false 995 | python-versions = ">=3.7.0" 996 | files = [ 997 | {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, 998 | {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, 999 | ] 1000 | 1001 | [package.dependencies] 1002 | h11 = ">=0.9.0,<1" 1003 | 1004 | [metadata] 1005 | lock-version = "2.0" 1006 | python-versions = "^3.12" 1007 | content-hash = "bc18f5021d5e8de103274eb936a08bc3dd03f15c82c6d33c24e827b32230f071" 1008 | --------------------------------------------------------------------------------