├── 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 += '
'.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 |
--------------------------------------------------------------------------------