refactor(patreon): DRY the campaigns-API request (#161)
_lookup_via_api and resolve_display_name shared ~90% of their body (same endpoint, params, headers, error handling — differing only in which field they pluck from data[0]). Extract _campaigns_api_first(vanity, cookies_path) -> dict|None; callers pluck the campaign id vs the display name. Return-value behavior preserved (the display-name path additionally gains the helper's more granular warning logs). Covered by the existing test_patreon_resolver.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NsmJSQxnNxGgtM5Yz4GAqi
This commit is contained in:
@@ -91,48 +91,46 @@ def _sync_lookup(vanity: str, cookies_path: str | None) -> str | None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _lookup_via_api(vanity: str, cookies_path: str | None) -> str | None:
|
def _campaigns_api_first(vanity: str, cookies_path: str | None) -> dict | None:
|
||||||
|
"""The first `data` object from Patreon's campaigns API filtered by vanity
|
||||||
|
(`?filter[vanity]=<vanity>&fields[campaign]=name`), or None on any failure
|
||||||
|
(network / non-200 / non-JSON / empty). The single request shape shared by
|
||||||
|
_lookup_via_api (plucks the campaign id) and resolve_display_name (plucks the
|
||||||
|
display name)."""
|
||||||
jar = _load_cookie_jar(cookies_path)
|
jar = _load_cookie_jar(cookies_path)
|
||||||
headers = {
|
|
||||||
"User-Agent": _USER_AGENT,
|
|
||||||
"Accept": "application/vnd.api+json",
|
|
||||||
}
|
|
||||||
params = {
|
|
||||||
"filter[vanity]": vanity,
|
|
||||||
"fields[campaign]": "name",
|
|
||||||
}
|
|
||||||
try:
|
try:
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
_CAMPAIGNS_URL,
|
_CAMPAIGNS_URL,
|
||||||
params=params,
|
params={"filter[vanity]": vanity, "fields[campaign]": "name"},
|
||||||
headers=headers,
|
headers={"User-Agent": _USER_AGENT, "Accept": "application/vnd.api+json"},
|
||||||
cookies=jar,
|
cookies=jar,
|
||||||
timeout=_TIMEOUT_SECONDS,
|
timeout=_TIMEOUT_SECONDS,
|
||||||
)
|
)
|
||||||
except requests.RequestException as exc:
|
except requests.RequestException as exc:
|
||||||
log.warning("Patreon campaigns API request failed for vanity=%s: %s", vanity, exc)
|
log.warning("Patreon campaigns API request failed for vanity=%s: %s", vanity, exc)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
log.warning(
|
log.warning(
|
||||||
"Patreon campaigns API returned HTTP %d for vanity=%s",
|
"Patreon campaigns API returned HTTP %d for vanity=%s",
|
||||||
resp.status_code, vanity,
|
resp.status_code, vanity,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = resp.json()
|
payload = resp.json()
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
log.warning("Patreon campaigns API returned non-JSON for vanity=%s: %s", vanity, exc)
|
log.warning("Patreon campaigns API returned non-JSON for vanity=%s: %s", vanity, exc)
|
||||||
return None
|
return None
|
||||||
|
data = payload.get("data") if isinstance(payload, dict) else None
|
||||||
|
if not isinstance(data, list) or not data or not isinstance(data[0], dict):
|
||||||
|
return None
|
||||||
|
return data[0]
|
||||||
|
|
||||||
if not isinstance(payload, dict):
|
|
||||||
|
def _lookup_via_api(vanity: str, cookies_path: str | None) -> str | None:
|
||||||
|
first = _campaigns_api_first(vanity, cookies_path)
|
||||||
|
if first is None:
|
||||||
return None
|
return None
|
||||||
data = payload.get("data")
|
campaign_id = first.get("id")
|
||||||
if not isinstance(data, list) or not data:
|
|
||||||
return None
|
|
||||||
first = data[0] if isinstance(data[0], dict) else None
|
|
||||||
campaign_id = first.get("id") if first else None
|
|
||||||
if not isinstance(campaign_id, str) or not campaign_id:
|
if not isinstance(campaign_id, str) or not campaign_id:
|
||||||
return None
|
return None
|
||||||
log.info("Resolved Patreon vanity=%s → campaign_id=%s", vanity, campaign_id)
|
log.info("Resolved Patreon vanity=%s → campaign_id=%s", vanity, campaign_id)
|
||||||
@@ -144,24 +142,10 @@ def resolve_display_name(vanity: str, cookies_path: str | None) -> str | None:
|
|||||||
(`fields[campaign]=name`), used to name the Artist at add-time (#130). None
|
(`fields[campaign]=name`), used to name the Artist at add-time (#130). None
|
||||||
on any failure — the caller falls back to the vanity handle. Sync: call from
|
on any failure — the caller falls back to the vanity handle. Sync: call from
|
||||||
an executor."""
|
an executor."""
|
||||||
jar = _load_cookie_jar(cookies_path)
|
first = _campaigns_api_first(vanity, cookies_path)
|
||||||
try:
|
if first is None:
|
||||||
resp = requests.get(
|
|
||||||
_CAMPAIGNS_URL,
|
|
||||||
params={"filter[vanity]": vanity, "fields[campaign]": "name"},
|
|
||||||
headers={"User-Agent": _USER_AGENT, "Accept": "application/vnd.api+json"},
|
|
||||||
cookies=jar,
|
|
||||||
timeout=_TIMEOUT_SECONDS,
|
|
||||||
)
|
|
||||||
if resp.status_code != 200:
|
|
||||||
return None
|
return None
|
||||||
data = resp.json().get("data")
|
name = (first.get("attributes") or {}).get("name")
|
||||||
except (requests.RequestException, ValueError) as exc:
|
|
||||||
log.warning("Patreon name lookup failed for vanity=%s: %s", vanity, exc)
|
|
||||||
return None
|
|
||||||
if not isinstance(data, list) or not data or not isinstance(data[0], dict):
|
|
||||||
return None
|
|
||||||
name = (data[0].get("attributes") or {}).get("name")
|
|
||||||
return name.strip() if isinstance(name, str) and name.strip() else None
|
return name.strip() if isinstance(name, str) and name.strip() else None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user