# compute.py # coding: utf-8 import re import pprint from functools import reduce from data import data, overrides # - support where queries # - [ ] basic queries # - [ ] boolean sets of queries # - [ ] nested queries # - write tests class RequiredKeyException(Exception): def __init__(self, keys=[], msg='', data={}): self.value = keys or msg self.data = data def __str__(self): return 'missing keys: ' + repr(self.value) + str(self.data) class UnknownPropertyException(Exception): def __init__(self, value): self.value = value def __str__(self): return 'unknown value: ' + repr(self.value) class BadTypeException(Exception): def __init__(self, msg, data): self.value = msg self.data = data def __str__(self): return self.value + ' ' + repr(self.data) def pluck(list, prop): return [i[prop] for i in list if prop in i] def add(a, b): return a + b def sum(iter): return reduce(add, iter, 0) # Helper Functions def isRequirement(name): return bool(re.match(r'[A-Z]|[0-9][A-Z\- ]', name)) def compare_course(course, to): # course might have more keys than the dict we're comparing it to # 'to' will have some combination of 'year', 'semester', 'department', 'number', and 'section' for key in ['year', 'semester', 'department', 'number', 'section']: if key in to and course[key] != to[key]: return False return True #compared = {key: val # for key, val in course.items() # if to.get(key, False)} #return bool(compared) def checkForCourse(filter, courses): return any([c for c in courses if compare_course(course=c, to=filter)]) def get_occurrences(course, courses): return [c for c in courses if compare_course(course=c, to=filter)] def assertKeys(dict, *keys): missingKeys = [key for key in keys if key not in dict] if missingKeys: raise RequiredKeyException(keys=missingKeys, data=dict) def countCourses(courses): #courses::pluck('crsid')::uniq()::len() return len(set(pluck(courses, 'crsid'))) def countDepartments(courses): # courses::pluck('departments')::sum() return sum(pluck(courses, 'departments')) def countCredits(courses): return sum(pluck(courses, 'credits')) def pathToOverride(path): return '.'.join(path).lower() def hasOverride(path, overrides): #print pathToOverride(path), overrides, pathToOverride(path) in overrides return pathToOverride(path) in overrides def getOverride(path, overrides): return overrides[pathToOverride(path)] def findOperatorType(operator): if '$eq' in operator: return '$eq' elif '$ne' in operator: return '$ne' elif '$lt' in operator: return '$lt' elif '$lte' in operator: return '$lte' elif '$gt' in operator: return '$gt' elif '$gte' in operator: return '$gte' else: raise RequiredKeyException(msg='no valid operators ($eq, $ne, $lt, $lte, $gt, $gte) could be found', data=operator) def compareCourseAgainstOperator(course, key, operator): # key: gereqs, operator: {$eq: "EIN"} # key: year, operator: { # "$type": "operator", # "$lte": { # "$name": "max", # "$prop": "year", # "$type": "function", # "$where": [{ # "$type": "qualification", "gereqs": { # "$type": "operator", "$eq": "BTS-T" # } # }] # } # } } kind = findOperatorType(operator) if type(operator[kind]) is dict: # we compute the value of the function-over-where-query style operators # earlier, in the filterByQualification function. assertKeys(operator[kind], '$computed-value') simplifiedOperator = {kind: operator[kind]['$computed-value']} return compareCourseAgainstOperator(course, key, simplifiedOperator) elif type(operator[kind]) is list: raise BadTypeException(msg='what would a comparison to a list even do? oh, wait; i suppose it could compare against one of several values... well, im not doing that right now. if you want it, edit the PEG and stick appropriate stuff in here (probably simplest to just call this function again with each possible value and return true if any are true.)') else: # it's a static value; a number or string if kind == '$eq': return (course[key] == operator[kind] if type(course[key]) is not list else operator[kind] in course[key]) elif kind == '$ne': return (course[key] != operator[kind] if type(course[key]) is not list else operator[kind] not in course[key]) elif kind == '$lt': return course[key] < operator[kind] elif kind == '$lte': return course[key] <= operator[kind] elif kind == '$gt': return course[key] > operator[kind] elif kind == '$gte': return course[key] >= operator[kind] def filterByQualification(list, qualification): # { "$type":"qualification", $key: "gereqs", $value: {"$type": "operator", "$eq": "EIN"} } # { "$type":"qualification", $key: "year", value: { # "$type": "operator", # "$lte": { # "$name": "max", # "$prop": "year", # "$type": "function", # "$where": { # "$type": "qualification", $key: "gereqs", $value: { # "$type": "operator", "$eq": "BTS-T" # } # } # } # } } operator = qualification['$value'] kind = findOperatorType(operator) if type(operator[kind]) is dict: value = operator[kind] if value['$type'] == 'function': func = None if value['$name'] == 'max': func = max elif value['$name'] == 'min': func = min filtered = filterfilterByWhereClause(value['$where']) items = pluck(value['$prop'], filtered) computed = func(items) value['$computed-value'] = computed print qualification key = qualification['$key'] operator = qualification['$value'] filtered = [course for course in list if compareCourseAgainstOperator(course, key, operator)] #if compareQualificationToCourse(qualification, course)] print len(list), len(filtered) return filtered def filterByWhereClause(list, clause): # {gereqs = EIN & year <= max(year) from {gereqs = BTS-T}} # { # "$type": "boolean", # "$and": [ # { "$type":"qualification", $key: "gereqs", $value: {"$type": "operator", "$eq": "EIN"} }, # { "$type":"qualification", $key: "year", $value: { # "$type": "operator", # "$lte": { # "$name": "max", # "$prop": "year", # "$type": "function", # "$where": { # "$type": "qualification", $key: "gereqs", $value: { # "$type": "operator", "$eq": "BTS-T" # } # } # } # } } # ] # } if clause['$type'] == 'qualifier': #print 'qualification' return filterByQualification(list, clause) elif clause['$type'] == 'boolean': #print 'boolean' if '$and' in clause: filtered = list for q in clause['$and']: filtered = filterByWhereClause(filtered, q) return filtered elif '$or' in clause: filtrations = set() for q in clause['$or']: filtrations = filtrations.union(filterByWhereClause(list, q)) return filtrations else: raise RequiredKeyException(msg='neither $or nor $and could be found', data=clause) else: raise BadTypeException(msg='wth kind of type is this clause?', data=clause) # Compute Functions: # There are two types of compute functions: those that need the surrounding context, and those that don't. # Contained Computes: # course, occurrence, where def compute_course(expr, courses): query = {key: value for key, value in expr.items() if key not in ['$type']} return checkForCourse(filter=query, courses=courses) def compute_occurrence(expr, courses): assertKeys(expr, '$course', '$count') clause = {key: val for key, val in expr['$course'] if key in ['department', 'number', 'section']} filtered = get_occurrences(clause, courses) return len(filtered) >= expr['$count'] def compute_where(expr, courses): assertKeys(expr, '$where', '$count') filtered = filterByWhereClause(courses, expr['$where']) expr['_matches'] = filtered return len(filtered) >= expr['$count'] # Contextual Computes: # boolean, modifier, of, reference def compute_boolean(expr, ctx, courses): if '$or' in expr: return any([compute_chunk(req, ctx, courses) for req in expr['$or']]) elif '$and' in expr: return all([compute_chunk(req, ctx, courses) for req in expr['$and']]) elif '$not' in expr: return not (compute_chunk(expr['$not'], ctx, courses)) else: print print result raise RequiredKeyException(msg='none of $or, $and, or $not could be found') def compute_of(expr, ctx, courses): assertKeys(expr, '$of', '$count') evaluated = [compute_chunk(req, ctx, courses) for req in expr['$of']] truthy = [i for i in evaluated if i] return len(truthy) >= expr['$count'] def compute_reference(expr, ctx, courses): assertKeys(expr, '$requirement') if expr['$requirement'] in ctx: target = ctx[expr['$requirement']] return target['computed'] else: return False def compute_modifier(expr, ctx, courses): assertKeys(expr, '$what', '$count', '$from') what = expr['$what'] if what not in ['course', 'department', 'credit']: raise UnknownPropertyException(what) if expr['$from'] == 'children': return 'not yet implemented' elif expr['$from'] == 'where': assertKeys(expr, '$where', '$count') filtered = filterByWhereClause(courses, expr['$where']) if what == 'course': num = countCourses(filtered) elif what == 'department': num = countDepartments(filtered) elif what == 'credit': num = countCredits(filtered) return num >= expr['$count'] # And, of course, the function that dispatches the appropriate compute: def compute_chunk(expr, ctx, courses): #print #print 'expression:', expr #print 'context:', ctx assertKeys(expr, '$type') type = expr['$type'] computed = False if type == 'boolean': computed = compute_boolean(expr, ctx, courses) elif type == 'course': computed = compute_course(expr, courses) elif type == 'modifier': computed = compute_modifier(expr, ctx, courses) elif type == 'occurrence': computed = compute_occurrence(expr, courses) elif type == 'of': computed = compute_of(expr, ctx, courses) elif type == 'reference': computed = compute_reference(expr, ctx, courses) elif type == 'where': computed = compute_where(expr, courses) expr['_result'] = computed return computed # The overall computation is done by compute, which is in charge of computing sub-requirements and such. def compute(requirement, path, courses=[], overrides={}): this_name = path[-1] #print '' #print requirement, this_name requirement = { name: compute(req, path+[name], courses, overrides) if isRequirement(name) else req for name, req in requirement.items() } computed = False if 'result' in requirement: computed = compute_chunk(requirement['result'], requirement, courses) # requirement['result'] = result elif 'message' in requirement: computed = False # show a button to toggle overriding pass else: #raise RequiredKeyException(msg='one of message or result is required') print('one of message or result is required') requirement['computed'] = computed if hasOverride(path, overrides): requirement['overridden'] = True requirement['computed'] = getOverride(path, overrides) return requirement def main(): assertKeys(data, 'name', 'result', 'type', 'revision') name = data['name'] type = data['type'] courses = [ {"department": ["ART"], "number": 102}, {"department": ["ART"], "number": 103}, {"department": ["ART"], "number": 104}, {"department": ["ART"], "number": 106}, {"department": ["ART"], "number": 205}, {"department": ["ART"], "number": 207}, {"department": ["ART"], "number": 221}, {"department": ["ART"], "number": 222}, {"department": ["ART"], "number": 223}, {"department": ["ART"], "number": 224}, {"department": ["ART"], "number": 225}, {"department": ["ART"], "number": 226}, {"department": ["ART"], "number": 227}, {"department": ["ART"], "number": 228}, {"department": ["ART"], "number": 229}, {"department": ["ART"], "number": 232}, {"department": ["ART"], "number": 233}, {"department": ["ART"], "number": 234}, {"department": ["ART"], "number": 236}, {"department": ["ART"], "number": 238}, {"department": ["ART"], "number": 343}, {'department': ['ART'], 'number': 398}, {'department': ['ART', 'ASIAN'], 'number': 259}, {'department': ['CSCI'], 'number': 241}, ] pp = pprint.PrettyPrinter(width=40, indent=2) c = compute(requirement=data, path=[type, name], courses=courses, overrides=overrides) #print pprint.pformat(c, indent=2) print 'outcome:', c['computed'] #print data def test(): print compareCourseAgainstOperator( {'department': ['CSCI']}, 'department', {'$eq': 'ART'}) if __name__ == "__main__": #test() main()