Skip to content
  • Andrey Azeev's avatar
    3501eb07
    SokolovApi: Redis-shared token cache + batch /product?sku=... · 3501eb07
    Andrey Azeev authored
    Подключена lib-aio-redis v1.1.0 (P14/P5 OK):
    - RedisSettings (host/port/password/db/socket_*/max_connections/
      health_check_interval), все OPTIONAL с дефолтами localhost:6379.
    - src/utils/redis_client.py — canon lifespan-обёртка
      redis_lifespan() -> AsyncIterator[AsyncRedis | None] + get_redis +
      RedisDep. При RedisConnectionError на старте yield None —
      приложение продолжает работать с per-worker in-memory кэшем.
    - app.py lifespan: redis_client = enter_async_context(redis_lifespan())
      → app.state.redis. SokolovApi(redis=redis_client) после Redis.
    
    SokolovApi token cache (Redis-first, in-memory fallback):
    - Ключ "sokolov:access_token", TTL 23h (Sokolov docs: 1 день, с запасом).
    - _get_access_token(stale_token=None): cache → Redis → login под Lock.
    - Stale-token aware refresh: на 401 в _get_access_token(stale_token=T)
      под локом проверяем self._token != T и Redis != T — если кто-то
      уже обновил, login не повторяется. Защита от thundering herd
      при N параллельных 401.
    - parse_json_response из lib_aiohttp для login + get_products.
    
    SokolovApi fixes:
    - BaseAPI(__init__) больше не получает headers= (default headers
      перекрывали per-call Bearer для products). Headers теперь per-call.
    - _basic_auth_headers/_bearer_headers — instance methods (раньше
      classmethod без args — login был сломан).
    - get_products(refresh_on_401: bool = True) — новый параметр
      для контроля retry поведения.
    
    Batch endpoint GET /api/sokolov/product?sku=...&sku=... (до 10):
    - Old GET /api/sokolov/product/{vendor_code} удалён.
    - response_model=dict[str, Product | None].
    - SokolovService.get_products_by_skus:
      1) dict.fromkeys дедуп СНАЧАЛА (сохраняет порядок первого появления);
      2) первый sku sequential (refresh_on_401=True) — валидирует токен
         для всего батча; если None → 502;
      3) остальные через asyncio.gather (refresh_on_401=False) — на 401
         просто None для slot, никакого login-storm на upstream;
      4) return_exceptions=True — упавший слот → None, не валит весь batch.
    - _build_product: complectProducts=attributes.get("complect-products")
      or [] (был ValidationError на None).
    
    Health endpoint полностью удалён (override от пользователя):
    - src/routers/health/, src/settings/health_settings.py git rm.
    - src/guard/__init__.py: verify_api_health_token убран из __all__.
    - src/routers/__init__.py: больше нет health_router.
    - env.template / .env.example: убран блок APP__HEALTHCHECK_*.
    - tests/conftest.py: убран APP__HEALTHCHECK_TOKEN default.
    - tests/test_app_startup.py: RequestIdMiddleware тесты на 404-пути
      NO_AUTH_PATH (auth_headers fixture больше не нужен).
    
    CI deploy переменные суффиксированы _NODE3 (gitlab-ci.yml + README):
    SSH_PRIVATE_KEY_NODE3, DEPLOY_HOST_NODE3, DEPLOY_USER_NODE3,
    DEPLOY_PORT_NODE3.
    
    Sokolov client paths приведены в соответствие docs:
    - host = "https://api.b2b.sokolov.net" (linter временно ставил
      https://b2b.sokolov.ru — это не API endpoint).
    - login path /ru-ru/login (был баг /login vs /ru-ru/login).
    
    tests/unit/test_sokolov_service.py (7 кейсов):
    - empty list → {}
    - single sku → 1 call
    - 3 skus → 3 calls (first sequential + rest gather)
    - dedup preserves order ["A","B","A"] → 2 calls
    - first None → 502 raised, gather не стартует
    - parallel exception → None для slot
    - empty data → None
    
    Verification:
    - poetry run ruff check → All checks passed
    - poetry run pytest → 11 passed, coverage 86.06% ≥ 80%
    - poetry check → All set!
    - grep "git.web.luc.kz" pyproject.toml | grep -v "tag = " → пусто (P5)
    - 5 git-либ с tag (lib-aiohttp, lib-utils, lib-auth-guard,
      lucente-observability, lib-aio-redis)
    3501eb07
    SokolovApi: Redis-shared token cache + batch /product?sku=...
    Andrey Azeev authored
    Подключена lib-aio-redis v1.1.0 (P14/P5 OK):
    - RedisSettings (host/port/password/db/socket_*/max_connections/
      health_check_interval), все OPTIONAL с дефолтами localhost:6379.
    - src/utils/redis_client.py — canon lifespan-обёртка
      redis_lifespan() -> AsyncIterator[AsyncRedis | None] + get_redis +
      RedisDep. При RedisConnectionError на старте yield None —
      приложение продолжает работать с per-worker in-memory кэшем.
    - app.py lifespan: redis_client = enter_async_context(redis_lifespan())
      → app.state.redis. SokolovApi(redis=redis_client) после Redis.
    
    SokolovApi token cache (Redis-first, in-memory fallback):
    - Ключ "sokolov:access_token", TTL 23h (Sokolov docs: 1 день, с запасом).
    - _get_access_token(stale_token=None): cache → Redis → login под Lock.
    - Stale-token aware refresh: на 401 в _get_access_token(stale_token=T)
      под локом проверяем self._token != T и Redis != T — если кто-то
      уже обновил, login не повторяется. Защита от thundering herd
      при N параллельных 401.
    - parse_json_response из lib_aiohttp для login + get_products.
    
    SokolovApi fixes:
    - BaseAPI(__init__) больше не получает headers= (default headers
      перекрывали per-call Bearer для products). Headers теперь per-call.
    - _basic_auth_headers/_bearer_headers — instance methods (раньше
      classmethod без args — login был сломан).
    - get_products(refresh_on_401: bool = True) — новый параметр
      для контроля retry поведения.
    
    Batch endpoint GET /api/sokolov/product?sku=...&sku=... (до 10):
    - Old GET /api/sokolov/product/{vendor_code} удалён.
    - response_model=dict[str, Product | None].
    - SokolovService.get_products_by_skus:
      1) dict.fromkeys дедуп СНАЧАЛА (сохраняет порядок первого появления);
      2) первый sku sequential (refresh_on_401=True) — валидирует токен
         для всего батча; если None → 502;
      3) остальные через asyncio.gather (refresh_on_401=False) — на 401
         просто None для slot, никакого login-storm на upstream;
      4) return_exceptions=True — упавший слот → None, не валит весь batch.
    - _build_product: complectProducts=attributes.get("complect-products")
      or [] (был ValidationError на None).
    
    Health endpoint полностью удалён (override от пользователя):
    - src/routers/health/, src/settings/health_settings.py git rm.
    - src/guard/__init__.py: verify_api_health_token убран из __all__.
    - src/routers/__init__.py: больше нет health_router.
    - env.template / .env.example: убран блок APP__HEALTHCHECK_*.
    - tests/conftest.py: убран APP__HEALTHCHECK_TOKEN default.
    - tests/test_app_startup.py: RequestIdMiddleware тесты на 404-пути
      NO_AUTH_PATH (auth_headers fixture больше не нужен).
    
    CI deploy переменные суффиксированы _NODE3 (gitlab-ci.yml + README):
    SSH_PRIVATE_KEY_NODE3, DEPLOY_HOST_NODE3, DEPLOY_USER_NODE3,
    DEPLOY_PORT_NODE3.
    
    Sokolov client paths приведены в соответствие docs:
    - host = "https://api.b2b.sokolov.net" (linter временно ставил
      https://b2b.sokolov.ru — это не API endpoint).
    - login path /ru-ru/login (был баг /login vs /ru-ru/login).
    
    tests/unit/test_sokolov_service.py (7 кейсов):
    - empty list → {}
    - single sku → 1 call
    - 3 skus → 3 calls (first sequential + rest gather)
    - dedup preserves order ["A","B","A"] → 2 calls
    - first None → 502 raised, gather не стартует
    - parallel exception → None для slot
    - empty data → None
    
    Verification:
    - poetry run ruff check → All checks passed
    - poetry run pytest → 11 passed, coverage 86.06% ≥ 80%
    - poetry check → All set!
    - grep "git.web.luc.kz" pyproject.toml | grep -v "tag = " → пусто (P5)
    - 5 git-либ с tag (lib-aiohttp, lib-utils, lib-auth-guard,
      lucente-observability, lib-aio-redis)
Loading