├── .dockerignore ├── bot ├── __init__.py ├── db │ ├── __init__.py │ ├── data │ │ └── .gitkeep │ ├── migrate.py │ ├── sqlite.py │ └── database.py ├── fetch │ ├── __init__.py │ ├── schedule.py │ ├── search.py │ └── models.py ├── logs │ ├── __init__.py │ └── lazy_logger.py ├── parse │ ├── __init__.py │ ├── semester.py │ └── formating.py ├── handlers │ ├── __init__.py │ ├── states.py │ ├── ImportantDays.py │ ├── info.py │ ├── favorite.py │ ├── events.py │ ├── construct.py │ ├── send.py │ ├── inline.py │ └── handler.py ├── __main__.py ├── config.py ├── setup.py └── start.py ├── .idea ├── .gitignore └── vcs.xml ├── .gitattributes ├── .env.example ├── docker-compose.yml ├── pyproject.toml ├── Dockerfile ├── README.md ├── .github └── workflows │ └── main.yml ├── .gitignore └── poetry.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /bot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bot/db/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bot/db/data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bot/fetch/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bot/logs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bot/parse/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bot/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /bot/__main__.py: -------------------------------------------------------------------------------- 1 | from bot import start 2 | 3 | if __name__ == "__main__": 4 | start.main() 5 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | TZ=Europe/Moscow 2 | API_URL="https://your.api" 3 | TOKEN="123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" 4 | ADMINS="486782304,123456789,987654321" 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /bot/logs/lazy_logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class LazyLogger: 5 | def __init__(self): 6 | self.logger = logging.getLogger("bot.handlers") 7 | self.logger.setLevel("INFO") 8 | 9 | 10 | lazy_logger = LazyLogger() 11 | -------------------------------------------------------------------------------- /bot/handlers/states.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class EInlineStep(enum.Enum): 5 | ask_item = 0 6 | ask_week = 1 7 | ask_day = 2 8 | completed = 3 9 | unk_error = -1 10 | 11 | 12 | ITEM_CLARIFY, GETWEEK, GETDAY = map(chr, range(3)) 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | mirea-teacher-schedule-bot: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | env_file: 8 | - .env 9 | restart: unless-stopped 10 | volumes: 11 | - ./bot/db/data:/app/bot/db/data -------------------------------------------------------------------------------- /bot/db/migrate.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from peewee import SqliteDatabase, TextField 4 | from playhouse.migrate import SqliteMigrator, migrate 5 | 6 | db = SqliteDatabase(os.path.join(os.path.dirname(__file__), "data/bot.db")) 7 | 8 | 9 | def migrate_db(): 10 | migrator = SqliteMigrator(db) 11 | 12 | favorite_field = TextField(null=True) 13 | migrate(migrator.add_column("schedulebot", "favorite", favorite_field)) 14 | 15 | 16 | migrate_db() 17 | -------------------------------------------------------------------------------- /bot/db/sqlite.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from peewee import Model, PrimaryKeyField, SqliteDatabase, TextField 4 | 5 | db = SqliteDatabase(os.path.join(os.path.dirname(__file__), "data/bot.db")) 6 | 7 | 8 | class ScheduleBot(Model): 9 | id = PrimaryKeyField(unique=True) 10 | username = TextField(null=True) 11 | first_name = TextField(null=True) 12 | last_name = TextField(null=True) 13 | favorite = TextField(null=True) 14 | 15 | class Meta: 16 | database = db 17 | -------------------------------------------------------------------------------- /bot/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dataclasses import dataclass, field 3 | 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv() 7 | 8 | 9 | def parse_admins(admins_string): 10 | return [int(admin) for admin in admins_string.split(",")] 11 | 12 | 13 | @dataclass 14 | class Config: 15 | token: str = os.getenv("TOKEN") 16 | api_url: str = os.getenv("API_URL") 17 | admins: list = field(default_factory=lambda: parse_admins(os.getenv("ADMINS"))) 18 | 19 | 20 | settings = Config() 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "schedule-bot" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Slava Kuryaev <92830706+necrosskull@users.noreply.github.com>"] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.11" 10 | python-telegram-bot = "^20.8" 11 | python-dotenv = "^1.0.0" 12 | pydantic = "^2.5.3" 13 | peewee = {extras = ["playhouse"], version = "^3.17.1"} 14 | 15 | 16 | [build-system] 17 | requires = ["poetry-core"] 18 | build-backend = "poetry.core.masonry.api" 19 | -------------------------------------------------------------------------------- /bot/setup.py: -------------------------------------------------------------------------------- 1 | def setup(application): 2 | import bot.handlers.events as events 3 | import bot.handlers.favorite as favorite 4 | import bot.handlers.handler as handler 5 | import bot.handlers.info as info 6 | import bot.handlers.inline as inline 7 | from bot.db.sqlite import ScheduleBot, db 8 | 9 | db.connect() 10 | db.create_tables([ScheduleBot]) 11 | db.close() 12 | 13 | info.init_handlers(application) 14 | events.init_handlers(application) 15 | favorite.init_handlers(application) 16 | handler.init_handlers(application) 17 | inline.init_handlers(application) 18 | -------------------------------------------------------------------------------- /bot/handlers/ImportantDays.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | SIGN = 0 4 | DATE = 1 5 | INTERVAL = 2 6 | 7 | __today_year = datetime.date.today().year 8 | important_days = [ 9 | ["❤️", datetime.date(year=__today_year, month=2, day=14), 2], 10 | ["❄️", datetime.date(year=__today_year, month=12, day=31), 10], 11 | ["🎖️", datetime.date(year=__today_year, month=2, day=23), 1], 12 | ["🌷", datetime.date(year=__today_year, month=3, day=8), 2], 13 | ["🤡", datetime.date(year=__today_year, month=4, day=1), 2], 14 | ["⚒️", datetime.date(year=__today_year, month=5, day=1), 1], 15 | ["🎖️", datetime.date(year=__today_year, month=5, day=9), 2], 16 | ] 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim-bullseye AS python 2 | 3 | # Poetry configuration 4 | ENV POETRY_HOME="/opt/poetry" \ 5 | POETRY_NO_INTERACTION=1 \ 6 | POETRY_VERSION=1.7.1 \ 7 | POETRY_VIRTUALENVS_CREATE=false 8 | 9 | # Install poetry 10 | RUN pip install "poetry==$POETRY_VERSION" 11 | 12 | # Create a project directory 13 | WORKDIR /app 14 | 15 | # Copy poetry.lock and pyproject.toml 16 | COPY pyproject.toml poetry.lock ./ 17 | 18 | # Install dependencies 19 | RUN poetry install --no-dev --no-root --no-interaction --no-ansi 20 | 21 | RUN if [ -e "./bot/db/data/bot.db" ]; then \ 22 | cp ./bot/db/data/bot.db /app/bot/db/data/bot.db; \ 23 | fi 24 | # Copy the rest of the project 25 | COPY . . 26 | 27 | # Run the application 28 | CMD python -m bot 29 | -------------------------------------------------------------------------------- /bot/start.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from telegram.ext import Application 4 | 5 | from bot.config import settings 6 | 7 | logging.basicConfig( 8 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO 9 | ) 10 | logging.getLogger("httpx").setLevel(logging.WARNING) 11 | 12 | 13 | def main() -> None: 14 | """Start the bot.""" 15 | from bot import setup 16 | 17 | application = ( 18 | Application.builder() 19 | .token(settings.token) 20 | .post_init(post_init=post_init) 21 | .build() 22 | ) 23 | 24 | setup.setup(application) 25 | 26 | application.run_polling() 27 | 28 | 29 | async def post_init(application: Application) -> None: 30 | application.bot_data["maintenance_mode"] = False 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Телеграм бот для просмотра расписания РТУ МИРЭА 2 | 3 | [![BOT - LINK](https://img.shields.io/static/v1?label=BOT&message=LINK&color=229ed9&style=for-the-badge)](https://t.me/mirea_teachers_bot) 4 | 5 | ## О проекте 6 | 7 | Проект написан на языке `Python` с использованием библиотеки `python-telegram-bot 20.x` 8 | 9 | Расписание берется через [API](https://github.com/0niel/university-app/tree/master/api), который предоставляет 10 | расписание в формате `JSON.` 11 | 12 | Бот находится в стадии активной разработки, поэтому возможны ошибки и недоработки. 13 | *** 14 | 15 | ## Админские команды 16 | 17 | - `/work` - Включить режим обслуживания, когда бот всем отвечает, что он временно недоступен. 18 | - `/send` - Сделать рассылку всем пользователям бота. 19 | 20 | # Запуск бота 21 | 22 | ### Локальный запуск 23 | 24 | 1. Установите все необходимые зависимости, используя Poetry: 25 | 26 | ```bash 27 | poetry install 28 | ``` 29 | 30 | 2. Добавьте файл `.env` в корневую директорию проекта и заполните его по примеру `.env.example` 31 | 3. Запустите приложение: 32 | 33 | ```bash 34 | poetry run python -m bot 35 | ``` 36 | 37 | ### Запуск с использованием Docker 38 | 39 | Для начала добавьте файл `.env` в корневую директорию проекта и заполните его по примеру `.env.example`, затем выполните 40 | команду: 41 | 42 | ```bash 43 | docker-compose up -d 44 | ``` 45 | -------------------------------------------------------------------------------- /bot/fetch/schedule.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | import httpx 4 | 5 | from bot.config import settings 6 | from bot.fetch.models import Lesson, LessonSchedule, ScheduleData, SearchItem 7 | 8 | 9 | async def get_schedule(target: SearchItem) -> ScheduleData | None: 10 | base_url = f"{settings.api_url}/api/v1/schedule/{target.type}/{target.uid}" 11 | 12 | async with httpx.AsyncClient() as client: 13 | try: 14 | response = await client.get(base_url) 15 | response.raise_for_status() 16 | json_response = response.json() 17 | 18 | except Exception: 19 | return None 20 | 21 | return ScheduleData(**json_response) 22 | 23 | 24 | def get_lessons(user_data: ScheduleData, dates: list[date] = None) -> list[Lesson]: 25 | lessons_list = [] 26 | for item in user_data.data: 27 | if isinstance(item, LessonSchedule): 28 | for date in item.dates: 29 | if dates: 30 | if date in dates: 31 | lesson = item.model_copy() 32 | lessons_list.append( 33 | Lesson(dates=date, **lesson.model_dump(exclude={"dates"})) 34 | ) 35 | else: 36 | lesson = item.model_copy() 37 | lessons_list.append( 38 | Lesson(dates=date, **lesson.model_dump(exclude={"dates"})) 39 | ) 40 | 41 | lessons_list.sort(key=lambda x: (x.dates, x.lesson_bells.number)) 42 | return lessons_list 43 | -------------------------------------------------------------------------------- /bot/handlers/info.py: -------------------------------------------------------------------------------- 1 | from telegram import Update 2 | from telegram.ext import CommandHandler, ContextTypes 3 | 4 | 5 | async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): 6 | """ 7 | Привествие бота при использовании команды /start 8 | """ 9 | await context.bot.send_message( 10 | chat_id=update.effective_chat.id, 11 | text="Привет!\nЯ бот, который поможет вам найти " 12 | "расписание любого *преподавателя* и не только!\n\n" 13 | "Для получения расписания напишите:\n\n" 14 | "👥 Номер группы (например, `ИКБО-20-23`)\n" 15 | "🧑‍🏫 Фамилию преподавателя (например, `Карпов Д.А.`)\n" 16 | "🏫 Номер аудитории (например, `Г-212`)\n\n" 17 | "Для сохранения расписания в избранное используйте команду /save.\n\n" 18 | "Также вы можете использовать inline-режим, " 19 | "для этого в любом чате наберите *@mirea_teachers_bot* + *фамилию* и нажмите на кнопку с фамилией " 20 | "преподавателя.\n\n", 21 | parse_mode="Markdown", 22 | ) 23 | 24 | 25 | async def about(update: Update, context: ContextTypes.DEFAULT_TYPE): 26 | """ 27 | Информация о боте при использовании команды /about 28 | """ 29 | await context.bot.send_message( 30 | chat_id=update.effective_chat.id, 31 | text="*MIREA Teacher Schedule Bot*\n" 32 | "*Разработан* [necrosskull](https://github.com/necrosskull)\n\n" 33 | "*Исходный код: https://github.com/necrosskull/mirea-teacher-schedule-bot*", 34 | parse_mode="Markdown", 35 | ) 36 | 37 | 38 | def init_handlers(application): 39 | application.add_handler(CommandHandler("start", start)) 40 | application.add_handler(CommandHandler("about", about)) 41 | application.add_handler(CommandHandler("help", start)) 42 | -------------------------------------------------------------------------------- /bot/handlers/favorite.py: -------------------------------------------------------------------------------- 1 | from telegram import Update 2 | from telegram.ext import ( 3 | Application, 4 | CommandHandler, 5 | ContextTypes, 6 | ConversationHandler, 7 | MessageHandler, 8 | filters, 9 | ) 10 | 11 | from bot.db.database import add_favorite, get_user_favorites, insert_new_user 12 | 13 | ASK_FAVOURITE = map(chr, range(3, 4)) 14 | 15 | 16 | async def save_favourite(update: Update, context: ContextTypes.DEFAULT_TYPE): 17 | """ 18 | Привествие бота при использовании команды /start 19 | """ 20 | insert_new_user(update, context) 21 | await context.bot.send_message( 22 | chat_id=update.effective_chat.id, 23 | text="ℹ️ Введите запрос для сохранения в избранное\n\nПример: `ИКБО-20-23`", 24 | parse_mode="Markdown", 25 | ) 26 | return ASK_FAVOURITE 27 | 28 | 29 | async def ask_favourite(update: Update, context: ContextTypes.DEFAULT_TYPE): 30 | add_favorite(update, context) 31 | query = get_user_favorites(update, context) 32 | await context.bot.send_message( 33 | chat_id=update.effective_chat.id, 34 | text=f"✅ Успешно добавлено: {query}\n\nЧтобы посмотреть сохраненное расписание, используйте команду /fav", 35 | parse_mode="Markdown", 36 | ) 37 | return ConversationHandler.END 38 | 39 | 40 | def init_handlers(application: Application): 41 | conv_handler = ConversationHandler( 42 | entry_points=[CommandHandler("save", save_favourite, block=False)], 43 | states={ 44 | ASK_FAVOURITE: [ 45 | MessageHandler( 46 | filters.TEXT & ~filters.COMMAND, ask_favourite, block=False 47 | ) 48 | ], 49 | }, 50 | fallbacks=[CommandHandler("save", save_favourite, block=False)], 51 | ) 52 | application.add_handler(conv_handler) 53 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | APP_NAME: mirea-teacher-schedule-bot 10 | REGISTRY_IMAGE: ghcr.io/necrosskull/mirea-teacher-schedule-bot 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Login to Registry 19 | run: echo ${{ secrets.CR_TOKEN }} | docker login ghcr.io -u USERNAME --password-stdin 20 | - name: Build and Push Image 21 | run: | 22 | docker build \ 23 | --pull \ 24 | --cache-from ${{ env.REGISTRY_IMAGE }}:latest \ 25 | --label "org.image.title=${{ github.repository }}" \ 26 | --label "org.image.url=${{ github.repositoryUrl }}" \ 27 | --label "org.image.created=${{ github.event.created_at }}" \ 28 | --label "org.image.revision=${{ github.ref_name }}" \ 29 | --label "org.image.version=${{ github.sha }}" \ 30 | --tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} \ 31 | . 32 | docker push ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} 33 | docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.REGISTRY_IMAGE }}:latest 34 | docker push ${{ env.REGISTRY_IMAGE }}:latest 35 | 36 | deploy: 37 | runs-on: bot-runner 38 | needs: [build] 39 | steps: 40 | - name: Login to Registry 41 | run: echo ${{ secrets.CR_TOKEN }} | docker login ghcr.io -u USERNAME --password-stdin 42 | - name: Deploy 43 | run: | 44 | docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} 45 | docker container rm -f ${{ env.APP_NAME }} 46 | docker run -d --name ${{ env.APP_NAME }} --env-file ${{ secrets.ENV }} \ 47 | ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} 48 | -------------------------------------------------------------------------------- /bot/db/database.py: -------------------------------------------------------------------------------- 1 | from telegram import Update 2 | from telegram.ext import ContextTypes 3 | 4 | from bot.db.sqlite import ScheduleBot, db 5 | 6 | 7 | def insert_new_user(update: Update, context: ContextTypes.DEFAULT_TYPE): 8 | """ 9 | Добавление нового пользователя в базу данных 10 | @param update: Обновление 11 | @param context: Контекст 12 | @return: None 13 | """ 14 | user = update.effective_user 15 | try: 16 | db.connect() 17 | 18 | usr, created = ScheduleBot.get_or_create( 19 | id=user.id, 20 | defaults={ 21 | "username": user.username, 22 | "first_name": user.first_name, 23 | "last_name": user.last_name, 24 | }, 25 | ) 26 | 27 | if not created: 28 | usr.username = user.username 29 | usr.first_name = user.first_name 30 | usr.last_name = user.last_name 31 | usr.save() 32 | 33 | except Exception: 34 | pass 35 | finally: 36 | db.close() 37 | 38 | 39 | def add_favorite(update: Update, context: ContextTypes.DEFAULT_TYPE): 40 | try: 41 | db.connect() 42 | user = ScheduleBot.get_by_id(update.effective_user.id) 43 | user.favorite = update.message.text 44 | user.save() 45 | except Exception: 46 | pass 47 | finally: 48 | db.close() 49 | 50 | 51 | def get_user_favorites(update: Update, context: ContextTypes.DEFAULT_TYPE): 52 | try: 53 | db.connect() 54 | user = ScheduleBot.get_or_none( 55 | ScheduleBot.id == update.effective_user.id, 56 | ScheduleBot.favorite.is_null(False), 57 | ) 58 | 59 | if user: 60 | favorites = user.favorite 61 | return favorites 62 | else: 63 | return None 64 | except Exception: 65 | return None 66 | finally: 67 | db.close() 68 | -------------------------------------------------------------------------------- /bot/fetch/search.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import httpx 4 | 5 | from bot.config import settings 6 | from bot.fetch.models import ScheduleEndpoints, SearchItem, SearchResults 7 | 8 | 9 | async def search_schedule(query) -> list[SearchItem] | None: 10 | base_url = f"{settings.api_url}/api/v1/schedule/search/" 11 | 12 | results = {} 13 | async with httpx.AsyncClient() as client: 14 | tasks = [] 15 | 16 | for search_type in ScheduleEndpoints: 17 | url = base_url + search_type.value 18 | params = {"query": query} 19 | 20 | task = client.get(url, params=params) 21 | tasks.append(task) 22 | 23 | try: 24 | responses = await asyncio.gather(*tasks) 25 | 26 | for search_type, response in zip( 27 | ScheduleEndpoints, 28 | responses, 29 | ): 30 | search_type = search_type.value 31 | response.raise_for_status() 32 | json_response = response.json() 33 | 34 | results[search_type] = [] 35 | 36 | if "results" in json_response and len(json_response["results"]) > 0: 37 | for item in json_response.get("results", []): 38 | item["type"] = search_type 39 | if search_type == "classrooms": 40 | campus_short_name = item.get("campus", {}).get( 41 | "short_name", "" 42 | ) 43 | if campus_short_name: 44 | item["name"] = f"{item['name']} ({campus_short_name})" 45 | else: 46 | item["name"] = item["name"] 47 | 48 | results[search_type].append(SearchItem(**item)) 49 | 50 | search_results = SearchResults(**results) 51 | except Exception: 52 | return None 53 | return [item for _, items in search_results for item in items] 54 | -------------------------------------------------------------------------------- /bot/fetch/models.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from datetime import date, datetime 3 | from typing import Annotated 4 | 5 | from pydantic import BaseModel, BeforeValidator, field_validator 6 | 7 | 8 | class SearchItem(BaseModel): 9 | type: str 10 | uid: int 11 | name: str | None = "" 12 | 13 | @field_validator("type") 14 | def singularize_type(cls, value): 15 | return value[:-1] if value.endswith("s") else value 16 | 17 | 18 | class ScheduleEndpoints(enum.Enum): 19 | teachers = "teachers" 20 | groups = "groups" 21 | classrooms = "classrooms" 22 | 23 | 24 | class SearchResults(BaseModel): 25 | teachers: list[SearchItem] | None = None 26 | groups: list[SearchItem] | None = None 27 | classrooms: list[SearchItem] | None = None 28 | 29 | 30 | class Campus(BaseModel): 31 | latitude: float | None = "" 32 | longitude: float | None = "" 33 | name: str | None = "" 34 | short_name: str | None = "" 35 | 36 | 37 | class Classroom(BaseModel): 38 | campus: Campus | None = None 39 | name: str | None = "" 40 | 41 | 42 | class Teacher(BaseModel): 43 | name: str | None = "" 44 | 45 | 46 | class LessonBells(BaseModel): 47 | end_time: str | None = "" 48 | number: int | None = "" 49 | start_time: str | None = "" 50 | 51 | 52 | def validate_dates(value: list[str]) -> list[date]: 53 | return [datetime.strptime(date, "%d-%m-%Y").date() for date in set(value)] 54 | 55 | 56 | Dates = Annotated[list[date], BeforeValidator(validate_dates)] 57 | 58 | 59 | class LessonSchedule(BaseModel): 60 | classrooms: list[Classroom] | None = None 61 | dates: Dates | None = None 62 | groups: list[str] | None = "" 63 | lesson_bells: LessonBells 64 | lesson_type: str | None = "" 65 | subject: str | None = "" 66 | teachers: list[Teacher] | None = None 67 | type: str | None = "" 68 | 69 | 70 | class Holiday(BaseModel): 71 | dates: Dates | None = None 72 | title: str | None = "" 73 | type: str | None = "" 74 | 75 | 76 | class Lesson(LessonSchedule): 77 | dates: date 78 | 79 | 80 | class ScheduleData(BaseModel): 81 | data: list[LessonSchedule | Holiday] 82 | -------------------------------------------------------------------------------- /bot/handlers/events.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from telegram import Update 4 | from telegram.ext import Application, CommandHandler, ContextTypes 5 | 6 | import bot.logs.lazy_logger as logger 7 | from bot.config import settings 8 | from bot.db.sqlite import ScheduleBot, db 9 | 10 | 11 | async def toggle_maintenance_mode(update: Update, context: ContextTypes.DEFAULT_TYPE): 12 | """Toggle maintenance mode""" 13 | 14 | if update.message.from_user.id not in settings.admins: 15 | return 16 | 17 | maintenance_message = " ".join(context.args) if context.args else None 18 | context.bot_data["maintenance_message"] = maintenance_message 19 | 20 | if context.bot_data["maintenance_mode"]: 21 | context.bot_data["maintenance_mode"] = False 22 | await context.bot.send_message( 23 | chat_id=update.effective_chat.id, 24 | text="❌ Режим обслуживания отключен", 25 | ) 26 | else: 27 | context.bot_data["maintenance_mode"] = True 28 | await context.bot.send_message( 29 | chat_id=update.effective_chat.id, 30 | text="✅ Режим обслуживания включен", 31 | ) 32 | 33 | 34 | async def send_message_to_all_users(update: Update, context: ContextTypes.DEFAULT_TYPE): 35 | """Send message to all users""" 36 | 37 | if update.message.from_user.id not in settings.admins: 38 | return 39 | 40 | if not context.args: 41 | return 42 | 43 | message = update.message.text[6:] 44 | try: 45 | db.connect() 46 | 47 | users = ScheduleBot.select() 48 | user_ids = [user.id for user in users] 49 | 50 | except Exception: 51 | pass 52 | finally: 53 | db.close() 54 | 55 | for user in user_ids: 56 | await asyncio.sleep(0.5) 57 | try: 58 | await context.bot.send_message( 59 | chat_id=user, 60 | text=message, 61 | parse_mode="Markdown", 62 | disable_web_page_preview=True, 63 | ) 64 | logger.lazy_logger.logger.info(f"Message sent to {user}") 65 | except Exception as e: 66 | logger.lazy_logger.logger.info(f"Error sending message to {user}: {e}") 67 | try: 68 | db.connect() 69 | ScheduleBot.delete_by_id(user) 70 | except Exception: 71 | pass 72 | finally: 73 | db.close() 74 | 75 | 76 | def init_handlers(application: Application): 77 | application.add_handler( 78 | CommandHandler("work", toggle_maintenance_mode, block=False) 79 | ) 80 | application.add_handler( 81 | CommandHandler("send", send_message_to_all_users, block=False) 82 | ) 83 | -------------------------------------------------------------------------------- /bot/parse/semester.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | class Period: 5 | def __init__(self, year_start, year_end, semester): 6 | self.year_start = year_start 7 | self.year_end = year_end 8 | self.semester = semester 9 | 10 | 11 | class week: 12 | week: int 13 | 14 | 15 | class weekday: 16 | weekday: int 17 | 18 | 19 | def get_semester_start_date(year_start, year_end, semester): 20 | if semester == 1: 21 | start_date = datetime.date(year_start, 9, 1) 22 | if start_date.weekday() == 6: 23 | start_date += datetime.timedelta(days=1) 24 | return start_date 25 | 26 | start_date = datetime.date(year_end, 2, 1) 27 | start_date += datetime.timedelta(days=8) 28 | 29 | if start_date.weekday() == 6: 30 | start_date += datetime.timedelta(days=1) 31 | 32 | return start_date 33 | 34 | 35 | def get_period(date: datetime.date) -> Period: 36 | if date.month >= 8: 37 | return Period(date.year, date.year + 1, 1) 38 | elif date.month < 2: # Если ещё январь, то это первый семестр 39 | return Period(date.year - 1, date.year, 1) 40 | else: 41 | return Period(date.year - 1, date.year, 2) 42 | 43 | 44 | def get_semester_start_date_from_period(): 45 | current_date = datetime.date.today() 46 | period = get_period(current_date) 47 | semester_start_date = get_semester_start_date( 48 | period.year_start, period.year_end, period.semester 49 | ) 50 | return semester_start_date 51 | 52 | 53 | def get_current_week_number() -> int: 54 | current_date = datetime.date.today() 55 | semester_start_date = get_semester_start_date_from_period() 56 | 57 | if current_date < semester_start_date: 58 | semester_start_date = semester_start_date.replace(year=current_date.year - 1) 59 | 60 | week = (current_date - semester_start_date).days // 7 + 1 61 | 62 | return week 63 | 64 | 65 | def get_week_by_date(date: datetime.date | str) -> int: 66 | if isinstance(date, str): 67 | date = datetime.datetime.strptime(date, "%Y-%m-%d").date() 68 | 69 | semester_start_date = get_semester_start_date_from_period() 70 | 71 | if date < semester_start_date: 72 | semester_start_date = semester_start_date.replace(year=date.year - 1) 73 | 74 | if date < semester_start_date: 75 | return 1 76 | 77 | week = (date - semester_start_date).days // 7 + 1 78 | 79 | return week 80 | 81 | 82 | def get_date(week: int, day: int) -> list[datetime.date]: 83 | semester_start_date = get_semester_start_date_from_period() 84 | start_weekday = semester_start_date.weekday() + 1 85 | weekday_diff = day - start_weekday 86 | days_to_add = (week - 1) * 7 87 | return [semester_start_date + datetime.timedelta(days=days_to_add + weekday_diff)] 88 | 89 | 90 | def get_dates_for_week(week_number: int) -> list[datetime.date]: 91 | semester_start_date = get_semester_start_date_from_period() 92 | start_weekday = semester_start_date.weekday() + 1 93 | days_to_add = (week_number - 1) * 7 - start_weekday + 1 94 | start_date_of_week = semester_start_date + datetime.timedelta(days=days_to_add) 95 | return [start_date_of_week + datetime.timedelta(days=i) for i in range(7)][:6] 96 | 97 | 98 | def get_week_and_weekday(date: datetime.date | str) -> tuple[int, int]: 99 | if isinstance(date, str): 100 | date = datetime.datetime.strptime(date, "%Y-%m-%d").date() 101 | 102 | semester_start_date = get_semester_start_date_from_period() 103 | 104 | if date < semester_start_date: 105 | semester_start_date = semester_start_date.replace(year=date.year - 1) 106 | 107 | if date < semester_start_date: 108 | return 1, date.weekday() + 1 109 | 110 | week = (date - semester_start_date).days // 7 + 1 111 | weekday = date.weekday() + 1 112 | 113 | return week, weekday 114 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | .idea/inspectionProfiles/profiles_settings.xml 154 | .idea/teacherbot.iml 155 | .idea/modules.xml 156 | .idea/misc.xml 157 | .idea/inspectionProfiles/Project_Default.xml 158 | .idea/vcs.xml 159 | /testinline.py 160 | weekcheck.py 161 | <<<<<<< HEAD 162 | 163 | .idea/ 164 | ======= 165 | >>>>>>> main 166 | bot/exambot.py 167 | bot/exams.json 168 | bot/inlinebot.py 169 | bot/rassilka.py 170 | exams.json 171 | rassilka.py 172 | test2.py 173 | bot/cmstest.py 174 | bot/handlers/roomtester.py 175 | bot/db/data/bot.db 176 | bot/test.py 177 | bot/old_main.py 178 | -------------------------------------------------------------------------------- /bot/parse/formating.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from telegram.ext import ContextTypes 4 | 5 | from bot.fetch.models import Lesson 6 | from bot.logs.lazy_logger import lazy_logger 7 | from bot.parse.semester import get_week_and_weekday 8 | 9 | 10 | def format_outputs(lessons: list[Lesson], context: ContextTypes.DEFAULT_TYPE): 11 | """ 12 | Format the parsed schedule into human-readable text blocks. 13 | 14 | Parameters: 15 | - parsed_schedule (list): List of dictionaries representing parsed schedule data. 16 | - context (object): Context object containing user-specific data. 17 | 18 | Returns: 19 | - blocks (list): List of formatted text blocks. 20 | 21 | """ 22 | text = "" 23 | 24 | WEEKDAYS = { 25 | 1: "Понедельник", 26 | 2: "Вторник", 27 | 3: "Среда", 28 | 4: "Четверг", 29 | 5: "Пятница", 30 | 6: "Суббота", 31 | } 32 | 33 | MONTHS = { 34 | 1: "Января", 35 | 2: "Февраля", 36 | 3: "Марта", 37 | 4: "Апреля", 38 | 5: "Мая", 39 | 6: "Июня", 40 | 7: "Июля", 41 | 8: "Августа", 42 | 9: "Сентября", 43 | 10: "Октября", 44 | 11: "Ноября", 45 | 12: "Декабря", 46 | } 47 | 48 | blocks = [] 49 | 50 | for lesson in lessons: 51 | error_message = None 52 | week, weekday = get_week_and_weekday(lesson.dates) 53 | match lesson.lesson_type.lower(): 54 | case "lecture": 55 | lesson_type = "Лекция" 56 | case "laboratorywork": 57 | lesson_type = "Лабораторная" 58 | case "practice": 59 | lesson_type = "Практика" 60 | case "individualwork": 61 | lesson_type = "Сам. работа" 62 | case "exam": 63 | lesson_type = "Экзамен" 64 | case "consultation": 65 | lesson_type = "Консультация" 66 | case "coursework": 67 | lesson_type = "Курс. раб." 68 | case "courseproject": 69 | lesson_type = "Курс. проект" 70 | case "credit": 71 | lesson_type = "Зачет" 72 | case _: 73 | lesson_type = "Неизвестно" 74 | 75 | formatted_time = ( 76 | f"{lesson.lesson_bells.start_time} – {lesson.lesson_bells.end_time}" 77 | ) 78 | 79 | groups = ", ".join(lesson.groups) 80 | teachers = ", ".join(teacher.name for teacher in lesson.teachers) 81 | campus = ( 82 | f"({lesson.classrooms[0].campus.short_name})" 83 | if lesson.classrooms and lesson.classrooms[0].campus 84 | else "" 85 | ) 86 | room = lesson.classrooms[0].name if lesson.classrooms else "" 87 | 88 | try: 89 | text += f"📝 Пара № {lesson.lesson_bells.number} в ⏰ {formatted_time}\n" 90 | text += f"📝 {lesson.subject}\n" 91 | text += f"📚 {lesson_type}\n" 92 | if len(groups) > 0: 93 | text += f"👥 Группы: {groups}\n" 94 | text += f"👨🏻‍🏫 Преподаватели: {teachers}\n" 95 | text += f"🏫 Аудитории: {room} {campus}\n" 96 | text += f"📅 Неделя: {week}\n" 97 | text += f"🗓️ {lesson.dates.day} {MONTHS[lesson.dates.month]} ({WEEKDAYS[weekday]})\n\n" 98 | 99 | blocks.append(text) 100 | text = "" 101 | 102 | except Exception as e: 103 | target_info = { 104 | "type": "error", 105 | "item": context.user_data["item"].model_dump(), 106 | "week": week, 107 | "weekday": weekday, 108 | "error": str(e), 109 | } 110 | 111 | if str(e) != error_message: 112 | error_message = str(e) 113 | lazy_logger.logger.error(json.dumps(target_info, ensure_ascii=False)) 114 | text = "Ошибка при получении расписания" 115 | blocks.append(text) 116 | text = "" 117 | 118 | return blocks 119 | 120 | return blocks 121 | -------------------------------------------------------------------------------- /bot/handlers/construct.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from telegram import InlineKeyboardButton, InlineKeyboardMarkup 4 | 5 | from bot.fetch.models import Lesson, ScheduleData, SearchItem 6 | from bot.fetch.schedule import get_lessons 7 | from bot.handlers import ImportantDays as ImportantDays 8 | from bot.parse.semester import get_current_week_number, get_dates_for_week 9 | 10 | 11 | def construct_item_markup(schedule_items: list[SearchItem]) -> InlineKeyboardMarkup: 12 | btns = [] 13 | for item in schedule_items: 14 | callback = f"{item.type}:{item.uid}" 15 | btns = btns + [[InlineKeyboardButton(item.name, callback_data=callback)]] 16 | btns = btns + [[(InlineKeyboardButton("Назад", callback_data="back"))]] 17 | TEACHER_CLARIFY_MARKUP = InlineKeyboardMarkup(btns) 18 | 19 | return TEACHER_CLARIFY_MARKUP 20 | 21 | 22 | def construct_weeks_markup(): 23 | """ 24 | Создает KeyboardMarkup со списком недель, а также подставляет эмодзи 25 | если текущий день соответствует некоторой памятной дате+-интервал 26 | """ 27 | current_week = get_current_week_number() 28 | week_indicator = "◖" 29 | week_indicator1 = "◗" 30 | today = datetime.date.today() 31 | 32 | for day in ImportantDays.important_days: 33 | if abs((day[ImportantDays.DATE] - today).days) <= day[ImportantDays.INTERVAL]: 34 | week_indicator = day[ImportantDays.SIGN] 35 | week_indicator1 = day[ImportantDays.SIGN] 36 | 37 | week_buttons = [] 38 | row_buttons = [] 39 | 40 | week_constraint = 18 41 | 42 | for i in range(1, week_constraint): 43 | button_text = ( 44 | f"{week_indicator}{i}{week_indicator1}" if i == current_week else str(i) 45 | ) 46 | row_buttons.append(InlineKeyboardButton(text=button_text, callback_data=i)) 47 | 48 | if len(row_buttons) == 4 or i == week_constraint - 1: 49 | week_buttons.append(tuple(row_buttons)) 50 | row_buttons = [] 51 | 52 | date_buttons = [ 53 | [ 54 | InlineKeyboardButton("Сегодня", callback_data="today"), 55 | InlineKeyboardButton("Завтра", callback_data="tomorrow"), 56 | ], 57 | [InlineKeyboardButton("Назад", callback_data="back")], 58 | ] 59 | 60 | if current_week >= 17: 61 | if current_week == 17: 62 | current_week_button = [ 63 | [ 64 | InlineKeyboardButton("18", callback_data=18), 65 | InlineKeyboardButton("19", callback_data=19), 66 | ] 67 | ] 68 | else: 69 | current_week_button = [ 70 | [ 71 | InlineKeyboardButton( 72 | f"{current_week - 1 if current_week > 18 else ''}", 73 | callback_data=current_week - 1, 74 | ), 75 | InlineKeyboardButton( 76 | f"◖{current_week}◗", callback_data=current_week 77 | ), 78 | InlineKeyboardButton( 79 | f"{current_week + 1}", callback_data=current_week + 1 80 | ), 81 | ] 82 | ] 83 | else: 84 | current_week_button = [] 85 | 86 | reply_mark = InlineKeyboardMarkup(week_buttons + current_week_button + date_buttons) 87 | 88 | return reply_mark 89 | 90 | 91 | def construct_workdays(week: int, schedule: ScheduleData, selected_date=None): 92 | weekdays = { 93 | 1: "ПН", 94 | 2: "ВТ", 95 | 3: "СР", 96 | 4: "ЧТ", 97 | 5: "ПТ", 98 | 6: "СБ", 99 | } 100 | 101 | dates = get_dates_for_week(week) 102 | lessons: list[Lesson] = get_lessons(schedule, dates) 103 | 104 | lesson_dates = [lesson.dates for lesson in lessons] 105 | 106 | button_rows = [] 107 | row = [] 108 | 109 | for i, date in enumerate(dates, start=1): 110 | sign = "" 111 | sign1 = "" 112 | callback = str(date) 113 | 114 | if ( 115 | selected_date 116 | and date 117 | == datetime.datetime.strptime(str(selected_date), "%Y-%m-%d").date() 118 | ): 119 | sign = "◖" 120 | sign1 = "◗" 121 | 122 | if date not in lesson_dates: 123 | sign = "⛔" 124 | callback = "chill" 125 | 126 | row.append( 127 | InlineKeyboardButton( 128 | text=f"{sign}{weekdays[i]}{sign1 if sign1 else sign}", 129 | callback_data=callback, 130 | ) 131 | ) 132 | 133 | if len(row) == 3 or i == 6: 134 | button_rows.append(tuple(row)) 135 | row = [] 136 | 137 | if lesson_dates: 138 | button_rows.append( 139 | (InlineKeyboardButton(text="На неделю", callback_data="week"),) 140 | ) 141 | 142 | button_rows.append((InlineKeyboardButton(text="Назад", callback_data="back"),)) 143 | ready_markup = InlineKeyboardMarkup(button_rows) 144 | 145 | return ready_markup 146 | -------------------------------------------------------------------------------- /bot/handlers/send.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from telegram import Update 4 | from telegram.ext import ContextTypes 5 | 6 | from bot.fetch.models import SearchItem 7 | from bot.fetch.schedule import get_lessons 8 | from bot.handlers import construct as construct 9 | from bot.handlers import states as st 10 | from bot.parse.formating import format_outputs 11 | from bot.parse.semester import ( 12 | get_dates_for_week, 13 | get_week_and_weekday, 14 | ) 15 | 16 | 17 | async def send_item_clarity( 18 | update: Update, context: ContextTypes.DEFAULT_TYPE, firsttime=False 19 | ): 20 | schedule_items = context.user_data["available_items"] 21 | few_teachers_markup = construct.construct_item_markup(schedule_items) 22 | if firsttime: 23 | message = await context.bot.send_message( 24 | chat_id=update.effective_chat.id, 25 | text="ℹ️ Выберите расписание:", 26 | reply_markup=few_teachers_markup, 27 | ) 28 | context.user_data["message_id"] = message.message_id 29 | 30 | else: 31 | await update.callback_query.edit_message_text( 32 | text="ℹ️ Выберите расписание:", reply_markup=few_teachers_markup 33 | ) 34 | 35 | return st.ITEM_CLARIFY 36 | 37 | 38 | async def send_week_selector( 39 | update: Update, context: ContextTypes.DEFAULT_TYPE, firsttime=False 40 | ): 41 | selected_item: SearchItem = context.user_data["item"] 42 | type_text = "" 43 | if len(selected_item.name) > 0: 44 | match selected_item.type: 45 | case "teacher": 46 | type_text = f"ℹ️ Расписание преподавателя: {selected_item.name}" 47 | case "classroom": 48 | type_text = f"ℹ️ Расписание аудитории: {selected_item.name}" 49 | case "group": 50 | type_text = f"ℹ️ Расписание группы: {selected_item.name}" 51 | 52 | text = f"{type_text}\n🗓️ Выберите неделю:" 53 | 54 | if firsttime: 55 | message = await context.bot.send_message( 56 | chat_id=update.effective_chat.id, 57 | text=text, 58 | reply_markup=construct.construct_weeks_markup(), 59 | ) 60 | context.user_data["message_id"] = message.message_id 61 | 62 | else: 63 | await update.callback_query.edit_message_text( 64 | text=text, reply_markup=construct.construct_weeks_markup() 65 | ) 66 | 67 | return st.GETWEEK 68 | 69 | 70 | async def send_day_selector(update: Update, context: ContextTypes.DEFAULT_TYPE): 71 | selected_item: SearchItem = context.user_data["item"] 72 | week = context.user_data["week"] 73 | schedule = context.user_data["schedule"] 74 | 75 | workdays = construct.construct_workdays(week, schedule) 76 | 77 | type_text = "" 78 | if len(selected_item.name) > 0: 79 | match selected_item.type: 80 | case "teacher": 81 | type_text = f"ℹ️ Расписание преподавателя: {selected_item.name}" 82 | case "classroom": 83 | type_text = f"ℹ️ Расписание аудитории: {selected_item.name}" 84 | case "group": 85 | type_text = f"ℹ️ Расписание группы: {selected_item.name}" 86 | 87 | text = f"{type_text}\n🗓️ Выбрана неделя: {week}\n📅 Выберите день:" 88 | 89 | await update.callback_query.edit_message_text( 90 | text=text, 91 | reply_markup=workdays, 92 | ) 93 | 94 | return st.GETDAY 95 | 96 | 97 | async def send_result( 98 | update: Update, context: ContextTypes.DEFAULT_TYPE, show_week=False 99 | ): 100 | schedule_data = context.user_data["schedule"] 101 | 102 | date = context.user_data.get("date", None) 103 | week = context.user_data.get("week", None) 104 | 105 | if week: 106 | week = int(week) 107 | else: 108 | week, _ = get_week_and_weekday(date) 109 | 110 | dates_list = [] 111 | 112 | if show_week: 113 | dates_list = get_dates_for_week(week) 114 | else: 115 | dates_list = [datetime.strptime(str(date), "%Y-%m-%d").date()] 116 | 117 | lessons = get_lessons(schedule_data, dates_list) 118 | 119 | if len(lessons) == 0: 120 | await update.callback_query.answer(text="В этот день пар нет.", show_alert=True) 121 | return st.GETWEEK 122 | 123 | blocks_of_text = format_outputs(lessons, context) 124 | 125 | return await telegram_delivery_optimisation( 126 | update, context, blocks_of_text, show_week=show_week 127 | ) 128 | 129 | 130 | async def telegram_delivery_optimisation( 131 | update: Update, context: ContextTypes.DEFAULT_TYPE, blocks: list, show_week=False 132 | ): 133 | week = context.user_data.get("week", None) 134 | date = context.user_data.get("date", None) 135 | 136 | if week is None: 137 | week, _ = get_week_and_weekday(date) 138 | 139 | schedule = context.user_data["schedule"] 140 | 141 | if show_week: 142 | workdays = construct.construct_workdays(week, schedule) 143 | else: 144 | workdays = construct.construct_workdays(week, schedule, selected_date=date) 145 | 146 | chunk = "" 147 | first = True 148 | for block in blocks: 149 | if len(chunk) + len(block) <= 4096: 150 | chunk += block 151 | 152 | else: 153 | if first: 154 | if update.callback_query.inline_message_id: 155 | await update.callback_query.answer( 156 | text="Слишком длинное расписание, пожалуйста, воспользуйтесь личными сообщениями бота или " 157 | "выберите конкретный день недели", 158 | show_alert=True, 159 | ) 160 | break 161 | 162 | await update.callback_query.edit_message_text(chunk) 163 | first = False 164 | 165 | else: 166 | await context.bot.send_message( 167 | chat_id=update.effective_chat.id, text=chunk 168 | ) 169 | 170 | chunk = block 171 | 172 | if chunk: 173 | if first: 174 | await update.callback_query.edit_message_text(chunk, reply_markup=workdays) 175 | 176 | else: 177 | message = await context.bot.send_message( 178 | chat_id=update.effective_chat.id, 179 | text=chunk, 180 | reply_markup=workdays, 181 | ) 182 | context.user_data["message_id"] = message.message_id 183 | 184 | return st.GETDAY 185 | 186 | 187 | async def resend_name_input(update: Update, context: ContextTypes.DEFAULT_TYPE): 188 | await update.callback_query.answer(text="Введите новый запрос.", show_alert=True) 189 | -------------------------------------------------------------------------------- /bot/handlers/inline.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from telegram import InlineQueryResultArticle, InputTextMessageContent, Update 4 | from telegram.ext import ( 5 | Application, 6 | CallbackContext, 7 | CallbackQueryHandler, 8 | ChosenInlineResultHandler, 9 | ContextTypes, 10 | InlineQueryHandler, 11 | ) 12 | 13 | import bot.handlers.construct as construct 14 | import bot.handlers.handler as handler 15 | import bot.logs.lazy_logger as logger 16 | from bot.db.database import get_user_favorites 17 | from bot.fetch.models import SearchItem 18 | from bot.fetch.schedule import get_schedule 19 | from bot.fetch.search import search_schedule 20 | from bot.handlers import states as st 21 | from bot.handlers.states import EInlineStep 22 | 23 | 24 | async def handle_inline_query(update: Update, context: ContextTypes.DEFAULT_TYPE): 25 | """ 26 | Обработчик инлайн запросов 27 | Создает Inline отображение 28 | """ 29 | 30 | if context.bot_data["maintenance_mode"]: 31 | return 32 | 33 | if len(update.inline_query.query) > 2: 34 | logger.lazy_logger.logger.info( 35 | json.dumps( 36 | { 37 | "type": "query", 38 | "queryId": update.inline_query.id, 39 | "query": update.inline_query.query.lower(), 40 | **update.inline_query.from_user.to_dict(), 41 | }, 42 | ensure_ascii=False, 43 | ) 44 | ) 45 | 46 | inline_query = update.inline_query 47 | query = inline_query.query.lower() 48 | 49 | await handle_query(update, context, query) 50 | 51 | 52 | async def handle_query(update: Update, context: CallbackContext, query: str): 53 | inline_results = [] 54 | schedule_items = [] 55 | description = "" 56 | favorite = get_user_favorites(update, context) 57 | 58 | if favorite: 59 | description = "Сохраненное расписание" 60 | schedule_items: list[SearchItem] = await search_schedule(favorite) 61 | 62 | if len(query) > 2: 63 | description = "Нажми, чтобы посмотреть расписание" 64 | inline_results = [] 65 | schedule_items: list[SearchItem] = await search_schedule(query) 66 | 67 | for item in schedule_items: 68 | name = item.name 69 | if item.type == "teacher": 70 | name_parts = item.name.split() 71 | 72 | if len(name_parts) > 1: 73 | last_name = name_parts[0] 74 | 75 | # Для запроса вида "Иванов И.И. или Иванов И.И" 76 | if ( 77 | name_parts[1][-1] == "." 78 | or len(name_parts[1]) > 1 79 | and name_parts[1][-2] == "." 80 | ): 81 | initials = name_parts[1] 82 | else: 83 | # Для запроса вида "Иванов Иван Иванович и прочих" 84 | initials = "".join([part[0] + "." for part in name_parts[1:3]]) 85 | 86 | name = last_name + " " + initials 87 | id_str = f"{item.type}:{item.uid}:{name}" 88 | 89 | inline_results.append( 90 | InlineQueryResultArticle( 91 | id=id_str, 92 | title=item.name, 93 | description=description, 94 | input_message_content=InputTextMessageContent( 95 | message_text=f"ℹ️ Выбрано расписание: {item.name}!\n" 96 | + "🗓️ Выберите неделю:" 97 | ), 98 | reply_markup=construct.construct_weeks_markup(), 99 | ) 100 | ) 101 | 102 | return await update.inline_query.answer( 103 | inline_results, 104 | cache_time=5, 105 | is_personal=True, 106 | ) 107 | 108 | 109 | async def answer_inline_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): 110 | """ 111 | В случае отработки события ChosenInlineHandler запоминает выбранного преподавателя 112 | и выставляет текущий шаг Inline запроса на ask_day 113 | """ 114 | if update.chosen_inline_result is not None: 115 | type, uid, name = update.chosen_inline_result.result_id.split(":") 116 | 117 | selected_item = SearchItem(type=type, uid=uid, name=name) 118 | 119 | context.user_data["item"] = selected_item 120 | 121 | context.user_data["inline_step"] = EInlineStep.ask_week 122 | context.user_data[ 123 | "inline_message_id" 124 | ] = update.chosen_inline_result.inline_message_id 125 | context.user_data["message_id"] = update.chosen_inline_result.inline_message_id 126 | 127 | return 128 | 129 | 130 | async def inline_dispatcher(update: Update, context: CallbackContext): 131 | """ 132 | Обработка вызовов в чатах на основании Callback вызова 133 | """ 134 | if "inline_step" not in context.user_data: 135 | await deny_inline_usage(update) 136 | return 137 | 138 | # Если Id сообщения в котором мы нажимаем на кнопки не совпадает с тем, что было сохранено в контексте при вызове 139 | # меню, то отказываем в обработке 140 | 141 | if ( 142 | update.callback_query.inline_message_id 143 | and update.callback_query.inline_message_id 144 | != context.user_data["inline_message_id"] 145 | ): 146 | await deny_inline_usage(update) 147 | return 148 | 149 | status = context.user_data["inline_step"] 150 | if status == EInlineStep.completed or status == EInlineStep.ask_item: 151 | await deny_inline_usage(update) 152 | return 153 | 154 | context.user_data["schedule"] = await get_schedule(context.user_data["item"]) 155 | 156 | if status == EInlineStep.ask_week: # Изначально мы находимся на этапе выбора недели 157 | context.user_data["available_items"] = None 158 | 159 | target = await handler.got_week_handler( 160 | update, context 161 | ) # Обработка выбора недели 162 | # Затем как только мы выбрали неделю, мы переходим на этап выбора дня 163 | if target == st.GETDAY: 164 | context.user_data["inline_step"] = EInlineStep.ask_day 165 | 166 | if status == EInlineStep.ask_day: # При выборе дня, статус меняется на ask_day 167 | target = await handler.got_day_handler(update, context) # Обработка выбора дня 168 | 169 | if ( 170 | target == st.GETWEEK 171 | ): # Если пользователь вернулся назад на выбор недели, то мы переходим на этап выбора недели 172 | context.user_data["inline_step"] = EInlineStep.ask_week 173 | 174 | return 175 | 176 | 177 | async def deny_inline_usage(update: Update): 178 | """ 179 | Показывает предупреждение пользователю, если он не может использовать имеющийся Inline вызов 180 | """ 181 | await update.callback_query.answer( 182 | text="Вы не можете использовать это меню, т.к. оно не относится к вашему запросу", 183 | show_alert=True, 184 | ) 185 | return 186 | 187 | 188 | def init_handlers(application: Application): 189 | application.add_handler(InlineQueryHandler(handle_inline_query, block=False)) 190 | application.add_handler( 191 | ChosenInlineResultHandler(answer_inline_handler, block=False) 192 | ) 193 | application.add_handler(CallbackQueryHandler(inline_dispatcher, block=False)) 194 | -------------------------------------------------------------------------------- /bot/handlers/handler.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | 4 | from telegram import Update 5 | from telegram.error import BadRequest 6 | from telegram.ext import ( 7 | Application, 8 | CallbackQueryHandler, 9 | CommandHandler, 10 | ContextTypes, 11 | ConversationHandler, 12 | MessageHandler, 13 | filters, 14 | ) 15 | 16 | from bot.db.database import get_user_favorites, insert_new_user 17 | from bot.fetch.models import SearchItem 18 | from bot.fetch.schedule import get_schedule 19 | from bot.fetch.search import search_schedule 20 | from bot.handlers import send as send 21 | from bot.handlers import states as st 22 | from bot.logs.lazy_logger import lazy_logger 23 | 24 | 25 | async def get_query_handler( 26 | update: Update, context: ContextTypes.DEFAULT_TYPE, fav=None 27 | ): 28 | """ 29 | Реакция бота на получение запроса от пользователя 30 | :param update - Update класс API 31 | :param context - CallbackContext класс API 32 | :return: int сигнатура следующего состояния 33 | """ 34 | if update.message and update.message.via_bot: 35 | return 36 | elif update.edited_message and update.edited_message.via_bot: 37 | return 38 | 39 | insert_new_user(update, context) 40 | 41 | if fav: 42 | user_query = fav 43 | else: 44 | user_query = update.message.text 45 | 46 | lazy_logger.logger.info( 47 | json.dumps( 48 | { 49 | "type": "request", 50 | "query": user_query.lower(), 51 | **update.message.from_user.to_dict(), 52 | }, 53 | ensure_ascii=False, 54 | ) 55 | ) 56 | 57 | if context.bot_data["maintenance_mode"]: 58 | await maintenance_message(update, context) 59 | return 60 | 61 | if len(user_query) < 3: 62 | await context.bot.send_message( 63 | chat_id=update.effective_chat.id, 64 | text="❌ Слишком короткий запрос\nПопробуйте еще раз", 65 | ) 66 | return 67 | 68 | if user_query.lower().startswith("ауд"): 69 | await context.bot.send_message( 70 | chat_id=update.effective_chat.id, 71 | text="ℹ️ Для поиска по аудиториям, просто введите её название, например: `Г-212`", 72 | parse_mode="Markdown", 73 | ) 74 | return 75 | 76 | schedule_items = await search_schedule(user_query) 77 | 78 | if schedule_items is None: 79 | await context.bot.send_message( 80 | chat_id=update.effective_chat.id, 81 | text="❌ Не нашлось результатов по вашему запросу\nПопробуйте еще раз", 82 | ) 83 | return 84 | 85 | if len(schedule_items) > 1: 86 | context.user_data["available_items"] = schedule_items 87 | return await send.send_item_clarity(update, context, True) 88 | 89 | elif len(schedule_items) == 0: 90 | await context.bot.send_message( 91 | chat_id=update.effective_chat.id, 92 | text="❌ Не нашлось результатов по вашему запросу\nПопробуйте еще раз", 93 | parse_mode="Markdown", 94 | ) 95 | return 96 | 97 | else: 98 | context.user_data["available_items"] = None 99 | context.user_data["item"] = schedule_items[0] 100 | context.user_data["schedule"] = await get_schedule(schedule_items[0]) 101 | 102 | return await send.send_week_selector(update, context, True) 103 | 104 | 105 | async def got_item_clarification_handler( 106 | update: Update, context: ContextTypes.DEFAULT_TYPE 107 | ): 108 | query = update.callback_query 109 | 110 | if await deny_old_message(update, context, query=query): 111 | return 112 | 113 | if query.data == "back": 114 | return await send.resend_name_input(update, context) 115 | 116 | type, uid = query.data.split(":") 117 | 118 | schedule_items: list[SearchItem] = context.user_data["available_items"] 119 | 120 | selected_item = None 121 | for item in schedule_items: 122 | if item.type == type and item.uid == int(uid): 123 | selected_item: SearchItem = item 124 | break 125 | 126 | if selected_item not in schedule_items: 127 | await update.callback_query.answer( 128 | text="Ошибка, сделайте новый запрос", show_alert=True 129 | ) 130 | 131 | context.user_data["item"] = selected_item 132 | clarified_schedule = await get_schedule(selected_item) 133 | context.user_data["schedule"] = clarified_schedule 134 | 135 | await query.answer() 136 | 137 | return await send.send_week_selector(update, context) 138 | 139 | 140 | async def got_week_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): 141 | """ 142 | Реакция бота на получение информации о выбранной недели в состоянии GETWEEK 143 | @param update: Update class of API 144 | @param context: CallbackContext of API 145 | @return: Int код шага 146 | """ 147 | query = update.callback_query 148 | 149 | if await deny_old_message(update, context, query=query): 150 | return 151 | 152 | selected_button = query.data 153 | 154 | if selected_button == "back": 155 | if context.user_data["available_items"] is None: 156 | return await send.resend_name_input(update, context) 157 | 158 | return await send.send_item_clarity(update, context) 159 | 160 | elif selected_button == "today": 161 | today = datetime.date.today() 162 | context.user_data["date"] = today 163 | context.user_data["week"] = None 164 | 165 | return await send.send_result(update, context) 166 | 167 | elif selected_button == "tomorrow": 168 | tommorow = datetime.date.today() + datetime.timedelta(days=1) 169 | context.user_data["date"] = tommorow 170 | context.user_data["week"] = None 171 | 172 | return await send.send_result(update, context) 173 | 174 | elif selected_button.isdigit(): 175 | selected_week = int(selected_button) 176 | context.user_data["week"] = selected_week 177 | 178 | return await send.send_day_selector(update, context) 179 | 180 | else: 181 | await update.callback_query.answer( 182 | text="Ошибка, ожидается неделя", show_alert=False 183 | ) 184 | 185 | return st.GETWEEK 186 | 187 | 188 | async def got_day_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): 189 | """ 190 | Реакция бота на выбор дня недели, предоставленный пользователю, в состоянии GETDAY 191 | @param update: Update class of API 192 | @param context: CallbackContext of API 193 | @return: Int код шага 194 | """ 195 | query = update.callback_query 196 | show_week = False 197 | if await deny_old_message(update, context, query=query): 198 | return 199 | 200 | selected_button = query.data 201 | 202 | if selected_button == "chill": 203 | await update.callback_query.answer(text="В этот день пар нет.", show_alert=True) 204 | 205 | return st.GETDAY 206 | 207 | if selected_button == "back": 208 | return await send.send_week_selector(update, context) 209 | 210 | if selected_button == "week": 211 | selected_day = None 212 | show_week = True 213 | 214 | else: 215 | selected_day = selected_button 216 | context.user_data["date"] = selected_day 217 | 218 | try: 219 | await send.send_result(update, context, show_week=show_week) 220 | 221 | except BadRequest: 222 | await update.callback_query.answer( 223 | text="Вы уже выбрали этот день", show_alert=False 224 | ) 225 | else: 226 | await query.answer() 227 | 228 | return st.GETDAY 229 | 230 | 231 | async def deny_old_message( 232 | update: Update, context: ContextTypes.DEFAULT_TYPE, query=None 233 | ): 234 | message_id = None 235 | 236 | if query.inline_message_id: 237 | message_id = query.inline_message_id 238 | if query.message: 239 | message_id = query.message.message_id 240 | 241 | if context.user_data["message_id"] != message_id: 242 | await query.answer( 243 | text="Это сообщение не относится к вашему текущему запросу, повторите ваш запрос!", 244 | show_alert=True, 245 | ) 246 | return True 247 | 248 | 249 | async def maintenance_message(update: Update, context: ContextTypes.DEFAULT_TYPE): 250 | maintenance_text = ( 251 | context.bot_data["maintenance_message"] 252 | if context.bot_data["maintenance_message"] 253 | else None 254 | ) 255 | 256 | text = ( 257 | f"{maintenance_text}" 258 | if maintenance_text 259 | else "Бот находится на техническом обслуживании, скоро всё заработает!" 260 | ) 261 | 262 | await context.bot.send_message( 263 | chat_id=update.effective_chat.id, 264 | text=text, 265 | ) 266 | 267 | 268 | async def favourite(update: Update, context: ContextTypes.DEFAULT_TYPE): 269 | query = get_user_favorites(update, context) 270 | 271 | if not query: 272 | await context.bot.send_message( 273 | chat_id=update.effective_chat.id, 274 | text="❌ У вас нет сохраненного расписания\nПопробуйте добавить его с помощью команды /save", 275 | parse_mode="Markdown", 276 | ) 277 | return 278 | 279 | return await get_query_handler(update, context, fav=query) 280 | 281 | 282 | def init_handlers(application: Application): 283 | conv_handler = ConversationHandler( 284 | entry_points=[ 285 | MessageHandler( 286 | filters.TEXT & ~filters.COMMAND, get_query_handler, block=False 287 | ), 288 | CommandHandler("fav", favourite, block=False), 289 | ], 290 | states={ 291 | st.ITEM_CLARIFY: [ 292 | CallbackQueryHandler(got_item_clarification_handler, block=False) 293 | ], 294 | st.GETDAY: [CallbackQueryHandler(got_day_handler, block=False)], 295 | st.GETWEEK: [CallbackQueryHandler(got_week_handler, block=False)], 296 | }, 297 | fallbacks=[ 298 | MessageHandler( 299 | filters.TEXT & ~filters.COMMAND, get_query_handler, block=False 300 | ), 301 | CommandHandler("fav", favourite, block=False), 302 | ], 303 | ) 304 | application.add_handler(conv_handler) 305 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "annotated-types" 5 | version = "0.6.0" 6 | description = "Reusable constraint types to use with typing.Annotated" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, 11 | {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "4.2.0" 17 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 18 | optional = false 19 | python-versions = ">=3.8" 20 | files = [ 21 | {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, 22 | {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, 23 | ] 24 | 25 | [package.dependencies] 26 | idna = ">=2.8" 27 | sniffio = ">=1.1" 28 | 29 | [package.extras] 30 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 31 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 32 | trio = ["trio (>=0.23)"] 33 | 34 | [[package]] 35 | name = "certifi" 36 | version = "2024.2.2" 37 | description = "Python package for providing Mozilla's CA Bundle." 38 | optional = false 39 | python-versions = ">=3.6" 40 | files = [ 41 | {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, 42 | {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, 43 | ] 44 | 45 | [[package]] 46 | name = "h11" 47 | version = "0.14.0" 48 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 49 | optional = false 50 | python-versions = ">=3.7" 51 | files = [ 52 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 53 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 54 | ] 55 | 56 | [[package]] 57 | name = "httpcore" 58 | version = "1.0.2" 59 | description = "A minimal low-level HTTP client." 60 | optional = false 61 | python-versions = ">=3.8" 62 | files = [ 63 | {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, 64 | {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, 65 | ] 66 | 67 | [package.dependencies] 68 | certifi = "*" 69 | h11 = ">=0.13,<0.15" 70 | 71 | [package.extras] 72 | asyncio = ["anyio (>=4.0,<5.0)"] 73 | http2 = ["h2 (>=3,<5)"] 74 | socks = ["socksio (==1.*)"] 75 | trio = ["trio (>=0.22.0,<0.23.0)"] 76 | 77 | [[package]] 78 | name = "httpx" 79 | version = "0.26.0" 80 | description = "The next generation HTTP client." 81 | optional = false 82 | python-versions = ">=3.8" 83 | files = [ 84 | {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"}, 85 | {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"}, 86 | ] 87 | 88 | [package.dependencies] 89 | anyio = "*" 90 | certifi = "*" 91 | httpcore = "==1.*" 92 | idna = "*" 93 | sniffio = "*" 94 | 95 | [package.extras] 96 | brotli = ["brotli", "brotlicffi"] 97 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 98 | http2 = ["h2 (>=3,<5)"] 99 | socks = ["socksio (==1.*)"] 100 | 101 | [[package]] 102 | name = "idna" 103 | version = "3.6" 104 | description = "Internationalized Domain Names in Applications (IDNA)" 105 | optional = false 106 | python-versions = ">=3.5" 107 | files = [ 108 | {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, 109 | {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, 110 | ] 111 | 112 | [[package]] 113 | name = "peewee" 114 | version = "3.17.1" 115 | description = "a little orm" 116 | optional = false 117 | python-versions = "*" 118 | files = [ 119 | {file = "peewee-3.17.1.tar.gz", hash = "sha256:e009ac4227c4fdc0058a56e822ad5987684f0a1fbb20fed577200785102581c3"}, 120 | ] 121 | 122 | [[package]] 123 | name = "pydantic" 124 | version = "2.6.1" 125 | description = "Data validation using Python type hints" 126 | optional = false 127 | python-versions = ">=3.8" 128 | files = [ 129 | {file = "pydantic-2.6.1-py3-none-any.whl", hash = "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f"}, 130 | {file = "pydantic-2.6.1.tar.gz", hash = "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9"}, 131 | ] 132 | 133 | [package.dependencies] 134 | annotated-types = ">=0.4.0" 135 | pydantic-core = "2.16.2" 136 | typing-extensions = ">=4.6.1" 137 | 138 | [package.extras] 139 | email = ["email-validator (>=2.0.0)"] 140 | 141 | [[package]] 142 | name = "pydantic-core" 143 | version = "2.16.2" 144 | description = "" 145 | optional = false 146 | python-versions = ">=3.8" 147 | files = [ 148 | {file = "pydantic_core-2.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c"}, 149 | {file = "pydantic_core-2.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2"}, 150 | {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a"}, 151 | {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05"}, 152 | {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b"}, 153 | {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8"}, 154 | {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef"}, 155 | {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990"}, 156 | {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b"}, 157 | {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731"}, 158 | {file = "pydantic_core-2.16.2-cp310-none-win32.whl", hash = "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485"}, 159 | {file = "pydantic_core-2.16.2-cp310-none-win_amd64.whl", hash = "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f"}, 160 | {file = "pydantic_core-2.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11"}, 161 | {file = "pydantic_core-2.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978"}, 162 | {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f"}, 163 | {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e"}, 164 | {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8"}, 165 | {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7"}, 166 | {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272"}, 167 | {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113"}, 168 | {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8"}, 169 | {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97"}, 170 | {file = "pydantic_core-2.16.2-cp311-none-win32.whl", hash = "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b"}, 171 | {file = "pydantic_core-2.16.2-cp311-none-win_amd64.whl", hash = "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc"}, 172 | {file = "pydantic_core-2.16.2-cp311-none-win_arm64.whl", hash = "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0"}, 173 | {file = "pydantic_core-2.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039"}, 174 | {file = "pydantic_core-2.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b"}, 175 | {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e"}, 176 | {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522"}, 177 | {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379"}, 178 | {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15"}, 179 | {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380"}, 180 | {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb"}, 181 | {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e"}, 182 | {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc"}, 183 | {file = "pydantic_core-2.16.2-cp312-none-win32.whl", hash = "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d"}, 184 | {file = "pydantic_core-2.16.2-cp312-none-win_amd64.whl", hash = "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890"}, 185 | {file = "pydantic_core-2.16.2-cp312-none-win_arm64.whl", hash = "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943"}, 186 | {file = "pydantic_core-2.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17"}, 187 | {file = "pydantic_core-2.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec"}, 188 | {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55"}, 189 | {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4"}, 190 | {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4"}, 191 | {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f"}, 192 | {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf"}, 193 | {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc"}, 194 | {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b"}, 195 | {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f"}, 196 | {file = "pydantic_core-2.16.2-cp38-none-win32.whl", hash = "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a"}, 197 | {file = "pydantic_core-2.16.2-cp38-none-win_amd64.whl", hash = "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a"}, 198 | {file = "pydantic_core-2.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77"}, 199 | {file = "pydantic_core-2.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286"}, 200 | {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7"}, 201 | {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33"}, 202 | {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c"}, 203 | {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646"}, 204 | {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878"}, 205 | {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55"}, 206 | {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3"}, 207 | {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2"}, 208 | {file = "pydantic_core-2.16.2-cp39-none-win32.whl", hash = "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469"}, 209 | {file = "pydantic_core-2.16.2-cp39-none-win_amd64.whl", hash = "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a"}, 210 | {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82"}, 211 | {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a"}, 212 | {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545"}, 213 | {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d"}, 214 | {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784"}, 215 | {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7"}, 216 | {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8"}, 217 | {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25"}, 218 | {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e"}, 219 | {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545"}, 220 | {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5"}, 221 | {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20"}, 222 | {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753"}, 223 | {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a"}, 224 | {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804"}, 225 | {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2"}, 226 | {file = "pydantic_core-2.16.2.tar.gz", hash = "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06"}, 227 | ] 228 | 229 | [package.dependencies] 230 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 231 | 232 | [[package]] 233 | name = "python-dotenv" 234 | version = "1.0.1" 235 | description = "Read key-value pairs from a .env file and set them as environment variables" 236 | optional = false 237 | python-versions = ">=3.8" 238 | files = [ 239 | {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, 240 | {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, 241 | ] 242 | 243 | [package.extras] 244 | cli = ["click (>=5.0)"] 245 | 246 | [[package]] 247 | name = "python-telegram-bot" 248 | version = "20.8" 249 | description = "We have made you a wrapper you can't refuse" 250 | optional = false 251 | python-versions = ">=3.8" 252 | files = [ 253 | {file = "python-telegram-bot-20.8.tar.gz", hash = "sha256:0e1e4a6dbce3f4ba606990d66467a5a2d2018368fe44756fae07410a74e960dc"}, 254 | {file = "python_telegram_bot-20.8-py3-none-any.whl", hash = "sha256:a98ddf2f237d6584b03a2f8b20553e1b5e02c8d3a1ea8e17fd06cc955af78c14"}, 255 | ] 256 | 257 | [package.dependencies] 258 | httpx = ">=0.26.0,<0.27.0" 259 | 260 | [package.extras] 261 | all = ["APScheduler (>=3.10.4,<3.11.0)", "aiolimiter (>=1.1.0,<1.2.0)", "cachetools (>=5.3.2,<5.4.0)", "cryptography (>=39.0.1)", "httpx[http2]", "httpx[socks]", "pytz (>=2018.6)", "tornado (>=6.4,<7.0)"] 262 | callback-data = ["cachetools (>=5.3.2,<5.4.0)"] 263 | ext = ["APScheduler (>=3.10.4,<3.11.0)", "aiolimiter (>=1.1.0,<1.2.0)", "cachetools (>=5.3.2,<5.4.0)", "pytz (>=2018.6)", "tornado (>=6.4,<7.0)"] 264 | http2 = ["httpx[http2]"] 265 | job-queue = ["APScheduler (>=3.10.4,<3.11.0)", "pytz (>=2018.6)"] 266 | passport = ["cryptography (>=39.0.1)"] 267 | rate-limiter = ["aiolimiter (>=1.1.0,<1.2.0)"] 268 | socks = ["httpx[socks]"] 269 | webhooks = ["tornado (>=6.4,<7.0)"] 270 | 271 | [[package]] 272 | name = "sniffio" 273 | version = "1.3.0" 274 | description = "Sniff out which async library your code is running under" 275 | optional = false 276 | python-versions = ">=3.7" 277 | files = [ 278 | {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, 279 | {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, 280 | ] 281 | 282 | [[package]] 283 | name = "typing-extensions" 284 | version = "4.9.0" 285 | description = "Backported and Experimental Type Hints for Python 3.8+" 286 | optional = false 287 | python-versions = ">=3.8" 288 | files = [ 289 | {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, 290 | {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, 291 | ] 292 | 293 | [metadata] 294 | lock-version = "2.0" 295 | python-versions = "^3.11" 296 | content-hash = "c40f0aa522e73c93184dfccb1df97364bbc7383c11c7d80232694b2724bc7ec1" 297 | --------------------------------------------------------------------------------