feat(schemas): T35 add Pydantic public API schemas

- PublicStatsResponse: summary + period info
- PublicUsageResponse: paginated usage items
- PublicKeyInfo: key metadata with stats (no values!)
- ApiToken schemas: create, response, create-response
- 25 unit tests, 100% coverage
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-07 16:15:22 +02:00
parent 16f740f023
commit a8095f4df7
3 changed files with 826 additions and 0 deletions

View File

@@ -20,6 +20,19 @@ from openrouter_monitor.schemas.stats import (
UsageStatsCreate,
UsageStatsResponse,
)
from openrouter_monitor.schemas.public_api import (
ApiTokenCreate,
ApiTokenCreateResponse,
ApiTokenResponse,
PaginationInfo,
PeriodInfo,
PublicKeyInfo,
PublicKeyListResponse,
PublicStatsResponse,
PublicUsageItem,
PublicUsageResponse,
SummaryInfo,
)
__all__ = [
"UserRegister",
@@ -37,4 +50,16 @@ __all__ = [
"StatsByModel",
"StatsByDate",
"DashboardResponse",
# Public API schemas
"ApiTokenCreate",
"ApiTokenCreateResponse",
"ApiTokenResponse",
"PublicStatsResponse",
"PublicUsageResponse",
"PublicKeyInfo",
"PublicKeyListResponse",
"SummaryInfo",
"PeriodInfo",
"PublicUsageItem",
"PaginationInfo",
]

View File

@@ -0,0 +1,347 @@
"""Public API Pydantic schemas for OpenRouter API Key Monitor.
T35: Pydantic schemas for public API endpoints.
These schemas define the data structures for the public API v1 endpoints.
"""
import datetime
from decimal import Decimal
from typing import Dict, List, Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator
class ApiTokenCreate(BaseModel):
"""Schema for creating a new API token.
Attributes:
name: Human-readable name for the token (1-100 characters)
"""
name: str = Field(
...,
min_length=1,
max_length=100,
description="Human-readable name for the token",
examples=["Production API Token", "Development Key"]
)
class ApiTokenResponse(BaseModel):
"""Schema for API token response (returned to client).
IMPORTANT: This schema does NOT include the token value for security.
The plaintext token is only shown once at creation time (ApiTokenCreateResponse).
Attributes:
id: Token ID
name: Token name
created_at: Creation timestamp
last_used_at: Last usage timestamp (None if never used)
is_active: Whether the token is active
"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(
...,
description="Token ID",
examples=[1]
)
name: str = Field(
...,
description="Token name",
examples=["Production Token"]
)
created_at: datetime.datetime = Field(
...,
description="Creation timestamp",
examples=["2024-01-15T12:00:00"]
)
last_used_at: Optional[datetime.datetime] = Field(
default=None,
description="Last usage timestamp (None if never used)",
examples=["2024-01-20T15:30:00"]
)
is_active: bool = Field(
...,
description="Whether the token is active",
examples=[True]
)
class ApiTokenCreateResponse(BaseModel):
"""Schema for API token creation response.
IMPORTANT: This is the ONLY time the plaintext token is shown.
After creation, the token cannot be retrieved again.
Attributes:
id: Token ID
name: Token name
token: Plaintext token (shown only once!)
created_at: Creation timestamp
"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(
...,
description="Token ID",
examples=[1]
)
name: str = Field(
...,
description="Token name",
examples=["Production Token"]
)
token: str = Field(
...,
description="Plaintext token (shown only once at creation!)",
examples=["or_api_abc123xyz789def456"]
)
created_at: datetime.datetime = Field(
...,
description="Creation timestamp",
examples=["2024-01-15T12:00:00"]
)
class SummaryInfo(BaseModel):
"""Schema for statistics summary.
Attributes:
total_requests: Total number of requests
total_cost: Total cost in USD
total_tokens: Total tokens (input + output)
"""
total_requests: int = Field(
...,
ge=0,
description="Total number of requests",
examples=[1000]
)
total_cost: Decimal = Field(
...,
ge=0,
description="Total cost in USD",
examples=["5.678901"]
)
total_tokens: int = Field(
default=0,
ge=0,
description="Total tokens (input + output)",
examples=[50000]
)
class PeriodInfo(BaseModel):
"""Schema for statistics period information.
Attributes:
start_date: Start date of the period
end_date: End date of the period
days: Number of days in the period
"""
start_date: datetime.date = Field(
...,
description="Start date of the period",
examples=["2024-01-01"]
)
end_date: datetime.date = Field(
...,
description="End date of the period",
examples=["2024-01-31"]
)
days: int = Field(
...,
ge=0,
description="Number of days in the period",
examples=[30]
)
class PublicStatsResponse(BaseModel):
"""Schema for public API stats response.
Attributes:
summary: Aggregated statistics summary
period: Period information (start_date, end_date, days)
"""
summary: SummaryInfo = Field(
...,
description="Aggregated statistics summary"
)
period: PeriodInfo = Field(
...,
description="Period information (start_date, end_date, days)"
)
class PublicUsageItem(BaseModel):
"""Schema for a single usage item in public API.
IMPORTANT: This only includes the API key NAME, not the actual key value.
Attributes:
date: Date of the statistics
api_key_name: Name of the API key (not the value!)
model: AI model name
requests_count: Number of requests
tokens_input: Number of input tokens
tokens_output: Number of output tokens
cost: Cost in USD
"""
date: datetime.date = Field(
...,
description="Date of the statistics",
examples=["2024-01-15"]
)
api_key_name: str = Field(
...,
description="Name of the API key (not the value!)",
examples=["Production Key"]
)
model: str = Field(
...,
description="AI model name",
examples=["gpt-4"]
)
requests_count: int = Field(
...,
ge=0,
description="Number of requests",
examples=[100]
)
tokens_input: int = Field(
default=0,
ge=0,
description="Number of input tokens",
examples=[5000]
)
tokens_output: int = Field(
default=0,
ge=0,
description="Number of output tokens",
examples=[3000]
)
cost: Decimal = Field(
...,
ge=0,
description="Cost in USD",
examples=["0.123456"]
)
class PaginationInfo(BaseModel):
"""Schema for pagination information.
Attributes:
page: Current page number (1-indexed)
limit: Items per page
total: Total number of items
pages: Total number of pages
"""
page: int = Field(
...,
ge=1,
description="Current page number (1-indexed)",
examples=[1]
)
limit: int = Field(
...,
ge=1,
description="Items per page",
examples=[100]
)
total: int = Field(
...,
ge=0,
description="Total number of items",
examples=[250]
)
pages: int = Field(
...,
ge=0,
description="Total number of pages",
examples=[3]
)
class PublicUsageResponse(BaseModel):
"""Schema for public API usage response with pagination.
Attributes:
items: List of usage items
pagination: Pagination information
"""
items: List[PublicUsageItem] = Field(
...,
description="List of usage items"
)
pagination: PaginationInfo = Field(
...,
description="Pagination information"
)
class PublicKeyInfo(BaseModel):
"""Schema for public API key information.
IMPORTANT: This schema does NOT include the actual API key value,
only metadata and aggregated statistics.
Attributes:
id: Key ID
name: Key name
is_active: Whether the key is active
stats: Aggregated statistics (total_requests, total_cost)
"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(
...,
description="Key ID",
examples=[1]
)
name: str = Field(
...,
description="Key name",
examples=["Production Key"]
)
is_active: bool = Field(
...,
description="Whether the key is active",
examples=[True]
)
stats: Dict = Field(
...,
description="Aggregated statistics (total_requests, total_cost)",
examples=[{"total_requests": 1000, "total_cost": "5.50"}]
)
class PublicKeyListResponse(BaseModel):
"""Schema for public API key list response.
Attributes:
items: List of API keys with statistics
total: Total number of keys
"""
items: List[PublicKeyInfo] = Field(
...,
description="List of API keys with statistics"
)
total: int = Field(
...,
ge=0,
description="Total number of keys",
examples=[5]
)