feat: community category federation in EbayCategoryCache.refresh()
Adds optional community_store param to refresh(). Credentialed instances publish leaf categories to the shared community PostgreSQL after a successful Taxonomy API fetch. Credentialless instances pull from community (requires >= 10 rows) before falling back to the hardcoded bootstrap. Adds 3 new tests (14 total, all passing).
This commit is contained in:
parent
0b8cb63968
commit
15718ab431
2 changed files with 98 additions and 0 deletions
|
|
@ -120,17 +120,50 @@ class EbayCategoryCache:
|
|||
def refresh(
|
||||
self,
|
||||
token_manager: Optional["EbayTokenManager"] = None,
|
||||
community_store: Optional[object] = None,
|
||||
) -> int:
|
||||
"""Fetch the eBay category tree and upsert leaf nodes into SQLite.
|
||||
|
||||
Args:
|
||||
token_manager: An `EbayTokenManager` instance for the Taxonomy API.
|
||||
If None, falls back to seeding the hardcoded bootstrap table.
|
||||
community_store: Optional SnipeCommunityStore instance.
|
||||
If provided and token_manager is set, publish leaves after a successful
|
||||
Taxonomy API fetch.
|
||||
If provided and token_manager is None, fetch from community before
|
||||
falling back to the hardcoded bootstrap (requires >= 10 rows).
|
||||
|
||||
Returns:
|
||||
Number of leaf categories stored.
|
||||
"""
|
||||
if token_manager is None:
|
||||
# Try community store first
|
||||
if community_store is not None:
|
||||
try:
|
||||
community_cats = community_store.fetch_categories()
|
||||
if len(community_cats) >= 10:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
self._conn.executemany(
|
||||
"INSERT OR REPLACE INTO ebay_categories"
|
||||
" (category_id, name, full_path, is_leaf, refreshed_at)"
|
||||
" VALUES (?, ?, ?, 1, ?)",
|
||||
[(cid, name, path, now) for cid, name, path in community_cats],
|
||||
)
|
||||
self._conn.commit()
|
||||
log.info(
|
||||
"EbayCategoryCache: loaded %d categories from community store.",
|
||||
len(community_cats),
|
||||
)
|
||||
return len(community_cats)
|
||||
log.info(
|
||||
"EbayCategoryCache: community store has %d categories (< 10) — falling back to bootstrap.",
|
||||
len(community_cats),
|
||||
)
|
||||
except Exception:
|
||||
log.warning(
|
||||
"EbayCategoryCache: community store fetch failed — falling back to bootstrap.",
|
||||
exc_info=True,
|
||||
)
|
||||
self._seed_bootstrap()
|
||||
cur = self._conn.execute("SELECT COUNT(*) FROM ebay_categories")
|
||||
return cur.fetchone()[0]
|
||||
|
|
@ -173,6 +206,17 @@ class EbayCategoryCache:
|
|||
"EbayCategoryCache: refreshed %d leaf categories from eBay Taxonomy API.",
|
||||
len(leaves),
|
||||
)
|
||||
|
||||
# Publish to community store if available
|
||||
if community_store is not None:
|
||||
try:
|
||||
community_store.publish_categories(leaves)
|
||||
except Exception:
|
||||
log.warning(
|
||||
"EbayCategoryCache: failed to publish categories to community store.",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
return len(leaves)
|
||||
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -162,3 +162,57 @@ def test_refresh_api_error_logs_warning(db, caplog):
|
|||
|
||||
# Falls back to bootstrap on API error
|
||||
assert count >= BOOTSTRAP_MIN
|
||||
|
||||
|
||||
def test_refresh_publishes_to_community_when_creds_available(db):
|
||||
"""After a successful Taxonomy API refresh, categories are published to community store."""
|
||||
mock_tm = MagicMock()
|
||||
mock_tm.get_token.return_value = "fake-token"
|
||||
|
||||
id_resp = MagicMock()
|
||||
id_resp.raise_for_status = MagicMock()
|
||||
id_resp.json.return_value = {"categoryTreeId": "0"}
|
||||
|
||||
tree_resp = MagicMock()
|
||||
tree_resp.raise_for_status = MagicMock()
|
||||
tree_resp.json.return_value = _make_tree_response()
|
||||
|
||||
mock_community = MagicMock()
|
||||
mock_community.publish_categories.return_value = 2
|
||||
|
||||
with patch("app.platforms.ebay.categories.requests.get") as mock_get:
|
||||
mock_get.side_effect = [id_resp, tree_resp]
|
||||
cache = EbayCategoryCache(db)
|
||||
cache.refresh(mock_tm, community_store=mock_community)
|
||||
|
||||
mock_community.publish_categories.assert_called_once()
|
||||
published = mock_community.publish_categories.call_args[0][0]
|
||||
assert len(published) == 2
|
||||
|
||||
|
||||
def test_refresh_fetches_from_community_when_no_creds(db):
|
||||
"""Without creds, community categories are used when available (>= 10 rows)."""
|
||||
mock_community = MagicMock()
|
||||
mock_community.fetch_categories.return_value = [
|
||||
(str(i), f"Cat {i}", f"Path > Cat {i}") for i in range(15)
|
||||
]
|
||||
|
||||
cache = EbayCategoryCache(db)
|
||||
count = cache.refresh(token_manager=None, community_store=mock_community)
|
||||
|
||||
assert count == 15
|
||||
cur = db.execute("SELECT COUNT(*) FROM ebay_categories")
|
||||
assert cur.fetchone()[0] == 15
|
||||
|
||||
|
||||
def test_refresh_falls_back_to_bootstrap_when_community_sparse(db):
|
||||
"""Falls back to bootstrap if community returns fewer than 10 rows."""
|
||||
mock_community = MagicMock()
|
||||
mock_community.fetch_categories.return_value = [
|
||||
("1", "Only One", "Path > Only One")
|
||||
]
|
||||
|
||||
cache = EbayCategoryCache(db)
|
||||
count = cache.refresh(token_manager=None, community_store=mock_community)
|
||||
|
||||
assert count >= BOOTSTRAP_MIN
|
||||
|
|
|
|||
Loading…
Reference in a new issue