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(
|
def refresh(
|
||||||
self,
|
self,
|
||||||
token_manager: Optional["EbayTokenManager"] = None,
|
token_manager: Optional["EbayTokenManager"] = None,
|
||||||
|
community_store: Optional[object] = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Fetch the eBay category tree and upsert leaf nodes into SQLite.
|
"""Fetch the eBay category tree and upsert leaf nodes into SQLite.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
token_manager: An `EbayTokenManager` instance for the Taxonomy API.
|
token_manager: An `EbayTokenManager` instance for the Taxonomy API.
|
||||||
If None, falls back to seeding the hardcoded bootstrap table.
|
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:
|
Returns:
|
||||||
Number of leaf categories stored.
|
Number of leaf categories stored.
|
||||||
"""
|
"""
|
||||||
if token_manager is None:
|
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()
|
self._seed_bootstrap()
|
||||||
cur = self._conn.execute("SELECT COUNT(*) FROM ebay_categories")
|
cur = self._conn.execute("SELECT COUNT(*) FROM ebay_categories")
|
||||||
return cur.fetchone()[0]
|
return cur.fetchone()[0]
|
||||||
|
|
@ -173,6 +206,17 @@ class EbayCategoryCache:
|
||||||
"EbayCategoryCache: refreshed %d leaf categories from eBay Taxonomy API.",
|
"EbayCategoryCache: refreshed %d leaf categories from eBay Taxonomy API.",
|
||||||
len(leaves),
|
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)
|
return len(leaves)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
||||||
|
|
@ -162,3 +162,57 @@ def test_refresh_api_error_logs_warning(db, caplog):
|
||||||
|
|
||||||
# Falls back to bootstrap on API error
|
# Falls back to bootstrap on API error
|
||||||
assert count >= BOOTSTRAP_MIN
|
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