initial project setup with README and ignore
This commit is contained in:
2
app/core/__init__.py
Normal file
2
app/core/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Core application components."""
|
||||
|
||||
63
app/core/arrow_utils.py
Normal file
63
app/core/arrow_utils.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
High-performance utilities using Apache Arrow and NumPy for geographic data.
|
||||
Provides vectorized operations for distances and coordinate processing.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
import pyarrow.parquet as pq
|
||||
import logging
|
||||
from typing import List, Dict, Any, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def calculate_haversine_matrix_vectorized(lats: np.ndarray, lons: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Calculate an N x N distance matrix using the Haversine formula.
|
||||
Fully vectorized using NumPy for O(N^2) speed improvement over Python loops.
|
||||
"""
|
||||
# Earth's radius in kilometers
|
||||
R = 6371.0
|
||||
|
||||
# Convert degrees to radians
|
||||
lats_rad = np.radians(lats)
|
||||
lons_rad = np.radians(lons)
|
||||
|
||||
# Create meshgrids for pairwise differences
|
||||
# lats.reshape(-1, 1) creates a column vector
|
||||
# lats.reshape(1, -1) creates a row vector
|
||||
# Subtracting them creates an N x N matrix of differences
|
||||
dlat = lats_rad.reshape(-1, 1) - lats_rad.reshape(1, -1)
|
||||
dlon = lons_rad.reshape(-1, 1) - lons_rad.reshape(1, -1)
|
||||
|
||||
# Haversine formula
|
||||
a = np.sin(dlat / 2)**2 + np.cos(lats_rad.reshape(-1, 1)) * np.cos(lats_rad.reshape(1, -1)) * np.sin(dlon / 2)**2
|
||||
c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a))
|
||||
|
||||
return R * c
|
||||
|
||||
def orders_to_arrow_table(orders: List[Dict[str, Any]]) -> pa.Table:
|
||||
"""
|
||||
Convert a list of order dictionaries to an Apache Arrow Table.
|
||||
This enables zero-copy operations and efficient columnar storage.
|
||||
"""
|
||||
return pa.Table.from_pylist(orders)
|
||||
|
||||
def save_optimized_route_parquet(orders: List[Dict[str, Any]], filename: str):
|
||||
"""
|
||||
Save optimized route data to a Parquet file for high-speed analysis.
|
||||
Useful for logging and historical simulation replays.
|
||||
"""
|
||||
try:
|
||||
table = orders_to_arrow_table(orders)
|
||||
pq.write_table(table, filename)
|
||||
logger.info(f" Saved route data to Parquet: {filename}")
|
||||
except Exception as e:
|
||||
logger.error(f" Failed to save Parquet: {e}")
|
||||
|
||||
def load_route_parquet(filename: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Load route data from a Parquet file and return as a list of dicts.
|
||||
"""
|
||||
table = pq.read_table(filename)
|
||||
return table.to_pylist()
|
||||
26
app/core/constants.py
Normal file
26
app/core/constants.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""API constants and configuration."""
|
||||
|
||||
# API Configuration
|
||||
API_VERSION = "2.0.0"
|
||||
API_TITLE = "Route Optimization API"
|
||||
API_DESCRIPTION = "Professional API for delivery route optimization"
|
||||
|
||||
# Route Optimization Limits
|
||||
MAX_DELIVERIES = 50
|
||||
MIN_DELIVERIES = 1
|
||||
|
||||
# Coordinate Validation
|
||||
MIN_LATITUDE = -90
|
||||
MAX_LATITUDE = 90
|
||||
MIN_LONGITUDE = -180
|
||||
MAX_LONGITUDE = 180
|
||||
|
||||
# Algorithm Types
|
||||
ALGORITHM_GREEDY = "greedy"
|
||||
ALGORITHM_TSP = "tsp"
|
||||
|
||||
# Response Messages
|
||||
MESSAGE_SUCCESS = "Route optimized successfully"
|
||||
MESSAGE_VALIDATION_ERROR = "Request validation failed"
|
||||
MESSAGE_INTERNAL_ERROR = "An unexpected error occurred"
|
||||
|
||||
112
app/core/exception_handlers.py
Normal file
112
app/core/exception_handlers.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""Professional exception handlers for the API."""
|
||||
|
||||
import logging
|
||||
from fastapi import Request, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
|
||||
from app.core.exceptions import APIException
|
||||
from app.models.errors import ErrorResponse, ErrorDetail
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def api_exception_handler(request: Request, exc: APIException) -> JSONResponse:
|
||||
"""Handle custom API exceptions."""
|
||||
request_id = getattr(request.state, "request_id", None)
|
||||
|
||||
error_response = ErrorResponse(
|
||||
success=False,
|
||||
error=ErrorDetail(
|
||||
field=exc.field,
|
||||
message=exc.message,
|
||||
code=exc.code
|
||||
),
|
||||
path=request.url.path,
|
||||
request_id=request_id
|
||||
)
|
||||
|
||||
logger.warning(f"API Exception: {exc.code} - {exc.message} (Request ID: {request_id})")
|
||||
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content=error_response.model_dump(exclude_none=True)
|
||||
)
|
||||
|
||||
|
||||
async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse:
|
||||
"""Handle HTTP exceptions."""
|
||||
request_id = getattr(request.state, "request_id", None)
|
||||
|
||||
error_response = ErrorResponse(
|
||||
success=False,
|
||||
error=ErrorDetail(
|
||||
message=exc.detail,
|
||||
code="HTTP_ERROR"
|
||||
),
|
||||
path=request.url.path,
|
||||
request_id=request_id
|
||||
)
|
||||
|
||||
logger.warning(f"HTTP Exception: {exc.status_code} - {exc.detail} (Request ID: {request_id})")
|
||||
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content=error_response.model_dump(exclude_none=True)
|
||||
)
|
||||
|
||||
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
||||
"""Handle validation errors with detailed field information."""
|
||||
request_id = getattr(request.state, "request_id", None)
|
||||
|
||||
errors = exc.errors()
|
||||
if errors:
|
||||
first_error = errors[0]
|
||||
field = ".".join(str(loc) for loc in first_error.get("loc", []))
|
||||
message = first_error.get("msg", "Validation error")
|
||||
else:
|
||||
field = None
|
||||
message = "Validation error"
|
||||
|
||||
error_response = ErrorResponse(
|
||||
success=False,
|
||||
error=ErrorDetail(
|
||||
field=field,
|
||||
message=message,
|
||||
code="VALIDATION_ERROR"
|
||||
),
|
||||
path=request.url.path,
|
||||
request_id=request_id
|
||||
)
|
||||
|
||||
logger.warning(f"Validation Error: {message} (Field: {field}, Request ID: {request_id})")
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content=error_response.model_dump(exclude_none=True)
|
||||
)
|
||||
|
||||
|
||||
async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||
"""Handle unexpected exceptions."""
|
||||
request_id = getattr(request.state, "request_id", None)
|
||||
|
||||
error_response = ErrorResponse(
|
||||
success=False,
|
||||
error=ErrorDetail(
|
||||
message="An unexpected error occurred. Please try again later.",
|
||||
code="INTERNAL_SERVER_ERROR"
|
||||
),
|
||||
path=request.url.path,
|
||||
request_id=request_id
|
||||
)
|
||||
|
||||
logger.error(f"Unexpected Error: {str(exc)} (Request ID: {request_id})", exc_info=True)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content=error_response.model_dump(exclude_none=True)
|
||||
)
|
||||
|
||||
70
app/core/exceptions.py
Normal file
70
app/core/exceptions.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Custom exceptions for the API."""
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
|
||||
class APIException(HTTPException):
|
||||
"""Base API exception with structured error format."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
status_code: int,
|
||||
message: str,
|
||||
field: str = None,
|
||||
code: str = None,
|
||||
detail: str = None
|
||||
):
|
||||
self.message = message
|
||||
self.field = field
|
||||
self.code = code or self._get_default_code(status_code)
|
||||
super().__init__(status_code=status_code, detail=detail or message)
|
||||
|
||||
def _get_default_code(self, status_code: int) -> str:
|
||||
"""Get default error code based on status code."""
|
||||
codes = {
|
||||
400: "BAD_REQUEST",
|
||||
401: "UNAUTHORIZED",
|
||||
403: "FORBIDDEN",
|
||||
404: "NOT_FOUND",
|
||||
409: "CONFLICT",
|
||||
422: "VALIDATION_ERROR",
|
||||
429: "RATE_LIMIT_EXCEEDED",
|
||||
500: "INTERNAL_SERVER_ERROR",
|
||||
503: "SERVICE_UNAVAILABLE"
|
||||
}
|
||||
return codes.get(status_code, "UNKNOWN_ERROR")
|
||||
|
||||
|
||||
class ValidationError(APIException):
|
||||
"""Validation error exception."""
|
||||
|
||||
def __init__(self, message: str, field: str = None):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
message=message,
|
||||
field=field,
|
||||
code="VALIDATION_ERROR"
|
||||
)
|
||||
|
||||
|
||||
class NotFoundError(APIException):
|
||||
"""Resource not found exception."""
|
||||
|
||||
def __init__(self, message: str = "Resource not found"):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
message=message,
|
||||
code="NOT_FOUND"
|
||||
)
|
||||
|
||||
|
||||
class RateLimitError(APIException):
|
||||
"""Rate limit exceeded exception."""
|
||||
|
||||
def __init__(self, message: str = "Rate limit exceeded"):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
||||
message=message,
|
||||
code="RATE_LIMIT_EXCEEDED"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user