feat: AM-2 wire swiftly_auth.get_token() into LuckyCaliforniaScraper

fetch_category() now calls swiftly_auth.get_token() to mint a fresh
Firebase JWT on demand when no explicit bearer_token override is
pinned by tests. The cache short-circuit means the per-call mint
overhead is ~zero in the steady state.

- Removed the empty-token short-circuit; auto-mint makes it moot
- Updated _AUTH_ERROR_MESSAGE: 401-after-mint now points at the spec
  (Lucky tightening anon-auth) rather than asking for manual capture
- Replaced test_swiftly_auth_error_when_token_missing with a positive
  test that verifies fetch_category mints when bearer_token is None
- bearer_token constructor arg preserved for the 401-path test

Full suite: 92/92 green. Live verification via
scripts/spike_swiftly_ingest.py --confirm-live deferred to next step
per HANDOFF AM-2 halt boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 15:27:37 -07:00
co-authored by Claude Opus 4.7
parent 5e0a49e4ae
commit dfd79a9d08
2 changed files with 33 additions and 18 deletions
+12 -6
View File
@@ -36,6 +36,10 @@ import requests
from bs4 import BeautifulSoup
from app.config import settings
from app.services.swiftly_auth import (
SwiftlyAuthMintError,
get_token as _get_swiftly_token,
)
logger = logging.getLogger(__name__)
@@ -53,8 +57,8 @@ _USER_AGENT = (
)
_AUTH_ERROR_MESSAGE = (
"SWIFTLY_BEARER_TOKEN expired — request a fresh token from the user "
"(capture from luckysupermarkets.com network tab on a /search/api/v1 request)"
"Swiftly returned 401 with a freshly minted Firebase JWT — Lucky may have "
"tightened anon-auth restrictions. See docs/specs/2026-05-06-swiftly-token-auto-mint.md."
)
@@ -87,7 +91,10 @@ class LuckyCaliforniaScraper:
rate_limit_seconds: float = 0.75,
timeout: int = 60,
) -> None:
self.bearer_token = bearer_token or settings.SWIFTLY_BEARER_TOKEN
# When ``bearer_token`` is None, fetch_category() mints via Firebase
# REST through ``swiftly_auth.get_token()``. Tests pin a fixed token
# to exercise the 401 path without going through Firebase.
self.bearer_token = bearer_token
self.store_id = store_id or settings.LUCKY_STORE_ID
self.api_base = (api_base or settings.SWIFTLY_API_BASE).rstrip("/")
self.categories_url = categories_url or settings.SWIFTLY_CATEGORIES_URL
@@ -198,13 +205,12 @@ class LuckyCaliforniaScraper:
# ------------------------------------------------------------------
def fetch_category(self, slug: str) -> List[Dict[str, Any]]:
"""Fetch every product in a category. Raises ``SwiftlyAuthError`` on 401."""
if not self.bearer_token:
raise SwiftlyAuthError(_AUTH_ERROR_MESSAGE)
token = self.bearer_token if self.bearer_token else _get_swiftly_token()
self._rate_limit()
url = f"{self.api_base}/search/api/v1/products/categories"
params = {"cat": slug, "store": self.store_id, "limit": 10000}
headers = {"Authorization": f"Bearer {self.bearer_token}"}
headers = {"Authorization": f"Bearer {token}"}
resp = self.api_session.get(
url, params=params, headers=headers, timeout=self.timeout
)