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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,11 @@ uv run generate-new-token
uv run python-template-server

# Server runs at https://localhost:443/api
# Health check: curl -k https://localhost:443/api/health
# Swagger UI: https://localhost:443/api/docs
# Redoc: https://localhost:443/api/redoc
# Metrics: curl -k https://localhost:443/api/metrics
# Health check: curl -k https://localhost:443/api/health
# Login (requires authentication): curl -k -H "X-API-Key: your-token-here" https://localhost:443/api/login
```

## Using as a Template
Expand Down
2 changes: 1 addition & 1 deletion configuration/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
},
"security": {
"hsts_max_age": 31536000,
"content_security_policy": "default-src 'self'"
"content_security_policy": "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; img-src 'self' data: https://cdn.jsdelivr.net https://fastapi.tiangolo.com"
},
"rate_limit": {
"enabled": true,
Expand Down
69 changes: 69 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ All endpoints are mounted under the `/api` prefix.
- [Grafana Dashboards](#grafana-dashboards)
- [Endpoints](#endpoints)
- [GET /api/health](#get-apihealth)
- [GET /api/login](#get-apilogin)
- [Request and Response Models (Pydantic)](#request-and-response-models-pydantic)
- [API Documentation](#api-documentation)
- [Swagger UI (/api/docs)](#swagger-ui-apidocs)
- [ReDoc (/api/redoc)](#redoc-apiredoc)

## Authentication

Expand Down Expand Up @@ -211,13 +215,78 @@ curl -k https://localhost:443/api/health
}
```

### GET /api/login

**Purpose**: Verify API token and return successful login message.

**Authentication**: Required (API key must be provided)

**Rate Limiting**: Subject to rate limits (default: 100/minute)

**Request**: None

**Response Model**: `GetLoginResponse`
- `code` (int): HTTP status code
- `message` (string): Login status message
- `timestamp` (string): ISO 8601 timestamp

**Example Request**:
```bash
curl -k https://localhost:443/api/login \
-H "X-API-Key: your-api-token-here"
```

**Example Response** (200 OK):
```json
{
"code": 200,
"message": "Login successful.",
"timestamp": "2025-11-22T12:00:00.000000Z"
}
```

**Error Responses**:
- `401 Unauthorized`: Missing or invalid API key
- `429 Too Many Requests`: Rate limit exceeded

## Request and Response Models (Pydantic)

The primary Pydantic models are defined in `python_template_server/models.py`:
- `BaseResponse`: Base model with code, message, and timestamp fields
- `GetHealthResponse`: Extends BaseResponse with status field (HEALTHY/UNHEALTHY)
- `GetLoginResponse`: Extends BaseResponse for login endpoint responses
- `TemplateServerConfig`: Configuration model for server settings (security, rate limiting, JSON response)

**Extending Configurations**: Extend the `TemplateServerConfig` class to get the necessary server setup configuration.

**Extending Models**: When building your own server, create custom response models by extending `BaseResponse` for consistent API responses.

## API Documentation

FastAPI automatically generates interactive API documentation, providing two different interfaces for exploring and testing the API.

### Swagger UI (/api/docs)

**URL**: `https://localhost:443/api/docs`

**Purpose**: Interactive API documentation with "Try it out" functionality

**Features**:
- Execute API calls directly from the browser
- View request/response schemas
- Test authentication with API keys
- Explore all available endpoints
- View models and their properties

### ReDoc (/api/redoc)

**URL**: `https://localhost:443/api/redoc`

**Purpose**: Alternative API documentation with a clean, three-panel layout

**Features**:
- Read-only documentation interface
- Clean, responsive design
- Search functionality
- Detailed schema information
- Markdown support in descriptions
7 changes: 4 additions & 3 deletions docs/DOCKER_DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,17 +174,18 @@ docker compose down -v
**Base URL**: `https://localhost:443`

**API Endpoints**:
- Health Check: `GET /api/health` (publicly accessible, no authentication required)
- Metrics: `GET /api/metrics` (publicly accessible, no authentication required)
- Health Check: `GET /api/health` (publicly accessible, no authentication required)
- Login: `GET /api/login` (requires authentication with X-API-Key header)
- Custom Endpoints: Defined in your server subclass (authentication may be required)

**Example Request**:
```bash
# Using curl (with self-signed cert)
curl -k https://localhost:443/api/health

# Authenticated request to custom endpoint
curl -k -H "X-API-Key: your-token-here" https://localhost:443/api/your-endpoint
# Login endpoint (authenticated)
curl -k -H "X-API-Key: your-token-here" https://localhost:443/api/login
```

### Prometheus
Expand Down
13 changes: 7 additions & 6 deletions docs/SMG.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,20 @@ uv run python-template-server
The backend will be available at `https://localhost:443/api` by default.

**Available Endpoints:**
- Health Check: `https://localhost:443/api/health`
- Prometheus Metrics: `https://localhost:443/api/metrics`
- Health Check: `https://localhost:443/api/health`
- Login: `https://localhost:443/api/login` (requires authentication)

**Testing the API:**
```sh
# Health check (no auth required)
curl -k https://localhost:443/api/health

# Metrics endpoint (no auth required)
curl -k https://localhost:443/api/metrics

# Add custom authenticated endpoints in your server subclass
curl -k -H "X-API-Key: your-token-here" https://localhost:443/api/your-endpoint
# Health check (no auth required)
curl -k https://localhost:443/api/health

# Login endpoint (requires authentication)
curl -k -H "X-API-Key: your-token-here" https://localhost:443/api/login
```

### Testing, Linting, and Type Checking
Expand Down
4 changes: 4 additions & 0 deletions python_template_server/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,7 @@ class GetHealthResponse(BaseResponse):
"""Response model for the health endpoint."""

status: ServerHealthStatus = Field(..., description="Health status of the server")


class GetLoginResponse(BaseResponse):
"""Response model for login endpoint."""
11 changes: 11 additions & 0 deletions python_template_server/template_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from python_template_server.models import (
CustomJSONResponse,
GetHealthResponse,
GetLoginResponse,
ResponseCode,
ServerHealthStatus,
TemplateServerConfig,
Expand Down Expand Up @@ -328,6 +329,7 @@ def setup_routes(self) -> None:

"""
self.add_unauthenticated_route("/health", self.get_health, GetHealthResponse, ["GET"], limited=False)
self.add_authenticated_route("/login", self.get_login, GetLoginResponse, methods=["GET"])

async def get_health(self, request: Request) -> GetHealthResponse:
"""Get server health.
Expand All @@ -351,3 +353,12 @@ async def get_health(self, request: Request) -> GetHealthResponse:
timestamp=GetHealthResponse.current_timestamp(),
status=ServerHealthStatus.HEALTHY,
)

async def get_login(self, request: Request) -> GetLoginResponse:
"""Handle user login and return a success response."""
logger.info("User login successful.")
return GetLoginResponse(
code=ResponseCode.OK,
message="Login successful.",
timestamp=GetLoginResponse.current_timestamp(),
)
22 changes: 20 additions & 2 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
CertificateConfigModel,
CustomJSONResponse,
GetHealthResponse,
GetLoginResponse,
JSONResponseConfigModel,
MetricConfig,
MetricTypes,
Expand Down Expand Up @@ -291,7 +292,8 @@ class TestBaseResponse:

def test_model_dump(self) -> None:
"""Test the model_dump method."""
config_dict: dict = {"code": ResponseCode.OK, "message": "Success", "timestamp": "2025-11-22T12:00:00Z"}
timestamp = BaseResponse.current_timestamp()
config_dict: dict = {"code": ResponseCode.OK, "message": "Success", "timestamp": timestamp}
response = BaseResponse(**config_dict)
assert response.model_dump() == config_dict

Expand All @@ -301,11 +303,27 @@ class TestGetHealthResponse:

def test_model_dump(self) -> None:
"""Test the model_dump method."""
timestamp = GetHealthResponse.current_timestamp()
config_dict: dict = {
"code": ResponseCode.OK,
"message": "Server is healthy",
"timestamp": "2025-11-22T12:00:00Z",
"timestamp": timestamp,
"status": ServerHealthStatus.HEALTHY,
}
response = GetHealthResponse(**config_dict)
assert response.model_dump() == config_dict


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

def test_model_dump(self) -> None:
"""Test the model_dump method."""
timestamp = GetLoginResponse.current_timestamp()
config_dict: dict = {
"code": ResponseCode.OK,
"message": "Login successful",
"timestamp": timestamp,
}
response = GetLoginResponse(**config_dict)
assert response.model_dump() == config_dict
33 changes: 31 additions & 2 deletions tests/test_template_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@ def test_setup_routes(self, mock_template_server: MockTemplateServer) -> None:
routes = [route.path for route in api_routes]
expected_endpoints = [
"/health",
"/login",
"/metrics",
"/unauthenticated-endpoint",
"/authenticated-endpoint",
Expand All @@ -573,7 +574,7 @@ def test_setup_routes(self, mock_template_server: MockTemplateServer) -> None:
assert endpoint in routes


class TestHealthEndpoint:
class TestGetHealthEndpoint:
"""Integration tests for the /health endpoint."""

def test_get_health(self, mock_template_server: TemplateServer) -> None:
Expand Down Expand Up @@ -602,7 +603,7 @@ def test_get_health_token_not_configured(self, mock_template_server: TemplateSer
assert token_gauge is not None
assert token_gauge._value.get() == 0

def test_health_endpoint(
def test_get_health_endpoint(
self, mock_template_server: TemplateServer, mock_verify_token: MagicMock, mock_timestamp: str
) -> None:
"""Test /health endpoint returns 200."""
Expand All @@ -618,3 +619,31 @@ def test_health_endpoint(
"timestamp": mock_timestamp,
"status": ServerHealthStatus.HEALTHY,
}


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

def test_get_login(self, mock_template_server: TemplateServer) -> None:
"""Test the /login endpoint method."""
request = MagicMock()
response = asyncio.run(mock_template_server.get_login(request))

assert response.code == ResponseCode.OK
assert response.message == "Login successful."

def test_get_login_endpoint(
self, mock_template_server: TemplateServer, mock_verify_token: MagicMock, mock_timestamp: str
) -> None:
"""Test /login endpoint returns 200."""
mock_verify_token.return_value = True
app = mock_template_server.app
client = TestClient(app)

response = client.get("/login", headers={"X-API-Key": "test-token"})
assert response.status_code == ResponseCode.OK
assert response.json() == {
"code": ResponseCode.OK,
"message": "Login successful.",
"timestamp": mock_timestamp,
}