Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions python_template_server/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,12 @@ class GetHealthResponse(BaseResponse):
"""Response model for the health endpoint."""


class GetConfigResponse(BaseResponse):
"""Response model for API config endpoint."""

config: TemplateServerConfig = Field(..., description="Current configuration of the template server")
version: str = Field(..., description="Version of the server")


class GetLoginResponse(BaseResponse):
"""Response model for login endpoint."""
5 changes: 2 additions & 3 deletions python_template_server/routers/base_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,12 @@ def add_route(
:param bool authentication_required: Whether authentication is required for this route
"""
try:
limited_method = None
if limited and self.limiter is not None:
limited_method = self.limiter.limit(self.rate_limit)(handler_function)
handler_function = self.limiter.limit(self.rate_limit)(handler_function)

self.router.add_api_route(
path=endpoint,
endpoint=limited_method or handler_function,
endpoint=handler_function,
methods=methods,
response_model=response_model,
dependencies=[Security(self._verify_api_key)] if authentication_required else None,
Expand Down
32 changes: 31 additions & 1 deletion python_template_server/routers/template_server_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@

from fastapi import Request

from python_template_server.models import GetHealthResponse, GetLoginResponse
from python_template_server.models import GetConfigResponse, GetHealthResponse, GetLoginResponse, TemplateServerConfig
from python_template_server.routers import BaseRouter


class TemplateServerRouter(BaseRouter):
"""Router for the template server with health and login endpoints."""

def configure_router(self, config: TemplateServerConfig, version: str) -> None:
"""Configure the router with server configuration and version.

:param TemplateServerConfig config: The server configuration
:param str version: The server version
"""
self.config = config
self.version = version

def setup_routes(self) -> None:
"""Set up the API routes for the template server."""
self.add_route(
Expand All @@ -19,6 +28,14 @@ def setup_routes(self) -> None:
limited=False,
authentication_required=False,
)
self.add_route(
endpoint="/config",
handler_function=self.get_config,
response_model=GetConfigResponse,
methods=["GET"],
limited=False,
authentication_required=False,
)
self.add_route(
endpoint="/login",
handler_function=self.get_login,
Expand All @@ -37,6 +54,19 @@ async def get_health(self, request: Request) -> GetHealthResponse:
"""
return GetHealthResponse(message="Server is healthy")

async def get_config(self, request: Request) -> GetConfigResponse:
"""Get server configuration.

:param Request request: The incoming HTTP request
:return GetConfigResponse: Configuration response
:raise HTTPException: If the server token is not configured
"""
return GetConfigResponse(
message="Configuration retrieved successfully.",
config=self.config,
version=self.version,
)

async def get_login(self, request: Request) -> GetLoginResponse:
"""Handle user login and return a success response.

Expand Down
5 changes: 4 additions & 1 deletion python_template_server/template_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ def _routers(self) -> list[BaseRouter]:

:return list[BaseRouter]: List of BaseRouter instances
"""
TEMPLATE_SERVER_ROUTER.configure_router(config=self.config, version=self.package_metadata["Version"])
return [TEMPLATE_SERVER_ROUTER, *self.routers]

@property
Expand Down Expand Up @@ -287,7 +288,9 @@ async def _custom_404_handler(self, request: Request, exc: Exception) -> Respons
def _setup_routes(self) -> None:
"""Set up API routes."""
for router in self._routers:
router.configure(self.hashed_token, self.limiter, self.config.rate_limit.rate_limit)
router.configure(
hashed_token=self.hashed_token, limiter=self.limiter, rate_limit=self.config.rate_limit.rate_limit
)
router.setup_routes()
self.app.include_router(router.router)
routes = {route.path for route in router.router.routes if isinstance(route, APIRoute)}
Expand Down
8 changes: 7 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,18 @@ def mock_limiter() -> Limiter:


@pytest.fixture
def mock_template_server_router(mock_limiter: Limiter) -> TemplateServerRouter:
def mock_template_server_router(
mock_limiter: Limiter, mock_template_server_config: TemplateServerConfig
) -> TemplateServerRouter:
"""Provide a TemplateServerRouter instance for testing."""
TEMPLATE_SERVER_ROUTER.configure(
hashed_token="hashed_value", # noqa: S106
limiter=mock_limiter,
rate_limit="10/minute",
)
TEMPLATE_SERVER_ROUTER.setup_routes()
TEMPLATE_SERVER_ROUTER.configure_router(
config=mock_template_server_config,
version="1.0.0",
)
return TEMPLATE_SERVER_ROUTER
18 changes: 18 additions & 0 deletions tests/routers/test_template_server_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from fastapi import Request
from fastapi.routing import APIRoute

from python_template_server.models import TemplateServerConfig
from python_template_server.routers import TemplateServerRouter


Expand All @@ -19,6 +20,7 @@ def test_setup_routes(self, mock_template_server_router: TemplateServerRouter) -
routes = [route.path for route in api_routes]
expected_endpoints = [
"/health",
"/config",
"/login",
]
for endpoint in expected_endpoints:
Expand All @@ -40,6 +42,22 @@ def test_get_health(self, mock_template_server_router: TemplateServerRouter, moc
assert isinstance(response.timestamp, str)


class TestGetConfigEndpoint:
"""Integration tests for the /config endpoint."""

@pytest.fixture
def mock_request_object(self) -> Request:
"""Provide a mock Request object."""
return MagicMock(spec=Request)

def test_get_config(self, mock_template_server_router: TemplateServerRouter, mock_request_object: Request) -> None:
"""Test the /config endpoint method."""
response = asyncio.run(mock_template_server_router.get_config(mock_request_object))
assert isinstance(response.config, TemplateServerConfig)
assert response.version == mock_template_server_router.version
assert isinstance(response.timestamp, str)


class TestGetLoginEndpoint:
"""Integration tests for the /login endpoint."""

Expand Down
17 changes: 17 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
CORSConfigModel,
CustomJSONResponse,
DatabaseConfig,
GetConfigResponse,
GetHealthResponse,
GetLoginResponse,
JSONResponseConfigModel,
Expand Down Expand Up @@ -240,6 +241,22 @@ def test_model_dump(self) -> None:
assert response.model_dump() == config_dict


class TestGetConfigResponse:
"""Unit tests for the GetConfigResponse class."""

def test_model_dump(self, mock_template_server_config: TemplateServerConfig) -> None:
"""Test the model_dump method."""
timestamp = GetConfigResponse.current_timestamp()
config_dict: dict = {
"message": "Configuration retrieved successfully.",
"timestamp": timestamp,
"config": mock_template_server_config.model_dump(),
"version": "1.0.0",
}
response = GetConfigResponse(**config_dict)
assert response.model_dump() == config_dict


class TestGetLoginResponse:
"""Unit tests for the GetLoginResponse class."""

Expand Down
16 changes: 8 additions & 8 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading