""" 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")