bd - api
This commit is contained in:
82
.dockerignore
Normal file
82
.dockerignore
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
*.egg-info
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.pytest_cache
|
||||||
|
.coverage
|
||||||
|
htmlcov
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env
|
||||||
|
venv
|
||||||
|
ENV
|
||||||
|
env.bak
|
||||||
|
venv.bak
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp
|
||||||
|
temp
|
||||||
|
.tmp
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Node modules (если есть)
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
|
||||||
|
# Images (не включаем картинки в контейнер)
|
||||||
|
images/
|
||||||
|
*.jpg
|
||||||
|
*.jpeg
|
||||||
|
*.png
|
||||||
|
*.gif
|
||||||
|
*.bmp
|
||||||
|
*.tiff
|
||||||
|
|
||||||
|
# Bot files (можно исключить файлы бота, если они не нужны в API контейнере)
|
||||||
|
bot.py
|
||||||
|
bot2.py
|
||||||
14
.env.example
Normal file
14
.env.example
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Настройки базы данных PostgreSQL
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=password
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=telegram_bot
|
||||||
|
|
||||||
|
# Настройки API
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
API_PORT=8000
|
||||||
|
API_RELOAD=true
|
||||||
|
|
||||||
|
# Настройки логирования
|
||||||
|
LOG_LEVEL=INFO
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/venv
|
||||||
|
/.venv
|
||||||
|
__pycache__/
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
357
API_README.md
Normal file
357
API_README.md
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
# Telegram Bot API
|
||||||
|
|
||||||
|
FastAPI приложение для взаимодействия с Telegram ботом и базой данных PostgreSQL через SQLAlchemy.
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
bot-telegram/
|
||||||
|
├── api/ # FastAPI приложение
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── main.py # Основное приложение
|
||||||
|
│ ├── routers.py # API маршруты
|
||||||
|
│ ├── schemas.py # Pydantic схемы
|
||||||
|
│ └── config.py # Конфигурация
|
||||||
|
├── database/ # Модули базы данных
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── database.py # Подключение к БД
|
||||||
|
│ ├── models.py # SQLAlchemy модели
|
||||||
|
│ └── ...
|
||||||
|
├── pyproject.toml # Зависимости проекта
|
||||||
|
├── .env.example # Пример переменных окружения
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Установка и настройка
|
||||||
|
|
||||||
|
### Способ 1: Запуск через Docker (рекомендуется)
|
||||||
|
|
||||||
|
#### 1. Запуск с Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Клонируйте репозиторий и перейдите в директорию
|
||||||
|
cd bot-telegram
|
||||||
|
|
||||||
|
# Запустите все сервисы (API + PostgreSQL)
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Для просмотра логов
|
||||||
|
docker-compose logs -f api
|
||||||
|
|
||||||
|
# Остановка сервисов
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Запуск с pgAdmin (опционально)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск с админкой для базы данных
|
||||||
|
docker-compose --profile admin up -d
|
||||||
|
|
||||||
|
# pgAdmin будет доступен по адресу: http://localhost:5050
|
||||||
|
# Email: admin@admin.com
|
||||||
|
# Password: admin
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Сборка только API контейнера
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Сборка образа
|
||||||
|
docker build -t telegram-bot-api .
|
||||||
|
|
||||||
|
# Запуск контейнера (требует запущенный PostgreSQL)
|
||||||
|
docker run -d \
|
||||||
|
--name telegram-bot-api \
|
||||||
|
-p 8000:8000 \
|
||||||
|
-e DB_HOST=host.docker.internal \
|
||||||
|
-e DB_PASSWORD=your_password \
|
||||||
|
telegram-bot-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Способ 2: Локальная установка
|
||||||
|
|
||||||
|
#### 1. Установка зависимостей
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Используя uv (рекомендуется)
|
||||||
|
uv sync
|
||||||
|
|
||||||
|
# Или используя pip
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Настройка базы данных
|
||||||
|
|
||||||
|
1. Создайте файл `.env` на основе `.env.example`:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Отредактируйте `.env` файл с вашими настройками PostgreSQL:
|
||||||
|
```env
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=your_password
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=telegram_bot
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Убедитесь, что PostgreSQL запущен и база данных создана:
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE telegram_bot;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Запуск API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Из корневой директории проекта
|
||||||
|
python run_api.py
|
||||||
|
|
||||||
|
# Или с помощью uvicorn напрямую
|
||||||
|
uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
API будет доступно по адресу: http://localhost:8000
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Общие endpoints
|
||||||
|
|
||||||
|
- `GET /` - Корневая информация
|
||||||
|
- `GET /health` - Проверка состояния приложения
|
||||||
|
- `GET /docs` - Swagger UI документация (автоматически)
|
||||||
|
- `GET /redoc` - ReDoc документация (автоматически)
|
||||||
|
|
||||||
|
### Пользователи (`/api/v1/users`)
|
||||||
|
|
||||||
|
- `GET /api/v1/users/` - Получить всех пользователей
|
||||||
|
- `GET /api/v1/users/{telegram_id}` - Получить пользователя по Telegram ID
|
||||||
|
- `POST /api/v1/users/` - Создать нового пользователя
|
||||||
|
- `PUT /api/v1/users/{telegram_id}` - Обновить пользователя
|
||||||
|
- `DELETE /api/v1/users/{telegram_id}` - Деактивировать пользователя
|
||||||
|
|
||||||
|
### Сообщения (`/api/v1/messages`)
|
||||||
|
|
||||||
|
- `GET /api/v1/messages/` - Получить сообщения с фильтрацией
|
||||||
|
- `GET /api/v1/messages/{message_id}` - Получить конкретное сообщение
|
||||||
|
- `POST /api/v1/messages/` - Создать новое сообщение
|
||||||
|
|
||||||
|
### Настройки бота (`/api/v1/settings`)
|
||||||
|
|
||||||
|
- `GET /api/v1/settings/` - Получить все настройки как словарь
|
||||||
|
- `GET /api/v1/settings/list` - Получить все настройки как список объектов
|
||||||
|
- `GET /api/v1/settings/{key}` - Получить настройку по ключу
|
||||||
|
- `POST /api/v1/settings/` - Создать или обновить настройку
|
||||||
|
- `PUT /api/v1/settings/{key}` - Обновить существующую настройку
|
||||||
|
- `DELETE /api/v1/settings/{key}` - Удалить настройку
|
||||||
|
|
||||||
|
## Примеры использования
|
||||||
|
|
||||||
|
### Создание пользователя
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/api/v1/users/" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"telegram_id": 123456789,
|
||||||
|
"username": "john_doe",
|
||||||
|
"first_name": "John",
|
||||||
|
"last_name": "Doe",
|
||||||
|
"language_code": "en"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Создание сообщения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/api/v1/messages/" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"telegram_message_id": 12345,
|
||||||
|
"user_id": 1,
|
||||||
|
"text": "Привет!",
|
||||||
|
"message_type": "text"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Настройка бота
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/api/v1/settings/" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"key": "welcome_message",
|
||||||
|
"value": "Добро пожаловать в наш бот!",
|
||||||
|
"description": "Приветственное сообщение для новых пользователей"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Модели данных
|
||||||
|
|
||||||
|
### User (Пользователь)
|
||||||
|
- `id` - Внутренний ID
|
||||||
|
- `telegram_id` - Telegram ID пользователя (уникальный)
|
||||||
|
- `username` - Username в Telegram
|
||||||
|
- `first_name` - Имя
|
||||||
|
- `last_name` - Фамилия
|
||||||
|
- `language_code` - Код языка (по умолчанию 'ru')
|
||||||
|
- `is_active` - Активен ли пользователь
|
||||||
|
- `is_admin` - Является ли администратором
|
||||||
|
- `created_at` - Дата создания
|
||||||
|
- `updated_at` - Дата обновления
|
||||||
|
|
||||||
|
### Message (Сообщение)
|
||||||
|
- `id` - Внутренний ID
|
||||||
|
- `telegram_message_id` - ID сообщения в Telegram
|
||||||
|
- `user_id` - ID пользователя (внешний ключ)
|
||||||
|
- `text` - Текст сообщения
|
||||||
|
- `message_type` - Тип сообщения (text, photo, document, etc.)
|
||||||
|
- `created_at` - Дата создания
|
||||||
|
|
||||||
|
### BotSettings (Настройки бота)
|
||||||
|
- `id` - Внутренний ID
|
||||||
|
- `key` - Ключ настройки (уникальный)
|
||||||
|
- `value` - Значение настройки
|
||||||
|
- `description` - Описание настройки
|
||||||
|
- `created_at` - Дата создания
|
||||||
|
- `updated_at` - Дата обновления
|
||||||
|
|
||||||
|
## Переменные окружения
|
||||||
|
|
||||||
|
| Переменная | Описание | По умолчанию |
|
||||||
|
|------------|----------|--------------|
|
||||||
|
| `DB_USER` | Пользователь PostgreSQL | `postgres` |
|
||||||
|
| `DB_PASSWORD` | Пароль PostgreSQL | `password` |
|
||||||
|
| `DB_HOST` | Хост PostgreSQL | `localhost` |
|
||||||
|
| `DB_PORT` | Порт PostgreSQL | `5432` |
|
||||||
|
| `DB_NAME` | Имя базы данных | `telegram_bot` |
|
||||||
|
| `API_HOST` | Хост API сервера | `0.0.0.0` |
|
||||||
|
| `API_PORT` | Порт API сервера | `8000` |
|
||||||
|
| `API_RELOAD` | Автоперезагрузка в dev режиме | `true` |
|
||||||
|
| `LOG_LEVEL` | Уровень логирования | `INFO` |
|
||||||
|
|
||||||
|
## Разработка
|
||||||
|
|
||||||
|
### Структура API
|
||||||
|
|
||||||
|
API организовано по модульному принципу:
|
||||||
|
|
||||||
|
- `main.py` - Основное приложение FastAPI
|
||||||
|
- `routers.py` - Определения маршрутов и бизнес-логика
|
||||||
|
- `schemas.py` - Pydantic схемы для валидации данных
|
||||||
|
- `config.py` - Конфигурация приложения
|
||||||
|
|
||||||
|
### Документация
|
||||||
|
|
||||||
|
FastAPI автоматически генерирует интерактивную документацию:
|
||||||
|
|
||||||
|
- Swagger UI: http://localhost:8000/docs
|
||||||
|
- ReDoc: http://localhost:8000/redoc
|
||||||
|
|
||||||
|
### Логирование
|
||||||
|
|
||||||
|
Приложение использует стандартный модуль `logging` Python. Уровень логирования можно настроить через переменную окружения `LOG_LEVEL`.
|
||||||
|
|
||||||
|
## Интеграция с Telegram ботом
|
||||||
|
|
||||||
|
Этот API предназначен для использования Telegram ботом. Бот может:
|
||||||
|
|
||||||
|
1. Регистрировать новых пользователей при первом взаимодействии
|
||||||
|
2. Сохранять все входящие сообщения
|
||||||
|
3. Получать и обновлять настройки бота
|
||||||
|
4. Управлять пользователями (активация/деактивация, права администратора)
|
||||||
|
|
||||||
|
Пример интеграции с aiogram или python-telegram-bot будет добавлен позже.
|
||||||
|
|
||||||
|
## Docker контейнеризация
|
||||||
|
|
||||||
|
### Структура Docker файлов
|
||||||
|
|
||||||
|
- **`Dockerfile`** - Многоступенчатая сборка для оптимизации размера образа
|
||||||
|
- **`docker-compose.yml`** - Оркестрация API + PostgreSQL + pgAdmin
|
||||||
|
- **`.dockerignore`** - Исключение ненужных файлов из контекста сборки
|
||||||
|
- **`init.sql`** - Скрипт инициализации базы данных
|
||||||
|
|
||||||
|
### Docker Compose сервисы
|
||||||
|
|
||||||
|
1. **`postgres`** - PostgreSQL 15 база данных (отдельный контейнер)
|
||||||
|
- Порт: 5432
|
||||||
|
- Данные сохраняются в Docker volume
|
||||||
|
- Автоматическая проверка готовности
|
||||||
|
|
||||||
|
2. **`app`** - Единое приложение (API + database логика в одном контейнере)
|
||||||
|
- Порт: 8000
|
||||||
|
- Включает FastAPI сервер и все модули для работы с БД
|
||||||
|
- Подключается к внешней PostgreSQL по сети
|
||||||
|
- Автоматически ждет готовности БД
|
||||||
|
|
||||||
|
3. **`pgadmin`** - Веб-админка PostgreSQL (опционально)
|
||||||
|
- Порт: 5050
|
||||||
|
- Запускается только с профилем `admin`
|
||||||
|
|
||||||
|
### Команды Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Сборка и запуск всех сервисов
|
||||||
|
docker-compose up --build -d
|
||||||
|
|
||||||
|
# Просмотр логов конкретного сервиса
|
||||||
|
docker-compose logs -f api
|
||||||
|
docker-compose logs -f postgres
|
||||||
|
|
||||||
|
# Перезапуск сервиса
|
||||||
|
docker-compose restart api
|
||||||
|
|
||||||
|
# Остановка и удаление контейнеров
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Остановка с удалением volumes (ВНИМАНИЕ: удаляет данные БД)
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
# Запуск только определенного сервиса
|
||||||
|
docker-compose up postgres -d
|
||||||
|
|
||||||
|
# Выполнение команд внутри контейнера
|
||||||
|
docker-compose exec api bash
|
||||||
|
docker-compose exec postgres psql -U postgres -d telegram_bot
|
||||||
|
```
|
||||||
|
|
||||||
|
### Переменные окружения в Docker
|
||||||
|
|
||||||
|
В `docker-compose.yml` настроены следующие переменные:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
# База данных
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: telegram_bot
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASSWORD: password
|
||||||
|
|
||||||
|
# API
|
||||||
|
API_HOST: 0.0.0.0
|
||||||
|
API_PORT: 8000
|
||||||
|
API_RELOAD: "false"
|
||||||
|
LOG_LEVEL: INFO
|
||||||
|
```
|
||||||
|
|
||||||
|
### Volumes и сети
|
||||||
|
|
||||||
|
- **Volume `postgres_data`** - Постоянное хранение данных PostgreSQL
|
||||||
|
- **Network `bot_network`** - Изолированная сеть для взаимодействия сервисов
|
||||||
|
|
||||||
|
### Производственное развертывание
|
||||||
|
|
||||||
|
Для продакшена рекомендуется:
|
||||||
|
|
||||||
|
1. Изменить пароли по умолчанию
|
||||||
|
2. Настроить внешние тома для данных
|
||||||
|
3. Использовать reverse proxy (nginx)
|
||||||
|
4. Настроить SSL сертификаты
|
||||||
|
5. Ограничить CORS origins
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Пример для продакшена
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
126
DOCKER_COMMANDS.md
Normal file
126
DOCKER_COMMANDS.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Docker команды для управления Telegram Bot API
|
||||||
|
|
||||||
|
## Основные команды
|
||||||
|
|
||||||
|
### Запуск и остановка
|
||||||
|
```bash
|
||||||
|
# Запуск всех сервисов
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Запуск с пересборкой
|
||||||
|
docker-compose up --build -d
|
||||||
|
|
||||||
|
# Запуск с админкой
|
||||||
|
docker-compose --profile admin up -d
|
||||||
|
|
||||||
|
# Остановка
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Остановка с удалением данных
|
||||||
|
docker-compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Логи и мониторинг
|
||||||
|
```bash
|
||||||
|
# Просмотр логов всех сервисов
|
||||||
|
docker-compose logs
|
||||||
|
|
||||||
|
# Логи конкретного сервиса
|
||||||
|
docker-compose logs -f app
|
||||||
|
docker-compose logs -f postgres
|
||||||
|
|
||||||
|
# Статус сервисов
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### Управление сервисами
|
||||||
|
```bash
|
||||||
|
# Перезапуск сервиса
|
||||||
|
docker-compose restart app
|
||||||
|
|
||||||
|
# Остановка конкретного сервиса
|
||||||
|
docker-compose stop app
|
||||||
|
|
||||||
|
# Масштабирование (если нужно)
|
||||||
|
docker-compose up --scale app=2 -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Работа с контейнерами
|
||||||
|
```bash
|
||||||
|
# Выполнение команд в контейнере приложения
|
||||||
|
docker-compose exec app bash
|
||||||
|
docker-compose exec app python -c "from database.database import test_db_connection; print(test_db_connection())"
|
||||||
|
|
||||||
|
# Подключение к PostgreSQL
|
||||||
|
docker-compose exec postgres psql -U postgres -d telegram_bot
|
||||||
|
|
||||||
|
# Просмотр таблиц
|
||||||
|
docker-compose exec postgres psql -U postgres -d telegram_bot -c "\dt"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Отладка и разработка
|
||||||
|
```bash
|
||||||
|
# Запуск в режиме разработки (с volume для кода)
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
# Просмотр переменных окружения в контейнере
|
||||||
|
docker-compose exec app env
|
||||||
|
|
||||||
|
# Проверка сетевого подключения
|
||||||
|
docker-compose exec app ping postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
### Очистка и обслуживание
|
||||||
|
```bash
|
||||||
|
# Удаление неиспользуемых образов
|
||||||
|
docker image prune
|
||||||
|
|
||||||
|
# Удаление всех остановленных контейнеров
|
||||||
|
docker container prune
|
||||||
|
|
||||||
|
# Полная очистка Docker системы
|
||||||
|
docker system prune -a
|
||||||
|
|
||||||
|
# Просмотр использования места
|
||||||
|
docker system df
|
||||||
|
```
|
||||||
|
|
||||||
|
## Готовые скрипты
|
||||||
|
|
||||||
|
### Скрипт для быстрого запуска (start.sh)
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
echo "Запуск Telegram Bot API..."
|
||||||
|
docker-compose up --build -d
|
||||||
|
echo "API доступно по адресу: http://localhost:8000"
|
||||||
|
echo "Документация: http://localhost:8000/docs"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Скрипт для остановки (stop.sh)
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
echo "Остановка Telegram Bot API..."
|
||||||
|
docker-compose down
|
||||||
|
echo "Сервисы остановлены"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Скрипт для полного сброса (reset.sh)
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
echo "ВНИМАНИЕ: Это удалит все данные из базы!"
|
||||||
|
read -p "Продолжить? (y/N) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
docker-compose down -v
|
||||||
|
docker-compose up --build -d
|
||||||
|
echo "Система сброшена и перезапущена"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Полезные ссылки после запуска
|
||||||
|
|
||||||
|
- API: http://localhost:8000
|
||||||
|
- Swagger UI: http://localhost:8000/docs
|
||||||
|
- ReDoc: http://localhost:8000/redoc
|
||||||
|
- Health Check: http://localhost:8000/health
|
||||||
|
- pgAdmin: http://localhost:5050 (если запущен с профилем admin)
|
||||||
63
Dockerfile
Normal file
63
Dockerfile
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Многоступенчатая сборка для оптимизации размера образа
|
||||||
|
|
||||||
|
# Этап 1: Сборка зависимостей
|
||||||
|
FROM python:3.12-slim AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Устанавливаем системные зависимости для сборки
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Устанавливаем uv для управления зависимостями
|
||||||
|
RUN pip install uv
|
||||||
|
|
||||||
|
# Копируем файлы зависимостей
|
||||||
|
COPY pyproject.toml uv.lock* ./
|
||||||
|
|
||||||
|
# Устанавливаем зависимости
|
||||||
|
RUN uv sync --frozen
|
||||||
|
|
||||||
|
# Этап 2: Производственный образ
|
||||||
|
FROM python:3.12-slim AS production
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Устанавливаем только необходимые runtime зависимости
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libpq5 \
|
||||||
|
postgresql-client \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& apt-get clean
|
||||||
|
|
||||||
|
# Копируем установленные зависимости из builder stage
|
||||||
|
COPY --from=builder /app/.venv /app/.venv
|
||||||
|
|
||||||
|
# Копируем исходный код приложения (единое приложение с API и database)
|
||||||
|
COPY api/ ./api/
|
||||||
|
COPY database/ ./database/
|
||||||
|
COPY run_api.py ./
|
||||||
|
COPY pyproject.toml ./
|
||||||
|
|
||||||
|
# Создаем непривилегированного пользователя для безопасности
|
||||||
|
RUN groupadd -r appuser && useradd -r -g appuser -s /bin/bash appuser
|
||||||
|
RUN chown -R appuser:appuser /app
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Открываем порт для API
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Устанавливаем переменные окружения
|
||||||
|
ENV PYTHONPATH=/app
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
|
|
||||||
|
# Проверка здоровья контейнера
|
||||||
|
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
||||||
|
CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1
|
||||||
|
|
||||||
|
# Команда для запуска единого приложения (API + database логика)
|
||||||
|
CMD ["python", "run_api.py"]
|
||||||
27
api/__init__.py
Normal file
27
api/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
"""
|
||||||
|
API пакет для Telegram Bot
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .main import app
|
||||||
|
from .routers import users_router, messages_router, settings_router
|
||||||
|
from .schemas import (
|
||||||
|
UserCreate, UserUpdate, UserResponse,
|
||||||
|
MessageCreate, MessageResponse,
|
||||||
|
BotSettingCreate, BotSettingUpdate, BotSettingResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
__all__ = [
|
||||||
|
"app",
|
||||||
|
"users_router",
|
||||||
|
"messages_router",
|
||||||
|
"settings_router",
|
||||||
|
"UserCreate",
|
||||||
|
"UserUpdate",
|
||||||
|
"UserResponse",
|
||||||
|
"MessageCreate",
|
||||||
|
"MessageResponse",
|
||||||
|
"BotSettingCreate",
|
||||||
|
"BotSettingUpdate",
|
||||||
|
"BotSettingResponse"
|
||||||
|
]
|
||||||
37
api/config.py
Normal file
37
api/config.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"""
|
||||||
|
Конфигурация для API приложения
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Загрузка переменных окружения из .env файла
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
class APIConfig:
|
||||||
|
"""Конфигурация API приложения"""
|
||||||
|
|
||||||
|
# Основные настройки
|
||||||
|
TITLE = "Telegram Bot API"
|
||||||
|
DESCRIPTION = "API для взаимодействия с Telegram ботом и базой данных"
|
||||||
|
VERSION = "1.0.0"
|
||||||
|
|
||||||
|
# Настройки сервера
|
||||||
|
HOST = os.getenv("API_HOST", "0.0.0.0")
|
||||||
|
PORT = int(os.getenv("API_PORT", "8000"))
|
||||||
|
RELOAD = os.getenv("API_RELOAD", "true").lower() == "true"
|
||||||
|
|
||||||
|
# Настройки CORS
|
||||||
|
CORS_ORIGINS = ["*"] # В продакшене стоит ограничить
|
||||||
|
CORS_CREDENTIALS = True
|
||||||
|
CORS_METHODS = ["*"]
|
||||||
|
CORS_HEADERS = ["*"]
|
||||||
|
|
||||||
|
# Настройки логирования
|
||||||
|
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
||||||
|
|
||||||
|
# API префикс
|
||||||
|
API_V1_PREFIX = "/api/v1"
|
||||||
|
|
||||||
|
# Глобальный экземпляр конфигурации
|
||||||
|
config = APIConfig()
|
||||||
95
api/main.py
Normal file
95
api/main.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"""
|
||||||
|
FastAPI приложение для взаимодействия с Telegram ботом
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Depends, HTTPException, status
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Добавляем путь к корневой директории проекта
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from database.database import db, get_db_session, init_database, test_db_connection
|
||||||
|
from database.models import User, Message, BotSettings
|
||||||
|
from api.routers import users_router, messages_router, settings_router
|
||||||
|
from api.schemas import HealthCheckResponse, RootResponse
|
||||||
|
from api.config import config
|
||||||
|
|
||||||
|
# Настройка логирования
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
"""Управление жизненным циклом приложения"""
|
||||||
|
# Инициализация при запуске
|
||||||
|
logger.info("Запуск FastAPI приложения...")
|
||||||
|
|
||||||
|
# Проверка подключения к БД
|
||||||
|
if not test_db_connection():
|
||||||
|
logger.error("Не удалось подключиться к базе данных!")
|
||||||
|
raise Exception("Database connection failed")
|
||||||
|
|
||||||
|
# Создание таблиц если они не существуют
|
||||||
|
try:
|
||||||
|
init_database()
|
||||||
|
logger.info("База данных инициализирована")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка инициализации базы данных: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
# Очистка при завершении
|
||||||
|
logger.info("Завершение работы FastAPI приложения...")
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
# Создание FastAPI приложения
|
||||||
|
app = FastAPI(
|
||||||
|
title=config.TITLE,
|
||||||
|
description=config.DESCRIPTION,
|
||||||
|
version=config.VERSION,
|
||||||
|
lifespan=lifespan
|
||||||
|
)
|
||||||
|
|
||||||
|
# Настройка CORS
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=config.CORS_ORIGINS,
|
||||||
|
allow_credentials=config.CORS_CREDENTIALS,
|
||||||
|
allow_methods=config.CORS_METHODS,
|
||||||
|
allow_headers=config.CORS_HEADERS,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Подключение роутеров
|
||||||
|
app.include_router(users_router, prefix=config.API_V1_PREFIX)
|
||||||
|
app.include_router(messages_router, prefix=config.API_V1_PREFIX)
|
||||||
|
app.include_router(settings_router, prefix=config.API_V1_PREFIX)
|
||||||
|
|
||||||
|
@app.get("/", response_model=RootResponse)
|
||||||
|
async def root():
|
||||||
|
"""Корневой эндпоинт"""
|
||||||
|
return RootResponse(
|
||||||
|
message=config.TITLE,
|
||||||
|
version=config.VERSION,
|
||||||
|
status="running"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/health", response_model=HealthCheckResponse)
|
||||||
|
async def health_check():
|
||||||
|
"""Проверка состояния приложения"""
|
||||||
|
db_status = test_db_connection()
|
||||||
|
return HealthCheckResponse(
|
||||||
|
status="healthy" if db_status else "unhealthy",
|
||||||
|
database="connected" if db_status else "disconnected"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run("main:app", host=config.HOST, port=config.PORT, reload=config.RELOAD)
|
||||||
259
api/routers.py
Normal file
259
api/routers.py
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
"""
|
||||||
|
API маршруты для работы с пользователями, сообщениями и настройками
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List, Optional
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Добавляем путь к корневой директории проекта
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from database.database import db
|
||||||
|
from database.models import User, Message, BotSettings
|
||||||
|
from api.schemas import (
|
||||||
|
UserCreate, UserUpdate, UserResponse,
|
||||||
|
MessageCreate, MessageResponse,
|
||||||
|
BotSettingCreate, BotSettingUpdate, BotSettingResponse,
|
||||||
|
MessageResponseModel, ErrorResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
# Зависимость для получения сессии БД
|
||||||
|
def get_db():
|
||||||
|
"""Получение сессии базы данных"""
|
||||||
|
session = db.get_session_sync()
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
# Роутер для работы с пользователями
|
||||||
|
users_router = APIRouter(prefix="/users", tags=["Users"])
|
||||||
|
|
||||||
|
@users_router.get("/{telegram_id}", response_model=UserResponse)
|
||||||
|
async def get_user_by_telegram_id(telegram_id: int, db_session: Session = Depends(get_db)):
|
||||||
|
"""Получение пользователя по Telegram ID"""
|
||||||
|
user = db_session.query(User).filter(User.telegram_id == telegram_id).first()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Пользователь не найден"
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
|
@users_router.get("/", response_model=List[UserResponse])
|
||||||
|
async def get_all_users(
|
||||||
|
limit: int = Query(default=100, ge=1, le=1000),
|
||||||
|
offset: int = Query(default=0, ge=0),
|
||||||
|
is_active: Optional[bool] = Query(default=None),
|
||||||
|
db_session: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Получение списка всех пользователей с фильтрацией"""
|
||||||
|
query = db_session.query(User)
|
||||||
|
|
||||||
|
if is_active is not None:
|
||||||
|
query = query.filter(User.is_active == is_active)
|
||||||
|
|
||||||
|
users = query.offset(offset).limit(limit).all()
|
||||||
|
return users
|
||||||
|
|
||||||
|
@users_router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_user(user_data: UserCreate, db_session: Session = Depends(get_db)):
|
||||||
|
"""Создание нового пользователя"""
|
||||||
|
# Проверяем, существует ли уже пользователь
|
||||||
|
existing_user = db_session.query(User).filter(User.telegram_id == user_data.telegram_id).first()
|
||||||
|
if existing_user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Пользователь уже существует"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создаем нового пользователя
|
||||||
|
new_user = User(**user_data.model_dump())
|
||||||
|
db_session.add(new_user)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(new_user)
|
||||||
|
|
||||||
|
return new_user
|
||||||
|
|
||||||
|
@users_router.put("/{telegram_id}", response_model=UserResponse)
|
||||||
|
async def update_user(
|
||||||
|
telegram_id: int,
|
||||||
|
user_data: UserUpdate,
|
||||||
|
db_session: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Обновление данных пользователя"""
|
||||||
|
user = db_session.query(User).filter(User.telegram_id == telegram_id).first()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Пользователь не найден"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обновляем только переданные поля
|
||||||
|
update_data = user_data.model_dump(exclude_unset=True)
|
||||||
|
for field, value in update_data.items():
|
||||||
|
setattr(user, field, value)
|
||||||
|
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(user)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
@users_router.delete("/{telegram_id}", response_model=MessageResponseModel)
|
||||||
|
async def delete_user(telegram_id: int, db_session: Session = Depends(get_db)):
|
||||||
|
"""Деактивация пользователя (мягкое удаление)"""
|
||||||
|
user = db_session.query(User).filter(User.telegram_id == telegram_id).first()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Пользователь не найден"
|
||||||
|
)
|
||||||
|
|
||||||
|
user.is_active = False
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
return MessageResponseModel(message="Пользователь деактивирован")
|
||||||
|
|
||||||
|
# Роутер для работы с сообщениями
|
||||||
|
messages_router = APIRouter(prefix="/messages", tags=["Messages"])
|
||||||
|
|
||||||
|
@messages_router.get("/", response_model=List[MessageResponse])
|
||||||
|
async def get_messages(
|
||||||
|
user_id: Optional[int] = Query(default=None),
|
||||||
|
message_type: Optional[str] = Query(default=None),
|
||||||
|
limit: int = Query(default=100, ge=1, le=1000),
|
||||||
|
offset: int = Query(default=0, ge=0),
|
||||||
|
db_session: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Получение сообщений с фильтрацией"""
|
||||||
|
query = db_session.query(Message)
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
query = query.filter(Message.user_id == user_id)
|
||||||
|
|
||||||
|
if message_type:
|
||||||
|
query = query.filter(Message.message_type == message_type)
|
||||||
|
|
||||||
|
messages = query.order_by(Message.created_at.desc()).offset(offset).limit(limit).all()
|
||||||
|
return messages
|
||||||
|
|
||||||
|
@messages_router.get("/{message_id}", response_model=MessageResponse)
|
||||||
|
async def get_message(message_id: int, db_session: Session = Depends(get_db)):
|
||||||
|
"""Получение конкретного сообщения по ID"""
|
||||||
|
message = db_session.query(Message).filter(Message.id == message_id).first()
|
||||||
|
if not message:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Сообщение не найдено"
|
||||||
|
)
|
||||||
|
return message
|
||||||
|
|
||||||
|
@messages_router.post("/", response_model=MessageResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_message(message_data: MessageCreate, db_session: Session = Depends(get_db)):
|
||||||
|
"""Создание нового сообщения"""
|
||||||
|
# Проверяем, существует ли пользователь
|
||||||
|
user = db_session.query(User).filter(User.id == message_data.user_id).first()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Пользователь не найден"
|
||||||
|
)
|
||||||
|
|
||||||
|
new_message = Message(**message_data.model_dump())
|
||||||
|
db_session.add(new_message)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(new_message)
|
||||||
|
|
||||||
|
return new_message
|
||||||
|
|
||||||
|
# Роутер для работы с настройками бота
|
||||||
|
settings_router = APIRouter(prefix="/settings", tags=["Bot Settings"])
|
||||||
|
|
||||||
|
@settings_router.get("/", response_model=dict)
|
||||||
|
async def get_all_settings(db_session: Session = Depends(get_db)):
|
||||||
|
"""Получение всех настроек бота в виде словаря"""
|
||||||
|
settings = db_session.query(BotSettings).all()
|
||||||
|
return {setting.key: setting.value for setting in settings}
|
||||||
|
|
||||||
|
@settings_router.get("/list", response_model=List[BotSettingResponse])
|
||||||
|
async def get_all_settings_list(db_session: Session = Depends(get_db)):
|
||||||
|
"""Получение всех настроек бота в виде списка объектов"""
|
||||||
|
settings = db_session.query(BotSettings).all()
|
||||||
|
return settings
|
||||||
|
|
||||||
|
@settings_router.get("/{key}", response_model=BotSettingResponse)
|
||||||
|
async def get_setting(key: str, db_session: Session = Depends(get_db)):
|
||||||
|
"""Получение конкретной настройки по ключу"""
|
||||||
|
setting = db_session.query(BotSettings).filter(BotSettings.key == key).first()
|
||||||
|
if not setting:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Настройка не найдена"
|
||||||
|
)
|
||||||
|
return setting
|
||||||
|
|
||||||
|
@settings_router.post("/", response_model=BotSettingResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_or_update_setting(
|
||||||
|
setting_data: BotSettingCreate,
|
||||||
|
db_session: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Создание или обновление настройки"""
|
||||||
|
existing_setting = db_session.query(BotSettings).filter(BotSettings.key == setting_data.key).first()
|
||||||
|
|
||||||
|
if existing_setting:
|
||||||
|
# Обновляем существующую настройку
|
||||||
|
update_data = setting_data.model_dump(exclude={"key"})
|
||||||
|
for field, value in update_data.items():
|
||||||
|
if value is not None:
|
||||||
|
setattr(existing_setting, field, value)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(existing_setting)
|
||||||
|
return existing_setting
|
||||||
|
else:
|
||||||
|
# Создаем новую настройку
|
||||||
|
new_setting = BotSettings(**setting_data.model_dump())
|
||||||
|
db_session.add(new_setting)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(new_setting)
|
||||||
|
return new_setting
|
||||||
|
|
||||||
|
@settings_router.put("/{key}", response_model=BotSettingResponse)
|
||||||
|
async def update_setting(
|
||||||
|
key: str,
|
||||||
|
setting_data: BotSettingUpdate,
|
||||||
|
db_session: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Обновление существующей настройки"""
|
||||||
|
setting = db_session.query(BotSettings).filter(BotSettings.key == key).first()
|
||||||
|
if not setting:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Настройка не найдена"
|
||||||
|
)
|
||||||
|
|
||||||
|
update_data = setting_data.model_dump(exclude_unset=True)
|
||||||
|
for field, value in update_data.items():
|
||||||
|
setattr(setting, field, value)
|
||||||
|
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(setting)
|
||||||
|
|
||||||
|
return setting
|
||||||
|
|
||||||
|
@settings_router.delete("/{key}", response_model=MessageResponseModel)
|
||||||
|
async def delete_setting(key: str, db_session: Session = Depends(get_db)):
|
||||||
|
"""Удаление настройки"""
|
||||||
|
setting = db_session.query(BotSettings).filter(BotSettings.key == key).first()
|
||||||
|
if not setting:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Настройка не найдена"
|
||||||
|
)
|
||||||
|
|
||||||
|
db_session.delete(setting)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
return MessageResponseModel(message=f"Настройка '{key}' удалена")
|
||||||
18
api/run_api.py
Normal file
18
api/run_api.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""
|
||||||
|
Скрипт для запуска API сервера
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
from api.config import config
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(f"Запуск {config.TITLE} v{config.VERSION}")
|
||||||
|
print(f"Документация будет доступна по адресу: http://{config.HOST}:{config.PORT}/docs")
|
||||||
|
|
||||||
|
uvicorn.run(
|
||||||
|
"api.main:app",
|
||||||
|
host=config.HOST,
|
||||||
|
port=config.PORT,
|
||||||
|
reload=config.RELOAD,
|
||||||
|
log_level=config.LOG_LEVEL.lower()
|
||||||
|
)
|
||||||
123
api/schemas.py
Normal file
123
api/schemas.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
"""
|
||||||
|
Pydantic схемы для валидации данных API
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Базовые схемы для пользователей
|
||||||
|
|
||||||
|
class UserBase(BaseModel):
|
||||||
|
"""Базовая схема пользователя"""
|
||||||
|
telegram_id: int = Field(..., description="Telegram ID пользователя")
|
||||||
|
username: Optional[str] = Field(None, max_length=255, description="Username в Telegram")
|
||||||
|
first_name: Optional[str] = Field(None, max_length=255, description="Имя пользователя")
|
||||||
|
last_name: Optional[str] = Field(None, max_length=255, description="Фамилия пользователя")
|
||||||
|
language_code: str = Field(default="ru", max_length=10, description="Код языка")
|
||||||
|
|
||||||
|
class UserCreate(UserBase):
|
||||||
|
"""Схема для создания пользователя"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class UserUpdate(BaseModel):
|
||||||
|
"""Схема для обновления пользователя"""
|
||||||
|
username: Optional[str] = Field(None, max_length=255)
|
||||||
|
first_name: Optional[str] = Field(None, max_length=255)
|
||||||
|
last_name: Optional[str] = Field(None, max_length=255)
|
||||||
|
language_code: Optional[str] = Field(None, max_length=10)
|
||||||
|
is_active: Optional[bool] = None
|
||||||
|
|
||||||
|
class UserResponse(UserBase):
|
||||||
|
"""Схема ответа с данными пользователя"""
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
is_active: bool
|
||||||
|
is_admin: bool
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
# Базовые схемы для сообщений
|
||||||
|
|
||||||
|
class MessageBase(BaseModel):
|
||||||
|
"""Базовая схема сообщения"""
|
||||||
|
telegram_message_id: int = Field(..., description="ID сообщения в Telegram")
|
||||||
|
user_id: int = Field(..., description="ID пользователя в нашей БД")
|
||||||
|
text: Optional[str] = Field(None, description="Текст сообщения")
|
||||||
|
message_type: str = Field(default="text", max_length=50, description="Тип сообщения")
|
||||||
|
|
||||||
|
class MessageCreate(MessageBase):
|
||||||
|
"""Схема для создания сообщения"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MessageResponse(MessageBase):
|
||||||
|
"""Схема ответа с данными сообщения"""
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
# Базовые схемы для настроек бота
|
||||||
|
|
||||||
|
class BotSettingBase(BaseModel):
|
||||||
|
"""Базовая схема настройки бота"""
|
||||||
|
key: str = Field(..., max_length=255, description="Ключ настройки")
|
||||||
|
value: Optional[str] = Field(None, description="Значение настройки")
|
||||||
|
description: Optional[str] = Field(None, description="Описание настройки")
|
||||||
|
|
||||||
|
class BotSettingCreate(BotSettingBase):
|
||||||
|
"""Схема для создания настройки"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class BotSettingUpdate(BaseModel):
|
||||||
|
"""Схема для обновления настройки"""
|
||||||
|
value: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
class BotSettingResponse(BotSettingBase):
|
||||||
|
"""Схема ответа с данными настройки"""
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
# Схемы для общих ответов API
|
||||||
|
|
||||||
|
class MessageResponseModel(BaseModel):
|
||||||
|
"""Общая схема ответа с сообщением"""
|
||||||
|
message: str
|
||||||
|
status: str = "success"
|
||||||
|
|
||||||
|
class ErrorResponse(BaseModel):
|
||||||
|
"""Схема ответа с ошибкой"""
|
||||||
|
detail: str
|
||||||
|
status: str = "error"
|
||||||
|
|
||||||
|
class HealthCheckResponse(BaseModel):
|
||||||
|
"""Схема ответа проверки здоровья"""
|
||||||
|
status: str
|
||||||
|
database: str
|
||||||
|
|
||||||
|
class RootResponse(BaseModel):
|
||||||
|
"""Схема корневого ответа"""
|
||||||
|
message: str
|
||||||
|
version: str
|
||||||
|
status: str
|
||||||
|
|
||||||
|
# Схемы для пагинации
|
||||||
|
|
||||||
|
class PaginationParams(BaseModel):
|
||||||
|
"""Параметры пагинации"""
|
||||||
|
limit: int = Field(default=100, ge=1, le=1000, description="Количество элементов на странице")
|
||||||
|
offset: int = Field(default=0, ge=0, description="Смещение")
|
||||||
|
|
||||||
|
class PaginatedResponse(BaseModel):
|
||||||
|
"""Схема ответа с пагинацией"""
|
||||||
|
items: list
|
||||||
|
total: int
|
||||||
|
limit: int
|
||||||
|
offset: int
|
||||||
|
has_next: bool
|
||||||
|
has_prev: bool
|
||||||
11
database/.env.example
Normal file
11
database/.env.example
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Переменные окружения для подключения к PostgreSQL
|
||||||
|
|
||||||
|
# Параметры подключения к базе данных
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=your_password_here
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=telegram_bot
|
||||||
|
|
||||||
|
# Для продакшена можно также использовать полную строку подключения:
|
||||||
|
# DATABASE_URL=postgresql://username:password@localhost:5432/database_name
|
||||||
102
database/README.md
Normal file
102
database/README.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# База данных PostgreSQL для Telegram бота
|
||||||
|
|
||||||
|
Этот модуль содержит полную настройку для работы с PostgreSQL через SQLAlchemy.
|
||||||
|
|
||||||
|
## Структура файлов
|
||||||
|
|
||||||
|
- `database.py` - Основные классы для подключения к базе данных
|
||||||
|
- `models.py` - Модели SQLAlchemy (таблицы)
|
||||||
|
- `repositories.py` - Репозитории для работы с данными
|
||||||
|
- `services.py` - Высокоуровневые сервисы
|
||||||
|
- `example.py` - Примеры использования
|
||||||
|
- `requirements.txt` - Зависимости
|
||||||
|
- `.env.example` - Пример файла с переменными окружения
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
### 1. Установка зависимостей
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Настройка базы данных
|
||||||
|
|
||||||
|
Создайте файл `.env` на основе `.env.example`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=your_password
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=telegram_bot
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Основное использование
|
||||||
|
|
||||||
|
```python
|
||||||
|
from database import db_service, setup_database
|
||||||
|
|
||||||
|
# Настройка базы данных (создание таблиц)
|
||||||
|
setup_database()
|
||||||
|
|
||||||
|
# Регистрация пользователя
|
||||||
|
user, created = db_service.register_user(
|
||||||
|
telegram_id=123456789,
|
||||||
|
username="example_user",
|
||||||
|
first_name="Иван"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Сохранение сообщения
|
||||||
|
message = db_service.save_message(
|
||||||
|
telegram_message_id=1001,
|
||||||
|
telegram_user_id=123456789,
|
||||||
|
text="Привет!"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Работа с настройками
|
||||||
|
db_service.set_setting("bot_name", "Мой Бот")
|
||||||
|
bot_name = db_service.get_setting("bot_name")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Основные возможности
|
||||||
|
|
||||||
|
### Модели данных
|
||||||
|
|
||||||
|
- **User** - Пользователи телеграм бота
|
||||||
|
- **Message** - Сообщения от пользователей
|
||||||
|
- **BotSettings** - Настройки бота
|
||||||
|
|
||||||
|
### Репозитории
|
||||||
|
|
||||||
|
- **UserRepository** - CRUD операции с пользователями
|
||||||
|
- **MessageRepository** - CRUD операции с сообщениями
|
||||||
|
- **SettingsRepository** - CRUD операции с настройками
|
||||||
|
|
||||||
|
### Сервисы
|
||||||
|
|
||||||
|
- **DatabaseService** - Высокоуровневый API для работы с данными
|
||||||
|
|
||||||
|
## Примеры использования
|
||||||
|
|
||||||
|
Запустите `example.py` для демонстрации основных возможностей:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Настройки
|
||||||
|
|
||||||
|
Все настройки подключения к базе данных можно задать через переменные окружения:
|
||||||
|
|
||||||
|
- `DB_USER` - имя пользователя PostgreSQL
|
||||||
|
- `DB_PASSWORD` - пароль
|
||||||
|
- `DB_HOST` - хост (по умолчанию localhost)
|
||||||
|
- `DB_PORT` - порт (по умолчанию 5432)
|
||||||
|
- `DB_NAME` - имя базы данных
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- PostgreSQL 10+
|
||||||
|
- SQLAlchemy 2.0+
|
||||||
37
database/__init__.py
Normal file
37
database/__init__.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"""
|
||||||
|
Инициализация пакета database
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .database import Database, DatabaseConfig, db, get_db_session, init_database, test_db_connection
|
||||||
|
from .models import User, Message, BotSettings
|
||||||
|
from .repositories import UserRepository, MessageRepository, SettingsRepository
|
||||||
|
from .services import DatabaseService, db_service, setup_database
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# Основные классы
|
||||||
|
'Database',
|
||||||
|
'DatabaseConfig',
|
||||||
|
|
||||||
|
# Глобальные объекты
|
||||||
|
'db',
|
||||||
|
'db_service',
|
||||||
|
|
||||||
|
# Функции
|
||||||
|
'get_db_session',
|
||||||
|
'init_database',
|
||||||
|
'test_db_connection',
|
||||||
|
'setup_database',
|
||||||
|
|
||||||
|
# Модели
|
||||||
|
'User',
|
||||||
|
'Message',
|
||||||
|
'BotSettings',
|
||||||
|
|
||||||
|
# Репозитории
|
||||||
|
'UserRepository',
|
||||||
|
'MessageRepository',
|
||||||
|
'SettingsRepository',
|
||||||
|
|
||||||
|
# Сервисы
|
||||||
|
'DatabaseService',
|
||||||
|
]
|
||||||
180
database/database.py
Normal file
180
database/database.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
"""
|
||||||
|
Модуль для подключения к базе данных PostgreSQL через SQLAlchemy
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine, MetaData
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy.pool import StaticPool
|
||||||
|
from contextlib import contextmanager
|
||||||
|
import os
|
||||||
|
from typing import Generator
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Настройка логирования
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Базовый класс для всех моделей
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class DatabaseConfig:
|
||||||
|
"""Конфигурация базы данных"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Параметры подключения к PostgreSQL
|
||||||
|
self.DB_USER = os.getenv('DB_USER', 'postgres')
|
||||||
|
self.DB_PASSWORD = os.getenv('DB_PASSWORD', 'password')
|
||||||
|
self.DB_HOST = os.getenv('DB_HOST', 'localhost')
|
||||||
|
self.DB_PORT = os.getenv('DB_PORT', '5432')
|
||||||
|
self.DB_NAME = os.getenv('DB_NAME', 'telegram_bot')
|
||||||
|
|
||||||
|
# Строка подключения
|
||||||
|
self.DATABASE_URL = f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
||||||
|
|
||||||
|
# Настройки SQLAlchemy
|
||||||
|
self.SQLALCHEMY_ENGINE_OPTIONS = {
|
||||||
|
'pool_pre_ping': True, # Проверка соединения перед использованием
|
||||||
|
'pool_recycle': 300, # Переподключение каждые 5 минут
|
||||||
|
'pool_size': 10, # Размер пула соединений
|
||||||
|
'max_overflow': 20, # Максимальное количество дополнительных соединений
|
||||||
|
'echo': False, # Логирование SQL запросов (для отладки)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
"""Класс для управления подключением к базе данных"""
|
||||||
|
|
||||||
|
def __init__(self, config: DatabaseConfig = None):
|
||||||
|
self.config = config or DatabaseConfig()
|
||||||
|
self.engine = None
|
||||||
|
self.SessionLocal = None
|
||||||
|
self._initialize()
|
||||||
|
|
||||||
|
def _initialize(self):
|
||||||
|
"""Инициализация подключения к базе данных"""
|
||||||
|
try:
|
||||||
|
# Создание движка SQLAlchemy
|
||||||
|
self.engine = create_engine(
|
||||||
|
self.config.DATABASE_URL,
|
||||||
|
**self.config.SQLALCHEMY_ENGINE_OPTIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создание фабрики сессий
|
||||||
|
self.SessionLocal = sessionmaker(
|
||||||
|
autocommit=False,
|
||||||
|
autoflush=False,
|
||||||
|
bind=self.engine
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Подключение к базе данных успешно инициализировано")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при инициализации базы данных: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def create_tables(self):
|
||||||
|
"""Создание всех таблиц в базе данных"""
|
||||||
|
try:
|
||||||
|
Base.metadata.create_all(bind=self.engine)
|
||||||
|
logger.info("Таблицы успешно созданы")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при создании таблиц: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def drop_tables(self):
|
||||||
|
"""Удаление всех таблиц из базы данных"""
|
||||||
|
try:
|
||||||
|
Base.metadata.drop_all(bind=self.engine)
|
||||||
|
logger.info("Таблицы успешно удалены")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при удалении таблиц: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def get_session(self) -> Generator:
|
||||||
|
"""Контекстный менеджер для получения сессии базы данных"""
|
||||||
|
session = self.SessionLocal()
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
session.rollback()
|
||||||
|
logger.error(f"Ошибка в сессии базы данных: {e}")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def get_session_sync(self):
|
||||||
|
"""Получение синхронной сессии (не забудьте закрыть!)"""
|
||||||
|
return self.SessionLocal()
|
||||||
|
|
||||||
|
def test_connection(self) -> bool:
|
||||||
|
"""Проверка подключения к базе данных"""
|
||||||
|
try:
|
||||||
|
with self.get_session() as session:
|
||||||
|
session.execute("SELECT 1")
|
||||||
|
logger.info("Подключение к базе данных работает корректно")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка подключения к базе данных: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Закрытие подключения к базе данных"""
|
||||||
|
if self.engine:
|
||||||
|
self.engine.dispose()
|
||||||
|
logger.info("Подключение к базе данных закрыто")
|
||||||
|
|
||||||
|
# Глобальный экземпляр базы данных
|
||||||
|
db = Database()
|
||||||
|
|
||||||
|
# Удобные функции для использования в приложении
|
||||||
|
def get_db_session():
|
||||||
|
"""Получение сессии базы данных для использования в приложении"""
|
||||||
|
return db.get_session()
|
||||||
|
|
||||||
|
def init_database():
|
||||||
|
"""Инициализация базы данных (создание таблиц)"""
|
||||||
|
db.create_tables()
|
||||||
|
|
||||||
|
def test_db_connection():
|
||||||
|
"""Тестирование подключения к базе данных"""
|
||||||
|
return db.test_connection()
|
||||||
|
|
||||||
|
def load_image_from_file(file_path: str, alt_text: str = None):
|
||||||
|
"""
|
||||||
|
Быстрая загрузка изображения из файла в базу данных
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Путь к файлу изображения
|
||||||
|
alt_text: Альтернативный текст
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Image: Объект изображения или None при ошибке
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import mimetypes
|
||||||
|
from database.repositories import ImageRepository
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
filename = os.path.basename(file_path)
|
||||||
|
mime_type, _ = mimetypes.guess_type(filename)
|
||||||
|
if not mime_type:
|
||||||
|
mime_type = 'image/jpeg'
|
||||||
|
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
image_data = f.read()
|
||||||
|
|
||||||
|
with get_db_session() as session:
|
||||||
|
image = ImageRepository.create_image(
|
||||||
|
session, filename, mime_type, image_data, alt_text
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
return image
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка загрузки изображения {file_path}: {e}")
|
||||||
|
return None
|
||||||
145
database/example.py
Normal file
145
database/example.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
"""
|
||||||
|
Пример использования базы данных
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Добавляем путь к проекту в PYTHONPATH
|
||||||
|
project_root = Path(__file__).parent.parent
|
||||||
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
|
from database.database import db, test_db_connection
|
||||||
|
from database.services import db_service, setup_database
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Настройка логирования
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Основная функция для демонстрации работы с базой данных"""
|
||||||
|
print("🚀 Пример работы с PostgreSQL через SQLAlchemy")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# 1. Тестирование подключения
|
||||||
|
print("1. Тестирование подключения к базе данных...")
|
||||||
|
if test_db_connection():
|
||||||
|
print("✅ Подключение успешно!")
|
||||||
|
else:
|
||||||
|
print("❌ Ошибка подключения!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. Настройка базы данных (создание таблиц)
|
||||||
|
print("\n2. Настройка базы данных...")
|
||||||
|
if setup_database():
|
||||||
|
print("✅ База данных настроена!")
|
||||||
|
else:
|
||||||
|
print("❌ Ошибка настройки базы данных!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. Работа с пользователями
|
||||||
|
print("\n3. Работа с пользователями...")
|
||||||
|
|
||||||
|
# Регистрация пользователя
|
||||||
|
user, created = db_service.register_user(
|
||||||
|
telegram_id=123456789,
|
||||||
|
username="test_user",
|
||||||
|
first_name="Иван",
|
||||||
|
last_name="Иванов"
|
||||||
|
)
|
||||||
|
print(f"Пользователь: {user.first_name} {user.last_name} (@{user.username})")
|
||||||
|
print(f"Создан новый: {'Да' if created else 'Нет'}")
|
||||||
|
|
||||||
|
# Повторная регистрация (должен вернуть существующего)
|
||||||
|
user2, created2 = db_service.register_user(
|
||||||
|
telegram_id=123456789,
|
||||||
|
username="test_user_updated"
|
||||||
|
)
|
||||||
|
print(f"Повторная регистрация - создан новый: {'Да' if created2 else 'Нет'}")
|
||||||
|
|
||||||
|
# 4. Работа с сообщениями
|
||||||
|
print("\n4. Работа с сообщениями...")
|
||||||
|
|
||||||
|
# Сохранение сообщения
|
||||||
|
message = db_service.save_message(
|
||||||
|
telegram_message_id=1001,
|
||||||
|
telegram_user_id=123456789,
|
||||||
|
text="Привет! Это тестовое сообщение",
|
||||||
|
message_type="text"
|
||||||
|
)
|
||||||
|
print(f"Сообщение сохранено: ID {message.id if message else 'Ошибка'}")
|
||||||
|
|
||||||
|
# Получение сообщений пользователя
|
||||||
|
messages = db_service.get_user_messages(123456789, limit=10)
|
||||||
|
print(f"Количество сообщений пользователя: {len(messages)}")
|
||||||
|
|
||||||
|
# 5. Работа с настройками
|
||||||
|
print("\n5. Работа с настройками...")
|
||||||
|
|
||||||
|
# Установка настройки
|
||||||
|
db_service.set_setting("welcome_message", "Добро пожаловать в бот!", "Приветственное сообщение")
|
||||||
|
|
||||||
|
# Получение настройки
|
||||||
|
welcome_msg = db_service.get_setting("welcome_message", "Привет!")
|
||||||
|
print(f"Приветственное сообщение: {welcome_msg}")
|
||||||
|
|
||||||
|
# 6. Статистика
|
||||||
|
print("\n6. Статистика...")
|
||||||
|
user_count = db_service.get_user_count()
|
||||||
|
print(f"Всего пользователей: {user_count}")
|
||||||
|
|
||||||
|
print("\n✅ Демонстрация завершена успешно!")
|
||||||
|
|
||||||
|
def example_usage_in_telegram_bot():
|
||||||
|
"""Пример использования в телеграм боте"""
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("📱 Пример использования в телеграм боте:")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Имитация обработки сообщения от пользователя
|
||||||
|
telegram_user_id = 987654321
|
||||||
|
message_text = "Какая сегодня погода?"
|
||||||
|
|
||||||
|
# 1. Регистрируем/получаем пользователя
|
||||||
|
user, created = db_service.register_user(
|
||||||
|
telegram_id=telegram_user_id,
|
||||||
|
username="weather_user",
|
||||||
|
first_name="Мария"
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
print(f"🆕 Новый пользователь: {user.first_name}")
|
||||||
|
# Отправляем приветственное сообщение
|
||||||
|
welcome_msg = db_service.get_setting("welcome_message", "Привет!")
|
||||||
|
print(f"Отправляем: {welcome_msg}")
|
||||||
|
|
||||||
|
# 2. Сохраняем сообщение
|
||||||
|
message = db_service.save_message(
|
||||||
|
telegram_message_id=2001,
|
||||||
|
telegram_user_id=telegram_user_id,
|
||||||
|
text=message_text
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"💬 Сохранено сообщение: '{message_text[:30]}...'")
|
||||||
|
|
||||||
|
# 3. Обновляем активность пользователя
|
||||||
|
db_service.update_user_info(telegram_user_id, is_active=True)
|
||||||
|
|
||||||
|
print("✅ Обработка сообщения завершена")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
example_usage_in_telegram_bot()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка в примере: {e}")
|
||||||
|
print(f"❌ Ошибка: {e}")
|
||||||
|
finally:
|
||||||
|
# Закрываем подключение
|
||||||
|
db.close()
|
||||||
|
print("\n🔒 Подключение к базе данных закрыто")
|
||||||
271
database/load_images.py
Normal file
271
database/load_images.py
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
"""
|
||||||
|
Скрипт для загрузки изображений из папки images/ в базу данных
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import mimetypes
|
||||||
|
from pathlib import Path
|
||||||
|
from database.database import db, get_db_session
|
||||||
|
from database.services import ImageService
|
||||||
|
from database.repositories import ImageRepository
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_content_type(filename: str) -> str:
|
||||||
|
"""Определение MIME типа изображения по расширению файла"""
|
||||||
|
mime_type, _ = mimetypes.guess_type(filename)
|
||||||
|
|
||||||
|
# Если mimetypes не смог определить, делаем по расширению
|
||||||
|
if not mime_type:
|
||||||
|
ext = filename.lower().split('.')[-1]
|
||||||
|
mime_types = {
|
||||||
|
'jpg': 'image/jpeg',
|
||||||
|
'jpeg': 'image/jpeg',
|
||||||
|
'png': 'image/png',
|
||||||
|
'gif': 'image/gif',
|
||||||
|
'bmp': 'image/bmp',
|
||||||
|
'webp': 'image/webp'
|
||||||
|
}
|
||||||
|
mime_type = mime_types.get(ext, 'image/jpeg')
|
||||||
|
|
||||||
|
return mime_type
|
||||||
|
|
||||||
|
|
||||||
|
def load_image_to_db(image_path: str, alt_text: str = None) -> bool:
|
||||||
|
"""
|
||||||
|
Загрузка одного изображения в базу данных
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_path: Путь к файлу изображения
|
||||||
|
alt_text: Альтернативный текст для изображения
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если загрузка успешна, False если ошибка
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Проверяем, существует ли файл
|
||||||
|
if not os.path.isfile(image_path):
|
||||||
|
logger.error(f"Файл не найден: {image_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Получаем информацию о файле
|
||||||
|
filename = os.path.basename(image_path)
|
||||||
|
content_type = get_image_content_type(filename)
|
||||||
|
|
||||||
|
# Читаем бинарные данные
|
||||||
|
with open(image_path, 'rb') as f:
|
||||||
|
image_data = f.read()
|
||||||
|
|
||||||
|
# Проверяем размер файла (ограничение PostgreSQL ~1GB, но лучше меньше)
|
||||||
|
file_size = len(image_data)
|
||||||
|
if file_size > 10 * 1024 * 1024: # 10MB лимит
|
||||||
|
logger.warning(f"Файл слишком большой ({file_size} байт): {filename}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Сохраняем в базу данных
|
||||||
|
with get_db_session() as session:
|
||||||
|
# Проверяем, не существует ли уже такой файл
|
||||||
|
existing_image = ImageRepository.get_image_by_filename(session, filename)
|
||||||
|
if existing_image:
|
||||||
|
logger.info(f"Изображение уже существует в БД: {filename}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Создаем новую запись
|
||||||
|
image = ImageRepository.create_image(
|
||||||
|
session=session,
|
||||||
|
filename=filename,
|
||||||
|
content_type=content_type,
|
||||||
|
image_data=image_data,
|
||||||
|
alt_text=alt_text or f"Изображение {filename}"
|
||||||
|
)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
logger.info(f"Загружено изображение: {filename} (ID: {image.id}, размер: {file_size} байт)")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при загрузке изображения {image_path}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def load_images_from_directory(images_dir: str = "images") -> dict:
|
||||||
|
"""
|
||||||
|
Загрузка всех изображений из указанной директории
|
||||||
|
|
||||||
|
Args:
|
||||||
|
images_dir: Путь к директории с изображениями
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Статистика загрузки
|
||||||
|
"""
|
||||||
|
if not os.path.isdir(images_dir):
|
||||||
|
logger.error(f"Директория не найдена: {images_dir}")
|
||||||
|
return {"success": 0, "failed": 0, "skipped": 0}
|
||||||
|
|
||||||
|
# Поддерживаемые форматы изображений
|
||||||
|
supported_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}
|
||||||
|
|
||||||
|
stats = {"success": 0, "failed": 0, "skipped": 0}
|
||||||
|
|
||||||
|
logger.info(f"Начинаем загрузку изображений из директории: {images_dir}")
|
||||||
|
|
||||||
|
# Проходим по всем файлам в директории
|
||||||
|
for filename in os.listdir(images_dir):
|
||||||
|
file_path = os.path.join(images_dir, filename)
|
||||||
|
|
||||||
|
# Пропускаем директории
|
||||||
|
if os.path.isdir(file_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Проверяем расширение файла
|
||||||
|
file_ext = Path(filename).suffix.lower()
|
||||||
|
if file_ext not in supported_extensions:
|
||||||
|
logger.info(f"Пропускаем файл неподдерживаемого формата: {filename}")
|
||||||
|
stats["skipped"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Загружаем изображение
|
||||||
|
if load_image_to_db(file_path):
|
||||||
|
stats["success"] += 1
|
||||||
|
else:
|
||||||
|
stats["failed"] += 1
|
||||||
|
|
||||||
|
logger.info(f"Загрузка завершена. Успешно: {stats['success']}, "
|
||||||
|
f"Ошибок: {stats['failed']}, Пропущено: {stats['skipped']}")
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def list_images_in_db():
|
||||||
|
"""Показать все изображения в базе данных"""
|
||||||
|
try:
|
||||||
|
with get_db_session() as session:
|
||||||
|
images = ImageRepository.get_all_images(session)
|
||||||
|
|
||||||
|
if not images:
|
||||||
|
logger.info("В базе данных нет изображений")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"Изображения в базе данных ({len(images)} шт.):")
|
||||||
|
for image in images:
|
||||||
|
size_kb = image.file_size / 1024
|
||||||
|
logger.info(f" ID: {image.id}, Файл: {image.filename}, "
|
||||||
|
f"Тип: {image.content_type}, Размер: {size_kb:.1f} KB")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при получении списка изображений: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def delete_image_from_db(filename: str = None, image_id: int = None) -> bool:
|
||||||
|
"""
|
||||||
|
Удаление изображения из базы данных
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Имя файла для удаления
|
||||||
|
image_id: ID изображения для удаления
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если удаление успешно
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with get_db_session() as session:
|
||||||
|
if filename:
|
||||||
|
image = ImageRepository.get_image_by_filename(session, filename)
|
||||||
|
elif image_id:
|
||||||
|
image = ImageRepository.get_image_by_id(session, image_id)
|
||||||
|
else:
|
||||||
|
logger.error("Необходимо указать filename или image_id")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not image:
|
||||||
|
logger.error("Изображение не найдено")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if ImageRepository.delete_image(session, image.id):
|
||||||
|
session.commit()
|
||||||
|
logger.info(f"Изображение удалено: {image.filename}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error("Ошибка при удалении изображения")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при удалении изображения: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
"""Главная функция для запуска скрипта"""
|
||||||
|
|
||||||
|
print("🖼️ Скрипт загрузки изображений в базу данных")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Проверяем подключение к БД
|
||||||
|
try:
|
||||||
|
from database.database import test_db_connection
|
||||||
|
if not test_db_connection():
|
||||||
|
print("❌ Ошибка подключения к базе данных!")
|
||||||
|
exit(1)
|
||||||
|
print("✅ Подключение к базе данных успешно")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка: {e}")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# Меню
|
||||||
|
while True:
|
||||||
|
print("\nВыберите действие:")
|
||||||
|
print("1. Загрузить все изображения из папки images/")
|
||||||
|
print("2. Загрузить конкретное изображение")
|
||||||
|
print("3. Показать все изображения в БД")
|
||||||
|
print("4. Удалить изображение из БД")
|
||||||
|
print("5. Выход")
|
||||||
|
|
||||||
|
choice = input("\nВведите номер (1-5): ").strip()
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
stats = load_images_from_directory("images")
|
||||||
|
print(f"\n📊 Результат: загружено {stats['success']}, "
|
||||||
|
f"ошибок {stats['failed']}, пропущено {stats['skipped']}")
|
||||||
|
|
||||||
|
elif choice == "2":
|
||||||
|
image_path = input("Введите путь к изображению: ").strip()
|
||||||
|
alt_text = input("Введите описание (необязательно): ").strip() or None
|
||||||
|
if load_image_to_db(image_path, alt_text):
|
||||||
|
print("✅ Изображение загружено!")
|
||||||
|
else:
|
||||||
|
print("❌ Ошибка загрузки")
|
||||||
|
|
||||||
|
elif choice == "3":
|
||||||
|
list_images_in_db()
|
||||||
|
|
||||||
|
elif choice == "4":
|
||||||
|
print("Удаление по:")
|
||||||
|
print("1. Имени файла")
|
||||||
|
print("2. ID")
|
||||||
|
sub_choice = input("Выберите (1-2): ").strip()
|
||||||
|
|
||||||
|
if sub_choice == "1":
|
||||||
|
filename = input("Введите имя файла: ").strip()
|
||||||
|
if delete_image_from_db(filename=filename):
|
||||||
|
print("✅ Изображение удалено!")
|
||||||
|
else:
|
||||||
|
print("❌ Ошибка удаления")
|
||||||
|
elif sub_choice == "2":
|
||||||
|
try:
|
||||||
|
image_id = int(input("Введите ID: ").strip())
|
||||||
|
if delete_image_from_db(image_id=image_id):
|
||||||
|
print("✅ Изображение удалено!")
|
||||||
|
else:
|
||||||
|
print("❌ Ошибка удаления")
|
||||||
|
except ValueError:
|
||||||
|
print("❌ Неверный ID")
|
||||||
|
|
||||||
|
elif choice == "5":
|
||||||
|
print("👋 До свидания!")
|
||||||
|
break
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("❌ Неверный выбор")
|
||||||
208
database/models.py
Normal file
208
database/models.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
"""
|
||||||
|
Базовые модели для работы с базой данных
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, BigInteger, ForeignKey, LargeBinary
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from database.database import Base
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
"""Модель пользователя телеграм бота"""
|
||||||
|
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
telegram_id = Column(BigInteger, unique=True, index=True, nullable=False)
|
||||||
|
username = Column(String(255), nullable=True)
|
||||||
|
first_name = Column(String(255), nullable=True)
|
||||||
|
last_name = Column(String(255), nullable=True)
|
||||||
|
language_code = Column(String(10), default='ru')
|
||||||
|
is_active = Column(Boolean, default=True)
|
||||||
|
is_admin = Column(Boolean, default=False)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
# Связь с результатами тестов
|
||||||
|
test_results = relationship("TestResult", back_populates="user")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<User(id={self.id}, telegram_id={self.telegram_id}, username={self.username})>"
|
||||||
|
|
||||||
|
class Message(Base):
|
||||||
|
"""Модель сообщений"""
|
||||||
|
|
||||||
|
__tablename__ = "messages"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
telegram_message_id = Column(BigInteger, nullable=False)
|
||||||
|
user_id = Column(Integer, nullable=False) # Внешний ключ на users.id
|
||||||
|
text = Column(Text, nullable=True)
|
||||||
|
message_type = Column(String(50), default='text') # text, photo, document, etc.
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Message(id={self.id}, user_id={self.user_id}, type={self.message_type})>"
|
||||||
|
|
||||||
|
class BotSettings(Base):
|
||||||
|
"""Модель настроек бота"""
|
||||||
|
|
||||||
|
__tablename__ = "bot_settings"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
key = Column(String(255), unique=True, nullable=False)
|
||||||
|
value = Column(Text, nullable=True)
|
||||||
|
description = Column(Text, nullable=True)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<BotSettings(key={self.key}, value={self.value})>"
|
||||||
|
|
||||||
|
|
||||||
|
# ===== МОДЕЛИ ДЛЯ СИСТЕМЫ ТЕСТИРОВАНИЯ =====
|
||||||
|
|
||||||
|
class TestCategory(Base):
|
||||||
|
"""Категории тестов"""
|
||||||
|
|
||||||
|
__tablename__ = "test_categories"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
name = Column(String(255), unique=True, nullable=False) # "Тест Климова", "Профориентация" и т.д.
|
||||||
|
description = Column(Text, nullable=True)
|
||||||
|
is_active = Column(Boolean, default=True)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
# Связь с тестами
|
||||||
|
tests = relationship("Test", back_populates="category")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<TestCategory(id={self.id}, name={self.name})>"
|
||||||
|
|
||||||
|
|
||||||
|
class Test(Base):
|
||||||
|
"""Конкретные тесты"""
|
||||||
|
|
||||||
|
__tablename__ = "tests"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
category_id = Column(Integer, ForeignKey("test_categories.id"), nullable=False)
|
||||||
|
title = Column(String(255), nullable=False) # "📘 Тест Климова"
|
||||||
|
description = Column(Text, nullable=True)
|
||||||
|
instructions = Column(Text, nullable=True) # Инструкции перед тестом
|
||||||
|
is_active = Column(Boolean, default=True)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
# Связи
|
||||||
|
category = relationship("TestCategory", back_populates="tests")
|
||||||
|
questions = relationship("Question", back_populates="test", cascade="all, delete-orphan")
|
||||||
|
user_results = relationship("TestResult", back_populates="test")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Test(id={self.id}, title={self.title})>"
|
||||||
|
|
||||||
|
|
||||||
|
class Image(Base):
|
||||||
|
"""Хранение изображений в базе данных"""
|
||||||
|
|
||||||
|
__tablename__ = "images"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
filename = Column(String(255), nullable=False) # Оригинальное имя файла
|
||||||
|
content_type = Column(String(100), nullable=False) # image/jpeg, image/png и т.д.
|
||||||
|
file_size = Column(Integer, nullable=False) # Размер в байтах
|
||||||
|
image_data = Column(LargeBinary, nullable=False) # Бинарные данные изображения
|
||||||
|
alt_text = Column(String(255), nullable=True) # Альтернативный текст для доступности
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
# Связь с вопросами
|
||||||
|
questions = relationship("Question", back_populates="image")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Image(id={self.id}, filename={self.filename}, size={self.file_size})>"
|
||||||
|
|
||||||
|
|
||||||
|
class Question(Base):
|
||||||
|
"""Вопросы тестов"""
|
||||||
|
|
||||||
|
__tablename__ = "questions"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
test_id = Column(Integer, ForeignKey("tests.id"), nullable=False)
|
||||||
|
order_index = Column(Integer, nullable=False) # Порядок вопроса в тесте
|
||||||
|
question_text = Column(Text, nullable=True) # Текст вопроса (может быть пустым если только картинка)
|
||||||
|
image_id = Column(Integer, ForeignKey("images.id"), nullable=True) # Привязка к изображению
|
||||||
|
is_active = Column(Boolean, default=True)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
# Связи
|
||||||
|
test = relationship("Test", back_populates="questions")
|
||||||
|
image = relationship("Image", back_populates="questions")
|
||||||
|
answer_options = relationship("AnswerOption", back_populates="question", cascade="all, delete-orphan")
|
||||||
|
user_answers = relationship("UserAnswer", back_populates="question")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Question(id={self.id}, test_id={self.test_id}, order={self.order_index})>"
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerOption(Base):
|
||||||
|
"""Варианты ответов на вопросы"""
|
||||||
|
|
||||||
|
__tablename__ = "answer_options"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
question_id = Column(Integer, ForeignKey("questions.id"), nullable=False)
|
||||||
|
option_text = Column(Text, nullable=False) # Текст варианта ответа
|
||||||
|
category = Column(String(255), nullable=True) # Категория для подсчета результатов (например, "Человек-природа")
|
||||||
|
order_index = Column(Integer, nullable=False) # Порядок варианта (1, 2, 3...)
|
||||||
|
is_correct = Column(Boolean, default=False) # Для тестов с правильными ответами
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
# Связи
|
||||||
|
question = relationship("Question", back_populates="answer_options")
|
||||||
|
user_answers = relationship("UserAnswer", back_populates="selected_option")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<AnswerOption(id={self.id}, question_id={self.question_id}, text='{self.option_text[:50]}')>"
|
||||||
|
|
||||||
|
|
||||||
|
class TestResult(Base):
|
||||||
|
"""Результаты прохождения тестов пользователями"""
|
||||||
|
|
||||||
|
__tablename__ = "test_results"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
test_id = Column(Integer, ForeignKey("tests.id"), nullable=False)
|
||||||
|
started_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
is_completed = Column(Boolean, default=False)
|
||||||
|
result_data = Column(Text, nullable=True) # JSON с результатами по категориям
|
||||||
|
|
||||||
|
# Связи
|
||||||
|
user = relationship("User", back_populates="test_results")
|
||||||
|
test = relationship("Test", back_populates="user_results")
|
||||||
|
answers = relationship("UserAnswer", back_populates="test_result", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<TestResult(id={self.id}, user_id={self.user_id}, test_id={self.test_id}, completed={self.is_completed})>"
|
||||||
|
|
||||||
|
|
||||||
|
class UserAnswer(Base):
|
||||||
|
"""Ответы пользователей на конкретные вопросы"""
|
||||||
|
|
||||||
|
__tablename__ = "user_answers"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
test_result_id = Column(Integer, ForeignKey("test_results.id"), nullable=False)
|
||||||
|
question_id = Column(Integer, ForeignKey("questions.id"), nullable=False)
|
||||||
|
selected_option_id = Column(Integer, ForeignKey("answer_options.id"), nullable=False)
|
||||||
|
answered_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
# Связи
|
||||||
|
test_result = relationship("TestResult", back_populates="answers")
|
||||||
|
question = relationship("Question", back_populates="user_answers")
|
||||||
|
selected_option = relationship("AnswerOption", back_populates="user_answers")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<UserAnswer(id={self.id}, question_id={self.question_id}, option_id={self.selected_option_id})>"
|
||||||
185
database/repositories.py
Normal file
185
database/repositories.py
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
"""
|
||||||
|
Репозитории для работы с моделями базы данных системы тестирования
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional, List
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import and_, or_
|
||||||
|
from database.models import (
|
||||||
|
User, Message, BotSettings, Test, TestCategory, Question,
|
||||||
|
AnswerOption, TestResult, UserAnswer, Image
|
||||||
|
)
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class UserRepository:
|
||||||
|
"""Репозиторий для работы с пользователями"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_user(session: Session, telegram_id: int, username: str = None,
|
||||||
|
first_name: str = None, last_name: str = None) -> User:
|
||||||
|
"""Создание нового пользователя"""
|
||||||
|
user = User(
|
||||||
|
telegram_id=telegram_id,
|
||||||
|
username=username,
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name
|
||||||
|
)
|
||||||
|
session.add(user)
|
||||||
|
session.flush() # Получаем ID без коммита
|
||||||
|
return user
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_user_by_telegram_id(session: Session, telegram_id: int) -> Optional[User]:
|
||||||
|
"""Получение пользователя по Telegram ID"""
|
||||||
|
return session.query(User).filter(User.telegram_id == telegram_id).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_user_by_id(session: Session, user_id: int) -> Optional[User]:
|
||||||
|
"""Получение пользователя по ID"""
|
||||||
|
return session.query(User).filter(User.id == user_id).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all_users(session: Session, active_only: bool = True) -> List[User]:
|
||||||
|
"""Получение всех пользователей"""
|
||||||
|
query = session.query(User)
|
||||||
|
if active_only:
|
||||||
|
query = query.filter(User.is_active == True)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_user(session: Session, telegram_id: int, **kwargs) -> Optional[User]:
|
||||||
|
"""Обновление данных пользователя"""
|
||||||
|
user = UserRepository.get_user_by_telegram_id(session, telegram_id)
|
||||||
|
if user:
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
if hasattr(user, key):
|
||||||
|
setattr(user, key, value)
|
||||||
|
session.flush()
|
||||||
|
return user
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_or_create_user(session: Session, telegram_id: int, **kwargs) -> tuple[User, bool]:
|
||||||
|
"""Получение или создание пользователя. Возвращает (user, created)"""
|
||||||
|
user = UserRepository.get_user_by_telegram_id(session, telegram_id)
|
||||||
|
if user:
|
||||||
|
return user, False
|
||||||
|
else:
|
||||||
|
user = UserRepository.create_user(session, telegram_id, **kwargs)
|
||||||
|
return user, True
|
||||||
|
|
||||||
|
class MessageRepository:
|
||||||
|
"""Репозиторий для работы с сообщениями"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_message(session: Session, telegram_message_id: int, user_id: int,
|
||||||
|
text: str = None, message_type: str = 'text') -> Message:
|
||||||
|
"""Создание нового сообщения"""
|
||||||
|
message = Message(
|
||||||
|
telegram_message_id=telegram_message_id,
|
||||||
|
user_id=user_id,
|
||||||
|
text=text,
|
||||||
|
message_type=message_type
|
||||||
|
)
|
||||||
|
session.add(message)
|
||||||
|
session.flush()
|
||||||
|
return message
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_messages_by_user(session: Session, user_id: int, limit: int = 100) -> List[Message]:
|
||||||
|
"""Получение сообщений пользователя"""
|
||||||
|
return (session.query(Message)
|
||||||
|
.filter(Message.user_id == user_id)
|
||||||
|
.order_by(Message.created_at.desc())
|
||||||
|
.limit(limit)
|
||||||
|
.all())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_recent_messages(session: Session, limit: int = 100) -> List[Message]:
|
||||||
|
"""Получение последних сообщений"""
|
||||||
|
return (session.query(Message)
|
||||||
|
.order_by(Message.created_at.desc())
|
||||||
|
.limit(limit)
|
||||||
|
.all())
|
||||||
|
|
||||||
|
class SettingsRepository:
|
||||||
|
"""Репозиторий для работы с настройками бота"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_setting(session: Session, key: str) -> Optional[str]:
|
||||||
|
"""Получение значения настройки"""
|
||||||
|
setting = session.query(BotSettings).filter(BotSettings.key == key).first()
|
||||||
|
return setting.value if setting else None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_setting(session: Session, key: str, value: str, description: str = None) -> BotSettings:
|
||||||
|
"""Установка значения настройки"""
|
||||||
|
setting = session.query(BotSettings).filter(BotSettings.key == key).first()
|
||||||
|
if setting:
|
||||||
|
setting.value = value
|
||||||
|
if description:
|
||||||
|
setting.description = description
|
||||||
|
else:
|
||||||
|
setting = BotSettings(key=key, value=value, description=description)
|
||||||
|
session.add(setting)
|
||||||
|
session.flush()
|
||||||
|
return setting
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all_settings(session: Session) -> List[BotSettings]:
|
||||||
|
"""Получение всех настроек"""
|
||||||
|
return session.query(BotSettings).all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_setting(session: Session, key: str) -> bool:
|
||||||
|
"""Удаление настройки"""
|
||||||
|
setting = session.query(BotSettings).filter(BotSettings.key == key).first()
|
||||||
|
if setting:
|
||||||
|
session.delete(setting)
|
||||||
|
session.flush()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class ImageRepository:
|
||||||
|
"""Репозиторий для работы с изображениями"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_image(session: Session, filename: str, content_type: str,
|
||||||
|
image_data: bytes, alt_text: str = None) -> Image:
|
||||||
|
"""Создание записи изображения"""
|
||||||
|
image = Image(
|
||||||
|
filename=filename,
|
||||||
|
content_type=content_type,
|
||||||
|
file_size=len(image_data),
|
||||||
|
image_data=image_data,
|
||||||
|
alt_text=alt_text
|
||||||
|
)
|
||||||
|
session.add(image)
|
||||||
|
session.flush()
|
||||||
|
return image
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_image_by_id(session: Session, image_id: int) -> Optional[Image]:
|
||||||
|
"""Получение изображения по ID"""
|
||||||
|
return session.query(Image).filter(Image.id == image_id).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_image_by_filename(session: Session, filename: str) -> Optional[Image]:
|
||||||
|
"""Получение изображения по имени файла"""
|
||||||
|
return session.query(Image).filter(Image.filename == filename).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all_images(session: Session) -> List[Image]:
|
||||||
|
"""Получение всех изображений"""
|
||||||
|
return session.query(Image).all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_image(session: Session, image_id: int) -> bool:
|
||||||
|
"""Удаление изображения"""
|
||||||
|
image = session.query(Image).filter(Image.id == image_id).first()
|
||||||
|
if image:
|
||||||
|
session.delete(image)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
6
database/requirements.txt
Normal file
6
database/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Необходимые зависимости для работы с PostgreSQL и SQLAlchemy
|
||||||
|
|
||||||
|
sqlalchemy>=2.0.0
|
||||||
|
psycopg2-binary>=2.9.0 # PostgreSQL адаптер для Python
|
||||||
|
python-dotenv>=1.0.0 # Для работы с переменными окружения
|
||||||
|
alembic>=1.12.0 # Для миграций базы данных (опционально)
|
||||||
358
database/services.py
Normal file
358
database/services.py
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
"""
|
||||||
|
Сервисы для работы с системой тестирования
|
||||||
|
Основная бизнес-логика для получения вопросов, сохранения ответов и работы с изображениями
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import io
|
||||||
|
from typing import List, Optional, Dict, Any, Tuple
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import and_
|
||||||
|
|
||||||
|
from database.models import (
|
||||||
|
User, Test, TestCategory, Question, AnswerOption,
|
||||||
|
TestResult, UserAnswer, Image
|
||||||
|
)
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageService:
|
||||||
|
"""Сервис для работы с изображениями в базе данных"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_image_to_db(
|
||||||
|
db: Session,
|
||||||
|
filename: str,
|
||||||
|
content_type: str,
|
||||||
|
image_data: bytes,
|
||||||
|
alt_text: str = None
|
||||||
|
) -> Image:
|
||||||
|
"""
|
||||||
|
Сохранение изображения в базу данных
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Сессия базы данных
|
||||||
|
filename: Имя файла
|
||||||
|
content_type: MIME тип (image/jpeg, image/png и т.д.)
|
||||||
|
image_data: Бинарные данные изображения
|
||||||
|
alt_text: Альтернативный текст
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Image: Созданная запись изображения
|
||||||
|
"""
|
||||||
|
image = Image(
|
||||||
|
filename=filename,
|
||||||
|
content_type=content_type,
|
||||||
|
file_size=len(image_data),
|
||||||
|
image_data=image_data,
|
||||||
|
alt_text=alt_text
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(image)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(image)
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_image_data(db: Session, image_id: int) -> Optional[Tuple[bytes, str, str]]:
|
||||||
|
"""
|
||||||
|
Получение данных изображения из БД
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Сессия базы данных
|
||||||
|
image_id: ID изображения
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[bytes, str, str]: (image_data, content_type, filename) или None
|
||||||
|
"""
|
||||||
|
image = db.query(Image).filter(Image.id == image_id).first()
|
||||||
|
|
||||||
|
if image:
|
||||||
|
return image.image_data, image.content_type, image.filename
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_image_by_filename(db: Session, filename: str) -> Optional[Image]:
|
||||||
|
"""Получение изображения по имени файла"""
|
||||||
|
return db.query(Image).filter(Image.filename == filename).first()
|
||||||
|
|
||||||
|
|
||||||
|
class TestService:
|
||||||
|
"""Сервис для работы с тестами"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_available_tests(db: Session) -> List[Test]:
|
||||||
|
"""Получение всех активных тестов"""
|
||||||
|
return db.query(Test).filter(Test.is_active == True).all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_test_by_title(db: Session, title: str) -> Optional[Test]:
|
||||||
|
"""Получение теста по названию (например, '📘 Тест Климова')"""
|
||||||
|
return db.query(Test).filter(
|
||||||
|
and_(Test.title == title, Test.is_active == True)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_test_questions(db: Session, test_id: int) -> List[Question]:
|
||||||
|
"""
|
||||||
|
Получение всех вопросов теста в правильном порядке
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Сессия базы данных
|
||||||
|
test_id: ID теста
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Question]: Список вопросов с загруженными связями
|
||||||
|
"""
|
||||||
|
return db.query(Question).filter(
|
||||||
|
and_(Question.test_id == test_id, Question.is_active == True)
|
||||||
|
).order_by(Question.order_index).all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def start_test(db: Session, user_id: int, test_id: int) -> TestResult:
|
||||||
|
"""
|
||||||
|
Начало прохождения теста пользователем
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Сессия базы данных
|
||||||
|
user_id: ID пользователя
|
||||||
|
test_id: ID теста
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TestResult: Созданная запись результата теста
|
||||||
|
"""
|
||||||
|
# Проверяем, есть ли незавершенный тест
|
||||||
|
existing_result = db.query(TestResult).filter(
|
||||||
|
and_(
|
||||||
|
TestResult.user_id == user_id,
|
||||||
|
TestResult.test_id == test_id,
|
||||||
|
TestResult.is_completed == False
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_result:
|
||||||
|
return existing_result
|
||||||
|
|
||||||
|
# Создаем новый результат теста
|
||||||
|
test_result = TestResult(
|
||||||
|
user_id=user_id,
|
||||||
|
test_id=test_id,
|
||||||
|
is_completed=False
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(test_result)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(test_result)
|
||||||
|
|
||||||
|
return test_result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_answer(
|
||||||
|
db: Session,
|
||||||
|
test_result_id: int,
|
||||||
|
question_id: int,
|
||||||
|
selected_option_id: int
|
||||||
|
) -> UserAnswer:
|
||||||
|
"""
|
||||||
|
Сохранение ответа пользователя на вопрос
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Сессия базы данных
|
||||||
|
test_result_id: ID результата теста
|
||||||
|
question_id: ID вопроса
|
||||||
|
selected_option_id: ID выбранного варианта ответа
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
UserAnswer: Созданная запись ответа
|
||||||
|
"""
|
||||||
|
# Проверяем, есть ли уже ответ на этот вопрос
|
||||||
|
existing_answer = db.query(UserAnswer).filter(
|
||||||
|
and_(
|
||||||
|
UserAnswer.test_result_id == test_result_id,
|
||||||
|
UserAnswer.question_id == question_id
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_answer:
|
||||||
|
# Обновляем существующий ответ
|
||||||
|
existing_answer.selected_option_id = selected_option_id
|
||||||
|
db.commit()
|
||||||
|
return existing_answer
|
||||||
|
|
||||||
|
# Создаем новый ответ
|
||||||
|
user_answer = UserAnswer(
|
||||||
|
test_result_id=test_result_id,
|
||||||
|
question_id=question_id,
|
||||||
|
selected_option_id=selected_option_id
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(user_answer)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(user_answer)
|
||||||
|
|
||||||
|
return user_answer
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def complete_test(db: Session, test_result_id: int) -> TestResult:
|
||||||
|
"""
|
||||||
|
Завершение теста и подсчет результатов
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Сессия базы данных
|
||||||
|
test_result_id: ID результата теста
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TestResult: Обновленная запись результата с подсчитанными данными
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
test_result = db.query(TestResult).filter(
|
||||||
|
TestResult.id == test_result_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not test_result:
|
||||||
|
raise ValueError(f"TestResult с ID {test_result_id} не найден")
|
||||||
|
|
||||||
|
# Получаем все ответы пользователя для этого теста
|
||||||
|
answers = db.query(UserAnswer).filter(
|
||||||
|
UserAnswer.test_result_id == test_result_id
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# Подсчитываем результаты по категориям
|
||||||
|
category_scores = {}
|
||||||
|
|
||||||
|
for answer in answers:
|
||||||
|
option = db.query(AnswerOption).filter(
|
||||||
|
AnswerOption.id == answer.selected_option_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if option and option.category:
|
||||||
|
category = option.category
|
||||||
|
category_scores[category] = category_scores.get(category, 0) + 1
|
||||||
|
|
||||||
|
# Сохраняем результаты в JSON формате
|
||||||
|
test_result.result_data = json.dumps(category_scores, ensure_ascii=False)
|
||||||
|
test_result.completed_at = datetime.now()
|
||||||
|
test_result.is_completed = True
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(test_result)
|
||||||
|
|
||||||
|
return test_result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_test_results(db: Session, test_result_id: int) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Получение и форматирование результатов теста
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Сессия базы данных
|
||||||
|
test_result_id: ID результата теста
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: Словарь с результатами и процентами
|
||||||
|
"""
|
||||||
|
test_result = db.query(TestResult).filter(
|
||||||
|
TestResult.id == test_result_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not test_result or not test_result.result_data:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Парсим результаты
|
||||||
|
category_scores = json.loads(test_result.result_data)
|
||||||
|
|
||||||
|
# Получаем максимальные возможные баллы для каждой категории
|
||||||
|
questions = TestService.get_test_questions(db, test_result.test_id)
|
||||||
|
max_scores = {}
|
||||||
|
|
||||||
|
for question in questions:
|
||||||
|
for option in question.answer_options:
|
||||||
|
if option.category:
|
||||||
|
max_scores[option.category] = max_scores.get(option.category, 0) + 1
|
||||||
|
|
||||||
|
# Вычисляем проценты
|
||||||
|
results = {}
|
||||||
|
for category, score in category_scores.items():
|
||||||
|
max_score = max_scores.get(category, 1)
|
||||||
|
percentage = (score / max_score) * 100
|
||||||
|
results[category] = {
|
||||||
|
'score': score,
|
||||||
|
'max_score': max_score,
|
||||||
|
'percentage': round(percentage, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_user_test_history(db: Session, user_id: int) -> List[TestResult]:
|
||||||
|
"""Получение истории тестов пользователя"""
|
||||||
|
return db.query(TestResult).filter(
|
||||||
|
and_(TestResult.user_id == user_id, TestResult.is_completed == True)
|
||||||
|
).order_by(TestResult.completed_at.desc()).all()
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionService:
|
||||||
|
"""Сервис для работы с вопросами"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_question_with_options(db: Session, question_id: int) -> Optional[Question]:
|
||||||
|
"""
|
||||||
|
Получение вопроса со всеми вариантами ответов и изображением
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Сессия базы данных
|
||||||
|
question_id: ID вопроса
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Question: Вопрос с загруженными связями или None
|
||||||
|
"""
|
||||||
|
return db.query(Question).filter(Question.id == question_id).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_question_for_bot(db: Session, question: Question) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Форматирование вопроса для отправки в бот
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: Сессия базы данных
|
||||||
|
question: Объект вопроса
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: Сформатированные данные вопроса
|
||||||
|
"""
|
||||||
|
# Получаем варианты ответов
|
||||||
|
options = db.query(AnswerOption).filter(
|
||||||
|
AnswerOption.question_id == question.id
|
||||||
|
).order_by(AnswerOption.order_index).all()
|
||||||
|
|
||||||
|
# Получаем данные изображения, если есть
|
||||||
|
image_data = None
|
||||||
|
if question.image_id:
|
||||||
|
image_info = ImageService.get_image_data(db, question.image_id)
|
||||||
|
if image_info:
|
||||||
|
image_data = {
|
||||||
|
'data': image_info[0],
|
||||||
|
'content_type': image_info[1],
|
||||||
|
'filename': image_info[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': question.id,
|
||||||
|
'text': question.question_text,
|
||||||
|
'order_index': question.order_index,
|
||||||
|
'image': image_data,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'id': option.id,
|
||||||
|
'text': option.option_text,
|
||||||
|
'category': option.category,
|
||||||
|
'order_index': option.order_index
|
||||||
|
}
|
||||||
|
for option in options
|
||||||
|
]
|
||||||
|
}
|
||||||
20
docker-compose.dev.yml
Normal file
20
docker-compose.dev.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Docker Compose override для разработки
|
||||||
|
# Используйте: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
# В режиме разработки монтируем исходный код для hot reload
|
||||||
|
volumes:
|
||||||
|
- ./api:/app/api
|
||||||
|
- ./database:/app/database
|
||||||
|
- ./run_api.py:/app/run_api.py
|
||||||
|
|
||||||
|
# Включаем автоперезагрузку и отладку в dev режиме
|
||||||
|
environment:
|
||||||
|
API_RELOAD: "true"
|
||||||
|
LOG_LEVEL: DEBUG
|
||||||
|
|
||||||
|
# Переопределяем команду для dev режима с автоперезагрузкой
|
||||||
|
command: ["python", "-m", "uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||||
51
docker-compose.yml
Normal file
51
docker-compose.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# База данных PostgreSQL (отдельный сервис)
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
container_name: telegram_bot_db
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: telegram_bot
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
networks:
|
||||||
|
- bot_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -h localhost -p 5432 -q -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Единое приложение (API + database логика в одном контейнере)
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
container_name: telegram_bot_app
|
||||||
|
environment:
|
||||||
|
# Настройки подключения к внешней PostgreSQL
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: telegram_bot
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASSWORD: password
|
||||||
|
|
||||||
|
# Настройки API сервера
|
||||||
|
API_HOST: 0.0.0.0
|
||||||
|
API_PORT: 8000
|
||||||
|
API_RELOAD: "false"
|
||||||
|
LOG_LEVEL: INFO
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- bot_network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|
||||||
32
init.sql
Normal file
32
init.sql
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
-- Скрипт инициализации базы данных для Telegram Bot API
|
||||||
|
-- Этот файл автоматически выполняется при первом запуске PostgreSQL контейнера
|
||||||
|
|
||||||
|
-- Создание базы данных (если еще не создана)
|
||||||
|
-- В docker-compose база уже создается через POSTGRES_DB
|
||||||
|
|
||||||
|
-- Создание пользователя для приложения (если нужен отдельный пользователь)
|
||||||
|
-- DO $$
|
||||||
|
-- BEGIN
|
||||||
|
-- IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'bot_user') THEN
|
||||||
|
-- CREATE USER bot_user WITH PASSWORD 'bot_password';
|
||||||
|
-- GRANT CONNECT ON DATABASE telegram_bot TO bot_user;
|
||||||
|
-- GRANT USAGE ON SCHEMA public TO bot_user;
|
||||||
|
-- GRANT CREATE ON SCHEMA public TO bot_user;
|
||||||
|
-- END IF;
|
||||||
|
-- END
|
||||||
|
-- $$;
|
||||||
|
|
||||||
|
-- Подключаемся к нужной базе данных
|
||||||
|
\c telegram_bot;
|
||||||
|
|
||||||
|
-- Создаем расширения если нужны
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- Примечание: Таблицы будут созданы автоматически через SQLAlchemy
|
||||||
|
-- при первом запуске API приложения
|
||||||
|
|
||||||
|
-- Можно добавить начальные данные, если нужно
|
||||||
|
-- INSERT INTO bot_settings (key, value, description) VALUES
|
||||||
|
-- ('welcome_message', 'Добро пожаловать в наш бот!', 'Приветственное сообщение для новых пользователей'),
|
||||||
|
-- ('max_messages_per_day', '100', 'Максимальное количество сообщений от пользователя в день')
|
||||||
|
-- ON CONFLICT (key) DO NOTHING;
|
||||||
6
main.py
Normal file
6
main.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
def main():
|
||||||
|
print("Hello from bot-telegram!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[project]
|
||||||
|
name = "bot-telegram"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Telegram bot with FastAPI backend"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi>=0.104.1",
|
||||||
|
"uvicorn[standard]>=0.24.0",
|
||||||
|
"sqlalchemy>=2.0.23",
|
||||||
|
"psycopg2-binary>=2.9.7",
|
||||||
|
"alembic>=1.12.1",
|
||||||
|
"pydantic>=2.5.0",
|
||||||
|
"python-multipart>=0.0.6",
|
||||||
|
"python-dotenv>=1.0.0",
|
||||||
|
]
|
||||||
571
uv.lock
generated
Normal file
571
uv.lock
generated
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 2
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alembic"
|
||||||
|
version = "1.16.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "mako" },
|
||||||
|
{ name = "sqlalchemy" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9a/ca/4dc52902cf3491892d464f5265a81e9dff094692c8a049a3ed6a05fe7ee8/alembic-1.16.5.tar.gz", hash = "sha256:a88bb7f6e513bd4301ecf4c7f2206fe93f9913f9b48dac3b78babde2d6fe765e", size = 1969868, upload-time = "2025-08-27T18:02:05.668Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/4a/4c61d4c84cfd9befb6fa08a702535b27b21fff08c946bc2f6139decbf7f7/alembic-1.16.5-py3-none-any.whl", hash = "sha256:e845dfe090c5ffa7b92593ae6687c5cb1a101e91fa53868497dbd79847f9dbe3", size = 247355, upload-time = "2025-08-27T18:02:07.37Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-types"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.10.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "sniffio" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bot-telegram"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "alembic" },
|
||||||
|
{ name = "fastapi" },
|
||||||
|
{ name = "psycopg2-binary" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "python-multipart" },
|
||||||
|
{ name = "sqlalchemy" },
|
||||||
|
{ name = "uvicorn", extra = ["standard"] },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "alembic", specifier = ">=1.12.1" },
|
||||||
|
{ name = "fastapi", specifier = ">=0.104.1" },
|
||||||
|
{ name = "psycopg2-binary", specifier = ">=2.9.7" },
|
||||||
|
{ name = "pydantic", specifier = ">=2.5.0" },
|
||||||
|
{ name = "python-dotenv", specifier = ">=1.0.0" },
|
||||||
|
{ name = "python-multipart", specifier = ">=0.0.6" },
|
||||||
|
{ name = "sqlalchemy", specifier = ">=2.0.23" },
|
||||||
|
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.2.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastapi"
|
||||||
|
version = "0.116.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "starlette" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485, upload-time = "2025-07-11T16:22:32.057Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "greenlet"
|
||||||
|
version = "3.2.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h11"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httptools"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683, upload-time = "2024-10-16T19:44:30.175Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337, upload-time = "2024-10-16T19:44:31.786Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796, upload-time = "2024-10-16T19:44:32.825Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837, upload-time = "2024-10-16T19:44:33.974Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289, upload-time = "2024-10-16T19:44:35.111Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779, upload-time = "2024-10-16T19:44:36.253Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634, upload-time = "2024-10-16T19:44:37.357Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload-time = "2024-10-16T19:44:38.738Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload-time = "2024-10-16T19:44:39.818Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload-time = "2024-10-16T19:44:41.189Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805, upload-time = "2024-10-16T19:44:42.384Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858, upload-time = "2024-10-16T19:44:43.959Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042, upload-time = "2024-10-16T19:44:45.071Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload-time = "2024-10-16T19:44:46.46Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.10"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mako"
|
||||||
|
version = "1.3.10"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markupsafe" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markupsafe"
|
||||||
|
version = "3.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg2-binary"
|
||||||
|
version = "2.9.10"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764, upload-time = "2024-10-16T11:24:58.126Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/7d/465cc9795cf76f6d329efdafca74693714556ea3891813701ac1fee87545/psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", size = 3044771, upload-time = "2024-10-16T11:20:35.234Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/31/6d225b7b641a1a2148e3ed65e1aa74fc86ba3fee850545e27be9e1de893d/psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", size = 3275336, upload-time = "2024-10-16T11:20:38.742Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/b7/a68c2b4bff1cbb1728e3ec864b2d92327c77ad52edcd27922535a8366f68/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", size = 2851637, upload-time = "2024-10-16T11:20:42.145Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/b1/cfedc0e0e6f9ad61f8657fd173b2f831ce261c02a08c0b09c652b127d813/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", size = 3082097, upload-time = "2024-10-16T11:20:46.185Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/ed/0a8e4153c9b769f59c02fb5e7914f20f0b2483a19dae7bf2db54b743d0d0/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", size = 3264776, upload-time = "2024-10-16T11:20:50.879Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/db/d09da68c6a0cdab41566b74e0a6068a425f077169bed0946559b7348ebe9/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", size = 3020968, upload-time = "2024-10-16T11:20:56.819Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/28/4d6f8c255f0dfffb410db2b3f9ac5218d959a66c715c34cac31081e19b95/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", size = 2872334, upload-time = "2024-10-16T11:21:02.411Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/f7/20d7bf796593c4fea95e12119d6cc384ff1f6141a24fbb7df5a668d29d29/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", size = 2822722, upload-time = "2024-10-16T11:21:09.01Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/e4/0c407ae919ef626dbdb32835a03b6737013c3cc7240169843965cada2bdf/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", size = 2920132, upload-time = "2024-10-16T11:21:16.339Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312, upload-time = "2024-10-16T11:21:25.584Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191, upload-time = "2024-10-16T11:21:29.912Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031, upload-time = "2024-10-16T11:21:34.211Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699, upload-time = "2024-10-16T11:21:42.841Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245, upload-time = "2024-10-16T11:21:51.989Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631, upload-time = "2024-10-16T11:21:57.584Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140, upload-time = "2024-10-16T11:22:02.005Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762, upload-time = "2024-10-16T11:22:06.412Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967, upload-time = "2024-10-16T11:22:11.583Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326, upload-time = "2024-10-16T11:22:16.406Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712, upload-time = "2024-10-16T11:22:21.366Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155, upload-time = "2024-10-16T11:22:25.684Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356, upload-time = "2024-10-16T11:22:30.562Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224, upload-time = "2025-01-04T20:09:19.234Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic"
|
||||||
|
version = "2.11.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-types" },
|
||||||
|
{ name = "pydantic-core" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-core"
|
||||||
|
version = "2.33.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-multipart"
|
||||||
|
version = "0.0.20"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sniffio"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlalchemy"
|
||||||
|
version = "2.0.43"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/db/20c78f1081446095450bdc6ee6cc10045fce67a8e003a5876b6eaafc5cc4/sqlalchemy-2.0.43-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:20d81fc2736509d7a2bd33292e489b056cbae543661bb7de7ce9f1c0cd6e7f24", size = 2134891, upload-time = "2025-08-11T15:51:13.019Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/0a/3d89034ae62b200b4396f0f95319f7d86e9945ee64d2343dcad857150fa2/sqlalchemy-2.0.43-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b9fc27650ff5a2c9d490c13c14906b918b0de1f8fcbb4c992712d8caf40e83", size = 2123061, upload-time = "2025-08-11T15:51:14.319Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/10/2711f7ff1805919221ad5bee205971254845c069ee2e7036847103ca1e4c/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6772e3ca8a43a65a37c88e2f3e2adfd511b0b1da37ef11ed78dea16aeae85bd9", size = 3320384, upload-time = "2025-08-11T15:52:35.088Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/0e/3d155e264d2ed2778484006ef04647bc63f55b3e2d12e6a4f787747b5900/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a113da919c25f7f641ffbd07fbc9077abd4b3b75097c888ab818f962707eb48", size = 3329648, upload-time = "2025-08-11T15:56:34.153Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/81/635100fb19725c931622c673900da5efb1595c96ff5b441e07e3dd61f2be/sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4286a1139f14b7d70141c67a8ae1582fc2b69105f1b09d9573494eb4bb4b2687", size = 3258030, upload-time = "2025-08-11T15:52:36.933Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/ed/a99302716d62b4965fded12520c1cbb189f99b17a6d8cf77611d21442e47/sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:529064085be2f4d8a6e5fab12d36ad44f1909a18848fcfbdb59cc6d4bbe48efe", size = 3294469, upload-time = "2025-08-11T15:56:35.553Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/a2/3a11b06715149bf3310b55a98b5c1e84a42cfb949a7b800bc75cb4e33abc/sqlalchemy-2.0.43-cp312-cp312-win32.whl", hash = "sha256:b535d35dea8bbb8195e7e2b40059e2253acb2b7579b73c1b432a35363694641d", size = 2098906, upload-time = "2025-08-11T15:55:00.645Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/09/405c915a974814b90aa591280623adc6ad6b322f61fd5cff80aeaef216c9/sqlalchemy-2.0.43-cp312-cp312-win_amd64.whl", hash = "sha256:1c6d85327ca688dbae7e2b06d7d84cfe4f3fffa5b5f9e21bb6ce9d0e1a0e0e0a", size = 2126260, upload-time = "2025-08-11T15:55:02.965Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/1c/a7260bd47a6fae7e03768bf66451437b36451143f36b285522b865987ced/sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", size = 2130598, upload-time = "2025-08-11T15:51:15.903Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/84/8a337454e82388283830b3586ad7847aa9c76fdd4f1df09cdd1f94591873/sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", size = 2118415, upload-time = "2025-08-11T15:51:17.256Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/ff/22ab2328148492c4d71899d62a0e65370ea66c877aea017a244a35733685/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", size = 3248707, upload-time = "2025-08-11T15:52:38.444Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/29/11ae2c2b981de60187f7cbc84277d9d21f101093d1b2e945c63774477aba/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f", size = 3253602, upload-time = "2025-08-11T15:56:37.348Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/61/987b6c23b12c56d2be451bc70900f67dd7d989d52b1ee64f239cf19aec69/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738", size = 3183248, upload-time = "2025-08-11T15:52:39.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/85/29d216002d4593c2ce1c0ec2cec46dda77bfbcd221e24caa6e85eff53d89/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", size = 3219363, upload-time = "2025-08-11T15:56:39.11Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/e4/bd78b01919c524f190b4905d47e7630bf4130b9f48fd971ae1c6225b6f6a/sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", size = 2096718, upload-time = "2025-08-11T15:55:05.349Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/a5/ca2f07a2a201f9497de1928f787926613db6307992fe5cda97624eb07c2f/sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", size = 2123200, upload-time = "2025-08-11T15:55:07.932Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "starlette"
|
||||||
|
version = "0.47.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/15/b9/cc3017f9a9c9b6e27c5106cc10cc7904653c3eec0729793aec10479dd669/starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9", size = 2584144, upload-time = "2025-08-24T13:36:42.122Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991, upload-time = "2025-08-24T13:36:40.887Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-inspection"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uvicorn"
|
||||||
|
version = "0.35.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
standard = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "httptools" },
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
|
||||||
|
{ name = "watchfiles" },
|
||||||
|
{ name = "websockets" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uvloop"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284, upload-time = "2024-10-14T23:37:47.833Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349, upload-time = "2024-10-14T23:37:50.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089, upload-time = "2024-10-14T23:37:51.703Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770, upload-time = "2024-10-14T23:37:54.122Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321, upload-time = "2024-10-14T23:37:55.766Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022, upload-time = "2024-10-14T23:37:58.195Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload-time = "2024-10-14T23:38:06.385Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload-time = "2024-10-14T23:38:08.416Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "watchfiles"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406, upload-time = "2025-06-15T19:06:59.42Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/b8/858957045a38a4079203a33aaa7d23ea9269ca7761c8a074af3524fbb240/watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179", size = 402339, upload-time = "2025-06-15T19:05:24.516Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/28/98b222cca751ba68e88521fabd79a4fab64005fc5976ea49b53fa205d1fa/watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5", size = 394409, upload-time = "2025-06-15T19:05:25.469Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/50/dee79968566c03190677c26f7f47960aff738d32087087bdf63a5473e7df/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297", size = 450939, upload-time = "2025-06-15T19:05:26.494Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/45/a7b56fb129700f3cfe2594a01aa38d033b92a33dddce86c8dfdfc1247b72/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0", size = 457270, upload-time = "2025-06-15T19:05:27.466Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/c8/fa5ef9476b1d02dc6b5e258f515fcaaecf559037edf8b6feffcbc097c4b8/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e", size = 483370, upload-time = "2025-06-15T19:05:28.548Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/68/42cfcdd6533ec94f0a7aab83f759ec11280f70b11bfba0b0f885e298f9bd/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee", size = 598654, upload-time = "2025-06-15T19:05:29.997Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/74/b2a1544224118cc28df7e59008a929e711f9c68ce7d554e171b2dc531352/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd", size = 478667, upload-time = "2025-06-15T19:05:31.172Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/77/e3362fe308358dc9f8588102481e599c83e1b91c2ae843780a7ded939a35/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f", size = 452213, upload-time = "2025-06-15T19:05:32.299Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/17/c8f1a36540c9a1558d4faf08e909399e8133599fa359bf52ec8fcee5be6f/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4", size = 626718, upload-time = "2025-06-15T19:05:33.415Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/45/fb599be38b4bd38032643783d7496a26a6f9ae05dea1a42e58229a20ac13/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f", size = 623098, upload-time = "2025-06-15T19:05:34.534Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/e7/fdf40e038475498e160cd167333c946e45d8563ae4dd65caf757e9ffe6b4/watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd", size = 279209, upload-time = "2025-06-15T19:05:35.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/d3/3ae9d5124ec75143bdf088d436cba39812122edc47709cd2caafeac3266f/watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47", size = 292786, upload-time = "2025-06-15T19:05:36.559Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/2f/7dd4fc8b5f2b34b545e19629b4a018bfb1de23b3a496766a2c1165ca890d/watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6", size = 284343, upload-time = "2025-06-15T19:05:37.5Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/42/fae874df96595556a9089ade83be34a2e04f0f11eb53a8dbf8a8a5e562b4/watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30", size = 402004, upload-time = "2025-06-15T19:05:38.499Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/55/a77e533e59c3003d9803c09c44c3651224067cbe7fb5d574ddbaa31e11ca/watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a", size = 393671, upload-time = "2025-06-15T19:05:39.52Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/68/b0afb3f79c8e832e6571022611adbdc36e35a44e14f129ba09709aa4bb7a/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc", size = 449772, upload-time = "2025-06-15T19:05:40.897Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/05/46dd1f6879bc40e1e74c6c39a1b9ab9e790bf1f5a2fe6c08b463d9a807f4/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b", size = 456789, upload-time = "2025-06-15T19:05:42.045Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/ca/0eeb2c06227ca7f12e50a47a3679df0cd1ba487ea19cf844a905920f8e95/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895", size = 482551, upload-time = "2025-06-15T19:05:43.781Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/47/2cecbd8694095647406645f822781008cc524320466ea393f55fe70eed3b/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a", size = 597420, upload-time = "2025-06-15T19:05:45.244Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/7e/82abc4240e0806846548559d70f0b1a6dfdca75c1b4f9fa62b504ae9b083/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b", size = 477950, upload-time = "2025-06-15T19:05:46.332Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c", size = 451706, upload-time = "2025-06-15T19:05:47.459Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/b5/5516cf46b033192d544102ea07c65b6f770f10ed1d0a6d388f5d3874f6e4/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b", size = 625814, upload-time = "2025-06-15T19:05:48.654Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/dd/7c1331f902f30669ac3e754680b6edb9a0dd06dea5438e61128111fadd2c/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb", size = 622820, upload-time = "2025-06-15T19:05:50.088Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/14/36d7a8e27cd128d7b1009e7715a7c02f6c131be9d4ce1e5c3b73d0e342d8/watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9", size = 279194, upload-time = "2025-06-15T19:05:51.186Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/41/2dd88054b849aa546dbeef5696019c58f8e0774f4d1c42123273304cdb2e/watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7", size = 292349, upload-time = "2025-06-15T19:05:52.201Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/cf/421d659de88285eb13941cf11a81f875c176f76a6d99342599be88e08d03/watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5", size = 283836, upload-time = "2025-06-15T19:05:53.265Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/10/6faf6858d527e3599cc50ec9fcae73590fbddc1420bd4fdccfebffeedbc6/watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1", size = 400343, upload-time = "2025-06-15T19:05:54.252Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/20/5cb7d3966f5e8c718006d0e97dfe379a82f16fecd3caa7810f634412047a/watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339", size = 392916, upload-time = "2025-06-15T19:05:55.264Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/07/d8f1176328fa9e9581b6f120b017e286d2a2d22ae3f554efd9515c8e1b49/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633", size = 449582, upload-time = "2025-06-15T19:05:56.317Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/e8/80a14a453cf6038e81d072a86c05276692a1826471fef91df7537dba8b46/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011", size = 456752, upload-time = "2025-06-15T19:05:57.359Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/25/0853b3fe0e3c2f5af9ea60eb2e781eade939760239a72c2d38fc4cc335f6/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670", size = 481436, upload-time = "2025-06-15T19:05:58.447Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/9e/4af0056c258b861fbb29dcb36258de1e2b857be4a9509e6298abcf31e5c9/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf", size = 596016, upload-time = "2025-06-15T19:05:59.59Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/fa/95d604b58aa375e781daf350897aaaa089cff59d84147e9ccff2447c8294/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4", size = 476727, upload-time = "2025-06-15T19:06:01.086Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/95/fe479b2664f19be4cf5ceeb21be05afd491d95f142e72d26a42f41b7c4f8/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20", size = 451864, upload-time = "2025-06-15T19:06:02.144Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/8a/3c4af14b93a15ce55901cd7a92e1a4701910f1768c78fb30f61d2b79785b/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef", size = 625626, upload-time = "2025-06-15T19:06:03.578Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/f5/cf6aa047d4d9e128f4b7cde615236a915673775ef171ff85971d698f3c2c/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb", size = 622744, upload-time = "2025-06-15T19:06:05.066Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/00/70f75c47f05dea6fd30df90f047765f6fc2d6eb8b5a3921379b0b04defa2/watchfiles-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297", size = 402114, upload-time = "2025-06-15T19:06:06.186Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/03/acd69c48db4a1ed1de26b349d94077cca2238ff98fd64393f3e97484cae6/watchfiles-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018", size = 393879, upload-time = "2025-06-15T19:06:07.369Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/c8/a9a2a6f9c8baa4eceae5887fecd421e1b7ce86802bcfc8b6a942e2add834/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0", size = 450026, upload-time = "2025-06-15T19:06:08.476Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/51/d572260d98388e6e2b967425c985e07d47ee6f62e6455cefb46a6e06eda5/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12", size = 457917, upload-time = "2025-06-15T19:06:09.988Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/2d/4258e52917bf9f12909b6ec314ff9636276f3542f9d3807d143f27309104/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb", size = 483602, upload-time = "2025-06-15T19:06:11.088Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/99/bee17a5f341a4345fe7b7972a475809af9e528deba056f8963d61ea49f75/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77", size = 596758, upload-time = "2025-06-15T19:06:12.197Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/76/e4bec1d59b25b89d2b0716b41b461ed655a9a53c60dc78ad5771fda5b3e6/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92", size = 477601, upload-time = "2025-06-15T19:06:13.391Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/fa/a514292956f4a9ce3c567ec0c13cce427c158e9f272062685a8a727d08fc/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e", size = 451936, upload-time = "2025-06-15T19:06:14.656Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/5d/c3bf927ec3bbeb4566984eba8dd7a8eb69569400f5509904545576741f88/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b", size = 626243, upload-time = "2025-06-15T19:06:16.232Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/65/6e12c042f1a68c556802a84d54bb06d35577c81e29fba14019562479159c/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259", size = 623073, upload-time = "2025-06-15T19:06:17.457Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/ab/7f79d9bf57329e7cbb0a6fd4c7bd7d0cee1e4a8ef0041459f5409da3506c/watchfiles-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f", size = 400872, upload-time = "2025-06-15T19:06:18.57Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/d5/3f7bf9912798e9e6c516094db6b8932df53b223660c781ee37607030b6d3/watchfiles-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e", size = 392877, upload-time = "2025-06-15T19:06:19.55Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/c5/54ec7601a2798604e01c75294770dbee8150e81c6e471445d7601610b495/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa", size = 449645, upload-time = "2025-06-15T19:06:20.66Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/04/c2f44afc3b2fce21ca0b7802cbd37ed90a29874f96069ed30a36dfe57c2b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8", size = 457424, upload-time = "2025-06-15T19:06:21.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/b0/eec32cb6c14d248095261a04f290636da3df3119d4040ef91a4a50b29fa5/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f", size = 481584, upload-time = "2025-06-15T19:06:22.777Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/e2/ca4bb71c68a937d7145aa25709e4f5d68eb7698a25ce266e84b55d591bbd/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e", size = 596675, upload-time = "2025-06-15T19:06:24.226Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/dd/b0e4b7fb5acf783816bc950180a6cd7c6c1d2cf7e9372c0ea634e722712b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb", size = 477363, upload-time = "2025-06-15T19:06:25.42Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/c4/088825b75489cb5b6a761a4542645718893d395d8c530b38734f19da44d2/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147", size = 452240, upload-time = "2025-06-15T19:06:26.552Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/8c/22b074814970eeef43b7c44df98c3e9667c1f7bf5b83e0ff0201b0bd43f9/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8", size = 625607, upload-time = "2025-06-15T19:06:27.606Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/fa/a4f5c2046385492b2273213ef815bf71a0d4c1943b784fb904e184e30201/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db", size = 623315, upload-time = "2025-06-15T19:06:29.076Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "websockets"
|
||||||
|
version = "15.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user