Главная/Проекты/Проект

Проект 1. Python CLI

Выполнять после: раздела 06
Опирается на разделы: 03, 04, 05, 06
Ориентировочное время: 4 часа
Оценивание: rubric, проходной результат 70


Задача

Написать и контейнеризовать инструмент командной строки logstat, считающий статистику по журналам в формате JSON Lines.

Задача выбрана так, чтобы естественным образом потребовать всё, что проверяет проект: аргументы, конфигурацию из двух источников, разделение потоков вывода, различимые коды возврата и данные, приходящие как из файла, так и со стандартного ввода.

Что делает инструмент

Читает журнал, где каждая строка — объект JSON:

text
{"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.

bash
docker run --rm -i logstat --group-by status --top 5 < access.jsonl

Требования

Обязательные

ТребованиеПроверяется
1Аргументы командной строки доходят до приложения через docker runChecklist 1
2Конфигурация читается из файла TOML и из переменных окруженияChecklist 2
3Приоритет источников: аргументы > окружение > файл > умолчанияChecklist 2
4Результат идёт в stdout, диагностика — в stderrChecklist 3
5Не менее трёх различимых кодов возвратаChecklist 4
6Чтение со стандартного ввода и из файлаChecklist 5
7Unit-тесты, покрытие не ниже 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 нужно по другой причине: ошибка использования означает, что вызывающий должен изменить команду, а ошибка выполнения — что изменилось окружение.

Приоритет конфигурации

text
умолчания  ←  файл TOML  ←  переменные окружения  ←  аргументы
   низший                                            высший

Отдельное требование, вытекающее из практики: инструмент должен уметь показать, откуда взялось каждое значение. Без этого на вопрос «почему применилось не то, что я задал» отвечают перебором.

Структура файлов

text
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 без значения не должен перекрывать переменную окружения.

Опора: 6.5, 11.2.

Шаг 3. Оболочка командной строки

Написать cli.py: argparse, чтение из файла или stdin, вывод результата в stdout, диагностики в stderr, коды возврата.

Опора: 6.8, 4.6.

Шаг 4. Тесты

Покрыть все четыре кода возврата и разделение потоков. Проверять stdout и stderr раздельно: тест, читающий их вместе, пропустит самую частую ошибку — диагностику, попавшую в результат.

Опора: раздел 15.

Шаг 5. Dockerfile

Multi-stage: основа, установка, тесты, итоговый образ.

Порядок инструкций определяет, работает ли кэш: файлы, влияющие на установку зависимостей, копируются до исходников.

Опора: 5.7, 5.9.

Шаг 6. Проверка

Пройти CHECKLIST.md целиком, выполняя команды, а не просматривая их.


Критерии завершения

Проект завершён, когда выполняются все утверждения:

  1. docker run --rm logstat --version печатает версию и возвращает 0.
  2. echo '{"status":200}' | docker run --rm -i logstat --top 1 печатает таблицу.
  3. docker run --rm -i logstat --filter status=999 --top 3 < данные возвращает 3.
  4. docker run --rm -e LOGSTAT_TOP=неверно -i logstat --top 3 < данные возвращает 2.
  5. docker run --rm -i logstat --format json --top 3 < данные | python3 -m json.tool разбирается без ошибок.
  6. docker build --target test . падает, если сломать любой тест.
  7. Повторная сборка после изменения только src/ не переустанавливает зависимости.
  8. 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Проверка воспроизводимости: две сборки подряд дают один digest14.3

Задания 1–4 реализованы в эталонном решении; 5 и 6 — нет.


Отношение к примеру python-cli

В курсе есть рабочий пример resources/examples/python-cli — инструмент wordfreq, разбираемый в уроке 6.8.

Он решает другую задачу и служит образцом устройства, а не ответом. Смотреть на него можно и нужно: он показывает раскладку файлов, форму ENTRYPOINT и CMD, стадию тестов. Скопировать из него решение не получится — там нет ни конфигурации из трёх источников, ни четвёртого кода возврата, ни фильтрации.


Навигация

Список проверок →
Эталонное решение → — открывать после собственной попытки
Вернуться к проектам
Главное оглавление

Markdown на GitHub ↗