Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions РукосуеваЕД/РукосуеваЕД.md
Original file line number Diff line number Diff line change
@@ -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.<method>(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