requisite_processor 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.3
2
+ Name: requisite_processor
3
+ Version: 0.1.4
4
+ Summary: Module for working with course requisite data and checking requisite satisfaciton.
5
+ Author: Michael Hickman
6
+ Author-email: mhickman@unm.edu
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Requisite Processor
16
+ The `RequisiteProcessor` class is designed to evaluate whether the prerequisite rules for a course are met given a list of completed courses and test score data.
17
+
18
+ ## Prerequisite Data
19
+ This module can make use of any arbitrary prerequisite information given that it is formatted correctly. For more information on the necessary structure as well as the latest prerequsite data for UNM courses, see this repo:
20
+
21
+ https://lobogit.unm.edu/unm-data-analytics/requisite-data#
22
+
23
+ ## Usage
24
+
25
+ ### Initialization
26
+ Initialize the RequisiteProcessor with a dictionary of prerequisite rules.
27
+
28
+ ```python
29
+ from requisite_processor.requisite_processor import RequisiteProcessor
30
+
31
+ prerequisite_rules = {
32
+ "202310": {
33
+ "MATH 1512": {
34
+ "type": "course",
35
+ "course": {"code": "MATH 1234"},
36
+ "minimum_course_grade": "C"
37
+ }
38
+ }
39
+ }
40
+
41
+ processor = RequisiteProcessor(prerequisite_rules)
42
+ ```
43
+
44
+ ### Processor Methods
45
+ The RequisiteProcessor contains the following methods:
46
+ *availabile_academic_periods*:
47
+ Returns a list of all academic periods in the dataset.
48
+ ```python
49
+ academic_periods = processor.availabile_academic_periods
50
+ print(academic_periods) # Output: ['202310', '202380']
51
+ ```
52
+
53
+ *latest_available_academic_period*:
54
+ Returns the most recent academic period.
55
+ ```python
56
+ latest_period = processor.latest_available_academic_period
57
+ print(latest_period) # Output: '202380'
58
+ ```
59
+
60
+ *check_satisfaction*:
61
+ Checks if a student satisfies the prerequisites for a course in a specific academic period.
62
+
63
+ Parameters:
64
+
65
+ - course_code (str): Course to check prerequisites for.
66
+ - courses_taken (list[dict], optional): List of completed courses and grades. Defaults to [].
67
+ - test_scores (dict, optional): Test scores dictionary. Defaults to {}.
68
+ - academic_period (str, optional): Academic period. Defaults to the latest available.
69
+ - ignore_tests (bool, optional): Ignore test requirements. Defaults to False.
70
+ - ignore_grades (bool, optional): Ignore grade requirements. Defaults to False.
71
+
72
+ Returns:
73
+ RequisiteCheckResult object. The result of the prerequisite check.
74
+
75
+ See below for example usage.
76
+
77
+ ### Example Prerequisite Satisfaction Check
78
+
79
+ ```python
80
+ courses_taken = [{"code": "MATH 1250", "grade": "B"}]
81
+ test_scores = {"A01": 28}
82
+
83
+ result = processor.check_satisfaction(
84
+ course_code="MATH 1512",
85
+ courses_taken=courses_taken,
86
+ test_scores=test_scores
87
+ )
88
+
89
+ print(result.satisfied) # Output: True or False
90
+ ```
91
+
92
+ ## Future Work
93
+ - Write tests
94
+ - Allow for more customization in how requisites are processed.
95
+ - Write additional helper functions such as string representation of requirements, flattend requirements and more.
@@ -0,0 +1,81 @@
1
+ # Requisite Processor
2
+ The `RequisiteProcessor` class is designed to evaluate whether the prerequisite rules for a course are met given a list of completed courses and test score data.
3
+
4
+ ## Prerequisite Data
5
+ This module can make use of any arbitrary prerequisite information given that it is formatted correctly. For more information on the necessary structure as well as the latest prerequsite data for UNM courses, see this repo:
6
+
7
+ https://lobogit.unm.edu/unm-data-analytics/requisite-data#
8
+
9
+ ## Usage
10
+
11
+ ### Initialization
12
+ Initialize the RequisiteProcessor with a dictionary of prerequisite rules.
13
+
14
+ ```python
15
+ from requisite_processor.requisite_processor import RequisiteProcessor
16
+
17
+ prerequisite_rules = {
18
+ "202310": {
19
+ "MATH 1512": {
20
+ "type": "course",
21
+ "course": {"code": "MATH 1234"},
22
+ "minimum_course_grade": "C"
23
+ }
24
+ }
25
+ }
26
+
27
+ processor = RequisiteProcessor(prerequisite_rules)
28
+ ```
29
+
30
+ ### Processor Methods
31
+ The RequisiteProcessor contains the following methods:
32
+ *availabile_academic_periods*:
33
+ Returns a list of all academic periods in the dataset.
34
+ ```python
35
+ academic_periods = processor.availabile_academic_periods
36
+ print(academic_periods) # Output: ['202310', '202380']
37
+ ```
38
+
39
+ *latest_available_academic_period*:
40
+ Returns the most recent academic period.
41
+ ```python
42
+ latest_period = processor.latest_available_academic_period
43
+ print(latest_period) # Output: '202380'
44
+ ```
45
+
46
+ *check_satisfaction*:
47
+ Checks if a student satisfies the prerequisites for a course in a specific academic period.
48
+
49
+ Parameters:
50
+
51
+ - course_code (str): Course to check prerequisites for.
52
+ - courses_taken (list[dict], optional): List of completed courses and grades. Defaults to [].
53
+ - test_scores (dict, optional): Test scores dictionary. Defaults to {}.
54
+ - academic_period (str, optional): Academic period. Defaults to the latest available.
55
+ - ignore_tests (bool, optional): Ignore test requirements. Defaults to False.
56
+ - ignore_grades (bool, optional): Ignore grade requirements. Defaults to False.
57
+
58
+ Returns:
59
+ RequisiteCheckResult object. The result of the prerequisite check.
60
+
61
+ See below for example usage.
62
+
63
+ ### Example Prerequisite Satisfaction Check
64
+
65
+ ```python
66
+ courses_taken = [{"code": "MATH 1250", "grade": "B"}]
67
+ test_scores = {"A01": 28}
68
+
69
+ result = processor.check_satisfaction(
70
+ course_code="MATH 1512",
71
+ courses_taken=courses_taken,
72
+ test_scores=test_scores
73
+ )
74
+
75
+ print(result.satisfied) # Output: True or False
76
+ ```
77
+
78
+ ## Future Work
79
+ - Write tests
80
+ - Allow for more customization in how requisites are processed.
81
+ - Write additional helper functions such as string representation of requirements, flattend requirements and more.
@@ -0,0 +1,15 @@
1
+ [tool.poetry]
2
+ name = "requisite_processor"
3
+ version = "0.1.4"
4
+ description = "Module for working with course requisite data and checking requisite satisfaciton."
5
+ authors = ["Michael Hickman <mhickman@unm.edu>"]
6
+ readme = "README.md"
7
+ packages = [{include = "requisite_processor"}]
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.10"
11
+
12
+
13
+ [build-system]
14
+ requires = ["poetry-core"]
15
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,13 @@
1
+ class RequisiteCheckResult:
2
+ def __init__(self, satisfied = False, met_condition = None, failed_condition = None):
3
+ self.satisfied = satisfied
4
+ self.met_conditions = []
5
+ self.failed_conditions = []
6
+
7
+ if met_condition is not None:
8
+ self.met_conditions = [met_condition]
9
+
10
+ if failed_condition is not None:
11
+ self.failed_conditions = [failed_condition]
12
+
13
+
@@ -0,0 +1,233 @@
1
+ import re
2
+ from requisite_processor.requisite_check import RequisiteCheckResult
3
+
4
+ class RequisiteProcessor:
5
+ def __init__(self, prerequisites: dict):
6
+ self.prerequisite_rules = prerequisites
7
+
8
+ @property
9
+ def availabile_academic_periods(self) -> list[str]:
10
+ """Returns a list of all academic periods in the provided prerequiste data.
11
+
12
+ Returns:
13
+ list[str]: List of academic period strings. Ex: ['202310', '202380']
14
+ """
15
+ return self.prerequisite_rules.keys()
16
+
17
+
18
+ @property
19
+ def latest_available_academic_period(self) -> str:
20
+ """Returns the most recent academic period of the periods in the given prerequisite data.
21
+
22
+ Returns:
23
+ str: Academic period string. Ex: '202310'
24
+ """
25
+ available = list(map(int, self.availabile_academic_periods))
26
+ available.sort()
27
+ return str(available[-1])
28
+
29
+
30
+ def check_satisfaction(
31
+ self,
32
+ course_code: str,
33
+ courses_taken: list[dict] = None,
34
+ test_scores = None,
35
+ academic_period = None,
36
+ ignore_tests = False,
37
+ ignore_grades = False
38
+ ) -> RequisiteCheckResult:
39
+ """Determines if a list of courses and test scores meets the prerequisite
40
+ requirements for a given course in a given academic period.
41
+
42
+ Args:
43
+ course_code (str): The code of the course to check prerequisite satisfaction for. Ex: 'MATH 1512'
44
+
45
+ courses_taken (list[dict], optional): A list of course and grade information. Each entry
46
+ should be a dictionary with a code and grade key. Ex: [{ 'code': 'MATH 1512', 'grade': 'A-'}]. Defaults to [].
47
+
48
+ test_scores (dict, optional): A dictionary with test score information. Each key of the dictionary should be a
49
+ test code where the value is the score for the associated test. Ex: { 'A01': 27 }. Defaults to {}.
50
+
51
+ academic_period (_type_, optional): The academic period to pull prerequisites from. Defaults to the latest academic period in the
52
+ requirement infomation.
53
+
54
+ ignore_tests (bool, optional): Setting to True will ignore test requirements when checking for
55
+ satisfaction. Defaults to False.
56
+
57
+ ignore_grades (bool, optional): Setting to true will ingore grade requirements and will consider
58
+ any grade as acceptable. Defaults to False.
59
+
60
+ Raises:
61
+ AcademicPeriodNotFound: Raised when the given academic period is not present in the given prerequisite data.
62
+ CourseNotFound: Raised when the given course is not found within in the given academic period.
63
+
64
+ Returns:
65
+ RequisiteCheckResult: Object that includes result of the check.
66
+ """
67
+
68
+ if courses_taken is None:
69
+ courses_taken = []
70
+
71
+ if academic_period is None:
72
+ academic_period = self.latest_available_academic_period
73
+
74
+ if academic_period not in self.prerequisite_rules:
75
+ raise self.AcademicPeriodNotFound(f"The academic period '{academic_period}' is not found in the given prerequisite data.")
76
+
77
+ if course_code not in self.prerequisite_rules[academic_period]:
78
+ raise self.CourseNotFound(f"Course {course_code} not found in {academic_period}")
79
+
80
+ prereq_rule = self.prerequisite_rules[academic_period][course_code]
81
+
82
+ if ignore_tests is True:
83
+ prereq_rule = self.__reduce_rule(prereq_rule, lambda x: x['type'] in ['and', 'or', 'course'])
84
+
85
+ if prereq_rule is None:
86
+ return RequisiteCheckResult(True)
87
+
88
+ return self.__traverse_prereqs(prereq_rule, academic_period, courses_taken, test_scores, ignore_grades)
89
+
90
+
91
+ def __grade_score(self, grade) -> float:
92
+ grades = {
93
+ 'A+': 4.3, 'A': 4, 'A-': 3.7,
94
+ 'B+': 3.3, 'B': 3, 'B-': 2.7,
95
+ 'C+': 2.3, 'C': 2, 'C-': 2,
96
+ 'CR': 2,
97
+ 'D+': 1.7, 'D': 1, 'D-': 0.7,
98
+ }
99
+
100
+ processed_grade = re.sub('(T|X|\\*)', '', grade)
101
+ return grades.get(processed_grade, 0)
102
+
103
+
104
+ def __traverse_prereqs(self, prereq_data: dict, academic_period, courses_taken, test_scores, ignore_grades):
105
+ if prereq_data['type'] == 'course':
106
+ selected_course = None
107
+ for course in courses_taken:
108
+ if course['code'] == prereq_data['course']['code'] and (ignore_grades is True or self.__grade_score(course['grade']) >= self.__grade_score(prereq_data['minimum_course_grade'])):
109
+ selected_course = course
110
+ break
111
+
112
+ if selected_course is not None:
113
+ return RequisiteCheckResult(True, met_condition = { 'type': 'course', 'code': selected_course['code'] })
114
+ else:
115
+ return RequisiteCheckResult(False, failed_condition = { 'type': 'course', 'code': prereq_data['course']['code'] })
116
+
117
+ if prereq_data['type'] == 'placement_test':
118
+ test_code = prereq_data['placement_test']['test_code']
119
+ if test_code not in test_scores:
120
+ return RequisiteCheckResult(False, failed_condition = { 'type': 'placement_test', 'code': test_code, 'score': prereq_data['minimum_test_score'] })
121
+
122
+ if test_scores[test_code] >= prereq_data['minimum_test_score']:
123
+ return RequisiteCheckResult(True, met_condition = { 'type': 'placement_test', 'score': prereq_data['minimum_test_score'], 'code': test_code })
124
+ else:
125
+ return RequisiteCheckResult(False, failed_condition = { 'type': 'placement_test', 'score': prereq_data['minimum_test_score'], 'code': test_code })
126
+
127
+
128
+ if prereq_data['type'] == 'and':
129
+ prereq_operands = []
130
+ for sub_prereq_data in prereq_data['operands']:
131
+ if sub_prereq_data['type'] == 'course' and sub_prereq_data['concurrency_ind'] == True:
132
+ continue
133
+ prereq_operands.append(sub_prereq_data)
134
+
135
+ if len(prereq_operands) == 0:
136
+ return RequisiteCheckResult(True)
137
+
138
+ results = []
139
+ for sub_prereq_data in prereq_operands:
140
+ results.append(self.__traverse_prereqs(sub_prereq_data, academic_period, courses_taken, test_scores, ignore_grades))
141
+
142
+ result = RequisiteCheckResult(True)
143
+ for r in results:
144
+ if r is None:
145
+ continue
146
+
147
+ result.satisfied = r.satisfied and result.satisfied
148
+ if r.satisfied:
149
+ result.met_conditions = result.met_conditions + r.met_conditions
150
+
151
+ result.failed_conditions = result.failed_conditions + r.failed_conditions
152
+
153
+ return result
154
+
155
+ if prereq_data['type'] == 'or':
156
+ prereq_operands = []
157
+
158
+ for sub_prereq_data in prereq_data['operands']:
159
+ if sub_prereq_data['type'] == 'course' and sub_prereq_data['concurrency_ind'] is True:
160
+ continue
161
+
162
+ prereq_operands.append(sub_prereq_data)
163
+
164
+ if len(prereq_operands) == 0:
165
+ return RequisiteCheckResult(True)
166
+
167
+ results = []
168
+ for sub_prereq_data in prereq_operands:
169
+ results.append(self.__traverse_prereqs(sub_prereq_data, academic_period, courses_taken, test_scores, ignore_grades))
170
+
171
+ result = RequisiteCheckResult(False)
172
+
173
+ for r in results:
174
+ if r is None:
175
+ continue
176
+
177
+ if result.satisfied is False and r.satisfied is True:
178
+ result.satisfied = True
179
+ elif r.satisfied is True:
180
+ result.satisfied = True
181
+
182
+ if r.satisfied:
183
+ result.met_conditions = result.met_conditions + r.met_conditions
184
+ result.failed_conditions = result.failed_conditions + r.failed_conditions
185
+
186
+ return result
187
+
188
+
189
+ def __reduce_rule(self, prereq_data, filter_rule):
190
+ if prereq_data is None:
191
+ return None
192
+
193
+ if 'operands' in prereq_data:
194
+ new_operands = []
195
+ for operand in prereq_data['operands']:
196
+ reduced_operand = self.__reduce_rule(operand, filter_rule)
197
+ if reduced_operand is not None:
198
+ new_operands.append(reduced_operand)
199
+
200
+ if len(new_operands) == 1:
201
+ if prereq_data.get('root', False) is True:
202
+ new_operands[0]['root'] = True
203
+
204
+ return new_operands[0]
205
+
206
+ elif len(new_operands) == 0:
207
+ return None
208
+ else:
209
+ new_prereq = {
210
+ 'type': prereq_data['type'],
211
+ 'operands': new_operands
212
+ }
213
+
214
+ if prereq_data.get('root', False) is True:
215
+ new_prereq['root'] = True
216
+
217
+ return new_prereq
218
+ else:
219
+ return False if filter(prereq_data) is False else prereq_data
220
+
221
+ class AcademicPeriodNotFound(Exception):
222
+ """An exception to be raised when an academic period is not found in the
223
+ given prerequisite data.
224
+ """
225
+ def __init__(self, *args):
226
+ super().__init__(*args)
227
+
228
+ class CourseNotFound(Exception):
229
+ """An exception to be raised when a course is not found in the
230
+ given prerequisite data.
231
+ """
232
+ def __init__(self, *args):
233
+ super().__init__(*args)