-
Notifications
You must be signed in to change notification settings - Fork 0
[US-2.2 / OBT-201] Language usage stats endpoint #98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
58d0d63
747a7e3
bea98e5
a4c5f5f
13245eb
14fc189
6df3f09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,16 @@ | ||
| from fastapi import APIRouter, Depends, status | ||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
|
|
||
| from app.core.auth_middleware import get_current_user | ||
| from app.core.auth_middleware import get_current_user, require_platform_admin | ||
| from app.core.database import get_db | ||
| from app.core.exceptions import NotFoundError | ||
| from app.db.models.auth import User | ||
| from app.models.language import LanguageCreate, LanguageResponse | ||
| from app.models.language import ( | ||
| LanguageCreate, | ||
| LanguageProjectRef, | ||
| LanguageResponse, | ||
| LanguageStatsResponse, | ||
| ) | ||
| from app.services import language_service | ||
|
|
||
| router = APIRouter() | ||
|
|
@@ -50,3 +55,17 @@ async def get_language_by_id( | |
| ) -> LanguageResponse: | ||
| language = await language_service.get_language_or_404(db, language_id) | ||
| return LanguageResponse.model_validate(language) | ||
|
|
||
|
|
||
| @router.get("/{language_id}/stats", response_model=LanguageStatsResponse) | ||
| async def get_language_stats( | ||
| language_id: str, | ||
| db: AsyncSession = Depends(get_db), | ||
| _: User = Depends(require_platform_admin), | ||
| ) -> LanguageStatsResponse: | ||
| projects = await language_service.get_language_stats(db, language_id) | ||
| return LanguageStatsResponse( | ||
| language_id=language_id, | ||
| project_count=len(projects), | ||
| projects=[LanguageProjectRef(id=project_id, name=name) for project_id, name in projects], | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ps: typed outputs issue again — building the |
||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| from sqlalchemy import select | ||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
|
|
||
| from app.db.models.project import Project | ||
| from app.services.language.get_language_or_404 import get_language_or_404 | ||
|
|
||
|
|
||
| async def get_language_stats(db: AsyncSession, language_id: str) -> list[tuple[str, str]]: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For me it is relevant type the outputs whenever possible. |
||
| await get_language_or_404(db, language_id) | ||
| stmt = ( | ||
| select(Project.id, Project.name) | ||
| .where(Project.language_id == language_id) | ||
| .order_by(Project.name) | ||
| ) | ||
| result = await db.execute(stmt) | ||
| return [(row.id, row.name) for row in result.all()] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import pytest | ||
|
|
||
| from app.core.exceptions import NotFoundError | ||
| from app.services import language_service | ||
| from tests.baker import make_language, make_project | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_language_stats_lists_projects(db_session) -> None: | ||
| lang = await make_language(db_session, code="ksa") | ||
| other = await make_language(db_session, code="oth") | ||
| p1 = await make_project(db_session, language_id=lang.id, name="P1") | ||
| p2 = await make_project(db_session, language_id=lang.id, name="P2") | ||
| await make_project(db_session, language_id=other.id, name="P3") | ||
|
|
||
| projects = await language_service.get_language_stats(db_session, lang.id) | ||
| assert projects == [(p1.id, p1.name), (p2.id, p2.name)] | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_language_stats_empty_when_unused(db_session) -> None: | ||
| lang = await make_language(db_session, code="ksb") | ||
| projects = await language_service.get_language_stats(db_session, lang.id) | ||
| assert projects == [] | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_language_stats_missing_raises_not_found(db_session) -> None: | ||
| with pytest.raises(NotFoundError, match=r"Language .* not found"): | ||
| await language_service.get_language_stats( | ||
| db_session, "00000000-0000-0000-0000-000000000000" | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that correct? Relative to main this
/statsroute is new — it was added on the first commit of this same PR (58d0d63) — so I think nothing was ever exposed, but the body reads like a leak that shipped. And therequire_admin_or_manageryou mention is not on main, it comes on #93. The admin guard itself is a good call, just please fix the body and the "403 instead of 200" breaking change checkbox — they do not apply for an endpoint that never existed outside this PR.