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

1""" 

2FastAPI dependencies for feature gating and maintenance mode. 

3""" 

4 

5from typing import Callable, Optional 

6 

7from fastapi import Depends, HTTPException, Request, status 

8 

9from server.services.growthbook.context import TargetingContext, resolve_targeting_context 

10from server.services.growthbook.evaluation import feature_evaluation_service 

11 

12 

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. 

21 

22 Example: 

23 @router.get("/foo", dependencies=[Depends(require_feature("teacher.dashboard"))]) 

24 """ 

25 

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 ) 

48 

49 return _dependency 

50 

51 

52def require_teacher_made_assignment_access() -> Callable: 

53 """ 

54 Route guard for teacher-made assignment list/detail/review routes. 

55 

56 Requires teacher.teacher_made_tests.limit_10 or 

57 teacher.teacher_made_tests.unlimited — not teacher.assignment_access. 

58 """ 

59 

60 async def _dependency(request: Request) -> None: 

61 from server.services.growthbook.teacher_assignment_quota import ( 

62 enforce_teacher_made_assignment_access, 

63 ) 

64 

65 ctx = await resolve_targeting_context(request, require_district=True) 

66 await _enforce_maintenance(ctx) 

67 await enforce_teacher_made_assignment_access(request) 

68 

69 return _dependency 

70 

71 

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 ) 

83 

84 

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 ) 

91 

92 

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)