Coverage for server / utilities / exam_catalog.py: 98%

59 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-10-04 09:33 +0000

1""" 

2Strict global-assignment catalog: exactly one global assignment per tiered exam feature key. 

3 

4Maps GrowthBook keys `teacher.exam.{staar|tsi|sat|act}.{1-6}` to global_assignments documents. 

5""" 

6 

7from typing import Any, Optional 

8 

9EXAM_SEGMENT_TO_ASSIGNMENT_TYPE: dict[str, str] = { 

10 "staar": "STAAR", 

11 "tsi": "TSI", 

12 "sat": "SAT", 

13 "act": "ACT", 

14} 

15 

16ASSIGNMENT_TYPE_TO_EXAM_SEGMENT: dict[str, str] = { 

17 v: k for k, v in EXAM_SEGMENT_TO_ASSIGNMENT_TYPE.items() 

18} 

19 

20TIERED_EXAM_TYPES = frozenset(ASSIGNMENT_TYPE_TO_EXAM_SEGMENT.keys()) 

21 

22TIERED_EXAM_FEATURE_KEYS = frozenset( 

23 f"teacher.exam.{exam}.{slot}" 

24 for exam in ("staar", "tsi", "sat", "act") 

25 for slot in range(1, 7) 

26) 

27 

28 

29def tiered_exam_feature_keys() -> frozenset[str]: 

30 return TIERED_EXAM_FEATURE_KEYS 

31 

32 

33def _assignment_type_value(assignment_type: Any) -> str: 

34 if hasattr(assignment_type, "value"): 

35 return str(assignment_type.value).strip().upper() 

36 return str(assignment_type or "").strip().upper() 

37 

38 

39def _format_value(format_value: Any) -> str: 

40 return str(format_value or "").strip().lower() 

41 

42 

43def is_tiered_practice_global(assignment_type: Any, format_value: Any) -> bool: 

44 return ( 

45 _assignment_type_value(assignment_type) in TIERED_EXAM_TYPES 

46 and _format_value(format_value) == "practice" 

47 ) 

48 

49 

50def parse_exam_feature_key(feature_key: str) -> tuple[str, int]: 

51 normalized = (feature_key or "").strip().lower() 

52 if normalized not in TIERED_EXAM_FEATURE_KEYS: 

53 raise ValueError( 

54 f"feature_key must be one of the 24 tiered exam keys " 

55 f"(teacher.exam.{{staar|tsi|sat|act}}.{{1-6}}). Got: {feature_key!r}" 

56 ) 

57 _prefix, _exam, exam_segment, slot_str = normalized.split(".") 

58 return exam_segment, int(slot_str) 

59 

60 

61def assignment_type_for_feature_key(feature_key: str) -> str: 

62 exam_segment, _slot = parse_exam_feature_key(feature_key) 

63 return EXAM_SEGMENT_TO_ASSIGNMENT_TYPE[exam_segment] 

64 

65 

66def feature_key_for_assignment(assignment_type: Any, exam_slot: int) -> str: 

67 exam_segment = ASSIGNMENT_TYPE_TO_EXAM_SEGMENT.get(_assignment_type_value(assignment_type)) 

68 if not exam_segment: 

69 raise ValueError("assignment type must be STAAR, TSI, SAT, or ACT") 

70 if exam_slot not in range(1, 7): 

71 raise ValueError("exam_slot must be between 1 and 6") 

72 key = f"teacher.exam.{exam_segment}.{exam_slot}" 

73 if key not in TIERED_EXAM_FEATURE_KEYS: 

74 raise ValueError(f"Invalid exam catalog key: {key}") 

75 return key 

76 

77 

78def validate_assignment_feature_key_rules( 

79 *, 

80 feature_key: Optional[str], 

81 assignment_type: Any, 

82 format_value: Any, 

83) -> None: 

84 type_value = _assignment_type_value(assignment_type) 

85 format_norm = _format_value(format_value) 

86 key_norm = (feature_key or "").strip().lower() or None 

87 

88 if is_tiered_practice_global(type_value, format_norm): 

89 if not key_norm: 

90 raise ValueError( 

91 "feature_key is required for tiered practice globals " 

92 "(STAAR/TSI/SAT/ACT with format=practice). " 

93 "Use one of the 24 keys: teacher.exam.{staar|tsi|sat|act}.{1-6}." 

94 ) 

95 elif key_norm: 

96 raise ValueError( 

97 "feature_key is only allowed on tiered practice globals " 

98 "(type STAAR/TSI/SAT/ACT and format=practice)." 

99 ) 

100 

101 if not key_norm: 

102 return 

103 

104 expected_type = assignment_type_for_feature_key(key_norm) 

105 if type_value != expected_type: 

106 raise ValueError( 

107 f"feature_key {key_norm!r} requires type {expected_type}, got {type_value}." 

108 ) 

109 

110 

111def catalog_entry(feature_key: str) -> dict[str, Any]: 

112 from server.services.growthbook.registry import get_feature_definition, min_plan_for_feature 

113 

114 definition = get_feature_definition(feature_key) 

115 exam_segment, slot = parse_exam_feature_key(feature_key) 

116 return { 

117 "feature_key": feature_key, 

118 "assignment_type": EXAM_SEGMENT_TO_ASSIGNMENT_TYPE[exam_segment], 

119 "exam_slot": slot, 

120 "description": definition.description if definition else feature_key, 

121 "min_plan": min_plan_for_feature(feature_key), 

122 } 

123 

124 

125def list_exam_catalog_entries() -> list[dict[str, Any]]: 

126 order = ["staar", "tsi", "sat", "act"] 

127 

128 def sort_key(key: str) -> tuple[int, int]: 

129 segment, slot = parse_exam_feature_key(key) 

130 return order.index(segment), slot 

131 

132 return [catalog_entry(key) for key in sorted(TIERED_EXAM_FEATURE_KEYS, key=sort_key)]