first
This commit is contained in:
16
.env.example
Normal file
16
.env.example
Normal file
@ -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
|
||||||
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@ -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
|
||||||
|
*~
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@ -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"]
|
||||||
144
README.md
Normal file
144
README.md
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
# Birthday Bot (bdbot)
|
||||||
|
|
||||||
|
Telegram-бот для отслеживания дней рождения и отправки тематических поздравлений в чатах.
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
- 📅 Отслеживание дней рождения пользователей
|
||||||
|
- 🎉 Автоматическая отправка тематических поздравлений
|
||||||
|
- 📊 Статистика по чатам
|
||||||
|
- 🔔 Уведомления о предстоящих днях рождения
|
||||||
|
- 🎨 12 тем для персонализации поздравлений
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- Python 3.12+
|
||||||
|
- PostgreSQL 14+
|
||||||
|
- Docker и Docker Compose (опционально)
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
### Локальная установка
|
||||||
|
|
||||||
|
1. Клонируйте репозиторий:
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
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
|
||||||
30
bot.py
Normal file
30
bot.py
Normal file
@ -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
|
||||||
33
config.py
Normal file
33
config.py
Normal file
@ -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")
|
||||||
85
database.py
Normal file
85
database.py
Normal file
@ -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"<User(user_id={self.user_id}, first_name={self.first_name})>"
|
||||||
|
|
||||||
|
|
||||||
|
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"<Chat(chat_id={self.chat_id}, chat_title={self.chat_title})>"
|
||||||
|
|
||||||
|
|
||||||
|
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"<UserChat(user_id={self.user_id}, chat_id={self.chat_id})>"
|
||||||
|
|
||||||
|
|
||||||
|
# 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()
|
||||||
37
docker-compose.yml
Normal file
37
docker-compose.yml
Normal file
@ -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:
|
||||||
1
handlers/__init__.py
Normal file
1
handlers/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Handlers package
|
||||||
169
handlers/command_handlers.py
Normal file
169
handlers/command_handlers.py
Normal file
@ -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
|
||||||
185
handlers/group_handlers.py
Normal file
185
handlers/group_handlers.py
Normal file
@ -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()
|
||||||
305
handlers/private_handlers.py
Normal file
305
handlers/private_handlers.py
Normal file
@ -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
|
||||||
93
handlers/scheduler.py
Normal file
93
handlers/scheduler.py
Normal file
@ -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
|
||||||
25
main.py
Normal file
25
main.py
Normal file
@ -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()
|
||||||
155
messages.py
Normal file
155
messages.py
Normal file
@ -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}"
|
||||||
14
pyproject.toml
Normal file
14
pyproject.toml
Normal file
@ -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",
|
||||||
|
]
|
||||||
323
uv.lock
generated
Normal file
323
uv.lock
generated
Normal file
@ -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 },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user