chainshift-dashboard / core /api_client.py
GitHub Action
Sync from GitHub
ef78361
Raw
History Blame Contribute Delete
5.46 kB
"""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)