diff --git a/python/valuecell/server/api/exceptions.py b/python/valuecell/server/api/exceptions.py new file mode 100644 index 000000000..1f0d9d3e3 --- /dev/null +++ b/python/valuecell/server/api/exceptions.py @@ -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(), + ) diff --git a/python/valuecell/server/api/routers/__init__.py b/python/valuecell/server/api/routers/__init__.py index e69de29bb..3e64b20fc 100644 --- a/python/valuecell/server/api/routers/__init__.py +++ b/python/valuecell/server/api/routers/__init__.py @@ -0,0 +1,8 @@ +"""API router module.""" + +from .i18n import create_i18n_router, get_i18n_router + +__all__ = [ + "create_i18n_router", + "get_i18n_router", +] diff --git a/python/valuecell/server/api/routers/i18n.py b/python/valuecell/server/api/routers/i18n.py new file mode 100644 index 000000000..3b7eb9d78 --- /dev/null +++ b/python/valuecell/server/api/routers/i18n.py @@ -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"] diff --git a/python/valuecell/server/api/schemas/__init__.py b/python/valuecell/server/api/schemas/__init__.py index e69de29bb..fbb867382 100644 --- a/python/valuecell/server/api/schemas/__init__.py +++ b/python/valuecell/server/api/schemas/__init__.py @@ -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", +] diff --git a/python/valuecell/server/api/schemas/base.py b/python/valuecell/server/api/schemas/base.py new file mode 100644 index 000000000..46652f596 --- /dev/null +++ b/python/valuecell/server/api/schemas/base.py @@ -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") diff --git a/python/valuecell/server/api/schemas/i18n.py b/python/valuecell/server/api/schemas/i18n.py new file mode 100644 index 000000000..9ff0206e6 --- /dev/null +++ b/python/valuecell/server/api/schemas/i18n.py @@ -0,0 +1,199 @@ +"""I18n related API schemas for ValueCell application.""" + +from typing import Dict, Any, List, Optional +from datetime import datetime +from pydantic import BaseModel, Field, validator + +from ...core.constants import SUPPORTED_LANGUAGE_CODES + + +# I18n related data models +class I18nConfigData(BaseModel): + """I18n configuration data model.""" + + language: str = Field(..., description="Current language") + timezone: str = Field(..., description="Current timezone") + date_format: str = Field(..., description="Date format") + time_format: str = Field(..., description="Time format") + datetime_format: str = Field(..., description="DateTime format") + currency_symbol: str = Field(..., description="Currency symbol") + number_format: Dict[str, str] = Field(..., description="Number format") + is_rtl: bool = Field(..., description="Whether text is right-to-left") + + +class SupportedLanguage(BaseModel): + """Supported language schema.""" + + code: str = Field(..., description="Language code") + name: str = Field(..., description="Language name") + is_current: bool = Field(..., description="Whether this is the current language") + + +class SupportedLanguagesData(BaseModel): + """Supported languages data.""" + + languages: List[SupportedLanguage] = Field( + ..., description="List of supported languages" + ) + current: str = Field(..., description="Current language code") + + +class TimezoneInfo(BaseModel): + """Timezone information schema.""" + + value: str = Field(..., description="Timezone value") + label: str = Field(..., description="Timezone display name") + is_current: bool = Field(..., description="Whether this is the current timezone") + + +class TimezonesData(BaseModel): + """Timezones data.""" + + timezones: List[TimezoneInfo] = Field(..., description="List of timezones") + current: str = Field(..., description="Current timezone") + + +# API request models +class LanguageRequest(BaseModel): + """Language change request.""" + + language: str = Field(..., description="Language code to set") + + @validator("language") + def validate_language(cls, v): + if v not in SUPPORTED_LANGUAGE_CODES: + raise ValueError(f"Language {v} is not supported") + return v + + +class TimezoneRequest(BaseModel): + """Timezone change request.""" + + timezone: str = Field(..., description="Timezone to set") + + +class LanguageDetectionRequest(BaseModel): + """Language detection request.""" + + accept_language: str = Field(..., description="Accept-Language header value") + + +class TranslationRequest(BaseModel): + """Translation request.""" + + key: str = Field(..., description="Translation key") + language: Optional[str] = Field(None, description="Target language") + variables: Optional[Dict[str, Any]] = Field( + default_factory=dict, description="Variables for string formatting" + ) + + +class DateTimeFormatRequest(BaseModel): + """DateTime formatting request.""" + + datetime: str = Field(..., description="ISO datetime string") + format_type: str = Field( + "datetime", description="Format type: date, time, or datetime" + ) + + +class NumberFormatRequest(BaseModel): + """Number formatting request.""" + + number: float = Field(..., description="Number to format") + decimal_places: int = Field(2, description="Number of decimal places") + + +class CurrencyFormatRequest(BaseModel): + """Currency formatting request.""" + + amount: float = Field(..., description="Amount to format") + decimal_places: int = Field(2, description="Number of decimal places") + + +class UserI18nSettingsData(BaseModel): + """User i18n settings data.""" + + user_id: Optional[str] = Field(None, description="User ID") + language: str = Field(default="en-US", description="User language") + timezone: str = Field(default="UTC", description="User timezone") + created_at: Optional[datetime] = Field(None, description="Creation timestamp") + updated_at: Optional[datetime] = Field(None, description="Update timestamp") + + @validator("language") + def validate_language(cls, v): + if v not in SUPPORTED_LANGUAGE_CODES: + raise ValueError(f"Language {v} is not supported") + return v + + +class UserI18nSettingsRequest(BaseModel): + """User i18n settings update request.""" + + language: Optional[str] = Field(None, description="Language to update") + timezone: Optional[str] = Field(None, description="Timezone to update") + + @validator("language") + def validate_language(cls, v): + if v and v not in SUPPORTED_LANGUAGE_CODES: + raise ValueError(f"Language {v} is not supported") + return v + + +class AgentI18nContextData(BaseModel): + """Agent i18n context data for inter-agent communication.""" + + language: str = Field(..., description="Language") + timezone: str = Field(..., description="Timezone") + currency_symbol: str = Field(..., description="Currency symbol") + date_format: str = Field(..., description="Date format") + time_format: str = Field(..., description="Time format") + number_format: Dict[str, str] = Field(..., description="Number format") + user_id: Optional[str] = Field(None, description="User ID") + session_id: Optional[str] = Field(None, description="Session ID") + + +class LanguageDetectionData(BaseModel): + """Language detection result data.""" + + detected_language: str = Field(..., description="Detected language") + language_name: str = Field(..., description="Language name") + is_supported: bool = Field(..., description="Whether the language is supported") + + +class TranslationData(BaseModel): + """Translation result data.""" + + key: str = Field(..., description="Translation key") + translated_text: str = Field(..., description="Translated text") + language: str = Field(..., description="Target language") + variables: Dict[str, Any] = Field(default_factory=dict, description="Variables") + + +class DateTimeFormatData(BaseModel): + """DateTime formatting result data.""" + + original: str = Field(..., description="Original datetime") + formatted: str = Field(..., description="Formatted datetime") + format_type: str = Field(..., description="Format type") + language: str = Field(..., description="Language") + timezone: str = Field(..., description="Timezone") + + +class NumberFormatData(BaseModel): + """Number formatting result data.""" + + original: float = Field(..., description="Original number") + formatted: str = Field(..., description="Formatted number") + decimal_places: int = Field(..., description="Number of decimal places") + language: str = Field(..., description="Language") + + +class CurrencyFormatData(BaseModel): + """Currency formatting result data.""" + + original: float = Field(..., description="Original amount") + formatted: str = Field(..., description="Formatted amount") + decimal_places: int = Field(..., description="Number of decimal places") + language: str = Field(..., description="Language") + currency_symbol: str = Field(..., description="Currency symbol")