first commit

This commit is contained in:
2025-11-24 23:10:55 -06:00
commit 8315fa51c9
279 changed files with 74600 additions and 0 deletions

View File

@@ -0,0 +1,336 @@
"""
API Client for Public Web Frontend
Provides HTTP request wrapper for communicating with the API backend.
Handles session cookie forwarding, error handling, and response parsing.
"""
import requests
from flask import request as flask_request, session as flask_session
from typing import Optional, Any
from app.config import load_config
from .logging import get_logger
logger = get_logger(__name__)
class APIError(Exception):
"""Base exception for API errors."""
def __init__(self, message: str, status_code: int = 500, details: Optional[dict] = None):
self.message = message
self.status_code = status_code
self.details = details or {}
super().__init__(self.message)
class APITimeoutError(APIError):
"""Raised when API request times out."""
def __init__(self, message: str = "Request timed out"):
super().__init__(message, status_code=504)
class APINotFoundError(APIError):
"""Raised when resource not found (404)."""
def __init__(self, message: str = "Resource not found"):
super().__init__(message, status_code=404)
class APIAuthenticationError(APIError):
"""Raised when authentication fails (401)."""
def __init__(self, message: str = "Authentication required"):
super().__init__(message, status_code=401)
class APIClient:
"""
HTTP client for making requests to the API backend.
Usage:
client = APIClient()
# GET request
response = client.get("/api/v1/characters")
characters = response.get("result", [])
# POST request
response = client.post("/api/v1/characters", data={"name": "Hero"})
# DELETE request
client.delete("/api/v1/characters/123")
"""
def __init__(self):
"""Initialize API client with config."""
self.config = load_config()
self.base_url = self.config.api.base_url.rstrip('/')
self.timeout = self.config.api.timeout
self.verify_ssl = self.config.api.verify_ssl
def _get_cookies(self) -> dict:
"""
Get cookies to forward to API.
Returns:
Dictionary of cookies to forward (session cookie).
"""
cookies = {}
# Get session cookie from Flask session (stored after login)
try:
if 'api_session_cookie' in flask_session:
cookies['coc_session'] = flask_session['api_session_cookie']
except RuntimeError:
# Outside of request context
pass
return cookies
def _save_session_cookie(self, response: requests.Response) -> None:
"""
Save session cookie from API response to Flask session.
Args:
response: Response from requests library.
"""
try:
session_cookie = response.cookies.get('coc_session')
if session_cookie:
flask_session['api_session_cookie'] = session_cookie
logger.debug("Saved API session cookie to Flask session")
except RuntimeError:
# Outside of request context
pass
def _get_headers(self) -> dict:
"""
Get default headers for API requests.
Returns:
Dictionary of headers.
"""
return {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
def _handle_response(self, response: requests.Response) -> dict:
"""
Handle API response and raise appropriate exceptions.
Args:
response: Response from requests library.
Returns:
Parsed JSON response.
Raises:
APIError: For various HTTP error codes.
"""
try:
data = response.json()
except ValueError:
data = {}
# Check for errors
if response.status_code == 401:
error_msg = data.get('error', {}).get('message', 'Authentication required')
raise APIAuthenticationError(error_msg)
if response.status_code == 404:
error_msg = data.get('error', {}).get('message', 'Resource not found')
raise APINotFoundError(error_msg)
if response.status_code >= 400:
error_msg = data.get('error', {}).get('message', f'API error: {response.status_code}')
error_details = data.get('error', {}).get('details', {})
raise APIError(error_msg, response.status_code, error_details)
return data
def get(self, endpoint: str, params: Optional[dict] = None) -> dict:
"""
Make GET request to API.
Args:
endpoint: API endpoint (e.g., "/api/v1/characters").
params: Optional query parameters.
Returns:
Parsed JSON response.
Raises:
APIError: For various error conditions.
"""
url = f"{self.base_url}{endpoint}"
try:
response = requests.get(
url,
params=params,
headers=self._get_headers(),
cookies=self._get_cookies(),
timeout=self.timeout,
verify=self.verify_ssl
)
logger.debug("API GET request", url=url, status=response.status_code)
return self._handle_response(response)
except requests.exceptions.Timeout:
logger.error("API timeout", url=url)
raise APITimeoutError()
except requests.exceptions.ConnectionError as e:
logger.error("API connection error", url=url, error=str(e))
raise APIError(f"Could not connect to API: {str(e)}", status_code=503)
except requests.exceptions.RequestException as e:
logger.error("API request error", url=url, error=str(e))
raise APIError(f"Request failed: {str(e)}")
def post(self, endpoint: str, data: Optional[dict] = None) -> dict:
"""
Make POST request to API.
Args:
endpoint: API endpoint (e.g., "/api/v1/characters").
data: Request body data.
Returns:
Parsed JSON response.
Raises:
APIError: For various error conditions.
"""
url = f"{self.base_url}{endpoint}"
try:
response = requests.post(
url,
json=data or {},
headers=self._get_headers(),
cookies=self._get_cookies(),
timeout=self.timeout,
verify=self.verify_ssl
)
logger.debug("API POST request", url=url, status=response.status_code)
# Save session cookie if present (for login responses)
self._save_session_cookie(response)
return self._handle_response(response)
except requests.exceptions.Timeout:
logger.error("API timeout", url=url)
raise APITimeoutError()
except requests.exceptions.ConnectionError as e:
logger.error("API connection error", url=url, error=str(e))
raise APIError(f"Could not connect to API: {str(e)}", status_code=503)
except requests.exceptions.RequestException as e:
logger.error("API request error", url=url, error=str(e))
raise APIError(f"Request failed: {str(e)}")
def delete(self, endpoint: str) -> dict:
"""
Make DELETE request to API.
Args:
endpoint: API endpoint (e.g., "/api/v1/characters/123").
Returns:
Parsed JSON response.
Raises:
APIError: For various error conditions.
"""
url = f"{self.base_url}{endpoint}"
try:
response = requests.delete(
url,
headers=self._get_headers(),
cookies=self._get_cookies(),
timeout=self.timeout,
verify=self.verify_ssl
)
logger.debug("API DELETE request", url=url, status=response.status_code)
return self._handle_response(response)
except requests.exceptions.Timeout:
logger.error("API timeout", url=url)
raise APITimeoutError()
except requests.exceptions.ConnectionError as e:
logger.error("API connection error", url=url, error=str(e))
raise APIError(f"Could not connect to API: {str(e)}", status_code=503)
except requests.exceptions.RequestException as e:
logger.error("API request error", url=url, error=str(e))
raise APIError(f"Request failed: {str(e)}")
def put(self, endpoint: str, data: Optional[dict] = None) -> dict:
"""
Make PUT request to API.
Args:
endpoint: API endpoint.
data: Request body data.
Returns:
Parsed JSON response.
Raises:
APIError: For various error conditions.
"""
url = f"{self.base_url}{endpoint}"
try:
response = requests.put(
url,
json=data or {},
headers=self._get_headers(),
cookies=self._get_cookies(),
timeout=self.timeout,
verify=self.verify_ssl
)
logger.debug("API PUT request", url=url, status=response.status_code)
return self._handle_response(response)
except requests.exceptions.Timeout:
logger.error("API timeout", url=url)
raise APITimeoutError()
except requests.exceptions.ConnectionError as e:
logger.error("API connection error", url=url, error=str(e))
raise APIError(f"Could not connect to API: {str(e)}", status_code=503)
except requests.exceptions.RequestException as e:
logger.error("API request error", url=url, error=str(e))
raise APIError(f"Request failed: {str(e)}")
# Singleton instance
_api_client: Optional[APIClient] = None
def get_api_client() -> APIClient:
"""
Get singleton API client instance.
Returns:
APIClient instance.
"""
global _api_client
if _api_client is None:
_api_client = APIClient()
return _api_client