Spaces:
Sleeping
Sleeping
| """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 | |
| # ======================================================================== | |
| def extract_domain(url: str) -> str: | |
| """Extract domain from URL.""" | |
| try: | |
| parsed = urlparse(url) | |
| return parsed.netloc or url | |
| except Exception: | |
| return url | |
| 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)) | |
| 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) | |