diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index e008c9287..06958f6cd 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,12 +1,13 @@ -# Note: You can use any Debian/Ubuntu based image you want. -FROM mcr.microsoft.com/devcontainers/python:3.7-bullseye - +ARG PY_VER +ARG DISTRO +FROM mcr.microsoft.com/devcontainers/python:${PY_VER}-${DISTRO} RUN \ apt update && \ apt-get install bash-completion graphviz default-mysql-client -y && \ pip install flake8 black faker ipykernel pytest pytest-cov nose nose-cov datajoint && \ pip uninstall datajoint -y +USER root ENV DJ_HOST fakeservices.datajoint.io ENV DJ_USER root -ENV DJ_PASS simple \ No newline at end of file +ENV DJ_PASS password diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a5db4d4c5..35b1d0835 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,22 +1,22 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose { - "name": "Development", - "dockerComposeFile": "docker-compose.yaml", + "name": "Existing Docker Compose (Extend)", + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": [ + "../LNX-docker-compose.yml", + "docker-compose.yml" + ], + // The 'service' property is the name of the service for the container that VS Code should + // use. Update this value and .devcontainer/docker-compose.yml to the real service name. "service": "app", + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - // Use this environment variable if you need to bind mount your local source code into a new container. - "remoteEnv": { - "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}" - }, - // https://containers.dev/features - "features": { - "ghcr.io/devcontainers/features/docker-in-docker:2": {}, - "ghcr.io/devcontainers/features/git:1": {}, - "ghcr.io/eitsupi/devcontainer-features/jq-likes:1": {}, - "ghcr.io/guiyomh/features/vim:0": {} - }, - "onCreateCommand": "pip install -e .", - "postStartCommand": "MYSQL_VER=8.0 MINIO_VER=RELEASE.2022-08-11T04-37-28Z docker compose -f local-docker-compose.yml down && docker volume prune -f && MYSQL_VER=8.0 MINIO_VER=RELEASE.2022-08-11T04-37-28Z docker compose -f local-docker-compose.yml up --build --wait", + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [ 80, 443, @@ -24,11 +24,25 @@ 8080, 9000 ], + // Uncomment the next line if you want start specific services in your Docker Compose config. + // "runServices": [], + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + "shutdownAction": "stopCompose", + "onCreateCommand": "python3 -m pip install -e .", + "features": { + "ghcr.io/devcontainers/features/git:1": {}, + }, + // Configure tool-specific properties. "customizations": { "vscode": { "extensions": [ "ms-python.python" ] } + }, + "remoteEnv": { + "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}" } + // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "devcontainer" } \ No newline at end of file diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml deleted file mode 100644 index a456ed151..000000000 --- a/.devcontainer/docker-compose.yaml +++ /dev/null @@ -1,10 +0,0 @@ -version: "3" -services: - app: - build: . - extra_hosts: - - fakeservices.datajoint.io:127.0.0.1 - volumes: - - ../..:/workspaces:cached - entrypoint: /usr/local/share/docker-init.sh - command: tail -f /dev/null diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..1ee8ea148 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,32 @@ +version: '2.4' +services: + # Update this to the name of the service you want to work with in your docker-compose.yml file + app: + # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer + # folder. Note that the path of the Dockerfile and context is relative to the *primary* + # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile" + # array). The sample below assumes your primary file is in the root of your project. + container_name: devcontainer + image: devcontainer + build: + context: . + dockerfile: .devcontainer/Dockerfile + args: + - PY_VER=${PY_VER:-3.8} + - DISTRO=${DISTRO:-buster} + + volumes: + # Update this to wherever you want VS Code to mount the folder of your project + - ..:/workspaces:cached + + # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. + # cap_add: + # - SYS_PTRACE + # security_opt: + # - seccomp:unconfined + + user: root + + # Overrides default command so things don't shut down after the process ends. + command: /bin/sh -c "while sleep 1000; do :; done" + diff --git a/.github/workflows/development.yaml b/.github/workflows/development.yaml index fdcbcd677..e9a72f485 100644 --- a/.github/workflows/development.yaml +++ b/.github/workflows/development.yaml @@ -58,6 +58,10 @@ jobs: py_ver: ["3.9"] mysql_ver: ["8.0", "5.7"] include: + - py_ver: "3.11" + mysql_ver: "8.0" + - py_ver: "3.10" + mysql_ver: "8.0" - py_ver: "3.8" mysql_ver: "5.7" - py_ver: "3.7" @@ -77,7 +81,7 @@ jobs: - name: Run primary tests env: PY_VER: ${{matrix.py_ver}} - DJ_PASS: simple + DJ_PASS: password MYSQL_VER: ${{matrix.mysql_ver}} DISTRO: alpine MINIO_VER: RELEASE.2021-09-03T03-56-13Z diff --git a/.gitignore b/.gitignore index 4b7bdb2c4..6e1d664ff 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,6 @@ docs/site !.vscode/settings.json !.vscode/launch.json -!.devcontainer/devcontainer.json \ No newline at end of file +!.devcontainer/devcontainer.json +!.devcontainer/docker-compose.yml + diff --git a/LNX-docker-compose.yml b/LNX-docker-compose.yml index 9c0a95b78..4214c72f7 100644 --- a/LNX-docker-compose.yml +++ b/LNX-docker-compose.yml @@ -7,9 +7,10 @@ x-net: services: db: <<: *net - image: datajoint/mysql:${MYSQL_VER} + image: datajoint/mysql:${MYSQL_VER:-5.7} environment: - - MYSQL_ROOT_PASSWORD=${DJ_PASS} + - MYSQL_ROOT_PASSWORD=${DJ_PASS:-password} + command: mysqld --default-authentication-plugin=mysql_native_password # ports: # - "3306:3306" # volumes: @@ -21,7 +22,7 @@ services: interval: 15s minio: <<: *net - image: minio/minio:${MINIO_VER} + image: minio/minio:${MINIO_VER:-RELEASE.2022-08-11T04-37-28Z} environment: - MINIO_ACCESS_KEY=datajoint - MINIO_SECRET_KEY=datajoint @@ -44,7 +45,7 @@ services: interval: 15s fakeservices.datajoint.io: <<: *net - image: datajoint/nginx:v0.2.6 + image: datajoint/nginx:v0.2.8 environment: - ADD_db_TYPE=DATABASE - ADD_db_ENDPOINT=db:3306 @@ -58,7 +59,7 @@ services: # - "3306:3306" app: <<: *net - image: datajoint/djtest:py${PY_VER}-${DISTRO} + image: datajoint/djtest:py${PY_VER:-3.11}-${DISTRO:-alpine} depends_on: db: condition: service_healthy @@ -69,7 +70,7 @@ services: environment: - DJ_HOST=fakeservices.datajoint.io - DJ_USER=root - - DJ_PASS + - DJ_PASS=password - DJ_TEST_HOST=fakeservices.datajoint.io - DJ_TEST_USER=datajoint - DJ_TEST_PASSWORD=datajoint @@ -86,14 +87,12 @@ services: - -c - | set -e - pip install --user nose nose-cov pip install -e . pip list --format=freeze | grep datajoint pytest -sv --cov-report term-missing --cov=datajoint tests - nosetests -vsw tests_old --with-coverage --cover-package=datajoint # ports: # - "8888:8888" - user: ${HOST_UID}:anaconda + user: ${HOST_UID:-1000}:anaconda volumes: - .:/src - /tmp/.X11-unix:/tmp/.X11-unix:rw diff --git a/datajoint/connection.py b/datajoint/connection.py index 65b096ab0..45c75e702 100644 --- a/datajoint/connection.py +++ b/datajoint/connection.py @@ -182,6 +182,8 @@ def __init__(self, host, user, password, port=None, init_fun=None, use_tls=None) elif port is None: port = config["database.port"] self.conn_info = dict(host=host, port=port, user=user, passwd=password) + if use_tls is None: + use_tls = config["database.use_tls"] if use_tls is not False: self.conn_info["ssl"] = ( use_tls if isinstance(use_tls, dict) else {"ssl": {}} diff --git a/docs/src/develop.md b/docs/src/develop.md index 4acb9ed35..99f291652 100644 --- a/docs/src/develop.md +++ b/docs/src/develop.md @@ -39,24 +39,16 @@ The following will verify there are no regression errors by running our test sui - Entire test suite: ``` - nosetests -vw tests_old + pytest -sv --cov-report term-missing --cov=datajoint tests ``` - > Note: We are in the process of upgrading to `pytest` tests. To run those, use: - > ``` - > pytest -sv --cov-report term-missing --cov=datajoint tests - > ``` - A single functional test: ``` - nosetests -vs --tests=tests_old.test_external_class:test_insert_and_fetch + pytest -sv tests/test_connection.py::test_dj_conn ``` - > Note: We are in the process of upgrading to `pytest` tests. To run those, use: - > ``` - > pytest -sv tests/test_connection.py::test_dj_conn - > ``` - A single class test: ``` - nosetests -vs --tests=tests_old.test_fetch:TestFetch.test_getattribute_for_fetch1 + pytest -sv tests/test_aggr_regressions.py::TestIssue558 ``` ### Style Tests @@ -104,7 +96,7 @@ It is often useful in development to connect to DataJoint's relational database Connect as follows to the database running within your developer environment: ``` -mysql -hfakeservices.datajoint.io -uroot -psimple +mysql -hfakeservices.datajoint.io -uroot -ppassword ``` ### Documentation diff --git a/tests/__init__.py b/tests/__init__.py index 8b825a042..e69de29bb 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,69 +0,0 @@ -import datajoint as dj -from packaging import version -import pytest -import os - -PREFIX = "djtest" - -CONN_INFO_ROOT = dict( - host=os.getenv("DJ_HOST"), - user=os.getenv("DJ_USER"), - password=os.getenv("DJ_PASS"), -) - - -@pytest.fixture -def connection_root(): - """Root user database connection.""" - dj.config["safemode"] = False - connection = dj.Connection( - host=os.getenv("DJ_HOST"), - user=os.getenv("DJ_USER"), - password=os.getenv("DJ_PASS"), - ) - yield connection - dj.config["safemode"] = True - connection.close() - - -@pytest.fixture -def connection_test(connection_root): - """Test user database connection.""" - database = f"{PREFIX}%%" - credentials = dict( - host=os.getenv("DJ_HOST"), user="datajoint", password="datajoint" - ) - permission = "ALL PRIVILEGES" - - # Create MySQL users - if version.parse( - connection_root.query("select @@version;").fetchone()[0] - ) >= version.parse("8.0.0"): - # create user if necessary on mysql8 - connection_root.query( - f""" - CREATE USER IF NOT EXISTS '{credentials["user"]}'@'%%' - IDENTIFIED BY '{credentials["password"]}'; - """ - ) - connection_root.query( - f""" - GRANT {permission} ON `{database}`.* - TO '{credentials["user"]}'@'%%'; - """ - ) - else: - # grant permissions. For MySQL 5.7 this also automatically creates user - # if not exists - connection_root.query( - f""" - GRANT {permission} ON `{database}`.* - TO '{credentials["user"]}'@'%%' - IDENTIFIED BY '{credentials["password"]}'; - """ - ) - - connection = dj.Connection(**credentials) - yield connection - connection_root.query(f"""DROP USER `{credentials["user"]}`""") - connection.close() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..91db43e30 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,515 @@ +import datajoint as dj +from packaging import version +from typing import Dict, List +import os +from os import environ, remove +import minio +import urllib3 +import certifi +import shutil +import pytest +import networkx as nx +import json +from pathlib import Path +from datajoint import errors +from datajoint.errors import ( + ADAPTED_TYPE_SWITCH, + FILEPATH_FEATURE_SWITCH, + DataJointError, +) +from . import ( + schema, + schema_simple, + schema_advanced, + schema_adapted, + schema_external, + schema_uuid as schema_uuid_module, +) + + +@pytest.fixture(scope="session") +def prefix(): + return os.environ.get("DJ_TEST_DB_PREFIX", "djtest") + + +@pytest.fixture(scope="session") +def monkeysession(): + with pytest.MonkeyPatch.context() as mp: + yield mp + + +@pytest.fixture(scope="module") +def monkeymodule(): + with pytest.MonkeyPatch.context() as mp: + yield mp + + +@pytest.fixture +def enable_adapted_types(monkeypatch): + monkeypatch.setenv(ADAPTED_TYPE_SWITCH, "TRUE") + yield + monkeypatch.delenv(ADAPTED_TYPE_SWITCH, raising=True) + + +@pytest.fixture +def enable_filepath_feature(monkeypatch): + monkeypatch.setenv(FILEPATH_FEATURE_SWITCH, "TRUE") + yield + monkeypatch.delenv(FILEPATH_FEATURE_SWITCH, raising=True) + + +@pytest.fixture(scope="session") +def db_creds_test() -> Dict: + return dict( + host=os.getenv("DJ_TEST_HOST", "fakeservices.datajoint.io"), + user=os.getenv("DJ_TEST_USER", "datajoint"), + password=os.getenv("DJ_TEST_PASSWORD", "datajoint"), + ) + + +@pytest.fixture(scope="session") +def db_creds_root() -> Dict: + return dict( + host=os.getenv("DJ_HOST", "fakeservices.datajoint.io"), + user=os.getenv("DJ_USER", "root"), + password=os.getenv("DJ_PASS", "password"), + ) + + +@pytest.fixture(scope="session") +def setup_ssl_config(): + dj.config["database.use_tls"] = False + yield + + +@pytest.fixture(scope="session") +def connection_root_bare(db_creds_root, setup_ssl_config): + connection = dj.Connection(**db_creds_root) + yield connection + + +@pytest.fixture(scope="session") +def connection_root(connection_root_bare, prefix): + """Root user database connection.""" + dj.config["safemode"] = False + conn_root = connection_root_bare + # Create MySQL users + if version.parse( + conn_root.query("select @@version;").fetchone()[0] + ) >= version.parse("8.0.0"): + # create user if necessary on mysql8 + conn_root.query( + """ + CREATE USER IF NOT EXISTS 'datajoint'@'%%' + IDENTIFIED BY 'datajoint'; + """ + ) + conn_root.query( + """ + CREATE USER IF NOT EXISTS 'djview'@'%%' + IDENTIFIED BY 'djview'; + """ + ) + conn_root.query( + """ + CREATE USER IF NOT EXISTS 'djssl'@'%%' + IDENTIFIED BY 'djssl' + REQUIRE SSL; + """ + ) + conn_root.query("GRANT ALL PRIVILEGES ON `djtest%%`.* TO 'datajoint'@'%%';") + conn_root.query("GRANT SELECT ON `djtest%%`.* TO 'djview'@'%%';") + conn_root.query("GRANT SELECT ON `djtest%%`.* TO 'djssl'@'%%';") + else: + # grant permissions. For MySQL 5.7 this also automatically creates user + # if not exists + conn_root.query( + """ + GRANT ALL PRIVILEGES ON `djtest%%`.* TO 'datajoint'@'%%' + IDENTIFIED BY 'datajoint'; + """ + ) + conn_root.query( + "GRANT SELECT ON `djtest%%`.* TO 'djview'@'%%' IDENTIFIED BY 'djview';" + ) + conn_root.query( + """ + GRANT SELECT ON `djtest%%`.* TO 'djssl'@'%%' + IDENTIFIED BY 'djssl' + REQUIRE SSL; + """ + ) + + yield conn_root + + # Teardown + conn_root.query("SET FOREIGN_KEY_CHECKS=0") + cur = conn_root.query('SHOW DATABASES LIKE "{}\\_%%"'.format(prefix)) + for db in cur.fetchall(): + conn_root.query("DROP DATABASE `{}`".format(db[0])) + conn_root.query("SET FOREIGN_KEY_CHECKS=1") + if os.path.exists("dj_local_conf.json"): + remove("dj_local_conf.json") + + # Remove created users + conn_root.query("DROP USER IF EXISTS `datajoint`") + conn_root.query("DROP USER IF EXISTS `djview`") + conn_root.query("DROP USER IF EXISTS `djssl`") + conn_root.close() + + +@pytest.fixture(scope="session") +def connection_test(connection_root, prefix, db_creds_test): + """Test user database connection.""" + database = f"{prefix}%%" + permission = "ALL PRIVILEGES" + + # Create MySQL users + if version.parse( + connection_root.query("select @@version;").fetchone()[0] + ) >= version.parse("8.0.0"): + # create user if necessary on mysql8 + connection_root.query( + f""" + CREATE USER IF NOT EXISTS '{db_creds_test["user"]}'@'%%' + IDENTIFIED BY '{db_creds_test["password"]}'; + """ + ) + connection_root.query( + f""" + GRANT {permission} ON `{database}`.* + TO '{db_creds_test["user"]}'@'%%'; + """ + ) + else: + # grant permissions. For MySQL 5.7 this also automatically creates user + # if not exists + connection_root.query( + f""" + GRANT {permission} ON `{database}`.* + TO '{db_creds_test["user"]}'@'%%' + IDENTIFIED BY '{db_creds_test["password"]}'; + """ + ) + + connection = dj.Connection(**db_creds_test) + yield connection + connection_root.query(f"""DROP USER `{db_creds_test["user"]}`""") + connection.close() + + +@pytest.fixture(scope="session") +def s3_creds() -> Dict: + return dict( + endpoint=os.environ.get("S3_ENDPOINT", "fakeservices.datajoint.io"), + access_key=os.environ.get("S3_ACCESS_KEY", "datajoint"), + secret_key=os.environ.get("S3_SECRET_KEY", "datajoint"), + bucket=os.environ.get("S3_BUCKET", "datajoint.test"), + ) + + +@pytest.fixture(scope="session") +def stores_config(s3_creds, tmpdir_factory): + stores_config = { + "raw": dict(protocol="file", location=tmpdir_factory.mktemp("raw")), + "repo": dict( + stage=tmpdir_factory.mktemp("repo"), + protocol="file", + location=tmpdir_factory.mktemp("repo"), + ), + "repo-s3": dict( + s3_creds, + protocol="s3", + location="dj/repo", + stage=tmpdir_factory.mktemp("repo-s3"), + ), + "local": dict( + protocol="file", location=tmpdir_factory.mktemp("local"), subfolding=(1, 1) + ), + "share": dict( + s3_creds, protocol="s3", location="dj/store/repo", subfolding=(2, 4) + ), + } + return stores_config + + +@pytest.fixture +def mock_stores(stores_config): + og_stores_config = dj.config.get("stores") + dj.config["stores"] = stores_config + yield + if og_stores_config is None: + del dj.config["stores"] + else: + dj.config["stores"] = og_stores_config + + +@pytest.fixture +def mock_cache(tmpdir_factory): + og_cache = dj.config.get("cache") + dj.config["cache"] = tmpdir_factory.mktemp("cache") + yield + if og_cache is None: + del dj.config["cache"] + else: + dj.config["cache"] = og_cache + + +@pytest.fixture +def schema_any(connection_test, prefix): + schema_any = dj.Schema( + prefix + "_test1", schema.LOCALS_ANY, connection=connection_test + ) + assert schema.LOCALS_ANY, "LOCALS_ANY is empty" + try: + schema_any.jobs.delete() + except DataJointError: + pass + schema_any(schema.TTest) + schema_any(schema.TTest2) + schema_any(schema.TTest3) + schema_any(schema.NullableNumbers) + schema_any(schema.TTestExtra) + schema_any(schema.TTestNoExtra) + schema_any(schema.Auto) + schema_any(schema.User) + schema_any(schema.Subject) + schema_any(schema.Language) + schema_any(schema.Experiment) + schema_any(schema.Trial) + schema_any(schema.Ephys) + schema_any(schema.Image) + schema_any(schema.UberTrash) + schema_any(schema.UnterTrash) + schema_any(schema.SimpleSource) + schema_any(schema.SigIntTable) + schema_any(schema.SigTermTable) + schema_any(schema.DjExceptionName) + schema_any(schema.ErrorClass) + schema_any(schema.DecimalPrimaryKey) + schema_any(schema.IndexRich) + schema_any(schema.ThingA) + schema_any(schema.ThingB) + schema_any(schema.ThingC) + schema_any(schema.Parent) + schema_any(schema.Child) + schema_any(schema.ComplexParent) + schema_any(schema.ComplexChild) + schema_any(schema.SubjectA) + schema_any(schema.SessionA) + schema_any(schema.SessionStatusA) + schema_any(schema.SessionDateA) + schema_any(schema.Stimulus) + schema_any(schema.Longblob) + yield schema_any + try: + schema_any.jobs.delete() + except DataJointError: + pass + schema_any.drop() + + +@pytest.fixture +def schema_simp(connection_test, prefix): + schema = dj.Schema( + prefix + "_relational", schema_simple.LOCALS_SIMPLE, connection=connection_test + ) + schema(schema_simple.IJ) + schema(schema_simple.JI) + schema(schema_simple.A) + schema(schema_simple.B) + schema(schema_simple.L) + schema(schema_simple.D) + schema(schema_simple.E) + schema(schema_simple.F) + schema(schema_simple.F) + schema(schema_simple.DataA) + schema(schema_simple.DataB) + schema(schema_simple.Website) + schema(schema_simple.Profile) + schema(schema_simple.Website) + schema(schema_simple.TTestUpdate) + schema(schema_simple.ArgmaxTest) + schema(schema_simple.ReservedWord) + schema(schema_simple.OutfitLaunch) + yield schema + schema.drop() + + +@pytest.fixture +def schema_adv(connection_test, prefix): + schema = dj.Schema( + prefix + "_advanced", + schema_advanced.LOCALS_ADVANCED, + connection=connection_test, + ) + schema(schema_advanced.Person) + schema(schema_advanced.Parent) + schema(schema_advanced.Subject) + schema(schema_advanced.Prep) + schema(schema_advanced.Slice) + schema(schema_advanced.Cell) + schema(schema_advanced.InputCell) + schema(schema_advanced.LocalSynapse) + schema(schema_advanced.GlobalSynapse) + yield schema + schema.drop() + + +@pytest.fixture +def schema_ext( + connection_test, enable_filepath_feature, mock_stores, mock_cache, prefix +): + schema = dj.Schema( + prefix + "_extern", + context=schema_external.LOCALS_EXTERNAL, + connection=connection_test, + ) + schema(schema_external.Simple) + schema(schema_external.SimpleRemote) + schema(schema_external.Seed) + schema(schema_external.Dimension) + schema(schema_external.Image) + schema(schema_external.Attach) + schema(schema_external.Filepath) + schema(schema_external.FilepathS3) + yield schema + schema.drop() + + +@pytest.fixture +def schema_uuid(connection_test, prefix): + schema = dj.Schema( + prefix + "_test1", + context=schema_uuid_module.LOCALS_UUID, + connection=connection_test, + ) + schema(schema_uuid_module.Basic) + schema(schema_uuid_module.Topic) + schema(schema_uuid_module.Item) + yield schema + schema.drop() + + +@pytest.fixture(scope="session") +def http_client(): + # Initialize httpClient with relevant timeout. + client = urllib3.PoolManager( + timeout=30, + cert_reqs="CERT_REQUIRED", + ca_certs=certifi.where(), + retries=urllib3.Retry( + total=3, backoff_factor=0.2, status_forcelist=[500, 502, 503, 504] + ), + ) + yield client + + +@pytest.fixture(scope="session") +def minio_client_bare(s3_creds, http_client): + """Initialize MinIO with an endpoint and access/secret keys.""" + client = minio.Minio( + s3_creds["endpoint"], + access_key=s3_creds["access_key"], + secret_key=s3_creds["secret_key"], + secure=True, + http_client=http_client, + ) + return client + + +@pytest.fixture(scope="session") +def minio_client(s3_creds, minio_client_bare): + """Initialize a MinIO client and create buckets for testing session.""" + # Setup MinIO bucket + aws_region = "us-east-1" + try: + minio_client_bare.make_bucket(s3_creds["bucket"], location=aws_region) + except minio.error.S3Error as e: + if e.code != "BucketAlreadyOwnedByYou": + raise e + + yield minio_client_bare + + # Teardown S3 + objs = list(minio_client_bare.list_objects(s3_creds["bucket"], recursive=True)) + objs = [ + minio_client_bare.remove_object( + s3_creds["bucket"], o.object_name.encode("utf-8") + ) + for o in objs + ] + minio_client_bare.remove_bucket(s3_creds["bucket"]) + + +@pytest.fixture +def test(schema_any): + yield schema.TTest() + + +@pytest.fixture +def test2(schema_any): + yield schema.TTest2() + + +@pytest.fixture +def test_extra(schema_any): + yield schema.TTestExtra() + + +@pytest.fixture +def test_no_extra(schema_any): + yield schema.TTestNoExtra() + + +@pytest.fixture +def user(schema_any): + return schema.User() + + +@pytest.fixture +def lang(schema_any): + yield schema.Language() + + +@pytest.fixture +def languages(lang) -> List: + og_contents = lang.contents + languages = og_contents.copy() + yield languages + lang.contents = og_contents + + +@pytest.fixture +def subject(schema_any): + yield schema.Subject() + + +@pytest.fixture +def experiment(schema_any): + return schema.Experiment() + + +@pytest.fixture +def ephys(schema_any): + return schema.Ephys() + + +@pytest.fixture +def img(schema_any): + return schema.Image() + + +@pytest.fixture +def trial(schema_any): + return schema.Trial() + + +@pytest.fixture +def channel(schema_any): + return schema.Ephys.Channel() + + +@pytest.fixture +def trash(schema_any): + return schema.UberTrash() 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 new file mode 100644 index 000000000..3f31649cc --- /dev/null +++ b/tests/schema.py @@ -0,0 +1,454 @@ +""" +Sample schema with realistic tables for testing +""" + +import random +import numpy as np +import datajoint as dj +import inspect + + +class TTest(dj.Lookup): + """ + doc string + """ + + definition = """ + key : int # key + --- + value : int # value + """ + contents = [(k, 2 * k) for k in range(10)] + + +class TTest2(dj.Manual): + definition = """ + key : int # key + --- + value : int # value + """ + + +class TTest3(dj.Manual): + definition = """ + key : int + --- + value : varchar(300) + """ + + +class NullableNumbers(dj.Manual): + definition = """ + key : int + --- + fvalue = null : float + dvalue = null : double + ivalue = null : int + """ + + +class TTestExtra(dj.Manual): + """ + clone of Test but with an extra field + """ + + definition = TTest.definition + "\nextra : int # extra int\n" + + +class TTestNoExtra(dj.Manual): + """ + clone of Test but with no extra fields + """ + + definition = TTest.definition + + +class Auto(dj.Lookup): + definition = """ + id :int auto_increment + --- + name :varchar(12) + """ + + def fill(self): + if not self: + self.insert([dict(name="Godel"), dict(name="Escher"), dict(name="Bach")]) + + +class User(dj.Lookup): + definition = """ # lab members + username: varchar(12) + """ + contents = [ + ["Jake"], + ["Cathryn"], + ["Shan"], + ["Fabian"], + ["Edgar"], + ["George"], + ["Dimitri"], + ] + + +class Subject(dj.Lookup): + definition = """ # Basic information about animal subjects used in experiments + subject_id :int # unique subject id + --- + real_id :varchar(40) # real-world name. Omit if the same as subject_id + species = "mouse" :enum('mouse', 'monkey', 'human') + date_of_birth :date + subject_notes :varchar(4000) + unique index (real_id, species) + """ + + contents = [ + [1551, "1551", "mouse", "2015-04-01", "genetically engineered super mouse"], + [10, "Curious George", "monkey", "2008-06-30", ""], + [1552, "1552", "mouse", "2015-06-15", ""], + [1553, "1553", "mouse", "2016-07-01", ""], + ] + + +class Language(dj.Lookup): + definition = """ + # languages spoken by some of the developers + # additional comments are ignored + name : varchar(40) # name of the developer + language : varchar(40) # language + """ + contents = [ + ("Fabian", "English"), + ("Edgar", "English"), + ("Dimitri", "English"), + ("Dimitri", "Ukrainian"), + ("Fabian", "German"), + ("Edgar", "Japanese"), + ] + + +class Experiment(dj.Imported): + definition = """ # information about experiments + -> Subject + experiment_id :smallint # experiment number for this subject + --- + experiment_date :date # date when experiment was started + -> [nullable] User + data_path="" :varchar(255) # file path to recorded data + notes="" :varchar(2048) # e.g. purpose of experiment + entry_time=CURRENT_TIMESTAMP :timestamp # automatic timestamp + """ + + fake_experiments_per_subject = 5 + + def make(self, key): + """ + populate with random data + """ + from datetime import date, timedelta + + users = [None, None] + list(User().fetch()["username"]) + random.seed("Amazing Seed4") + self.insert( + dict( + key, + experiment_id=experiment_id, + experiment_date=( + date.today() - timedelta(random.expovariate(1 / 30)) + ).isoformat(), + username=random.choice(users), + ) + for experiment_id in range(self.fake_experiments_per_subject) + ) + + +class Trial(dj.Imported): + definition = """ # a trial within an experiment + -> Experiment.proj(animal='subject_id') + trial_id :smallint # trial number + --- + start_time :double # (s) + """ + + class Condition(dj.Part): + definition = """ # trial conditions + -> Trial + cond_idx : smallint # condition number + ---- + orientation : float # degrees + """ + + def make(self, key): + """populate with random data (pretend reading from raw files)""" + random.seed("Amazing Seed") + trial = self.Condition() + for trial_id in range(10): + key["trial_id"] = trial_id + self.insert1(dict(key, start_time=random.random() * 1e9)) + trial.insert( + dict(key, cond_idx=cond_idx, orientation=random.random() * 360) + for cond_idx in range(30) + ) + + +class Ephys(dj.Imported): + definition = """ # some kind of electrophysiological recording + -> Trial + ---- + sampling_frequency :double # (Hz) + duration :decimal(7,3) # (s) + """ + + class Channel(dj.Part): + definition = """ # subtable containing individual channels + -> master + channel :tinyint unsigned # channel number within Ephys + ---- + voltage : longblob + current = null : longblob # optional current to test null handling + """ + + def _make_tuples(self, key): + """ + populate with random data + """ + random.seed(str(key)) + row = dict( + key, sampling_frequency=6000, duration=np.minimum(2, random.expovariate(1)) + ) + self.insert1(row) + number_samples = int(row["duration"] * row["sampling_frequency"] + 0.5) + sub = self.Channel() + sub.insert( + dict( + key, + channel=channel, + voltage=np.float32(np.random.randn(number_samples)), + ) + for channel in range(2) + ) + + +class Image(dj.Manual): + definition = """ + # table for testing blob inserts + id : int # image identifier + --- + img : longblob # image + """ + + +class UberTrash(dj.Lookup): + definition = """ + id : int + --- + """ + contents = [(1,)] + + +class UnterTrash(dj.Lookup): + definition = """ + -> UberTrash + my_id : int + --- + """ + contents = [(1, 1), (1, 2)] + + +class SimpleSource(dj.Lookup): + definition = """ + id : int # id + """ + contents = [(x,) for x in range(10)] + + +class SigIntTable(dj.Computed): + definition = """ + -> SimpleSource + """ + + def _make_tuples(self, key): + raise KeyboardInterrupt + + +class SigTermTable(dj.Computed): + definition = """ + -> SimpleSource + """ + + def make(self, key): + raise SystemExit("SIGTERM received") + + +class DjExceptionName(dj.Lookup): + definition = """ + dj_exception_name: char(64) + """ + + @property + def contents(self): + return [ + [member_name] + for member_name, member_type in inspect.getmembers(dj.errors) + if inspect.isclass(member_type) and issubclass(member_type, Exception) + ] + + +class ErrorClass(dj.Computed): + definition = """ + -> DjExceptionName + """ + + def make(self, key): + exception_name = key["dj_exception_name"] + raise getattr(dj.errors, exception_name) + + +class DecimalPrimaryKey(dj.Lookup): + definition = """ + id : decimal(4,3) + """ + contents = list(zip((0.1, 0.25, 3.99))) + + +class IndexRich(dj.Manual): + definition = """ + -> Subject + --- + -> [unique, nullable] User.proj(first="username") + first_date : date + value : int + index (first_date, value) + """ + + +# Schema for issue 656 +class ThingA(dj.Manual): + definition = """ + a: int + """ + + +class ThingB(dj.Manual): + definition = """ + b1: int + b2: int + --- + b3: int + """ + + +class ThingC(dj.Manual): + definition = """ + -> ThingA + --- + -> [unique, nullable] ThingB + """ + + +class Parent(dj.Lookup): + definition = """ + parent_id: int + --- + name: varchar(30) + """ + contents = [(1, "Joe")] + + +class Child(dj.Lookup): + definition = """ + -> Parent + child_id: int + --- + name: varchar(30) + """ + contents = [(1, 12, "Dan")] + + +# Related to issue #886 (8), #883 (5) +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))] + + +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))] + + +class SubjectA(dj.Lookup): + definition = """ + subject_id: varchar(32) + --- + dob : date + sex : enum('M', 'F', 'U') + """ + contents = [ + ("mouse1", "2020-09-01", "M"), + ("mouse2", "2020-03-19", "F"), + ("mouse3", "2020-08-23", "F"), + ] + + +class SessionA(dj.Lookup): + definition = """ + -> SubjectA + session_start_time: datetime + --- + session_dir='' : varchar(32) + """ + contents = [ + ("mouse1", "2020-12-01 12:32:34", ""), + ("mouse1", "2020-12-02 12:32:34", ""), + ("mouse1", "2020-12-03 12:32:34", ""), + ("mouse1", "2020-12-04 12:32:34", ""), + ] + + +class SessionStatusA(dj.Lookup): + definition = """ + -> SessionA + --- + status: enum('in_training', 'trained_1a', 'trained_1b', 'ready4ephys') + """ + contents = [ + ("mouse1", "2020-12-01 12:32:34", "in_training"), + ("mouse1", "2020-12-02 12:32:34", "trained_1a"), + ("mouse1", "2020-12-03 12:32:34", "trained_1b"), + ("mouse1", "2020-12-04 12:32:34", "ready4ephys"), + ] + + +class SessionDateA(dj.Lookup): + definition = """ + -> SubjectA + session_date: date + """ + contents = [ + ("mouse1", "2020-12-01"), + ("mouse1", "2020-12-02"), + ("mouse1", "2020-12-03"), + ("mouse1", "2020-12-04"), + ] + + +class Stimulus(dj.Lookup): + definition = """ + id: int + --- + contrast: int + brightness: int + """ + + +class Longblob(dj.Manual): + definition = """ + id: int + --- + data: longblob + """ + + +LOCALS_ANY = {k: v for k, v in locals().items() if inspect.isclass(v)} +__all__ = list(LOCALS_ANY) diff --git a/tests/schema_adapted.py b/tests/schema_adapted.py new file mode 100644 index 000000000..ab9a02e76 --- /dev/null +++ b/tests/schema_adapted.py @@ -0,0 +1,62 @@ +import datajoint as dj +import inspect +import networkx as nx +import json +from pathlib import Path +import tempfile + + +class GraphAdapter(dj.AttributeAdapter): + attribute_type = "longblob" # this is how the attribute will be declared + + @staticmethod + def get(obj): + # convert edge list into a graph + return nx.Graph(obj) + + @staticmethod + def put(obj): + # convert graph object into an edge list + assert isinstance(obj, nx.Graph) + return list(obj.edges) + + +class LayoutToFilepath(dj.AttributeAdapter): + """ + An adapted data type that saves a graph layout into fixed filepath + """ + + attribute_type = "filepath@repo-s3" + + @staticmethod + def get(path): + with open(path, "r") as f: + return json.load(f) + + @staticmethod + def put(layout): + path = Path(dj.config["stores"]["repo-s3"]["stage"], "layout.json") + with open(str(path), "w") as f: + json.dump(layout, f) + return path + + +class Connectivity(dj.Manual): + definition = """ + connid : int + --- + conn_graph = null : + """ + + +class Layout(dj.Manual): + definition = """ + # stores graph layout + -> Connectivity + --- + layout: + """ + + +LOCALS_ADAPTED = {k: v for k, v in locals().items() if inspect.isclass(v)} +__all__ = list(LOCALS_ADAPTED) diff --git a/tests/schema_advanced.py b/tests/schema_advanced.py new file mode 100644 index 000000000..6a35cb34a --- /dev/null +++ b/tests/schema_advanced.py @@ -0,0 +1,140 @@ +import datajoint as dj +import inspect + + +class Person(dj.Manual): + definition = """ + person_id : int + ---- + full_name : varchar(60) + sex : enum('M','F') + """ + + def fill(self): + """ + fill fake names from www.fakenamegenerator.com + """ + self.insert( + ( + (0, "May K. Hall", "F"), + (1, "Jeffrey E. Gillen", "M"), + (2, "Hanna R. Walters", "F"), + (3, "Russel S. James", "M"), + (4, "Robbin J. Fletcher", "F"), + (5, "Wade J. Sullivan", "M"), + (6, "Dorothy J. Chen", "F"), + (7, "Michael L. Kowalewski", "M"), + (8, "Kimberly J. Stringer", "F"), + (9, "Mark G. Hair", "M"), + (10, "Mary R. Thompson", "F"), + (11, "Graham C. Gilpin", "M"), + (12, "Nelda T. Ruggeri", "F"), + (13, "Bryan M. Cummings", "M"), + (14, "Sara C. Le", "F"), + (15, "Myron S. Jaramillo", "M"), + ) + ) + + +class Parent(dj.Manual): + definition = """ + -> Person + parent_sex : enum('M','F') + --- + -> Person.proj(parent='person_id') + """ + + def fill(self): + def make_parent(pid, parent): + return dict( + person_id=pid, + parent=parent, + parent_sex=(Person & {"person_id": parent}).fetch1("sex"), + ) + + self.insert( + make_parent(*r) + for r in ( + (0, 2), + (0, 3), + (1, 4), + (1, 5), + (2, 4), + (2, 5), + (3, 4), + (3, 7), + (4, 7), + (4, 8), + (5, 9), + (5, 10), + (6, 9), + (6, 10), + (7, 11), + (7, 12), + (8, 11), + (8, 14), + (9, 11), + (9, 12), + (10, 13), + (10, 14), + (11, 14), + (11, 15), + (12, 14), + (12, 15), + ) + ) + + +class Subject(dj.Manual): + definition = """ + subject : int + --- + -> [unique, nullable] Person + """ + + +class Prep(dj.Manual): + definition = """ + prep : int + """ + + +class Slice(dj.Manual): + definition = """ + -> Prep + slice : int + """ + + +class Cell(dj.Manual): + definition = """ + -> Slice + cell : int + """ + + +class InputCell(dj.Manual): + definition = """ # a synapse within the slice + -> Cell + -> Cell.proj(input="cell") + """ + + +class LocalSynapse(dj.Manual): + definition = """ # a synapse within the slice + -> Cell.proj(presynaptic='cell') + -> Cell.proj(postsynaptic='cell') + """ + + +class GlobalSynapse(dj.Manual): + # Mix old-style and new-style projected foreign keys + definition = """ + # a synapse within the slice + -> Cell.proj(pre_slice="slice", pre_cell="cell") + -> Cell.proj(post_slice="slice", post_cell="cell") + """ + + +LOCALS_ADVANCED = {k: v for k, v in locals().items() if inspect.isclass(v)} +__all__ = list(LOCALS_ADVANCED) diff --git a/tests/schema_aggr_regress.py b/tests/schema_aggr_regress.py new file mode 100644 index 000000000..9b85bfffb --- /dev/null +++ b/tests/schema_aggr_regress.py @@ -0,0 +1,51 @@ +import datajoint as dj +import itertools +import inspect + + +class R(dj.Lookup): + definition = """ + r : char(1) + """ + contents = zip("ABCDFGHIJKLMNOPQRST") + + +class Q(dj.Lookup): + definition = """ + -> R + """ + contents = zip("ABCDFGH") + + +class S(dj.Lookup): + definition = """ + -> R + s : int + """ + contents = itertools.product("ABCDF", range(10)) + + +class A(dj.Lookup): + definition = """ + id: int + """ + contents = zip(range(10)) + + +class B(dj.Lookup): + definition = """ + -> A + id2: int + """ + contents = zip(range(5), range(5, 10)) + + +class X(dj.Lookup): + definition = """ + id: int + """ + contents = zip(range(10)) + + +LOCALS_AGGR_REGRESS = {k: v for k, v in locals().items() if inspect.isclass(v)} +__all__ = list(LOCALS_AGGR_REGRESS) diff --git a/tests/schema_alter.py b/tests/schema_alter.py new file mode 100644 index 000000000..d607bc7c4 --- /dev/null +++ b/tests/schema_alter.py @@ -0,0 +1,56 @@ +import datajoint as dj +import inspect + + +class Experiment(dj.Imported): + original_definition = """ # information about experiments + -> Subject + experiment_id :smallint # experiment number for this subject + --- + experiment_date :date # date when experiment was started + -> [nullable] User + data_path="" :varchar(255) # file path to recorded data + notes="" :varchar(2048) # e.g. purpose of experiment + entry_time=CURRENT_TIMESTAMP :timestamp # automatic timestamp + """ + + definition1 = """ # Experiment + -> Subject + experiment_id :smallint # experiment number for this subject + --- + data_path : int # some number + extra=null : longblob # just testing + -> [nullable] User + subject_notes=null :varchar(2048) # {notes} e.g. purpose of experiment + entry_time=CURRENT_TIMESTAMP :timestamp # automatic timestamp + """ + + +class Parent(dj.Manual): + definition = """ + parent_id: int + """ + + class Child(dj.Part): + definition = """ + -> Parent + """ + definition_new = """ + -> master + --- + child_id=null: int + """ + + class Grandchild(dj.Part): + definition = """ + -> master.Child + """ + definition_new = """ + -> master.Child + --- + grandchild_id=null: int + """ + + +LOCALS_ALTER = {k: v for k, v in locals().items() if inspect.isclass(v)} +__all__ = list(LOCALS_ALTER) diff --git a/tests/schema_external.py b/tests/schema_external.py new file mode 100644 index 000000000..ce51af9c5 --- /dev/null +++ b/tests/schema_external.py @@ -0,0 +1,88 @@ +""" +A schema for testing external attributes +""" + +import tempfile +import inspect +import datajoint as dj +import numpy as np + + +class Simple(dj.Manual): + definition = """ + simple : int + --- + item : blob@local + """ + + +class SimpleRemote(dj.Manual): + definition = """ + simple : int + --- + item : blob@share + """ + + +class Seed(dj.Lookup): + definition = """ + seed : int + """ + contents = zip(range(4)) + + +class Dimension(dj.Lookup): + definition = """ + dim : int + --- + dimensions : blob + """ + contents = ([0, [100, 50]], [1, [3, 4, 8, 6]]) + + +class Image(dj.Computed): + definition = """ + # table for storing + -> Seed + -> Dimension + ---- + img : blob@share # objects are stored as specified by dj.config['stores']['share'] + neg : blob@local # objects are stored as specified by dj.config['stores']['local'] + """ + + def make(self, key): + np.random.seed(key["seed"]) + img = np.random.rand(*(Dimension() & key).fetch1("dimensions")) + self.insert1(dict(key, img=img, neg=-img.astype(np.float32))) + + +class Attach(dj.Manual): + definition = """ + # table for storing attachments + attach : int + ---- + img : attach@share # attachments are stored as specified by: dj.config['stores']['raw'] + txt : attach # attachments are stored directly in the database + """ + + +class Filepath(dj.Manual): + definition = """ + # table for file management + fnum : int # test comment containing : + --- + img : filepath@repo # managed files + """ + + +class FilepathS3(dj.Manual): + definition = """ + # table for file management + fnum : int + --- + img : filepath@repo-s3 # managed files + """ + + +LOCALS_EXTERNAL = {k: v for k, v in locals().items() if inspect.isclass(v)} +__all__ = list(LOCALS_EXTERNAL) diff --git a/tests/schema_privileges.py b/tests/schema_privileges.py new file mode 100644 index 000000000..b53d6b264 --- /dev/null +++ b/tests/schema_privileges.py @@ -0,0 +1,34 @@ +import datajoint as dj +import inspect + + +class Parent(dj.Lookup): + definition = """ + id: int + """ + contents = [(1,)] + + +class Child(dj.Computed): + definition = """ + -> Parent + """ + + def make(self, key): + self.insert1(key) + + +class NoAccess(dj.Lookup): + definition = """ + string: varchar(10) + """ + + +class NoAccessAgain(dj.Manual): + definition = """ + -> NoAccess + """ + + +LOCALS_PRIV = {k: v for k, v in locals().items() if inspect.isclass(v)} +__all__ = list(LOCALS_PRIV) diff --git a/tests/schema_simple.py b/tests/schema_simple.py new file mode 100644 index 000000000..e751a9c6e --- /dev/null +++ b/tests/schema_simple.py @@ -0,0 +1,265 @@ +""" +A simple, abstract schema to test relational algebra +""" +import random +import datajoint as dj +import itertools +import hashlib +import uuid +import faker +import numpy as np +from datetime import date, timedelta +import inspect + + +class IJ(dj.Lookup): + definition = """ # tests restrictions + i : int + j : int + """ + contents = list(dict(i=i, j=j + 2) for i in range(3) for j in range(3)) + + +class JI(dj.Lookup): + definition = """ # tests restrictions by relations when attributes are reordered + j : int + i : int + """ + contents = list(dict(i=i + 1, j=j) for i in range(3) for j in range(3)) + + +class A(dj.Lookup): + definition = """ + id_a :int + --- + cond_in_a :tinyint + """ + contents = [(i, i % 4 > i % 3) for i in range(10)] + + +class B(dj.Computed): + definition = """ + -> A + id_b :int + --- + mu :float # mean value + sigma :float # standard deviation + n :smallint # number samples + """ + + class C(dj.Part): + definition = """ + -> B + id_c :int + --- + value :float # normally distributed variables according to parameters in B + """ + + def make(self, key): + random.seed(str(key)) + sub = B.C() + for i in range(4): + key["id_b"] = i + mu = random.normalvariate(0, 10) + sigma = random.lognormvariate(0, 4) + n = random.randint(0, 10) + self.insert1(dict(key, mu=mu, sigma=sigma, n=n)) + sub.insert( + dict(key, id_c=j, value=random.normalvariate(mu, sigma)) + for j in range(n) + ) + + +class L(dj.Lookup): + definition = """ + id_l: int + --- + cond_in_l :tinyint + """ + contents = [(i, i % 3 >= i % 5) for i in range(30)] + + +class D(dj.Computed): + definition = """ + -> A + id_d :int + --- + -> L + """ + + def _make_tuples(self, key): + # make reference to a random tuple from L + random.seed(str(key)) + lookup = list(L().fetch("KEY")) + self.insert(dict(key, id_d=i, **random.choice(lookup)) for i in range(4)) + + +class E(dj.Computed): + definition = """ + -> B + -> D + --- + -> L + """ + + class F(dj.Part): + definition = """ + -> E + id_f :int + --- + -> B.C + """ + + def make(self, key): + random.seed(str(key)) + self.insert1(dict(key, **random.choice(list(L().fetch("KEY"))))) + sub = E.F() + references = list((B.C() & key).fetch("KEY")) + random.shuffle(references) + sub.insert( + dict(key, id_f=i, **ref) + for i, ref in enumerate(references) + if random.getrandbits(1) + ) + + +class F(dj.Manual): + definition = """ + id: int + ---- + date=null: date + """ + + +class DataA(dj.Lookup): + definition = """ + idx : int + --- + a : int + """ + contents = list(zip(range(5), range(5))) + + +class DataB(dj.Lookup): + definition = """ + idx : int + --- + a : int + """ + contents = list(zip(range(5), range(5, 10))) + + +class Website(dj.Lookup): + definition = """ + url_hash : uuid + --- + url : varchar(1000) + """ + + def insert1_url(self, url): + hashed = hashlib.sha1() + hashed.update(url.encode()) + url_hash = uuid.UUID(bytes=hashed.digest()[:16]) + self.insert1(dict(url=url, url_hash=url_hash), skip_duplicates=True) + return url_hash + + +class Profile(dj.Manual): + definition = """ + ssn : char(11) + --- + name : varchar(70) + residence : varchar(255) + blood_group : enum('A+', 'A-', 'AB+', 'AB-', 'B+', 'B-', 'O+', 'O-') + username : varchar(120) + birthdate : date + job : varchar(120) + sex : enum('M', 'F') + """ + + class Website(dj.Part): + definition = """ + -> master + -> Website + """ + + def populate_random(self, n=10): + fake = faker.Faker() + faker.Faker.seed(0) # make test deterministic + for _ in range(n): + profile = fake.profile() + with self.connection.transaction: + self.insert1(profile, ignore_extra_fields=True) + for url in profile["website"]: + self.Website().insert1( + dict(ssn=profile["ssn"], url_hash=Website().insert1_url(url)) + ) + + +class TTestUpdate(dj.Lookup): + definition = """ + primary_key : int + --- + string_attr : varchar(255) + num_attr=null : float + blob_attr : longblob + """ + + contents = [ + (0, "my_string", 0.0, np.random.randn(10, 2)), + (1, "my_other_string", 1.0, np.random.randn(20, 1)), + ] + + +class ArgmaxTest(dj.Lookup): + definition = """ + primary_key : int + --- + secondary_key : char(2) + val : float + """ + + n = 10 + + @property + def contents(self): + n = self.n + yield from zip( + range(n**2), + itertools.chain(*itertools.repeat(tuple(map(chr, range(100, 100 + n))), n)), + np.random.rand(n**2), + ) + + +class ReservedWord(dj.Manual): + definition = """ + # Test of SQL reserved words + key : int + --- + in : varchar(25) + from : varchar(25) + int : int + select : varchar(25) + """ + + +class OutfitLaunch(dj.Lookup): + definition = """ + # Monthly released designer outfits + release_id: int + --- + day: date + """ + contents = [(0, date.today() - timedelta(days=15))] + + class OutfitPiece(dj.Part, dj.Lookup): + definition = """ + # Outfit piece associated with outfit + -> OutfitLaunch + piece: varchar(20) + """ + contents = [(0, "jeans"), (0, "sneakers"), (0, "polo")] + + +LOCALS_SIMPLE = {k: v for k, v in locals().items() if inspect.isclass(v)} +__all__ = list(LOCALS_SIMPLE) diff --git a/tests/schema_university.py b/tests/schema_university.py new file mode 100644 index 000000000..c569dbcbf --- /dev/null +++ b/tests/schema_university.py @@ -0,0 +1,112 @@ +import datajoint as dj +import inspect + + +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 + """ + + +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) + """ + + +class StudentMajor(dj.Manual): + definition = """ + -> Student + --- + -> Department + declare_date : date # when student declared her major + """ + + +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 + """ + + +class Term(dj.Manual): + definition = """ + term_year : year + term : enum('Spring', 'Summer', 'Fall') + """ + + +class Section(dj.Manual): + definition = """ + -> Course + -> Term + section : char(1) + --- + auditorium : varchar(12) + """ + + +class CurrentTerm(dj.Manual): + definition = """ + omega=0 : tinyint + --- + -> Term + """ + + +class Enroll(dj.Manual): + definition = """ + -> Student + -> Section + """ + + +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], + ] + + +class Grade(dj.Manual): + definition = """ + -> Enroll + --- + -> LetterGrade + """ + + +LOCALS_UNI = {k: v for k, v in locals().items() if inspect.isclass(v)} +__all__ = list(LOCALS_UNI) diff --git a/tests/schema_uuid.py b/tests/schema_uuid.py new file mode 100644 index 000000000..00b45ee78 --- /dev/null +++ b/tests/schema_uuid.py @@ -0,0 +1,49 @@ +import uuid +import inspect +import datajoint as dj + +top_level_namespace_id = uuid.UUID("00000000-0000-0000-0000-000000000000") + + +class Basic(dj.Manual): + definition = """ + item : uuid + --- + number : int + """ + + +class Topic(dj.Manual): + definition = """ + # A topic for items + topic_id : uuid # internal identification of a topic, reflects topic name + --- + topic : varchar(8000) # full topic name used to generate the topic id + """ + + def add(self, topic): + """add a new topic with a its UUID""" + self.insert1( + dict(topic_id=uuid.uuid5(top_level_namespace_id, topic), topic=topic) + ) + + +class Item(dj.Computed): + definition = """ + item_id : uuid # internal identification of + --- + -> Topic + word : varchar(8000) + """ + + key_source = Topic # test key source that is not instantiated + + def make(self, key): + for word in ("Habenula", "Hippocampus", "Hypothalamus", "Hypophysis"): + self.insert1( + dict(key, word=word, item_id=uuid.uuid5(key["topic_id"], word)) + ) + + +LOCALS_UUID = {k: v for k, v in locals().items() if inspect.isclass(v)} +__all__ = list(LOCALS_UUID) diff --git a/tests/test_adapted_attributes.py b/tests/test_adapted_attributes.py new file mode 100644 index 000000000..714da8a69 --- /dev/null +++ b/tests/test_adapted_attributes.py @@ -0,0 +1,135 @@ +import os +import pytest +import tempfile +import datajoint as dj +import networkx as nx +from itertools import zip_longest +from . import schema_adapted +from .schema_adapted import Connectivity, Layout + + +@pytest.fixture +def schema_name(prefix): + return prefix + "_test_custom_datatype" + + +@pytest.fixture +def adapted_graph_instance(): + yield schema_adapted.GraphAdapter() + + +@pytest.fixture +def schema_ad( + connection_test, + adapted_graph_instance, + enable_adapted_types, + enable_filepath_feature, + s3_creds, + tmpdir, + schema_name, +): + dj.config["stores"] = { + "repo-s3": dict( + s3_creds, protocol="s3", location="adapted/repo", stage=str(tmpdir) + ) + } + context = { + **schema_adapted.LOCALS_ADAPTED, + "graph": adapted_graph_instance, + "layout_to_filepath": schema_adapted.LayoutToFilepath(), + } + schema = dj.schema(schema_name, context=context, connection=connection_test) + schema(schema_adapted.Connectivity) + schema(schema_adapted.Layout) + yield schema + schema.drop() + + +@pytest.fixture +def local_schema(schema_ad, schema_name): + """Fixture for testing spawned classes""" + local_schema = dj.Schema(schema_name) + local_schema.spawn_missing_classes() + yield local_schema + local_schema.drop() + + +@pytest.fixture +def schema_virtual_module(schema_ad, adapted_graph_instance, schema_name): + """Fixture for testing virtual modules""" + schema_virtual_module = dj.VirtualModule( + "virtual_module", schema_name, add_objects={"graph": adapted_graph_instance} + ) + return schema_virtual_module + + +def test_adapted_type(schema_ad): + c = Connectivity() + graphs = [ + nx.lollipop_graph(4, 2), + nx.star_graph(5), + nx.barbell_graph(3, 1), + nx.cycle_graph(5), + ] + c.insert((i, g) for i, g in enumerate(graphs)) + returned_graphs = c.fetch("conn_graph", order_by="connid") + for g1, g2 in zip(graphs, returned_graphs): + assert isinstance(g2, nx.Graph) + assert len(g1.edges) == len(g2.edges) + assert 0 == len(nx.symmetric_difference(g1, g2).edges) + c.delete() + + +def test_adapted_filepath_type(schema_ad, minio_client): + """https://github.com/datajoint/datajoint-python/issues/684""" + c = Connectivity() + c.delete() + c.insert1((0, nx.lollipop_graph(4, 2))) + + layout = nx.spring_layout(c.fetch1("conn_graph")) + # make json friendly + layout = {str(k): [round(r, ndigits=4) for r in v] for k, v in layout.items()} + t = Layout() + t.insert1((0, layout)) + result = t.fetch1("layout") + assert result == layout + t.delete() + c.delete() + + +def test_adapted_spawned(local_schema, enable_adapted_types): + c = Connectivity() # a spawned class + graphs = [ + nx.lollipop_graph(4, 2), + nx.star_graph(5), + nx.barbell_graph(3, 1), + nx.cycle_graph(5), + ] + c.insert((i, g) for i, g in enumerate(graphs)) + returned_graphs = c.fetch("conn_graph", order_by="connid") + for g1, g2 in zip(graphs, returned_graphs): + assert isinstance(g2, nx.Graph) + assert len(g1.edges) == len(g2.edges) + assert 0 == len(nx.symmetric_difference(g1, g2).edges) + c.delete() + + +def test_adapted_virtual(schema_virtual_module): + c = schema_virtual_module.Connectivity() + graphs = [ + nx.lollipop_graph(4, 2), + nx.star_graph(5), + nx.barbell_graph(3, 1), + nx.cycle_graph(5), + ] + c.insert((i, g) for i, g in enumerate(graphs)) + c.insert1({"connid": 100}) # test work with NULLs + returned_graphs = c.fetch("conn_graph", order_by="connid") + for g1, g2 in zip_longest(graphs, returned_graphs): + if g1 is None: + assert g2 is None + else: + assert isinstance(g2, nx.Graph) + assert len(g1.edges) == len(g2.edges) + assert 0 == len(nx.symmetric_difference(g1, g2).edges) + c.delete() diff --git a/tests/test_admin.py b/tests/test_admin.py index 1ab89c1af..43b418f80 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -7,19 +7,17 @@ import pymysql import pytest -from . import CONN_INFO_ROOT - @pytest.fixture() -def user_alice() -> dict: +def user_alice(db_creds_root) -> dict: # set up - reset config, log in as root, and create a new user alice # reset dj.config manually because its state may be changed by these tests if os.path.exists(dj.settings.LOCALCONFIG): os.remove(dj.settings.LOCALCONFIG) dj.config["database.password"] = os.getenv("DJ_PASS") - root_conn = dj.conn(**CONN_INFO_ROOT, reset=True) + root_conn = dj.conn(**db_creds_root, reset=True) new_credentials = dict( - host=CONN_INFO_ROOT["host"], + host=db_creds_root["host"], user="alice", password="oldpass", ) diff --git a/tests/test_aggr_regressions.py b/tests/test_aggr_regressions.py new file mode 100644 index 000000000..7cc5119ea --- /dev/null +++ b/tests/test_aggr_regressions.py @@ -0,0 +1,127 @@ +""" +Regression tests for issues 386, 449, 484, and 558 — all related to processing complex aggregations and projections. +""" + +import pytest +import datajoint as dj +import uuid +from .schema_uuid import Topic, Item, top_level_namespace_id +from .schema_aggr_regress import R, Q, S, A, B, X, LOCALS_AGGR_REGRESS + + +@pytest.fixture(scope="function") +def schema_aggr_reg(connection_test, prefix): + schema = dj.Schema( + prefix + "_aggr_regress", + context=LOCALS_AGGR_REGRESS, + connection=connection_test, + ) + schema(R) + schema(Q) + schema(S) + yield schema + schema.drop() + + +@pytest.fixture(scope="function") +def schema_aggr_reg_with_abx(connection_test, prefix): + schema = dj.Schema( + prefix + "_aggr_regress_with_abx", + context=LOCALS_AGGR_REGRESS, + connection=connection_test, + ) + schema(R) + schema(Q) + schema(S) + schema(A) + schema(B) + schema(X) + yield schema + schema.drop() + + +def test_issue386(schema_aggr_reg): + """ + --------------- 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 + """ + result = R.aggr(S, n="count(*)") & "n=10" + result = Q & result + result.fetch() + + +def test_issue449(schema_aggr_reg): + """ + ---------------- ISSUE 449 ------------------ + Issue 449 arises from incorrect group by attributes after joining with a dj.U() + """ + result = dj.U("n") * R.aggr(S, n="max(s)") + result.fetch() + + +def test_issue484(schema_aggr_reg): + """ + ---------------- ISSUE 484 ----------------- + Issue 484 + """ + 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() + + +def test_union_join(schema_aggr_reg_with_abx): + """ + This test fails if it runs after TestIssue558. + + https://github.com/datajoint/datajoint-python/issues/930 + """ + A.insert(zip([100, 200, 300, 400, 500, 600])) + B.insert([(100, 11), (200, 22), (300, 33), (400, 44)]) + q1 = B & "id < 300" + q2 = B & "id > 300" + + expected_data = [ + {"id": 0, "id2": 5}, + {"id": 1, "id2": 6}, + {"id": 2, "id2": 7}, + {"id": 3, "id2": 8}, + {"id": 4, "id2": 9}, + {"id": 100, "id2": 11}, + {"id": 200, "id2": 22}, + {"id": 400, "id2": 44}, + ] + + assert ((q1 + q2) * A).fetch(as_dict=True) == expected_data + + +class TestIssue558: + """ + --------------- 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. + """ + + def test_issue558_part1(self, schema_aggr_reg_with_abx): + q = (A - B).proj(id2="3") + assert len(A - B) == len(q) + + def test_issue558_part2(self, schema_aggr_reg_with_abx): + d = dict(id=3, id2=5) + assert len(X & d) == len((X & d).proj(id2="3")) + + +def test_left_join_len(schema_uuid): + Topic().add("jeff") + Item.populate() + Topic().add("jeff2") + Topic().add("jeff3") + q = Topic.join( + Item - dict(topic_id=uuid.uuid5(top_level_namespace_id, "jeff")), left=True + ) + qf = q.fetch() + assert len(q) == len(qf) diff --git a/tests/test_alter.py b/tests/test_alter.py new file mode 100644 index 000000000..5146d6266 --- /dev/null +++ b/tests/test_alter.py @@ -0,0 +1,65 @@ +import pytest +import re +import datajoint as dj +from . import schema as schema_any_module +from .schema_alter import Experiment, Parent, LOCALS_ALTER + +COMBINED_CONTEXT = { + **schema_any_module.LOCALS_ANY, + **LOCALS_ALTER, +} + + +@pytest.fixture +def schema_alter(connection_test, schema_any): + # Overwrite Experiment and Parent nodes + schema_any(Experiment, context=LOCALS_ALTER) + schema_any(Parent, context=LOCALS_ALTER) + yield schema_any + schema_any.drop() + + +class TestAlter: + def verify_alter(self, schema_alter, table, attribute_sql): + definition_original = schema_alter.connection.query( + f"SHOW CREATE TABLE {table.full_table_name}" + ).fetchone()[1] + table.definition = table.definition_new + table.alter(prompt=False) + definition_new = schema_alter.connection.query( + f"SHOW CREATE TABLE {table.full_table_name}" + ).fetchone()[1] + assert ( + re.sub(f"{attribute_sql},\n ", "", definition_new) == definition_original + ) + + def test_alter(self, schema_alter): + original = schema_alter.connection.query( + "SHOW CREATE TABLE " + Experiment.full_table_name + ).fetchone()[1] + Experiment.definition = Experiment.definition1 + Experiment.alter(prompt=False, context=COMBINED_CONTEXT) + altered = schema_alter.connection.query( + "SHOW CREATE TABLE " + Experiment.full_table_name + ).fetchone()[1] + assert original != altered + Experiment.definition = Experiment.original_definition + Experiment().alter(prompt=False, context=COMBINED_CONTEXT) + restored = schema_alter.connection.query( + "SHOW CREATE TABLE " + Experiment.full_table_name + ).fetchone()[1] + assert altered != restored + assert original == restored + + def test_alter_part(self, schema_alter): + """ + https://github.com/datajoint/datajoint-python/issues/936 + """ + self.verify_alter( + schema_alter, table=Parent.Child, attribute_sql="`child_id` .* DEFAULT NULL" + ) + self.verify_alter( + schema_alter, + table=Parent.Grandchild, + attribute_sql="`grandchild_id` .* DEFAULT NULL", + ) diff --git a/tests/test_attach.py b/tests/test_attach.py new file mode 100644 index 000000000..b3ecea04e --- /dev/null +++ b/tests/test_attach.py @@ -0,0 +1,66 @@ +import pytest +from pathlib import Path +import os +from .schema_external import Attach + + +def test_attach_attributes(schema_ext, minio_client, tmpdir_factory): + """Test saving files in attachments""" + # create a mock file + table = Attach() + source_folder = tmpdir_factory.mktemp("source") + for i in range(2): + attach1 = Path(source_folder, "attach1.img") + data1 = os.urandom(100) + with attach1.open("wb") as f: + f.write(data1) + attach2 = Path(source_folder, "attach2.txt") + data2 = os.urandom(200) + with attach2.open("wb") as f: + f.write(data2) + table.insert1(dict(attach=i, img=attach1, txt=attach2)) + + download_folder = Path(tmpdir_factory.mktemp("download")) + keys, path1, path2 = table.fetch( + "KEY", "img", "txt", download_path=download_folder, order_by="KEY" + ) + + # verify that different attachment are renamed if their filenames collide + assert path1[0] != path2[0] + assert path1[0] != path1[1] + assert Path(path1[0]).parent == download_folder + with Path(path1[-1]).open("rb") as f: + check1 = f.read() + with Path(path2[-1]).open("rb") as f: + check2 = f.read() + assert data1 == check1 + assert data2 == check2 + + # verify that existing files are not duplicated if their filename matches issue #592 + p1, p2 = (Attach & keys[0]).fetch1("img", "txt", download_path=download_folder) + assert p1 == path1[0] + assert p2 == path2[0] + + +def test_return_string(schema_ext, minio_client, tmpdir_factory): + """Test returning string on fetch""" + # create a mock file + table = Attach() + source_folder = tmpdir_factory.mktemp("source") + + attach1 = Path(source_folder, "attach1.img") + data1 = os.urandom(100) + with attach1.open("wb") as f: + f.write(data1) + attach2 = Path(source_folder, "attach2.txt") + data2 = os.urandom(200) + with attach2.open("wb") as f: + f.write(data2) + table.insert1(dict(attach=2, img=attach1, txt=attach2)) + + download_folder = Path(tmpdir_factory.mktemp("download")) + keys, path1, path2 = table.fetch( + "KEY", "img", "txt", download_path=download_folder, order_by="KEY" + ) + + assert isinstance(path1[0], str) diff --git a/tests/test_autopopulate.py b/tests/test_autopopulate.py new file mode 100644 index 000000000..d1f0726e1 --- /dev/null +++ b/tests/test_autopopulate.py @@ -0,0 +1,127 @@ +import pytest +from datajoint import DataJointError +import datajoint as dj +import pymysql +from . import schema + + +def test_populate(trial, subject, experiment, ephys, channel): + # test simple populate + assert subject, "root tables are empty" + assert not experiment, "table already filled?" + experiment.populate() + assert len(experiment) == len(subject) * experiment.fake_experiments_per_subject + + # test restricted populate + assert not trial, "table already filled?" + restriction = subject.proj(animal="subject_id").fetch("KEY")[0] + d = trial.connection.dependencies + d.load() + trial.populate(restriction) + assert trial, "table was not populated" + key_source = trial.key_source + assert len(key_source & trial) == len(key_source & restriction) + assert len(key_source - trial) == len(key_source - restriction) + + # test subtable populate + assert not ephys + assert not channel + ephys.populate() + assert ephys + assert channel + + +def test_populate_with_success_count(subject, experiment, trial): + # test simple populate + assert subject, "root tables are empty" + assert not experiment, "table already filled?" + ret = experiment.populate() + success_count = ret["success_count"] + assert len(experiment.key_source & experiment) == success_count + + # test restricted populate + assert not trial, "table already filled?" + restriction = subject.proj(animal="subject_id").fetch("KEY")[0] + d = trial.connection.dependencies + d.load() + ret = trial.populate(restriction, suppress_errors=True) + success_count = ret["success_count"] + assert len(trial.key_source & trial) == success_count + + +def test_populate_exclude_error_and_ignore_jobs(schema_any, subject, experiment): + # test simple populate + assert subject, "root tables are empty" + assert not experiment, "table already filled?" + + keys = experiment.key_source.fetch("KEY", limit=2) + for idx, key in enumerate(keys): + if idx == 0: + schema_any.jobs.ignore(experiment.table_name, key) + else: + schema_any.jobs.error(experiment.table_name, key, "") + + experiment.populate(reserve_jobs=True) + assert len(experiment.key_source & experiment) == len(experiment.key_source) - 2 + + +def test_allow_direct_insert(subject, experiment): + assert subject, "root tables are empty" + key = subject.fetch("KEY", limit=1)[0] + key["experiment_id"] = 1000 + key["experiment_date"] = "2018-10-30" + experiment.insert1(key, allow_direct_insert=True) + + +@pytest.mark.parametrize("processes", [None, 2]) +def test_multi_processing(subject, experiment, processes): + assert subject, "root tables are empty" + assert not experiment, "table already filled?" + experiment.populate(processes=None) + assert len(experiment) == len(subject) * experiment.fake_experiments_per_subject + + +def test_allow_insert(subject, experiment): + assert subject, "root tables are empty" + key = subject.fetch("KEY")[0] + key["experiment_id"] = 1001 + key["experiment_date"] = "2018-10-30" + with pytest.raises(DataJointError): + experiment.insert1(key) + + +def test_load_dependencies(prefix): + schema = dj.Schema(f"{prefix}_load_dependencies_populate") + + @schema + class ImageSource(dj.Lookup): + definition = """ + image_source_id: int + """ + contents = [(0,)] + + @schema + class Image(dj.Imported): + definition = """ + -> ImageSource + --- + image_data: longblob + """ + + def make(self, key): + self.insert1(dict(key, image_data=dict())) + + Image.populate() + + @schema + class Crop(dj.Computed): + definition = """ + -> Image + --- + crop_image: longblob + """ + + def make(self, key): + self.insert1(dict(key, crop_image=dict())) + + Crop.populate() diff --git a/tests_old/test_blob.py b/tests/test_blob.py similarity index 65% rename from tests_old/test_blob.py rename to tests/test_blob.py index 3765edc57..12039f7fb 100644 --- a/tests_old/test_blob.py +++ b/tests/test_blob.py @@ -1,21 +1,21 @@ +import pytest import datajoint as dj import timeit import numpy as np import uuid -from . import schema from decimal import Decimal from datetime import datetime from datajoint.blob import pack, unpack from numpy.testing import assert_array_equal -from nose.tools import ( - assert_equal, - assert_true, - assert_false, - assert_list_equal, - assert_set_equal, - assert_tuple_equal, - assert_dict_equal, -) +from pytest import approx +from .schema import Longblob + + +@pytest.fixture +def enable_feature_32bit_dims(): + dj.blob.use_32bit_dims = True + yield + dj.blob.use_32bit_dims = False def test_pack(): @@ -24,19 +24,19 @@ def test_pack(): -3.7e-2, np.float64(3e31), -np.inf, - np.int8(-3), - np.uint8(-1), + np.array(-3).astype(np.uint8), + np.array(-1).astype(np.uint8), np.int16(-33), - np.uint16(-33), + np.array(-33).astype(np.uint16), np.int32(-3), - np.uint32(-1), + np.array(-1).astype(np.uint32), np.int64(373), - np.uint64(-3), + np.array(-3).astype(np.uint64), ): - assert_equal(x, unpack(pack(x)), "Scalars don't match!") + assert x == approx(unpack(pack(x)), rel=1e-6), "Scalars don't match!" x = np.nan - assert_true(np.isnan(unpack(pack(x))), "nan scalar did not match!") + assert np.isnan(unpack(pack(x))), "nan scalar did not match!" x = np.random.randn(8, 10) assert_array_equal(x, unpack(pack(x)), "Arrays do not match!") @@ -45,7 +45,7 @@ def test_pack(): assert_array_equal(x, unpack(pack(x)), "Arrays do not match!") x = 7j - assert_equal(x, unpack(pack(x)), "Complex scalar does not match") + assert x == unpack(pack(x)), "Complex scalar does not match" x = np.float32(np.random.randn(3, 4, 5)) assert_array_equal(x, unpack(pack(x)), "Arrays do not match!") @@ -54,41 +54,37 @@ def test_pack(): assert_array_equal(x, unpack(pack(x)), "Arrays do not match!") x = None - assert_true(unpack(pack(x)) is None, "None did not match") + assert unpack(pack(x)) is None, "None did not match" x = -255 y = unpack(pack(x)) - assert_true( - x == y and isinstance(y, int) and not isinstance(y, np.ndarray), - "Scalar int did not match", - ) + assert ( + x == y and isinstance(y, int) and not isinstance(y, np.ndarray) + ), "Scalar int did not match" x = -25523987234234287910987234987098245697129798713407812347 y = unpack(pack(x)) - assert_true( - x == y and isinstance(y, int) and not isinstance(y, np.ndarray), - "Unbounded int did not match", - ) + assert ( + x == y and isinstance(y, int) and not isinstance(y, np.ndarray) + ), "Unbounded int did not match" x = 7.0 y = unpack(pack(x)) - assert_true( - x == y and isinstance(y, float) and not isinstance(y, np.ndarray), - "Scalar float did not match", - ) + assert ( + x == y and isinstance(y, float) and not isinstance(y, np.ndarray) + ), "Scalar float did not match" x = 7j y = unpack(pack(x)) - assert_true( - x == y and isinstance(y, complex) and not isinstance(y, np.ndarray), - "Complex scalar did not match", - ) + assert ( + x == y and isinstance(y, complex) and not isinstance(y, np.ndarray) + ), "Complex scalar did not match" x = True - assert_true(unpack(pack(x)) is True, "Scalar bool did not match") + assert unpack(pack(x)) is True, "Scalar bool did not match" x = [None] - assert_list_equal(x, unpack(pack(x))) + assert [None] == unpack(pack(x)) x = { "name": "Anonymous", @@ -98,22 +94,22 @@ def test_pack(): (11, 12): None, } y = unpack(pack(x)) - assert_dict_equal(x, y, "Dict do not match!") - assert_false( - isinstance(["range"][0], np.ndarray), "Scalar int was coerced into array." - ) + assert x == y, "Dict do not match!" + assert not isinstance( + ["range"][0], np.ndarray + ), "Scalar int was coerced into array." x = uuid.uuid4() - assert_equal(x, unpack(pack(x)), "UUID did not match") + assert x == unpack(pack(x)), "UUID did not match" x = Decimal("-112122121.000003000") - assert_equal(x, unpack(pack(x)), "Decimal did not pack/unpack correctly") + assert x == unpack(pack(x)), "Decimal did not pack/unpack correctly" x = [1, datetime.now(), {1: "one", "two": 2}, (1, 2)] - assert_list_equal(x, unpack(pack(x)), "List did not pack/unpack correctly") + assert x == unpack(pack(x)), "List did not pack/unpack correctly" x = (1, datetime.now(), {1: "one", "two": 2}, (uuid.uuid4(), 2)) - assert_tuple_equal(x, unpack(pack(x)), "Tuple did not pack/unpack correctly") + assert x == unpack(pack(x)), "Tuple did not pack/unpack correctly" x = ( 1, @@ -121,36 +117,34 @@ def test_pack(): {"yes!": [1, 2, np.array((3, 4))]}, ) y = unpack(pack(x)) - assert_dict_equal(x[1], y[1]) + assert x[1] == y[1] assert_array_equal(x[2]["yes!"][2], y[2]["yes!"][2]) x = {"elephant"} - assert_set_equal(x, unpack(pack(x)), "Set did not pack/unpack correctly") + assert x == unpack(pack(x)), "Set did not pack/unpack correctly" x = tuple(range(10)) - assert_tuple_equal( - x, unpack(pack(range(10))), "Iterator did not pack/unpack correctly" - ) + assert x == unpack(pack(range(10))), "Iterator did not pack/unpack correctly" x = Decimal("1.24") - assert_true(x == unpack(pack(x)), "Decimal object did not pack/unpack correctly") + assert x == approx(unpack(pack(x))), "Decimal object did not pack/unpack correctly" x = datetime.now() - assert_true(x == unpack(pack(x)), "Datetime object did not pack/unpack correctly") + assert x == unpack(pack(x)), "Datetime object did not pack/unpack correctly" x = np.bool_(True) - assert_true(x == unpack(pack(x)), "Numpy bool object did not pack/unpack correctly") + assert x == unpack(pack(x)), "Numpy bool object did not pack/unpack correctly" x = "test" - assert_true(x == unpack(pack(x)), "String object did not pack/unpack correctly") + assert x == unpack(pack(x)), "String object did not pack/unpack correctly" x = np.array(["yes"]) - assert_true( - x == unpack(pack(x)), "Numpy string array object did not pack/unpack correctly" - ) + assert x == unpack( + pack(x) + ), "Numpy string array object did not pack/unpack correctly" x = np.datetime64("1998").astype("datetime64[us]") - assert_true(x == unpack(pack(x))) + assert x == unpack(pack(x)) def test_recarrays(): @@ -183,19 +177,19 @@ def test_complex(): assert_array_equal(x, unpack(pack(x)), "Arrays do not match!") -def test_insert_longblob(): +def test_insert_longblob(schema_any): insert_dj_blob = {"id": 1, "data": [1, 2, 3]} - schema.Longblob.insert1(insert_dj_blob) - assert (schema.Longblob & "id=1").fetch1() == insert_dj_blob - (schema.Longblob & "id=1").delete() + Longblob.insert1(insert_dj_blob) + assert (Longblob & "id=1").fetch1() == insert_dj_blob + (Longblob & "id=1").delete() query_mym_blob = {"id": 1, "data": np.array([1, 2, 3])} - schema.Longblob.insert1(query_mym_blob) - assert (schema.Longblob & "id=1").fetch1()["data"].all() == query_mym_blob[ - "data" - ].all() - (schema.Longblob & "id=1").delete() + Longblob.insert1(query_mym_blob) + assert (Longblob & "id=1").fetch1()["data"].all() == query_mym_blob["data"].all() + (Longblob & "id=1").delete() + +def test_insert_longblob_32bit(schema_any, enable_feature_32bit_dims): query_32_blob = ( "INSERT INTO djtest_test1.longblob (id, data) VALUES (1, " "X'6D596D00530200000001000000010000000400000068697473007369646573007461736B73007374" @@ -206,8 +200,8 @@ def test_insert_longblob(): "00000041020000000100000008000000040000000000000053007400610067006500200031003000')" ) dj.conn().query(query_32_blob).fetchall() - dj.blob.use_32bit_dims = True - assert (schema.Longblob & "id=1").fetch1() == { + fetched = (Longblob & "id=1").fetch1() + expected = { "id": 1, "data": np.rec.array( [ @@ -223,26 +217,34 @@ def test_insert_longblob(): dtype=[("hits", "O"), ("sides", "O"), ("tasks", "O"), ("stage", "O")], ), } - (schema.Longblob & "id=1").delete() - dj.blob.use_32bit_dims = False + assert fetched["id"] == expected["id"] + assert np.array_equal(fetched["data"], expected["data"]) + (Longblob & "id=1").delete() def test_datetime_serialization_speed(): # If this fails that means for some reason deserializing/serializing # np arrays of np.datetime64 types is now slower than regular arrays of datetime + assert not dj.blob.use_32bit_dims, "32 bit dims should be off for this test" + context = dict( + np=np, + datetime=datetime, + pack=pack, + unpack=unpack, + ) optimized_exe_time = timeit.timeit( setup="myarr=pack(np.array([np.datetime64('2022-10-13 03:03:13') for _ in range(0, 10000)]))", stmt="unpack(myarr)", number=10, - globals=globals(), + globals=context, ) print(f"np time {optimized_exe_time}") baseline_exe_time = timeit.timeit( setup="myarr2=pack(np.array([datetime(2022,10,13,3,3,13) for _ in range (0, 10000)]))", stmt="unpack(myarr2)", number=10, - globals=globals(), + globals=context, ) print(f"python time {baseline_exe_time}") diff --git a/tests/test_blob_matlab.py b/tests/test_blob_matlab.py new file mode 100644 index 000000000..8e467cf06 --- /dev/null +++ b/tests/test_blob_matlab.py @@ -0,0 +1,171 @@ +import numpy as np +import pytest +import datajoint as dj +from datajoint.blob import pack, unpack +from numpy.testing import assert_array_equal + + +class Blob(dj.Manual): + definition = """ # diverse types of blobs + id : int + ----- + comment : varchar(255) + blob : longblob + """ + + +def insert_blobs(schema): + """ + This function inserts blobs resulting from the following datajoint-matlab code: + + self.insert({ + 1 'simple string' 'character string' + 2 '1D vector' 1:15:180 + 3 'string array' {'string1' 'string2'} + 4 'struct array' struct('a', {1,2}, 'b', {struct('c', magic(3)), struct('C', magic(5))}) + 5 '3D double array' reshape(1:24, [2,3,4]) + 6 '3D uint8 array' reshape(uint8(1:24), [2,3,4]) + 7 '3D complex array' fftn(reshape(1:24, [2,3,4])) + }) + + and then dumped using the command + mysqldump -u username -p --hex-blob test_schema blob_table > blob.sql + """ + + schema.connection.query( + """ + INSERT INTO {table_name} VALUES + (1,'simple string',0x6D596D00410200000000000000010000000000000010000000000000000400000000000000630068006100720061006300740065007200200073007400720069006E006700), + (2,'1D vector',0x6D596D0041020000000000000001000000000000000C000000000000000600000000000000000000000000F03F00000000000030400000000000003F4000000000000047400000000000804E4000000000000053400000000000C056400000000000805A400000000000405E4000000000000061400000000000E062400000000000C06440), + (3,'string array',0x6D596D00430200000000000000010000000000000002000000000000002F0000000000000041020000000000000001000000000000000700000000000000040000000000000073007400720069006E00670031002F0000000000000041020000000000000001000000000000000700000000000000040000000000000073007400720069006E0067003200), + (4,'struct array',0x6D596D005302000000000000000100000000000000020000000000000002000000610062002900000000000000410200000000000000010000000000000001000000000000000600000000000000000000000000F03F9000000000000000530200000000000000010000000000000001000000000000000100000063006900000000000000410200000000000000030000000000000003000000000000000600000000000000000000000000204000000000000008400000000000001040000000000000F03F0000000000001440000000000000224000000000000018400000000000001C40000000000000004029000000000000004102000000000000000100000000000000010000000000000006000000000000000000000000000040100100000000000053020000000000000001000000000000000100000000000000010000004300E9000000000000004102000000000000000500000000000000050000000000000006000000000000000000000000003140000000000000374000000000000010400000000000002440000000000000264000000000000038400000000000001440000000000000184000000000000028400000000000003240000000000000F03F0000000000001C400000000000002A400000000000003340000000000000394000000000000020400000000000002C400000000000003440000000000000354000000000000000400000000000002E400000000000003040000000000000364000000000000008400000000000002240), + (5,'3D double array',0x6D596D004103000000000000000200000000000000030000000000000004000000000000000600000000000000000000000000F03F000000000000004000000000000008400000000000001040000000000000144000000000000018400000000000001C40000000000000204000000000000022400000000000002440000000000000264000000000000028400000000000002A400000000000002C400000000000002E40000000000000304000000000000031400000000000003240000000000000334000000000000034400000000000003540000000000000364000000000000037400000000000003840), + (6,'3D uint8 array',0x6D596D0041030000000000000002000000000000000300000000000000040000000000000009000000000000000102030405060708090A0B0C0D0E0F101112131415161718), + (7,'3D complex array',0x6D596D0041030000000000000002000000000000000300000000000000040000000000000006000000010000000000000000C0724000000000000028C000000000000038C0000000000000000000000000000038C0000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000AA4C58E87AB62B400000000000000000AA4C58E87AB62BC0000000000000008000000000000052400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000008000000000000052C000000000000000800000000000000080000000000000008000000000000000800000000000000080 + ); + """.format( + table_name=Blob.full_table_name + ) + ) + + +@pytest.fixture +def schema_blob(connection_test, prefix): + schema = dj.Schema(prefix + "_test1", dict(Blob=Blob), connection=connection_test) + schema(Blob) + yield schema + schema.drop() + + +@pytest.fixture +def schema_blob_pop(schema_blob): + assert not dj.config["safemode"], "safemode must be disabled" + Blob().delete() + insert_blobs(schema_blob) + return schema_blob + + +def test_complex_matlab_blobs(schema_blob_pop): + """ + test correct de-serialization of various blob types + """ + blobs = Blob().fetch("blob", order_by="KEY") + + blob = blobs[0] # 'simple string' 'character string' + assert blob[0] == "character string" + + blob = blobs[1] # '1D vector' 1:15:180 + assert_array_equal(blob, np.r_[1:180:15][None, :]) + assert_array_equal(blob, unpack(pack(blob))) + + blob = blobs[2] # 'string array' {'string1' 'string2'} + assert isinstance(blob, dj.MatCell) + assert_array_equal(blob, np.array([["string1", "string2"]])) + assert_array_equal(blob, unpack(pack(blob))) + + blob = blobs[ + 3 + ] # 'struct array' struct('a', {1,2}, 'b', {struct('c', magic(3)), struct('C', magic(5))}) + assert isinstance(blob, dj.MatStruct) + assert tuple(blob.dtype.names) == ("a", "b") + assert_array_equal(blob.a[0, 0], np.array([[1.0]])) + assert_array_equal(blob.a[0, 1], np.array([[2.0]])) + assert isinstance(blob.b[0, 1], dj.MatStruct) + assert tuple(blob.b[0, 1].C[0, 0].shape) == (5, 5) + b = unpack(pack(blob)) + assert_array_equal(b[0, 0].b[0, 0].c, blob[0, 0].b[0, 0].c) + assert_array_equal(b[0, 1].b[0, 0].C, blob[0, 1].b[0, 0].C) + + blob = blobs[4] # '3D double array' reshape(1:24, [2,3,4]) + assert_array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order="F")) + assert blob.dtype == "float64" + assert_array_equal(blob, unpack(pack(blob))) + + blob = blobs[5] # reshape(uint8(1:24), [2,3,4]) + assert np.array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order="F")) + assert blob.dtype == "uint8" + assert_array_equal(blob, unpack(pack(blob))) + + blob = blobs[6] # fftn(reshape(1:24, [2,3,4])) + assert tuple(blob.shape) == (2, 3, 4) + assert blob.dtype == "complex128" + assert_array_equal(blob, unpack(pack(blob))) + + +def test_complex_matlab_squeeze(schema_blob_pop): + """ + test correct de-serialization of various blob types + """ + blob = (Blob & "id=1").fetch1( + "blob", squeeze=True + ) # 'simple string' 'character string' + assert blob == "character string" + + blob = (Blob & "id=2").fetch1("blob", squeeze=True) # '1D vector' 1:15:180 + assert_array_equal(blob, np.r_[1:180:15]) + + blob = (Blob & "id=3").fetch1( + "blob", squeeze=True + ) # 'string array' {'string1' 'string2'} + assert isinstance(blob, dj.MatCell) + assert_array_equal(blob, np.array(["string1", "string2"])) + + blob = (Blob & "id=4").fetch1( + "blob", squeeze=True + ) # 'struct array' struct('a', {1,2}, 'b', {struct('c', magic(3)), struct('C', magic(5))}) + assert isinstance(blob, dj.MatStruct) + assert tuple(blob.dtype.names) == ("a", "b") + assert_array_equal( + blob.a, + np.array( + [ + 1.0, + 2, + ] + ), + ) + assert isinstance(blob[1].b, dj.MatStruct) + assert tuple(blob[1].b.C.item().shape) == (5, 5) + + blob = (Blob & "id=5").fetch1( + "blob", squeeze=True + ) # '3D double array' reshape(1:24, [2,3,4]) + assert np.array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order="F")) + assert blob.dtype == "float64" + + blob = (Blob & "id=6").fetch1("blob", squeeze=True) # reshape(uint8(1:24), [2,3,4]) + assert np.array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order="F")) + assert blob.dtype == "uint8" + + blob = (Blob & "id=7").fetch1("blob", squeeze=True) # fftn(reshape(1:24, [2,3,4])) + assert tuple(blob.shape) == (2, 3, 4) + assert blob.dtype == "complex128" + + +def test_iter(schema_blob_pop): + """ + test iterator over the entity set + """ + from_iter = {d["id"]: d for d in Blob()} + assert len(from_iter) == len(Blob()) + assert from_iter[1]["blob"] == "character string" diff --git a/tests/test_bypass_serialization.py b/tests/test_bypass_serialization.py new file mode 100644 index 000000000..90fc35090 --- /dev/null +++ b/tests/test_bypass_serialization.py @@ -0,0 +1,56 @@ +import pytest +import datajoint as dj +import numpy as np +from numpy.testing import assert_array_equal + +test_blob = np.array([1, 2, 3]) + + +class Input(dj.Lookup): + definition = """ + id: int + --- + data: blob + """ + contents = [(0, test_blob)] + + +class Output(dj.Manual): + definition = """ + id: int + --- + data: blob + """ + + +@pytest.fixture +def schema_in(connection_test, prefix): + schema = dj.Schema( + prefix + "_test_bypass_serialization_in", + context=dict(Input=Input), + connection=connection_test, + ) + schema(Input) + yield schema + schema.drop() + + +@pytest.fixture +def schema_out(connection_test, prefix): + schema = dj.Schema( + prefix + "_test_blob_bypass_serialization_out", + context=dict(Output=Output), + connection=connection_test, + ) + schema(Output) + yield schema + schema.drop() + + +def test_bypass_serialization(schema_in, schema_out): + dj.blob.bypass_serialization = True + contents = Input.fetch(as_dict=True) + assert isinstance(contents[0]["data"], bytes) + Output.insert(contents) + dj.blob.bypass_serialization = False + assert_array_equal(Input.fetch1("data"), Output.fetch1("data")) diff --git a/tests/test_cascading_delete.py b/tests/test_cascading_delete.py new file mode 100644 index 000000000..70fedf687 --- /dev/null +++ b/tests/test_cascading_delete.py @@ -0,0 +1,123 @@ +import pytest +import datajoint as dj +from .schema_simple import A, B, D, E, L, Website, Profile +from .schema import ComplexChild, ComplexParent + + +@pytest.fixture +def schema_simp_pop(schema_simp): + A().insert(A.contents, skip_duplicates=True) + L().insert(L.contents, skip_duplicates=True) + B().populate() + D().populate() + E().populate() + yield schema_simp + + +def test_delete_tree(schema_simp_pop): + assert not dj.config["safemode"], "safemode must be off for testing" + assert ( + L() and A() and B() and B.C() and D() and E() and E.F() + ), "schema is not populated" + A().delete() + assert not A() or B() or B.C() or D() or E() or E.F(), "incomplete delete" + + +def test_stepwise_delete(schema_simp_pop): + assert not dj.config["safemode"], "safemode must be off for testing" + assert L() and A() and B() and B.C(), "schema population failed" + B.C().delete(force=True) + assert not B.C(), "failed to delete child tables" + B().delete() + assert ( + not B() + ), "failed to delete from the parent table following child table deletion" + + +def test_delete_tree_restricted(schema_simp_pop): + assert not dj.config["safemode"], "safemode must be off for testing" + assert ( + L() and A() and B() and B.C() and D() and E() and E.F() + ), "schema is not populated" + cond = "cond_in_a" + rel = A() & cond + rest = dict( + A=len(A()) - len(rel), + B=len(B() - rel), + C=len(B.C() - rel), + D=len(D() - rel), + E=len(E() - rel), + F=len(E.F() - rel), + ) + rel.delete() + assert not ( + rel or B() & rel or B.C() & rel or D() & rel or E() & rel or (E.F() & rel) + ), "incomplete delete" + assert len(A()) == rest["A"], "invalid delete restriction" + assert len(B()) == rest["B"], "invalid delete restriction" + assert len(B.C()) == rest["C"], "invalid delete restriction" + assert len(D()) == rest["D"], "invalid delete restriction" + assert len(E()) == rest["E"], "invalid delete restriction" + assert len(E.F()) == rest["F"], "invalid delete restriction" + + +def test_delete_lookup(schema_simp_pop): + assert not dj.config["safemode"], "safemode must be off for testing" + assert bool( + L() and A() and B() and B.C() and D() and E() and E.F() + ), "schema is not populated" + L().delete() + assert not bool(L() or D() or E() or E.F()), "incomplete delete" + A().delete() # delete all is necessary because delete L deletes from subtables. + + +def test_delete_lookup_restricted(schema_simp_pop): + assert not dj.config["safemode"], "safemode must be off for testing" + assert ( + L() and A() and B() and B.C() and D() and E() and E.F() + ), "schema is not populated" + rel = L() & "cond_in_l" + original_count = len(L()) + deleted_count = len(rel) + rel.delete() + assert len(L()) == original_count - deleted_count + + +def test_delete_complex_keys(schema_any): + """ + https://github.com/datajoint/datajoint-python/issues/883 + https://github.com/datajoint/datajoint-python/issues/886 + """ + assert not 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" + + +def test_delete_master(schema_simp_pop): + Profile().populate_random() + Profile().delete() + + +def test_delete_parts(schema_simp_pop): + """test issue #151""" + with pytest.raises(dj.DataJointError): + Profile().populate_random() + Website().delete() + + +def test_drop_part(schema_simp_pop): + """test issue #374""" + with pytest.raises(dj.DataJointError): + Website().drop() diff --git a/tests/test_connection.py b/tests/test_connection.py index 1916da951..497255753 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -5,40 +5,36 @@ import datajoint as dj from datajoint import DataJointError import numpy as np -from . import CONN_INFO_ROOT, connection_root, connection_test - -from . import PREFIX import pytest +class Subjects(dj.Manual): + definition = """ + #Basic subject + subject_id : int # unique subject id + --- + real_id : varchar(40) # real-world name + species = "mouse" : enum('mouse', 'monkey', 'human') # species + """ + + @pytest.fixture -def schema(connection_test): - schema = dj.Schema(PREFIX + "_transactions", locals(), connection=connection_test) +def schema_tx(connection_test, prefix): + schema = dj.Schema( + prefix + "_transactions", + context=dict(Subjects=Subjects), + connection=connection_test, + ) + schema(Subjects) yield schema schema.drop() -@pytest.fixture -def Subjects(schema): - @schema - class Subjects(dj.Manual): - definition = """ - #Basic subject - subject_id : int # unique subject id - --- - real_id : varchar(40) # real-world name - species = "mouse" : enum('mouse', 'monkey', 'human') # species - """ - - yield Subjects - Subjects.drop() - - -def test_dj_conn(): +def test_dj_conn(db_creds_root): """ Should be able to establish a connection as root user """ - c = dj.conn(**CONN_INFO_ROOT) + c = dj.conn(**db_creds_root) assert c.is_connected @@ -49,24 +45,24 @@ def test_dj_connection_class(connection_test): assert connection_test.is_connected -def test_persistent_dj_conn(): +def test_persistent_dj_conn(db_creds_root): """ conn() method should provide persistent connection across calls. Setting reset=True should create a new persistent connection. """ - c1 = dj.conn(**CONN_INFO_ROOT) + c1 = dj.conn(**db_creds_root) c2 = dj.conn() - c3 = dj.conn(**CONN_INFO_ROOT) - c4 = dj.conn(reset=True, **CONN_INFO_ROOT) - c5 = dj.conn(**CONN_INFO_ROOT) + c3 = dj.conn(**db_creds_root) + c4 = dj.conn(reset=True, **db_creds_root) + c5 = dj.conn(**db_creds_root) assert c1 is c2 assert c1 is c3 assert c1 is not c4 assert c4 is c5 -def test_repr(): - c1 = dj.conn(**CONN_INFO_ROOT) +def test_repr(db_creds_root): + c1 = dj.conn(**db_creds_root) assert "disconnected" not in repr(c1) and "connected" in repr(c1) @@ -75,7 +71,7 @@ def test_active(connection_test): assert conn.in_transaction, "Transaction is not active" -def test_transaction_rollback(connection_test, Subjects): +def test_transaction_rollback(schema_tx, connection_test): """Test transaction cancellation using a with statement""" tmp = np.array( [(1, "Peter", "mouse"), (2, "Klara", "monkey")], @@ -100,13 +96,13 @@ def test_transaction_rollback(connection_test, Subjects): ), "Length is not 0. Expected because rollback should have happened." -def test_cancel(connection_test, Subjects): +def test_cancel(schema_tx, connection_test): """Tests cancelling a transaction explicitly""" tmp = np.array( [(1, "Peter", "mouse"), (2, "Klara", "monkey")], - Subjects.heading.as_dtype, + Subjects().heading.as_dtype, ) - Subjects.delete_quick() + Subjects().delete_quick() Subjects.insert1(tmp[0]) connection_test.start_transaction() Subjects.insert1(tmp[1]) diff --git a/tests/test_declare.py b/tests/test_declare.py new file mode 100644 index 000000000..dfca54c27 --- /dev/null +++ b/tests/test_declare.py @@ -0,0 +1,339 @@ +import pytest +from .schema import * +import datajoint as dj +import inspect +from datajoint.declare import declare + + +def test_schema_decorator(schema_any): + assert issubclass(Subject, dj.Lookup) + assert not issubclass(Subject, dj.Part) + + +def test_class_help(schema_any): + help(TTest) + help(TTest2) + assert TTest.definition in TTest.__doc__ + assert TTest.definition in TTest2.__doc__ + + +def test_instance_help(schema_any): + help(TTest()) + help(TTest2()) + assert TTest().definition in TTest().__doc__ + assert TTest2().definition in TTest2().__doc__ + + +def test_describe(schema_any): + """real_definition should match original definition""" + rel = Experiment() + context = inspect.currentframe().f_globals + s1 = declare(rel.full_table_name, rel.definition, context) + s2 = declare(rel.full_table_name, rel.describe(), context) + assert s1 == s2 + + +def test_describe_indexes(schema_any): + """real_definition should match original definition""" + rel = IndexRich() + context = inspect.currentframe().f_globals + s1 = declare(rel.full_table_name, rel.definition, context) + s2 = declare(rel.full_table_name, rel.describe(), context) + assert s1 == s2 + + +def test_describe_dependencies(schema_any): + """real_definition should match original definition""" + rel = ThingC() + context = inspect.currentframe().f_globals + s1 = declare(rel.full_table_name, rel.definition, context) + s2 = declare(rel.full_table_name, rel.describe(), context) + assert s1 == s2 + + +def test_part(schema_any): + """ + Lookup and part with the same name. See issue #365 + """ + local_schema = dj.Schema(schema_any.database) + + @local_schema + class Type(dj.Lookup): + definition = """ + type : varchar(255) + """ + contents = zip(("Type1", "Type2", "Type3")) + + @local_schema + class TypeMaster(dj.Manual): + definition = """ + master_id : int + """ + + class Type(dj.Part): + definition = """ + -> TypeMaster + -> Type + """ + + +def test_attributes(schema_any): + """ + Test autoincrement declaration + """ + auto = Auto() + auto.fill() + subject = Subject() + experiment = Experiment() + trial = Trial() + ephys = Ephys() + channel = Ephys.Channel() + + assert auto.heading.names == ["id", "name"] + assert auto.heading.attributes["id"].autoincrement + + # test attribute declarations + assert subject.heading.names == [ + "subject_id", + "real_id", + "species", + "date_of_birth", + "subject_notes", + ] + assert subject.primary_key == ["subject_id"] + assert subject.heading.attributes["subject_id"].numeric + assert not subject.heading.attributes["real_id"].numeric + + assert experiment.heading.names == [ + "subject_id", + "experiment_id", + "experiment_date", + "username", + "data_path", + "notes", + "entry_time", + ] + assert experiment.primary_key == ["subject_id", "experiment_id"] + + assert trial.heading.names == [ # tests issue #516 + "animal", + "experiment_id", + "trial_id", + "start_time", + ] + assert trial.primary_key == ["animal", "experiment_id", "trial_id"] + + assert ephys.heading.names == [ + "animal", + "experiment_id", + "trial_id", + "sampling_frequency", + "duration", + ] + assert ephys.primary_key == ["animal", "experiment_id", "trial_id"] + + assert channel.heading.names == [ + "animal", + "experiment_id", + "trial_id", + "channel", + "voltage", + "current", + ] + assert channel.primary_key == ["animal", "experiment_id", "trial_id", "channel"] + assert channel.heading.attributes["voltage"].is_blob + + +def test_dependencies(schema_any): + user = User() + subject = Subject() + experiment = Experiment() + trial = Trial() + ephys = Ephys() + channel = Ephys.Channel() + + assert experiment.full_table_name in user.children(primary=False) + assert set(experiment.parents(primary=False)) == {user.full_table_name} + assert experiment.full_table_name in user.children(primary=False) + assert set(experiment.parents(primary=False)) == {user.full_table_name} + assert set( + s.full_table_name for s in experiment.parents(primary=False, as_objects=True) + ) == {user.full_table_name} + + assert experiment.full_table_name in subject.descendants() + assert experiment.full_table_name in { + s.full_table_name for s in subject.descendants(as_objects=True) + } + assert subject.full_table_name in experiment.ancestors() + assert subject.full_table_name in { + s.full_table_name for s in experiment.ancestors(as_objects=True) + } + + assert trial.full_table_name in experiment.descendants() + assert trial.full_table_name in { + s.full_table_name for s in experiment.descendants(as_objects=True) + } + assert experiment.full_table_name in trial.ancestors() + assert experiment.full_table_name in { + s.full_table_name for s in trial.ancestors(as_objects=True) + } + + assert set(trial.children(primary=True)) == { + ephys.full_table_name, + trial.Condition.full_table_name, + } + assert set(trial.parts()) == {trial.Condition.full_table_name} + assert set(s.full_table_name for s in trial.parts(as_objects=True)) == { + trial.Condition.full_table_name + } + assert set(ephys.parents(primary=True)) == {trial.full_table_name} + assert set( + s.full_table_name for s in ephys.parents(primary=True, as_objects=True) + ) == {trial.full_table_name} + assert set(ephys.children(primary=True)) == {channel.full_table_name} + assert set( + s.full_table_name for s in ephys.children(primary=True, as_objects=True) + ) == {channel.full_table_name} + assert set(channel.parents(primary=True)) == {ephys.full_table_name} + assert set( + s.full_table_name for s in channel.parents(primary=True, as_objects=True) + ) == {ephys.full_table_name} + + +def test_descendants_only_contain_part_table(schema_any): + """issue #927""" + + class A(dj.Manual): + definition = """ + a: int + """ + + class B(dj.Manual): + definition = """ + -> A + b: int + """ + + class Master(dj.Manual): + definition = """ + table_master: int + """ + + class Part(dj.Part): + definition = """ + -> master + -> B + """ + + context = dict(A=A, B=B, Master=Master) + schema_any(A, context=context) + schema_any(B, context=context) + schema_any(Master, context=context) + assert A.descendants() == [ + "`djtest_test1`.`a`", + "`djtest_test1`.`b`", + "`djtest_test1`.`master__part`", + ] + + +def test_bad_attribute_name(schema_any): + class BadName(dj.Manual): + definition = """ + Bad_name : int + """ + + with pytest.raises(dj.DataJointError): + schema_any(BadName) + + +def test_bad_fk_rename(schema_any): + """issue #381""" + + class A(dj.Manual): + definition = """ + a : int + """ + + class B(dj.Manual): + definition = """ + b -> A # invalid, the new syntax is (b) -> A + """ + + schema_any(A) + with pytest.raises(dj.DataJointError): + schema_any(B) + + +def test_primary_nullable_foreign_key(schema_any): + class Q(dj.Manual): + definition = """ + -> [nullable] Experiment + """ + + with pytest.raises(dj.DataJointError): + schema_any(Q) + + +def test_invalid_foreign_key_option(schema_any): + class R(dj.Manual): + definition = """ + -> Experiment + ---- + -> [optional] User + """ + + with pytest.raises(dj.DataJointError): + schema_any(R) + + +def test_unsupported_datatype(schema_any): + class Q(dj.Manual): + definition = """ + experiment : int + --- + description : text + """ + + with pytest.raises(dj.DataJointError): + schema_any(Q) + + +def test_int_datatype(schema_any): + @schema_any + class Owner(dj.Manual): + definition = """ + ownerid : int + --- + car_count : integer + """ + + +def test_unsupported_int_datatype(schema_any): + class Driver(dj.Manual): + definition = """ + driverid : tinyint + --- + car_count : tinyinteger + """ + + with pytest.raises(dj.DataJointError): + schema_any(Driver) + + +def test_long_table_name(schema_any): + """ + test issue #205 -- reject table names over 64 characters in length + """ + + class WhyWouldAnyoneCreateATableNameThisLong(dj.Manual): + definition = """ + master : int + """ + + class WithSuchALongPartNameThatItCrashesMySQL(dj.Part): + definition = """ + -> (master) + """ + + with pytest.raises(dj.DataJointError): + schema_any(WhyWouldAnyoneCreateATableNameThisLong) diff --git a/tests_old/test_dependencies.py b/tests/test_dependencies.py similarity index 62% rename from tests_old/test_dependencies.py rename to tests/test_dependencies.py index c359b602a..312e5f8ad 100644 --- a/tests_old/test_dependencies.py +++ b/tests/test_dependencies.py @@ -1,60 +1,56 @@ -from nose.tools import assert_true, raises, assert_list_equal -from .schema import * +import datajoint as dj +from datajoint import errors +from pytest import raises from datajoint.dependencies import unite_master_parts +from .schema import * def test_unite_master_parts(): - assert_list_equal( - unite_master_parts( - [ - "`s`.`a`", - "`s`.`a__q`", - "`s`.`b`", - "`s`.`c`", - "`s`.`c__q`", - "`s`.`b__q`", - "`s`.`d`", - "`s`.`a__r`", - ] - ), + assert unite_master_parts( [ "`s`.`a`", "`s`.`a__q`", - "`s`.`a__r`", "`s`.`b`", - "`s`.`b__q`", "`s`.`c`", "`s`.`c__q`", + "`s`.`b__q`", "`s`.`d`", - ], - ) - assert_list_equal( - unite_master_parts( - [ - "`lab`.`#equipment`", - "`cells`.`cell_analysis_method`", - "`cells`.`cell_analysis_method_task_type`", - "`cells`.`cell_analysis_method_users`", - "`cells`.`favorite_selection`", - "`cells`.`cell_analysis_method__cell_selection_params`", - "`lab`.`#equipment__config`", - "`cells`.`cell_analysis_method__field_detect_params`", - ] - ), + "`s`.`a__r`", + ] + ) == [ + "`s`.`a`", + "`s`.`a__q`", + "`s`.`a__r`", + "`s`.`b`", + "`s`.`b__q`", + "`s`.`c`", + "`s`.`c__q`", + "`s`.`d`", + ] + assert unite_master_parts( [ "`lab`.`#equipment`", - "`lab`.`#equipment__config`", "`cells`.`cell_analysis_method`", - "`cells`.`cell_analysis_method__cell_selection_params`", - "`cells`.`cell_analysis_method__field_detect_params`", "`cells`.`cell_analysis_method_task_type`", "`cells`.`cell_analysis_method_users`", "`cells`.`favorite_selection`", - ], - ) - - -def test_nullable_dependency(): + "`cells`.`cell_analysis_method__cell_selection_params`", + "`lab`.`#equipment__config`", + "`cells`.`cell_analysis_method__field_detect_params`", + ] + ) == [ + "`lab`.`#equipment`", + "`lab`.`#equipment__config`", + "`cells`.`cell_analysis_method`", + "`cells`.`cell_analysis_method__cell_selection_params`", + "`cells`.`cell_analysis_method__field_detect_params`", + "`cells`.`cell_analysis_method_task_type`", + "`cells`.`cell_analysis_method_users`", + "`cells`.`favorite_selection`", + ] + + +def test_nullable_dependency(schema_any): """test nullable unique foreign key""" # Thing C has a nullable dependency on B whose primary key is composite a = ThingA() @@ -80,11 +76,10 @@ def test_nullable_dependency(): c.insert1(dict(a=3, b1=1, b2=1)) c.insert1(dict(a=4, b1=1, b2=2)) - assert_true(len(c) == len(c.fetch()) == 5) + assert len(c) == len(c.fetch()) == 5 -@raises(dj.errors.DuplicateError) -def test_unique_dependency(): +def test_unique_dependency(schema_any): """test nullable unique foreign key""" # Thing C has a nullable dependency on B whose primary key is composite @@ -104,4 +99,5 @@ def test_unique_dependency(): c.insert1(dict(a=0, b1=1, b2=1)) # duplicate foreign key attributes = not ok - c.insert1(dict(a=1, b1=1, b2=1)) + with raises(errors.DuplicateError): + c.insert1(dict(a=1, b1=1, b2=1)) diff --git a/tests/test_erd.py b/tests/test_erd.py new file mode 100644 index 000000000..8a2d1d3ac --- /dev/null +++ b/tests/test_erd.py @@ -0,0 +1,64 @@ +import datajoint as dj +from .schema_simple import LOCALS_SIMPLE, A, B, D, E, L, OutfitLaunch +from .schema_advanced import * + + +def test_decorator(schema_simp): + assert issubclass(A, dj.Lookup) + assert not issubclass(A, dj.Part) + assert B.database == schema_simp.database + assert issubclass(B.C, dj.Part) + assert B.C.database == schema_simp.database + assert B.C.master is B and E.F.master is E + + +def test_dependencies(schema_simp): + deps = schema_simp.connection.dependencies + deps.load() + assert all(cls.full_table_name in deps for cls in (A, B, B.C, D, E, E.F, L)) + assert set(A().children()) == set([B.full_table_name, D.full_table_name]) + assert set(D().parents(primary=True)) == set([A.full_table_name]) + assert set(D().parents(primary=False)) == set([L.full_table_name]) + assert set(deps.descendants(L.full_table_name)).issubset( + cls.full_table_name for cls in (L, D, E, E.F) + ) + + +def test_erd(schema_simp): + assert dj.diagram.diagram_active, "Failed to import networkx and pydot" + erd = dj.ERD(schema_simp, context=LOCALS_SIMPLE) + graph = erd._make_graph() + assert set(cls.__name__ for cls in (A, B, D, E, L)).issubset(graph.nodes()) + + +def test_erd_algebra(schema_simp): + erd0 = dj.ERD(B) + erd1 = erd0 + 3 + erd2 = dj.Di(E) - 3 + erd3 = erd1 * erd2 + erd4 = (erd0 + E).add_parts() - B - E + assert erd0.nodes_to_show == set(cls.full_table_name for cls in [B]) + assert erd1.nodes_to_show == set(cls.full_table_name for cls in (B, B.C, E, E.F)) + assert erd2.nodes_to_show == set(cls.full_table_name for cls in (A, B, D, E, L)) + assert erd3.nodes_to_show == set(cls.full_table_name for cls in (B, E)) + assert erd4.nodes_to_show == set(cls.full_table_name for cls in (B.C, E.F)) + + +def test_repr_svg(schema_adv): + erd = dj.ERD(schema_adv, context=dict()) + svg = erd._repr_svg_() + assert svg.startswith("") + + +def test_make_image(schema_simp): + erd = dj.ERD(schema_simp, context=dict()) + img = erd.make_image() + assert img.ndim == 3 and img.shape[2] in (3, 4) + + +def test_part_table_parsing(schema_simp): + # https://github.com/datajoint/datajoint-python/issues/882 + erd = dj.Di(schema_simp, context=LOCALS_SIMPLE) + graph = erd._make_graph() + assert "OutfitLaunch" in graph.nodes() + assert "OutfitLaunch.OutfitPiece" in graph.nodes() diff --git a/tests/test_external.py b/tests/test_external.py new file mode 100644 index 000000000..1e212b7d9 --- /dev/null +++ b/tests/test_external.py @@ -0,0 +1,131 @@ +import numpy as np +from numpy.testing import assert_array_equal +from datajoint.external import ExternalTable +from datajoint.blob import pack, unpack +import datajoint as dj +from .schema_external import SimpleRemote, Simple +import os + + +def test_external_put(schema_ext, mock_stores, mock_cache): + """ + external storage put and get and remove + """ + ext = ExternalTable( + schema_ext.connection, store="raw", database=schema_ext.database + ) + initial_length = len(ext) + input_ = np.random.randn(3, 7, 8) + count = 7 + extra = 3 + for i in range(count): + hash1 = ext.put(pack(input_)) + for i in range(extra): + hash2 = ext.put(pack(np.random.randn(4, 3, 2))) + + fetched_hashes = ext.fetch("hash") + assert all(hash in fetched_hashes for hash in (hash1, hash2)) + assert len(ext) == initial_length + 1 + extra + + output_ = unpack(ext.get(hash1)) + assert_array_equal(input_, output_) + + +class TestLeadingSlash: + def test_s3_leading_slash(self, schema_ext, mock_stores, mock_cache, minio_client): + """ + s3 external storage configured with leading slash + """ + self._leading_slash(schema_ext, index=100, store="share") + + def test_file_leading_slash( + self, schema_ext, mock_stores, mock_cache, minio_client + ): + """ + File external storage configured with leading slash + """ + self._leading_slash(schema_ext, index=200, store="local") + + def _leading_slash(self, schema_ext, index, store): + oldConfig = dj.config["stores"][store]["location"] + value = np.array([1, 2, 3]) + + id = index + dj.config["stores"][store]["location"] = "leading/slash/test" + SimpleRemote.insert([{"simple": id, "item": value}]) + assert np.array_equal( + value, (SimpleRemote & "simple={}".format(id)).fetch1("item") + ) + + id = index + 1 + dj.config["stores"][store]["location"] = "/leading/slash/test" + SimpleRemote.insert([{"simple": id, "item": value}]) + assert np.array_equal( + value, (SimpleRemote & "simple={}".format(id)).fetch1("item") + ) + + id = index + 2 + dj.config["stores"][store]["location"] = "leading\\slash\\test" + SimpleRemote.insert([{"simple": id, "item": value}]) + assert np.array_equal( + value, (SimpleRemote & "simple={}".format(id)).fetch1("item") + ) + + id = index + 3 + dj.config["stores"][store]["location"] = "f:\\leading\\slash\\test" + SimpleRemote.insert([{"simple": id, "item": value}]) + assert np.array_equal( + value, (SimpleRemote & "simple={}".format(id)).fetch1("item") + ) + + id = index + 4 + dj.config["stores"][store]["location"] = "f:\\leading/slash\\test" + SimpleRemote.insert([{"simple": id, "item": value}]) + assert np.array_equal( + value, (SimpleRemote & "simple={}".format(id)).fetch1("item") + ) + + id = index + 5 + dj.config["stores"][store]["location"] = "/" + SimpleRemote.insert([{"simple": id, "item": value}]) + assert np.array_equal( + value, (SimpleRemote & "simple={}".format(id)).fetch1("item") + ) + + id = index + 6 + dj.config["stores"][store]["location"] = "C:\\" + SimpleRemote.insert([{"simple": id, "item": value}]) + assert np.array_equal( + value, (SimpleRemote & "simple={}".format(id)).fetch1("item") + ) + + id = index + 7 + dj.config["stores"][store]["location"] = "" + SimpleRemote.insert([{"simple": id, "item": value}]) + assert np.array_equal( + value, (SimpleRemote & "simple={}".format(id)).fetch1("item") + ) + + dj.config["stores"][store]["location"] = oldConfig + + +def test_remove_fail(schema_ext, mock_stores, mock_cache, minio_client): + """ + https://github.com/datajoint/datajoint-python/issues/953 + """ + assert dj.config["stores"]["local"]["location"] + + data = dict(simple=2, item=[1, 2, 3]) + Simple.insert1(data) + path1 = dj.config["stores"]["local"]["location"] + "/djtest_extern/4/c/" + currentMode = int(oct(os.stat(path1).st_mode), 8) + os.chmod(path1, 0o40555) + (Simple & "simple=2").delete() + listOfErrors = schema_ext.external["local"].delete(delete_external_files=True) + + assert ( + len(schema_ext.external["local"] & dict(hash=listOfErrors[0][0])) == 1 + ), "unexpected number of rows in external table" + # ---------------------CLEAN UP-------------------- + os.chmod(path1, currentMode) + listOfErrors = schema_ext.external["local"].delete(delete_external_files=True) diff --git a/tests/test_external_class.py b/tests/test_external_class.py new file mode 100644 index 000000000..15136a944 --- /dev/null +++ b/tests/test_external_class.py @@ -0,0 +1,50 @@ +from numpy.testing import assert_almost_equal +import datajoint as dj +from . import schema_external + + +def test_heading(schema_ext, mock_stores): + heading = schema_external.Simple().heading + assert "item" in heading + assert heading["item"].is_external + + +def test_insert_and_fetch(schema_ext, mock_stores, mock_cache): + original_list = [1, 3, 8] + schema_external.Simple().insert1(dict(simple=1, item=original_list)) + # test fetch + q = (schema_external.Simple() & {"simple": 1}).fetch("item")[0] + assert list(q) == original_list + # test fetch1 as a tuple + q = (schema_external.Simple() & {"simple": 1}).fetch1("item") + assert list(q) == original_list + # test fetch1 as a dict + q = (schema_external.Simple() & {"simple": 1}).fetch1() + assert list(q["item"]) == original_list + # test without cache + previous_cache = dj.config["cache"] + dj.config["cache"] = None + q = (schema_external.Simple() & {"simple": 1}).fetch1() + assert list(q["item"]) == original_list + # test with cache + dj.config["cache"] = previous_cache + q = (schema_external.Simple() & {"simple": 1}).fetch1() + assert list(q["item"]) == original_list + + +def test_populate(schema_ext, mock_stores): + image = schema_external.Image() + image.populate() + remaining, total = image.progress() + assert ( + total == len(schema_external.Dimension() * schema_external.Seed()) + and remaining == 0 + ) + for img, neg, dimensions in zip( + *(image * schema_external.Dimension()).fetch("img", "neg", "dimensions") + ): + assert list(img.shape) == list(dimensions) + assert_almost_equal(img, -neg) + image.delete() + for external_table in image.external.values(): + external_table.delete(display_progress=False, delete_external_files=True) diff --git a/tests/test_fetch.py b/tests/test_fetch.py new file mode 100644 index 000000000..4f45ae9e9 --- /dev/null +++ b/tests/test_fetch.py @@ -0,0 +1,393 @@ +import pytest +from typing import List +from operator import itemgetter +import itertools +import numpy as np +import decimal +import pandas +import warnings +from . import schema +import datajoint as dj +import os +import logging +import io + + +def test_getattribute(subject): + """Testing Fetch.__call__ with attributes""" + list1 = sorted(subject.proj().fetch(as_dict=True), key=itemgetter("subject_id")) + list2 = sorted(subject.fetch(dj.key), key=itemgetter("subject_id")) + for l1, l2 in zip(list1, list2): + assert l1 == l2, "Primary key is not returned correctly" + + tmp = subject.fetch(order_by="subject_id") + + subject_notes, key, real_id = 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 l1 == l2, "Primary key is not returned correctly" + + +def test_getattribute_for_fetch1(subject): + """Testing Fetch1.__call__ with attributes""" + assert (subject & "subject_id=10").fetch1("subject_id") == 10 + assert (subject & "subject_id=10").fetch1("subject_id", "species") == ( + 10, + "monkey", + ) + + +def test_order_by(lang, languages): + """Tests order_by sorting order""" + for ord_name, ord_lang in itertools.product(*2 * [["ASC", "DESC"]]): + cur = lang.fetch(order_by=("name " + ord_name, "language " + ord_lang)) + languages.sort(key=itemgetter(1), reverse=ord_lang == "DESC") + languages.sort(key=itemgetter(0), reverse=ord_name == "DESC") + for c, l in zip(cur, languages): + assert np.all( + cc == ll for cc, ll in zip(c, l) + ), "Sorting order is different" + + +def test_order_by_default(lang, languages): + """Tests order_by sorting order with defaults""" + cur = lang.fetch(order_by=("language", "name DESC")) + languages.sort(key=itemgetter(0), reverse=True) + languages.sort(key=itemgetter(1), reverse=False) + for c, l in zip(cur, languages): + assert np.all([cc == ll for cc, ll in zip(c, l)]), "Sorting order is different" + + +def test_limit(lang): + """Test the limit kwarg""" + limit = 4 + cur = lang.fetch(limit=limit) + assert len(cur) == limit, "Length is not correct" + + +def test_order_by_limit(lang, languages): + """Test the combination of order by and limit kwargs""" + cur = lang.fetch(limit=4, order_by=["language", "name DESC"]) + languages.sort(key=itemgetter(0), reverse=True) + languages.sort(key=itemgetter(1), reverse=False) + assert len(cur) == 4, "Length is not correct" + for c, l in list(zip(cur, languages))[:4]: + assert np.all([cc == ll for cc, ll in zip(c, l)]), "Sorting order is different" + + +def test_head_tail(schema_any): + query = schema.User * schema.Language + n = 5 + frame = query.head(n, format="frame") + assert isinstance(frame, pandas.DataFrame) + array = query.head(n, format="array") + assert array.size == n + assert len(frame) == n + assert query.primary_key == frame.index.names + + n = 4 + frame = query.tail(n, format="frame") + array = query.tail(n, format="array") + assert array.size == n + assert len(frame) == n + assert query.primary_key == frame.index.names + + +def test_limit_offset(lang, languages): + """Test the limit and offset kwargs together""" + cur = lang.fetch(offset=2, limit=4, order_by=["language", "name DESC"]) + languages.sort(key=itemgetter(0), reverse=True) + languages.sort(key=itemgetter(1), reverse=False) + assert len(cur) == 4, "Length is not correct" + for c, l in list(zip(cur, languages[2:6])): + assert np.all([cc == ll for cc, ll in zip(c, l)]), "Sorting order is different" + + +def test_iter(lang, languages): + """Test iterator""" + cur = lang.fetch(order_by=["language", "name DESC"]) + languages.sort(key=itemgetter(0), reverse=True) + languages.sort(key=itemgetter(1), reverse=False) + for (name, lang_val), (tname, tlang) in list(zip(cur, languages)): + assert name == tname and lang_val == tlang, "Values are not the same" + # now as dict + cur = lang.fetch(as_dict=True, order_by=("language", "name DESC")) + for row, (tname, tlang) in list(zip(cur, languages)): + assert ( + row["name"] == tname and row["language"] == tlang + ), "Values are not the same" + + +def test_keys(lang, languages): + """test key fetch""" + languages.sort(key=itemgetter(0), reverse=True) + languages.sort(key=itemgetter(1), reverse=False) + + lang = schema.Language() + cur = lang.fetch("name", "language", order_by=("language", "name DESC")) + cur2 = list(lang.fetch("KEY", order_by=["language", "name DESC"])) + + for c, c2 in zip(zip(*cur), cur2): + assert c == tuple(c2.values()), "Values are not the same" + + +def test_attributes_as_dict(subject): + """ + Issue #595 + """ + attrs = ("species", "date_of_birth") + result = subject.fetch(*attrs, as_dict=True) + assert bool(result) and len(result) == len(subject) + assert set(result[0]) == set(attrs) + + +def test_fetch1_step1(lang, languages): + assert ( + lang.contents + == languages + == [ + ("Fabian", "English"), + ("Edgar", "English"), + ("Dimitri", "English"), + ("Dimitri", "Ukrainian"), + ("Fabian", "German"), + ("Edgar", "Japanese"), + ] + ), "Unexpected contents in Language table" + key = {"name": "Edgar", "language": "Japanese"} + true = languages[-1] + dat = (lang & key).fetch1() + for k, (ke, c) in zip(true, dat.items()): + assert k == c == (lang & key).fetch1(ke), "Values are not the same" + + +def test_misspelled_attribute(schema_any): + with pytest.raises(dj.DataJointError): + f = (schema.Language & 'lang = "ENGLISH"').fetch() + + +def test_repr(subject): + """Test string representation of fetch, returning table preview""" + repr = subject.fetch.__repr__() + n = len(repr.strip().split("\n")) + limit = dj.config["display.limit"] + # 3 lines are used for headers (2) and summary statement (1) + assert n - 3 <= limit + + +def test_fetch_none(lang): + """Test preparing attributes for getitem""" + with pytest.raises(dj.DataJointError): + lang.fetch(None) + + +def test_asdict(lang): + """Test returns as dictionaries""" + d = lang.fetch(as_dict=True) + for dd in d: + assert isinstance(dd, dict) + + +def test_offset(lang, languages): + """Tests offset""" + cur = lang.fetch(limit=4, offset=1, order_by=["language", "name DESC"]) + + languages.sort(key=itemgetter(0), reverse=True) + languages.sort(key=itemgetter(1), reverse=False) + assert len(cur) == 4, "Length is not correct" + for c, l in list(zip(cur, languages[1:]))[:4]: + assert np.all([cc == ll for cc, ll in zip(c, l)]), "Sorting order is different" + + +def test_limit_warning(lang): + """Tests whether warning is raised if offset is used without limit.""" + logger = logging.getLogger("datajoint") + log_capture = io.StringIO() + stream_handler = logging.StreamHandler(log_capture) + log_format = logging.Formatter( + "[%(asctime)s][%(funcName)s][%(levelname)s]: %(message)s" + ) + stream_handler.setFormatter(log_format) + stream_handler.set_name("test_limit_warning") + logger.addHandler(stream_handler) + lang.fetch(offset=1) + + log_contents = log_capture.getvalue() + log_capture.close() + + for handler in logger.handlers: # Clean up handler + if handler.name == "test_limit_warning": + logger.removeHandler(handler) + assert "[WARNING]: Offset set, but no limit." in log_contents + + +def test_len(lang): + """Tests __len__""" + assert len(lang.fetch()) == len(lang), "__len__ is not behaving properly" + + +def test_fetch1_step2(lang): + """Tests whether fetch1 raises error""" + with pytest.raises(dj.DataJointError): + lang.fetch1() + + +def test_fetch1_step3(lang): + """Tests whether fetch1 raises error""" + with pytest.raises(dj.DataJointError): + lang.fetch1("name") + + +def test_decimal(schema_any): + """Tests that decimal fields are correctly fetched and used in restrictions, see issue #334""" + rel = schema.DecimalPrimaryKey() + assert len(rel.fetch()), "Table DecimalPrimaryKey contents are empty" + rel.insert1([decimal.Decimal("3.1415926")]) + keys = rel.fetch() + assert len(keys) > 0 + assert len(rel & keys[0]) == 1 + keys = rel.fetch(dj.key) + assert len(keys) >= 2 + assert len(rel & keys[1]) == 1 + + +def test_nullable_numbers(schema_any): + """test mixture of values and nulls in numeric attributes""" + table = schema.NullableNumbers() + table.insert( + ( + ( + k, + np.random.randn(), + np.random.randint(-1000, 1000), + np.random.randn(), + ) + for k in range(10) + ) + ) + table.insert1((100, None, None, None)) + f, d, i = table.fetch("fvalue", "dvalue", "ivalue") + assert None in i + assert any(np.isnan(d)) + assert any(np.isnan(f)) + + +def test_fetch_format(subject): + """test fetch_format='frame'""" + with dj.config(fetch_format="frame"): + # test if lists are both dicts + list1 = sorted(subject.proj().fetch(as_dict=True), key=itemgetter("subject_id")) + list2 = sorted(subject.fetch(dj.key), key=itemgetter("subject_id")) + for l1, l2 in zip(list1, list2): + assert l1 == l2, "Primary key is not returned correctly" + + # tests if pandas dataframe + tmp = subject.fetch(order_by="subject_id") + assert isinstance(tmp, pandas.DataFrame) + tmp = tmp.to_records() + + subject_notes, key, real_id = 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 l1 == l2, "Primary key is not returned correctly" + + +def test_key_fetch1(subject): + """test KEY fetch1 - issue #976""" + with dj.config(fetch_format="array"): + k1 = (subject & "subject_id=10").fetch1("KEY") + with dj.config(fetch_format="frame"): + k2 = (subject & "subject_id=10").fetch1("KEY") + assert k1 == k2 + + +def test_same_secondary_attribute(schema_any): + children = (schema.Child * schema.Parent().proj()).fetch()["name"] + assert len(children) == 1 + assert children[0] == "Dan" + + +def test_query_caching(schema_any): + # initialize cache directory + os.mkdir(os.path.expanduser("~/dj_query_cache")) + + with dj.config(query_cache=os.path.expanduser("~/dj_query_cache")): + conn = schema.TTest3.connection + # insert sample data and load cache + schema.TTest3.insert([dict(key=100 + i, value=200 + i) for i in range(2)]) + conn.set_query_cache(query_cache="main") + cached_res = schema.TTest3().fetch() + # attempt to insert while caching enabled + try: + schema.TTest3.insert([dict(key=200 + i, value=400 + i) for i in range(2)]) + assert False, "Insert allowed while query caching enabled" + except dj.DataJointError: + conn.set_query_cache() + # insert new data + schema.TTest3.insert([dict(key=600 + i, value=800 + i) for i in range(2)]) + # re-enable cache to access old results + conn.set_query_cache(query_cache="main") + previous_cache = schema.TTest3().fetch() + # verify properly cached and how to refresh results + assert all([c == p for c, p in zip(cached_res, previous_cache)]) + conn.set_query_cache() + uncached_res = schema.TTest3().fetch() + assert len(uncached_res) > len(cached_res) + # purge query cache + conn.purge_query_cache() + + # reset cache directory state (will fail if purge was unsuccessful) + os.rmdir(os.path.expanduser("~/dj_query_cache")) + + +def test_fetch_group_by(schema_any): + """ + https://github.com/datajoint/datajoint-python/issues/914 + """ + + assert schema.Parent().fetch("KEY", order_by="name") == [{"parent_id": 1}] + + +def test_dj_u_distinct(schema_any): + """ + Test developed to see if removing DISTINCT from the select statement + generation breaks the dj.U universal set implementation + """ + + # Contents to be inserted + contents = [(1, 2, 3), (2, 2, 3), (3, 3, 2), (4, 5, 5)] + schema.Stimulus.insert(contents) + + # Query the whole table + test_query = schema.Stimulus() + + # Use dj.U to create a list of unique contrast and brightness combinations + result = dj.U("contrast", "brightness") & test_query + expected_result = [ + {"contrast": 2, "brightness": 3}, + {"contrast": 3, "brightness": 2}, + {"contrast": 5, "brightness": 5}, + ] + + fetched_result = result.fetch(as_dict=True, order_by=("contrast", "brightness")) + schema.Stimulus.delete_quick() + assert fetched_result == expected_result + + +def test_backslash(schema_any): + """ + https://github.com/datajoint/datajoint-python/issues/999 + """ + expected = "She\\Hulk" + schema.Parent.insert([(2, expected)]) + q = schema.Parent & dict(name=expected) + assert q.fetch1("name") == expected + q.delete() diff --git a/tests/test_fetch_same.py b/tests/test_fetch_same.py new file mode 100644 index 000000000..32d041347 --- /dev/null +++ b/tests/test_fetch_same.py @@ -0,0 +1,68 @@ +import pytest +import numpy as np +import datajoint as dj + + +class ProjData(dj.Manual): + definition = """ + id : int + --- + resp : float + sim : float + big : longblob + blah : varchar(10) + """ + + +@pytest.fixture +def schema_fetch_same(connection_test, prefix): + schema = dj.Schema( + prefix + "_fetch_same", + context=dict(ProjData=ProjData), + connection=connection_test, + ) + schema(ProjData) + ProjData().insert( + [ + {"id": 0, "resp": 20.33, "sim": 45.324, "big": 3, "blah": "yes"}, + { + "id": 1, + "resp": 94.3, + "sim": 34.23, + "big": {"key1": np.random.randn(20, 10)}, + "blah": "si", + }, + { + "id": 2, + "resp": 1.90, + "sim": 10.23, + "big": np.random.randn(4, 2), + "blah": "sim", + }, + ] + ) + yield schema + schema.drop() + + +def test_object_conversion_one(schema_fetch_same): + new = ProjData().proj(sub="resp").fetch("sub") + assert new.dtype == np.float64 + + +def test_object_conversion_two(schema_fetch_same): + [sub, add] = ProjData().proj(sub="resp", add="sim").fetch("sub", "add") + assert sub.dtype == np.float64 + assert add.dtype == np.float64 + + +def test_object_conversion_all(schema_fetch_same): + new = ProjData().proj(sub="resp", add="sim").fetch() + assert new["sub"].dtype == np.float64 + assert new["add"].dtype == np.float64 + + +def test_object_no_convert(schema_fetch_same): + new = ProjData().fetch() + assert new["big"].dtype == "object" + assert new["blah"].dtype == "object" diff --git a/tests/test_filepath.py b/tests/test_filepath.py new file mode 100644 index 000000000..54478e476 --- /dev/null +++ b/tests/test_filepath.py @@ -0,0 +1,265 @@ +import pytest +import datajoint as dj +import os +from pathlib import Path +import random +from .schema_external import Filepath, FilepathS3 +import logging +import io + + +def test_path_match(schema_ext, enable_filepath_feature, minio_client, store="repo"): + """test file path matches and empty file""" + ext = schema_ext.external[store] + stage_path = dj.config["stores"][store]["stage"] + + # create a mock file + relpath = "path/to/films" + managed_file = Path(stage_path, relpath, "vid.mov") + managed_file.parent.mkdir(parents=True, exist_ok=True) + open(str(managed_file), "a").close() + + # put the file + uuid = ext.upload_filepath(str(managed_file)) + + # remove + managed_file.unlink() + assert not managed_file.exists() + + # check filepath + assert (ext & {"hash": uuid}).fetch1("filepath") == str( + managed_file.relative_to(stage_path).as_posix() + ) + + # # Download the file and check its contents. + restored_path, checksum = ext.download_filepath(uuid) + assert restored_path == str(managed_file) + assert checksum == dj.hash.uuid_from_file(str(managed_file)) + + # cleanup + ext.delete(delete_external_files=True) + + +@pytest.mark.parametrize("store", ("repo", "repo-s3")) +def test_filepath(enable_filepath_feature, schema_ext, store): + """test file management""" + ext = schema_ext.external[store] + stage_path = dj.config["stores"][store]["stage"] + filename = "picture.dat" + + # create a mock file + relpath = "one/two/three" + managed_file = Path(stage_path, relpath, filename) + managed_file.parent.mkdir(parents=True, exist_ok=True) + data = os.urandom(3000) + with managed_file.open("wb") as f: + f.write(data) + + # put the same file twice to ensure storing once + uuid1 = ext.upload_filepath(str(managed_file)) + # no duplication should arise if file is the same + uuid2 = ext.upload_filepath(str(managed_file)) + assert uuid1 == uuid2 + + # remove to ensure downloading + managed_file.unlink() + assert not managed_file.exists() + + # Download the file and check its contents. Repeat causes no download from remote + for _ in 1, 2: + restored_path, checksum = ext.download_filepath(uuid1) + assert restored_path == str(managed_file) + assert checksum == dj.hash.uuid_from_file(str(managed_file)) + + # verify same data + with managed_file.open("rb") as f: + synced_data = f.read() + assert data == synced_data + + # cleanup + ext.delete(delete_external_files=True) + assert not ext.exists(ext._make_external_filepath(str(Path(relpath, filename)))) + + +@pytest.mark.parametrize("store", ("repo", "repo-s3")) +def test_duplicate_upload(schema_ext, store): + ext = schema_ext.external[store] + stage_path = dj.config["stores"][store]["stage"] + relpath = "one/two/three" + managed_file = Path(stage_path, relpath, "plot.dat") + managed_file.parent.mkdir(parents=True, exist_ok=True) + with managed_file.open("wb") as f: + f.write(os.urandom(300)) + ext.upload_filepath(str(managed_file)) + ext.upload_filepath(str(managed_file)) # this is fine because the file is the same + + +@pytest.mark.parametrize("store", ("repo", "repo-s3")) +def test_duplicate_error(schema_ext, store): + """syncing duplicate non-matching file should fail""" + ext = schema_ext.external[store] + stage_path = dj.config["stores"][store]["stage"] + relpath = "one/two/three" + managed_file = Path(stage_path, relpath, "thesis.dat") + managed_file.parent.mkdir(parents=True, exist_ok=True) + with managed_file.open("wb") as f: + f.write(os.urandom(300)) + ext.upload_filepath(str(managed_file)) + with managed_file.open("wb") as f: + f.write(os.urandom(300)) + # this should raise exception because the file has changed + with pytest.raises(dj.DataJointError): + ext.upload_filepath(str(managed_file)) + + +class TestFilepath: + def _test_filepath_class( + self, table=Filepath(), store="repo", verify_checksum=True + ): + if not verify_checksum: + dj.config["filepath_checksum_size_limit"] = 0 + stage_path = dj.config["stores"][store]["stage"] + # create a mock file + relative_path = "one/two/three" + managed_file = Path(stage_path, relative_path, "attachment.dat") + managed_file.parent.mkdir(parents=True, exist_ok=True) + data = os.urandom(3000) + with managed_file.open("wb") as f: + f.write(data) + with managed_file.open("rb") as f: + contents = f.read() + assert data == contents + + # upload file into shared repo + table.insert1((1, str(managed_file))) + + # remove file locally + managed_file.unlink() + assert not managed_file.is_file() + + # fetch file from remote + filepath = (table & {"fnum": 1}).fetch1("img") + assert filepath == str(managed_file) + + # verify original contents + with managed_file.open("rb") as f: + contents = f.read() + assert data == contents + + # delete from table + table.delete() + assert table.external[store] + + # delete from external table + table.external[store].delete(delete_external_files=True) + dj.config["filepath_checksum_size_limit"] = None + + @pytest.mark.parametrize( + "table, store, n_repeats", + ( + (Filepath(), "repo", 2), + (FilepathS3(), "repo-s3", 2), + ), + ) + def test_filepath_class( + self, + schema_ext, + table, + store, + n_repeats, + minio_client, + enable_filepath_feature, + verify_checksum=True, + ): + for _ in range(n_repeats): + self._test_filepath_class(table, store, verify_checksum) + + def test_filepath_class_no_checksum(self, schema_ext, enable_filepath_feature): + logger = logging.getLogger("datajoint") + log_capture = io.StringIO() + stream_handler = logging.StreamHandler(log_capture) + log_format = logging.Formatter( + "[%(asctime)s][%(funcName)s][%(levelname)s]: %(message)s" + ) + stream_handler.setFormatter(log_format) + stream_handler.set_name("test_limit_warning") + logger.addHandler(stream_handler) + self._test_filepath_class(table=Filepath(), store="repo", verify_checksum=False) + log_contents = log_capture.getvalue() + log_capture.close() + for handler in logger.handlers: # Clean up handler + if handler.name == "test_limit_warning": + logger.removeHandler(handler) + assert "Skipped checksum for file with hash:" in log_contents + + +@pytest.mark.parametrize( + "table, store", + ( + (Filepath(), "repo"), + (FilepathS3(), "repo-s3"), + ), +) +def test_filepath_cleanup(table, store, schema_ext, enable_filepath_feature): + """test deletion of filepath entries from external table""" + stage_path = dj.config["stores"][store]["stage"] + n = 20 + contents = os.urandom(345) + for i in range(n): + relative_path = Path(*random.sample(("one", "two", "three", "four"), k=3)) + managed_file = Path(stage_path, relative_path, "file.dat") + managed_file.parent.mkdir(parents=True, exist_ok=True) + with managed_file.open("wb") as f: + f.write(contents) # same in all files + table.insert1((i, str(managed_file))) + assert len(table) == n + + ext = schema_ext.external[store] + + assert len(table) == n + assert 0 < len(ext) < n + + (table & "fnum in (1, 2, 3, 4, 5, 6)").delete() + m = n - len(table) # number deleted + assert m == 6 + + ext.delete(delete_external_files=True) # delete unused entries + assert 0 < len(ext) <= n - m + + +def test_delete_without_files( + schema_ext, + enable_filepath_feature, + store="repo", +): + """test deletion of filepath entries from external table without removing files""" + # do not delete unused entries + schema_ext.external[store].delete(delete_external_files=False) + + +def test_return_string( + schema_ext, enable_filepath_feature, table=Filepath(), store="repo" +): + """test returning string on fetch""" + stage_path = dj.config["stores"][store]["stage"] + # create a mock file + relative_path = "this/is/a/test" + managed_file = Path(stage_path, relative_path, "string.dat") + managed_file.parent.mkdir(parents=True, exist_ok=True) + data = os.urandom(3000) + with managed_file.open("wb") as f: + f.write(data) + with managed_file.open("rb") as f: + contents = f.read() + assert data == contents + + # upload file into shared repo + table.insert1((138, str(managed_file))) + + # remove file locally + managed_file.unlink() + assert not managed_file.is_file() + + # fetch file from remote + filepath = (table & {"fnum": 138}).fetch1("img") + assert isinstance(filepath, str) diff --git a/tests/test_foreign_keys.py b/tests/test_foreign_keys.py new file mode 100644 index 000000000..18daa952a --- /dev/null +++ b/tests/test_foreign_keys.py @@ -0,0 +1,47 @@ +from datajoint.declare import declare +from .schema_advanced import * + + +def test_aliased_fk(schema_adv): + person = Person() + parent = Parent() + person.delete() + assert not person + assert not parent + person.fill() + parent.fill() + assert person + assert parent + link = person.proj(parent_name="full_name", parent="person_id") + parents = person * parent * link + parents &= dict(full_name="May K. Hall") + assert set(parents.fetch("parent_name")) == {"Hanna R. Walters", "Russel S. James"} + delete_count = person.delete() + assert delete_count == 16 + + +def test_describe(schema_adv): + """real_definition should match original definition""" + for rel in (LocalSynapse, GlobalSynapse): + describe = rel.describe() + s1 = declare(rel.full_table_name, rel.definition, schema_adv.context)[0].split( + "\n" + ) + s2 = declare(rel.full_table_name, describe, globals())[0].split("\n") + for c1, c2 in zip(s1, s2): + assert c1 == c2 + + +def test_delete(schema_adv): + person = Person() + parent = Parent() + person.delete() + assert not person + assert not parent + person.fill() + parent.fill() + assert parent + original_len = len(parent) + to_delete = len(parent & "11 in (person_id, parent)") + (person & "person_id=11").delete() + assert to_delete and len(parent) == original_len - to_delete diff --git a/tests_old/test_groupby.py b/tests/test_groupby.py similarity index 93% rename from tests_old/test_groupby.py rename to tests/test_groupby.py index 3d3be530e..109972760 100644 --- a/tests_old/test_groupby.py +++ b/tests/test_groupby.py @@ -1,7 +1,7 @@ from .schema_simple import A, D -def test_aggr_with_proj(): +def test_aggr_with_proj(schema_simp): # issue #944 - only breaks with MariaDB # MariaDB implements the SQL:1992 standard that prohibits fields in the select statement that are # not also in the GROUP BY statement. diff --git a/tests/test_hash.py b/tests/test_hash.py new file mode 100644 index 000000000..a88c45316 --- /dev/null +++ b/tests/test_hash.py @@ -0,0 +1,6 @@ +from datajoint import hash + + +def test_hash(): + assert hash.uuid_from_buffer(b"abc").hex == "900150983cd24fb0d6963f7d28e17f72" + assert hash.uuid_from_buffer(b"").hex == "d41d8cd98f00b204e9800998ecf8427e" diff --git a/tests/test_jobs.py b/tests/test_jobs.py new file mode 100644 index 000000000..9d1d4636b --- /dev/null +++ b/tests/test_jobs.py @@ -0,0 +1,146 @@ +import pytest +from . import schema +from datajoint.jobs import ERROR_MESSAGE_LENGTH, TRUNCATION_APPENDIX +import random +import string +import datajoint as dj + + +def test_reserve_job(subject, schema_any): + assert subject + table_name = "fake_table" + + # reserve jobs + for key in subject.fetch("KEY"): + assert schema_any.jobs.reserve(table_name, key), "failed to reserve a job" + + # refuse jobs + for key in subject.fetch("KEY"): + assert not schema_any.jobs.reserve( + table_name, key + ), "failed to respect reservation" + + # complete jobs + for key in subject.fetch("KEY"): + schema_any.jobs.complete(table_name, key) + assert not schema_any.jobs, "failed to free jobs" + + # reserve jobs again + for key in subject.fetch("KEY"): + assert schema_any.jobs.reserve(table_name, key), "failed to reserve new jobs" + + # finish with error + for key in subject.fetch("KEY"): + schema_any.jobs.error(table_name, key, "error message") + + # refuse jobs with errors + for key in subject.fetch("KEY"): + assert not schema_any.jobs.reserve( + table_name, key + ), "failed to ignore error jobs" + + # clear error jobs + (schema_any.jobs & dict(status="error")).delete() + assert not schema_any.jobs, "failed to clear error jobs" + + +def test_restrictions(schema_any): + jobs = schema_any.jobs + jobs.delete() + jobs.reserve("a", {"key": "a1"}) + jobs.reserve("a", {"key": "a2"}) + jobs.reserve("b", {"key": "b1"}) + jobs.error("a", {"key": "a2"}, "error") + jobs.error("b", {"key": "b1"}, "error") + + assert len(jobs & {"table_name": "a"}) == 2 + assert len(jobs & {"status": "error"}) == 2 + assert len(jobs & {"table_name": "a", "status": "error"}) == 1 + jobs.delete() + + +def test_sigint(schema_any): + try: + schema.SigIntTable().populate(reserve_jobs=True) + except KeyboardInterrupt: + pass + + assert len(schema_any.jobs.fetch()), "SigInt jobs table is empty" + status, error_message = schema_any.jobs.fetch1("status", "error_message") + assert status == "error" + assert error_message == "KeyboardInterrupt" + + +def test_sigterm(schema_any): + try: + schema.SigTermTable().populate(reserve_jobs=True) + except SystemExit: + pass + + assert len(schema_any.jobs.fetch()), "SigTerm jobs table is empty" + status, error_message = schema_any.jobs.fetch1("status", "error_message") + assert status == "error" + assert error_message == "SystemExit: SIGTERM received" + + +def test_suppress_dj_errors(schema_any): + """test_suppress_dj_errors: dj errors suppressible w/o native py blobs""" + with dj.config(enable_python_native_blobs=False): + schema.ErrorClass.populate(reserve_jobs=True, suppress_errors=True) + assert len(schema.DjExceptionName()) == len(schema_any.jobs) > 0 + + +def test_long_error_message(subject, schema_any): + # create long error message + long_error_message = "".join( + random.choice(string.ascii_letters) for _ in range(ERROR_MESSAGE_LENGTH + 100) + ) + short_error_message = "".join( + random.choice(string.ascii_letters) for _ in range(ERROR_MESSAGE_LENGTH // 2) + ) + assert subject + table_name = "fake_table" + + key = subject.fetch("KEY", limit=1)[0] + + # test long error message + schema_any.jobs.reserve(table_name, key) + schema_any.jobs.error(table_name, key, long_error_message) + error_message = schema_any.jobs.fetch1("error_message") + assert ( + len(error_message) == ERROR_MESSAGE_LENGTH + ), "error message is longer than max allowed" + assert error_message.endswith( + TRUNCATION_APPENDIX + ), "appropriate ending missing for truncated error message" + schema_any.jobs.delete() + + # test long error message + schema_any.jobs.reserve(table_name, key) + schema_any.jobs.error(table_name, key, short_error_message) + error_message = schema_any.jobs.fetch1("error_message") + assert error_message == short_error_message, "error messages do not agree" + assert not error_message.endswith( + TRUNCATION_APPENDIX + ), "error message should not be truncated" + schema_any.jobs.delete() + + +def test_long_error_stack(subject, schema_any): + # create long error stack + STACK_SIZE = ( + 89942 # Does not fit into small blob (should be 64k, but found to be higher) + ) + long_error_stack = "".join( + random.choice(string.ascii_letters) for _ in range(STACK_SIZE) + ) + assert subject + table_name = "fake_table" + + key = subject.fetch("KEY", limit=1)[0] + + # test long error stack + schema_any.jobs.reserve(table_name, key) + schema_any.jobs.error(table_name, key, "error message", long_error_stack) + error_stack = schema_any.jobs.fetch1("error_stack") + assert error_stack == long_error_stack, "error stacks do not agree" diff --git a/tests/test_json.py b/tests/test_json.py new file mode 100644 index 000000000..803737165 --- /dev/null +++ b/tests/test_json.py @@ -0,0 +1,230 @@ +import pytest +import inspect +from datajoint.declare import declare +import datajoint as dj +import numpy as np +from packaging.version import Version + + +@pytest.fixture +def skip_for_mysql_lt_8(connection_test): + """ + Skip test if MySQL version is less than 8.0.0 + """ + mysql_version = connection_root.query("select @@version;").fetchone()[0] + if Version(mysql_version) < Version("8.0.0"): + pytest.skip("These tests require MySQL >= v8.0.0", allow_module_level=True) + + +@pytest.mark.usefixtures("skip_for_mysql_lt_8") +class Team(dj.Lookup): + definition = """ + name: varchar(40) + --- + car=null: json + unique index(car.name:char(20)) + uniQue inDex ( name, car.name:char(20), (json_value(`car`, _utf8mb4'$.length' returning decimal(4, 1))) ) + """ + contents = [ + ( + "engineering", + { + "name": "Rever", + "length": 20.5, + "inspected": True, + "tire_pressure": [32, 31, 33, 34], + "headlights": [ + { + "side": "left", + "hyper_white": None, + }, + { + "side": "right", + "hyper_white": None, + }, + ], + }, + ), + ( + "business", + { + "name": "Chaching", + "length": 100, + "safety_inspected": False, + "tire_pressure": [34, 30, 27, 32], + "headlights": [ + { + "side": "left", + "hyper_white": True, + }, + { + "side": "right", + "hyper_white": True, + }, + ], + }, + ), + ( + "marketing", + None, + ), + ] + + +@pytest.fixture +def schema_json(connection_test, prefix): + schema = dj.Schema( + prefix + "_json", context=dict(Team=Team), connection=connection_test + ) + schema(Team) + yield schema + schema.drop() + + +def test_insert_update(schema_json): + car = { + "name": "Discovery", + "length": 22.9, + "inspected": None, + "tire_pressure": [35, 36, 34, 37], + "headlights": [ + { + "side": "left", + "hyper_white": True, + }, + { + "side": "right", + "hyper_white": True, + }, + ], + } + + Team.insert1({"name": "research", "car": car}) + q = Team & {"name": "research"} + assert q.fetch1("car") == car + + car.update({"length": 23}) + Team.update1({"name": "research", "car": car}) + assert q.fetch1("car") == car + + try: + Team.insert1({"name": "hr", "car": car}) + raise Exception("Inserted non-unique car name.") + except dj.DataJointError: + pass + + q.delete_quick() + assert not q + + +def test_describe(schema_json): + rel = Team() + context = inspect.currentframe().f_globals + s1 = declare(rel.full_table_name, rel.definition, context) + s2 = declare(rel.full_table_name, rel.describe(), context) + assert s1 == s2 + + +def test_restrict(schema_json): + # dict + assert (Team & {"car.name": "Chaching"}).fetch1("name") == "business" + + assert (Team & {"car.length": 20.5}).fetch1("name") == "engineering" + + assert (Team & {"car.inspected": "true"}).fetch1("name") == "engineering" + + assert (Team & {"car.inspected:unsigned": True}).fetch1("name") == "engineering" + + assert (Team & {"car.safety_inspected": "false"}).fetch1("name") == "business" + + assert (Team & {"car.safety_inspected:unsigned": False}).fetch1( + "name" + ) == "business" + + assert (Team & {"car.headlights[0].hyper_white": None}).fetch( + "name", order_by="name", as_dict=True + ) == [ + {"name": "engineering"}, + {"name": "marketing"}, + ] # if entire record missing, JSON key is missing, or value set to JSON null + + assert (Team & {"car": None}).fetch1("name") == "marketing" + + assert (Team & {"car.tire_pressure": [34, 30, 27, 32]}).fetch1("name") == "business" + + assert ( + Team & {"car.headlights[1]": {"side": "right", "hyper_white": True}} + ).fetch1("name") == "business" + + # sql operators + assert (Team & "`car`->>'$.name' LIKE '%ching%'").fetch1( + "name" + ) == "business", "Missing substring" + + assert (Team & "`car`->>'$.length' > 30").fetch1("name") == "business", "<= 30" + + assert ( + Team & "JSON_VALUE(`car`, '$.safety_inspected' RETURNING UNSIGNED) = 0" + ).fetch1("name") == "business", "Has `safety_inspected` set to `true`" + + assert (Team & "`car`->>'$.headlights[0].hyper_white' = 'null'").fetch1( + "name" + ) == "engineering", "Has 1st `headlight` with `hyper_white` not set to `null`" + + assert (Team & "`car`->>'$.inspected' IS NOT NULL").fetch1( + "name" + ) == "engineering", "Missing `inspected` key" + + assert (Team & "`car`->>'$.tire_pressure' = '[34, 30, 27, 32]'").fetch1( + "name" + ) == "business", "`tire_pressure` array did not match" + + assert ( + Team + & """`car`->>'$.headlights[1]' = '{"side": "right", "hyper_white": true}'""" + ).fetch1("name") == "business", "2nd `headlight` object did not match" + + +def test_proj(schema_json): + # proj necessary since we need to rename indexed value into a proper attribute name + assert Team.proj(car_length="car.length").fetch( + as_dict=True, order_by="car_length" + ) == [ + {"name": "marketing", "car_length": None}, + {"name": "business", "car_length": "100"}, + {"name": "engineering", "car_length": "20.5"}, + ] + + assert Team.proj(car_length="car.length:decimal(4, 1)").fetch( + as_dict=True, order_by="car_length" + ) == [ + {"name": "marketing", "car_length": None}, + {"name": "engineering", "car_length": 20.5}, + {"name": "business", "car_length": 100.0}, + ] + + assert Team.proj( + car_width="JSON_VALUE(`car`, '$.length' RETURNING float) - 15" + ).fetch(as_dict=True, order_by="car_width") == [ + {"name": "marketing", "car_width": None}, + {"name": "engineering", "car_width": 5.5}, + {"name": "business", "car_width": 85.0}, + ] + + assert ( + (Team & {"name": "engineering"}).proj(car_tire_pressure="car.tire_pressure") + ).fetch1("car_tire_pressure") == "[32, 31, 33, 34]" + + assert np.array_equal( + Team.proj(car_inspected="car.inspected").fetch( + "car_inspected", order_by="name" + ), + np.array([None, "true", None]), + ) + + assert np.array_equal( + Team.proj(car_inspected="car.inspected:unsigned").fetch( + "car_inspected", order_by="name" + ), + np.array([None, 1, None]), + ) diff --git a/tests/test_log.py b/tests/test_log.py new file mode 100644 index 000000000..4b6e64613 --- /dev/null +++ b/tests/test_log.py @@ -0,0 +1,5 @@ +def test_log(schema_any): + ts, events = (schema_any.log & 'event like "Declared%%"').fetch( + "timestamp", "event" + ) + assert len(ts) >= 2 diff --git a/tests/test_nan.py b/tests/test_nan.py new file mode 100644 index 000000000..68a28079c --- /dev/null +++ b/tests/test_nan.py @@ -0,0 +1,51 @@ +import numpy as np +import datajoint as dj +import pytest + + +class NanTest(dj.Manual): + definition = """ + id :int + --- + value=null :double + """ + + +@pytest.fixture +def schema_nan(connection_test, prefix): + schema = dj.Schema( + prefix + "_nantest", context=dict(NanTest=NanTest), connection=connection_test + ) + schema(NanTest) + yield schema + schema.drop() + + +@pytest.fixture +def arr_a(): + return np.array([0, 1 / 3, np.nan, np.pi, np.nan]) + + +@pytest.fixture +def schema_nan_pop(schema_nan, arr_a): + rel = NanTest() + with dj.config(safemode=False): + rel.delete() + rel.insert(((i, value) for i, value in enumerate(arr_a))) + return schema_nan + + +def test_insert_nan(schema_nan_pop, arr_a): + """Test fetching of null values""" + b = NanTest().fetch("value", order_by="id") + assert (np.isnan(arr_a) == np.isnan(b)).all(), "incorrect handling of Nans" + assert np.allclose( + arr_a[np.logical_not(np.isnan(arr_a))], b[np.logical_not(np.isnan(b))] + ), "incorrect storage of floats" + + +def test_nulls_do_not_affect_primary_keys(schema_nan_pop, arr_a): + """Test against a case that previously caused a bug when skipping existing entries.""" + NanTest().insert( + ((i, value) for i, value in enumerate(arr_a)), skip_duplicates=True + ) diff --git a/tests_old/test_plugin.py b/tests/test_plugin.py similarity index 99% rename from tests_old/test_plugin.py rename to tests/test_plugin.py index f70f4c2ef..95933d2ff 100644 --- a/tests_old/test_plugin.py +++ b/tests/test_plugin.py @@ -1,3 +1,4 @@ +import pytest import datajoint.errors as djerr import datajoint.plugin as p import pkg_resources diff --git a/tests/test_privileges.py b/tests/test_privileges.py new file mode 100644 index 000000000..57880081c --- /dev/null +++ b/tests/test_privileges.py @@ -0,0 +1,117 @@ +import os +import pytest +import datajoint as dj +from . import schema, schema_privileges + +namespace = locals() + + +@pytest.fixture +def schema_priv(connection_test): + schema = dj.Schema( + context=schema_privileges.LOCALS_PRIV, + connection=connection_test, + ) + schema(schema_privileges.Parent) + schema(schema_privileges.Child) + schema(schema_privileges.NoAccess) + schema(schema_privileges.NoAccessAgain) + yield schema + if schema.is_activated(): + schema.drop() + + +@pytest.fixture +def connection_djsubset(connection_root, db_creds_root, schema_priv, prefix): + user = "djsubset" + conn = dj.conn(**db_creds_root, reset=True) + schema_priv.activate(f"{prefix}_schema_privileges") + conn.query( + f""" + CREATE USER IF NOT EXISTS '{user}'@'%%' + IDENTIFIED BY '{user}' + """ + ) + conn.query( + f""" + GRANT SELECT, INSERT, UPDATE, DELETE + ON `{prefix}_schema_privileges`.`#parent` + TO '{user}'@'%%' + """ + ) + conn.query( + f""" + GRANT SELECT, INSERT, UPDATE, DELETE + ON `{prefix}_schema_privileges`.`__child` + TO '{user}'@'%%' + """ + ) + conn_djsubset = dj.conn( + host=db_creds_root["host"], + user=user, + password=user, + reset=True, + ) + yield conn_djsubset + conn.query(f"DROP USER {user}") + conn.query(f"DROP DATABASE {prefix}_schema_privileges") + + +@pytest.fixture +def connection_djview(connection_root, db_creds_root): + """ + A connection with only SELECT privilege to djtest schemas. + Requires connection_root fixture so that `djview` user exists. + """ + connection = dj.conn( + host=db_creds_root["host"], + user="djview", + password="djview", + reset=True, + ) + yield connection + + +class TestUnprivileged: + def test_fail_create_schema(self, connection_djview): + """creating a schema with no CREATE privilege""" + with pytest.raises(dj.DataJointError): + return dj.Schema( + "forbidden_schema", namespace, connection=connection_djview + ) + + def test_insert_failure(self, connection_djview, schema_any): + unprivileged = dj.Schema( + schema_any.database, namespace, connection=connection_djview + ) + unprivileged.spawn_missing_classes() + assert issubclass(Language, dj.Lookup) and len(Language()) == len( + schema.Language() + ), "failed to spawn missing classes" + with pytest.raises(dj.DataJointError): + Language().insert1(("Socrates", "Greek")) + + def test_failure_to_create_table(self, connection_djview, schema_any): + unprivileged = dj.Schema( + schema_any.database, namespace, connection=connection_djview + ) + + @unprivileged + class Try(dj.Manual): + definition = """ # should not matter really + id : int + --- + value : float + """ + + with pytest.raises(dj.DataJointError): + Try().insert1((1, 1.5)) + + +class TestSubset: + def test_populate_activate(self, connection_djsubset, schema_priv, prefix): + schema_priv.activate( + f"{prefix}_schema_privileges", create_schema=True, create_tables=False + ) + schema_privileges.Child.populate() + assert schema_privileges.Child.progress(display=False)[0] == 0 diff --git a/tests/test_reconnection.py b/tests/test_reconnection.py new file mode 100644 index 000000000..5eea4af11 --- /dev/null +++ b/tests/test_reconnection.py @@ -0,0 +1,32 @@ +""" +Collection of test cases to test connection module. +""" + +import pytest +import datajoint as dj +from datajoint import DataJointError + + +@pytest.fixture +def conn(connection_root, db_creds_root): + return dj.conn(reset=True, **db_creds_root) + + +def test_close(conn): + assert conn.is_connected, "Connection should be alive" + conn.close() + assert not conn.is_connected, "Connection should now be closed" + + +def test_reconnect(conn): + assert conn.is_connected, "Connection should be alive" + conn.close() + conn.query("SHOW DATABASES;", reconnect=True).fetchall() + assert conn.is_connected, "Connection should be alive" + + +def test_reconnect_throws_error_in_transaction(conn): + assert conn.is_connected, "Connection should be alive" + with conn.transaction, pytest.raises(DataJointError): + conn.close() + conn.query("SHOW DATABASES;", reconnect=True).fetchall() diff --git a/tests/test_relation.py b/tests/test_relation.py new file mode 100644 index 000000000..169ffc29a --- /dev/null +++ b/tests/test_relation.py @@ -0,0 +1,293 @@ +import pytest +from inspect import getmembers +import re +import pandas +import numpy as np +import datajoint as dj +from datajoint.table import Table +from unittest.mock import patch +from . import schema + + +def test_contents(user, subject): + """ + test the ability of tables to self-populate using the contents property + """ + # test contents + assert user + assert len(user) == len(user.contents) + u = user.fetch(order_by=["username"]) + assert list(u["username"]) == sorted([s[0] for s in user.contents]) + + # test prepare + assert subject + assert len(subject) == len(subject.contents) + u = subject.fetch(order_by=["subject_id"]) + assert list(u["subject_id"]) == sorted([s[0] for s in subject.contents]) + + +def test_misnamed_attribute1(user): + with pytest.raises(dj.DataJointError): + user.insert([dict(username="Bob"), dict(user="Alice")]) + + +def test_misnamed_attribute2(user): + with pytest.raises(KeyError): + user.insert1(dict(user="Bob")) + + +def test_extra_attribute1(user): + with pytest.raises(KeyError): + user.insert1(dict(username="Robert", spouse="Alice")) + + +def test_extra_attribute2(user): + user.insert1(dict(username="Robert", spouse="Alice"), ignore_extra_fields=True) + + +def test_missing_definition(schema_any): + class MissingDefinition(dj.Manual): + definitions = """ # misspelled definition + id : int + --- + comment : varchar(16) # otherwise everything's normal + """ + + with pytest.raises(NotImplementedError): + schema_any(MissingDefinition, context=dict(MissingDefinition=MissingDefinition)) + + +def test_empty_insert1(user): + with pytest.raises(dj.DataJointError): + user.insert1(()) + + +def test_empty_insert(user): + with pytest.raises(dj.DataJointError): + user.insert([()]) + + +def test_wrong_arguments_insert(user): + with pytest.raises(dj.DataJointError): + user.insert1(("First", "Second")) + + +def test_wrong_insert_type(user): + with pytest.raises(dj.DataJointError): + user.insert1(3) + + +def test_insert_select(subject, test, test2): + test2.delete() + test2.insert(test) + assert len(test2) == len(test) + + original_length = len(subject) + elements = subject.proj(..., s="subject_id") + elements = elements.proj( + "real_id", + "date_of_birth", + "subject_notes", + subject_id="s+1000", + species='"human"', + ) + subject.insert(elements, ignore_extra_fields=True) + assert len(subject) == 2 * original_length + + +def test_insert_pandas_roundtrip(test, test2): + """ensure fetched frames can be inserted""" + test2.delete() + n = len(test) + assert n > 0 + df = test.fetch(format="frame") + assert isinstance(df, pandas.DataFrame) + assert len(df) == n + test2.insert(df) + assert len(test2) == n + + +def test_insert_pandas_userframe(test, test2): + """ + ensure simple user-created frames (1 field, non-custom index) + can be inserted without extra index adjustment + """ + test2.delete() + n = len(test) + assert n > 0 + df = pandas.DataFrame(test.fetch()) + assert isinstance(df, pandas.DataFrame) + assert len(df) == n + test2.insert(df) + assert len(test2) == n + + +def test_insert_select_ignore_extra_fields0(test, test_extra): + """need ignore extra fields for insert select""" + test_extra.insert1((test.fetch("key").max() + 1, 0, 0)) + with pytest.raises(dj.DataJointError): + test.insert(test_extra) + + +def test_insert_select_ignore_extra_fields1(test, test_extra): + """make sure extra fields works in insert select""" + test_extra.delete() + keyno = test.fetch("key").max() + 1 + test_extra.insert1((keyno, 0, 0)) + test.insert(test_extra, ignore_extra_fields=True) + assert keyno in test.fetch("key") + + +def test_insert_select_ignore_extra_fields2(test_no_extra, test): + """make sure insert select still works when ignoring extra fields when there are none""" + test_no_extra.delete() + test_no_extra.insert(test, ignore_extra_fields=True) + + +def test_insert_select_ignore_extra_fields3(test, test_no_extra, test_extra): + """make sure insert select works for from query result""" + # Recreate table state from previous tests + keyno = test.fetch("key").max() + 1 + test_extra.insert1((keyno, 0, 0)) + test.insert(test_extra, ignore_extra_fields=True) + + assert len(test_extra.fetch("key")), "test_extra is empty" + test_no_extra.delete() + assert len(test_extra.fetch("key")), "test_extra is empty" + keystr = str(test_extra.fetch("key").max()) + test_no_extra.insert((test_extra & "`key`=" + keystr), ignore_extra_fields=True) + + +def test_skip_duplicates(test_no_extra, test): + """test that skip_duplicates works when inserting from another table""" + test_no_extra.delete() + test_no_extra.insert(test, ignore_extra_fields=True, skip_duplicates=True) + test_no_extra.insert(test, ignore_extra_fields=True, skip_duplicates=True) + + +def test_replace(subject): + """ + Test replacing or ignoring duplicate entries + """ + key = dict(subject_id=7) + date = "2015-01-01" + subject.insert1(dict(key, real_id=7, date_of_birth=date, subject_notes="")) + assert date == str((subject & key).fetch1("date_of_birth")), "incorrect insert" + date = "2015-01-02" + subject.insert1( + dict(key, real_id=7, date_of_birth=date, subject_notes=""), + skip_duplicates=True, + ) + assert date != str((subject & key).fetch1("date_of_birth")), "inappropriate replace" + subject.insert1( + dict(key, real_id=7, date_of_birth=date, subject_notes=""), replace=True + ) + assert date == str((subject & key).fetch1("date_of_birth")), "replace failed" + + +def test_delete_quick(subject): + """Tests quick deletion""" + tmp = np.array( + [ + (2, "Klara", "monkey", "2010-01-01", ""), + (1, "Peter", "mouse", "2015-01-01", ""), + ], + dtype=subject.heading.as_dtype, + ) + subject.insert(tmp) + s = subject & ("subject_id in (%s)" % ",".join(str(r) for r in tmp["subject_id"])) + assert len(s) == 2, "insert did not work." + s.delete_quick() + assert len(s) == 0, "delete did not work." + + +def test_skip_duplicate(subject): + """Tests if duplicates are properly skipped.""" + tmp = np.array( + [ + (2, "Klara", "monkey", "2010-01-01", ""), + (1, "Peter", "mouse", "2015-01-01", ""), + ], + dtype=subject.heading.as_dtype, + ) + subject.insert(tmp) + tmp = np.array( + [ + (2, "Klara", "monkey", "2010-01-01", ""), + (1, "Peter", "mouse", "2015-01-01", ""), + ], + dtype=subject.heading.as_dtype, + ) + subject.insert(tmp, skip_duplicates=True) + + +def test_not_skip_duplicate(subject): + """Tests if duplicates are not skipped.""" + tmp = np.array( + [ + (2, "Klara", "monkey", "2010-01-01", ""), + (2, "Klara", "monkey", "2010-01-01", ""), + (1, "Peter", "mouse", "2015-01-01", ""), + ], + dtype=subject.heading.as_dtype, + ) + with pytest.raises(dj.errors.DuplicateError): + subject.insert(tmp, skip_duplicates=False) + + +def test_no_error_suppression(test): + """skip_duplicates=True should not suppress other errors""" + with pytest.raises(dj.errors.MissingAttributeError): + test.insert([dict(key=100)], skip_duplicates=True) + + +def test_blob_insert(img): + """Tests inserting and retrieving blobs.""" + X = np.random.randn(20, 10) + img.insert1((1, X)) + Y = img.fetch()[0]["img"] + assert np.all(X == Y), "Inserted and retrieved image are not identical" + + +def test_drop(trash): + """Tests dropping tables""" + dj.config["safemode"] = True + with patch.object(dj.utils, "input", create=True, return_value="yes"): + trash.drop() + try: + trash.fetch() + raise Exception("Fetched after table dropped.") + except dj.DataJointError: + pass + finally: + dj.config["safemode"] = False + + +def test_table_regexp(schema_any): + """Test whether table names are matched by regular expressions""" + + def relation_selector(attr): + try: + return issubclass(attr, Table) + except TypeError: + return False + + tiers = [dj.Imported, dj.Manual, dj.Lookup, dj.Computed] + for name, rel in getmembers(schema, relation_selector): + assert re.match( + rel.tier_regexp, rel.table_name + ), "Regular expression does not match for {name}".format(name=name) + for tier in tiers: + assert issubclass(rel, tier) or not re.match( + tier.tier_regexp, rel.table_name + ), "Regular expression matches for {name} but should not".format(name=name) + + +def test_table_size(experiment): + """test getting the size of the table and its indices in bytes""" + number_of_bytes = experiment.size_on_disk + assert isinstance(number_of_bytes, int) and number_of_bytes > 100 + + +def test_repr_html(ephys): + assert ephys._repr_html_().strip().startswith("3").primary_key) + assert list((dj.U("start_time") & trial).primary_key) == ["start_time"] + + +def test_invalid_restriction(schema_any): + with raises(dj.DataJointError): + result = dj.U("color") & dict(color="red") + + +def test_ineffective_restriction(lang): + rel = lang & dj.U("language") + assert rel.make_sql() == lang.make_sql() + + +def test_join(experiment): + rel = experiment * dj.U("experiment_date") + assert experiment.primary_key == ["subject_id", "experiment_id"] + assert rel.primary_key == experiment.primary_key + ["experiment_date"] + + rel = dj.U("experiment_date") * experiment + assert experiment.primary_key == ["subject_id", "experiment_id"] + assert rel.primary_key == experiment.primary_key + ["experiment_date"] + + +def test_invalid_join(schema_any): + with raises(dj.DataJointError): + rel = dj.U("language") * dict(language="English") + + +def test_repr_without_attrs(schema_any): + """test dj.U() display""" + query = dj.U().aggr(Language, n="count(*)") + repr(query) + + +def test_aggregations(schema_any): + lang = Language() + # test total aggregation on expression object + n1 = dj.U().aggr(lang, n="count(*)").fetch1("n") + assert n1 == len(lang.fetch()) + # test total aggregation on expression class + n2 = dj.U().aggr(Language, n="count(*)").fetch1("n") + assert n1 == n2 + rel = dj.U("language").aggr(Language, number_of_speakers="count(*)") + assert len(rel) == len(set(l[1] for l in Language.contents)) + assert (rel & 'language="English"').fetch1("number_of_speakers") == 3 + + +def test_argmax(schema_any): + rel = TTest() + # get the tuples corresponding to the maximum value + mx = (rel * dj.U().aggr(rel, mx="max(value)")) & "mx=value" + assert mx.fetch("value")[0] == max(rel.fetch("value")) + + +def test_aggr(schema_any, schema_simp): + rel = ArgmaxTest() + amax1 = (dj.U("val") * rel) & dj.U("secondary_key").aggr(rel, val="min(val)") + amax2 = (dj.U("val") * rel) * dj.U("secondary_key").aggr(rel, val="min(val)") + assert ( + len(amax1) == len(amax2) == rel.n + ), "Aggregated argmax with join and restriction does not yield the same length." diff --git a/tests/test_relational_operand.py b/tests/test_relational_operand.py new file mode 100644 index 000000000..65c6a5d74 --- /dev/null +++ b/tests/test_relational_operand.py @@ -0,0 +1,572 @@ +import pytest +import random +import string +import pandas +import datetime +import numpy as np +import datajoint as dj +from .schema_simple import * +from .schema import * + + +@pytest.fixture +def schema_simp_pop(schema_simp): + """ + Schema simple with data populated. + """ + og_a_contents = A.contents.copy() + og_l_contents = L.contents.copy() + B.populate() + D.populate() + E.populate() + yield schema_simp + A.contents = og_a_contents + L.contents = og_l_contents + + +@pytest.fixture +def schema_any_pop(schema_any): + """ + Schema any with data populated. + """ + Experiment.populate() + yield schema_any + + +def test_populate(schema_simp_pop): + assert not B().progress(display=False)[0], "B incompletely populated" + assert not D().progress(display=False)[0], "D incompletely populated" + assert not E().progress(display=False)[0], "E incompletely populated" + + assert len(B()) == 40, "B populated incorrectly" + assert len(B.C()) > 0, "C populated incorrectly" + assert len(D()) == 40, "D populated incorrectly" + assert len(E()) == len(B()) * len(D()) / len(A()), "E populated incorrectly" + assert len(E.F()) > 0, "F populated incorrectly" + + +def test_free_relation(schema_simp_pop): + b = B() + free = dj.FreeTable(b.connection, b.full_table_name) + assert repr(free).startswith("FreeTable") and b.full_table_name in repr(free) + r = "n>5" + assert (B() & r).make_sql() == (free & r).make_sql() + + +def test_rename(schema_simp_pop): + # test renaming + x = B().proj(i="id_a") & "i in (1,2,3,4)" + lenx = len(x) + assert len(x) == len( + B() & "id_a in (1,2,3,4)" + ), "incorrect restriction of renamed attributes" + assert len(x & "id_b in (1,2)") == len( + B() & "id_b in (1,2) and id_a in (1,2,3,4)" + ), "incorrect restriction of renamed restriction" + assert len(x) == lenx, "restriction modified original" + y = x.proj(j="i") + assert len(y) == len( + B() & "id_a in (1,2,3,4)" + ), "incorrect projection of restriction" + z = y & "j in (3, 4, 5, 6)" + assert len(z) == len(B() & "id_a in (3,4)"), "incorrect nested subqueries" + + +def test_rename_order(schema_simp_pop): + """ + Renaming projection should not change the order of the primary key attributes. + See issues #483 and #516. + """ + pk1 = D.primary_key + pk2 = D.proj(a="id_a").primary_key + assert ["a" if i == "id_a" else i for i in pk1] == pk2 + + +def test_join(schema_simp_pop): + # Test cartesian product + x = A() + y = L() + rel = x * y + assert len(rel) == len(x) * len(y), "incorrect join" + assert set(x.heading.names).union(y.heading.names) == set( + rel.heading.names + ), "incorrect join heading" + assert set(x.primary_key).union(y.primary_key) == set( + rel.primary_key + ), "incorrect join primary_key" + + # Test cartesian product of restricted relations + x = A() & "cond_in_a=1" + y = L() & "cond_in_l=1" + rel = x * y + assert len(rel) == len(x) * len(y), "incorrect join" + assert set(x.heading.names).union(y.heading.names) == set( + rel.heading.names + ), "incorrect join heading" + assert set(x.primary_key).union(y.primary_key) == set( + rel.primary_key + ), "incorrect join primary_key" + + # Test join with common attributes + cond = A() & "cond_in_a=1" + x = B() & cond + y = D() + rel = x * y + assert len(rel) >= len(x) and len(rel) >= len(y), "incorrect join" + assert not rel - cond, "incorrect join, restriction, or antijoin" + assert set(x.heading.names).union(y.heading.names) == set( + rel.heading.names + ), "incorrect join heading" + assert set(x.primary_key).union(y.primary_key) == set( + rel.primary_key + ), "incorrect join primary_key" + + # test renamed join + x = B().proj( + i="id_a" + ) # rename the common attribute to achieve full cartesian product + y = D() + rel = x * y + assert len(rel) == len(x) * len(y), "incorrect join" + assert set(x.heading.names).union(y.heading.names) == set( + rel.heading.names + ), "incorrect join heading" + assert set(x.primary_key).union(y.primary_key) == set( + rel.primary_key + ), "incorrect join primary_key" + x = B().proj(a="id_a") + y = D() + rel = x * y + assert len(rel) == len(x) * len(y), "incorrect join" + assert set(x.heading.names).union(y.heading.names) == set( + rel.heading.names + ), "incorrect join heading" + assert set(x.primary_key).union(y.primary_key) == set( + rel.primary_key + ), "incorrect join primary_key" + + # test pairing + # Approach 1: join then restrict + x = A.proj(a1="id_a", c1="cond_in_a") + y = A.proj(a2="id_a", c2="cond_in_a") + rel = x * y & "c1=0" & "c2=1" + lenx = len(x & "c1=0") + leny = len(y & "c2=1") + assert lenx + leny == len(A()), "incorrect restriction" + assert len(rel) == len(x & "c1=0") * len(y & "c2=1"), "incorrect pairing" + # Approach 2: restrict then join + x = (A & "cond_in_a=0").proj(a1="id_a") + y = (A & "cond_in_a=1").proj(a2="id_a") + assert len(rel) == len(x * y) + + +def test_issue_376(schema_any_pop): + tab = TTest3() + tab.delete_quick() + tab.insert(((1, "%%%"), (2, "one%"), (3, "one"))) + assert len(tab & 'value="%%%"') == 1 + assert len(tab & {"value": "%%%"}) == 1 + assert len(tab & 'value like "o%"') == 2 + assert len(tab & 'value like "o%%"') == 2 + + +def test_issue_463(schema_simp_pop): + assert ((A & B) * B).fetch().size == len(A * B) + + +def test_project(schema_simp_pop): + x = A().proj(a="id_a") # rename + assert x.heading.names == ["a"], "renaming does not work" + x = A().proj(a="(id_a)") # extend + assert set(x.heading.names) == set(("id_a", "a")), "extend does not work" + + # projection after restriction + cond = L() & "cond_in_l" + assert len(D() & cond) + len(D() - cond) == len(D()), "failed semijoin or antijoin" + assert len((D() & cond).proj()) == len((D() & cond)), ( + "projection failed: altered its argument" "s cardinality" + ) + + +def test_rename_non_dj_attribute( + connection_test, schema_simp_pop, schema_any_pop, prefix +): + schema = prefix + "_test1" + connection_test.query( + f"CREATE TABLE {schema}.test_table (oldID int PRIMARY KEY)" + ).fetchall() + mySchema = dj.VirtualModule(schema, schema) + assert ( + "oldID" + not in mySchema.TestTable.proj(new_name="oldID").heading.attributes.keys() + ), "Failed to rename attribute correctly" + connection_test.query(f"DROP TABLE {schema}.test_table") + + +def test_union(schema_simp_pop): + x = set(zip(*IJ.fetch("i", "j"))) + y = set(zip(*JI.fetch("i", "j"))) + assert ( + 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 + assert x.union(y) == z + assert len(IJ + JI) == len(z) + + +def test_outer_union_fail(schema_simp_pop): + """Union of two tables with different primary keys raises an error.""" + with pytest.raises(dj.DataJointError): + A() + B() + + +def test_outer_union_fail(schema_any_pop): + """Union of two tables with different primary keys raises an error.""" + t = Trial + Ephys + t.fetch() + assert set(t.heading.names) == set(Trial.heading.names) | set(Ephys.heading.names) + len(t) + + +def test_preview(schema_simp_pop): + with dj.config(display__limit=7): + x = A().proj(a="id_a") + s = x.preview() + assert len(s.split("\n")) == len(x) + 2 + + +def test_heading_repr(schema_simp_pop): + x = A * D + s = repr(x.heading) + assert len( + list( + 1 + for g in s.split("\n") + if g.strip() and not g.strip().startswith(("-", "#")) + ) + ) == len(x.heading.attributes) + + +def test_aggregate(schema_simp_pop): + x = B().aggregate(B.C()) + assert len(x) == len(B() & B.C()) + + x = B().aggregate(B.C(), keep_all_rows=True) + assert len(x) == len(B()) # test LEFT join + + assert len((x & "id_b=0").fetch()) == len( + B() & "id_b=0" + ) # test restricted aggregation + + x = B().aggregate( + B.C(), + "n", + count="count(id_c)", + mean="avg(value)", + max="max(value)", + keep_all_rows=True, + ) + assert len(x) == len(B()) + y = x & "mean>0" # restricted aggregation + assert len(y) > 0 + assert all(y.fetch("mean") > 0) + for n, count, mean, max_, key in zip(*x.fetch("n", "count", "mean", "max", dj.key)): + assert n == count, "aggregation failed (count)" + values = (B.C() & key).fetch("value") + assert bool(len(values)) == bool(n), "aggregation failed (restriction)" + if n: + assert np.isclose( + mean, values.mean(), rtol=1e-4, atol=1e-5 + ), "aggregation failed (mean)" + assert np.isclose( + max_, values.max(), rtol=1e-4, atol=1e-5 + ), "aggregation failed (max)" + + +def test_aggr(schema_simp_pop): + x = B.aggr(B.C) + l1 = len(x) + l2 = len(B & B.C) + assert l1 == l2 + + x = B().aggr(B.C(), keep_all_rows=True) + assert len(x) == len(B()) # test LEFT join + + assert len((x & "id_b=0").fetch()) == len( + B() & "id_b=0" + ) # test restricted aggregation + + x = B().aggr( + B.C(), + "n", + count="count(id_c)", + mean="avg(value)", + max="max(value)", + keep_all_rows=True, + ) + assert len(x) == len(B()) + y = x & "mean>0" # restricted aggregation + assert len(y) > 0 + assert all(y.fetch("mean") > 0) + for n, count, mean, max_, key in zip(*x.fetch("n", "count", "mean", "max", dj.key)): + assert n == count, "aggregation failed (count)" + values = (B.C() & key).fetch("value") + assert bool(len(values)) == bool(n), "aggregation failed (restriction)" + if n: + assert np.isclose( + mean, values.mean(), rtol=1e-4, atol=1e-5 + ), "aggregation failed (mean)" + assert np.isclose( + max_, values.max(), rtol=1e-4, atol=1e-5 + ), "aggregation failed (max)" + + +def test_semijoin(schema_simp_pop): + """ + test that semijoins and antijoins are formed correctly + """ + x = IJ() + y = JI() + n = len(x & y.fetch(as_dict=True)) + m = len(x - y.fetch(as_dict=True)) + assert n > 0 and m > 0 + assert len(x) == m + n + assert len(x & y.fetch()) == n + assert len(x - y.fetch()) == m + semi = x & y + anti = x - y + assert len(semi) == n + assert len(anti) == m + + +def test_pandas_fetch_and_restriction(schema_simp_pop): + q = L & "cond_in_l = 0" + df = q.fetch(format="frame") # pandas dataframe + assert isinstance(df, pandas.DataFrame) + assert len(E & q) == len(E & df) + + +def test_restriction_by_null(schema_any_pop): + assert len(Experiment & "username is null") > 0 + assert len(Experiment & "username is not null") > 0 + + +def test_restriction_between(schema_any_pop): # see issue + assert len(Experiment & 'username between "S" and "Z"') < len(Experiment()) + + +def test_restrictions_by_lists(schema_simp_pop): + x = D() + y = L() & "cond_in_l" + + lenx = len(x) + assert lenx > 0 and len(y) > 0 and len(x & y) < len(x), "incorrect test setup" + + assert len(D()) == len(D & dj.AndList([])) + assert len(D & []) == 0 + assert len(D & [[]]) == 0 # an OR-list of OR-list + + lenx = len(x) + assert lenx > 0 and len(y) > 0 and len(x & y) < len(x), "incorrect test setup" + assert len(x & y) == len(D * L & "cond_in_l"), "incorrect semijoin" + assert len(x - y) == len(x) - len(x & y), "incorrect antijoin" + assert len(y - x) == len(y) - len(y & x), "incorrect antijoin" + assert len(x & []) == 0, "incorrect restriction by an empty list" + assert len(x & ()) == 0, "incorrect restriction by an empty tuple" + assert len(x & set()) == 0, "incorrect restriction by an empty set" + assert len(x - []) == lenx, "incorrect restriction by an empty list" + assert len(x - ()) == lenx, "incorrect restriction by an empty tuple" + assert len(x - set()) == lenx, "incorrect restriction by an empty set" + assert len(x & {}) == lenx, "incorrect restriction by a tuple with no attributes" + assert len(x - {}) == 0, "incorrect restriction by a tuple with no attributes" + assert ( + len(x & {"foo": 0}) == lenx + ), "incorrect restriction by a tuple with no matching attributes" + assert ( + len(x - {"foo": 0}) == 0 + ), "incorrect restriction by a tuple with no matching attributes" + assert len(x & y) == len(x & y.fetch()), "incorrect restriction by a list" + assert len(x - y) == len(x - y.fetch()), "incorrect restriction by a list" + w = A() + assert len(w) > 0, "incorrect test setup: w is empty" + assert ( + bool(set(w.heading.names) & set(y.heading.names)) + != "incorrect test setup: w and y should have no common attributes" + ) + assert len(w) == len(w & y), "incorrect restriction without common attributes" + assert len(w - y) == 0, "incorrect restriction without common attributes" + + +def test_datetime(schema_any_pop): + """Test date retrieval""" + date = Experiment().fetch("experiment_date")[0] + e1 = Experiment() & dict(experiment_date=str(date)) + e2 = Experiment() & dict(experiment_date=date) + assert len(e1) == len(e2) > 0, "Two date restriction do not yield the same result" + + +def test_date(schema_simp_pop): + """Test date update""" + # https://github.com/datajoint/datajoint-python/issues/664 + F.insert1((2, "2019-09-25")) + + new_value = None + F.update1(dict((F & "id=2").fetch1("KEY"), date=new_value)) + assert (F & "id=2").fetch1("date") == new_value + + new_value = datetime.date(2019, 10, 25) + F.update1(dict((F & "id=2").fetch1("KEY"), date=new_value)) + assert (F & "id=2").fetch1("date") == new_value + + F.update1(dict((F & "id=2").fetch1("KEY"), date=None)) + assert (F & "id=2").fetch1("date") == None + + +def test_join_project(schema_simp_pop): + """Test join of projected relations with matching non-primary key""" + q = DataA.proj() * DataB.proj() + assert ( + len(q) == len(DataA()) == len(DataB()) + ), "Join of projected relations does not work" + + +def test_ellipsis(schema_any_pop): + r = Experiment.proj(..., "- data_path").head(1, as_dict=True) + assert set(Experiment.heading).difference(r[0]) == {"data_path"} + + +def test_update_single_key(schema_simp_pop): + """Test that only one row can be updated""" + with pytest.raises(dj.DataJointError): + TTestUpdate.update1( + dict(TTestUpdate.fetch1("KEY"), string_attr="my new string") + ) + + +def test_update_no_primary(schema_simp_pop): + """Test that no primary key can be updated""" + with pytest.raises(dj.DataJointError): + TTestUpdate.update1(dict(TTestUpdate.fetch1("KEY"), primary_key=2)) + + +def test_update_missing_attribute(schema_simp_pop): + """Test that attribute is in table""" + with pytest.raises(dj.DataJointError): + TTestUpdate.update1(dict(TTestUpdate.fetch1("KEY"), not_existing=2)) + + +def test_update_string_attribute(schema_simp_pop): + """Test replacing a string value""" + rel = TTestUpdate() & dict(primary_key=0) + s = "".join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(10) + ) + TTestUpdate.update1(dict(rel.fetch1("KEY"), string_attr=s)) + assert s == rel.fetch1("string_attr"), "Updated string does not match" + + +def test_update_numeric_attribute(schema_simp_pop): + """Test replacing a string value""" + rel = TTestUpdate() & dict(primary_key=0) + s = random.randint(0, 10) + TTestUpdate.update1(dict(rel.fetch1("KEY"), num_attr=s)) + assert s == rel.fetch1("num_attr"), "Updated integer does not match" + TTestUpdate.update1(dict(rel.fetch1("KEY"), num_attr=None)) + assert np.isnan(rel.fetch1("num_attr")), "Numeric value is not NaN" + + +def test_update_blob_attribute(schema_simp_pop): + """Test replacing a string value""" + rel = TTestUpdate() & dict(primary_key=0) + s = rel.fetch1("blob_attr") + TTestUpdate.update1(dict(rel.fetch1("KEY"), blob_attr=s.T)) + assert s.T.shape == rel.fetch1("blob_attr").shape, "Array dimensions do not match" + + +def test_reserved_words(schema_simp_pop): + """Test the user of SQL reserved words as attributes""" + rel = ReservedWord() + rel.insert1( + {"key": 1, "in": "ouch", "from": "bummer", "int": 3, "select": "major pain"} + ) + assert (rel & {"key": 1, "in": "ouch", "from": "bummer"}).fetch1("int") == 3 + assert (rel.proj("int", double="from") & {"double": "bummer"}).fetch1("int") == 3 + (rel & {"key": 1}).delete() + + +def test_reserved_words2(schema_simp_pop): + """Test the user of SQL reserved words as attributes""" + rel = ReservedWord() + rel.insert1( + {"key": 1, "in": "ouch", "from": "bummer", "int": 3, "select": "major pain"} + ) + with pytest.raises(dj.DataJointError): + (rel & "key=1").fetch( + "in" + ) # error because reserved word `key` is not in backquotes. See issue #249 + + +def test_permissive_join_basic(schema_any_pop): + """Verify join compatibility check is skipped for join""" + Child @ Parent + + +def test_permissive_restriction_basic(schema_any_pop): + """Verify join compatibility check is skipped for restriction""" + Child ^ Parent + + +def test_complex_date_restriction(schema_simp_pop): + # https://github.com/datajoint/datajoint-python/issues/892 + """Test a complex date restriction""" + q = OutfitLaunch & "day between curdate() - interval 30 day and curdate()" + assert len(q) == 1 + q = OutfitLaunch & "day between curdate() - interval 4 week and curdate()" + assert len(q) == 1 + q = OutfitLaunch & "day between curdate() - interval 1 month and curdate()" + assert len(q) == 1 + q = OutfitLaunch & "day between curdate() - interval 1 year and curdate()" + assert len(q) == 1 + q = OutfitLaunch & "`day` between curdate() - interval 30 day and curdate()" + assert len(q) == 1 + q.delete() + + +def test_null_dict_restriction(schema_simp_pop): + # https://github.com/datajoint/datajoint-python/issues/824 + """Test a restriction for null using dict""" + F.insert([dict(id=5)]) + q = F & dj.AndList([dict(id=5), "date is NULL"]) + assert len(q) == 1 + q = F & dict(id=5, date=None) + assert len(q) == 1 + + +def test_joins_with_aggregation(schema_any_pop): + # https://github.com/datajoint/datajoint-python/issues/898 + # https://github.com/datajoint/datajoint-python/issues/899 + subjects = SubjectA.aggr( + SessionStatusA & 'status="trained_1a" or status="trained_1b"', + date_trained="min(date(session_start_time))", + ) + assert len(SessionDateA * subjects) == 4 + assert len(subjects * SessionDateA) == 4 + + subj_query = SubjectA.aggr( + SessionA * SessionStatusA & 'status="trained_1a" or status="trained_1b"', + date_trained="min(date(session_start_time))", + ) + session_dates = ( + SessionDateA * (subj_query & 'date_trained<"2020-12-21"') + ) & "session_date master + -> Unit + """ + + test_schema.drop() + + +def test_list_tables(schema_simp): + """ + https://github.com/datajoint/datajoint-python/issues/838 + """ + assert set( + [ + "reserved_word", + "#l", + "#a", + "__d", + "__b", + "__b__c", + "__e", + "__e__f", + "#outfit_launch", + "#outfit_launch__outfit_piece", + "#i_j", + "#j_i", + "#t_test_update", + "#data_a", + "#data_b", + "f", + "#argmax_test", + "#website", + "profile", + "profile__website", + ] + ) == set(schema_simp.list_tables()) + + +def test_schema_save_any(schema_any): + assert "class Experiment(dj.Imported)" in schema_any.code + + +def test_schema_save_empty(schema_empty): + assert "class Experiment(dj.Imported)" in schema_empty.code + + +def test_uppercase_schema(db_creds_root): + """ + https://github.com/datajoint/datajoint-python/issues/564 + """ + dj.conn(**db_creds_root, reset=True) + schema1 = dj.Schema("Schema_A") + + @schema1 + class Subject(dj.Manual): + definition = """ + name: varchar(32) + """ + + Schema_A = dj.VirtualModule("Schema_A", "Schema_A") + + schema2 = dj.Schema("schema_b") + + @schema2 + class Recording(dj.Manual): + definition = """ + -> Schema_A.Subject + id: smallint + """ + + schema2.drop() + schema1.drop() diff --git a/tests/test_schema_keywords.py b/tests/test_schema_keywords.py new file mode 100644 index 000000000..22ed1c2a0 --- /dev/null +++ b/tests/test_schema_keywords.py @@ -0,0 +1,49 @@ +import pytest +import datajoint as dj + + +class A(dj.Manual): + definition = """ + a_id: int # a id + """ + + +class B(dj.Manual): + source = None + definition = """ + -> self.source + b_id: int # b id + """ + + class H(dj.Part): + definition = """ + -> master + name: varchar(128) # name + """ + + class C(dj.Part): + definition = """ + -> master + -> master.H + """ + + +class D(B): + source = A + + +@pytest.fixture +def schema_kwd(connection_test, prefix): + schema = dj.Schema(prefix + "_keywords", connection=connection_test) + schema(A) + schema(D) + yield schema + schema.drop() + + +def test_inherited_part_table(schema_kwd): + assert "a_id" in D().heading.attributes + assert "b_id" in D().heading.attributes + assert "a_id" in D.C().heading.attributes + assert "b_id" in D.C().heading.attributes + assert "name" in D.C().heading.attributes diff --git a/tests_old/test_settings.py b/tests/test_settings.py similarity index 69% rename from tests_old/test_settings.py rename to tests/test_settings.py index 63c3dad36..b937d5ad3 100644 --- a/tests_old/test_settings.py +++ b/tests/test_settings.py @@ -1,8 +1,8 @@ import pprint import random import string -from datajoint import settings -from nose.tools import assert_true, assert_equal, raises +import pytest +from datajoint import DataJointError, settings import datajoint as dj import os @@ -14,7 +14,7 @@ def test_load_save(): dj.config.save("tmp.json") conf = settings.Config() conf.load("tmp.json") - assert_true(conf == dj.config, "Two config files do not match.") + assert conf == dj.config os.remove("tmp.json") @@ -25,7 +25,7 @@ def test_singleton(): conf.load("tmp.json") conf["dummy.val"] = 2 - assert_true(conf == dj.config, "Config does not behave like a singleton.") + assert conf == dj.config os.remove("tmp.json") @@ -34,36 +34,36 @@ def test_singleton2(): conf = settings.Config() conf["dummy.val"] = 2 _ = settings.Config() # a new instance should not delete dummy.val - assert_true(conf["dummy.val"] == 2, "Config does not behave like a singleton.") + assert conf["dummy.val"] == 2 -@raises(dj.DataJointError) def test_validator(): """Testing validator""" - dj.config["database.port"] = "harbor" + with pytest.raises(DataJointError): + dj.config["database.port"] = "harbor" def test_del(): """Testing del""" dj.config["peter"] = 2 - assert_true("peter" in dj.config) + assert "peter" in dj.config del dj.config["peter"] - assert_true("peter" not in dj.config) + assert "peter" not in dj.config def test_len(): """Testing len""" - assert_equal(len(dj.config), len(dj.config._conf)) + len(dj.config) == len(dj.config._conf) def test_str(): """Testing str""" - assert_equal(str(dj.config), pprint.pformat(dj.config._conf, indent=4)) + str(dj.config) == pprint.pformat(dj.config._conf, indent=4) def test_repr(): """Testing repr""" - assert_equal(repr(dj.config), pprint.pformat(dj.config._conf, indent=4)) + repr(dj.config) == pprint.pformat(dj.config._conf, indent=4) def test_save(): @@ -76,7 +76,7 @@ def test_save(): os.rename(settings.LOCALCONFIG, tmpfile) moved = True dj.config.save_local() - assert_true(os.path.isfile(settings.LOCALCONFIG)) + assert os.path.isfile(settings.LOCALCONFIG) if moved: os.rename(tmpfile, settings.LOCALCONFIG) @@ -101,5 +101,5 @@ def test_contextmanager(): """Testing context manager""" dj.config["arbitrary.stuff"] = 7 with dj.config(arbitrary__stuff=10): - assert_true(dj.config["arbitrary.stuff"] == 10) - assert_true(dj.config["arbitrary.stuff"] == 7) + assert dj.config["arbitrary.stuff"] == 10 + assert dj.config["arbitrary.stuff"] == 7 diff --git a/tests/test_tls.py b/tests/test_tls.py new file mode 100644 index 000000000..22558af5b --- /dev/null +++ b/tests/test_tls.py @@ -0,0 +1,32 @@ +import pytest +import datajoint as dj +from pymysql.err import OperationalError + + +def test_secure_connection(db_creds_test, connection_test): + result = ( + dj.conn(reset=True, **db_creds_test) + .query("SHOW STATUS LIKE 'Ssl_cipher';") + .fetchone()[1] + ) + assert len(result) > 0 + + +def test_insecure_connection(db_creds_test, connection_test): + result = ( + dj.conn(use_tls=False, reset=True, **db_creds_test) + .query("SHOW STATUS LIKE 'Ssl_cipher';") + .fetchone()[1] + ) + assert result == "" + + +def test_reject_insecure(db_creds_test, connection_test): + with pytest.raises(OperationalError): + dj.conn( + db_creds_test["host"], + user="djssl", + password="djssl", + use_tls=False, + reset=True, + ).query("SHOW STATUS LIKE 'Ssl_cipher';").fetchone()[1] diff --git a/tests/test_university.py b/tests/test_university.py new file mode 100644 index 000000000..800ee7cdf --- /dev/null +++ b/tests/test_university.py @@ -0,0 +1,169 @@ +import pytest +import hashlib +from pathlib import Path +from datajoint import DataJointError +import datajoint as dj +from .schema_university import * +from . import schema_university + + +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] + + +@pytest.fixture +def schema_uni_inactive(): + schema = dj.Schema(context=schema_university.LOCALS_UNI) + schema(Student) + schema(Department) + schema(StudentMajor) + schema(Course) + schema(Term) + schema(Section) + schema(CurrentTerm) + schema(Enroll) + schema(LetterGrade) + schema(Grade) + yield schema + schema.drop() + + +@pytest.fixture +def schema_uni(db_creds_test, schema_uni_inactive, connection_test, prefix): + # Deferred activation + schema_uni_inactive.activate( + prefix + "_university", connection=dj.conn(**db_creds_test) + ) + # --------------- Fill University ------------------- + test_data_dir = Path(__file__).parent / "data" + for table in ( + Student, + Department, + StudentMajor, + Course, + Term, + CurrentTerm, + Section, + Enroll, + Grade, + ): + path = test_data_dir / Path(table.__name__ + ".csv") + assert path.is_file(), f"File {path} is not a file" + assert path.exists(), f"File {path} does not exist" + table().insert(path) + return schema_uni_inactive + + +def test_activate_unauthorized(schema_uni_inactive, db_creds_test, connection_test): + with pytest.raises(DataJointError): + schema_uni_inactive.activate( + "unauthorized", connection=dj.conn(**db_creds_test) + ) + + +def test_fill(schema_uni): + """check that the randomized tables are consistently defined""" + # check randomized tables + assert len(Student()) == 300 and _hash4(Student) == "1e1a" + assert len(StudentMajor()) == 226 and _hash4(StudentMajor) == "3129" + assert len(Section()) == 756 and _hash4(Section) == "dc7e" + assert len(Enroll()) == 3364 and _hash4(Enroll) == "177d" + assert len(Grade()) == 3027 and _hash4(Grade) == "4a9d" + + +def test_restrict(schema_uni): + """ + 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 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 len(set(state1)) == len(set(state2)) == 44 + assert 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 len(s1) == 11 + assert s1 == s2 + + millennials = Student & 'date_of_birth between "1981-01-01" and "1996-12-31"' + assert len(millennials) == 170 + millennials_no_math = millennials - (Enroll & 'dept="MATH"') + assert len(millennials_no_math) == 53 + + inactive_students = Student - (Enroll & CurrentTerm) + assert len(inactive_students) == 204 + + # Females who are active or major in non-math + special = Student & [Enroll, StudentMajor - {"dept": "MATH"}] & {"sex": "F"} + assert len(special) == 158 + + +def test_advanced_join(schema_uni): + """test advanced joins""" + # Students with ungraded courses in current term + ungraded = Enroll * CurrentTerm - Grade + assert len(ungraded) == 34 + + # add major + major = StudentMajor.proj(..., major="dept") + assert len(ungraded.join(major, left=True)) == len(ungraded) == 34 + assert len(ungraded.join(major)) == len(ungraded & major) == 31 + + +def test_union(schema_uni): + # effective left join Enroll with Major + q1 = (Enroll & "student_id=101") + (Enroll & "student_id=102") + q2 = Enroll & "student_id in (101, 102)" + assert len(q1) == len(q2) == 41 + + +def test_aggr(schema_uni): + avg_grade_per_course = Course.aggr( + Grade * LetterGrade, avg_grade="round(avg(points), 2)" + ) + assert 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 len(gpa) == 261 + assert 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 len(set(section.fetch("dept"))) == 1 + assert len(section) == 17 + assert 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 not any( + name in section.heading.names for name in Grade.heading.secondary_attributes + ) + assert len(set(section.fetch("dept"))) == 1 + assert len(section) == 168 + assert bool(section) diff --git a/tests/test_update1.py b/tests/test_update1.py new file mode 100644 index 000000000..f29d2ab0e --- /dev/null +++ b/tests/test_update1.py @@ -0,0 +1,146 @@ +import pytest +import os +import numpy as np +from pathlib import Path +import tempfile +import datajoint as dj +from datajoint import DataJointError + + +class Thing(dj.Manual): + definition = """ + thing : int + --- + number=0 : int + frac : float + picture = null : attach@update_store + params = null : longblob + img_file = null: filepath@update_repo + timestamp = CURRENT_TIMESTAMP : datetime + """ + + +@pytest.fixture(scope="module") +def mock_stores_update(tmpdir_factory): + og_stores_config = dj.config.get("stores") + if "stores" not in dj.config: + dj.config["stores"] = {} + dj.config["stores"]["update_store"] = dict( + protocol="file", location=tmpdir_factory.mktemp("store") + ) + dj.config["stores"]["update_repo"] = dict( + stage=tmpdir_factory.mktemp("repo_stage"), + protocol="file", + location=tmpdir_factory.mktemp("repo_loc"), + ) + yield + if og_stores_config is None: + del dj.config["stores"] + else: + dj.config["stores"] = og_stores_config + + +@pytest.fixture +def schema_update1(connection_test, prefix): + schema = dj.Schema( + prefix + "_update1", context=dict(Thing=Thing), connection=connection_test + ) + schema(Thing) + yield schema + schema.drop() + + +def test_update1(tmpdir, enable_filepath_feature, schema_update1, mock_stores_update): + """Test normal updates""" + # CHECK 1 -- initial insert + key = dict(thing=1) + Thing.insert1(dict(key, frac=0.5)) + check1 = Thing.fetch1() + + # CHECK 2 -- some updates + # numbers and datetimes + Thing.update1(dict(key, number=3, frac=30, timestamp="2020-01-01 10:00:00")) + # attachment + attach_file = Path(tmpdir, "attach1.dat") + buffer1 = os.urandom(100) + attach_file.write_bytes(buffer1) + Thing.update1(dict(key, picture=attach_file)) + attach_file.unlink() + assert not attach_file.is_file() + + # filepath + stage_path = dj.config["stores"]["update_repo"]["stage"] + relpath, filename = "one/two/three", "picture.dat" + managed_file = Path(stage_path, relpath, filename) + managed_file.parent.mkdir(parents=True, exist_ok=True) + original_file_data = os.urandom(3000) + with managed_file.open("wb") as f: + f.write(original_file_data) + Thing.update1(dict(key, img_file=managed_file)) + managed_file.unlink() + assert not managed_file.is_file() + + check2 = Thing.fetch1(download_path=tmpdir) + buffer2 = Path(check2["picture"]).read_bytes() # read attachment + final_file_data = managed_file.read_bytes() # read filepath + + # CHECK 3 -- reset to default values using None + Thing.update1( + dict( + key, + number=None, + timestamp=None, + picture=None, + img_file=None, + params=np.random.randn(3, 3), + ) + ) + check3 = Thing.fetch1() + + assert ( + check1["number"] == 0 and check1["picture"] is None and check1["params"] is None + ) + + assert ( + check2["number"] == 3 + and check2["frac"] == 30.0 + and check2["picture"] is not None + and check2["params"] is None + and buffer1 == buffer2 + ) + + assert ( + check3["number"] == 0 + and check3["frac"] == 30.0 + and check3["picture"] is None + and check3["img_file"] is None + and isinstance(check3["params"], np.ndarray) + ) + + assert check3["timestamp"] > check2["timestamp"] + assert buffer1 == buffer2 + assert original_file_data == final_file_data + + +def test_update1_nonexistent( + enable_filepath_feature, schema_update1, mock_stores_update +): + with pytest.raises(DataJointError): + # updating a non-existent entry + Thing.update1(dict(thing=100, frac=0.5)) + + +def test_update1_noprimary(enable_filepath_feature, schema_update1, mock_stores_update): + with pytest.raises(DataJointError): + # missing primary key + Thing.update1(dict(number=None)) + + +def test_update1_misspelled_attribute( + enable_filepath_feature, schema_update1, mock_stores_update +): + key = dict(thing=17) + Thing.insert1(dict(key, frac=1.5)) + with pytest.raises(DataJointError): + # misspelled attribute + Thing.update1(dict(key, numer=3)) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 000000000..04325db56 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,25 @@ +""" +Collection of test cases to test core module. +""" +from datajoint import DataJointError +from datajoint.utils import from_camel_case, to_camel_case +import pytest + + +def test_from_camel_case(): + assert from_camel_case("AllGroups") == "all_groups" + with pytest.raises(DataJointError): + from_camel_case("repNames") + with pytest.raises(DataJointError): + from_camel_case("10_all") + with pytest.raises(DataJointError): + from_camel_case("hello world") + with pytest.raises(DataJointError): + from_camel_case("#baisc_names") + + +def test_to_camel_case(): + assert to_camel_case("all_groups") == "AllGroups" + assert to_camel_case("hello") == "Hello" + assert to_camel_case("this_is_a_sample_case") == "ThisIsASampleCase" + assert to_camel_case("This_is_Mixed") == "ThisIsMixed" diff --git a/tests/test_uuid.py b/tests/test_uuid.py new file mode 100644 index 000000000..d99aa6c4c --- /dev/null +++ b/tests/test_uuid.py @@ -0,0 +1,69 @@ +import pytest +import uuid +from .schema_uuid import Basic, Item, Topic +from datajoint import DataJointError +from itertools import count + + +def test_uuid(schema_uuid): + """test inserting and fetching of UUID attributes and restricting by UUID attributes""" + u, n = uuid.uuid4(), -1 + Basic().insert1(dict(item=u, number=n)) + Basic().insert(zip(map(uuid.uuid1, range(20)), count())) + number = (Basic() & {"item": u}).fetch1("number") + assert number == n + item = (Basic & {"number": n}).fetch1("item") + assert u == item + + +def test_string_uuid(schema_uuid): + """test that only UUID objects are accepted when inserting UUID fields""" + u, n = "00000000-0000-0000-0000-000000000000", 24601 + Basic().insert1(dict(item=u, number=n)) + k, m = (Basic & {"item": u}).fetch1("KEY", "number") + assert m == n + assert isinstance(k["item"], uuid.UUID) + + +def test_invalid_uuid_insert1(schema_uuid): + """test that only UUID objects are accepted when inserting UUID fields""" + u, n = 0, 24601 + with pytest.raises(DataJointError): + Basic().insert1(dict(item=u, number=n)) + + +def test_invalid_uuid_insert2(schema_uuid): + """test that only UUID objects are accepted when inserting UUID fields""" + u, n = "abc", 24601 + with pytest.raises(DataJointError): + Basic().insert1(dict(item=u, number=n)) + + +def test_invalid_uuid_restrict1(schema_uuid): + """test that only UUID objects are accepted when inserting UUID fields""" + u = 0 + with pytest.raises(DataJointError): + k, m = (Basic & {"item": u}).fetch1("KEY", "number") + + +def test_invalid_uuid_restrict1(schema_uuid): + """test that only UUID objects are accepted when inserting UUID fields""" + u = "abc" + with pytest.raises(DataJointError): + k, m = (Basic & {"item": u}).fetch1("KEY", "number") + + +def test_uuid_dependencies(schema_uuid): + """test the use of UUID in foreign keys""" + for word in ( + "Neuroscience", + "Knowledge", + "Curiosity", + "Inspiration", + "Science", + "Philosophy", + "Conscience", + ): + Topic().add(word) + Item.populate() + assert Item().progress() == (0, len(Topic())) diff --git a/tests/test_virtual_module.py b/tests/test_virtual_module.py new file mode 100644 index 000000000..bd8a0c754 --- /dev/null +++ b/tests/test_virtual_module.py @@ -0,0 +1,7 @@ +import datajoint as dj +from datajoint.user_tables import UserTable + + +def test_virtual_module(schema_any, connection_test): + module = dj.VirtualModule("module", schema_any.database, connection=connection_test) + assert issubclass(module.Experiment, UserTable) diff --git a/tests_old/test_blob_matlab.py b/tests_old/test_blob_matlab.py deleted file mode 100644 index 6104c9291..000000000 --- a/tests_old/test_blob_matlab.py +++ /dev/null @@ -1,175 +0,0 @@ -import numpy as np -import datajoint as dj -from datajoint.blob import pack, unpack - -from nose.tools import assert_equal, assert_true, assert_tuple_equal, assert_false -from numpy.testing import assert_array_equal - -from . import PREFIX, CONN_INFO - -schema = dj.Schema(PREFIX + "_test1", locals(), connection=dj.conn(**CONN_INFO)) - - -@schema -class Blob(dj.Manual): - definition = """ # diverse types of blobs - id : int - ----- - comment : varchar(255) - blob : longblob - """ - - -def insert_blobs(): - """ - This function inserts blobs resulting from the following datajoint-matlab code: - - self.insert({ - 1 'simple string' 'character string' - 2 '1D vector' 1:15:180 - 3 'string array' {'string1' 'string2'} - 4 'struct array' struct('a', {1,2}, 'b', {struct('c', magic(3)), struct('C', magic(5))}) - 5 '3D double array' reshape(1:24, [2,3,4]) - 6 '3D uint8 array' reshape(uint8(1:24), [2,3,4]) - 7 '3D complex array' fftn(reshape(1:24, [2,3,4])) - }) - - and then dumped using the command - mysqldump -u username -p --hex-blob test_schema blob_table > blob.sql - """ - - schema.connection.query( - """ - INSERT INTO {table_name} VALUES - (1,'simple string',0x6D596D00410200000000000000010000000000000010000000000000000400000000000000630068006100720061006300740065007200200073007400720069006E006700), - (2,'1D vector',0x6D596D0041020000000000000001000000000000000C000000000000000600000000000000000000000000F03F00000000000030400000000000003F4000000000000047400000000000804E4000000000000053400000000000C056400000000000805A400000000000405E4000000000000061400000000000E062400000000000C06440), - (3,'string array',0x6D596D00430200000000000000010000000000000002000000000000002F0000000000000041020000000000000001000000000000000700000000000000040000000000000073007400720069006E00670031002F0000000000000041020000000000000001000000000000000700000000000000040000000000000073007400720069006E0067003200), - (4,'struct array',0x6D596D005302000000000000000100000000000000020000000000000002000000610062002900000000000000410200000000000000010000000000000001000000000000000600000000000000000000000000F03F9000000000000000530200000000000000010000000000000001000000000000000100000063006900000000000000410200000000000000030000000000000003000000000000000600000000000000000000000000204000000000000008400000000000001040000000000000F03F0000000000001440000000000000224000000000000018400000000000001C40000000000000004029000000000000004102000000000000000100000000000000010000000000000006000000000000000000000000000040100100000000000053020000000000000001000000000000000100000000000000010000004300E9000000000000004102000000000000000500000000000000050000000000000006000000000000000000000000003140000000000000374000000000000010400000000000002440000000000000264000000000000038400000000000001440000000000000184000000000000028400000000000003240000000000000F03F0000000000001C400000000000002A400000000000003340000000000000394000000000000020400000000000002C400000000000003440000000000000354000000000000000400000000000002E400000000000003040000000000000364000000000000008400000000000002240), - (5,'3D double array',0x6D596D004103000000000000000200000000000000030000000000000004000000000000000600000000000000000000000000F03F000000000000004000000000000008400000000000001040000000000000144000000000000018400000000000001C40000000000000204000000000000022400000000000002440000000000000264000000000000028400000000000002A400000000000002C400000000000002E40000000000000304000000000000031400000000000003240000000000000334000000000000034400000000000003540000000000000364000000000000037400000000000003840), - (6,'3D uint8 array',0x6D596D0041030000000000000002000000000000000300000000000000040000000000000009000000000000000102030405060708090A0B0C0D0E0F101112131415161718), - (7,'3D complex array',0x6D596D0041030000000000000002000000000000000300000000000000040000000000000006000000010000000000000000C0724000000000000028C000000000000038C0000000000000000000000000000038C0000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000AA4C58E87AB62B400000000000000000AA4C58E87AB62BC0000000000000008000000000000052400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000008000000000000052C000000000000000800000000000000080000000000000008000000000000000800000000000000080 - ); - """.format( - table_name=Blob.full_table_name - ) - ) - - -class TestFetch: - @classmethod - def setup_class(cls): - assert_false(dj.config["safemode"], "safemode must be disabled") - Blob().delete() - insert_blobs() - - @staticmethod - def test_complex_matlab_blobs(): - """ - test correct de-serialization of various blob types - """ - blobs = Blob().fetch("blob", order_by="KEY") - - blob = blobs[0] # 'simple string' 'character string' - assert_equal(blob[0], "character string") - - blob = blobs[1] # '1D vector' 1:15:180 - assert_array_equal(blob, np.r_[1:180:15][None, :]) - assert_array_equal(blob, unpack(pack(blob))) - - blob = blobs[2] # 'string array' {'string1' 'string2'} - assert_true(isinstance(blob, dj.MatCell)) - assert_array_equal(blob, np.array([["string1", "string2"]])) - assert_array_equal(blob, unpack(pack(blob))) - - blob = blobs[ - 3 - ] # 'struct array' struct('a', {1,2}, 'b', {struct('c', magic(3)), struct('C', magic(5))}) - assert_true(isinstance(blob, dj.MatStruct)) - assert_tuple_equal(blob.dtype.names, ("a", "b")) - assert_array_equal(blob.a[0, 0], np.array([[1.0]])) - assert_array_equal(blob.a[0, 1], np.array([[2.0]])) - assert_true(isinstance(blob.b[0, 1], dj.MatStruct)) - assert_tuple_equal(blob.b[0, 1].C[0, 0].shape, (5, 5)) - b = unpack(pack(blob)) - assert_array_equal(b[0, 0].b[0, 0].c, blob[0, 0].b[0, 0].c) - assert_array_equal(b[0, 1].b[0, 0].C, blob[0, 1].b[0, 0].C) - - blob = blobs[4] # '3D double array' reshape(1:24, [2,3,4]) - assert_array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order="F")) - assert_true(blob.dtype == "float64") - assert_array_equal(blob, unpack(pack(blob))) - - blob = blobs[5] # reshape(uint8(1:24), [2,3,4]) - assert_true(np.array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order="F"))) - assert_true(blob.dtype == "uint8") - assert_array_equal(blob, unpack(pack(blob))) - - blob = blobs[6] # fftn(reshape(1:24, [2,3,4])) - assert_tuple_equal(blob.shape, (2, 3, 4)) - assert_true(blob.dtype == "complex128") - assert_array_equal(blob, unpack(pack(blob))) - - @staticmethod - def test_complex_matlab_squeeze(): - """ - test correct de-serialization of various blob types - """ - blob = (Blob & "id=1").fetch1( - "blob", squeeze=True - ) # 'simple string' 'character string' - assert_equal(blob, "character string") - - blob = (Blob & "id=2").fetch1( - "blob", squeeze=True - ) # '1D vector' 1:15:180 - assert_array_equal(blob, np.r_[1:180:15]) - - blob = (Blob & "id=3").fetch1( - "blob", squeeze=True - ) # 'string array' {'string1' 'string2'} - assert_true(isinstance(blob, dj.MatCell)) - assert_array_equal(blob, np.array(["string1", "string2"])) - - blob = (Blob & "id=4").fetch1( - "blob", squeeze=True - ) # 'struct array' struct('a', {1,2}, 'b', {struct('c', magic(3)), struct('C', magic(5))}) - assert_true(isinstance(blob, dj.MatStruct)) - assert_tuple_equal(blob.dtype.names, ("a", "b")) - assert_array_equal( - blob.a, - np.array( - [ - 1.0, - 2, - ] - ), - ) - assert_true(isinstance(blob[1].b, dj.MatStruct)) - assert_tuple_equal(blob[1].b.C.item().shape, (5, 5)) - - blob = (Blob & "id=5").fetch1( - "blob", squeeze=True - ) # '3D double array' reshape(1:24, [2,3,4]) - assert_true(np.array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order="F"))) - assert_true(blob.dtype == "float64") - - blob = (Blob & "id=6").fetch1( - "blob", squeeze=True - ) # reshape(uint8(1:24), [2,3,4]) - assert_true(np.array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order="F"))) - assert_true(blob.dtype == "uint8") - - blob = (Blob & "id=7").fetch1( - "blob", squeeze=True - ) # fftn(reshape(1:24, [2,3,4])) - assert_tuple_equal(blob.shape, (2, 3, 4)) - assert_true(blob.dtype == "complex128") - - @staticmethod - def test_iter(): - """ - test iterator over the entity set - """ - from_iter = {d["id"]: d for d in Blob()} - assert_equal(len(from_iter), len(Blob())) - assert_equal(from_iter[1]["blob"], "character string") diff --git a/tests_old/test_erd.py b/tests_old/test_erd.py deleted file mode 100644 index 1a6293431..000000000 --- a/tests_old/test_erd.py +++ /dev/null @@ -1,87 +0,0 @@ -from nose.tools import assert_false, assert_true -import datajoint as dj -from .schema_simple import A, B, D, E, L, schema, OutfitLaunch -from . import schema_advanced - -namespace = locals() - - -class TestERD: - @staticmethod - def setup(): - """ - class-level test setup. Executes before each test method. - """ - - @staticmethod - def test_decorator(): - assert_true(issubclass(A, dj.Lookup)) - assert_false(issubclass(A, dj.Part)) - assert_true(B.database == schema.database) - assert_true(issubclass(B.C, dj.Part)) - assert_true(B.C.database == schema.database) - assert_true(B.C.master is B and E.F.master is E) - - @staticmethod - def test_dependencies(): - deps = schema.connection.dependencies - deps.load() - assert_true( - all(cls.full_table_name in deps for cls in (A, B, B.C, D, E, E.F, L)) - ) - assert_true(set(A().children()) == set([B.full_table_name, D.full_table_name])) - assert_true(set(D().parents(primary=True)) == set([A.full_table_name])) - assert_true(set(D().parents(primary=False)) == set([L.full_table_name])) - assert_true( - set(deps.descendants(L.full_table_name)).issubset( - cls.full_table_name for cls in (L, D, E, E.F) - ) - ) - - @staticmethod - def test_erd(): - assert_true(dj.diagram.diagram_active, "Failed to import networkx and pydot") - erd = dj.ERD(schema, context=namespace) - graph = erd._make_graph() - assert_true( - set(cls.__name__ for cls in (A, B, D, E, L)).issubset(graph.nodes()) - ) - - @staticmethod - def test_erd_algebra(): - erd0 = dj.ERD(B) - erd1 = erd0 + 3 - erd2 = dj.Di(E) - 3 - erd3 = erd1 * erd2 - erd4 = (erd0 + E).add_parts() - B - E - assert_true(erd0.nodes_to_show == set(cls.full_table_name for cls in [B])) - assert_true( - erd1.nodes_to_show == set(cls.full_table_name for cls in (B, B.C, E, E.F)) - ) - assert_true( - erd2.nodes_to_show == set(cls.full_table_name for cls in (A, B, D, E, L)) - ) - assert_true(erd3.nodes_to_show == set(cls.full_table_name for cls in (B, E))) - assert_true( - erd4.nodes_to_show == set(cls.full_table_name for cls in (B.C, E.F)) - ) - - @staticmethod - def test_repr_svg(): - erd = dj.ERD(schema_advanced, context=namespace) - svg = erd._repr_svg_() - assert_true(svg.startswith("")) - - @staticmethod - def test_make_image(): - erd = dj.ERD(schema, context=namespace) - img = erd.make_image() - assert_true(img.ndim == 3 and img.shape[2] in (3, 4)) - - @staticmethod - def test_part_table_parsing(): - # https://github.com/datajoint/datajoint-python/issues/882 - erd = dj.Di(schema) - graph = erd._make_graph() - assert "OutfitLaunch" in graph.nodes() - assert "OutfitLaunch.OutfitPiece" in graph.nodes() diff --git a/tests_old/test_foreign_keys.py b/tests_old/test_foreign_keys.py deleted file mode 100644 index d082960e4..000000000 --- a/tests_old/test_foreign_keys.py +++ /dev/null @@ -1,51 +0,0 @@ -from nose.tools import assert_equal, assert_false, assert_true -from datajoint.declare import declare - -from . import schema_advanced - - -def test_aliased_fk(): - person = schema_advanced.Person() - parent = schema_advanced.Parent() - person.delete() - assert_false(person) - assert_false(parent) - person.fill() - parent.fill() - assert_true(person) - assert_true(parent) - link = person.proj(parent_name="full_name", parent="person_id") - parents = person * parent * link - parents &= dict(full_name="May K. Hall") - assert_equal( - set(parents.fetch("parent_name")), {"Hanna R. Walters", "Russel S. James"} - ) - delete_count = person.delete() - assert delete_count == 16 - - -def test_describe(): - """real_definition should match original definition""" - for rel in (schema_advanced.LocalSynapse, schema_advanced.GlobalSynapse): - describe = rel.describe() - s1 = declare( - rel.full_table_name, rel.definition, schema_advanced.schema.context - )[0].split("\n") - s2 = declare(rel.full_table_name, describe, globals())[0].split("\n") - for c1, c2 in zip(s1, s2): - assert_equal(c1, c2) - - -def test_delete(): - person = schema_advanced.Person() - parent = schema_advanced.Parent() - person.delete() - assert_false(person) - assert_false(parent) - person.fill() - parent.fill() - assert_true(parent) - original_len = len(parent) - to_delete = len(parent & "11 in (person_id, parent)") - (person & "person_id=11").delete() - assert_true(to_delete and len(parent) == original_len - to_delete) diff --git a/tests_old/test_hash.py b/tests_old/test_hash.py deleted file mode 100644 index dc88290eb..000000000 --- a/tests_old/test_hash.py +++ /dev/null @@ -1,7 +0,0 @@ -from nose.tools import assert_equal -from datajoint import hash - - -def test_hash(): - assert_equal(hash.uuid_from_buffer(b"abc").hex, "900150983cd24fb0d6963f7d28e17f72") - assert_equal(hash.uuid_from_buffer(b"").hex, "d41d8cd98f00b204e9800998ecf8427e") diff --git a/tests_old/test_json.py b/tests_old/test_json.py deleted file mode 100644 index b9b13e4ee..000000000 --- a/tests_old/test_json.py +++ /dev/null @@ -1,219 +0,0 @@ -import inspect -from datajoint.declare import declare -import datajoint as dj -import numpy as np -from distutils.version import LooseVersion -from . import PREFIX - -if LooseVersion(dj.conn().query("select @@version;").fetchone()[0]) >= LooseVersion( - "8.0.0" -): - schema = dj.Schema(PREFIX + "_json") - Team = None - - def setup(): - global Team - - @schema - class Team(dj.Lookup): - definition = """ - name: varchar(40) - --- - car=null: json - unique index(car.name:char(20)) - uniQue inDex ( name, car.name:char(20), (json_value(`car`, _utf8mb4'$.length' returning decimal(4, 1))) ) - """ - contents = [ - ( - "engineering", - { - "name": "Rever", - "length": 20.5, - "inspected": True, - "tire_pressure": [32, 31, 33, 34], - "headlights": [ - { - "side": "left", - "hyper_white": None, - }, - { - "side": "right", - "hyper_white": None, - }, - ], - }, - ), - ( - "business", - { - "name": "Chaching", - "length": 100, - "safety_inspected": False, - "tire_pressure": [34, 30, 27, 32], - "headlights": [ - { - "side": "left", - "hyper_white": True, - }, - { - "side": "right", - "hyper_white": True, - }, - ], - }, - ), - ( - "marketing", - None, - ), - ] - - def teardown(): - schema.drop() - - def test_insert_update(): - car = { - "name": "Discovery", - "length": 22.9, - "inspected": None, - "tire_pressure": [35, 36, 34, 37], - "headlights": [ - { - "side": "left", - "hyper_white": True, - }, - { - "side": "right", - "hyper_white": True, - }, - ], - } - - Team.insert1({"name": "research", "car": car}) - q = Team & {"name": "research"} - assert q.fetch1("car") == car - - car.update({"length": 23}) - Team.update1({"name": "research", "car": car}) - assert q.fetch1("car") == car - - try: - Team.insert1({"name": "hr", "car": car}) - raise Exception("Inserted non-unique car name.") - except dj.DataJointError: - pass - - q.delete_quick() - assert not q - - def test_describe(): - rel = Team() - context = inspect.currentframe().f_globals - s1 = declare(rel.full_table_name, rel.definition, context) - s2 = declare(rel.full_table_name, rel.describe(), context) - assert s1 == s2 - - def test_restrict(): - # dict - assert (Team & {"car.name": "Chaching"}).fetch1("name") == "business" - - assert (Team & {"car.length": 20.5}).fetch1("name") == "engineering" - - assert (Team & {"car.inspected": "true"}).fetch1("name") == "engineering" - - assert (Team & {"car.inspected:unsigned": True}).fetch1("name") == "engineering" - - assert (Team & {"car.safety_inspected": "false"}).fetch1("name") == "business" - - assert (Team & {"car.safety_inspected:unsigned": False}).fetch1( - "name" - ) == "business" - - assert (Team & {"car.headlights[0].hyper_white": None}).fetch( - "name", order_by="name", as_dict=True - ) == [ - {"name": "engineering"}, - {"name": "marketing"}, - ] # if entire record missing, JSON key is missing, or value set to JSON null - - assert (Team & {"car": None}).fetch1("name") == "marketing" - - assert (Team & {"car.tire_pressure": [34, 30, 27, 32]}).fetch1( - "name" - ) == "business" - - assert ( - Team & {"car.headlights[1]": {"side": "right", "hyper_white": True}} - ).fetch1("name") == "business" - - # sql operators - assert (Team & "`car`->>'$.name' LIKE '%ching%'").fetch1( - "name" - ) == "business", "Missing substring" - - assert (Team & "`car`->>'$.length' > 30").fetch1("name") == "business", "<= 30" - - assert ( - Team & "JSON_VALUE(`car`, '$.safety_inspected' RETURNING UNSIGNED) = 0" - ).fetch1("name") == "business", "Has `safety_inspected` set to `true`" - - assert (Team & "`car`->>'$.headlights[0].hyper_white' = 'null'").fetch1( - "name" - ) == "engineering", "Has 1st `headlight` with `hyper_white` not set to `null`" - - assert (Team & "`car`->>'$.inspected' IS NOT NULL").fetch1( - "name" - ) == "engineering", "Missing `inspected` key" - - assert (Team & "`car`->>'$.tire_pressure' = '[34, 30, 27, 32]'").fetch1( - "name" - ) == "business", "`tire_pressure` array did not match" - - assert ( - Team - & """`car`->>'$.headlights[1]' = '{"side": "right", "hyper_white": true}'""" - ).fetch1("name") == "business", "2nd `headlight` object did not match" - - def test_proj(): - # proj necessary since we need to rename indexed value into a proper attribute name - assert Team.proj(car_length="car.length").fetch( - as_dict=True, order_by="car_length" - ) == [ - {"name": "marketing", "car_length": None}, - {"name": "business", "car_length": "100"}, - {"name": "engineering", "car_length": "20.5"}, - ] - - assert Team.proj(car_length="car.length:decimal(4, 1)").fetch( - as_dict=True, order_by="car_length" - ) == [ - {"name": "marketing", "car_length": None}, - {"name": "engineering", "car_length": 20.5}, - {"name": "business", "car_length": 100.0}, - ] - - assert Team.proj( - car_width="JSON_VALUE(`car`, '$.length' RETURNING float) - 15" - ).fetch(as_dict=True, order_by="car_width") == [ - {"name": "marketing", "car_width": None}, - {"name": "engineering", "car_width": 5.5}, - {"name": "business", "car_width": 85.0}, - ] - - assert ( - (Team & {"name": "engineering"}).proj(car_tire_pressure="car.tire_pressure") - ).fetch1("car_tire_pressure") == "[32, 31, 33, 34]" - - assert np.array_equal( - Team.proj(car_inspected="car.inspected").fetch( - "car_inspected", order_by="name" - ), - np.array([None, "true", None]), - ) - - assert np.array_equal( - Team.proj(car_inspected="car.inspected:unsigned").fetch( - "car_inspected", order_by="name" - ), - np.array([None, 1, None]), - ) diff --git a/tests_old/test_log.py b/tests_old/test_log.py deleted file mode 100644 index 86a48bc37..000000000 --- a/tests_old/test_log.py +++ /dev/null @@ -1,9 +0,0 @@ -from nose.tools import assert_true -from . import schema - - -def test_log(): - ts, events = (schema.schema.log & 'event like "Declared%%"').fetch( - "timestamp", "event" - ) - assert_true(len(ts) >= 2) diff --git a/tests_old/test_nan.py b/tests_old/test_nan.py deleted file mode 100644 index b06848fdf..000000000 --- a/tests_old/test_nan.py +++ /dev/null @@ -1,45 +0,0 @@ -import numpy as np -from nose.tools import assert_true -import datajoint as dj -from . import PREFIX, CONN_INFO - -schema = dj.Schema(PREFIX + "_nantest", locals(), connection=dj.conn(**CONN_INFO)) - - -@schema -class NanTest(dj.Manual): - definition = """ - id :int - --- - value=null :double - """ - - -class TestNaNInsert: - @classmethod - def setup_class(cls): - cls.rel = NanTest() - with dj.config(safemode=False): - cls.rel.delete() - a = np.array([0, 1 / 3, np.nan, np.pi, np.nan]) - cls.rel.insert(((i, value) for i, value in enumerate(a))) - cls.a = a - - def test_insert_nan(self): - """Test fetching of null values""" - b = self.rel.fetch("value", order_by="id") - assert_true( - (np.isnan(self.a) == np.isnan(b)).all(), "incorrect handling of Nans" - ) - assert_true( - np.allclose( - self.a[np.logical_not(np.isnan(self.a))], b[np.logical_not(np.isnan(b))] - ), - "incorrect storage of floats", - ) - - def test_nulls_do_not_affect_primary_keys(self): - """Test against a case that previously caused a bug when skipping existing entries.""" - self.rel.insert( - ((i, value) for i, value in enumerate(self.a)), skip_duplicates=True - ) diff --git a/tests_old/test_relation_u.py b/tests_old/test_relation_u.py deleted file mode 100644 index ff30711b3..000000000 --- a/tests_old/test_relation_u.py +++ /dev/null @@ -1,88 +0,0 @@ -from nose.tools import assert_equal, assert_true, raises, assert_list_equal -from . import schema, schema_simple -import datajoint as dj - - -class TestU: - """ - Test tables: insert, delete - """ - - @classmethod - def setup_class(cls): - cls.user = schema.User() - cls.language = schema.Language() - cls.subject = schema.Subject() - cls.experiment = schema.Experiment() - cls.trial = schema.Trial() - cls.ephys = schema.Ephys() - cls.channel = schema.Ephys.Channel() - cls.img = schema.Image() - cls.trash = schema.UberTrash() - - def test_restriction(self): - language_set = {s[1] for s in self.language.contents} - rel = dj.U("language") & self.language - assert_list_equal(rel.heading.names, ["language"]) - assert_true(len(rel) == len(language_set)) - assert_true(set(rel.fetch("language")) == language_set) - # Test for issue #342 - rel = self.trial * dj.U("start_time") - assert_list_equal(rel.primary_key, self.trial.primary_key + ["start_time"]) - assert_list_equal(rel.primary_key, (rel & "trial_id>3").primary_key) - assert_list_equal((dj.U("start_time") & self.trial).primary_key, ["start_time"]) - - @staticmethod - @raises(dj.DataJointError) - def test_invalid_restriction(): - result = dj.U("color") & dict(color="red") - - def test_ineffective_restriction(self): - rel = self.language & dj.U("language") - assert_true(rel.make_sql() == self.language.make_sql()) - - def test_join(self): - rel = self.experiment * dj.U("experiment_date") - assert_equal(self.experiment.primary_key, ["subject_id", "experiment_id"]) - assert_equal(rel.primary_key, self.experiment.primary_key + ["experiment_date"]) - - rel = dj.U("experiment_date") * self.experiment - assert_equal(self.experiment.primary_key, ["subject_id", "experiment_id"]) - assert_equal(rel.primary_key, self.experiment.primary_key + ["experiment_date"]) - - @staticmethod - @raises(dj.DataJointError) - def test_invalid_join(): - rel = dj.U("language") * dict(language="English") - - def test_repr_without_attrs(self): - """test dj.U() display""" - query = dj.U().aggr(schema.Language, n="count(*)") - repr(query) - - def test_aggregations(self): - lang = schema.Language() - # test total aggregation on expression object - n1 = dj.U().aggr(lang, n="count(*)").fetch1("n") - assert_equal(n1, len(lang.fetch())) - # test total aggregation on expression class - n2 = dj.U().aggr(schema.Language, n="count(*)").fetch1("n") - assert_equal(n1, n2) - rel = dj.U("language").aggr(schema.Language, number_of_speakers="count(*)") - assert_equal(len(rel), len(set(l[1] for l in schema.Language.contents))) - assert_equal((rel & 'language="English"').fetch1("number_of_speakers"), 3) - - def test_argmax(self): - rel = schema.TTest() - # get the tuples corresponding to maximum value - mx = (rel * dj.U().aggr(rel, mx="max(value)")) & "mx=value" - assert_equal(mx.fetch("value")[0], max(rel.fetch("value"))) - - def test_aggr(self): - rel = schema_simple.ArgmaxTest() - amax1 = (dj.U("val") * rel) & dj.U("secondary_key").aggr(rel, val="min(val)") - amax2 = (dj.U("val") * rel) * dj.U("secondary_key").aggr(rel, val="min(val)") - assert_true( - len(amax1) == len(amax2) == rel.n, - "Aggregated argmax with join and restriction does not yield same length.", - ) diff --git a/tests_old/test_schema_keywords.py b/tests_old/test_schema_keywords.py deleted file mode 100644 index 49f380f57..000000000 --- a/tests_old/test_schema_keywords.py +++ /dev/null @@ -1,46 +0,0 @@ -from . import PREFIX, CONN_INFO -import datajoint as dj -from nose.tools import assert_true - - -schema = dj.Schema(PREFIX + "_keywords", connection=dj.conn(**CONN_INFO)) - - -@schema -class A(dj.Manual): - definition = """ - a_id: int # a id - """ - - -class B(dj.Manual): - source = None - definition = """ - -> self.source - b_id: int # b id - """ - - class H(dj.Part): - definition = """ - -> master - name: varchar(128) # name - """ - - class C(dj.Part): - definition = """ - -> master - -> master.H - """ - - -@schema -class D(B): - source = A - - -def test_inherited_part_table(): - assert_true("a_id" in D().heading.attributes) - assert_true("b_id" in D().heading.attributes) - assert_true("a_id" in D.C().heading.attributes) - assert_true("b_id" in D.C().heading.attributes) - assert_true("name" in D.C().heading.attributes) diff --git a/tests_old/test_utils.py b/tests_old/test_utils.py deleted file mode 100644 index b5ed96af3..000000000 --- a/tests_old/test_utils.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -Collection of test cases to test core module. -""" -from nose.tools import assert_true, assert_raises, assert_equal -from datajoint import DataJointError -from datajoint.utils import from_camel_case, to_camel_case - - -def setup(): - pass - - -def teardown(): - pass - - -def test_from_camel_case(): - assert_equal(from_camel_case("AllGroups"), "all_groups") - with assert_raises(DataJointError): - from_camel_case("repNames") - with assert_raises(DataJointError): - from_camel_case("10_all") - with assert_raises(DataJointError): - from_camel_case("hello world") - with assert_raises(DataJointError): - from_camel_case("#baisc_names") - - -def test_to_camel_case(): - assert_equal(to_camel_case("all_groups"), "AllGroups") - assert_equal(to_camel_case("hello"), "Hello") - assert_equal(to_camel_case("this_is_a_sample_case"), "ThisIsASampleCase") - assert_equal(to_camel_case("This_is_Mixed"), "ThisIsMixed") diff --git a/tests_old/test_virtual_module.py b/tests_old/test_virtual_module.py deleted file mode 100644 index 58180916f..000000000 --- a/tests_old/test_virtual_module.py +++ /dev/null @@ -1,12 +0,0 @@ -from nose.tools import assert_true -import datajoint as dj -from datajoint.user_tables import UserTable -from . import schema -from . import CONN_INFO - - -def test_virtual_module(): - module = dj.VirtualModule( - "module", schema.schema.database, connection=dj.conn(**CONN_INFO) - ) - assert_true(issubclass(module.Experiment, UserTable))