20 KiB
Расписание
Публичное расписание учебных групп факультета: источник данных — Яндекс.Таблица университета (файл xlsx), который скачивается, разбирается, превращается в JSON API и отображается на фронте.
Быстрый старт
Нужны Python 3.11+, bash (Linux, macOS или WSL на Windows) и публичная ссылка на таблицу.
python3 --version # должен быть 3.11 или новее
cp .env.example .env # в SCHEDULE_LINK вписать публичную ссылку Я.Диска
make run # создаст .venv, поставит зависимости и запустит оба сервиса
Открыть http://127.0.0.1:8080. Остановить — Ctrl+C.
Подробнее — разделы «Зависимости» и «Запуск» ниже.
Архитектура
Яндекс.Диск (xlsx)
│ скачивание по публичной ссылке
▼
api (FastAPI), порт 8000, в docker публикуется на 127.0.0.1
• скачивает и разбирает xlsx без сторонних библиотек
• кэш в памяти: TTL + проверка даты изменения файла + stale-while-error
• один воркер, поэтому кэш общий
▲ SCHEDULE_API=http://api:8000 (внутри docker-сети)
│ SCHEDULE_API=http://127.0.0.1:8000 (локальная разработка)
frontend (FastAPI: статика + прокси), прод :8090, dev :8080
• отдаёт index.html / app.js / style.css / картинки
• /api/* — тонкий прокси к api (браузер не знает адрес бэкенда)
▲
nginx (https://schedule.our-place.ru) → 127.0.0.1:8090
Как устроено расписание в таблице
Лист «Бакалавры и магистры» — это сетка:
столбцы 0–1 даты «числителя»: начало и конец недели (серийные номера Excel)
столбцы 2–3 даты «знаменателя»
столбец 4 день недели (указан один раз на блок дня)
столбец 5 номер пары и время (ячейка объединена на две строки пары)
столбцы 6+ группы — по одному столбцу на группу (скрытые столбцы пропускаются)
Каждый день занимает блок строк, и строки идут парами: «числитель» / «знаменатель». У каждой пары строк — своя неделя семестра, а даты в ячейках — это соседние недели по порядку. Поэтому недели семестра собираются со всего листа (повторы отбрасываются по паре «чётность + дата»), а текущая чётность («Сейчас» на бейдже) определяется по сегодняшней дате.
Как разбирается ячейка занятия
Ячейка свободного формата, и её содержимое проходит единый конвейер
(schedule/lessons.py) без особых веток на число строк:
- Красные фрагменты (в xlsx помечаются как
⟪...⟫ещё при чтении строк) выносятся в полеnoteи из остального текста убираются, даже если внутри них спрятано другое занятие; обрамляющие скобки у таких пометок снимаются. - Маркер курса по выбору «к.в. [- N]» может быть отдельной строкой или префиксом предмета. Если в ячейке несколько курсов, а маркер один, его получают все курсы этой ячейки.
- Каждая строка относится к одной из ролей:
- аудитория —
ауд. N,каб. N, URL,онлайн,зал…(включая корпус: «юридический корпус, 3 этаж ауд. 22»); - преподаватель — фамилия с инициалами (
Смирнов А.В.), полное ФИО, организация (ПравоТех,НПО Криста); преподавателей может быть несколько (через,или/); имя допускается в одной строке с предметом и (или) аудиторией; - предмет — всё, что стоит до преподавателя/аудитории (название может переноситься на несколько строк);
- заметка — время («начало в 14:05»), даты («с 8.09.26»), «ссылка в эл. университете» и прочие примечания.
- аудитория —
- Результат приводится к единому виду: время
ЧЧ:ММ-ЧЧ:ММ, аудиторияауд. N.
В одной ячейке может быть несколько занятий: они разделяются пустой строкой, новым маркером «к.в.» или началом нового предмета сразу после строки аудитории/преподавателя. Если пустой строкой (или красной заметкой, снятой с целой строки) от предмета оторвался только преподаватель и/или аудитория, этот кусок достраивает предыдущее занятие, а не создаёт занятие без предмета — иначе в расписании аудитории оставались бы одни группы.
Итог по паре: {time, ч: {raw, lessons[]}, з: {raw, lessons[]}}, где
lesson = {marker, subject, teacher, room, note}.
Как отображается
- Если занятия числителя и знаменателя совпадают, пара показывается одним списком без пометки недели (одинаково для групп, преподавателей и аудиторий); иначе — две колонки «числитель»/«знаменатель», пустая заменяется на «Окно».
- Подпись недели появляется только там, где недели действительно различаются; бейдж «Сейчас» подсвечивает текущую неделю.
- Длинные ссылки в карточке переносятся по служебным символам и каждые
24 символа (
<wbr>), поэтому карточка не вылезает за экран даже в старых браузерах, где нетoverflow-wrap. - Приложение можно «установить» на главный экран (манифест + service
worker + иконки приложения); на iOS иконку берёт
apple-touch-icon.
Структура кода
src/
backend/
main.py FastAPI: маршруты, create_app(cache), /healthz
schedule/
xlsx.py чтение xlsx: общие строки, объединённые ячейки,
скрытые столбцы, красный текст, серийные даты
source.py скачивание файла по публичной ссылке Я.Диска
lessons.py разбор ячеек занятий (общий конвейер ролей)
parsing.py лист → расписание, недели, индексы преподавателей/аудиторий
cache.py ScheduleCache: блокировка, интервал, stale-while-error
errors.py ScheduleError
frontend/
main.py FastAPI: статика + прокси /api/* (таймаут 150 c)
static/
index.html, app.js, style.css
manifest.json PWA: имя, иконки, standalone-режим
sw.js минимальный service worker (без кэша) для установки приложения
img/ логотипы (owl.svg, owl-white.svg), иконки приложения
(app-icon.svg, icon-192/512.png, apple-touch-icon.png, favicon-32.png)
tools/
make_icons.py генерация иконок приложения из логотипа (нужен rsvg-convert)
deploy/
nginx-site.sh генерация и включение конфига nginx (SSL — сертификат родителя)
nginx-dev.conf конфиг-заготовка для стенда без wildcard у родителя
README-dev.md как поднять второй стенд рядом с основным
tests/
conftest.py общие фикстуры
xlsx_builder.py генерация xlsx в памяти (без openpyxl)
fixtures.py общая тестовая таблица
test_lessons.py все форматы ячеек
test_xlsx.py чтение книги
test_parsing.py разбор листов, недели, индексы
test_api.py API и поведение кэша (интервал/stale, 404/400/502)
test_deploy.py проверка, что образы копируют все нужные файлы
API
Полный список методов отдаёт сам бэкенд: GET /.
| Метод | Путь | Описание |
|---|---|---|
| GET | /groups |
список групп (сначала БО, потом МО) |
| GET | /schedule/{group} |
расписание группы с разбором на занятия |
| GET | /schedule/{group}/{day} |
расписание группы на конкретный день |
| GET | /today?group={group} |
расписание группы на сегодня |
| GET | /teachers |
список преподавателей |
| GET | /teacher/{name} |
расписание преподавателя (+ full_name) |
| GET | /rooms |
список аудиторий (занятые ∪ лист «аудитории») |
| GET | /room/{label} |
занятость аудитории |
| GET | /rooms-day?day={день} |
занятость всех аудиторий за день |
| GET | /rooms-free?day={день}&period={N} |
свободные аудитории |
| GET | /week |
недели семестра и текущая чётность |
| GET | /status |
состояние кэша (cache_ttl_seconds, last_updated, table_modified, error) |
| GET | /healthz |
живость процесса (не обращается к таблице) |
Фронт проксирует только то, что использует, под префиксом /api/...:
groups, schedule/{group}, teachers, teacher/{name}, rooms,
room/{label}, rooms-day, rooms-free, week, status.
Остальное (today, schedule/{group}/{day}) доступно только на бэкенде.
Переменные окружения
| Переменная | По умолчанию | Где | Описание |
|---|---|---|---|
SCHEDULE_LINK |
— | api | публичная ссылка Я.Диска, обязательна |
SCHEDULE_INTERVAL_MINUTES |
15 |
api | как часто перечитывать таблицу, минут |
SCHEDULE_CACHE_TTL |
900 |
api | устаревшее: интервал в секундах (если не задан предыдущий) |
SCHEDULE_RETRY_AFTER |
60 |
api | пауза между попытками при сбое источника |
SCHEDULE_API |
http://127.0.0.1:8000 |
frontend | адрес бэкенда (в docker: http://api:8000) |
SCHEDULE_PROXY_TIMEOUT |
150 |
frontend | таймаут прокси к бэкенду, сек |
API_PORT |
8000 |
docker | внешний порт api (публикуется на 127.0.0.1) |
FRONTEND_PORT |
8080 |
docker | внешний порт frontend (публикуется на 127.0.0.1) |
.env ищется вверх от каталога модуля: и backend, и frontend находят общий
.env в корне репозитория; load_dotenv не перезаписывает уже выставленные
переменные окружения.
Зависимости
Проект состоит из двух сервисов на Python (FastAPI): api и frontend.
Всё ставится одной командой make venv (или вручную, см. ниже).
Системные требования
| Что | Зачем |
|---|---|
| Python 3.11+ | оба сервиса |
| Docker + docker compose v2 | только для запуска в контейнерах (опционально) |
Python-зависимости — закреплены в requirements.txt (рабочие) и
requirements-dev.txt (тесты):
| Пакет | Версия | Назначение |
|---|---|---|
| fastapi | 0.141.1 | веб-фреймворк обоих сервисов |
| uvicorn | 0.52.4 | ASGI-сервер, которым запускаются сервисы |
| python-dotenv | 1.2.3 | чтение .env |
| requests | 2.34.2 | скачивание файла с Яндекс.Диска и прокси к API |
| pytest | 9.1.1 | тесты (make test) — только dev |
| httpx2 | 2.12.0 | HTTP-клиент (наследник httpx) для TestClient в тестах — только dev |
Ничего, кроме этого, не нужно: xlsx разбирается стандартной библиотекой
(zipfile + xml.etree), базы данных нет, кэш — в памяти процесса.
Если установка не идёт
- Проверьте
python3 --version: нужен 3.11 или новее. Если в системе подpython3скрывается старая версия (например, 3.6 на старых CentOS), создайте окружение нужной версией явно:python3.12 -m venv .venv. - Debian/Ubuntu: если
make venvпадает на создании окружения, доставьте пакет venv:sudo apt install python3-venv(для нестандартной версии — например,python3.12-venv). httpx2иhttpcore2— новые пакеты (август 2026, httpx перешёл под опеку Pydantic и был переименован). В коде и тестах модуль импортируется какhttpx2;import httpxбольше не работает. В рабочем коде httpx2 не используется — нужен только тестам.
Если pip не находит пакеты: зеркала
Если pip настроен на корпоративное/национальное зеркало, оно может отставать от PyPI — свежие пакеты на нём ещё не появились. Диагностика и обход:
pip index versions httpx2 # есть ли пакет на текущем индексе
pip install -r requirements-dev.txt -i <зеркало> # разово через другое зеркало
pip config set global.index-url <зеркало> # закрепить зеркало навсегда
Зеркала, проверенные 07.10.2026 (пакеты httpx2/httpcore2 на месте;
URL подставлять как есть в -i / index-url):
| Зеркало | URL |
|---|---|
| PyPI, официальный | https://pypi.org/simple |
| TUNA (Tsinghua) | https://pypi.tuna.tsinghua.edu.cn/simple |
| Huawei Cloud | https://mirrors.huaweicloud.com/repository/pypi/simple |
| BFSU | https://mirrors.bfsu.edu.cn/pypi/web/simple |
| SJTU | https://mirror.sjtu.edu.cn/pypi/web/simple |
| USTC | https://mirrors.ustc.edu.cn/pypi/simple |
| SUSTech | https://mirrors.sustech.edu.cn/pypi/simple |
Makefile написан под bash: Linux, macOS или WSL. На Windows без WSL используйте Docker-вариант из раздела «В Docker».
Запуск
Без Docker
git clone <адрес репозитория> && cd Schedule
cp .env.example .env # открыть .env и вписать SCHEDULE_LINK
make run # = make venv + запуск api и сайта
make run создаёт виртуальное окружение .venv, ставит зависимости и
поднимает два сервиса с авто-перезагрузкой:
- API — http://127.0.0.1:8000 (например,
/statusи/groups); - сайт — http://127.0.0.1:8080 (его и открывать в браузере).
Чтобы отвязать процесс от терминала, стоит запускать через nohup make run &
Остановить — Ctrl+C. Данные скачиваются сразу на старте приложения
(скачивание занимает несколько секунд) и дальше обновляются по интервалу.
Полезные команды (make help — весь список):
| Команда | Что делает |
|---|---|
make venv |
создать .venv и установить зависимости |
make run (или make dev) |
запустить API и сайт |
make run-api / make run-front |
запустить только один из сервисов |
make test |
прогнать тесты |
Если хочется без make — то же самое руками:
python3 -m venv .venv
.venv/bin/pip install -r requirements-dev.txt
.venv/bin/python -m uvicorn main:app --reload --port 8000 # в каталоге src/backend
.venv/bin/python -m uvicorn main:app --reload --port 8080 # в каталоге src/frontend
В Docker
cp .env.example .env # вписать SCHEDULE_LINK (и порты при необходимости)
./deploy.sh # = docker compose build && up -d
У обоих сервисов есть healthcheck на /healthz; frontend стартует после того,
как api стал healthy. Обновление: git pull && docker compose up -d --build.
Настройка nginx (прод)
Скрипт генерирует и включает конфиг сайта: http → https, прокси на
FRONTEND_PORT из .env, после проверки nginx -t перечитывает nginx.
SSL обеспечивается сертификатом родительского домена (обычно wildcard
*.our-place.ru) — отдельный certbot для поддомена не нужен.
sudo ls /etc/letsencrypt/live/ # имя сертификата родительского домена
sudo make nginx-install # или: sudo deploy/nginx-site.sh
Параметры: --domain sched.example.ru, --cert example.ru (по умолчанию —
родительский домен от --domain), --port 8090 (по умолчанию —
FRONTEND_PORT из .env). Посмотреть конфиг, ничего не меняя:
make nginx-print. Если wildcard у родителя нет — выпустите сертификат
скриптом certbot, как для dev-стенда (см. deploy/README-dev.md).
Тесты
make test # 121 тест, сеть не нужна