326 lines
10 KiB
Python
326 lines
10 KiB
Python
"""
|
|
Response formatting for API endpoints.
|
|
"""
|
|
|
|
import json
|
|
from typing import Any, Dict, Optional, List
|
|
from datetime import datetime
|
|
from ..utils.logging import get_logger
|
|
from ..utils.timing import get_current_timestamp
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class ResponseFormatter:
|
|
"""
|
|
Formats API responses with consistent structure and metadata.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize response formatter"""
|
|
self.responses_formatted = 0
|
|
logger.info("Response formatter initialized")
|
|
|
|
def success(self, data: Any, message: str = "Success",
|
|
metadata: Optional[Dict] = None) -> Dict[str, Any]:
|
|
"""
|
|
Format successful response.
|
|
|
|
Args:
|
|
data: Response data
|
|
message: Success message
|
|
metadata: Additional metadata
|
|
|
|
Returns:
|
|
Dict: Formatted response
|
|
"""
|
|
response = {
|
|
'success': True,
|
|
'message': message,
|
|
'data': data,
|
|
'timestamp': get_current_timestamp().isoformat(),
|
|
'metadata': metadata or {}
|
|
}
|
|
|
|
self.responses_formatted += 1
|
|
return response
|
|
|
|
def error(self, message: str, error_code: str = "UNKNOWN_ERROR",
|
|
details: Optional[Dict] = None, status_code: int = 400) -> Dict[str, Any]:
|
|
"""
|
|
Format error response.
|
|
|
|
Args:
|
|
message: Error message
|
|
error_code: Error code
|
|
details: Error details
|
|
status_code: HTTP status code
|
|
|
|
Returns:
|
|
Dict: Formatted error response
|
|
"""
|
|
response = {
|
|
'success': False,
|
|
'error': {
|
|
'message': message,
|
|
'code': error_code,
|
|
'details': details or {},
|
|
'status_code': status_code
|
|
},
|
|
'timestamp': get_current_timestamp().isoformat()
|
|
}
|
|
|
|
self.responses_formatted += 1
|
|
return response
|
|
|
|
def paginated(self, data: List[Any], page: int, page_size: int,
|
|
total_count: int, message: str = "Success") -> Dict[str, Any]:
|
|
"""
|
|
Format paginated response.
|
|
|
|
Args:
|
|
data: Page data
|
|
page: Current page number
|
|
page_size: Items per page
|
|
total_count: Total number of items
|
|
message: Success message
|
|
|
|
Returns:
|
|
Dict: Formatted paginated response
|
|
"""
|
|
total_pages = (total_count + page_size - 1) // page_size
|
|
|
|
pagination = {
|
|
'page': page,
|
|
'page_size': page_size,
|
|
'total_count': total_count,
|
|
'total_pages': total_pages,
|
|
'has_next': page < total_pages,
|
|
'has_previous': page > 1
|
|
}
|
|
|
|
return self.success(
|
|
data=data,
|
|
message=message,
|
|
metadata={'pagination': pagination}
|
|
)
|
|
|
|
def heatmap_response(self, heatmap_data, symbol: str,
|
|
exchange: Optional[str] = None) -> Dict[str, Any]:
|
|
"""
|
|
Format heatmap data response.
|
|
|
|
Args:
|
|
heatmap_data: Heatmap data
|
|
symbol: Trading symbol
|
|
exchange: Exchange name (None for consolidated)
|
|
|
|
Returns:
|
|
Dict: Formatted heatmap response
|
|
"""
|
|
if not heatmap_data:
|
|
return self.error("Heatmap data not found", "HEATMAP_NOT_FOUND", status_code=404)
|
|
|
|
# Convert heatmap to API format
|
|
formatted_data = {
|
|
'symbol': heatmap_data.symbol,
|
|
'timestamp': heatmap_data.timestamp.isoformat(),
|
|
'bucket_size': heatmap_data.bucket_size,
|
|
'exchange': exchange,
|
|
'points': [
|
|
{
|
|
'price': point.price,
|
|
'volume': point.volume,
|
|
'intensity': point.intensity,
|
|
'side': point.side
|
|
}
|
|
for point in heatmap_data.data
|
|
]
|
|
}
|
|
|
|
metadata = {
|
|
'total_points': len(heatmap_data.data),
|
|
'bid_points': len([p for p in heatmap_data.data if p.side == 'bid']),
|
|
'ask_points': len([p for p in heatmap_data.data if p.side == 'ask']),
|
|
'data_type': 'consolidated' if not exchange else 'exchange_specific'
|
|
}
|
|
|
|
return self.success(
|
|
data=formatted_data,
|
|
message=f"Heatmap data for {symbol}",
|
|
metadata=metadata
|
|
)
|
|
|
|
def orderbook_response(self, orderbook_data, symbol: str, exchange: str) -> Dict[str, Any]:
|
|
"""
|
|
Format order book response.
|
|
|
|
Args:
|
|
orderbook_data: Order book data
|
|
symbol: Trading symbol
|
|
exchange: Exchange name
|
|
|
|
Returns:
|
|
Dict: Formatted order book response
|
|
"""
|
|
if not orderbook_data:
|
|
return self.error("Order book not found", "ORDERBOOK_NOT_FOUND", status_code=404)
|
|
|
|
# Convert order book to API format
|
|
formatted_data = {
|
|
'symbol': orderbook_data.symbol,
|
|
'exchange': orderbook_data.exchange,
|
|
'timestamp': orderbook_data.timestamp.isoformat(),
|
|
'sequence_id': orderbook_data.sequence_id,
|
|
'bids': [
|
|
{
|
|
'price': bid.price,
|
|
'size': bid.size,
|
|
'count': bid.count
|
|
}
|
|
for bid in orderbook_data.bids
|
|
],
|
|
'asks': [
|
|
{
|
|
'price': ask.price,
|
|
'size': ask.size,
|
|
'count': ask.count
|
|
}
|
|
for ask in orderbook_data.asks
|
|
],
|
|
'mid_price': orderbook_data.mid_price,
|
|
'spread': orderbook_data.spread,
|
|
'bid_volume': orderbook_data.bid_volume,
|
|
'ask_volume': orderbook_data.ask_volume
|
|
}
|
|
|
|
metadata = {
|
|
'bid_levels': len(orderbook_data.bids),
|
|
'ask_levels': len(orderbook_data.asks),
|
|
'total_bid_volume': orderbook_data.bid_volume,
|
|
'total_ask_volume': orderbook_data.ask_volume
|
|
}
|
|
|
|
return self.success(
|
|
data=formatted_data,
|
|
message=f"Order book for {symbol}@{exchange}",
|
|
metadata=metadata
|
|
)
|
|
|
|
def metrics_response(self, metrics_data, symbol: str, exchange: str) -> Dict[str, Any]:
|
|
"""
|
|
Format metrics response.
|
|
|
|
Args:
|
|
metrics_data: Metrics data
|
|
symbol: Trading symbol
|
|
exchange: Exchange name
|
|
|
|
Returns:
|
|
Dict: Formatted metrics response
|
|
"""
|
|
if not metrics_data:
|
|
return self.error("Metrics not found", "METRICS_NOT_FOUND", status_code=404)
|
|
|
|
# Convert metrics to API format
|
|
formatted_data = {
|
|
'symbol': metrics_data.symbol,
|
|
'exchange': metrics_data.exchange,
|
|
'timestamp': metrics_data.timestamp.isoformat(),
|
|
'mid_price': metrics_data.mid_price,
|
|
'spread': metrics_data.spread,
|
|
'spread_percentage': metrics_data.spread_percentage,
|
|
'bid_volume': metrics_data.bid_volume,
|
|
'ask_volume': metrics_data.ask_volume,
|
|
'volume_imbalance': metrics_data.volume_imbalance,
|
|
'depth_10': metrics_data.depth_10,
|
|
'depth_50': metrics_data.depth_50
|
|
}
|
|
|
|
return self.success(
|
|
data=formatted_data,
|
|
message=f"Metrics for {symbol}@{exchange}"
|
|
)
|
|
|
|
def status_response(self, status_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Format system status response.
|
|
|
|
Args:
|
|
status_data: System status data
|
|
|
|
Returns:
|
|
Dict: Formatted status response
|
|
"""
|
|
return self.success(
|
|
data=status_data,
|
|
message="System status",
|
|
metadata={'response_count': self.responses_formatted}
|
|
)
|
|
|
|
def rate_limit_error(self, client_stats: Dict[str, float]) -> Dict[str, Any]:
|
|
"""
|
|
Format rate limit error response.
|
|
|
|
Args:
|
|
client_stats: Client rate limit statistics
|
|
|
|
Returns:
|
|
Dict: Formatted rate limit error
|
|
"""
|
|
return self.error(
|
|
message="Rate limit exceeded",
|
|
error_code="RATE_LIMIT_EXCEEDED",
|
|
details={
|
|
'remaining_tokens': client_stats['remaining_tokens'],
|
|
'reset_time': client_stats['reset_time'],
|
|
'requests_last_minute': client_stats['requests_last_minute']
|
|
},
|
|
status_code=429
|
|
)
|
|
|
|
def validation_error(self, field: str, message: str) -> Dict[str, Any]:
|
|
"""
|
|
Format validation error response.
|
|
|
|
Args:
|
|
field: Field that failed validation
|
|
message: Validation error message
|
|
|
|
Returns:
|
|
Dict: Formatted validation error
|
|
"""
|
|
return self.error(
|
|
message=f"Validation error: {message}",
|
|
error_code="VALIDATION_ERROR",
|
|
details={'field': field, 'message': message},
|
|
status_code=400
|
|
)
|
|
|
|
def to_json(self, response: Dict[str, Any], indent: Optional[int] = None) -> str:
|
|
"""
|
|
Convert response to JSON string.
|
|
|
|
Args:
|
|
response: Response dictionary
|
|
indent: JSON indentation (None for compact)
|
|
|
|
Returns:
|
|
str: JSON string
|
|
"""
|
|
try:
|
|
return json.dumps(response, indent=indent, ensure_ascii=False, default=str)
|
|
except Exception as e:
|
|
logger.error(f"Error converting response to JSON: {e}")
|
|
return json.dumps(self.error("JSON serialization failed", "JSON_ERROR"))
|
|
|
|
def get_stats(self) -> Dict[str, int]:
|
|
"""Get formatter statistics"""
|
|
return {
|
|
'responses_formatted': self.responses_formatted
|
|
}
|
|
|
|
def reset_stats(self) -> None:
|
|
"""Reset formatter statistics"""
|
|
self.responses_formatted = 0
|
|
logger.info("Response formatter statistics reset") |