├── .dockerignore ├── .gitignore ├── Makefile ├── README.md ├── bot ├── Dockerfile ├── alembic.ini ├── alembic │ ├── README │ ├── env.py │ ├── script.py.mako │ └── versions │ │ └── ecf84257431a_.py ├── client.py ├── make_migrations.sh ├── poetry.lock ├── pyproject.toml └── src │ ├── __init__.py │ ├── manager.py │ ├── models.py │ └── utils.py └── docker-compose.yml /.dockerignore: -------------------------------------------------------------------------------- 1 | resources/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | resources/ 2 | .env 3 | 4 | __pycache__/ 5 | .idea/ -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @echo "make start - Run all services" 3 | @echo "make down - Shut down all services" 4 | @echo "make restart - Remove all data and start from scratch" 5 | @echo "make volume - Create volume for grafana" 6 | @exit 0 7 | 8 | start: 9 | docker-compose up -d --build 10 | 11 | down: 12 | docker-compose down 13 | 14 | restart: 15 | docker-compose down && rm -rf resources && docker-compose up -d --build 16 | 17 | volume: 18 | docker volume create --name=grafana-data -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram bot project template 2 | 3 | This repo contains boilerplate code and infrastructure provisioning for development of telegram bot. 4 | 5 | ## Services 6 | - **Bot**: Entrypoint and business logic of application 7 | - **Postgres**: Database 8 | - **Minio**: Storage for different files (images, videos, pdf, etc.) 9 | - **PgAdmin**: Administration of Postgres 10 | - **Grafana**: Visualization of application metrics 11 | 12 | ## Automation 13 | - **Infrastructure**: All services reside inside docker containers and managed by docker-compose. 14 | - **Database migrations**: New migrations are created and applied by SqlAlchemy and alembic. 15 | - **Grafana**: Datasource and dashboard are provisioned with YAML files. 16 | 17 | # Getting started 18 | Create `.env` file and add a bot TOKEN from https://t.me/BotFather 19 | ``` 20 | TOKEN="Your secret token" 21 | POSTGRES_USER="postgresuser" 22 | POSTGRES_PASSWORD="postgrespass" 23 | MINIO_ROOT_USER="minio" 24 | MINIO_ROOT_PASSWORD="minio123" 25 | PROD=false 26 | ``` 27 | 28 | Create volume for Grafana 29 | ``` 30 | make volume 31 | ``` 32 | 33 | ### Launch all services: 34 | ``` 35 | make start 36 | ``` 37 | 38 | ### See all commands: 39 | ``` 40 | make 41 | ``` 42 | 43 | # Monitoring 44 | 45 | - Grafana: http://localhost:3000 46 | - Minio console: http://localhost:9001 47 | - PgAdmin: http://localhost:5050 48 | -------------------------------------------------------------------------------- /bot/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | RUN pip install -U pip 4 | RUN pip install poetry 5 | 6 | WORKDIR /bot 7 | 8 | COPY poetry.lock pyproject.toml /bot/ 9 | RUN poetry config virtualenvs.create false \ 10 | && poetry install --no-dev --no-interaction --no-ansi 11 | 12 | COPY . . 13 | 14 | CMD python admin.py -------------------------------------------------------------------------------- /bot/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # sys.path path, will be prepended to sys.path if present. 11 | # defaults to the current working directory. 12 | prepend_sys_path = . 13 | 14 | # timezone to use when rendering the date within the migration file 15 | # as well as the filename. 16 | # If specified, requires the python-dateutil library that can be 17 | # installed by adding `alembic[tz]` to the pip requirements 18 | # string value is passed to dateutil.tz.gettz() 19 | # leave blank for localtime 20 | # timezone = 21 | 22 | # max length of characters to apply to the 23 | # "slug" field 24 | # truncate_slug_length = 40 25 | 26 | # set to 'true' to run the environment during 27 | # the 'revision' command, regardless of autogenerate 28 | # revision_environment = false 29 | 30 | # set to 'true' to allow .pyc and .pyo files without 31 | # a source .py file to be detected as revisions in the 32 | # versions/ directory 33 | # sourceless = false 34 | 35 | # version location specification; This defaults 36 | # to alembic/versions. When using multiple version 37 | # directories, initial revisions must be specified with --version-path. 38 | # The path separator used here should be the separator specified by "version_path_separator" below. 39 | # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions 40 | 41 | # version path separator; As mentioned above, this is the character used to split 42 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 43 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 44 | # Valid values for version_path_separator are: 45 | # 46 | # version_path_separator = : 47 | # version_path_separator = ; 48 | # version_path_separator = space 49 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 50 | 51 | # the output encoding used when revision files 52 | # are written from script.py.mako 53 | # output_encoding = utf-8 54 | 55 | sqlalchemy.url = driver://user:pass@localhost/dbname 56 | 57 | 58 | [post_write_hooks] 59 | # post_write_hooks defines scripts or Python functions that are run 60 | # on newly generated revision scripts. See the documentation for further 61 | # detail and examples 62 | 63 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 64 | # hooks = black 65 | # black.type = console_scripts 66 | # black.entrypoint = black 67 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 68 | 69 | # Logging configuration 70 | [loggers] 71 | keys = root,sqlalchemy,alembic 72 | 73 | [handlers] 74 | keys = console 75 | 76 | [formatters] 77 | keys = generic 78 | 79 | [logger_root] 80 | level = WARN 81 | handlers = console 82 | qualname = 83 | 84 | [logger_sqlalchemy] 85 | level = WARN 86 | handlers = 87 | qualname = sqlalchemy.engine 88 | 89 | [logger_alembic] 90 | level = INFO 91 | handlers = 92 | qualname = alembic 93 | 94 | [handler_console] 95 | class = StreamHandler 96 | args = (sys.stderr,) 97 | level = NOTSET 98 | formatter = generic 99 | 100 | [formatter_generic] 101 | format = %(levelname)-5.5s [%(name)s] %(message)s 102 | datefmt = %H:%M:%S 103 | -------------------------------------------------------------------------------- /bot/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. 2 | 3 | alembic revision --autogenerate -m "comment" -------------------------------------------------------------------------------- /bot/alembic/env.py: -------------------------------------------------------------------------------- 1 | import os 2 | from logging.config import fileConfig 3 | 4 | from alembic import context 5 | from sqlalchemy import engine_from_config 6 | from sqlalchemy import pool 7 | 8 | from src.models import Base 9 | 10 | # this is the Alembic Config object, which provides 11 | # access to the values within the .ini file in use. 12 | config = context.config 13 | 14 | # Interpret the config file for Python logging. 15 | # This line sets up loggers basically. 16 | fileConfig(config.config_file_name) 17 | config.set_main_option('sqlalchemy.url', os.environ['DB_URL']) 18 | 19 | # add your model's MetaData object here 20 | # for 'autogenerate' support 21 | # from myapp import mymodel 22 | # target_metadata = mymodel.Base.metadata 23 | target_metadata = Base.metadata 24 | 25 | 26 | # other values from the config, defined by the needs of env.py, 27 | # can be acquired: 28 | # my_important_option = config.get_main_option("my_important_option") 29 | # ... etc. 30 | 31 | 32 | def run_migrations_offline(): 33 | """Run migrations in 'offline' mode. 34 | 35 | This configures the context with just a URL 36 | and not an Engine, though an Engine is acceptable 37 | here as well. By skipping the Engine creation 38 | we don't even need a DBAPI to be available. 39 | 40 | Calls to context.execute() here emit the given string to the 41 | script output. 42 | 43 | """ 44 | url = config.get_main_option("sqlalchemy.url") 45 | context.configure( 46 | url=url, 47 | target_metadata=target_metadata, 48 | literal_binds=True, 49 | dialect_opts={"paramstyle": "named"}, 50 | compare_type=True, 51 | ) 52 | 53 | with context.begin_transaction(): 54 | context.run_migrations() 55 | 56 | 57 | def run_migrations_online(): 58 | """Run migrations in 'online' mode. 59 | 60 | In this scenario we need to create an Engine 61 | and associate a connection with the context. 62 | 63 | """ 64 | connectable = engine_from_config( 65 | config.get_section(config.config_ini_section), 66 | prefix="sqlalchemy.", 67 | poolclass=pool.NullPool, 68 | ) 69 | 70 | with connectable.connect() as connection: 71 | context.configure( 72 | connection=connection, 73 | target_metadata=target_metadata, 74 | compare_type=True, 75 | ) 76 | 77 | with context.begin_transaction(): 78 | context.run_migrations() 79 | 80 | 81 | if context.is_offline_mode(): 82 | run_migrations_offline() 83 | else: 84 | run_migrations_online() 85 | -------------------------------------------------------------------------------- /bot/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /bot/alembic/versions/ecf84257431a_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: ecf84257431a 4 | Revises: 5 | Create Date: 2022-06-25 22:25:45.483257 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'ecf84257431a' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('admins', 22 | sa.Column('id', sa.BigInteger(), nullable=False), 23 | sa.Column('fullname', sa.String(), nullable=True), 24 | sa.Column('username', sa.String(), nullable=True), 25 | sa.Column('registration_datetime', sa.DateTime(), server_default=sa.text('now()'), nullable=True), 26 | sa.PrimaryKeyConstraint('id') 27 | ) 28 | op.create_index(op.f('ix_admins_id'), 'admins', ['id'], unique=False) 29 | op.create_table('groups', 30 | sa.Column('id', sa.BigInteger(), nullable=False), 31 | sa.Column('name', sa.String(length=64), nullable=True), 32 | sa.Column('group_type', sa.Enum('reading', 'sleeping', name='grouptype'), nullable=True), 33 | sa.Column('invite', sa.BigInteger(), nullable=True), 34 | sa.Column('channel_id', sa.BigInteger(), nullable=True), 35 | sa.Column('deposit', sa.Integer(), nullable=True), 36 | sa.Column('rest_day_price_to_bank', sa.Integer(), nullable=True), 37 | sa.Column('start_date', sa.Date(), nullable=True), 38 | sa.Column('creation_datetime', sa.DateTime(), server_default=sa.text('now()'), nullable=True), 39 | sa.PrimaryKeyConstraint('id'), 40 | sa.UniqueConstraint('channel_id') 41 | ) 42 | op.create_index(op.f('ix_groups_id'), 'groups', ['id'], unique=False) 43 | op.create_index(op.f('ix_groups_invite'), 'groups', ['invite'], unique=True) 44 | op.create_index(op.f('ix_groups_name'), 'groups', ['name'], unique=True) 45 | op.create_table('users', 46 | sa.Column('id', sa.BigInteger(), nullable=False), 47 | sa.Column('fullname', sa.String(), nullable=True), 48 | sa.Column('username', sa.String(), nullable=True), 49 | sa.Column('registration_datetime', sa.DateTime(), server_default=sa.text('now()'), nullable=True), 50 | sa.PrimaryKeyConstraint('id') 51 | ) 52 | op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False) 53 | op.create_table('administration_relation', 54 | sa.Column('group_id', sa.BigInteger(), nullable=False), 55 | sa.Column('admin_id', sa.BigInteger(), nullable=False), 56 | sa.ForeignKeyConstraint(['admin_id'], ['admins.id'], ), 57 | sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ), 58 | sa.PrimaryKeyConstraint('group_id', 'admin_id') 59 | ) 60 | op.create_index(op.f('ix_administration_relation_admin_id'), 'administration_relation', ['admin_id'], unique=False) 61 | op.create_index(op.f('ix_administration_relation_group_id'), 'administration_relation', ['group_id'], unique=False) 62 | op.create_table('kicked_relation', 63 | sa.Column('user_id', sa.BigInteger(), nullable=False), 64 | sa.Column('group_id', sa.BigInteger(), nullable=False), 65 | sa.Column('kick_datetime', sa.DateTime(), server_default=sa.text('now()'), nullable=True), 66 | sa.Column('kick_day', sa.Integer(), nullable=True), 67 | sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ), 68 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 69 | sa.PrimaryKeyConstraint('user_id', 'group_id') 70 | ) 71 | op.create_table('participation_relation', 72 | sa.Column('user_id', sa.BigInteger(), nullable=False), 73 | sa.Column('group_id', sa.BigInteger(), nullable=False), 74 | sa.Column('entered_datetime', sa.DateTime(), server_default=sa.text('now()'), nullable=True), 75 | sa.Column('participation_details', sa.Text(), nullable=True), 76 | sa.Column('attempts_bought', sa.Integer(), nullable=True), 77 | sa.Column('notification_time', sa.Time(), nullable=True), 78 | sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ), 79 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 80 | sa.PrimaryKeyConstraint('user_id', 'group_id') 81 | ) 82 | op.create_index(op.f('ix_participation_relation_group_id'), 'participation_relation', ['group_id'], unique=False) 83 | op.create_index(op.f('ix_participation_relation_user_id'), 'participation_relation', ['user_id'], unique=False) 84 | op.create_table('reports', 85 | sa.Column('id', sa.BigInteger(), nullable=False), 86 | sa.Column('sender', sa.BigInteger(), nullable=True), 87 | sa.Column('group', sa.BigInteger(), nullable=True), 88 | sa.Column('tg_msg_id', sa.Integer(), nullable=True), 89 | sa.Column('approved', sa.Boolean(), nullable=True), 90 | sa.Column('day', sa.Integer(), nullable=True), 91 | sa.Column('sent_datetime', sa.DateTime(), server_default=sa.text('now()'), nullable=True), 92 | sa.ForeignKeyConstraint(['group'], ['groups.id'], ), 93 | sa.ForeignKeyConstraint(['sender'], ['users.id'], ), 94 | sa.PrimaryKeyConstraint('id') 95 | ) 96 | op.create_index(op.f('ix_reports_group'), 'reports', ['group'], unique=False) 97 | op.create_index(op.f('ix_reports_id'), 'reports', ['id'], unique=False) 98 | op.create_index(op.f('ix_reports_sender'), 'reports', ['sender'], unique=False) 99 | op.create_index(op.f('ix_reports_sent_datetime'), 'reports', ['sent_datetime'], unique=False) 100 | op.create_index(op.f('ix_reports_tg_msg_id'), 'reports', ['tg_msg_id'], unique=False) 101 | # ### end Alembic commands ### 102 | 103 | 104 | def downgrade(): 105 | # ### commands auto generated by Alembic - please adjust! ### 106 | op.drop_index(op.f('ix_reports_tg_msg_id'), table_name='reports') 107 | op.drop_index(op.f('ix_reports_sent_datetime'), table_name='reports') 108 | op.drop_index(op.f('ix_reports_sender'), table_name='reports') 109 | op.drop_index(op.f('ix_reports_id'), table_name='reports') 110 | op.drop_index(op.f('ix_reports_group'), table_name='reports') 111 | op.drop_table('reports') 112 | op.drop_index(op.f('ix_participation_relation_user_id'), table_name='participation_relation') 113 | op.drop_index(op.f('ix_participation_relation_group_id'), table_name='participation_relation') 114 | op.drop_table('participation_relation') 115 | op.drop_table('kicked_relation') 116 | op.drop_index(op.f('ix_administration_relation_group_id'), table_name='administration_relation') 117 | op.drop_index(op.f('ix_administration_relation_admin_id'), table_name='administration_relation') 118 | op.drop_table('administration_relation') 119 | op.drop_index(op.f('ix_users_id'), table_name='users') 120 | op.drop_table('users') 121 | op.drop_index(op.f('ix_groups_name'), table_name='groups') 122 | op.drop_index(op.f('ix_groups_invite'), table_name='groups') 123 | op.drop_index(op.f('ix_groups_id'), table_name='groups') 124 | op.drop_table('groups') 125 | op.drop_index(op.f('ix_admins_id'), table_name='admins') 126 | op.drop_table('admins') 127 | # ### end Alembic commands ### 128 | -------------------------------------------------------------------------------- /bot/client.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import os 3 | 4 | from sqlalchemy import create_engine 5 | from telegram import Update, Bot 6 | from telegram.ext import ( 7 | Updater, CallbackContext, CommandHandler, PicklePersistence, ConversationHandler, MessageHandler, Filters 8 | ) 9 | 10 | from src.manager import Manager 11 | from src.utils import get_logger, load_cities, markup_keyboard 12 | 13 | 14 | class ConvState(enum.Enum): 15 | pass 16 | 17 | 18 | def start(update: Update, context: CallbackContext) -> ConvState: 19 | user = update.effective_user 20 | 21 | if not manager.is_user_registered(user_id=user.id): 22 | # Register the user 23 | manager.register_user( 24 | user_id=user.id, 25 | fullname=user.full_name, 26 | username=user.username, 27 | ) 28 | 29 | user.send_message("""Это бот Отчетыватель 30 | 31 | - Принимает ежедневные отчеты 32 | - Можно настроить напоминания""" 33 | ) 34 | # TODO timezone 35 | 36 | # TODO invitation 37 | 38 | return 39 | 40 | 41 | def main_menu(update: Update, context: CallbackContext) -> ConvState: 42 | user = update.effective_user 43 | 44 | groups_names = manager.get_user_groups(user_id=user.id) 45 | keyboard = markup_keyboard( 46 | buttons=[[group_name] for group_name in groups_names], 47 | ) 48 | user.send_message( 49 | text="Выбери Хаб:", 50 | reply_markup=keyboard, 51 | ) 52 | 53 | return ConversationHandler.END 54 | 55 | 56 | ############ 57 | # Handlers # 58 | ############ 59 | 60 | # noinspection PyTypeChecker 61 | def build_conv_handler(): 62 | conv_handler = ConversationHandler( 63 | entry_points=[ 64 | ], 65 | states={ 66 | }, 67 | fallbacks=[ 68 | MessageHandler(Filters.all, main_menu), 69 | ], 70 | ) 71 | return conv_handler 72 | 73 | 74 | def add_handlers(dispatcher): 75 | dispatcher.add_handler(build_conv_handler()) 76 | 77 | handlers = [ 78 | CommandHandler("start", start), 79 | 80 | MessageHandler(Filters.all, main_menu), 81 | ] 82 | 83 | for handler in handlers: 84 | dispatcher.add_handler(handler) 85 | 86 | 87 | def main(admin_token: str, persistence_filename: str): 88 | logger.info(admin_token) 89 | persistence = PicklePersistence(filename=persistence_filename) 90 | updater = Updater(admin_token, persistence=persistence) 91 | add_handlers(dispatcher=updater.dispatcher) 92 | 93 | logger.info("Start working!") 94 | updater.start_polling(drop_pending_updates=True) 95 | updater.idle() 96 | 97 | 98 | if __name__ == '__main__': 99 | is_prod = os.getenv("PROD", "false").lower() == "true" 100 | logger = get_logger(is_prod, file_path="bot_data/admin_bot.log") 101 | 102 | cities = load_cities('cities.csv') 103 | 104 | engine = create_engine(os.getenv('DB_URL')) 105 | manager = Manager(engine) 106 | 107 | client_bot = Bot(os.environ['CLIENT_TOKEN']) 108 | 109 | main( 110 | admin_token=os.environ['ADMIN_TOKEN'], 111 | persistence_filename=os.getenv('PERSISTENCE', 'bot_data/admin_bot_persistence.data'), 112 | ) 113 | -------------------------------------------------------------------------------- /bot/make_migrations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$PROD" != "true" ] 4 | then 5 | echo "Generating migrations files..." 6 | alembic upgrade head 7 | alembic revision --autogenerate 8 | fi 9 | 10 | echo "Applying migrations..." 11 | alembic upgrade head 12 | 13 | echo "Migrations done!" 14 | -------------------------------------------------------------------------------- /bot/poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "alembic" 3 | version = "1.7.7" 4 | description = "A database migration tool for SQLAlchemy." 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [package.dependencies] 10 | Mako = "*" 11 | SQLAlchemy = ">=1.3.0" 12 | 13 | [package.extras] 14 | tz = ["python-dateutil"] 15 | 16 | [[package]] 17 | name = "appnope" 18 | version = "0.1.3" 19 | description = "Disable App Nap on macOS >= 10.9" 20 | category = "dev" 21 | optional = false 22 | python-versions = "*" 23 | 24 | [[package]] 25 | name = "apscheduler" 26 | version = "3.6.3" 27 | description = "In-process task scheduler with Cron-like capabilities" 28 | category = "main" 29 | optional = false 30 | python-versions = "*" 31 | 32 | [package.dependencies] 33 | pytz = "*" 34 | six = ">=1.4.0" 35 | tzlocal = ">=1.2" 36 | 37 | [package.extras] 38 | asyncio = ["trollius"] 39 | doc = ["sphinx", "sphinx-rtd-theme"] 40 | gevent = ["gevent"] 41 | mongodb = ["pymongo (>=2.8)"] 42 | redis = ["redis (>=3.0)"] 43 | rethinkdb = ["rethinkdb (>=2.4.0)"] 44 | sqlalchemy = ["sqlalchemy (>=0.8)"] 45 | testing = ["pytest", "pytest-cov", "pytest-tornado5", "mock", "pytest-asyncio (<0.6)", "pytest-asyncio"] 46 | tornado = ["tornado (>=4.3)"] 47 | twisted = ["twisted"] 48 | zookeeper = ["kazoo"] 49 | 50 | [[package]] 51 | name = "asttokens" 52 | version = "2.0.5" 53 | description = "Annotate AST trees with source code positions" 54 | category = "dev" 55 | optional = false 56 | python-versions = "*" 57 | 58 | [package.dependencies] 59 | six = "*" 60 | 61 | [package.extras] 62 | test = ["astroid", "pytest"] 63 | 64 | [[package]] 65 | name = "backcall" 66 | version = "0.2.0" 67 | description = "Specifications for callback functions passed in to an API" 68 | category = "dev" 69 | optional = false 70 | python-versions = "*" 71 | 72 | [[package]] 73 | name = "black" 74 | version = "22.3.0" 75 | description = "The uncompromising code formatter." 76 | category = "dev" 77 | optional = false 78 | python-versions = ">=3.6.2" 79 | 80 | [package.dependencies] 81 | click = ">=8.0.0" 82 | mypy-extensions = ">=0.4.3" 83 | pathspec = ">=0.9.0" 84 | platformdirs = ">=2" 85 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 86 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 87 | 88 | [package.extras] 89 | colorama = ["colorama (>=0.4.3)"] 90 | d = ["aiohttp (>=3.7.4)"] 91 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 92 | uvloop = ["uvloop (>=0.15.2)"] 93 | 94 | [[package]] 95 | name = "cachetools" 96 | version = "4.2.2" 97 | description = "Extensible memoizing collections and decorators" 98 | category = "main" 99 | optional = false 100 | python-versions = "~=3.5" 101 | 102 | [[package]] 103 | name = "certifi" 104 | version = "2021.10.8" 105 | description = "Python package for providing Mozilla's CA Bundle." 106 | category = "main" 107 | optional = false 108 | python-versions = "*" 109 | 110 | [[package]] 111 | name = "click" 112 | version = "8.1.2" 113 | description = "Composable command line interface toolkit" 114 | category = "dev" 115 | optional = false 116 | python-versions = ">=3.7" 117 | 118 | [package.dependencies] 119 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 120 | 121 | [[package]] 122 | name = "colorama" 123 | version = "0.4.4" 124 | description = "Cross-platform colored terminal text." 125 | category = "main" 126 | optional = false 127 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 128 | 129 | [[package]] 130 | name = "decorator" 131 | version = "5.1.1" 132 | description = "Decorators for Humans" 133 | category = "dev" 134 | optional = false 135 | python-versions = ">=3.5" 136 | 137 | [[package]] 138 | name = "executing" 139 | version = "0.8.3" 140 | description = "Get the currently executing AST node of a frame, and other information" 141 | category = "dev" 142 | optional = false 143 | python-versions = "*" 144 | 145 | [[package]] 146 | name = "greenlet" 147 | version = "1.1.2" 148 | description = "Lightweight in-process concurrent programming" 149 | category = "main" 150 | optional = false 151 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" 152 | 153 | [package.extras] 154 | docs = ["sphinx"] 155 | 156 | [[package]] 157 | name = "ipython" 158 | version = "8.2.0" 159 | description = "IPython: Productive Interactive Computing" 160 | category = "dev" 161 | optional = false 162 | python-versions = ">=3.8" 163 | 164 | [package.dependencies] 165 | appnope = {version = "*", markers = "sys_platform == \"darwin\""} 166 | backcall = "*" 167 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 168 | decorator = "*" 169 | jedi = ">=0.16" 170 | matplotlib-inline = "*" 171 | pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} 172 | pickleshare = "*" 173 | prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" 174 | pygments = ">=2.4.0" 175 | stack-data = "*" 176 | traitlets = ">=5" 177 | 178 | [package.extras] 179 | all = ["black", "Sphinx (>=1.3)", "ipykernel", "nbconvert", "nbformat", "ipywidgets", "notebook", "ipyparallel", "qtconsole", "pytest (<7.1)", "pytest-asyncio", "testpath", "curio", "matplotlib (!=3.2.0)", "numpy (>=1.19)", "pandas", "trio"] 180 | black = ["black"] 181 | doc = ["Sphinx (>=1.3)"] 182 | kernel = ["ipykernel"] 183 | nbconvert = ["nbconvert"] 184 | nbformat = ["nbformat"] 185 | notebook = ["ipywidgets", "notebook"] 186 | parallel = ["ipyparallel"] 187 | qtconsole = ["qtconsole"] 188 | test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] 189 | test_extra = ["pytest (<7.1)", "pytest-asyncio", "testpath", "curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.19)", "pandas", "trio"] 190 | 191 | [[package]] 192 | name = "jedi" 193 | version = "0.18.1" 194 | description = "An autocompletion tool for Python that can be used for text editors." 195 | category = "dev" 196 | optional = false 197 | python-versions = ">=3.6" 198 | 199 | [package.dependencies] 200 | parso = ">=0.8.0,<0.9.0" 201 | 202 | [package.extras] 203 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 204 | testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"] 205 | 206 | [[package]] 207 | name = "loguru" 208 | version = "0.6.0" 209 | description = "Python logging made (stupidly) simple" 210 | category = "main" 211 | optional = false 212 | python-versions = ">=3.5" 213 | 214 | [package.dependencies] 215 | colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} 216 | win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} 217 | 218 | [package.extras] 219 | dev = ["colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "tox (>=3.9.0)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "black (>=19.10b0)", "isort (>=5.1.1)", "Sphinx (>=4.1.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)"] 220 | 221 | [[package]] 222 | name = "mako" 223 | version = "1.2.0" 224 | description = "A super-fast templating language that borrows the best ideas from the existing templating languages." 225 | category = "main" 226 | optional = false 227 | python-versions = ">=3.7" 228 | 229 | [package.dependencies] 230 | MarkupSafe = ">=0.9.2" 231 | 232 | [package.extras] 233 | babel = ["babel"] 234 | lingua = ["lingua"] 235 | testing = ["pytest"] 236 | 237 | [[package]] 238 | name = "markupsafe" 239 | version = "2.1.1" 240 | description = "Safely add untrusted strings to HTML/XML markup." 241 | category = "main" 242 | optional = false 243 | python-versions = ">=3.7" 244 | 245 | [[package]] 246 | name = "matplotlib-inline" 247 | version = "0.1.3" 248 | description = "Inline Matplotlib backend for Jupyter" 249 | category = "dev" 250 | optional = false 251 | python-versions = ">=3.5" 252 | 253 | [package.dependencies] 254 | traitlets = "*" 255 | 256 | [[package]] 257 | name = "minio" 258 | version = "7.1.6" 259 | description = "MinIO Python SDK for Amazon S3 Compatible Cloud Storage" 260 | category = "main" 261 | optional = false 262 | python-versions = "*" 263 | 264 | [package.dependencies] 265 | certifi = "*" 266 | urllib3 = "*" 267 | 268 | [[package]] 269 | name = "mypy-extensions" 270 | version = "0.4.3" 271 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 272 | category = "dev" 273 | optional = false 274 | python-versions = "*" 275 | 276 | [[package]] 277 | name = "parso" 278 | version = "0.8.3" 279 | description = "A Python Parser" 280 | category = "dev" 281 | optional = false 282 | python-versions = ">=3.6" 283 | 284 | [package.extras] 285 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 286 | testing = ["docopt", "pytest (<6.0.0)"] 287 | 288 | [[package]] 289 | name = "pathspec" 290 | version = "0.9.0" 291 | description = "Utility library for gitignore style pattern matching of file paths." 292 | category = "dev" 293 | optional = false 294 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 295 | 296 | [[package]] 297 | name = "pexpect" 298 | version = "4.8.0" 299 | description = "Pexpect allows easy control of interactive console applications." 300 | category = "dev" 301 | optional = false 302 | python-versions = "*" 303 | 304 | [package.dependencies] 305 | ptyprocess = ">=0.5" 306 | 307 | [[package]] 308 | name = "pickleshare" 309 | version = "0.7.5" 310 | description = "Tiny 'shelve'-like database with concurrency support" 311 | category = "dev" 312 | optional = false 313 | python-versions = "*" 314 | 315 | [[package]] 316 | name = "platformdirs" 317 | version = "2.5.1" 318 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 319 | category = "dev" 320 | optional = false 321 | python-versions = ">=3.7" 322 | 323 | [package.extras] 324 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 325 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 326 | 327 | [[package]] 328 | name = "prompt-toolkit" 329 | version = "3.0.29" 330 | description = "Library for building powerful interactive command lines in Python" 331 | category = "dev" 332 | optional = false 333 | python-versions = ">=3.6.2" 334 | 335 | [package.dependencies] 336 | wcwidth = "*" 337 | 338 | [[package]] 339 | name = "psycopg2-binary" 340 | version = "2.9.3" 341 | description = "psycopg2 - Python-PostgreSQL Database Adapter" 342 | category = "main" 343 | optional = false 344 | python-versions = ">=3.6" 345 | 346 | [[package]] 347 | name = "ptyprocess" 348 | version = "0.7.0" 349 | description = "Run a subprocess in a pseudo terminal" 350 | category = "dev" 351 | optional = false 352 | python-versions = "*" 353 | 354 | [[package]] 355 | name = "pure-eval" 356 | version = "0.2.2" 357 | description = "Safely evaluate AST nodes without side effects" 358 | category = "dev" 359 | optional = false 360 | python-versions = "*" 361 | 362 | [package.extras] 363 | tests = ["pytest"] 364 | 365 | [[package]] 366 | name = "pygments" 367 | version = "2.11.2" 368 | description = "Pygments is a syntax highlighting package written in Python." 369 | category = "dev" 370 | optional = false 371 | python-versions = ">=3.5" 372 | 373 | [[package]] 374 | name = "python-telegram-bot" 375 | version = "13.9" 376 | description = "We have made you a wrapper you can't refuse" 377 | category = "main" 378 | optional = false 379 | python-versions = ">=3.6" 380 | 381 | [package.dependencies] 382 | APScheduler = "3.6.3" 383 | cachetools = "4.2.2" 384 | certifi = "*" 385 | pytz = ">=2018.6" 386 | tornado = ">=6.1" 387 | 388 | [package.extras] 389 | json = ["ujson"] 390 | passport = ["cryptography (!=3.4,!=3.4.1,!=3.4.2,!=3.4.3)"] 391 | socks = ["pysocks"] 392 | 393 | [[package]] 394 | name = "pytz" 395 | version = "2022.1" 396 | description = "World timezone definitions, modern and historical" 397 | category = "main" 398 | optional = false 399 | python-versions = "*" 400 | 401 | [[package]] 402 | name = "pytz-deprecation-shim" 403 | version = "0.1.0.post0" 404 | description = "Shims to make deprecation of pytz easier" 405 | category = "main" 406 | optional = false 407 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" 408 | 409 | [package.dependencies] 410 | tzdata = {version = "*", markers = "python_version >= \"3.6\""} 411 | 412 | [[package]] 413 | name = "six" 414 | version = "1.16.0" 415 | description = "Python 2 and 3 compatibility utilities" 416 | category = "main" 417 | optional = false 418 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 419 | 420 | [[package]] 421 | name = "sqlalchemy" 422 | version = "1.4.28" 423 | description = "Database Abstraction Library" 424 | category = "main" 425 | optional = false 426 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" 427 | 428 | [package.dependencies] 429 | greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} 430 | 431 | [package.extras] 432 | aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] 433 | aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"] 434 | asyncio = ["greenlet (!=0.4.17)"] 435 | asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3)"] 436 | mariadb_connector = ["mariadb (>=1.0.1)"] 437 | mssql = ["pyodbc"] 438 | mssql_pymssql = ["pymssql"] 439 | mssql_pyodbc = ["pyodbc"] 440 | mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"] 441 | mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"] 442 | mysql_connector = ["mysql-connector-python"] 443 | oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"] 444 | postgresql = ["psycopg2 (>=2.7)"] 445 | postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"] 446 | postgresql_pg8000 = ["pg8000 (>=1.16.6)"] 447 | postgresql_psycopg2binary = ["psycopg2-binary"] 448 | postgresql_psycopg2cffi = ["psycopg2cffi"] 449 | pymysql = ["pymysql (<1)", "pymysql"] 450 | sqlcipher = ["sqlcipher3-binary"] 451 | 452 | [[package]] 453 | name = "stack-data" 454 | version = "0.2.0" 455 | description = "Extract data from python stack frames and tracebacks for informative displays" 456 | category = "dev" 457 | optional = false 458 | python-versions = "*" 459 | 460 | [package.dependencies] 461 | asttokens = "*" 462 | executing = "*" 463 | pure-eval = "*" 464 | 465 | [package.extras] 466 | tests = ["pytest", "typeguard", "pygments", "littleutils", "cython"] 467 | 468 | [[package]] 469 | name = "tomli" 470 | version = "2.0.1" 471 | description = "A lil' TOML parser" 472 | category = "dev" 473 | optional = false 474 | python-versions = ">=3.7" 475 | 476 | [[package]] 477 | name = "tornado" 478 | version = "6.1" 479 | description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." 480 | category = "main" 481 | optional = false 482 | python-versions = ">= 3.5" 483 | 484 | [[package]] 485 | name = "traitlets" 486 | version = "5.1.1" 487 | description = "Traitlets Python configuration system" 488 | category = "dev" 489 | optional = false 490 | python-versions = ">=3.7" 491 | 492 | [package.extras] 493 | test = ["pytest"] 494 | 495 | [[package]] 496 | name = "typing-extensions" 497 | version = "4.1.1" 498 | description = "Backported and Experimental Type Hints for Python 3.6+" 499 | category = "dev" 500 | optional = false 501 | python-versions = ">=3.6" 502 | 503 | [[package]] 504 | name = "tzdata" 505 | version = "2022.1" 506 | description = "Provider of IANA time zone data" 507 | category = "main" 508 | optional = false 509 | python-versions = ">=2" 510 | 511 | [[package]] 512 | name = "tzlocal" 513 | version = "4.2" 514 | description = "tzinfo object for the local timezone" 515 | category = "main" 516 | optional = false 517 | python-versions = ">=3.6" 518 | 519 | [package.dependencies] 520 | pytz-deprecation-shim = "*" 521 | tzdata = {version = "*", markers = "platform_system == \"Windows\""} 522 | 523 | [package.extras] 524 | devenv = ["black", "pyroma", "pytest-cov", "zest.releaser"] 525 | test = ["pytest-mock (>=3.3)", "pytest (>=4.3)"] 526 | 527 | [[package]] 528 | name = "urllib3" 529 | version = "1.26.9" 530 | description = "HTTP library with thread-safe connection pooling, file post, and more." 531 | category = "main" 532 | optional = false 533 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 534 | 535 | [package.extras] 536 | brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] 537 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 538 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 539 | 540 | [[package]] 541 | name = "wcwidth" 542 | version = "0.2.5" 543 | description = "Measures the displayed width of unicode strings in a terminal" 544 | category = "dev" 545 | optional = false 546 | python-versions = "*" 547 | 548 | [[package]] 549 | name = "win32-setctime" 550 | version = "1.1.0" 551 | description = "A small Python utility to set file creation time on Windows" 552 | category = "main" 553 | optional = false 554 | python-versions = ">=3.5" 555 | 556 | [package.extras] 557 | dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] 558 | 559 | [metadata] 560 | lock-version = "1.1" 561 | python-versions = "^3.9" 562 | content-hash = "956fa456894b7013b89355633eea4d4cc1d0459686a992407aea92ec64dd1b63" 563 | 564 | [metadata.files] 565 | alembic = [ 566 | {file = "alembic-1.7.7-py3-none-any.whl", hash = "sha256:29be0856ec7591c39f4e1cb10f198045d890e6e2274cf8da80cb5e721a09642b"}, 567 | {file = "alembic-1.7.7.tar.gz", hash = "sha256:4961248173ead7ce8a21efb3de378f13b8398e6630fab0eb258dc74a8af24c58"}, 568 | ] 569 | appnope = [ 570 | {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, 571 | {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, 572 | ] 573 | apscheduler = [ 574 | {file = "APScheduler-3.6.3-py2.py3-none-any.whl", hash = "sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526"}, 575 | {file = "APScheduler-3.6.3.tar.gz", hash = "sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244"}, 576 | ] 577 | asttokens = [ 578 | {file = "asttokens-2.0.5-py2.py3-none-any.whl", hash = "sha256:0844691e88552595a6f4a4281a9f7f79b8dd45ca4ccea82e5e05b4bbdb76705c"}, 579 | {file = "asttokens-2.0.5.tar.gz", hash = "sha256:9a54c114f02c7a9480d56550932546a3f1fe71d8a02f1bc7ccd0ee3ee35cf4d5"}, 580 | ] 581 | backcall = [ 582 | {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, 583 | {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, 584 | ] 585 | black = [ 586 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, 587 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, 588 | {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, 589 | {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, 590 | {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, 591 | {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, 592 | {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, 593 | {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, 594 | {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, 595 | {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, 596 | {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, 597 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, 598 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, 599 | {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, 600 | {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, 601 | {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, 602 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, 603 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, 604 | {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, 605 | {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, 606 | {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, 607 | {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, 608 | {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, 609 | ] 610 | cachetools = [ 611 | {file = "cachetools-4.2.2-py3-none-any.whl", hash = "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001"}, 612 | {file = "cachetools-4.2.2.tar.gz", hash = "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"}, 613 | ] 614 | certifi = [ 615 | {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, 616 | {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, 617 | ] 618 | click = [ 619 | {file = "click-8.1.2-py3-none-any.whl", hash = "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e"}, 620 | {file = "click-8.1.2.tar.gz", hash = "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"}, 621 | ] 622 | colorama = [ 623 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 624 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 625 | ] 626 | decorator = [ 627 | {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, 628 | {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, 629 | ] 630 | executing = [ 631 | {file = "executing-0.8.3-py2.py3-none-any.whl", hash = "sha256:d1eef132db1b83649a3905ca6dd8897f71ac6f8cac79a7e58a1a09cf137546c9"}, 632 | {file = "executing-0.8.3.tar.gz", hash = "sha256:c6554e21c6b060590a6d3be4b82fb78f8f0194d809de5ea7df1c093763311501"}, 633 | ] 634 | greenlet = [ 635 | {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, 636 | {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, 637 | {file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"}, 638 | {file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"}, 639 | {file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"}, 640 | {file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"}, 641 | {file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"}, 642 | {file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"}, 643 | {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, 644 | {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, 645 | {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, 646 | {file = "greenlet-1.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965"}, 647 | {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, 648 | {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, 649 | {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, 650 | {file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"}, 651 | {file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"}, 652 | {file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"}, 653 | {file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"}, 654 | {file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"}, 655 | {file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"}, 656 | {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, 657 | {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, 658 | {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, 659 | {file = "greenlet-1.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f"}, 660 | {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, 661 | {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, 662 | {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, 663 | {file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"}, 664 | {file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"}, 665 | {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, 666 | {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, 667 | {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, 668 | {file = "greenlet-1.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe"}, 669 | {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, 670 | {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, 671 | {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, 672 | {file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"}, 673 | {file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"}, 674 | {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, 675 | {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, 676 | {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, 677 | {file = "greenlet-1.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2"}, 678 | {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, 679 | {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, 680 | {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, 681 | {file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"}, 682 | {file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"}, 683 | {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, 684 | {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, 685 | {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, 686 | {file = "greenlet-1.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3"}, 687 | {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, 688 | {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, 689 | {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, 690 | ] 691 | ipython = [ 692 | {file = "ipython-8.2.0-py3-none-any.whl", hash = "sha256:1b672bfd7a48d87ab203d9af8727a3b0174a4566b4091e9447c22fb63ea32857"}, 693 | {file = "ipython-8.2.0.tar.gz", hash = "sha256:70e5eb132cac594a34b5f799bd252589009905f05104728aea6a403ec2519dc1"}, 694 | ] 695 | jedi = [ 696 | {file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"}, 697 | {file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"}, 698 | ] 699 | loguru = [ 700 | {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"}, 701 | {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"}, 702 | ] 703 | mako = [ 704 | {file = "Mako-1.2.0-py3-none-any.whl", hash = "sha256:23aab11fdbbb0f1051b93793a58323ff937e98e34aece1c4219675122e57e4ba"}, 705 | {file = "Mako-1.2.0.tar.gz", hash = "sha256:9a7c7e922b87db3686210cf49d5d767033a41d4010b284e747682c92bddd8b39"}, 706 | ] 707 | markupsafe = [ 708 | {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, 709 | {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, 710 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, 711 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, 712 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, 713 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, 714 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, 715 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, 716 | {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, 717 | {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, 718 | {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, 719 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, 720 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, 721 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, 722 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, 723 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, 724 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, 725 | {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, 726 | {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, 727 | {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, 728 | {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, 729 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, 730 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, 731 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, 732 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, 733 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, 734 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, 735 | {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, 736 | {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, 737 | {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, 738 | {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, 739 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, 740 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, 741 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, 742 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, 743 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, 744 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, 745 | {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, 746 | {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, 747 | {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, 748 | ] 749 | matplotlib-inline = [ 750 | {file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"}, 751 | {file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"}, 752 | ] 753 | minio = [ 754 | {file = "minio-7.1.6-py3-none-any.whl", hash = "sha256:1ed6d4df7bbd505be984657ecce81ac6b196f434acf23315227aa892c98f9531"}, 755 | {file = "minio-7.1.6.tar.gz", hash = "sha256:54a5e6eefcc958c88c493cf116ba86e52341efab88686163594f2e9410385124"}, 756 | ] 757 | mypy-extensions = [ 758 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 759 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 760 | ] 761 | parso = [ 762 | {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, 763 | {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, 764 | ] 765 | pathspec = [ 766 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 767 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 768 | ] 769 | pexpect = [ 770 | {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, 771 | {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, 772 | ] 773 | pickleshare = [ 774 | {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, 775 | {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, 776 | ] 777 | platformdirs = [ 778 | {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, 779 | {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, 780 | ] 781 | prompt-toolkit = [ 782 | {file = "prompt_toolkit-3.0.29-py3-none-any.whl", hash = "sha256:62291dad495e665fca0bda814e342c69952086afb0f4094d0893d357e5c78752"}, 783 | {file = "prompt_toolkit-3.0.29.tar.gz", hash = "sha256:bd640f60e8cecd74f0dc249713d433ace2ddc62b65ee07f96d358e0b152b6ea7"}, 784 | ] 785 | psycopg2-binary = [ 786 | {file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"}, 787 | {file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"}, 788 | {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65"}, 789 | {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092"}, 790 | {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76"}, 791 | {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9"}, 792 | {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4"}, 793 | {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa"}, 794 | {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e"}, 795 | {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42"}, 796 | {file = "psycopg2_binary-2.9.3-cp310-cp310-win32.whl", hash = "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029"}, 797 | {file = "psycopg2_binary-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53"}, 798 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e"}, 799 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1"}, 800 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460"}, 801 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281"}, 802 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f"}, 803 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca"}, 804 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af"}, 805 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57"}, 806 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d"}, 807 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b"}, 808 | {file = "psycopg2_binary-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2"}, 809 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1"}, 810 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33"}, 811 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae"}, 812 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094"}, 813 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063"}, 814 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef"}, 815 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb"}, 816 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232"}, 817 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554"}, 818 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba"}, 819 | {file = "psycopg2_binary-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71"}, 820 | {file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667"}, 821 | {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272"}, 822 | {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834"}, 823 | {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24"}, 824 | {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c"}, 825 | {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd"}, 826 | {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004"}, 827 | {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1"}, 828 | {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307"}, 829 | {file = "psycopg2_binary-2.9.3-cp38-cp38-win32.whl", hash = "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce"}, 830 | {file = "psycopg2_binary-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e"}, 831 | {file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9"}, 832 | {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42"}, 833 | {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39"}, 834 | {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c"}, 835 | {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4"}, 836 | {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35"}, 837 | {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb"}, 838 | {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7"}, 839 | {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8"}, 840 | {file = "psycopg2_binary-2.9.3-cp39-cp39-win32.whl", hash = "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d"}, 841 | {file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"}, 842 | ] 843 | ptyprocess = [ 844 | {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, 845 | {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, 846 | ] 847 | pure-eval = [ 848 | {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, 849 | {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, 850 | ] 851 | pygments = [ 852 | {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"}, 853 | {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, 854 | ] 855 | python-telegram-bot = [ 856 | {file = "python-telegram-bot-13.9.tar.gz", hash = "sha256:512d7a84f4bf4e59b7acaf87a38e29c60f65a2717ebf6455b4d66fd058326b1b"}, 857 | {file = "python_telegram_bot-13.9-py3-none-any.whl", hash = "sha256:a0018979585054f9c8da70198701053705ff1cd2839cc74b2da5371e3ada9ada"}, 858 | ] 859 | pytz = [ 860 | {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, 861 | {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, 862 | ] 863 | pytz-deprecation-shim = [ 864 | {file = "pytz_deprecation_shim-0.1.0.post0-py2.py3-none-any.whl", hash = "sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6"}, 865 | {file = "pytz_deprecation_shim-0.1.0.post0.tar.gz", hash = "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d"}, 866 | ] 867 | six = [ 868 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 869 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 870 | ] 871 | sqlalchemy = [ 872 | {file = "SQLAlchemy-1.4.28-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:e659f256b7d402338563913bdeba53bf1eadd4c09e6f6dc93cc47938f7962a8f"}, 873 | {file = "SQLAlchemy-1.4.28-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:38df997ffa9007e953ad574f2263f61b9b683fd63ae397480ea4960be9bda0fd"}, 874 | {file = "SQLAlchemy-1.4.28-cp27-cp27m-win_amd64.whl", hash = "sha256:6dd6fa51cf08d9433d28802228d2204e175324f1a284c4492e4af2dd36a2d485"}, 875 | {file = "SQLAlchemy-1.4.28-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:bb2d8530b7cc94b7fd9341843c3e49b6db48ea22313a8db9df21c41615b5e7b1"}, 876 | {file = "SQLAlchemy-1.4.28-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:3b64f5d1c1d0e5f2ed4aa66f2b65ff6bdcdf4c5cc83b71c4bbf69695b09e9e19"}, 877 | {file = "SQLAlchemy-1.4.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c02991e22ddce134ef1093ef5a9d5de448fc87b91432e4f879826e93cd1c7"}, 878 | {file = "SQLAlchemy-1.4.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:387365c157e96eceacdd6c5468815ad05a523ba778680de4c8139a029e1fe044"}, 879 | {file = "SQLAlchemy-1.4.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5639800f1cfe751569af2242041b30a08a6c0b9e5d95ed674ec8082d381eff13"}, 880 | {file = "SQLAlchemy-1.4.28-cp310-cp310-win32.whl", hash = "sha256:261fcb3ff8c59e17ec44f9e61713a44ceaa97ae816da978d5cd1dc2c36f32478"}, 881 | {file = "SQLAlchemy-1.4.28-cp310-cp310-win_amd64.whl", hash = "sha256:29d10796e5604ab7bc067eda7231a2d2411a51eda43082673641245a49d1c4bb"}, 882 | {file = "SQLAlchemy-1.4.28-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:4490b10f83cd56ca2cdcd94b140d89911ac331e42a727b79157963b1b04fdd0c"}, 883 | {file = "SQLAlchemy-1.4.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83ee7f6fa5faed23996c67044376d46815f65183ad6d744d94d68b18cdef060b"}, 884 | {file = "SQLAlchemy-1.4.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f667a947378bcb12a371ab38bed1b708f3a682d1ba30176422652082919285a2"}, 885 | {file = "SQLAlchemy-1.4.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61965abc63c8b54038574698888e91a126753a4bdc0ec001397acb14501834e0"}, 886 | {file = "SQLAlchemy-1.4.28-cp36-cp36m-win32.whl", hash = "sha256:41a02030f8934b0de843341e7014192a0c16ee2726a06da154c81153fbe56b33"}, 887 | {file = "SQLAlchemy-1.4.28-cp36-cp36m-win_amd64.whl", hash = "sha256:c3497cd63c5f90112b8882ea4dd694052166f779ce9055cd5c4305e0b76d72d9"}, 888 | {file = "SQLAlchemy-1.4.28-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:5d91dce14ac3347bce301062ca825e7fb7e15c133f3909f15989e94878b1082f"}, 889 | {file = "SQLAlchemy-1.4.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08e39d65b38d4c3f77c4c9bf090b0ba4ec5721a6e0a74b63d2a9781cdcacf142"}, 890 | {file = "SQLAlchemy-1.4.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c85ead1d17acc5e8b282c578394dba253728bcbcbeb66e4ef0e25f4bab53935a"}, 891 | {file = "SQLAlchemy-1.4.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:daddcd6ba1706cc5fcc9cfaa913aa4bf331172dc7efd385fe3ee1feae3b513bc"}, 892 | {file = "SQLAlchemy-1.4.28-cp37-cp37m-win32.whl", hash = "sha256:ce4f2b34378561bc2e42635888fe86efe13d104ba1d95b5ca67b4d60d8e53e67"}, 893 | {file = "SQLAlchemy-1.4.28-cp37-cp37m-win_amd64.whl", hash = "sha256:4999b03daa6c9afb9a0bf9e3b8769128ef1880557dacfca86fa7562920c49f6b"}, 894 | {file = "SQLAlchemy-1.4.28-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:dd041324328cece3ccdf70cfbd71b5ab968e564a22318ffd88b054f5eadeb9be"}, 895 | {file = "SQLAlchemy-1.4.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf2c1d64c4ee0f30e08e1844ff0acf3c1b6c4277c0e89ec3e8bf1722d245b108"}, 896 | {file = "SQLAlchemy-1.4.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:525e962af8f25fc24ce019e6f237d49f8720d757a8a56c9b4caa2d91e2c66111"}, 897 | {file = "SQLAlchemy-1.4.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b72744fed32ecf2bf786d2e2f6756c04126c323ba939f47177b9722775626889"}, 898 | {file = "SQLAlchemy-1.4.28-cp38-cp38-win32.whl", hash = "sha256:b5541355b8d4970753d4f7292f73a320704b20406e06cd29b469d156f0a484d8"}, 899 | {file = "SQLAlchemy-1.4.28-cp38-cp38-win_amd64.whl", hash = "sha256:cf3a3c2f32d53a4166b2eb8de35f93bcb640e51c32033024af500017d8e8a8c9"}, 900 | {file = "SQLAlchemy-1.4.28-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:dfa093bd8ecfceafff62078910178567323005e44fbe4d7933e6cbce4512cea2"}, 901 | {file = "SQLAlchemy-1.4.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:555d56b71f61b4c9fa55fe203fe6e1e561c9385fa97c5849783ae050a89113af"}, 902 | {file = "SQLAlchemy-1.4.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c90b21360cf14d33c8a004f991aa336c7906a8db825d4ec38722c5ff1c47dada"}, 903 | {file = "SQLAlchemy-1.4.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2019b332cf4f9a513133fdf056dc4cecec7fbae7016ebc574d0f310103eed7ee"}, 904 | {file = "SQLAlchemy-1.4.28-cp39-cp39-win32.whl", hash = "sha256:ca500f30619daf863ab1c66d57d53a0987361a8f3266454290198aabd18f2599"}, 905 | {file = "SQLAlchemy-1.4.28-cp39-cp39-win_amd64.whl", hash = "sha256:853de08e881dae0305647dd61b4429758f11d1bf02a9faf02793cad44bb2e0d5"}, 906 | {file = "SQLAlchemy-1.4.28.tar.gz", hash = "sha256:7fdb7b775fb0739d3e71461509f978beb788935bc0aa9e47df14837cb33e5226"}, 907 | ] 908 | stack-data = [ 909 | {file = "stack_data-0.2.0-py3-none-any.whl", hash = "sha256:999762f9c3132308789affa03e9271bbbe947bf78311851f4d485d8402ed858e"}, 910 | {file = "stack_data-0.2.0.tar.gz", hash = "sha256:45692d41bd633a9503a5195552df22b583caf16f0b27c4e58c98d88c8b648e12"}, 911 | ] 912 | tomli = [ 913 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 914 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 915 | ] 916 | tornado = [ 917 | {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, 918 | {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, 919 | {file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"}, 920 | {file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"}, 921 | {file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"}, 922 | {file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"}, 923 | {file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"}, 924 | {file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"}, 925 | {file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"}, 926 | {file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"}, 927 | {file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"}, 928 | {file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"}, 929 | {file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"}, 930 | {file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"}, 931 | {file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"}, 932 | {file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"}, 933 | {file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"}, 934 | {file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"}, 935 | {file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"}, 936 | {file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"}, 937 | {file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"}, 938 | {file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"}, 939 | {file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"}, 940 | {file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"}, 941 | {file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"}, 942 | {file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"}, 943 | {file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"}, 944 | {file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"}, 945 | {file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"}, 946 | {file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"}, 947 | {file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"}, 948 | {file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"}, 949 | {file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"}, 950 | {file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"}, 951 | {file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"}, 952 | {file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"}, 953 | {file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"}, 954 | {file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"}, 955 | {file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"}, 956 | {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"}, 957 | {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, 958 | ] 959 | traitlets = [ 960 | {file = "traitlets-5.1.1-py3-none-any.whl", hash = "sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033"}, 961 | {file = "traitlets-5.1.1.tar.gz", hash = "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7"}, 962 | ] 963 | typing-extensions = [ 964 | {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, 965 | {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, 966 | ] 967 | tzdata = [ 968 | {file = "tzdata-2022.1-py2.py3-none-any.whl", hash = "sha256:238e70234214138ed7b4e8a0fab0e5e13872edab3be586ab8198c407620e2ab9"}, 969 | {file = "tzdata-2022.1.tar.gz", hash = "sha256:8b536a8ec63dc0751342b3984193a3118f8fca2afe25752bb9b7fffd398552d3"}, 970 | ] 971 | tzlocal = [ 972 | {file = "tzlocal-4.2-py3-none-any.whl", hash = "sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745"}, 973 | {file = "tzlocal-4.2.tar.gz", hash = "sha256:ee5842fa3a795f023514ac2d801c4a81d1743bbe642e3940143326b3a00addd7"}, 974 | ] 975 | urllib3 = [ 976 | {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, 977 | {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, 978 | ] 979 | wcwidth = [ 980 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 981 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 982 | ] 983 | win32-setctime = [ 984 | {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, 985 | {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, 986 | ] 987 | -------------------------------------------------------------------------------- /bot/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "bot" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["bakhteev-v@yandex.ru"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.9" 9 | loguru = "0.6.0" 10 | alembic = "^1.7.7" 11 | SQLAlchemy = "1.4.28" 12 | python-telegram-bot = "13.9" 13 | psycopg2-binary = "^2.9.3" 14 | minio = "7.1.6" 15 | 16 | [tool.poetry.dev-dependencies] 17 | black = "^22.1.0" 18 | ipython = "^8.1.1" 19 | 20 | [build-system] 21 | requires = ["poetry-core>=1.0.0"] 22 | build-backend = "poetry.core.masonry.api" -------------------------------------------------------------------------------- /bot/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vbakhteev/telegram_bot_template/415899d977135408a530c982eef6a0bf73f73e28/bot/src/__init__.py -------------------------------------------------------------------------------- /bot/src/manager.py: -------------------------------------------------------------------------------- 1 | import random 2 | from datetime import date 3 | from typing import Optional, List, Tuple 4 | 5 | from sqlalchemy.orm import sessionmaker 6 | 7 | from .models import User, Admin, Group, GroupType, Administration, Participation 8 | from .utils import generate_invite 9 | 10 | 11 | class Manager: 12 | def __init__(self, engine): 13 | self.engine = engine 14 | self.SessionMaker = sessionmaker(bind=engine) 15 | 16 | def register_user(self, user_id: int, fullname: str, username: str): 17 | with self.SessionMaker() as session: 18 | user = User(id=user_id, fullname=fullname, username=username) 19 | 20 | session.add(user) 21 | session.commit() 22 | 23 | def is_user_registered(self, user_id: int) -> bool: 24 | with self.SessionMaker() as session: 25 | user = self._get_user(user_id, session) 26 | return user is not None 27 | 28 | def register_admin(self, admin_id: int, fullname: str, username: str): 29 | with self.SessionMaker() as session: 30 | admin = Admin(id=admin_id, fullname=fullname, username=username) 31 | 32 | session.add(admin) 33 | session.commit() 34 | 35 | def is_admin_registered(self, admin_id: int) -> bool: 36 | with self.SessionMaker() as session: 37 | admin = self._get_admin( 38 | admin_id=admin_id, 39 | session=session, 40 | ) 41 | return admin is not None 42 | 43 | def get_admin_groups_names(self, admin_id: int) -> List[str]: 44 | with self.SessionMaker() as session: 45 | administrations: List[Administration] = session.query(Administration). \ 46 | filter(Administration.admin_id == admin_id). \ 47 | all() 48 | 49 | return [adm.group.name for adm in administrations] 50 | 51 | def get_user_groups(self, user_id: int) -> List[Group]: 52 | with self.SessionMaker() as session: 53 | participations: List[Participation] = session.query(Participation). \ 54 | filter(Participation.admin_id == user_id). \ 55 | all() 56 | 57 | return [p.group for p in participations] 58 | 59 | def is_admin_group(self, admin_id: int, group_name) -> Tuple[bool, Optional[int]]: 60 | with self.SessionMaker() as session: 61 | group: Optional[Group] = session.query(Group).filter(Group.name == group_name).first() 62 | if group is None: 63 | return False, None 64 | 65 | admins_of_group = [administration.admin_id for administration in group.admins] 66 | is_admin = admin_id in admins_of_group 67 | return is_admin, group.id 68 | 69 | def create_group( 70 | self, 71 | admin_id: int, 72 | group_type: GroupType, 73 | deposit: int, 74 | rest_day_price_to_bank: int, 75 | start_date: date, 76 | cities: List[Tuple[str, str]], 77 | ) -> Tuple[int, str, int]: 78 | with self.SessionMaker() as session: 79 | group_name = generate_new_name(cities, session) 80 | invite = generate_new_invite(session) 81 | 82 | group = Group( 83 | name=group_name, 84 | group_type=group_type, 85 | invite=invite, 86 | deposit=deposit, 87 | rest_day_price_to_bank=rest_day_price_to_bank, 88 | start_date=start_date, 89 | ) 90 | session.add(group) 91 | session.commit() 92 | 93 | administration = Administration( 94 | admin_id=admin_id, 95 | group_id=group.id, 96 | ) 97 | session.add(administration) 98 | session.commit() 99 | 100 | return group.id, group_name, invite 101 | 102 | def set_channel_id_by_name( 103 | self, 104 | channel_id: int, 105 | group_name: str, 106 | ) -> bool: 107 | with self.SessionMaker() as session: 108 | group: Optional[Group] = session.query(Group).filter(Group.name == group_name).first() 109 | 110 | if group is None: 111 | return False 112 | 113 | group.channel_id = channel_id 114 | session.commit() 115 | 116 | return True 117 | 118 | def is_channel_set(self, group_id: int) -> bool: 119 | with self.SessionMaker() as session: 120 | group = self._get_group(group_id=group_id, session=session) 121 | return group.channel_id is not None 122 | 123 | ######### 124 | 125 | def _get_user(self, user_id: int, session) -> Optional[User]: 126 | return session.query(User).get(user_id) 127 | 128 | def _get_admin(self, admin_id: int, session) -> Optional[Admin]: 129 | return session.query(Admin).get(admin_id) 130 | 131 | def _get_group(self, group_id: int, session) -> Optional[Group]: 132 | return session.query(Group).get(group_id) 133 | 134 | 135 | def generate_new_invite(session) -> int: 136 | for _ in range(20): 137 | invite = generate_invite() 138 | group = session.query(Group).filter(Group.invite == invite).first() 139 | if group is None: 140 | return invite 141 | 142 | return -1 143 | 144 | 145 | def generate_new_name(cities: List[Tuple[str, str]], session) -> str: 146 | flag, city = random.choice(cities) 147 | 148 | n_cities_in_db = session.query(Group).filter(Group.name.contains(city)).count() 149 | 150 | name = f'{city} {flag * (n_cities_in_db + 1)}' 151 | return name 152 | -------------------------------------------------------------------------------- /bot/src/models.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | from sqlalchemy import Column, ForeignKey, String, DateTime, Time, Date, BigInteger, Enum, Integer, Boolean, Text 4 | from sqlalchemy.orm import declarative_base, relationship 5 | from sqlalchemy.sql import func 6 | 7 | from .utils import todict 8 | 9 | 10 | class GroupType(enum.Enum): 11 | reading = enum.auto() 12 | sleeping = enum.auto() 13 | 14 | def get_name(self): 15 | return self.name 16 | 17 | 18 | class Base: 19 | def __repr__(self): 20 | params = ', '.join(f'{k}={v}' for k, v in todict(self).items()) 21 | return f"{self.__class__.__name__}({params})" 22 | 23 | 24 | Base = declarative_base(cls=Base) 25 | 26 | 27 | class User(Base): 28 | __tablename__ = 'users' 29 | 30 | id = Column(BigInteger, primary_key=True, index=True) # from Telegram 31 | 32 | fullname = Column(String) 33 | username = Column(String) 34 | 35 | registration_datetime = Column(DateTime, server_default=func.now()) 36 | 37 | groups = relationship("ParticipationRelation", back_populates="user") 38 | 39 | 40 | class Group(Base): 41 | __tablename__ = 'groups' 42 | 43 | id = Column(BigInteger, primary_key=True, index=True) 44 | name = Column(String(64), unique=True, index=True) 45 | group_type = Column(Enum(GroupType), nullable=True) 46 | invite = Column(BigInteger, index=True, unique=True) 47 | channel_id = Column(BigInteger, nullable=True, unique=True, default=None) 48 | 49 | deposit = Column(Integer) 50 | rest_day_price_to_bank = Column(Integer) 51 | 52 | start_date = Column(Date) 53 | creation_datetime = Column(DateTime, server_default=func.now()) 54 | 55 | admins = relationship("Administration", back_populates="group") 56 | participants = relationship("ParticipationRelation", back_populates="group") 57 | 58 | 59 | class Report(Base): 60 | __tablename__ = 'reports' 61 | 62 | id = Column(BigInteger, primary_key=True, index=True) 63 | 64 | sender = Column(ForeignKey('users.id'), index=True) 65 | group = Column(ForeignKey('groups.id'), index=True) 66 | 67 | tg_msg_id = Column(Integer, index=True) 68 | approved = Column(Boolean, nullable=True, default=None) 69 | 70 | day = Column(Integer) 71 | sent_datetime = Column(DateTime, server_default=func.now(), index=True) 72 | 73 | 74 | class Admin(Base): 75 | __tablename__ = 'admins' 76 | 77 | id = Column(BigInteger, primary_key=True, index=True) 78 | 79 | fullname = Column(String) 80 | username = Column(String) 81 | 82 | registration_datetime = Column(DateTime, server_default=func.now()) 83 | 84 | groups = relationship("Administration", back_populates="admin") 85 | 86 | 87 | class Administration(Base): 88 | __tablename__ = 'administration_relation' 89 | 90 | group_id = Column(ForeignKey('groups.id'), primary_key=True, index=True) 91 | admin_id = Column(ForeignKey('admins.id'), primary_key=True, index=True) 92 | 93 | admin = relationship("Admin", back_populates="groups") 94 | group = relationship("Group", back_populates="admins") 95 | 96 | 97 | class Participation(Base): 98 | __tablename__ = 'participation_relation' 99 | 100 | user_id = Column(ForeignKey('users.id'), primary_key=True, index=True) 101 | group_id = Column(ForeignKey('groups.id'), primary_key=True, index=True) 102 | entered_datetime = Column(DateTime, server_default=func.now()) 103 | 104 | participation_details = Column(Text, nullable=True) 105 | attempts_bought = Column(Integer, default=0) 106 | notification_time = Column(Time, nullable=True) # Local time 107 | 108 | user = relationship("User", back_populates="groups") 109 | group = relationship("Group", back_populates="participants") 110 | 111 | 112 | class KickedRelation(Base): 113 | __tablename__ = 'kicked_relation' 114 | 115 | user_id = Column(ForeignKey('users.id'), primary_key=True) 116 | group_id = Column(ForeignKey('groups.id'), primary_key=True) 117 | kick_datetime = Column(DateTime, server_default=func.now()) 118 | kick_day = Column(Integer) 119 | -------------------------------------------------------------------------------- /bot/src/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | from typing import Optional, List, Tuple 4 | 5 | from telegram import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton 6 | from telegram.ext import Filters 7 | 8 | 9 | def get_match_regex(*strs_to_match): 10 | s = '|'.join(strs_to_match) 11 | return Filters.regex(f'^({s})$') 12 | 13 | 14 | def todict(obj): 15 | """Return the object's dict excluding private attributes, 16 | sqlalchemy state and relationship attributes. 17 | """ 18 | excl = ("_sa_adapter", "_sa_instance_state") 19 | return { 20 | k: v 21 | for k, v in vars(obj).items() 22 | if not k.startswith("_") and not any(hasattr(v, a) for a in excl) 23 | } 24 | 25 | 26 | def get_logger(is_prod: bool, file_path: Optional[str] = None, name='bot'): 27 | logging.basicConfig( 28 | level=logging.INFO if is_prod else logging.DEBUG, 29 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 30 | ) 31 | 32 | logger = logging.getLogger(name=name) 33 | if is_prod: 34 | for handler in logging.root.handlers: 35 | handler.addFilter(logging.Filter(name)) 36 | logger.addHandler(logging.FileHandler(file_path or 'bot.log')) 37 | 38 | return logger 39 | 40 | 41 | def markup_keyboard( 42 | buttons: List[List[str]], one_time_keyboard=False 43 | ) -> ReplyKeyboardMarkup: 44 | return ReplyKeyboardMarkup( 45 | [[KeyboardButton(y) for y in x] for x in buttons], 46 | one_time_keyboard=one_time_keyboard, 47 | ) 48 | 49 | 50 | def inline_keyboard( 51 | buttons: List[List[str]], callbacks: List[List[str]] 52 | ) -> InlineKeyboardMarkup: 53 | keyboard = [] 54 | for buttons_row, callbacks_row in zip(buttons, callbacks): 55 | keyboard.append( 56 | [ 57 | inline_button(button, callback) 58 | for button, callback in zip(buttons_row, callbacks_row) 59 | ] 60 | ) 61 | return InlineKeyboardMarkup(keyboard) 62 | 63 | 64 | def inline_button(text: str, callback_data: str) -> InlineKeyboardButton: 65 | return InlineKeyboardButton(text=text, callback_data=callback_data) 66 | 67 | 68 | def generate_invite() -> int: 69 | return random.randint(0, 2 ** 32 - 1) 70 | 71 | 72 | def load_cities(path) -> List[Tuple[str, str]]: 73 | cities = [] 74 | with open(path) as f: 75 | for line in f: 76 | 77 | flag, city = line.strip().split(',') 78 | cities.append((flag, city)) 79 | 80 | return cities 81 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | tg_bot: 5 | image: tg_bot 6 | container_name: bot_container 7 | build: 8 | context: ./bot 9 | environment: 10 | PROD: $PROD 11 | CLIENT_TOKEN: $CLIENT_TOKEN 12 | ADMIN_TOKEN: $ADMIN_TOKEN 13 | DB_URL: "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres:5432/postgresdb" 14 | volumes: 15 | - ./resources/bot:/bot/bot_data 16 | depends_on: 17 | postgres: 18 | condition: service_healthy 19 | migrations: 20 | condition: service_completed_successfully 21 | 22 | migrations: 23 | image: tg_bot 24 | build: 25 | context: ./bot 26 | depends_on: 27 | postgres: 28 | condition: service_healthy 29 | environment: 30 | PROD: $PROD 31 | DB_URL: "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres:5432/postgresdb" 32 | volumes: 33 | - ./bot/alembic:/bot/alembic 34 | restart: "no" 35 | entrypoint: [ "./make_migrations.sh" ] 36 | 37 | postgres: 38 | image: library/postgres:14.2-alpine 39 | container_name: postgres_container 40 | environment: 41 | POSTGRES_DB: "postgresdb" 42 | POSTGRES_USER: $POSTGRES_USER 43 | POSTGRES_PASSWORD: $POSTGRES_PASSWORD 44 | PGDATA: "/var/lib/postgresql/data/pgdata" 45 | volumes: 46 | - ./resources/db:/var/lib/postgresql/data 47 | # ports: 48 | # - "5432:5432" 49 | healthcheck: 50 | test: [ "CMD-SHELL", "pg_isready -U $POSTGRES_USER -d postgresdb" ] 51 | interval: 10s 52 | timeout: 5s 53 | retries: 5 54 | start_period: 10s 55 | restart: always 56 | 57 | pgadmin: 58 | image: dpage/pgadmin4 59 | container_name: pgadmin_container 60 | restart: always 61 | environment: 62 | PGADMIN_DEFAULT_EMAIL: admin@admin.com 63 | PGADMIN_DEFAULT_PASSWORD: root 64 | ports: 65 | - "5050:80" 66 | 67 | 68 | volumes: 69 | grafana-data: 70 | external: true --------------------------------------------------------------------------------