Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions python/valuecell/server/api/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""API exception handling module."""

from typing import Dict, Any
from fastapi import HTTPException, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError

from .schemas import ErrorResponse, StatusCode


class APIException(Exception):
"""Custom API exception base class."""

def __init__(self, code: StatusCode, message: str, details: Dict[str, Any] = None):
self.code = code
self.message = message
self.details = details or {}
super().__init__(message)


class UnauthorizedException(APIException):
"""Unauthorized exception."""

def __init__(self, message: str = "Unauthorized access"):
super().__init__(StatusCode.UNAUTHORIZED, message)


class NotFoundException(APIException):
"""Resource not found exception."""

def __init__(self, message: str = "Resource not found"):
super().__init__(StatusCode.NOT_FOUND, message)


class ForbiddenException(APIException):
"""Forbidden access exception."""

def __init__(self, message: str = "Forbidden access"):
super().__init__(StatusCode.FORBIDDEN, message)


class InternalServerException(APIException):
"""Internal server error exception."""

def __init__(self, message: str = "Internal server error"):
super().__init__(StatusCode.INTERNAL_ERROR, message)


async def api_exception_handler(request: Request, exc: APIException) -> JSONResponse:
"""API exception handler."""
return JSONResponse(
status_code=200, # HTTP status code is always 200, error info is in response body
content=ErrorResponse.create(code=exc.code, msg=exc.message).dict(),
)


async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
"""HTTP exception handler."""
# Map HTTP status codes to our status codes
status_code_mapping = {
400: StatusCode.BAD_REQUEST,
401: StatusCode.UNAUTHORIZED,
403: StatusCode.FORBIDDEN,
404: StatusCode.NOT_FOUND,
500: StatusCode.INTERNAL_ERROR,
}

api_code = status_code_mapping.get(exc.status_code, StatusCode.INTERNAL_ERROR)
return JSONResponse(
status_code=200,
content=ErrorResponse.create(code=api_code, msg=str(exc.detail)).dict(),
)


async def validation_exception_handler(
request: Request, exc: RequestValidationError
) -> JSONResponse:
"""Request validation exception handler."""
# Extract validation error information
error_details = []
for error in exc.errors():
error_details.append(
{
"field": ".".join(str(x) for x in error["loc"]),
"message": error["msg"],
"type": error["type"],
}
)

return JSONResponse(
status_code=200,
content=ErrorResponse.create(
code=StatusCode.BAD_REQUEST,
msg=f"Request parameter validation failed: {'; '.join([f'{e["field"]}: {e["message"]}' for e in error_details])}",
).dict(),
)


async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse:
"""General exception handler."""
return JSONResponse(
status_code=200,
content=ErrorResponse.create(
code=StatusCode.INTERNAL_ERROR,
msg="Internal server error, please try again later",
).dict(),
)
8 changes: 8 additions & 0 deletions python/valuecell/server/api/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""API router module."""

from .i18n import create_i18n_router, get_i18n_router

__all__ = [
"create_i18n_router",
"get_i18n_router",
]
48 changes: 48 additions & 0 deletions python/valuecell/server/api/routers/i18n.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""RESTful i18n API router module."""

from fastapi import APIRouter
from ..i18n_api import get_i18n_api


def create_i18n_router() -> APIRouter:
"""Create RESTful style i18n router.

API path design:
- GET /api/v1/i18n/config - Get i18n configuration
- GET /api/v1/i18n/languages - Get supported languages list
- PUT /api/v1/i18n/language - Set language
- GET /api/v1/i18n/timezones - Get supported timezones list
- PUT /api/v1/i18n/timezone - Set timezone
- POST /api/v1/i18n/language/detect - Detect language
- POST /api/v1/i18n/translate - Translate text
- POST /api/v1/i18n/format/datetime - Format datetime
- POST /api/v1/i18n/format/number - Format number
- POST /api/v1/i18n/format/currency - Format currency
- GET /api/v1/i18n/users/{user_id}/settings - Get user i18n settings
- PUT /api/v1/i18n/users/{user_id}/settings - Update user i18n settings
- GET /api/v1/i18n/agents/context - Get Agent i18n context

Returns:
APIRouter: Configured i18n router
"""
# Get existing i18n router, but modify prefix to comply with RESTful style
i18n_api = get_i18n_api()
router = i18n_api.router

# Update router prefix to comply with RESTful API versioning
router.prefix = "/api/v1/i18n"

return router


def get_i18n_router() -> APIRouter:
"""Get i18n router instance (backward compatible).

Returns:
APIRouter: Configured i18n router
"""
return create_i18n_router()


# Export the router functions
__all__ = ["create_i18n_router", "get_i18n_router"]
63 changes: 63 additions & 0 deletions python/valuecell/server/api/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""API schemas package."""

from .base import (
StatusCode,
BaseResponse,
SuccessResponse,
ErrorResponse,
AppInfoData,
HealthCheckData,
)
from .i18n import (
I18nConfigData,
SupportedLanguage,
SupportedLanguagesData,
TimezoneInfo,
TimezonesData,
LanguageRequest,
TimezoneRequest,
LanguageDetectionRequest,
TranslationRequest,
DateTimeFormatRequest,
NumberFormatRequest,
CurrencyFormatRequest,
UserI18nSettingsData,
UserI18nSettingsRequest,
AgentI18nContextData,
LanguageDetectionData,
TranslationData,
DateTimeFormatData,
NumberFormatData,
CurrencyFormatData,
)

__all__ = [
# Base schemas
"StatusCode",
"BaseResponse",
"SuccessResponse",
"ErrorResponse",
"AppInfoData",
"HealthCheckData",
# I18n schemas
"I18nConfigData",
"SupportedLanguage",
"SupportedLanguagesData",
"TimezoneInfo",
"TimezonesData",
"LanguageRequest",
"TimezoneRequest",
"LanguageDetectionRequest",
"TranslationRequest",
"DateTimeFormatRequest",
"NumberFormatRequest",
"CurrencyFormatRequest",
"UserI18nSettingsData",
"UserI18nSettingsRequest",
"AgentI18nContextData",
"LanguageDetectionData",
"TranslationData",
"DateTimeFormatData",
"NumberFormatData",
"CurrencyFormatData",
]
74 changes: 74 additions & 0 deletions python/valuecell/server/api/schemas/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Base API schemas for ValueCell application."""

from typing import Optional, Generic, TypeVar
from datetime import datetime
from enum import IntEnum
from pydantic import BaseModel, Field

T = TypeVar("T")


class StatusCode(IntEnum):
"""Unified API status code enumeration."""

# Success status codes
SUCCESS = 0

# Client error status codes
BAD_REQUEST = 400 # Bad request parameters
UNAUTHORIZED = 401 # Unauthorized access
FORBIDDEN = 403 # Forbidden access
NOT_FOUND = 404 # Resource not found

# Server error status codes
INTERNAL_ERROR = 500 # Internal server error


class BaseResponse(BaseModel, Generic[T]):
"""Unified API response base model."""

code: int = Field(..., description="Status code")
msg: str = Field(..., description="Response message")
data: Optional[T] = Field(None, description="Response data")


class SuccessResponse(BaseResponse[T]):
"""Success response model."""

code: int = Field(default=StatusCode.SUCCESS, description="Success status code")
msg: str = Field(default="success", description="Success message")

@classmethod
def create(cls, data: T = None, msg: str = "success") -> "SuccessResponse[T]":
"""Create success response."""
return cls(code=StatusCode.SUCCESS, msg=msg, data=data)


class ErrorResponse(BaseResponse[None]):
"""Error response model."""

code: int = Field(..., description="Error status code")
msg: str = Field(..., description="Error message")
data: None = Field(default=None, description="Data is null for errors")

@classmethod
def create(cls, code: StatusCode, msg: str) -> "ErrorResponse":
"""Create error response."""
return cls(code=code, msg=msg, data=None)


# Common data response models
class AppInfoData(BaseModel):
"""Application information data."""

name: str = Field(..., description="Application name")
version: str = Field(..., description="Application version")
environment: str = Field(..., description="Runtime environment")


class HealthCheckData(BaseModel):
"""Health check data."""

status: str = Field(..., description="Service status")
version: str = Field(..., description="Service version")
timestamp: Optional[datetime] = Field(None, description="Check timestamp")
Loading