Re-review feedback (PR #260 head 8191a85):
1. tests/scrapers/test_nspd_bulk_client.py:360 — test referenced bulk_mod._SEMAPHORE
which was renamed to _SEMAPHORE_LIMIT in previous commit. AttributeError на
полном pytest run. Заменено: assert bulk_mod._SEMAPHORE_LIMIT == 3 (smoke).
max_concurrent <= 3 уже валидирует throttling — capacity check был лишним.
2. app/services/scrapers/nspd_client.py docstrings (lines 483, 506) — обновлены
ссылки NSPDBulkClient._SEMAPHORE(3) → NSPDBulkClient._sem (per-instance,
capacity=3) чтобы соответствовать новой архитектуре.
PR #260 second-opinion review 🔴 BLOCK → fixes:
1. nspd_bulk_client.py: _SEMAPHORE module-level → per-instance self._sem
создаётся в __aenter__ под текущий event loop. Sequential asyncio.run()
в search_by_quarter (11 layers × asyncio.run() per layer) bind'ил
module-level semaphore к first loop'у → RuntimeError на втором вызове →
все cells return_exceptions=True проглатывали → grid возвращал 0 features.
2. nspd_sync.py: harvest_quarter добавлен time_limit=900 (hard) рядом с
soft_time_limit=600. Safety net если task игнорит SoftTimeLimitExceeded.
3. tests/test_nspd_client.py: добавлен _make_fake_grid_walk helper +
monkeypatch NSPDClient.get_features_in_bbox_grid в 3 тестах
(search_by_quarter_core_only / _with_zouit / _layers_fetched_with_risks).
Без этого area layers били бы по живому NSPD API → CI flaky.
Tests: 37 passed, 1 skipped (3 area-dispatch + 34 grid-walk).