Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.

Commit 49b1f4a

Browse files
committed
chore: move version check into dashboard router, mock Github API call in tests
1 parent 3e55e18 commit 49b1f4a

File tree

3 files changed

+58
-53
lines changed

3 files changed

+58
-53
lines changed

src/codegate/dashboard/dashboard.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import asyncio
2+
from concurrent.futures import ThreadPoolExecutor
23
import json
34
from typing import AsyncGenerator, List, Optional
45

6+
from httpx import AsyncClient, HTTPStatusError
57
import structlog
6-
from fastapi import APIRouter, Depends, FastAPI
8+
from fastapi import APIRouter, Depends, FastAPI, HTTPException
79
from fastapi.responses import StreamingResponse
10+
from codegate import __version__
811

912
from codegate.dashboard.post_processing import (
1013
parse_get_alert_conversation,
@@ -18,13 +21,30 @@
1821
dashboard_router = APIRouter(tags=["Dashboard"])
1922
db_reader = None
2023

21-
2224
def get_db_reader():
2325
global db_reader
2426
if db_reader is None:
2527
db_reader = DbReader()
2628
return db_reader
2729

30+
def get_http_client() -> AsyncClient:
31+
return AsyncClient()
32+
33+
async def fetch_latest_version(client: AsyncClient) -> str:
34+
url = "https://api.github.com/repos/stacklok/codegate/releases/latest"
35+
headers = {
36+
"Accept": "application/vnd.github+json",
37+
"X-GitHub-Api-Version": "2022-11-28"
38+
}
39+
response = await client.get(url, headers=headers)
40+
response.raise_for_status()
41+
data = response.json()
42+
return data.get("tag_name", "unknown")
43+
44+
def fetch_latest_version_sync(client: AsyncClient) -> str:
45+
loop = asyncio.new_event_loop()
46+
asyncio.set_event_loop(loop)
47+
return loop.run_until_complete(fetch_latest_version(client))
2848

2949
@dashboard_router.get("/dashboard/messages")
3050
def get_messages(db_reader: DbReader = Depends(get_db_reader)) -> List[Conversation]:
@@ -61,6 +81,32 @@ async def stream_sse():
6181
"""
6282
return StreamingResponse(generate_sse_events(), media_type="text/event-stream")
6383

84+
@dashboard_router.get("/dashboard/version")
85+
def version_check(client: AsyncClient = Depends(get_http_client)):
86+
try:
87+
with ThreadPoolExecutor() as executor:
88+
latest_version = executor.submit(fetch_latest_version_sync, client).result()
89+
90+
# normalize the versions as github will return them with a 'v' prefix
91+
current_version = __version__.lstrip('v')
92+
latest_version_stripped = latest_version.lstrip('v')
93+
94+
is_latest: bool = latest_version_stripped == current_version
95+
96+
return {
97+
"current_version": current_version,
98+
"latest_version": latest_version_stripped,
99+
"is_latest": is_latest,
100+
"error": None,
101+
}
102+
except HTTPException as e:
103+
return {
104+
"current_version": __version__,
105+
"latest_version": "unknown",
106+
"is_latest": None,
107+
"error": e.detail
108+
}
109+
64110

65111
def generate_openapi():
66112
# Create a temporary FastAPI app instance

src/codegate/server.py

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
from typing import AsyncGenerator
33

44
import structlog
5-
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request
5+
from fastapi import APIRouter, Depends, FastAPI, Request
66
from fastapi.middleware.cors import CORSMiddleware
77
from fastapi.responses import JSONResponse
88
from starlette.middleware.errors import ServerErrorMiddleware
9-
from httpx import AsyncClient, HTTPStatusError
109

1110
from codegate import __description__, __version__
1211
from codegate.dashboard.dashboard import dashboard_router
@@ -20,7 +19,6 @@
2019

2120
logger = structlog.get_logger("codegate")
2221

23-
2422
async def custom_error_handler(request, exc: Exception):
2523
"""This is a Middleware to handle exceptions and log them."""
2624
# Capture the stack trace
@@ -29,24 +27,6 @@ async def custom_error_handler(request, exc: Exception):
2927
logger.error(traceback.print_list(extracted_traceback[-3:]))
3028
return JSONResponse({"error": str(exc)}, status_code=500)
3129

32-
async def get_http_client() -> AsyncGenerator[AsyncClient, None]:
33-
async with AsyncClient() as client:
34-
yield client
35-
36-
async def fetch_latest_version(client: AsyncClient) -> str:
37-
url = "https://api.github.com/repos/stacklok/codegate/releases/latest"
38-
headers = {
39-
"Accept": "application/vnd.github+json",
40-
"X-GitHub-Api-Version": "2022-11-28"
41-
}
42-
try:
43-
response = await client.get(url, headers=headers)
44-
response.raise_for_status()
45-
data = response.json()
46-
return data.get("tag_name", "unknown")
47-
except HTTPStatusError as e:
48-
raise HTTPException(status_code=e.response.status_code, detail=str(e))
49-
5030
def init_app(pipeline_factory: PipelineFactory) -> FastAPI:
5131
"""Create the FastAPI application."""
5232
app = FastAPI(
@@ -113,26 +93,6 @@ async def log_user_agent(request: Request, call_next):
11393
async def health_check():
11494
return {"status": "healthy"}
11595

116-
@system_router.get("/version")
117-
async def version_check(client: AsyncClient = Depends(get_http_client)):
118-
try:
119-
latest_version = await fetch_latest_version(client)
120-
121-
# normalize the versions as github will return them with a 'v' prefix
122-
current_version = __version__.lstrip('v')
123-
latest_version_stripped = latest_version.lstrip('v')
124-
125-
is_latest: bool = latest_version_stripped == current_version
126-
127-
return {
128-
"current_version": current_version,
129-
"latest_version": latest_version_stripped,
130-
"is_latest": is_latest,
131-
}
132-
except HTTPException as e:
133-
return {"current_version": __version__, "latest_version": "unknown", "error": e.detail}
134-
135-
13696
app.include_router(system_router)
13797
app.include_router(dashboard_router)
13898

tests/test_server.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -81,21 +81,20 @@ def test_health_check(test_client: TestClient) -> None:
8181
assert response.status_code == 200
8282
assert response.json() == {"status": "healthy"}
8383

84-
def test_version_endpoint(test_client: TestClient) -> None:
84+
@pytest.mark.usefixtures("test_client")
85+
@patch("codegate.dashboard.dashboard.fetch_latest_version", new_callable=AsyncMock)
86+
def test_version_endpoint(mock_fetch_latest_version, test_client) -> None:
8587
"""Test the version endpoint."""
86-
response = test_client.get("/version")
88+
mock_fetch_latest_version.return_value = "foo"
89+
90+
response = test_client.get("/dashboard/version")
8791
assert response.status_code == 200
8892

8993
response_data = response.json()
90-
assert "current_version" in response_data
91-
assert isinstance(response_data["current_version"], str)
92-
93-
assert "latest_version" in response_data
94-
assert isinstance(response_data["latest_version"], str)
95-
96-
assert "is_latest" in response_data
97-
assert isinstance(response_data["is_latest"], bool)
9894

95+
assert response_data["current_version"] == __version__.lstrip('v')
96+
assert response_data["latest_version"] == "foo"
97+
assert response_data["is_latest"] is False
9998

10099
@patch("codegate.pipeline.secrets.manager.SecretsManager")
101100
@patch("codegate.server.ProviderRegistry")

0 commit comments

Comments
 (0)