File size: 4,187 Bytes
a04c9e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f32824f
 
 
 
a04c9e9
 
 
 
 
 
 
 
 
 
 
f32824f
 
 
a04c9e9
 
 
 
 
 
 
 
 
f32824f
 
 
 
 
a04c9e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import time
import json
import logging
import requests
from datetime import datetime
from functools import wraps
from typing import Dict, Any

logger = logging.getLogger(__name__)

def with_error_handling(func):
    """Decorator to add comprehensive error handling."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        try:
            safe_args = []
            for i, arg in enumerate(args[:2]):
                if isinstance(arg, (str, int, float, bool)):
                    safe_args.append(str(arg))
                else:
                    safe_args.append(f"<{type(arg).__name__}>")
            logger.info(f"Starting {func.__name__} with args: {safe_args}")
            
            result = func(*args, **kwargs)
            
            if isinstance(result, dict) and 'error' not in result:
                result = standardize_response(result, func.__name__)
            
            execution_time = time.time() - start_time
            logger.info(f"Completed {func.__name__} in {execution_time:.2f}s")
            return result
            
        except requests.exceptions.Timeout:
            logger.error(f"Timeout in {func.__name__}")
            return create_error_response("Request timeout", func.__name__)
        except requests.exceptions.ConnectionError:
            logger.error(f"Connection error in {func.__name__}")
            return create_error_response("Connection failed", func.__name__)
        except requests.exceptions.RequestException as e:
            logger.error(f"Request error in {func.__name__}: {e}")
            return create_error_response(f"Request failed: {str(e)}", func.__name__)
        except Exception as e:
            logger.error(f"Unexpected error in {func.__name__}: {e}")
            return create_error_response(f"Unexpected error: {str(e)}", func.__name__)
    
    return wrapper

def standardize_response(data: Dict[str, Any], source: str) -> Dict[str, Any]:
    """Standardize API response format with metadata."""
    # Check if data is already a standardized response to avoid double nesting
    if isinstance(data, dict) and "data" in data and "metadata" in data and "status" in data:
        return data
    
    return {
        "data": data,
        "metadata": {
            "source": source,
            "timestamp": datetime.now().isoformat(),
            "version": "1.1.0",
            "cached": False
        },
        "status": "success"
    }

def create_error_response(error_msg: str, source: str, error_code: str = None) -> Dict[str, Any]:
    """Create standardized error response with optional error code."""
    response = {
        "data": None,
        "metadata": {
            "source": source,
            "timestamp": datetime.now().isoformat(),
            "version": "1.1.0"
        },
        "status": "error",
        "error": error_msg
    }
    
    if error_code:
        response["error_code"] = error_code
    
    return response

def make_api_request(url: str, params: Dict[str, Any], timeout: int = 15, max_retries: int = 3) -> requests.Response:
    """Make API request with retry logic and rate limiting."""
    for attempt in range(max_retries):
        try:
            time.sleep(0.1 * attempt)
            response = requests.get(url, params=params, timeout=timeout)
            if response.status_code == 429:
                wait_time = 2 ** attempt
                logger.warning(f"Rate limited, waiting {wait_time}s before retry {attempt + 1}")
                time.sleep(wait_time)
                continue
            return response
        except requests.exceptions.RequestException as e:
            if attempt == max_retries - 1:
                raise
            logger.warning(f"Request attempt {attempt + 1} failed: {e}")
    
    raise requests.exceptions.RequestException("Max retries exceeded")

def format_json_output(obj: Any) -> str:
    """
    Formats a Python object as a pretty-printed JSON string.

    Args:
        obj: The Python object to format.

    Returns:
        A JSON string with an indent of 2 and UTF-8 characters preserved.
    """
    return json.dumps(obj, indent=2, ensure_ascii=False)