feat(site-finder): weight profiles service — Pydantic + CRUD (#114 sub-PR 2/4) #137

Merged
lekss361 merged 1 commit from feat/114-weight-profiles-service into main 2026-05-14 21:29:37 +00:00
lekss361 commented 2026-05-14 21:26:05 +00:00 (Migrated from github.com)

Summary

#114 sub-PR 2 of 4. Backend Pydantic v2 models + CRUD service.

API (backend/app/services/site_finder/weight_profiles.py)

  • list/get/get_default/create/update/delete_profile
  • resolve_weights(db, user_id?, profile_id?) → falls back to _SYSTEM_POI_WEIGHTS

Validation

  • ALLOWED_CATEGORIES (mirror parcels.py _POI_WEIGHTS keys)
  • Weight bounds [-2.0, 3.0]
  • Default-uniqueness в одной transaction

Tests

14 passing (mock-based, no DB).

Vault

code/modules/Module_Weight_Profiles_Service.md NEW

Refs: #114

## Summary #114 sub-PR 2 of 4. Backend Pydantic v2 models + CRUD service. ## API (`backend/app/services/site_finder/weight_profiles.py`) - `list/get/get_default/create/update/delete_profile` - `resolve_weights(db, user_id?, profile_id?)` → falls back to `_SYSTEM_POI_WEIGHTS` ## Validation - ALLOWED_CATEGORIES (mirror parcels.py `_POI_WEIGHTS` keys) - Weight bounds [-2.0, 3.0] - Default-uniqueness в одной transaction ## Tests 14 passing (mock-based, no DB). ## Vault `code/modules/Module_Weight_Profiles_Service.md` NEW Refs: #114
lekss361 commented 2026-05-14 21:29:25 +00:00 (Migrated from github.com)

Bot review (SHA be3b60e, CI )

Чистая foundation для #114 sub-PR 2/4. Service module + 14 unit-тестов, inert до sub-PR 3 (endpoints).

Validation surface

Pydantic v2 idiomaticfield_validator с @classmethod, model_config = {"from_attributes": True}
Shared _validate_weights_dict — DRY между WeightProfileCreate и WeightProfileUpdate
ALLOWED_CATEGORIES (11 keys) mirrors _POI_WEIGHTS в parcels.py — единый source of truth
Bounds [-2.0, 3.0] покрывают и penalty (tram_stop = −0.5) и boost (park = +2.5)

Transaction safety

is_default atomic toggle_UNSET_DEFAULT + INSERT/UPDATE + db.commit() в одной tx. Race из «два profile is_default=TRUE одновременно» невозможна.
update_profile PATCH semantics — только заданные поля в sets; если ничего кроме updated_at = NOW() — пропускается UPDATE.

resolve_weights priority chain

Корректный приоритет:

  1. profile_id + user_id → конкретный профиль
  2. user_id only → default-профиль
  3. Иначе → system _SYSTEM_POI_WEIGHTS

Empty weights fallback — если профиль найден но weights = {}, возвращаются system defaults (covered тестом test_resolve_weights_empty_profile_weights_fallback)
dict(profile.weights) — возвращается копия, не reference (covered test_resolve_weights_system_default_returns_copy)

Тесты (14)

Покрытие хорошее: validation (unknown cat / >MAX / <MIN / valid / empty / boundary / update None / update unknown) + resolve_weights (no args / copy semantics / default profile / specific profile / not-found fallback / empty-weights fallback).

Минор (не блокеры — fix в sub-PR 3 или ignore)

  1. db: Any в сигнатурах — лучше Session (sqlalchemy.orm). Type-hint hygiene, не runtime issue.
  2. update_profile делает get_profile дважды — start для existence check + end для return. Slightly wasteful, но readable. Если станет hot path — можно вернуть результат RETURNING * от UPDATE.
  3. Tests monkey-patch module attrs через wp_module.get_profile = lambda ... + try/finally restore. Работает корректно, но pytest-mock mocker.patch.object чище и auto-restoring. Не влияет на корректность.

approve merge

## Bot review (SHA `be3b60e`, CI ✅) Чистая foundation для #114 sub-PR 2/4. Service module + 14 unit-тестов, inert до sub-PR 3 (endpoints). ### Validation surface ✅ **Pydantic v2 idiomatic** — `field_validator` с `@classmethod`, `model_config = {"from_attributes": True}` ✅ **Shared `_validate_weights_dict`** — DRY между `WeightProfileCreate` и `WeightProfileUpdate` ✅ **`ALLOWED_CATEGORIES` (11 keys)** mirrors `_POI_WEIGHTS` в `parcels.py` — единый source of truth ✅ **Bounds `[-2.0, 3.0]`** покрывают и penalty (tram_stop = −0.5) и boost (park = +2.5) ### Transaction safety ✅ **`is_default` atomic toggle** — `_UNSET_DEFAULT` + `INSERT/UPDATE` + `db.commit()` в одной tx. Race из «два profile is_default=TRUE одновременно» невозможна. ✅ **`update_profile` PATCH semantics** — только заданные поля в `sets`; если ничего кроме `updated_at = NOW()` — пропускается UPDATE. ### resolve_weights priority chain Корректный приоритет: 1. `profile_id` + `user_id` → конкретный профиль 2. `user_id` only → default-профиль 3. Иначе → system `_SYSTEM_POI_WEIGHTS` ✅ **Empty `weights` fallback** — если профиль найден но `weights = {}`, возвращаются system defaults (covered тестом `test_resolve_weights_empty_profile_weights_fallback`) ✅ **`dict(profile.weights)`** — возвращается копия, не reference (covered `test_resolve_weights_system_default_returns_copy`) ### Тесты (14) Покрытие хорошее: validation (unknown cat / >MAX / <MIN / valid / empty / boundary / update None / update unknown) + resolve_weights (no args / copy semantics / default profile / specific profile / not-found fallback / empty-weights fallback). ### Минор (не блокеры — fix в sub-PR 3 или ignore) 1. **`db: Any`** в сигнатурах — лучше `Session` (sqlalchemy.orm). Type-hint hygiene, не runtime issue. 2. **`update_profile` делает `get_profile` дважды** — start для existence check + end для return. Slightly wasteful, но readable. Если станет hot path — можно вернуть результат `RETURNING *` от UPDATE. 3. **Tests monkey-patch module attrs** через `wp_module.get_profile = lambda ...` + try/finally restore. Работает корректно, но `pytest-mock` `mocker.patch.object` чище и auto-restoring. Не влияет на корректность. approve merge
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#137
No description provided.