Coverage for server / services / growthbook / dependencies.py: 76%
29 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
1"""
2FastAPI dependencies for feature gating and maintenance mode.
3"""
5from typing import Callable, Optional
7from fastapi import Depends, HTTPException, Request, status
9from server.services.growthbook.context import TargetingContext, resolve_targeting_context
10from server.services.growthbook.evaluation import feature_evaluation_service
13def require_feature(
14 feature_key: str,
15 *,
16 district_id_param: Optional[str] = None,
17 school_id_param: Optional[str] = None,
18) -> Callable:
19 """
20 Declarative route guard.
22 Example:
23 @router.get("/foo", dependencies=[Depends(require_feature("teacher.dashboard"))])
24 """
26 async def _dependency(request: Request) -> None:
27 district_id = (
28 request.path_params.get(district_id_param)
29 if district_id_param
30 else None
31 )
32 school_id = (
33 request.path_params.get(school_id_param) if school_id_param else None
34 )
35 ctx = await resolve_targeting_context(
36 request,
37 district_id=district_id,
38 school_id=school_id,
39 require_district=True,
40 )
41 await _enforce_maintenance(ctx)
42 enabled = await feature_evaluation_service.is_enabled(feature_key, ctx)
43 if not enabled:
44 raise HTTPException(
45 status_code=status.HTTP_403_FORBIDDEN,
46 detail=f"Feature '{feature_key}' is not enabled for this context.",
47 )
49 return _dependency
52def require_teacher_made_assignment_access() -> Callable:
53 """
54 Route guard for teacher-made assignment list/detail/review routes.
56 Requires teacher.teacher_made_tests.limit_10 or
57 teacher.teacher_made_tests.unlimited — not teacher.assignment_access.
58 """
60 async def _dependency(request: Request) -> None:
61 from server.services.growthbook.teacher_assignment_quota import (
62 enforce_teacher_made_assignment_access,
63 )
65 ctx = await resolve_targeting_context(request, require_district=True)
66 await _enforce_maintenance(ctx)
67 await enforce_teacher_made_assignment_access(request)
69 return _dependency
72async def get_targeting_context(
73 request: Request,
74 district_id: Optional[str] = None,
75 school_id: Optional[str] = None,
76) -> TargetingContext:
77 return await resolve_targeting_context(
78 request,
79 district_id=district_id,
80 school_id=school_id,
81 require_district=True,
82 )
85async def _enforce_maintenance(ctx: TargetingContext) -> None:
86 if await feature_evaluation_service.is_maintenance_mode(ctx):
87 raise HTTPException(
88 status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
89 detail="Platform is in maintenance mode. Please try again later.",
90 )
93async def maintenance_mode_guard(request: Request) -> None:
94 """Middleware-style dependency for routes that should respect maintenance mode."""
95 ctx = await resolve_targeting_context(request, use_cache=True, require_district=True)
96 await _enforce_maintenance(ctx)