flare / api_executor.py
ciyidogan's picture
Update api_executor.py
8dbff89 verified
raw
history blame
7.79 kB
"""
Flare – API Executor (v2.0 Β· session-aware token management)
"""
from __future__ import annotations
import json, re, time, requests
from typing import Any, Dict, Optional
from utils import log
from config_provider import ConfigProvider, APIConfig
from session import Session
cfg = ConfigProvider.get()
_placeholder = re.compile(r"\{\{\s*([^\}]+?)\s*\}\}")
def _render(obj: Any, session: Session, api_name: str) -> Any:
"""Render template with session variables and tokens"""
def replacer(match):
key = match.group(1)
if key.startswith("variables."):
var_name = key.split(".", 1)[1]
return str(session.variables.get(var_name, ""))
if key.startswith("auth_tokens."):
parts = key.split(".")
if len(parts) >= 3:
token_api = parts[1]
token_field = parts[2]
token_data = session.auth_tokens.get(token_api, {})
return str(token_data.get(token_field, ""))
if key.startswith("config."):
attr_name = key.split(".", 1)[1]
return str(getattr(cfg.global_config, attr_name, ""))
return match.group(0)
if isinstance(obj, str):
return _placeholder.sub(replacer, obj)
if isinstance(obj, dict):
return {k: _render(v, session, api_name) for k, v in obj.items()}
if isinstance(obj, list):
return [_render(v, session, api_name) for v in obj]
return obj
def _fetch_token(api: APIConfig, session: Session) -> None:
"""Fetch new auth token"""
if not api.auth or not api.auth.enabled:
return
log(f"πŸ”‘ Fetching token for {api.name}")
try:
body = _render(api.auth.token_request_body, session, api.name)
headers = {"Content-Type": "application/json"}
response = requests.post(
str(api.auth.token_endpoint),
json=body,
headers=headers,
timeout=api.timeout_seconds
)
response.raise_for_status()
json_data = response.json()
# Extract token using path
token = json_data
for path_part in api.auth.response_token_path.split("."):
token = token.get(path_part)
if token is None:
raise ValueError(f"Token path {api.auth.response_token_path} not found in response")
# Store in session
session.auth_tokens[api.name] = {
"token": token,
"expiry": time.time() + 3500, # ~1 hour
"refresh_token": json_data.get("refresh_token")
}
log(f"βœ… Token obtained for {api.name}")
except Exception as e:
log(f"❌ Token fetch failed for {api.name}: {e}")
raise
def _refresh_token(api: APIConfig, session: Session) -> bool:
"""Refresh existing token"""
if not api.auth or not api.auth.token_refresh_endpoint:
return False
token_info = session.auth_tokens.get(api.name, {})
if not token_info.get("refresh_token"):
return False
log(f"πŸ”„ Refreshing token for {api.name}")
try:
body = _render(api.auth.token_refresh_body or {}, session, api.name)
body["refresh_token"] = token_info["refresh_token"]
response = requests.post(
str(api.auth.token_refresh_endpoint),
json=body,
timeout=api.timeout_seconds
)
response.raise_for_status()
json_data = response.json()
# Extract new token
token = json_data
for path_part in api.auth.response_token_path.split("."):
token = token.get(path_part)
if token is None:
raise ValueError(f"Token path {api.auth.response_token_path} not found in refresh response")
# Update session
session.auth_tokens[api.name] = {
"token": token,
"expiry": time.time() + 3500,
"refresh_token": json_data.get("refresh_token", token_info["refresh_token"])
}
log(f"βœ… Token refreshed for {api.name}")
return True
except Exception as e:
log(f"❌ Token refresh failed for {api.name}: {e}")
return False
def _ensure_token(api: APIConfig, session: Session) -> None:
"""Ensure valid token exists for API"""
if not api.auth or not api.auth.enabled:
return
token_info = session.auth_tokens.get(api.name)
# No token yet
if not token_info:
_fetch_token(api, session)
return
# Token still valid
if token_info.get("expiry", 0) > time.time():
return
# Try refresh first
if _refresh_token(api, session):
return
# Refresh failed, get new token
_fetch_token(api, session)
def call_api(api: APIConfig, session: Session) -> requests.Response:
"""Execute API call with automatic token management"""
# Ensure valid token
_ensure_token(api, session)
# Prepare request
headers = _render(api.headers, session, api.name)
body = _render(api.body_template, session, api.name)
# Handle proxy
proxies = None
if api.proxy:
if isinstance(api.proxy, str):
proxies = {"http": api.proxy, "https": api.proxy}
elif hasattr(api.proxy, "enabled") and api.proxy.enabled:
proxy_url = str(api.proxy.url)
proxies = {"http": proxy_url, "https": proxy_url}
# Prepare request parameters
request_params = {
"method": api.method,
"url": str(api.url),
"headers": headers,
"timeout": api.timeout_seconds
}
# Add body based on method
if api.method in ("POST", "PUT", "PATCH"):
request_params["json"] = body
elif api.method == "GET" and body:
request_params["params"] = body
if proxies:
request_params["proxies"] = proxies
# Execute with retry
retry_count = api.retry.retry_count if api.retry else 0
last_error = None
for attempt in range(retry_count + 1):
try:
log(f"🌐 API call: {api.name} {api.method} {api.url} (attempt {attempt + 1}/{retry_count + 1})")
response = requests.request(**request_params)
# Handle 401 Unauthorized
if response.status_code == 401 and api.auth and api.auth.enabled and attempt < retry_count:
log(f"πŸ”’ Got 401, refreshing token for {api.name}")
_fetch_token(api, session) # Force new token
headers = _render(api.headers, session, api.name) # Re-render headers
request_params["headers"] = headers
continue
response.raise_for_status()
log(f"βœ… API call successful: {api.name} ({response.status_code})")
return response
except requests.exceptions.Timeout as e:
last_error = e
log(f"⏱️ API timeout for {api.name} (attempt {attempt + 1})")
except requests.exceptions.RequestException as e:
last_error = e
log(f"❌ API error for {api.name}: {e}")
# Retry backoff
if attempt < retry_count:
backoff = api.retry.backoff_seconds if api.retry else 2
if api.retry and api.retry.strategy == "exponential":
backoff = backoff * (2 ** attempt)
log(f"⏳ Waiting {backoff}s before retry...")
time.sleep(backoff)
# All retries failed
if last_error:
raise last_error
raise requests.exceptions.RequestException(f"API call failed after {retry_count + 1} attempts")