Проект 1. Python CLI
Выполнять после: раздела 06
Опирается на разделы: 03, 04, 05, 06
Ориентировочное время: 4 часа
Оценивание: rubric, проходной результат 70
Задача
Написать и контейнеризовать инструмент командной строки logstat, считающий статистику по журналам в формате JSON Lines.
Задача выбрана так, чтобы естественным образом потребовать всё, что проверяет проект: аргументы, конфигурацию из двух источников, разделение потоков вывода, различимые коды возврата и данные, приходящие как из файла, так и со стандартного ввода.
Что делает инструмент
Читает журнал, где каждая строка — объект JSON:
{"ts": "2026-08-04T10:00:00Z", "status": 200, "path": "/api/items", "method": "GET", "ms": 12}
{"ts": "2026-08-04T10:00:02Z", "status": 404, "path": "/api/missing", "method": "GET", "ms": 3}
Группирует записи по указанному полю, считает количество и долю, выводит таблицу или JSON.
docker run --rm -i logstat --group-by status --top 5 < access.jsonl
Требования
Обязательные
| № | Требование | Проверяется |
|---|---|---|
| 1 | Аргументы командной строки доходят до приложения через docker run | Checklist 1 |
| 2 | Конфигурация читается из файла TOML и из переменных окружения | Checklist 2 |
| 3 | Приоритет источников: аргументы > окружение > файл > умолчания | Checklist 2 |
| 4 | Результат идёт в stdout, диагностика — в stderr | Checklist 3 |
| 5 | Не менее трёх различимых кодов возврата | Checklist 4 |
| 6 | Чтение со стандартного ввода и из файла | Checklist 5 |
| 7 | Unit-тесты, покрытие не ниже 90 % | Checklist 6 |
| 8 | Тесты выполняются как стадия сборки и останавливают её при провале | Checklist 6 |
| 9 | Кэш сборки работает: изменение кода не пересобирает зависимости | Checklist 7 |
| 10 | Запуск не от root, идентификатор числовой | Checklist 8 |
Требования, невыполнение которых снимает работу
Независимо от суммы баллов (rubric):
- аргументы
docker runне доходят до приложения; - код возврата приложения теряется и всегда равен нулю;
- образ запускается от
rootбез обоснования; - тесты в образе есть, но их провал не останавливает сборку.
Архитектура
Коды возврата
Требование 5 — не формальность. Инструмент командной строки встраивают в скрипты, и там важно различать случаи, которые человек различает интонацией:
| Код | Значение | Пример |
|---|---|---|
| 0 | Успех, записи найдены | Обычная работа |
| 1 | Ошибка выполнения | Файл не найден, данные испорчены при --strict |
| 2 | Ошибка использования | Неверное значение параметра |
| 3 | Записей не найдено | Фильтр не дал совпадений |
Различие 0 и 3 — существенное. logstat --filter status=500 | wc -l в скрипте мониторинга должен уметь отличить «ошибок нет» от «журнал не прочитался». Тот же приём применён в уроке 16.4 к сканированию образов.
Различие 1 и 2 нужно по другой причине: ошибка использования означает, что вызывающий должен изменить команду, а ошибка выполнения — что изменилось окружение.
Приоритет конфигурации
умолчания ← файл TOML ← переменные окружения ← аргументы
низший высший
Отдельное требование, вытекающее из практики: инструмент должен уметь показать, откуда взялось каждое значение. Без этого на вопрос «почему применилось не то, что я задал» отвечают перебором.
Структура файлов
logstat/
├── Dockerfile
├── .dockerignore
├── pyproject.toml
├── README.md
├── logstat.example.toml
├── src/
│ └── logstat/
│ ├── __init__.py
│ ├── __main__.py # python -m logstat
│ ├── cli.py # разбор аргументов, потоки, коды возврата
│ ├── config.py # три источника и их приоритет
│ ├── errors.py # исключения с прикреплённым кодом возврата
│ └── stats.py # разбор записей и подсчёт
└── tests/
├── test_cli.py
├── test_config.py
└── test_stats.py
Раскладка src/ выбрана не для красоты: она исключает случайный импорт пакета из рабочего каталога вместо установленного, из-за которого тесты проходят локально и падают в образе (урок 6.3).
Пошаговый план
Шаг 1. Подсчёт без оболочки
Написать stats.py: разбор JSON Lines, группировка, подсчёт.
Решить сразу два вопроса, которые потом трудно менять:
- что делать со строкой, которая не разобралась, — пропустить или прервать работу;
- что делать с записью, где нужного поля нет.
Опора: 6.2.
Шаг 2. Конфигурация
Написать config.py: три источника, приоритет, приведение типов с внятными сообщениями.
Требование, о котором легко забыть: отсутствующий аргумент — не то же самое, что заданный пустым. --top без значения не должен перекрывать переменную окружения.
Шаг 3. Оболочка командной строки
Написать cli.py: argparse, чтение из файла или stdin, вывод результата в stdout, диагностики в stderr, коды возврата.
Шаг 4. Тесты
Покрыть все четыре кода возврата и разделение потоков. Проверять stdout и stderr раздельно: тест, читающий их вместе, пропустит самую частую ошибку — диагностику, попавшую в результат.
Опора: раздел 15.
Шаг 5. Dockerfile
Multi-stage: основа, установка, тесты, итоговый образ.
Порядок инструкций определяет, работает ли кэш: файлы, влияющие на установку зависимостей, копируются до исходников.
Шаг 6. Проверка
Пройти CHECKLIST.md целиком, выполняя команды, а не просматривая их.
Критерии завершения
Проект завершён, когда выполняются все утверждения:
docker run --rm logstat --versionпечатает версию и возвращает 0.echo '{"status":200}' | docker run --rm -i logstat --top 1печатает таблицу.docker run --rm -i logstat --filter status=999 --top 3 < данныевозвращает 3.docker run --rm -e LOGSTAT_TOP=неверно -i logstat --top 3 < данныевозвращает 2.docker run --rm -i logstat --format json --top 3 < данные | python3 -m json.toolразбирается без ошибок.docker build --target test .падает, если сломать любой тест.- Повторная сборка после изменения только
src/не переустанавливает зависимости. docker run --rm logstat id -u— либо не работает (нетidв образе), либо печатает не0.
Восьмой пункт сформулирован так намеренно: отсутствие id в образе — это хорошо, и проверять USER в таком случае нужно через docker image inspect.
Дополнительные задания
| № | Задание | Что добавляет |
|---|---|---|
| 1 | Ключ --show-config с указанием источника каждого значения | Диагностируемость конфигурации |
| 2 | Режим --strict: прерываться на первой неразобранной строке | Различие «пропустить» и «отвергнуть» |
| 3 | Устойчивость к BrokenPipeError при logstat ... | head | Корректное поведение в конвейере |
| 4 | Вывод в JSON, пригодный для передачи дальше | Инструмент как звено конвейера |
| 5 | Образ на базе distroless или alpine; сравнить размер | Оптимизация (3.5) |
| 6 | Проверка воспроизводимости: две сборки подряд дают один digest | 14.3 |
Задания 1–4 реализованы в эталонном решении; 5 и 6 — нет.
Отношение к примеру python-cli
В курсе есть рабочий пример resources/examples/python-cli — инструмент wordfreq, разбираемый в уроке 6.8.
Он решает другую задачу и служит образцом устройства, а не ответом. Смотреть на него можно и нужно: он показывает раскладку файлов, форму ENTRYPOINT и CMD, стадию тестов. Скопировать из него решение не получится — там нет ни конфигурации из трёх источников, ни четвёртого кода возврата, ни фильтрации.
Навигация
Список проверок →
Эталонное решение → — открывать после собственной попытки
Вернуться к проектам
Главное оглавление