Files
TimeTracker/app/utils/api_deprecation.py
T
Dries Peeters 999f5c6319 feat(api): clarify /api vs /api/v1 and reduce duplication
Document the dual HTTP surface everywhere integrators look: OpenAPI intro and servers, ARCHITECTURE, REST_API, API_VERSIONING (deprecated vs internal routes, shared modules), and CONTRIBUTING (v1-first rule).

Session JSON routes in app/routes/api.py that overlap REST v1 now return X-API-Deprecated and a Link header with rel successor-version, implemented via app/utils/api_deprecation.py.

Extract shared global search into app/services/global_search_service.py for both GET /api/search and GET /api/v1/search while preserving legacy short-query 200 empty responses and v1 400 validation.

Refactor legacy POST /api/timer/start, /api/timer/stop, and the start path of /api/timer/resume to use TimeTrackingService; keep existing socketio emits for the web UI.

Add tests/test_api_deprecation_headers.py and adjust search partial-failure tests to patch Project.query on the service module.
2026-04-16 15:36:01 +02:00

58 lines
1.8 KiB
Python

"""Helpers for marking session-based /api routes that overlap with /api/v1."""
from __future__ import annotations
from functools import wraps
from typing import Any, Callable, Optional, Tuple, Union
from flask import make_response
RouteReturn = Union[Any, Tuple[Any, int], Tuple[Any, int, dict]]
def apply_deprecated_headers_to_result(
result: RouteReturn,
successor_path: Optional[str] = None,
) -> Any:
"""
Add deprecation headers to a Flask view return value (Response, or (rv, status), or triple).
successor_path: path only (e.g. '/api/v1/search'); emitted as Link rel=successor-version.
"""
if isinstance(result, tuple):
if len(result) == 3:
resp = make_response(result[0], result[1], result[2])
elif len(result) == 2:
resp = make_response(result[0], result[1])
else:
resp = make_response(result[0])
else:
resp = make_response(result)
resp.headers["X-API-Deprecated"] = "true"
if successor_path:
resp.headers["Link"] = f'<{successor_path}>; rel="successor-version"'
return resp
def deprecated_session_api(successor_path: Optional[str]) -> Callable[[Callable[..., RouteReturn]], Callable[..., Any]]:
"""
Decorate a view: after it runs, stamp X-API-Deprecated (and optional Link) on the response.
Use *inside* @login_required so unauthenticated responses are unchanged:
@login_required
@deprecated_session_api("/api/v1/search")
def search(): ...
"""
def decorator(view_func: Callable[..., RouteReturn]) -> Callable[..., Any]:
@wraps(view_func)
def wrapped(*args: Any, **kwargs: Any) -> Any:
out = view_func(*args, **kwargs)
return apply_deprecated_headers_to_result(out, successor_path)
return wrapped
return decorator