Spaces:
Building
Building
""" | |
Flare – API Executor (v2.0 · session-aware token management) | |
""" | |
from __future__ import annotations | |
import json, re, time, requests | |
from typing import Any, Dict, Optional, Union | |
from utils import log | |
from config_provider import ConfigProvider, APIConfig | |
from session import Session | |
_placeholder = re.compile(r"\{\{\s*([^\}]+?)\s*\}\}") | |
def _get_variable_value(session: Session, var_path: str) -> Any: | |
cfg = ConfigProvider.get() | |
"""Get variable value with proper type from session""" | |
if var_path.startswith("variables."): | |
var_name = var_path.split(".", 1)[1] | |
return session.variables.get(var_name) | |
elif var_path.startswith("auth_tokens."): | |
parts = var_path.split(".") | |
if len(parts) >= 3: | |
token_api = parts[1] | |
token_field = parts[2] | |
token_data = session.auth_tokens.get(token_api, {}) | |
return token_data.get(token_field) | |
elif var_path.startswith("config."): | |
attr_name = var_path.split(".", 1)[1] | |
return getattr(cfg.global_config, attr_name, None) | |
return None | |
def _render_value(value: Any) -> Union[str, int, float, bool, None]: | |
"""Convert value to appropriate JSON type""" | |
if value is None: | |
return None | |
elif isinstance(value, bool): | |
return value | |
elif isinstance(value, (int, float)): | |
return value | |
elif isinstance(value, str): | |
# Check if it's a number string | |
if value.isdigit(): | |
return int(value) | |
try: | |
return float(value) | |
except ValueError: | |
pass | |
# Check if it's a boolean string | |
if value.lower() in ('true', 'false'): | |
return value.lower() == 'true' | |
# Return as string | |
return value | |
else: | |
return str(value) | |
def _render_json(obj: Any, session: Session, api_name: str) -> Any: | |
"""Render JSON preserving types""" | |
if isinstance(obj, str): | |
# Check if entire string is a template | |
template_match = _placeholder.fullmatch(obj.strip()) | |
if template_match: | |
# This is a pure template like {{variables.pnr}} | |
var_path = template_match.group(1).strip() | |
value = _get_variable_value(session, var_path) | |
return _render_value(value) | |
else: | |
# String with embedded templates or regular string | |
def replacer(match): | |
var_path = match.group(1).strip() | |
value = _get_variable_value(session, var_path) | |
return str(value) if value is not None else "" | |
return _placeholder.sub(replacer, obj) | |
elif isinstance(obj, dict): | |
return {k: _render_json(v, session, api_name) for k, v in obj.items()} | |
elif isinstance(obj, list): | |
return [_render_json(v, session, api_name) for v in obj] | |
else: | |
# Return as-is for numbers, booleans, None | |
return obj | |
def _render(obj: Any, session: Session, api_name: str) -> Any: | |
"""Render template with session variables and tokens""" | |
# For headers and other string-only contexts | |
if isinstance(obj, str): | |
def replacer(match): | |
var_path = match.group(1).strip() | |
value = _get_variable_value(session, var_path) | |
return str(value) if value is not None else "" | |
return _placeholder.sub(replacer, obj) | |
elif isinstance(obj, dict): | |
return {k: _render(v, session, api_name) for k, v in obj.items()} | |
elif 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: | |
# Use _render_json for body to preserve types | |
body = _render_json(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_json(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) # Headers are always strings | |
body = _render_json(api.body_template, session, api.name) # Body preserves types | |
# 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 | |
response = 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})") | |
log(f"📋 Request body: {json.dumps(body, ensure_ascii=False)}") | |
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})") | |
# Response mapping işlemi | |
if response.status_code in (200, 201, 202, 204) and hasattr(api, 'response_mappings') and api.response_mappings: | |
try: | |
if response.status_code != 204 and response.content: | |
response_json = response.json() | |
for mapping in api.response_mappings: | |
var_name = mapping.get('variable_name') | |
var_type = mapping.get('type', 'str') | |
json_path = mapping.get('json_path') | |
if not all([var_name, json_path]): | |
continue | |
# JSON path'ten değeri al | |
value = response_json | |
for path_part in json_path.split('.'): | |
if isinstance(value, dict): | |
value = value.get(path_part) | |
if value is None: | |
break | |
if value is not None: | |
# Type conversion | |
if var_type == 'int': | |
value = int(value) | |
elif var_type == 'float': | |
value = float(value) | |
elif var_type == 'bool': | |
value = bool(value) | |
elif var_type == 'date': | |
# ISO format'ta sakla | |
value = str(value) | |
else: # str | |
value = str(value) | |
# Session'a kaydet | |
session.variables[var_name] = value | |
log(f"📝 Mapped response value: {var_name} = {value}") | |
except Exception as e: | |
log(f"⚠️ Response mapping error: {e}") | |
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") |