vibeon-translator / api /routes /kinyarwanda_numbers.py
Vibeon Deploy Bot
deploy: vibeon-translator 2026-08-23T13:56:02Z
2909918
Raw
History Blame Contribute Delete
9.58 kB
"""
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", {}),
)