From 641b88138d3c94fce2da0318eeb0d50f9d7db044 Mon Sep 17 00:00:00 2001 From: Olly Hearn Date: Wed, 28 Jan 2026 11:30:30 +0300 Subject: [PATCH] first --- .env.example | 16 ++ .gitignore | 20 +++ .python-version | 1 + Dockerfile | 19 +++ README.md | 144 ++++++++++++++++ bot.py | 30 ++++ config.py | 33 ++++ database.py | 85 +++++++++ docker-compose.yml | 37 ++++ handlers/__init__.py | 1 + handlers/command_handlers.py | 169 ++++++++++++++++++ handlers/group_handlers.py | 185 ++++++++++++++++++++ handlers/private_handlers.py | 305 +++++++++++++++++++++++++++++++++ handlers/scheduler.py | 93 ++++++++++ main.py | 25 +++ messages.py | 155 +++++++++++++++++ pyproject.toml | 14 ++ uv.lock | 323 +++++++++++++++++++++++++++++++++++ 18 files changed, 1655 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 bot.py create mode 100644 config.py create mode 100644 database.py create mode 100644 docker-compose.yml create mode 100644 handlers/__init__.py create mode 100644 handlers/command_handlers.py create mode 100644 handlers/group_handlers.py create mode 100644 handlers/private_handlers.py create mode 100644 handlers/scheduler.py create mode 100644 main.py create mode 100644 messages.py create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..61da93a --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# Telegram Bot Token +# Get it from @BotFather on Telegram +BOT_TOKEN=your_bot_token_here + +# PostgreSQL Database URL +# Format: postgresql://user:password@host:port/database +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/bdbot + +# Notification time (24-hour format: HH:MM) +# Default: 09:00 +NOTIFICATION_TIME=09:00 + +# Timezone for notifications +# Examples: UTC, Europe/Moscow, America/New_York +# Default: UTC +TIMEZONE=UTC diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04667f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +# Environment variables +.env + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3941c46 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +# Copy project files +COPY pyproject.toml ./ +COPY . . + +# Install Python dependencies +RUN pip install --no-cache-dir uv && \ + uv pip install --system -e . + +# Run the bot +CMD ["python", "main.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c680ed --- /dev/null +++ b/README.md @@ -0,0 +1,144 @@ +# Birthday Bot (bdbot) + +Telegram-бот для отслеживания дней рождения и отправки тематических поздравлений в чатах. + +## Возможности + +- 📅 Отслеживание дней рождения пользователей +- 🎉 Автоматическая отправка тематических поздравлений +- 📊 Статистика по чатам +- 🔔 Уведомления о предстоящих днях рождения +- 🎨 12 тем для персонализации поздравлений + +## Требования + +- Python 3.12+ +- PostgreSQL 14+ +- Docker и Docker Compose (опционально) + +## Установка + +### Локальная установка + +1. Клонируйте репозиторий: +```bash +git clone +cd bdbot +``` + +2. Установите зависимости: +```bash +pip install uv +uv pip install -e . +``` + +3. Создайте файл `.env` на основе `.env.example`: +```bash +cp .env.example .env +``` + +4. Заполните `.env` файл: +```env +BOT_TOKEN=your_bot_token_here +DATABASE_URL=postgresql://user:password@localhost:5432/bdbot +NOTIFICATION_TIME=09:00 +TIMEZONE=Europe/Moscow +``` + +5. Создайте базу данных PostgreSQL и убедитесь, что она доступна. + +6. Запустите бота: +```bash +python main.py +``` + +### Docker Compose + +1. Создайте файл `.env` (см. выше). + +2. Запустите с помощью Docker Compose: +```bash +docker-compose up -d +``` + +## Использование + +### Для администраторов чата + +1. Добавьте бота в чат +2. Выдайте боту права администратора +3. Бот автоматически покажет статистику и предложит участникам поделиться днем рождения + +### Для пользователей + +1. Напишите боту в личку `/start` +2. Отправьте свой день рождения в формате `ДД.ММ` или `ДД.ММ.ГГГГ` (например: `15.03` или `15.03.1990`) +3. Выберите тему предпочтений из предложенных вариантов +4. Готово! Бот будет поздравлять вас во всех чатах, где вы состоите + +### Команды в чатах + +- `/stats` - Показать статистику: сколько человек поделились днем рождения +- `/week` - Показать дни рождения на ближайшие 7 дней +- `/month` - Показать дни рождения на ближайшие 30 дней +- `/help` - Показать справку по командам + +### Обновление данных + +Используйте команду `/update` в личке с ботом, чтобы обновить свой день рождения или тему предпочтений. + +## Темы предпочтений + +1. Автомобили +2. Спорт +3. Танцы +4. Музыка +5. Аниме +6. Игры +7. Книги +8. Кино +9. Путешествия +10. Еда +11. Технологии +12. Искусство + +## Конфигурация + +Все настройки находятся в файле `.env`: + +- `BOT_TOKEN` - токен Telegram бота (получить у @BotFather) +- `DATABASE_URL` - строка подключения к PostgreSQL +- `NOTIFICATION_TIME` - время отправки поздравлений (формат: HH:MM, по умолчанию 09:00) +- `TIMEZONE` - часовой пояс (например: Europe/Moscow, UTC) + +## Структура проекта + +``` +bdbot/ +├── main.py # Точка входа +├── bot.py # Основной класс бота +├── config.py # Конфигурация +├── database.py # Модели базы данных +├── messages.py # Тематические поздравления +├── handlers/ # Обработчики событий +│ ├── group_handlers.py # Обработка событий в группах +│ ├── private_handlers.py # Обработка команд в личке +│ ├── command_handlers.py # Обработка команд в группах +│ └── scheduler.py # Планировщик поздравлений +├── Dockerfile # Docker образ +├── docker-compose.yml # Docker Compose конфигурация +└── .env.example # Пример конфигурации +``` + +## Разработка + +Проект использует: +- Python 3.12+ +- pyTelegramBotAPI для работы с Telegram API +- SQLAlchemy для работы с базой данных +- APScheduler для планирования задач +- PostgreSQL как основную БД + +## Лицензия + +from olly & cursor with <3 diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..5c6c07f --- /dev/null +++ b/bot.py @@ -0,0 +1,30 @@ +"""Main bot module.""" +import telebot +from config import Config +from database import init_db +from handlers.group_handlers import register_group_handlers +from handlers.private_handlers import register_private_handlers +from handlers.command_handlers import register_command_handlers + + +def create_bot() -> telebot.TeleBot: + """Create and configure the bot.""" + # Validate configuration + Config.validate() + + # Ensure BOT_TOKEN is not None + if not Config.BOT_TOKEN: + raise ValueError("BOT_TOKEN is required") + + # Initialize database + init_db() + + # Create bot instance + bot = telebot.TeleBot(Config.BOT_TOKEN) + + # Register all handlers + register_group_handlers(bot) + register_private_handlers(bot) + register_command_handlers(bot) + + return bot diff --git a/config.py b/config.py new file mode 100644 index 0000000..147f0b6 --- /dev/null +++ b/config.py @@ -0,0 +1,33 @@ +"""Configuration module for loading environment variables.""" +import os +from typing import Optional +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + + +class Config: + """Application configuration.""" + + # Telegram Bot Token + BOT_TOKEN: Optional[str] = os.getenv("BOT_TOKEN") + if not BOT_TOKEN: + raise ValueError("BOT_TOKEN environment variable is required") + + # Database configuration + DATABASE_URL: Optional[str] = os.getenv("DATABASE_URL") + if not DATABASE_URL: + raise ValueError("DATABASE_URL environment variable is required") + + # Notification settings + NOTIFICATION_TIME: str = os.getenv("NOTIFICATION_TIME", "09:00") or "09:00" + TIMEZONE: str = os.getenv("TIMEZONE", "UTC") or "UTC" + + @classmethod + def validate(cls) -> None: + """Validate that all required configuration is present.""" + if not cls.BOT_TOKEN: + raise ValueError("BOT_TOKEN is required") + if not cls.DATABASE_URL: + raise ValueError("DATABASE_URL is required") diff --git a/database.py b/database.py new file mode 100644 index 0000000..77d1c14 --- /dev/null +++ b/database.py @@ -0,0 +1,85 @@ +"""Database models and session management.""" +from typing import Generator +from sqlalchemy import create_engine, BigInteger, String, Integer, Boolean, ForeignKey, Column +from sqlalchemy.orm import declarative_base, sessionmaker, relationship, Session +from sqlalchemy.schema import UniqueConstraint +from config import Config + +Base = declarative_base() + + +class User(Base): + """User model - stores user information and birthday.""" + __tablename__ = "users" + + user_id = Column(BigInteger, primary_key=True) + username = Column(String, nullable=True) + first_name = Column(String, nullable=False) + birthday_day = Column(Integer, nullable=False) + birthday_month = Column(Integer, nullable=False) + birthday_year = Column(Integer, nullable=True) + preference_theme = Column(String, nullable=False) + + # Relationships + chats = relationship("UserChat", back_populates="user", cascade="all, delete-orphan") + + def __repr__(self): + return f"" + + +class Chat(Base): + """Chat model - stores chat information.""" + __tablename__ = "chats" + + chat_id = Column(BigInteger, primary_key=True) + chat_title = Column(String, nullable=False) + bot_is_admin = Column(Boolean, default=False, nullable=False) + + # Relationships + users = relationship("UserChat", back_populates="chat", cascade="all, delete-orphan") + + def __repr__(self): + return f"" + + +class UserChat(Base): + """Many-to-many relationship between users and chats.""" + __tablename__ = "user_chats" + + user_id = Column(BigInteger, ForeignKey("users.user_id"), primary_key=True) + chat_id = Column(BigInteger, ForeignKey("chats.chat_id"), primary_key=True) + + # Relationships + user = relationship("User", back_populates="chats") + chat = relationship("Chat", back_populates="users") + + __table_args__ = ( + UniqueConstraint("user_id", "chat_id", name="unique_user_chat"), + ) + + def __repr__(self): + return f"" + + +# Database engine and session +engine = create_engine(Config.DATABASE_URL, echo=False) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +def get_db() -> Generator[Session, None, None]: + """Get database session.""" + db = SessionLocal() + try: + yield db + finally: + db.close() + + +def init_db() -> None: + """Initialize database - create all tables.""" + Base.metadata.create_all(bind=engine) + + +def get_db_session() -> Session: + """Get a database session (for use without context manager).""" + return SessionLocal() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b896355 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.8' + +services: + postgres: + image: postgres:14-alpine + container_name: bdbot_postgres + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: bdbot + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + + bot: + build: . + container_name: bdbot + depends_on: + postgres: + condition: service_healthy + environment: + BOT_TOKEN: ${BOT_TOKEN} + DATABASE_URL: postgresql://postgres:postgres@postgres:5432/bdbot + NOTIFICATION_TIME: ${NOTIFICATION_TIME:-09:00} + TIMEZONE: ${TIMEZONE:-UTC} + env_file: + - .env + restart: unless-stopped + +volumes: + postgres_data: diff --git a/handlers/__init__.py b/handlers/__init__.py new file mode 100644 index 0000000..0f6f20e --- /dev/null +++ b/handlers/__init__.py @@ -0,0 +1 @@ +# Handlers package diff --git a/handlers/command_handlers.py b/handlers/command_handlers.py new file mode 100644 index 0000000..27fed90 --- /dev/null +++ b/handlers/command_handlers.py @@ -0,0 +1,169 @@ +"""Handlers for group commands.""" +import telebot +from datetime import datetime, timedelta, date +from typing import Dict, List +from sqlalchemy.orm import Session +from database import get_db_session, User, Chat, UserChat +from collections import defaultdict + + +def register_command_handlers(bot: telebot.TeleBot) -> None: + """Register all command handlers.""" + + @bot.message_handler(commands=['stats'], chat_types=['group', 'supergroup']) + def handle_stats(message: telebot.types.Message) -> None: + """Handle /stats command - show statistics.""" + chat_id = message.chat.id + + db = get_db_session() + try: + # Check if bot is admin + chat = db.query(Chat).filter(Chat.chat_id == chat_id).first() + if not chat or not chat.bot_is_admin: + bot.reply_to(message, "Мне нужны права администратора для выполнения этой команды.") + return + + # Get total members count + try: + total_members = bot.get_chat_member_count(chat_id) + except Exception: + total_members = 0 + + # Get users who shared birthday + users_with_birthday = db.query(User).join(UserChat).filter( + UserChat.chat_id == chat_id + ).distinct().count() + + users_without_birthday = total_members - users_with_birthday + + # Format message + if total_members > 0: + percentage = (users_with_birthday / total_members) * 100 + stats_text = ( + f"📊 Статистика чата:\n\n" + f"• Всего участников: {total_members}\n" + f"• Поделились днем рождения: {users_with_birthday}\n" + f"• Не поделились: {users_without_birthday}\n" + f"• Процент: {percentage:.1f}%" + ) + else: + stats_text = ( + f"📊 Статистика чата:\n\n" + f"• Поделились днем рождения: {users_with_birthday}" + ) + + bot.reply_to(message, stats_text) + finally: + db.close() + + @bot.message_handler(commands=['week'], chat_types=['group', 'supergroup']) + def handle_week(message: telebot.types.Message) -> None: + """Handle /week command - show birthdays for next 7 days.""" + chat_id = message.chat.id + + db = get_db_session() + try: + # Check if bot is admin + chat = db.query(Chat).filter(Chat.chat_id == chat_id).first() + if not chat or not chat.bot_is_admin: + bot.reply_to(message, "Мне нужны права администратора для выполнения этой команды.") + return + + # Get birthdays for next 7 days + today = datetime.now().date() + birthdays = get_birthdays_in_range(db, chat_id, today, days=7) + + if not birthdays: + bot.reply_to(message, "На ближайшие 7 дней дней рождений не запланировано.") + return + + # Format message + message_text = "🎂 Дни рождения на ближайшие 7 дней:\n\n" + for date_str, names in sorted(birthdays.items()): + names_list = ", ".join(names) + message_text += f"• {date_str}: {names_list}\n" + + bot.reply_to(message, message_text) + finally: + db.close() + + @bot.message_handler(commands=['month'], chat_types=['group', 'supergroup']) + def handle_month(message: telebot.types.Message) -> None: + """Handle /month command - show birthdays for next 30 days.""" + chat_id = message.chat.id + + db = get_db_session() + try: + # Check if bot is admin + chat = db.query(Chat).filter(Chat.chat_id == chat_id).first() + if not chat or not chat.bot_is_admin: + bot.reply_to(message, "Мне нужны права администратора для выполнения этой команды.") + return + + # Get birthdays for next 30 days + today = datetime.now().date() + birthdays = get_birthdays_in_range(db, chat_id, today, days=30) + + if not birthdays: + bot.reply_to(message, "На ближайшие 30 дней дней рождений не запланировано.") + return + + # Format message + message_text = "🎂 Дни рождения на ближайшие 30 дней:\n\n" + for date_str, names in sorted(birthdays.items()): + names_list = ", ".join(names) + message_text += f"• {date_str}: {names_list}\n" + + bot.reply_to(message, message_text) + finally: + db.close() + + @bot.message_handler(commands=['help'], chat_types=['group', 'supergroup']) + def handle_help(message: telebot.types.Message) -> None: + """Handle /help command - show help message.""" + help_text = ( + "📚 Команды бота:\n\n" + "/stats - Показать статистику: сколько человек поделились днем рождения\n" + "/week - Показать дни рождения на ближайшие 7 дней\n" + "/month - Показать дни рождения на ближайшие 30 дней\n" + "/help - Показать это сообщение\n\n" + "Чтобы поделиться своим днем рождения, напиши боту в личку /start\n\n" + "from olly & cursor with <3" + ) + bot.reply_to(message, help_text) + + +def get_birthdays_in_range(db: Session, chat_id: int, start_date: date, days: int) -> Dict[str, List[str]]: + """Get birthdays in the specified date range for users in the chat.""" + birthdays = defaultdict(list) + + # Get all users in this chat + users = db.query(User).join(UserChat).filter( + UserChat.chat_id == chat_id + ).distinct().all() + + end_date = start_date + timedelta(days=days) + + for user in users: + # Create birthday date for current year + try: + birthday_this_year = datetime(start_date.year, user.birthday_month, user.birthday_day).date() + except ValueError: + # Invalid date (e.g., Feb 29 in non-leap year) + continue + + # Check if birthday falls in range + if start_date <= birthday_this_year < end_date: + date_str = f"{user.birthday_day:02d}.{user.birthday_month:02d}" + birthdays[date_str].append(user.first_name) + else: + # Check next year if we're near year end + try: + birthday_next_year = datetime(start_date.year + 1, user.birthday_month, user.birthday_day).date() + if start_date <= birthday_next_year < end_date: + date_str = f"{user.birthday_day:02d}.{user.birthday_month:02d}" + birthdays[date_str].append(user.first_name) + except ValueError: + pass + + return birthdays diff --git a/handlers/group_handlers.py b/handlers/group_handlers.py new file mode 100644 index 0000000..759f206 --- /dev/null +++ b/handlers/group_handlers.py @@ -0,0 +1,185 @@ +"""Handlers for group events.""" +import telebot +from typing import Optional +from telebot import types +from sqlalchemy.orm import Session +from database import get_db_session, User, Chat, UserChat + + +def register_group_handlers(bot: telebot.TeleBot) -> None: + """Register all group event handlers.""" + + @bot.message_handler(content_types=['new_chat_members']) + def handle_new_member(message: telebot.types.Message) -> None: + """Handle bot being added to a chat.""" + # Check if bot was added + bot_me = bot.get_me() + if not bot_me: + return + + for member in message.new_chat_members: + if member.id == bot_me.id: + chat_id = message.chat.id + chat_title: str = message.chat.title or "Unknown" + + db = get_db_session() + try: + # Check if chat exists + chat = db.query(Chat).filter(Chat.chat_id == chat_id).first() + if not chat: + chat = Chat(chat_id=chat_id, chat_title=chat_title, bot_is_admin=False) + db.add(chat) + db.commit() + + # Check if bot is admin + try: + bot_me = bot.get_me() + if not bot_me: + return + bot_member = bot.get_chat_member(chat_id, bot_me.id) + is_admin = bot_member.status in ['administrator', 'creator'] + chat.bot_is_admin = is_admin + db.commit() + + if not is_admin: + # Request admin rights + bot.reply_to( + message, + "Привет! Мне нужны права администратора, чтобы видеть список участников чата. " + "Пожалуйста, выдайте мне права администратора." + ) + else: + # Sync users and show statistics + sync_chat_users(bot, db, chat_id) + show_statistics(bot, message.chat) + except Exception: + # Bot might not have permission to check + chat.bot_is_admin = False + db.commit() + bot.reply_to( + message, + "Привет! Мне нужны права администратора, чтобы видеть список участников чата. " + "Пожалуйста, выдайте мне права администратора." + ) + finally: + db.close() + break + + @bot.chat_member_handler() + def handle_chat_member_update(message: telebot.types.ChatMemberUpdated) -> None: + """Handle chat member updates (including bot becoming admin).""" + bot_me = bot.get_me() + if not bot_me: + return + + if message.new_chat_member.user.id == bot_me.id: + # Bot's status changed + chat_id = message.chat.id + chat_title: str = message.chat.title or "Unknown" + + db = get_db_session() + try: + chat = db.query(Chat).filter(Chat.chat_id == chat_id).first() + if not chat: + chat = Chat(chat_id=chat_id, chat_title=chat_title, bot_is_admin=False) + db.add(chat) + + # Check if bot became admin + is_admin = message.new_chat_member.status in ['administrator', 'creator'] + was_admin = chat.bot_is_admin if chat else False + if chat: + chat.bot_is_admin = is_admin + chat.chat_title = chat_title + db.commit() + + if is_admin and not was_admin: + # Bot just became admin - sync users and show statistics + sync_chat_users(bot, db, chat_id) + show_statistics(bot, message.chat) + finally: + db.close() + + +def sync_chat_users(bot: telebot.TeleBot, db: Session, chat_id: int) -> None: + """Sync users from chat with database.""" + try: + # Get all chat members (this requires admin rights) + # Note: This is a simplified approach - in production you might want to + # use get_chat_administrators or iterate through members differently + # For now, we'll sync users as they interact with the bot + + # Get all users who already shared birthday + existing_users = db.query(User).all() + + # Try to check if they're in this chat and add relationships + for user in existing_users: + try: + member = bot.get_chat_member(chat_id, user.user_id) + if member.status not in ['left', 'kicked']: + # User is in chat - ensure relationship exists + user_chat = db.query(UserChat).filter( + UserChat.user_id == user.user_id, + UserChat.chat_id == chat_id + ).first() + + if not user_chat: + user_chat = UserChat(user_id=user.user_id, chat_id=chat_id) + db.add(user_chat) + except Exception: + # User might have blocked bot or is not in chat + pass + + db.commit() + except Exception: + # If sync fails, continue anyway + pass + + +def show_statistics(bot: telebot.TeleBot, chat: telebot.types.Chat) -> None: + """Show statistics about users who shared their birthday.""" + db = get_db_session() + try: + chat_id = chat.id + + # Get all chat members + try: + members_count = bot.get_chat_member_count(chat_id) + except Exception: + members_count = 0 + + # Get users from this chat who shared birthday + users_with_birthday = db.query(User).join(UserChat).filter( + UserChat.chat_id == chat_id + ).distinct().count() + + # Create message + if members_count > 0: + percentage = (users_with_birthday / members_count) * 100 + message_text = ( + f"Отлично! Теперь я админ этого чата.\n\n" + f"📊 Статистика:\n" + f"• Поделились днем рождения: {users_with_birthday} из {members_count} участников\n" + f"• Процент: {percentage:.1f}%" + ) + else: + message_text = ( + f"Отлично! Теперь я админ этого чата.\n\n" + f"📊 Поделились днем рождения: {users_with_birthday} участников" + ) + + # Create inline keyboard with button to start private chat + bot_me = bot.get_me() + if not bot_me or not bot_me.username: + bot.send_message(chat_id, message_text) + return + + keyboard = types.InlineKeyboardMarkup() + start_button = types.InlineKeyboardButton( + text="Поделиться днем рождения", + url=f"https://t.me/{bot_me.username}?start=share_birthday" + ) + keyboard.add(start_button) + + bot.send_message(chat_id, message_text, reply_markup=keyboard) + finally: + db.close() diff --git a/handlers/private_handlers.py b/handlers/private_handlers.py new file mode 100644 index 0000000..bb19cba --- /dev/null +++ b/handlers/private_handlers.py @@ -0,0 +1,305 @@ +"""Handlers for private messages.""" +import re +from typing import Optional, Dict +import telebot +from telebot import types +from sqlalchemy.orm import Session +from database import get_db_session, User, Chat, UserChat +from messages import THEMES, get_theme_emoji + + +# User states for conversation flow +user_states: Dict[int, str] = {} + + +def register_private_handlers(bot: telebot.TeleBot) -> None: + """Register all private message handlers.""" + + @bot.message_handler(commands=['start'], chat_types=['private']) + def handle_start(message: telebot.types.Message) -> None: + """Handle /start command in private chat.""" + if not message.from_user: + return + + user_id = message.from_user.id + username: Optional[str] = message.from_user.username + first_name: str = message.from_user.first_name or "Пользователь" + + db = get_db_session() + try: + # Check if user exists + user = db.query(User).filter(User.user_id == user_id).first() + + if user: + # User already exists - ask if they want to update + bot.send_message( + user_id, + f"Привет, {first_name}! Ты уже поделился со мной днем рождения.\n" + f"Используй /update, чтобы обновить свои данные." + ) + else: + # New user - ask for birthday + user_states[user_id] = 'waiting_birthday' + bot.send_message( + user_id, + f"Привет, {first_name}! 👋\n\n" + f"Пришли мне свой день рождения в формате ДД.ММ или ДД.ММ.ГГГГ\n" + f"Например: 15.03 или 15.03.1990" + ) + finally: + db.close() + + @bot.message_handler(commands=['update'], chat_types=['private']) + def handle_update(message: telebot.types.Message) -> None: + """Handle /update command in private chat.""" + if not message.from_user: + return + + user_id = message.from_user.id + first_name: str = message.from_user.first_name or "Пользователь" + + db = get_db_session() + try: + user = db.query(User).filter(User.user_id == user_id).first() + if user: + user_states[user_id] = 'waiting_birthday' + bot.send_message( + user_id, + f"Хорошо, {first_name}! Пришли мне свой день рождения в формате ДД.ММ или ДД.ММ.ГГГГ" + ) + else: + bot.send_message( + user_id, + "Ты еще не поделился со мной днем рождения. Используй /start для начала." + ) + finally: + db.close() + + @bot.message_handler(func=lambda m: m.chat.type == 'private' and m.from_user and m.from_user.id in user_states and m.text) + def handle_birthday_input(message: telebot.types.Message) -> None: + """Handle birthday input from user.""" + if not message.from_user or not message.text: + return + + user_id = message.from_user.id + state = user_states.get(user_id) + + if state == 'waiting_birthday': + # Parse birthday + birthday_text = message.text.strip() + parsed = parse_birthday(birthday_text) + + if not parsed: + bot.send_message( + user_id, + "Неверный формат даты. Используй формат ДД.ММ или ДД.ММ.ГГГГ\n" + "Например: 15.03 или 15.03.1990" + ) + return + + day, month, year = parsed + + # Validate date + if not is_valid_date(day, month, year): + bot.send_message( + user_id, + "Неверная дата. Проверь правильность дня и месяца." + ) + return + + # Save birthday and ask for preference + db = get_db_session() + try: + if not message.from_user: + return + username: Optional[str] = message.from_user.username + first_name: str = message.from_user.first_name or "Пользователь" + + user = db.query(User).filter(User.user_id == user_id).first() + if user: + # Update existing user + user.birthday_day = day + user.birthday_month = month + user.birthday_year = year + user.first_name = first_name + user.username = username + else: + # Create new user + user = User( + user_id=user_id, + username=username, + first_name=first_name, + birthday_day=day, + birthday_month=month, + birthday_year=year, + preference_theme="Музыка" # Default theme + ) + db.add(user) + + db.commit() + + # Ask for preference theme + user_states[user_id] = 'waiting_preference' + ask_preference_theme(bot, user_id) + finally: + db.close() + + elif state == 'waiting_preference': + # User selected preference theme + if not message.text: + return + + theme_text = message.text.strip() + + if theme_text not in THEMES: + bot.send_message( + user_id, + "Пожалуйста, выбери одну из предложенных тем, нажав на кнопку." + ) + return + + # Save preference + db = get_db_session() + try: + user = db.query(User).filter(User.user_id == user_id).first() + if user: + user.preference_theme = theme_text + db.commit() + + # Update user in all chats + update_user_in_chats(bot, db, user) + + bot.send_message( + user_id, + f"Отлично! Я запомнил твои предпочтения: {theme_text}\n\n" + f"Теперь я буду поздравлять тебя с днем рождения во всех чатах, где ты состоишь!" + ) + user_states.pop(user_id, None) + finally: + db.close() + + @bot.callback_query_handler(func=lambda call: call.data and call.data.startswith('theme_')) + def handle_theme_selection(call: telebot.types.CallbackQuery) -> None: + """Handle theme selection from inline keyboard.""" + if not call.from_user or not call.data or not call.message: + return + + user_id = call.from_user.id + + if user_states.get(user_id) != 'waiting_preference': + bot.answer_callback_query(call.id, "Это действие недоступно сейчас.") + return + + theme = call.data.replace('theme_', '') + + if theme not in THEMES: + bot.answer_callback_query(call.id, "Неверная тема.") + return + + # Save preference + db = get_db_session() + try: + user = db.query(User).filter(User.user_id == user_id).first() + if user: + user.preference_theme = theme + db.commit() + + # Update user in all chats + update_user_in_chats(bot, db, user) + + bot.answer_callback_query(call.id, f"Выбрано: {theme}") + bot.edit_message_text( + f"Отлично! Я запомнил твои предпочтения: {theme}\n\n" + f"Теперь я буду поздравлять тебя с днем рождения во всех чатах, где ты состоишь!", + chat_id=call.message.chat.id, + message_id=call.message.message_id + ) + user_states.pop(user_id, None) + finally: + db.close() + + +def parse_birthday(text: str) -> Optional[tuple[int, int, Optional[int]]]: + """Parse birthday from text. Returns (day, month, year) or None.""" + # Try DD.MM.YYYY format + match = re.match(r'^(\d{1,2})\.(\d{1,2})\.(\d{4})$', text) + if match: + day, month, year = int(match.group(1)), int(match.group(2)), int(match.group(3)) + return (day, month, year) + + # Try DD.MM format + match = re.match(r'^(\d{1,2})\.(\d{1,2})$', text) + if match: + day, month = int(match.group(1)), int(match.group(2)) + return (day, month, None) + + return None + + +def is_valid_date(day: int, month: int, year: Optional[int]) -> bool: + """Check if date is valid.""" + if month < 1 or month > 12: + return False + if day < 1 or day > 31: + return False + + # Check specific month limits + if month in [4, 6, 9, 11] and day > 30: + return False + if month == 2: + if year: + # Check leap year + if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0): + max_day = 29 + else: + max_day = 28 + else: + max_day = 29 # Assume leap year if year not provided + if day > max_day: + return False + + return True + + +def ask_preference_theme(bot: telebot.TeleBot, user_id: int) -> None: + """Ask user to select preference theme.""" + keyboard = types.InlineKeyboardMarkup(row_width=2) + + for theme in THEMES: + emoji = get_theme_emoji(theme) + button = types.InlineKeyboardButton( + text=f"{emoji} {theme}", + callback_data=f'theme_{theme}' + ) + keyboard.add(button) + + bot.send_message( + user_id, + "Что тебе нравится? Выбери одну из тем:", + reply_markup=keyboard + ) + + +def update_user_in_chats(bot: telebot.TeleBot, db: Session, user: User) -> None: + """Update user information in all chats where bot is admin.""" + # Get all chats where bot is admin + admin_chats = db.query(Chat).filter(Chat.bot_is_admin == True).all() + + for chat in admin_chats: + try: + # Check if user is member of this chat + member = bot.get_chat_member(chat.chat_id, user.user_id) + if member.status not in ['left', 'kicked']: + # User is in chat - add/update relationship + user_chat = db.query(UserChat).filter( + UserChat.user_id == user.user_id, + UserChat.chat_id == chat.chat_id + ).first() + + if not user_chat: + user_chat = UserChat(user_id=user.user_id, chat_id=chat.chat_id) + db.add(user_chat) + db.commit() + except Exception: + # User might have blocked bot or bot was removed from chat + pass diff --git a/handlers/scheduler.py b/handlers/scheduler.py new file mode 100644 index 0000000..d66d90a --- /dev/null +++ b/handlers/scheduler.py @@ -0,0 +1,93 @@ +"""Scheduler for daily birthday notifications.""" +import telebot +from datetime import datetime +from typing import Optional +from apscheduler.schedulers.blocking import BlockingScheduler +from apscheduler.triggers.cron import CronTrigger +import pytz +from database import get_db_session, User, Chat, UserChat +from messages import format_birthday_greeting +from config import Config + + +def send_birthday_notifications(bot: telebot.TeleBot) -> None: + """Send birthday notifications to all chats for users with birthday today.""" + db = get_db_session() + try: + today = datetime.now().date() + day = today.day + month = today.month + + # Get all users with birthday today + users_with_birthday = db.query(User).filter( + User.birthday_day == day, + User.birthday_month == month + ).all() + + if not users_with_birthday: + return + + # For each user, send greetings to all their chats + for user in users_with_birthday: + # Get all chats where user is a member + user_chats = db.query(Chat).join(UserChat).filter( + UserChat.user_id == user.user_id, + Chat.bot_is_admin == True + ).all() + + greeting = format_birthday_greeting(user.first_name, user.preference_theme) + + for chat in user_chats: + try: + # Check if user is still in chat + member = bot.get_chat_member(chat.chat_id, user.user_id) + if member.status not in ['left', 'kicked']: + bot.send_message(chat.chat_id, greeting) + except telebot.apihelper.ApiTelegramException as e: + # Handle errors: user blocked bot, bot removed from chat, etc. + if e.error_code == 403: + # Bot was blocked or removed from chat + # Update chat status + chat.bot_is_admin = False + db.commit() + elif e.error_code == 400: + # Invalid chat or user + pass + # Silently continue for other errors + except Exception: + # Other errors - continue + pass + finally: + db.close() + + +def setup_scheduler(bot: telebot.TeleBot) -> BlockingScheduler: + """Setup and start the scheduler for daily birthday notifications.""" + # Parse notification time + time_str: str = Config.NOTIFICATION_TIME + try: + hour, minute = map(int, time_str.split(':')) + except (ValueError, AttributeError): + hour, minute = 9, 0 # Default to 9:00 + + # Get timezone + timezone_str: str = Config.TIMEZONE + try: + tz = pytz.timezone(timezone_str) + except (pytz.exceptions.UnknownTimeZoneError, AttributeError): + tz = pytz.UTC + + # Create scheduler + scheduler = BlockingScheduler(timezone=tz) + + # Add daily job + scheduler.add_job( + send_birthday_notifications, + trigger=CronTrigger(hour=hour, minute=minute), + args=[bot], + id='daily_birthday_notifications', + name='Send daily birthday notifications', + replace_existing=True + ) + + return scheduler diff --git a/main.py b/main.py new file mode 100644 index 0000000..caba575 --- /dev/null +++ b/main.py @@ -0,0 +1,25 @@ +"""Main entry point for the bot.""" +import threading +from bot import create_bot +from handlers.scheduler import setup_scheduler + + +def main() -> None: + """Main function to start the bot and scheduler.""" + # Create bot + bot = create_bot() + + # Setup scheduler + scheduler = setup_scheduler(bot) + + # Start scheduler in a separate thread + scheduler_thread = threading.Thread(target=scheduler.start, daemon=True) + scheduler_thread.start() + + # Start bot polling + print("Bot is starting...") + bot.infinity_polling(none_stop=True) + + +if __name__ == "__main__": + main() diff --git a/messages.py b/messages.py new file mode 100644 index 0000000..dec6156 --- /dev/null +++ b/messages.py @@ -0,0 +1,155 @@ +"""Themed birthday messages dictionaries.""" +import random + +# Themes list +THEMES = [ + "Автомобили", + "Спорт", + "Танцы", + "Музыка", + "Аниме", + "Игры", + "Книги", + "Кино", + "Путешествия", + "Еда", + "Технологии", + "Искусство", +] + +# Emoji dictionary for each theme +THEME_EMOJIS = { + "Автомобили": "🚗", + "Спорт": "⚽", + "Танцы": "💃", + "Музыка": "🎵", + "Аниме": "🎌", + "Игры": "🎮", + "Книги": "📚", + "Кино": "🎬", + "Путешествия": "✈️", + "Еда": "🍕", + "Технологии": "💻", + "Искусство": "🎨", +} + + +def get_theme_emoji(theme: str) -> str: + """Get emoji for the given theme.""" + return THEME_EMOJIS.get(theme, "🎉") # Default emoji + +# Birthday messages for each theme +BIRTHDAY_MESSAGES = { + "Автомобили": [ + "желаю тебе купить мазератти!", + "пусть твоя машина всегда заводится с первого раза!", + "желаю тебе обновить свой автопарк!", + "пусть дороги будут ровными, а бензин дешевым!", + "желаю тебе найти свою идеальную машину!", + "пусть каждый поворот будет безопасным!", + ], + "Спорт": [ + "желаю тебе новых спортивных достижений!", + "пусть каждый день будет полон энергии!", + "желаю тебе побить все личные рекорды!", + "пусть здоровье будет крепким, а дух сильным!", + "желаю тебе найти свой идеальный вид спорта!", + "пусть спорт приносит только радость!", + ], + "Танцы": [ + "желаю тебе танцевать до упаду!", + "пусть каждый танец будет особенным!", + "желаю тебе найти свой ритм!", + "пусть музыка всегда вдохновляет тебя!", + "желаю тебе новых танцевальных открытий!", + "пусть движения будут плавными и грациозными!", + ], + "Музыка": [ + "желаю тебе найти свою любимую мелодию!", + "пусть музыка всегда поднимает настроение!", + "желаю тебе открыть новые музыкальные горизонты!", + "пусть каждый день звучит как симфония!", + "желаю тебе научиться играть на новом инструменте!", + "пусть музыка вдохновляет тебя на великие дела!", + ], + "Аниме": [ + "желаю тебе найти свое аниме года!", + "пусть каждый сезон приносит новые открытия!", + "желаю тебе встретить единомышленников!", + "пусть аниме всегда поднимает настроение!", + "желаю тебе посмотреть все лучшие тайтлы!", + "пусть каждый день будет как новый эпизод!", + ], + "Игры": [ + "желаю тебе пройти все игры из wishlist!", + "пусть каждый геймплей будет захватывающим!", + "желаю тебе найти свою игру года!", + "пусть лаги обходят тебя стороной!", + "желаю тебе новых игровых достижений!", + "пусть каждый рейд будет успешным!", + ], + "Книги": [ + "желаю тебе прочитать все книги из списка!", + "пусть каждая книга открывает новые миры!", + "желаю тебе найти свою книгу года!", + "пусть чтение всегда приносит удовольствие!", + "желаю тебе открыть новых любимых авторов!", + "пусть библиотека пополняется интересными книгами!", + ], + "Кино": [ + "желаю тебе посмотреть все фильмы из списка!", + "пусть каждый фильм оставляет яркие впечатления!", + "желаю тебе найти свой фильм года!", + "пусть кино всегда вдохновляет тебя!", + "желаю тебе открыть новых любимых режиссеров!", + "пусть каждый просмотр приносит радость!", + ], + "Путешествия": [ + "желаю тебе посетить все места из wishlist!", + "пусть каждое путешествие будет незабываемым!", + "желаю тебе открыть новые страны и города!", + "пусть дороги ведут к интересным местам!", + "желаю тебе найти свое идеальное направление!", + "пусть каждый отпуск будет полон приключений!", + ], + "Еда": [ + "желаю тебе попробовать все блюда из списка!", + "пусть каждый прием пищи приносит удовольствие!", + "желаю тебе открыть новые вкусы!", + "пусть кулинарные эксперименты всегда удаются!", + "желаю тебе найти свое идеальное блюдо!", + "пусть каждый ресторан удивляет тебя!", + ], + "Технологии": [ + "желаю тебе освоить новые технологии!", + "пусть каждый код компилируется с первого раза!", + "желаю тебе найти свое призвание в IT!", + "пусть баги обходят тебя стороной!", + "желаю тебе новых технологических открытий!", + "пусть каждый проект приносит успех!", + ], + "Искусство": [ + "желаю тебе вдохновения для новых творений!", + "пусть каждый арт-проект будет особенным!", + "желаю тебе найти свой уникальный стиль!", + "пусть творчество всегда приносит радость!", + "желаю тебе открыть новые художественные горизонты!", + "пусть каждый день полон вдохновения!", + ], +} + + +def get_birthday_message(theme: str) -> str: + """Get a random birthday message for the given theme.""" + if theme not in BIRTHDAY_MESSAGES: + theme = "Музыка" # Default theme + + messages = BIRTHDAY_MESSAGES.get(theme, BIRTHDAY_MESSAGES["Музыка"]) + return random.choice(messages) + + +def format_birthday_greeting(first_name: str, theme: str) -> str: + """Format a complete birthday greeting with emoji.""" + emoji = get_theme_emoji(theme) + message = get_birthday_message(theme) + return f"{emoji} С днем рождения {first_name}, {message}" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9834de9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "bdbot" +version = "0.1.0" +description = "Telegram bot for tracking birthdays and sending themed greetings" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "pyTelegramBotAPI >= 4.30.0", + "SQLAlchemy >= 2.0.46", + "psycopg2-binary >= 2.9.11", + "python-dotenv >= 1.2.1", + "APScheduler >= 3.11.2", + "pytz >= 2025.2", +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..d56394c --- /dev/null +++ b/uv.lock @@ -0,0 +1,323 @@ +version = 1 +revision = 1 +requires-python = ">=3.12" + +[[package]] +name = "apscheduler" +version = "3.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/12/3e4389e5920b4c1763390c6d371162f3784f86f85cd6d6c1bfe68eef14e2/apscheduler-3.11.2.tar.gz", hash = "sha256:2a9966b052ec805f020c8c4c3ae6e6a06e24b1bf19f2e11d91d8cca0473eef41", size = 108683 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/64/2e54428beba8d9992aa478bb8f6de9e4ecaa5f8f513bcfd567ed7fb0262d/apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d", size = 64439 }, +] + +[[package]] +name = "bdbot" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "apscheduler" }, + { name = "psycopg2-binary" }, + { name = "pytelegrambotapi" }, + { name = "python-dotenv" }, + { name = "pytz" }, + { name = "sqlalchemy" }, +] + +[package.metadata] +requires-dist = [ + { name = "apscheduler", specifier = ">=3.10.0" }, + { name = "psycopg2-binary", specifier = ">=2.9.0" }, + { name = "pytelegrambotapi", specifier = ">=4.0.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "pytz", specifier = ">=2023.3" }, + { name = "sqlalchemy", specifier = ">=2.0.0" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425 }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162 }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558 }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497 }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240 }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471 }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864 }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647 }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110 }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839 }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667 }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535 }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816 }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694 }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131 }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390 }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091 }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936 }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180 }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346 }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874 }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076 }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601 }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376 }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825 }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583 }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366 }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300 }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465 }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404 }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092 }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408 }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746 }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889 }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641 }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779 }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035 }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542 }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524 }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395 }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680 }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045 }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687 }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014 }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044 }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940 }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104 }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743 }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402 }, +] + +[[package]] +name = "greenlet" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443 }, + { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359 }, + { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805 }, + { url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363 }, + { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947 }, + { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487 }, + { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087 }, + { url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156 }, + { url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403 }, + { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205 }, + { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284 }, + { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274 }, + { url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375 }, + { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904 }, + { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316 }, + { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549 }, + { url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042 }, + { url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294 }, + { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737 }, + { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422 }, + { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219 }, + { url = "https://files.pythonhosted.org/packages/e2/89/b95f2ddcc5f3c2bc09c8ee8d77be312df7f9e7175703ab780f2014a0e781/greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d", size = 671455 }, + { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237 }, + { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261 }, + { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719 }, + { url = "https://files.pythonhosted.org/packages/52/cb/c21a3fd5d2c9c8b622e7bede6d6d00e00551a5ee474ea6d831b5f567a8b4/greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a", size = 228125 }, + { url = "https://files.pythonhosted.org/packages/6a/8e/8a2db6d11491837af1de64b8aff23707c6e85241be13c60ed399a72e2ef8/greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79", size = 227519 }, + { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706 }, + { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209 }, + { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300 }, + { url = "https://files.pythonhosted.org/packages/7c/25/c51a63f3f463171e09cb586eb64db0861eb06667ab01a7968371a24c4f3b/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab", size = 662574 }, + { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842 }, + { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917 }, + { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092 }, + { url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181 }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603 }, + { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509 }, + { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159 }, + { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234 }, + { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236 }, + { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083 }, + { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281 }, + { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010 }, + { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641 }, + { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940 }, + { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147 }, + { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572 }, + { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529 }, + { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242 }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258 }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295 }, + { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133 }, + { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383 }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168 }, + { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712 }, + { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549 }, + { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215 }, + { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567 }, + { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755 }, + { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646 }, + { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701 }, + { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293 }, + { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184 }, + { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650 }, + { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663 }, + { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737 }, + { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643 }, + { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913 }, +] + +[[package]] +name = "pytelegrambotapi" +version = "4.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/62/e94e4aec454e7e5fba9092792318aa0242782176211f09f0c9cdec2db1c6/pytelegrambotapi-4.30.0.tar.gz", hash = "sha256:0bbfe244e008c9e7fb9a5c1ddffb1d99b28089387f0706d4a8aad496feccda06", size = 1369554 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/e4/05567359bc13d965d0008de8c57bdb81db7036d0f8ccf734e53276cda08e/pytelegrambotapi-4.30.0-py3-none-any.whl", hash = "sha256:cef0a61cfb21a320c597984985a7f417e35e67807f40cfc43bcb9ad3ebe944e6", size = 300109 }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230 }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.46" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c", size = 2153405 }, + { url = "https://files.pythonhosted.org/packages/06/6c/3192e24486749862f495ddc6584ed730c0c994a67550ec395d872a2ad650/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9", size = 3334702 }, + { url = "https://files.pythonhosted.org/packages/ea/a2/b9f33c8d68a3747d972a0bb758c6b63691f8fb8a49014bc3379ba15d4274/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b", size = 3347664 }, + { url = "https://files.pythonhosted.org/packages/aa/d2/3e59e2a91eaec9db7e8dc6b37b91489b5caeb054f670f32c95bcba98940f/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53", size = 3277372 }, + { url = "https://files.pythonhosted.org/packages/dd/dd/67bc2e368b524e2192c3927b423798deda72c003e73a1e94c21e74b20a85/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e", size = 3312425 }, + { url = "https://files.pythonhosted.org/packages/43/82/0ecd68e172bfe62247e96cb47867c2d68752566811a4e8c9d8f6e7c38a65/sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb", size = 2113155 }, + { url = "https://files.pythonhosted.org/packages/bc/2a/2821a45742073fc0331dc132552b30de68ba9563230853437cac54b2b53e/sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff", size = 2140078 }, + { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268 }, + { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511 }, + { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881 }, + { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559 }, + { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728 }, + { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295 }, + { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076 }, + { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533 }, + { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208 }, + { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292 }, + { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497 }, + { url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079 }, + { url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216 }, + { url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208 }, + { url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994 }, + { url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990 }, + { url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215 }, + { url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867 }, + { url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202 }, + { url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296 }, + { url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008 }, + { url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137 }, + { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882 }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521 }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026 }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584 }, +]