Skip to content

Commit 7c977aa

Browse files
authored
feat: opt i18n code to /server/api dir (#18)
1 parent 85f36a4 commit 7c977aa

6 files changed

Lines changed: 499 additions & 0 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""API exception handling module."""
2+
3+
from typing import Dict, Any
4+
from fastapi import HTTPException, Request
5+
from fastapi.responses import JSONResponse
6+
from fastapi.exceptions import RequestValidationError
7+
8+
from .schemas import ErrorResponse, StatusCode
9+
10+
11+
class APIException(Exception):
12+
"""Custom API exception base class."""
13+
14+
def __init__(self, code: StatusCode, message: str, details: Dict[str, Any] = None):
15+
self.code = code
16+
self.message = message
17+
self.details = details or {}
18+
super().__init__(message)
19+
20+
21+
class UnauthorizedException(APIException):
22+
"""Unauthorized exception."""
23+
24+
def __init__(self, message: str = "Unauthorized access"):
25+
super().__init__(StatusCode.UNAUTHORIZED, message)
26+
27+
28+
class NotFoundException(APIException):
29+
"""Resource not found exception."""
30+
31+
def __init__(self, message: str = "Resource not found"):
32+
super().__init__(StatusCode.NOT_FOUND, message)
33+
34+
35+
class ForbiddenException(APIException):
36+
"""Forbidden access exception."""
37+
38+
def __init__(self, message: str = "Forbidden access"):
39+
super().__init__(StatusCode.FORBIDDEN, message)
40+
41+
42+
class InternalServerException(APIException):
43+
"""Internal server error exception."""
44+
45+
def __init__(self, message: str = "Internal server error"):
46+
super().__init__(StatusCode.INTERNAL_ERROR, message)
47+
48+
49+
async def api_exception_handler(request: Request, exc: APIException) -> JSONResponse:
50+
"""API exception handler."""
51+
return JSONResponse(
52+
status_code=200, # HTTP status code is always 200, error info is in response body
53+
content=ErrorResponse.create(code=exc.code, msg=exc.message).dict(),
54+
)
55+
56+
57+
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
58+
"""HTTP exception handler."""
59+
# Map HTTP status codes to our status codes
60+
status_code_mapping = {
61+
400: StatusCode.BAD_REQUEST,
62+
401: StatusCode.UNAUTHORIZED,
63+
403: StatusCode.FORBIDDEN,
64+
404: StatusCode.NOT_FOUND,
65+
500: StatusCode.INTERNAL_ERROR,
66+
}
67+
68+
api_code = status_code_mapping.get(exc.status_code, StatusCode.INTERNAL_ERROR)
69+
return JSONResponse(
70+
status_code=200,
71+
content=ErrorResponse.create(code=api_code, msg=str(exc.detail)).dict(),
72+
)
73+
74+
75+
async def validation_exception_handler(
76+
request: Request, exc: RequestValidationError
77+
) -> JSONResponse:
78+
"""Request validation exception handler."""
79+
# Extract validation error information
80+
error_details = []
81+
for error in exc.errors():
82+
error_details.append(
83+
{
84+
"field": ".".join(str(x) for x in error["loc"]),
85+
"message": error["msg"],
86+
"type": error["type"],
87+
}
88+
)
89+
90+
return JSONResponse(
91+
status_code=200,
92+
content=ErrorResponse.create(
93+
code=StatusCode.BAD_REQUEST,
94+
msg=f"Request parameter validation failed: {'; '.join([f'{e["field"]}: {e["message"]}' for e in error_details])}",
95+
).dict(),
96+
)
97+
98+
99+
async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse:
100+
"""General exception handler."""
101+
return JSONResponse(
102+
status_code=200,
103+
content=ErrorResponse.create(
104+
code=StatusCode.INTERNAL_ERROR,
105+
msg="Internal server error, please try again later",
106+
).dict(),
107+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""API router module."""
2+
3+
from .i18n import create_i18n_router, get_i18n_router
4+
5+
__all__ = [
6+
"create_i18n_router",
7+
"get_i18n_router",
8+
]
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""RESTful i18n API router module."""
2+
3+
from fastapi import APIRouter
4+
from ..i18n_api import get_i18n_api
5+
6+
7+
def create_i18n_router() -> APIRouter:
8+
"""Create RESTful style i18n router.
9+
10+
API path design:
11+
- GET /api/v1/i18n/config - Get i18n configuration
12+
- GET /api/v1/i18n/languages - Get supported languages list
13+
- PUT /api/v1/i18n/language - Set language
14+
- GET /api/v1/i18n/timezones - Get supported timezones list
15+
- PUT /api/v1/i18n/timezone - Set timezone
16+
- POST /api/v1/i18n/language/detect - Detect language
17+
- POST /api/v1/i18n/translate - Translate text
18+
- POST /api/v1/i18n/format/datetime - Format datetime
19+
- POST /api/v1/i18n/format/number - Format number
20+
- POST /api/v1/i18n/format/currency - Format currency
21+
- GET /api/v1/i18n/users/{user_id}/settings - Get user i18n settings
22+
- PUT /api/v1/i18n/users/{user_id}/settings - Update user i18n settings
23+
- GET /api/v1/i18n/agents/context - Get Agent i18n context
24+
25+
Returns:
26+
APIRouter: Configured i18n router
27+
"""
28+
# Get existing i18n router, but modify prefix to comply with RESTful style
29+
i18n_api = get_i18n_api()
30+
router = i18n_api.router
31+
32+
# Update router prefix to comply with RESTful API versioning
33+
router.prefix = "/api/v1/i18n"
34+
35+
return router
36+
37+
38+
def get_i18n_router() -> APIRouter:
39+
"""Get i18n router instance (backward compatible).
40+
41+
Returns:
42+
APIRouter: Configured i18n router
43+
"""
44+
return create_i18n_router()
45+
46+
47+
# Export the router functions
48+
__all__ = ["create_i18n_router", "get_i18n_router"]
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""API schemas package."""
2+
3+
from .base import (
4+
StatusCode,
5+
BaseResponse,
6+
SuccessResponse,
7+
ErrorResponse,
8+
AppInfoData,
9+
HealthCheckData,
10+
)
11+
from .i18n import (
12+
I18nConfigData,
13+
SupportedLanguage,
14+
SupportedLanguagesData,
15+
TimezoneInfo,
16+
TimezonesData,
17+
LanguageRequest,
18+
TimezoneRequest,
19+
LanguageDetectionRequest,
20+
TranslationRequest,
21+
DateTimeFormatRequest,
22+
NumberFormatRequest,
23+
CurrencyFormatRequest,
24+
UserI18nSettingsData,
25+
UserI18nSettingsRequest,
26+
AgentI18nContextData,
27+
LanguageDetectionData,
28+
TranslationData,
29+
DateTimeFormatData,
30+
NumberFormatData,
31+
CurrencyFormatData,
32+
)
33+
34+
__all__ = [
35+
# Base schemas
36+
"StatusCode",
37+
"BaseResponse",
38+
"SuccessResponse",
39+
"ErrorResponse",
40+
"AppInfoData",
41+
"HealthCheckData",
42+
# I18n schemas
43+
"I18nConfigData",
44+
"SupportedLanguage",
45+
"SupportedLanguagesData",
46+
"TimezoneInfo",
47+
"TimezonesData",
48+
"LanguageRequest",
49+
"TimezoneRequest",
50+
"LanguageDetectionRequest",
51+
"TranslationRequest",
52+
"DateTimeFormatRequest",
53+
"NumberFormatRequest",
54+
"CurrencyFormatRequest",
55+
"UserI18nSettingsData",
56+
"UserI18nSettingsRequest",
57+
"AgentI18nContextData",
58+
"LanguageDetectionData",
59+
"TranslationData",
60+
"DateTimeFormatData",
61+
"NumberFormatData",
62+
"CurrencyFormatData",
63+
]
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Base API schemas for ValueCell application."""
2+
3+
from typing import Optional, Generic, TypeVar
4+
from datetime import datetime
5+
from enum import IntEnum
6+
from pydantic import BaseModel, Field
7+
8+
T = TypeVar("T")
9+
10+
11+
class StatusCode(IntEnum):
12+
"""Unified API status code enumeration."""
13+
14+
# Success status codes
15+
SUCCESS = 0
16+
17+
# Client error status codes
18+
BAD_REQUEST = 400 # Bad request parameters
19+
UNAUTHORIZED = 401 # Unauthorized access
20+
FORBIDDEN = 403 # Forbidden access
21+
NOT_FOUND = 404 # Resource not found
22+
23+
# Server error status codes
24+
INTERNAL_ERROR = 500 # Internal server error
25+
26+
27+
class BaseResponse(BaseModel, Generic[T]):
28+
"""Unified API response base model."""
29+
30+
code: int = Field(..., description="Status code")
31+
msg: str = Field(..., description="Response message")
32+
data: Optional[T] = Field(None, description="Response data")
33+
34+
35+
class SuccessResponse(BaseResponse[T]):
36+
"""Success response model."""
37+
38+
code: int = Field(default=StatusCode.SUCCESS, description="Success status code")
39+
msg: str = Field(default="success", description="Success message")
40+
41+
@classmethod
42+
def create(cls, data: T = None, msg: str = "success") -> "SuccessResponse[T]":
43+
"""Create success response."""
44+
return cls(code=StatusCode.SUCCESS, msg=msg, data=data)
45+
46+
47+
class ErrorResponse(BaseResponse[None]):
48+
"""Error response model."""
49+
50+
code: int = Field(..., description="Error status code")
51+
msg: str = Field(..., description="Error message")
52+
data: None = Field(default=None, description="Data is null for errors")
53+
54+
@classmethod
55+
def create(cls, code: StatusCode, msg: str) -> "ErrorResponse":
56+
"""Create error response."""
57+
return cls(code=code, msg=msg, data=None)
58+
59+
60+
# Common data response models
61+
class AppInfoData(BaseModel):
62+
"""Application information data."""
63+
64+
name: str = Field(..., description="Application name")
65+
version: str = Field(..., description="Application version")
66+
environment: str = Field(..., description="Runtime environment")
67+
68+
69+
class HealthCheckData(BaseModel):
70+
"""Health check data."""
71+
72+
status: str = Field(..., description="Service status")
73+
version: str = Field(..., description="Service version")
74+
timestamp: Optional[datetime] = Field(None, description="Check timestamp")

0 commit comments

Comments
 (0)