diff --git "a/\320\240\321\203\320\272\320\276\321\201\321\203\320\265\320\262\320\260\320\225\320\224/\320\240\321\203\320\272\320\276\321\201\321\203\320\265\320\262\320\260\320\225\320\224.md" "b/\320\240\321\203\320\272\320\276\321\201\321\203\320\265\320\262\320\260\320\225\320\224/\320\240\321\203\320\272\320\276\321\201\321\203\320\265\320\262\320\260\320\225\320\224.md" new file mode 100644 index 0000000..2d9e5f1 --- /dev/null +++ "b/\320\240\321\203\320\272\320\276\321\201\321\203\320\265\320\262\320\260\320\225\320\224/\320\240\321\203\320\272\320\276\321\201\321\203\320\265\320\262\320\260\320\225\320\224.md" @@ -0,0 +1,60 @@ +# 103. Рефакторинг + +## 1. Выбранный проект + +- **Репозиторий:** [evrone/toggl-python](https://github.com/evrone/toggl-python) +- **Описание:** типизированная Python-обёртка над Toggl Track API v9 с валидацией запросов на стороне клиента (через Pydantic), чтобы избегать лишних сетевых запросов с заведомо некорректными параметрами. +- **Стек технологий:** Python (3.8–3.13), HTTPX (HTTP/2-клиент), Pydantic v2, Poetry, pytest + respx (мок HTTP-запросов) + pytest-cov, ruff (линтер/форматтер), pre-commit, nox. + +## 2. Использованные принципы рефакторинга + +- **Extract Method / Pull Up Method** — повторяющийся код вынесен в отдельные методы и поднят в общий базовый класс, от которого уже наследовались все затронутые классы. +- **DRY (Don't Repeat Yourself)** — устранены два независимых источника дублирования кода (см. ниже). + +## 3. Описание выполненного рефакторинга + +### Обнаруженные проблемы + +Проект построен вокруг трёх классов-сущностей — `Workspace`, `CurrentUser`, `ReportTimeEntry` — каждый из которых наследуется от общего `ApiWrapper` (даёт HTTP-клиент и метод проверки статуса ответа). При этом в самих сущностях (`toggl_python/entities/*.py`) повторялся один и тот же шаблон кода: + +1. **Дублирование запрос → валидация ответа** (~25 повторений суммарно по трём файлам): + ```python + response = self.client.(url=..., params=..., json=...) + self.raise_for_status(response) + response_body = response.json() + return SomeResponse.model_validate(response_body) + ``` + и его варианты — для списков (`[Schema.model_validate(x) for x in response_body]`) и для булевых ответов (`return response.is_success`). + +2. **Дублирование сериализации payload'а** (13 повторений): + ```python + payload = payload_schema.model_dump(mode="json", exclude_none=True[, exclude_unset=True]) + ``` + одна и та же связка параметров `model_dump` перед каждым запросом, формирующим query-параметры или тело запроса. + +### Как проблемы были решены + +1. В базовый класс `ApiWrapper` (`toggl_python/api.py`) добавлены четыре защищённых метода, обобщающих типовые сценарии обращения к API: + - `_request` — выполнить запрос и проверить статус; + - `_request_and_validate` — выполнить запрос и провалидировать тело ответа как один объект схемы; + - `_request_and_validate_list` — то же самое для списка объектов; + - `_request_and_check_success` — выполнить запрос и вернуть `bool` по успешности. + + Методы типизированы через `TypeVar`, ограниченный `pydantic.BaseModel`, поэтому подходят для любой схемы ответа. Все 24 метода в `Workspace`, `CurrentUser` и `ReportTimeEntry` переведены на использование этих хелперов — каждый метод-эндпоинт теперь состоит из сборки параметров/тела запроса и одного вызова хелпера. + +2. В `toggl_python/schemas/base.py` добавлена функция `dump_payload()`, инкапсулирующая повторяющийся вызов `model_dump(mode="json", exclude_none=True, exclude_unset=...)`. Вызовы с другой семантикой (`model_dump_json()`, "сырой" `model_dump(mode="json")` без `exclude_none` в `BulkEditMethodParams`) намеренно не тронуты, так как относятся к другому паттерну. + +Рефакторинг **чисто структурный** — поведение и публичный интерфейс классов не изменились, что подтверждено полным прогоном существующего тестового набора без единой правки в тестах бизнес-логики. + +### Тестирование + +- Добавлен `tests/test_api.py` — юнит-тесты новых хелперов `ApiWrapper` (успешные ответы для одиночного объекта, списка, булевого результата, а также проброс исключения `BadRequest` при ошибочном статусе), с моками HTTP через `respx`. +- Добавлен `tests/test_schemas_base.py` — юнит-тесты `dump_payload()` (поведение `exclude_none` и `exclude_unset`). +- Весь остальной тестовый набор (`test_workspace.py`, `test_user.py`, `test_project.py`, `test_time_entry.py`, `test_report_time_entry.py`) прошёл **без изменений**, что и подтверждает сохранение исходной функциональности. +- Итог прогона (`pytest -m "not integration"`): 191 пройден, покрытие 97.85% (порог проекта — 95%). Линтер `ruff check` не выдаёт замечаний по затронутым файлам. + +Изменения оформлены пятью атомарными коммитами в отдельной ветке `refactor/entities-deduplication`: добавление хелперов в `ApiWrapper`, рефакторинг `Workspace`, рефакторинг `CurrentUser`, рефакторинг `ReportTimeEntry`, вынесение `dump_payload()`. + +## 4. Ссылка на Pull Request + +https://github.com/evrone/toggl-python/pull/104