File size: 9,580 Bytes
2909918
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
"""
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", {}),
    )