-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathutils.py
163 lines (126 loc) · 4.89 KB
/
utils.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
from re import match
import click
from settings import COURSE_NAME_PATTERN, COURSE_TYPES_TO_FLAGS
class ValidationError(Exception):
def __init__(self, message: str, details: str):
super().__init__(message)
self.message = message
self.details = details
def parse_course_str(raw_class: str):
'''
This is the key parser for the class/course strings
String Format
-------------
Underlying assumptions about the format of the string (with the dept removed):
`F 01A. 01Z`
`F 1AHL 01`
First character - campus ('F' or 'D')
Next 4 characters - course name / ID with leading 0's and possibly a trailing '.'
Last characters - section string (1-3 chars)
NOTE: As of Fall 2020 data, all strings are between 7-8 chars,
and all section strings are between 2-3 chars.
Components
----------
Dept - Department
Course - Course name / ID (ex. '1AL')
Section - Class section (ex. '1HZ')
Flags - Extra flags from the section (ex. {'H', 'Z'})
:param raw_class: (str) The unparsed string, ex. `C S F001A01Z`
:return: (dict) the parsed data {'dept', 'course', 'section', 'flags'}
'''
# Split the raw course string by a space, to separate different parts
# ex. 'C S F001A01Z' => ['C', 'S', 'F001A01Z']
parts = raw_class.split(' ')
if len(parts) < 2:
raise ValidationError(
f"Raw course string ('{raw_class}') is invalid",
'At least two space separated parts could not be found'
)
# All parts excluding the last one are assumed to be part of the department name
# ex. `C S` => `CS`
dept = ''.join(parts[0:-1])
# The last part is the actual course string (without the department)
# ex. `F001A01Z`
without_dept = parts[-1]
if len(without_dept) < 6 or len(without_dept) > 8:
raise ValidationError(
f"Invalid course + section string ('{without_dept}')",
'Length is not between 6-8 chars'
)
# First five characters are the course name
# ex. `D001A` or `F04BH`
course = without_dept[0:5]
# Regex to clean the leading 'F'/'D' and extraneous 0's
match_obj = match(COURSE_NAME_PATTERN, course)
if not match_obj or not match_obj.groups():
raise ValidationError(
f"Whoops, the course (ex. '24A') could not be extracted from '{course}'",
'The course name regex does not match'
)
if len(match_obj[0]) < 5:
raise ValidationError(
f"Whoops, the course (ex. '24A') could not be extracted from '{course}'",
'The course name regex does not fully match'
)
# Cleaned course name, e.g. `4A`
cleaned_course = match_obj[1]
# The last chars are the class section + flags
# ex. `01Z` or `5ZH`
section = without_dept[5:]
# Extract flags by filtering nonalphabets from the class section string
flags = set(filter(str.isalpha, section))
return {
'dept': dept,
'course': cleaned_course,
'section': section,
'flags': flags,
}
def get_class_type(campus: str, flags: set):
'''
From a given set of class flags, return the type of the class
:param campus: (str) The campus (check COURSE_TYPES_TO_FLAGS)
:param flags: (set) The flags for the class
:return: (str) The type of class
'''
class_type = None
for name, flag in COURSE_TYPES_TO_FLAGS[campus].items():
# Exclude 'standard' because that is the fallback case
if name != 'standard' and flag in flags:
if class_type:
# TODO: should this be a warning instead?
raise ValidationError('Class has multiple types in its flags', '')
class_type = name
if not class_type:
class_type = 'standard'
return class_type
def log(tag, color, message, details=None, pad=True):
'''
Log a message to stdout
:param tag: (str) The tag, ex. `info` or `warn`
:param color: (str) The color for the tag (passed to colorama)
:param message: (str) The log message string
:param details: (dict) Extra details to show {'label': 'value'}
:param pad: (bool) Whether the extra detail lines should be padded
'''
formatted_tag = click.style(tag, fg=color, bold=True)
print(f'{formatted_tag} {message}')
if details:
format_label = lambda text: click.style(text, fg='white', dim=True, bold=True)
for label, msg in details.items():
padding = len(tag) * ' ' + ' ' if pad else ''
print(padding + format_label(label), msg)
def log_info(*args, **kwargs):
'''
Log an info message (see log())
'''
log('info', 'green', *args, **kwargs)
def log_warn(*args, **kwargs):
'''
Log a warning (see log())
'''
log('warn', 'yellow', *args, **kwargs)
def log_err(*args, **kwargs):
'''
Log an error (see log())
'''
log('err', 'red', *args, **kwargs)