Одна команда для учёбы в Moodle: сроки и баллы, материалы курсов, отправка ответов, релизы на GitVerse и SourceCraft, видео на Rutube — и готовая утренняя сводка, которую дочитывает ИИ-агент.
- Возможности
- Как выглядит сводка
- Требования
- Установка
- Быстрый старт
- Ежедневная сводка
- ИИ-оператор
- Команды
- Настройка
- Обновление и версии
- Рабочий процесс
- Документация и устройство
- Разработка
- Лицензия
- Сроки и задания. Дедлайны со статусом ответа в ТУИС, просроченное, сдвинутые сроки, новые задания. Видит то, чего нет в обычном списке: элементы с ограничением доступа, выбор темы доклада — и выбрана ли она. Тесты — отдельной таблицей на месяц вперёд: попытки, лимит времени, начатые и не отправленные.
- Баллы и уведомления. Итог по курсу (если Moodle его прячет — считается из работ), новые баллы и непрочитанные уведомления с прошлого запуска; автоматические отсеяны. Новые отзывы преподавателя — разделом «Отзывы», работа «на доработку» — в «Горит».
- Объявления. Новые записи форумов объявлений курсов — по id, а не по дате: правки и посты задним числом тоже видны.
- Материалы курсов. Появившиеся файлы поимённо и загрузка их в
stash/предмета. - Ответ на задание. Текст со ссылками на репозитории, релизы и скринкасты, вложения,
отправка — только с
--confirm: сохранение ответа в Moodle необратимо. - Релизы. Создание релиза по тегу и загрузка файлов на GitVerse и SourceCraft из каталога любого курса; ссылки в заметках подменяются под хостинг.
- Rutube. Вход, свои видео и плейлисты, загрузка файла или импорт по URL.
- Состояние работ. Что собрано, закоммичено, выпущено и отправлено — по git, собранным PDF и релизам.
- Для агента.
--jsonу любой команды — те же данные машиночитаемо; универсальная инструкция для ИИ-оператора ставится одной командой.
study state --pull печатает готовый markdown; разделы, в которых пусто, не выводятся.
Пример с выдуманными курсами (databases, crypto — папки из config.env, у курса
без папки в колонке стоит его название из Moodle):
# Учёба · чт 08.10 09:00
## Сроки
| Когда | Осталось | Работа | Курс | Состояние |
| ----- | -------- | ------ | ---- | --------- |
| **06.10 23:59** | **просрочено** | **ДЗ 2 — Нормализация до 3НФ** | **databases** | **не сдано** |
| 12.10 23:59 | 4 дн | ЛР 3 — Индексы и планы запросов | databases | не сдано |
| 15.10 23:59 | 7 дн | Тема доклада к лекции 5 | crypto | не выбрана, 9 вариантов |
| 15.10 23:59 | 7 дн | Доклад к лекции 5 | crypto | доступ закрыт |
| 20.10 23:55 | 12 дн | Расчётно-графическая работа | Теоретическая механика | не сдано |
## Тесты
| Когда | Осталось | Тест | Курс | Попытки | Время |
| ----- | -------- | ---- | ---- | ------- | ----- |
| **09.10 18:00** | **1 дн** | **Тест после лекции №4** | **crypto** | **0 из 2** | **15 мин** |
| 23.10 18:00 | 15 дн | Тест после лекции №5 | crypto | 0 из 2 | 15 мин |
## Баллы
| Курс | Итого | Новое |
| ---- | ----- | ----- |
| databases | 14.50 / 20 | ЛР 2 4.50/5 |
| crypto | 9.00 / 12 | — |
## Уведомления
| Когда | Тема |
| ----- | ---- |
| 07.10 16:42 | Оценена работа «ЛР 2. Хранимые процедуры» |
## Объявления
| Когда | Курс | Тема |
| ----- | ---- | ---- |
| 08.10 09:12 | databases | **Лекция 5 переносится** · Петров П.: на пятницу, аудитория та же. |
## Новое в курсах
| Курс | Раздел | Что | Файлы |
| ---- | ------ | --- | ----- |
| crypto | Лекции | Материалы лекции 5: новые файлы | 05-rsa.pdf, 05-rsa-slides.pdf |
| databases | Общее | Объявления: изменены настройки | — |
## Горит
- **ДЗ 2 — Нормализация до 3НФ** · databases · до 06.10 23:59 · просрочено · методички: databases/stash/
- **ЛР 3 — Индексы и планы запросов** · databases · до 12.10 23:59 · 4 дн · методички: databases/stash/«Горит» — несданное, что просрочено или сдаётся в ближайшую неделю и что ТУИС ещё примет: работа с закрытым приёмом (у РУДН cutoff = срок) остаётся в «Сроках» как «приём закрыт», а в «Горит» идёт её «Пересдача …»; очное задание без ответа в ТУИС — тоже не «Горит». Это вывод программы, агент к нему ничего не дописывает.
- Linux, macOS или Windows 10+ (в том числе WSL)
python33.8+, только стандартная библиотекаgit- токены Moodle (служба Moodle mobile web service), GitVerse, SourceCraft — каждый нужен только для своего сервиса
Linux, macOS:
curl -fsSL https://raw.githubusercontent.com/nowherewashere/study-digest/master/install.py -o install.py && python3 install.py; rm -f install.pyWindows (PowerShell или Windows Terminal; у Python из Microsoft Store лаунчера py нет —
тогда python install.py; в Git Bash — winpty py install.py, иначе вопросы не задаются):
iwr https://raw.githubusercontent.com/nowherewashere/study-digest/master/install.py -OutFile install.py; py install.py; del install.pyУстановка скачивается и запускается, а не льётся в интерпретатор через пайп: она задаёт
вопросы. install.py проверяет git и python3, клонирует репозиторий в
<учебная директория>/.digest и передаёт дело study setup: токены, каталоги, проверка
связи с Moodle, команда study в PATH, выбор ИИ-оператора, настройка курсов и первое
наполнение stash/. Семь экранов, итог со всеми результатами; study setup можно
запускать снова — перенастроить токены или курсы.
Команда study кладётся в ~/.local/bin: на Linux и macOS — симлинк (macOS: добавить
export PATH="$HOME/.local/bin:$PATH" в ~/.zprofile), на Windows — study.cmd для cmd
и PowerShell и sh-скрипт study для Git Bash; каталог добавляется в PATH пользователя
с согласия, подействует в новом терминале. Все настройки и секреты — в .digest/config.env
(на POSIX права 600; вне git, в репозитории только config.env.example). Токены Rutube
инструмент ведёт сам в .digest/.secrets/.
Где взять токены
| Сервис | Где |
|---|---|
| Moodle | профиль → «Ключи безопасности» → служба Moodle mobile web service; значение показывается один раз |
| GitVerse | иконка пользователя → Настройки → Управление токенами, доступ «Репозитории» |
| SourceCraft | Home → Access → Personal Access Tokens |
| Rutube | jwt (аккаунты через VK ID / Gazprom ID): один раз скопировать refreshToken из cookie браузера (rutube.ru → DevTools → Application → Cookies) и сохранить study rt jwt — дальше годовой refresh сам выпускает короткий access. token (только аккаунты с паролем): study rt login. Режим задаёт --mode {auto,jwt,token}, auto предпочитает jwt |
study me # токен Moodle работает?
study courses --setup # какие курсы не отслеживать и как зовутся их папки
study agent claude # инструкция для агента в CLAUDE.md (codex, gemini, copilot — см. ниже)
study files --pull # материалы всех курсов в stash/ (study setup предлагает это сам)
study digest # первый запуск сохраняет снимок состояния
study state --pull # сводка + новые файлы курсов в stash/Учебная директория вокруг .digest/ устроена просто: на каждый курс — папка с именем
из строки CODE в config.env, внутри stash/ (материалы), tuis/ (ссылки на записи
и заготовки ответов), NOTES.md (заметки по предмету; заготовку с шапкой и таблицами
кладут study setup и study courses --setup, готовый файл не трогают) и репозиторий курса.
Основной режим: задача ИИ-оператора раз в день запускает study state --pull и печатает
вывод как есть — сводка уже свёрстана программой, агент ничего не дописывает, файлов
не читает и работу не начинает. Промпт задачи — docs/daily-digest-prompt.md.
Code → Routines → New routine → Local (облачной задаче недоступны локальные файлы и токены):
| Поле | Значение |
|---|---|
| Working folder | корень учебной директории |
| Schedule | Daily, 9:00 |
| Instructions | текст из docs/daily-digest-prompt.md после черты |
После создания — Run now и «always allow» на запросах разрешений: разрешение выдаётся
на study (в варианте с WSL — на wsl.exe) один раз, иначе следующие запуски встанут
на подтверждении.
Ничего особенного: команда study в ~/.local/bin, задача запускает study state --pull
из корня учебной директории. Если PATH задачи не содержит ~/.local/bin (macOS без строки
в ~/.zprofile), промпт подсказывает запасной путь — python3 .digest/study state --pull.
Claude Code Desktop на Windows выполняет команды в Git Bash; он находит sh-скрипт
~/.local/bin/study, если %USERPROFILE%\.local\bin есть в PATH пользователя (это
предлагает study setup), иначе — py .digest/study state --pull от рабочей папки.
Вывод идёт в UTF-8 независимо от кодовой страницы консоли, файлы stash/ и NOTES.md
читаются по обычным путям.
Вариант, когда программа живёт в WSL, а задача запускается в Windows. Рабочая папка задачи —
\\wsl.localhost\<дистрибутив>\home\<…>\study, оболочка при этом windows-овая (Git Bash),
и все команды идут через wsl.exe:
MSYS_NO_PATHCONV=1 wsl.exe -d <дистрибутив> -- bash -c '~/work/study/.digest/study state --pull'Грабли
MSYS_NO_PATHCONV=1обязателен: без него Git Bash подставляет windows-путь вместо unix-пути (/tmp/x.sh→C:/Program Files/Git/mnt/c/...), и команда не находится.- Однострочники
bash -lc '…'переживают не всякие кавычки: в циклеfor d in …переменная приходит пустой. Всё длиннее одной команды — в скрипт, записанный из Windows прямо в WSL (//wsl.localhost/<дистрибутив>/tmp/x.sh), запускbash /tmp/x.sh. - Файлы (
stash/,NOTES.md,config.env) читаются по windows-пути напрямую, но симлинки WSL через\\wsl.localhostне открываются — поэтому файл оператора это копия, а не ссылка. - Установка тоже из WSL:
install.pyтам, где будет работать программа.
Инструкция для агента — один текст, docs/AGENTS.md: что лежит в учебной
директории, что умеет study, чего делать нельзя. У операторов различаются только имена
файлов, которые они читают, поэтому study agent <код> кладёт этот текст блоком в нужный
файл в корне учебной директории:
| Код | Кто читает | Файл |
|---|---|---|
claude |
Claude Code | CLAUDE.md |
codex |
OpenAI Codex; тот же файл читают Cursor, Copilot coding agent, Jules, Zed | AGENTS.md |
gemini |
Gemini CLI | GEMINI.md |
copilot |
GitHub Copilot в редакторе | .github/copilot-instructions.md |
Блок стоит между маркерами study:begin … study:end; всё вне его — личные правила
(предметы, стиль коммитов, кто что делает), их команда не трогает. После обновления
инструмента — study agent <код> ещё раз: блок перепишется, личное останется. Без
аргумента команда показывает, какие файлы есть и актуален ли в них блок; операторов
можно поставить несколько: study agent claude codex.
--help у любой команды. --json до или после имени команды печатает те же данные
машиночитаемо — ровно то, что видит текстовый вывод, без второго обхода API. У state
и digest в JSON есть schema — версия структуры: ломающее изменение увеличивает её,
добавление полей — нет. Остальные команды отдают ответ API как есть.
Коды возврата: 0 — успех; 1 — план без подтверждения (submit без --confirm,
rt upload без --confirm, update без --yes или с ответом «нет»); 2 — ошибка
(текст и подсказка в stderr, а также неверные аргументы); 130 — прерывание.
study state: now, days, tuis (то же, что у digest, или null при --local),
courses[] — курсы с папкой: id, code, title, flow (профиль сдачи), dir, repo
(ветка, тег, незакоммиченное, remotes; у file — null), releases по хостингам,
unreleased_tags, labs[] (готовность отчёта, презентации, видео, пара с заданием ТУИС);
update — есть ли обновление инструмента; errors.
study digest (и tuis внутри state): first_run, since, courses, new_courses,
deadlines (в окне), overdue (просрочено, не сдано), submitted (просрочено, сдано),
not_started (несданное из обоих списков, кроме закрытых к приёму и очных — источник
«Горит»), retakes (пересдачи
с retake_of и needed), quizzes, updates (новое в курсах), new_assignments, moved
(сдвинутые сроки), notifications, announcements (новое в форумах объявлений), feedback
(новые отзывы преподавателя), grades, outside (сроки в скрытых курсах), errors.
У каждого задания: assign_id, cmid, course, name, short, due (с учётом продления),
submission (new · draft · reopened · submitted · offline · hidden), graded,
grade, feedback, closed (приём закрыт), opens, canedit, locked, intro, lab,
retake. Время везде — объект {ts, iso, text, full, left, left_sec, overdue}.
| Команда | Что делает |
|---|---|
study state [--days N] [--local] [--no-save] [--pull] [--since X] |
готовая сводка; --local — без ТУИС, --pull — забрать новые файлы в stash/ |
study digest [--days N] [--no-save] [--since X] |
та же сводка, только по ТУИС |
study task <код> <NN|id|cmid> |
карточка задания: текст, срок, что принимает, состояние и отзыв, что есть на диске; ничего не сохраняет |
study answer <код> <NN> [--tag T] |
заготовка ответа по лабораторной — курсы с профилем release |
study agent [код …] |
файл инструкций для ИИ-оператора: состояние / поставить |
study setup |
первоначальная настройка: токены, каталоги, команда в PATH, оператор, курсы |
study update [--check] |
обновить инструмент из репозитория и блоки агента; --check — только посмотреть |
study --version |
версия: тег и число коммитов после него |
| Команда | Что делает |
|---|---|
study me |
владелец токена и число доступных функций |
study courses [--all] [--setup] |
курсы: список / интерактивная настройка игнора и папок |
study assigns [--course id|код] |
задания и сроки; в --json — ещё и intro, текст задания |
study calendar [--days N] |
события календаря (только дедлайны) |
study grades [--course id|код] |
баллы |
study status <id-задания> |
состояние ответа на задание |
study files [<курс>] [--pull] [--all] [--force] |
файлы курса: что появилось, забрать в stash/; перезалитый в ТУИС файл перекачивается поверх копии (mtime копии = timemodified из ТУИС); --force — всё заново; без курса — по всем папкам из config.env, строкой на курс |
study upload <файл>… [--itemid N] |
загрузка вложений, печатает itemid |
study submit <id> [--text F] [--attach <файл>…] [--files <itemid>] --confirm |
отправка ответа; без --confirm — план и код возврата 1. План показывает, что принимает задание (текст, число/типы/размер файлов), и отказывает при несовпадении даже с --confirm; --attach загружает файлы сам при отправке |
study functions [подстрока] · study call <функция> [ключ=значение …] |
ручки Moodle и произвольный вызов; повтор ключа — массив: options[ids][0]=101 |
study gv releases | release <тег> --title T --notes F | update <тег> --notes F | asset <id-релиза> <файл> | api <путь>
study sc releases | release <тег> --title T --notes F | update <тег> --notes F | asset <тег> <файл> | api <путь>
Репозиторий берётся из git-remote каталога, откуда запущена команда (origin — GitVerse,
src — SourceCraft; remote на другой хостинг не считается), иначе из GV_REPO / SC_REPO.
gv release подставляет SHA коммита по тегу: GitVerse не принимает имя ветки.
study rt jwt [--refresh T] | login [--email E] | me | api <путь>
study rt categories | video <id> | edit <id> [--title T] [--desc F] [--category N] [--age A] [--hidden | --visible]
study rt playlist list | create --title T [--hidden] | add <id-плейлиста> <id-видео>
study rt upload <файл> | --url U [--title T] [--category N] [--age A] [--hidden] [--playlist ID] [--slot lab|report|presentation|defense] --confirm
Всё изменяемое — в config.env.
| Задача | Что править |
|---|---|
| другой набор курсов в новом семестре | study courses --setup (COURSE_IGNORE, строки CODE) |
| как сдаётся курс | FLOW <id> release|file — спрашивает study courses --setup; без строки угадывается по репозиторию в папке |
| адрес Moodle, токены, окно дедлайнов | TUIS_URL, TUIS_TOKEN и др., DIGEST_DAYS |
| вид сводки | src/study/digest.py, функция render |
| шаги задачи агента | docs/daily-digest-prompt.md |
| правила для агента | docs/AGENTS.md, затем study agent <код> |
| новый источник данных | src/study/moodle.py + src/study/digest.py + запись в docs/tuis-api.md |
| новый хостинг кода | класс в src/study/hosting.py + запись в docs/hosting-api.md |
| новый ИИ-оператор | строка в OPERATORS, src/study/agent.py |
Курсы. Отслеживаются все курсы пользователя в ТУИС, кроме COURSE_IGNORE (id через
пробел). Папку курса задаёт CODE <id> <имя папки>; курс без папки попадает в сводку по
срокам, но локального состояния (git, релизы, лабы) у него нет. Профиль сдачи задаёт
FLOW <id> release|file: release — репозиторий курса из шаблона преподавателя, git-flow,
релиз с отчётом и презентацией на обоих хостингах, скринкасты, study answer, папка
tuis/; file — отчёт файлом, работа в <код>/labNN/, сдача study submit --attach,
репозиторий и хостинги не смотрятся. Без строки FLOW профиль угадывается: release,
если в папке курса есть репозиторий, иначе file (study courses показывает его со знаком
«?»). Курсы из COURSE_IGNORE
в сводке не появляются нигде; сроки курса, скрытого в самом ТУИС (не в игноре), показываются
разделом «Сроки в скрытых курсах» — вдруг скрыт по ошибке. Курс, которого не было в снимке
(новая запись или снятый игнор), сводка объявляет строкой «Новый курс в ТУИС: … — строка
CODE или COURSE_IGNORE в config.env». Это объявление бывает один раз, поэтому курс без
папки сводка дальше называет строкой «Без папки (файлы не скачиваются): … (id N)» в каждом
запуске, пока ему не задали CODE или не отправили в COURSE_IGNORE.
Снимок состояния. «Новое» в сводке — новое с прошлого запуска: .digest/.state.json
(время запуска, сроки, баллы, список и состав курсов) обновляется каждым state/digest без --no-save, копия
за день — в .digest/state/ГГГГ-ММ-ДД.json (60 дней). --since пересчитывает от другой
точки: --since 2026-09-12 или --since 3 (дней назад) — ближайший снимок не позже даты,
--since never — как при первом запуске, --since all — новым считается всё. С --no-save
это взгляд назад, без него следующий снимок перепишется. Файлы пишутся через временный
и os.replace, обрыв не оставит половины; если .state.json всё же повреждён (правка
руками), берётся последний целый дневной снимок, а без него — как первый запуск, о чём
сводка говорит в строке «Не удалось». Файлы курса сверяются с составом из снимка, а не
только по дате: файл, скопированный преподавателем из прошлогоднего курса или открытый
студентам, приходит со старой timemodified, и core_course_get_updates_since его не видит.
Сбои сети. Чтение из Moodle (все ручки, кроме отправки ответа), загрузка файлов и список
релизов повторяются до трёх раз с паузой 2 с при обрыве, таймауте и HTTP 502–504; на 4xx,
ошибку сертификата и ошибку в теле ответа Moodle повторов нет. Отправка ответа, релизы и
загрузка видео идут одной попыткой. Раздел, который так и не удалось собрать, сводка
пропускает и перечисляет в конце строкой «Не удалось: …» (в JSON — errors).
.digest/ — обычный git-клон, поэтому обновление — study update: git pull --ff-only
и заново вписанный блок в файлах операторов (личное вне блока не трогается). Сводка
study state сама проверяет origin (мягко: fetch до 20 с и одна повторная попытка,
без запросов пароля; молчание GitHub — строка в «Не удалось», не сбой сводки) и, если
есть новое, добавляет в конец строку «Обновление study: v1.0.0 → v1.1.0, 3 коммита — study update»;
агент по своей инструкции предлагает обновиться, но не делает этого без согласия — код
работает с токенами.
Перед pull команда показывает, что приедет: коммиты, diff --stat и результат
git verify-commit верхнего коммита, — и в терминале спрашивает Обновить? [y/N]. Без
терминала (задача агента, скрипт) нужен --yes, иначе печатается тот же план и код возврата 1.
study update --check показывает план без изменений. Подпись проверяется ключом автора из
связки gpg: без него строка «подпись НЕ проверена» — предупреждение, а не запрет; чтобы
проверка что-то значила, импортируйте ключ из подписанных тегов репозитория.
Версии — теги vX.Y.Z на master и GitHub Releases
с заметками по conventional commits; study --version печатает тег и число коммитов
после него. master всегда рабочий — именно его ставит install.py.
Нашли ошибку. Issue или pull request в этот репозиторий — как обычно. Локально код
лучше не править: study update делает git pull --ff-only, и локальный коммит его остановит,
а незакоммиченная правка при следующем pull того же файла даст конфликт. Первый внешний
PR (падение --since all на Windows) вошёл в v1.4.0 вместе с регрессионным тестом.
Материалы. study files <код> показывает появившееся с прошлой сводки, --all — всё,
--pull скачивает документы (.pdf, .docx, .md, .tex, архивы и подобные) до 50 МБ
в <код>/stash/; в пустую stash/ забирает всё — так наполняют её в первый раз (study setup
предлагает это сам). study files --pull без курса проходит по всем папкам из config.env.
Страницы курса, видео и образы пропускаются; ссылки курса (mod_url: онлайн-лекции и т. п.)
показываются строкой «ссылка … → URL» и попадают в «Новое в курсах»; файл, лежащий где
угодно внутри stash/, повторно не качается.
Карточка задания. study task <код> <NN|id|cmid> печатает всё, что нужно, чтобы разобрать
задание: текст целиком, срок, что принимает (число, типы и размер файлов, текст), состояние
ответа, балл и отзыв преподавателя, что уже есть на диске (у release — лаба репозитория
с готовностью отчёта, презентации и видео; у file — <код>/labNN/ и собранные PDF),
файлы stash/ с этим номером в имени. Ничего не сохраняет: источник — ТУИС.
Два источника заданий. mod_assign_get_assignments знает всё про приём ответа, но молчит
о заданиях с ограничением доступа; core_course_get_contents отдаёт их, но знает только имя,
срок, описание и причину недоступности. Слияние делает реестр (study/assigns.py), и на нём
стоят сводка, study task, study assigns и предполёт study submit — поэтому одно и то же
задание везде видно одинаково. У задания, пришедшего только из состава курса, нет assign_id:
карточка покажет срок и причину («нужна группа …»), а отправка откажет — слать некуда.
Ответ на лабораторную (профиль release). study answer <код> <NN> собирает текст ответа:
репозитории — из git-remote, тег — последний или --tag, PDF отчёта и презентации — из _output/.
Ссылок на скринкасты в API нет, они хранятся в <код>/tuis/labNN.env (четыре записи
на двух площадках плюс два плейлиста); первый запуск создаёт файл пустым, второй пишет
<код>/tuis/labNN.md. Затем
study submit <id> --text <код>/tuis/labNN.md --attach <отчёт.pdf> <презентация.pdf>:
план сверяет текст и файлы с настройками задания, с --confirm загружает файлы
и отправляет (старый путь study upload → --files <itemid> остаётся). Оба файла
лежат вне репозитория курса. Полный чек-лист от выполнения до ответа —
docs/lab-submission.md.
| Файл | Для кого |
|---|---|
docs/AGENTS.md |
ИИ-агент: что здесь лежит, команды, правила, грабли — ставится study agent |
docs/daily-digest-prompt.md |
задача ежедневной сводки |
docs/lab-submission.md |
чек-лист сдачи работы с релизом и скринкастами |
docs/tuis-api.md |
ручки Moodle и их особенности |
docs/hosting-api.md |
API GitVerse и SourceCraft |
.digest/
├── study запускающий файл (лаунчер); на Windows включает UTF-8 и VT-режим
├── src/study/ пакет: cli · digest (сводка) · moodle · hosting · rutube · files
│ · answer · courses · agent · setup · config · net · fmt · local · snapshot
├── docs/ документация
├── tests/ unittest, только stdlib; сеть подменена, ответы ТУИС — в fixtures/
├── install.py установка: зависимости, клон, затем study setup
├── setup.sh обёртка над install.py ради старой ссылки
├── config.env.example шаблон настроек
├── pyproject.toml настройки проверки кода (ruff); пакет через pip не ставится
└── config.env · .secrets/ · .state.json · state/ личное, вне git
Все внешние вызовы идут через src/study/net.py (urllib), особенности каждого API спрятаны
в его клиенте; cli.py только разбирает аргументы и печатает.
python3 -m unittest -v # тесты, без сети и токенов (см. ниже)
uvx ruff check src tests study install.py # стиль и ошибки, правила в pyproject.toml
shellcheck setup.shТесты не ходят в сеть: tests/__init__.py подменяет urllib.request.urlopen
заглушкой, которая падает на любом вызове, а тесты сетевых модулей ставят поверх net.send
tests/fakes.py: FakeNet — очередь ответов по (метод, подстроки: в «МЕТОД url» или целиком
поле формы k=v) и запись всего отправленного (form, json_body, files, заголовки,
сырое тело). Ответы Moodle — tests/fixtures/*.json по форме из docs/tuis-api.md, эталон
сводки — fixtures/digest.md; время в них отсчитано от fakes.NOW (16.09.2026 09:00 MSK),
тесты сводки фиксируют time.time и TZ. Git подменяется картой ответов (hosting) или
временными репозиториями (answer, state, update, install); токены — тестовые строки
во временном config.env, переменные окружения study и глобальный gitconfig на тесты не влияют.
study setup и install.py проходятся с подменёнными input/getpass. Кодировка у каждого
read_text/write_text/open указана явно — за этим следит tests/test_style.py. Тесты,
которые качают файлы, подменяют ROOT временным каталогом — иначе запись уйдёт в настоящий
<код>/stash/. Синтаксис — Python 3.8: без match, X | Y в аннотациях, removeprefix.
То же самое гоняет CI на ubuntu, macos и windows с Python 3.8 и 3.12 при каждом push
(TZ=MSK-3: на Windows нет tzset, пояс задаётся окружением). Коммиты — conventional
(feat, fix, docs, refactor, style, test, ci), версии — теги vX.Y.Z и Releases.
MIT.