feat(#104): admin endpoint POST /admin/scrape/ekburg-permits для manual trigger #239

Merged
lekss361 merged 1 commit from feat/104-ekburg-permits-admin-endpoint into main 2026-05-17 05:21:16 +00:00
Owner

Summary

  • Добавлен POST /api/v1/admin/scrape/ekburg-permits в admin_scrape.py по паттерну trigger_pzz_sync / trigger_poi_sync
  • TriggerEkburgPermitsRequest: year int|None, ge=2022, le=2030
  • year=None -> refresh_all.apply_async() (scope all_years_2022_2026, ~3-8 мин)
  • year=N -> refresh_year.apply_async(args=[N]) (scope year_N, ~1 мин)
  • Endpoint защищён AdminTokenAuth (тот же паттерн)
  • 4 smoke-теста: refresh_all / refresh_year / invalid_year (2021, 2031) -> 422 / no_token -> 401/503

Test plan

  • Lint: ruff check OK (pre-commit passed)
  • year=2021 -> 422, year=2031 -> 422
  • After merge smoke (один год): curl -X POST https://gendsgn.ru/api/v1/admin/scrape/ekburg-permits -H "X-Admin-Token: $SCRAPE_ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"year": 2026}'
  • After merge full refresh: curl -X POST https://gendsgn.ru/api/v1/admin/scrape/ekburg-permits -H "X-Admin-Token: $SCRAPE_ADMIN_TOKEN" -d '{}'
  • Verify DB: SELECT permit_type, source_year, count(*) FROM ekburg_construction_permits GROUP BY 1,2 ORDER BY 2,1;

Closes #104

## Summary - Добавлен POST /api/v1/admin/scrape/ekburg-permits в admin_scrape.py по паттерну trigger_pzz_sync / trigger_poi_sync - TriggerEkburgPermitsRequest: year int|None, ge=2022, le=2030 - year=None -> refresh_all.apply_async() (scope all_years_2022_2026, ~3-8 мин) - year=N -> refresh_year.apply_async(args=[N]) (scope year_N, ~1 мин) - Endpoint защищён AdminTokenAuth (тот же паттерн) - 4 smoke-теста: refresh_all / refresh_year / invalid_year (2021, 2031) -> 422 / no_token -> 401/503 ## Test plan - [ ] Lint: ruff check OK (pre-commit passed) - [ ] year=2021 -> 422, year=2031 -> 422 - [ ] After merge smoke (один год): curl -X POST https://gendsgn.ru/api/v1/admin/scrape/ekburg-permits -H "X-Admin-Token: $SCRAPE_ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"year": 2026}' - [ ] After merge full refresh: curl -X POST https://gendsgn.ru/api/v1/admin/scrape/ekburg-permits -H "X-Admin-Token: $SCRAPE_ADMIN_TOKEN" -d '{}' - [ ] Verify DB: SELECT permit_type, source_year, count(*) FROM ekburg_construction_permits GROUP BY 1,2 ORDER BY 2,1; Closes #104
lekss361 added 1 commit 2026-05-17 05:15:44 +00:00
Wraps refresh_all / refresh_year Celery tasks behind the existing
AdminTokenAuth gate so the table can be populated on demand after
first deploy instead of waiting for the monthly beat (1st of month).

- TriggerEkburgPermitsRequest: year int|None, ge=2022 le=2030
- year=None  -> refresh_all.apply_async()  (scope all_years_2022_2026)
- year=N     -> refresh_year.apply_async(args=[N])  (scope year_N)
- 4 smoke tests: all/year/invalid_year/no_token
Author
Owner

Deep Code Review — verdict: APPROVE

Паттерн идентичен trigger_pzz_sync / trigger_poi_sync (deferred import → apply_async() → return task_id). Auth через AdminTokenAuth (Depends(verify_admin_token), 401/503) корректно. Pydantic year: int | None = Field(default=None, ge=2022, le=2030)POST {} работает, тест подтверждает.

Key findings:

  • Task names tasks.ekburg_permits_sync.refresh_year / refresh_all зарегистрированы в celery_app.py includes + beat schedule ekburg-permits-monthly уже использует refresh_all (вместе работают: beat и manual могут пересечься, но ON CONFLICT (permit_type, permit_number) DO UPDATE делает idempotent, lock не нужен).
  • 4 теста корректно покрывают: refresh_all/refresh_year/invalid_year (parametrized 2021/2031/1999/9999) / no_token (401 или 503). Mock Celery — appropriate для unit-теста endpoint'а.
  • Underlying task использует with db.begin_nested() per-row SAVEPOINT + CAST(:raw_row AS jsonb) — соответствует backend.md rules.

Nits (не блокирующее):

  • queued_at: "now" — literal string вместо ISO timestamp (тот же паттерн в pzz/poi-sync, established convention).
  • le=2030 шире чем данные EKBURG_PERMITS_URLS (только 2022-2026) — год 2027-2030 пройдёт валидацию, но task вернёт {"errors": 1} через EKBURG_PERMITS_URLS.get(year) → None. Forward-looking, OK.
  • Vault code/modules/ekburg_permits_parser.md уже содержит описание endpoint'а и тестов — actualize done.

Мержу.

**Deep Code Review — verdict: APPROVE** Паттерн идентичен `trigger_pzz_sync` / `trigger_poi_sync` (deferred import → `apply_async()` → return `task_id`). Auth через `AdminTokenAuth` (Depends(verify_admin_token), 401/503) корректно. Pydantic `year: int | None = Field(default=None, ge=2022, le=2030)` — `POST {}` работает, тест подтверждает. **Key findings:** - Task names `tasks.ekburg_permits_sync.refresh_year` / `refresh_all` зарегистрированы в `celery_app.py` includes + beat schedule `ekburg-permits-monthly` уже использует `refresh_all` (вместе работают: beat и manual могут пересечься, но `ON CONFLICT (permit_type, permit_number) DO UPDATE` делает idempotent, lock не нужен). - 4 теста корректно покрывают: refresh_all/refresh_year/invalid_year (parametrized 2021/2031/1999/9999) / no_token (401 или 503). Mock Celery — appropriate для unit-теста endpoint'а. - Underlying task использует `with db.begin_nested()` per-row SAVEPOINT + `CAST(:raw_row AS jsonb)` — соответствует backend.md rules. **Nits (не блокирующее):** - `queued_at: "now"` — literal string вместо ISO timestamp (тот же паттерн в pzz/poi-sync, established convention). - `le=2030` шире чем данные `EKBURG_PERMITS_URLS` (только 2022-2026) — год 2027-2030 пройдёт валидацию, но task вернёт `{"errors": 1}` через `EKBURG_PERMITS_URLS.get(year)` → None. Forward-looking, OK. - Vault `code/modules/ekburg_permits_parser.md` уже содержит описание endpoint'а и тестов — actualize done. Мержу.
lekss361 merged commit b8a15cac09 into main 2026-05-17 05:21:16 +00:00
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#239
No description provided.