Локальный грейдер для курсов «Поколение Python» на Stepik. Скачивает данные задачи с сайта и позволяет не только проверить решение локально, но и сравнить несколько решений более честно: сначала по корректности, потом по benchmark-метрикам.
Курсы:
- Что умеет
- Архитектура модулей
- Структура проекта
- Установка
- Быстрый старт
- Работа с API Stepik
- Режимы работы
- Формат тест-кейсов
- Конфигурация
- Зависимости
- Диагностика
- Ограничения и безопасность
- Что изменилось по сравнению с оригиналом
- Python версия
| Скрипт | Архитектурный слой | Что делает |
|---|---|---|
storage.py |
Infrastructure / Utilities | Чтение и запись JSON-файлов (load_json_file, save_json_file, save_secrets); нет зависимостей от других модулей проекта |
stepik_client.py |
Infrastructure / HTTP | OAuth2-авторизация, requests.Session, GET-запросы к Stepik REST API, скачивание сабмишнов |
at_first.py |
Domain / Application | Управление конфигом и secrets, разбор URL шага, построение директорий задач (slugify, build_task_directory), сохранение файлов задачи, автоизвлечение тест-кейсов из HTML-таблицы и ZIP-архивов, оркестрация вызовов API |
test.py |
Application | Проверяет решения локально, сравнивает несколько решений, запускает subprocess-benchmark и timeit-microbench |
executor.py |
Infrastructure | Запускатель решений: compile + exec с таймаутом и изолированным namespace |
microbench_runner.py |
Infrastructure | Timeit-микробенчмарк через exec + contextlib (без запуска нового процесса) |
diagnostik_stepik.py |
Infrastructure | Диагностика: проверяет структуру ответа API и корректность токена авторизации |
Основные возможности:
- ✅ Запуск решений против наборов тест-кейсов (
tests/N+tests/N.clue) - 📋 Автоматическое извлечение тест-кейсов из HTML-таблицы в тексте задачи Stepik
- 📦 Автоскачивание тестов из ZIP-архива по ссылке в тексте задачи
- 🔗 Обнаружение ссылок на GitHub-тесты с подсказкой скачать вручную
- 📊 Сравнение нескольких решений одной задачи в таблице
- 🚀 Subprocess-бенчмарк с замером времени и памяти
- ⚡ Timeit-микробенчмарк через
exec+contextlib(без запуска нового процесса) - 🔍 Диагностика окружения и авторизация через Stepik API
Граф зависимостей — DAG без циклов:
at_first.py ──→ storage.py
at_first.py ──→ stepik_client.py
stepik_client.py ──→ storage.py
test.py ──→ executor.py
test.py ──→ microbench_runner.py
diagnostik_stepik.py ──→ stepik_client.py
Слои (снизу вверх):
┌─────────────────────────────────────────────────┐
│ Domain / Application │
│ at_first.py │ test.py │ diagnostik_stepik │
├─────────────────────────────────────────────────┤
│ Infrastructure │
│ stepik_client.py │ executor.py │
│ microbench_runner.py │
├─────────────────────────────────────────────────┤
│ Infrastructure / Utilities (leaf, no deps) │
│ storage.py │
└─────────────────────────────────────────────────┘
storage.py — leaf-модуль: не импортирует ничего из проекта, легко тестируется изолированно.
Stepik-Python-Grader/
├── test.py # Главный модуль: 4 режима работы
├── executor.py # Запускатель решений: compile + exec с таймаутом
├── microbench_runner.py # Timeit-микробенчмарк через exec
├── at_first.py # Domain: конфиг, slugify, построение папок, оркестрация API
├── stepik_client.py # Infrastructure: OAuth2, requests.Session, Stepik API
├── storage.py # Utilities: load/save JSON, save_secrets (нет project-зависимостей)
├── diagnostik_stepik.py # Диагностика API и токена
├── tests/
│ ├── test_executor.py
│ ├── test_microbench.py
│ └── test_slugify.py
├── pyproject.toml # Конфигурация проекта (ruff, pytest, зависимости)
├── requirements.txt # Runtime-зависимости
├── secrets.json.example # Шаблон файла с OAuth-токеном
├── stepik_config.json.example # Шаблон конфига Stepik
└── README.md
Локально обычно появляются:
StepikTasks/
stepik_config.json
secrets.json
errors.txt
stepik_diagnostics/
Эти файлы и папки держи в .gitignore.
git clone https://github.com/ArtVsMark/Stepik-Python-Grader.git
cd Stepik-Python-Grader# Windows
python -m venv .venv
.venv\Scripts\activate
# macOS / Linux
python3 -m venv .venv
source .venv/bin/activatepip install -r requirements.txtДля разработки (линтер, тесты):
pip install -e ".[dev]"python test.pyПри запуске появится меню:
Choose mode:
1 - test single file
2 - compare all solutions in top-level folder
3 - benchmark passed solutions
4 - microbench (timeit, any solution type)
Memory mode: parent process (fast, rough)
Subprocess timeout: 10.0s per test
Enter mode (1/2/3/4):
1. Создай OAuth-приложение на Stepik
- Зайди на https://stepik.org/oauth2/applications/
- Нажми + New Application
- Заполни поля:
| Поле | Значение |
|---|---|
| Name | любое, например my-grader |
| Client type | Confidential |
| Authorization grant type | Authorization code |
| Redirect uris | http://localhost:8080/callback |
- Нажми Save — Stepik покажет
Client IDиClient Secret.
Скопируй шаблон:
cp secrets.json.example secrets.jsonЗаполни своими значениями:
{
"client_id": "<Client ID из настроек приложения Stepik>",
"client_secret": "<Client Secret из настроек приложения Stepik>",
"redirect_uri": "http://localhost:8080/callback",
"access_token": "",
"refresh_token": "",
"expires_at": 0
}| Поле | Что это |
|---|---|
client_id |
ID OAuth-приложения в Stepik |
client_secret |
секрет OAuth-приложения |
redirect_uri |
адрес для возврата после авторизации |
access_token |
текущий токен доступа, заполняется автоматически |
refresh_token |
токен обновления, заполняется автоматически |
expires_at |
время истечения access_token (Unix-timestamp), заполняется автоматически |
secrets.json— локальный файл, не должен попадать в Git. При первом запуске оставьaccess_token,refresh_token,expires_atпустыми — скрипт заполнит их сам черезstorage.save_secrets().
python at_first.pyПри первом запуске:
- будет предложено выбрать корневую папку (по умолчанию
StepikTasks) и путь кsecrets.json, - откроется браузер для подтверждения доступа,
- после успешной авторизации токены сохранятся в
secrets.jsonчерезstorage.save_secrets().
Введи URL шага, например:
URL шага: https://stepik.org/lesson/569749/step/4?unit=564263
Скрипт создаст структуру:
StepikTasks/
└── название-курса/
└── название-секции/
└── название-урока/
└── 04/ # только номер, если у шага нет заголовка
└── 04-название-шага/ # номер + slug, если заголовок есть
├── task4_1.py # основное решение (из шаблона задачи или пустой)
├── task4_2.py # заготовка для альтернативного решения (всегда создаётся)
├── solution.py # последний сабмишн с сайта (если доступен)
├── meta.json # метаданные шага (id, lesson, course, ...)
├── task.md # текст задачи в Markdown/HTML
└── tests/
├── 1 # входные данные теста №1
├── 1.clue # ожидаемый вывод теста №1
├── 1.type # тип теста (только для function-style)
├── 2
├── 2.clue
└── ...
Схема именования рабочих файлов:
| Файл | Содержимое | Создаётся |
|---|---|---|
task{N}_1.py |
шаблон из задачи (или пустой, если шаблона нет) | всегда |
task{N}_2.py |
заготовка для альтернативного решения 1 | всегда (только если файл ещё не существует) |
task{N}_3.py и далее |
альтернативные решения 2, 3, … | вручную |
solution.py |
последний сабмишн с сайта | если сабмишн доступен |
Повторный запуск
at_first.pyдля того же шага не перезапишетtask{N}_2.pyи выше — твои наработки сохранятся.
at_first.py перебирает источники по приоритету — первый успешный выигрывает:
| Приоритет | Источник | Поведение |
|---|---|---|
| 1 | ZIP-ссылка в HTML задачи | Скачивается автоматически, распаковывается в tests/ |
| 2 | HTML-таблица в тексте задачи | Парсится автоматически в tests/N + tests/N.clue |
| 3 | Ссылка на GitHub в HTML | Адрес печатается в консоль — скачать вручную |
| 4 | Ничего не найдено | Предупреждение ⚠️, остальные файлы уже сохранены |
OAuth-поток полностью реализован в stepik_client.py (create_user_session, authorize_via_browser, refresh_access_token); at_first.py только оркестрирует вызовы.
Быстро прогнать одно решение:
Enter path to solution file (relative or absolute): module1/task1/task1_1.py
module1/task1/task1_1.py: 5/5 tests, total=0.1234s, avg=0.0247s, peak_memory=25.30 MB, status=OK
Проходит по всей папке, находит все task*.py и верифицирует каждый. Результаты — таблица, сгруппированная по задачам.
📂 module1/task1
--------------------------------------------------------------------
File Passed Total time Avg time Peak memory Status Fail test
--------------------------------------------------------------------
module1/task1/task1_1.py 5/5 0.1234 0.0247 25.30 OK -
module1/task1/task1_2.py 5/5 0.1456 0.0291 24.80 OK -
Режим 2 — проверка корректности, не полноценный benchmark.
Запускает N повторений для каждого прошедшего все тесты решения через отдельный процесс. Выводит min / median / mean / max / std-dev и сравнивает решения относительно быстрейшего.
Профили нагрузки (repeats):
| # | Режим | Повторений |
|---|---|---|
| 1 | low | 5 |
| 2 | medium | 15 |
| 3 | high | 50 |
| 4 | custom | 5–100 |
Что показывает benchmark:
| Поле | Значение |
|---|---|
Runs |
всего запусков |
Min |
лучший замер |
Median |
медианное время — главный ориентир |
Mean |
среднее время |
Max |
худший замер |
Std dev |
разброс замеров (мало → стабильно) |
Memory |
пиковая память |
Relative |
относительное время к лучшему решению |
Verdict |
SIMILAR, SLOWER, MUCH SLOWER |
🚀 Benchmark: module1/task1
---------------------------------------------------------------------
File Runs Min Median Mean Max Std dev Memory Relative Verdict
---------------------------------------------------------------------
module1/task1/task1_1.py 25 0.0234 0.0249 0.0250 0.0279 0.0011 25.30 100.0% SIMILAR
module1/task1/task1_2.py 25 0.0257 0.0271 0.0273 0.0301 0.0013 24.80 108.9% SLOWER
Замеряет время через timeit.timeit внутри одного процесса — без накладных расходов на запуск интерпретатора. Поддерживает script-style (с input()) и function-only решения.
Количество вызовов (calls per run):
| # | Режим | Вызовов |
|---|---|---|
| 1 | fast | 500 |
| 2 | normal | 1 000 |
| 3 | thorough | 5 000 |
| 4 | deep | 50 000 |
| 5 | hard | 100 000 |
| 6 | custom | 100–500 000 |
Режим
hard— только для коротких детерминированных функций.
⚡ Micro-bench (timeit): module1/task1
---------------------------------------------------------------------------
File Repeats Min, us Median, us Mean, us Max, us Std dev, us Relative Verdict
---------------------------------------------------------------------------
module1/task1/task1_1.py 1000 12.34 13.01 13.12 15.67 0.82 100.0% SIMILAR
module1/task1/task1_2.py 1000 14.21 15.34 15.45 18.90 1.12 117.9% MUCH SLOWER
module1/
└── task1/
├── task1_1.py # основное решение
├── task1_2.py # альтернативное решение 1
└── tests/
├── 1 # входные данные теста №1 (stdin)
├── 1.clue # ожидаемый вывод теста №1
├── 1.type # тип теста: файл присутствует только для function-style задач,
│ # содержит строку "function"
├── 2
├── 2.clue
└── ...
Типы тестов (*.type):
| Значение в файле | Когда создаётся | Поведение |
|---|---|---|
| (файл отсутствует) | stdin-задача | входные данные подаются через stdin |
function |
function-style задача | входные данные — объявление переменной (x = 5), передаётся через exec |
Кодировка определяется автоматически через chardet.
При скачивании задачи через
at_first.pyфайлыtests/N,tests/N.clueи при необходимостиtests/N.typeсоздаются автоматически из ZIP-архива или HTML-таблицы в тексте задачи. Если ни ZIP, ни таблицы нет — папкуtests/нужно заполнить вручную.
При первом запуске at_first.py предложит указать:
Укажи корневую папку для всех задач Stepik [StepikTasks]:
Укажи путь к secrets.json [secrets.json]:
Значения сохраняются в stepik_config.json. Структура директорий внутри:
StepikTasks/
└── <курс>/<секция>/<урок>/<NN>/ или <NN-шаг>/
В test.py константа SUBPROCESS_TIMEOUT (по умолчанию 10.0 с) защищает от зависания:
SUBPROCESS_TIMEOUT = 10.0 # секундВ executor.py таймаут передаётся через переменную окружения EXECUTOR_TIMEOUT (по умолчанию 10 с). На Unix — signal.alarm; на Windows — SUBPROCESS_TIMEOUT:
TIMEOUT: int = int(os.environ.get("EXECUTOR_TIMEOUT", "10"))MEASURE_CHILD_MEMORY = False # True — честнее, но медленнееFalse— RSS родительского процесса (быстро, приблизительно)True— мониторинг дочернего процесса черезpsutilв отдельном потоке
MICROBENCH_MAX_CASES = 5Ограничивает число тест-кейсов при timeit-замерах для стабильного std-dev.
| Пакет | Назначение | Используется в |
|---|---|---|
requests>=2.34 |
HTTP-запросы к Stepik API, OAuth2, скачивание ZIP | stepik_client.py, at_first.py |
psutil>=5.9 |
Замер памяти и мониторинг процессов | test.py, executor.py |
chardet>=5.0 |
Авто-определение кодировки файлов | executor.py |
Dev-зависимости (pip install -e ".[dev]"):
| Пакет | Назначение |
|---|---|
pytest>=8.2 |
Тестирование |
ruff>=0.4 |
Линтер и форматтер |
Если at_first.py не нашёл данных шага автоматически:
python diagnostik_stepik.pyСкрипт сохранит в папку stepik_diagnostics/:
lesson_debug.jsonstep_debug.jsondiagnostic_result.json
diagnostik_stepik.py также позволяет:
- проверить доступность Stepik API;
- убедиться в корректности токена авторизации;
- получить информацию о курсе, уроке или задаче по ID.
- Режимы 1–3 (
executor.py): решения запускаются через отдельный subprocess. Код компилируется черезcompile(source, "<solution>", "exec")и выполняется в изолированном namespace{"__builtins__": __builtins__}. На Unix —signal.alarm(TIMEOUT); на Windows —SUBPROCESS_TIMEOUT. - Режим 4 (
microbench_runner.py): решения запускаются черезexec(compiled, {})внутри одного процесса.stdin/stdoutперенаправляются черезcontextlib.redirect_stdin/contextlib.redirect_stdout. - Microbench без таймаута: бесконечный цикл в решении подвесит grader. Используй только с проверенными решениями.
- Нет sandbox: grader не изолирует файловую систему или сеть. Запускай только доверенные решения.
Этот форк существенно расширяет оригинальный проект PavloOps/python_generation_grader:
| Возможность | Оригинал | Этот форк |
|---|---|---|
| Проверка одного файла | ✅ | ✅ |
| Сравнение нескольких решений | ❌ | ✅ |
| Subprocess-benchmark | ❌ | ✅ режим 3 |
| Timeit-microbench | ❌ | ✅ режим 4 |
| Разделение корректности и benchmark | ❌ | ✅ |
| Профили нагрузки | ❌ | ✅ low/medium/high/custom |
| Оценка по median (не одиночный замер) | ❌ | ✅ |
| Вердикт SIMILAR / SLOWER / MUCH SLOWER | ❌ | ✅ |
| OAuth2 + скачивание данных задачи с API | ❌ | ✅ |
| Автоизвлечение тест-кейсов из HTML-таблицы | ❌ | ✅ Sprint 4 |
| Автоскачивание тестов из ZIP-архива | ❌ | ✅ Sprint 4 |
| Обнаружение ссылок на GitHub-тесты | ❌ | ✅ Sprint 4 |
Поддержка function-style тестов (*.type) |
❌ | ✅ Sprint 4 |
| Схема файлов task{N}_1.py / task{N}_2.py | ❌ | ✅ Sprint 5 |
| Диагностика API | ❌ | ✅ |
| Поддержка function-only решений | ❌ | ✅ |
Выделенный HTTP/OAuth слой (stepik_client.py) |
❌ | ✅ Sprint 3 |
Утилиты хранилища без project-зависимостей (storage.py) |
❌ | ✅ Sprint 3 |
| pyproject.toml (ruff, pytest, зависимости) | ❌ | ✅ |
| Pre-commit хуки (ruff check + ruff format) | ❌ | ✅ |
| Unit-тесты (19 тестов) | ❌ | ✅ |
Python 3.11+