diff --git a/.gitignore b/.gitignore index cc979da25..7b6b58d7d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ notebook .vscode __main__.py jupyter_custom.js -apk_requirements.txt \ No newline at end of file +apk_requirements.txt +.eggs \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 90e429bb1..35e78375a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,18 @@ ## Release notes -### 0.13.0 -- TBD +### 0.13.0 -- Mar 24, 2021 +* Re-implement query transpilation into SQL, fixing issues (#386, #449, #450, #484). PR #754 +* Re-implement cascading deletes for better performance. PR #839. +* Add table method `.update1` to update a row in the table with new values PR #763 +* Python datatypes are now enabled by default in blobs (#761). PR #785 +* Added permissive join and restriction operators `@` and `^` (#785) PR #754 * Support DataJoint datatype and connection plugins (#715, #729) PR 730, #735 -* Allow updating specified secondary attributes using `update1` PR #763 -* add dj.key_hash reference to dj.hash.key_hash, treat as 'public api' -* default enable_python_native_blobs to True -* Remove python 3.5 support +* Add `dj.key_hash` alias to `dj.hash.key_hash` +* Default enable_python_native_blobs to True +* Bugfix - Regression error on joins with same attribute name (#857) PR #878 +* Bugfix - Error when `fetch1('KEY')` when `dj.config['fetch_format']='frame'` set (#876) PR #880, #878 +* Bugfix - Error when cascading deletes in tables with many, complex keys (#883, #886) PR #839 +* Drop support for Python 3.5 ### 0.12.8 -- Jan 12, 2021 * table.children, .parents, .descendents, and ancestors can return queryable objects. PR #833 diff --git a/LNX-docker-compose.yml b/LNX-docker-compose.yml index 64d8b7ba3..c91f4c01e 100644 --- a/LNX-docker-compose.yml +++ b/LNX-docker-compose.yml @@ -32,7 +32,7 @@ services: interval: 1s fakeservices.datajoint.io: <<: *net - image: raphaelguzman/nginx:v0.0.13 + image: datajoint/nginx:v0.0.16 environment: - ADD_db_TYPE=DATABASE - ADD_db_ENDPOINT=db:3306 @@ -72,14 +72,17 @@ services: - COVERALLS_SERVICE_NAME - COVERALLS_REPO_TOKEN working_dir: /src - command: > - /bin/sh -c - " - pip install --user nose nose-cov coveralls .; - pip freeze | grep datajoint; - nosetests -vsw tests --with-coverage --cover-package=datajoint && coveralls; - # jupyter notebook; - " + command: + - sh + - -c + - | + set -e + pip install --user -r test_requirements.txt + pip install --user . + pip freeze | grep datajoint + nosetests -vsw tests --with-coverage --cover-package=datajoint + coveralls + # jupyter notebook # ports: # - "8888:8888" user: ${UID}:${GID} diff --git a/datajoint/__init__.py b/datajoint/__init__.py index c26fc8435..d0303d2dd 100644 --- a/datajoint/__init__.py +++ b/datajoint/__init__.py @@ -15,7 +15,7 @@ """ __author__ = "DataJoint Contributors" -__date__ = "February 7, 2019" +__date__ = "November 7, 2020" __all__ = ['__author__', '__version__', 'config', 'conn', 'Connection', 'Schema', 'schema', 'VirtualModule', 'create_virtual_module', diff --git a/datajoint/autopopulate.py b/datajoint/autopopulate.py index 87101818d..9e9615144 100644 --- a/datajoint/autopopulate.py +++ b/datajoint/autopopulate.py @@ -7,7 +7,6 @@ from tqdm import tqdm from .expression import QueryExpression, AndList from .errors import DataJointError, LostConnectionError -from .table import FreeTable import signal # noinspection PyExceptionInherit,PyCallingNonCallable @@ -58,8 +57,8 @@ def make(self, key): @property def target(self): """ - relation to be populated. - Typically, AutoPopulate are mixed into a Relation object and the target is self. + :return: table to be populated. + In the typical case, dj.AutoPopulate is mixed into a dj.Table class by inheritance and the target is self. """ return self @@ -67,6 +66,7 @@ def _job_key(self, key): """ :param key: they key returned for the job from the key source :return: the dict to use to generate the job reservation hash + This method allows subclasses to control the job reservation granularity. """ return key @@ -136,7 +136,7 @@ def handler(signum, frame): make = self._make_tuples if hasattr(self, '_make_tuples') else self.make - for key in (tqdm(keys) if display_progress else keys): + for key in (tqdm(keys, desc=self.__class__.__name__) if display_progress else keys): if max_calls is not None and call_count >= max_calls: break if not reserve_jobs or jobs.reserve(self.target.table_name, self._job_key(key)): diff --git a/datajoint/blob.py b/datajoint/blob.py index 04750ba00..225c68c1f 100644 --- a/datajoint/blob.py +++ b/datajoint/blob.py @@ -11,11 +11,10 @@ import uuid import numpy as np from .errors import DataJointError -from .utils import OrderedDict from .settings import config -mxClassID = OrderedDict(( +mxClassID = dict(( # see http://www.mathworks.com/help/techdoc/apiref/mxclassid.html ('mxUNKNOWN_CLASS', None), ('mxCELL_CLASS', None), @@ -346,8 +345,8 @@ def pack_set(self, t): len_u64(it) + it for it in (self.pack_blob(i) for i in t)) def read_dict(self): - return OrderedDict((self.read_blob(self.read_value()), self.read_blob(self.read_value())) - for _ in range(self.read_value())) + return dict((self.read_blob(self.read_value()), self.read_blob(self.read_value())) + for _ in range(self.read_value())) def pack_dict(self, d): return b"\4" + len_u64(d) + b"".join( diff --git a/datajoint/condition.py b/datajoint/condition.py new file mode 100644 index 000000000..126ed9f69 --- /dev/null +++ b/datajoint/condition.py @@ -0,0 +1,209 @@ +""" methods for generating SQL WHERE clauses from datajoint restriction conditions """ + +import inspect +import collections +import re +import uuid +import datetime +import decimal +import numpy +import pandas +from .errors import DataJointError + + +class PromiscuousOperand: + """ + A container for an operand to ignore join compatibility + """ + def __init__(self, operand): + self.operand = operand + + +class AndList(list): + """ + A list of conditions to by applied to a query expression by logical conjunction: the conditions are AND-ed. + All other collections (lists, sets, other entity sets, etc) are applied by logical disjunction (OR). + + Example: + expr2 = expr & dj.AndList((cond1, cond2, cond3)) + is equivalent to + expr2 = expr & cond1 & cond2 & cond3 + """ + def append(self, restriction): + if isinstance(restriction, AndList): + # extend to reduce nesting + self.extend(restriction) + else: + super().append(restriction) + + +class Not: + """ invert restriction """ + def __init__(self, restriction): + self.restriction = restriction + + +def assert_join_compatibility(expr1, expr2): + """ + Determine if expressions expr1 and expr2 are join-compatible. To be join-compatible, + the matching attributes in the two expressions must be in the primary key of one or the + other expression. + Raises an exception if not compatible. + :param expr1: A QueryExpression object + :param expr2: A QueryExpression object + """ + from .expression import QueryExpression, U + + for rel in (expr1, expr2): + if not isinstance(rel, (U, QueryExpression)): + raise DataJointError('Object %r is not a QueryExpression and cannot be joined.' % rel) + if not isinstance(expr1, U) and not isinstance(expr2, U): # dj.U is always compatible + try: + raise DataJointError( + "Cannot join query expressions on dependent attribute `%s`" % next( + r for r in set(expr1.heading.secondary_attributes).intersection( + expr2.heading.secondary_attributes))) + except StopIteration: + pass # all ok + + +def make_condition(query_expression, condition, columns): + """ + Translate the input condition into the equivalent SQL condition (a string) + :param query_expression: a dj.QueryExpression object to apply condition + :param condition: any valid restriction object. + :param columns: a set passed by reference to collect all column names used in the condition. + :return: an SQL condition string or a boolean value. + """ + from .expression import QueryExpression, Aggregation, U + + def prep_value(k, v): + """prepare value v for inclusion as a string in an SQL condition""" + if query_expression.heading[k].uuid: + if not isinstance(v, uuid.UUID): + try: + v = uuid.UUID(v) + except (AttributeError, ValueError): + raise DataJointError('Badly formed UUID {v} in restriction by `{k}`'.format(k=k, v=v)) from None + return "X'%s'" % v.bytes.hex() + if isinstance(v, (datetime.date, datetime.datetime, datetime.time, decimal.Decimal)): + return '"%s"' % v + if isinstance(v, str): + return '"%s"' % v.replace('%', '%%') + return '%r' % v + + negate = False + while isinstance(condition, Not): + negate = not negate + condition = condition.restriction + template = "NOT (%s)" if negate else "%s" + + # restrict by string + if isinstance(condition, str): + columns.update(extract_column_names(condition)) + return template % condition.strip().replace("%", "%%") # escape % in strings, see issue #376 + + # restrict by AndList + if isinstance(condition, AndList): + # omit all conditions that evaluate to True + items = [item for item in (make_condition(query_expression, cond, columns) for cond in condition) + if item is not True] + if any(item is False for item in items): + return negate # if any item is False, the whole thing is False + if not items: + return not negate # and empty AndList is True + return template % ('(' + ') AND ('.join(items) + ')') + + # restriction by dj.U evaluates to True + if isinstance(condition, U): + return not negate + + # restrict by boolean + if isinstance(condition, bool): + return negate != condition + + # restrict by a mapping such as a dict -- convert to an AndList of string equality conditions + if isinstance(condition, collections.abc.Mapping): + common_attributes = set(condition).intersection(query_expression.heading.names) + if not common_attributes: + return not negate # no matching attributes -> evaluates to True + columns.update(common_attributes) + return template % ('(' + ') AND ('.join( + '`%s`=%s' % (k, prep_value(k, condition[k])) for k in common_attributes) + ')') + + # restrict by a numpy record -- convert to an AndList of string equality conditions + if isinstance(condition, numpy.void): + common_attributes = set(condition.dtype.fields).intersection(query_expression.heading.names) + if not common_attributes: + return not negate # no matching attributes -> evaluate to True + columns.update(common_attributes) + return template % ('(' + ') AND ('.join( + '`%s`=%s' % (k, prep_value(k, condition[k])) for k in common_attributes) + ')') + + # restrict by a QueryExpression subclass -- trigger instantiation and move on + if inspect.isclass(condition) and issubclass(condition, QueryExpression): + condition = condition() + + # restrict by another expression (aka semijoin and antijoin) + check_compatibility = True + if isinstance(condition, PromiscuousOperand): + condition = condition.operand + check_compatibility = False + + if isinstance(condition, QueryExpression): + if check_compatibility: + assert_join_compatibility(query_expression, condition) + common_attributes = [q for q in condition.heading.names if q in query_expression.heading.names] + columns.update(common_attributes) + if isinstance(condition, Aggregation): + condition = condition.make_subquery() + return ( + # without common attributes, any non-empty set matches everything + (not negate if condition else negate) if not common_attributes + else '({fields}) {not_}in ({subquery})'.format( + fields='`' + '`,`'.join(common_attributes) + '`', + not_="not " if negate else "", + subquery=condition.make_sql(common_attributes))) + + # restrict by pandas.DataFrames + if isinstance(condition, pandas.DataFrame): + condition = condition.to_records() # convert to numpy.recarray and move on + + # if iterable (but not a string, a QueryExpression, or an AndList), treat as an OrList + try: + or_list = [make_condition(query_expression, q, columns) for q in condition] + except TypeError: + raise DataJointError('Invalid restriction type %r' % condition) + else: + or_list = [item for item in or_list if item is not False] # ignore all False conditions + if any(item is True for item in or_list): # if any item is True, the whole thing is True + return not negate + return template % ('(%s)' % ' OR '.join(or_list)) if or_list else negate # an empty or list is False + + +def extract_column_names(sql_expression): + """ + extract all presumed column names from an sql expression such as the WHERE clause, for example. + :param sql_expression: a string containing an SQL expression + :return: set of extracted column names + This may be MySQL-specific for now. + """ + assert isinstance(sql_expression, str) + result = set() + s = sql_expression # for terseness + # remove escaped quotes + s = re.sub(r'(\\\")|(\\\')', '', s) + # remove quoted text + s = re.sub(r"'[^']*'", "", s) + s = re.sub(r'"[^"]*"', '', s) + # find all tokens in back quotes and remove them + result.update(re.findall(r"`([a-z][a-z_0-9]*)`", s)) + s = re.sub(r"`[a-z][a-z_0-9]*`", '', s) + # remove space before parentheses + s = re.sub(r"\s*\(", "(", s) + # remove tokens followed by ( since they must be functions + s = re.sub(r"(\b[a-z][a-z_0-9]*)\(", "(", s) + remaining_tokens = set(re.findall(r"\b[a-z][a-z_0-9]*\b", s)) + # update result removing reserved words + result.update(remaining_tokens - {"is", "in", "between", "like", "and", "or", "null", "not"}) + return result diff --git a/datajoint/connection.py b/datajoint/connection.py index 28375d586..14e457d0b 100644 --- a/datajoint/connection.py +++ b/datajoint/connection.py @@ -7,10 +7,14 @@ import pymysql as client import logging from getpass import getpass +import re +import pathlib from .settings import config from . import errors from .dependencies import Dependencies +from .blob import pack, unpack +from .hash import uuid_from_buffer from .plugin import connection_plugins logger = logging.getLogger(__name__) @@ -65,6 +69,8 @@ def translate_query_error(client_error, query): # Integrity errors if err == 1062: return errors.DuplicateError(*args) + if err == 1451: + return errors.IntegrityError(*args) if err == 1452: return errors.IntegrityError(*args) # Syntax errors @@ -116,6 +122,29 @@ def conn(host=None, user=None, password=None, *, init_fun=None, reset=False, use return conn.connection +class EmulatedCursor: + """acts like a cursor""" + def __init__(self, data): + self._data = data + self._iter = iter(self._data) + + def __iter__(self): + return self + + def __next__(self): + return next(self._iter) + + def fetchall(self): + return self._data + + def fetchone(self): + return next(self._iter) + + @property + def rowcount(self): + return len(self._data) + + class Connection: """ A dj.Connection object manages a connection to a database server. @@ -147,12 +176,13 @@ def __init__(self, host, user, password, port=None, init_fun=None, use_tls=None, self.init_fun = init_fun print("Connecting {user}@{host}:{port}".format(**self.conn_info)) self._conn = None + self._query_cache = None connect_host_hook(self) if self.is_connected: logger.info("Connected {user}@{host}:{port}".format(**self.conn_info)) self.connection_id = self.query('SELECT connection_id()').fetchone()[0] else: - raise errors.ConnectionError('Connection failed.') + raise errors.LostConnectionError('Connection failed.') self._in_transaction = False self.schemas = dict() self.dependencies = Dependencies(self) @@ -166,9 +196,7 @@ def __repr__(self): connected=connected, **self.conn_info) def connect(self): - """ - Connects to the database server. - """ + """ Connect to the database server.""" with warnings.catch_warnings(): warnings.filterwarnings('ignore', '.*deprecated.*') try: @@ -190,6 +218,16 @@ def connect(self): k == 'ssl' and self.conn_info['ssl_input'] is None)}) self._conn.autocommit(True) + def set_query_cache(self, query_cache): + """ + When query_cache is not None, the connection switches into the query caching mode, which entails: + 1. Only SELECT queries are allowed. + 2. The results of queries are cached under the path indicated by dj.config['query_cache'] + 3. query_cache is a string that differentiates different cache states. + :param query_cache: a string to initialize the hash for query results + """ + self._query_cache = query_cache + def close(self): self._conn.close() @@ -198,16 +236,12 @@ def register(self, schema): self.dependencies.clear() def ping(self): - """ - Pings the connection. Raises an exception if the connection is closed. - """ + """ Ping the connection or raises an exception if the connection is closed. """ self._conn.ping(reconnect=False) @property def is_connected(self): - """ - Returns true if the object is connected to the database server. - """ + """ Return true if the object is connected to the database server. """ try: self.ping() except: @@ -215,7 +249,7 @@ def is_connected(self): return True @staticmethod - def _execute_query(cursor, query, args, cursor_class, suppress_warnings): + def _execute_query(cursor, query, args, suppress_warnings): try: with warnings.catch_warnings(): if suppress_warnings: @@ -235,13 +269,29 @@ def query(self, query, args=(), *, as_dict=False, suppress_warnings=True, reconn :param suppress_warnings: If True, suppress all warnings arising from underlying query library :param reconnect: when None, get from config, when True, attempt to reconnect if disconnected """ + # check cache first: + use_query_cache = bool(self._query_cache) + if use_query_cache and not re.match(r"\s*(SELECT|SHOW)", query): + raise errors.DataJointError("Only SELECT query are allowed when query caching is on.") + if use_query_cache: + if not config['query_cache']: + raise errors.DataJointError("Provide filepath dj.config['query_cache'] when using query caching.") + hash_ = uuid_from_buffer((str(self._query_cache) + re.sub(r'`\$\w+`', '', query)).encode() + pack(args)) + cache_path = pathlib.Path(config['query_cache']) / str(hash_) + try: + buffer = cache_path.read_bytes() + except FileNotFoundError: + pass # proceed to query the database + else: + return EmulatedCursor(unpack(buffer)) + if reconnect is None: reconnect = config['database.reconnect'] logger.debug("Executing SQL:" + query[:query_log_max_length]) cursor_class = client.cursors.DictCursor if as_dict else client.cursors.Cursor cursor = self._conn.cursor(cursor=cursor_class) try: - self._execute_query(cursor, query, args, cursor_class, suppress_warnings) + self._execute_query(cursor, query, args, suppress_warnings) except errors.LostConnectionError: if not reconnect: raise @@ -252,7 +302,13 @@ def query(self, query, args=(), *, as_dict=False, suppress_warnings=True, reconn raise errors.LostConnectionError("Connection was lost during a transaction.") logger.debug("Re-executing") cursor = self._conn.cursor(cursor=cursor_class) - self._execute_query(cursor, query, args, cursor_class, suppress_warnings) + self._execute_query(cursor, query, args, suppress_warnings) + + if use_query_cache: + data = cursor.fetchall() + cache_path.write_bytes(pack(data)) + return EmulatedCursor(data) + return cursor def get_user(self): diff --git a/datajoint/declare.py b/datajoint/declare.py index 1423dba15..e94c1bfef 100644 --- a/datajoint/declare.py +++ b/datajoint/declare.py @@ -5,11 +5,10 @@ import re import pyparsing as pp import logging +import warnings from .errors import DataJointError, _support_filepath_types, FILEPATH_FEATURE_SWITCH from .attribute_adapter import get_adapter -from .utils import OrderedDict - UUID_DATA_TYPE = 'binary(16)' MAX_TABLE_NAME_LENGTH = 64 CONSTANT_LITERALS = {'CURRENT_TIMESTAMP'} # SQL literals to be used without quotes (case insensitive) @@ -52,7 +51,7 @@ def match_type(attribute_type): def build_foreign_key_parser_old(): - # old-style foreign key parser. Superceded by expression-based syntax. See issue #436 + # old-style foreign key parser. Superseded by expression-based syntax. See issue #436 # This will be deprecated in a future release. left = pp.Literal('(').suppress() right = pp.Literal(')').suppress() @@ -126,7 +125,7 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig """ # Parse and validate from .table import Table - from .expression import Projection + from .expression import QueryExpression obsolete = False # See issue #436. Old style to be deprecated in a future release try: @@ -153,15 +152,18 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig raise DataJointError('Primary dependencies cannot be nullable in line "{line}"'.format(line=line)) if obsolete: + warnings.warn( + 'Line "{line}" uses obsolete syntax that will no longer be supported in datajoint 0.14. ' + 'For details, see issue #780 https://github.com/datajoint/datajoint-python/issues/780'.format(line=line)) if not isinstance(ref, type) or not issubclass(ref, Table): raise DataJointError('Foreign key reference %r must be a valid query' % result.ref_table) if isinstance(ref, type) and issubclass(ref, Table): ref = ref() - # check that dependency is of supported type - if (not isinstance(ref, (Table, Projection)) or len(ref.restriction) or - (isinstance(ref, Projection) and (not isinstance(ref._arg, Table) or len(ref._arg.restriction)))): + # check that dependency is of a supported type + if (not isinstance(ref, QueryExpression) or len(ref.restriction) or + len(ref.support) != 1 or not isinstance(ref.support[0], str)): raise DataJointError('Dependency "%s" is not supported (yet). Use a base table or its projection.' % result.ref_table) @@ -202,21 +204,20 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig ref = ref.proj(**dict(zip(new_attrs, ref_attrs))) # declare new foreign key attributes - base = ref._arg if isinstance(ref, Projection) else ref # base reference table - for attr, ref_attr in zip(ref.primary_key, base.primary_key): + for attr in ref.primary_key: if attr not in attributes: attributes.append(attr) if primary_key is not None: primary_key.append(attr) attr_sql.append( - base.heading[ref_attr].sql.replace(ref_attr, attr, 1).replace('NOT NULL ', '', int(is_nullable))) + ref.heading[attr].sql.replace('NOT NULL ', '', int(is_nullable))) # declare the foreign key foreign_key_sql.append( 'FOREIGN KEY (`{fk}`) REFERENCES {ref} (`{pk}`) ON UPDATE CASCADE ON DELETE RESTRICT'.format( fk='`,`'.join(ref.primary_key), - pk='`,`'.join(base.primary_key), - ref=base.full_table_name)) + pk='`,`'.join(ref.heading[name].original_name for name in ref.primary_key), + ref=ref.support[0])) # declare unique index if is_unique: @@ -300,7 +301,7 @@ def _make_attribute_alter(new, old, primary_key): name_regexp = re.compile(r"^`(?P\w+)`") original_regexp = re.compile(r'COMMENT "{\s*(?P\w+)\s*}') matched = ((name_regexp.match(d), original_regexp.search(d)) for d in new) - new_names = OrderedDict((d.group('name'), n and n.group('name')) for d, n in matched) + new_names = dict((d.group('name'), n and n.group('name')) for d, n in matched) old_names = [name_regexp.search(d).group('name') for d in old] # verify that original names are only used once diff --git a/datajoint/dependencies.py b/datajoint/dependencies.py index a4e1afb0c..c1a331b7d 100644 --- a/datajoint/dependencies.py +++ b/datajoint/dependencies.py @@ -1,7 +1,7 @@ import networkx as nx import itertools import re -from collections import defaultdict, OrderedDict +from collections import defaultdict from .errors import DataJointError @@ -86,7 +86,7 @@ def load(self, force=True): WHERE referenced_table_name NOT LIKE "~%%" AND (referenced_table_schema in ('{schemas}') OR referenced_table_schema is not NULL AND table_schema in ('{schemas}')) """.format(schemas="','".join(self._conn.schemas)), as_dict=True)) - fks = defaultdict(lambda: dict(attr_map=OrderedDict())) + fks = defaultdict(lambda: dict(attr_map=dict())) for key in keys: d = fks[(key['constraint_name'], key['referencing_table'], key['referenced_table'])] d['referencing_table'] = key['referencing_table'] diff --git a/datajoint/diagram.py b/datajoint/diagram.py index 41ead0712..dd48e7b17 100644 --- a/datajoint/diagram.py +++ b/datajoint/diagram.py @@ -145,7 +145,7 @@ def is_part(part, master): """ :param part: `database`.`table_name` :param master: `database`.`table_name` - :return: True if part is part of master, + :return: True if part is part of master. """ part = [s.strip('`') for s in part.split('.')] master = [s.strip('`') for s in master.split('.')] diff --git a/datajoint/expression.py b/datajoint/expression.py index 4e4924fa9..9b7d10544 100644 --- a/datajoint/expression.py +++ b/datajoint/expression.py @@ -1,313 +1,130 @@ -import collections from itertools import count import logging import inspect -import numpy as np +import copy import re -import datetime -import decimal -import pandas -import uuid -import binascii # for Python 3.4 compatibility from .settings import config from .errors import DataJointError from .fetch import Fetch, Fetch1 +from .preview import preview, repr_html +from .condition import AndList, Not, \ + make_condition, assert_join_compatibility, extract_column_names, PromiscuousOperand logger = logging.getLogger(__name__) -def assert_join_compatibility(rel1, rel2): - """ - Determine if expressions rel1 and rel2 are join-compatible. To be join-compatible, the matching attributes - in the two expressions must be in the primary key of one or the other expression. - Raises an exception if not compatible. - :param rel1: A QueryExpression object - :param rel2: A QueryExpression object - """ - for rel in (rel1, rel2): - if not isinstance(rel, (U, QueryExpression)): - raise DataJointError('Object %r is not a QueryExpression and cannot be joined.' % rel) - if not isinstance(rel1, U) and not isinstance(rel2, U): # dj.U is always compatible - try: - raise DataJointError("Cannot join query expressions on dependent attribute `%s`" % next(r for r in set( - rel1.heading.secondary_attributes).intersection(rel2.heading.secondary_attributes))) - except StopIteration: - pass - - -class AndList(list): - """ - A list of restrictions to by applied to a query expression. The restrictions are AND-ed. - Each restriction can be a list or set or a query expression whose elements are OR-ed. - But the elements that are lists can contain other AndLists. - - Example: - rel2 = rel & dj.AndList((cond1, cond2, cond3)) - is equivalent to - rel2 = rel & cond1 & cond2 & cond3 +class QueryExpression: """ + QueryExpression implements query operators to derive new entity set from its input. + A QueryExpression object generates a SELECT statement in SQL. + QueryExpression operators are restrict, join, proj, aggr, and union. - def append(self, restriction): - if isinstance(restriction, AndList): - # extend to reduce nesting - self.extend(restriction) - else: - super().append(restriction) - + A QueryExpression object has a support, a restriction (an AndList), and heading. + Property `heading` (type dj.Heading) contains information about the attributes. + It is loaded from the database and updated by proj. -def is_true(restriction): - return restriction is True or isinstance(restriction, AndList) and not len(restriction) + Property `support` is the list of table names or other QueryExpressions to be joined. + The restriction is applied first without having access to the attributes generated by the projection. + Then projection is applied by selecting modifying the heading attribute. -class QueryExpression: + Application of operators does not always lead to the creation of a subquery. + A subquery is generated when: + 1. A restriction is applied on any computed or renamed attributes + 2. A projection is applied remapping remapped attributes + 3. Subclasses: Join, Aggregation, and Union have additional specific rules. """ - QueryExpression implements query operators to derive new entity sets from its inputs. - When fetching data from the database, the expression is compiled into an SQL expression. - QueryExpression operators are restrict, join, proj, aggr, and union. - """ - - def __init__(self, arg=None): - if arg is None: # initialize - # initialize - self._restriction = AndList() - self._distinct = False - self._heading = None - else: # copy - assert isinstance(arg, QueryExpression), 'Cannot make QueryExpression from %s' % arg.__class__.__name__ - self._restriction = AndList(arg._restriction) - self._distinct = arg.distinct - self._heading = arg._heading + _restriction = None + _restriction_attributes = None + _left = [] # True for left joins, False for inner joins + _original_heading = None # heading before projections - @classmethod - def create(cls): # pragma: no cover - """abstract method for creating an instance""" - assert False, "Abstract method `create` must be overridden in subclass." + # subclasses or instantiators must provide values + _connection = None + _heading = None + _support = None @property def connection(self): - """ - :return: the dj.Connection object - """ + """ a dj.Connection object """ + assert self._connection is not None return self._connection + @property + def support(self): + """ A list of table names or subqueries to from the FROM clause """ + assert self._support is not None + return self._support + @property def heading(self): - """ - :return: the dj.Heading object for the query expression - """ + """ a dj.Heading object, reflects the effects of the projection operator .proj """ return self._heading @property - def distinct(self): - """ - :return: True if the DISTINCT modifier is required to make valid result - """ - return self._distinct + def original_heading(self): + """ a dj.Heading object reflecting the attributes before projection """ + return self._original_heading or self.heading @property def restriction(self): - """ - :return: The AndList of restrictions applied to input to produce the result. - """ - assert isinstance(self._restriction, AndList) + """ a AndList object of restrictions applied to input to produce the result """ + if self._restriction is None: + self._restriction = AndList() return self._restriction + @property + def restriction_attributes(self): + """ the set of attribute names invoked in the WHERE clause """ + if self._restriction_attributes is None: + self._restriction_attributes = set() + return self._restriction_attributes + @property def primary_key(self): return self.heading.primary_key - def _make_condition(self, arg): - """ - Translate the input arg into the equivalent SQL condition (a string) - :param arg: any valid restriction object. - :return: an SQL condition string or a boolean value. - """ - def prep_value(k, v): - """prepare value v for inclusion as a string in an SQL condition""" - if self.heading[k].uuid: - if not isinstance(v, uuid.UUID): - try: - v = uuid.UUID(v) - except (AttributeError, ValueError): - raise DataJointError('Badly formed UUID {v} in restriction by `{k}`'.format(k=k, v=v)) - return "X'%s'" % binascii.hexlify(v.bytes).decode() - if isinstance(v, (datetime.date, datetime.datetime, datetime.time, decimal.Decimal)): - return '"%s"' % v - return '%r' % v - - negate = False - while isinstance(arg, Not): - negate = not negate - arg = arg.restriction - template = "NOT (%s)" if negate else "%s" - - # restrict by string - if isinstance(arg, str): - return template % arg.strip().replace("%", "%%") # escape % in strings, see issue #376 - - # restrict by AndList - if isinstance(arg, AndList): - # omit all conditions that evaluate to True - items = [item for item in (self._make_condition(i) for i in arg) if item is not True] - if any(item is False for item in items): - return negate # if any item is False, the whole thing is False - if not items: - return not negate # and empty AndList is True - return template % ('(' + ') AND ('.join(items) + ')') - - # restriction by dj.U evaluates to True - if isinstance(arg, U): - return not negate - - # restrict by boolean - if isinstance(arg, bool): - return negate != arg - - # restrict by a mapping such as a dict -- convert to an AndList of string equality conditions - if isinstance(arg, collections.abc.Mapping): - return template % self._make_condition( - AndList('`%s`=%s' % (k, prep_value(k, v)) for k, v in arg.items() if k in self.heading)) - - # restrict by a numpy record -- convert to an AndList of string equality conditions - if isinstance(arg, np.void): - return template % self._make_condition( - AndList(('`%s`=%s' % (k, prep_value(k, arg[k])) for k in arg.dtype.fields if k in self.heading))) - - # restrict by a QueryExpression subclass -- triggers instantiation - if inspect.isclass(arg) and issubclass(arg, QueryExpression): - arg = arg() - - # restrict by another expression (aka semijoin and antijoin) - if isinstance(arg, QueryExpression): - assert_join_compatibility(self, arg) - common_attributes = [q for q in arg.heading.names if q in self.heading.names] - return ( - # without common attributes, any non-empty set matches everything - (not negate if arg else negate) if not common_attributes - else '({fields}) {not_}in ({subquery})'.format( - fields='`' + '`,`'.join(common_attributes) + '`', - not_="not " if negate else "", - subquery=arg.make_sql(common_attributes))) - - # restrict by pandas.DataFrames - if isinstance(arg, pandas.DataFrame): - arg = arg.to_records() # convert to np.recarray - - # if iterable (but not a string, a QueryExpression, or an AndList), treat as an OrList - try: - or_list = [self._make_condition(q) for q in arg] - except TypeError: - raise DataJointError('Invalid restriction type %r' % arg) - else: - or_list = [item for item in or_list if item is not False] # ignore all False conditions - if any(item is True for item in or_list): # if any item is True, the whole thing is True - return not negate - return template % ('(%s)' % ' OR '.join(or_list)) if or_list else negate # an empty or list is False + _subquery_alias_count = count() # count for alias names used in from_clause + + def from_clause(self): + support = ('(' + src.make_sql() + ') as `_s%x`' % next( + self._subquery_alias_count) if isinstance(src, QueryExpression) else src for src in self.support) + clause = next(support) + for s, left in zip(support, self._left): + clause += 'NATURAL{left} JOIN {clause}'.format( + left=" LEFT" if left else "", + clause=s) + return clause - @property def where_clause(self): - """ - convert self.restriction to the SQL WHERE clause - """ - cond = self._make_condition(self.restriction) - return '' if cond is True else ' WHERE %s' % cond + return '' if not self.restriction else ' WHERE(%s)' % ')AND('.join( + str(s) for s in self.restriction) - def get_select_fields(self, select_fields=None): + def make_sql(self, fields=None): """ - :return: string specifying the attributes to return + Make the SQL SELECT statement. + :param fields: used to explicitly set the select attributes """ - return self.heading.as_sql if select_fields is None else self.heading.project(select_fields).as_sql + distinct = self.heading.names == self.primary_key + return 'SELECT {distinct}{fields} FROM {from_}{where}'.format( + distinct="DISTINCT " if distinct else "", + fields=self.heading.as_sql(fields or self.heading.names), + from_=self.from_clause(), where=self.where_clause()) # --------- query operators ----------- - - def __mul__(self, other): - """ - natural join of query expressions `self` and `other` - """ - return other * self if isinstance(other, U) else Join.create(self, other) - - def __add__(self, other): - """ - union of two entity sets `self` and `other` - """ - return Union.create(self, other) - - def proj(self, *attributes, **named_attributes): - """ - Projection operator. - :param attributes: attributes to be included in the result. (The primary key is already included). - :param named_attributes: new attributes computed or renamed from existing attributes. - :return: the projected expression. - Primary key attributes cannot be excluded but may be renamed. - Thus self.proj() leaves only the primary key attributes of self. - self.proj(a='id') renames the attribute 'id' into 'a' and includes 'a' in the projection. - self.proj(a='expr') adds a new field a with the value computed with an SQL expression. - self.proj(a='(id)') adds a new computed field named 'a' that has the same value as id - Each attribute can only be used once in attributes or named_attributes. - If the attribute list contains an Ellipsis ..., then all secondary attributes are included - If an entry of the attribute list starts with a dash, e.g. '-attr', then the secondary attribute - attr will be excluded, if already present but ignored if not found. - """ - return Projection.create(self, attributes, named_attributes) - - def aggr(self, group, *attributes, keep_all_rows=False, **named_attributes): - """ - Aggregation/projection operator - :param group: an entity set whose entities will be grouped per entity of `self` - :param attributes: attributes of self to include in the result - :param keep_all_rows: True = preserve the number of elements in the result (equivalent of LEFT JOIN in SQL) - :param named_attributes: renamings and computations on attributes of self and group - :return: an entity set representing the result of the aggregation/projection operator of entities from `group` - per entity of `self` - """ - return GroupBy.create(self, group, keep_all_rows=keep_all_rows, - attributes=attributes, named_attributes=named_attributes) - - aggregate = aggr # aliased name for aggr - - def __iand__(self, restriction): - """ - in-place restriction. - A subquery is created if the argument has renamed attributes. Then the restriction is not in place. - - See QueryExpression.restrict for more detail. - """ - if is_true(restriction): - return self - return (Subquery.create(self) if self.heading.expressions else self).restrict(restriction) - - def __and__(self, restriction): - """ - Restriction operator - :return: a restricted copy of the input argument - See QueryExpression.restrict for more detail. - """ - return (Subquery.create(self) # the HAVING clause in GroupBy can handle renamed attributes but WHERE cannot - if not(is_true(restriction)) and self.heading.expressions and not isinstance(self, GroupBy) - else self.__class__(self)).restrict(restriction) - - def __isub__(self, restriction): - """ - in-place inverted restriction aka antijoin - - See QueryExpression.restrict for more detail. - """ - return self.restrict(Not(restriction)) - - def __sub__(self, restriction): - """ - inverted restriction aka antijoin - :return: a restricted copy of the argument - - See QueryExpression.restrict for more detail. - """ - return self & Not(restriction) + def make_subquery(self): + """ create a new SELECT statement where self is the FROM clause """ + result = QueryExpression() + result._connection = self.connection + result._support = [self] + result._heading = self.heading.make_subquery_heading() + return result def restrict(self, restriction): """ - In-place restriction. Restricts the result to a specified subset of the input. - rel.restrict(restriction) is equivalent to rel = rel & restriction or rel &= restriction - rel.restrict(Not(restriction)) is equivalent to rel = rel - restriction or rel -= restriction + Produces a new expression with the new restriction applied. + rel.restrict(restriction) is equivalent to rel & restriction. + rel.restrict(Not(restriction)) is equivalent to rel - restriction The primary key of the result is unaffected. Successive restrictions are combined as logical AND: r & a & b is equivalent to r & AndList((a, b)) Any QueryExpression, collection, or sequence other than an AndList are treated as OrLists @@ -348,11 +165,236 @@ def restrict(self, restriction): :param restriction: a sequence or an array (treated as OR list), another QueryExpression, an SQL condition string, or an AndList. """ - assert is_true(restriction) or not self.heading.expressions or isinstance(self, GroupBy), \ - "Cannot restrict a projection with renamed attributes in place." - self.restriction.append(restriction) - return self + attributes = set() + new_condition = make_condition(self, restriction, attributes) + if new_condition is True: + return self # restriction has no effect, return the same object + # check that all attributes in condition are present in the query + try: + raise DataJointError("Attribute `%s` is not found in query." % next( + attr for attr in attributes if attr not in self.heading.names)) + except StopIteration: + pass # all ok + # If the new condition uses any new attributes, a subquery is required. + # However, Aggregation's HAVING statement works fine with aliased attributes. + need_subquery = isinstance(self, Union) or ( + not isinstance(self, Aggregation) and self.heading.new_attributes) + if need_subquery: + result = self.make_subquery() + else: + result = copy.copy(self) + result._restriction = AndList(self.restriction) # copy to preserve the original + result.restriction.append(new_condition) + result.restriction_attributes.update(attributes) + return result + def restrict_in_place(self, restriction): + self.__dict__.update(self.restrict(restriction).__dict__) + + def __and__(self, restriction): + """ + Restriction operator + :return: a restricted copy of the input argument + See QueryExpression.restrict for more detail. + """ + return self.restrict(restriction) + + def __xor__(self, restriction): + """ + Restriction operator ignoring compatibility check. + """ + if inspect.isclass(restriction) and issubclass(restriction, QueryExpression): + restriction = restriction() + if isinstance(restriction, Not): + return self.restrict(Not(PromiscuousOperand(restriction.restriction))) + return self.restrict(PromiscuousOperand(restriction)) + + def __sub__(self, restriction): + """ + Inverted restriction + :return: a restricted copy of the input argument + See QueryExpression.restrict for more detail. + """ + return self.restrict(Not(restriction)) + + def __neg__(self): + if isinstance(self, Not): + return self.restriction + return Not(self) + + def __mul__(self, other): + """ join of query expressions `self` and `other` """ + return self.join(other) + + def __matmul__(self, other): + if inspect.isclass(other) and issubclass(other, QueryExpression): + other = other() # instantiate + return self.join(other, semantic_check=False) + + def join(self, other, semantic_check=True, left=False): + """ + create the joined QueryExpression. + a * b is short for A.join(B) + a @ b is short for A.join(B, semantic_check=False) + Additionally, left=True will retain the rows of self, effectively performing a left join. + """ + # trigger subqueries if joining on renamed attributes + if isinstance(other, U): + return other * self + if inspect.isclass(other) and issubclass(other, QueryExpression): + other = other() # instantiate + if not isinstance(other, QueryExpression): + raise DataJointError("The argument of join must be a QueryExpression") + if semantic_check: + assert_join_compatibility(self, other) + join_attributes = set(n for n in self.heading.names if n in other.heading.names) + # needs subquery if FROM class has common attributes with the other's FROM clause + need_subquery1 = need_subquery2 = bool( + (set(self.original_heading.names) & set(other.original_heading.names)) + - join_attributes) + # need subquery if any of the join attributes are derived + need_subquery1 = need_subquery1 or any(n in self.heading.new_attributes for n in join_attributes) + need_subquery2 = need_subquery2 or any(n in other.heading.new_attributes for n in join_attributes) + if need_subquery1: + self = self.make_subquery() + if need_subquery2: + other = other.make_subquery() + result = QueryExpression() + result._connection = self.connection + result._support = self.support + other.support + result._left = self._left + [left] + other._left + result._heading = self.heading.join(other.heading) + result._restriction = AndList(self.restriction) + result._restriction.append(other.restriction) + result._original_heading = self.original_heading.join(other.original_heading) + assert len(result.support) == len(result._left) + 1 + return result + + def __add__(self, other): + """union""" + return Union.create(self, other) + + def proj(self, *attributes, **named_attributes): + """ + Projection operator. + :param attributes: attributes to be included in the result. (The primary key is already included). + :param named_attributes: new attributes computed or renamed from existing attributes. + :return: the projected expression. + Primary key attributes cannot be excluded but may be renamed. + If the attribute list contains an Ellipsis ..., then all secondary attributes are included too + Prefixing an attribute name with a dash '-attr' removes the attribute from the list if present. + Keyword arguments can be used to rename attributes as in name='attr', duplicate them as in name='(attr)', or + self.proj(...) or self.proj(Ellipsis) -- include all attributes (return self) + self.proj() -- include only primary key + self.proj('attr1', 'attr2') -- include primary key and attributes attr1 and attr2 + self.proj(..., '-attr1', '-attr2') -- include attributes except attr1 and attr2 + self.proj(name1='attr1') -- include primary key and 'attr1' renamed as name1 + self.proj('attr1', dup='(attr1)') -- include primary key and attribute attr1 twice, with the duplicate 'dup' + self.proj(k='abs(attr1)') adds the new attribute k with the value computed as an expression (SQL syntax) + from other attributes available before the projection. + Each attribute name can only be used once. + """ + # new attributes in parentheses are included again with the new name without removing original + duplication_pattern = re.compile(r'\s*\(\s*(?P[a-z][a-z_0-9]*)\s*\)\s*$') + # attributes without parentheses renamed + rename_pattern = re.compile(r'\s*(?P[a-z][a-z_0-9]*)\s*$') + replicate_map = {k: m.group('name') + for k, m in ((k, duplication_pattern.match(v)) for k, v in named_attributes.items()) if m} + rename_map = {k: m.group('name') + for k, m in ((k, rename_pattern.match(v)) for k, v in named_attributes.items()) if m} + compute_map = {k: v for k, v in named_attributes.items() + if not duplication_pattern.match(v) and not rename_pattern.match(v)} + attributes = set(attributes) + # include primary key + attributes.update((k for k in self.primary_key if k not in rename_map.values())) + # include all secondary attributes with Ellipsis + if Ellipsis in attributes: + attributes.discard(Ellipsis) + attributes.update((a for a in self.heading.secondary_attributes + if a not in attributes and a not in rename_map.values())) + try: + raise DataJointError("%s is not a valid data type for an attribute in .proj" % next( + a for a in attributes if not isinstance(a, str))) + except StopIteration: + pass # normal case + # remove excluded attributes, specified as `-attr' + excluded = set(a for a in attributes if a.strip().startswith('-')) + attributes.difference_update(excluded) + excluded = set(a.lstrip('-').strip() for a in excluded) + attributes.difference_update(excluded) + try: + raise DataJointError("Cannot exclude primary key attribute %s", next( + a for a in excluded if a in self.primary_key)) + except StopIteration: + pass # all ok + # check that all attributes exist in heading + try: + raise DataJointError( + 'Attribute `%s` not found.' % next(a for a in attributes if a not in self.heading.names)) + except StopIteration: + pass # all ok + + # check that all mentioned names are present in heading + mentions = attributes.union(replicate_map.values()).union(rename_map.values()) + try: + raise DataJointError("Attribute '%s' not found." % next(a for a in mentions if not self.heading.names)) + except StopIteration: + pass # all ok + + # check that newly created attributes do not clash with any other selected attributes + try: + raise DataJointError("Attribute `%s` already exists" % next( + a for a in rename_map if a in attributes.union(compute_map).union(replicate_map))) + except StopIteration: + pass # all ok + try: + raise DataJointError("Attribute `%s` already exists" % next( + a for a in compute_map if a in attributes.union(rename_map).union(replicate_map))) + except StopIteration: + pass # all ok + try: + raise DataJointError("Attribute `%s` already exists" % next( + a for a in replicate_map if a in attributes.union(rename_map).union(compute_map))) + except StopIteration: + pass # all ok + + # need a subquery if the projection remaps any remapped attributes + used = set(q for v in compute_map.values() for q in extract_column_names(v)) + used.update(rename_map.values()) + used.update(replicate_map.values()) + used.intersection_update(self.heading.names) + need_subquery = isinstance(self, Union) or any( + self.heading[name].attribute_expression is not None for name in used) + if not need_subquery and self.restriction: + # need a subquery if the restriction applies to attributes that have been renamed + need_subquery = any(name in self.restriction_attributes for name in self.heading.new_attributes) + + result = self.make_subquery() if need_subquery else copy.copy(self) + result._original_heading = result.original_heading + result._heading = result.heading.select( + attributes, rename_map=dict(**rename_map, **replicate_map), compute_map=compute_map) + return result + + def aggr(self, group, *attributes, keep_all_rows=False, **named_attributes): + """ + Aggregation of the type U('attr1','attr2').aggr(group, computation="QueryExpression") + has the primary key ('attr1','attr2') and performs aggregation computations for all matching elements of `group`. + :param group: The query expression to be aggregated. + :param keep_all_rows: True=keep all the rows from self. False=keep only rows that match entries in group. + :param named_attributes: computations of the form new_attribute="sql expression on attributes of group" + :return: The derived query expression + """ + if Ellipsis in attributes: + # expand ellipsis to include only attributes from the left table + attributes = set(attributes) + attributes.discard(Ellipsis) + attributes.update(self.heading.secondary_attributes) + return Aggregation.create( + self, group=group, keep_all_rows=keep_all_rows).proj(*attributes, **named_attributes) + + aggregate = aggr # alias for aggr + + # ---------- Fetch operators -------------------- @property def fetch1(self): return Fetch1(self) @@ -381,158 +423,29 @@ def tail(self, limit=25, **fetch_kwargs): """ return self.fetch(order_by="KEY DESC", limit=limit, **fetch_kwargs)[::-1] - def attributes_in_restriction(self): - """ - :return: list of attributes that are probably used in the restriction. - The function errs on the side of false positives. - For example, if the restriction is "val='id'", then the attribute 'id' would be flagged. - This is used internally for optimizing SQL statements. - """ - return set(name for name in self.heading.names - if re.search(r'\b' + name + r'\b', self.where_clause)) - - def __repr__(self): - return super().__repr__() if config['loglevel'].lower() == 'debug' else self.preview() - - def preview(self, limit=None, width=None): - """ - returns a preview of the contents of the query. - """ - heading = self.heading - rel = self.proj(*heading.non_blobs) - if limit is None: - limit = config['display.limit'] - if width is None: - width = config['display.width'] - tuples = rel.fetch(limit=limit+1, format="array") - has_more = len(tuples) > limit - tuples = tuples[:limit] - columns = heading.names - widths = {f: min(max([len(f)] + - [len(str(e)) for e in tuples[f]] if f in tuples.dtype.names else - [len('=BLOB=')]) + 4, width) for f in columns} - templates = {f: '%%-%d.%ds' % (widths[f], widths[f]) for f in columns} - return ( - ' '.join([templates[f] % ('*' + f if f in rel.primary_key else f) for f in columns]) + '\n' + - ' '.join(['+' + '-' * (widths[column] - 2) + '+' for column in columns]) + '\n' + - '\n'.join(' '.join(templates[f] % (tup[f] if f in tup.dtype.names else '=BLOB=') - for f in columns) for tup in tuples) + - ('\n ...\n' if has_more else '\n') + - (' (Total: %d)\n' % len(rel) if config['display.show_tuple_count'] else '')) - - def _repr_html_(self): - heading = self.heading - rel = self.proj(*heading.non_blobs) - info = heading.table_info - tuples = rel.fetch(limit=config['display.limit']+1, format='array') - has_more = len(tuples) > config['display.limit'] - tuples = tuples[0:config['display.limit']] - - css = """ - - """ - head_template = """
-

{column}

- {comment} -
""" - return """ - {css} - {title} -
- - - {body} -
{head}
- {ellipsis} - {count}
- """.format( - css=css, - title="" if info is None else "%s" % info['comment'], - head=''.join( - head_template.format(column=c, comment=heading.attributes[c].comment, - primary='primary' if c in self.primary_key else 'nonprimary') for c in - heading.names), - ellipsis='

...

' if has_more else '', - body=''.join( - ['\n'.join(['%s' % (tup[name] if name in tup.dtype.names else '=BLOB=') - for name in heading.names]) - for tup in tuples]), - count=('

Total: %d

' % len(rel)) if config['display.show_tuple_count'] else '') - - def make_sql(self, select_fields=None): - return 'SELECT {fields} FROM {from_}{where}'.format( - fields=("DISTINCT " if self.distinct else "") + self.get_select_fields(select_fields), - from_=self.from_clause, - where=self.where_clause) - def __len__(self): - """ - number of elements in the result set. - """ + """ :return: number of elements in the result set """ return self.connection.query( - 'SELECT count({count}) FROM {from_}{where}'.format( - count='DISTINCT `{pk}`'.format(pk='`,`'.join(self.primary_key)) if self.distinct and self.primary_key else '*', - from_=self.from_clause, - where=self.where_clause)).fetchone()[0] + 'SELECT count(DISTINCT {fields}) FROM {from_}{where}'.format( + fields=self.heading.as_sql(self.primary_key, include_aliases=False), + from_=self.from_clause(), + where=self.where_clause())).fetchone()[0] def __bool__(self): """ - :return: True if the result is not empty. Equivalent to len(rel)>0 but may be more efficient. + :return: True if the result is not empty. Equivalent to len(self) > 0 but often faster. """ - return len(self) > 0 + return bool(self.connection.query( + 'SELECT EXISTS(SELECT 1 FROM {from_}{where})'.format( + from_=self.from_clause(), + where=self.where_clause())).fetchone()[0]) def __contains__(self, item): """ returns True if item is found in the . :param item: any restriction - (item in query_expression) is equivalent to bool(query_expression & item) but may be executed more efficiently. + (item in query_expression) is equivalent to bool(query_expression & item) but may be + executed more efficiently. """ return bool(self & item) # May be optimized e.g. using an EXISTS query @@ -546,7 +459,8 @@ def __next__(self): key = self._iter_keys.pop(0) except AttributeError: # self._iter_keys is missing because __iter__ has not been called. - raise TypeError("'QueryExpression' object is not an iterator. Use iter(obj) to create an iterator.") + raise TypeError("A QueryExpression object is not an iterator. " + "Use iter(obj) to create an iterator.") except IndexError: raise StopIteration else: @@ -556,7 +470,8 @@ def __next__(self): try: return (self & key).fetch1() except DataJointError: - # The data may have been deleted since the moment the keys were fetched -- move on to next entry. + # The data may have been deleted since the moment the keys were fetched + # -- move on to next entry. return next(self) def cursor(self, offset=0, limit=None, order_by=None, as_dict=False): @@ -574,282 +489,133 @@ def cursor(self, offset=0, limit=None, order_by=None, as_dict=False): logger.debug(sql) return self.connection.query(sql, as_dict=as_dict) + def __repr__(self): + return super().__repr__() if config['loglevel'].lower() == 'debug' else self.preview() + + def preview(self, limit=None, width=None): + """ :return: a string of preview of the contents of the query. """ + return preview(self, limit, width) -class Not: - """ - invert restriction - """ - def __init__(self, restriction): - self.restriction = restriction + def _repr_html_(self): + """ :return: HTML to display table in Jupyter notebook. """ + return repr_html(self) -class Join(QueryExpression): +class Aggregation(QueryExpression): """ - Join operator. - Join is a private DataJoint class not exposed to users. See QueryExpression.__mul__ for details. + Aggregation.create(arg, group, comp1='calc1', ..., compn='calcn') yields an entity set + with primary key from arg. + The computed arguments comp1, ..., compn use aggregation calculations on the attributes of + group or simple projections and calculations on the attributes of arg. + Aggregation is used QueryExpression.aggr and U.aggr. + Aggregation is a private class in DataJoint, not exposed to users. """ - - def __init__(self, arg=None): - super().__init__(arg) - if arg is not None: - assert isinstance(arg, Join), "Join copy constructor requires a Join object" - self._connection = arg.connection - self._heading = arg.heading - self._arg1 = arg._arg1 - self._arg2 = arg._arg2 - self._left = arg._left + _left_restrict = None # the pre-GROUP BY conditions for the WHERE clause + _subquery_alias_count = count() @classmethod - def create(cls, arg1, arg2, keep_all_rows=False): - obj = cls() - if inspect.isclass(arg2) and issubclass(arg2, QueryExpression): - arg2 = arg2() # instantiate if joining with a class - assert_join_compatibility(arg1, arg2) - if arg1.connection != arg2.connection: - raise DataJointError("Cannot join query expressions from different connections.") - obj._connection = arg1.connection - obj._arg1 = cls.make_argument_subquery(arg1) - obj._arg2 = cls.make_argument_subquery(arg2) - obj._distinct = obj._arg1.distinct or obj._arg2.distinct - obj._left = keep_all_rows - obj._heading = obj._arg1.heading.join(obj._arg2.heading) - obj.restrict(obj._arg1.restriction) - obj.restrict(obj._arg2.restriction) - return obj - - @staticmethod - def make_argument_subquery(arg): - """ - Decide when a Join argument needs to be wrapped in a subquery - """ - return Subquery.create(arg) if isinstance(arg, (GroupBy, Projection)) or arg.restriction else arg + def create(cls, arg, group, keep_all_rows=False): + if inspect.isclass(group) and issubclass(group, QueryExpression): + group = group() # instantiate if a class + assert isinstance(group, QueryExpression) + if keep_all_rows and len(group.support) > 1: + group = group.make_subquery() # subquery if left joining a join + join = arg.join(group, left=keep_all_rows) # reuse the join logic + result = cls() + result._connection = join.connection + result._heading = join.heading.set_primary_key(arg.primary_key) # use left operand's primary key + result._support = join.support + result._left = join._left + result._left_restrict = join.restriction # WHERE clause applied before GROUP BY + result._grouping_attributes = result.primary_key + + return result - @property - def from_clause(self): - return '{from1} NATURAL{left} JOIN {from2}'.format( - from1=self._arg1.from_clause, - left=" LEFT" if self._left else "", - from2=self._arg2.from_clause) + def where_clause(self): + return '' if not self._left_restrict else ' WHERE (%s)' % ')AND('.join( + str(s) for s in self._left_restrict) + + def make_sql(self, fields=None): + fields = self.heading.as_sql(fields or self.heading.names) + assert self._grouping_attributes or not self.restriction + distinct = set(self.heading.names) == set(self.primary_key) + return 'SELECT {distinct}{fields} FROM {from_}{where}{group_by}'.format( + distinct="DISTINCT " if distinct else "", + fields=fields, + from_=self.from_clause(), + where=self.where_clause(), + group_by="" if not self.primary_key else ( + " GROUP BY `%s`" % '`,`'.join(self._grouping_attributes) + + ("" if not self.restriction else ' HAVING (%s)' % ')AND('.join(self.restriction)))) + + def __len__(self): + return self.connection.query( + 'SELECT count(1) FROM ({subquery}) `${alias:x}`'.format( + subquery=self.make_sql(), + alias=next(self._subquery_alias_count))).fetchone()[0] + + def __bool__(self): + return bool(self.connection.query( + 'SELECT EXISTS({sql})'.format(sql=self.make_sql()))) class Union(QueryExpression): """ Union is the private DataJoint class that implements the union operator. """ - - __count = count() - - def __init__(self, arg=None): - super().__init__(arg) - if arg is not None: - assert isinstance(arg, Union), "Union copy constructore requires a Union object" - self._connection = arg.connection - self._heading = arg.heading - self._arg1 = arg._arg1 - self._arg2 = arg._arg2 - @classmethod def create(cls, arg1, arg2): - obj = cls() if inspect.isclass(arg2) and issubclass(arg2, QueryExpression): arg2 = arg2() # instantiate if a class - if not isinstance(arg1, QueryExpression) or not isinstance(arg2, QueryExpression): - raise DataJointError('an QueryExpression can only be unioned with another QueryExpression') + if not isinstance(arg2, QueryExpression): + raise DataJointError( + "A QueryExpression can only be unioned with another QueryExpression") if arg1.connection != arg2.connection: - raise DataJointError("Cannot operate on QueryExpressions originating from different connections.") - if set(arg1.heading.names) != set(arg2.heading.names): - raise DataJointError('Union requires the same attributes in both arguments') - if any(not v.in_key for v in arg1.heading.attributes.values()) or \ - all(not v.in_key for v in arg2.heading.attributes.values()): - raise DataJointError('Union arguments must not have any secondary attributes.') - obj._connection = arg1.connection - obj._heading = arg1.heading - obj._arg1 = arg1 - obj._arg2 = arg2 - return obj - - def make_sql(self, select_fields=None): - return "SELECT {_fields} FROM {_from}{_where}".format( - _fields=self.get_select_fields(select_fields), - _from=self.from_clause, - _where=self.where_clause) + raise DataJointError( + "Cannot operate on QueryExpressions originating from different connections.") + if set(arg1.primary_key) != set(arg2.primary_key): + raise DataJointError("The operands of a union must share the same primary key.") + if set(arg1.heading.secondary_attributes) & set(arg2.heading.secondary_attributes): + raise DataJointError( + "The operands of a union must not share any secondary attributes.") + result = cls() + result._connection = arg1.connection + result._heading = arg1.heading.join(arg2.heading) + result._support = [arg1, arg2] + return result + + def make_sql(self): + arg1, arg2 = self._support + if not arg1.heading.secondary_attributes and not arg2.heading.secondary_attributes: + # no secondary attributes: use UNION DISTINCT + fields = arg1.primary_key + return "({sql1}) UNION ({sql2})".format( + sql1=arg1.make_sql(fields), + sql2=arg2.make_sql(fields)) + # with secondary attributes, use union of left join with antijoin + fields = self.heading.names + sql1 = arg1.join(arg2, left=True).make_sql(fields) + sql2 = (arg2 - arg1).proj( + ..., **{k: 'NULL' for k in arg1.heading.secondary_attributes}).make_sql(fields) + return "({sql1}) UNION ({sql2})".format(sql1=sql1, sql2=sql2) - @property def from_clause(self): - return ("(SELECT {fields} FROM {from1}{where1} UNION SELECT {fields} FROM {from2}{where2}) as `_u%x`".format( - fields=self.get_select_fields(None), from1=self._arg1.from_clause, - where1=self._arg1.where_clause, - from2=self._arg2.from_clause, - where2=self._arg2.where_clause)) % next(self.__count) - - -class Projection(QueryExpression): - """ - Projection is a private DataJoint class that implements the projection operator. - See QueryExpression.proj() for user interface. - """ - - def __init__(self, arg=None): - super().__init__(arg) - if arg is not None: - assert isinstance(arg, Projection), "Projection copy constructor requires a Projection object." - self._connection = arg.connection - self._heading = arg.heading - self._arg = arg._arg - - @staticmethod - def prepare_attribute_lists(arg, attributes, named_attributes): - # check that all attributes are strings - has_ellipsis = Ellipsis in attributes - attributes = [a for a in attributes if a is not Ellipsis] - try: - raise DataJointError("Attribute names must be strings or ..., got %s" % next( - type(a) for a in attributes if not isinstance(a, str))) - except StopIteration: - pass - named_attributes = {k: v.strip() for k, v in named_attributes.items()} # clean up - excluded_attributes = set(a.lstrip('-').strip() for a in attributes if a.startswith('-')) - if has_ellipsis: - included_already = set(named_attributes.values()) - attributes = [a for a in arg.heading.secondary_attributes if a not in included_already] - # process excluded attributes - attributes = [a for a in attributes if a not in excluded_attributes] - return attributes, named_attributes - - @classmethod - def create(cls, arg, attributes, named_attributes, include_primary_key=True): - """ - :param arg: The QueryExpression to be projected - :param attributes: attributes to select - :param named_attributes: new attributes to create by renaming or computing - :param include_primary_key: True if the primary key must be included even if it's not in attributes. - :return: the resulting Projection object - """ - obj = cls() - obj._connection = arg.connection - - if inspect.isclass(arg) and issubclass(arg, QueryExpression): - arg = arg() # instantiate if a class - - attributes, named_attributes = Projection.prepare_attribute_lists(arg, attributes, named_attributes) - obj._distinct = arg.distinct + """ The union does not use a FROM clause """ + assert False - if include_primary_key: # include primary key of the QueryExpression - attributes = (list(a for a in arg.primary_key if a not in named_attributes.values()) + - list(a for a in attributes if a not in arg.primary_key)) - else: - # make distinct if the primary key is not completely selected - obj._distinct = obj._distinct or not set(arg.primary_key).issubset( - set(attributes) | set(named_attributes.values())) - if obj._distinct or cls._need_subquery(arg, attributes, named_attributes): - obj._arg = Subquery.create(arg) - obj._heading = obj._arg.heading.project(attributes, named_attributes) - if not include_primary_key: - obj._heading = obj._heading.extend_primary_key(attributes) - else: - obj._arg = arg - obj._heading = obj._arg.heading.project(attributes, named_attributes) - obj &= arg.restriction # copy restriction when no subquery - return obj - - @staticmethod - def _need_subquery(arg, attributes, named_attributes): - """ - Decide whether the projection argument needs to be wrapped in a subquery - """ - if arg.heading.expressions or arg.distinct: # argument has any renamed (computed) attributes - return True - restricting_attributes = arg.attributes_in_restriction() - return (not restricting_attributes.issubset(attributes) or # if any restricting attribute is projected out or - any(v.strip() in restricting_attributes for v in named_attributes.values())) # or renamed - - @property - def from_clause(self): - return self._arg.from_clause - - -class GroupBy(QueryExpression): - """ - GroupBy(rel, comp1='expr1', ..., compn='exprn') yields an entity set with the primary key specified by rel.heading. - The computed arguments comp1, ..., compn use aggregation operators on the attributes of rel. - GroupBy is used QueryExpression.aggr and U.aggr. - GroupBy is a private class in DataJoint, not exposed to users. - """ - - def __init__(self, arg=None): - super().__init__(arg) - if arg is not None: - # copy constructor - assert isinstance(arg, GroupBy), "GroupBy copy constructor requires a GroupBy object" - self._connection = arg.connection - self._heading = arg.heading - self._arg = arg._arg - self._keep_all_rows = arg._keep_all_rows - - @classmethod - def create(cls, arg, group, attributes, named_attributes, keep_all_rows=False): - if inspect.isclass(group) and issubclass(group, QueryExpression): - group = group() # instantiate if a class - attributes, named_attributes = Projection.prepare_attribute_lists(arg, attributes, named_attributes) - assert_join_compatibility(arg, group) - obj = cls() - obj._keep_all_rows = keep_all_rows - obj._arg = (Join.make_argument_subquery(group) if isinstance(arg, U) - else Join.create(arg, group, keep_all_rows=keep_all_rows)) - obj._connection = obj._arg.connection - # always include primary key of arg - attributes = (list(a for a in arg.primary_key if a not in named_attributes.values()) + - list(a for a in attributes if a not in arg.primary_key)) - obj._heading = obj._arg.heading.project( - attributes, named_attributes, force_primary_key=arg.primary_key) - return obj - - def make_sql(self, select_fields=None): - return 'SELECT {fields} FROM {from_}{where} GROUP BY `{group_by}`{having}'.format( - fields=self.get_select_fields(select_fields), - from_=self._arg.from_clause, - where=self._arg.where_clause, - group_by='`,`'.join(self.primary_key), - having=re.sub(r'^ WHERE', ' HAVING', self.where_clause)) + def where_clause(self): + """ The union does not use a WHERE clause """ + assert False def __len__(self): - return len(Subquery.create(self)) - - -class Subquery(QueryExpression): - """ - A Subquery encapsulates its argument in a SELECT statement, enabling its use as a subquery. - The attribute list and the WHERE clause are resolved. Thus, a subquery no longer has any renamed attributes. - A subquery of a subquery is a just a copy of the subquery with no change in SQL. - """ - __count = count() - - def __init__(self, arg=None): - super().__init__(arg) - if arg is not None: - # copy constructor - assert isinstance(arg, Subquery) - self._connection = arg.connection - self._heading = arg.heading - self._arg = arg._arg - - @classmethod - def create(cls, arg): - """ - construct a subquery from arg - """ - obj = cls() - obj._connection = arg.connection - obj._heading = arg.heading.make_subquery_heading() - obj._arg = arg - return obj - - @property - def from_clause(self): - return '(' + self._arg.make_sql() + ') as `_s%x`' % next(self.__count) + return self.connection.query( + 'SELECT count(1) FROM ({subquery}) `${alias:x}`'.format( + subquery=self.make_sql(), + alias=next(QueryExpression._subquery_alias_count))).fetchone()[0] - def get_select_fields(self, select_fields=None): - return '*' if select_fields is None else self.heading.project(select_fields).as_sql + def __bool__(self): + return bool(self.connection.query( + 'SELECT EXISTS({sql})'.format(sql=self.make_sql()))) class U: @@ -910,28 +676,41 @@ def __init__(self, *primary_key): def primary_key(self): return self._primary_key - def __and__(self, query_expression): - if inspect.isclass(query_expression) and issubclass(query_expression, QueryExpression): - query_expression = query_expression() # instantiate if a class - if not isinstance(query_expression, QueryExpression): + def __and__(self, other): + if inspect.isclass(other) and issubclass(other, QueryExpression): + other = other() # instantiate if a class + if not isinstance(other, QueryExpression): raise DataJointError('Set U can only be restricted with a QueryExpression.') - return Projection.create(query_expression, attributes=self.primary_key, - named_attributes=dict(), include_primary_key=False) + result = copy.copy(other) + result._heading = result.heading.set_primary_key(self.primary_key) + result = result.proj() + return result - def __mul__(self, query_expression): + def join(self, other, left=False): """ Joining U with a query expression has the effect of promoting the attributes of U to the primary key of the other query expression. - :param query_expression: a query expression to join with. + :param other: the other query expression to join with. + :param left: ignored. dj.U always acts as if left=False :return: a copy of the other query expression with the primary key extended. """ - if inspect.isclass(query_expression) and issubclass(query_expression, QueryExpression): - query_expression = query_expression() # instantiate if a class - if not isinstance(query_expression, QueryExpression): + if inspect.isclass(other) and issubclass(other, QueryExpression): + other = other() # instantiate if a class + if not isinstance(other, QueryExpression): raise DataJointError('Set U can only be joined with a QueryExpression.') - copy = query_expression.__class__(query_expression) # invoke copy constructor - copy._heading = copy.heading.extend_primary_key(self.primary_key) - return copy + try: + raise DataJointError( + 'Attribute `%s` not found' % next(k for k in self.primary_key if k not in other.heading.names)) + except StopIteration: + pass # all ok + result = copy.copy(other) + result._heading = result.heading.set_primary_key( + other.primary_key + [k for k in self.primary_key if k not in other.primary_key]) + return result + + def __mul__(self, other): + """ shorthand for join """ + return self.join(other) def aggr(self, group, **named_attributes): """ @@ -941,9 +720,9 @@ def aggr(self, group, **named_attributes): :param named_attributes: computations of the form new_attribute="sql expression on attributes of group" :return: The derived query expression """ - if self.primary_key: - return GroupBy.create( - self, group=group, keep_all_rows=False, attributes=(), named_attributes=named_attributes) - return Projection.create(group, attributes=(), named_attributes=named_attributes, include_primary_key=False) + if named_attributes.get('keep_all_rows', False): + raise DataJointError( + 'Cannot set keep_all_rows=True when aggregating on a universal set.') + return Aggregation.create(self, group=group, keep_all_rows=False).proj(**named_attributes) aggregate = aggr # alias for aggr diff --git a/datajoint/external.py b/datajoint/external.py index f89b753b7..c35a69b91 100644 --- a/datajoint/external.py +++ b/datajoint/external.py @@ -4,7 +4,8 @@ from .settings import config from .errors import DataJointError, MissingExternalFile from .hash import uuid_from_buffer, uuid_from_file -from .table import Table +from .table import Table, FreeTable +from .heading import Heading from .declare import EXTERNAL_TABLE_ROOT from . import s3 from .utils import safe_write, safe_copy @@ -25,24 +26,18 @@ class ExternalTable(Table): The table tracking externally stored objects. Declare as ExternalTable(connection, database) """ - def __init__(self, connection, store=None, database=None): - - # copy constructor -- all QueryExpressions must provide - if isinstance(connection, ExternalTable): - other = connection # the first argument is interpreted as the other object - super().__init__(other) - self.store = other.store - self.spec = other.spec - self.database = other.database - self._connection = other._connection - return - - # nominal constructor - super().__init__() + def __init__(self, connection, store, database): self.store = store self.spec = config.get_store_spec(store) + self._s3 = None self.database = database self._connection = connection + self._heading = Heading(table_info=dict( + conn=connection, + database=database, + table_name=self.table_name, + context=None)) + self._support = [self.full_table_name] if not self.is_declared: self.declare() self._s3 = None @@ -308,7 +303,7 @@ def unused(self): query expression for unused hashes :return: self restricted to elements that are not in use by any tables in the schema """ - return self - ["hash IN (SELECT `{column_name}` FROM {referencing_table})".format(**ref) + return self - [FreeTable(self.connection, ref['referencing_table']).proj(hash=ref['column_name']) for ref in self.references] def used(self): @@ -316,7 +311,7 @@ def used(self): query expression for used hashes :return: self restricted to elements that in use by tables in the schema """ - return self & ["hash IN (SELECT `{column_name}` FROM {referencing_table})".format(**ref) + return self & [FreeTable(self.connection, ref['referencing_table']).proj(hash=ref['column_name']) for ref in self.references] def delete(self, *, delete_external_files=None, limit=None, display_progress=True): diff --git a/datajoint/fetch.py b/datajoint/fetch.py index c2e6649ad..caf17d5ac 100644 --- a/datajoint/fetch.py +++ b/datajoint/fetch.py @@ -10,7 +10,7 @@ from . import blob, hash from .errors import DataJointError from .settings import config -from .utils import OrderedDict, safe_write +from .utils import safe_write class key: @@ -28,7 +28,7 @@ def is_key(attr): def to_dicts(recarray): """convert record array to a dictionaries""" for rec in recarray: - yield OrderedDict(zip(recarray.dtype.names, rec.tolist())) + yield dict(zip(recarray.dtype.names, rec.tolist())) def _get(connection, attr, data, squeeze, download_path): @@ -149,49 +149,56 @@ def __call__(self, *attrs, offset=None, limit=None, order_by=None, format=None, attrs = list(self._expression.primary_key) + [ a for a in attrs if a not in self._expression.primary_key] if as_dict is None: - as_dict = bool(attrs) # default to True for "KEY" and False when fetching entire result + as_dict = bool(attrs) # default to True for "KEY" and False otherwise # format should not be specified with attrs or is_dict=True if format is not None and (as_dict or attrs): raise DataJointError('Cannot specify output format when as_dict=True or ' 'when attributes are selected to be fetched separately.') if format not in {None, "array", "frame"}: - raise DataJointError('Fetch output format must be in {{"array", "frame"}} but "{}" was given'.format(format)) + raise DataJointError( + 'Fetch output format must be in ' + '{{"array", "frame"}} but "{}" was given'.format(format)) if not (attrs or as_dict) and format is None: format = config['fetch_format'] # default to array if format not in {"array", "frame"}: - raise DataJointError('Invalid entry "{}" in datajoint.config["fetch_format"]: use "array" or "frame"'.format( - format)) + raise DataJointError( + 'Invalid entry "{}" in datajoint.config["fetch_format"]: ' + 'use "array" or "frame"'.format(format)) if limit is None and offset is not None: warnings.warn('Offset set, but no limit. Setting limit to a large number. ' 'Consider setting a limit explicitly.') limit = 8000000000 # just a very large number to effect no limit - get = partial(_get, self._expression.connection, squeeze=squeeze, download_path=download_path) + get = partial(_get, self._expression.connection, + squeeze=squeeze, download_path=download_path) if attrs: # a list of attributes provided attributes = [a for a in attrs if not is_key(a)] - ret = self._expression.proj(*attributes).fetch( + ret = self._expression.proj(*attributes) + ret = ret.fetch( offset=offset, limit=limit, order_by=order_by, as_dict=False, squeeze=squeeze, download_path=download_path, - format='array' - ) + format='array') if attrs_as_dict: ret = [{k: v for k, v in zip(ret.dtype.names, x) if k in attrs} for x in ret] else: - return_values = [ - list((to_dicts if as_dict else lambda x: x)(ret[self._expression.primary_key])) if is_key(attribute) - else ret[attribute] for attribute in attrs] + return_values = [list( + (to_dicts if as_dict else lambda x: x)(ret[self._expression.primary_key])) + if is_key(attribute) else ret[attribute] + for attribute in attrs] ret = return_values[0] if len(attrs) == 1 else return_values else: # fetch all attributes as a numpy.record_array or pandas.DataFrame - cur = self._expression.cursor(as_dict=as_dict, limit=limit, offset=offset, order_by=order_by) + cur = self._expression.cursor( + as_dict=as_dict, limit=limit, offset=offset, order_by=order_by) heading = self._expression.heading if as_dict: - ret = [OrderedDict((name, get(heading[name], d[name])) for name in heading.names) for d in cur] + ret = [dict((name, get(heading[name], d[name])) + for name in heading.names) for d in cur] else: ret = list(cur.fetchall()) record_type = (heading.as_dtype if not ret else np.dtype( - [(name, type(value)) # use the first element to determine the type for blobs + [(name, type(value)) # use the first element to determine blob type if heading[name].is_blob and isinstance(value, numbers.Number) else (name, heading.as_dtype[name]) for value, name in zip(ret[0], heading.as_dtype.names)])) @@ -200,6 +207,7 @@ def __call__(self, *attrs, offset=None, limit=None, order_by=None, format=None, except Exception as e: raise e for name in heading: + # unpack blobs and externals ret[name] = list(map(partial(get, heading[name]), ret[name])) if format == "frame": ret = pandas.DataFrame(ret).set_index(heading.primary_key) @@ -208,15 +216,15 @@ def __call__(self, *attrs, offset=None, limit=None, order_by=None, format=None, class Fetch1: """ - Fetch object for fetching exactly one row. - :param relation: relation the fetch object fetches data from + Fetch object for fetching the result of a query yielding one row. + :param expression: a query expression to fetch from. """ - def __init__(self, relation): - self._expression = relation + def __init__(self, expression): + self._expression = expression def __call__(self, *attrs, squeeze=False, download_path='.'): """ - Fetches the expression results from the database when the expression is known to yield only one entry. + Fetches the result of a query expression that yields one entry. If no attributes are specified, returns the result as a dict. If attributes are specified returns the corresponding results as a tuple. @@ -225,7 +233,8 @@ def __call__(self, *attrs, squeeze=False, download_path='.'): d = rel.fetch1() # as a dictionary a, b = rel.fetch1('a', 'b') # as a tuple - :params *attrs: attributes to return when expanding into a tuple. If empty, the return result is a dict + :params *attrs: attributes to return when expanding into a tuple. + If attrs is empty, the return result is a dict :param squeeze: When true, remove extra dimensions from arrays in attributes :param download_path: for fetches that download data, e.g. attachments :return: the one tuple in the relation in the form of a dict @@ -236,17 +245,20 @@ def __call__(self, *attrs, squeeze=False, download_path='.'): cur = self._expression.cursor(as_dict=True) ret = cur.fetchone() if not ret or cur.fetchone(): - raise DataJointError('fetch1 should only be used for relations with exactly one tuple') - ret = OrderedDict((name, _get(self._expression.connection, heading[name], ret[name], - squeeze=squeeze, download_path=download_path)) - for name in heading.names) + raise DataJointError('fetch1 requires exactly one tuple in the input set.') + ret = dict((name, _get(self._expression.connection, heading[name], ret[name], + squeeze=squeeze, download_path=download_path)) + for name in heading.names) else: # fetch some attributes, return as tuple attributes = [a for a in attrs if not is_key(a)] - result = self._expression.proj(*attributes).fetch(squeeze=squeeze, download_path=download_path) + result = self._expression.proj(*attributes).fetch( + squeeze=squeeze, download_path=download_path, format="array") if len(result) != 1: - raise DataJointError('fetch1 should only return one tuple. %d tuples were found' % len(result)) + raise DataJointError( + 'fetch1 should only return one tuple. %d tuples found' % len(result)) return_values = tuple( - next(to_dicts(result[self._expression.primary_key])) if is_key(attribute) else result[attribute][0] + next(to_dicts(result[self._expression.primary_key])) + if is_key(attribute) else result[attribute][0] for attribute in attrs) ret = return_values[0] if len(attrs) == 1 else return_values return ret diff --git a/datajoint/heading.py b/datajoint/heading.py index aaed7f8bf..076a2204e 100644 --- a/datajoint/heading.py +++ b/datajoint/heading.py @@ -5,7 +5,6 @@ import logging from .errors import DataJointError, _support_filepath_types, FILEPATH_FEATURE_SWITCH from .declare import UUID_DATA_TYPE, SPECIAL_TYPES, TYPE_PATTERN, EXTERNAL_TYPES, NATIVE_TYPES -from .utils import OrderedDict from .attribute_adapter import get_adapter, AttributeAdapter @@ -15,7 +14,7 @@ name=None, type='expression', in_key=False, nullable=False, default=None, comment='calculated attribute', autoincrement=False, numeric=None, string=None, uuid=False, is_blob=False, is_attachment=False, is_filepath=False, is_external=False, adapter=None, - store=None, unsupported=False, sql_expression=None, database=None, dtype=object) + store=None, unsupported=False, attribute_expression=None, database=None, dtype=object) class Attribute(namedtuple('_Attribute', default_attribute_properties)): @@ -24,20 +23,16 @@ class Attribute(namedtuple('_Attribute', default_attribute_properties)): """ def todict(self): """Convert namedtuple to dict.""" - return OrderedDict((name, self[i]) for i, name in enumerate(self._fields)) + return dict((name, self[i]) for i, name in enumerate(self._fields)) @property def sql_type(self): - """ - :return: datatype (as string) in database. In most cases, it is the same as self.type - """ + """ :return: datatype (as string) in database. In most cases, it is the same as self.type """ return UUID_DATA_TYPE if self.uuid else self.type @property def sql_comment(self): - """ - :return: full comment for the SQL declaration. Includes custom type specification - """ + """ :return: full comment for the SQL declaration. Includes custom type specification """ return (':uuid:' if self.uuid else '') + self.comment @property @@ -51,29 +46,48 @@ def sql(self): return '`{name}` {type} NOT NULL COMMENT "{comment}"'.format( name=self.name, type=self.sql_type, comment=self.sql_comment) + @property + def original_name(self): + if self.attribute_expression is None: + return self.name + assert self.attribute_expression.startswith('`') + return self.attribute_expression.strip('`') + class Heading: """ Local class for relations' headings. - Heading contains the property attributes, which is an OrderedDict in which the keys are + Heading contains the property attributes, which is an dict in which the keys are the attribute names and the values are Attributes. """ - def __init__(self, arg=None): + def __init__(self, attribute_specs=None, table_info=None): """ - :param arg: a list of dicts with the same keys as Attribute + :param attribute_specs: a list of dicts with the same keys as Attribute + :param table_info: a dict with information to load the heading from the database """ - assert not isinstance(arg, Heading), 'Headings cannot be copied' self.indexes = None - self.table_info = None - self.attributes = None if arg is None else OrderedDict( - (q['name'], Attribute(**q)) for q in arg) + self.table_info = table_info + self._table_status = None + self._attributes = None if attribute_specs is None else dict( + (q['name'], Attribute(**q)) for q in attribute_specs) def __len__(self): return 0 if self.attributes is None else len(self.attributes) - def __bool__(self): - return self.attributes is not None + @property + def table_status(self): + if self.table_info is None: + return None + if self._table_status is None: + self._init_from_database() + return self._table_status + + @property + def attributes(self): + if self._attributes is None: + self._init_from_database() # lazy loading from database + return self._attributes @property def names(self): @@ -96,8 +110,8 @@ def non_blobs(self): return [k for k, v in self.attributes.items() if not v.is_blob and not v.is_attachment and not v.is_filepath] @property - def expressions(self): - return [k for k, v in self.attributes.items() if v.sql_expression is not None] + def new_attributes(self): + return [k for k, v in self.attributes.items() if v.attribute_expression is not None] def __getitem__(self, name): """shortcut to the attribute""" @@ -107,12 +121,10 @@ def __repr__(self): """ :return: heading representation in DataJoint declaration format but without foreign key expansion """ - if self.attributes is None: - return 'heading not loaded' in_key = True ret = '' - if self.table_info: - ret += '# ' + self.table_info['comment'] + '\n' + if self._table_status is not None: + ret += '# ' + self.table_status['comment'] + '\n' for v in self.attributes.values(): if in_key and not v.in_key: ret += '---\n' @@ -135,33 +147,31 @@ def as_dtype(self): names=self.names, formats=[v.dtype for v in self.attributes.values()])) - @property - def as_sql(self): + def as_sql(self, fields, include_aliases=True): """ - represent heading as SQL field list + represent heading as the SQL SELECT clause. """ - return ','.join('`%s`' % name if self.attributes[name].sql_expression is None - else '%s as `%s`' % (self.attributes[name].sql_expression, name) - for name in self.names) + return ','.join( + '`%s`' % name if self.attributes[name].attribute_expression is None + else self.attributes[name].attribute_expression + (' as `%s`' % name if include_aliases else '') + for name in fields) def __iter__(self): return iter(self.attributes) - def init_from_database(self, conn, database, table_name, context): - """ - initialize heading from a database table. The table must exist already. - """ + def _init_from_database(self): + """ initialize heading from an existing database table. """ + conn, database, table_name, context = ( + self.table_info[k] for k in ('conn', 'database', 'table_name', 'context')) info = conn.query('SHOW TABLE STATUS FROM `{database}` WHERE name="{table_name}"'.format( table_name=table_name, database=database), as_dict=True).fetchone() if info is None: if table_name == '~log': logger.warning('Could not create the ~log table') return - else: - raise DataJointError('The table `{database}`.`{table_name}` is not defined.'.format( - table_name=table_name, database=database)) - self.table_info = {k.lower(): v for k, v in info.items()} - + raise DataJointError('The table `{database}`.`{table_name}` is not defined.'.format( + table_name=table_name, database=database)) + self._table_status = {k.lower(): v for k, v in info.items()} cur = conn.query( 'SHOW FULL COLUMNS FROM `{table_name}` IN `{database}`'.format( table_name=table_name, database=database), as_dict=True) @@ -182,7 +192,6 @@ def init_from_database(self, conn, database, table_name, context): attributes = [{rename_map[k] if k in rename_map else k: v for k, v in x.items() if k not in fields_to_drop} for x in attributes] - numeric_types = { ('float', False): np.float64, ('float', True): np.float64, @@ -213,7 +222,7 @@ def init_from_database(self, conn, database, table_name, context): string=any(TYPE_PATTERN[t].match(attr['type']) for t in ('ENUM', 'TEMPORAL', 'STRING')), is_blob=bool(TYPE_PATTERN['INTERNAL_BLOB'].match(attr['type'])), uuid=False, is_attachment=False, is_filepath=False, adapter=None, - store=None, is_external=False, sql_expression=None) + store=None, is_external=False, attribute_expression=None) if any(TYPE_PATTERN[t].match(attr['type']) for t in ('INTEGER', 'FLOAT')): attr['type'] = re.sub(r'\(\d+\)', '', attr['type'], count=1) # strip size off integers and floats @@ -293,7 +302,7 @@ def init_from_database(self, conn, database, table_name, context): # restore adapted type name attr['type'] = adapter_name - self.attributes = OrderedDict(((q['name'], Attribute(**q)) for q in attributes)) + self._attributes = dict(((q['name'], Attribute(**q)) for q in attributes)) # Read and tabulate secondary indexes keys = defaultdict(dict) @@ -309,32 +318,27 @@ def init_from_database(self, conn, database, table_name, context): nullable=any(v['nullable'] for v in item.values())) for item in keys.values()} - def project(self, attribute_list, named_attributes=None, force_primary_key=None): + def select(self, select_list, rename_map=None, compute_map=None): """ derive a new heading by selecting, renaming, or computing attributes. In relational algebra these operators are known as project, rename, and extend. - :param attribute_list: the full list of existing attributes to include - :param force_primary_key: attributes to force to be converted to primary - :param named_attributes: dictionary of renamed attributes + :param select_list: the full list of existing attributes to include + :param rename_map: dictionary of renamed attributes: keys=new names, values=old names + :param compute_map: a direction of computed attributes + This low-level method performs no error checking. """ - try: # check for missing attributes - raise DataJointError('Attribute `%s` is not found' % next(a for a in attribute_list if a not in self.names)) - except StopIteration: - if named_attributes is None: - named_attributes = {} - if force_primary_key is None: - force_primary_key = set() - rename_map = {v: k for k, v in named_attributes.items() if v in self.attributes} - - # copied and renamed attributes - copy_attrs = (dict(self.attributes[k].todict(), - in_key=self.attributes[k].in_key or k in force_primary_key, - **({'name': rename_map[k], 'sql_expression': '`%s`' % k} if k in rename_map else {})) - for k in self.attributes if k in rename_map or k in attribute_list) - compute_attrs = (dict(default_attribute_properties, name=new_name, sql_expression=expr) - for new_name, expr in named_attributes.items() if expr not in rename_map) - - return Heading(chain(copy_attrs, compute_attrs)) + rename_map = rename_map or {} + compute_map = compute_map or {} + copy_attrs = list() + for name in self.attributes: + if name in select_list: + copy_attrs.append(self.attributes[name].todict()) + copy_attrs.extend(( + dict(self.attributes[old_name].todict(), name=new_name, attribute_expression='`%s`' % old_name) + for new_name, old_name in rename_map.items() if old_name == name)) + compute_attrs = (dict(default_attribute_properties, name=new_name, attribute_expression=expr) + for new_name, expr in compute_map.items()) + return Heading(chain(copy_attrs, compute_attrs)) def join(self, other): """ @@ -347,20 +351,18 @@ def join(self, other): [self.attributes[name].todict() for name in self.secondary_attributes if name not in other.primary_key] + [other.attributes[name].todict() for name in other.secondary_attributes if name not in self.primary_key]) - def make_subquery_heading(self): + def set_primary_key(self, primary_key): """ - Create a new heading with removed attribute sql_expressions. - Used by subqueries, which resolve the sql_expressions. + Create a new heading with the specified primary key. + This low-level method performs no error checking. """ - return Heading(dict(v.todict(), sql_expression=None) for v in self.attributes.values()) + return Heading(chain( + (dict(self.attributes[name].todict(), in_key=True) for name in primary_key), + (dict(self.attributes[name].todict(), in_key=False) for name in self.names if name not in primary_key))) - def extend_primary_key(self, new_attributes): + def make_subquery_heading(self): """ - Create a new heading in which the primary key also includes new_attributes. - :param new_attributes: new attributes to be added to the primary key. + Create a new heading with removed attribute sql_expressions. + Used by subqueries, which resolve the sql_expressions. """ - try: # check for missing attributes - raise DataJointError('Attribute `%s` is not found' % next(a for a in new_attributes if a not in self.names)) - except StopIteration: - return Heading(dict(v.todict(), in_key=v.in_key or v.name in new_attributes) - for v in self.attributes.values()) + return Heading(dict(v.todict(), attribute_expression=None) for v in self.attributes.values()) diff --git a/datajoint/jobs.py b/datajoint/jobs.py index 2efed5afb..97433382d 100644 --- a/datajoint/jobs.py +++ b/datajoint/jobs.py @@ -4,6 +4,7 @@ from .table import Table from .settings import config from .errors import DuplicateError +from .heading import Heading ERROR_MESSAGE_LENGTH = 2047 TRUNCATION_APPENDIX = '...truncated' @@ -13,18 +14,17 @@ class JobTable(Table): """ A base relation with no definition. Allows reserving jobs """ - def __init__(self, arg, database=None): - if isinstance(arg, JobTable): - super().__init__(arg) - # copy constructor - self.database = arg.database - self._connection = arg._connection - self._definition = arg._definition - self._user = arg._user - return - super().__init__() + def __init__(self, conn, database): self.database = database - self._connection = arg + self._connection = conn + self._heading = Heading(table_info=dict( + conn=conn, + database=database, + table_name=self.table_name, + context=None + )) + self._support = [self.full_table_name] + self._definition = """ # job reservation table for `{database}` table_name :varchar(255) # className of the table key_hash :char(32) # key hash diff --git a/datajoint/preview.py b/datajoint/preview.py new file mode 100644 index 000000000..f3daeebf5 --- /dev/null +++ b/datajoint/preview.py @@ -0,0 +1,113 @@ +""" methods for generating previews of query expression results in python command line and Jupyter """ + +from .settings import config + + +def preview(query_expression, limit, width): + heading = query_expression.heading + rel = query_expression.proj(*heading.non_blobs) + if limit is None: + limit = config['display.limit'] + if width is None: + width = config['display.width'] + tuples = rel.fetch(limit=limit + 1, format="array") + has_more = len(tuples) > limit + tuples = tuples[:limit] + columns = heading.names + widths = {f: min(max([len(f)] + + [len(str(e)) for e in tuples[f]] if f in tuples.dtype.names else [len('=BLOB=')]) + 4, width) for f + in columns} + templates = {f: '%%-%d.%ds' % (widths[f], widths[f]) for f in columns} + return ( + ' '.join([templates[f] % ('*' + f if f in rel.primary_key else f) for f in columns]) + '\n' + + ' '.join(['+' + '-' * (widths[column] - 2) + '+' for column in columns]) + '\n' + + '\n'.join(' '.join(templates[f] % (tup[f] if f in tup.dtype.names else '=BLOB=') + for f in columns) for tup in tuples) + + ('\n ...\n' if has_more else '\n') + + (' (Total: %d)\n' % len(rel) if config['display.show_tuple_count'] else '')) + + +def repr_html(query_expression): + heading = query_expression.heading + rel = query_expression.proj(*heading.non_blobs) + info = heading.table_status + tuples = rel.fetch(limit=config['display.limit'] + 1, format='array') + has_more = len(tuples) > config['display.limit'] + tuples = tuples[0:config['display.limit']] + + css = """ + + """ + head_template = """
+

{column}

+ {comment} +
""" + return """ + {css} + {title} +
+ + + {body} +
{head}
+ {ellipsis} + {count}
+ """.format( + css=css, + title="" if info is None else "%s" % info['comment'], + head=''.join( + head_template.format(column=c, comment=heading.attributes[c].comment, + primary='primary' if c in query_expression.primary_key else 'nonprimary') for c in + heading.names), + ellipsis='

...

' if has_more else '', + body=''.join( + ['\n'.join(['%s' % (tup[name] if name in tup.dtype.names else '=BLOB=') + for name in heading.names]) + for tup in tuples]), + count=('

Total: %d

' % len(rel)) if config['display.show_tuple_count'] else '') diff --git a/datajoint/schemas.py b/datajoint/schemas.py index 046f20767..2c050a915 100644 --- a/datajoint/schemas.py +++ b/datajoint/schemas.py @@ -1,5 +1,4 @@ import warnings -import pymysql import logging import inspect import re @@ -8,7 +7,7 @@ from .connection import conn from .diagram import Diagram, _get_tier from .settings import config -from .errors import DataJointError +from .errors import DataJointError, AccessError from .jobs import JobTable from .external import ExternalMapping from .heading import Heading @@ -42,48 +41,173 @@ class Schema: It also specifies the namespace `context` in which other UserTable classes are defined. """ - def __init__(self, schema_name, context=None, *, connection=None, create_schema=True, create_tables=True): + def __init__(self, schema_name=None, context=None, *, connection=None, create_schema=True, + create_tables=True, add_objects=None): """ - Associate database schema `schema_name`. If the schema does not exist, attempt to create it on the server. + Associate database schema `schema_name`. If the schema does not exist, attempt to + create it on the server. + + If the schema_name is omitted, then schema.activate(..) must be called later + to associate with the database. :param schema_name: the database schema to associate. :param context: dictionary for looking up foreign key references, leave None to use local context. :param connection: Connection object. Defaults to datajoint.conn(). :param create_schema: When False, do not create the schema and raise an error if missing. :param create_tables: When False, do not create tables and raise errors when accessing missing tables. + :param add_objects: a mapping with additional objects to make available to the context in which table classes + are declared. """ - if connection is None: - connection = conn() self._log = None - - self.database = schema_name self.connection = connection + self.database = None self.context = context + self.create_schema = create_schema self.create_tables = create_tables self._jobs = None self.external = ExternalMapping(self) + self.add_objects = add_objects + self.declare_list = [] + if schema_name: + self.activate(schema_name) + + def is_activated(self): + return self.database is not None + def activate(self, schema_name=None, *, connection=None, create_schema=None, + create_tables=None, add_objects=None): + """ + Associate database schema `schema_name`. If the schema does not exist, attempt to + create it on the server. + :param schema_name: the database schema to associate. + schema_name=None is used to assert that the schema has already been activated. + :param connection: Connection object. Defaults to datajoint.conn(). + :param create_schema: If False, do not create the schema and raise an error if missing. + :param create_tables: If False, do not create tables and raise errors when attempting + to access missing tables. + :param add_objects: a mapping with additional objects to make available to the context + in which table classes are declared. + """ + if schema_name is None: + if self.exists: + return + raise DataJointError("Please provide a schema_name to activate the schema.") + if self.database is not None and self.exists: + if self.database == schema_name: # already activated + return + raise DataJointError( + "The schema is already activated for schema {db}.".format(db=self.database)) + if connection is not None: + self.connection = connection + if self.connection is None: + self.connection = conn() + self.database = schema_name + if create_schema is not None: + self.create_schema = create_schema + if create_tables is not None: + self.create_tables = create_tables + if add_objects: + self.add_objects = add_objects if not self.exists: - if not create_schema: + if not self.create_schema or not self.database: raise DataJointError( - "Database named `{name}` was not defined. " + "Database `{name}` has not yet been declared. " "Set argument create_schema=True to create it.".format(name=schema_name)) + # create database + logger.info("Creating schema `{name}`.".format(name=schema_name)) + try: + self.connection.query("CREATE DATABASE `{name}`".format(name=schema_name)) + except AccessError: + raise DataJointError( + "Schema `{name}` does not exist and could not be created. " + "Check permissions.".format(name=schema_name)) else: - # create database - logger.info("Creating schema `{name}`.".format(name=schema_name)) - try: - connection.query("CREATE DATABASE `{name}`".format(name=schema_name)) - except pymysql.OperationalError: - raise DataJointError( - "Schema `{name}` does not exist and could not be created. " - "Check permissions.".format(name=schema_name)) + self.log('created') + self.connection.register(self) + + # decorate all tables already decorated + for cls, context in self.declare_list: + if self.add_objects: + context = dict(context, **self.add_objects) + self._decorate_master(cls, context) + + def _assert_exists(self, message=None): + if not self.exists: + raise DataJointError( + message or "Schema `{db}` has not been created.".format(db=self.database)) + + def __call__(self, cls, *, context=None): + """ + Binds the supplied class to a schema. This is intended to be used as a decorator. + :param cls: class to decorate. + :param context: supplied when called from spawn_missing_classes + """ + context = context or self.context or inspect.currentframe().f_back.f_locals + if issubclass(cls, Part): + raise DataJointError('The schema decorator should not be applied to Part relations') + if self.is_activated(): + self._decorate_master(cls, context) + else: + self.declare_list.append((cls, context)) + return cls + + def _decorate_master(self, cls, context): + """ + :param cls: the master class to process + :param context: the class' declaration context + """ + self._decorate_table(cls, context=dict(context, self=cls, **{cls.__name__: cls})) + # Process part tables + for part in ordered_dir(cls): + if part[0].isupper(): + part = getattr(cls, part) + if inspect.isclass(part) and issubclass(part, Part): + part._master = cls + # allow addressing master by name or keyword 'master' + self._decorate_table(part, context=dict( + context, master=cls, self=part, **{cls.__name__: cls})) + + def _decorate_table(self, table_class, context, assert_declared=False): + """ + assign schema properties to the table class and declare the table + """ + table_class.database = self.database + table_class._connection = self.connection + table_class._heading = Heading(table_info=dict( + conn=self.connection, + database=self.database, + table_name=table_class.table_name, + context=context)) + table_class._support = [table_class.full_table_name] + table_class.declaration_context = context + + # instantiate the class, declare the table if not already + instance = table_class() + is_declared = instance.is_declared + if not is_declared: + if not self.create_tables or assert_declared: + raise DataJointError('Table `%s` not declared' % instance.table_name) + instance.declare(context) + is_declared = is_declared or instance.is_declared + + # add table definition to the doc string + if isinstance(table_class.definition, str): + table_class.__doc__ = (table_class.__doc__ or "") + "\nTable definition:\n\n" + table_class.definition + + # fill values in Lookup tables from their contents property + if isinstance(instance, Lookup) and hasattr(instance, 'contents') and is_declared: + contents = list(instance.contents) + if len(contents) > len(instance): + if instance.heading.has_autoincrement: + warnings.warn(('Contents has changed but cannot be inserted because ' + '{table} has autoincrement.').format( + table=instance.__class__.__name__)) else: - self.log('created') - self.log('connect') - connection.register(self) + instance.insert(contents, skip_duplicates=True) @property def log(self): + self._assert_exists() if self._log is None: self._log = Log(self.connection, self.database) return self._log @@ -96,6 +220,7 @@ def size_on_disk(self): """ :return: size of the entire schema in bytes """ + self._assert_exists() return int(self.connection.query( """ SELECT SUM(data_length + index_length) @@ -108,6 +233,7 @@ def spawn_missing_classes(self, context=None): in the context. :param context: alternative context to place the missing classes into, e.g. locals() """ + self._assert_exists() if context is None: if self.context is not None: context = self.context @@ -143,7 +269,7 @@ def spawn_missing_classes(self, context=None): raise DataJointError('The table %s does not follow DataJoint naming conventions' % table_name) part_class = type(class_name, (Part,), dict(definition=...)) part_class._master = master_class - self.process_table_class(part_class, context=context, assert_declared=True) + self._decorate_table(part_class, context=context, assert_declared=True) setattr(master_class, class_name, part_class) def drop(self, force=False): @@ -151,7 +277,8 @@ def drop(self, force=False): Drop the associated schema if it exists """ if not self.exists: - logger.info("Schema named `{database}` does not exist. Doing nothing.".format(database=self.database)) + logger.info("Schema named `{database}` does not exist. Doing nothing.".format( + database=self.database)) elif (not config['safemode'] or force or user_choice("Proceed to delete entire schema `%s`?" % self.database, default='no') == 'yes'): @@ -159,73 +286,22 @@ def drop(self, force=False): try: self.connection.query("DROP DATABASE `{database}`".format(database=self.database)) logger.info("Schema `{database}` was dropped successfully.".format(database=self.database)) - except pymysql.OperationalError: - raise DataJointError("An attempt to drop schema `{database}` " - "has failed. Check permissions.".format(database=self.database)) + except AccessError: + raise AccessError( + "An attempt to drop schema `{database}` " + "has failed. Check permissions.".format(database=self.database)) @property def exists(self): """ :return: true if the associated schema exists on the server """ - cur = self.connection.query("SHOW DATABASES LIKE '{database}'".format(database=self.database)) - return cur.rowcount > 0 - - def process_table_class(self, table_class, context, assert_declared=False): - """ - assign schema properties to the relation class and declare the table - """ - table_class.database = self.database - table_class._connection = self.connection - table_class._heading = Heading() - table_class.declaration_context = context - - # instantiate the class, declare the table if not already - instance = table_class() - is_declared = instance.is_declared - if not is_declared: - if not self.create_tables or assert_declared: - raise DataJointError('Table `%s` not declared' % instance.table_name) - else: - instance.declare(context) - is_declared = is_declared or instance.is_declared - - # add table definition to the doc string - if isinstance(table_class.definition, str): - table_class.__doc__ = (table_class.__doc__ or "") + "\nTable definition:\n\n" + table_class.definition - - # fill values in Lookup tables from their contents property - if isinstance(instance, Lookup) and hasattr(instance, 'contents') and is_declared: - contents = list(instance.contents) - if len(contents) > len(instance): - if instance.heading.has_autoincrement: - warnings.warn( - 'Contents has changed but cannot be inserted because {table} has autoincrement.'.format( - table=instance.__class__.__name__)) - else: - instance.insert(contents, skip_duplicates=True) - - def __call__(self, cls, *, context=None): - """ - Binds the supplied class to a schema. This is intended to be used as a decorator. - :param cls: class to decorate. - :param context: supplied when called from spawn_missing_classes - """ - context = context or self.context or inspect.currentframe().f_back.f_locals - if issubclass(cls, Part): - raise DataJointError('The schema decorator should not be applied to Part relations') - self.process_table_class(cls, context=dict(context, self=cls, **{cls.__name__: cls})) - - # Process part relations - for part in ordered_dir(cls): - if part[0].isupper(): - part = getattr(cls, part) - if inspect.isclass(part) and issubclass(part, Part): - part._master = cls - # allow addressing master by name or keyword 'master' - self.process_table_class(part, context=dict( - context, master=cls, self=part, **{cls.__name__: cls})) - return cls + if self.database is None: + raise DataJointError("Schema must be activated first.") + return bool(self.connection.query( + "SELECT schema_name " + "FROM information_schema.schemata " + "WHERE schema_name = '{database}'".format(database=self.database)).rowcount) @property def jobs(self): @@ -233,12 +309,14 @@ def jobs(self): schema.jobs provides a view of the job reservation table for the schema :return: jobs table """ + self._assert_exists() if self._jobs is None: self._jobs = JobTable(self.connection, self.database) return self._jobs @property def code(self): + self._assert_exists() return self.save() def save(self, python_filename=None): @@ -247,6 +325,7 @@ def save(self, python_filename=None): This method is in preparation for a future release and is not officially supported. :return: a string containing the body of a complete Python module defining this schema. """ + self._assert_exists() module_count = itertools.count() # add virtual modules for referenced modules with names vmod0, vmod1, ... module_lookup = collections.defaultdict(lambda: 'vmod' + str(next(module_count))) @@ -261,19 +340,21 @@ def make_class_definition(table): indent += ' ' class_name = to_camel_case(class_name) - def repl(s): + def replace(s): d, tabs = s.group(1), s.group(2) return ('' if d == db else (module_lookup[d]+'.')) + '.'.join( to_camel_case(tab) for tab in tabs.lstrip('__').split('__')) - return ('' if tier == 'Part' else '\n@schema\n') + \ - '{indent}class {class_name}(dj.{tier}):\n{indent} definition = """\n{indent} {defi}"""'.format( + return ('' if tier == 'Part' else '\n@schema\n') + ( + '{indent}class {class_name}(dj.{tier}):\n' + '{indent} definition = """\n' + '{indent} {defi}"""').format( class_name=class_name, indent=indent, tier=tier, - defi=re.sub( - r'`([^`]+)`.`([^`]+)`', repl, - FreeTable(self.connection, table).describe(printout=False).replace('\n', '\n ' + indent))) + defi=re.sub(r'`([^`]+)`.`([^`]+)`', replace, + FreeTable(self.connection, table).describe(printout=False) + ).replace('\n', '\n ' + indent)) diagram = Diagram(self) body = '\n\n'.join(make_class_definition(table) for table in diagram.topological_sort()) @@ -284,25 +365,24 @@ def repl(s): for k, v in module_lookup.items()), body)) if python_filename is None: return python_code - else: - with open(python_filename, 'wt') as f: - f.write(python_code) + with open(python_filename, 'wt') as f: + f.write(python_code) def list_tables(self): """ Return a list of all tables in the schema except tables with ~ in first character such as ~logs and ~job - :return: A list of table names in their raw datajoint naming convection form + :return: A list of table names from the database schema. """ - return [table_name for (table_name,) in self.connection.query(""" SELECT table_name FROM information_schema.tables - WHERE table_schema = %s and table_name NOT LIKE '~%%'""", args=(self.database))] + WHERE table_schema = %s and table_name NOT LIKE '~%%'""", args=(self.database,))] class VirtualModule(types.ModuleType): """ - A virtual module which will contain context for schema. + A virtual module imitates a Python module representing a DataJoint schema from table definitions in the database. + It declares the schema objects and a class for each table. """ def __init__(self, module_name, schema_name, *, create_schema=False, create_tables=False, connection=None, add_objects=None): @@ -318,8 +398,8 @@ def __init__(self, module_name, schema_name, *, create_schema=False, :return: the python module containing classes from the schema object and the table classes """ super(VirtualModule, self).__init__(name=module_name) - _schema = Schema(schema_name, create_schema=create_schema, create_tables=create_tables, - connection=connection) + _schema = Schema(schema_name, create_schema=create_schema, + create_tables=create_tables, connection=connection) if add_objects: self.__dict__.update(add_objects) self.__dict__['schema'] = _schema @@ -331,4 +411,7 @@ def list_schemas(connection=None): :param connection: a dj.Connection object :return: list of all accessible schemas on the server """ - return [r[0] for r in (connection or conn()).query('SHOW SCHEMAS') if r[0] not in {'information_schema'}] + return [r[0] for r in (connection or conn()).query( + 'SELECT schema_name ' + 'FROM information_schema.schemata ' + 'WHERE schema_name <> "information_schema"')] diff --git a/datajoint/settings.py b/datajoint/settings.py index 28d386bdd..3ebcf3ed9 100644 --- a/datajoint/settings.py +++ b/datajoint/settings.py @@ -5,7 +5,6 @@ import json import os import pprint -from collections import OrderedDict import logging import collections from enum import Enum @@ -30,7 +29,7 @@ } prefix_to_role = dict(zip(role_to_prefix.values(), role_to_prefix)) -default = OrderedDict({ +default = dict({ 'database.host': 'localhost', 'database.password': None, 'database.user': None, diff --git a/datajoint/table.py b/datajoint/table.py index aaee1e2ee..53da2dc3c 100644 --- a/datajoint/table.py +++ b/datajoint/table.py @@ -6,18 +6,37 @@ import pandas import logging import uuid +import re from pathlib import Path from .settings import config from .declare import declare, alter +from .condition import make_condition from .expression import QueryExpression from . import blob -from .utils import user_choice, OrderedDict +from .utils import user_choice from .heading import Heading -from .errors import DuplicateError, AccessError, DataJointError, UnknownAttributeError +from .errors import (DuplicateError, AccessError, DataJointError, UnknownAttributeError, + IntegrityError) from .version import __version__ as version logger = logging.getLogger(__name__) +foreign_key_error_regexp = re.compile( + r"[\w\s:]*\((?P`[^`]+`.`[^`]+`), " + r"CONSTRAINT (?P`[^`]+`) " + r"(FOREIGN KEY \((?P[^)]+)\) " + r"REFERENCES (?P`[^`]+`(\.`[^`]+`)?) \((?P[^)]+)\)[\s\w]+\))?") + +constraint_info_query = ' '.join(""" + SELECT + COLUMN_NAME as fk_attrs, + CONCAT('`', REFERENCED_TABLE_SCHEMA, '`.`', REFERENCED_TABLE_NAME, '`') as parent, + REFERENCED_COLUMN_NAME as pk_attrs + FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE + WHERE + CONSTRAINT_NAME = %s AND TABLE_SCHEMA = %s AND TABLE_NAME = %s; + """.split()) + class _RenameMap(tuple): """ for internal use """ @@ -31,23 +50,21 @@ class Table(QueryExpression): table name, database, and definition. A Relation implements insert and delete methods in addition to inherited relational operators. """ - _heading = None + + _table_name = None # must be defined in subclass + _log_ = None # placeholder for the Log table object + + # These properties must be set by the schema decorator (schemas.py) at class level or by FreeTable at instance level database = None - _log_ = None declaration_context = None - # -------------- required by QueryExpression ----------------- # @property - def heading(self): - """ - :return: table heading. If the table is not declared, attempts to declare it first. - """ - if self._heading is None: - self._heading = Heading() # instance-level heading - if not self._heading and self.connection is not None: # lazy loading of heading - self._heading.init_from_database( - self.connection, self.database, self.table_name, self.declaration_context) - return self._heading + def table_name(self): + return self._table_name + + @property + def definition(self): + raise NotImplementedError('Subclasses of Table must implement the `definition` property') def declare(self, context=None): """ @@ -76,8 +93,8 @@ def alter(self, prompt=True, context=None): Alter the table definition from self.definition """ if self.connection.in_transaction: - raise DataJointError('Cannot update table declaration inside a transaction, ' - 'e.g. from inside a populate/make call') + raise DataJointError( + 'Cannot update table declaration inside a transaction, e.g. from inside a populate/make call') if context is None: frame = inspect.currentframe().f_back context = dict(frame.f_globals, **frame.f_locals) @@ -99,11 +116,11 @@ def alter(self, prompt=True, context=None): # skip if no create privilege pass else: + self.__class__._heading = Heading(table_info=self.heading.table_info) # reset heading if prompt: print('Table altered') self._log('Altered ' + self.full_table_name) - @property def from_clause(self): """ :return: the FROM clause of SQL SELECT statements. @@ -207,7 +224,7 @@ def external(self): def update1(self, row): """ - Update an existing entry in the table. + update1 updates one existing entry in the table. Caution: Updates are not part of the DataJoint data manipulation model. For strict data integrity, use delete and insert. :param row: a dict containing the primary key and the attributes to update. @@ -236,7 +253,7 @@ def update1(self, row): query = "UPDATE {table} SET {assignments} WHERE {where}".format( table=self.full_table_name, assignments=",".join('`%s`=%s' % r[:2] for r in row), - where=self._make_condition(key)) + where=make_condition(self, key, set())) self.connection.query(query, args=list(r[2] for r in row if r[2] is not None)) def insert1(self, row, **kwargs): @@ -250,7 +267,6 @@ def insert1(self, row, **kwargs): def insert(self, rows, replace=False, skip_duplicates=False, ignore_extra_fields=False, allow_direct_insert=None): """ Insert a collection of rows. - :param rows: An iterable where an element is a numpy record, a dict-like object, a pandas.DataFrame, a sequence, or a query expression with the same heading as table self. :param replace: If True, replaces the existing tuple. @@ -258,13 +274,11 @@ def insert(self, rows, replace=False, skip_duplicates=False, ignore_extra_fields :param ignore_extra_fields: If False, fields that are not in the heading raise error. :param allow_direct_insert: applies only in auto-populated tables. If False (default), insert are allowed only from inside the make callback. - Example:: >>> relation.insert([ >>> dict(subject_id=7, species="mouse", date_of_birth="2014-09-01"), >>> dict(subject_id=8, species="mouse", date_of_birth="2014-09-02")]) """ - if isinstance(rows, pandas.DataFrame): # drop 'extra' synthetic index for 1-field index case - # frames with more advanced indices should be prepared by user. @@ -294,7 +308,7 @@ def insert(self, rows, replace=False, skip_duplicates=False, ignore_extra_fields command='REPLACE' if replace else 'INSERT', fields='`' + '`,`'.join(fields) + '`', table=self.full_table_name, - select=rows.make_sql(select_fields=fields), + select=rows.make_sql(fields), duplicate=(' ON DUPLICATE KEY UPDATE `{pk}`={table}.`{pk}`'.format( table=self.full_table_name, pk=self.primary_key[0]) if skip_duplicates else '')) @@ -307,112 +321,123 @@ def insert(self, rows, replace=False, skip_duplicates=False, ignore_extra_fields try: query = "{command} INTO {destination}(`{fields}`) VALUES {placeholders}{duplicate}".format( command='REPLACE' if replace else 'INSERT', - destination=self.from_clause, + destination=self.from_clause(), fields='`,`'.join(field_list), placeholders=','.join('(' + ','.join(row['placeholders']) + ')' for row in rows), duplicate=(' ON DUPLICATE KEY UPDATE `{pk}`=`{pk}`'.format(pk=self.primary_key[0]) if skip_duplicates else '')) self.connection.query(query, args=list( - itertools.chain.from_iterable((v for v in r['values'] if v is not None) for r in rows))) + itertools.chain.from_iterable( + (v for v in r['values'] if v is not None) for r in rows))) except UnknownAttributeError as err: - raise err.suggest('To ignore extra fields in insert, set ignore_extra_fields=True') + raise err.suggest( + 'To ignore extra fields in insert, set ignore_extra_fields=True') except DuplicateError as err: - raise err.suggest('To ignore duplicate entries in insert, set skip_duplicates=True') + raise err.suggest( + 'To ignore duplicate entries in insert, set skip_duplicates=True') def delete_quick(self, get_count=False): """ Deletes the table without cascading and without user prompt. If this table has populated dependent tables, this will fail. """ - query = 'DELETE FROM ' + self.full_table_name + self.where_clause + query = 'DELETE FROM ' + self.full_table_name + self.where_clause() self.connection.query(query) count = self.connection.query("SELECT ROW_COUNT()").fetchone()[0] if get_count else None self._log(query[:255]) return count - def delete(self, verbose=True): + def _delete_cascade(self): + """service function to perform cascading deletes recursively.""" + max_attempts = 50 + delete_count = 0 + for _ in range(max_attempts): + try: + delete_count += self.delete_quick(get_count=True) + except IntegrityError as error: + match = foreign_key_error_regexp.match(error.args[0]).groupdict() + if "`.`" not in match['child']: # if schema name missing, use self + match['child'] = '{}.{}'.format(self.full_table_name.split(".")[0], + match['child']) + if match['pk_attrs'] is not None: # fully matched, adjusting the keys + match['fk_attrs'] = [k.strip('`') for k in match['fk_attrs'].split(',')] + match['pk_attrs'] = [k.strip('`') for k in match['pk_attrs'].split(',')] + else: # only partially matched, querying with constraint to determine keys + match['fk_attrs'], match['parent'], match['pk_attrs'] = list(map( + list, zip(*self.connection.query(constraint_info_query, args=( + match['name'].strip('`'), + *[_.strip('`') for _ in match['child'].split('`.`')] + )).fetchall()))) + match['parent'] = match['parent'][0] + # restrict child by self if + # 1. if self's restriction attributes are not in child's primary key + # 2. if child renames any attributes + # otherwise restrict child by self's restriction. + child = FreeTable(self.connection, match['child']) + if set(self.restriction_attributes) <= set(child.primary_key) and \ + match['fk_attrs'] == match['pk_attrs']: + child._restriction = self._restriction + elif match['fk_attrs'] != match['pk_attrs']: + child &= self.proj(**dict(zip(match['fk_attrs'], + match['pk_attrs']))) + else: + child &= self.proj() + delete_count += child._delete_cascade() + else: + print("Deleting {count} rows from {table}".format( + count=delete_count, table=self.full_table_name)) + break + else: + raise DataJointError('Exceeded maximum number of delete attempts.') + return delete_count + + def delete(self, transaction=True, safemode=None): """ Deletes the contents of the table and its dependent tables, recursively. - User is prompted for confirmation if config['safemode'] is set to True. + + :param transaction: if True, use the entire delete becomes an atomic transaction. + :param safemode: If True, prohibit nested transactions and prompt to confirm. Default + is dj.config['safemode']. """ - conn = self.connection - already_in_transaction = conn.in_transaction - safe = config['safemode'] - if already_in_transaction and safe: - raise DataJointError('Cannot delete within a transaction in safemode. ' - 'Set dj.config["safemode"] = False or complete the ongoing transaction first.') - graph = conn.dependencies - graph.load() - delete_list = OrderedDict( - (name, _RenameMap(next(iter(graph.parents(name).items()))) if name.isdigit() else FreeTable(conn, name)) - for name in graph.descendants(self.full_table_name)) - - # construct restrictions for each relation - restrict_by_me = set() - # restrictions: Or-Lists of restriction conditions for each table. - # Uncharacteristically of Or-Lists, an empty entry denotes "delete everything". - restrictions = collections.defaultdict(list) - # restrict by self - if self.restriction: - restrict_by_me.add(self.full_table_name) - restrictions[self.full_table_name].append(self.restriction) # copy own restrictions - # restrict by renamed nodes - restrict_by_me.update(table for table in delete_list if table.isdigit()) # restrict by all renamed nodes - # restrict by secondary dependencies - for table in delete_list: - restrict_by_me.update(graph.children(table, primary=False)) # restrict by any non-primary dependents - - # compile restriction lists - for name, table in delete_list.items(): - for dep in graph.children(name): - # if restrict by me, then restrict by the entire relation otherwise copy restrictions - restrictions[dep].extend([table] if name in restrict_by_me else restrictions[name]) - - # apply restrictions - for name, table in delete_list.items(): - if not name.isdigit() and restrictions[name]: # do not restrict by an empty list - table.restrict([ - r.proj() if isinstance(r, FreeTable) else ( - delete_list[r[0]].proj(**{a: b for a, b in r[1]['attr_map'].items()}) - if isinstance(r, _RenameMap) else r) - for r in restrictions[name]]) - if safe: - print('About to delete:') - - if not already_in_transaction: - conn.start_transaction() - total = 0 + safemode = config['safemode'] if safemode is None else safemode + + # Start transaction + if transaction: + if not self.connection.in_transaction: + self.connection.start_transaction() + else: + if not safemode: + transaction = False + else: + raise DataJointError( + "Delete cannot use a transaction within an ongoing transaction. " + "Set transaction=False or safemode=False).") + + # Cascading delete try: - for name, table in reversed(list(delete_list.items())): - if not name.isdigit(): - count = table.delete_quick(get_count=True) - total += count - if (verbose or safe) and count: - print('{table}: {count} items'.format(table=name, count=count)) + delete_count = self._delete_cascade() except: - # Delete failed, perhaps due to insufficient privileges. Cancel transaction. - if not already_in_transaction: - conn.cancel_transaction() + if transaction: + self.connection.cancel_transaction() raise + + # Confirm and commit + if delete_count == 0: + if safemode: + print('Nothing to delete.') + if transaction: + self.connection.cancel_transaction() else: - assert not (already_in_transaction and safe) - if not total: - print('Nothing to delete') - if not already_in_transaction: - conn.cancel_transaction() + if not safemode or user_choice("Commit deletes?", default='no') == 'yes': + if transaction: + self.connection.commit_transaction() + if safemode: + print('Deletes committed.') else: - if already_in_transaction: - if verbose: - print('The delete is pending within the ongoing transaction.') - else: - if not safe or user_choice("Proceed?", default='no') == 'yes': - conn.commit_transaction() - if verbose or safe: - print('Committed.') - else: - conn.cancel_transaction() - if verbose or safe: - print('Cancelled deletes.') + if transaction: + self.connection.cancel_transaction() + if safemode: + print('Deletes cancelled') def drop_quick(self): """ @@ -473,8 +498,8 @@ def describe(self, context=None, printout=True): self.connection.dependencies.load() parents = self.parents(foreign_key_info=True) in_key = True - definition = ('# ' + self.heading.table_info['comment'] + '\n' - if self.heading.table_info['comment'] else '') + definition = ('# ' + self.heading.table_status['comment'] + '\n' + if self.heading.table_status['comment'] else '') attributes_thus_far = set() attributes_declared = set() indexes = self.heading.indexes.copy() @@ -528,6 +553,8 @@ def describe(self, context=None, printout=True): def _update(self, attrname, value=None): """ + This is a deprecated function to be removed in datajoint 0.14. Use .update1 instead. + Updates a field in an existing tuple. This is not a datajoyous operation and should not be used routinely. Relational database maintain referential integrity on the level of a tuple. Therefore, the UPDATE operator can violate referential integrity. The datajoyous way to update information is @@ -538,8 +565,8 @@ def _update(self, attrname, value=None): 2. the update attribute must not be in primary key Example: - >>> (v2p.Mice() & key).update('mouse_dob', '2011-01-01') - >>> (v2p.Mice() & key).update( 'lens') # set the value to NULL + >>> (v2p.Mice() & key)._update('mouse_dob', '2011-01-01') + >>> (v2p.Mice() & key)._update( 'lens') # set the value to NULL """ if len(self) != 1: raise DataJointError('Update is only allowed on one tuple at a time') @@ -563,10 +590,10 @@ def _update(self, attrname, value=None): else: placeholder = '%s' if value is not None else 'NULL' command = "UPDATE {full_table_name} SET `{attrname}`={placeholder} {where_clause}".format( - full_table_name=self.from_clause, + full_table_name=self.from_clause(), attrname=attrname, placeholder=placeholder, - where_clause=self.where_clause) + where_clause=self.where_clause()) self.connection.query(command, args=(value, ) if value is not None else ()) # --- private helper functions ---- @@ -711,29 +738,21 @@ class FreeTable(Table): """ A base relation without a dedicated class. Each instance is associated with a table specified by full_table_name. - :param arg: a dj.Connection or a dj.FreeTable + :param conn: a dj.Connection object + :param full_table_name: in format `database`.`table_name` """ - - def __init__(self, arg, full_table_name=None): - super().__init__() - if isinstance(arg, FreeTable): - # copy constructor - self.database = arg.database - self._table_name = arg._table_name - self._connection = arg._connection - else: - self.database, self._table_name = (s.strip('`') for s in full_table_name.split('.')) - self._connection = arg + def __init__(self, conn, full_table_name): + self.database, self._table_name = (s.strip('`') for s in full_table_name.split('.')) + self._connection = conn + self._support = [full_table_name] + self._heading = Heading(table_info=dict( + conn=conn, + database=self.database, + table_name=self.table_name, + context=None)) def __repr__(self): - return "FreeTable(`%s`.`%s`)" % (self.database, self._table_name) - - @property - def table_name(self): - """ - :return: the table name in the schema - """ - return self._table_name + return "FreeTable(`%s`.`%s`)\n" % (self.database, self._table_name) + super().__repr__() class Log(Table): @@ -743,21 +762,20 @@ class Log(Table): :param skip_logging: if True, then log entry is skipped by default. See __call__ """ - def __init__(self, arg, database=None, skip_logging=False): - super().__init__() - - if isinstance(arg, Log): - # copy constructor - self.database = arg.database - self.skip_logging = arg.skip_logging - self._connection = arg._connection - self._definition = arg._definition - self._user = arg._user - return + _table_name = '~log' + def __init__(self, conn, database, skip_logging=False): self.database = database self.skip_logging = skip_logging - self._connection = arg + self._connection = conn + self._heading = Heading(table_info=dict( + conn=conn, + database=database, + table_name=self.table_name, + context=None + )) + self._support = [self.full_table_name] + self._definition = """ # event logging table for `{database}` id :int unsigned auto_increment # event order id --- @@ -768,6 +786,8 @@ def __init__(self, arg, database=None, skip_logging=False): event="" :varchar(255) # event message """.format(database=database) + super().__init__() + if not self.is_declared: self.declare() self.connection.dependencies.clear() @@ -777,10 +797,6 @@ def __init__(self, arg, database=None, skip_logging=False): def definition(self): return self._definition - @property - def table_name(self): - return '~log' - def __call__(self, event, skip_logging=None): """ :param event: string to write into the log table diff --git a/datajoint/user_tables.py b/datajoint/user_tables.py index 3326bcb52..b7bfd9db2 100644 --- a/datajoint/user_tables.py +++ b/datajoint/user_tables.py @@ -2,7 +2,6 @@ Hosts the table tiers, user relations should be derived from. """ -import collections from .table import Table from .autopopulate import AutoPopulate from .utils import from_camel_case, ClassProperty @@ -11,35 +10,20 @@ _base_regexp = r'[a-z][a-z0-9]*(_[a-z][a-z0-9]*)*' # attributes that trigger instantiation of user classes + + supported_class_attrs = { 'key_source', 'describe', 'alter', 'heading', 'populate', 'progress', 'primary_key', - 'proj', 'aggr', 'fetch', 'fetch1', 'head', 'tail', - 'descendants', 'parts', 'ancestors', 'parents', 'children', + 'proj', 'aggr', 'join', 'fetch', 'fetch1', 'head', 'tail', + 'descendants', 'ancestors', 'parts', 'parents', 'children', 'insert', 'insert1', 'update1', 'drop', 'drop_quick', 'delete', 'delete_quick'} -class OrderedClass(type): +class TableMeta(type): """ - Class whose members are ordered - See https://docs.python.org/3/reference/datamodel.html#metaclass-example - - Note: Since Python 3.6, _ordered_class_members will no longer be necessary (PEP 520) - https://www.python.org/dev/peps/pep-0520/ + TableMeta subclasses allow applying some instance methods and properties directly + at class level. For example, this allows Table.fetch() instead of Table().fetch(). """ - @classmethod - def __prepare__(metacls, name, bases, **kwds): - return collections.OrderedDict() - - def __new__(cls, name, bases, namespace, **kwds): - result = type.__new__(cls, name, bases, dict(namespace)) - result._ordered_class_members = list(namespace) - return result - - def __setattr__(cls, name, value): - if hasattr(cls, '_ordered_class_members'): - cls._ordered_class_members.append(name) - super().__setattr__(name, value) - def __getattribute__(cls, name): # trigger instantiation for supported class attrs return (cls().__getattribute__(name) if name in supported_class_attrs @@ -48,12 +32,21 @@ def __getattribute__(cls, name): def __and__(cls, arg): return cls() & arg + def __xor__(cls, arg): + return cls() ^ arg + def __sub__(cls, arg): return cls() - arg + def __neg__(cls): + return -cls() + def __mul__(cls, arg): return cls() * arg + def __matmul__(cls, arg): + return cls() @ arg + def __add__(cls, arg): return cls() + arg @@ -61,13 +54,17 @@ def __iter__(cls): return iter(cls()) -class UserTable(Table, metaclass=OrderedClass): +class UserTable(Table, metaclass=TableMeta): """ A subclass of UserTable is a dedicated class interfacing a base relation. UserTable is initialized by the decorator generated by schema(). """ + # set by @schema _connection = None _heading = None + _support = None + + # set by subclass tier_regexp = None _prefix = None @@ -76,7 +73,8 @@ def definition(self): """ :return: a string containing the table definition using the DataJoint DDL. """ - raise NotImplementedError('Subclasses of Table must implement the property "definition"') + raise NotImplementedError( + 'Subclasses of Table must implement the property "definition"') @ClassProperty def connection(cls): @@ -85,7 +83,7 @@ def connection(cls): @ClassProperty def table_name(cls): """ - :returns: the table name of the table formatted for mysql. + :return: the table name of the table formatted for mysql. """ if cls._prefix is None: raise AttributeError('Class prefix is not defined!') @@ -94,8 +92,11 @@ def table_name(cls): @ClassProperty def full_table_name(cls): if cls not in {Manual, Imported, Lookup, Computed, Part, UserTable}: + # for derived classes only if cls.database is None: - raise DataJointError('Class %s is not properly declared (schema decorator not applied?)' % cls.__name__) + raise DataJointError( + 'Class %s is not properly declared (schema decorator not applied?)' % + cls.__name__) return r"`{0:s}`.`{1:s}`".format(cls.database, cls.table_name) @@ -144,7 +145,6 @@ class Part(UserTable): """ _connection = None - _heading = None _master = None tier_regexp = r'(?P' + '|'.join( diff --git a/datajoint/utils.py b/datajoint/utils.py index a20f26059..42b18222b 100644 --- a/datajoint/utils.py +++ b/datajoint/utils.py @@ -3,17 +3,9 @@ import re from pathlib import Path import shutil -import sys from .errors import DataJointError -if sys.version_info[1] < 6: - from collections import OrderedDict -else: - # use dict in Python 3.6+ -- They are already ordered and look nicer - OrderedDict = dict - - class ClassProperty: def __init__(self, f): self.f = f diff --git a/datajoint/version.py b/datajoint/version.py index a7571b6c4..4ac209b43 100644 --- a/datajoint/version.py +++ b/datajoint/version.py @@ -1,3 +1,3 @@ -__version__ = "0.13.0" +__version__ = "0.13.dev7" assert len(__version__) <= 10 # The log table limits version to the 10 characters diff --git a/dev_guide/transpiler_specs.md b/dev_guide/transpiler_specs.md new file mode 100644 index 000000000..c5c85ca4b --- /dev/null +++ b/dev_guide/transpiler_specs.md @@ -0,0 +1,116 @@ +# Design specifications of the DataJoint-to-SQL Transpiler +This document contains information and reasoning that went into the design of the DataJoint-to-SQL transpiler for DataJoint for Python version 0.13. + +MySQL appears to differ from standard SQL by the sequence of evaluating the clauses of the SELECT statement. + +``` +Standard SQL: FROM > WHERE > GROUP BY > HAVING > SELECT +MySQL: FROM > WHERE > SELECT > GROUP BY > HAVING +``` + +> TODO: verify with latest SQL standards and postgres / CockroachDB implementations and whether this order can be configured + +Moving `SELECT` to an earlier phase allows the `GROUP BY` and `HAVING` clauses to use alias column names created by the `SELECT` clause. +The current implementation targets the MySQL implementation where table column aliases can be used in `HAVING`. +If postgres or CockroachDB cannot be coerced to work this way, restrictions of aggregations will have to be updated accordingly. + +## QueryExpression +`QueryExpression` is the main object representing a distinct `SELECT` statement. +It implements operators `&`, `*`, and `proj` — restriction, join, and projection. + +Property `heading` describes all attributes. + +Operator `proj` creates a new heading. + +Property `restriction` contains the `AndList` of conditions. Operator `&` creates a new restriction appending the new condition to the input's restriction. + +Property `support` represents the `FROM` clause and contains a list of either `QueryExpression` objects or table names in the case of base queries. +The joint operator `*` adds new elements to the `support` attribute. + +At least one element must be present in `support`. Multiple elements in `support` indicate a join. + +From the user's perspective `QueryExpression` objects are immutable: once created they cannot be modified. All operators derive new objects. + +### Alias attributes +`proj` can create an alias attribute by renaming an existing attribute or calculating a new attribute. +Alias attributes are the primary reason why subqueries are sometimes required. + +### Subqueries +Projections, restrictions, and joins do not necessarily trigger new subqueries: the resulting `QueryExpression` object simply merges the properties of its inputs into self: `heading`, `restriction`, and `support`. + +The input object is treated as a subquery in the following cases: +1. A restriction is applied that uses alias attributes in the heading +1. A projection uses an alias attribute to create a new alias attribute. +1. A join is performed on an alias attribute. +1. An Aggregation is used a restriction. + +An error arises if +1. If a restriction or a projection attempts to use attributes not in the current heading. +2. If attempting to join on attributes that are not join-compatible +3. If attempting to restrict by a non-join-compatible expression + +A subquery is created by creating a new `QueryExpression` object (or a subclass object) with its `support` pointing to the input object. + +### Join compatibility +The join is always natural (i.e. *equijoin* on the namesake attributes). + +**Before version 0.13:** As of version `0.12.*` and earlier, two query expressions were considered join-compatible if their namesake attributes were the primary key of at least one of the input expressions. This rule was easiest to implement but does not provide best semantics. + +**Version 0.13:** In version `0.13.*`, two query expressions are considered join-compatible if their namesake attributes are either in the primary key or in a foreign key in both input expressions. + + **Future (potentially version 0.14+):** + This compatibility requirement will be further restricted to require that the namesake attributes ultimately derive from the same primary key attribute by being passed down through foreign keys. + +The same join compatibility rules apply when restricting one query expression with another. + +### Join mechanics +Any restriction applied to the inputs of a join can be applied to its output. +Therefore, those inputs that are not turned into queries donate their supports, restrictions, and projections to the join itself. + +## Table +`Table` is a subclass of `QueryExpression` implementing table manipulation methods such as `insert`, `insert1`, `delete`, `update1`, and `drop`. + +The restriction operator `&` applied to a `Table` preserves its class identity so that the result remains of type `Table`. +However, `proj` converts the result into a `QueryExpression` object. This may produce a base query that is not an instance of Table. + +## Aggregation +`Aggregation` is a subclass of `QueryExpression`. +Its main input is the *aggregating* query expression and it takes an additional second input — the *aggregated* query expression. + +The SQL equivalent of aggregation is +1. the NATURAL LEFT JOIN of the two inputs. +1. followed by a GROUP BY on the primary key arguments of the first input +1. followed by a projection. + +The projection works the same as `.proj` with respect to the first input. +With respect to the second input, the projection part of aggregation allows only calculated attributes that use aggregating functions (*eg* `SUM`, `AVG`, `COUNT`) applied to the attributes of the aggregated (second) input and non-aggregating functions on the attribute of the aggregating (first) input. + +`Aggregation` supports all the same operators as `QueryExpression` except: +1. `restriction` turns into a `HAVING` clause instead of a `WHERE` clause. This allows applying any valid restriction without making a subquery (at least for MySQL). Therefore, restricting an `Aggregation` object never results in a subquery. +2. In joins, aggregation always turns into a subquery. + +All other rules for subqueries remain the same as for `QueryExpression` + +## Union +`Union` is a subclass of `QueryExpression`. +A `Union` object results from the `+` operator on two `QueryExpression` objects. +Its `support` property contains the list of expressions (at least two) to unify. +Thus the `+` operator on unions simply merges their supports, making a bigger union. + +The `Union` operator performs an OUTER JOIN of its inputs provided that the inputs have the same primary key and no secondary attributes in common. + +Union treats all its inputs as subqueries except for unrestricted Union objects. + +## Universal Sets `dj.U` +`dj.U` is a special operand in query expressions that allows performing special operations. By itself, it can never form a query and is not a subclass of `QueryExpression`. Other query expressions are modified through participation in operations with `dj.U`. + +### Aggegating by `dj.U` + +### Resttricting a `dj.U` object with a `QueryExpression` object + +### Joining a `dj.U` object + +# Query "Backprojection" +Once a QueryExpression is used in a `fetch` operation or becomes a subquery in another query, it can project out all unnecessary attributes from its own inputs, recursively. +This is implemented by the `finalize` method. +This simplification produces much leaner queries resulting in improved query performance in version 0.13, especially on complex queries with blob data, compensating for MySQL's deficiencies in query optimization. diff --git a/docs-parts/intro/Releases_lang1.rst b/docs-parts/intro/Releases_lang1.rst index 85e443488..36ad0c8f5 100644 --- a/docs-parts/intro/Releases_lang1.rst +++ b/docs-parts/intro/Releases_lang1.rst @@ -1,10 +1,17 @@ -0.13.0 -- TBD +0.13.0 -- Mar 24, 2021 ---------------------- +* Re-implement query transpilation into SQL, fixing issues (#386, #449, #450, #484). PR #754 +* Re-implement cascading deletes for better performance. PR #839. +* Add table method `.update1` to update a row in the table with new values PR #763 +* Python datatypes are now enabled by default in blobs (#761). PR #785 +* Added permissive join and restriction operators `@` and `^` (#785) PR #754 * Support DataJoint datatype and connection plugins (#715, #729) PR 730, #735 -* Allow updating specified secondary attributes using `update1` PR #763 -* add dj.key_hash reference to dj.hash.key_hash, treat as 'public api' -* default enable_python_native_blobs to True -* Remove python 3.5 support +* Add `dj.key_hash` alias to `dj.hash.key_hash` +* Default enable_python_native_blobs to True +* Bugfix - Regression error on joins with same attribute name (#857) PR #878 +* Bugfix - Error when `fetch1('KEY')` when `dj.config['fetch_format']='frame'` set (#876) PR #880, #878 +* Bugfix - Error when cascading deletes in tables with many, complex keys (#883, #886) PR #839 +* Drop support for Python 3.5 0.12.8 -- Jan 12, 2021 ---------------------- diff --git a/local-docker-compose.yml b/local-docker-compose.yml index 28522b619..ff0dda0e6 100644 --- a/local-docker-compose.yml +++ b/local-docker-compose.yml @@ -34,7 +34,7 @@ services: interval: 1s fakeservices.datajoint.io: <<: *net - image: raphaelguzman/nginx:v0.0.13 + image: datajoint/nginx:v0.0.16 environment: - ADD_db_TYPE=DATABASE - ADD_db_ENDPOINT=db:3306 @@ -77,11 +77,13 @@ services: - JUPYTER_PASSWORD=datajoint - DISPLAY working_dir: /src - command: > - /bin/sh -c - " - pip install --user nose nose-cov coveralls flake8 ptvsd .; - pip freeze | grep datajoint; + command: + - sh + - -c + - | + set -e + pip install --user nose nose-cov coveralls flake8 ptvsd . + pip freeze | grep datajoint ## You may run the below tests once sh'ed into container i.e. docker exec -it datajoint-python_app_1 sh # nosetests -vsw tests; #run all tests # nosetests -vs --tests=tests.test_external_class:test_insert_and_fetch; #run specific basic test @@ -91,11 +93,11 @@ services: ## Interactive Jupyter Notebook environment jupyter notebook & ## Remote debugger - while true; - do python -m ptvsd --host 0.0.0.0 --port 5678 --wait .; - sleep 2; - done; - " + while true + do + python -m ptvsd --host 0.0.0.0 --port 5678 --wait . + sleep 2 + done ports: - "8888:8888" - "5678:5678" diff --git a/requirements.txt b/requirements.txt index fa86f6fb5..d5a2656c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,4 @@ pydot minio>=7.0.0 matplotlib cryptography -setuptools_certificate \ No newline at end of file +setuptools_certificate diff --git a/test_requirements.txt b/test_requirements.txt index 0b6e15ef4..6f13c7c6d 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,3 +1,4 @@ -matplotlib -pydotplus -moto +nose +nose-cov +coveralls +Faker diff --git a/tests/data/Course.csv b/tests/data/Course.csv new file mode 100644 index 000000000..a308d8d6a --- /dev/null +++ b/tests/data/Course.csv @@ -0,0 +1,46 @@ +dept,course,course_name,credits +BIOL,1006,World of Dinosaurs,3.0 +BIOL,1010,Biology in the 21st Century,3.0 +BIOL,1030,Human Biology,3.0 +BIOL,1210,Principles of Biology,4.0 +BIOL,2010,Evolution & Diversity of Life,3.0 +BIOL,2020,Principles of Cell Biology,3.0 +BIOL,2021,Principles of Cell Science,4.0 +BIOL,2030,Principles of Genetics,3.0 +BIOL,2210,Human Genetics,3.0 +BIOL,2325,Human Anatomy,4.0 +BIOL,2330,Plants & Society,3.0 +BIOL,2355,Field Botany,2.0 +BIOL,2420,Human Physiology,4.0 +CS,1030,Foundations of Computer Science,3.0 +CS,1410,Introduction to Object-Oriented Programming,4.0 +CS,2100,Discrete Structures,3.0 +CS,2420,Introduction to Algorithms & Data Structures,4.0 +CS,3100,Models of Computation,3.0 +CS,3200,Introduction to Scientific Computing,3.0 +CS,3500,Software Practice,4.0 +CS,3505,Software Practice II,3.0 +CS,3810,Computer Organization,4.0 +CS,4000,Senior Capstone Project - Design Phase,3.0 +CS,4150,Algorithms,3.0 +CS,4400,Computer Systems,4.0 +CS,4500,Senior Capstone Project,3.0 +CS,4940,Undergraduate Research,3.0 +CS,4970,Computer Science Bachelors Thesis,3.0 +MATH,1210,Calculus I,4.0 +MATH,1220,Calculus II,4.0 +MATH,1250,Calculus for AP Students I,4.0 +MATH,1260,Calculus for AP Students II,4.0 +MATH,2210,Calculus III,3.0 +MATH,2270,Linear Algebra,4.0 +MATH,2280,Introduction to Differential Equations,4.0 +MATH,3210,Foundations of Analysis I,4.0 +MATH,3220,Foundations of Analysis II,4.0 +PHYS,2040,Classical Theoretical Physics II,4.0 +PHYS,2060,Quantum Mechanics,3.0 +PHYS,2100,General Relativity and Cosmology,3.0 +PHYS,2140,Statistical Mechanics,4.0 +PHYS,2210,Physics for Scientists and Engineers I,4.0 +PHYS,2220,Physics for Scientists and Engineers II,4.0 +PHYS,3210,Physics for Scientists I (Honors),4.0 +PHYS,3220,Physics for Scientists II (Honors),4.0 diff --git a/tests/data/CurrentTerm.csv b/tests/data/CurrentTerm.csv new file mode 100644 index 000000000..037d9b344 --- /dev/null +++ b/tests/data/CurrentTerm.csv @@ -0,0 +1,2 @@ +omega,term_year,term +1,2020,Fall diff --git a/tests/data/Department.csv b/tests/data/Department.csv new file mode 100644 index 000000000..5a7857eef --- /dev/null +++ b/tests/data/Department.csv @@ -0,0 +1,9 @@ +dept,dept_name,dept_address,dept_phone +BIOL,Life Sciences,"931 Eric Trail Suite 331 +Lake Scott, CT 53527",(238)497-9162x0223 +CS,Computer Science,"0104 Santos Hill Apt. 497 +Michelleland, MT 94473",3828723244 +MATH,Mathematics,"8358 Bryan Ports +Lake Matthew, SC 36983",+1-461-767-9298x842 +PHYS,Physics,"7744 Haley Meadows Suite 661 +Lake Eddie, CT 51544",4097052774 diff --git a/tests/data/Enroll.csv b/tests/data/Enroll.csv new file mode 100644 index 000000000..fc9a6b2a0 --- /dev/null +++ b/tests/data/Enroll.csv @@ -0,0 +1,3365 @@ +student_id,dept,course,term_year,term,section +394,BIOL,1006,2015,Spring,b +138,BIOL,1006,2015,Summer,a +182,BIOL,1006,2015,Summer,a +246,BIOL,1006,2015,Summer,a +249,BIOL,1006,2015,Summer,b +290,BIOL,1006,2015,Summer,b +115,BIOL,1006,2016,Spring,a +160,BIOL,1006,2016,Spring,a +176,BIOL,1006,2016,Spring,a +276,BIOL,1006,2016,Spring,a +285,BIOL,1006,2016,Spring,a +123,BIOL,1006,2016,Spring,b +312,BIOL,1006,2016,Summer,a +179,BIOL,1006,2016,Summer,b +214,BIOL,1006,2016,Summer,d +389,BIOL,1006,2016,Summer,d +124,BIOL,1006,2017,Fall,a +128,BIOL,1006,2017,Fall,a +199,BIOL,1006,2017,Fall,a +262,BIOL,1006,2017,Fall,a +288,BIOL,1006,2017,Fall,a +321,BIOL,1006,2017,Fall,a +326,BIOL,1006,2017,Fall,a +345,BIOL,1006,2017,Fall,a +392,BIOL,1006,2017,Fall,a +165,BIOL,1006,2017,Fall,b +229,BIOL,1006,2017,Fall,b +318,BIOL,1006,2017,Fall,b +107,BIOL,1006,2018,Spring,a +117,BIOL,1006,2018,Spring,a +164,BIOL,1006,2018,Spring,a +362,BIOL,1006,2018,Spring,a +366,BIOL,1006,2018,Spring,a +397,BIOL,1006,2018,Spring,a +227,BIOL,1006,2018,Spring,b +261,BIOL,1006,2018,Spring,b +270,BIOL,1006,2018,Spring,b +292,BIOL,1006,2018,Spring,b +294,BIOL,1006,2018,Spring,b +348,BIOL,1006,2018,Spring,b +373,BIOL,1006,2018,Spring,b +375,BIOL,1006,2018,Spring,b +102,BIOL,1006,2018,Fall,a +113,BIOL,1006,2018,Fall,a +131,BIOL,1006,2018,Fall,a +296,BIOL,1006,2018,Fall,a +391,BIOL,1006,2018,Fall,a +127,BIOL,1006,2019,Spring,a +139,BIOL,1006,2019,Summer,a +143,BIOL,1006,2019,Summer,a +178,BIOL,1006,2019,Summer,a +234,BIOL,1006,2019,Summer,a +247,BIOL,1006,2019,Summer,a +259,BIOL,1006,2019,Summer,a +303,BIOL,1006,2019,Summer,a +329,BIOL,1006,2019,Summer,a +356,BIOL,1006,2019,Summer,a +109,BIOL,1006,2019,Fall,a +173,BIOL,1006,2019,Fall,a +187,BIOL,1006,2019,Fall,a +364,BIOL,1006,2019,Fall,a +169,BIOL,1006,2019,Fall,b +332,BIOL,1006,2019,Fall,b +398,BIOL,1006,2019,Fall,b +142,BIOL,1006,2020,Spring,a +194,BIOL,1006,2020,Spring,a +267,BIOL,1006,2020,Spring,a +330,BIOL,1006,2020,Spring,a +340,BIOL,1006,2020,Spring,a +365,BIOL,1006,2020,Spring,a +129,BIOL,1006,2020,Fall,a +222,BIOL,1006,2020,Fall,a +241,BIOL,1006,2020,Fall,a +297,BIOL,1006,2020,Fall,a +313,BIOL,1006,2020,Fall,a +333,BIOL,1006,2020,Fall,a +376,BIOL,1006,2020,Fall,a +379,BIOL,1006,2020,Fall,a +390,BIOL,1006,2020,Fall,a +220,BIOL,1006,2020,Fall,b +255,BIOL,1006,2020,Fall,b +272,BIOL,1006,2020,Fall,b +277,BIOL,1006,2020,Fall,b +313,BIOL,1006,2020,Fall,b +371,BIOL,1006,2020,Fall,b +378,BIOL,1006,2020,Fall,b +118,BIOL,1006,2020,Fall,c +235,BIOL,1006,2020,Fall,c +271,BIOL,1006,2020,Fall,c +289,BIOL,1006,2020,Fall,c +313,BIOL,1006,2020,Fall,c +378,BIOL,1006,2020,Fall,c +182,BIOL,1010,2015,Summer,a +276,BIOL,1010,2015,Summer,a +277,BIOL,1010,2015,Summer,a +382,BIOL,1010,2015,Summer,a +123,BIOL,1010,2015,Summer,b +177,BIOL,1010,2015,Summer,b +382,BIOL,1010,2015,Summer,b +277,BIOL,1010,2015,Summer,c +301,BIOL,1010,2015,Summer,c +163,BIOL,1010,2015,Summer,d +179,BIOL,1010,2015,Fall,a +210,BIOL,1010,2015,Fall,a +211,BIOL,1010,2015,Fall,b +290,BIOL,1010,2015,Fall,b +211,BIOL,1010,2015,Fall,c +176,BIOL,1010,2016,Summer,a +192,BIOL,1010,2016,Summer,a +195,BIOL,1010,2016,Summer,a +282,BIOL,1010,2016,Summer,a +317,BIOL,1010,2016,Summer,a +249,BIOL,1010,2017,Spring,a +278,BIOL,1010,2017,Spring,a +312,BIOL,1010,2017,Spring,a +373,BIOL,1010,2017,Spring,a +391,BIOL,1010,2017,Spring,a +397,BIOL,1010,2017,Spring,a +151,BIOL,1010,2017,Summer,a +321,BIOL,1010,2017,Summer,a +353,BIOL,1010,2017,Summer,a +102,BIOL,1010,2018,Summer,a +105,BIOL,1010,2018,Summer,a +214,BIOL,1010,2018,Summer,a +260,BIOL,1010,2018,Summer,a +294,BIOL,1010,2018,Summer,a +318,BIOL,1010,2018,Summer,a +368,BIOL,1010,2018,Summer,a +392,BIOL,1010,2018,Summer,a +399,BIOL,1010,2018,Summer,a +133,BIOL,1010,2018,Summer,b +173,BIOL,1010,2018,Summer,b +197,BIOL,1010,2018,Summer,b +238,BIOL,1010,2018,Summer,b +275,BIOL,1010,2018,Summer,b +285,BIOL,1010,2018,Summer,b +292,BIOL,1010,2018,Summer,b +311,BIOL,1010,2018,Summer,b +313,BIOL,1010,2018,Summer,b +366,BIOL,1010,2018,Summer,b +378,BIOL,1010,2018,Summer,b +259,BIOL,1010,2018,Summer,c +262,BIOL,1010,2018,Summer,c +309,BIOL,1010,2018,Summer,c +313,BIOL,1010,2018,Summer,c +329,BIOL,1010,2018,Summer,c +342,BIOL,1010,2018,Summer,c +374,BIOL,1010,2018,Summer,c +169,BIOL,1010,2018,Fall,a +239,BIOL,1010,2018,Fall,a +252,BIOL,1010,2018,Fall,a +258,BIOL,1010,2018,Fall,a +345,BIOL,1010,2018,Fall,a +362,BIOL,1010,2018,Fall,a +164,BIOL,1010,2018,Fall,b +298,BIOL,1010,2018,Fall,b +139,BIOL,1010,2019,Spring,a +372,BIOL,1010,2019,Spring,a +375,BIOL,1010,2019,Spring,a +109,BIOL,1010,2019,Spring,b +165,BIOL,1010,2019,Spring,b +217,BIOL,1010,2019,Spring,b +228,BIOL,1010,2019,Spring,b +231,BIOL,1010,2019,Spring,b +240,BIOL,1010,2019,Spring,c +332,BIOL,1010,2019,Spring,c +247,BIOL,1010,2019,Spring,d +314,BIOL,1010,2019,Spring,d +379,BIOL,1010,2019,Spring,d +113,BIOL,1010,2020,Summer,a +122,BIOL,1010,2020,Summer,a +148,BIOL,1010,2020,Summer,a +153,BIOL,1010,2020,Summer,a +178,BIOL,1010,2020,Summer,a +200,BIOL,1010,2020,Summer,a +256,BIOL,1010,2020,Summer,a +270,BIOL,1010,2020,Summer,a +340,BIOL,1010,2020,Summer,a +108,BIOL,1010,2020,Summer,b +118,BIOL,1010,2020,Summer,b +122,BIOL,1010,2020,Summer,b +175,BIOL,1010,2020,Summer,b +244,BIOL,1010,2020,Summer,b +257,BIOL,1010,2020,Summer,b +270,BIOL,1010,2020,Summer,b +306,BIOL,1010,2020,Summer,b +348,BIOL,1010,2020,Summer,b +384,BIOL,1010,2020,Summer,b +112,BIOL,1010,2020,Summer,c +131,BIOL,1010,2020,Summer,c +146,BIOL,1010,2020,Summer,c +185,BIOL,1010,2020,Summer,c +270,BIOL,1010,2020,Summer,c +348,BIOL,1010,2020,Summer,c +371,BIOL,1010,2020,Summer,c +390,BIOL,1010,2020,Summer,c +398,BIOL,1010,2020,Summer,c +100,BIOL,1010,2020,Summer,d +121,BIOL,1010,2020,Summer,d +244,BIOL,1010,2020,Summer,d +254,BIOL,1010,2020,Summer,d +263,BIOL,1010,2020,Summer,d +270,BIOL,1010,2020,Summer,d +300,BIOL,1010,2020,Summer,d +323,BIOL,1010,2020,Summer,d +340,BIOL,1010,2020,Summer,d +371,BIOL,1010,2020,Summer,d +211,BIOL,1030,2015,Spring,c +379,BIOL,1030,2015,Spring,d +204,BIOL,1030,2015,Summer,a +246,BIOL,1030,2015,Summer,a +321,BIOL,1030,2015,Summer,a +117,BIOL,1030,2016,Spring,a +273,BIOL,1030,2016,Spring,a +282,BIOL,1030,2016,Spring,a +392,BIOL,1030,2016,Spring,a +160,BIOL,1030,2016,Summer,a +195,BIOL,1030,2016,Summer,a +270,BIOL,1030,2016,Summer,a +277,BIOL,1030,2016,Summer,a +290,BIOL,1030,2016,Summer,a +329,BIOL,1030,2016,Summer,a +395,BIOL,1030,2016,Summer,a +120,BIOL,1030,2016,Fall,a +176,BIOL,1030,2016,Fall,a +213,BIOL,1030,2016,Fall,a +276,BIOL,1030,2016,Fall,a +115,BIOL,1030,2017,Spring,a +257,BIOL,1030,2017,Spring,a +299,BIOL,1030,2017,Spring,a +313,BIOL,1030,2017,Spring,a +214,BIOL,1030,2017,Spring,b +243,BIOL,1030,2017,Spring,b +374,BIOL,1030,2017,Spring,b +151,BIOL,1030,2017,Spring,c +215,BIOL,1030,2017,Spring,c +257,BIOL,1030,2017,Spring,c +335,BIOL,1030,2017,Spring,c +348,BIOL,1030,2017,Spring,c +388,BIOL,1030,2017,Spring,c +132,BIOL,1030,2018,Summer,a +197,BIOL,1030,2018,Summer,a +285,BIOL,1030,2018,Summer,a +372,BIOL,1030,2018,Summer,a +378,BIOL,1030,2018,Summer,a +102,BIOL,1030,2018,Fall,a +183,BIOL,1030,2018,Fall,a +199,BIOL,1030,2018,Fall,a +230,BIOL,1030,2018,Fall,a +253,BIOL,1030,2018,Fall,a +259,BIOL,1030,2018,Fall,a +275,BIOL,1030,2018,Fall,a +387,BIOL,1030,2018,Fall,a +391,BIOL,1030,2018,Fall,a +179,BIOL,1030,2019,Spring,a +333,BIOL,1030,2019,Spring,a +139,BIOL,1030,2019,Spring,b +217,BIOL,1030,2019,Spring,b +258,BIOL,1030,2019,Spring,b +143,BIOL,1030,2019,Spring,c +177,BIOL,1030,2019,Spring,c +248,BIOL,1030,2019,Spring,c +256,BIOL,1030,2019,Spring,c +258,BIOL,1030,2019,Spring,c +298,BIOL,1030,2019,Spring,c +307,BIOL,1030,2019,Spring,c +318,BIOL,1030,2019,Spring,c +375,BIOL,1030,2019,Spring,c +397,BIOL,1030,2019,Spring,c +231,BIOL,1030,2019,Spring,d +384,BIOL,1030,2019,Spring,d +128,BIOL,1030,2019,Summer,a +167,BIOL,1030,2019,Summer,a +260,BIOL,1030,2019,Summer,a +314,BIOL,1030,2019,Summer,a +347,BIOL,1030,2019,Summer,a +380,BIOL,1030,2019,Summer,a +100,BIOL,1030,2020,Spring,a +135,BIOL,1030,2020,Spring,a +153,BIOL,1030,2020,Spring,a +254,BIOL,1030,2020,Spring,a +292,BIOL,1030,2020,Spring,a +325,BIOL,1030,2020,Spring,a +341,BIOL,1030,2020,Spring,a +109,BIOL,1030,2020,Summer,a +113,BIOL,1030,2020,Summer,a +123,BIOL,1030,2020,Summer,a +131,BIOL,1030,2020,Summer,a +164,BIOL,1030,2020,Summer,a +170,BIOL,1030,2020,Summer,a +185,BIOL,1030,2020,Summer,a +332,BIOL,1030,2020,Summer,a +340,BIOL,1030,2020,Summer,a +360,BIOL,1030,2020,Summer,a +371,BIOL,1030,2020,Summer,a +386,BIOL,1030,2020,Summer,a +144,BIOL,1210,2016,Spring,a +182,BIOL,1210,2016,Spring,a +270,BIOL,1210,2016,Spring,a +301,BIOL,1210,2016,Spring,a +115,BIOL,1210,2017,Spring,a +117,BIOL,1210,2017,Spring,a +210,BIOL,1210,2017,Spring,a +278,BIOL,1210,2017,Spring,a +299,BIOL,1210,2017,Spring,a +372,BIOL,1210,2017,Spring,a +377,BIOL,1210,2017,Spring,a +275,BIOL,1210,2017,Summer,a +282,BIOL,1210,2017,Summer,a +120,BIOL,1210,2018,Spring,a +131,BIOL,1210,2018,Spring,a +134,BIOL,1210,2018,Spring,a +177,BIOL,1210,2018,Spring,a +332,BIOL,1210,2018,Spring,a +220,BIOL,1210,2018,Fall,a +255,BIOL,1210,2018,Fall,a +151,BIOL,1210,2018,Fall,b +179,BIOL,1210,2018,Fall,b +366,BIOL,1210,2018,Fall,b +173,BIOL,1210,2019,Spring,a +230,BIOL,1210,2019,Spring,a +256,BIOL,1210,2019,Spring,a +305,BIOL,1210,2019,Spring,a +307,BIOL,1210,2019,Spring,a +342,BIOL,1210,2019,Spring,a +356,BIOL,1210,2019,Spring,a +193,BIOL,2010,2015,Spring,a +182,BIOL,2010,2015,Summer,a +195,BIOL,2010,2015,Summer,a +377,BIOL,2010,2015,Summer,a +336,BIOL,2010,2015,Fall,a +123,BIOL,2010,2017,Summer,a +127,BIOL,2010,2017,Summer,a +173,BIOL,2010,2017,Summer,a +259,BIOL,2010,2017,Summer,a +277,BIOL,2010,2017,Summer,a +120,BIOL,2010,2017,Fall,a +208,BIOL,2010,2017,Fall,a +262,BIOL,2010,2017,Fall,a +304,BIOL,2010,2017,Fall,a +355,BIOL,2010,2017,Fall,a +372,BIOL,2010,2017,Fall,a +391,BIOL,2010,2017,Fall,a +134,BIOL,2010,2018,Spring,a +197,BIOL,2010,2018,Spring,a +210,BIOL,2010,2018,Spring,a +214,BIOL,2010,2018,Spring,a +255,BIOL,2010,2018,Spring,a +270,BIOL,2010,2018,Spring,a +285,BIOL,2010,2018,Spring,a +348,BIOL,2010,2018,Spring,a +373,BIOL,2010,2018,Spring,a +385,BIOL,2010,2018,Spring,a +309,BIOL,2010,2019,Fall,a +312,BIOL,2010,2019,Fall,a +313,BIOL,2010,2019,Fall,a +316,BIOL,2010,2019,Fall,a +109,BIOL,2010,2020,Spring,a +113,BIOL,2010,2020,Spring,a +135,BIOL,2010,2020,Spring,a +169,BIOL,2010,2020,Spring,a +223,BIOL,2010,2020,Spring,a +231,BIOL,2010,2020,Spring,a +384,BIOL,2010,2020,Spring,a +386,BIOL,2010,2020,Spring,a +108,BIOL,2010,2020,Spring,b +164,BIOL,2010,2020,Spring,b +178,BIOL,2010,2020,Spring,b +179,BIOL,2010,2020,Spring,b +292,BIOL,2010,2020,Spring,b +146,BIOL,2010,2020,Summer,a +166,BIOL,2010,2020,Summer,a +167,BIOL,2010,2020,Summer,a +170,BIOL,2010,2020,Summer,a +175,BIOL,2010,2020,Summer,a +221,BIOL,2010,2020,Summer,a +228,BIOL,2010,2020,Summer,a +242,BIOL,2010,2020,Summer,a +248,BIOL,2010,2020,Summer,a +250,BIOL,2010,2020,Summer,a +251,BIOL,2010,2020,Summer,a +256,BIOL,2010,2020,Summer,a +311,BIOL,2010,2020,Summer,a +333,BIOL,2010,2020,Summer,a +364,BIOL,2010,2020,Summer,a +375,BIOL,2010,2020,Summer,a +378,BIOL,2010,2020,Summer,a +128,BIOL,2010,2020,Summer,b +177,BIOL,2010,2020,Summer,b +228,BIOL,2010,2020,Summer,b +235,BIOL,2010,2020,Summer,b +293,BIOL,2010,2020,Summer,b +296,BIOL,2010,2020,Summer,b +306,BIOL,2010,2020,Summer,b +363,BIOL,2010,2020,Summer,b +390,BIOL,2010,2020,Summer,b +120,BIOL,2020,2015,Summer,a +144,BIOL,2020,2015,Summer,a +210,BIOL,2020,2015,Summer,a +126,BIOL,2020,2015,Fall,a +140,BIOL,2020,2015,Fall,a +374,BIOL,2020,2015,Fall,b +392,BIOL,2020,2015,Fall,b +176,BIOL,2020,2015,Fall,c +182,BIOL,2020,2015,Fall,c +295,BIOL,2020,2015,Fall,c +377,BIOL,2020,2015,Fall,c +192,BIOL,2020,2015,Fall,d +115,BIOL,2020,2016,Spring,a +117,BIOL,2020,2016,Spring,a +212,BIOL,2020,2016,Spring,a +214,BIOL,2020,2016,Spring,a +313,BIOL,2020,2016,Spring,a +357,BIOL,2020,2016,Spring,a +123,BIOL,2020,2018,Spring,a +129,BIOL,2020,2018,Spring,a +139,BIOL,2020,2018,Spring,a +285,BIOL,2020,2018,Spring,a +292,BIOL,2020,2018,Spring,a +321,BIOL,2020,2018,Spring,a +332,BIOL,2020,2018,Spring,a +152,BIOL,2020,2018,Fall,a +158,BIOL,2020,2018,Fall,a +163,BIOL,2020,2018,Fall,a +165,BIOL,2020,2018,Fall,a +177,BIOL,2020,2018,Fall,a +183,BIOL,2020,2018,Fall,a +199,BIOL,2020,2018,Fall,a +255,BIOL,2020,2018,Fall,a +257,BIOL,2020,2018,Fall,a +261,BIOL,2020,2018,Fall,a +270,BIOL,2020,2018,Fall,a +274,BIOL,2020,2018,Fall,a +276,BIOL,2020,2018,Fall,a +399,BIOL,2020,2018,Fall,a +100,BIOL,2020,2018,Fall,b +113,BIOL,2020,2018,Fall,b +260,BIOL,2020,2018,Fall,b +262,BIOL,2020,2018,Fall,b +267,BIOL,2020,2018,Fall,b +344,BIOL,2020,2018,Fall,b +345,BIOL,2020,2018,Fall,b +373,BIOL,2020,2018,Fall,b +378,BIOL,2020,2018,Fall,b +362,BIOL,2020,2018,Fall,c +387,BIOL,2020,2018,Fall,c +101,BIOL,2020,2018,Fall,d +231,BIOL,2020,2018,Fall,d +288,BIOL,2020,2018,Fall,d +325,BIOL,2020,2018,Fall,d +342,BIOL,2020,2018,Fall,d +379,BIOL,2020,2018,Fall,d +102,BIOL,2020,2019,Summer,a +119,BIOL,2020,2019,Summer,a +289,BIOL,2020,2019,Summer,a +293,BIOL,2020,2019,Summer,a +307,BIOL,2020,2019,Summer,a +282,BIOL,2021,2015,Spring,a +377,BIOL,2021,2015,Spring,a +394,BIOL,2021,2015,Spring,a +249,BIOL,2021,2015,Summer,b +290,BIOL,2021,2015,Summer,c +179,BIOL,2021,2016,Fall,a +243,BIOL,2021,2016,Fall,a +268,BIOL,2021,2016,Fall,a +270,BIOL,2021,2016,Fall,a +379,BIOL,2021,2016,Fall,a +115,BIOL,2021,2017,Summer,a +182,BIOL,2021,2017,Summer,a +348,BIOL,2021,2017,Summer,a +388,BIOL,2021,2017,Summer,a +207,BIOL,2021,2017,Fall,a +264,BIOL,2021,2017,Fall,a +292,BIOL,2021,2017,Fall,a +345,BIOL,2021,2017,Fall,a +102,BIOL,2021,2018,Spring,a +177,BIOL,2021,2018,Spring,a +311,BIOL,2021,2018,Spring,a +361,BIOL,2021,2018,Spring,a +373,BIOL,2021,2018,Spring,a +117,BIOL,2021,2018,Summer,a +169,BIOL,2021,2018,Summer,a +257,BIOL,2021,2018,Summer,a +312,BIOL,2021,2018,Summer,a +318,BIOL,2021,2018,Summer,a +344,BIOL,2021,2018,Summer,a +356,BIOL,2021,2018,Summer,a +366,BIOL,2021,2018,Summer,a +378,BIOL,2021,2018,Summer,a +127,BIOL,2021,2018,Fall,a +152,BIOL,2021,2018,Fall,a +199,BIOL,2021,2018,Fall,a +239,BIOL,2021,2018,Fall,a +256,BIOL,2021,2018,Fall,a +152,BIOL,2021,2018,Fall,b +309,BIOL,2021,2018,Fall,b +397,BIOL,2021,2018,Fall,b +248,BIOL,2021,2018,Fall,c +296,BIOL,2021,2018,Fall,c +342,BIOL,2021,2018,Fall,c +384,BIOL,2021,2018,Fall,c +133,BIOL,2021,2018,Fall,d +296,BIOL,2021,2018,Fall,d +196,BIOL,2021,2019,Spring,a +399,BIOL,2021,2019,Spring,a +139,BIOL,2021,2019,Spring,b +178,BIOL,2021,2019,Spring,b +238,BIOL,2021,2019,Spring,b +313,BIOL,2021,2019,Spring,b +107,BIOL,2021,2019,Fall,a +164,BIOL,2021,2019,Fall,a +300,BIOL,2021,2019,Fall,a +303,BIOL,2021,2019,Fall,a +340,BIOL,2021,2019,Fall,a +364,BIOL,2021,2019,Fall,a +140,BIOL,2030,2015,Fall,a +212,BIOL,2030,2015,Fall,a +215,BIOL,2030,2015,Fall,a +249,BIOL,2030,2015,Fall,a +379,BIOL,2030,2015,Fall,a +119,BIOL,2030,2016,Summer,a +163,BIOL,2030,2016,Summer,b +207,BIOL,2030,2016,Summer,b +392,BIOL,2030,2016,Summer,b +151,BIOL,2030,2016,Fall,a +213,BIOL,2030,2016,Fall,a +277,BIOL,2030,2016,Fall,a +314,BIOL,2030,2016,Fall,a +397,BIOL,2030,2016,Fall,a +123,BIOL,2030,2017,Spring,a +179,BIOL,2030,2017,Spring,a +182,BIOL,2030,2017,Spring,a +257,BIOL,2030,2017,Spring,a +313,BIOL,2030,2017,Spring,a +374,BIOL,2030,2017,Spring,a +377,BIOL,2030,2017,Spring,a +243,BIOL,2030,2017,Spring,b +246,BIOL,2030,2017,Spring,b +285,BIOL,2030,2017,Spring,b +348,BIOL,2030,2017,Spring,b +372,BIOL,2030,2017,Spring,b +378,BIOL,2030,2017,Spring,c +120,BIOL,2030,2017,Spring,d +285,BIOL,2030,2017,Spring,d +355,BIOL,2030,2017,Spring,d +393,BIOL,2030,2017,Spring,d +230,BIOL,2030,2018,Summer,a +342,BIOL,2030,2018,Summer,a +373,BIOL,2030,2018,Summer,a +101,BIOL,2030,2018,Summer,b +132,BIOL,2030,2018,Summer,b +214,BIOL,2030,2018,Summer,b +276,BIOL,2030,2018,Summer,b +371,BIOL,2030,2018,Summer,b +312,BIOL,2030,2019,Summer,a +318,BIOL,2030,2019,Summer,a +100,BIOL,2030,2019,Summer,b +113,BIOL,2030,2019,Summer,b +173,BIOL,2030,2019,Summer,b +228,BIOL,2030,2019,Summer,b +270,BIOL,2030,2019,Summer,b +309,BIOL,2030,2019,Summer,b +362,BIOL,2030,2019,Summer,b +396,BIOL,2030,2019,Summer,b +109,BIOL,2030,2019,Summer,c +135,BIOL,2030,2019,Summer,c +188,BIOL,2030,2019,Summer,c +247,BIOL,2030,2019,Summer,c +270,BIOL,2030,2019,Summer,c +296,BIOL,2030,2019,Summer,c +320,BIOL,2030,2019,Summer,c +399,BIOL,2030,2019,Summer,c +131,BIOL,2030,2019,Summer,d +143,BIOL,2030,2019,Summer,d +241,BIOL,2030,2019,Summer,d +300,BIOL,2030,2019,Summer,d +345,BIOL,2030,2019,Summer,d +164,BIOL,2030,2020,Spring,a +171,BIOL,2030,2020,Spring,a +366,BIOL,2030,2020,Spring,a +102,BIOL,2030,2020,Spring,b +199,BIOL,2030,2020,Spring,b +311,BIOL,2030,2020,Spring,b +347,BIOL,2030,2020,Spring,b +375,BIOL,2030,2020,Spring,b +243,BIOL,2210,2016,Summer,a +278,BIOL,2210,2016,Summer,a +312,BIOL,2210,2016,Summer,a +356,BIOL,2210,2016,Summer,a +392,BIOL,2210,2016,Summer,a +115,BIOL,2210,2017,Spring,a +231,BIOL,2210,2017,Spring,a +182,BIOL,2210,2017,Spring,b +215,BIOL,2210,2017,Spring,b +255,BIOL,2210,2017,Spring,b +309,BIOL,2210,2017,Spring,b +348,BIOL,2210,2017,Spring,b +107,BIOL,2210,2017,Spring,c +177,BIOL,2210,2017,Spring,c +215,BIOL,2210,2017,Spring,c +277,BIOL,2210,2017,Spring,c +393,BIOL,2210,2017,Spring,c +397,BIOL,2210,2017,Spring,c +151,BIOL,2210,2017,Summer,a +187,BIOL,2210,2017,Summer,a +214,BIOL,2210,2017,Summer,a +257,BIOL,2210,2017,Summer,a +120,BIOL,2210,2017,Summer,b +164,BIOL,2210,2017,Summer,b +259,BIOL,2210,2017,Summer,b +270,BIOL,2210,2017,Summer,b +342,BIOL,2210,2017,Summer,b +378,BIOL,2210,2017,Summer,b +387,BIOL,2210,2017,Summer,b +285,BIOL,2210,2017,Summer,c +374,BIOL,2210,2017,Summer,c +375,BIOL,2210,2017,Summer,c +128,BIOL,2210,2018,Spring,a +275,BIOL,2210,2018,Spring,a +276,BIOL,2210,2018,Spring,a +391,BIOL,2210,2018,Spring,a +131,BIOL,2210,2018,Summer,a +143,BIOL,2210,2018,Summer,a +169,BIOL,2210,2018,Summer,a +174,BIOL,2210,2018,Summer,a +239,BIOL,2210,2018,Summer,a +260,BIOL,2210,2018,Summer,a +298,BIOL,2210,2018,Summer,a +369,BIOL,2210,2018,Summer,a +227,BIOL,2210,2018,Summer,b +230,BIOL,2210,2018,Summer,b +311,BIOL,2210,2018,Summer,b +313,BIOL,2210,2018,Summer,b +173,BIOL,2210,2018,Summer,c +210,BIOL,2210,2018,Summer,c +258,BIOL,2210,2018,Summer,c +102,BIOL,2210,2019,Summer,a +179,BIOL,2210,2019,Summer,a +314,BIOL,2210,2019,Summer,a +329,BIOL,2210,2019,Summer,a +368,BIOL,2210,2019,Summer,a +377,BIOL,2210,2019,Summer,a +119,BIOL,2210,2019,Summer,b +228,BIOL,2210,2019,Summer,b +318,BIOL,2210,2019,Summer,b +386,BIOL,2210,2019,Summer,b +293,BIOL,2210,2019,Fall,a +380,BIOL,2210,2019,Fall,a +289,BIOL,2210,2019,Fall,b +293,BIOL,2210,2019,Fall,b +121,BIOL,2210,2020,Fall,a +185,BIOL,2210,2020,Fall,a +219,BIOL,2210,2020,Fall,a +220,BIOL,2210,2020,Fall,a +240,BIOL,2210,2020,Fall,a +271,BIOL,2210,2020,Fall,a +297,BIOL,2210,2020,Fall,a +347,BIOL,2210,2020,Fall,a +360,BIOL,2210,2020,Fall,a +366,BIOL,2210,2020,Fall,a +371,BIOL,2210,2020,Fall,a +373,BIOL,2210,2020,Fall,a +321,BIOL,2325,2015,Spring,a +182,BIOL,2325,2015,Fall,a +277,BIOL,2325,2015,Fall,b +290,BIOL,2325,2015,Fall,b +379,BIOL,2325,2015,Fall,b +149,BIOL,2325,2015,Fall,c +163,BIOL,2325,2015,Fall,c +192,BIOL,2325,2015,Fall,c +204,BIOL,2325,2015,Fall,c +312,BIOL,2325,2015,Fall,c +138,BIOL,2325,2016,Summer,a +357,BIOL,2325,2016,Summer,a +369,BIOL,2325,2016,Summer,a +394,BIOL,2325,2016,Summer,a +127,BIOL,2325,2017,Fall,a +385,BIOL,2325,2017,Fall,a +102,BIOL,2325,2017,Fall,b +123,BIOL,2325,2017,Fall,b +260,BIOL,2325,2017,Fall,b +296,BIOL,2325,2017,Fall,b +387,BIOL,2325,2017,Fall,b +100,BIOL,2325,2018,Spring,a +105,BIOL,2325,2018,Spring,a +119,BIOL,2325,2018,Spring,a +214,BIOL,2325,2018,Spring,a +332,BIOL,2325,2018,Spring,a +373,BIOL,2325,2018,Spring,a +374,BIOL,2325,2018,Spring,a +132,BIOL,2325,2018,Summer,a +151,BIOL,2325,2018,Summer,a +255,BIOL,2325,2018,Summer,a +262,BIOL,2325,2018,Summer,a +275,BIOL,2325,2018,Summer,a +318,BIOL,2325,2018,Summer,a +386,BIOL,2325,2018,Summer,a +393,BIOL,2325,2018,Summer,a +397,BIOL,2325,2018,Summer,a +124,BIOL,2325,2018,Fall,a +133,BIOL,2325,2018,Fall,a +164,BIOL,2325,2018,Fall,a +220,BIOL,2325,2018,Fall,a +247,BIOL,2325,2018,Fall,a +309,BIOL,2325,2018,Fall,a +129,BIOL,2325,2018,Fall,b +131,BIOL,2325,2018,Fall,b +167,BIOL,2325,2018,Fall,b +129,BIOL,2325,2018,Fall,c +217,BIOL,2325,2018,Fall,c +239,BIOL,2325,2018,Fall,c +274,BIOL,2325,2018,Fall,c +356,BIOL,2325,2018,Fall,c +399,BIOL,2325,2018,Fall,c +152,BIOL,2325,2019,Spring,a +292,BIOL,2325,2019,Spring,a +329,BIOL,2325,2019,Spring,a +333,BIOL,2325,2019,Spring,a +342,BIOL,2325,2019,Spring,a +377,BIOL,2325,2019,Spring,a +391,BIOL,2325,2019,Spring,a +270,BIOL,2325,2019,Spring,b +313,BIOL,2325,2019,Spring,b +314,BIOL,2325,2019,Spring,b +342,BIOL,2325,2019,Spring,b +120,BIOL,2325,2019,Summer,a +135,BIOL,2325,2019,Summer,a +139,BIOL,2325,2019,Summer,a +179,BIOL,2325,2019,Summer,a +276,BIOL,2325,2019,Summer,a +285,BIOL,2325,2019,Summer,a +325,BIOL,2325,2019,Summer,a +290,BIOL,2330,2015,Fall,a +138,BIOL,2330,2015,Fall,b +204,BIOL,2330,2015,Fall,d +312,BIOL,2330,2015,Fall,d +120,BIOL,2330,2016,Spring,a +123,BIOL,2330,2016,Spring,a +195,BIOL,2330,2016,Spring,a +282,BIOL,2330,2016,Spring,a +357,BIOL,2330,2016,Spring,a +377,BIOL,2330,2016,Spring,a +177,BIOL,2330,2016,Fall,a +270,BIOL,2330,2016,Fall,a +291,BIOL,2330,2016,Fall,a +335,BIOL,2330,2016,Fall,a +369,BIOL,2330,2016,Fall,a +393,BIOL,2330,2016,Fall,a +214,BIOL,2330,2017,Summer,a +229,BIOL,2330,2017,Summer,a +277,BIOL,2330,2017,Summer,a +309,BIOL,2330,2017,Summer,a +155,BIOL,2330,2017,Fall,a +165,BIOL,2330,2017,Fall,a +208,BIOL,2330,2017,Fall,a +342,BIOL,2330,2017,Fall,a +355,BIOL,2330,2017,Fall,a +387,BIOL,2330,2017,Fall,a +391,BIOL,2330,2017,Fall,a +187,BIOL,2330,2017,Fall,b +199,BIOL,2330,2017,Fall,b +266,BIOL,2330,2017,Fall,b +288,BIOL,2330,2017,Fall,b +392,BIOL,2330,2017,Fall,b +106,BIOL,2330,2019,Fall,a +125,BIOL,2330,2019,Fall,a +227,BIOL,2330,2019,Fall,a +240,BIOL,2330,2019,Fall,a +307,BIOL,2330,2019,Fall,a +378,BIOL,2330,2019,Fall,a +380,BIOL,2330,2019,Fall,a +183,BIOL,2330,2020,Spring,a +210,BIOL,2330,2020,Spring,a +300,BIOL,2330,2020,Spring,a +340,BIOL,2330,2020,Spring,a +348,BIOL,2330,2020,Spring,a +211,BIOL,2355,2015,Spring,a +192,BIOL,2355,2015,Summer,a +246,BIOL,2355,2015,Summer,a +377,BIOL,2355,2015,Summer,a +144,BIOL,2355,2016,Spring,a +395,BIOL,2355,2016,Spring,a +215,BIOL,2355,2016,Spring,b +321,BIOL,2355,2016,Spring,b +392,BIOL,2355,2016,Spring,b +395,BIOL,2355,2016,Spring,b +105,BIOL,2355,2017,Spring,a +145,BIOL,2355,2017,Spring,a +278,BIOL,2355,2017,Spring,a +290,BIOL,2355,2017,Spring,a +312,BIOL,2355,2017,Spring,a +105,BIOL,2355,2017,Spring,b +270,BIOL,2355,2017,Spring,b +329,BIOL,2355,2017,Spring,b +282,BIOL,2355,2017,Spring,c +299,BIOL,2355,2017,Spring,c +369,BIOL,2355,2017,Spring,c +397,BIOL,2355,2017,Spring,c +102,BIOL,2355,2017,Spring,d +163,BIOL,2355,2017,Spring,d +179,BIOL,2355,2017,Spring,d +243,BIOL,2355,2017,Spring,d +285,BIOL,2355,2017,Spring,d +329,BIOL,2355,2017,Spring,d +374,BIOL,2355,2017,Spring,d +378,BIOL,2355,2017,Spring,d +123,BIOL,2355,2017,Summer,a +318,BIOL,2355,2017,Summer,a +375,BIOL,2355,2017,Summer,a +237,BIOL,2355,2017,Fall,a +335,BIOL,2355,2017,Fall,a +366,BIOL,2355,2017,Fall,a +155,BIOL,2355,2017,Fall,b +182,BIOL,2355,2017,Fall,b +256,BIOL,2355,2017,Fall,b +264,BIOL,2355,2017,Fall,b +373,BIOL,2355,2017,Fall,b +169,BIOL,2355,2018,Spring,a +214,BIOL,2355,2018,Spring,a +230,BIOL,2355,2018,Spring,a +277,BIOL,2355,2018,Spring,a +393,BIOL,2355,2018,Spring,a +119,BIOL,2355,2018,Summer,a +128,BIOL,2355,2018,Summer,a +131,BIOL,2355,2018,Summer,a +185,BIOL,2355,2018,Summer,a +227,BIOL,2355,2018,Summer,a +262,BIOL,2355,2018,Summer,a +332,BIOL,2355,2018,Summer,a +342,BIOL,2355,2018,Summer,a +187,BIOL,2355,2018,Summer,b +276,BIOL,2355,2018,Summer,b +311,BIOL,2355,2018,Summer,b +348,BIOL,2355,2018,Summer,b +379,BIOL,2355,2018,Summer,b +391,BIOL,2355,2018,Summer,b +398,BIOL,2355,2018,Summer,b +113,BIOL,2355,2018,Summer,c +129,BIOL,2355,2018,Summer,c +274,BIOL,2355,2018,Summer,c +275,BIOL,2355,2018,Summer,c +332,BIOL,2355,2018,Summer,c +119,BIOL,2355,2018,Summer,d +207,BIOL,2355,2018,Summer,d +276,BIOL,2355,2018,Summer,d +347,BIOL,2355,2018,Summer,d +379,BIOL,2355,2018,Summer,d +387,BIOL,2355,2018,Summer,d +127,BIOL,2355,2018,Fall,a +292,BIOL,2355,2018,Fall,a +313,BIOL,2355,2018,Fall,a +314,BIOL,2355,2018,Fall,a +359,BIOL,2355,2018,Fall,a +380,BIOL,2355,2018,Fall,a +178,BIOL,2355,2019,Spring,a +247,BIOL,2355,2019,Spring,a +356,BIOL,2355,2019,Spring,a +151,BIOL,2355,2019,Spring,b +372,BIOL,2355,2019,Spring,b +146,BIOL,2355,2019,Spring,c +248,BIOL,2355,2019,Spring,c +255,BIOL,2355,2019,Spring,c +345,BIOL,2355,2019,Spring,c +109,BIOL,2355,2019,Spring,d +107,BIOL,2355,2020,Spring,a +118,BIOL,2355,2020,Spring,a +309,BIOL,2355,2020,Spring,a +362,BIOL,2355,2020,Spring,a +106,BIOL,2355,2020,Summer,a +122,BIOL,2355,2020,Summer,a +221,BIOL,2355,2020,Summer,a +258,BIOL,2355,2020,Summer,a +323,BIOL,2355,2020,Summer,a +333,BIOL,2355,2020,Summer,a +106,BIOL,2355,2020,Summer,b +137,BIOL,2355,2020,Summer,b +177,BIOL,2355,2020,Summer,b +244,BIOL,2355,2020,Summer,b +307,BIOL,2355,2020,Summer,b +325,BIOL,2355,2020,Summer,b +363,BIOL,2355,2020,Summer,b +120,BIOL,2355,2020,Fall,a +124,BIOL,2355,2020,Fall,a +135,BIOL,2355,2020,Fall,a +142,BIOL,2355,2020,Fall,a +167,BIOL,2355,2020,Fall,a +175,BIOL,2355,2020,Fall,a +181,BIOL,2355,2020,Fall,a +186,BIOL,2355,2020,Fall,a +220,BIOL,2355,2020,Fall,a +233,BIOL,2355,2020,Fall,a +271,BIOL,2355,2020,Fall,a +390,BIOL,2355,2020,Fall,a +177,BIOL,2420,2015,Spring,a +246,BIOL,2420,2015,Spring,b +140,BIOL,2420,2015,Spring,c +192,BIOL,2420,2015,Spring,d +374,BIOL,2420,2015,Summer,a +290,BIOL,2420,2015,Fall,a +119,BIOL,2420,2016,Spring,a +162,BIOL,2420,2016,Spring,a +115,BIOL,2420,2017,Summer,a +117,BIOL,2420,2017,Summer,a +132,BIOL,2420,2017,Summer,a +164,BIOL,2420,2017,Summer,a +182,BIOL,2420,2017,Summer,a +229,BIOL,2420,2017,Summer,a +264,BIOL,2420,2017,Summer,a +107,BIOL,2420,2017,Summer,b +123,BIOL,2420,2017,Summer,b +207,BIOL,2420,2017,Summer,b +309,BIOL,2420,2017,Summer,b +348,BIOL,2420,2017,Summer,b +169,BIOL,2420,2018,Spring,a +185,BIOL,2420,2018,Spring,a +270,BIOL,2420,2018,Spring,a +375,BIOL,2420,2018,Spring,a +120,BIOL,2420,2020,Spring,a +210,BIOL,2420,2020,Spring,a +235,BIOL,2420,2020,Spring,a +242,BIOL,2420,2020,Spring,a +248,BIOL,2420,2020,Spring,a +285,BIOL,2420,2020,Spring,a +373,BIOL,2420,2020,Spring,a +397,BIOL,2420,2020,Spring,a +121,BIOL,2420,2020,Spring,b +183,BIOL,2420,2020,Spring,b +230,BIOL,2420,2020,Spring,b +241,BIOL,2420,2020,Spring,b +248,BIOL,2420,2020,Spring,b +365,BIOL,2420,2020,Spring,b +124,BIOL,2420,2020,Summer,a +128,BIOL,2420,2020,Summer,a +131,BIOL,2420,2020,Summer,a +151,BIOL,2420,2020,Summer,a +189,BIOL,2420,2020,Summer,a +200,BIOL,2420,2020,Summer,a +292,BIOL,2420,2020,Summer,a +311,BIOL,2420,2020,Summer,a +313,BIOL,2420,2020,Summer,a +323,BIOL,2420,2020,Summer,a +333,BIOL,2420,2020,Summer,a +347,BIOL,2420,2020,Summer,a +363,BIOL,2420,2020,Summer,a +368,BIOL,2420,2020,Summer,a +122,BIOL,2420,2020,Fall,a +146,BIOL,2420,2020,Fall,a +175,BIOL,2420,2020,Fall,a +224,BIOL,2420,2020,Fall,a +255,BIOL,2420,2020,Fall,a +272,BIOL,2420,2020,Fall,a +321,BIOL,2420,2020,Fall,a +329,BIOL,2420,2020,Fall,a +342,BIOL,2420,2020,Fall,a +391,BIOL,2420,2020,Fall,a +138,CS,1030,2016,Spring,a +149,CS,1030,2016,Spring,a +162,CS,1030,2016,Spring,a +290,CS,1030,2016,Spring,a +291,CS,1030,2016,Spring,a +312,CS,1030,2016,Spring,a +348,CS,1030,2016,Spring,a +395,CS,1030,2016,Spring,a +123,CS,1030,2016,Summer,a +214,CS,1030,2016,Summer,a +245,CS,1030,2016,Summer,a +277,CS,1030,2016,Summer,a +385,CS,1030,2016,Summer,a +393,CS,1030,2016,Summer,a +102,CS,1030,2016,Fall,a +116,CS,1030,2016,Fall,a +243,CS,1030,2016,Fall,a +262,CS,1030,2016,Fall,a +321,CS,1030,2016,Fall,a +128,CS,1030,2018,Fall,a +238,CS,1030,2018,Fall,a +256,CS,1030,2018,Fall,a +305,CS,1030,2018,Fall,a +344,CS,1030,2018,Fall,a +366,CS,1030,2018,Fall,a +387,CS,1030,2018,Fall,a +143,CS,1030,2019,Fall,a +260,CS,1030,2019,Fall,a +285,CS,1030,2019,Fall,a +398,CS,1030,2019,Fall,a +173,CS,1030,2019,Fall,b +185,CS,1030,2019,Fall,b +210,CS,1030,2019,Fall,b +247,CS,1030,2019,Fall,b +303,CS,1030,2019,Fall,b +329,CS,1030,2019,Fall,b +359,CS,1030,2019,Fall,b +100,CS,1030,2020,Spring,a +122,CS,1030,2020,Spring,a +175,CS,1030,2020,Spring,a +221,CS,1030,2020,Spring,a +307,CS,1030,2020,Spring,a +170,CS,1030,2020,Spring,b +332,CS,1030,2020,Spring,b +391,CS,1030,2020,Spring,b +118,CS,1030,2020,Spring,c +120,CS,1030,2020,Spring,c +124,CS,1030,2020,Spring,c +135,CS,1030,2020,Spring,c +309,CS,1030,2020,Spring,c +119,CS,1030,2020,Fall,a +131,CS,1030,2020,Fall,a +167,CS,1030,2020,Fall,a +181,CS,1030,2020,Fall,a +202,CS,1030,2020,Fall,a +227,CS,1030,2020,Fall,a +255,CS,1030,2020,Fall,a +271,CS,1030,2020,Fall,a +342,CS,1030,2020,Fall,a +347,CS,1030,2020,Fall,a +215,CS,1410,2015,Summer,b +276,CS,1410,2015,Summer,b +182,CS,1410,2015,Summer,c +172,CS,1410,2015,Summer,d +270,CS,1410,2015,Summer,d +301,CS,1410,2015,Summer,d +382,CS,1410,2015,Summer,d +216,CS,1410,2016,Spring,a +335,CS,1410,2016,Spring,a +355,CS,1410,2016,Spring,a +216,CS,1410,2016,Spring,b +273,CS,1410,2016,Spring,b +291,CS,1410,2016,Spring,b +335,CS,1410,2016,Spring,b +207,CS,1410,2016,Summer,a +389,CS,1410,2016,Summer,a +394,CS,1410,2016,Summer,a +290,CS,1410,2017,Spring,a +391,CS,1410,2017,Spring,a +120,CS,1410,2018,Spring,a +231,CS,1410,2018,Spring,a +348,CS,1410,2018,Spring,a +100,CS,1410,2018,Spring,b +107,CS,1410,2018,Spring,b +109,CS,1410,2018,Spring,b +120,CS,1410,2018,Spring,b +164,CS,1410,2018,Spring,b +199,CS,1410,2018,Spring,b +203,CS,1410,2018,Spring,b +229,CS,1410,2018,Spring,b +109,CS,1410,2018,Spring,c +388,CS,1410,2018,Spring,c +199,CS,1410,2018,Spring,d +275,CS,1410,2018,Spring,d +307,CS,1410,2018,Spring,d +366,CS,1410,2018,Spring,d +392,CS,1410,2018,Spring,d +121,CS,1410,2020,Spring,a +122,CS,1410,2020,Spring,a +267,CS,1410,2020,Spring,a +312,CS,1410,2020,Spring,a +200,CS,1410,2020,Spring,b +277,CS,1410,2020,Spring,b +329,CS,1410,2020,Spring,b +375,CS,1410,2020,Spring,b +277,CS,2100,2015,Summer,a +313,CS,2100,2015,Summer,a +214,CS,2100,2016,Spring,a +276,CS,2100,2016,Spring,a +295,CS,2100,2016,Spring,a +123,CS,2100,2016,Summer,a +179,CS,2100,2016,Summer,a +160,CS,2100,2016,Summer,b +179,CS,2100,2016,Summer,b +262,CS,2100,2016,Summer,b +335,CS,2100,2016,Summer,b +374,CS,2100,2016,Summer,b +388,CS,2100,2016,Summer,b +134,CS,2100,2016,Summer,c +278,CS,2100,2016,Summer,c +256,CS,2100,2017,Spring,a +377,CS,2100,2017,Spring,a +378,CS,2100,2017,Spring,a +143,CS,2100,2017,Fall,a +163,CS,2100,2017,Fall,a +215,CS,2100,2017,Fall,a +311,CS,2100,2017,Fall,a +348,CS,2100,2017,Fall,a +356,CS,2100,2017,Fall,a +366,CS,2100,2017,Fall,a +101,CS,2100,2018,Spring,a +185,CS,2100,2018,Spring,a +255,CS,2100,2018,Spring,a +361,CS,2100,2018,Spring,a +387,CS,2100,2018,Spring,a +258,CS,2100,2018,Summer,a +261,CS,2100,2018,Summer,a +270,CS,2100,2018,Summer,a +369,CS,2100,2018,Summer,a +133,CS,2100,2018,Summer,b +182,CS,2100,2018,Summer,b +285,CS,2100,2018,Summer,b +329,CS,2100,2018,Summer,b +139,CS,2100,2018,Summer,c +258,CS,2100,2018,Summer,c +298,CS,2100,2018,Summer,c +329,CS,2100,2018,Summer,c +332,CS,2100,2018,Summer,c +345,CS,2100,2018,Summer,c +371,CS,2100,2018,Summer,c +381,CS,2100,2018,Summer,c +392,CS,2100,2018,Summer,c +393,CS,2100,2018,Summer,c +158,CS,2100,2018,Fall,a +230,CS,2100,2018,Fall,a +292,CS,2100,2018,Fall,a +373,CS,2100,2018,Fall,a +257,CS,2100,2018,Fall,b +309,CS,2100,2018,Fall,b +344,CS,2100,2018,Fall,b +384,CS,2100,2018,Fall,b +124,CS,2100,2018,Fall,c +196,CS,2100,2018,Fall,c +217,CS,2100,2018,Fall,c +231,CS,2100,2018,Fall,c +252,CS,2100,2018,Fall,c +257,CS,2100,2018,Fall,c +164,CS,2100,2018,Fall,d +199,CS,2100,2018,Fall,d +253,CS,2100,2018,Fall,d +259,CS,2100,2018,Fall,d +391,CS,2100,2018,Fall,d +399,CS,2100,2018,Fall,d +107,CS,2100,2019,Spring,a +240,CS,2100,2019,Spring,a +307,CS,2100,2019,Spring,a +379,CS,2100,2019,Spring,a +156,CS,2100,2019,Spring,b +312,CS,2100,2019,Spring,b +241,CS,2100,2019,Summer,a +293,CS,2100,2019,Summer,a +296,CS,2100,2019,Summer,a +314,CS,2100,2019,Summer,a +347,CS,2100,2019,Summer,a +390,CS,2100,2019,Summer,a +106,CS,2100,2019,Summer,b +131,CS,2100,2019,Summer,b +169,CS,2100,2019,Summer,b +194,CS,2100,2019,Summer,b +238,CS,2100,2019,Summer,b +359,CS,2100,2019,Summer,b +368,CS,2100,2019,Summer,b +118,CS,2100,2019,Fall,a +181,CS,2100,2019,Fall,a +223,CS,2100,2019,Fall,a +386,CS,2100,2019,Fall,a +118,CS,2100,2019,Fall,b +178,CS,2100,2019,Fall,b +235,CS,2100,2019,Fall,b +321,CS,2100,2019,Fall,b +397,CS,2100,2019,Fall,b +118,CS,2100,2019,Fall,c +146,CS,2100,2019,Fall,c +220,CS,2100,2019,Fall,c +260,CS,2100,2019,Fall,c +318,CS,2100,2019,Fall,c +397,CS,2100,2019,Fall,c +120,CS,2100,2019,Fall,d +146,CS,2100,2019,Fall,d +181,CS,2100,2019,Fall,d +183,CS,2100,2019,Fall,d +316,CS,2100,2019,Fall,d +152,CS,2100,2020,Spring,a +167,CS,2100,2020,Spring,a +228,CS,2100,2020,Spring,a +122,CS,2100,2020,Fall,a +171,CS,2100,2020,Fall,a +177,CS,2100,2020,Fall,a +191,CS,2100,2020,Fall,a +219,CS,2100,2020,Fall,a +247,CS,2100,2020,Fall,a +289,CS,2100,2020,Fall,a +333,CS,2100,2020,Fall,a +138,CS,2420,2015,Spring,a +277,CS,2420,2015,Spring,a +377,CS,2420,2015,Spring,a +160,CS,2420,2015,Summer,a +204,CS,2420,2015,Summer,a +140,CS,2420,2015,Summer,c +302,CS,2420,2015,Summer,c +276,CS,2420,2015,Fall,a +115,CS,2420,2016,Spring,a +312,CS,2420,2016,Spring,a +348,CS,2420,2016,Spring,a +385,CS,2420,2016,Spring,a +389,CS,2420,2016,Spring,a +172,CS,2420,2016,Summer,a +195,CS,2420,2016,Summer,a +314,CS,2420,2016,Summer,a +321,CS,2420,2016,Summer,a +163,CS,2420,2016,Fall,a +177,CS,2420,2016,Fall,a +229,CS,2420,2016,Fall,a +245,CS,2420,2016,Fall,a +282,CS,2420,2016,Fall,a +313,CS,2420,2016,Fall,a +369,CS,2420,2016,Fall,a +392,CS,2420,2016,Fall,a +105,CS,2420,2016,Fall,b +117,CS,2420,2016,Fall,b +151,CS,2420,2016,Fall,b +215,CS,2420,2016,Fall,b +262,CS,2420,2016,Fall,b +268,CS,2420,2016,Fall,b +295,CS,2420,2016,Fall,b +329,CS,2420,2016,Fall,b +243,CS,2420,2016,Fall,c +270,CS,2420,2016,Fall,c +397,CS,2420,2016,Fall,c +119,CS,2420,2017,Summer,a +353,CS,2420,2017,Summer,a +361,CS,2420,2017,Summer,a +132,CS,2420,2017,Summer,b +285,CS,2420,2017,Summer,b +299,CS,2420,2017,Summer,b +309,CS,2420,2017,Summer,b +179,CS,2420,2017,Summer,c +208,CS,2420,2017,Summer,c +261,CS,2420,2017,Summer,c +288,CS,2420,2017,Summer,c +311,CS,2420,2017,Summer,c +372,CS,2420,2017,Summer,c +120,CS,2420,2017,Fall,a +123,CS,2420,2017,Fall,a +128,CS,2420,2017,Fall,a +326,CS,2420,2017,Fall,a +387,CS,2420,2017,Fall,a +107,CS,2420,2018,Spring,a +296,CS,2420,2018,Spring,a +124,CS,2420,2019,Summer,a +131,CS,2420,2019,Summer,a +199,CS,2420,2019,Summer,a +356,CS,2420,2019,Summer,a +390,CS,2420,2019,Summer,a +133,CS,2420,2020,Summer,a +153,CS,2420,2020,Summer,a +167,CS,2420,2020,Summer,a +219,CS,2420,2020,Summer,a +220,CS,2420,2020,Summer,a +231,CS,2420,2020,Summer,a +233,CS,2420,2020,Summer,a +263,CS,2420,2020,Summer,a +365,CS,2420,2020,Summer,a +368,CS,2420,2020,Summer,a +168,CS,2420,2020,Fall,a +222,CS,2420,2020,Fall,a +225,CS,2420,2020,Fall,a +230,CS,2420,2020,Fall,a +345,CS,2420,2020,Fall,a +163,CS,3100,2015,Summer,a +172,CS,3100,2015,Summer,a +276,CS,3100,2015,Summer,a +302,CS,3100,2015,Summer,a +215,CS,3100,2015,Summer,b +214,CS,3100,2016,Spring,a +243,CS,3100,2016,Spring,a +120,CS,3100,2016,Spring,b +138,CS,3100,2016,Spring,b +285,CS,3100,2016,Spring,b +374,CS,3100,2016,Spring,b +134,CS,3100,2016,Spring,d +138,CS,3100,2016,Spring,d +192,CS,3100,2016,Spring,d +195,CS,3100,2016,Spring,d +207,CS,3100,2016,Summer,a +182,CS,3100,2016,Fall,a +213,CS,3100,2016,Fall,a +277,CS,3100,2016,Fall,a +314,CS,3100,2016,Fall,a +378,CS,3100,2016,Fall,a +392,CS,3100,2016,Fall,a +210,CS,3100,2017,Spring,a +261,CS,3100,2017,Spring,a +210,CS,3100,2017,Spring,b +255,CS,3100,2017,Spring,b +355,CS,3100,2017,Spring,b +385,CS,3100,2017,Spring,b +393,CS,3100,2017,Summer,a +123,CS,3100,2017,Fall,a +124,CS,3100,2017,Fall,a +139,CS,3100,2017,Fall,a +237,CS,3100,2017,Fall,a +260,CS,3100,2017,Fall,a +264,CS,3100,2017,Fall,a +296,CS,3100,2017,Fall,a +391,CS,3100,2017,Fall,a +397,CS,3100,2017,Fall,a +196,CS,3100,2019,Spring,a +129,CS,3100,2019,Spring,b +288,CS,3100,2019,Spring,b +348,CS,3100,2019,Spring,b +366,CS,3100,2019,Spring,b +399,CS,3100,2019,Spring,b +211,CS,3200,2015,Spring,b +138,CS,3200,2015,Fall,a +249,CS,3200,2015,Fall,a +134,CS,3200,2015,Fall,b +179,CS,3200,2015,Fall,b +312,CS,3200,2015,Fall,c +336,CS,3200,2015,Fall,c +282,CS,3200,2015,Fall,d +295,CS,3200,2015,Fall,d +182,CS,3200,2016,Summer,a +246,CS,3200,2016,Summer,a +270,CS,3200,2016,Summer,a +290,CS,3200,2016,Summer,a +357,CS,3200,2016,Summer,a +373,CS,3200,2016,Summer,a +379,CS,3200,2016,Summer,a +176,CS,3200,2016,Summer,b +207,CS,3200,2016,Summer,b +246,CS,3200,2016,Summer,b +120,CS,3200,2016,Fall,a +268,CS,3200,2016,Fall,a +102,CS,3200,2016,Fall,b +313,CS,3200,2016,Fall,b +348,CS,3200,2016,Fall,b +123,CS,3200,2016,Fall,c +229,CS,3200,2016,Fall,c +291,CS,3200,2016,Fall,c +105,CS,3200,2016,Fall,d +107,CS,3200,2016,Fall,d +151,CS,3200,2016,Fall,d +369,CS,3200,2016,Fall,d +385,CS,3200,2016,Fall,d +116,CS,3200,2017,Spring,a +264,CS,3200,2017,Spring,a +377,CS,3200,2017,Spring,a +397,CS,3200,2017,Spring,a +133,CS,3200,2018,Spring,a +165,CS,3200,2018,Spring,a +197,CS,3200,2018,Spring,a +257,CS,3200,2018,Spring,a +274,CS,3200,2018,Spring,a +255,CS,3200,2018,Spring,b +276,CS,3200,2018,Spring,b +391,CS,3200,2018,Spring,b +109,CS,3200,2018,Spring,c +285,CS,3200,2018,Spring,c +388,CS,3200,2018,Spring,c +139,CS,3200,2019,Spring,a +164,CS,3200,2019,Spring,a +277,CS,3200,2019,Spring,a +372,CS,3200,2019,Spring,a +131,CS,3200,2020,Spring,a +194,CS,3200,2020,Spring,a +228,CS,3200,2020,Spring,a +303,CS,3200,2020,Spring,a +342,CS,3200,2020,Spring,a +187,CS,3200,2020,Spring,b +108,CS,3200,2020,Spring,c +248,CS,3200,2020,Spring,c +325,CS,3200,2020,Spring,c +332,CS,3200,2020,Spring,c +378,CS,3200,2020,Spring,c +398,CS,3200,2020,Spring,c +112,CS,3200,2020,Summer,a +113,CS,3200,2020,Summer,a +177,CS,3200,2020,Summer,a +185,CS,3200,2020,Summer,a +231,CS,3200,2020,Summer,a +242,CS,3200,2020,Summer,a +254,CS,3200,2020,Summer,a +260,CS,3200,2020,Summer,a +292,CS,3200,2020,Summer,a +306,CS,3200,2020,Summer,a +311,CS,3200,2020,Summer,a +375,CS,3200,2020,Summer,a +124,CS,3200,2020,Fall,a +135,CS,3200,2020,Fall,a +161,CS,3200,2020,Fall,a +178,CS,3200,2020,Fall,a +230,CS,3200,2020,Fall,a +345,CS,3200,2020,Fall,a +376,CS,3200,2020,Fall,a +149,CS,3500,2015,Fall,b +246,CS,3500,2015,Fall,b +313,CS,3500,2015,Fall,b +123,CS,3500,2016,Spring,a +229,CS,3500,2016,Spring,a +277,CS,3500,2016,Spring,a +374,CS,3500,2016,Spring,a +395,CS,3500,2016,Spring,a +107,CS,3500,2016,Summer,a +282,CS,3500,2016,Summer,a +288,CS,3500,2016,Summer,a +379,CS,3500,2016,Summer,a +292,CS,3500,2017,Summer,a +311,CS,3500,2017,Summer,a +182,CS,3500,2017,Fall,a +314,CS,3500,2017,Fall,a +335,CS,3500,2017,Fall,a +391,CS,3500,2017,Fall,a +109,CS,3500,2017,Fall,b +131,CS,3500,2017,Fall,b +355,CS,3500,2017,Fall,b +203,CS,3500,2017,Fall,c +275,CS,3500,2017,Fall,c +294,CS,3500,2017,Fall,c +309,CS,3500,2017,Fall,c +385,CS,3500,2017,Fall,c +392,CS,3500,2017,Fall,c +118,CS,3500,2019,Summer,a +152,CS,3500,2019,Summer,a +179,CS,3500,2019,Summer,a +228,CS,3500,2019,Summer,a +258,CS,3500,2019,Summer,a +276,CS,3500,2019,Summer,a +396,CS,3500,2019,Summer,a +180,CS,3500,2019,Fall,a +255,CS,3500,2019,Fall,a +332,CS,3500,2019,Fall,a +377,CS,3500,2019,Fall,a +380,CS,3500,2019,Fall,a +397,CS,3500,2019,Fall,a +108,CS,3500,2019,Fall,b +133,CS,3500,2019,Fall,b +171,CS,3500,2019,Fall,b +199,CS,3500,2019,Fall,b +223,CS,3500,2019,Fall,b +270,CS,3500,2019,Fall,b +321,CS,3500,2019,Fall,b +375,CS,3500,2019,Fall,b +143,CS,3500,2019,Fall,c +363,CS,3500,2019,Fall,c +112,CS,3500,2020,Summer,a +124,CS,3500,2020,Summer,a +127,CS,3500,2020,Summer,a +142,CS,3500,2020,Summer,a +164,CS,3500,2020,Summer,a +166,CS,3500,2020,Summer,a +247,CS,3500,2020,Summer,a +260,CS,3500,2020,Summer,a +281,CS,3500,2020,Summer,a +312,CS,3500,2020,Summer,a +325,CS,3500,2020,Summer,a +329,CS,3500,2020,Summer,a +331,CS,3500,2020,Summer,a +333,CS,3500,2020,Summer,a +347,CS,3500,2020,Summer,a +348,CS,3500,2020,Summer,a +364,CS,3500,2020,Summer,a +365,CS,3500,2020,Summer,a +373,CS,3500,2020,Summer,a +386,CS,3500,2020,Summer,a +192,CS,3505,2015,Spring,a +282,CS,3505,2015,Spring,a +211,CS,3505,2015,Fall,a +313,CS,3505,2015,Fall,a +182,CS,3505,2015,Fall,b +335,CS,3505,2015,Fall,b +392,CS,3505,2015,Fall,b +126,CS,3505,2015,Fall,c +162,CS,3505,2015,Fall,c +348,CS,3505,2015,Fall,d +107,CS,3505,2016,Summer,a +163,CS,3505,2016,Summer,a +290,CS,3505,2016,Summer,a +378,CS,3505,2016,Summer,a +393,CS,3505,2016,Summer,a +123,CS,3505,2016,Fall,a +379,CS,3505,2016,Fall,a +116,CS,3505,2016,Fall,b +249,CS,3505,2016,Fall,b +329,CS,3505,2016,Fall,b +151,CS,3505,2017,Summer,a +260,CS,3505,2017,Summer,a +312,CS,3505,2017,Summer,a +124,CS,3505,2017,Fall,a +128,CS,3505,2017,Fall,a +199,CS,3505,2017,Fall,a +214,CS,3505,2017,Fall,a +355,CS,3505,2017,Fall,a +397,CS,3505,2017,Fall,a +102,CS,3505,2017,Fall,b +131,CS,3505,2017,Fall,b +177,CS,3505,2017,Fall,b +199,CS,3505,2017,Fall,b +208,CS,3505,2017,Fall,b +294,CS,3505,2017,Fall,b +321,CS,3505,2017,Fall,b +385,CS,3505,2017,Fall,b +100,CS,3505,2018,Summer,a +101,CS,3505,2018,Summer,a +197,CS,3505,2018,Summer,a +247,CS,3505,2018,Summer,a +255,CS,3505,2018,Summer,a +368,CS,3505,2018,Summer,a +374,CS,3505,2018,Summer,a +377,CS,3505,2018,Summer,a +386,CS,3505,2018,Summer,a +127,CS,3505,2018,Summer,b +143,CS,3505,2018,Summer,b +173,CS,3505,2018,Summer,b +185,CS,3505,2018,Summer,b +247,CS,3505,2018,Summer,b +259,CS,3505,2018,Summer,b +262,CS,3505,2018,Summer,b +288,CS,3505,2018,Summer,b +156,CS,3505,2018,Fall,a +179,CS,3505,2018,Fall,a +240,CS,3505,2018,Fall,a +256,CS,3505,2018,Fall,a +258,CS,3505,2018,Fall,a +305,CS,3505,2018,Fall,a +345,CS,3505,2018,Fall,a +371,CS,3505,2018,Fall,a +252,CS,3505,2018,Fall,b +285,CS,3505,2018,Fall,c +371,CS,3505,2018,Fall,c +396,CS,3505,2018,Fall,c +152,CS,3505,2019,Spring,a +228,CS,3505,2019,Spring,a +241,CS,3505,2019,Spring,a +276,CS,3505,2019,Spring,a +320,CS,3505,2019,Spring,a +187,CS,3505,2019,Spring,b +230,CS,3505,2019,Spring,b +314,CS,3505,2019,Spring,b +358,CS,3505,2019,Spring,b +119,CS,3505,2019,Summer,a +169,CS,3505,2019,Summer,a +220,CS,3505,2019,Summer,a +296,CS,3505,2019,Summer,a +307,CS,3505,2019,Summer,a +129,CS,3505,2019,Summer,b +223,CS,3505,2019,Summer,b +238,CS,3505,2019,Summer,b +296,CS,3505,2019,Summer,b +298,CS,3505,2019,Summer,b +300,CS,3505,2019,Summer,b +340,CS,3505,2019,Summer,b +372,CS,3505,2019,Summer,b +373,CS,3505,2019,Summer,b +380,CS,3505,2019,Summer,b +129,CS,3505,2019,Summer,c +300,CS,3505,2019,Summer,c +384,CS,3505,2019,Summer,c +113,CS,3505,2019,Summer,d +133,CS,3505,2019,Summer,d +270,CS,3505,2019,Summer,d +292,CS,3505,2019,Summer,d +318,CS,3505,2019,Summer,d +356,CS,3505,2019,Summer,d +362,CS,3505,2019,Summer,d +178,CS,3505,2019,Fall,a +284,CS,3505,2019,Fall,a +391,CS,3505,2019,Fall,a +118,CS,3505,2019,Fall,b +289,CS,3505,2019,Fall,b +309,CS,3505,2019,Fall,b +399,CS,3505,2019,Fall,b +194,CS,3505,2019,Fall,c +235,CS,3505,2019,Fall,c +248,CS,3505,2019,Fall,c +311,CS,3505,2019,Fall,c +391,CS,3505,2019,Fall,c +146,CS,3505,2020,Spring,a +164,CS,3505,2020,Spring,a +277,CS,3505,2020,Spring,a +332,CS,3505,2020,Spring,a +137,CS,3505,2020,Summer,a +200,CS,3505,2020,Summer,a +219,CS,3505,2020,Summer,a +257,CS,3505,2020,Summer,a +267,CS,3505,2020,Summer,a +306,CS,3505,2020,Summer,a +365,CS,3505,2020,Summer,a +142,CS,3505,2020,Fall,a +339,CS,3505,2020,Fall,a +398,CS,3505,2020,Fall,a +106,CS,3505,2020,Fall,b +110,CS,3505,2020,Fall,b +121,CS,3505,2020,Fall,b +333,CS,3505,2020,Fall,b +109,CS,3505,2020,Fall,c +120,CS,3505,2020,Fall,c +171,CS,3505,2020,Fall,c +250,CS,3505,2020,Fall,c +293,CS,3505,2020,Fall,c +390,CS,3505,2020,Fall,c +140,CS,3810,2015,Spring,a +276,CS,3810,2015,Spring,a +123,CS,3810,2016,Summer,a +160,CS,3810,2016,Summer,a +314,CS,3810,2016,Summer,a +393,CS,3810,2016,Summer,a +107,CS,3810,2016,Fall,a +195,CS,3810,2016,Fall,a +213,CS,3810,2016,Fall,a +282,CS,3810,2016,Fall,a +285,CS,3810,2016,Fall,a +348,CS,3810,2016,Fall,a +105,CS,3810,2016,Fall,b +116,CS,3810,2016,Fall,b +245,CS,3810,2016,Fall,b +264,CS,3810,2016,Fall,b +329,CS,3810,2016,Fall,b +335,CS,3810,2016,Fall,b +173,CS,3810,2018,Spring,a +179,CS,3810,2018,Spring,a +230,CS,3810,2018,Spring,a +237,CS,3810,2018,Spring,a +255,CS,3810,2018,Spring,a +305,CS,3810,2018,Spring,a +313,CS,3810,2018,Spring,a +372,CS,3810,2018,Spring,a +388,CS,3810,2018,Spring,a +129,CS,3810,2018,Summer,a +177,CS,3810,2018,Summer,a +260,CS,3810,2018,Summer,a +374,CS,3810,2018,Summer,a +386,CS,3810,2018,Summer,a +177,CS,3810,2018,Summer,b +214,CS,3810,2018,Summer,b +231,CS,3810,2018,Summer,b +270,CS,3810,2018,Summer,b +288,CS,3810,2018,Summer,b +344,CS,3810,2018,Summer,b +377,CS,3810,2018,Summer,b +399,CS,3810,2018,Summer,b +128,CS,3810,2018,Summer,c +129,CS,3810,2018,Summer,c +133,CS,3810,2018,Summer,c +151,CS,3810,2018,Summer,c +240,CS,3810,2018,Summer,c +257,CS,3810,2018,Summer,c +311,CS,3810,2018,Summer,c +182,CS,3810,2018,Summer,d +210,CS,3810,2018,Summer,d +252,CS,3810,2018,Summer,d +270,CS,3810,2018,Summer,d +312,CS,3810,2018,Summer,d +356,CS,3810,2018,Summer,d +379,CS,3810,2018,Summer,d +127,CS,3810,2019,Fall,a +131,CS,3810,2019,Fall,a +241,CS,3810,2019,Fall,a +258,CS,3810,2019,Fall,a +333,CS,3810,2019,Fall,a +102,CS,3810,2019,Fall,b +359,CS,3810,2019,Fall,b +113,CS,3810,2020,Fall,a +124,CS,3810,2020,Fall,a +171,CS,3810,2020,Fall,a +187,CS,3810,2020,Fall,a +220,CS,3810,2020,Fall,a +225,CS,3810,2020,Fall,a +233,CS,3810,2020,Fall,a +340,CS,3810,2020,Fall,a +347,CS,3810,2020,Fall,a +193,CS,4000,2015,Spring,a +160,CS,4000,2015,Summer,a +282,CS,4000,2015,Fall,a +307,CS,4000,2015,Fall,a +138,CS,4000,2016,Fall,a +276,CS,4000,2016,Fall,a +321,CS,4000,2016,Fall,a +378,CS,4000,2016,Fall,a +393,CS,4000,2016,Fall,a +151,CS,4000,2017,Spring,a +187,CS,4000,2017,Spring,a +207,CS,4000,2017,Spring,a +255,CS,4000,2017,Spring,a +134,CS,4000,2017,Summer,a +139,CS,4000,2017,Summer,a +179,CS,4000,2017,Summer,a +259,CS,4000,2017,Summer,a +318,CS,4000,2017,Summer,a +373,CS,4000,2017,Summer,a +107,CS,4000,2017,Fall,a +163,CS,4000,2017,Fall,a +252,CS,4000,2017,Fall,a +262,CS,4000,2017,Fall,a +291,CS,4000,2017,Fall,a +342,CS,4000,2017,Fall,a +361,CS,4000,2017,Fall,a +163,CS,4000,2017,Fall,b +329,CS,4000,2017,Fall,b +345,CS,4000,2017,Fall,b +361,CS,4000,2017,Fall,b +164,CS,4000,2018,Spring,a +173,CS,4000,2018,Spring,a +203,CS,4000,2018,Spring,a +275,CS,4000,2018,Spring,a +313,CS,4000,2018,Spring,a +385,CS,4000,2018,Spring,a +127,CS,4000,2019,Spring,a +256,CS,4000,2019,Spring,a +169,CS,4000,2020,Spring,a +181,CS,4000,2020,Spring,a +254,CS,4000,2020,Spring,a +257,CS,4000,2020,Spring,a +285,CS,4000,2020,Spring,a +312,CS,4000,2020,Spring,a +364,CS,4000,2020,Spring,a +375,CS,4000,2020,Spring,a +386,CS,4000,2020,Spring,a +123,CS,4000,2020,Spring,b +152,CS,4000,2020,Spring,b +181,CS,4000,2020,Spring,b +257,CS,4000,2020,Spring,b +309,CS,4000,2020,Spring,b +311,CS,4000,2020,Spring,b +371,CS,4000,2020,Spring,b +109,CS,4000,2020,Fall,a +110,CS,4000,2020,Fall,a +118,CS,4000,2020,Fall,a +120,CS,4000,2020,Fall,a +131,CS,4000,2020,Fall,a +161,CS,4000,2020,Fall,a +185,CS,4000,2020,Fall,a +277,CS,4000,2020,Fall,a +292,CS,4000,2020,Fall,a +341,CS,4000,2020,Fall,a +348,CS,4000,2020,Fall,a +366,CS,4000,2020,Fall,a +368,CS,4000,2020,Fall,a +376,CS,4000,2020,Fall,a +397,CS,4000,2020,Fall,a +162,CS,4150,2015,Summer,a +176,CS,4150,2015,Summer,a +192,CS,4150,2015,Summer,a +204,CS,4150,2015,Summer,a +348,CS,4150,2015,Summer,b +163,CS,4150,2016,Summer,a +245,CS,4150,2016,Summer,a +249,CS,4150,2016,Summer,a +378,CS,4150,2016,Summer,a +249,CS,4150,2016,Summer,b +264,CS,4150,2016,Summer,b +285,CS,4150,2016,Summer,b +288,CS,4150,2016,Summer,b +131,CS,4150,2018,Fall,a +240,CS,4150,2018,Fall,a +270,CS,4150,2018,Fall,a +292,CS,4150,2018,Fall,a +362,CS,4150,2018,Fall,a +391,CS,4150,2018,Fall,a +255,CS,4150,2018,Fall,b +371,CS,4150,2018,Fall,b +102,CS,4150,2019,Spring,a +210,CS,4150,2019,Spring,a +260,CS,4150,2019,Spring,a +106,CS,4150,2020,Spring,a +120,CS,4150,2020,Spring,a +123,CS,4150,2020,Spring,a +125,CS,4150,2020,Spring,a +179,CS,4150,2020,Spring,a +277,CS,4150,2020,Spring,a +314,CS,4150,2020,Spring,a +396,CS,4150,2020,Spring,a +397,CS,4150,2020,Spring,a +135,CS,4150,2020,Fall,a +148,CS,4150,2020,Fall,a +235,CS,4150,2020,Fall,a +309,CS,4150,2020,Fall,a +329,CS,4150,2020,Fall,a +339,CS,4150,2020,Fall,a +347,CS,4150,2020,Fall,a +386,CS,4150,2020,Fall,a +120,CS,4400,2015,Summer,a +140,CS,4400,2015,Summer,a +215,CS,4400,2015,Summer,a +277,CS,4400,2015,Summer,a +290,CS,4400,2015,Summer,a +392,CS,4400,2015,Fall,b +282,CS,4400,2015,Fall,c +373,CS,4400,2015,Fall,c +149,CS,4400,2016,Spring,a +307,CS,4400,2016,Spring,a +179,CS,4400,2016,Summer,a +262,CS,4400,2016,Summer,a +138,CS,4400,2016,Fall,a +102,CS,4400,2017,Spring,a +246,CS,4400,2017,Spring,a +249,CS,4400,2017,Spring,a +329,CS,4400,2017,Spring,a +369,CS,4400,2017,Spring,a +231,CS,4400,2017,Spring,b +255,CS,4400,2017,Spring,b +309,CS,4400,2017,Spring,b +276,CS,4400,2017,Spring,c +313,CS,4400,2017,Spring,c +388,CS,4400,2017,Spring,c +321,CS,4400,2019,Spring,a +333,CS,4400,2019,Spring,a +379,CS,4400,2019,Spring,a +109,CS,4400,2019,Spring,b +128,CS,4400,2019,Spring,b +151,CS,4400,2019,Spring,b +275,CS,4400,2019,Spring,b +169,CS,4400,2019,Spring,c +187,CS,4400,2019,Spring,c +248,CS,4400,2019,Spring,c +257,CS,4400,2019,Spring,d +312,CS,4400,2019,Spring,d +345,CS,4400,2019,Spring,d +146,CS,4400,2019,Summer,a +167,CS,4400,2019,Summer,a +173,CS,4400,2019,Summer,a +234,CS,4400,2019,Summer,a +285,CS,4400,2019,Summer,a +287,CS,4400,2019,Summer,a +294,CS,4400,2019,Summer,a +325,CS,4400,2019,Summer,a +397,CS,4400,2019,Summer,a +398,CS,4400,2019,Summer,a +135,CS,4400,2019,Summer,b +143,CS,4400,2019,Summer,b +177,CS,4400,2019,Summer,b +267,CS,4400,2019,Summer,b +285,CS,4400,2019,Summer,b +298,CS,4400,2019,Summer,b +332,CS,4400,2019,Summer,b +368,CS,4400,2019,Summer,b +391,CS,4400,2019,Summer,b +183,CS,4400,2019,Fall,a +241,CS,4400,2019,Fall,a +124,CS,4400,2019,Fall,b +259,CS,4400,2019,Fall,b +364,CS,4400,2019,Fall,b +377,CS,4400,2019,Fall,b +113,CS,4400,2020,Spring,a +170,CS,4400,2020,Spring,a +199,CS,4400,2020,Spring,a +228,CS,4400,2020,Spring,a +348,CS,4400,2020,Spring,a +390,CS,4400,2020,Spring,a +119,CS,4400,2020,Fall,a +123,CS,4400,2020,Fall,a +131,CS,4400,2020,Fall,a +152,CS,4400,2020,Fall,a +230,CS,4400,2020,Fall,a +258,CS,4400,2020,Fall,a +272,CS,4400,2020,Fall,a +378,CS,4400,2020,Fall,a +106,CS,4400,2020,Fall,b +127,CS,4400,2020,Fall,b +185,CS,4400,2020,Fall,b +202,CS,4400,2020,Fall,b +235,CS,4400,2020,Fall,b +292,CS,4400,2020,Fall,b +340,CS,4400,2020,Fall,b +276,CS,4500,2015,Summer,a +290,CS,4500,2015,Summer,b +215,CS,4500,2016,Spring,a +317,CS,4500,2016,Spring,a +119,CS,4500,2016,Spring,b +138,CS,4500,2016,Spring,b +149,CS,4500,2016,Spring,b +162,CS,4500,2016,Spring,b +179,CS,4500,2016,Spring,b +215,CS,4500,2016,Spring,b +285,CS,4500,2016,Spring,b +301,CS,4500,2016,Spring,b +307,CS,4500,2016,Spring,b +321,CS,4500,2016,Spring,b +357,CS,4500,2016,Spring,b +117,CS,4500,2016,Fall,a +176,CS,4500,2016,Fall,a +177,CS,4500,2016,Fall,a +309,CS,4500,2016,Fall,a +139,CS,4500,2017,Summer,a +207,CS,4500,2017,Summer,a +335,CS,4500,2017,Summer,a +348,CS,4500,2017,Summer,a +378,CS,4500,2017,Summer,a +101,CS,4500,2018,Spring,a +128,CS,4500,2018,Spring,a +132,CS,4500,2018,Spring,a +182,CS,4500,2018,Spring,a +203,CS,4500,2018,Spring,a +231,CS,4500,2018,Spring,a +294,CS,4500,2018,Spring,a +329,CS,4500,2018,Spring,a +361,CS,4500,2018,Spring,a +132,CS,4500,2018,Spring,b +270,CS,4500,2018,Spring,b +305,CS,4500,2018,Spring,b +318,CS,4500,2018,Spring,b +379,CS,4500,2018,Spring,b +133,CS,4500,2018,Spring,c +164,CS,4500,2018,Spring,c +312,CS,4500,2018,Spring,c +369,CS,4500,2018,Spring,c +128,CS,4500,2018,Spring,d +313,CS,4500,2018,Spring,d +345,CS,4500,2018,Spring,d +366,CS,4500,2018,Spring,d +391,CS,4500,2018,Spring,d +107,CS,4500,2019,Summer,a +123,CS,4500,2019,Summer,a +185,CS,4500,2019,Summer,a +248,CS,4500,2019,Summer,a +333,CS,4500,2019,Summer,a +340,CS,4500,2019,Summer,a +371,CS,4500,2019,Summer,a +386,CS,4500,2019,Summer,a +256,CS,4500,2019,Fall,a +260,CS,4500,2019,Fall,a +293,CS,4500,2019,Fall,a +303,CS,4500,2019,Fall,a +131,CS,4500,2019,Fall,b +173,CS,4500,2019,Fall,b +250,CS,4500,2019,Fall,b +255,CS,4500,2019,Fall,b +300,CS,4500,2019,Fall,b +398,CS,4500,2019,Fall,b +131,CS,4500,2019,Fall,c +143,CS,4500,2019,Fall,c +256,CS,4500,2019,Fall,c +274,CS,4500,2019,Fall,c +316,CS,4500,2019,Fall,c +109,CS,4500,2019,Fall,d +194,CS,4500,2019,Fall,d +220,CS,4500,2019,Fall,d +254,CS,4500,2019,Fall,d +255,CS,4500,2019,Fall,d +296,CS,4500,2019,Fall,d +341,CS,4500,2019,Fall,d +365,CS,4500,2019,Fall,d +108,CS,4500,2020,Spring,a +142,CS,4500,2020,Spring,a +169,CS,4500,2020,Spring,a +200,CS,4500,2020,Spring,a +364,CS,4500,2020,Spring,a +373,CS,4500,2020,Spring,a +127,CS,4500,2020,Summer,a +152,CS,4500,2020,Summer,a +167,CS,4500,2020,Summer,a +240,CS,4500,2020,Summer,a +368,CS,4500,2020,Summer,a +397,CS,4500,2020,Summer,a +138,CS,4940,2015,Summer,a +117,CS,4940,2017,Fall,a +143,CS,4940,2017,Fall,a +260,CS,4940,2017,Fall,a +294,CS,4940,2017,Fall,a +311,CS,4940,2017,Fall,a +326,CS,4940,2017,Fall,a +119,CS,4940,2017,Fall,b +379,CS,4940,2017,Fall,b +167,CS,4940,2019,Fall,a +220,CS,4940,2019,Fall,a +255,CS,4940,2019,Fall,a +256,CS,4940,2019,Fall,a +285,CS,4940,2019,Fall,a +314,CS,4940,2019,Fall,a +398,CS,4940,2019,Fall,a +100,CS,4940,2020,Summer,a +170,CS,4940,2020,Summer,a +200,CS,4940,2020,Summer,a +228,CS,4940,2020,Summer,a +251,CS,4940,2020,Summer,a +258,CS,4940,2020,Summer,a +277,CS,4940,2020,Summer,a +292,CS,4940,2020,Summer,a +313,CS,4940,2020,Summer,a +331,CS,4940,2020,Summer,a +362,CS,4940,2020,Summer,a +378,CS,4940,2020,Summer,a +386,CS,4940,2020,Summer,a +391,CS,4940,2020,Summer,a +397,CS,4940,2020,Summer,a +100,CS,4940,2020,Summer,b +123,CS,4940,2020,Summer,b +127,CS,4940,2020,Summer,b +171,CS,4940,2020,Summer,b +177,CS,4940,2020,Summer,b +194,CS,4940,2020,Summer,b +231,CS,4940,2020,Summer,b +233,CS,4940,2020,Summer,b +247,CS,4940,2020,Summer,b +250,CS,4940,2020,Summer,b +251,CS,4940,2020,Summer,b +258,CS,4940,2020,Summer,b +271,CS,4940,2020,Summer,b +277,CS,4940,2020,Summer,b +300,CS,4940,2020,Summer,b +312,CS,4940,2020,Summer,b +321,CS,4940,2020,Summer,b +339,CS,4940,2020,Summer,b +345,CS,4940,2020,Summer,b +391,CS,4940,2020,Summer,b +397,CS,4940,2020,Summer,b +107,CS,4970,2016,Fall,a +123,CS,4970,2016,Fall,a +145,CS,4970,2016,Fall,a +268,CS,4970,2016,Fall,a +276,CS,4970,2016,Fall,a +285,CS,4970,2016,Fall,a +335,CS,4970,2016,Fall,a +394,CS,4970,2016,Fall,a +177,CS,4970,2016,Fall,b +179,CS,4970,2016,Fall,b +249,CS,4970,2016,Fall,b +276,CS,4970,2016,Fall,b +285,CS,4970,2016,Fall,b +291,CS,4970,2016,Fall,b +312,CS,4970,2016,Fall,b +313,CS,4970,2016,Fall,b +397,CS,4970,2016,Fall,b +116,CS,4970,2017,Spring,a +120,CS,4970,2017,Spring,a +282,CS,4970,2017,Spring,a +295,CS,4970,2017,Spring,a +314,CS,4970,2017,Spring,a +393,CS,4970,2017,Spring,a +117,CS,4970,2017,Summer,a +261,CS,4970,2017,Summer,a +288,CS,4970,2017,Summer,a +231,CS,4970,2018,Summer,a +270,CS,4970,2018,Summer,a +277,CS,4970,2018,Summer,a +344,CS,4970,2018,Summer,a +398,CS,4970,2018,Summer,a +100,CS,4970,2018,Summer,b +105,CS,4970,2018,Summer,b +132,CS,4970,2018,Summer,b +227,CS,4970,2018,Summer,b +277,CS,4970,2018,Summer,b +348,CS,4970,2018,Summer,b +133,CS,4970,2018,Summer,c +163,CS,4970,2018,Summer,c +185,CS,4970,2018,Summer,c +214,CS,4970,2018,Summer,c +220,CS,4970,2018,Summer,c +372,CS,4970,2018,Summer,c +387,CS,4970,2018,Summer,c +392,CS,4970,2018,Summer,c +274,CS,4970,2018,Fall,a +128,CS,4970,2018,Fall,b +247,CS,4970,2018,Fall,b +262,CS,4970,2018,Fall,b +267,CS,4970,2018,Fall,b +386,CS,4970,2018,Fall,b +121,CS,4970,2018,Fall,c +143,CS,4970,2018,Fall,c +196,CS,4970,2018,Fall,c +102,CS,4970,2018,Fall,d +121,CS,4970,2018,Fall,d +178,CS,4970,2018,Fall,d +255,CS,4970,2018,Fall,d +267,CS,4970,2018,Fall,d +342,CS,4970,2018,Fall,d +356,CS,4970,2018,Fall,d +165,CS,4970,2019,Spring,a +275,CS,4970,2019,Spring,a +351,CS,4970,2019,Spring,a +366,CS,4970,2019,Spring,a +311,CS,4970,2019,Spring,b +345,CS,4970,2019,Spring,b +364,CS,4970,2019,Spring,b +124,CS,4970,2019,Summer,a +199,CS,4970,2019,Summer,a +289,CS,4970,2019,Summer,a +300,CS,4970,2019,Summer,a +368,CS,4970,2019,Summer,a +378,CS,4970,2019,Summer,a +113,CS,4970,2019,Summer,b +164,CS,4970,2019,Summer,b +298,CS,4970,2019,Summer,b +325,CS,4970,2019,Summer,b +359,CS,4970,2019,Summer,b +378,CS,4970,2019,Summer,b +391,CS,4970,2019,Summer,b +173,CS,4970,2019,Summer,c +333,CS,4970,2019,Summer,c +363,CS,4970,2019,Summer,c +119,CS,4970,2019,Summer,d +135,CS,4970,2019,Summer,d +164,CS,4970,2019,Summer,d +294,CS,4970,2019,Summer,d +303,CS,4970,2019,Summer,d +329,CS,4970,2019,Summer,d +362,CS,4970,2019,Summer,d +399,CS,4970,2019,Summer,d +194,CS,4970,2019,Fall,a +235,CS,4970,2019,Fall,a +250,CS,4970,2019,Fall,a +127,CS,4970,2019,Fall,b +131,CS,4970,2019,Fall,b +293,CS,4970,2019,Fall,b +321,CS,4970,2019,Fall,b +152,CS,4970,2019,Fall,c +200,CS,4970,2019,Fall,c +259,CS,4970,2019,Fall,c +318,CS,4970,2019,Fall,d +340,CS,4970,2019,Fall,d +347,CS,4970,2019,Fall,d +112,CS,4970,2020,Summer,a +221,CS,4970,2020,Summer,a +242,CS,4970,2020,Summer,a +251,CS,4970,2020,Summer,a +257,CS,4970,2020,Summer,a +118,CS,4970,2020,Summer,b +151,CS,4970,2020,Summer,b +187,CS,4970,2020,Summer,b +219,CS,4970,2020,Summer,b +221,CS,4970,2020,Summer,b +222,CS,4970,2020,Summer,b +309,CS,4970,2020,Summer,b +373,CS,4970,2020,Summer,b +379,CS,4970,2020,Summer,b +146,CS,4970,2020,Summer,c +233,CS,4970,2020,Summer,c +257,CS,4970,2020,Summer,c +260,CS,4970,2020,Summer,c +292,CS,4970,2020,Summer,c +339,CS,4970,2020,Summer,c +379,CS,4970,2020,Summer,c +384,CS,4970,2020,Summer,c +109,CS,4970,2020,Summer,d +146,CS,4970,2020,Summer,d +151,CS,4970,2020,Summer,d +171,CS,4970,2020,Summer,d +228,CS,4970,2020,Summer,d +254,CS,4970,2020,Summer,d +307,CS,4970,2020,Summer,d +309,CS,4970,2020,Summer,d +379,CS,4970,2020,Summer,d +390,CS,4970,2020,Summer,d +122,CS,4970,2020,Fall,a +191,CS,4970,2020,Fall,a +136,CS,4970,2020,Fall,b +283,CS,4970,2020,Fall,b +130,CS,4970,2020,Fall,c +148,CS,4970,2020,Fall,c +281,CS,4970,2020,Fall,c +186,CS,4970,2020,Fall,d +202,CS,4970,2020,Fall,d +323,CS,4970,2020,Fall,d +341,CS,4970,2020,Fall,d +120,MATH,1210,2015,Summer,a +138,MATH,1210,2015,Summer,a +117,MATH,1210,2016,Spring,a +119,MATH,1210,2016,Spring,a +144,MATH,1210,2016,Spring,a +270,MATH,1210,2016,Spring,a +276,MATH,1210,2016,Spring,a +229,MATH,1210,2016,Spring,b +295,MATH,1210,2016,Spring,b +335,MATH,1210,2016,Spring,b +182,MATH,1210,2016,Spring,c +277,MATH,1210,2016,Spring,c +179,MATH,1210,2016,Spring,d +273,MATH,1210,2016,Spring,d +277,MATH,1210,2016,Spring,d +295,MATH,1210,2016,Spring,d +214,MATH,1210,2016,Fall,a +249,MATH,1210,2016,Fall,a +397,MATH,1210,2016,Fall,a +215,MATH,1210,2016,Fall,b +278,MATH,1210,2016,Fall,b +357,MATH,1210,2016,Fall,b +378,MATH,1210,2016,Fall,b +107,MATH,1210,2016,Fall,c +195,MATH,1210,2016,Fall,c +285,MATH,1210,2016,Fall,c +369,MATH,1210,2016,Fall,c +379,MATH,1210,2016,Fall,c +195,MATH,1210,2016,Fall,d +385,MATH,1210,2016,Fall,d +356,MATH,1210,2017,Spring,a +394,MATH,1210,2017,Spring,a +345,MATH,1210,2017,Summer,a +230,MATH,1210,2017,Summer,b +210,MATH,1210,2017,Summer,c +342,MATH,1210,2017,Summer,c +387,MATH,1210,2017,Summer,c +392,MATH,1210,2017,Summer,c +102,MATH,1210,2018,Spring,a +199,MATH,1210,2018,Spring,a +372,MATH,1210,2018,Spring,a +257,MATH,1210,2018,Summer,a +279,MATH,1210,2018,Summer,a +288,MATH,1210,2018,Summer,a +368,MATH,1210,2018,Summer,a +371,MATH,1210,2018,Summer,a +398,MATH,1210,2018,Summer,a +167,MATH,1210,2018,Fall,a +177,MATH,1210,2018,Fall,a +185,MATH,1210,2018,Fall,a +231,MATH,1210,2018,Fall,a +311,MATH,1210,2018,Fall,a +312,MATH,1210,2018,Fall,a +384,MATH,1210,2018,Fall,a +104,MATH,1210,2018,Fall,b +128,MATH,1210,2018,Fall,b +163,MATH,1210,2018,Fall,b +178,MATH,1210,2018,Fall,b +133,MATH,1210,2019,Spring,a +294,MATH,1210,2019,Spring,a +307,MATH,1210,2019,Spring,a +332,MATH,1210,2019,Spring,a +333,MATH,1210,2019,Spring,a +348,MATH,1210,2019,Spring,a +351,MATH,1210,2019,Spring,a +275,MATH,1210,2019,Spring,b +123,MATH,1210,2019,Summer,a +124,MATH,1210,2019,Summer,a +228,MATH,1210,2019,Summer,a +255,MATH,1210,2019,Summer,a +313,MATH,1210,2019,Summer,a +135,MATH,1210,2020,Spring,a +220,MATH,1210,2020,Spring,a +310,MATH,1210,2020,Spring,a +373,MATH,1210,2020,Spring,a +390,MATH,1210,2020,Spring,a +106,MATH,1210,2020,Spring,b +108,MATH,1210,2020,Spring,b +260,MATH,1210,2020,Spring,b +386,MATH,1210,2020,Spring,b +192,MATH,1220,2015,Summer,a +211,MATH,1220,2015,Summer,a +162,MATH,1220,2015,Summer,b +270,MATH,1220,2015,Summer,b +280,MATH,1220,2015,Summer,b +195,MATH,1220,2015,Summer,c +245,MATH,1220,2015,Summer,c +282,MATH,1220,2015,Summer,c +377,MATH,1220,2015,Summer,c +210,MATH,1220,2016,Spring,a +307,MATH,1220,2016,Spring,a +313,MATH,1220,2016,Spring,a +357,MATH,1220,2016,Spring,a +389,MATH,1220,2016,Spring,a +116,MATH,1220,2017,Spring,a +187,MATH,1220,2017,Spring,a +256,MATH,1220,2017,Spring,a +299,MATH,1220,2017,Spring,a +117,MATH,1220,2017,Spring,b +163,MATH,1220,2017,Spring,b +179,MATH,1220,2017,Spring,b +182,MATH,1220,2017,Spring,b +259,MATH,1220,2017,Spring,b +260,MATH,1220,2017,Spring,b +285,MATH,1220,2017,Spring,b +314,MATH,1220,2017,Spring,b +388,MATH,1220,2017,Spring,b +393,MATH,1220,2017,Spring,b +117,MATH,1220,2017,Spring,c +145,MATH,1220,2017,Spring,c +277,MATH,1220,2017,Spring,c +355,MATH,1220,2017,Spring,c +385,MATH,1220,2017,Spring,c +105,MATH,1220,2017,Spring,d +260,MATH,1220,2017,Spring,d +378,MATH,1220,2017,Spring,d +215,MATH,1220,2017,Summer,a +165,MATH,1220,2018,Spring,a +173,MATH,1220,2018,Spring,a +276,MATH,1220,2018,Spring,a +312,MATH,1220,2018,Spring,a +332,MATH,1220,2018,Spring,a +375,MATH,1220,2018,Spring,a +131,MATH,1220,2018,Spring,b +169,MATH,1220,2018,Spring,b +309,MATH,1220,2018,Spring,b +362,MATH,1220,2018,Spring,b +139,MATH,1220,2018,Summer,a +185,MATH,1220,2018,Summer,a +348,MATH,1220,2018,Summer,a +127,MATH,1220,2019,Fall,a +133,MATH,1220,2019,Fall,a +181,MATH,1220,2019,Fall,a +231,MATH,1220,2019,Fall,a +234,MATH,1220,2019,Fall,a +248,MATH,1220,2019,Fall,a +254,MATH,1220,2019,Fall,a +323,MATH,1220,2019,Fall,a +341,MATH,1220,2019,Fall,a +102,MATH,1220,2019,Fall,b +120,MATH,1220,2019,Fall,b +123,MATH,1220,2019,Fall,b +152,MATH,1220,2019,Fall,b +180,MATH,1220,2019,Fall,b +274,MATH,1220,2019,Fall,b +321,MATH,1220,2019,Fall,b +366,MATH,1220,2019,Fall,b +135,MATH,1220,2019,Fall,c +247,MATH,1220,2019,Fall,c +358,MATH,1220,2019,Fall,c +390,MATH,1220,2019,Fall,c +396,MATH,1220,2019,Fall,c +100,MATH,1220,2020,Spring,a +151,MATH,1220,2020,Spring,a +178,MATH,1220,2020,Spring,a +228,MATH,1220,2020,Spring,a +118,MATH,1220,2020,Summer,a +164,MATH,1220,2020,Summer,a +281,MATH,1220,2020,Summer,a +293,MATH,1220,2020,Summer,a +329,MATH,1220,2020,Summer,a +397,MATH,1220,2020,Summer,a +211,MATH,1250,2015,Spring,c +276,MATH,1250,2015,Spring,c +149,MATH,1250,2015,Fall,a +172,MATH,1250,2015,Fall,a +335,MATH,1250,2015,Fall,a +214,MATH,1250,2016,Spring,a +290,MATH,1250,2016,Spring,a +377,MATH,1250,2016,Spring,a +270,MATH,1250,2016,Summer,a +285,MATH,1250,2016,Summer,a +373,MATH,1250,2016,Summer,a +215,MATH,1250,2016,Fall,a +138,MATH,1250,2016,Fall,b +182,MATH,1250,2016,Fall,b +120,MATH,1250,2016,Fall,c +374,MATH,1250,2016,Fall,c +127,MATH,1250,2017,Summer,a +173,MATH,1250,2017,Summer,a +292,MATH,1250,2017,Summer,a +355,MATH,1250,2017,Summer,a +127,MATH,1250,2017,Summer,b +210,MATH,1250,2017,Summer,b +311,MATH,1250,2017,Summer,b +230,MATH,1250,2017,Summer,c +257,MATH,1250,2017,Summer,c +117,MATH,1250,2017,Summer,d +208,MATH,1250,2017,Summer,d +109,MATH,1250,2018,Spring,a +123,MATH,1250,2018,Spring,a +260,MATH,1250,2018,Spring,a +274,MATH,1250,2018,Spring,a +345,MATH,1250,2018,Spring,a +361,MATH,1250,2018,Spring,a +379,MATH,1250,2018,Spring,a +385,MATH,1250,2018,Spring,a +392,MATH,1250,2018,Spring,a +102,MATH,1250,2018,Summer,a +247,MATH,1250,2018,Summer,a +255,MATH,1250,2018,Summer,a +312,MATH,1250,2018,Summer,a +332,MATH,1250,2018,Summer,a +356,MATH,1250,2018,Summer,a +372,MATH,1250,2018,Summer,a +101,MATH,1250,2018,Summer,b +119,MATH,1250,2018,Summer,b +239,MATH,1250,2018,Summer,b +313,MATH,1250,2018,Summer,b +321,MATH,1250,2018,Summer,b +368,MATH,1250,2018,Summer,b +100,MATH,1250,2018,Summer,c +139,MATH,1250,2018,Summer,c +158,MATH,1250,2018,Summer,c +197,MATH,1250,2018,Summer,c +207,MATH,1250,2018,Summer,c +261,MATH,1250,2018,Summer,c +277,MATH,1250,2018,Summer,c +288,MATH,1250,2018,Summer,c +321,MATH,1250,2018,Summer,c +362,MATH,1250,2018,Summer,c +106,MATH,1250,2020,Summer,a +108,MATH,1250,2020,Summer,a +133,MATH,1250,2020,Summer,a +135,MATH,1250,2020,Summer,a +151,MATH,1250,2020,Summer,a +167,MATH,1250,2020,Summer,a +185,MATH,1250,2020,Summer,a +231,MATH,1250,2020,Summer,a +281,MATH,1250,2020,Summer,a +289,MATH,1250,2020,Summer,a +309,MATH,1250,2020,Summer,a +342,MATH,1250,2020,Summer,a +378,MATH,1250,2020,Summer,a +384,MATH,1250,2020,Summer,a +386,MATH,1250,2020,Summer,a +391,MATH,1250,2020,Summer,a +177,MATH,1260,2015,Spring,c +144,MATH,1260,2015,Summer,a +162,MATH,1260,2015,Summer,a +211,MATH,1260,2015,Summer,a +229,MATH,1260,2016,Fall,a +278,MATH,1260,2016,Fall,a +304,MATH,1260,2017,Summer,a +353,MATH,1260,2017,Summer,a +361,MATH,1260,2017,Summer,a +252,MATH,1260,2017,Fall,a +260,MATH,1260,2017,Fall,a +291,MATH,1260,2017,Fall,a +133,MATH,1260,2019,Spring,a +256,MATH,1260,2019,Spring,a +347,MATH,1260,2019,Spring,a +152,MATH,1260,2019,Spring,b +169,MATH,1260,2019,Spring,b +179,MATH,1260,2019,Spring,b +187,MATH,1260,2019,Spring,b +247,MATH,1260,2019,Spring,b +277,MATH,1260,2019,Spring,b +285,MATH,1260,2019,Spring,b +313,MATH,1260,2019,Spring,b +356,MATH,1260,2019,Spring,b +102,MATH,1260,2019,Spring,c +165,MATH,1260,2019,Spring,c +293,MATH,1260,2019,Spring,c +321,MATH,1260,2019,Spring,c +113,MATH,1260,2019,Summer,a +118,MATH,1260,2019,Summer,a +124,MATH,1260,2019,Summer,a +131,MATH,1260,2019,Summer,a +185,MATH,1260,2019,Summer,a +257,MATH,1260,2019,Summer,a +276,MATH,1260,2019,Summer,a +318,MATH,1260,2019,Summer,a +391,MATH,1260,2019,Summer,a +397,MATH,1260,2019,Summer,a +120,MATH,1260,2019,Summer,b +123,MATH,1260,2019,Summer,b +194,MATH,1260,2019,Summer,b +276,MATH,1260,2019,Summer,b +303,MATH,1260,2019,Summer,b +314,MATH,1260,2019,Summer,b +377,MATH,1260,2019,Summer,b +100,MATH,1260,2019,Fall,a +108,MATH,1260,2019,Fall,a +258,MATH,1260,2019,Fall,a +309,MATH,1260,2019,Fall,a +364,MATH,1260,2019,Fall,a +375,MATH,1260,2019,Fall,a +164,MATH,1260,2020,Spring,a +173,MATH,1260,2020,Spring,a +231,MATH,1260,2020,Spring,a +235,MATH,1260,2020,Spring,a +242,MATH,1260,2020,Spring,a +276,MATH,2210,2015,Spring,b +120,MATH,2210,2015,Summer,c +212,MATH,2210,2015,Summer,c +348,MATH,2210,2015,Summer,c +172,MATH,2210,2015,Fall,a +182,MATH,2210,2015,Fall,a +373,MATH,2210,2015,Fall,a +176,MATH,2210,2017,Spring,a +208,MATH,2210,2017,Spring,a +215,MATH,2210,2017,Spring,a +249,MATH,2210,2017,Spring,a +261,MATH,2210,2017,Spring,a +270,MATH,2210,2017,Spring,a +314,MATH,2210,2017,Spring,a +128,MATH,2210,2017,Summer,a +277,MATH,2210,2017,Summer,a +361,MATH,2210,2017,Summer,a +387,MATH,2210,2017,Summer,a +392,MATH,2210,2017,Summer,a +117,MATH,2210,2018,Spring,a +123,MATH,2210,2018,Spring,a +262,MATH,2210,2018,Spring,a +391,MATH,2210,2018,Spring,a +131,MATH,2210,2018,Spring,b +185,MATH,2210,2018,Spring,b +197,MATH,2210,2018,Spring,b +199,MATH,2210,2018,Spring,b +229,MATH,2210,2018,Spring,b +230,MATH,2210,2018,Spring,b +231,MATH,2210,2018,Spring,b +239,MATH,2210,2018,Spring,b +256,MATH,2210,2018,Spring,b +275,MATH,2210,2018,Spring,b +309,MATH,2210,2018,Spring,b +369,MATH,2210,2018,Spring,b +102,MATH,2210,2019,Spring,a +169,MATH,2210,2019,Spring,a +285,MATH,2210,2019,Spring,a +119,MATH,2210,2019,Spring,b +173,MATH,2210,2019,Spring,b +228,MATH,2210,2019,Spring,b +285,MATH,2210,2019,Spring,b +296,MATH,2210,2019,Spring,b +305,MATH,2210,2019,Spring,b +342,MATH,2210,2019,Spring,b +375,MATH,2210,2019,Spring,b +113,MATH,2210,2020,Spring,a +255,MATH,2210,2020,Spring,a +274,MATH,2210,2020,Spring,a +347,MATH,2210,2020,Spring,a +124,MATH,2210,2020,Spring,b +170,MATH,2210,2020,Spring,b +200,MATH,2210,2020,Spring,b +241,MATH,2210,2020,Spring,c +251,MATH,2210,2020,Spring,c +274,MATH,2210,2020,Spring,c +122,MATH,2210,2020,Fall,a +136,MATH,2210,2020,Fall,a +167,MATH,2210,2020,Fall,a +175,MATH,2210,2020,Fall,a +179,MATH,2210,2020,Fall,a +225,MATH,2210,2020,Fall,a +272,MATH,2210,2020,Fall,a +281,MATH,2210,2020,Fall,a +329,MATH,2210,2020,Fall,a +345,MATH,2210,2020,Fall,a +378,MATH,2210,2020,Fall,a +384,MATH,2210,2020,Fall,a +397,MATH,2210,2020,Fall,a +179,MATH,2270,2015,Fall,a +212,MATH,2270,2015,Fall,a +210,MATH,2270,2015,Fall,b +313,MATH,2270,2015,Fall,b +132,MATH,2270,2017,Summer,a +143,MATH,2270,2017,Summer,a +277,MATH,2270,2017,Summer,a +304,MATH,2270,2017,Summer,a +318,MATH,2270,2017,Summer,a +107,MATH,2270,2017,Fall,a +109,MATH,2270,2017,Fall,a +292,MATH,2270,2017,Fall,a +329,MATH,2270,2017,Fall,a +246,MATH,2270,2017,Fall,b +259,MATH,2270,2017,Fall,b +342,MATH,2270,2017,Fall,b +356,MATH,2270,2017,Fall,b +120,MATH,2270,2017,Fall,c +131,MATH,2270,2017,Fall,c +182,MATH,2270,2017,Fall,c +394,MATH,2270,2017,Fall,c +102,MATH,2270,2017,Fall,d +107,MATH,2270,2017,Fall,d +123,MATH,2270,2017,Fall,d +124,MATH,2270,2017,Fall,d +128,MATH,2270,2017,Fall,d +182,MATH,2270,2017,Fall,d +276,MATH,2270,2017,Fall,d +291,MATH,2270,2017,Fall,d +312,MATH,2270,2017,Fall,d +314,MATH,2270,2017,Fall,d +397,MATH,2270,2017,Fall,d +255,MATH,2270,2019,Spring,a +285,MATH,2270,2019,Spring,a +366,MATH,2270,2019,Spring,a +379,MATH,2270,2019,Spring,a +139,MATH,2270,2019,Summer,a +146,MATH,2270,2019,Summer,a +173,MATH,2270,2019,Summer,a +248,MATH,2270,2019,Summer,a +377,MATH,2270,2019,Summer,a +194,MATH,2270,2019,Summer,b +303,MATH,2270,2019,Summer,b +325,MATH,2270,2019,Summer,b +378,MATH,2270,2019,Summer,b +183,MATH,2270,2019,Summer,c +345,MATH,2270,2019,Summer,c +396,MATH,2270,2019,Summer,c +399,MATH,2270,2019,Summer,c +254,MATH,2270,2019,Fall,a +333,MATH,2270,2019,Fall,a +175,MATH,2270,2020,Spring,a +178,MATH,2270,2020,Spring,a +223,MATH,2270,2020,Spring,a +258,MATH,2270,2020,Spring,a +270,MATH,2270,2020,Spring,a +309,MATH,2270,2020,Spring,a +130,MATH,2270,2020,Fall,a +152,MATH,2270,2020,Fall,a +177,MATH,2270,2020,Fall,a +181,MATH,2270,2020,Fall,a +230,MATH,2270,2020,Fall,a +240,MATH,2270,2020,Fall,a +331,MATH,2270,2020,Fall,a +348,MATH,2270,2020,Fall,a +360,MATH,2270,2020,Fall,a +373,MATH,2270,2020,Fall,a +391,MATH,2270,2020,Fall,a +398,MATH,2270,2020,Fall,a +119,MATH,2270,2020,Fall,b +127,MATH,2270,2020,Fall,b +129,MATH,2270,2020,Fall,b +135,MATH,2270,2020,Fall,b +167,MATH,2270,2020,Fall,b +186,MATH,2270,2020,Fall,b +260,MATH,2270,2020,Fall,b +321,MATH,2270,2020,Fall,b +331,MATH,2270,2020,Fall,b +348,MATH,2270,2020,Fall,b +371,MATH,2270,2020,Fall,b +391,MATH,2270,2020,Fall,b +204,MATH,2280,2015,Summer,a +249,MATH,2280,2015,Summer,a +123,MATH,2280,2015,Fall,a +276,MATH,2280,2015,Fall,a +393,MATH,2280,2016,Fall,a +182,MATH,2280,2018,Spring,a +230,MATH,2280,2018,Spring,a +238,MATH,2280,2018,Spring,a +256,MATH,2280,2018,Spring,a +262,MATH,2280,2018,Spring,a +307,MATH,2280,2018,Spring,a +387,MATH,2280,2018,Spring,a +173,MATH,2280,2018,Fall,a +220,MATH,2280,2018,Fall,a +259,MATH,2280,2018,Fall,a +342,MATH,2280,2018,Fall,a +104,MATH,2280,2018,Fall,b +119,MATH,2280,2018,Fall,b +165,MATH,2280,2018,Fall,b +227,MATH,2280,2018,Fall,b +359,MATH,2280,2018,Fall,b +119,MATH,2280,2018,Fall,c +120,MATH,2280,2018,Fall,c +178,MATH,2280,2018,Fall,c +196,MATH,2280,2018,Fall,c +309,MATH,2280,2018,Fall,c +345,MATH,2280,2018,Fall,c +100,MATH,2280,2019,Fall,a +102,MATH,2280,2019,Fall,a +270,MATH,2280,2019,Fall,a +314,MATH,2280,2019,Fall,a +133,MATH,2280,2019,Fall,b +247,MATH,2280,2019,Fall,b +267,MATH,2280,2019,Fall,b +318,MATH,2280,2019,Fall,b +379,MATH,2280,2019,Fall,b +390,MATH,2280,2019,Fall,b +146,MATH,2280,2019,Fall,c +223,MATH,2280,2019,Fall,c +234,MATH,2280,2019,Fall,c +248,MATH,2280,2019,Fall,c +270,MATH,2280,2019,Fall,c +292,MATH,2280,2019,Fall,c +107,MATH,2280,2020,Spring,a +183,MATH,2280,2020,Spring,a +210,MATH,2280,2020,Spring,a +255,MATH,2280,2020,Spring,a +285,MATH,2280,2020,Spring,a +313,MATH,2280,2020,Spring,a +106,MATH,2280,2020,Spring,b +169,MATH,2280,2020,Spring,b +285,MATH,2280,2020,Spring,b +398,MATH,2280,2020,Spring,b +177,MATH,3210,2015,Spring,b +282,MATH,3210,2015,Spring,b +394,MATH,3210,2015,Spring,b +144,MATH,3210,2015,Summer,a +210,MATH,3210,2015,Summer,a +215,MATH,3210,2015,Summer,a +301,MATH,3210,2015,Summer,a +126,MATH,3210,2015,Fall,a +172,MATH,3210,2015,Fall,a +246,MATH,3210,2015,Fall,a +307,MATH,3210,2015,Fall,a +313,MATH,3210,2015,Fall,a +374,MATH,3210,2015,Fall,a +138,MATH,3210,2015,Fall,b +192,MATH,3210,2015,Fall,c +172,MATH,3210,2015,Fall,d +335,MATH,3210,2015,Fall,d +149,MATH,3210,2016,Spring,a +229,MATH,3210,2016,Spring,a +276,MATH,3210,2016,Spring,a +102,MATH,3210,2016,Fall,a +134,MATH,3210,2016,Fall,a +195,MATH,3210,2016,Fall,a +277,MATH,3210,2016,Fall,a +120,MATH,3210,2017,Spring,a +207,MATH,3210,2017,Spring,a +304,MATH,3210,2017,Spring,a +107,MATH,3210,2017,Summer,a +292,MATH,3210,2017,Summer,a +309,MATH,3210,2017,Summer,a +372,MATH,3210,2017,Summer,a +270,MATH,3210,2019,Spring,a +348,MATH,3210,2019,Spring,a +364,MATH,3210,2019,Spring,a +378,MATH,3210,2019,Spring,a +399,MATH,3210,2019,Spring,a +259,MATH,3210,2019,Spring,b +314,MATH,3210,2019,Spring,b +321,MATH,3210,2019,Spring,b +124,MATH,3210,2019,Fall,a +223,MATH,3210,2019,Fall,a +230,MATH,3210,2019,Fall,a +248,MATH,3210,2019,Fall,a +284,MATH,3210,2019,Fall,a +285,MATH,3210,2019,Fall,a +358,MATH,3210,2019,Fall,a +123,MATH,3210,2020,Spring,a +146,MATH,3210,2020,Spring,a +181,MATH,3210,2020,Spring,a +251,MATH,3210,2020,Spring,a +113,MATH,3210,2020,Summer,a +135,MATH,3210,2020,Summer,a +166,MATH,3210,2020,Summer,a +171,MATH,3210,2020,Summer,a +187,MATH,3210,2020,Summer,a +260,MATH,3210,2020,Summer,a +312,MATH,3210,2020,Summer,a +368,MATH,3210,2020,Summer,a +391,MATH,3210,2020,Summer,a +109,MATH,3210,2020,Fall,a +200,MATH,3210,2020,Fall,a +227,MATH,3210,2020,Fall,a +255,MATH,3210,2020,Fall,a +256,MATH,3210,2020,Fall,a +289,MATH,3210,2020,Fall,a +329,MATH,3210,2020,Fall,a +365,MATH,3210,2020,Fall,a +386,MATH,3210,2020,Fall,a +397,MATH,3210,2020,Fall,a +210,MATH,3220,2016,Spring,a +285,MATH,3220,2016,Spring,a +373,MATH,3220,2016,Spring,a +195,MATH,3220,2016,Spring,b +301,MATH,3220,2016,Spring,b +392,MATH,3220,2016,Spring,b +119,MATH,3220,2016,Spring,c +216,MATH,3220,2016,Spring,c +374,MATH,3220,2016,Spring,c +192,MATH,3220,2016,Spring,d +210,MATH,3220,2016,Spring,d +290,MATH,3220,2016,Spring,d +394,MATH,3220,2016,Spring,d +163,MATH,3220,2016,Summer,a +214,MATH,3220,2016,Summer,a +270,MATH,3220,2016,Summer,a +276,MATH,3220,2016,Summer,a +278,MATH,3220,2016,Summer,a +246,MATH,3220,2016,Fall,a +277,MATH,3220,2016,Fall,a +385,MATH,3220,2016,Fall,a +134,MATH,3220,2016,Fall,b +245,MATH,3220,2016,Fall,b +264,MATH,3220,2016,Fall,b +329,MATH,3220,2016,Fall,b +123,MATH,3220,2017,Spring,a +176,MATH,3220,2017,Spring,a +391,MATH,3220,2017,Spring,a +102,MATH,3220,2017,Fall,a +107,MATH,3220,2017,Fall,a +207,MATH,3220,2017,Fall,a +266,MATH,3220,2017,Fall,a +311,MATH,3220,2017,Fall,a +377,MATH,3220,2017,Fall,a +139,MATH,3220,2017,Fall,b +261,MATH,3220,2017,Fall,b +326,MATH,3220,2017,Fall,b +366,MATH,3220,2017,Fall,b +237,MATH,3220,2018,Spring,a +292,MATH,3220,2018,Spring,a +296,MATH,3220,2018,Spring,a +345,MATH,3220,2018,Spring,a +362,MATH,3220,2018,Spring,a +379,MATH,3220,2018,Spring,a +101,MATH,3220,2018,Spring,b +132,MATH,3220,2018,Spring,b +312,MATH,3220,2018,Spring,b +387,MATH,3220,2018,Spring,b +127,MATH,3220,2018,Spring,c +131,MATH,3220,2018,Spring,c +165,MATH,3220,2018,Spring,c +229,MATH,3220,2018,Spring,c +305,MATH,3220,2018,Spring,c +309,MATH,3220,2018,Spring,c +312,MATH,3220,2018,Spring,c +129,MATH,3220,2018,Spring,d +179,MATH,3220,2018,Spring,d +203,MATH,3220,2018,Spring,d +238,MATH,3220,2018,Spring,d +177,PHYS,2040,2015,Spring,a +192,PHYS,2040,2015,Spring,a +245,PHYS,2040,2015,Fall,a +149,PHYS,2040,2015,Fall,b +295,PHYS,2040,2015,Fall,b +312,PHYS,2040,2015,Fall,b +373,PHYS,2040,2015,Fall,b +374,PHYS,2040,2015,Fall,b +210,PHYS,2040,2015,Fall,c +212,PHYS,2040,2015,Fall,c +307,PHYS,2040,2015,Fall,c +387,PHYS,2040,2015,Fall,c +321,PHYS,2040,2016,Spring,a +389,PHYS,2040,2016,Spring,a +292,PHYS,2040,2017,Summer,a +203,PHYS,2040,2017,Fall,a +237,PHYS,2040,2017,Fall,a +259,PHYS,2040,2017,Fall,a +314,PHYS,2040,2017,Fall,a +379,PHYS,2040,2017,Fall,a +119,PHYS,2040,2017,Fall,b +256,PHYS,2040,2017,Fall,b +285,PHYS,2040,2017,Fall,b +132,PHYS,2040,2017,Fall,c +187,PHYS,2040,2017,Fall,c +214,PHYS,2040,2017,Fall,c +230,PHYS,2040,2017,Fall,c +266,PHYS,2040,2017,Fall,c +270,PHYS,2040,2017,Fall,c +314,PHYS,2040,2017,Fall,c +348,PHYS,2040,2017,Fall,c +101,PHYS,2040,2018,Spring,a +105,PHYS,2040,2018,Spring,a +123,PHYS,2040,2018,Spring,a +169,PHYS,2040,2018,Spring,a +227,PHYS,2040,2018,Spring,a +342,PHYS,2040,2018,Spring,a +178,PHYS,2040,2019,Spring,a +275,PHYS,2040,2019,Spring,a +296,PHYS,2040,2019,Spring,a +372,PHYS,2040,2019,Spring,a +391,PHYS,2040,2019,Spring,a +399,PHYS,2040,2019,Spring,a +152,PHYS,2040,2019,Spring,b +305,PHYS,2040,2019,Spring,b +120,PHYS,2040,2020,Spring,a +125,PHYS,2040,2020,Spring,a +128,PHYS,2040,2020,Spring,a +131,PHYS,2040,2020,Spring,a +194,PHYS,2040,2020,Spring,a +267,PHYS,2040,2020,Spring,a +313,PHYS,2040,2020,Spring,a +377,PHYS,2060,2015,Spring,a +115,PHYS,2060,2016,Spring,a +195,PHYS,2060,2016,Spring,a +229,PHYS,2060,2016,Spring,a +355,PHYS,2060,2016,Spring,a +379,PHYS,2060,2016,Spring,a +392,PHYS,2060,2016,Spring,a +163,PHYS,2060,2016,Spring,b +290,PHYS,2060,2016,Spring,b +262,PHYS,2060,2016,Summer,a +264,PHYS,2060,2016,Summer,a +278,PHYS,2060,2016,Summer,a +373,PHYS,2060,2016,Summer,a +393,PHYS,2060,2016,Summer,a +276,PHYS,2060,2016,Summer,b +282,PHYS,2060,2016,Summer,b +285,PHYS,2060,2016,Summer,b +348,PHYS,2060,2016,Summer,b +374,PHYS,2060,2016,Summer,b +102,PHYS,2060,2018,Summer,a +131,PHYS,2060,2018,Summer,a +120,PHYS,2060,2018,Fall,a +156,PHYS,2060,2018,Fall,a +239,PHYS,2060,2018,Fall,a +298,PHYS,2060,2018,Fall,a +399,PHYS,2060,2018,Fall,a +127,PHYS,2060,2018,Fall,b +158,PHYS,2060,2018,Fall,b +247,PHYS,2060,2018,Fall,b +248,PHYS,2060,2018,Fall,b +257,PHYS,2060,2018,Fall,b +261,PHYS,2060,2018,Fall,b +270,PHYS,2060,2018,Fall,b +275,PHYS,2060,2018,Fall,b +311,PHYS,2060,2018,Fall,b +329,PHYS,2060,2018,Fall,b +127,PHYS,2060,2018,Fall,c +165,PHYS,2060,2018,Fall,c +217,PHYS,2060,2018,Fall,c +275,PHYS,2060,2018,Fall,c +311,PHYS,2060,2018,Fall,c +318,PHYS,2060,2018,Fall,c +329,PHYS,2060,2018,Fall,c +231,PHYS,2060,2018,Fall,d +252,PHYS,2060,2018,Fall,d +259,PHYS,2060,2018,Fall,d +288,PHYS,2060,2018,Fall,d +311,PHYS,2060,2018,Fall,d +230,PHYS,2060,2019,Summer,a +238,PHYS,2060,2019,Summer,a +277,PHYS,2060,2019,Summer,a +307,PHYS,2060,2019,Summer,a +312,PHYS,2060,2019,Summer,a +398,PHYS,2060,2019,Summer,a +106,PHYS,2060,2019,Summer,b +121,PHYS,2060,2019,Summer,b +179,PHYS,2060,2019,Summer,b +194,PHYS,2060,2019,Summer,b +294,PHYS,2060,2019,Summer,b +313,PHYS,2060,2019,Summer,b +366,PHYS,2060,2019,Summer,b +384,PHYS,2060,2019,Summer,b +397,PHYS,2060,2019,Summer,b +108,PHYS,2060,2019,Fall,a +185,PHYS,2060,2019,Fall,a +210,PHYS,2060,2019,Fall,a +359,PHYS,2060,2019,Fall,a +380,PHYS,2060,2019,Fall,a +171,PHYS,2060,2019,Fall,b +241,PHYS,2060,2019,Fall,b +274,PHYS,2060,2019,Fall,b +341,PHYS,2060,2019,Fall,b +368,PHYS,2060,2019,Fall,b +100,PHYS,2060,2019,Fall,c +123,PHYS,2060,2019,Fall,c +151,PHYS,2060,2019,Fall,c +177,PHYS,2060,2019,Fall,c +375,PHYS,2060,2019,Fall,c +122,PHYS,2060,2020,Spring,a +167,PHYS,2060,2020,Spring,a +223,PHYS,2060,2020,Spring,a +255,PHYS,2060,2020,Spring,a +310,PHYS,2060,2020,Spring,a +321,PHYS,2060,2020,Spring,a +153,PHYS,2060,2020,Spring,b +221,PHYS,2060,2020,Spring,b +240,PHYS,2060,2020,Spring,b +269,PHYS,2060,2020,Spring,b +292,PHYS,2060,2020,Spring,b +293,PHYS,2060,2020,Spring,b +321,PHYS,2060,2020,Spring,b +391,PHYS,2060,2020,Spring,b +112,PHYS,2060,2020,Fall,a +142,PHYS,2060,2020,Fall,a +178,PHYS,2060,2020,Fall,a +181,PHYS,2060,2020,Fall,a +187,PHYS,2060,2020,Fall,a +250,PHYS,2060,2020,Fall,a +371,PHYS,2060,2020,Fall,a +376,PHYS,2060,2020,Fall,a +390,PHYS,2060,2020,Fall,a +193,PHYS,2100,2015,Spring,a +277,PHYS,2100,2015,Spring,b +321,PHYS,2100,2015,Spring,b +120,PHYS,2100,2016,Fall,a +312,PHYS,2100,2016,Fall,a +314,PHYS,2100,2016,Fall,a +392,PHYS,2100,2016,Fall,a +176,PHYS,2100,2016,Fall,b +179,PHYS,2100,2016,Fall,b +278,PHYS,2100,2016,Fall,b +177,PHYS,2100,2017,Summer,a +262,PHYS,2100,2017,Summer,a +276,PHYS,2100,2017,Summer,a +375,PHYS,2100,2017,Summer,a +117,PHYS,2100,2017,Summer,b +177,PHYS,2100,2017,Summer,b +215,PHYS,2100,2017,Summer,b +307,PHYS,2100,2017,Summer,b +377,PHYS,2100,2017,Summer,b +378,PHYS,2100,2017,Summer,b +151,PHYS,2100,2017,Summer,c +173,PHYS,2100,2017,Summer,c +215,PHYS,2100,2017,Summer,c +264,PHYS,2100,2017,Summer,c +353,PHYS,2100,2017,Summer,c +355,PHYS,2100,2017,Summer,c +246,PHYS,2100,2017,Fall,a +374,PHYS,2100,2017,Fall,a +387,PHYS,2100,2017,Fall,a +128,PHYS,2100,2018,Fall,a +158,PHYS,2100,2018,Fall,a +185,PHYS,2100,2018,Fall,a +285,PHYS,2100,2018,Fall,a +288,PHYS,2100,2018,Fall,a +366,PHYS,2100,2019,Summer,a +386,PHYS,2100,2019,Summer,a +399,PHYS,2100,2019,Summer,a +282,PHYS,2140,2015,Spring,a +192,PHYS,2140,2015,Spring,b +394,PHYS,2140,2015,Spring,b +140,PHYS,2140,2015,Summer,a +172,PHYS,2140,2015,Summer,b +176,PHYS,2140,2015,Summer,b +270,PHYS,2140,2015,Summer,b +138,PHYS,2140,2015,Summer,c +246,PHYS,2140,2015,Summer,c +373,PHYS,2140,2015,Summer,c +120,PHYS,2140,2015,Fall,a +276,PHYS,2140,2015,Fall,a +123,PHYS,2140,2016,Spring,a +117,PHYS,2140,2016,Spring,b +313,PHYS,2140,2016,Spring,b +134,PHYS,2140,2016,Spring,c +215,PHYS,2140,2016,Spring,c +307,PHYS,2140,2016,Spring,c +312,PHYS,2140,2016,Summer,a +317,PHYS,2140,2016,Summer,a +277,PHYS,2140,2016,Summer,b +392,PHYS,2140,2016,Summer,b +116,PHYS,2140,2016,Fall,a +335,PHYS,2140,2016,Fall,a +387,PHYS,2140,2016,Fall,a +177,PHYS,2140,2017,Summer,a +255,PHYS,2140,2017,Summer,a +285,PHYS,2140,2017,Summer,a +314,PHYS,2140,2017,Summer,a +187,PHYS,2140,2017,Fall,a +259,PHYS,2140,2017,Fall,a +361,PHYS,2140,2017,Fall,b +379,PHYS,2140,2017,Fall,b +101,PHYS,2140,2018,Summer,a +105,PHYS,2140,2018,Summer,a +113,PHYS,2140,2018,Summer,a +128,PHYS,2140,2018,Summer,a +143,PHYS,2140,2018,Summer,a +151,PHYS,2140,2018,Summer,a +231,PHYS,2140,2018,Summer,a +298,PHYS,2140,2018,Summer,a +199,PHYS,2140,2018,Summer,b +305,PHYS,2140,2018,Summer,b +369,PHYS,2140,2018,Summer,b +163,PHYS,2140,2018,Fall,a +253,PHYS,2140,2018,Fall,a +386,PHYS,2140,2018,Fall,a +129,PHYS,2140,2019,Fall,a +167,PHYS,2140,2019,Fall,a +227,PHYS,2140,2019,Fall,a +329,PHYS,2140,2019,Fall,a +366,PHYS,2140,2019,Fall,a +371,PHYS,2140,2019,Fall,a +289,PHYS,2140,2019,Fall,b +318,PHYS,2140,2019,Fall,b +362,PHYS,2140,2019,Fall,b +377,PHYS,2140,2019,Fall,b +119,PHYS,2140,2020,Fall,a +131,PHYS,2140,2020,Fall,a +136,PHYS,2140,2020,Fall,a +146,PHYS,2140,2020,Fall,a +175,PHYS,2140,2020,Fall,a +185,PHYS,2140,2020,Fall,a +222,PHYS,2140,2020,Fall,a +235,PHYS,2140,2020,Fall,a +267,PHYS,2140,2020,Fall,a +292,PHYS,2140,2020,Fall,a +297,PHYS,2140,2020,Fall,a +309,PHYS,2140,2020,Fall,a +345,PHYS,2140,2020,Fall,a +391,PHYS,2140,2020,Fall,a +246,PHYS,2210,2015,Fall,a +374,PHYS,2210,2015,Fall,b +392,PHYS,2210,2015,Fall,b +379,PHYS,2210,2015,Fall,c +177,PHYS,2210,2017,Summer,a +230,PHYS,2210,2017,Summer,a +231,PHYS,2210,2017,Summer,a +373,PHYS,2210,2017,Summer,a +179,PHYS,2210,2017,Summer,b +285,PHYS,2210,2017,Summer,b +326,PHYS,2210,2017,Summer,b +127,PHYS,2210,2017,Summer,c +342,PHYS,2210,2017,Summer,c +208,PHYS,2210,2017,Summer,d +261,PHYS,2210,2017,Summer,d +304,PHYS,2210,2017,Summer,d +373,PHYS,2210,2017,Summer,d +101,PHYS,2210,2018,Fall,a +113,PHYS,2210,2018,Fall,a +183,PHYS,2210,2018,Fall,a +296,PHYS,2210,2018,Fall,a +329,PHYS,2210,2018,Fall,a +113,PHYS,2210,2018,Fall,b +120,PHYS,2210,2018,Fall,b +133,PHYS,2210,2018,Fall,b +151,PHYS,2210,2018,Fall,b +270,PHYS,2210,2018,Fall,b +274,PHYS,2210,2018,Fall,b +288,PHYS,2210,2018,Fall,b +378,PHYS,2210,2018,Fall,b +120,PHYS,2210,2018,Fall,c +124,PHYS,2210,2018,Fall,c +332,PHYS,2210,2018,Fall,c +362,PHYS,2210,2018,Fall,c +119,PHYS,2210,2019,Spring,a +238,PHYS,2210,2019,Spring,a +255,PHYS,2210,2019,Spring,a +305,PHYS,2210,2019,Spring,a +311,PHYS,2210,2019,Spring,a +157,PHYS,2210,2019,Spring,b +199,PHYS,2210,2019,Spring,b +238,PHYS,2210,2019,Spring,b +102,PHYS,2210,2019,Spring,c +165,PHYS,2210,2019,Spring,c +253,PHYS,2210,2019,Spring,c +292,PHYS,2210,2019,Spring,c +368,PHYS,2210,2019,Spring,c +391,PHYS,2210,2019,Spring,c +187,PHYS,2210,2019,Spring,d +255,PHYS,2210,2019,Spring,d +257,PHYS,2210,2019,Spring,d +391,PHYS,2210,2019,Spring,d +128,PHYS,2210,2019,Summer,a +256,PHYS,2210,2019,Summer,a +289,PHYS,2210,2019,Summer,a +359,PHYS,2210,2019,Summer,a +397,PHYS,2210,2019,Summer,a +123,PHYS,2210,2019,Fall,a +135,PHYS,2210,2019,Fall,a +143,PHYS,2210,2019,Fall,a +241,PHYS,2210,2019,Fall,a +340,PHYS,2210,2019,Fall,a +108,PHYS,2210,2019,Fall,b +171,PHYS,2210,2019,Fall,b +200,PHYS,2210,2019,Fall,b +309,PHYS,2210,2019,Fall,b +312,PHYS,2210,2019,Fall,b +333,PHYS,2210,2019,Fall,b +345,PHYS,2210,2019,Fall,b +363,PHYS,2210,2019,Fall,b +366,PHYS,2210,2019,Fall,b +396,PHYS,2210,2019,Fall,b +123,PHYS,2210,2019,Fall,c +221,PHYS,2210,2019,Fall,c +276,PHYS,2210,2019,Fall,c +347,PHYS,2210,2019,Fall,c +371,PHYS,2210,2019,Fall,c +390,PHYS,2210,2019,Fall,c +303,PHYS,2210,2019,Fall,d +374,PHYS,2220,2015,Spring,a +179,PHYS,2220,2015,Fall,a +276,PHYS,2220,2015,Fall,a +321,PHYS,2220,2015,Fall,a +282,PHYS,2220,2015,Fall,b +172,PHYS,2220,2016,Summer,a +317,PHYS,2220,2016,Summer,a +378,PHYS,2220,2016,Summer,a +391,PHYS,2220,2016,Summer,a +245,PHYS,2220,2016,Fall,a +295,PHYS,2220,2016,Fall,a +356,PHYS,2220,2016,Fall,a +385,PHYS,2220,2016,Fall,a +119,PHYS,2220,2017,Spring,a +176,PHYS,2220,2017,Spring,a +187,PHYS,2220,2017,Spring,a +256,PHYS,2220,2017,Spring,a +313,PHYS,2220,2017,Spring,a +372,PHYS,2220,2017,Spring,a +120,PHYS,2220,2017,Spring,b +312,PHYS,2220,2017,Spring,b +355,PHYS,2220,2017,Spring,b +151,PHYS,2220,2017,Spring,c +187,PHYS,2220,2017,Spring,c +270,PHYS,2220,2017,Spring,c +277,PHYS,2220,2017,Spring,c +119,PHYS,2220,2017,Spring,d +163,PHYS,2220,2017,Spring,d +249,PHYS,2220,2017,Spring,d +288,PHYS,2220,2017,Spring,d +312,PHYS,2220,2017,Spring,d +102,PHYS,2220,2018,Spring,a +105,PHYS,2220,2018,Spring,a +107,PHYS,2220,2018,Spring,a +128,PHYS,2220,2018,Spring,a +132,PHYS,2220,2018,Spring,a +134,PHYS,2220,2018,Spring,a +210,PHYS,2220,2018,Spring,a +214,PHYS,2220,2018,Spring,a +227,PHYS,2220,2018,Spring,a +237,PHYS,2220,2018,Spring,a +239,PHYS,2220,2018,Spring,a +305,PHYS,2220,2018,Spring,a +231,PHYS,2220,2018,Summer,a +255,PHYS,2220,2018,Summer,a +257,PHYS,2220,2018,Summer,a +342,PHYS,2220,2018,Summer,a +344,PHYS,2220,2018,Summer,a +373,PHYS,2220,2018,Summer,a +393,PHYS,2220,2018,Summer,a +123,PHYS,2220,2018,Fall,a +133,PHYS,2220,2018,Fall,a +177,PHYS,2220,2018,Fall,a +178,PHYS,2220,2018,Fall,a +196,PHYS,2220,2018,Fall,a +267,PHYS,2220,2018,Fall,a +285,PHYS,2220,2018,Fall,a +292,PHYS,2220,2018,Fall,a +332,PHYS,2220,2018,Fall,a +241,PHYS,2220,2019,Spring,a +113,PHYS,2220,2020,Spring,a +124,PHYS,2220,2020,Spring,a +175,PHYS,2220,2020,Spring,a +235,PHYS,2220,2020,Spring,a +106,PHYS,2220,2020,Summer,a +118,PHYS,2220,2020,Summer,a +121,PHYS,2220,2020,Summer,a +127,PHYS,2220,2020,Summer,a +194,PHYS,2220,2020,Summer,a +247,PHYS,2220,2020,Summer,a +293,PHYS,2220,2020,Summer,a +296,PHYS,2220,2020,Summer,a +309,PHYS,2220,2020,Summer,a +311,PHYS,2220,2020,Summer,a +339,PHYS,2220,2020,Summer,a +345,PHYS,2220,2020,Summer,a +164,PHYS,2220,2020,Summer,b +242,PHYS,2220,2020,Summer,b +289,PHYS,2220,2020,Summer,b +300,PHYS,2220,2020,Summer,b +323,PHYS,2220,2020,Summer,b +390,PHYS,2220,2020,Summer,b +109,PHYS,2220,2020,Fall,a +228,PHYS,2220,2020,Fall,a +386,PHYS,2220,2020,Fall,a +107,PHYS,3210,2016,Summer,a +249,PHYS,3210,2016,Summer,a +134,PHYS,3210,2016,Summer,b +172,PHYS,3210,2016,Summer,b +249,PHYS,3210,2016,Summer,b +314,PHYS,3210,2016,Summer,b +123,PHYS,3210,2016,Fall,a +260,PHYS,3210,2016,Fall,a +321,PHYS,3210,2016,Fall,a +139,PHYS,3210,2017,Summer,a +179,PHYS,3210,2017,Summer,a +230,PHYS,3210,2017,Summer,a +246,PHYS,3210,2017,Summer,a +373,PHYS,3210,2017,Summer,a +378,PHYS,3210,2017,Summer,a +391,PHYS,3210,2017,Summer,a +393,PHYS,3210,2017,Summer,a +208,PHYS,3210,2017,Summer,b +264,PHYS,3210,2017,Summer,b +379,PHYS,3210,2017,Summer,b +155,PHYS,3210,2017,Fall,a +262,PHYS,3210,2017,Fall,a +270,PHYS,3210,2017,Fall,a +335,PHYS,3210,2017,Fall,a +377,PHYS,3210,2017,Fall,a +397,PHYS,3210,2017,Fall,a +119,PHYS,3210,2018,Spring,a +229,PHYS,3210,2018,Spring,a +277,PHYS,3210,2018,Spring,a +294,PHYS,3210,2018,Spring,a +385,PHYS,3210,2018,Spring,a +274,PHYS,3210,2018,Spring,b +372,PHYS,3210,2018,Spring,b +102,PHYS,3210,2018,Spring,c +105,PHYS,3210,2018,Spring,c +197,PHYS,3210,2018,Spring,c +209,PHYS,3210,2018,Spring,c +374,PHYS,3210,2018,Spring,c +381,PHYS,3210,2018,Spring,c +101,PHYS,3210,2018,Fall,a +109,PHYS,3210,2018,Fall,a +227,PHYS,3210,2018,Fall,a +276,PHYS,3210,2018,Fall,a +285,PHYS,3210,2018,Fall,a +113,PHYS,3210,2019,Spring,a +258,PHYS,3210,2019,Spring,a +329,PHYS,3210,2019,Spring,a +351,PHYS,3210,2019,Spring,a +356,PHYS,3210,2019,Spring,a +384,PHYS,3210,2019,Spring,a +217,PHYS,3210,2019,Spring,b +312,PHYS,3210,2019,Spring,b +351,PHYS,3210,2019,Spring,b +231,PHYS,3210,2019,Spring,c +258,PHYS,3210,2019,Spring,c +292,PHYS,3210,2019,Spring,c +329,PHYS,3210,2019,Spring,c +375,PHYS,3210,2019,Spring,c +156,PHYS,3210,2019,Spring,d +173,PHYS,3210,2019,Spring,d +128,PHYS,3210,2019,Summer,a +133,PHYS,3210,2019,Summer,a +146,PHYS,3210,2019,Summer,a +177,PHYS,3210,2019,Summer,a +199,PHYS,3210,2019,Summer,a +133,PHYS,3210,2019,Summer,b +152,PHYS,3210,2019,Summer,b +255,PHYS,3210,2019,Summer,b +287,PHYS,3210,2019,Summer,b +313,PHYS,3210,2019,Summer,b +362,PHYS,3210,2019,Summer,b +366,PHYS,3210,2019,Summer,b +106,PHYS,3210,2019,Summer,c +152,PHYS,3210,2019,Summer,c +167,PHYS,3210,2019,Summer,c +188,PHYS,3210,2019,Summer,c +307,PHYS,3210,2019,Summer,c +309,PHYS,3210,2019,Summer,c +333,PHYS,3210,2019,Summer,c +345,PHYS,3210,2019,Summer,c +100,PHYS,3210,2019,Fall,a +178,PHYS,3210,2019,Fall,a +125,PHYS,3210,2020,Spring,a +131,PHYS,3210,2020,Spring,a +183,PHYS,3210,2020,Spring,a +185,PHYS,3210,2020,Spring,a +254,PHYS,3210,2020,Spring,a +310,PHYS,3210,2020,Spring,a +348,PHYS,3210,2020,Spring,a +390,PHYS,3210,2020,Spring,a +175,PHYS,3210,2020,Summer,a +187,PHYS,3210,2020,Summer,a +240,PHYS,3210,2020,Summer,a +300,PHYS,3210,2020,Summer,a +136,PHYS,3210,2020,Fall,a +153,PHYS,3210,2020,Fall,a +228,PHYS,3210,2020,Fall,a +289,PHYS,3210,2020,Fall,a +293,PHYS,3210,2020,Fall,a +297,PHYS,3210,2020,Fall,a +306,PHYS,3210,2020,Fall,a +339,PHYS,3210,2020,Fall,a +342,PHYS,3210,2020,Fall,a +121,PHYS,3210,2020,Fall,b +129,PHYS,3210,2020,Fall,b +200,PHYS,3210,2020,Fall,b +228,PHYS,3210,2020,Fall,b +256,PHYS,3210,2020,Fall,b +130,PHYS,3210,2020,Fall,c +331,PHYS,3210,2020,Fall,c +115,PHYS,3220,2016,Summer,a +195,PHYS,3220,2016,Summer,a +285,PHYS,3220,2016,Summer,a +312,PHYS,3220,2016,Summer,a +107,PHYS,3220,2016,Summer,b +123,PHYS,3220,2016,Summer,b +277,PHYS,3220,2016,Summer,b +119,PHYS,3220,2017,Summer,a +139,PHYS,3220,2017,Summer,a +215,PHYS,3220,2017,Summer,a +329,PHYS,3220,2017,Summer,a +392,PHYS,3220,2017,Summer,a +120,PHYS,3220,2017,Fall,a +131,PHYS,3220,2017,Fall,a +155,PHYS,3220,2017,Fall,a +214,PHYS,3220,2017,Fall,a +237,PHYS,3220,2017,Fall,a +109,PHYS,3220,2017,Fall,b +203,PHYS,3220,2017,Fall,b +345,PHYS,3220,2017,Fall,b +213,PHYS,3220,2017,Fall,c +230,PHYS,3220,2017,Fall,c +307,PHYS,3220,2017,Fall,c +127,PHYS,3220,2017,Fall,d +187,PHYS,3220,2017,Fall,d +252,PHYS,3220,2017,Fall,d +270,PHYS,3220,2017,Fall,d +276,PHYS,3220,2017,Fall,d +288,PHYS,3220,2017,Fall,d +128,PHYS,3220,2018,Summer,a +143,PHYS,3220,2018,Summer,a +260,PHYS,3220,2018,Summer,a +377,PHYS,3220,2018,Summer,a +379,PHYS,3220,2018,Summer,a +398,PHYS,3220,2018,Summer,a +102,PHYS,3220,2020,Spring,a +133,PHYS,3220,2020,Spring,a +170,PHYS,3220,2020,Spring,a +267,PHYS,3220,2020,Spring,a +310,PHYS,3220,2020,Spring,a +227,PHYS,3220,2020,Spring,b +241,PHYS,3220,2020,Spring,b +251,PHYS,3220,2020,Spring,b +255,PHYS,3220,2020,Spring,b +269,PHYS,3220,2020,Spring,b +321,PHYS,3220,2020,Spring,b +348,PHYS,3220,2020,Spring,b +106,PHYS,3220,2020,Spring,c +152,PHYS,3220,2020,Spring,c +185,PHYS,3220,2020,Spring,c +194,PHYS,3220,2020,Spring,c +200,PHYS,3220,2020,Spring,c +241,PHYS,3220,2020,Spring,c +251,PHYS,3220,2020,Spring,c +271,PHYS,3220,2020,Spring,c +296,PHYS,3220,2020,Spring,c +325,PHYS,3220,2020,Spring,c +365,PHYS,3220,2020,Spring,c +124,PHYS,3220,2020,Spring,d +167,PHYS,3220,2020,Spring,d +185,PHYS,3220,2020,Spring,d +227,PHYS,3220,2020,Spring,d +303,PHYS,3220,2020,Spring,d +341,PHYS,3220,2020,Spring,d +342,PHYS,3220,2020,Spring,d +373,PHYS,3220,2020,Spring,d diff --git a/tests/data/Grade.csv b/tests/data/Grade.csv new file mode 100644 index 000000000..8ba592194 --- /dev/null +++ b/tests/data/Grade.csv @@ -0,0 +1,3028 @@ +student_id,dept,course,term_year,term,section,grade +100,CS,1030,2020,Spring,a,A +101,PHYS,2040,2018,Spring,a,A +102,BIOL,1006,2018,Fall,a,A +104,MATH,2280,2018,Fall,b,A +105,PHYS,3210,2018,Spring,c,A +107,MATH,3210,2017,Summer,a,A +107,PHYS,2220,2018,Spring,a,A +109,BIOL,2355,2019,Spring,d,A +113,CS,3200,2020,Summer,a,A +113,CS,3505,2019,Summer,d,A +115,BIOL,1030,2017,Spring,a,A +118,CS,2100,2019,Fall,b,A +119,BIOL,2355,2018,Summer,d,A +119,CS,3505,2019,Summer,a,A +119,CS,4940,2017,Fall,b,A +119,MATH,2280,2018,Fall,c,A +119,PHYS,3210,2018,Spring,a,A +120,PHYS,2060,2018,Fall,a,A +122,CS,4970,2020,Fall,a,A +123,BIOL,2030,2017,Spring,a,A +123,BIOL,2325,2017,Fall,b,A +123,BIOL,2355,2017,Summer,a,A +123,CS,4940,2020,Summer,b,A +123,MATH,3220,2017,Spring,a,A +124,CS,2100,2018,Fall,c,A +124,CS,2420,2019,Summer,a,A +124,MATH,3210,2019,Fall,a,A +125,BIOL,2330,2019,Fall,a,A +127,BIOL,2355,2018,Fall,a,A +127,PHYS,2060,2018,Fall,c,A +127,PHYS,2220,2020,Summer,a,A +128,BIOL,1006,2017,Fall,a,A +128,BIOL,2010,2020,Summer,b,A +128,CS,3505,2017,Fall,a,A +128,CS,4500,2018,Spring,a,A +132,BIOL,1030,2018,Summer,a,A +132,CS,4500,2018,Spring,b,A +132,CS,4970,2018,Summer,b,A +135,CS,4400,2019,Summer,b,A +139,BIOL,1006,2019,Summer,a,A +139,CS,4000,2017,Summer,a,A +140,CS,3810,2015,Spring,a,A +140,CS,4400,2015,Summer,a,A +143,CS,2100,2017,Fall,a,A +145,MATH,1220,2017,Spring,c,A +146,CS,4970,2020,Summer,c,A +146,PHYS,2140,2020,Fall,a,A +149,BIOL,2325,2015,Fall,c,A +149,PHYS,2040,2015,Fall,b,A +151,BIOL,2355,2019,Spring,b,A +151,CS,4970,2020,Summer,b,A +151,MATH,1220,2020,Spring,a,A +152,BIOL,2021,2018,Fall,b,A +155,PHYS,3210,2017,Fall,a,A +155,PHYS,3220,2017,Fall,a,A +165,BIOL,2330,2017,Fall,a,A +165,MATH,1260,2019,Spring,c,A +166,CS,3500,2020,Summer,a,A +167,BIOL,2355,2020,Fall,a,A +167,PHYS,3220,2020,Spring,d,A +168,CS,2420,2020,Fall,a,A +169,CS,2100,2019,Summer,b,A +169,MATH,2280,2020,Spring,b,A +169,PHYS,2040,2018,Spring,a,A +170,CS,4940,2020,Summer,a,A +173,BIOL,1006,2019,Fall,a,A +173,MATH,2210,2019,Spring,b,A +175,PHYS,3210,2020,Summer,a,A +176,BIOL,1006,2016,Spring,a,A +176,PHYS,2140,2015,Summer,b,A +177,BIOL,2330,2016,Fall,a,A +177,BIOL,2420,2015,Spring,a,A +177,CS,3810,2018,Summer,b,A +177,MATH,1260,2015,Spring,c,A +179,CS,2100,2016,Summer,a,A +179,PHYS,2060,2019,Summer,b,A +185,MATH,1250,2020,Summer,a,A +185,MATH,1260,2019,Summer,a,A +186,MATH,2270,2020,Fall,b,A +187,CS,4970,2020,Summer,b,A +187,PHYS,3210,2020,Summer,a,A +191,CS,4970,2020,Fall,a,A +192,BIOL,2020,2015,Fall,d,A +200,PHYS,3220,2020,Spring,c,A +203,PHYS,3220,2017,Fall,b,A +207,BIOL,2355,2018,Summer,d,A +207,CS,1410,2016,Summer,a,A +207,MATH,1250,2018,Summer,c,A +210,MATH,3220,2016,Spring,d,A +214,MATH,3220,2016,Summer,a,A +215,CS,4500,2016,Spring,b,A +215,PHYS,2140,2016,Spring,c,A +216,CS,1410,2016,Spring,b,A +217,BIOL,1010,2019,Spring,b,A +217,PHYS,2060,2018,Fall,c,A +223,PHYS,2060,2020,Spring,a,A +224,BIOL,2420,2020,Fall,a,A +227,BIOL,2330,2019,Fall,a,A +228,CS,4970,2020,Summer,d,A +229,CS,2420,2016,Fall,a,A +230,CS,3505,2019,Spring,b,A +230,MATH,1250,2017,Summer,c,A +230,PHYS,2210,2017,Summer,a,A +231,BIOL,2210,2017,Spring,a,A +231,CS,2100,2018,Fall,c,A +231,MATH,1220,2019,Fall,a,A +234,CS,4400,2019,Summer,a,A +237,CS,3810,2018,Spring,a,A +238,BIOL,2021,2019,Spring,b,A +240,MATH,2270,2020,Fall,a,A +241,CS,2100,2019,Summer,a,A +242,CS,4970,2020,Summer,a,A +246,BIOL,2420,2015,Spring,b,A +247,CS,3505,2018,Summer,a,A +249,BIOL,1006,2015,Summer,b,A +249,CS,4150,2016,Summer,a,A +249,CS,4150,2016,Summer,b,A +249,PHYS,3210,2016,Summer,b,A +252,CS,3810,2018,Summer,d,A +255,CS,2100,2018,Spring,a,A +255,CS,4400,2017,Spring,b,A +255,CS,4500,2019,Fall,d,A +256,CS,4500,2019,Fall,a,A +257,BIOL,1030,2017,Spring,c,A +257,CS,3505,2020,Summer,a,A +257,MATH,1250,2017,Summer,c,A +260,CS,4150,2019,Spring,a,A +262,CS,2420,2016,Fall,b,A +262,CS,4400,2016,Summer,a,A +262,CS,4970,2018,Fall,b,A +264,BIOL,2420,2017,Summer,a,A +264,PHYS,3210,2017,Summer,b,A +267,PHYS,2040,2020,Spring,a,A +269,PHYS,2060,2020,Spring,b,A +270,PHYS,2060,2018,Fall,b,A +271,CS,1030,2020,Fall,a,A +273,BIOL,1030,2016,Spring,a,A +274,PHYS,2060,2019,Fall,b,A +275,BIOL,1210,2017,Summer,a,A +275,BIOL,2210,2018,Spring,a,A +275,MATH,2210,2018,Spring,b,A +276,CS,3200,2018,Spring,b,A +276,CS,4970,2016,Fall,b,A +277,BIOL,2330,2017,Summer,a,A +277,CS,4000,2020,Fall,a,A +277,CS,4970,2018,Summer,a,A +277,PHYS,2100,2015,Spring,b,A +277,PHYS,2140,2016,Summer,b,A +282,CS,4970,2017,Spring,a,A +283,CS,4970,2020,Fall,b,A +285,BIOL,1010,2018,Summer,b,A +285,BIOL,2020,2018,Spring,a,A +285,BIOL,2030,2017,Spring,d,A +285,BIOL,2420,2020,Spring,a,A +285,CS,4400,2019,Summer,a,A +285,MATH,2280,2020,Spring,a,A +285,PHYS,2220,2018,Fall,a,A +288,MATH,1250,2018,Summer,c,A +289,PHYS,2140,2019,Fall,b,A +290,BIOL,1030,2016,Summer,a,A +292,BIOL,2010,2020,Spring,b,A +292,BIOL,2021,2017,Fall,a,A +292,CS,3200,2020,Summer,a,A +292,MATH,1250,2017,Summer,a,A +292,PHYS,2140,2020,Fall,a,A +293,BIOL,2210,2019,Fall,a,A +293,CS,2100,2019,Summer,a,A +293,PHYS,3210,2020,Fall,a,A +295,MATH,1210,2016,Spring,b,A +299,CS,2420,2017,Summer,b,A +300,CS,3505,2019,Summer,c,A +302,CS,2420,2015,Summer,c,A +307,CS,4400,2016,Spring,a,A +307,MATH,2280,2018,Spring,a,A +307,PHYS,2060,2019,Summer,a,A +310,PHYS,3220,2020,Spring,a,A +311,BIOL,2030,2020,Spring,b,A +311,BIOL,2420,2020,Summer,a,A +311,CS,3810,2018,Summer,c,A +312,BIOL,2330,2015,Fall,d,A +312,PHYS,2060,2019,Summer,a,A +313,BIOL,2420,2020,Summer,a,A +313,PHYS,2220,2017,Spring,a,A +314,BIOL,2030,2016,Fall,a,A +314,CS,3810,2016,Summer,a,A +314,MATH,1260,2019,Summer,b,A +314,MATH,2210,2017,Spring,a,A +318,BIOL,2355,2017,Summer,a,A +321,CS,3500,2019,Fall,b,A +321,CS,4400,2019,Spring,a,A +321,MATH,1220,2019,Fall,b,A +321,MATH,3210,2019,Spring,b,A +323,PHYS,2220,2020,Summer,b,A +329,BIOL,1006,2019,Summer,a,A +329,CS,4400,2017,Spring,a,A +331,PHYS,3210,2020,Fall,c,A +333,CS,3500,2020,Summer,a,A +333,CS,3810,2019,Fall,a,A +335,PHYS,2140,2016,Fall,a,A +336,BIOL,2010,2015,Fall,a,A +340,BIOL,1010,2020,Summer,d,A +340,BIOL,2021,2019,Fall,a,A +342,BIOL,2030,2018,Summer,a,A +342,PHYS,3220,2020,Spring,d,A +345,CS,4400,2019,Spring,d,A +345,PHYS,2210,2019,Fall,b,A +347,BIOL,2210,2020,Fall,a,A +347,BIOL,2420,2020,Summer,a,A +348,BIOL,2355,2018,Summer,b,A +348,CS,3200,2016,Fall,b,A +348,MATH,1220,2018,Summer,a,A +351,CS,4970,2019,Spring,a,A +353,BIOL,1010,2017,Summer,a,A +353,MATH,1260,2017,Summer,a,A +356,MATH,1210,2017,Spring,a,A +357,BIOL,2325,2016,Summer,a,A +359,MATH,2280,2018,Fall,b,A +362,BIOL,1006,2018,Spring,a,A +362,BIOL,2030,2019,Summer,b,A +362,PHYS,2140,2019,Fall,b,A +364,MATH,3210,2019,Spring,a,A +366,BIOL,2355,2017,Fall,a,A +366,CS,1410,2018,Spring,d,A +366,MATH,3220,2017,Fall,b,A +366,PHYS,3210,2019,Summer,b,A +368,CS,4500,2020,Summer,a,A +369,CS,2420,2016,Fall,a,A +369,CS,4400,2017,Spring,a,A +371,CS,3505,2018,Fall,c,A +372,MATH,1210,2018,Spring,a,A +373,BIOL,2355,2017,Fall,b,A +373,PHYS,2220,2018,Summer,a,A +374,PHYS,2100,2017,Fall,a,A +375,BIOL,2355,2017,Summer,a,A +377,BIOL,1210,2017,Spring,a,A +377,BIOL,2030,2017,Spring,a,A +378,PHYS,2210,2018,Fall,b,A +379,BIOL,2355,2018,Summer,b,A +379,CS,4970,2020,Summer,b,A +380,PHYS,2060,2019,Fall,a,A +384,CS,4970,2020,Summer,c,A +384,PHYS,3210,2019,Spring,a,A +386,BIOL,2325,2018,Summer,a,A +386,MATH,1250,2020,Summer,a,A +387,BIOL,2020,2018,Fall,c,A +387,MATH,2280,2018,Spring,a,A +387,PHYS,2100,2017,Fall,a,A +391,CS,4940,2020,Summer,a,A +391,CS,4940,2020,Summer,b,A +391,PHYS,2040,2019,Spring,a,A +391,PHYS,2140,2020,Fall,a,A +391,PHYS,2210,2019,Spring,d,A +392,BIOL,1006,2017,Fall,a,A +393,CS,3100,2017,Summer,a,A +394,MATH,2270,2017,Fall,c,A +394,PHYS,2140,2015,Spring,b,A +396,CS,3500,2019,Summer,a,A +397,BIOL,1010,2017,Spring,a,A +397,CS,3500,2019,Fall,a,A +397,CS,4940,2020,Summer,a,A +397,PHYS,3210,2017,Fall,a,A +399,PHYS,2060,2018,Fall,a,A +399,PHYS,2100,2019,Summer,a,A +100,MATH,1220,2020,Spring,a,A- +102,BIOL,1030,2018,Fall,a,A- +102,BIOL,2020,2019,Summer,a,A- +102,BIOL,2021,2018,Spring,a,A- +102,BIOL,2210,2019,Summer,a,A- +102,CS,4150,2019,Spring,a,A- +102,MATH,1250,2018,Summer,a,A- +107,BIOL,2021,2019,Fall,a,A- +107,CS,3505,2016,Summer,a,A- +107,PHYS,3220,2016,Summer,b,A- +108,BIOL,1010,2020,Summer,b,A- +109,BIOL,1030,2020,Summer,a,A- +109,CS,4970,2020,Summer,d,A- +110,CS,3505,2020,Fall,b,A- +113,BIOL,2030,2019,Summer,b,A- +113,MATH,2210,2020,Spring,a,A- +113,PHYS,2210,2018,Fall,a,A- +113,PHYS,2210,2018,Fall,b,A- +118,CS,4970,2020,Summer,b,A- +120,CS,4970,2017,Spring,a,A- +120,PHYS,2210,2018,Fall,c,A- +120,PHYS,3220,2017,Fall,a,A- +123,BIOL,1010,2015,Summer,b,A- +123,CS,2100,2016,Summer,a,A- +123,MATH,1250,2018,Spring,a,A- +123,MATH,1260,2019,Summer,b,A- +123,MATH,2270,2017,Fall,d,A- +123,MATH,3210,2020,Spring,a,A- +123,PHYS,2040,2018,Spring,a,A- +123,PHYS,3220,2016,Summer,b,A- +124,BIOL,2420,2020,Summer,a,A- +124,MATH,1260,2019,Summer,a,A- +126,BIOL,2020,2015,Fall,a,A- +126,MATH,3210,2015,Fall,a,A- +127,BIOL,2021,2018,Fall,a,A- +127,PHYS,3220,2017,Fall,d,A- +128,CS,1030,2018,Fall,a,A- +128,CS,2420,2017,Fall,a,A- +129,BIOL,2020,2018,Spring,a,A- +130,CS,4970,2020,Fall,c,A- +131,BIOL,1210,2018,Spring,a,A- +131,MATH,2210,2018,Spring,b,A- +133,MATH,1250,2020,Summer,a,A- +138,CS,4940,2015,Summer,a,A- +138,MATH,3210,2015,Fall,b,A- +142,BIOL,1006,2020,Spring,a,A- +142,CS,3500,2020,Summer,a,A- +143,CS,3500,2019,Fall,c,A- +143,CS,3505,2018,Summer,b,A- +143,PHYS,2140,2018,Summer,a,A- +144,BIOL,2020,2015,Summer,a,A- +151,BIOL,1010,2017,Summer,a,A- +151,CS,2420,2016,Fall,b,A- +160,CS,2420,2015,Summer,a,A- +162,MATH,1220,2015,Summer,b,A- +169,CS,3505,2019,Summer,a,A- +170,CS,4400,2020,Spring,a,A- +171,CS,4940,2020,Summer,b,A- +172,CS,2420,2016,Summer,a,A- +173,BIOL,1210,2019,Spring,a,A- +173,BIOL,2010,2017,Summer,a,A- +173,CS,4500,2019,Fall,b,A- +175,BIOL,2420,2020,Fall,a,A- +178,BIOL,2010,2020,Spring,b,A- +179,BIOL,1030,2019,Spring,a,A- +179,CS,4500,2016,Spring,b,A- +181,CS,4000,2020,Spring,b,A- +181,MATH,3210,2020,Spring,a,A- +182,MATH,1250,2016,Fall,b,A- +183,CS,2100,2019,Fall,d,A- +185,CS,1030,2019,Fall,b,A- +185,PHYS,2100,2018,Fall,a,A- +187,BIOL,2210,2017,Summer,a,A- +187,CS,3810,2020,Fall,a,A- +187,CS,4000,2017,Spring,a,A- +187,PHYS,2220,2017,Spring,c,A- +192,CS,3505,2015,Spring,a,A- +193,BIOL,2010,2015,Spring,a,A- +193,PHYS,2100,2015,Spring,a,A- +194,CS,4970,2019,Fall,a,A- +194,PHYS,2220,2020,Summer,a,A- +195,CS,3100,2016,Spring,d,A- +196,CS,3100,2019,Spring,a,A- +197,MATH,1250,2018,Summer,c,A- +199,BIOL,2020,2018,Fall,a,A- +199,CS,2100,2018,Fall,d,A- +202,CS,4400,2020,Fall,b,A- +203,CS,4500,2018,Spring,a,A- +204,CS,2420,2015,Summer,a,A- +208,BIOL,2010,2017,Fall,a,A- +208,MATH,2210,2017,Spring,a,A- +210,PHYS,2220,2018,Spring,a,A- +212,BIOL,2030,2015,Fall,a,A- +212,PHYS,2040,2015,Fall,c,A- +214,CS,4970,2018,Summer,c,A- +215,CS,4400,2015,Summer,a,A- +215,MATH,1250,2016,Fall,a,A- +215,MATH,2210,2017,Spring,a,A- +221,PHYS,2210,2019,Fall,c,A- +228,BIOL,2210,2019,Summer,b,A- +228,MATH,2210,2019,Spring,b,A- +229,MATH,3220,2018,Spring,c,A- +230,CS,4400,2020,Fall,a,A- +231,BIOL,1010,2019,Spring,b,A- +233,CS,4940,2020,Summer,b,A- +235,CS,3505,2019,Fall,c,A- +237,BIOL,2355,2017,Fall,a,A- +237,PHYS,2220,2018,Spring,a,A- +240,CS,3810,2018,Summer,c,A- +240,CS,4150,2018,Fall,a,A- +241,CS,3505,2019,Spring,a,A- +243,BIOL,2030,2017,Spring,b,A- +243,BIOL,2210,2016,Summer,a,A- +243,BIOL,2355,2017,Spring,d,A- +245,CS,3810,2016,Fall,b,A- +246,MATH,3220,2016,Fall,a,A- +247,BIOL,1006,2019,Summer,a,A- +247,BIOL,2355,2019,Spring,a,A- +248,CS,3505,2019,Fall,c,A- +248,MATH,3210,2019,Fall,a,A- +250,CS,4940,2020,Summer,b,A- +252,CS,3505,2018,Fall,b,A- +254,PHYS,3210,2020,Spring,a,A- +255,CS,4150,2018,Fall,b,A- +255,PHYS,2220,2018,Summer,a,A- +257,CS,3200,2018,Spring,a,A- +258,CS,4400,2020,Fall,a,A- +260,CS,3100,2017,Fall,a,A- +260,MATH,3210,2020,Summer,a,A- +261,CS,2100,2018,Summer,a,A- +261,MATH,3220,2017,Fall,b,A- +262,BIOL,2010,2017,Fall,a,A- +262,CS,3505,2018,Summer,b,A- +262,PHYS,2060,2016,Summer,a,A- +270,BIOL,2010,2018,Spring,a,A- +270,BIOL,2021,2016,Fall,a,A- +270,CS,3500,2019,Fall,b,A- +271,CS,4940,2020,Summer,b,A- +272,MATH,2210,2020,Fall,a,A- +275,BIOL,2325,2018,Summer,a,A- +276,BIOL,2020,2018,Fall,a,A- +276,CS,4000,2016,Fall,a,A- +276,PHYS,2060,2016,Summer,b,A- +276,PHYS,2100,2017,Summer,a,A- +277,BIOL,2355,2018,Spring,a,A- +277,CS,1030,2016,Summer,a,A- +277,CS,1410,2020,Spring,b,A- +277,CS,2420,2015,Spring,a,A- +277,CS,4150,2020,Spring,a,A- +277,MATH,3210,2016,Fall,a,A- +278,CS,2100,2016,Summer,c,A- +279,MATH,1210,2018,Summer,a,A- +282,BIOL,1030,2016,Spring,a,A- +282,CS,2420,2016,Fall,a,A- +282,PHYS,2060,2016,Summer,b,A- +285,MATH,1250,2016,Summer,a,A- +285,MATH,1260,2019,Spring,b,A- +285,PHYS,3210,2018,Fall,a,A- +287,PHYS,3210,2019,Summer,b,A- +288,BIOL,2020,2018,Fall,d,A- +288,CS,3100,2019,Spring,b,A- +288,PHYS,3220,2017,Fall,d,A- +289,PHYS,2220,2020,Summer,b,A- +289,PHYS,3210,2020,Fall,a,A- +290,BIOL,2330,2015,Fall,a,A- +290,CS,3200,2016,Summer,a,A- +292,CS,4400,2020,Fall,b,A- +292,CS,4970,2020,Summer,c,A- +292,PHYS,2220,2018,Fall,a,A- +293,CS,4970,2019,Fall,b,A- +293,PHYS,2060,2020,Spring,b,A- +294,CS,4500,2018,Spring,a,A- +295,CS,3200,2015,Fall,d,A- +296,BIOL,2021,2018,Fall,c,A- +296,MATH,2210,2019,Spring,b,A- +296,PHYS,2220,2020,Summer,a,A- +298,CS,4970,2019,Summer,b,A- +300,BIOL,2330,2020,Spring,a,A- +300,CS,4500,2019,Fall,b,A- +300,CS,4940,2020,Summer,b,A- +300,PHYS,3210,2020,Summer,a,A- +301,MATH,3220,2016,Spring,b,A- +305,BIOL,1210,2019,Spring,a,A- +305,MATH,3220,2018,Spring,c,A- +305,PHYS,2220,2018,Spring,a,A- +307,PHYS,2140,2016,Spring,c,A- +307,PHYS,3210,2019,Summer,c,A- +311,CS,4000,2020,Spring,b,A- +311,MATH,1250,2017,Summer,b,A- +311,MATH,3220,2017,Fall,a,A- +311,PHYS,2220,2020,Summer,a,A- +312,BIOL,2021,2018,Summer,a,A- +313,BIOL,1006,2020,Fall,b,A- +313,CS,3505,2015,Fall,a,A- +313,MATH,1250,2018,Summer,b,A- +314,MATH,1220,2017,Spring,b,A- +317,BIOL,1010,2016,Summer,a,A- +317,PHYS,2220,2016,Summer,a,A- +318,CS,4970,2019,Fall,d,A- +321,MATH,1250,2018,Summer,b,A- +321,MATH,1250,2018,Summer,c,A- +325,CS,3200,2020,Spring,c,A- +329,MATH,1220,2020,Summer,a,A- +329,MATH,3220,2016,Fall,b,A- +330,BIOL,1006,2020,Spring,a,A- +332,BIOL,2355,2018,Summer,c,A- +333,PHYS,2210,2019,Fall,b,A- +335,BIOL,1030,2017,Spring,c,A- +335,MATH,3210,2015,Fall,d,A- +339,CS,3505,2020,Fall,a,A- +340,BIOL,2330,2020,Spring,a,A- +342,BIOL,2325,2019,Spring,b,A- +342,BIOL,2355,2018,Summer,a,A- +342,PHYS,3210,2020,Fall,a,A- +344,CS,1030,2018,Fall,a,A- +345,CS,2100,2018,Summer,c,A- +345,CS,2420,2020,Fall,a,A- +345,PHYS,2220,2020,Summer,a,A- +347,CS,4150,2020,Fall,a,A- +348,CS,1410,2018,Spring,a,A- +348,CS,3500,2020,Summer,a,A- +357,CS,4500,2016,Spring,b,A- +359,CS,3810,2019,Fall,b,A- +359,PHYS,2060,2019,Fall,a,A- +361,CS,4500,2018,Spring,a,A- +361,MATH,2210,2017,Summer,a,A- +362,PHYS,3210,2019,Summer,b,A- +363,CS,4970,2019,Summer,c,A- +363,PHYS,2210,2019,Fall,b,A- +366,CS,3100,2019,Spring,b,A- +368,CS,2100,2019,Summer,b,A- +369,BIOL,2325,2016,Summer,a,A- +369,MATH,2210,2018,Spring,b,A- +371,CS,2100,2018,Summer,c,A- +372,BIOL,2355,2019,Spring,b,A- +373,BIOL,2420,2020,Spring,a,A- +373,CS,3200,2016,Summer,a,A- +373,CS,4400,2015,Fall,c,A- +373,PHYS,2060,2016,Summer,a,A- +374,BIOL,2325,2018,Spring,a,A- +374,CS,3100,2016,Spring,b,A- +374,MATH,3220,2016,Spring,c,A- +374,PHYS,2040,2015,Fall,b,A- +377,CS,3810,2018,Summer,b,A- +377,MATH,1260,2019,Summer,b,A- +378,BIOL,2030,2017,Spring,c,A- +378,PHYS,2220,2016,Summer,a,A- +379,BIOL,2021,2016,Fall,a,A- +379,CS,4940,2017,Fall,b,A- +379,CS,4970,2020,Summer,d,A- +379,PHYS,3220,2018,Summer,a,A- +380,BIOL,2330,2019,Fall,a,A- +384,MATH,1250,2020,Summer,a,A- +385,PHYS,3210,2018,Spring,a,A- +386,CS,3810,2018,Summer,a,A- +386,CS,4500,2019,Summer,a,A- +388,CS,3810,2018,Spring,a,A- +391,BIOL,2420,2020,Fall,a,A- +391,CS,3505,2019,Fall,a,A- +392,CS,4970,2018,Summer,c,A- +392,MATH,1210,2017,Summer,c,A- +392,PHYS,2060,2016,Spring,a,A- +393,BIOL,2355,2018,Spring,a,A- +393,CS,3505,2016,Summer,a,A- +395,CS,3500,2016,Spring,a,A- +396,MATH,2270,2019,Summer,c,A- +397,BIOL,1006,2018,Spring,a,A- +397,BIOL,2030,2016,Fall,a,A- +397,CS,3200,2017,Spring,a,A- +398,BIOL,1006,2019,Fall,b,A- +398,CS,4940,2019,Fall,a,A- +398,MATH,1210,2018,Summer,a,A- +399,CS,3810,2018,Summer,b,A- +100,CS,4970,2018,Summer,b,B +100,PHYS,3210,2019,Fall,a,B +102,BIOL,1010,2018,Summer,a,B +102,BIOL,2325,2017,Fall,b,B +105,BIOL,2355,2017,Spring,b,B +105,MATH,1220,2017,Spring,d,B +105,PHYS,2140,2018,Summer,a,B +106,MATH,1210,2020,Spring,b,B +106,MATH,1250,2020,Summer,a,B +106,PHYS,3220,2020,Spring,c,B +107,CS,3500,2016,Summer,a,B +107,PHYS,3210,2016,Summer,a,B +108,CS,3200,2020,Spring,c,B +108,MATH,1260,2019,Fall,a,B +109,BIOL,1006,2019,Fall,a,B +112,CS,3200,2020,Summer,a,B +113,CS,3810,2020,Fall,a,B +115,BIOL,2020,2016,Spring,a,B +117,BIOL,1006,2018,Spring,a,B +117,BIOL,2021,2018,Summer,a,B +118,CS,2100,2019,Fall,a,B +119,MATH,1210,2016,Spring,a,B +119,MATH,3220,2016,Spring,c,B +120,CS,1410,2018,Spring,b,B +121,PHYS,2060,2019,Summer,b,B +122,BIOL,1010,2020,Summer,b,B +122,CS,2100,2020,Fall,a,B +123,MATH,1210,2019,Summer,a,B +123,MATH,2210,2018,Spring,a,B +124,BIOL,2355,2020,Fall,a,B +124,CS,4970,2019,Summer,a,B +124,MATH,2270,2017,Fall,d,B +127,CS,4970,2019,Fall,b,B +127,MATH,1250,2017,Summer,a,B +127,MATH,3220,2018,Spring,c,B +128,BIOL,2210,2018,Spring,a,B +128,BIOL,2420,2020,Summer,a,B +129,CS,3505,2019,Summer,b,B +131,MATH,3220,2018,Spring,c,B +132,CS,4500,2018,Spring,a,B +133,BIOL,2021,2018,Fall,d,B +133,CS,3810,2018,Summer,c,B +134,CS,4000,2017,Summer,a,B +135,CS,3200,2020,Fall,a,B +135,MATH,1220,2019,Fall,c,B +139,MATH,1220,2018,Summer,a,B +143,CS,4970,2018,Fall,c,B +144,MATH,3210,2015,Summer,a,B +146,CS,2100,2019,Fall,c,B +149,CS,3500,2015,Fall,b,B +151,BIOL,2325,2018,Summer,a,B +151,BIOL,2420,2020,Summer,a,B +151,CS,4400,2019,Spring,b,B +151,MATH,1250,2020,Summer,a,B +152,MATH,1260,2019,Spring,b,B +153,BIOL,1010,2020,Summer,a,B +158,MATH,1250,2018,Summer,c,B +162,CS,4150,2015,Summer,a,B +163,MATH,1210,2018,Fall,b,B +164,BIOL,2030,2020,Spring,a,B +164,CS,3500,2020,Summer,a,B +164,CS,3505,2020,Spring,a,B +167,CS,4500,2020,Summer,a,B +169,CS,4000,2020,Spring,a,B +169,CS,4500,2020,Spring,a,B +170,MATH,2210,2020,Spring,b,B +170,PHYS,3220,2020,Spring,a,B +171,CS,3500,2019,Fall,b,B +171,CS,3810,2020,Fall,a,B +173,BIOL,1010,2018,Summer,b,B +173,CS,3505,2018,Summer,b,B +173,MATH,1250,2017,Summer,a,B +176,BIOL,1010,2016,Summer,a,B +176,BIOL,1030,2016,Fall,a,B +177,BIOL,1010,2015,Summer,b,B +177,CS,3810,2018,Summer,a,B +178,PHYS,2040,2019,Spring,a,B +179,CS,3500,2019,Summer,a,B +179,CS,3810,2018,Spring,a,B +179,MATH,1210,2016,Spring,d,B +180,MATH,1220,2019,Fall,b,B +181,CS,2100,2019,Fall,a,B +181,CS,2100,2019,Fall,d,B +181,CS,4000,2020,Spring,a,B +182,BIOL,1010,2015,Summer,a,B +185,BIOL,1010,2020,Summer,c,B +185,BIOL,2210,2020,Fall,a,B +187,PHYS,2040,2017,Fall,c,B +192,CS,3100,2016,Spring,d,B +199,BIOL,1006,2017,Fall,a,B +199,BIOL,2330,2017,Fall,b,B +199,CS,1410,2018,Spring,b,B +199,CS,3500,2019,Fall,b,B +200,BIOL,1010,2020,Summer,a,B +200,CS,3505,2020,Summer,a,B +204,BIOL,2325,2015,Fall,c,B +207,BIOL,2030,2016,Summer,b,B +207,CS,3200,2016,Summer,b,B +207,MATH,3220,2017,Fall,a,B +210,MATH,1220,2016,Spring,a,B +210,MATH,1250,2017,Summer,b,B +210,MATH,3220,2016,Spring,a,B +211,MATH,1260,2015,Summer,a,B +212,MATH,2210,2015,Summer,c,B +214,BIOL,2355,2018,Spring,a,B +214,MATH,1210,2016,Fall,a,B +215,CS,4500,2016,Spring,a,B +215,MATH,1210,2016,Fall,b,B +215,PHYS,2100,2017,Summer,b,B +216,CS,1410,2016,Spring,a,B +221,PHYS,2060,2020,Spring,b,B +227,BIOL,2210,2018,Summer,b,B +229,CS,1410,2018,Spring,b,B +229,CS,3500,2016,Spring,a,B +230,MATH,2270,2020,Fall,a,B +231,MATH,2210,2018,Spring,b,B +231,PHYS,2210,2017,Summer,a,B +234,BIOL,1006,2019,Summer,a,B +235,CS,4150,2020,Fall,a,B +238,MATH,2280,2018,Spring,a,B +240,BIOL,1010,2019,Spring,c,B +240,CS,3505,2018,Fall,a,B +241,BIOL,2420,2020,Spring,b,B +241,CS,3810,2019,Fall,a,B +241,MATH,2210,2020,Spring,c,B +246,CS,3200,2016,Summer,a,B +246,MATH,3210,2015,Fall,a,B +247,CS,4970,2018,Fall,b,B +247,MATH,1250,2018,Summer,a,B +248,BIOL,2021,2018,Fall,c,B +248,MATH,1220,2019,Fall,a,B +248,MATH,2270,2019,Summer,a,B +249,BIOL,1010,2017,Spring,a,B +249,BIOL,2030,2015,Fall,a,B +251,CS,4970,2020,Summer,a,B +251,MATH,2210,2020,Spring,c,B +255,CS,3810,2018,Spring,a,B +255,CS,4000,2017,Spring,a,B +255,MATH,2270,2019,Spring,a,B +255,PHYS,3210,2019,Summer,b,B +257,BIOL,1030,2017,Spring,a,B +258,BIOL,2355,2020,Summer,a,B +258,CS,3505,2018,Fall,a,B +258,CS,3810,2019,Fall,a,B +258,PHYS,3210,2019,Spring,a,B +260,BIOL,2210,2018,Summer,a,B +260,CS,2100,2019,Fall,c,B +264,PHYS,2060,2016,Summer,a,B +264,PHYS,2100,2017,Summer,c,B +267,CS,4400,2019,Summer,b,B +267,PHYS,2140,2020,Fall,a,B +267,PHYS,2220,2018,Fall,a,B +268,CS,2420,2016,Fall,b,B +270,BIOL,1210,2016,Spring,a,B +270,CS,3200,2016,Summer,a,B +270,CS,3810,2018,Summer,b,B +270,MATH,2270,2020,Spring,a,B +270,PHYS,2220,2017,Spring,c,B +274,BIOL,2355,2018,Summer,c,B +274,CS,3200,2018,Spring,a,B +276,BIOL,2325,2019,Summer,a,B +276,CS,1410,2015,Summer,b,B +276,CS,2100,2016,Spring,a,B +276,CS,2420,2015,Fall,a,B +276,CS,4500,2015,Summer,a,B +276,MATH,3220,2016,Summer,a,B +277,MATH,1220,2017,Spring,c,B +277,MATH,3220,2016,Fall,a,B +277,PHYS,2220,2017,Spring,c,B +277,PHYS,3210,2018,Spring,a,B +278,MATH,1210,2016,Fall,b,B +282,BIOL,2355,2017,Spring,c,B +285,BIOL,2030,2017,Spring,b,B +285,PHYS,2040,2017,Fall,b,B +288,CS,3500,2016,Summer,a,B +289,BIOL,1006,2020,Fall,c,B +289,MATH,1250,2020,Summer,a,B +290,BIOL,2021,2015,Summer,c,B +290,CS,1410,2017,Spring,a,B +292,CS,4150,2018,Fall,a,B +292,PHYS,2060,2020,Spring,b,B +292,PHYS,3210,2019,Spring,c,B +293,CS,4500,2019,Fall,a,B +294,CS,4970,2019,Summer,d,B +296,BIOL,2021,2018,Fall,d,B +296,CS,2100,2019,Summer,a,B +296,CS,3505,2019,Summer,b,B +297,BIOL,2210,2020,Fall,a,B +305,CS,3810,2018,Spring,a,B +306,PHYS,3210,2020,Fall,a,B +307,BIOL,1210,2019,Spring,a,B +307,MATH,1220,2016,Spring,a,B +309,BIOL,2330,2017,Summer,a,B +309,CS,4970,2020,Summer,d,B +309,MATH,2270,2020,Spring,a,B +309,MATH,3220,2018,Spring,c,B +309,PHYS,2210,2019,Fall,b,B +311,CS,3505,2019,Fall,c,B +312,BIOL,1010,2017,Spring,a,B +312,PHYS,2140,2016,Summer,a,B +312,PHYS,2220,2017,Spring,b,B +312,PHYS,2220,2017,Spring,d,B +312,PHYS,3210,2019,Spring,b,B +313,BIOL,1010,2018,Summer,b,B +314,BIOL,2355,2018,Fall,a,B +314,CS,2100,2019,Summer,a,B +314,MATH,3210,2019,Spring,b,B +314,PHYS,2140,2017,Summer,a,B +316,CS,2100,2019,Fall,d,B +318,BIOL,1030,2019,Spring,c,B +318,BIOL,2325,2018,Summer,a,B +318,CS,4500,2018,Spring,b,B +321,BIOL,1030,2015,Summer,a,B +321,CS,1030,2016,Fall,a,B +321,CS,4000,2016,Fall,a,B +321,CS,4500,2016,Spring,b,B +321,CS,4970,2019,Fall,b,B +321,PHYS,2040,2016,Spring,a,B +321,PHYS,3220,2020,Spring,b,B +323,BIOL,2355,2020,Summer,a,B +326,MATH,3220,2017,Fall,b,B +329,BIOL,2355,2017,Spring,b,B +329,CS,2100,2018,Summer,b,B +329,CS,3810,2016,Fall,b,B +329,PHYS,2060,2018,Fall,b,B +332,BIOL,2325,2018,Spring,a,B +332,MATH,1210,2019,Spring,a,B +333,BIOL,2355,2020,Summer,a,B +333,CS,2100,2020,Fall,a,B +333,MATH,2270,2019,Fall,a,B +335,CS,1410,2016,Spring,b,B +335,MATH,1250,2015,Fall,a,B +341,CS,4000,2020,Fall,a,B +342,MATH,1250,2020,Summer,a,B +344,CS,4970,2018,Summer,a,B +345,BIOL,2021,2017,Fall,a,B +345,BIOL,2030,2019,Summer,d,B +345,CS,4970,2019,Spring,b,B +348,BIOL,1010,2020,Summer,b,B +348,BIOL,2030,2017,Spring,b,B +348,CS,2100,2017,Fall,a,B +348,MATH,3210,2019,Spring,a,B +351,MATH,1210,2019,Spring,a,B +356,BIOL,2355,2019,Spring,a,B +357,BIOL,2020,2016,Spring,a,B +358,MATH,3210,2019,Fall,a,B +360,MATH,2270,2020,Fall,a,B +363,BIOL,2010,2020,Summer,b,B +364,CS,3500,2020,Summer,a,B +365,BIOL,2420,2020,Spring,b,B +366,BIOL,2021,2018,Summer,a,B +366,MATH,1220,2019,Fall,b,B +368,BIOL,1010,2018,Summer,a,B +368,CS,4000,2020,Fall,a,B +368,PHYS,2210,2019,Spring,c,B +369,BIOL,2210,2018,Summer,a,B +371,BIOL,1010,2020,Summer,d,B +372,CS,3810,2018,Spring,a,B +372,CS,4970,2018,Summer,c,B +373,PHYS,2040,2015,Fall,b,B +373,PHYS,2210,2017,Summer,d,B +375,BIOL,2210,2017,Summer,c,B +378,BIOL,1030,2018,Summer,a,B +378,BIOL,2330,2019,Fall,a,B +378,MATH,1250,2020,Summer,a,B +378,MATH,3210,2019,Spring,a,B +379,CS,4500,2018,Spring,b,B +379,MATH,2270,2019,Spring,a,B +380,CS,3500,2019,Fall,a,B +382,CS,1410,2015,Summer,d,B +384,CS,2100,2018,Fall,b,B +384,MATH,1210,2018,Fall,a,B +385,CS,4000,2018,Spring,a,B +386,CS,3500,2020,Summer,a,B +387,CS,1030,2018,Fall,a,B +390,CS,2100,2019,Summer,a,B +390,CS,2420,2019,Summer,a,B +390,CS,3505,2020,Fall,c,B +390,MATH,1220,2019,Fall,c,B +390,PHYS,2060,2020,Fall,a,B +390,PHYS,2210,2019,Fall,c,B +390,PHYS,2220,2020,Summer,b,B +391,CS,2100,2018,Fall,d,B +392,CS,4400,2015,Fall,b,B +392,MATH,2210,2017,Summer,a,B +397,MATH,1260,2019,Summer,a,B +398,PHYS,2060,2019,Summer,a,B +100,BIOL,2020,2018,Fall,b,B+ +100,MATH,1260,2019,Fall,a,B+ +101,PHYS,2140,2018,Summer,a,B+ +102,MATH,2270,2017,Fall,d,B+ +102,PHYS,2220,2018,Spring,a,B+ +105,CS,3200,2016,Fall,d,B+ +106,CS,3505,2020,Fall,b,B+ +107,BIOL,2355,2020,Spring,a,B+ +107,MATH,3220,2017,Fall,a,B+ +109,BIOL,2010,2020,Spring,a,B+ +110,CS,4000,2020,Fall,a,B+ +115,BIOL,1006,2016,Spring,a,B+ +115,BIOL,1210,2017,Spring,a,B+ +116,CS,3810,2016,Fall,b,B+ +117,MATH,1220,2017,Spring,c,B+ +117,MATH,2210,2018,Spring,a,B+ +118,CS,1030,2020,Spring,c,B+ +120,BIOL,2210,2017,Summer,b,B+ +120,CS,4400,2015,Summer,a,B+ +120,PHYS,2100,2016,Fall,a,B+ +120,PHYS,2140,2015,Fall,a,B+ +122,BIOL,1010,2020,Summer,a,B+ +123,BIOL,2420,2017,Summer,b,B+ +123,MATH,2280,2015,Fall,a,B+ +123,PHYS,2060,2019,Fall,c,B+ +124,CS,4400,2019,Fall,b,B+ +124,PHYS,2210,2018,Fall,c,B+ +127,CS,4000,2019,Spring,a,B+ +128,MATH,2210,2017,Summer,a,B+ +129,CS,3100,2019,Spring,b,B+ +129,CS,3505,2019,Summer,c,B+ +129,CS,3810,2018,Summer,c,B+ +131,CS,3200,2020,Spring,a,B+ +131,CS,3810,2019,Fall,a,B+ +131,CS,4500,2019,Fall,b,B+ +132,CS,2420,2017,Summer,b,B+ +134,CS,2100,2016,Summer,c,B+ +134,MATH,3220,2016,Fall,b,B+ +135,CS,4150,2020,Fall,a,B+ +135,MATH,3210,2020,Summer,a,B+ +140,BIOL,2030,2015,Fall,a,B+ +143,CS,4500,2019,Fall,c,B+ +143,CS,4940,2017,Fall,a,B+ +148,CS,4150,2020,Fall,a,B+ +151,BIOL,1210,2018,Fall,b,B+ +151,PHYS,2140,2018,Summer,a,B+ +152,CS,4970,2019,Fall,c,B+ +152,PHYS,3210,2019,Summer,b,B+ +153,PHYS,3210,2020,Fall,a,B+ +158,CS,2100,2018,Fall,a,B+ +160,BIOL,1030,2016,Summer,a,B+ +160,CS,3810,2016,Summer,a,B+ +163,BIOL,2325,2015,Fall,c,B+ +163,CS,4150,2016,Summer,a,B+ +163,MATH,3220,2016,Summer,a,B+ +166,BIOL,2010,2020,Summer,a,B+ +166,MATH,3210,2020,Summer,a,B+ +174,BIOL,2210,2018,Summer,a,B+ +176,CS,4150,2015,Summer,a,B+ +176,CS,4500,2016,Fall,a,B+ +177,BIOL,2021,2018,Spring,a,B+ +177,BIOL,2355,2020,Summer,b,B+ +179,CS,2420,2017,Summer,c,B+ +179,CS,4400,2016,Summer,a,B+ +179,MATH,3220,2018,Spring,d,B+ +179,PHYS,2100,2016,Fall,b,B+ +180,CS,3500,2019,Fall,a,B+ +181,MATH,1220,2019,Fall,a,B+ +182,BIOL,2020,2015,Fall,c,B+ +182,MATH,2270,2017,Fall,c,B+ +183,PHYS,2210,2018,Fall,a,B+ +185,PHYS,2060,2019,Fall,a,B+ +186,BIOL,2355,2020,Fall,a,B+ +187,BIOL,1006,2019,Fall,a,B+ +192,BIOL,2325,2015,Fall,c,B+ +192,CS,4150,2015,Summer,a,B+ +196,MATH,2280,2018,Fall,c,B+ +196,PHYS,2220,2018,Fall,a,B+ +197,CS,3200,2018,Spring,a,B+ +197,PHYS,3210,2018,Spring,c,B+ +200,MATH,3210,2020,Fall,a,B+ +207,CS,4500,2017,Summer,a,B+ +208,BIOL,2330,2017,Fall,a,B+ +210,MATH,2270,2015,Fall,b,B+ +210,MATH,2280,2020,Spring,a,B+ +210,PHYS,2040,2015,Fall,c,B+ +214,BIOL,1010,2018,Summer,a,B+ +214,BIOL,2020,2016,Spring,a,B+ +214,CS,1030,2016,Summer,a,B+ +214,MATH,1250,2016,Spring,a,B+ +215,BIOL,2210,2017,Spring,b,B+ +215,BIOL,2210,2017,Spring,c,B+ +217,BIOL,2325,2018,Fall,c,B+ +219,CS,2100,2020,Fall,a,B+ +220,CS,3810,2020,Fall,a,B+ +222,BIOL,1006,2020,Fall,a,B+ +222,CS,4970,2020,Summer,b,B+ +225,MATH,2210,2020,Fall,a,B+ +227,PHYS,2220,2018,Spring,a,B+ +227,PHYS,3220,2020,Spring,b,B+ +228,CS,4400,2020,Spring,a,B+ +228,MATH,1210,2019,Summer,a,B+ +228,PHYS,3210,2020,Fall,a,B+ +229,BIOL,2330,2017,Summer,a,B+ +229,PHYS,2060,2016,Spring,a,B+ +230,BIOL,2355,2018,Spring,a,B+ +231,BIOL,2020,2018,Fall,d,B+ +234,MATH,2280,2019,Fall,c,B+ +240,PHYS,3210,2020,Summer,a,B+ +243,CS,1030,2016,Fall,a,B+ +245,PHYS,2040,2015,Fall,a,B+ +246,BIOL,2030,2017,Spring,b,B+ +246,CS,4400,2017,Spring,a,B+ +246,PHYS,3210,2017,Summer,a,B+ +247,BIOL,1010,2019,Spring,d,B+ +247,CS,2100,2020,Fall,a,B+ +248,PHYS,2060,2018,Fall,b,B+ +249,CS,4400,2017,Spring,a,B+ +249,MATH,2210,2017,Spring,a,B+ +249,PHYS,3210,2016,Summer,a,B+ +254,BIOL,1010,2020,Summer,d,B+ +254,CS,3200,2020,Summer,a,B+ +255,CS,3200,2018,Spring,b,B+ +256,BIOL,1010,2020,Summer,a,B+ +256,CS,4000,2019,Spring,a,B+ +257,BIOL,1010,2020,Summer,b,B+ +257,CS,4000,2020,Spring,b,B+ +258,MATH,1260,2019,Fall,a,B+ +259,BIOL,1006,2019,Summer,a,B+ +259,MATH,3210,2019,Spring,b,B+ +259,PHYS,2040,2017,Fall,a,B+ +260,MATH,1210,2020,Spring,b,B+ +260,MATH,1250,2018,Spring,a,B+ +262,BIOL,2325,2018,Summer,a,B+ +262,MATH,2280,2018,Spring,a,B+ +263,CS,2420,2020,Summer,a,B+ +264,BIOL,2355,2017,Fall,b,B+ +264,CS,3100,2017,Fall,a,B+ +267,BIOL,1006,2020,Spring,a,B+ +269,PHYS,3220,2020,Spring,b,B+ +270,BIOL,1006,2018,Spring,b,B+ +270,BIOL,1010,2020,Summer,c,B+ +270,BIOL,1030,2016,Summer,a,B+ +270,BIOL,2020,2018,Fall,a,B+ +270,BIOL,2330,2016,Fall,a,B+ +270,BIOL,2420,2018,Spring,a,B+ +270,MATH,1220,2015,Summer,b,B+ +270,PHYS,2040,2017,Fall,c,B+ +270,PHYS,3210,2017,Fall,a,B+ +270,PHYS,3220,2017,Fall,d,B+ +271,BIOL,1006,2020,Fall,c,B+ +274,MATH,1220,2019,Fall,b,B+ +274,MATH,2210,2020,Spring,a,B+ +276,MATH,1210,2016,Spring,a,B+ +276,MATH,1220,2018,Spring,a,B+ +276,MATH,1260,2019,Summer,b,B+ +276,MATH,2210,2015,Spring,b,B+ +277,BIOL,1030,2016,Summer,a,B+ +277,BIOL,2010,2017,Summer,a,B+ +277,CS,4940,2020,Summer,a,B+ +278,BIOL,1210,2017,Spring,a,B+ +278,BIOL,2355,2017,Spring,a,B+ +281,MATH,2210,2020,Fall,a,B+ +282,BIOL,1210,2017,Summer,a,B+ +284,MATH,3210,2019,Fall,a,B+ +285,BIOL,2010,2018,Spring,a,B+ +285,CS,4150,2016,Summer,b,B+ +285,PHYS,2140,2017,Summer,a,B+ +288,PHYS,2210,2018,Fall,b,B+ +290,PHYS,2060,2016,Spring,b,B+ +292,MATH,3220,2018,Spring,a,B+ +293,BIOL,2020,2019,Summer,a,B+ +293,BIOL,2210,2019,Fall,b,B+ +293,MATH,1220,2020,Summer,a,B+ +294,PHYS,2060,2019,Summer,b,B+ +296,BIOL,1006,2018,Fall,a,B+ +296,BIOL,2010,2020,Summer,b,B+ +296,PHYS,3220,2020,Spring,c,B+ +300,BIOL,1010,2020,Summer,d,B+ +301,CS,4500,2016,Spring,b,B+ +301,MATH,3210,2015,Summer,a,B+ +303,MATH,1260,2019,Summer,b,B+ +304,MATH,2270,2017,Summer,a,B+ +306,CS,3200,2020,Summer,a,B+ +307,BIOL,2020,2019,Summer,a,B+ +309,BIOL,2021,2018,Fall,b,B+ +309,BIOL,2325,2018,Fall,a,B+ +309,CS,1030,2020,Spring,c,B+ +309,CS,2100,2018,Fall,b,B+ +310,PHYS,3210,2020,Spring,a,B+ +311,CS,2100,2017,Fall,a,B+ +311,PHYS,2210,2019,Spring,a,B+ +312,BIOL,1006,2016,Summer,a,B+ +312,CS,1030,2016,Spring,a,B+ +312,CS,1410,2020,Spring,a,B+ +312,CS,2100,2019,Spring,b,B+ +312,CS,3810,2018,Summer,d,B+ +312,MATH,1220,2018,Spring,a,B+ +312,MATH,3210,2020,Summer,a,B+ +313,CS,3810,2018,Spring,a,B+ +313,CS,4400,2017,Spring,c,B+ +313,PHYS,2140,2016,Spring,b,B+ +314,BIOL,1010,2019,Spring,d,B+ +314,CS,3505,2019,Spring,b,B+ +314,PHYS,2040,2017,Fall,c,B+ +317,PHYS,2140,2016,Summer,a,B+ +318,MATH,2280,2019,Fall,b,B+ +318,PHYS,2140,2019,Fall,b,B+ +321,PHYS,2100,2015,Spring,b,B+ +323,BIOL,1010,2020,Summer,d,B+ +326,BIOL,1006,2017,Fall,a,B+ +326,CS,2420,2017,Fall,a,B+ +329,CS,1410,2020,Spring,b,B+ +332,BIOL,1030,2020,Summer,a,B+ +332,PHYS,2210,2018,Fall,c,B+ +333,CS,3505,2020,Fall,b,B+ +333,PHYS,3210,2019,Summer,c,B+ +339,CS,4970,2020,Summer,c,B+ +340,CS,4970,2019,Fall,d,B+ +344,PHYS,2220,2018,Summer,a,B+ +345,BIOL,1006,2017,Fall,a,B+ +345,BIOL,1010,2018,Fall,a,B+ +345,CS,4500,2018,Spring,d,B+ +345,MATH,2270,2019,Summer,c,B+ +345,PHYS,3220,2017,Fall,b,B+ +348,BIOL,2420,2017,Summer,b,B+ +348,CS,2420,2016,Spring,a,B+ +348,MATH,2210,2015,Summer,c,B+ +355,BIOL,2030,2017,Spring,d,B+ +355,CS,3500,2017,Fall,b,B+ +355,PHYS,2060,2016,Spring,a,B+ +356,BIOL,2325,2018,Fall,c,B+ +357,MATH,1220,2016,Spring,a,B+ +359,CS,2100,2019,Summer,b,B+ +360,BIOL,2210,2020,Fall,a,B+ +361,CS,2100,2018,Spring,a,B+ +362,PHYS,2210,2018,Fall,c,B+ +364,CS,4000,2020,Spring,a,B+ +364,MATH,1260,2019,Fall,a,B+ +366,CS,1030,2018,Fall,a,B+ +366,CS,2100,2017,Fall,a,B+ +366,CS,4970,2019,Spring,a,B+ +368,CS,3505,2018,Summer,a,B+ +369,CS,3200,2016,Fall,d,B+ +371,CS,4000,2020,Spring,b,B+ +372,CS,3200,2019,Spring,a,B+ +372,CS,3505,2019,Summer,b,B+ +373,BIOL,1006,2018,Spring,b,B+ +373,BIOL,2325,2018,Spring,a,B+ +373,PHYS,2140,2015,Summer,c,B+ +374,MATH,3210,2015,Fall,a,B+ +374,PHYS,3210,2018,Spring,c,B+ +377,BIOL,2210,2019,Summer,a,B+ +377,CS,3505,2018,Summer,a,B+ +377,CS,4400,2019,Fall,b,B+ +378,BIOL,1006,2020,Fall,b,B+ +378,BIOL,2020,2018,Fall,b,B+ +378,CS,3100,2016,Fall,a,B+ +378,PHYS,3210,2017,Summer,a,B+ +379,BIOL,1030,2015,Spring,d,B+ +379,CS,3200,2016,Summer,a,B+ +379,MATH,2280,2019,Fall,b,B+ +380,BIOL,1030,2019,Summer,a,B+ +380,BIOL,2210,2019,Fall,a,B+ +384,BIOL,1010,2020,Summer,b,B+ +384,BIOL,2021,2018,Fall,c,B+ +384,MATH,2210,2020,Fall,a,B+ +385,BIOL,2325,2017,Fall,a,B+ +385,CS,3500,2017,Fall,c,B+ +385,MATH,1220,2017,Spring,c,B+ +388,CS,4400,2017,Spring,c,B+ +389,MATH,1220,2016,Spring,a,B+ +390,BIOL,1006,2020,Fall,a,B+ +390,BIOL,2010,2020,Summer,b,B+ +392,BIOL,1010,2018,Summer,a,B+ +392,PHYS,3220,2017,Summer,a,B+ +393,PHYS,3210,2017,Summer,a,B+ +394,BIOL,2021,2015,Spring,a,B+ +395,CS,1030,2016,Spring,a,B+ +396,BIOL,2030,2019,Summer,b,B+ +397,CS,4400,2019,Summer,a,B+ +397,MATH,1220,2020,Summer,a,B+ +397,PHYS,2210,2019,Summer,a,B+ +398,CS,1030,2019,Fall,a,B+ +399,BIOL,2030,2019,Summer,c,B+ +101,PHYS,2210,2018,Fall,a,B- +102,CS,1030,2016,Fall,a,B- +102,CS,3200,2016,Fall,b,B- +106,CS,4400,2020,Fall,b,B- +106,MATH,2280,2020,Spring,b,B- +106,PHYS,2220,2020,Summer,a,B- +107,CS,4970,2016,Fall,a,B- +109,BIOL,2030,2019,Summer,c,B- +109,CS,3200,2018,Spring,c,B- +109,CS,3500,2017,Fall,b,B- +109,MATH,1250,2018,Spring,a,B- +109,MATH,2270,2017,Fall,a,B- +113,BIOL,1006,2018,Fall,a,B- +113,PHYS,2220,2020,Spring,a,B- +115,BIOL,2021,2017,Summer,a,B- +115,PHYS,2060,2016,Spring,a,B- +116,CS,1030,2016,Fall,a,B- +116,CS,4970,2017,Spring,a,B- +117,BIOL,1030,2016,Spring,a,B- +117,MATH,1250,2017,Summer,d,B- +118,CS,3500,2019,Summer,a,B- +119,CS,2420,2017,Summer,a,B- +119,CS,4400,2020,Fall,a,B- +119,MATH,2210,2019,Spring,b,B- +120,BIOL,2010,2017,Fall,a,B- +120,MATH,1210,2015,Summer,a,B- +120,MATH,2210,2015,Summer,c,B- +120,MATH,3210,2017,Spring,a,B- +120,PHYS,2210,2018,Fall,b,B- +122,PHYS,2060,2020,Spring,a,B- +123,BIOL,1030,2020,Summer,a,B- +123,CS,1030,2016,Summer,a,B- +123,CS,3100,2017,Fall,a,B- +123,CS,4150,2020,Spring,a,B- +123,PHYS,2210,2019,Fall,c,B- +124,CS,3810,2020,Fall,a,B- +127,MATH,1250,2017,Summer,b,B- +127,PHYS,2210,2017,Summer,c,B- +128,PHYS,2210,2019,Summer,a,B- +128,PHYS,2220,2018,Spring,a,B- +131,CS,1030,2020,Fall,a,B- +131,CS,4400,2020,Fall,a,B- +131,MATH,2270,2017,Fall,c,B- +133,CS,2420,2020,Summer,a,B- +133,PHYS,3210,2019,Summer,a,B- +134,PHYS,2220,2018,Spring,a,B- +134,PHYS,3210,2016,Summer,b,B- +135,BIOL,2010,2020,Spring,a,B- +140,BIOL,2420,2015,Spring,c,B- +144,MATH,1260,2015,Summer,a,B- +146,BIOL,2355,2019,Spring,c,B- +146,CS,4400,2019,Summer,a,B- +151,CS,4000,2017,Spring,a,B- +151,CS,4970,2020,Summer,d,B- +152,BIOL,2325,2019,Spring,a,B- +152,CS,2100,2020,Spring,a,B- +152,CS,3505,2019,Spring,a,B- +152,CS,4400,2020,Fall,a,B- +153,PHYS,2060,2020,Spring,b,B- +155,BIOL,2355,2017,Fall,b,B- +156,CS,3505,2018,Fall,a,B- +163,CS,4970,2018,Summer,c,B- +164,CS,3200,2019,Spring,a,B- +165,MATH,3220,2018,Spring,c,B- +169,BIOL,2210,2018,Summer,a,B- +169,MATH,2210,2019,Spring,a,B- +170,BIOL,1030,2020,Summer,a,B- +171,CS,4970,2020,Summer,d,B- +173,MATH,1260,2020,Spring,a,B- +177,CS,2420,2016,Fall,a,B- +178,CS,2100,2019,Fall,b,B- +179,CS,4970,2016,Fall,b,B- +179,MATH,1220,2017,Spring,b,B- +179,PHYS,2210,2017,Summer,b,B- +182,BIOL,2420,2017,Summer,a,B- +187,BIOL,2330,2017,Fall,b,B- +187,CS,3505,2019,Spring,b,B- +187,MATH,3210,2020,Summer,a,B- +187,PHYS,2140,2017,Fall,a,B- +192,MATH,1220,2015,Summer,a,B- +194,CS,4500,2019,Fall,d,B- +194,MATH,2270,2019,Summer,b,B- +195,BIOL,1030,2016,Summer,a,B- +195,BIOL,2010,2015,Summer,a,B- +197,BIOL,1010,2018,Summer,b,B- +199,BIOL,2021,2018,Fall,a,B- +199,CS,4970,2019,Summer,a,B- +200,CS,4970,2019,Fall,c,B- +208,MATH,1250,2017,Summer,d,B- +208,PHYS,2210,2017,Summer,d,B- +210,BIOL,2420,2020,Spring,a,B- +213,BIOL,1030,2016,Fall,a,B- +213,CS,3100,2016,Fall,a,B- +214,BIOL,2010,2018,Spring,a,B- +215,BIOL,1030,2017,Spring,c,B- +215,MATH,1220,2017,Summer,a,B- +217,BIOL,1030,2019,Spring,b,B- +220,CS,4970,2018,Summer,c,B- +221,CS,4970,2020,Summer,a,B- +223,MATH,2270,2020,Spring,a,B- +228,BIOL,1010,2019,Spring,b,B- +228,BIOL,2030,2019,Summer,b,B- +228,CS,3500,2019,Summer,a,B- +229,CS,3200,2016,Fall,c,B- +229,MATH,1210,2016,Spring,b,B- +230,CS,3810,2018,Spring,a,B- +230,PHYS,2060,2019,Summer,a,B- +230,PHYS,3220,2017,Fall,c,B- +231,CS,1410,2018,Spring,a,B- +231,CS,3200,2020,Summer,a,B- +235,BIOL,2420,2020,Spring,a,B- +235,CS,2100,2019,Fall,b,B- +238,PHYS,2210,2019,Spring,b,B- +239,MATH,1250,2018,Summer,b,B- +239,PHYS,2060,2018,Fall,a,B- +244,BIOL,1010,2020,Summer,d,B- +244,BIOL,2355,2020,Summer,b,B- +246,BIOL,2355,2015,Summer,a,B- +246,CS,3500,2015,Fall,b,B- +247,BIOL,2030,2019,Summer,c,B- +247,PHYS,2220,2020,Summer,a,B- +248,BIOL,2010,2020,Summer,a,B- +248,MATH,2280,2019,Fall,c,B- +252,PHYS,3220,2017,Fall,d,B- +254,CS,4000,2020,Spring,a,B- +255,BIOL,2325,2018,Summer,a,B- +255,CS,4500,2019,Fall,b,B- +256,BIOL,2355,2017,Fall,b,B- +256,CS,4940,2019,Fall,a,B- +256,MATH,1260,2019,Spring,a,B- +258,BIOL,1010,2018,Fall,a,B- +258,BIOL,2210,2018,Summer,c,B- +258,CS,2100,2018,Summer,a,B- +258,CS,4940,2020,Summer,b,B- +259,CS,3505,2018,Summer,b,B- +259,PHYS,2060,2018,Fall,d,B- +260,BIOL,1010,2018,Summer,a,B- +260,BIOL,1030,2019,Summer,a,B- +260,CS,3200,2020,Summer,a,B- +261,PHYS,2060,2018,Fall,b,B- +264,BIOL,2021,2017,Fall,a,B- +267,CS,3505,2020,Summer,a,B- +267,PHYS,3220,2020,Spring,a,B- +268,CS,4970,2016,Fall,a,B- +270,BIOL,1010,2020,Summer,a,B- +270,PHYS,2140,2015,Summer,b,B- +271,BIOL,2210,2020,Fall,a,B- +275,CS,4400,2019,Spring,b,B- +276,BIOL,2210,2018,Spring,a,B- +276,PHYS,2140,2015,Fall,a,B- +277,CS,4400,2015,Summer,a,B- +277,MATH,2210,2017,Summer,a,B- +277,PHYS,3220,2016,Summer,b,B- +282,BIOL,2021,2015,Spring,a,B- +282,CS,3810,2016,Fall,a,B- +282,MATH,1220,2015,Summer,c,B- +285,BIOL,2210,2017,Summer,c,B- +288,CS,4150,2016,Summer,b,B- +290,BIOL,1006,2015,Summer,b,B- +290,BIOL,1010,2015,Fall,b,B- +290,BIOL,2420,2015,Fall,a,B- +290,MATH,1250,2016,Spring,a,B- +292,CS,3500,2017,Summer,a,B- +296,CS,2420,2018,Spring,a,B- +296,PHYS,2040,2019,Spring,a,B- +298,CS,4400,2019,Summer,b,B- +299,BIOL,1210,2017,Spring,a,B- +300,CS,3505,2019,Summer,b,B- +303,CS,1030,2019,Fall,b,B- +306,BIOL,1010,2020,Summer,b,B- +306,BIOL,2010,2020,Summer,b,B- +309,MATH,1250,2020,Summer,a,B- +309,MATH,2210,2018,Spring,b,B- +309,PHYS,2220,2020,Summer,a,B- +310,PHYS,2060,2020,Spring,a,B- +312,CS,3500,2020,Summer,a,B- +312,CS,4940,2020,Summer,b,B- +313,CS,2100,2015,Summer,a,B- +313,CS,4000,2018,Spring,a,B- +313,CS,4500,2018,Spring,d,B- +314,CS,3500,2017,Fall,a,B- +314,CS,4150,2020,Spring,a,B- +318,MATH,1260,2019,Summer,a,B- +321,BIOL,2020,2018,Spring,a,B- +321,BIOL,2325,2015,Spring,a,B- +321,BIOL,2355,2016,Spring,b,B- +321,CS,2420,2016,Summer,a,B- +321,PHYS,3210,2016,Fall,a,B- +325,BIOL,1030,2020,Spring,a,B- +329,MATH,3210,2020,Fall,a,B- +329,PHYS,3220,2017,Summer,a,B- +332,BIOL,1010,2019,Spring,c,B- +332,BIOL,1210,2018,Spring,a,B- +332,CS,2100,2018,Summer,c,B- +336,CS,3200,2015,Fall,c,B- +341,CS,4970,2020,Fall,d,B- +341,PHYS,3220,2020,Spring,d,B- +342,BIOL,2020,2018,Fall,d,B- +342,BIOL,2021,2018,Fall,c,B- +342,CS,4000,2017,Fall,a,B- +345,BIOL,2020,2018,Fall,b,B- +345,BIOL,2355,2019,Spring,c,B- +347,BIOL,1030,2019,Summer,a,B- +347,CS,2100,2019,Summer,a,B- +348,BIOL,2021,2017,Summer,a,B- +348,BIOL,2210,2017,Spring,b,B- +348,MATH,1210,2019,Spring,a,B- +348,PHYS,3210,2020,Spring,a,B- +348,PHYS,3220,2020,Spring,b,B- +353,PHYS,2100,2017,Summer,c,B- +355,BIOL,2330,2017,Fall,a,B- +356,BIOL,1006,2019,Summer,a,B- +356,CS,3505,2019,Summer,d,B- +356,MATH,1250,2018,Summer,a,B- +356,MATH,1260,2019,Spring,b,B- +359,CS,4970,2019,Summer,b,B- +360,BIOL,1030,2020,Summer,a,B- +361,CS,4000,2017,Fall,b,B- +361,MATH,1250,2018,Spring,a,B- +362,BIOL,2020,2018,Fall,c,B- +362,CS,4940,2020,Summer,a,B- +362,MATH,1250,2018,Summer,c,B- +364,CS,4500,2020,Spring,a,B- +365,CS,4500,2019,Fall,d,B- +366,BIOL,2210,2020,Fall,a,B- +368,BIOL,2420,2020,Summer,a,B- +369,MATH,1210,2016,Fall,c,B- +371,BIOL,2210,2020,Fall,a,B- +373,BIOL,2010,2018,Spring,a,B- +373,CS,2100,2018,Fall,a,B- +373,CS,4970,2020,Summer,b,B- +374,BIOL,2210,2017,Summer,c,B- +374,CS,2100,2016,Summer,b,B- +374,CS,3505,2018,Summer,a,B- +374,PHYS,2210,2015,Fall,b,B- +375,BIOL,1010,2019,Spring,a,B- +375,CS,3200,2020,Summer,a,B- +375,MATH,1260,2019,Fall,a,B- +376,PHYS,2060,2020,Fall,a,B- +377,MATH,1250,2016,Spring,a,B- +377,PHYS,3220,2018,Summer,a,B- +378,BIOL,1006,2020,Fall,c,B- +378,BIOL,1010,2018,Summer,b,B- +378,BIOL,2210,2017,Summer,b,B- +378,CS,4970,2019,Summer,a,B- +379,BIOL,2020,2018,Fall,d,B- +385,CS,2420,2016,Spring,a,B- +390,CS,4970,2020,Summer,d,B- +391,BIOL,2210,2018,Spring,a,B- +391,CS,3100,2017,Fall,a,B- +391,MATH,1260,2019,Summer,a,B- +391,MATH,3210,2020,Summer,a,B- +394,MATH,3220,2016,Spring,d,B- +397,CS,4000,2020,Fall,a,B- +398,CS,3505,2020,Fall,a,B- +398,CS,4970,2018,Summer,a,B- +100,BIOL,1030,2020,Spring,a,C +100,CS,1410,2018,Spring,b,C +102,MATH,1210,2018,Spring,a,C +102,MATH,1260,2019,Spring,c,C +106,BIOL,2355,2020,Summer,a,C +107,CS,3810,2016,Fall,a,C +107,MATH,2270,2017,Fall,a,C +109,CS,4400,2019,Spring,b,C +109,PHYS,2220,2020,Fall,a,C +109,PHYS,3210,2018,Fall,a,C +112,CS,4970,2020,Summer,a,C +115,PHYS,3220,2016,Summer,a,C +116,CS,3200,2017,Spring,a,C +117,CS,4500,2016,Fall,a,C +119,BIOL,2030,2016,Summer,a,C +119,BIOL,2355,2018,Summer,a,C +120,CS,3100,2016,Spring,b,C +120,CS,4000,2020,Fall,a,C +120,MATH,1220,2019,Fall,b,C +123,CS,3200,2016,Fall,c,C +123,CS,4500,2019,Summer,a,C +124,BIOL,2325,2018,Fall,a,C +124,CS,3100,2017,Fall,a,C +124,MATH,1210,2019,Summer,a,C +126,CS,3505,2015,Fall,c,C +127,CS,3505,2018,Summer,b,C +127,CS,3810,2019,Fall,a,C +128,CS,3810,2018,Summer,c,C +130,PHYS,3210,2020,Fall,c,C +131,BIOL,1006,2018,Fall,a,C +131,BIOL,2355,2018,Summer,a,C +131,CS,4970,2019,Fall,b,C +131,PHYS,2140,2020,Fall,a,C +131,PHYS,3220,2017,Fall,a,C +133,BIOL,2325,2018,Fall,a,C +133,CS,3200,2018,Spring,a,C +133,CS,4500,2018,Spring,c,C +133,PHYS,2220,2018,Fall,a,C +134,CS,3100,2016,Spring,d,C +135,BIOL,2030,2019,Summer,c,C +135,MATH,2270,2020,Fall,b,C +135,PHYS,2210,2019,Fall,a,C +136,MATH,2210,2020,Fall,a,C +138,BIOL,1006,2015,Summer,a,C +139,MATH,3220,2017,Fall,b,C +143,MATH,2270,2017,Summer,a,C +146,CS,2100,2019,Fall,d,C +146,MATH,3210,2020,Spring,a,C +151,CS,3810,2018,Summer,c,C +152,CS,3500,2019,Summer,a,C +152,CS,4500,2020,Summer,a,C +153,CS,2420,2020,Summer,a,C +157,PHYS,2210,2019,Spring,b,C +163,BIOL,1010,2015,Summer,d,C +163,CS,2100,2017,Fall,a,C +163,CS,3505,2016,Summer,a,C +163,CS,4000,2017,Fall,b,C +164,BIOL,1006,2018,Spring,a,C +164,BIOL,2010,2020,Spring,b,C +164,BIOL,2420,2017,Summer,a,C +164,CS,4500,2018,Spring,c,C +164,MATH,1260,2020,Spring,a,C +165,BIOL,2020,2018,Fall,a,C +165,MATH,2280,2018,Fall,b,C +167,MATH,1250,2020,Summer,a,C +167,MATH,2210,2020,Fall,a,C +169,BIOL,2010,2020,Spring,a,C +169,BIOL,2021,2018,Summer,a,C +169,CS,4400,2019,Spring,c,C +171,BIOL,2030,2020,Spring,a,C +171,CS,2100,2020,Fall,a,C +171,PHYS,2060,2019,Fall,b,C +172,MATH,1250,2015,Fall,a,C +172,PHYS,2140,2015,Summer,b,C +172,PHYS,2220,2016,Summer,a,C +172,PHYS,3210,2016,Summer,b,C +175,BIOL,2010,2020,Summer,a,C +175,CS,1030,2020,Spring,a,C +177,MATH,3210,2015,Spring,b,C +178,MATH,1210,2018,Fall,b,C +178,MATH,2270,2020,Spring,a,C +179,BIOL,1210,2018,Fall,b,C +179,CS,2100,2016,Summer,b,C +179,MATH,2270,2015,Fall,a,C +181,BIOL,2355,2020,Fall,a,C +181,PHYS,2060,2020,Fall,a,C +182,BIOL,2030,2017,Spring,a,C +182,BIOL,2325,2015,Fall,a,C +182,CS,3500,2017,Fall,a,C +182,MATH,2270,2017,Fall,d,C +183,BIOL,2330,2020,Spring,a,C +185,CS,2100,2018,Spring,a,C +185,MATH,1210,2018,Fall,a,C +186,CS,4970,2020,Fall,d,C +187,MATH,1260,2019,Spring,b,C +187,PHYS,2220,2017,Spring,a,C +191,CS,2100,2020,Fall,a,C +192,BIOL,1010,2016,Summer,a,C +194,MATH,1260,2019,Summer,b,C +195,BIOL,2330,2016,Spring,a,C +202,CS,4970,2020,Fall,d,C +203,CS,4000,2018,Spring,a,C +207,CS,3100,2016,Summer,a,C +210,BIOL,2020,2015,Summer,a,C +210,MATH,3210,2015,Summer,a,C +211,BIOL,1010,2015,Fall,b,C +212,BIOL,2020,2016,Spring,a,C +214,CS,3505,2017,Fall,a,C +214,CS,3810,2018,Summer,b,C +215,BIOL,2030,2015,Fall,a,C +215,PHYS,2100,2017,Summer,c,C +219,BIOL,2210,2020,Fall,a,C +220,CS,4940,2019,Fall,a,C +223,CS,3505,2019,Summer,b,C +227,PHYS,3210,2018,Fall,a,C +228,PHYS,2220,2020,Fall,a,C +229,MATH,3210,2016,Spring,a,C +230,MATH,3210,2019,Fall,a,C +230,PHYS,2040,2017,Fall,c,C +231,CS,3810,2018,Summer,b,C +231,MATH,1250,2020,Summer,a,C +237,CS,3100,2017,Fall,a,C +237,PHYS,2040,2017,Fall,a,C +239,BIOL,2210,2018,Summer,a,C +239,MATH,2210,2018,Spring,b,C +240,BIOL,2210,2020,Fall,a,C +241,PHYS,2060,2019,Fall,b,C +241,PHYS,2220,2019,Spring,a,C +241,PHYS,3220,2020,Spring,b,C +242,BIOL,2420,2020,Spring,a,C +248,CS,4500,2019,Summer,a,C +249,MATH,2280,2015,Summer,a,C +250,CS,4970,2019,Fall,a,C +251,BIOL,2010,2020,Summer,a,C +252,CS,2100,2018,Fall,c,C +252,PHYS,2060,2018,Fall,d,C +255,BIOL,2020,2018,Fall,a,C +255,CS,4940,2019,Fall,a,C +255,PHYS,2140,2017,Summer,a,C +256,PHYS,2220,2017,Spring,a,C +258,BIOL,1030,2019,Spring,c,C +259,MATH,2270,2017,Fall,b,C +260,PHYS,3210,2016,Fall,a,C +261,BIOL,1006,2018,Spring,b,C +261,CS,4970,2017,Summer,a,C +263,BIOL,1010,2020,Summer,d,C +267,BIOL,2020,2018,Fall,b,C +270,BIOL,2210,2017,Summer,b,C +270,CS,3810,2018,Summer,d,C +270,CS,4150,2018,Fall,a,C +270,CS,4500,2018,Spring,b,C +270,MATH,1250,2016,Summer,a,C +274,MATH,1250,2018,Spring,a,C +274,MATH,2210,2020,Spring,c,C +275,BIOL,1030,2018,Fall,a,C +275,MATH,1210,2019,Spring,b,C +275,PHYS,2040,2019,Spring,a,C +277,BIOL,2210,2017,Spring,c,C +277,MATH,1210,2016,Spring,d,C +277,PHYS,2060,2019,Summer,a,C +281,MATH,1220,2020,Summer,a,C +282,BIOL,1010,2016,Summer,a,C +282,BIOL,2330,2016,Spring,a,C +282,PHYS,2140,2015,Spring,a,C +285,CS,1030,2019,Fall,a,C +285,CS,4970,2016,Fall,a,C +285,MATH,2210,2019,Spring,b,C +288,CS,2420,2017,Summer,c,C +289,MATH,3210,2020,Fall,a,C +290,BIOL,2355,2017,Spring,a,C +291,CS,4000,2017,Fall,a,C +292,BIOL,2020,2018,Spring,a,C +292,PHYS,2210,2019,Spring,c,C +293,CS,3505,2020,Fall,c,C +293,MATH,1260,2019,Spring,c,C +295,CS,2420,2016,Fall,b,C +295,MATH,1210,2016,Spring,d,C +296,BIOL,2325,2017,Fall,b,C +298,BIOL,1010,2018,Fall,b,C +298,BIOL,1030,2019,Spring,c,C +300,BIOL,2021,2019,Fall,a,C +301,BIOL,1010,2015,Summer,c,C +303,BIOL,2021,2019,Fall,a,C +303,CS,4970,2019,Summer,d,C +307,BIOL,2355,2020,Summer,b,C +307,CS,1030,2020,Spring,a,C +307,CS,3505,2019,Summer,a,C +307,CS,4970,2020,Summer,d,C +307,MATH,1210,2019,Spring,a,C +307,PHYS,3220,2017,Fall,c,C +309,BIOL,2030,2019,Summer,b,C +309,BIOL,2355,2020,Spring,a,C +309,CS,4150,2020,Fall,a,C +309,MATH,2280,2018,Fall,c,C +311,BIOL,2021,2018,Spring,a,C +311,BIOL,2355,2018,Summer,b,C +311,CS,3200,2020,Summer,a,C +311,CS,4940,2017,Fall,a,C +312,CS,3505,2017,Summer,a,C +312,PHYS,2210,2019,Fall,b,C +313,BIOL,1006,2020,Fall,a,C +313,BIOL,1006,2020,Fall,c,C +313,CS,3500,2015,Fall,b,C +313,PHYS,3210,2019,Summer,b,C +318,CS,2100,2019,Fall,c,C +318,CS,3505,2019,Summer,d,C +323,BIOL,2420,2020,Summer,a,C +323,CS,4970,2020,Fall,d,C +325,BIOL,2325,2019,Summer,a,C +329,CS,3505,2016,Fall,b,C +329,CS,4000,2017,Fall,b,C +331,MATH,2270,2020,Fall,a,C +332,CS,3200,2020,Spring,c,C +333,BIOL,1006,2020,Fall,a,C +333,BIOL,2010,2020,Summer,a,C +333,MATH,1210,2019,Spring,a,C +335,CS,2100,2016,Summer,b,C +335,CS,3505,2015,Fall,b,C +340,BIOL,1030,2020,Summer,a,C +340,CS,3505,2019,Summer,b,C +340,CS,3810,2020,Fall,a,C +341,PHYS,2060,2019,Fall,b,C +345,CS,3505,2018,Fall,a,C +345,PHYS,2140,2020,Fall,a,C +348,CS,3810,2016,Fall,a,C +356,BIOL,2021,2018,Summer,a,C +356,CS,2420,2019,Summer,a,C +357,CS,3200,2016,Summer,a,C +361,BIOL,2021,2018,Spring,a,C +362,MATH,1220,2018,Spring,b,C +363,BIOL,2355,2020,Summer,b,C +364,CS,4970,2019,Spring,b,C +365,CS,3500,2020,Summer,a,C +366,BIOL,1010,2018,Summer,b,C +369,BIOL,2330,2016,Fall,a,C +371,BIOL,2030,2018,Summer,b,C +371,CS,4150,2018,Fall,b,C +372,BIOL,1030,2018,Summer,a,C +372,BIOL,2030,2017,Spring,b,C +372,MATH,3210,2017,Summer,a,C +372,PHYS,2040,2019,Spring,a,C +373,BIOL,2021,2018,Spring,a,C +373,CS,4000,2017,Summer,a,C +373,CS,4500,2020,Spring,a,C +373,MATH,2270,2020,Fall,a,C +373,PHYS,2210,2017,Summer,a,C +374,BIOL,1010,2018,Summer,c,C +374,CS,3500,2016,Spring,a,C +374,PHYS,2060,2016,Summer,b,C +374,PHYS,2220,2015,Spring,a,C +375,BIOL,1006,2018,Spring,b,C +375,CS,3500,2019,Fall,b,C +377,CS,2100,2017,Spring,a,C +378,BIOL,2010,2020,Summer,a,C +378,CS,3505,2016,Summer,a,C +378,CS,4150,2016,Summer,a,C +378,MATH,1210,2016,Fall,b,C +378,MATH,2270,2019,Summer,b,C +379,CS,3505,2016,Fall,a,C +379,PHYS,2140,2017,Fall,b,C +379,PHYS,2210,2015,Fall,c,C +381,CS,2100,2018,Summer,c,C +382,BIOL,1010,2015,Summer,b,C +385,CS,3100,2017,Spring,b,C +385,MATH,1250,2018,Spring,a,C +386,PHYS,2140,2018,Fall,a,C +387,MATH,2210,2017,Summer,a,C +387,PHYS,2040,2015,Fall,c,C +387,PHYS,2140,2016,Fall,a,C +388,MATH,1220,2017,Spring,b,C +389,CS,2420,2016,Spring,a,C +390,PHYS,3210,2020,Spring,a,C +391,BIOL,1010,2017,Spring,a,C +391,BIOL,1030,2018,Fall,a,C +391,CS,1410,2017,Spring,a,C +391,CS,4400,2019,Summer,b,C +391,MATH,3220,2017,Spring,a,C +392,BIOL,2210,2016,Summer,a,C +392,CS,3505,2015,Fall,b,C +392,PHYS,2210,2015,Fall,b,C +393,CS,4000,2016,Fall,a,C +393,PHYS,2220,2018,Summer,a,C +394,BIOL,2325,2016,Summer,a,C +394,CS,4970,2016,Fall,a,C +396,PHYS,2210,2019,Fall,b,C +397,BIOL,2325,2018,Summer,a,C +397,CS,3505,2017,Fall,a,C +397,MATH,1210,2016,Fall,a,C +398,PHYS,3220,2018,Summer,a,C +399,BIOL,2325,2018,Fall,c,C +399,MATH,2270,2019,Summer,c,C +100,BIOL,1010,2020,Summer,d,C+ +100,MATH,2280,2019,Fall,a,C+ +101,BIOL,2020,2018,Fall,d,C+ +102,BIOL,2030,2020,Spring,b,C+ +102,MATH,2210,2019,Spring,a,C+ +102,MATH,2280,2019,Fall,a,C+ +102,PHYS,3220,2020,Spring,a,C+ +105,BIOL,2325,2018,Spring,a,C+ +105,CS,2420,2016,Fall,b,C+ +105,PHYS,2040,2018,Spring,a,C+ +107,BIOL,2420,2017,Summer,b,C+ +107,CS,2100,2019,Spring,a,C+ +107,MATH,2270,2017,Fall,d,C+ +108,BIOL,2010,2020,Spring,b,C+ +108,MATH,1210,2020,Spring,b,C+ +108,PHYS,2210,2019,Fall,b,C+ +109,CS,4000,2020,Fall,a,C+ +109,PHYS,3220,2017,Fall,b,C+ +113,BIOL,2355,2018,Summer,c,C+ +113,PHYS,3210,2019,Spring,a,C+ +117,MATH,1220,2017,Spring,b,C+ +118,CS,3505,2019,Fall,b,C+ +118,PHYS,2220,2020,Summer,a,C+ +119,CS,4970,2019,Summer,d,C+ +119,PHYS,2220,2017,Spring,d,C+ +119,PHYS,3220,2017,Summer,a,C+ +120,BIOL,2030,2017,Spring,d,C+ +120,BIOL,2355,2020,Fall,a,C+ +120,CS,2420,2017,Fall,a,C+ +122,MATH,2210,2020,Fall,a,C+ +123,BIOL,1006,2016,Spring,b,C+ +123,CS,3500,2016,Spring,a,C+ +123,CS,3810,2016,Summer,a,C+ +123,PHYS,2220,2018,Fall,a,C+ +123,PHYS,3210,2016,Fall,a,C+ +124,PHYS,2220,2020,Spring,a,C+ +124,PHYS,3220,2020,Spring,d,C+ +127,BIOL,2010,2017,Summer,a,C+ +127,CS,3500,2020,Summer,a,C+ +128,MATH,1210,2018,Fall,b,C+ +131,CS,4500,2019,Fall,c,C+ +133,BIOL,1010,2018,Summer,b,C+ +133,MATH,1260,2019,Spring,a,C+ +134,BIOL,1210,2018,Spring,a,C+ +134,CS,3200,2015,Fall,b,C+ +134,PHYS,2140,2016,Spring,c,C+ +135,BIOL,1030,2020,Spring,a,C+ +135,CS,1030,2020,Spring,c,C+ +138,CS,1030,2016,Spring,a,C+ +138,CS,3100,2016,Spring,d,C+ +138,PHYS,2140,2015,Summer,c,C+ +139,CS,3100,2017,Fall,a,C+ +139,MATH,1250,2018,Summer,c,C+ +140,CS,2420,2015,Summer,c,C+ +140,PHYS,2140,2015,Summer,a,C+ +148,BIOL,1010,2020,Summer,a,C+ +149,CS,4400,2016,Spring,a,C+ +151,BIOL,1030,2017,Spring,c,C+ +151,BIOL,2030,2016,Fall,a,C+ +153,BIOL,1030,2020,Spring,a,C+ +155,BIOL,2330,2017,Fall,a,C+ +158,PHYS,2060,2018,Fall,b,C+ +163,CS,2420,2016,Fall,a,C+ +163,CS,3100,2015,Summer,a,C+ +164,BIOL,1030,2020,Summer,a,C+ +164,BIOL,2021,2019,Fall,a,C+ +164,CS,1410,2018,Spring,b,C+ +165,BIOL,1006,2017,Fall,b,C+ +165,BIOL,1010,2019,Spring,b,C+ +165,MATH,1220,2018,Spring,a,C+ +167,BIOL,1030,2019,Summer,a,C+ +167,MATH,1210,2018,Fall,a,C+ +169,BIOL,2420,2018,Spring,a,C+ +170,CS,1030,2020,Spring,b,C+ +171,MATH,3210,2020,Summer,a,C+ +173,BIOL,2030,2019,Summer,b,C+ +173,CS,4400,2019,Summer,a,C+ +175,BIOL,2355,2020,Fall,a,C+ +175,MATH,2210,2020,Fall,a,C+ +176,BIOL,2020,2015,Fall,c,C+ +176,PHYS,2100,2016,Fall,b,C+ +177,BIOL,1210,2018,Spring,a,C+ +177,BIOL,2010,2020,Summer,b,C+ +177,MATH,2270,2020,Fall,a,C+ +177,PHYS,2210,2017,Summer,a,C+ +178,BIOL,2355,2019,Spring,a,C+ +178,CS,3200,2020,Fall,a,C+ +178,PHYS,2060,2020,Fall,a,C+ +179,CS,3200,2015,Fall,b,C+ +179,MATH,2210,2020,Fall,a,C+ +182,BIOL,2210,2017,Spring,b,C+ +182,CS,3505,2015,Fall,b,C+ +182,CS,4500,2018,Spring,a,C+ +182,MATH,2280,2018,Spring,a,C+ +183,BIOL,1030,2018,Fall,a,C+ +183,BIOL,2020,2018,Fall,a,C+ +185,BIOL,1030,2020,Summer,a,C+ +185,CS,3505,2018,Summer,b,C+ +185,CS,4500,2019,Summer,a,C+ +187,MATH,1220,2017,Spring,a,C+ +187,PHYS,2060,2020,Fall,a,C+ +187,PHYS,3220,2017,Fall,d,C+ +194,CS,3505,2019,Fall,c,C+ +194,CS,4940,2020,Summer,b,C+ +195,MATH,1210,2016,Fall,c,C+ +196,CS,2100,2018,Fall,c,C+ +197,MATH,2210,2018,Spring,b,C+ +199,CS,2420,2019,Summer,a,C+ +200,PHYS,3210,2020,Fall,b,C+ +203,CS,3500,2017,Fall,c,C+ +204,BIOL,2330,2015,Fall,d,C+ +210,CS,1030,2019,Fall,b,C+ +210,PHYS,2060,2019,Fall,a,C+ +211,CS,3200,2015,Spring,b,C+ +213,BIOL,2030,2016,Fall,a,C+ +214,BIOL,1006,2016,Summer,d,C+ +214,BIOL,2325,2018,Spring,a,C+ +214,CS,2100,2016,Spring,a,C+ +215,CS,2100,2017,Fall,a,C+ +215,CS,2420,2016,Fall,b,C+ +219,CS,3505,2020,Summer,a,C+ +221,CS,1030,2020,Spring,a,C+ +223,BIOL,2010,2020,Spring,a,C+ +225,CS,2420,2020,Fall,a,C+ +225,CS,3810,2020,Fall,a,C+ +227,MATH,2280,2018,Fall,b,C+ +227,PHYS,2140,2019,Fall,a,C+ +227,PHYS,3220,2020,Spring,d,C+ +228,CS,2100,2020,Spring,a,C+ +229,MATH,1260,2016,Fall,a,C+ +229,MATH,2210,2018,Spring,b,C+ +231,BIOL,2010,2020,Spring,a,C+ +231,MATH,1260,2020,Spring,a,C+ +234,MATH,1220,2019,Fall,a,C+ +235,CS,4400,2020,Fall,b,C+ +238,MATH,3220,2018,Spring,d,C+ +241,CS,4400,2019,Fall,a,C+ +241,PHYS,3220,2020,Spring,c,C+ +242,BIOL,2010,2020,Summer,a,C+ +243,CS,2420,2016,Fall,c,C+ +245,CS,4150,2016,Summer,a,C+ +245,MATH,1220,2015,Summer,c,C+ +246,PHYS,2100,2017,Fall,a,C+ +246,PHYS,2210,2015,Fall,a,C+ +247,BIOL,2325,2018,Fall,a,C+ +247,MATH,2280,2019,Fall,b,C+ +248,BIOL,2355,2019,Spring,c,C+ +248,CS,3200,2020,Spring,c,C+ +249,CS,3505,2016,Fall,b,C+ +249,CS,4970,2016,Fall,b,C+ +249,PHYS,2220,2017,Spring,d,C+ +250,CS,3505,2020,Fall,c,C+ +253,CS,2100,2018,Fall,d,C+ +254,CS,4500,2019,Fall,d,C+ +255,BIOL,2010,2018,Spring,a,C+ +255,CS,3500,2019,Fall,a,C+ +255,MATH,1250,2018,Summer,a,C+ +255,PHYS,2210,2019,Spring,d,C+ +256,CS,4500,2019,Fall,c,C+ +256,PHYS,2040,2017,Fall,b,C+ +257,BIOL,2020,2018,Fall,a,C+ +257,BIOL,2021,2018,Summer,a,C+ +257,CS,4000,2020,Spring,a,C+ +257,MATH,1260,2019,Summer,a,C+ +257,PHYS,2060,2018,Fall,b,C+ +258,BIOL,1030,2019,Spring,b,C+ +258,CS,3500,2019,Summer,a,C+ +258,PHYS,3210,2019,Spring,c,C+ +260,BIOL,2325,2017,Fall,b,C+ +261,BIOL,2020,2018,Fall,a,C+ +262,BIOL,2020,2018,Fall,b,C+ +266,BIOL,2330,2017,Fall,b,C+ +270,BIOL,2355,2017,Spring,b,C+ +274,BIOL,2020,2018,Fall,a,C+ +275,CS,4970,2019,Spring,a,C+ +276,BIOL,1006,2016,Spring,a,C+ +276,CS,3100,2015,Summer,a,C+ +276,CS,3505,2019,Spring,a,C+ +277,BIOL,1010,2015,Summer,a,C+ +277,MATH,1210,2016,Spring,c,C+ +281,CS,4970,2020,Fall,c,C+ +282,CS,3505,2015,Spring,a,C+ +282,CS,4000,2015,Fall,a,C+ +285,MATH,1220,2017,Spring,b,C+ +285,MATH,3220,2016,Spring,a,C+ +285,PHYS,2210,2017,Summer,b,C+ +287,CS,4400,2019,Summer,a,C+ +289,BIOL,2210,2019,Fall,b,C+ +291,CS,1030,2016,Spring,a,C+ +291,CS,1410,2016,Spring,b,C+ +292,BIOL,1030,2020,Spring,a,C+ +292,MATH,2270,2017,Fall,a,C+ +292,MATH,3210,2017,Summer,a,C+ +295,CS,4970,2017,Spring,a,C+ +297,PHYS,2140,2020,Fall,a,C+ +298,CS,2100,2018,Summer,c,C+ +300,CS,4970,2019,Summer,a,C+ +304,MATH,3210,2017,Spring,a,C+ +307,BIOL,1030,2019,Spring,c,C+ +307,CS,1410,2018,Spring,d,C+ +309,BIOL,2210,2017,Spring,b,C+ +309,CS,2420,2017,Summer,b,C+ +309,CS,3500,2017,Fall,c,C+ +309,CS,4500,2016,Fall,a,C+ +309,MATH,1220,2018,Spring,b,C+ +309,MATH,3210,2017,Summer,a,C+ +311,BIOL,1010,2018,Summer,b,C+ +311,CS,4970,2019,Spring,b,C+ +312,PHYS,2100,2016,Fall,a,C+ +313,BIOL,1010,2018,Summer,c,C+ +313,BIOL,2010,2019,Fall,a,C+ +313,BIOL,2020,2016,Spring,a,C+ +313,MATH,1260,2019,Spring,b,C+ +314,BIOL,1030,2019,Summer,a,C+ +314,BIOL,2210,2019,Summer,a,C+ +314,CS,4970,2017,Spring,a,C+ +314,MATH,2270,2017,Fall,d,C+ +316,BIOL,2010,2019,Fall,a,C+ +318,BIOL,1010,2018,Summer,a,C+ +318,BIOL,2030,2019,Summer,a,C+ +318,BIOL,2210,2019,Summer,b,C+ +321,BIOL,2420,2020,Fall,a,C+ +321,CS,2100,2019,Fall,b,C+ +329,PHYS,3210,2019,Spring,c,C+ +331,MATH,2270,2020,Fall,b,C+ +332,BIOL,2355,2018,Summer,a,C+ +332,CS,4400,2019,Summer,b,C+ +332,MATH,1220,2018,Spring,a,C+ +333,BIOL,2325,2019,Spring,a,C+ +333,CS,4970,2019,Summer,c,C+ +335,CS,4970,2016,Fall,a,C+ +340,BIOL,1010,2020,Summer,a,C+ +342,BIOL,1210,2019,Spring,a,C+ +342,BIOL,2420,2020,Fall,a,C+ +348,BIOL,2330,2020,Spring,a,C+ +348,CS,4500,2017,Summer,a,C+ +348,MATH,2270,2020,Fall,a,C+ +348,PHYS,2040,2017,Fall,c,C+ +355,MATH,1220,2017,Spring,c,C+ +356,MATH,2270,2017,Fall,b,C+ +356,PHYS,2220,2016,Fall,a,C+ +366,BIOL,2030,2020,Spring,a,C+ +368,MATH,3210,2020,Summer,a,C+ +368,PHYS,2060,2019,Fall,b,C+ +369,PHYS,2140,2018,Summer,b,C+ +371,BIOL,1010,2020,Summer,c,C+ +371,PHYS,2140,2019,Fall,a,C+ +372,BIOL,2010,2017,Fall,a,C+ +372,PHYS,2220,2017,Spring,a,C+ +373,BIOL,2210,2020,Fall,a,C+ +374,CS,3810,2018,Summer,a,C+ +375,PHYS,3210,2019,Spring,c,C+ +377,BIOL,2020,2015,Fall,c,C+ +377,PHYS,2100,2017,Summer,b,C+ +378,CS,3200,2020,Spring,c,C+ +378,CS,4000,2016,Fall,a,C+ +378,MATH,1220,2017,Spring,d,C+ +379,BIOL,1006,2020,Fall,a,C+ +379,BIOL,2030,2015,Fall,a,C+ +380,BIOL,2355,2018,Fall,a,C+ +381,PHYS,3210,2018,Spring,c,C+ +382,BIOL,1010,2015,Summer,a,C+ +386,CS,4150,2020,Fall,a,C+ +387,CS,2100,2018,Spring,a,C+ +387,MATH,3220,2018,Spring,b,C+ +388,CS,2100,2016,Summer,b,C+ +389,BIOL,1006,2016,Summer,d,C+ +390,MATH,2280,2019,Fall,b,C+ +391,BIOL,1006,2018,Fall,a,C+ +391,CS,3505,2019,Fall,c,C+ +391,MATH,2210,2018,Spring,a,C+ +391,PHYS,2060,2020,Spring,b,C+ +392,BIOL,1030,2016,Spring,a,C+ +392,BIOL,2330,2017,Fall,b,C+ +392,CS,2100,2018,Summer,c,C+ +394,CS,1410,2016,Summer,a,C+ +395,BIOL,2355,2016,Spring,b,C+ +396,CS,4150,2020,Spring,a,C+ +397,BIOL,2420,2020,Spring,a,C+ +397,CS,2100,2019,Fall,b,C+ +397,CS,2100,2019,Fall,c,C+ +398,MATH,2270,2020,Fall,a,C+ +398,MATH,2280,2020,Spring,b,C+ +399,BIOL,2020,2018,Fall,a,C+ +399,BIOL,2021,2019,Spring,a,C+ +399,CS,2100,2018,Fall,d,C+ +399,MATH,3210,2019,Spring,a,C+ +100,CS,3505,2018,Summer,a,C- +101,BIOL,2030,2018,Summer,b,C- +102,MATH,1220,2019,Fall,b,C- +105,BIOL,1010,2018,Summer,a,C- +106,CS,2100,2019,Summer,b,C- +107,BIOL,2210,2017,Spring,c,C- +107,CS,4000,2017,Fall,a,C- +108,CS,4500,2020,Spring,a,C- +109,CS,1410,2018,Spring,b,C- +109,CS,3505,2020,Fall,c,C- +112,BIOL,1010,2020,Summer,c,C- +113,CS,4400,2020,Spring,a,C- +115,BIOL,2420,2017,Summer,a,C- +118,BIOL,2355,2020,Spring,a,C- +118,CS,2100,2019,Fall,c,C- +118,MATH,1220,2020,Summer,a,C- +119,MATH,2280,2018,Fall,b,C- +120,BIOL,2020,2015,Summer,a,C- +120,CS,4150,2020,Spring,a,C- +120,PHYS,2040,2020,Spring,a,C- +121,BIOL,1010,2020,Summer,d,C- +121,BIOL,2420,2020,Spring,b,C- +121,CS,4970,2018,Fall,d,C- +121,PHYS,3210,2020,Fall,b,C- +122,CS,1030,2020,Spring,a,C- +123,BIOL,2010,2017,Summer,a,C- +123,CS,4000,2020,Spring,b,C- +123,MATH,1220,2019,Fall,b,C- +124,CS,1030,2020,Spring,c,C- +124,CS,3200,2020,Fall,a,C- +125,PHYS,3210,2020,Spring,a,C- +127,BIOL,1006,2019,Spring,a,C- +127,PHYS,2060,2018,Fall,b,C- +131,BIOL,2420,2020,Summer,a,C- +133,CS,3500,2019,Fall,b,C- +133,MATH,1220,2019,Fall,a,C- +133,MATH,2280,2019,Fall,b,C- +135,BIOL,2325,2019,Summer,a,C- +136,CS,4970,2020,Fall,b,C- +137,CS,3505,2020,Summer,a,C- +138,CS,4000,2016,Fall,a,C- +138,CS,4400,2016,Fall,a,C- +138,MATH,1210,2015,Summer,a,C- +139,BIOL,2021,2019,Spring,b,C- +139,CS,4500,2017,Summer,a,C- +139,PHYS,3210,2017,Summer,a,C- +143,BIOL,1006,2019,Summer,a,C- +143,CS,1030,2019,Fall,a,C- +143,PHYS,3220,2018,Summer,a,C- +145,CS,4970,2016,Fall,a,C- +146,BIOL,2420,2020,Fall,a,C- +151,CS,3505,2017,Summer,a,C- +151,PHYS,2060,2019,Fall,c,C- +151,PHYS,2100,2017,Summer,c,C- +151,PHYS,2210,2018,Fall,b,C- +151,PHYS,2220,2017,Spring,c,C- +152,BIOL,2020,2018,Fall,a,C- +152,BIOL,2021,2018,Fall,a,C- +152,MATH,1220,2019,Fall,b,C- +152,PHYS,3210,2019,Summer,c,C- +160,BIOL,1006,2016,Spring,a,C- +161,CS,3200,2020,Fall,a,C- +163,CS,4000,2017,Fall,a,C- +164,BIOL,2325,2018,Fall,a,C- +164,CS,4000,2018,Spring,a,C- +164,CS,4970,2019,Summer,d,C- +165,PHYS,2060,2018,Fall,c,C- +167,BIOL,2325,2018,Fall,b,C- +167,PHYS,3210,2019,Summer,c,C- +171,CS,3505,2020,Fall,c,C- +172,MATH,2210,2015,Fall,a,C- +172,MATH,3210,2015,Fall,d,C- +173,CS,4000,2018,Spring,a,C- +173,MATH,1220,2018,Spring,a,C- +176,CS,3200,2016,Summer,b,C- +176,MATH,3220,2017,Spring,a,C- +177,BIOL,2020,2018,Fall,a,C- +177,CS,3200,2020,Summer,a,C- +177,CS,3505,2017,Fall,b,C- +177,CS,4970,2016,Fall,b,C- +177,PHYS,2140,2017,Summer,a,C- +179,BIOL,2325,2019,Summer,a,C- +179,MATH,1260,2019,Spring,b,C- +179,PHYS,2220,2015,Fall,a,C- +181,MATH,2270,2020,Fall,a,C- +182,CS,3810,2018,Summer,d,C- +182,MATH,1220,2017,Spring,b,C- +185,CS,3200,2020,Summer,a,C- +185,PHYS,3210,2020,Spring,a,C- +187,CS,4400,2019,Spring,c,C- +188,PHYS,3210,2019,Summer,c,C- +189,BIOL,2420,2020,Summer,a,C- +192,BIOL,2355,2015,Summer,a,C- +194,CS,3200,2020,Spring,a,C- +195,MATH,1210,2016,Fall,d,C- +195,MATH,3210,2016,Fall,a,C- +195,PHYS,2060,2016,Spring,a,C- +196,BIOL,2021,2019,Spring,a,C- +199,BIOL,1030,2018,Fall,a,C- +200,CS,4940,2020,Summer,a,C- +202,CS,1030,2020,Fall,a,C- +203,MATH,3220,2018,Spring,d,C- +204,BIOL,1030,2015,Summer,a,C- +207,BIOL,2021,2017,Fall,a,C- +210,CS,3100,2017,Spring,b,C- +211,MATH,1220,2015,Summer,a,C- +214,BIOL,2210,2017,Summer,a,C- +217,PHYS,3210,2019,Spring,b,C- +220,BIOL,2325,2018,Fall,a,C- +221,BIOL,2010,2020,Summer,a,C- +222,PHYS,2140,2020,Fall,a,C- +223,CS,2100,2019,Fall,a,C- +223,CS,3500,2019,Fall,b,C- +227,BIOL,1006,2018,Spring,b,C- +227,CS,4970,2018,Summer,b,C- +228,CS,3200,2020,Spring,a,C- +228,PHYS,3210,2020,Fall,b,C- +230,BIOL,2030,2018,Summer,a,C- +230,CS,2420,2020,Fall,a,C- +231,CS,4940,2020,Summer,b,C- +231,CS,4970,2018,Summer,a,C- +231,PHYS,3210,2019,Spring,c,C- +233,BIOL,2355,2020,Fall,a,C- +233,CS,2420,2020,Summer,a,C- +235,MATH,1260,2020,Spring,a,C- +235,PHYS,2220,2020,Spring,a,C- +237,PHYS,3220,2017,Fall,a,C- +242,MATH,1260,2020,Spring,a,C- +242,PHYS,2220,2020,Summer,b,C- +243,BIOL,1030,2017,Spring,b,C- +247,CS,4940,2020,Summer,b,C- +248,BIOL,2420,2020,Spring,b,C- +248,CS,4400,2019,Spring,c,C- +249,MATH,1210,2016,Fall,a,C- +251,CS,4940,2020,Summer,a,C- +251,PHYS,3220,2020,Spring,b,C- +253,BIOL,1030,2018,Fall,a,C- +256,BIOL,1030,2019,Spring,c,C- +256,MATH,2280,2018,Spring,a,C- +257,CS,4970,2020,Summer,c,C- +257,PHYS,2220,2018,Summer,a,C- +259,BIOL,2010,2017,Summer,a,C- +259,CS,4000,2017,Summer,a,C- +259,MATH,2280,2018,Fall,a,C- +260,CS,3810,2018,Summer,a,C- +260,MATH,2270,2020,Fall,b,C- +261,CS,2420,2017,Summer,c,C- +261,CS,3100,2017,Spring,a,C- +261,MATH,2210,2017,Spring,a,C- +262,CS,2100,2016,Summer,b,C- +266,MATH,3220,2017,Fall,a,C- +267,MATH,2280,2019,Fall,b,C- +268,CS,3200,2016,Fall,a,C- +270,CS,1410,2015,Summer,d,C- +270,MATH,2210,2017,Spring,a,C- +270,MATH,2280,2019,Fall,a,C- +270,MATH,2280,2019,Fall,c,C- +270,MATH,3220,2016,Summer,a,C- +270,PHYS,2210,2018,Fall,b,C- +271,BIOL,2355,2020,Fall,a,C- +271,PHYS,3220,2020,Spring,c,C- +275,BIOL,1010,2018,Summer,b,C- +275,BIOL,2355,2018,Summer,c,C- +275,CS,1410,2018,Spring,d,C- +275,CS,4000,2018,Spring,a,C- +276,CS,3810,2015,Spring,a,C- +276,MATH,1260,2019,Summer,a,C- +276,MATH,3210,2016,Spring,a,C- +276,PHYS,3210,2018,Fall,a,C- +277,BIOL,1010,2015,Summer,c,C- +277,CS,3100,2016,Fall,a,C- +278,BIOL,2210,2016,Summer,a,C- +278,MATH,1260,2016,Fall,a,C- +281,MATH,1250,2020,Summer,a,C- +285,CS,4970,2016,Fall,b,C- +285,MATH,1210,2016,Fall,c,C- +285,MATH,2270,2019,Spring,a,C- +285,MATH,2280,2020,Spring,b,C- +285,PHYS,2100,2018,Fall,a,C- +285,PHYS,3220,2016,Summer,a,C- +288,MATH,1210,2018,Summer,a,C- +290,CS,3505,2016,Summer,a,C- +290,CS,4400,2015,Summer,a,C- +291,MATH,2270,2017,Fall,d,C- +292,BIOL,1006,2018,Spring,b,C- +294,CS,3500,2017,Fall,c,C- +294,CS,3505,2017,Fall,b,C- +294,CS,4940,2017,Fall,a,C- +295,CS,2100,2016,Spring,a,C- +296,CS,3100,2017,Fall,a,C- +296,MATH,3220,2018,Spring,a,C- +297,PHYS,3210,2020,Fall,a,C- +300,PHYS,2220,2020,Summer,b,C- +305,CS,1030,2018,Fall,a,C- +307,BIOL,2330,2019,Fall,a,C- +307,PHYS,2040,2015,Fall,c,C- +309,MATH,1260,2019,Fall,a,C- +309,PHYS,2140,2020,Fall,a,C- +311,PHYS,2060,2018,Fall,c,C- +311,PHYS,2060,2018,Fall,d,C- +312,BIOL,2325,2015,Fall,c,C- +313,BIOL,2030,2017,Spring,a,C- +313,MATH,1210,2019,Summer,a,C- +313,MATH,2270,2015,Fall,b,C- +313,MATH,3210,2015,Fall,a,C- +314,CS,4940,2019,Fall,a,C- +314,PHYS,2040,2017,Fall,a,C- +314,PHYS,2100,2016,Fall,a,C- +317,CS,4500,2016,Spring,a,C- +318,MATH,2270,2017,Summer,a,C- +321,PHYS,2060,2020,Spring,a,C- +321,PHYS,2060,2020,Spring,b,C- +325,BIOL,2020,2018,Fall,d,C- +325,BIOL,2355,2020,Summer,b,C- +329,CS,1030,2019,Fall,b,C- +329,CS,4500,2018,Spring,a,C- +329,PHYS,2210,2018,Fall,a,C- +332,CS,1030,2020,Spring,b,C- +332,CS,3500,2019,Fall,a,C- +335,CS,3810,2016,Fall,b,C- +340,PHYS,2210,2019,Fall,a,C- +341,CS,4500,2019,Fall,d,C- +342,BIOL,2325,2019,Spring,a,C- +342,BIOL,2330,2017,Fall,a,C- +342,PHYS,2220,2018,Summer,a,C- +344,BIOL,2020,2018,Fall,b,C- +344,BIOL,2021,2018,Summer,a,C- +347,BIOL,2030,2020,Spring,b,C- +347,CS,4970,2019,Fall,d,C- +348,BIOL,1010,2020,Summer,c,C- +348,BIOL,2010,2018,Spring,a,C- +348,CS,1030,2016,Spring,a,C- +348,CS,3100,2019,Spring,b,C- +351,PHYS,3210,2019,Spring,a,C- +355,CS,1410,2016,Spring,a,C- +355,MATH,1250,2017,Summer,a,C- +356,CS,2100,2017,Fall,a,C- +362,MATH,3220,2018,Spring,a,C- +364,BIOL,2021,2019,Fall,a,C- +364,CS,4400,2019,Fall,b,C- +365,MATH,3210,2020,Fall,a,C- +366,PHYS,2210,2019,Fall,b,C- +368,CS,2420,2020,Summer,a,C- +368,CS,4400,2019,Summer,b,C- +368,MATH,1250,2018,Summer,b,C- +369,BIOL,2355,2017,Spring,c,C- +371,CS,4500,2019,Summer,a,C- +371,MATH,1210,2018,Summer,a,C- +371,PHYS,2210,2019,Fall,c,C- +372,BIOL,1010,2019,Spring,a,C- +373,BIOL,2030,2018,Summer,a,C- +373,CS,3500,2020,Summer,a,C- +373,MATH,1210,2020,Spring,a,C- +373,MATH,2210,2015,Fall,a,C- +374,BIOL,2420,2015,Summer,a,C- +374,MATH,1250,2016,Fall,c,C- +375,BIOL,1030,2019,Spring,c,C- +375,BIOL,2010,2020,Summer,a,C- +375,BIOL,2030,2020,Spring,b,C- +375,CS,1410,2020,Spring,b,C- +375,PHYS,2100,2017,Summer,a,C- +376,BIOL,1006,2020,Fall,a,C- +377,BIOL,2325,2019,Spring,a,C- +377,BIOL,2355,2015,Summer,a,C- +377,MATH,1220,2015,Summer,c,C- +377,MATH,3220,2017,Fall,a,C- +378,CS,4500,2017,Summer,a,C- +378,CS,4970,2019,Summer,b,C- +378,PHYS,2100,2017,Summer,b,C- +379,CS,3500,2016,Summer,a,C- +379,CS,3810,2018,Summer,d,C- +384,BIOL,2010,2020,Spring,a,C- +385,BIOL,2010,2018,Spring,a,C- +385,CS,1030,2016,Summer,a,C- +385,CS,3505,2017,Fall,b,C- +385,PHYS,2220,2016,Fall,a,C- +388,BIOL,2021,2017,Summer,a,C- +388,CS,3200,2018,Spring,c,C- +390,BIOL,1010,2020,Summer,c,C- +391,BIOL,2325,2019,Spring,a,C- +391,CS,4150,2018,Fall,a,C- +392,BIOL,2355,2016,Spring,b,C- +393,BIOL,2210,2017,Spring,c,C- +394,MATH,1210,2017,Spring,a,C- +396,CS,3505,2018,Fall,c,C- +397,BIOL,1030,2019,Spring,c,C- +397,CS,4970,2016,Fall,b,C- +397,MATH,2210,2020,Fall,a,C- +398,BIOL,1010,2020,Summer,c,C- +398,BIOL,2355,2018,Summer,b,C- +398,CS,4400,2019,Summer,a,C- +399,CS,4970,2019,Summer,d,C- +100,CS,4940,2020,Summer,a,D +101,MATH,1250,2018,Summer,b,D +106,CS,4150,2020,Spring,a,D +107,CS,3200,2016,Fall,d,D +107,MATH,2280,2020,Spring,a,D +109,BIOL,1010,2019,Spring,b,D +109,CS,4500,2019,Fall,d,D +113,BIOL,2020,2018,Fall,b,D +113,PHYS,2140,2018,Summer,a,D +116,MATH,1220,2017,Spring,a,D +117,BIOL,2020,2016,Spring,a,D +117,CS,4940,2017,Fall,a,D +117,PHYS,2140,2016,Spring,b,D +118,CS,4000,2020,Fall,a,D +119,CS,4500,2016,Spring,b,D +119,MATH,1250,2018,Summer,b,D +119,PHYS,2040,2017,Fall,b,D +119,PHYS,2140,2020,Fall,a,D +120,BIOL,2420,2020,Spring,a,D +120,CS,3505,2020,Fall,c,D +120,MATH,2270,2017,Fall,c,D +121,PHYS,2220,2020,Summer,a,D +123,BIOL,2330,2016,Spring,a,D +123,PHYS,2140,2016,Spring,a,D +125,CS,4150,2020,Spring,a,D +129,BIOL,2325,2018,Fall,b,D +129,BIOL,2325,2018,Fall,c,D +131,CS,3500,2017,Fall,b,D +131,PHYS,2060,2018,Summer,a,D +132,BIOL,2420,2017,Summer,a,D +132,MATH,3220,2018,Spring,b,D +132,PHYS,2220,2018,Spring,a,D +133,MATH,1210,2019,Spring,a,D +134,BIOL,2010,2018,Spring,a,D +136,PHYS,2140,2020,Fall,a,D +138,BIOL,2330,2015,Fall,b,D +138,CS,2420,2015,Spring,a,D +138,CS,4500,2016,Spring,b,D +139,BIOL,2325,2019,Summer,a,D +143,CS,4400,2019,Summer,b,D +144,BIOL,2355,2016,Spring,a,D +146,BIOL,2010,2020,Summer,a,D +148,CS,4970,2020,Fall,c,D +151,CS,3200,2016,Fall,d,D +152,CS,4000,2020,Spring,b,D +152,PHYS,2040,2019,Spring,b,D +160,CS,2100,2016,Summer,b,D +162,CS,4500,2016,Spring,b,D +163,BIOL,2020,2018,Fall,a,D +163,BIOL,2355,2017,Spring,d,D +163,MATH,1220,2017,Spring,b,D +165,CS,3200,2018,Spring,a,D +169,MATH,1220,2018,Spring,b,D +170,BIOL,2010,2020,Summer,a,D +171,PHYS,2210,2019,Fall,b,D +172,CS,3100,2015,Summer,a,D +172,MATH,3210,2015,Fall,a,D +173,PHYS,2100,2017,Summer,c,D +173,PHYS,3210,2019,Spring,d,D +175,BIOL,1010,2020,Summer,b,D +176,MATH,2210,2017,Spring,a,D +177,CS,4500,2016,Fall,a,D +177,PHYS,2100,2017,Summer,a,D +178,BIOL,1010,2020,Summer,a,D +178,MATH,1220,2020,Spring,a,D +178,MATH,2280,2018,Fall,c,D +179,BIOL,2010,2020,Spring,b,D +179,BIOL,2021,2016,Fall,a,D +182,CS,3100,2016,Fall,a,D +182,MATH,1210,2016,Spring,c,D +183,CS,4400,2019,Fall,a,D +183,MATH,2280,2020,Spring,a,D +183,PHYS,3210,2020,Spring,a,D +185,BIOL,2355,2018,Summer,a,D +185,CS,4400,2020,Fall,b,D +185,MATH,2210,2018,Spring,b,D +185,PHYS,3220,2020,Spring,c,D +187,PHYS,2210,2019,Spring,d,D +188,BIOL,2030,2019,Summer,c,D +193,CS,4000,2015,Spring,a,D +194,BIOL,1006,2020,Spring,a,D +194,PHYS,2040,2020,Spring,a,D +197,BIOL,2010,2018,Spring,a,D +199,PHYS,2140,2018,Summer,b,D +199,PHYS,2210,2019,Spring,b,D +200,CS,4500,2020,Spring,a,D +203,CS,1410,2018,Spring,b,D +204,MATH,2280,2015,Summer,a,D +208,CS,2420,2017,Summer,c,D +208,PHYS,3210,2017,Summer,b,D +210,BIOL,1010,2015,Fall,a,D +214,PHYS,2040,2017,Fall,c,D +214,PHYS,3220,2017,Fall,a,D +216,MATH,3220,2016,Spring,c,D +219,CS,2420,2020,Summer,a,D +219,CS,4970,2020,Summer,b,D +220,CS,4500,2019,Fall,d,D +220,MATH,1210,2020,Spring,a,D +228,BIOL,2010,2020,Summer,a,D +228,BIOL,2010,2020,Summer,b,D +229,BIOL,1006,2017,Fall,b,D +230,BIOL,2420,2020,Spring,b,D +231,CS,2420,2020,Summer,a,D +231,PHYS,2220,2018,Summer,a,D +233,CS,4970,2020,Summer,c,D +235,BIOL,2010,2020,Summer,b,D +235,PHYS,2140,2020,Fall,a,D +238,CS,3505,2019,Summer,b,D +239,BIOL,2325,2018,Fall,c,D +240,BIOL,2330,2019,Fall,a,D +240,PHYS,2060,2020,Spring,b,D +241,BIOL,2030,2019,Summer,d,D +242,CS,3200,2020,Summer,a,D +244,BIOL,1010,2020,Summer,b,D +245,CS,1030,2016,Summer,a,D +245,CS,2420,2016,Fall,a,D +245,MATH,3220,2016,Fall,b,D +245,PHYS,2220,2016,Fall,a,D +246,BIOL,1006,2015,Summer,a,D +246,CS,3200,2016,Summer,b,D +247,PHYS,2060,2018,Fall,b,D +248,BIOL,1030,2019,Spring,c,D +252,MATH,1260,2017,Fall,a,D +253,PHYS,2140,2018,Fall,a,D +253,PHYS,2210,2019,Spring,c,D +254,CS,4970,2020,Summer,d,D +254,MATH,2270,2019,Fall,a,D +256,BIOL,1210,2019,Spring,a,D +256,CS,1030,2018,Fall,a,D +256,MATH,2210,2018,Spring,b,D +257,BIOL,2030,2017,Spring,a,D +257,BIOL,2210,2017,Summer,a,D +257,CS,2100,2018,Fall,b,D +257,CS,2100,2018,Fall,c,D +257,CS,3810,2018,Summer,c,D +257,CS,4400,2019,Spring,d,D +258,MATH,2270,2020,Spring,a,D +259,BIOL,2210,2017,Summer,b,D +259,CS,4400,2019,Fall,b,D +259,CS,4970,2019,Fall,c,D +260,CS,4500,2019,Fall,a,D +260,MATH,1220,2017,Spring,d,D +262,PHYS,3210,2017,Fall,a,D +270,CS,2100,2018,Summer,a,D +274,PHYS,2210,2018,Fall,b,D +274,PHYS,3210,2018,Spring,b,D +276,BIOL,2030,2018,Summer,b,D +276,PHYS,2220,2015,Fall,a,D +277,BIOL,2030,2016,Fall,a,D +277,CS,3500,2016,Spring,a,D +277,MATH,1250,2018,Summer,c,D +278,PHYS,2060,2016,Summer,a,D +284,CS,3505,2019,Fall,a,D +285,BIOL,2355,2017,Spring,d,D +285,CS,4940,2019,Fall,a,D +285,PHYS,2060,2016,Summer,b,D +288,BIOL,1006,2017,Fall,a,D +292,CS,3505,2019,Summer,d,D +294,MATH,1210,2019,Spring,a,D +297,BIOL,1006,2020,Fall,a,D +298,CS,3505,2019,Summer,b,D +298,PHYS,2060,2018,Fall,a,D +301,BIOL,1210,2016,Spring,a,D +303,CS,4500,2019,Fall,a,D +303,PHYS,2210,2019,Fall,d,D +304,PHYS,2210,2017,Summer,d,D +305,CS,3505,2018,Fall,a,D +307,CS,2100,2019,Spring,a,D +307,CS,4500,2016,Spring,b,D +307,MATH,3210,2015,Fall,a,D +309,PHYS,3210,2019,Summer,c,D +311,BIOL,2210,2018,Summer,b,D +312,BIOL,2010,2019,Fall,a,D +312,BIOL,2355,2017,Spring,a,D +312,CS,4400,2019,Spring,d,D +312,MATH,1250,2018,Summer,a,D +313,BIOL,2021,2019,Spring,b,D +313,BIOL,2210,2018,Summer,b,D +313,CS,4940,2020,Summer,a,D +313,MATH,1220,2016,Spring,a,D +320,BIOL,2030,2019,Summer,c,D +321,MATH,1260,2019,Spring,c,D +321,PHYS,2220,2015,Fall,a,D +325,MATH,2270,2019,Summer,b,D +325,PHYS,3220,2020,Spring,c,D +329,CS,2100,2018,Summer,c,D +329,CS,2420,2016,Fall,b,D +332,BIOL,1006,2019,Fall,b,D +332,PHYS,2220,2018,Fall,a,D +333,CS,4400,2019,Spring,a,D +335,BIOL,2355,2017,Fall,a,D +335,CS,1410,2016,Spring,a,D +335,CS,4500,2017,Summer,a,D +335,MATH,1210,2016,Spring,b,D +339,PHYS,3210,2020,Fall,a,D +341,BIOL,1030,2020,Spring,a,D +342,MATH,2210,2019,Spring,b,D +342,MATH,2270,2017,Fall,b,D +342,PHYS,2210,2017,Summer,c,D +344,CS,3810,2018,Summer,b,D +345,CS,4940,2020,Summer,b,D +345,MATH,1210,2017,Summer,a,D +345,MATH,2210,2020,Fall,a,D +347,CS,1030,2020,Fall,a,D +347,MATH,1260,2019,Spring,a,D +347,MATH,2210,2020,Spring,a,D +348,CS,3505,2015,Fall,d,D +348,CS,4150,2015,Summer,b,D +348,CS,4400,2020,Spring,a,D +348,CS,4970,2018,Summer,b,D +355,CS,3505,2017,Fall,a,D +358,MATH,1220,2019,Fall,c,D +364,BIOL,1006,2019,Fall,a,D +365,BIOL,1006,2020,Spring,a,D +366,CS,4500,2018,Spring,d,D +372,BIOL,1210,2017,Spring,a,D +373,CS,3505,2019,Summer,b,D +374,BIOL,2030,2017,Spring,a,D +375,CS,4000,2020,Spring,a,D +375,PHYS,2060,2019,Fall,c,D +377,CS,2420,2015,Spring,a,D +377,CS,3200,2017,Spring,a,D +377,PHYS,2060,2015,Spring,a,D +378,CS,2100,2017,Spring,a,D +379,BIOL,1010,2019,Spring,d,D +379,MATH,3220,2018,Spring,a,D +379,PHYS,2060,2016,Spring,a,D +379,PHYS,3210,2017,Summer,b,D +385,CS,3200,2016,Fall,d,D +386,BIOL,1030,2020,Summer,a,D +386,BIOL,2010,2020,Spring,a,D +386,CS,2100,2019,Fall,a,D +386,CS,4000,2020,Spring,a,D +386,CS,4940,2020,Summer,a,D +387,BIOL,2210,2017,Summer,b,D +387,BIOL,2330,2017,Fall,a,D +391,MATH,2270,2020,Fall,b,D +392,CS,1410,2018,Spring,d,D +392,PHYS,2140,2016,Summer,b,D +393,BIOL,2325,2018,Summer,a,D +393,CS,2100,2018,Summer,c,D +393,MATH,2280,2016,Fall,a,D +393,PHYS,2060,2016,Summer,a,D +397,BIOL,2355,2017,Spring,c,D +397,MATH,2270,2017,Fall,d,D +397,PHYS,2060,2019,Summer,b,D +100,PHYS,2060,2019,Fall,c,D+ +102,BIOL,2355,2017,Spring,d,D+ +102,CS,4970,2018,Fall,d,D+ +102,PHYS,2210,2019,Spring,c,D+ +102,PHYS,3210,2018,Spring,c,D+ +105,BIOL,2355,2017,Spring,a,D+ +107,CS,1410,2018,Spring,b,D+ +107,CS,2420,2018,Spring,a,D+ +107,CS,4500,2019,Summer,a,D+ +109,CS,1410,2018,Spring,c,D+ +109,MATH,3210,2020,Fall,a,D+ +113,BIOL,2010,2020,Spring,a,D+ +113,MATH,1260,2019,Summer,a,D+ +118,BIOL,1006,2020,Fall,c,D+ +119,BIOL,2420,2016,Spring,a,D+ +119,PHYS,2210,2019,Spring,a,D+ +119,PHYS,2220,2017,Spring,a,D+ +120,BIOL,2325,2019,Summer,a,D+ +120,CS,1030,2020,Spring,c,D+ +120,CS,2100,2019,Fall,d,D+ +120,MATH,2280,2018,Fall,c,D+ +121,CS,3505,2020,Fall,b,D+ +122,BIOL,2355,2020,Summer,a,D+ +122,BIOL,2420,2020,Fall,a,D+ +124,CS,3500,2020,Summer,a,D+ +124,CS,3505,2017,Fall,a,D+ +125,PHYS,2040,2020,Spring,a,D+ +128,CS,4400,2019,Spring,b,D+ +128,MATH,2270,2017,Fall,d,D+ +128,PHYS,2140,2018,Summer,a,D+ +128,PHYS,3220,2018,Summer,a,D+ +129,MATH,3220,2018,Spring,d,D+ +130,MATH,2270,2020,Fall,a,D+ +131,BIOL,2030,2019,Summer,d,D+ +131,MATH,1220,2018,Spring,b,D+ +131,PHYS,2040,2020,Spring,a,D+ +132,MATH,2270,2017,Summer,a,D+ +132,PHYS,2040,2017,Fall,c,D+ +135,CS,4970,2019,Summer,d,D+ +135,MATH,1210,2020,Spring,a,D+ +135,MATH,1250,2020,Summer,a,D+ +138,CS,3100,2016,Spring,b,D+ +138,CS,3200,2015,Fall,a,D+ +138,MATH,1250,2016,Fall,b,D+ +139,BIOL,1030,2019,Spring,b,D+ +139,CS,3200,2019,Spring,a,D+ +139,PHYS,3220,2017,Summer,a,D+ +142,BIOL,2355,2020,Fall,a,D+ +143,BIOL,1030,2019,Spring,c,D+ +143,BIOL,2210,2018,Summer,a,D+ +144,BIOL,1210,2016,Spring,a,D+ +146,CS,4970,2020,Summer,d,D+ +146,PHYS,3210,2019,Summer,a,D+ +149,CS,1030,2016,Spring,a,D+ +156,PHYS,2060,2018,Fall,a,D+ +162,MATH,1260,2015,Summer,a,D+ +163,PHYS,2220,2017,Spring,d,D+ +164,BIOL,2210,2017,Summer,b,D+ +164,CS,2100,2018,Fall,d,D+ +164,CS,4970,2019,Summer,b,D+ +164,MATH,1220,2020,Summer,a,D+ +165,CS,4970,2019,Spring,a,D+ +167,CS,2420,2020,Summer,a,D+ +172,CS,1410,2015,Summer,d,D+ +173,BIOL,2210,2018,Summer,c,D+ +173,CS,4970,2019,Summer,c,D+ +175,MATH,2270,2020,Spring,a,D+ +177,PHYS,2040,2015,Spring,a,D+ +177,PHYS,2060,2019,Fall,c,D+ +178,BIOL,2021,2019,Spring,b,D+ +178,CS,3505,2019,Fall,a,D+ +179,BIOL,1010,2015,Fall,a,D+ +179,CS,4150,2020,Spring,a,D+ +179,PHYS,3210,2017,Summer,a,D+ +182,BIOL,2010,2015,Summer,a,D+ +182,BIOL,2355,2017,Fall,b,D+ +183,MATH,2270,2019,Summer,c,D+ +185,CS,4970,2018,Summer,c,D+ +185,PHYS,3220,2020,Spring,d,D+ +187,BIOL,2355,2018,Summer,b,D+ +192,BIOL,2420,2015,Spring,d,D+ +192,MATH,3220,2016,Spring,d,D+ +192,PHYS,2140,2015,Spring,b,D+ +194,PHYS,2060,2019,Summer,b,D+ +199,CS,3505,2017,Fall,a,D+ +204,CS,4150,2015,Summer,a,D+ +208,CS,3505,2017,Fall,b,D+ +209,PHYS,3210,2018,Spring,c,D+ +210,BIOL,2210,2018,Summer,c,D+ +210,BIOL,2330,2020,Spring,a,D+ +210,CS,3810,2018,Summer,d,D+ +211,CS,3505,2015,Fall,a,D+ +213,CS,3810,2016,Fall,a,D+ +214,BIOL,2030,2018,Summer,b,D+ +214,BIOL,2330,2017,Summer,a,D+ +214,PHYS,2220,2018,Spring,a,D+ +215,PHYS,3220,2017,Summer,a,D+ +217,CS,2100,2018,Fall,c,D+ +220,BIOL,1210,2018,Fall,a,D+ +220,BIOL,2210,2020,Fall,a,D+ +227,BIOL,2355,2018,Summer,a,D+ +227,MATH,3210,2020,Fall,a,D+ +228,CS,3505,2019,Spring,a,D+ +228,MATH,1220,2020,Spring,a,D+ +230,BIOL,2210,2018,Summer,b,D+ +230,MATH,1210,2017,Summer,b,D+ +231,CS,4400,2017,Spring,b,D+ +231,PHYS,2060,2018,Fall,d,D+ +233,CS,3810,2020,Fall,a,D+ +238,CS,2100,2019,Summer,b,D+ +239,BIOL,1010,2018,Fall,a,D+ +240,CS,4500,2020,Summer,a,D+ +241,BIOL,1006,2020,Fall,a,D+ +241,PHYS,2210,2019,Fall,a,D+ +247,MATH,1220,2019,Fall,c,D+ +249,BIOL,2021,2015,Summer,b,D+ +251,CS,4940,2020,Summer,b,D+ +254,BIOL,1030,2020,Spring,a,D+ +254,MATH,1220,2019,Fall,a,D+ +255,BIOL,1006,2020,Fall,b,D+ +255,BIOL,1210,2018,Fall,a,D+ +255,CS,4970,2018,Fall,d,D+ +255,MATH,1210,2019,Summer,a,D+ +255,MATH,3210,2020,Fall,a,D+ +255,PHYS,2060,2020,Spring,a,D+ +255,PHYS,2210,2019,Spring,a,D+ +255,PHYS,3220,2020,Spring,b,D+ +256,CS,2100,2017,Spring,a,D+ +256,CS,3505,2018,Fall,a,D+ +256,PHYS,2210,2019,Summer,a,D+ +257,CS,4970,2020,Summer,a,D+ +259,CS,2100,2018,Fall,d,D+ +259,MATH,1220,2017,Spring,b,D+ +260,CS,1030,2019,Fall,a,D+ +260,CS,3500,2020,Summer,a,D+ +260,MATH,1260,2017,Fall,a,D+ +262,BIOL,1006,2017,Fall,a,D+ +262,BIOL,2355,2018,Summer,a,D+ +262,CS,4000,2017,Fall,a,D+ +264,CS,3810,2016,Fall,b,D+ +264,CS,4150,2016,Summer,b,D+ +264,MATH,3220,2016,Fall,b,D+ +267,CS,4970,2018,Fall,d,D+ +270,BIOL,2325,2019,Spring,b,D+ +270,CS,3505,2019,Summer,d,D+ +270,MATH,1210,2016,Spring,a,D+ +270,MATH,3210,2019,Spring,a,D+ +273,CS,1410,2016,Spring,b,D+ +275,CS,3500,2017,Fall,c,D+ +276,BIOL,2355,2018,Summer,d,D+ +276,CS,4400,2017,Spring,c,D+ +276,MATH,2280,2015,Fall,a,D+ +276,PHYS,3220,2017,Fall,d,D+ +277,BIOL,1006,2020,Fall,b,D+ +277,CS,3505,2020,Spring,a,D+ +277,MATH,2270,2017,Summer,a,D+ +285,BIOL,2325,2019,Summer,a,D+ +285,CS,2100,2018,Summer,b,D+ +285,CS,3810,2016,Fall,a,D+ +285,MATH,2210,2019,Spring,a,D+ +288,CS,4970,2017,Summer,a,D+ +288,PHYS,2100,2018,Fall,a,D+ +288,PHYS,2220,2017,Spring,d,D+ +289,BIOL,2020,2019,Summer,a,D+ +289,CS,2100,2020,Fall,a,D+ +289,CS,3505,2019,Fall,b,D+ +290,CS,1030,2016,Spring,a,D+ +291,BIOL,2330,2016,Fall,a,D+ +291,MATH,1260,2017,Fall,a,D+ +292,BIOL,2325,2019,Spring,a,D+ +292,BIOL,2420,2020,Summer,a,D+ +292,CS,4000,2020,Fall,a,D+ +292,MATH,2280,2019,Fall,c,D+ +292,PHYS,2040,2017,Summer,a,D+ +294,BIOL,1006,2018,Spring,b,D+ +294,CS,4400,2019,Summer,a,D+ +295,PHYS,2040,2015,Fall,b,D+ +296,CS,4500,2019,Fall,d,D+ +298,BIOL,2210,2018,Summer,a,D+ +298,PHYS,2140,2018,Summer,a,D+ +299,BIOL,2355,2017,Spring,c,D+ +300,BIOL,2030,2019,Summer,d,D+ +302,CS,3100,2015,Summer,a,D+ +303,BIOL,1006,2019,Summer,a,D+ +305,CS,4500,2018,Spring,b,D+ +305,MATH,2210,2019,Spring,b,D+ +305,PHYS,2040,2019,Spring,b,D+ +305,PHYS,2140,2018,Summer,b,D+ +305,PHYS,2210,2019,Spring,a,D+ +307,PHYS,2100,2017,Summer,b,D+ +309,BIOL,2010,2019,Fall,a,D+ +309,CS,4000,2020,Spring,b,D+ +309,CS,4970,2020,Summer,b,D+ +310,MATH,1210,2020,Spring,a,D+ +311,CS,3500,2017,Summer,a,D+ +312,BIOL,2210,2016,Summer,a,D+ +312,CS,2420,2016,Spring,a,D+ +312,CS,3200,2015,Fall,c,D+ +312,MATH,3220,2018,Spring,b,D+ +312,PHYS,3220,2016,Summer,a,D+ +313,BIOL,1030,2017,Spring,a,D+ +313,CS,4970,2016,Fall,b,D+ +313,PHYS,2060,2019,Summer,b,D+ +314,CS,3100,2016,Fall,a,D+ +318,BIOL,1006,2017,Fall,b,D+ +318,BIOL,2021,2018,Summer,a,D+ +318,CS,4000,2017,Summer,a,D+ +321,BIOL,1006,2017,Fall,a,D+ +321,BIOL,1010,2017,Summer,a,D+ +321,CS,3505,2017,Fall,b,D+ +321,CS,4940,2020,Summer,b,D+ +323,MATH,1220,2019,Fall,a,D+ +325,CS,4970,2019,Summer,b,D+ +326,CS,4940,2017,Fall,a,D+ +326,PHYS,2210,2017,Summer,b,D+ +329,BIOL,1010,2018,Summer,c,D+ +329,BIOL,2355,2017,Spring,d,D+ +329,BIOL,2420,2020,Fall,a,D+ +329,CS,4970,2019,Summer,d,D+ +329,PHYS,2060,2018,Fall,c,D+ +331,CS,3500,2020,Summer,a,D+ +331,CS,4940,2020,Summer,a,D+ +332,BIOL,2020,2018,Spring,a,D+ +332,MATH,1250,2018,Summer,a,D+ +333,BIOL,1030,2019,Spring,a,D+ +333,CS,4500,2019,Summer,a,D+ +335,CS,3500,2017,Fall,a,D+ +339,CS,4150,2020,Fall,a,D+ +342,CS,3200,2020,Spring,a,D+ +345,CS,4000,2017,Fall,b,D+ +345,PHYS,3210,2019,Summer,c,D+ +347,BIOL,2355,2018,Summer,d,D+ +347,CS,3810,2020,Fall,a,D+ +355,PHYS,2220,2017,Spring,b,D+ +356,BIOL,2210,2016,Summer,a,D+ +356,CS,3810,2018,Summer,d,D+ +356,CS,4970,2018,Fall,d,D+ +356,PHYS,3210,2019,Spring,a,D+ +361,CS,4000,2017,Fall,a,D+ +361,MATH,1260,2017,Summer,a,D+ +362,BIOL,1010,2018,Fall,a,D+ +363,BIOL,2420,2020,Summer,a,D+ +365,CS,2420,2020,Summer,a,D+ +366,BIOL,1006,2018,Spring,a,D+ +369,CS,4500,2018,Spring,c,D+ +371,BIOL,1006,2020,Fall,b,D+ +371,CS,3505,2018,Fall,a,D+ +372,CS,2420,2017,Summer,c,D+ +373,MATH,1250,2016,Summer,a,D+ +373,MATH,3220,2016,Spring,a,D+ +373,PHYS,3220,2020,Spring,d,D+ +374,BIOL,2355,2017,Spring,d,D+ +375,MATH,2210,2019,Spring,b,D+ +377,CS,3500,2019,Fall,a,D+ +377,PHYS,2140,2019,Fall,b,D+ +378,BIOL,2021,2018,Summer,a,D+ +378,BIOL,2355,2017,Spring,d,D+ +378,MATH,2210,2020,Fall,a,D+ +379,BIOL,2325,2015,Fall,b,D+ +379,CS,2100,2019,Spring,a,D+ +379,CS,4400,2019,Spring,a,D+ +379,MATH,1210,2016,Fall,c,D+ +380,CS,3505,2019,Summer,b,D+ +386,MATH,1210,2020,Spring,b,D+ +386,MATH,3210,2020,Fall,a,D+ +387,BIOL,1030,2018,Fall,a,D+ +387,BIOL,2355,2018,Summer,d,D+ +387,CS,2420,2017,Fall,a,D+ +388,CS,1410,2018,Spring,c,D+ +389,PHYS,2040,2016,Spring,a,D+ +390,BIOL,2355,2020,Fall,a,D+ +391,CS,4500,2018,Spring,d,D+ +391,PHYS,2210,2019,Spring,c,D+ +392,BIOL,2020,2015,Fall,b,D+ +392,CS,2420,2016,Fall,a,D+ +394,BIOL,1006,2015,Spring,b,D+ +397,BIOL,2021,2018,Fall,b,D+ +397,CS,2420,2016,Fall,c,D+ +397,CS,3100,2017,Fall,a,D+ +397,CS,4500,2020,Summer,a,D+ +397,CS,4940,2020,Summer,b,D+ +398,CS,4500,2019,Fall,b,D+ +399,PHYS,2040,2019,Spring,a,D+ +100,BIOL,2030,2019,Summer,b,F +100,CS,4940,2020,Summer,b,F +101,CS,4500,2018,Spring,a,F +101,MATH,3220,2018,Spring,b,F +101,PHYS,3210,2018,Fall,a,F +102,CS,3810,2019,Fall,b,F +102,MATH,3210,2016,Fall,a,F +104,MATH,1210,2018,Fall,b,F +106,BIOL,2355,2020,Summer,b,F +106,PHYS,2060,2019,Summer,b,F +107,MATH,1210,2016,Fall,c,F +108,CS,3500,2019,Fall,b,F +112,PHYS,2060,2020,Fall,a,F +113,BIOL,1010,2020,Summer,a,F +113,MATH,3210,2020,Summer,a,F +115,BIOL,2210,2017,Spring,a,F +116,CS,3505,2016,Fall,b,F +117,BIOL,1210,2017,Spring,a,F +119,BIOL,2210,2019,Summer,b,F +119,BIOL,2325,2018,Spring,a,F +119,CS,1030,2020,Fall,a,F +119,MATH,2270,2020,Fall,b,F +120,BIOL,1030,2016,Fall,a,F +120,BIOL,2330,2016,Spring,a,F +120,CS,1410,2018,Spring,a,F +120,CS,3200,2016,Fall,a,F +120,MATH,1260,2019,Summer,b,F +121,CS,1410,2020,Spring,a,F +121,CS,4970,2018,Fall,c,F +122,CS,1410,2020,Spring,a,F +123,CS,4400,2020,Fall,a,F +127,CS,4400,2020,Fall,b,F +127,CS,4500,2020,Summer,a,F +128,BIOL,1030,2019,Summer,a,F +128,CS,4500,2018,Spring,d,F +129,BIOL,2355,2018,Summer,c,F +129,PHYS,3210,2020,Fall,b,F +131,BIOL,1030,2020,Summer,a,F +131,BIOL,2210,2018,Summer,a,F +131,BIOL,2325,2018,Fall,b,F +131,MATH,1260,2019,Summer,a,F +131,PHYS,3210,2020,Spring,a,F +132,BIOL,2030,2018,Summer,b,F +133,PHYS,3210,2019,Summer,b,F +137,BIOL,2355,2020,Summer,b,F +139,BIOL,1010,2019,Spring,a,F +139,BIOL,2020,2018,Spring,a,F +139,CS,2100,2018,Summer,c,F +142,CS,3505,2020,Fall,a,F +142,CS,4500,2020,Spring,a,F +143,BIOL,2030,2019,Summer,d,F +143,PHYS,2210,2019,Fall,a,F +146,CS,3505,2020,Spring,a,F +146,MATH,2280,2019,Fall,c,F +149,CS,4500,2016,Spring,b,F +149,MATH,1250,2015,Fall,a,F +151,BIOL,2210,2017,Summer,a,F +152,MATH,2270,2020,Fall,a,F +158,BIOL,2020,2018,Fall,a,F +158,PHYS,2100,2018,Fall,a,F +162,CS,1030,2016,Spring,a,F +162,CS,3505,2015,Fall,c,F +163,BIOL,2030,2016,Summer,b,F +163,PHYS,2140,2018,Fall,a,F +164,PHYS,2220,2020,Summer,b,F +167,BIOL,2010,2020,Summer,a,F +167,CS,4940,2019,Fall,a,F +167,PHYS,2060,2020,Spring,a,F +167,PHYS,2140,2019,Fall,a,F +169,BIOL,1006,2019,Fall,b,F +169,BIOL,2355,2018,Spring,a,F +175,PHYS,2220,2020,Spring,a,F +177,BIOL,2210,2017,Spring,c,F +177,CS,2100,2020,Fall,a,F +177,CS,4940,2020,Summer,b,F +177,MATH,1210,2018,Fall,a,F +178,BIOL,1006,2019,Summer,a,F +178,CS,4970,2018,Fall,d,F +178,PHYS,2220,2018,Fall,a,F +179,BIOL,1006,2016,Summer,b,F +179,BIOL,2030,2017,Spring,a,F +179,CS,3505,2018,Fall,a,F +181,CS,1030,2020,Fall,a,F +182,BIOL,1006,2015,Summer,a,F +182,BIOL,1210,2016,Spring,a,F +182,CS,1410,2015,Summer,c,F +182,CS,2100,2018,Summer,b,F +185,BIOL,2420,2018,Spring,a,F +185,CS,4000,2020,Fall,a,F +187,CS,3200,2020,Spring,b,F +192,MATH,3210,2015,Fall,c,F +194,CS,2100,2019,Summer,b,F +195,BIOL,1010,2016,Summer,a,F +195,CS,2420,2016,Summer,a,F +195,PHYS,3220,2016,Summer,a,F +197,BIOL,1030,2018,Summer,a,F +199,BIOL,2030,2020,Spring,b,F +199,CS,3505,2017,Fall,b,F +199,CS,4400,2020,Spring,a,F +200,CS,1410,2020,Spring,b,F +200,MATH,2210,2020,Spring,b,F +207,BIOL,2420,2017,Summer,b,F +210,BIOL,2010,2018,Spring,a,F +210,CS,3100,2017,Spring,a,F +210,CS,4150,2019,Spring,a,F +211,BIOL,1010,2015,Fall,c,F +211,BIOL,1030,2015,Spring,c,F +211,MATH,1250,2015,Spring,c,F +213,PHYS,3220,2017,Fall,c,F +220,BIOL,2355,2020,Fall,a,F +220,CS,3505,2019,Summer,a,F +221,BIOL,2355,2020,Summer,a,F +221,CS,4970,2020,Summer,b,F +223,MATH,3210,2019,Fall,a,F +229,PHYS,3210,2018,Spring,a,F +230,BIOL,1210,2019,Spring,a,F +230,MATH,2280,2018,Spring,a,F +231,BIOL,1030,2019,Spring,d,F +231,MATH,1210,2018,Fall,a,F +231,PHYS,2140,2018,Summer,a,F +237,MATH,3220,2018,Spring,a,F +238,BIOL,1010,2018,Summer,b,F +240,CS,2100,2019,Spring,a,F +243,BIOL,2021,2016,Fall,a,F +246,BIOL,1030,2015,Summer,a,F +247,CS,1030,2019,Fall,b,F +247,CS,3500,2020,Summer,a,F +247,CS,3505,2018,Summer,b,F +248,BIOL,2420,2020,Spring,a,F +250,PHYS,2060,2020,Fall,a,F +252,BIOL,1010,2018,Fall,a,F +252,CS,4000,2017,Fall,a,F +255,BIOL,2355,2019,Spring,c,F +255,BIOL,2420,2020,Fall,a,F +255,CS,3505,2018,Summer,a,F +255,MATH,2280,2020,Spring,a,F +256,BIOL,2021,2018,Fall,a,F +256,MATH,3210,2020,Fall,a,F +257,MATH,1210,2018,Summer,a,F +257,PHYS,2210,2019,Spring,d,F +258,CS,2100,2018,Summer,c,F +259,BIOL,1010,2018,Summer,c,F +259,PHYS,2140,2017,Fall,a,F +260,BIOL,2020,2018,Fall,b,F +260,CS,4940,2017,Fall,a,F +260,PHYS,3220,2018,Summer,a,F +261,MATH,1250,2018,Summer,c,F +261,PHYS,2210,2017,Summer,d,F +262,CS,1030,2016,Fall,a,F +267,CS,1410,2020,Spring,a,F +267,CS,4970,2018,Fall,b,F +268,BIOL,2021,2016,Fall,a,F +270,BIOL,1010,2020,Summer,b,F +270,BIOL,2030,2019,Summer,b,F +270,BIOL,2030,2019,Summer,c,F +270,CS,2420,2016,Fall,c,F +272,CS,4400,2020,Fall,a,F +274,CS,4970,2018,Fall,a,F +276,BIOL,1010,2015,Summer,a,F +276,BIOL,2355,2018,Summer,b,F +276,CS,3500,2019,Summer,a,F +276,CS,4970,2016,Fall,a,F +276,MATH,1250,2015,Spring,c,F +278,BIOL,1010,2017,Spring,a,F +278,MATH,3220,2016,Summer,a,F +280,MATH,1220,2015,Summer,b,F +281,CS,3500,2020,Summer,a,F +282,CS,3200,2015,Fall,d,F +282,CS,3500,2016,Summer,a,F +282,PHYS,2220,2015,Fall,b,F +285,CS,4500,2016,Spring,b,F +289,CS,4970,2019,Summer,a,F +290,BIOL,2325,2015,Fall,b,F +290,CS,4500,2015,Summer,b,F +290,MATH,3220,2016,Spring,d,F +292,BIOL,1010,2018,Summer,b,F +292,BIOL,2355,2018,Fall,a,F +292,CS,2100,2018,Fall,a,F +293,PHYS,2220,2020,Summer,a,F +299,MATH,1220,2017,Spring,a,F +301,CS,1410,2015,Summer,d,F +303,CS,3200,2020,Spring,a,F +303,MATH,2270,2019,Summer,b,F +303,PHYS,3220,2020,Spring,d,F +304,BIOL,2010,2017,Fall,a,F +304,MATH,1260,2017,Summer,a,F +307,CS,4000,2015,Fall,a,F +309,CS,3505,2019,Fall,b,F +309,CS,4400,2017,Spring,b,F +311,BIOL,2010,2020,Summer,a,F +311,MATH,1210,2018,Fall,a,F +311,PHYS,2060,2018,Fall,b,F +312,BIOL,2030,2019,Summer,a,F +312,MATH,1210,2018,Fall,a,F +312,PHYS,2040,2015,Fall,b,F +313,CS,3200,2016,Fall,b,F +313,MATH,2280,2020,Spring,a,F +313,PHYS,2040,2020,Spring,a,F +314,PHYS,3210,2016,Summer,b,F +320,CS,3505,2019,Spring,a,F +329,BIOL,1030,2016,Summer,a,F +329,BIOL,2210,2019,Summer,a,F +329,BIOL,2325,2019,Spring,a,F +329,CS,4150,2020,Fall,a,F +329,PHYS,2140,2019,Fall,a,F +332,CS,3505,2020,Spring,a,F +333,BIOL,2420,2020,Summer,a,F +335,BIOL,2330,2016,Fall,a,F +339,CS,4940,2020,Summer,b,F +339,PHYS,2220,2020,Summer,a,F +340,BIOL,1006,2020,Spring,a,F +340,CS,4500,2019,Summer,a,F +341,MATH,1220,2019,Fall,a,F +342,MATH,1210,2017,Summer,c,F +344,CS,2100,2018,Fall,b,F +345,CS,3200,2020,Fall,a,F +345,MATH,2280,2018,Fall,c,F +345,MATH,3220,2018,Spring,a,F +347,PHYS,2210,2019,Fall,c,F +348,PHYS,2060,2016,Summer,b,F +353,CS,2420,2017,Summer,a,F +355,BIOL,2010,2017,Fall,a,F +355,CS,3100,2017,Spring,b,F +355,PHYS,2100,2017,Summer,c,F +356,BIOL,1210,2019,Spring,a,F +358,CS,3505,2019,Spring,b,F +359,PHYS,2210,2019,Summer,a,F +361,CS,2420,2017,Summer,a,F +362,BIOL,2355,2020,Spring,a,F +363,CS,3500,2019,Fall,c,F +365,PHYS,3220,2020,Spring,c,F +366,PHYS,2060,2019,Summer,b,F +366,PHYS,2100,2019,Summer,a,F +368,BIOL,2210,2019,Summer,a,F +368,CS,4970,2019,Summer,a,F +368,MATH,1210,2018,Summer,a,F +371,MATH,2270,2020,Fall,b,F +372,MATH,1250,2018,Summer,a,F +373,BIOL,1010,2017,Spring,a,F +373,BIOL,2020,2018,Fall,b,F +377,PHYS,3210,2017,Fall,a,F +378,CS,4940,2020,Summer,a,F +379,BIOL,2355,2018,Summer,d,F +384,BIOL,1030,2019,Spring,d,F +385,MATH,1210,2016,Fall,d,F +386,CS,3505,2018,Summer,a,F +386,PHYS,2100,2019,Summer,a,F +386,PHYS,2220,2020,Fall,a,F +387,BIOL,2325,2017,Fall,b,F +387,MATH,1210,2017,Summer,c,F +389,CS,1410,2016,Summer,a,F +390,MATH,1210,2020,Spring,a,F +391,BIOL,2330,2017,Fall,a,F +391,CS,1030,2020,Spring,b,F +391,CS,4970,2019,Summer,b,F +391,MATH,1250,2020,Summer,a,F +391,MATH,2270,2020,Fall,a,F +391,PHYS,2220,2016,Summer,a,F +392,MATH,3220,2016,Spring,b,F +392,PHYS,2100,2016,Fall,a,F +393,CS,1030,2016,Summer,a,F +396,MATH,1220,2019,Fall,c,F +397,BIOL,2210,2017,Spring,c,F +399,BIOL,1010,2018,Summer,a,F diff --git a/tests/data/Section.csv b/tests/data/Section.csv new file mode 100644 index 000000000..8dc95361b --- /dev/null +++ b/tests/data/Section.csv @@ -0,0 +1,757 @@ +dept,course,term_year,term,section,auditorium +BIOL,1006,2015,Spring,a,C68 +BIOL,1006,2015,Spring,b,C22 +BIOL,1006,2015,Summer,a,D38 +BIOL,1006,2015,Summer,b,C15 +BIOL,1006,2016,Spring,a,B87 +BIOL,1006,2016,Spring,b,D72 +BIOL,1006,2016,Summer,a,A34 +BIOL,1006,2016,Summer,b,D48 +BIOL,1006,2016,Summer,c,F34 +BIOL,1006,2016,Summer,d,F48 +BIOL,1006,2017,Fall,a,E42 +BIOL,1006,2017,Fall,b,B83 +BIOL,1006,2018,Spring,a,F39 +BIOL,1006,2018,Spring,b,A18 +BIOL,1006,2018,Fall,a,A13 +BIOL,1006,2019,Spring,a,D59 +BIOL,1006,2019,Summer,a,F70 +BIOL,1006,2019,Fall,a,B54 +BIOL,1006,2019,Fall,b,D79 +BIOL,1006,2020,Spring,a,A89 +BIOL,1006,2020,Fall,a,C13 +BIOL,1006,2020,Fall,b,C70 +BIOL,1006,2020,Fall,c,F46 +BIOL,1010,2015,Summer,a,D12 +BIOL,1010,2015,Summer,b,F82 +BIOL,1010,2015,Summer,c,A7 +BIOL,1010,2015,Summer,d,B17 +BIOL,1010,2015,Fall,a,B9 +BIOL,1010,2015,Fall,b,E27 +BIOL,1010,2015,Fall,c,B43 +BIOL,1010,2015,Fall,d,E1 +BIOL,1010,2016,Summer,a,B70 +BIOL,1010,2017,Spring,a,A17 +BIOL,1010,2017,Summer,a,B76 +BIOL,1010,2018,Summer,a,E15 +BIOL,1010,2018,Summer,b,D58 +BIOL,1010,2018,Summer,c,E76 +BIOL,1010,2018,Fall,a,E6 +BIOL,1010,2018,Fall,b,F67 +BIOL,1010,2019,Spring,a,A8 +BIOL,1010,2019,Spring,b,D55 +BIOL,1010,2019,Spring,c,D92 +BIOL,1010,2019,Spring,d,A11 +BIOL,1010,2020,Summer,a,E71 +BIOL,1010,2020,Summer,b,D77 +BIOL,1010,2020,Summer,c,D65 +BIOL,1010,2020,Summer,d,A90 +BIOL,1030,2015,Spring,a,E93 +BIOL,1030,2015,Spring,b,D58 +BIOL,1030,2015,Spring,c,D44 +BIOL,1030,2015,Spring,d,D54 +BIOL,1030,2015,Summer,a,C55 +BIOL,1030,2016,Spring,a,F61 +BIOL,1030,2016,Summer,a,A56 +BIOL,1030,2016,Fall,a,B72 +BIOL,1030,2017,Spring,a,E43 +BIOL,1030,2017,Spring,b,D46 +BIOL,1030,2017,Spring,c,D93 +BIOL,1030,2018,Summer,a,B85 +BIOL,1030,2018,Fall,a,C72 +BIOL,1030,2019,Spring,a,E29 +BIOL,1030,2019,Spring,b,E99 +BIOL,1030,2019,Spring,c,E87 +BIOL,1030,2019,Spring,d,A78 +BIOL,1030,2019,Summer,a,F35 +BIOL,1030,2020,Spring,a,C45 +BIOL,1030,2020,Summer,a,E85 +BIOL,1210,2015,Spring,a,A12 +BIOL,1210,2015,Spring,b,B49 +BIOL,1210,2016,Spring,a,E77 +BIOL,1210,2017,Spring,a,F11 +BIOL,1210,2017,Summer,a,D78 +BIOL,1210,2018,Spring,a,A45 +BIOL,1210,2018,Fall,a,D68 +BIOL,1210,2018,Fall,b,A29 +BIOL,1210,2019,Spring,a,A27 +BIOL,2010,2015,Spring,a,B17 +BIOL,2010,2015,Summer,a,E72 +BIOL,2010,2015,Summer,b,C10 +BIOL,2010,2015,Fall,a,D3 +BIOL,2010,2017,Summer,a,C15 +BIOL,2010,2017,Fall,a,B80 +BIOL,2010,2018,Spring,a,C12 +BIOL,2010,2019,Fall,a,F44 +BIOL,2010,2020,Spring,a,A66 +BIOL,2010,2020,Spring,b,E66 +BIOL,2010,2020,Summer,a,C94 +BIOL,2010,2020,Summer,b,F19 +BIOL,2020,2015,Summer,a,F10 +BIOL,2020,2015,Fall,a,D60 +BIOL,2020,2015,Fall,b,E58 +BIOL,2020,2015,Fall,c,E83 +BIOL,2020,2015,Fall,d,E42 +BIOL,2020,2016,Spring,a,F41 +BIOL,2020,2018,Spring,a,C60 +BIOL,2020,2018,Fall,a,A83 +BIOL,2020,2018,Fall,b,A79 +BIOL,2020,2018,Fall,c,D60 +BIOL,2020,2018,Fall,d,F6 +BIOL,2020,2019,Summer,a,F25 +BIOL,2021,2015,Spring,a,C92 +BIOL,2021,2015,Summer,a,A32 +BIOL,2021,2015,Summer,b,D68 +BIOL,2021,2015,Summer,c,B47 +BIOL,2021,2016,Fall,a,F83 +BIOL,2021,2017,Summer,a,D37 +BIOL,2021,2017,Fall,a,E20 +BIOL,2021,2018,Spring,a,B45 +BIOL,2021,2018,Summer,a,F51 +BIOL,2021,2018,Fall,a,A40 +BIOL,2021,2018,Fall,b,F43 +BIOL,2021,2018,Fall,c,F90 +BIOL,2021,2018,Fall,d,F88 +BIOL,2021,2019,Spring,a,A83 +BIOL,2021,2019,Spring,b,E47 +BIOL,2021,2019,Fall,a,C99 +BIOL,2030,2015,Spring,a,A65 +BIOL,2030,2015,Spring,b,F68 +BIOL,2030,2015,Fall,a,B77 +BIOL,2030,2016,Summer,a,E22 +BIOL,2030,2016,Summer,b,A53 +BIOL,2030,2016,Fall,a,D79 +BIOL,2030,2017,Spring,a,D30 +BIOL,2030,2017,Spring,b,C61 +BIOL,2030,2017,Spring,c,B48 +BIOL,2030,2017,Spring,d,E57 +BIOL,2030,2018,Summer,a,B26 +BIOL,2030,2018,Summer,b,B33 +BIOL,2030,2019,Summer,a,F67 +BIOL,2030,2019,Summer,b,C11 +BIOL,2030,2019,Summer,c,C58 +BIOL,2030,2019,Summer,d,B56 +BIOL,2030,2020,Spring,a,D45 +BIOL,2030,2020,Spring,b,D7 +BIOL,2210,2016,Summer,a,C19 +BIOL,2210,2017,Spring,a,F18 +BIOL,2210,2017,Spring,b,D58 +BIOL,2210,2017,Spring,c,A3 +BIOL,2210,2017,Summer,a,E94 +BIOL,2210,2017,Summer,b,D15 +BIOL,2210,2017,Summer,c,B39 +BIOL,2210,2018,Spring,a,E59 +BIOL,2210,2018,Summer,a,D77 +BIOL,2210,2018,Summer,b,F66 +BIOL,2210,2018,Summer,c,F19 +BIOL,2210,2019,Summer,a,B86 +BIOL,2210,2019,Summer,b,E47 +BIOL,2210,2019,Fall,a,E65 +BIOL,2210,2019,Fall,b,D61 +BIOL,2210,2020,Fall,a,C9 +BIOL,2325,2015,Spring,a,F14 +BIOL,2325,2015,Spring,b,F97 +BIOL,2325,2015,Fall,a,F23 +BIOL,2325,2015,Fall,b,F60 +BIOL,2325,2015,Fall,c,D81 +BIOL,2325,2016,Summer,a,D5 +BIOL,2325,2017,Fall,a,E51 +BIOL,2325,2017,Fall,b,E61 +BIOL,2325,2018,Spring,a,B37 +BIOL,2325,2018,Summer,a,F43 +BIOL,2325,2018,Fall,a,D52 +BIOL,2325,2018,Fall,b,D44 +BIOL,2325,2018,Fall,c,D89 +BIOL,2325,2019,Spring,a,E35 +BIOL,2325,2019,Spring,b,F55 +BIOL,2325,2019,Summer,a,B70 +BIOL,2330,2015,Spring,a,B89 +BIOL,2330,2015,Fall,a,C79 +BIOL,2330,2015,Fall,b,C82 +BIOL,2330,2015,Fall,c,A10 +BIOL,2330,2015,Fall,d,D47 +BIOL,2330,2016,Spring,a,F87 +BIOL,2330,2016,Fall,a,F57 +BIOL,2330,2017,Summer,a,C47 +BIOL,2330,2017,Fall,a,E20 +BIOL,2330,2017,Fall,b,C48 +BIOL,2330,2019,Fall,a,A95 +BIOL,2330,2020,Spring,a,E16 +BIOL,2355,2015,Spring,a,C89 +BIOL,2355,2015,Spring,b,D26 +BIOL,2355,2015,Summer,a,D23 +BIOL,2355,2015,Summer,b,D12 +BIOL,2355,2015,Summer,c,C86 +BIOL,2355,2016,Spring,a,C21 +BIOL,2355,2016,Spring,b,F82 +BIOL,2355,2017,Spring,a,B31 +BIOL,2355,2017,Spring,b,A47 +BIOL,2355,2017,Spring,c,C60 +BIOL,2355,2017,Spring,d,E17 +BIOL,2355,2017,Summer,a,A9 +BIOL,2355,2017,Fall,a,F62 +BIOL,2355,2017,Fall,b,D74 +BIOL,2355,2018,Spring,a,F10 +BIOL,2355,2018,Summer,a,C17 +BIOL,2355,2018,Summer,b,E82 +BIOL,2355,2018,Summer,c,B56 +BIOL,2355,2018,Summer,d,A16 +BIOL,2355,2018,Fall,a,C22 +BIOL,2355,2019,Spring,a,B45 +BIOL,2355,2019,Spring,b,E37 +BIOL,2355,2019,Spring,c,C26 +BIOL,2355,2019,Spring,d,E36 +BIOL,2355,2020,Spring,a,E83 +BIOL,2355,2020,Summer,a,B22 +BIOL,2355,2020,Summer,b,F78 +BIOL,2355,2020,Fall,a,A4 +BIOL,2420,2015,Spring,a,E34 +BIOL,2420,2015,Spring,b,E54 +BIOL,2420,2015,Spring,c,A64 +BIOL,2420,2015,Spring,d,E38 +BIOL,2420,2015,Summer,a,C62 +BIOL,2420,2015,Fall,a,D39 +BIOL,2420,2016,Spring,a,B57 +BIOL,2420,2017,Summer,a,C94 +BIOL,2420,2017,Summer,b,C52 +BIOL,2420,2018,Spring,a,C31 +BIOL,2420,2020,Spring,a,B21 +BIOL,2420,2020,Spring,b,E93 +BIOL,2420,2020,Summer,a,D66 +BIOL,2420,2020,Fall,a,D3 +CS,1030,2016,Spring,a,A7 +CS,1030,2016,Summer,a,F87 +CS,1030,2016,Fall,a,A56 +CS,1030,2018,Fall,a,C71 +CS,1030,2019,Fall,a,E88 +CS,1030,2019,Fall,b,B13 +CS,1030,2020,Spring,a,C72 +CS,1030,2020,Spring,b,B26 +CS,1030,2020,Spring,c,D65 +CS,1030,2020,Fall,a,D67 +CS,1410,2015,Spring,a,E18 +CS,1410,2015,Summer,a,B51 +CS,1410,2015,Summer,b,F39 +CS,1410,2015,Summer,c,E66 +CS,1410,2015,Summer,d,F73 +CS,1410,2016,Spring,a,C43 +CS,1410,2016,Spring,b,D75 +CS,1410,2016,Summer,a,F81 +CS,1410,2017,Spring,a,E74 +CS,1410,2018,Spring,a,F80 +CS,1410,2018,Spring,b,D19 +CS,1410,2018,Spring,c,B5 +CS,1410,2018,Spring,d,F15 +CS,1410,2020,Spring,a,E61 +CS,1410,2020,Spring,b,F94 +CS,2100,2015,Summer,a,E49 +CS,2100,2016,Spring,a,C70 +CS,2100,2016,Summer,a,F88 +CS,2100,2016,Summer,b,F34 +CS,2100,2016,Summer,c,B32 +CS,2100,2017,Spring,a,C99 +CS,2100,2017,Fall,a,C62 +CS,2100,2018,Spring,a,F36 +CS,2100,2018,Summer,a,E49 +CS,2100,2018,Summer,b,D45 +CS,2100,2018,Summer,c,B38 +CS,2100,2018,Fall,a,A45 +CS,2100,2018,Fall,b,F33 +CS,2100,2018,Fall,c,B26 +CS,2100,2018,Fall,d,C72 +CS,2100,2019,Spring,a,B14 +CS,2100,2019,Spring,b,E31 +CS,2100,2019,Summer,a,E29 +CS,2100,2019,Summer,b,A13 +CS,2100,2019,Fall,a,A88 +CS,2100,2019,Fall,b,A71 +CS,2100,2019,Fall,c,B53 +CS,2100,2019,Fall,d,D62 +CS,2100,2020,Spring,a,C42 +CS,2100,2020,Fall,a,F74 +CS,2420,2015,Spring,a,A23 +CS,2420,2015,Summer,a,A51 +CS,2420,2015,Summer,b,B96 +CS,2420,2015,Summer,c,C5 +CS,2420,2015,Fall,a,A43 +CS,2420,2016,Spring,a,E68 +CS,2420,2016,Summer,a,E60 +CS,2420,2016,Fall,a,C21 +CS,2420,2016,Fall,b,F33 +CS,2420,2016,Fall,c,A95 +CS,2420,2017,Summer,a,B23 +CS,2420,2017,Summer,b,F52 +CS,2420,2017,Summer,c,E42 +CS,2420,2017,Fall,a,B18 +CS,2420,2018,Spring,a,A34 +CS,2420,2019,Summer,a,E2 +CS,2420,2020,Summer,a,D40 +CS,2420,2020,Fall,a,F99 +CS,3100,2015,Summer,a,C48 +CS,3100,2015,Summer,b,B18 +CS,3100,2016,Spring,a,C54 +CS,3100,2016,Spring,b,D97 +CS,3100,2016,Spring,c,F28 +CS,3100,2016,Spring,d,F97 +CS,3100,2016,Summer,a,A68 +CS,3100,2016,Fall,a,A73 +CS,3100,2017,Spring,a,E26 +CS,3100,2017,Spring,b,B22 +CS,3100,2017,Summer,a,A88 +CS,3100,2017,Fall,a,A66 +CS,3100,2019,Spring,a,E60 +CS,3100,2019,Spring,b,C93 +CS,3200,2015,Spring,a,E8 +CS,3200,2015,Spring,b,A61 +CS,3200,2015,Fall,a,F94 +CS,3200,2015,Fall,b,D48 +CS,3200,2015,Fall,c,D58 +CS,3200,2015,Fall,d,D49 +CS,3200,2016,Summer,a,E18 +CS,3200,2016,Summer,b,C16 +CS,3200,2016,Fall,a,E17 +CS,3200,2016,Fall,b,B1 +CS,3200,2016,Fall,c,C60 +CS,3200,2016,Fall,d,E55 +CS,3200,2017,Spring,a,B32 +CS,3200,2018,Spring,a,A5 +CS,3200,2018,Spring,b,D79 +CS,3200,2018,Spring,c,A31 +CS,3200,2019,Spring,a,F7 +CS,3200,2020,Spring,a,A18 +CS,3200,2020,Spring,b,C30 +CS,3200,2020,Spring,c,F74 +CS,3200,2020,Summer,a,F42 +CS,3200,2020,Fall,a,F67 +CS,3500,2015,Fall,a,F23 +CS,3500,2015,Fall,b,D72 +CS,3500,2016,Spring,a,F86 +CS,3500,2016,Summer,a,F54 +CS,3500,2017,Summer,a,B29 +CS,3500,2017,Fall,a,D8 +CS,3500,2017,Fall,b,D72 +CS,3500,2017,Fall,c,D32 +CS,3500,2019,Summer,a,B7 +CS,3500,2019,Fall,a,E6 +CS,3500,2019,Fall,b,B98 +CS,3500,2019,Fall,c,F72 +CS,3500,2020,Summer,a,C2 +CS,3505,2015,Spring,a,F97 +CS,3505,2015,Fall,a,B51 +CS,3505,2015,Fall,b,E42 +CS,3505,2015,Fall,c,D60 +CS,3505,2015,Fall,d,C40 +CS,3505,2016,Summer,a,D60 +CS,3505,2016,Fall,a,D98 +CS,3505,2016,Fall,b,B48 +CS,3505,2017,Summer,a,F19 +CS,3505,2017,Fall,a,E75 +CS,3505,2017,Fall,b,C20 +CS,3505,2018,Summer,a,B64 +CS,3505,2018,Summer,b,F44 +CS,3505,2018,Fall,a,F83 +CS,3505,2018,Fall,b,D22 +CS,3505,2018,Fall,c,C22 +CS,3505,2019,Spring,a,B70 +CS,3505,2019,Spring,b,A68 +CS,3505,2019,Summer,a,F7 +CS,3505,2019,Summer,b,D18 +CS,3505,2019,Summer,c,B9 +CS,3505,2019,Summer,d,A28 +CS,3505,2019,Fall,a,C8 +CS,3505,2019,Fall,b,F79 +CS,3505,2019,Fall,c,F63 +CS,3505,2020,Spring,a,D2 +CS,3505,2020,Summer,a,E37 +CS,3505,2020,Fall,a,F56 +CS,3505,2020,Fall,b,B14 +CS,3505,2020,Fall,c,E20 +CS,3810,2015,Spring,a,C46 +CS,3810,2016,Summer,a,F29 +CS,3810,2016,Fall,a,A84 +CS,3810,2016,Fall,b,F98 +CS,3810,2018,Spring,a,F22 +CS,3810,2018,Summer,a,F43 +CS,3810,2018,Summer,b,A68 +CS,3810,2018,Summer,c,B28 +CS,3810,2018,Summer,d,F73 +CS,3810,2019,Fall,a,E73 +CS,3810,2019,Fall,b,B41 +CS,3810,2020,Fall,a,D10 +CS,4000,2015,Spring,a,E50 +CS,4000,2015,Spring,b,E43 +CS,4000,2015,Summer,a,F93 +CS,4000,2015,Fall,a,C7 +CS,4000,2016,Fall,a,E77 +CS,4000,2017,Spring,a,A82 +CS,4000,2017,Summer,a,D30 +CS,4000,2017,Fall,a,D24 +CS,4000,2017,Fall,b,F49 +CS,4000,2018,Spring,a,B92 +CS,4000,2019,Spring,a,B95 +CS,4000,2020,Spring,a,D47 +CS,4000,2020,Spring,b,A17 +CS,4000,2020,Fall,a,E53 +CS,4150,2015,Summer,a,E77 +CS,4150,2015,Summer,b,D2 +CS,4150,2016,Summer,a,B74 +CS,4150,2016,Summer,b,F49 +CS,4150,2018,Fall,a,C33 +CS,4150,2018,Fall,b,F81 +CS,4150,2019,Spring,a,D14 +CS,4150,2020,Spring,a,D43 +CS,4150,2020,Fall,a,F77 +CS,4400,2015,Summer,a,B62 +CS,4400,2015,Fall,a,C38 +CS,4400,2015,Fall,b,F63 +CS,4400,2015,Fall,c,B42 +CS,4400,2016,Spring,a,D47 +CS,4400,2016,Summer,a,E70 +CS,4400,2016,Fall,a,A94 +CS,4400,2017,Spring,a,D38 +CS,4400,2017,Spring,b,A53 +CS,4400,2017,Spring,c,B82 +CS,4400,2019,Spring,a,E52 +CS,4400,2019,Spring,b,F54 +CS,4400,2019,Spring,c,C90 +CS,4400,2019,Spring,d,E77 +CS,4400,2019,Summer,a,A14 +CS,4400,2019,Summer,b,F86 +CS,4400,2019,Fall,a,A73 +CS,4400,2019,Fall,b,F83 +CS,4400,2020,Spring,a,D14 +CS,4400,2020,Fall,a,E72 +CS,4400,2020,Fall,b,E29 +CS,4500,2015,Summer,a,E89 +CS,4500,2015,Summer,b,C4 +CS,4500,2016,Spring,a,A15 +CS,4500,2016,Spring,b,F19 +CS,4500,2016,Fall,a,E62 +CS,4500,2017,Summer,a,D41 +CS,4500,2018,Spring,a,A44 +CS,4500,2018,Spring,b,F22 +CS,4500,2018,Spring,c,F32 +CS,4500,2018,Spring,d,E21 +CS,4500,2019,Summer,a,F24 +CS,4500,2019,Fall,a,D4 +CS,4500,2019,Fall,b,B58 +CS,4500,2019,Fall,c,D1 +CS,4500,2019,Fall,d,B36 +CS,4500,2020,Spring,a,A74 +CS,4500,2020,Summer,a,B47 +CS,4940,2015,Summer,a,E82 +CS,4940,2017,Fall,a,C79 +CS,4940,2017,Fall,b,F18 +CS,4940,2019,Fall,a,E50 +CS,4940,2020,Summer,a,F23 +CS,4940,2020,Summer,b,D37 +CS,4970,2016,Fall,a,E65 +CS,4970,2016,Fall,b,D88 +CS,4970,2017,Spring,a,D63 +CS,4970,2017,Summer,a,B38 +CS,4970,2018,Summer,a,E96 +CS,4970,2018,Summer,b,D71 +CS,4970,2018,Summer,c,E15 +CS,4970,2018,Fall,a,C70 +CS,4970,2018,Fall,b,A98 +CS,4970,2018,Fall,c,E28 +CS,4970,2018,Fall,d,A95 +CS,4970,2019,Spring,a,B39 +CS,4970,2019,Spring,b,A58 +CS,4970,2019,Summer,a,A57 +CS,4970,2019,Summer,b,A100 +CS,4970,2019,Summer,c,B95 +CS,4970,2019,Summer,d,C91 +CS,4970,2019,Fall,a,D22 +CS,4970,2019,Fall,b,B27 +CS,4970,2019,Fall,c,E45 +CS,4970,2019,Fall,d,E69 +CS,4970,2020,Summer,a,C38 +CS,4970,2020,Summer,b,E87 +CS,4970,2020,Summer,c,B97 +CS,4970,2020,Summer,d,A36 +CS,4970,2020,Fall,a,B90 +CS,4970,2020,Fall,b,B19 +CS,4970,2020,Fall,c,B98 +CS,4970,2020,Fall,d,D63 +MATH,1210,2015,Summer,a,F54 +MATH,1210,2016,Spring,a,A52 +MATH,1210,2016,Spring,b,C89 +MATH,1210,2016,Spring,c,C59 +MATH,1210,2016,Spring,d,C75 +MATH,1210,2016,Fall,a,F12 +MATH,1210,2016,Fall,b,D82 +MATH,1210,2016,Fall,c,C9 +MATH,1210,2016,Fall,d,D28 +MATH,1210,2017,Spring,a,B64 +MATH,1210,2017,Summer,a,C71 +MATH,1210,2017,Summer,b,E63 +MATH,1210,2017,Summer,c,F98 +MATH,1210,2018,Spring,a,D3 +MATH,1210,2018,Summer,a,D59 +MATH,1210,2018,Fall,a,B89 +MATH,1210,2018,Fall,b,F39 +MATH,1210,2019,Spring,a,C12 +MATH,1210,2019,Spring,b,C11 +MATH,1210,2019,Summer,a,B7 +MATH,1210,2020,Spring,a,B55 +MATH,1210,2020,Spring,b,F13 +MATH,1220,2015,Summer,a,A2 +MATH,1220,2015,Summer,b,A55 +MATH,1220,2015,Summer,c,D10 +MATH,1220,2016,Spring,a,A41 +MATH,1220,2017,Spring,a,B83 +MATH,1220,2017,Spring,b,B9 +MATH,1220,2017,Spring,c,A79 +MATH,1220,2017,Spring,d,D45 +MATH,1220,2017,Summer,a,F96 +MATH,1220,2018,Spring,a,B12 +MATH,1220,2018,Spring,b,B97 +MATH,1220,2018,Summer,a,C55 +MATH,1220,2019,Fall,a,E93 +MATH,1220,2019,Fall,b,F4 +MATH,1220,2019,Fall,c,F39 +MATH,1220,2020,Spring,a,B96 +MATH,1220,2020,Summer,a,B64 +MATH,1250,2015,Spring,a,A68 +MATH,1250,2015,Spring,b,A47 +MATH,1250,2015,Spring,c,B50 +MATH,1250,2015,Spring,d,E54 +MATH,1250,2015,Fall,a,D99 +MATH,1250,2016,Spring,a,A34 +MATH,1250,2016,Summer,a,D65 +MATH,1250,2016,Fall,a,D55 +MATH,1250,2016,Fall,b,A82 +MATH,1250,2016,Fall,c,E20 +MATH,1250,2017,Summer,a,B20 +MATH,1250,2017,Summer,b,D76 +MATH,1250,2017,Summer,c,F88 +MATH,1250,2017,Summer,d,C90 +MATH,1250,2018,Spring,a,B8 +MATH,1250,2018,Summer,a,A59 +MATH,1250,2018,Summer,b,A40 +MATH,1250,2018,Summer,c,F95 +MATH,1250,2020,Summer,a,F34 +MATH,1260,2015,Spring,a,C94 +MATH,1260,2015,Spring,b,A43 +MATH,1260,2015,Spring,c,C68 +MATH,1260,2015,Summer,a,E81 +MATH,1260,2016,Fall,a,C21 +MATH,1260,2017,Summer,a,F15 +MATH,1260,2017,Fall,a,A2 +MATH,1260,2019,Spring,a,A71 +MATH,1260,2019,Spring,b,F95 +MATH,1260,2019,Spring,c,B42 +MATH,1260,2019,Summer,a,C35 +MATH,1260,2019,Summer,b,E48 +MATH,1260,2019,Fall,a,A23 +MATH,1260,2020,Spring,a,A52 +MATH,2210,2015,Spring,a,C12 +MATH,2210,2015,Spring,b,A48 +MATH,2210,2015,Summer,a,C95 +MATH,2210,2015,Summer,b,D48 +MATH,2210,2015,Summer,c,D99 +MATH,2210,2015,Summer,d,F70 +MATH,2210,2015,Fall,a,B20 +MATH,2210,2017,Spring,a,A43 +MATH,2210,2017,Summer,a,F94 +MATH,2210,2018,Spring,a,D63 +MATH,2210,2018,Spring,b,B92 +MATH,2210,2019,Spring,a,D90 +MATH,2210,2019,Spring,b,D96 +MATH,2210,2020,Spring,a,A76 +MATH,2210,2020,Spring,b,D85 +MATH,2210,2020,Spring,c,B38 +MATH,2210,2020,Fall,a,F95 +MATH,2270,2015,Fall,a,B100 +MATH,2270,2015,Fall,b,A20 +MATH,2270,2017,Summer,a,D40 +MATH,2270,2017,Fall,a,A21 +MATH,2270,2017,Fall,b,C91 +MATH,2270,2017,Fall,c,A28 +MATH,2270,2017,Fall,d,C19 +MATH,2270,2019,Spring,a,F39 +MATH,2270,2019,Summer,a,A52 +MATH,2270,2019,Summer,b,E96 +MATH,2270,2019,Summer,c,A60 +MATH,2270,2019,Fall,a,A2 +MATH,2270,2020,Spring,a,B17 +MATH,2270,2020,Fall,a,F11 +MATH,2270,2020,Fall,b,C10 +MATH,2280,2015,Summer,a,D17 +MATH,2280,2015,Fall,a,C16 +MATH,2280,2016,Fall,a,F51 +MATH,2280,2018,Spring,a,C36 +MATH,2280,2018,Fall,a,E32 +MATH,2280,2018,Fall,b,D53 +MATH,2280,2018,Fall,c,D8 +MATH,2280,2019,Fall,a,E32 +MATH,2280,2019,Fall,b,E3 +MATH,2280,2019,Fall,c,F46 +MATH,2280,2020,Spring,a,C73 +MATH,2280,2020,Spring,b,D35 +MATH,3210,2015,Spring,a,C8 +MATH,3210,2015,Spring,b,D68 +MATH,3210,2015,Summer,a,B21 +MATH,3210,2015,Fall,a,C69 +MATH,3210,2015,Fall,b,F8 +MATH,3210,2015,Fall,c,B74 +MATH,3210,2015,Fall,d,D46 +MATH,3210,2016,Spring,a,B23 +MATH,3210,2016,Fall,a,C76 +MATH,3210,2017,Spring,a,E73 +MATH,3210,2017,Summer,a,D70 +MATH,3210,2019,Spring,a,A43 +MATH,3210,2019,Spring,b,B17 +MATH,3210,2019,Fall,a,C8 +MATH,3210,2020,Spring,a,B100 +MATH,3210,2020,Summer,a,C10 +MATH,3210,2020,Fall,a,D76 +MATH,3220,2016,Spring,a,F63 +MATH,3220,2016,Spring,b,B91 +MATH,3220,2016,Spring,c,F79 +MATH,3220,2016,Spring,d,B86 +MATH,3220,2016,Summer,a,B49 +MATH,3220,2016,Fall,a,B23 +MATH,3220,2016,Fall,b,F74 +MATH,3220,2017,Spring,a,E5 +MATH,3220,2017,Fall,a,E29 +MATH,3220,2017,Fall,b,A64 +MATH,3220,2018,Spring,a,B45 +MATH,3220,2018,Spring,b,B82 +MATH,3220,2018,Spring,c,A91 +MATH,3220,2018,Spring,d,F43 +PHYS,2040,2015,Spring,a,B53 +PHYS,2040,2015,Fall,a,A62 +PHYS,2040,2015,Fall,b,E84 +PHYS,2040,2015,Fall,c,B21 +PHYS,2040,2016,Spring,a,A38 +PHYS,2040,2017,Summer,a,B94 +PHYS,2040,2017,Fall,a,A44 +PHYS,2040,2017,Fall,b,E62 +PHYS,2040,2017,Fall,c,D84 +PHYS,2040,2018,Spring,a,B7 +PHYS,2040,2019,Spring,a,F94 +PHYS,2040,2019,Spring,b,F37 +PHYS,2040,2020,Spring,a,D20 +PHYS,2060,2015,Spring,a,F77 +PHYS,2060,2016,Spring,a,A61 +PHYS,2060,2016,Spring,b,C51 +PHYS,2060,2016,Summer,a,C12 +PHYS,2060,2016,Summer,b,D24 +PHYS,2060,2018,Summer,a,E8 +PHYS,2060,2018,Fall,a,A11 +PHYS,2060,2018,Fall,b,E53 +PHYS,2060,2018,Fall,c,E30 +PHYS,2060,2018,Fall,d,D67 +PHYS,2060,2019,Summer,a,D74 +PHYS,2060,2019,Summer,b,D39 +PHYS,2060,2019,Fall,a,F5 +PHYS,2060,2019,Fall,b,E74 +PHYS,2060,2019,Fall,c,E19 +PHYS,2060,2020,Spring,a,B22 +PHYS,2060,2020,Spring,b,B17 +PHYS,2060,2020,Fall,a,B81 +PHYS,2100,2015,Spring,a,C94 +PHYS,2100,2015,Spring,b,A12 +PHYS,2100,2016,Fall,a,F80 +PHYS,2100,2016,Fall,b,D15 +PHYS,2100,2017,Summer,a,A14 +PHYS,2100,2017,Summer,b,A37 +PHYS,2100,2017,Summer,c,C53 +PHYS,2100,2017,Fall,a,E78 +PHYS,2100,2018,Fall,a,F89 +PHYS,2100,2019,Summer,a,F31 +PHYS,2140,2015,Spring,a,C36 +PHYS,2140,2015,Spring,b,F88 +PHYS,2140,2015,Summer,a,B39 +PHYS,2140,2015,Summer,b,D100 +PHYS,2140,2015,Summer,c,C94 +PHYS,2140,2015,Fall,a,B57 +PHYS,2140,2016,Spring,a,F63 +PHYS,2140,2016,Spring,b,C8 +PHYS,2140,2016,Spring,c,B9 +PHYS,2140,2016,Summer,a,B100 +PHYS,2140,2016,Summer,b,E4 +PHYS,2140,2016,Fall,a,B8 +PHYS,2140,2017,Summer,a,F26 +PHYS,2140,2017,Fall,a,E51 +PHYS,2140,2017,Fall,b,A88 +PHYS,2140,2018,Summer,a,B61 +PHYS,2140,2018,Summer,b,C45 +PHYS,2140,2018,Fall,a,F89 +PHYS,2140,2019,Fall,a,B29 +PHYS,2140,2019,Fall,b,F27 +PHYS,2140,2020,Fall,a,F2 +PHYS,2210,2015,Fall,a,B33 +PHYS,2210,2015,Fall,b,C92 +PHYS,2210,2015,Fall,c,F36 +PHYS,2210,2017,Summer,a,E51 +PHYS,2210,2017,Summer,b,A66 +PHYS,2210,2017,Summer,c,C72 +PHYS,2210,2017,Summer,d,E37 +PHYS,2210,2018,Fall,a,F42 +PHYS,2210,2018,Fall,b,C84 +PHYS,2210,2018,Fall,c,F39 +PHYS,2210,2019,Spring,a,B8 +PHYS,2210,2019,Spring,b,E52 +PHYS,2210,2019,Spring,c,F18 +PHYS,2210,2019,Spring,d,F64 +PHYS,2210,2019,Summer,a,C54 +PHYS,2210,2019,Fall,a,E91 +PHYS,2210,2019,Fall,b,B44 +PHYS,2210,2019,Fall,c,B88 +PHYS,2210,2019,Fall,d,D86 +PHYS,2220,2015,Spring,a,E24 +PHYS,2220,2015,Fall,a,F72 +PHYS,2220,2015,Fall,b,B88 +PHYS,2220,2015,Fall,c,F12 +PHYS,2220,2016,Summer,a,D43 +PHYS,2220,2016,Fall,a,D16 +PHYS,2220,2017,Spring,a,E75 +PHYS,2220,2017,Spring,b,A61 +PHYS,2220,2017,Spring,c,E16 +PHYS,2220,2017,Spring,d,D68 +PHYS,2220,2018,Spring,a,B26 +PHYS,2220,2018,Summer,a,D19 +PHYS,2220,2018,Fall,a,A63 +PHYS,2220,2019,Spring,a,C82 +PHYS,2220,2020,Spring,a,E98 +PHYS,2220,2020,Summer,a,A17 +PHYS,2220,2020,Summer,b,F55 +PHYS,2220,2020,Fall,a,D1 +PHYS,3210,2016,Summer,a,B3 +PHYS,3210,2016,Summer,b,F94 +PHYS,3210,2016,Fall,a,C40 +PHYS,3210,2017,Summer,a,B9 +PHYS,3210,2017,Summer,b,C38 +PHYS,3210,2017,Fall,a,E44 +PHYS,3210,2018,Spring,a,B44 +PHYS,3210,2018,Spring,b,D46 +PHYS,3210,2018,Spring,c,B52 +PHYS,3210,2018,Fall,a,B94 +PHYS,3210,2019,Spring,a,A47 +PHYS,3210,2019,Spring,b,A49 +PHYS,3210,2019,Spring,c,C99 +PHYS,3210,2019,Spring,d,A77 +PHYS,3210,2019,Summer,a,F14 +PHYS,3210,2019,Summer,b,A7 +PHYS,3210,2019,Summer,c,D57 +PHYS,3210,2019,Fall,a,D90 +PHYS,3210,2020,Spring,a,F2 +PHYS,3210,2020,Summer,a,F67 +PHYS,3210,2020,Fall,a,B54 +PHYS,3210,2020,Fall,b,A66 +PHYS,3210,2020,Fall,c,A37 +PHYS,3220,2016,Summer,a,B46 +PHYS,3220,2016,Summer,b,C21 +PHYS,3220,2017,Summer,a,C31 +PHYS,3220,2017,Fall,a,A74 +PHYS,3220,2017,Fall,b,B12 +PHYS,3220,2017,Fall,c,A93 +PHYS,3220,2017,Fall,d,C83 +PHYS,3220,2018,Summer,a,C34 +PHYS,3220,2020,Spring,a,C55 +PHYS,3220,2020,Spring,b,A98 +PHYS,3220,2020,Spring,c,A18 +PHYS,3220,2020,Spring,d,B43 diff --git a/tests/data/Student.csv b/tests/data/Student.csv new file mode 100644 index 000000000..bdcf87846 --- /dev/null +++ b/tests/data/Student.csv @@ -0,0 +1,301 @@ +student_id,first_name,last_name,sex,date_of_birth,home_address,home_city,home_state,home_zip,home_phone +100,Allison,Hill,F,1991-05-09,819 Anthony Fields Suite 083,Jacquelinebury,IN,01352,+1-542-351-1615 +101,Lindsey,Roman,F,1995-05-18,618 Courtney Tunnel Apt. 310,Kendrashire,UT,50324,(525)534-1928x327 +102,William,Bowman,M,2005-01-07,030 Morales Centers Suite 953,Randallside,IL,32826,(969)653-2871x01226 +103,Janice,Carlson,F,1989-07-16,0184 Peterson Green,North Jenniferchester,PA,67043,+1-489-325-2880x9570 +104,Sherry,Decker,F,2004-04-08,117 Spence Mountain,New Staceyville,NJ,28261,001-346-578-7133 +105,Alisha,Spencer,F,1994-03-10,031 Heath Circle,New Jasonland,NH,62454,+1-631-165-6670x106 +106,Rebecca,Rodriguez,F,1987-11-30,24731 Michelle Orchard Apt. 801,Allisonville,GA,53066,(064)746-8723 +107,Tracy,Riley,F,2005-02-24,97882 William Summit Apt. 136,Port Johnstad,MA,77004,(435)346-2475x10799 +108,Mr.,Daniel,M,1995-07-04,2784 Archer Ports Apt. 841,Taylorland,NV,36198,534.874.0164x0052 +109,Deborah,Figueroa,F,1994-05-30,12805 Hernandez Creek,Port Laura,VT,28036,586.923.2260x25634 +110,Meredith,Reyes,F,1997-03-09,75433 James Heights,Rasmussenburgh,MD,70783,001-142-940-1965x569 +111,Stephanie,Lee,F,1997-01-06,8356 Elizabeth Highway,Lake Jennifer,IA,54029,482-366-2994x68044 +112,Rachel,Lawson,F,1990-12-07,872 Campbell Prairie,Clarenceshire,IA,26601,3791769367 +113,Brittany,Watts,F,2003-02-04,632 Dominguez Lodge Suite 172,Contrerasshire,WV,58509,872-774-3487x34714 +114,Gabriella,Orozco,F,1998-11-11,2316 Amy Lakes,West Rebeccastad,TX,75957,(546)688-9373x467 +115,Gabriella,Shelton,F,1997-01-15,2980 Vargas Prairie,South Michelleville,KS,60099,646-417-0805x310 +116,Travis,Gonzalez,M,1996-07-14,19374 Jackson Place,Dannyfort,CO,03866,663.193.1491x905 +117,Mary,Jones,F,2002-05-15,7165 Poole Road,Lake Tammy,SD,71040,(945)314-7379x965 +118,Samuel,White,M,1994-03-13,9480 Lee Forest Apt. 837,Travisfort,HI,91174,957.885.6855 +119,Devin,King,M,1986-05-27,82337 Brittany Skyway,Tinafort,LA,40119,+1-240-084-2710 +120,Julie,Alexander,F,1993-08-06,711 Charles Plaza,East Annaburgh,CT,55049,+1-677-496-4990x913 +121,Deborah,Miller,F,1993-07-27,67974 Keith Gateway Suite 134,Weberfurt,MA,71877,421.024.9947x17464 +122,Johnny,Miller,M,1995-05-20,40139 Smith Spring,Johnstonmouth,MT,58464,(967)175-6551 +123,Gary,Steele,M,1987-09-04,807 Johnny Cove Suite 808,North April,MO,58440,(824)771-0932 +124,Adam,Russell,M,2000-01-14,12748 Perry Manors Apt. 782,Port William,UT,36709,840-449-9727x875 +125,Patricia,Williams,F,1988-06-19,627 Martinez Vista Apt. 171,Stephenchester,NC,20733,(459)615-8657x809 +126,Jade,Thomas,F,2004-07-08,221 Reyes Rapid Apt. 923,East Jonathan,SD,38201,759-464-7436 +127,Ashley,James,F,1997-11-27,064 Michelle Spur,Lozanomouth,VA,30663,(394)210-4709 +128,Carlos,Browning,M,1990-09-16,85884 Scott Stream,Lake Julie,CO,10370,001-368-516-0481 +129,Megan,Chambers,F,2002-09-06,137 Nicole Park Suite 317,Turnerbury,WV,40394,382-675-8692 +130,Matthew,Bass,M,1986-08-24,53773 Garcia Rapids Suite 506,Port Stacy,CA,28302,5329318393 +131,David,Schroeder,M,1998-03-28,22842 Michelle Crescent Apt. 395,East Davidbury,AR,59257,(178)390-8470x0766 +132,John,Browning,M,1989-10-24,1249 Kelley Heights,Schmidtview,CO,92484,+1-836-736-5766x1565 +133,Brittany,Leblanc,F,2002-04-29,15280 Hoffman Highway Apt. 560,Burkeborough,GA,86580,(158)514-9368 +134,Dr.,Louis,M,1993-03-28,402 Kathryn Valleys Apt. 229,Chadmouth,CA,70032,752-545-9910x2290 +135,Denise,Stanley,F,1993-02-08,81561 Erika Meadow,Brandonbury,AL,40008,+1-445-107-6226x838 +136,Michael,Gomez,M,1994-03-14,7159 Richard Port Apt. 605,Port Stevechester,MI,14376,681-645-3521x81883 +137,Hannah,Luna,F,1996-11-30,24329 Katherine Circles Suite 779,Coleside,NY,82358,+1-527-177-4490x5814 +138,Anthony,Decker,M,1997-08-09,998 Betty Villages Suite 079,Marcport,AR,14067,001-182-037-7889x255 +139,George,Harper,M,1988-10-20,18644 Douglas Underpass Suite 519,Sabrinaburgh,NC,17402,652.816.8505 +140,Tiffany,Peterson,F,1998-09-26,214 Garcia Springs,Stephensontown,RI,17677,292-706-5379 +141,Nicole,Cole,F,1990-08-18,735 Hudson Loaf,Stricklandport,DC,26675,+1-075-818-1412x4782 +142,Susan,Velasquez,F,1986-02-05,6853 Christopher Flat Apt. 152,West Mariachester,OH,59300,001-043-289-8614x341 +143,Jennifer,Bauer,F,1988-10-31,980 Andrews Roads,North Michael,FL,88085,(518)888-8067x06540 +144,Austin,Allen,M,2001-06-29,5205 Li Drives,Marshallchester,SD,08771,3030548687 +145,Nicole,Lee,F,2000-05-12,541 Kim Knoll Apt. 652,South Sandra,SC,95801,9284511544 +146,Michelle,Jackson,F,2000-10-29,596 Tina Village,New Michaelfort,WV,19215,1355690927 +147,Jacqueline,Hines,F,2001-04-19,4310 Porter Junctions Suite 447,New Heathershire,CT,10207,(715)518-8442 +148,Timothy,Little,M,1988-06-05,32370 Ashley Loop Suite 291,West Jenniferport,MD,75854,517-785-2892 +149,Carl,Shaw,M,1991-08-28,4225 Perez Village Suite 414,Port Joshuastad,CA,84516,922.995.9001x094 +150,Randall,Butler,M,1996-10-13,4473 Cohen Green,North Scottport,NJ,41471,001-562-588-1537 +151,Jerry,Thomas,M,1994-02-09,632 Peck Roads Apt. 278,Port Tyler,MD,60431,(500)479-7480 +152,Jessica,Khan,F,2004-11-24,6098 Angela Circles Suite 849,Davidshire,SC,44945,001-239-868-0002x578 +153,Jordan,Hicks,M,2005-10-09,0551 Silva Squares Suite 097,New Teresa,HI,07232,(896)230-9130x7562 +154,Christina,Shaw,F,1994-11-30,028 Mark Prairie,Leeville,KY,46938,334.843.4437x5758 +155,Robert,Hill,M,1994-01-22,6524 Stephanie Cliff Suite 473,South Sarahchester,NM,77418,833.016.5712 +156,Krista,Hickman,F,1987-02-26,734 Debbie Union Apt. 938,Melissatown,MA,23541,001-672-400-4991x547 +157,Teresa,Rosales,F,1997-01-28,27420 Gibbs Parks,Thompsonhaven,TN,68039,122-753-0463 +158,Debra,Rivera,F,1998-08-19,53017 Richard Mills Suite 414,East Susan,MN,79896,878-339-1878x51910 +159,Stephanie,Harris,F,2001-08-26,713 Burns Turnpike,North David,NV,73743,406.403.9106x51801 +160,John,Mitchell,M,1986-09-10,656 Sally Isle Apt. 825,Port Phillipland,TN,99614,001-786-863-3752x431 +161,Timothy,Small,M,2005-07-09,7903 Morales Ford,Port Brianport,SD,96382,953.428.3644 +162,Jamie,Webster,F,1998-10-02,27086 Grant Crest Apt. 351,Booneton,FL,35688,901.398.3735x40331 +163,Paul,Rocha,M,1987-06-23,3854 Amanda Island Apt. 877,Port Terrancefort,LA,54755,320.489.9642x353 +164,Sandra,Porter,F,1993-10-17,77725 Jennifer Meadow Suite 808,Lake Sierrafurt,MA,83168,2038750997 +165,Alexis,Patel,F,2003-10-31,840 Wolfe Lane,Whiteside,ID,81736,546.156.7933 +166,Jonathan,Hamilton,M,1986-06-14,180 Rachel Rest Suite 401,Juanmouth,FL,41721,001-926-142-9396x856 +167,William,Brown,M,1988-06-02,9965 Joshua Well Apt. 586,New Donna,NM,32803,262-655-1104 +168,Philip,Garcia,M,2004-12-15,8610 Angela Pine,Shieldstown,RI,95507,001-398-262-2444x721 +169,Desiree,Evans,F,2000-07-27,799 Daniel Grove,Cookstad,KS,44375,+1-924-593-7526x5479 +170,Erika,Ramirez,F,1999-11-03,398 Katrina Burg,Sherryville,TN,09565,243.426.6179x79688 +171,Sergio,Barnes,M,1989-07-10,891 John Prairie Apt. 909,Byrdbury,WI,56921,4388899375 +172,Patricia,Chapman,F,2001-04-24,14611 Cross Inlet,Lake Adriana,CA,95134,401.051.2382 +173,Gary,Simmons,M,1992-04-12,2660 Ware Locks Apt. 033,New Laura,SC,70872,371-478-5969x6915 +174,Jimmy,Thompson,M,1991-10-25,912 John Cove Apt. 286,North Patrick,NY,91390,(742)257-9050x72368 +175,Jon,Cohen,M,2004-05-12,1903 Joshua Mountains Apt. 797,Danielland,SD,48586,+1-078-361-3407x4517 +176,Autumn,Cain,F,2003-06-04,962 Glover Stravenue Suite 958,South Mario,IN,35542,001-126-042-2325x367 +177,Mark,Brooks,M,1999-06-14,684 Wiley Locks Apt. 901,Stephenfurt,AR,70549,(637)454-5892 +178,Karina,Cooper,F,1989-02-04,70127 Victoria Lane,Blankenshiphaven,UT,36417,415.206.4361x10371 +179,Courtney,Frazier,F,2005-01-31,627 Patrick Row Apt. 554,Lake Karenland,DE,70035,2753269731 +180,Charles,Martinez,M,2003-07-15,2341 Carolyn Roads,Port Anthony,UT,27429,364.037.6137x9180 +181,Timothy,Anderson,M,2000-05-01,710 Smith Field,Frybury,OK,54952,+1-188-924-1418 +182,William,Moore,M,1990-08-03,146 Mathis Center Apt. 617,Brianfurt,DC,02161,+1-275-884-2524 +183,Bruce,Yoder,M,1989-11-04,4917 Michael Mill,Michaelberg,NH,95237,(800)030-7562 +184,Toni,Johnson,F,1996-06-28,3536 Flores Stream Suite 180,Lake Tinashire,MN,37503,870-534-9493x759 +185,Dr.,Patty,F,1989-01-31,60385 Steele Branch Apt. 641,Port Robertshire,DE,37178,3865719182 +186,James,Vargas,M,1996-05-29,44565 Joseph Circles Apt. 912,South Leeland,RI,59734,(112)490-3521x356 +187,Amy,Norman,F,1987-05-16,1994 Jones Wells,New Lisaton,SD,16560,001-029-667-0662x532 +188,Sophia,Johnson,F,1998-02-20,68701 Derrick Extensions,Foxstad,SC,50635,(759)856-4205x930 +189,Whitney,Robinson,F,2002-08-10,2239 Joanna Island Suite 599,Port Maryfort,NE,23511,0393087059 +190,Teresa,Foster,F,1995-12-10,26752 Hoffman Tunnel,Michaelfurt,ME,96707,096-902-9593 +191,Brian,Crawford,M,2000-01-03,5215 Joseph Forges,East Danieltown,OR,22303,(658)617-9327x1040 +192,Trevor,Jones,M,1992-05-20,815 Austin Manors,Port Frederickhaven,CO,27442,884-443-1069x87205 +193,Brandon,Colon,M,1998-06-27,32417 Parker Keys,New Christopher,FL,50497,(047)743-4902 +194,Michael,Miller,M,2005-05-13,938 Paul Mount Suite 793,North Raven,MO,68241,921.722.3320x61632 +195,Lisa,Mills,F,1987-03-12,99119 Floyd Track,Humphreyburgh,NH,62504,(629)960-6530 +196,Thomas,Prince,M,2003-06-14,47132 Julia Springs Apt. 691,East Madisonmouth,UT,07868,+1-148-628-9023x303 +197,Anthony,Ward,M,1988-12-29,6103 Brooke Drives,Matthewsborough,VT,98668,602.933.3346 +198,Sharon,Coffey,F,2001-10-19,29034 Hahn Road,Joshuaside,MN,29102,896.910.8589 +199,Edwin,Rodriguez,M,1999-09-08,4443 Kathy Turnpike Suite 965,Jenniferfurt,IL,55363,099-353-8758x4282 +200,John,Figueroa,M,1988-05-05,513 Julie Groves Suite 554,Stevenland,NY,76563,(381)684-6022x356 +201,Stephanie,Hatfield,F,2000-07-12,52500 Jason Springs,Ericmouth,CT,57348,760-083-5058x30033 +202,Gregory,Anderson,M,1990-05-20,04478 Morgan Tunnel Suite 575,Martinside,AL,29903,(098)215-0648 +203,Linda,Williams,F,2003-04-29,16761 Wells Dale Suite 046,Elaineburgh,CT,14252,+1-141-173-9348 +204,Mr.,Jason,M,1995-12-29,753 Emily Union Suite 721,Joneschester,NY,60368,012.045.5611 +205,Stefanie,Smith,F,1991-05-06,79415 White Knoll Suite 467,Banksfort,OH,08187,979-729-6590 +206,Sheryl,Acosta,F,1997-06-06,6701 Leon River,Katrinamouth,WI,88298,(916)375-6289x0028 +207,Samuel,Booth,M,2002-11-04,40838 Powell Ford,Lake Shane,MI,16060,001-016-608-8019 +208,Miss,Stefanie,F,1998-01-01,0375 Harvey Mall,Jenniferland,HI,45243,+1-488-510-2726x1493 +209,Tara,Long,F,2005-10-29,160 Monroe Path Suite 779,Taylorport,AZ,57230,(829)221-6995x8669 +210,Stacey,Hunt,F,2000-02-15,83339 Parks Valleys Apt. 288,Marcusland,MS,75295,846.081.0620x03424 +211,Brianna,Brown,F,1987-07-09,5719 Stevenson Trace,Annaberg,SC,38202,001-665-800-4397x359 +212,Craig,Hardy,M,1991-03-10,122 Wilson Camp,East Eugene,AL,61623,5909479851 +213,Evan,Robinson,M,1986-03-21,6886 Jeffrey Field,West Jeffery,NE,74076,573-993-0561 +214,Carol,Huber,F,1997-03-16,36138 Johns Run,Lake Charles,AK,94462,1024819346 +215,Mark,Hamilton,M,2004-01-26,9190 Jones Via Apt. 491,Port Patrick,AK,20990,(684)245-0882 +216,Aaron,Carlson,M,1988-03-18,53682 Jeffrey Street Apt. 290,Randolphshire,NV,38597,397.552.3149 +217,Cheryl,Tucker,F,1998-02-15,299 Leslie Lane Apt. 336,West Erin,MS,58874,+1-781-291-4283x411 +218,Sarah,Welch,F,1998-04-20,308 Patricia Mountains Suite 256,Lake Jessicaburgh,MT,52508,(392)827-2299x2750 +219,Katherine,Brown,F,1991-11-01,56770 Deborah Course,Schultzburgh,NH,75233,659-184-6386x5577 +220,Adriana,Macias,F,1993-02-01,4322 Carolyn Stravenue,Robertborough,ND,63287,603.029.9228x092 +221,Roberto,Valentine,M,1990-06-02,7236 Norton Stravenue Apt. 842,Matthewview,HI,51024,388-629-1279 +222,Sherry,Schmidt,F,2005-07-09,9806 Wood Camp,Jeromefort,ME,77708,247-314-9864 +223,Michelle,Clarke,F,1992-11-06,35651 Denise Fork,Hendersonborough,ND,99456,872-588-7449x56213 +224,Melissa,Martin,F,1988-08-22,8902 Cynthia Squares,Ruizstad,IL,49107,669.849.0277x0384 +225,Richard,Dixon,M,2005-10-02,530 Miller Gardens Apt. 669,North Janeside,OR,73785,439-376-9042x681 +226,Kathy,Morgan,F,1993-09-28,89476 Carrillo Shores Suite 779,Olsonberg,SC,29386,+1-658-804-3416x5182 +227,Hayden,Shannon,M,1987-05-11,373 John Fort Apt. 395,North Samanthafurt,NM,71473,+1-595-794-7284x6392 +228,Jay,Ayers,M,1994-11-11,271 Stevens Rest,East Biancaborough,IL,72402,(795)527-6365 +229,Jennifer,Hayes,F,1996-02-16,143 Chase Extensions Suite 270,South Wendyhaven,OK,64283,906.120.3471 +230,Felicia,Ward,F,2001-09-12,06159 Barbara Ports Apt. 455,Tonychester,ME,38056,225.699.6112x5355 +231,Michael,Jacobs,M,2003-10-01,598 Gutierrez Estates Apt. 341,West Codyside,AZ,52538,+1-114-921-6433x472 +232,Ryan,Johnson,M,1988-12-19,77848 Tara Ridge Apt. 979,New Amanda,MS,30271,(564)240-0825x478 +233,Thomas,Arroyo,M,1994-11-13,4930 Lopez Trail,East Jennifer,TN,29414,3894484631 +234,Dylan,Walsh,M,1993-04-23,3502 Amanda Estates,East Jenniferchester,DE,65195,475-705-1204x618 +235,Corey,Skinner,M,2003-08-24,36730 Jill Corner Suite 376,Larryborough,AZ,72535,743-503-1365 +236,Rebecca,Richards,F,1987-12-15,979 Kelli Forge,New Matthew,PA,08372,281-273-5857x306 +237,Brandy,Roach,F,1994-11-17,73928 Jessica Garden,Rochamouth,DE,39255,(708)620-9593x51863 +238,Kathleen,Arnold,F,2003-10-23,1181 Sharon Estate,North Jamestown,ME,64714,940.539.1037x1705 +239,Teresa,Perry,F,1992-01-03,480 Davenport Cliff Apt. 811,Amandaville,ID,82463,(861)957-6122x86852 +240,Krista,Garner,F,1995-04-23,004 Holmes Well,West Jeffrey,AK,90903,001-889-921-0752x245 +241,Danielle,Scott,F,2000-02-03,3157 Margaret Rest Suite 194,Lake Patrickmouth,KY,57426,001-139-060-4805x892 +242,Connie,Williams,F,2000-09-13,9981 Keith Key,North Ashleytown,CA,66275,+1-227-837-6938x983 +243,Deborah,Jordan,F,1988-11-02,66553 Brittney Brooks Apt. 597,Scottside,ND,20947,039-240-5147 +244,Evelyn,Singh,F,1986-03-15,879 Thomas Ridges Apt. 980,North James,IL,61444,4510463681 +245,Kari,Harper,F,2002-12-22,800 Alyssa Hill,East Michael,NM,31460,046.084.3256 +246,Jessica,Edwards,F,1988-03-23,29832 Janet Mount,Port Theresaland,VA,42115,(125)205-6647x42312 +247,Pamela,Salazar,F,1995-02-06,33051 Woods Mills Suite 526,North James,PA,02468,001-333-127-9757x366 +248,Roger,Cortez,M,1992-05-18,8808 Stephen Trail Suite 388,Lake Angela,NY,06962,644.726.4908 +249,Julie,Lucas,F,1989-01-08,98266 Angel Locks Suite 371,New Rebecca,OK,16694,751-868-9268 +250,Patricia,Barr,F,2002-09-16,22064 Kayla Lock Suite 123,Lake Alexanderport,SD,80190,(977)671-9903 +251,Donald,Fuller,M,2005-05-23,05020 Massey Greens,Williamsbury,ND,80597,+1-279-501-4556x168 +252,John,Martinez,M,2000-06-13,3390 Jessica Plaza,Webbchester,WY,38143,548.995.2997x8772 +253,Crystal,Roberts,F,1996-02-19,1396 Matthew Park,Alexville,SC,40841,(501)556-9902x3557 +254,Rebecca,Brewer,F,1988-03-04,857 Gutierrez Shoal Suite 495,Andrewmouth,VA,46847,001-405-682-9962x914 +255,Brandon,Wiley,M,2003-06-25,84215 Strickland Unions Apt. 078,West Timothyhaven,KS,13379,230.768.1040x91570 +256,Pamela,Reese,F,2004-08-11,3533 Amanda Springs Suite 422,North Cindy,GA,46417,249.321.4958 +257,Carlos,Ruiz,M,2001-10-06,66299 Vaughn Lock,West James,SD,10796,171.747.7332x945 +258,Michael,Ortega,M,1996-03-13,0171 Steven Drive Suite 992,Richardchester,NV,09797,(696)393-8276x15396 +259,Jessica,Cobb,F,1998-10-24,1971 Ford Oval,Thompsonshire,CO,78673,013-290-2278x469 +260,Christina,Maldonado,F,1989-08-26,465 Aguilar Plain Suite 240,South Brian,SD,47587,+1-036-965-6666x8327 +261,Janice,Middleton,F,2001-06-08,220 Alfred Roads,South Veronica,NY,55008,001-969-278-6876x532 +262,Adam,Jimenez,M,1988-12-05,89500 Bush Courts Apt. 128,Terrellmouth,AR,80464,189.490.5807 +263,Taylor,Berry,M,1995-11-05,442 Sandra Shoals,Anneton,DC,07266,+1-904-712-8144x2944 +264,Adrian,Rodriguez,M,2000-11-23,75243 Lauren Throughway Apt. 129,Mooreport,RI,31689,001-239-504-1027 +265,Eric,Reese,M,1995-03-12,6742 Graham Glen Suite 658,Blakeside,WV,57096,414-967-3938x525 +266,Michael,Decker,M,1990-01-01,75344 Andrew Common,Douglasfort,NY,93309,926-921-2447 +267,Robin,Thompson,F,1985-12-12,62712 Reynolds Plains Apt. 741,North Jessicamouth,MO,86073,001-642-569-0877x661 +268,Janice,Norris,F,1992-10-30,5546 Wendy Port,Lake Matthew,PA,38506,(063)461-5717 +269,Charles,Lee,M,2001-07-07,1847 Flowers Locks Suite 050,Lake Richard,NC,69067,001-829-310-2707x903 +270,Mark,Conway,M,1990-01-11,9111 Lauren Fields,Simmonsfort,ND,42999,001-982-530-9251x142 +271,Ann,Pearson,F,1996-03-02,723 Joseph Locks,East Heatherstad,NM,12038,083-318-1958x837 +272,Mary,Hill,F,1991-11-27,772 Sandra Causeway Apt. 364,Lake Katherine,OR,70933,078-113-7995 +273,Nicole,Villanueva,F,1992-07-11,36363 Brenda Causeway,East Chelsea,ME,60497,435.209.0421x7762 +274,Daniel,Phillips,M,2000-09-10,298 Miller Terrace Apt. 397,Ramirezchester,ID,43400,929.060.0780x686 +275,Rebecca,Nicholson,F,2001-09-12,0632 John Wells,New Evanview,NH,60117,+1-625-701-6580x464 +276,Logan,Johnston,M,1994-01-14,5085 Rodriguez Islands Suite 552,Janetmouth,DE,44400,(793)355-4864x01557 +277,Kelsey,Martinez,F,1990-12-14,4795 Dougherty Station Suite 137,West Haroldshire,DC,15184,(380)468-2756x7043 +278,John,Wade,M,1991-11-20,9242 Perez Islands Apt. 025,Port Christine,NE,24392,+1-223-105-9274x5238 +279,Mary,Spence,F,1995-12-23,841 Sullivan Mill,South Luketown,WI,43922,(492)975-1702x814 +280,Lisa,Robinson,F,1996-09-24,3983 Wang Extensions,Lake Ericashire,MD,64787,805.626.5650x4554 +281,Shannon,Miller,M,1998-09-15,426 Perry Street Suite 234,Port Valerie,WV,99606,646-287-9232 +282,Donna,Henry,F,1992-01-09,7873 Aaron Fort,Flowersview,VT,55178,(301)471-9597x9647 +283,Dr.,Jacqueline,F,2003-05-28,2572 Brian Island,Stephanietown,NY,10570,(219)285-5445 +284,Lauren,Morrow,F,1989-11-19,7652 Eric Fields Apt. 898,Marquezchester,MA,10514,+1-075-452-7985x2401 +285,Shannon,Thomas,F,1996-03-07,16110 Todd Camp,Lake Williamton,ID,09184,119.393.2501x24955 +286,Kathryn,Chandler,F,1992-01-27,90833 Jackson Shore Apt. 138,Wellschester,ND,14568,+1-663-836-1517x1827 +287,Michele,Hawkins,F,1992-01-08,47947 Richard Way,Lake Patricia,WA,48662,7167811266 +288,William,Figueroa,M,1999-07-16,3539 Powell Ford,South Kathy,NJ,99631,967-842-7114x773 +289,Chad,Garcia,M,2002-11-10,269 Hernandez Plains,North Karenmouth,GA,87282,(485)880-0616x7567 +290,Andrew,Hawkins,M,1991-03-28,762 Paul Skyway,Tracymouth,MN,74196,(647)969-5450x0902 +291,Hannah,Harmon,F,1987-03-11,1655 Brian Forest Apt. 491,Jonesburgh,AK,43245,(698)640-7905x696 +292,Brent,Freeman,M,1996-01-14,5294 Ryan Mews,Cobbfort,IN,06731,001-639-191-9541x987 +293,Angela,Colon,F,1993-03-01,5366 Zachary Ramp,Nicolestad,FL,65932,748.969.0835x72324 +294,Alexis,Robles,M,1986-08-06,603 Derek Forks,Hopkinsville,WI,64181,1594165162 +295,Laura,Mason,F,1994-07-28,8471 David Station Apt. 963,Robinsonland,IN,54027,+1-078-515-8673x4257 +296,Alex,Rasmussen,M,1996-02-27,0348 Danielle Ridges Suite 183,Priceside,WI,33994,343-275-6041 +297,Todd,Ruiz,M,1999-07-21,124 Bell Pines Suite 570,Davidsonville,NY,00904,(459)112-3829 +298,Ricky,Flores,M,1992-08-31,95431 Hunter Trail Suite 930,Leblancfurt,VA,61111,206.969.4215 +299,Keith,Smith,M,1992-01-21,713 Lee Throughway Suite 476,Lake Carolshire,ND,55332,204-439-7359x71072 +300,William,Sanders,M,1987-06-20,9411 Williams Viaduct,West Catherine,SC,93505,8964652809 +301,Christopher,Vasquez,M,1994-11-23,86241 Tiffany Mill,Campbellborough,VA,35001,(625)728-7032x0320 +302,Carla,Mcdonald,F,2005-11-05,7587 Daniel Roads Apt. 513,Whiteville,IL,87419,(089)261-3715 +303,Melanie,Becker,F,2005-04-14,520 Mariah Prairie Apt. 490,North Cindy,WV,96749,045-018-9616 +304,David,Wise,M,2003-05-13,66421 Laurie Rue,Mckeestad,CA,48664,(767)499-6165 +305,Jessica,Simmons,F,1994-05-19,3278 Warren Glens,Port Tim,CT,39876,(490)810-8186x61794 +306,Lauren,Mack,F,1994-09-28,2601 Janet Harbor Suite 794,Port Lisa,AR,79675,+1-168-006-1027x7697 +307,Valerie,Ward,F,1988-11-06,4122 Daniel Bridge Suite 037,Debraview,SC,25524,727.601.2277 +308,Scott,Richards,M,2002-07-09,050 Melanie Light Apt. 799,Yolandatown,MT,95477,(080)695-8146 +309,Audrey,Dean,F,1995-11-26,2437 Jesse Fields,Morganstad,NC,17692,001-665-729-3417 +310,Christina,Obrien,F,1997-05-30,433 Kidd Island,New Gregg,MO,08845,931-837-4550x84289 +311,Michael,House,M,1991-04-06,119 Garrison Corners,Williamville,GA,47901,001-787-125-5213 +312,Jennifer,Mack,F,1998-03-25,8214 Kari Island Suite 286,Taylorview,VT,68154,001-720-811-5562x606 +313,Margaret,Orr,F,1992-11-24,846 Erin Oval Apt. 550,Mcculloughstad,MD,84895,001-997-563-4108x562 +314,Kimberly,Lewis,F,2003-03-10,2008 Allen Springs,Valerieland,ME,82681,017-490-7539x989 +315,Elizabeth,Estrada,F,1999-08-16,68315 Lee Spur Apt. 266,North Pamelaport,LA,69478,864.976.7762x282 +316,Judith,Faulkner,F,1995-12-03,770 Raymond Islands Suite 961,New Billyland,WY,40249,(229)604-4327x0185 +317,Amanda,Olson,F,1999-11-09,6792 Wagner Lodge,South Michelle,SC,87598,658-074-1209x4818 +318,Tina,Weaver,F,1997-06-27,7801 Schmidt Vista Apt. 339,Lake Catherine,AZ,03550,608-564-1118x24224 +319,Christian,Farley,M,2005-11-10,200 Corey Crossroad,Scottside,AZ,31908,(886)140-5786 +320,Sarah,Mason,F,2002-04-29,2386 Peters Camp,Woodwardstad,DC,08388,465.398.4028 +321,Elizabeth,Foster,F,1996-11-11,4639 Pham Trail,Reidshire,IL,87306,795-020-9700x268 +322,Michele,Farmer,F,2001-01-17,1807 Gomez Station Suite 562,Cainshire,LA,25796,0453194337 +323,Mr.,Johnathan,M,1988-02-18,614 Snyder Oval,Arielfurt,AR,17310,938-430-8948 +324,Aaron,Simmons,M,2005-05-17,566 Erin Lodge Apt. 030,West Shane,FL,11223,+1-361-332-5411x0760 +325,Mark,Cook,M,1998-10-05,50583 Parsons Plains,Garrettmouth,AR,04871,120.704.9611 +326,Kristin,Phillips,F,2003-07-08,399 Patrick Square,Harveyborough,RI,60017,311-091-9392x845 +327,Nathaniel,Wallace,M,2003-03-05,49685 Nicole Springs Apt. 495,Port Zachary,DE,31615,+1-806-533-3153x7795 +328,Kylie,Rogers,F,1992-03-09,07303 Owens Ferry,Lake Lisa,ME,52970,+1-050-150-8124x7395 +329,Allen,Gonzalez,M,1998-08-03,583 Andrew Streets Suite 026,Nicoleborough,MN,48950,896.112.2338x65596 +330,David,Williams,M,2003-03-30,530 Ramirez Creek Suite 973,Kristenfort,DC,51372,872-558-7774x9690 +331,Stephanie,Hayes,F,2000-06-01,6925 Christopher Shore,South Jerry,MT,44590,(665)754-6027x341 +332,Bradley,Kirby,M,2004-05-25,311 Benjamin Fall Apt. 544,Kaylahaven,NJ,18571,001-044-566-9078x263 +333,Paul,Wells,M,1986-04-01,751 Jacob Springs Suite 377,Johnsonland,IA,97206,(553)666-8459x0902 +334,Troy,Rivera,M,1988-04-13,6636 Paul Mall Apt. 741,New Gregoryfort,AK,26584,001-643-348-1705x802 +335,Michelle,Wells,F,2001-06-11,8743 Douglas Centers Apt. 385,Suarezview,OR,38238,469-263-2967x629 +336,Michael,Williams,M,2003-01-30,841 Bowen Field,Port Angela,AR,14292,+1-567-243-8070x176 +337,Jennifer,Lee,F,1989-05-04,257 Carlos Orchard,Port Donaldfort,DC,02868,(186)210-4275 +338,Michelle,Stafford,F,1986-11-14,81647 Adam Springs,Mcfarlandbury,CA,55771,001-531-312-2068x155 +339,Taylor,Foster,F,1996-03-06,52065 Jason Fields,Joshuastad,VT,54384,+1-718-924-1956x252 +340,Stephen,Stewart,M,2000-07-01,9976 Harmon Mills,Alexandertown,CT,31485,001-910-257-4326 +341,Amanda,Mclean,F,1993-06-27,524 Kristin Bypass Suite 640,Lake Matthewville,VA,33051,685.270.1713x0232 +342,Christina,Coleman,F,1986-08-05,3471 Ward Isle,West Chelsea,DE,63677,+1-614-982-8246x747 +343,Kristina,Castillo,F,1999-01-05,30085 Sara Views Suite 567,Port Charles,WY,16816,001-236-458-7506x633 +344,Robert,Mccoy,M,1992-05-05,4972 Carrie Villages Suite 011,Sabrinabury,VT,68466,+1-264-488-6946x1195 +345,Daniel,Goodman,M,2005-03-19,70116 Pena Row,West Janeville,WV,59570,+1-230-234-6791x2141 +346,Destiny,Peterson,F,1994-12-18,100 Stephanie Prairie,Williamsberg,ME,68668,001-759-655-5535x669 +347,Shane,Drake,M,1999-12-23,209 Alyssa Village,Wrightview,UT,67991,050.505.7397x69156 +348,Todd,Alvarez,M,2001-02-07,64932 Walter Spurs Suite 027,Turnerfurt,UT,22528,001-783-332-1160x256 +349,Greg,Kent,M,1988-01-10,8633 Kelly Courts Apt. 931,Davidburgh,OR,41238,366.552.8993x160 +350,Nicole,Sweeney,F,1993-07-30,81497 Lewis Glens,Brownfort,OK,96531,+1-027-642-0865 +351,John,Bailey,M,2005-07-22,438 David Shore,Lindahaven,MN,21956,742-333-0591 +352,Kara,Landry,F,1986-04-25,6263 John Meadow Suite 261,Hancockfurt,NC,48646,117-830-9997 +353,Nichole,Bauer,F,2003-12-15,6492 Bryan Union,Lopezfort,NV,70810,(898)131-2920x8751 +354,Kenneth,Delgado,M,2004-02-03,118 Tammy Drive,Barrettberg,WV,38957,(975)859-8831x030 +355,Jennifer,Pierce,F,1998-10-24,71462 Jones Row Suite 359,Loristad,DE,57337,9314181861 +356,Brandon,Blankenship,M,1989-03-03,401 Tanya Isle,Port Gregorychester,SD,64676,(948)491-0256x25889 +357,Jennifer,Vargas,F,1995-04-21,226 Adams Valley Suite 539,South Scott,MN,38095,001-834-146-5111x312 +358,Patrick,Spencer,M,1997-08-29,682 Zachary Wells Suite 160,Rhondamouth,OH,98761,890.972.8321 +359,Casey,Gomez,M,1987-02-15,15381 Timothy Fort,New Phillipside,WV,68072,001-970-509-7545x105 +360,Adam,Jordan,M,1991-06-05,617 Kayla Forges Apt. 545,East Lisa,MI,58088,605-313-4026 +361,Erin,Johnson,F,1993-12-19,416 Tyler Rapid Apt. 686,Port Lauraland,AL,90211,5690674471 +362,Danielle,Hernandez,F,1990-12-24,436 Jasmine Station,Wayneville,NJ,83663,(260)432-6093 +363,Anthony,Russell,M,1995-08-17,56708 Brett Court Apt. 563,North Blake,OR,28285,(916)247-5541x108 +364,Carlos,Ward,M,1988-06-19,9534 Patrick Tunnel Apt. 910,Rhondafurt,OH,13429,001-954-738-2023x684 +365,James,Lawson,M,1994-01-09,9087 Le Forks,Phillipsburgh,HI,70436,242.403.3810 +366,Mackenzie,Compton,F,1989-07-16,426 Phillips Way Suite 053,Joshuaberg,NC,76950,001-649-837-3543 +367,Robert,Mullins,M,1996-06-21,527 Hunter Estates,Lopezport,NC,03259,(269)312-1637 +368,Tracy,Garcia,F,1989-07-15,916 Daniel Bridge Suite 023,Adamsside,SC,01732,(513)279-7245x72308 +369,Mark,Martinez,M,2002-08-27,86203 Ronald Curve,Jeremiahhaven,VT,15234,(131)451-9515 +370,Thomas,Huang,M,1988-07-08,9262 Mcdaniel Plaza,Port Joseph,LA,35287,+1-225-267-7119x642 +371,Wendy,White,F,1988-10-06,6952 Valdez Forge,South Amanda,SD,50914,689.313.5030x587 +372,Tammie,Brown,F,1998-07-26,247 Melissa Walk Suite 333,North Suzannechester,AK,56168,1917920252 +373,Angela,Carroll,F,1986-04-16,28476 Wallace Port,North Brianfurt,DC,21518,678-498-4362x4186 +374,Beth,Lewis,F,1995-02-07,891 Mcdonald Harbor,Margaretville,NY,26024,159-503-4281 +375,Linda,Avila,F,1999-03-18,0341 Cunningham Park Suite 005,West Tinamouth,MO,41719,001-215-681-8209 +376,John,Melton,M,2003-09-22,113 Aguirre Ports,Martinshire,OR,85880,001-572-545-9606x339 +377,Brittany,Burton,F,1990-09-12,48171 Geoffrey Green Apt. 955,East Kelseyberg,IL,58440,001-970-546-6927x589 +378,Michael,Hunter,M,2001-11-10,903 Castro Dale Apt. 629,North Paul,CA,61564,711.216.6365x15597 +379,Natalie,Wilson,F,1988-10-06,235 Huerta Springs Apt. 567,East Andrewmouth,ID,23583,461-476-8342 +380,Anna,Valenzuela,F,1996-12-07,56778 Martin Ridge Apt. 960,Patriciaville,NH,19456,502.727.5164x80727 +381,Kenneth,Johnson,M,2003-01-01,296 Jason Extension,Stephaniebury,IA,40735,+1-177-665-5868x5127 +382,Christopher,Larson,M,2004-06-14,649 Bullock Corners,Lake Christophertown,CO,98797,789-046-3378 +383,Christina,Harrison,F,2003-07-30,660 Casey Mission Apt. 446,Adamside,AK,49575,+1-955-296-3863x9609 +384,Todd,Myers,M,1989-02-03,26312 Welch Spurs,Burtonberg,WV,27208,609-209-8196 +385,Morgan,Lucero,F,1990-02-03,34383 Roman Isle Apt. 041,Burtonfurt,CO,60679,442-117-5361 +386,Joanne,Martin,F,1993-04-12,9015 Webb Plains Suite 284,Leetown,MT,20469,+1-130-523-1244x7315 +387,John,Lamb,M,1996-10-06,423 Clay Gateway Apt. 994,East Jenniferview,NJ,36109,966.395.5172x0849 +388,Charlene,Sanchez,F,1989-06-03,51050 Lewis Parks,East Carl,GA,29004,919.665.5330x770 +389,Jennifer,Martinez,F,2001-11-27,4090 Mitchell Streets,Port Samantha,NY,09604,644-556-1857 +390,Jennifer,Horton,F,1987-09-15,159 Jeffrey Stream Apt. 563,East Rachelbury,WY,90710,010.414.5964 +391,Tammy,Silva,F,1988-09-26,96718 Lane Prairie,Morrischester,IL,39329,331-170-3037x637 +392,Daniel,Garza,M,2005-07-23,472 Garcia Crescent Suite 679,Kimberlyville,DC,40759,271.130.7240x78754 +393,Krista,Gomez,F,2002-09-18,5074 Brandon Junction,Leeville,IN,80120,(103)131-0094x3181 +394,Sonya,Lyons,F,1994-01-14,47323 Keith Pine,Clintonport,MS,40520,(122)572-0765 +395,William,Ibarra,M,2001-04-27,57907 Kennedy Canyon Apt. 438,Karimouth,SC,44498,(584)745-7054x5897 +396,Michael,Chandler,M,2001-03-16,257 Becky Ridge Apt. 313,Grayland,NM,71924,001-824-556-9644x309 +397,Barbara,Pope,F,1990-02-13,1072 Edward Vista Suite 247,Lake Alexis,IN,78236,4065004254 +398,Jonathan,Mullen,M,1991-10-25,236 Miller Fields Apt. 536,Port Corey,IA,41229,592.342.6834x414 +399,Lori,Gardner,F,1996-03-17,2875 Jennings Island Apt. 766,Port Anthony,CA,18927,+1-985-298-9406x260 diff --git a/tests/data/StudentMajor.csv b/tests/data/StudentMajor.csv new file mode 100644 index 000000000..644a46492 --- /dev/null +++ b/tests/data/StudentMajor.csv @@ -0,0 +1,227 @@ +student_id,dept,declare_date +100,BIOL,2010-01-10 +102,CS,2019-01-13 +103,PHYS,2018-10-04 +104,CS,2010-11-04 +105,CS,2018-11-20 +107,MATH,2020-01-04 +108,PHYS,2012-09-26 +111,MATH,2001-04-19 +112,MATH,2000-07-12 +113,PHYS,2000-01-02 +114,MATH,2004-06-01 +115,BIOL,2006-11-19 +116,CS,2002-04-14 +117,PHYS,2002-08-13 +118,CS,2015-12-29 +120,MATH,2015-03-18 +121,BIOL,2010-01-05 +122,MATH,2006-11-17 +123,PHYS,2007-01-19 +124,MATH,2002-08-03 +125,CS,2004-12-02 +126,PHYS,2012-01-26 +127,CS,2013-04-17 +128,MATH,2001-03-10 +129,BIOL,2001-02-08 +130,CS,2019-10-27 +131,MATH,2007-07-10 +132,PHYS,2002-11-23 +134,CS,2000-04-10 +135,MATH,2001-06-24 +136,MATH,2014-01-09 +137,CS,2011-09-26 +139,CS,2019-08-21 +141,BIOL,2020-06-24 +142,CS,2000-01-02 +143,PHYS,2004-12-03 +144,CS,2009-12-05 +147,CS,2002-08-30 +148,PHYS,2014-04-18 +150,BIOL,2011-11-07 +151,PHYS,2003-07-14 +153,PHYS,2020-09-08 +156,PHYS,2018-07-10 +159,PHYS,2017-12-07 +160,MATH,2005-10-18 +161,MATH,2005-08-29 +162,MATH,2007-08-04 +163,BIOL,2015-09-17 +164,CS,2013-11-20 +165,CS,2008-09-25 +166,BIOL,2006-09-03 +167,MATH,2005-11-05 +168,PHYS,2004-07-07 +169,PHYS,2013-10-08 +171,PHYS,2016-12-25 +172,MATH,2005-07-17 +174,PHYS,2001-12-04 +175,CS,2018-10-22 +176,MATH,1999-10-29 +177,BIOL,2020-05-28 +178,PHYS,2002-04-10 +181,BIOL,2005-12-04 +182,PHYS,2000-02-18 +183,PHYS,2003-10-13 +184,MATH,1999-03-07 +185,CS,2011-03-27 +187,PHYS,2012-11-18 +188,PHYS,2018-05-03 +189,BIOL,2017-08-06 +191,MATH,2001-06-13 +194,CS,2010-08-05 +195,BIOL,2005-04-21 +196,CS,2020-11-07 +197,BIOL,2016-12-20 +198,CS,2015-11-19 +200,CS,2005-06-20 +203,BIOL,2006-01-22 +204,MATH,2018-05-29 +205,PHYS,2015-02-13 +206,CS,2016-01-16 +207,CS,2010-12-24 +210,BIOL,2011-02-17 +211,PHYS,2020-01-17 +212,BIOL,2018-01-04 +213,MATH,2003-09-10 +215,BIOL,2001-04-14 +216,MATH,2013-12-07 +217,PHYS,2013-07-18 +218,PHYS,2020-04-13 +219,MATH,2011-10-19 +220,PHYS,2001-05-30 +221,MATH,2018-05-14 +223,BIOL,2001-08-29 +224,PHYS,2003-04-30 +225,PHYS,2016-08-07 +226,PHYS,2009-02-23 +228,CS,2002-06-08 +230,MATH,2003-01-05 +231,MATH,2015-12-20 +232,CS,2006-11-05 +233,PHYS,2000-10-01 +234,CS,2019-06-20 +235,PHYS,2017-05-23 +236,BIOL,2010-04-05 +237,CS,1999-10-08 +238,CS,2006-08-16 +239,MATH,2008-11-11 +240,MATH,2007-07-22 +241,MATH,2012-04-14 +242,PHYS,2011-03-06 +243,MATH,2001-04-24 +244,CS,2004-05-15 +245,CS,2008-10-19 +246,PHYS,2001-07-18 +248,CS,2017-03-08 +249,MATH,2018-07-30 +250,BIOL,2007-03-19 +251,CS,2016-08-13 +252,BIOL,2019-10-19 +253,CS,2016-01-06 +254,PHYS,2009-08-16 +255,BIOL,2012-08-01 +256,PHYS,2020-01-19 +257,MATH,2000-12-04 +258,BIOL,2017-07-29 +259,PHYS,2002-10-09 +260,BIOL,2018-10-30 +261,BIOL,2015-01-10 +262,BIOL,2007-12-14 +263,MATH,2000-01-08 +264,CS,2000-02-06 +265,PHYS,2010-07-03 +267,PHYS,2013-05-04 +268,PHYS,2007-11-17 +269,PHYS,2005-10-27 +270,BIOL,2010-05-20 +272,CS,2001-01-08 +273,MATH,2003-09-28 +274,CS,2005-12-13 +275,BIOL,2017-08-12 +276,PHYS,2010-03-20 +277,PHYS,2001-02-13 +278,CS,2007-01-07 +279,MATH,2015-10-17 +280,PHYS,2001-06-25 +282,CS,2018-03-09 +283,CS,2019-10-03 +285,BIOL,2000-03-15 +286,MATH,2010-10-08 +287,MATH,2001-05-29 +288,PHYS,2013-02-28 +290,PHYS,2019-05-09 +292,MATH,2019-11-03 +293,BIOL,2001-09-28 +295,MATH,2017-10-05 +296,CS,2015-04-16 +299,PHYS,2003-05-28 +301,PHYS,2008-03-15 +302,MATH,2000-06-02 +304,MATH,2002-07-17 +305,PHYS,2000-03-18 +307,BIOL,2015-11-24 +308,MATH,2016-04-09 +311,BIOL,2006-08-31 +312,PHYS,2010-12-01 +313,CS,2013-09-06 +314,PHYS,2015-04-02 +315,BIOL,2009-04-28 +318,PHYS,2006-10-01 +319,CS,1999-09-24 +320,MATH,2000-11-18 +321,PHYS,1999-11-24 +322,BIOL,2005-09-03 +323,BIOL,2017-03-05 +324,CS,2019-09-10 +325,MATH,2011-11-28 +326,MATH,1999-08-13 +328,CS,2017-10-19 +329,CS,2015-05-29 +332,PHYS,2000-10-09 +334,MATH,2012-03-04 +336,PHYS,2011-11-02 +337,MATH,2003-04-06 +338,PHYS,2013-08-15 +340,CS,2013-07-10 +342,PHYS,2017-09-12 +343,PHYS,2003-09-09 +344,PHYS,2002-12-07 +345,CS,2013-11-25 +346,BIOL,2003-01-06 +348,PHYS,2019-12-13 +349,PHYS,2011-07-06 +350,CS,2010-12-20 +351,CS,2005-08-03 +352,MATH,2010-09-04 +353,PHYS,2013-11-07 +357,BIOL,2000-12-20 +358,CS,2007-02-07 +360,BIOL,2006-11-23 +362,BIOL,2002-02-17 +364,BIOL,2019-01-11 +365,BIOL,1999-05-05 +366,MATH,2006-09-23 +367,CS,2013-01-20 +368,CS,2017-03-30 +369,BIOL,2018-04-30 +370,PHYS,2000-07-22 +371,CS,1999-07-05 +372,CS,2007-07-03 +373,MATH,2000-12-07 +376,CS,2001-08-10 +378,MATH,2000-12-05 +379,PHYS,2003-04-24 +382,PHYS,2013-12-03 +383,PHYS,2005-02-22 +385,MATH,2008-08-12 +386,PHYS,2000-06-27 +390,CS,2009-09-08 +391,MATH,2010-11-24 +392,CS,2019-07-01 +393,CS,2007-04-24 +394,BIOL,2008-12-12 +395,PHYS,2003-06-01 +396,MATH,2019-08-16 +398,MATH,2012-07-14 +399,CS,2015-04-16 diff --git a/tests/data/Term.csv b/tests/data/Term.csv new file mode 100644 index 000000000..91c3400ae --- /dev/null +++ b/tests/data/Term.csv @@ -0,0 +1,19 @@ +term_year,term +2015,Spring +2015,Summer +2015,Fall +2016,Spring +2016,Summer +2016,Fall +2017,Spring +2017,Summer +2017,Fall +2018,Spring +2018,Summer +2018,Fall +2019,Spring +2019,Summer +2019,Fall +2020,Spring +2020,Summer +2020,Fall diff --git a/tests/schema.py b/tests/schema.py index 18ae09fda..1fd187637 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -146,7 +146,7 @@ def make(self, key): populate with random data """ from datetime import date, timedelta - users = User().fetch()['username'] + users = [None, None] + list(User().fetch()['username']) random.seed('Amazing Seed') self.insert( dict(key, @@ -155,7 +155,6 @@ def make(self, key): username=random.choice(users)) for experiment_id in range(self.fake_experiments_per_subject)) - @schema class Trial(dj.Imported): definition = """ # a trial within an experiment @@ -174,9 +173,7 @@ class Condition(dj.Part): """ def make(self, key): - """ - populate with random data (pretend reading from raw files) - """ + """ populate with random data (pretend reading from raw files) """ random.seed('Amazing Seed') trial = self.Condition() for trial_id in range(10): @@ -322,6 +319,7 @@ class IndexRich(dj.Manual): index (first_date, value) """ + # Schema for issue 656 @schema class ThingA(dj.Manual): @@ -348,3 +346,36 @@ class ThingC(dj.Manual): -> [unique, nullable] ThingB """ + +@schema +class Parent(dj.Lookup): + definition = """ + parent_id: int + --- + name: varchar(30) + """ + contents = [(1, 'Joe')] + + +@schema +class Child(dj.Lookup): + definition = """ + -> Parent + child_id: int + --- + name: varchar(30) + """ + contents = [(1, 12, 'Dan')] + +# Related to issue #886 (8), #883 (5) +@schema +class ComplexParent(dj.Lookup): + definition = '\n'.join(['parent_id_{}: int'.format(i+1) for i in range(8)]) + contents = [tuple(i for i in range(8))] + + +@schema +class ComplexChild(dj.Lookup): + definition = '\n'.join(['-> ComplexParent'] + ['child_id_{}: int'.format(i+1) + for i in range(1)]) + contents = [tuple(i for i in range(9))] diff --git a/tests/schema_university.py b/tests/schema_university.py new file mode 100644 index 000000000..bb2675f8d --- /dev/null +++ b/tests/schema_university.py @@ -0,0 +1,111 @@ +import datajoint as dj + +schema = dj.Schema() + + +@schema +class Student(dj.Manual): + definition = """ + student_id : int unsigned # university-wide ID number + --- + first_name : varchar(40) + last_name : varchar(40) + sex : enum('F', 'M', 'U') + date_of_birth : date + home_address : varchar(120) # mailing street address + home_city : varchar(60) # mailing address + home_state : char(2) # US state acronym: e.g. OH + home_zip : char(10) # zipcode e.g. 93979-4979 + home_phone : varchar(20) # e.g. 414.657.6883x0881 + """ + + +@schema +class Department(dj.Manual): + definition = """ + dept : varchar(6) # abbreviated department name, e.g. BIOL + --- + dept_name : varchar(200) # full department name + dept_address : varchar(200) # mailing address + dept_phone : varchar(20) + """ + + +@schema +class StudentMajor(dj.Manual): + definition = """ + -> Student + --- + -> Department + declare_date : date # when student declared her major + """ + + +@schema +class Course(dj.Manual): + definition = """ + -> Department + course : int unsigned # course number, e.g. 1010 + --- + course_name : varchar(200) # e.g. "Neurobiology of Sensation and Movement." + credits : decimal(3,1) # number of credits earned by completing the course + """ + + +@schema +class Term(dj.Manual): + definition = """ + term_year : year + term : enum('Spring', 'Summer', 'Fall') + """ + + +@schema +class Section(dj.Manual): + definition = """ + -> Course + -> Term + section : char(1) + --- + auditorium : varchar(12) + """ + + +@schema +class CurrentTerm(dj.Manual): + definition = """ + omega=0 : tinyint + --- + -> Term + """ + + +@schema +class Enroll(dj.Manual): + definition = """ + -> Student + -> Section + """ + + +@schema +class LetterGrade(dj.Lookup): + definition = """ + grade : char(2) + --- + points : decimal(3,2) + """ + contents = [ + ['A', 4.00], ['A-', 3.67], + ['B+', 3.33], ['B', 3.00], ['B-', 2.67], + ['C+', 2.33], ['C', 2.00], ['C-', 1.67], + ['D+', 1.33], ['D', 1.00], ['F', 0.00]] + + +@schema +class Grade(dj.Manual): + definition = """ + -> Enroll + --- + -> LetterGrade + """ \ No newline at end of file diff --git a/tests/test_adapted_attributes.py b/tests/test_adapted_attributes.py index 2769e5bd0..17acc897d 100644 --- a/tests/test_adapted_attributes.py +++ b/tests/test_adapted_attributes.py @@ -3,7 +3,7 @@ from itertools import zip_longest from nose.tools import assert_true, assert_equal, assert_dict_equal from . import schema_adapted as adapted -from .schema_adapted import graph, layout_to_filepath +from .schema_adapted import graph def test_adapted_type(c=adapted.Connectivity()): diff --git a/tests/test_aggr_regressions.py b/tests/test_aggr_regressions.py new file mode 100644 index 000000000..1cadfdf34 --- /dev/null +++ b/tests/test_aggr_regressions.py @@ -0,0 +1,105 @@ +""" +Regression tests for issues 386, 449, 484, and 558 — all related to processing complex aggregations and projections. +""" + +import itertools +from nose.tools import assert_equal, raises +import datajoint as dj +from . import PREFIX, CONN_INFO + +schema = dj.Schema(PREFIX + '_aggr_regress', connection=dj.conn(**CONN_INFO)) + +# --------------- ISSUE 386 ------------------- +# Issue 386 resulted from the loss of aggregated attributes when the aggregation was used as the restrictor +# Q & (R.aggr(S, n='count(*)') & 'n=2') +# Error: Unknown column 'n' in HAVING + + +@schema +class R(dj.Lookup): + definition = """ + r : char(1) + """ + contents = zip('ABCDFGHIJKLMNOPQRST') + + +@schema +class Q(dj.Lookup): + definition = """ + -> R + """ + contents = zip('ABCDFGH') + + +@schema +class S(dj.Lookup): + definition = """ + -> R + s : int + """ + contents = itertools.product('ABCDF', range(10)) + + +def test_issue386(): + result = R.aggr(S, n='count(*)') & 'n=10' + result = Q & result + result.fetch() + +# ---------------- ISSUE 449 ------------------ +# Issue 449 arises from incorrect group by attributes after joining with a dj.U() + + +def test_issue449(): + result = dj.U('n') * R.aggr(S, n='max(s)') + result.fetch() + + +# ---------------- ISSUE 484 ----------------- +# Issue 484 +def test_issue484(): + q = dj.U().aggr(S, n='max(s)') + n = q.fetch('n') + n = q.fetch1('n') + q = dj.U().aggr(S, n='avg(s)') + result = dj.U().aggr(q, m='max(n)') + result.fetch() + +# --------------- ISSUE 558 ------------------ +# Issue 558 resulted from the fact that DataJoint saves subqueries and often combines a restriction followed +# by a projection into a single SELECT statement, which in several unusual cases produces unexpected results. + + +@schema +class A(dj.Lookup): + definition = """ + id: int + """ + contents = zip(range(10)) + + +@schema +class B(dj.Lookup): + definition = """ + -> A + id2: int + """ + contents = zip(range(5), range(5, 10)) + + +@schema +class X(dj.Lookup): + definition = """ + id: int + """ + contents = zip(range(10)) + + +def test_issue558_part1(): + q = (A-B).proj(id2='3') + assert_equal(len(A - B), len(q)) + + +def test_issue558_part2(): + d = dict(id=3, id2=5) + assert_equal(len(X & d), len((X & d).proj(id2='3'))) + diff --git a/tests/test_alter.py b/tests/test_alter.py index b188bba0d..7a40ee822 100644 --- a/tests/test_alter.py +++ b/tests/test_alter.py @@ -1,5 +1,4 @@ from nose.tools import assert_equal, assert_not_equal - from .schema import * @@ -30,14 +29,13 @@ class Experiment(dj.Imported): def test_alter(): - original = schema.connection.query("SHOW CREATE TABLE " + Experiment.full_table_name).fetchone()[1] Experiment.definition = Experiment.definition1 Experiment.alter(prompt=False) altered = schema.connection.query("SHOW CREATE TABLE " + Experiment.full_table_name).fetchone()[1] + assert_not_equal(original, altered) Experiment.definition = Experiment.original_definition Experiment().alter(prompt=False) restored = schema.connection.query("SHOW CREATE TABLE " + Experiment.full_table_name).fetchone()[1] + assert_not_equal(altered, restored) assert_equal(original, restored) - assert_not_equal(original, altered) - diff --git a/tests/test_autopopulate.py b/tests/test_autopopulate.py index d3a382b8c..c9cb7f97d 100644 --- a/tests/test_autopopulate.py +++ b/tests/test_autopopulate.py @@ -34,7 +34,7 @@ def test_populate(self): # test restricted populate assert_false(self.trial, 'table already filled?') - restriction = dict(subject_id=self.subject.proj().fetch()['subject_id'][0]) + restriction = self.subject.proj(animal='subject_id').fetch('KEY')[0] d = self.trial.connection.dependencies d.load() self.trial.populate(restriction) diff --git a/tests/test_blob_migrate.py b/tests/test_blob_migrate.py index b76bc6790..54b0d75d0 100644 --- a/tests/test_blob_migrate.py +++ b/tests/test_blob_migrate.py @@ -1,5 +1,4 @@ -from nose.tools import assert_true, assert_false, assert_equal, \ - assert_list_equal, raises +from nose.tools import assert_equal, raises import datajoint as dj import os diff --git a/tests/test_cascading_delete.py b/tests/test_cascading_delete.py index 03aeeb2e2..6988d432a 100644 --- a/tests/test_cascading_delete.py +++ b/tests/test_cascading_delete.py @@ -1,6 +1,7 @@ -from nose.tools import assert_false, assert_true +from nose.tools import assert_false, assert_true, assert_equal import datajoint as dj from .schema_simple import A, B, D, E, L +from .schema import ComplexChild, ComplexParent class TestDelete: @@ -54,14 +55,12 @@ def test_delete_tree_restricted(): (E() & rel) or (E.F() & rel), 'incomplete delete') - assert_true( - len(A()) == rest['A'] and - len(B()) == rest['B'] and - len(B.C()) == rest['C'] and - len(D()) == rest['D'] and - len(E()) == rest['E'] and - len(E.F()) == rest['F'], - 'incorrect restricted delete') + assert_equal(len(A()), rest['A'], 'invalid delete restriction') + assert_equal(len(B()), rest['B'], 'invalid delete restriction') + assert_equal(len(B.C()), rest['C'], 'invalid delete restriction') + assert_equal(len(D()), rest['D'], 'invalid delete restriction') + assert_equal(len(E()), rest['E'], 'invalid delete restriction') + assert_equal(len(E.F()), rest['F'], 'invalid delete restriction') @staticmethod def test_delete_lookup(): @@ -80,3 +79,20 @@ def test_delete_lookup_restricted(): deleted_count = len(rel) rel.delete() assert_true(len(L()) == original_count - deleted_count) + + @staticmethod + def test_delete_complex_keys(): + # https://github.com/datajoint/datajoint-python/issues/883 + # https://github.com/datajoint/datajoint-python/issues/886 + assert_false(dj.config['safemode'], 'safemode must be off for testing') + parent_key_count = 8 + child_key_count = 1 + restriction = dict({'parent_id_{}'.format(i+1): i + for i in range(parent_key_count)}, + **{'child_id_{}'.format(i+1): (i + parent_key_count) + for i in range(child_key_count)}) + assert len(ComplexParent & restriction) == 1, 'Parent record missing' + assert len(ComplexChild & restriction) == 1, 'Child record missing' + (ComplexParent & restriction).delete() + assert len(ComplexParent & restriction) == 0, 'Parent record was not deleted' + assert len(ComplexChild & restriction) == 0, 'Child record was not deleted' diff --git a/tests/test_external_class.py b/tests/test_external_class.py index f8826fe42..e2018dd5b 100644 --- a/tests/test_external_class.py +++ b/tests/test_external_class.py @@ -1,4 +1,4 @@ -from nose.tools import assert_true, assert_list_equal, raises +from nose.tools import assert_true, assert_list_equal from numpy.testing import assert_almost_equal import datajoint as dj from . import schema_external as modu @@ -46,8 +46,10 @@ def test_populate(): assert_list_equal(list(img.shape), list(dimensions)) assert_almost_equal(img, -neg) image.delete() + dj.errors._switch_filepath_types(True) for external_table in image.external.values(): external_table.delete(display_progress=False, delete_external_files=True) + dj.errors._switch_filepath_types(False) diff --git a/tests/test_fetch.py b/tests/test_fetch.py index edd3772a0..fd4adb417 100644 --- a/tests/test_fetch.py +++ b/tests/test_fetch.py @@ -187,7 +187,7 @@ def test_limit_warning(self): def test_len(self): """Tests __len__""" - assert_true(len(self.lang.fetch()) == len(self.lang), '__len__ is not behaving properly') + assert_equal(len(self.lang.fetch()), len(self.lang), '__len__ is not behaving properly') @raises(dj.DataJointError) def test_fetch1_step2(self): @@ -222,24 +222,35 @@ def test_nullable_numbers(self): def test_fetch_format(self): """test fetch_format='frame'""" - dj.config['fetch_format'] = 'frame' - # test if lists are both dicts - list1 = sorted(self.subject.proj().fetch(as_dict=True), key=itemgetter('subject_id')) - list2 = sorted(self.subject.fetch(dj.key), key=itemgetter('subject_id')) - for l1, l2 in zip(list1, list2): - assert_dict_equal(l1, l2, 'Primary key is not returned correctly') - - # tests if pandas dataframe - tmp = self.subject.fetch(order_by='subject_id') - assert_true(isinstance(tmp, pandas.DataFrame)) - tmp = tmp.to_records() - - subject_notes, key, real_id = self.subject.fetch('subject_notes', dj.key, 'real_id') - - np.testing.assert_array_equal(sorted(subject_notes), sorted(tmp['subject_notes'])) - np.testing.assert_array_equal(sorted(real_id), sorted(tmp['real_id'])) - list1 = sorted(key, key=itemgetter('subject_id')) - for l1, l2 in zip(list1, list2): - assert_dict_equal(l1, l2, 'Primary key is not returned correctly') - # revert configuration of fetch format - dj.config['fetch_format'] = 'array' + with dj.config(fetch_format='frame'): + # test if lists are both dicts + list1 = sorted(self.subject.proj().fetch(as_dict=True), key=itemgetter('subject_id')) + list2 = sorted(self.subject.fetch(dj.key), key=itemgetter('subject_id')) + for l1, l2 in zip(list1, list2): + assert_dict_equal(l1, l2, 'Primary key is not returned correctly') + + # tests if pandas dataframe + tmp = self.subject.fetch(order_by='subject_id') + assert_true(isinstance(tmp, pandas.DataFrame)) + tmp = tmp.to_records() + + subject_notes, key, real_id = self.subject.fetch('subject_notes', dj.key, 'real_id') + + np.testing.assert_array_equal(sorted(subject_notes), sorted(tmp['subject_notes'])) + np.testing.assert_array_equal(sorted(real_id), sorted(tmp['real_id'])) + list1 = sorted(key, key=itemgetter('subject_id')) + for l1, l2 in zip(list1, list2): + assert_dict_equal(l1, l2, 'Primary key is not returned correctly') + + def test_key_fetch1(self): + """test KEY fetch1 - issue #976""" + with dj.config(fetch_format="array"): + k1 = (self.subject & 'subject_id=10').fetch1('KEY') + with dj.config(fetch_format="frame"): + k2 = (self.subject & 'subject_id=10').fetch1('KEY') + assert_equal(k1, k2) + + def test_same_secondary_attribute(self): + children = (schema.Child * schema.Parent().proj()).fetch()['name'] + assert len(children) == 1 + assert children[0] == 'Dan' diff --git a/tests/test_filepath.py b/tests/test_filepath.py index b54c82547..e01004827 100644 --- a/tests/test_filepath.py +++ b/tests/test_filepath.py @@ -13,6 +13,7 @@ def setUp(self): def test_path_match(store="repo"): """ test file path matches and empty file""" + dj.errors._switch_filepath_types(True) ext = schema.external[store] stage_path = dj.config['stores'][store]['stage'] @@ -25,11 +26,11 @@ def test_path_match(store="repo"): # put the file uuid = ext.upload_filepath(str(managed_file)) - #remove + # remove managed_file.unlink() assert_false(managed_file.exists()) - #check filepath + # check filepath assert_equal( (ext & {'hash': uuid}).fetch1('filepath'), str(managed_file.relative_to(stage_path).as_posix())) @@ -41,10 +42,13 @@ def test_path_match(store="repo"): # cleanup ext.delete(delete_external_files=True) + dj.errors._switch_filepath_types(False) def test_filepath(store="repo"): """ test file management """ + dj.errors._switch_filepath_types(True) + ext = schema.external[store] stage_path = dj.config['stores'][store]['stage'] filename = 'picture.dat' @@ -82,6 +86,8 @@ def test_filepath(store="repo"): ext.delete(delete_external_files=True) assert_false(ext.exists(ext._make_external_filepath(str(Path(relpath, filename))))) + dj.errors._switch_filepath_types(False) + def test_filepath_s3(): """ test file management with s3 """ diff --git a/tests/test_privileges.py b/tests/test_privileges.py index 85c899be4..2e483aefa 100644 --- a/tests/test_privileges.py +++ b/tests/test_privileges.py @@ -1,6 +1,5 @@ from nose.tools import assert_true, raises import datajoint as dj -from os import environ from . import schema, CONN_INFO namespace = locals() @@ -12,7 +11,7 @@ class TestUnprivileged: def setup_class(cls): """A connection with only SELECT privilege to djtest schemas""" cls.connection = dj.conn(host=CONN_INFO['host'], user='djview', password='djview', - reset=True) + reset=True) @raises(dj.DataJointError) def test_fail_create_schema(self): diff --git a/tests/test_relation.py b/tests/test_relation.py index 8c9c4ee69..37adab7f5 100644 --- a/tests/test_relation.py +++ b/tests/test_relation.py @@ -99,12 +99,13 @@ def test_insert_select(self): assert_equal(len(schema.TTest2()), len(schema.TTest())) original_length = len(self.subject) - self.subject.insert(self.subject.proj( - 'real_id', 'date_of_birth', 'subject_notes', subject_id='subject_id+1000', species='"human"')) + elements = self.subject.proj(..., s='subject_id') + elements = elements.proj('real_id', 'date_of_birth', 'subject_notes', subject_id='s+1000', species='"human"') + self.subject.insert(elements, ignore_extra_fields=True) assert_equal(len(self.subject), 2*original_length) def test_insert_pandas_roundtrip(self): - ''' ensure fetched frames can be inserted ''' + """ ensure fetched frames can be inserted """ schema.TTest2.delete() n = len(schema.TTest()) assert_true(n > 0) @@ -115,10 +116,10 @@ def test_insert_pandas_roundtrip(self): assert_equal(len(schema.TTest2()), n) def test_insert_pandas_userframe(self): - ''' + """ ensure simple user-created frames (1 field, non-custom index) can be inserted without extra index adjustment - ''' + """ schema.TTest2.delete() n = len(schema.TTest()) assert_true(n > 0) diff --git a/tests/test_relation_u.py b/tests/test_relation_u.py index 24b5a6d24..281b67d38 100644 --- a/tests/test_relation_u.py +++ b/tests/test_relation_u.py @@ -5,7 +5,7 @@ class TestU: """ - Test base relations: insert, delete + Test tables: insert, delete """ @classmethod diff --git a/tests/test_relational_operand.py b/tests/test_relational_operand.py index 090eba80f..43d3ee943 100644 --- a/tests/test_relational_operand.py +++ b/tests/test_relational_operand.py @@ -8,7 +8,7 @@ import datajoint as dj from .schema_simple import A, B, D, E, F, L, DataA, DataB, TTestUpdate, IJ, JI, ReservedWord -from .schema import Experiment, TTest3 +from .schema import Experiment, TTest3, Trial, Ephys def setup(): @@ -57,7 +57,8 @@ def test_rename(): y = x.proj(j='i') assert_equal(len(y), len(B() & 'id_a in (1,2,3,4)'), 'incorrect projection of restriction') - assert_equal(len(y & 'j in (3,4,5,6)'), len(B() & 'id_a in (3,4)'), + z = y & 'j in (3, 4, 5, 6)' + assert_equal(len(z), len(B() & 'id_a in (3,4)'), 'incorrect nested subqueries') @staticmethod @@ -110,8 +111,7 @@ def test_join(): x = B().proj(i='id_a') # rename the common attribute to achieve full cartesian product y = D() rel = x * y - assert_equal(len(rel), len(x) * len(y), - 'incorrect join') + assert_equal(len(rel), len(x) * len(y), 'incorrect join') assert_equal(set(x.heading.names).union(y.heading.names), set(rel.heading.names), 'incorrect join heading') assert_equal(set(x.primary_key).union(y.primary_key), set(rel.primary_key), @@ -176,11 +176,26 @@ def test_project(): @staticmethod def test_union(): - x = set(zip(*IJ.fetch('i','j'))) - y = set(zip(*JI.fetch('i','j'))) + x = set(zip(*IJ.fetch('i', 'j'))) + y = set(zip(*JI.fetch('i', 'j'))) assert_true(len(x) > 0 and len(y) > 0 and len(IJ() * JI()) < len(x)) # ensure the IJ and JI are non-trivial - z = set(zip(*(IJ + JI).fetch('i','j'))) # union + z = set(zip(*(IJ + JI).fetch('i', 'j'))) # union assert_set_equal(x.union(y), z) + assert_equal(len(IJ + JI), len(z)) + + @staticmethod + @raises(dj.DataJointError) + def test_outer_union_fail(): + """Union of two tables with different primary keys raises an error.""" + A() + B() + + @staticmethod + def test_outer_union_fail(): + """Union of two tables with different primary keys raises an error.""" + t = Trial + Ephys + t.fetch() + assert_set_equal(set(t.heading.names), set(Trial.heading.names) | set(Ephys.heading.names)) + len(t) @staticmethod def test_preview(): @@ -225,7 +240,9 @@ def test_aggregate(): @staticmethod def test_aggr(): x = B.aggr(B.C) - assert_equal(len(x), len(B() & B.C())) + l1 = len(x) + l2 = len(B & B.C) + assert_equal(l1, l2) x = B().aggr(B.C(), keep_all_rows=True) assert_equal(len(x), len(B())) # test LEFT join @@ -273,6 +290,15 @@ def test_pandas_fetch_and_restriction(): assert_true(isinstance(df, pandas.DataFrame)) assert_equal(len(E & q), len(E & df)) + @staticmethod + def test_restriction_by_null(): + assert_true(len(Experiment & 'username is null') > 0) + assert_true(len(Experiment & 'username is not null') > 0) + + @staticmethod + def test_restriction_between(): # see issue + assert_true(len(Experiment & 'username between "S" and "Z"') < len(Experiment())) + @staticmethod def test_restrictions_by_lists(): x = D() @@ -354,7 +380,8 @@ def test_date(): @staticmethod def test_join_project(): """Test join of projected relations with matching non-primary key""" - assert_true(len(DataA.proj() * DataB.proj()) == len(DataA()) == len(DataB()), + q = DataA.proj() * DataB.proj() + assert_true(len(q) == len(DataA()) == len(DataB()), "Join of projected relations does not work") @staticmethod diff --git a/tests/test_schema.py b/tests/test_schema.py index 2d868b60d..f7e19356d 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -26,14 +26,27 @@ def test_schema_size_on_disk(): assert_true(isinstance(number_of_bytes, int)) +def test_schema_list(): + schemas = dj.list_schemas() + assert_true(schema.schema.database in schemas) + + +@raises(dj.errors.AccessError) +def test_drop_unauthorized(): + info_schema = dj.schema('information_schema') + info_schema.drop() + + def test_namespace_population(): for name, rel in getmembers(schema, relation_selector): assert_true(hasattr(schema_empty, name), '{name} not found in schema_empty'.format(name=name)) - assert_true(rel.__base__ is getattr(schema_empty, name).__base__, 'Wrong tier for {name}'.format(name=name)) + assert_true(rel.__base__ is getattr(schema_empty, name).__base__, + 'Wrong tier for {name}'.format(name=name)) for name_part in dir(rel): if name_part[0].isupper() and part_selector(getattr(rel, name_part)): - assert_true(getattr(rel, name_part).__base__ is dj.Part, 'Wrong tier for {name}'.format(name=name_part)) + assert_true(getattr(rel, name_part).__base__ is dj.Part, + 'Wrong tier for {name}'.format(name=name_part)) @raises(dj.DataJointError) diff --git a/tests/test_university.py b/tests/test_university.py new file mode 100644 index 000000000..63321d6ef --- /dev/null +++ b/tests/test_university.py @@ -0,0 +1,122 @@ +from nose.tools import assert_true, assert_list_equal, assert_false, raises +import hashlib +from datajoint import DataJointError +from .schema_university import * +from . import PREFIX, CONN_INFO + + +def _hash4(table): + """hash of table contents""" + data = table.fetch(order_by="KEY", as_dict=True) + blob = dj.blob.pack(data, compress=False) + return hashlib.md5(blob).digest().hex()[:4] + + +@raises(DataJointError) +def test_activate_unauthorized(): + schema.activate('unauthorized', connection=dj.conn(**CONN_INFO)) + + +def test_activate(): + schema.activate(PREFIX + '_university', connection=dj.conn(**CONN_INFO)) # deferred activation + # --------------- Fill University ------------------- + for table in Student, Department, StudentMajor, Course, Term, CurrentTerm, Section, Enroll, Grade: + import csv + with open('./data/' + table.__name__ + '.csv') as f: + reader = csv.DictReader(f) + table().insert(reader) + + +def test_fill(): + """ check that the randomized tables are consistently defined """ + # check randomized tables + assert_true(len(Student()) == 300 and _hash4(Student) == '1e1a') + assert_true(len(StudentMajor()) == 226 and _hash4(StudentMajor) == '3129') + assert_true(len(Section()) == 756 and _hash4(Section) == 'dc7e') + assert_true(len(Enroll()) == 3364 and _hash4(Enroll) == '177d') + assert_true(len(Grade()) == 3027 and _hash4(Grade) == '4a9d') + + +def test_restrict(): + """ + test diverse restrictions from the university database. + This test relies on a specific instantiation of the database. + """ + utahns1 = Student & {'home_state': 'UT'} + utahns2 = Student & 'home_state="UT"' + assert_true(len(utahns1) == len(utahns2.fetch('KEY')) == 7) + + # male nonutahns + sex1, state1 = ((Student & 'sex="M"') - {'home_state': 'UT'}).fetch( + 'sex', 'home_state', order_by='student_id') + sex2, state2 = ((Student & 'sex="M"') - {'home_state': 'UT'}).fetch( + 'sex', 'home_state', order_by='student_id') + assert_true(len(set(state1)) == len(set(state2)) == 44) + assert_true(set(sex1).pop() == set(sex2).pop() == "M") + + # students from OK, NM, TX + s1 = (Student & [{'home_state': s} for s in ('OK', 'NM', 'TX')]).fetch( + "KEY", order_by="student_id") + s2 = (Student & 'home_state in ("OK", "NM", "TX")').fetch('KEY', order_by="student_id") + assert_true(len(s1) == 11) + assert_list_equal(s1, s2) + + millenials = Student & 'date_of_birth between "1981-01-01" and "1996-12-31"' + assert_true(len(millenials) == 170) + millenials_no_math = millenials - (Enroll & 'dept="MATH"') + assert_true(len(millenials_no_math) == 53) + + inactive_students = Student - (Enroll & CurrentTerm) + assert_true(len(inactive_students) == 204) + + # Females who are active or major in non-math + special = Student & [Enroll, StudentMajor - {'dept': "MATH"}] & {'sex': "F"} + assert_true(len(special) == 158) + + +def test_advanced_join(): + """test advanced joins""" + # Students with ungraded courses in current term + ungraded = Enroll * CurrentTerm - Grade + assert_true(len(ungraded) == 34) + + # add major + major = StudentMajor.proj(..., major='dept') + assert_true(len(ungraded.join(major, left=True)) == len(ungraded) == 34) + assert_true(len(ungraded.join(major)) == len(ungraded & major) == 31) + + +def test_union(): + # effective left join Enroll with Major + q1 = (Enroll & 'student_id=101') + (Enroll & 'student_id=102') + q2 = (Enroll & 'student_id in (101, 102)') + assert_true(len(q1) == len(q2) == 41) + + +def test_aggr(): + avg_grade_per_course = Course.aggr(Grade*LetterGrade, avg_grade='round(avg(points), 2)') + assert_true(len(avg_grade_per_course) == 45) + + # GPA + student_gpa = Student.aggr( + Course * Grade * LetterGrade, + gpa='round(sum(points*credits)/sum(credits), 2)') + gpa = student_gpa.fetch('gpa') + assert_true(len(gpa) == 261) + assert_true(2 < gpa.mean() < 3) + + # Sections in biology department with zero students in them + section = (Section & {"dept": "BIOL"}).aggr( + Enroll, n='count(student_id)', keep_all_rows=True) & 'n=0' + assert_true(len(set(section.fetch('dept'))) == 1) + assert_true(len(section) == 17) + assert_true(bool(section)) + + # Test correct use of ellipses in a similar query + section = (Section & {"dept": "BIOL"}).aggr( + Grade, ..., n='count(student_id)', keep_all_rows=True) & 'n>1' + assert_false( + any(name in section.heading.names for name in Grade.heading.secondary_attributes)) + assert_true(len(set(section.fetch('dept'))) == 1) + assert_true(len(section) == 168) + assert_true(bool(section)) diff --git a/tests/test_update1.py b/tests/test_update1.py index 1ce4e8eda..e52ff1a01 100644 --- a/tests/test_update1.py +++ b/tests/test_update1.py @@ -23,6 +23,7 @@ dj.errors._switch_filepath_types(True) + @schema class Thing(dj.Manual): definition = """