Spaces:
Sleeping
Sleeping
File size: 5,461 Bytes
ef78361 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | """ChainShift Gen3 API Client - Nudge Detection Focus.
Domain methods are in mixin files:
- api_client_sentiment.py: SentimentApiMixin (Gen3, Verification, Keyword)
- api_client_reports.py: ReportsApiMixin (Reports, Action Items)
- api_client_hierarchy.py: HierarchyApiMixin (Analysis Jobs, Hierarchy)
"""
import os
from typing import Any
from urllib.parse import urlparse
import requests
from dotenv import load_dotenv
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from core.api_client_sentiment import SentimentApiMixin
from core.api_client_reports import ReportsApiMixin
from core.api_client_hierarchy import HierarchyApiMixin
load_dotenv()
BASE_URL = os.getenv(
"CHAINSHIFT_API_URL",
"https://chainshift-service-api.vercel.app"
)
API_KEY = os.getenv("CHAINSHIFT_API_KEY", "")
def _create_session() -> requests.Session:
"""Create requests session with retry for transient errors."""
session = requests.Session()
retry = Retry(
total=3,
backoff_factor=0.5,
status_forcelist=[502, 503, 504],
)
session.mount("https://", HTTPAdapter(max_retries=retry))
session.mount("http://", HTTPAdapter(max_retries=retry))
return session
class ChainShiftClient(SentimentApiMixin, ReportsApiMixin, HierarchyApiMixin):
"""Gen3 API client for ChainShift Nudge Detection."""
def __init__(
self,
api_key: str | None = None,
access_token: str | None = None,
base_url: str | None = None,
):
self.api_key = api_key or API_KEY
self.base_url = base_url or BASE_URL
self.headers = {"X-API-Key": self.api_key} if self.api_key else {}
self._session = _create_session()
def set_api_key(self, api_key: str):
"""Set or update API key."""
self.api_key = api_key
self.headers = {"X-API-Key": self.api_key}
def _get(self, endpoint: str, params: dict | None = None) -> dict[str, Any]:
"""Make GET request to API."""
url = f"{self.base_url}{endpoint}"
response = self._session.get(url, headers=self.headers, params=params, timeout=120)
response.raise_for_status()
return response.json()
def _post(self, endpoint: str, data: dict | None = None) -> dict[str, Any]:
"""Make POST request to API."""
url = f"{self.base_url}{endpoint}"
response = self._session.post(url, headers=self.headers, json=data, timeout=300)
response.raise_for_status()
return response.json()
def _patch(self, endpoint: str, data: dict | None = None) -> dict[str, Any]:
"""Make PATCH request to API."""
url = f"{self.base_url}{endpoint}"
response = self._session.patch(url, headers=self.headers, json=data, timeout=120)
response.raise_for_status()
return response.json()
def _delete(self, endpoint: str) -> dict[str, Any]:
"""Make DELETE request to API."""
url = f"{self.base_url}{endpoint}"
response = self._session.delete(url, headers=self.headers, timeout=120)
response.raise_for_status()
return response.json()
# ========================================================================
# Campaign APIs
# ========================================================================
def get_campaigns(self, page: int = 1, page_size: int = 100) -> dict:
"""Get list of campaigns."""
return self._get("/api/v1/campaigns", {"page": page, "page_size": page_size})
def get_campaign(self, campaign_id: int) -> dict:
"""Get campaign details."""
return self._get(f"/api/v1/campaigns/{campaign_id}")
def get_campaign_brands(self, campaign_id: int) -> list[dict]:
"""Get brands for a campaign."""
resp = self._get(f"/api/v1/campaigns/{campaign_id}/brands")
return (resp or {}).get("data") or []
# ========================================================================
# Utility Methods
# ========================================================================
@staticmethod
def extract_domain(url: str) -> str:
"""Extract domain from URL."""
try:
parsed = urlparse(url)
return parsed.netloc or url
except Exception:
return url
@staticmethod
def aggregate_citation_domains(candidates: list[dict]) -> dict[str, int]:
"""Aggregate citation URLs by domain.
Returns: {domain: count}
"""
domain_counts: dict[str, int] = {}
for candidate in candidates:
urls = candidate.get("citation_urls", []) or []
for url in urls:
domain = ChainShiftClient.extract_domain(url)
if domain:
domain_counts[domain] = domain_counts.get(domain, 0) + 1
return dict(sorted(domain_counts.items(), key=lambda x: x[1], reverse=True))
@staticmethod
def calculate_risk_score(tier_stats: dict) -> float:
"""Calculate risk score (0-100) based on confidence tiers.
Formula: (HIGH * 1.0 + MEDIUM * 0.5 + LOW * 0.2) / total * 100
"""
high = tier_stats.get("HIGH", 0)
medium = tier_stats.get("MEDIUM", 0)
low = tier_stats.get("LOW", 0)
total = high + medium + low
if total == 0:
return 0.0
weighted = high * 1.0 + medium * 0.5 + low * 0.2
return min(100.0, (weighted / total) * 100)
|