Карточки "вопрос - ответ - пример кода" для подготовки к собеседованиям по Python. Проект состоит из трёх частей:
- бот присылает серию карточек по расписанию в Telegram и Discord;
- backend (REST API) хранит карточки и отдаёт их с поиском, фильтрами и пагинацией;
- frontend показывает карточки на сайте.
- Стек
- Структура проекта
- Быстрый старт
- Настройка (.env)
- Как работает бот
- API
- Импорт карточек из Markdown
- Разработка без Docker
- Тесты и линтер
- Обновление бота на сервере
| Часть | Технологии |
|---|---|
| Бот | Python 3.13, aiogram 3, APScheduler, aiohttp (Discord webhook), SQLAlchemy |
| Backend | FastAPI, SQLAlchemy, SQLite, JWT (PyJWT), pydantic-settings |
| Frontend | Vue 3, Vite, Pinia, highlight.js |
| Инфраструктура | Docker Compose, Nginx |
| Инструменты | uv, pytest, ruff, GitHub Actions |
backend/
app/
routers/ cards.py (публичное API), admin.py (CRUD, нужен токен)
scripts/ migrate_md.py, questions.md, new_questions.md
auth.py config.py database.py main.py models.py schemas.py
tests/
bot/
main.py расписание, отправка серий, команды бота
cards.py выбор карточек и форматирование под Telegram и Discord
discord_sender.py отправка через webhook с повторами
state.py хранение номера последней отправленной карточки
config.py чтение переменных окружения
tests/
frontend/
docker-compose.yml
.env.example
Backend и бот используют одну базу SQLite через общий том Docker (sqlite_data). Бот только читает карточки, записывает их backend (через API или скрипт импорта).
cp .env.example .env
# заполните значения, см. раздел «Настройка»
docker compose up -d --buildЗагрузить карточки в базу:
docker compose exec -e PYTHONPATH=/app backend uv run python app/scripts/migrate_md.py --file app/scripts/questions.mdСайт доступен на http://localhost:8080.
Если порт 8080 занят или на сервере уже есть свой Nginx, измените порт в блоке ports сервиса frontend в docker-compose.yml. На сервере, где работает только бот, этот блок можно закомментировать.
Запустить только бота:
docker compose up -d --no-deps botБез --no-deps вместе с ботом поднимется и backend, от которого он зависит в compose.
Важно: при старте бот сразу отправляет серию карточек, не дожидаясь расписания. Каждый перезапуск контейнера бота запускает рассылку.
Логи:
docker compose logs -f bot
docker compose logs -f backendФайл .env не попадает в Git. Шаблон: .env.example.
| Переменная | Обязательна | По умолчанию | Назначение |
|---|---|---|---|
BOT_TOKEN |
да | нет | токен Telegram-бота |
ADMIN_IDS |
да | нет | id пользователей Telegram через запятую; только они могут пользоваться командами бота и получают рассылку |
TELEGRAM_ENABLED |
нет | true |
false отключает отправку в Telegram |
DISCORD_WEBHOOK_URL |
нет | пусто | webhook канала Discord; если пусто, Discord отключён, в лог пишется предупреждение |
CARDS_PER_SESSION |
нет | 4 |
сколько карточек в одной серии |
PAUSE_BETWEEN_CARDS_SECONDS |
нет | 240 |
пауза между карточками в серии, секунды |
DATABASE_URL |
нет | sqlite:///./data/cards.db |
путь к базе карточек |
Бот не запустится, если отключены оба канала (TELEGRAM_ENABLED=false и пустой DISCORD_WEBHOOK_URL): отправлять было бы некуда. Переменные BOT_TOKEN и ADMIN_IDS проверяются при старте всегда, даже если Telegram отключён.
| Переменная | Обязательна | Назначение |
|---|---|---|
ADMIN_PASSWORD |
да | пароль администратора |
SECRET_KEY |
да | ключ подписи JWT-токенов |
Пустые значения секретов отклоняются при старте. Задавайте длинный случайный SECRET_KEY, например: python -c "import secrets; print(secrets.token_urlsafe(48))".
- Расписание: серия карточек отправляется в 7, 12, 17 и 22 часа (в Docker часовой пояс бота
Europe/Moscow) и сразу при запуске. - Порядок: карточки идут по возрастанию
id; после последней список начинается заново. Номер последней отправленной карточки хранится в файле состоянияbot_state.jsonи переживает перезапуск. Запись атомарная, обрыв посреди записи не портит файл. - Каналы: Telegram и Discord работают параллельно. Ошибка Discord только логируется. Если не удалась отправка в Telegram, сохраняется прогресс и через 15 минут серия повторяется.
- Длинные тексты: сообщения режутся по границам строк с учётом лимитов (4096 символов в Telegram, 2000 в Discord). Код отправляется отдельным сообщением: в Telegram в блоке
<pre>, в Discord в блоке```python. - Повторы: Discord при ответе 429 ждёт столько, сколько просит сервер (с потолком), и делает ограниченное число попыток.
- Команды (только для
ADMIN_IDS):/cardприсылает случайную карточку,/startвыводит справку. - Битые данные: если у карточки в базе повреждено поле
tags, она всё равно отправляется, только без тегов, а в лог попадает предупреждение с еёid.
Документация (Swagger) доступна на /docs у запущенного backend.
| Метод | URL | Доступ | Описание |
|---|---|---|---|
| GET | /api/health |
открытый | проверка работы сервиса |
| GET | /api/cards |
открытый | список карточек |
| GET | /api/cards/{id} |
открытый | одна карточка, 404 если нет |
| GET | /api/cards/categories |
открытый | список категорий |
| POST | /api/admin/login |
открытый | вход, возвращает JWT-токен |
| POST | /api/admin/cards |
токен | создать карточку |
| PUT | /api/admin/cards/{id} |
токен | обновить карточку |
| DELETE | /api/admin/cards/{id} |
токен | удалить карточку |
Параметры GET /api/cards:
| Параметр | Описание |
|---|---|
search |
подстрока в вопросе или ответе, без учёта регистра (в том числе для кириллицы) |
category |
точное совпадение категории |
difficulty |
easy, normal или hard |
tags |
теги через запятую, например list,dict; находятся карточки хотя бы с одним из них, совпадение точное (set не находит subset) |
page |
номер страницы, от 1 |
per_page |
размер страницы, от 1 до 500, по умолчанию 20 |
Ответ списка: items, total, page, per_page.
Пример:
curl "http://localhost:8000/api/cards?search=декоратор&difficulty=normal&per_page=5"Поля карточки: question, answer, code_example, category, tags (список строк), difficulty.
Скрипт backend/app/scripts/migrate_md.py разбирает Markdown-файл и добавляет карточки в базу.
# Категория
## Подкатегория (тоже становится категорией)
### Вопрос?
Текст ответа.
```python
print("пример кода")
- заголовки уровней 1 и 2 задают категорию;
- заголовки уровня 3 и глубже задают вопрос, текст под ним становится ответом;
- блоки кода попадают в `code_example`; если блоков несколько, они нумеруются комментариями `# Пример 1`, `# Пример 2`;
- импортированные карточки получают `difficulty = normal` и пустые теги, их можно поменять через API или админку.
### Запуск
Добавить карточки к существующим:
```bash
docker compose exec -e PYTHONPATH=/app backend uv run python app/scripts/migrate_md.py --file app/scripts/new_questions.md
Сначала удалить все карточки, затем импортировать (флаг --clear):
docker compose exec -e PYTHONPATH=/app backend uv run python app/scripts/migrate_md.py --file app/scripts/questions.md --clear- Дубли пропускаются. Карточка считается дублем, если в базе или раньше в этом же файле уже есть такая же пара вопрос + категория. Повторный запуск без
--clearбезопасен. В конце скрипт пишет, сколько карточек добавлено и сколько пропущено. - Незакрытые блоки кода. Если в файле остался незакрытый блок
```, перед импортом в консоль выводятся строкиВНИМАНИЕ: ...с названием вопроса и категории. Карточки при этом импортируются, предупреждение нужно, чтобы найти и поправить проблемное место. Проверка эвристическая: незакрытый блок с голой оградой в середине файла она может не заметить. - Пустые вопросы и ответы пропускаются.
Нужны Python 3.13, uv и Node.js.
# backend (API на http://localhost:8000, документация на /docs)
cd backend
uv sync
uv run uvicorn app.main:app --reload
# frontend (сайт на http://localhost:5173)
cd frontend
npm install
npm run devСекреты задаются в .env в корне проекта.
Backend и бот независимы: у каждого свои зависимости и свой набор тестов. Команды запускаются из каталога соответствующего пакета.
# backend
cd backend
uv run pytest -q
uv run ruff check && uv run ruff format --check
# бот
cd bot
uv run pytest tests/ -q
uv run ruff check && uv run ruff format --checkТесты backend работают с базой в памяти, тесты бота подменяют Telegram и Discord, сеть не нужна. Те же проверки (тесты и ruff) запускаются в GitHub Actions для каждого пуша и pull request.
Файлы questions.md и new_questions.md — это данные карточек, ruff их не форматирует.
git pull
docker compose build bot
docker compose up -d --no-deps botПересоздание контейнера запускает рассылку (см. выше). Не используйте docker compose down -v: флаг -v удаляет том с базой.
Если сайт публикуется за собственным Nginx, измените порт в docker-compose.yml и настройте проксирование на нужный порт:
location / {
proxy_pass http://localhost:<порт>;
}В CORS разрешены все источники, а на /api/admin/login нет ограничения числа попыток.