""" Kinyarwanda number-to-words conversion endpoints. Ported from the kinya-ai-suite project's number2words engine (rule-based, no external ML dependency). All routes require the same platform token used by the rest of the Vibeon integration surface. """ from datetime import datetime as dt_type from fastapi import APIRouter, HTTPException, status, Depends from pydantic import BaseModel, Field, field_validator, ConfigDict from typing import Optional from services.kinyarwanda_numbers_service import NumberFormattingError, NumberService from utils.auth import verify_platform_token router = APIRouter(prefix="/numbers", tags=["kinyarwanda-numbers"]) number_service = NumberService() # --- Request schemas -------------------------------------------------------- class CardinalRequest(BaseModel): number: int = Field(..., description="Integer to convert to Kinyarwanda words", ge=0, le=999999999) class OrdinalRequest(BaseModel): number: int = Field(..., description="Integer to convert to ordinal", ge=1) noun_class: Optional[str] = Field(None, description="Noun class (class1, class9)", pattern="^(class1|class9)$") class CurrencyRequest(BaseModel): amount: float = Field(..., description="Currency amount", ge=0) currency_code: str = Field("RWF", description="Currency code (default: RWF)") class DateRequest(BaseModel): day: int = Field(..., description="Day of month", ge=1, le=31) month: int = Field(..., description="Month (1-12)", ge=1, le=12) year: int = Field(..., description="Year", ge=1) class TimeRequest(BaseModel): hour: int = Field(..., description="Hour (0-23, where 0 represents midnight)", ge=0, le=23) minute: int = Field(..., description="Minute (0-59)", ge=0, le=59) class DateTimeRequest(BaseModel): model_config = ConfigDict(populate_by_name=True) dt_value: dt_type = Field(..., description="Datetime to format", alias="datetime") style: str = Field("spoken", description="Format style: 'spoken' or 'full'") @field_validator("style") @classmethod def validate_style(cls, v): if v not in ("spoken", "full"): raise ValueError("Style must be 'spoken' or 'full'") return v class FractionRequest(BaseModel): numerator: int = Field(..., description="Numerator", ge=1) denominator: int = Field(..., description="Denominator", ge=1) noun_class: Optional[str] = Field(None, description="Optional noun class") class WordsToNumberRequest(BaseModel): text: str = Field(..., description="Kinyarwanda number words to parse", min_length=1) # --- Response schemas -------------------------------------------------------- class CardinalResponse(BaseModel): number: int words: str success: bool = True class OrdinalResponse(BaseModel): number: int words: str noun_class: Optional[str] = None success: bool = True class CurrencyResponse(BaseModel): amount: float currency_code: str words: str success: bool = True class DateResponse(BaseModel): day: int month: int year: int words: str success: bool = True class TimeResponse(BaseModel): hour: int minute: int words: str success: bool = True class DateTimeResponse(BaseModel): datetime: str style: str words: str success: bool = True class FractionResponse(BaseModel): numerator: int denominator: int words: str success: bool = True class WordsToNumberResponse(BaseModel): text: str number: Optional[int] success: bool class NumberInfoResponse(BaseModel): model_name: str version: str description: str supported_features: dict success: bool = True # --- Routes -------------------------------------------------------- @router.post( "/cardinal", response_model=CardinalResponse, status_code=status.HTTP_200_OK, summary="Convert number to cardinal words", description="Convert an integer to its Kinyarwanda cardinal representation (e.g., 201 -> 'maganabiri narimwe')", ) async def convert_cardinal(request: CardinalRequest, token: str = Depends(verify_platform_token)): try: words = number_service.get_cardinal(request.number) return CardinalResponse(number=request.number, words=words) except NumberFormattingError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @router.post( "/ordinal", response_model=OrdinalResponse, status_code=status.HTTP_200_OK, summary="Convert number to ordinal words", description="Convert an integer to its Kinyarwanda ordinal representation with optional noun class", ) async def convert_ordinal(request: OrdinalRequest, token: str = Depends(verify_platform_token)): try: words = number_service.get_ordinal(request.number, request.noun_class) return OrdinalResponse(number=request.number, words=words, noun_class=request.noun_class) except NumberFormattingError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @router.post( "/currency", response_model=CurrencyResponse, status_code=status.HTTP_200_OK, summary="Format currency amount", description="Format a currency amount in Kinyarwanda (e.g., 1250 RWF -> 'amafaranga igihumbi na maganabiri na mirongo itanu')", ) async def format_currency_endpoint(request: CurrencyRequest, token: str = Depends(verify_platform_token)): try: words = number_service.format_currency_amount(request.amount, request.currency_code) return CurrencyResponse(amount=request.amount, currency_code=request.currency_code, words=words) except NumberFormattingError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @router.post( "/date", response_model=DateResponse, status_code=status.HTTP_200_OK, summary="Format date", description="Format a date in Kinyarwanda with month names", ) async def format_date_endpoint(request: DateRequest, token: str = Depends(verify_platform_token)): try: words = number_service.format_date_string(request.day, request.month, request.year) return DateResponse(day=request.day, month=request.month, year=request.year, words=words) except NumberFormattingError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @router.post( "/time", response_model=TimeResponse, status_code=status.HTTP_200_OK, summary="Format time", description="Format a time in Kinyarwanda using the 12-hour clock system", ) async def format_time_endpoint(request: TimeRequest, token: str = Depends(verify_platform_token)): try: words = number_service.format_time_string(request.hour, request.minute) return TimeResponse(hour=request.hour, minute=request.minute, words=words) except NumberFormattingError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @router.post( "/datetime", response_model=DateTimeResponse, status_code=status.HTTP_200_OK, summary="Format datetime", description="Format a datetime in Kinyarwanda", ) async def format_datetime_endpoint(request: DateTimeRequest, token: str = Depends(verify_platform_token)): try: words = number_service.format_datetime_string(request.dt_value, request.style) return DateTimeResponse(datetime=request.dt_value.isoformat(), style=request.style, words=words) except NumberFormattingError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @router.post( "/fraction", response_model=FractionResponse, status_code=status.HTTP_200_OK, summary="Format fraction", description="Format a fraction in Kinyarwanda (e.g., 1/2 -> 'igice cya kabiri')", ) async def format_fraction_endpoint(request: FractionRequest, token: str = Depends(verify_platform_token)): try: words = number_service.format_fraction_string(request.numerator, request.denominator, request.noun_class) return FractionResponse(numerator=request.numerator, denominator=request.denominator, words=words) except NumberFormattingError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @router.post( "/words-to-number", response_model=WordsToNumberResponse, status_code=status.HTTP_200_OK, summary="Parse words to number", description="Parse Kinyarwanda number words back to integer (inverse conversion)", ) async def words_to_number_endpoint(request: WordsToNumberRequest, token: str = Depends(verify_platform_token)): try: number = number_service.parse_words_to_number(request.text) return WordsToNumberResponse(text=request.text, number=number, success=number is not None) except Exception as e: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error parsing words: {str(e)}") @router.get( "/info", response_model=NumberInfoResponse, status_code=status.HTTP_200_OK, summary="Get module information", description="Get metadata and information about the numbers module", ) async def get_module_info(token: str = Depends(verify_platform_token)): info = number_service.get_module_info() return NumberInfoResponse( model_name=info.get("model_name", "kinyarwanda_number2words"), version=info.get("version", "1.0.0"), description=info.get("description", ""), supported_features=info.get("supported_features", {}), )