diff --git a/python_template_server/models.py b/python_template_server/models.py index 65b9da5..1a21444 100644 --- a/python_template_server/models.py +++ b/python_template_server/models.py @@ -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.""" diff --git a/python_template_server/routers/base_router.py b/python_template_server/routers/base_router.py index 1e81f9b..1f0c416 100644 --- a/python_template_server/routers/base_router.py +++ b/python_template_server/routers/base_router.py @@ -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, diff --git a/python_template_server/routers/template_server_router.py b/python_template_server/routers/template_server_router.py index 74c54b3..7430094 100644 --- a/python_template_server/routers/template_server_router.py +++ b/python_template_server/routers/template_server_router.py @@ -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( @@ -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, @@ -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. diff --git a/python_template_server/template_server.py b/python_template_server/template_server.py index 013410d..6b25dd2 100644 --- a/python_template_server/template_server.py +++ b/python_template_server/template_server.py @@ -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 @@ -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)} diff --git a/tests/conftest.py b/tests/conftest.py index 564acf4..2cc0394 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -202,7 +202,9 @@ 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 @@ -210,4 +212,8 @@ def mock_template_server_router(mock_limiter: Limiter) -> TemplateServerRouter: 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 diff --git a/tests/routers/test_template_server_router.py b/tests/routers/test_template_server_router.py index b3de572..2282598 100644 --- a/tests/routers/test_template_server_router.py +++ b/tests/routers/test_template_server_router.py @@ -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 @@ -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: @@ -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.""" diff --git a/tests/test_models.py b/tests/test_models.py index 20aaf67..963d6eb 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -11,6 +11,7 @@ CORSConfigModel, CustomJSONResponse, DatabaseConfig, + GetConfigResponse, GetHealthResponse, GetLoginResponse, JSONResponseConfigModel, @@ -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.""" diff --git a/uv.lock b/uv.lock index 13d6093..6425f09 100644 --- a/uv.lock +++ b/uv.lock @@ -434,11 +434,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.29.4" +version = "3.29.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/ee/29c668c50888588c432a702f7c2e8ee8a0c9e5286028d91f170308d6b2e9/filelock-3.29.5.tar.gz", hash = "sha256:6e6034c57a00a020e767f2614a5539863f056de7e7991d6d1473aef7ff73f156", size = 68927, upload-time = "2026-07-03T03:50:31.818Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e3/f1fae3647d170919c2cf2a898e77e7d1a4e5c7cae0aed7bb4bd3f5ebff6f/filelock-3.29.5-py3-none-any.whl", hash = "sha256:8af830889ba3a0ffcefbd6c7d2af8a54012058103771f2e10848222f476a1693", size = 45073, upload-time = "2026-07-03T03:50:30.445Z" }, ] [[package]] @@ -1112,7 +1112,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "cryptography", specifier = ">=49.0.0" }, - { name = "fastapi", specifier = ">=0.138.1" }, + { name = "fastapi", specifier = ">=0.139.0" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "itsdangerous", marker = "extra == 'docs'", specifier = ">=2.2.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.4.0" }, @@ -1295,14 +1295,14 @@ wheels = [ [[package]] name = "sphinx-autodoc-typehints" -version = "3.12.0" +version = "3.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/f2/d99787d34d881b2352c04ce02bcdf0a61adf54b389c86d438b7a8ac3ae20/sphinx_autodoc_typehints-3.12.0.tar.gz", hash = "sha256:6571eb33c72cdc616a9945730fcb43c5bcf3685e79f1e8aea144735e43d5230d", size = 83874, upload-time = "2026-06-25T15:44:47.786Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/ae/8ad5e79a0f7beac5e5810b2a8d0a1c6c63c795ba265a81970070abf15f32/sphinx_autodoc_typehints-3.12.1.tar.gz", hash = "sha256:4bbf6f6b907586d3b2a3ae2b23e0f86be939f19a7449e917d304df57ff9674d1", size = 84421, upload-time = "2026-07-03T13:52:09.056Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/28/fa0f2ff73b8ca7987ebaa107d369c95459741ac73567e5079b4a881a981b/sphinx_autodoc_typehints-3.12.0-py3-none-any.whl", hash = "sha256:1a639b7cf71be13c4d8a4f0b552dcfcdf32ff39ac33ecb47398acb85863c2faf", size = 42548, upload-time = "2026-06-25T15:44:46.306Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a2/d5964523f0c98e39ef608a09f3de23032aa32fbe3e98a262e2112c39f985/sphinx_autodoc_typehints-3.12.1-py3-none-any.whl", hash = "sha256:932472c9cc160e6a0dc34eca13d3d6a9823ed6e6dc505d7c12f6963c983cf8f6", size = 42950, upload-time = "2026-07-03T13:52:07.696Z" }, ] [[package]] @@ -1458,7 +1458,7 @@ wheels = [ [[package]] name = "template-python" version = "0.1.0" -source = { git = "https://github.com/javidahmed64592/template-python.git#2b3317e71f3f36d3dab1a4a810487a87c7704cc4" } +source = { git = "https://github.com/javidahmed64592/template-python.git#dec52cdf789238dede8332a72a41462681763cbf" } dependencies = [ { name = "pydantic" }, { name = "pyhere" },