Refactor data fetching hooks, add page size lint test

- Simplify useFetchData: remove unused URL building logic
- Add usePolledData initial implementation
- Add router page_size param validation test
- Update API reference docs
- Clean up tasks doc
This commit is contained in:
2026-05-04 06:48:24 +02:00
parent 0a3f9c6c16
commit 69e1726045
7 changed files with 134 additions and 93 deletions

View File

@@ -22,7 +22,7 @@ from __future__ import annotations
import asyncio
from typing import Annotated
from fastapi import APIRouter, Body, Path, status
from fastapi import APIRouter, Body, Path, Query, status
from app.dependencies import (
AuthDep,
@@ -43,6 +43,7 @@ from app.models.jail import (
JailListResponse,
)
from app.services import jail_service
from app.utils.constants import DEFAULT_PAGE_SIZE
router: APIRouter = APIRouter(prefix="/api/v1/jails", tags=["Jails"])
@@ -521,8 +522,8 @@ async def get_jail_banned_ips(
socket_path: Fail2BanSocketDep,
http_session: HttpSessionDep,
geo_cache: GeoCacheDep,
page: int = 1,
page_size: int = 25,
page: int = Query(default=1, ge=1, description="1-based page number."),
page_size: int = Query(default=DEFAULT_PAGE_SIZE, ge=1, le=100, description="Items per page (max 100)."),
search: str | None = None,
) -> JailBannedIpsResponse:
"""Return a paginated list of IPs currently banned by a specific jail.
@@ -539,7 +540,7 @@ async def get_jail_banned_ips(
http_session: Shared HTTP session for geolocation.
geo_cache: Geolocation cache instance.
page: 1-based page number (default 1, min 1).
page_size: Items per page (default 25, max 100).
page_size: Items per page (default 100, max 100).
search: Optional case-insensitive substring filter on the IP address.
Returns:

View File

@@ -0,0 +1,90 @@
"""Lint test: no hardcoded page_size defaults exist in routers.
Ensures all paginated endpoints derive their default from
:data:`~app.utils.constants.DEFAULT_PAGE_SIZE` rather than
hardcoding integer literals.
"""
from __future__ import annotations
import ast
from pathlib import Path
import pytest
ROUTERS_DIR = Path(__file__).parent.parent.parent / "app" / "routers"
class _PageSizeDefaultChecker(ast.NodeVisitor):
"""Visit Python source files and report hardcoded page_size defaults."""
def __init__(self, source: str) -> None:
self.source = source
self.lines = source.splitlines()
self.violations: list[str] = []
def _line_for(self, node: ast.AST) -> int:
return node.lineno # type: ignore[attr-defined]
def _extract_assign_value(self, target: ast.expr, value: ast.expr) -> int | None:
"""Return int value if *value* is an ast.Constant/ast.Num with an int."""
if isinstance(value, ast.Constant) and isinstance(value.value, int):
return value.value
if isinstance(value, ast.Num) and isinstance(value.n, int):
return value.n
return None
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
for default in node.args.defaults:
self._check_default(default, node.name)
for default in node.args.kw_defaults:
if default is not None:
self._check_default(default, node.name)
self.generic_visit(node)
def _check_default(self, node: ast.expr, fn_name: str) -> None:
# Look for patterns:
# page_size: SomeType = 25
# page_size: int = 25
if isinstance(node, ast.Constant) and isinstance(node.value, int):
lineno = node.lineno # type: ignore[attr-defined]
col = node.col_offset # type: ignore[attr-defined]
self.violations.append(
f"line {lineno}:{col} {fn_name} page_size default is hardcoded int ({node.value}); "
"use DEFAULT_PAGE_SIZE from app.utils.constants"
)
def _check_file(path: Path) -> list[str]:
"""Return list of violation messages for *path* (empty if clean)."""
try:
source = path.read_text()
except Exception as exc:
return [f"could not read {path}: {exc}"]
tree = ast.parse(source, filename=str(path))
checker = _PageSizeDefaultChecker(source)
checker.visit(tree)
# Filter to only violations that involve page_size.
# The checker already visits FunctionDef defaults broadly; we narrow to
# those that look like "page_size = <int>" by inspecting the line content.
filtered: list[str] = []
for msg in checker.violations:
# Extract line number from "line N:"
lineno = int(msg.split(":")[1])
line_text = source.splitlines()[lineno - 1]
if "page_size" in line_text:
filtered.append(msg)
return filtered
@pytest.mark.parametrize("router", sorted(ROUTERS_DIR.glob("*.py")), ids=lambda p: p.name)
def test_no_hardcoded_page_size_defaults(router: Path) -> None:
"""Router files must not contain hardcoded integer defaults for page_size.
All paginated endpoints should use ``default=DEFAULT_PAGE_SIZE`` from
:data:`~app.utils.constants.DEFAULT_PAGE_SIZE`.
"""
violations = _check_file(router)
assert violations == [], f"Hardcoded page_size defaults found in {router.name}:\n" + "\n".join(violations)