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.
- requisite_processor-0.1.4/PKG-INFO +95 -0
- requisite_processor-0.1.4/README.md +81 -0
- requisite_processor-0.1.4/pyproject.toml +15 -0
- requisite_processor-0.1.4/requisite_processor/__init__.py +0 -0
- requisite_processor-0.1.4/requisite_processor/requisite_check.py +13 -0
- requisite_processor-0.1.4/requisite_processor/requisite_processor.py +233 -0
|
@@ -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"
|
|
File without changes
|
|
@@ -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)
|