From 4f4e281030ae1433a98f41a41142171bb2337a2e Mon Sep 17 00:00:00 2001 From: Alec Li Date: Mon, 26 Dec 2022 16:08:05 -0800 Subject: [PATCH] Migrate to poetry, update dependencies, add pre-commit Migrate to poetry for dependency management Setup pre-commit hooks Fix django settings in upgrading to 4.x Add __init__ files as necessary to keep module structure Remove old test cases, reformat edited files --- .github/workflows/cypress.yml | 12 +- .github/workflows/django.yml | 27 +- .github/workflows/nodejs.yml | 4 +- .github/workflows/pre-commit.yml | 38 + .gitignore | 3 + .pre-commit-config.yaml | 47 + README.md | 100 +- csm_web/csm_web/admin.py | 22 +- csm_web/csm_web/settings.py | 104 +- .../migrations/0009_course_section_start.py | 15 +- csm_web/scheduler/serializers.py | 242 ++- csm_web/scheduler/tests/models/__init__.py | 0 .../scheduler/tests/models/test_attendance.py | 49 +- .../scheduler/tests/models/test_spacetime.py | 112 +- .../tests/models/test_word_of_the_day.py | 3 +- csm_web/scheduler/tests/old_test_api.py | 220 -- .../scheduler/tests/old_test_enroll_drop.py | 208 -- .../scheduler/tests/old_test_management.py | 43 - csm_web/scheduler/tests/old_test_models.py | 133 -- csm_web/scheduler/tests/utils.py | 70 - csm_web/scheduler/tests/utils/__init__.py | 0 csm_web/scheduler/utils/__init__.py | 0 poetry.lock | 1892 +++++++++++++++++ poetry.toml | 3 + pyproject.toml | 78 + requirements.txt | 99 +- setup.sh | 71 +- 27 files changed, 2577 insertions(+), 1018 deletions(-) create mode 100644 .github/workflows/pre-commit.yml create mode 100644 .pre-commit-config.yaml create mode 100644 csm_web/scheduler/tests/models/__init__.py delete mode 100644 csm_web/scheduler/tests/old_test_api.py delete mode 100644 csm_web/scheduler/tests/old_test_enroll_drop.py delete mode 100644 csm_web/scheduler/tests/old_test_management.py delete mode 100644 csm_web/scheduler/tests/old_test_models.py delete mode 100644 csm_web/scheduler/tests/utils.py create mode 100644 csm_web/scheduler/tests/utils/__init__.py create mode 100644 csm_web/scheduler/utils/__init__.py create mode 100644 poetry.lock create mode 100644 poetry.toml create mode 100644 pyproject.toml diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index 5aad5389..2dfbe059 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -2,9 +2,9 @@ name: Cypress E2E on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] env: PYTHON_VERSION: 3.9 @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - containers: [ 1, 2, 3, 4 ] + containers: [1, 2, 3, 4] services: postgres: image: postgres:14 @@ -23,7 +23,7 @@ jobs: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: csm_web_dev - ports: [ "5432:5432" ] + ports: ["5432:5432"] options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout @@ -71,7 +71,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - containers: [ 1, 2, 3, 4 ] + containers: [1, 2, 3, 4] services: postgres: image: postgres:14 @@ -79,7 +79,7 @@ jobs: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: csm_web_dev - ports: [ "5432:5432" ] + ports: ["5432:5432"] options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 6ca51d24..72658dfb 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -2,9 +2,9 @@ name: Django CI on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] env: PYTHON_VERSION: 3.9 @@ -13,6 +13,7 @@ jobs: pytest: name: "pytest" runs-on: ubuntu-latest + services: postgres: image: postgres:14 @@ -20,19 +21,27 @@ jobs: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: csm_web_dev - ports: [ "5432:5432" ] + ports: ["5432:5432"] options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v2 - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v2 + - name: Parse Python version from runtime.txt + run: | + sed 's/[^0-9.]//g' runtime.txt | head > .python-version + - name: Set up Python + uses: actions/setup-python@v4 + - name: Install Poetry + uses: snok/install-poetry@v1 with: - python-version: ${{ env.PYTHON_VERSION }} - - name: Install Dependencies + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + - name: Install dependencies with Poetry run: | - python -m pip install --upgrade pip - pip install -r requirements.txt + poetry install --no-root --with=dev + # add virtual environment to path for future steps + echo .venv/bin >> $GITHUB_PATH - name: Run Tests env: SECRET_KEY: ${{ secrets.SECRET_KEY }} diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index a4a642eb..6503ce49 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -5,9 +5,9 @@ name: Node.js CI on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] env: NODE_VERSION: 16.x diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..97b0efd0 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,38 @@ +name: pre-commit + +on: + pull_request: + branches: [master] + push: + branches: [master] + +jobs: + pre-commit: + name: "pre-commit" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v36.0.9 + - name: Parse Python version from runtime.txt + run: | + sed 's/[^0-9.]//g' runtime.txt | head > .python-version + - name: Set up Python + uses: actions/setup-python@v4 + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + - name: Install dependencies with Poetry + run: | + poetry install --no-root --with=dev --no-interaction --no-ansi + # add virtual environment to path for future steps + echo $(poetry env info --path)/bin >> $GITHUB_PATH + - name: Run pre-commit hooks + uses: pre-commit/action@v3.0.0 + with: + # only check changed files + extra_args: "--files ${{ steps.changed-files.outputs.all_changed_files }}" diff --git a/.gitignore b/.gitignore index 346219a6..41b9cef8 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,9 @@ ipython_config.py # pyenv .python-version +# asdf +.tool-versions + # celery beat schedule file celerybeat-schedule diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..76fcf2bc --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,47 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-merge-conflict + - id: detect-private-key + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + # python + - repo: https://github.com/psf/black + rev: 22.12.0 + hooks: + - id: black + - repo: https://github.com/PyCQA/isort + rev: 5.11.4 + hooks: + - id: isort + - repo: local + hooks: + - id: pylint + name: pylint + entry: pylint + language: system + types: [python] + args: [ + "-rn" # only display messages + ] + verbose: true # always display output + # dependencies + - repo: https://github.com/python-poetry/poetry + rev: 1.5.0 + hooks: + - id: poetry-check + - id: poetry-export + args: ["-f", "requirements.txt", "-o", "requirements.txt", "--without-hashes", "--with", "prod"] + # js + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.0.0-alpha.4 + hooks: + - id: prettier + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v8.30.0 + hooks: + - id: eslint + exclude: 'webpack\.config\.js' + verbose: true # always display output diff --git a/README.md b/README.md index d43b84d1..45692c8f 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,92 @@ # CSM Web Applications + This repository contains webapps that help support the infrastructure of CSM. Currently, it only holds Scheduler (our section signup and attendance system), but more are on the way. If you're unfamiliar with CSM and/or its web applications, check out [this repository's releases](https://github.com/csmberkeley/csm_web/releases) for a peek at what our web applications look like and what we've been working on lately. ## Installing + ### Requirements + We don't know what specific minimum version you would need for any of the following software, but the most recent version of any of the below should work. -* Python 3.8 - * [virtualenv](https://packaging.python.org/guides/installing-using-pip-and-virtualenv/) (`pip install virtualenv`) - * [autopep8](https://pypi.org/project/autopep8/) (`pip install autopep8`) -* [npm](https://www.npmjs.com/get-npm) - * [prettier](https://prettier.io/) (`npm install -g prettier`) - * [eslint](https://eslint.org/) (`npm install -g eslint`; `npm install -g eslint-plugin-react`) -* Postgresql (`brew install postgresql && brew services start postgresql`) -* [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli#download-and-install) (`brew tap heroku/brew && brew install heroku`) - * Create an account on [Heroku](https://id.heroku.com/login) and [login](https://devcenter.heroku.com/articles/heroku-cli#getting-started) -* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) (`brew install awscli`) - * We use an S3 bucket to store course resources. See [here](https://aws.amazon.com/s3/) to get started. -* Log in to AWS CLI (`aws configure `) This will prompt an interactive session to enter login credentials. - * AWS Access Key ID: (ask tech chair) - * AWS Secret Access Key: (ask tech chair) - * Default region name: us-east-1 - * Default output format: json +- Python 3.9.13 + - It is recommended that you use [`pyenv`](https://github.com/pyenv/pyenv) to manage python versions, so that you can use a consistent python version for `csm_web`, and another python version for your other projects. +- [`poetry`](https://python-poetry.org/docs/#installation) +- `npm` + - It is recommended that you use [`nvm`](https://github.com/nvm-sh/nvm) to manage node/npm versions, so that you can use a consistent node/npm version for `csm_web`, and another verison for your other projects. +- [PostgreSQL](https://www.postgresql.org/download/) +- [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli#download-and-install) + - Create an account on [Heroku](https://id.heroku.com/login) and [login](https://devcenter.heroku.com/articles/heroku-cli#getting-started) +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) + - We use an S3 bucket to store course resources. See [here](https://aws.amazon.com/s3/) to get started. +- Log in to AWS CLI (`aws configure`) This will prompt an interactive session to enter login credentials. + - AWS Access Key ID: (ask tech chair) + - AWS Secret Access Key: (ask tech chair) + - Default region name: `us-east-1` + - Default output format: `json` Other miscellaneous requirements will be installed by the commands below. ### Setup -To ensure package version consistency and avoid polluting your global package installations, we highly recommend running the server with [virtualenv](https://virtualenv.pypa.io/en/stable/) (`pip install virtualenv`). -From a terminal in the top level of the project directory, run `python3 -m virtualenv venv`. +To ensure package version consistency and avoid polluting your global package installations, we highly recommend running the server with a virtual environment. Python's standard library includes [venv](https://docs.python.org/3/library/venv.html), so you do not need to install anything new here. + +Firstly, make sure you have the right python version (see `runtime.txt` for the expected python version to install). If you're using `pyenv` to manage python versions (this is recommended), you can install the specified python version with `pyenv install `. + +From a terminal in the top level of the project directory, run `python3 -m venv venv`; if your system python version is different from the version required here, and you're using `pyenv`, run `PYENV_VERSION= python3 -m venv venv` instead (for example, `PYENV_VERSION=3.9.13 python3 -m venv venv`). This will initialize a new virtual environment in the `venv/` folder, with the correct base python version. To activate the environment, run `source venv/bin/activate`. You will need to run this command every time you open a new terminal. -Finally, run `./setup.sh`. This will install additional requirements needed by the server, and set up some necessary environment variables. +Finally, run `./setup.sh`. This will install additional requirements needed by the server, and set up some necessary environment variables. You should _not_ be running this script after it has succeeded and set up the environment for the first time. ## Running -To start the Django server, run `python csm_web/manage.py runserver` and visit `localhost:8000` in your browser. -Run `python csm_web/manage.py createtestdata` to generate some test data. If you ran `./setup.sh`, +To start the Django server, run `python3 csm_web/manage.py runserver` and visit `localhost:8000` in your browser. + +Run `python3 csm_web/manage.py createtestdata` to generate some test data. If you ran `./setup.sh`, this was done for you. -*If you are working on the frontend*: +_If you are working on the frontend_: Run `npm run watch`, which will automatically rebuild the JS bundle if any changes to the frontend JS are detected. Alternatively you can run `npm run dev` manually each time you make changes to the frontend. ## Troubleshooting + ### `setup.sh` Errors -* The following errors are likely caused by some quirks in our build system - if you set up a new virtual environment through normal commands, you may run into them. The solution for all of these should be to run `setup.sh` (you should be able to do this even after attempting to run pip/npm commands already). - * `django.core.exceptions.ImproperlyConfigured: The SECRET_KEY setting must not be empty.` - * When installing `psycopg2`, console output displays `ld: library not found for -lpq` or similar -* During `./setup.sh` or `pip3 install` I'm getting a `psycopg2` install error that looks like this: + +- The following errors are likely caused by some quirks in our build system - if you set up a new virtual environment through normal commands, you may run into them. The solution for all of these should be to run `setup.sh` (you should be able to do this even after attempting to run pip/npm commands already). + - `django.core.exceptions.ImproperlyConfigured: The SECRET_KEY setting must not be empty.` + - When installing `psycopg2`, console output displays `ld: library not found for -lpq` or similar +- During `./setup.sh` or `pip3 install` I'm getting a `psycopg2` install error that looks like this: + ``` Error: pg_config executable not found. - + pg_config is required to build psycopg2 from source. Please add the directory containing pg_config to the $PATH or specify the full executable path with the option: ``` - * You need to install Postgres locally (see [this](https://stackoverflow.com/a/12037133) SO post) -* During `./setup.sh`, this happens after `pip3 install` runs + +- You need to install Postgres locally (see [this](https://stackoverflow.com/a/12037133) SO post) +- During `./setup.sh`, this happens after `pip3 install` runs + ``` ./setup.sh:41: no such file or directory: /Users/jhshi/Documents/csm/csm_web/venv/.project_dir ./setup.sh:44: no such file or directory: /Users/jhshi/Documents/csm/csm_web/venv/bin/activate ./setup.sh:47: no such file or directory: /Users/jhshi/Documents/csm/csm_web/venv/bin/activate ``` - * This likely happens because you moved your folder after creating your virtualenv. Removing - and recreating your virtualenv should fix it. + +- This likely happens because you moved your folder after creating your virtualenv. Removing + and recreating your virtualenv should fix it. ### How do I access the `/admin` page? - * The `./setup.sh` script will create a user with username `demo_user` and password `pass`. You can access it by signing in through the admin page. - * If you wish to assign admin permissions to an account that uses OAuth (such as your Berkeley email), run the following commands in the Django shell (accessible by running `python3 csm_web/manage.py shell`): -``` + +- The `./setup.sh` script will create a user with username `demo_user` and password `pass`. You can access it by signing in through the admin page. +- If you wish to assign admin permissions to an account that uses OAuth (such as your Berkeley email), run the following commands in the Django shell (accessible by running `python3 csm_web/manage.py shell`): + +```py from scheduler.models import * # replace "my_username" with the prefix of your Berkeley email, as in "my_username@berkeley.edu" user = User.objects.get(username="my_username") @@ -82,25 +96,31 @@ user.save() ``` ### Miscellaneous + #### OSX: error on running `pip` + Try replacing `pip` with `pip3` instead. #### OAuth errors when trying to sign in locally + ``` Error 401: invalid_client The OAuth client was not found. ``` + OAuth secrets are sourced from the heroku repository: you'll need to log in to heroku, add the appropriate remote, rerun `setup.sh`, and then reactivate your virtualenv. ### On `runserver`, long stack trace ending with this: + ``` -django.db.utils.OperationalError: could not connect to server: Connection refused - Is the server running on host "localhost" (::1) and accepting - TCP/IP connections on port 5432? -could not connect to server: Connection refused - Is the server running on host "localhost" (127.0.0.1) and accepting - TCP/IP connections on port 5432? +django.db.utils.OperationalError: could not connect to server: Connection refused + Is the server running on host "localhost" (::1) and accepting + TCP/IP connections on port 5432? +could not connect to server: Connection refused + Is the server running on host "localhost" (127.0.0.1) and accepting + TCP/IP connections on port 5432? ``` + Your postgres server is likely not running. On a mac (which is the only platform we've done local testing on), run `brew services start postgres` before invoking `runserver` again. diff --git a/csm_web/csm_web/admin.py b/csm_web/csm_web/admin.py index dfe643f0..050daca9 100644 --- a/csm_web/csm_web/admin.py +++ b/csm_web/csm_web/admin.py @@ -1,25 +1,27 @@ -from django.contrib.admin import AdminSite as DefaultAdminSite -from django.views.decorators.cache import never_cache from django.conf import settings +from django.contrib.admin import AdminSite as DefaultAdminSite from django.shortcuts import redirect +from django.utils.decorators import method_decorator +from django.views.decorators.cache import never_cache class AdminSite(DefaultAdminSite): - site_header = f"{'Development ' if settings.DJANGO_ENV != settings.PRODUCTION else ''}Scheduler Admin" + site_header = ( + f"{'Development ' if settings.DJANGO_ENV != settings.PRODUCTION else ''}Scheduler" + " Admin" + ) site_title = "CSM Scheduler Admin" index_title = "" - @never_cache + @method_decorator(never_cache) def login(self, request, extra_context=None): if settings.DJANGO_ENV not in (settings.DEVELOPMENT, settings.STAGING): - """ - Disable Django's built-in admin login page outside of development - because it's a security liability - """ - return redirect('/') + # Disable Django's built-in admin login page outside of development + # because it's a security liability + return redirect("/") return super().login(request, extra_context) def each_context(self, request): context = super().each_context(request) - context['DJANGO_ENV'] = settings.DJANGO_ENV + context["DJANGO_ENV"] = settings.DJANGO_ENV return context diff --git a/csm_web/csm_web/settings.py b/csm_web/csm_web/settings.py index e9355659..ae6a1dc0 100644 --- a/csm_web/csm_web/settings.py +++ b/csm_web/csm_web/settings.py @@ -11,9 +11,10 @@ """ import os + +import sentry_sdk from factory.django import DjangoModelFactory from rest_framework.serializers import ModelSerializer, Serializer -import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration # Analogous to RAILS_ENV, is one of {prod, staging, dev}. Defaults to dev. This default can @@ -33,8 +34,12 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = DJANGO_ENV == DEVELOPMENT -if DJANGO_ENV == PRODUCTION or DJANGO_ENV == STAGING: - sentry_sdk.init(dsn=os.environ.get('SENTRY_DSN'), integrations=[DjangoIntegration()], send_default_pii=True) +if DJANGO_ENV in (PRODUCTION, STAGING): + sentry_sdk.init( + dsn=os.environ.get("SENTRY_DSN"), + integrations=[DjangoIntegration()], + send_default_pii=True, + ) ALLOWED_HOSTS = [] @@ -44,7 +49,7 @@ ("Alec Li", "alec.li@berkeley.edu"), ("Noor Mahini", "nmahini@berkeley.edu"), ("Rohan D'Souza", "rohan.ds1001@berkeley.edu"), - ("Naveen Gopalan", "ngopalan@berkeley.edu") + ("Naveen Gopalan", "ngopalan@berkeley.edu"), ] # Application definition @@ -74,7 +79,7 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "django.middleware.gzip.GZipMiddleware" + "django.middleware.gzip.GZipMiddleware", ] ROOT_URLCONF = "csm_web.urls" @@ -82,7 +87,7 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [os.path.join(BASE_DIR, 'templates/')], + "DIRS": [os.path.join(BASE_DIR, "templates/")], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -99,22 +104,22 @@ WSGI_APPLICATION = "csm_web.wsgi.application" -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" POSTGRES_EXTRA_AUTO_EXTENSION_SET_UP = False -POSTGRES_EXTRA_DB_BACKEND_BASE = 'django.db.backends.postgresql' +POSTGRES_EXTRA_DB_BACKEND_BASE = "django.db.backends.postgresql" # Database # https://docs.djangoproject.com/en/2.1/ref/settings/#databases if DEBUG: DATABASES = { - 'default': { - 'ENGINE': 'psqlextra.backend', - 'NAME': 'csm_web_dev', - 'USER': 'postgres', - 'PASSWORD': os.environ.get('POSTGRES_PASSWORD', ''), - 'HOST': 'localhost', - 'PORT': '5432', + "default": { + "ENGINE": "psqlextra.backend", + "NAME": "csm_web_dev", + "USER": "postgres", + "PASSWORD": os.environ.get("POSTGRES_PASSWORD", ""), + "HOST": "localhost", + "PORT": "5432", } } else: @@ -135,7 +140,9 @@ AUTH_PASSWORD_VALIDATORS = [ { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" + "NAME": ( + "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" + ) }, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, @@ -154,21 +161,19 @@ USE_I18N = True -USE_L10N = True - USE_TZ = True # AWS environment variables for S3 resource storage AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID") AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY") AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_STORAGE_BUCKET_NAME") -AWS_S3_SIGNATURE_VERSION = 's3v4' +AWS_S3_SIGNATURE_VERSION = "s3v4" AWS_S3_REGION_NAME = os.environ.get("AWS_S3_REGION_NAME") AWS_S3_FILE_OVERWRITE = False AWS_DEFAULT_ACL = None AWS_S3_VERIFY = True AWS_QUERYSTRING_AUTH = False # public bucket -DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' +DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.1/howto/static-files/ @@ -223,12 +228,10 @@ "DEFAULT_RENDERER_CLASSES": [ "djangorestframework_camel_case.render.CamelCaseJSONRenderer" ], - "DEFAULT_PERMISSION_CLASSES": [ - 'rest_framework.permissions.IsAuthenticated' - ], + "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"], "DEFAULT_PARSER_CLASSES": [ - 'djangorestframework_camel_case.parser.CamelCaseJSONParser', - ] + "djangorestframework_camel_case.parser.CamelCaseJSONParser", + ], } if DJANGO_ENV == DEVELOPMENT: @@ -238,36 +241,36 @@ # Cache CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', - 'LOCATION': 'db_cache_table', + "default": { + "BACKEND": "django.core.cache.backends.db.DatabaseCache", + "LOCATION": "db_cache_table", } } # Logging LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": { + "class": "logging.StreamHandler", }, }, - 'loggers': { - 'django': { - 'handlers': ['console'], - 'level': 'INFO', + "loggers": { + "django": { + "handlers": ["console"], + "level": "INFO", }, - 'scheduler.views': { - 'handlers': ['console'], - 'level': 'INFO', - 'propogate': True, + "scheduler.views": { + "handlers": ["console"], + "level": "INFO", + "propogate": True, + }, + "scheduler.models": { + "handlers": ["console"], + "level": "INFO", + "propogate": True, }, - 'scheduler.models': { - 'handlers': ['console'], - 'level': 'INFO', - 'propogate': True, - } }, } @@ -284,9 +287,12 @@ # Content Security Policy MIDDLEWARE.append("csp.middleware.CSPMiddleware") - CSP_DEFAULT_SRC = ("'none'", ) - CSP_SCRIPT_SRC = ("'self'", "https://unpkg.com/react@18/umd/", - "https://unpkg.com/react-dom@18/umd/") + CSP_DEFAULT_SRC = ("'none'",) + CSP_SCRIPT_SRC = ( + "'self'", + "https://unpkg.com/react@18/umd/", + "https://unpkg.com/react-dom@18/umd/", + ) CSP_STYLE_SRC = ("'self'", "https://fonts.googleapis.com") CSP_CONNECT_SRC = ("'self'",) CSP_IMG_SRC = ("'self'", "data:") diff --git a/csm_web/scheduler/migrations/0009_course_section_start.py b/csm_web/scheduler/migrations/0009_course_section_start.py index 3e526264..0368ae29 100644 --- a/csm_web/scheduler/migrations/0009_course_section_start.py +++ b/csm_web/scheduler/migrations/0009_course_section_start.py @@ -1,21 +1,24 @@ # Generated by Django 3.0.3 on 2020-02-20 01:40 import datetime + from django.db import migrations, models -from django.utils.timezone import utc class Migration(migrations.Migration): - dependencies = [ - ('scheduler', '0008_auto_20200112_2051'), + ("scheduler", "0008_auto_20200112_2051"), ] operations = [ migrations.AddField( - model_name='course', - name='section_start', - field=models.DateField(default=datetime.datetime(2020, 2, 20, 1, 40, 43, 642165, tzinfo=utc)), + model_name="course", + name="section_start", + field=models.DateField( + default=datetime.datetime( + 2020, 2, 20, 1, 40, 43, 642165, tzinfo=datetime.timezone.utc + ) + ), preserve_default=False, ), ] diff --git a/csm_web/scheduler/serializers.py b/csm_web/scheduler/serializers.py index bcb1636e..f0a20e78 100644 --- a/csm_web/scheduler/serializers.py +++ b/csm_web/scheduler/serializers.py @@ -1,7 +1,27 @@ -from rest_framework import serializers from enum import Enum + from django.utils import timezone -from .models import Attendance, Course, Link, SectionOccurrence, User, Student, Section, Mentor, Override, Spacetime, Coordinator, DayOfWeekField, Resource, Worksheet, Matcher, MatcherSlot, MatcherPreference +from rest_framework import serializers + +from .models import ( + Attendance, + Coordinator, + Course, + DayOfWeekField, + Link, + Matcher, + MatcherPreference, + MatcherSlot, + Mentor, + Override, + Resource, + Section, + SectionOccurrence, + Spacetime, + Student, + User, + Worksheet, +) class Role(Enum): @@ -11,21 +31,24 @@ class Role(Enum): def get_profile_role(profile): + """Return role (enum) depending on the profile type""" for role, klass in zip(Role, (Coordinator, Student, Mentor)): if isinstance(profile, klass): return role.value + return None def make_omittable(field_class, omit_key, *args, predicate=None, **kwargs): """ Behaves exactly as if the field were defined directly by calling `field_class(*args, **kwargs)`, - except that if `omit_key` is present in the context when the field is serialized and predicate returns True, - the value is omitted and `None` is returned instead. + except that if `omit_key` is present in the context when the field is serialized and predicate + returns True, the value is omitted and `None` is returned instead. Useful for when you want to leave out one or two fields in one view, while including them in - another view, without having to go through the trouble of writing two completely separate serializers. - This is a marked improvement over using a `SerializerMethodField` because this approach still allows - writing to the field to work without any additional machinery. + another view, without having to go through the trouble of writing two completely separate + serializers. + This is a marked improvement over using a `SerializerMethodField` because this approach + still allows writing to the field to work without any additional machinery. """ predicate_provided = predicate is not None predicate = predicate or (lambda _: True) @@ -33,14 +56,23 @@ def make_omittable(field_class, omit_key, *args, predicate=None, **kwargs): class OmittableField(field_class): def get_attribute(self, instance): """ - This is an important performance optimization that prevents us from hitting the DB for an - unconditionally omitted field, as by the time to_representation is called, the DB has already been queried - (because `value` has to come from *somewhere*). + This is an important performance optimization that prevents us from hitting the DB + for an unconditionally omitted field, as by the time to_representation is called, + the DB has already been queried (because `value` has to come from *somewhere*). """ - return None if self.context.get(omit_key) and not predicate_provided else super().get_attribute(instance) + return ( + None + if self.context.get(omit_key) and not predicate_provided + else super().get_attribute(instance) + ) def to_representation(self, value): - return None if self.context.get(omit_key) and predicate(value) else super().to_representation(value) + """Override to return None if key is omitted""" + return ( + None + if self.context.get(omit_key) and predicate(value) + else super().to_representation(value) + ) return OmittableField(*args, **kwargs) @@ -50,8 +82,11 @@ class OverrideReadOnlySerializer(serializers.ModelSerializer): date = serializers.DateField(format="%b. %-d") def get_spacetime(self, obj): + """Retrieve the serialized spacetime object associated with the override""" # Gets around cyclic dependency issue - return SpacetimeSerializer(obj.spacetime, context={**self.context, 'omit_overrides': True}).data + return SpacetimeSerializer( + obj.spacetime, context={**self.context, "omit_overrides": True} + ).data class Meta: model = Override @@ -60,20 +95,38 @@ class Meta: class SpacetimeSerializer(serializers.ModelSerializer): - time = serializers.SerializerMethodField() - location = make_omittable(serializers.CharField, 'omit_spacetime_links', - predicate=lambda location: location.startswith('http')) - override = make_omittable(OverrideReadOnlySerializer, 'omit_overrides', read_only=True) + location = make_omittable( + serializers.CharField, + "omit_spacetime_links", + predicate=lambda location: location.startswith("http"), + ) + override = make_omittable( + OverrideReadOnlySerializer, "omit_overrides", read_only=True + ) def get_time(self, obj): + """Format the spacetime""" + formatted_end_time = obj.end_time.strftime("%-I:%M %p") if obj.start_time.strftime("%p") != obj.end_time.strftime("%p"): - return f"{obj.day_of_week} {obj.start_time.strftime('%-I:%M %p')}-{obj.end_time.strftime('%-I:%M %p')}" - return f"{obj.day_of_week} {obj.start_time.strftime('%-I:%M')}-{obj.end_time.strftime('%-I:%M %p')}" + return ( + f"{obj.day_of_week} {obj.start_time.strftime('%-I:%M %p')}-{formatted_end_time}" + ) + return ( + f"{obj.day_of_week} {obj.start_time.strftime('%-I:%M')}-{formatted_end_time}" + ) class Meta: model = Spacetime - fields = ("start_time", "day_of_week", "time", "location", "id", "duration", "override") + fields = ( + "start_time", + "day_of_week", + "time", + "location", + "id", + "duration", + "override", + ) read_only_fields = ("time", "id", "override") @@ -82,20 +135,34 @@ class CourseSerializer(serializers.ModelSerializer): user_can_enroll = serializers.SerializerMethodField() def get_enrollment_open(self, obj): - user = self.context.get('request') and self.context.get('request').user - if user and user.priority_enrollment and user.priority_enrollment < obj.enrollment_start: + """Compute enrollment open time; takes priority enrollment into account""" + user = self.context.get("request") and self.context.get("request").user + if ( + user + and user.priority_enrollment + and user.priority_enrollment < obj.enrollment_start + ): now = timezone.now().astimezone(timezone.get_default_timezone()) return user.priority_enrollment < now < obj.enrollment_end - else: - return obj.is_open() + + return obj.is_open() def get_user_can_enroll(self, obj): - user = self.context.get('request') and self.context.get('request').user + """Determine whether the user can currently enroll in the course""" + user = self.context.get("request") and self.context.get("request").user return user and user.can_enroll_in_course(obj) class Meta: model = Course - fields = ("id", "name", "enrollment_start", "enrollment_open", "user_can_enroll", "is_restricted", "word_of_the_day_limit") + fields = ( + "id", + "name", + "enrollment_start", + "enrollment_open", + "user_can_enroll", + "is_restricted", + "word_of_the_day_limit", + ) class UserSerializer(serializers.ModelSerializer): @@ -104,29 +171,36 @@ class Meta: fields = ("id", "email", "first_name", "last_name", "priority_enrollment") -class ProfileSerializer(serializers.Serializer): - class VariableSourceCourseField(serializers.Field): - def __init__(self, *args, **kwargs): - self.target = kwargs.pop('target') - super().__init__(self, *args, **kwargs) +class ProfileSerializer(serializers.Serializer): # pylint: disable=abstract-method + class VariableSourceCourseField( + serializers.Field + ): # pylint: disable=abstract-method + def __init__(self, **kwargs): + self.target = kwargs.pop("target") + super().__init__(**kwargs) def to_representation(self, value): return getattr(value.course, self.target) id = serializers.IntegerField() - section_id = serializers.IntegerField(source='section.id', required=False) - section_spacetimes = SpacetimeSerializer(source='section.spacetimes', many=True, required=False) - course = VariableSourceCourseField(source='*', target='name', required=False) - course_title = VariableSourceCourseField(source='*', target='title', required=False) - course_id = VariableSourceCourseField(source='*', target='pk', required=False) + section_id = serializers.IntegerField(source="section.id", required=False) + section_spacetimes = SpacetimeSerializer( + source="section.spacetimes", many=True, required=False + ) + course = VariableSourceCourseField(source="*", target="name", required=False) + course_title = VariableSourceCourseField(source="*", target="title", required=False) + course_id = VariableSourceCourseField(source="*", target="pk", required=False) role = serializers.SerializerMethodField() def get_role(self, obj): + """Retrieve the profile role""" return get_profile_role(obj) class MentorSerializer(serializers.ModelSerializer): - email = make_omittable(serializers.EmailField, 'omit_mentor_emails', source='user.email') + email = make_omittable( + serializers.EmailField, "omit_mentor_emails", source="user.email" + ) class Meta: model = Mentor @@ -170,8 +244,8 @@ def update(self, instance, validated_data): class StudentSerializer(serializers.ModelSerializer): - email = serializers.EmailField(source='user.email') - attendances = AttendanceSerializer(source='attendance_set', many=True) + email = serializers.EmailField(source="user.email") + attendances = AttendanceSerializer(source="attendance_set", many=True) class Meta: model = Student @@ -182,19 +256,25 @@ class SectionSerializer(serializers.ModelSerializer): spacetimes = SpacetimeSerializer(many=True) num_students_enrolled = serializers.SerializerMethodField() mentor = MentorSerializer() - course = serializers.CharField(source='mentor.course.name') - course_title = serializers.CharField(source='mentor.course.title') + course = serializers.CharField(source="mentor.course.name") + course_title = serializers.CharField(source="mentor.course.title") user_role = serializers.SerializerMethodField() associated_profile_id = serializers.SerializerMethodField() - course_restricted = serializers.BooleanField(source='mentor.course.is_restricted') + course_restricted = serializers.BooleanField(source="mentor.course.is_restricted") def get_num_students_enrolled(self, obj): - return obj.num_students_annotation if hasattr(obj, 'num_students_annotation') else obj.current_student_count + """Retrieve the number of students enrolled in the section""" + return ( + obj.num_students_annotation + if hasattr(obj, "num_students_annotation") + else obj.current_student_count + ) def user_associated_profile(self, obj): - user = self.context.get('request') and self.context.get('request').user + """Retrieve the user profile associated with the section""" + user = self.context.get("request") and self.context.get("request").user if not user: - return + return None try: return obj.students.get(user=user) except Student.DoesNotExist: @@ -206,68 +286,89 @@ def user_associated_profile(self, obj): return None # no profile def get_user_role(self, obj): + """Retrieve the role of the associated user profile""" profile = self.user_associated_profile(obj) if not profile: - return + return None return get_profile_role(profile) def get_associated_profile_id(self, obj): + """Retrieve the id of the associated user profile""" profile = self.user_associated_profile(obj) return profile and profile.pk class Meta: model = Section - fields = ("id", "spacetimes", "mentor", "capacity", "associated_profile_id", - "num_students_enrolled", "description", "mentor", "course", "user_role", - "course_title", "course_restricted") + fields = ( + "id", + "spacetimes", + "mentor", + "capacity", + "associated_profile_id", + "num_students_enrolled", + "description", + "mentor", + "course", + "user_role", + "course_title", + "course_restricted", + ) class WorksheetSerializer(serializers.ModelSerializer): class Meta: model = Worksheet - fields = ['id', 'name', 'resource', 'worksheet_file', 'solution_file'] + fields = ["id", "name", "resource", "worksheet_file", "solution_file"] class LinkSerializer(serializers.ModelSerializer): class Meta: model = Link - fields = ['id', 'name', 'resource', 'url'] + fields = ["id", "name", "resource", "url"] class ResourceSerializer(serializers.ModelSerializer): - worksheets = WorksheetSerializer(source='worksheet_set', many=True) - links = LinkSerializer(source='link_set', many=True) + worksheets = WorksheetSerializer(source="worksheet_set", many=True) + links = LinkSerializer(source="link_set", many=True) class Meta: model = Resource - fields = ['id', 'course', 'week_num', 'date', 'topics', 'worksheets', 'links'] + fields = ["id", "course", "week_num", "date", "topics", "worksheets", "links"] class SectionOccurrenceSerializer(serializers.ModelSerializer): - attendances = AttendanceSerializer(source='attendance_set', many=True) + attendances = AttendanceSerializer(source="attendance_set", many=True) class Meta: model = SectionOccurrence - fields = ('id', 'date', 'section', 'attendances') + fields = ("id", "date", "section", "attendances") class OverrideSerializer(serializers.ModelSerializer): - location = serializers.CharField(source='spacetime.location') - start_time = serializers.TimeField(source='spacetime.start_time') + location = serializers.CharField(source="spacetime.location") + start_time = serializers.TimeField(source="spacetime.start_time") date = serializers.DateField() def create(self, validated_data): spacetime = Spacetime.objects.create( - **validated_data['spacetime'], day_of_week=DayOfWeekField.DAYS[validated_data['date'].weekday()], duration=validated_data['overriden_spacetime'].duration) - return Override.objects.create(date=validated_data['date'], overriden_spacetime=validated_data['overriden_spacetime'], - spacetime=spacetime) + **validated_data["spacetime"], + day_of_week=DayOfWeekField.DAYS[validated_data["date"].weekday()], + duration=validated_data["overriden_spacetime"].duration, + ) + return Override.objects.create( + date=validated_data["date"], + overriden_spacetime=validated_data["overriden_spacetime"], + spacetime=spacetime, + ) def update(self, instance, validated_data): - instance.date = validated_data['date'] - spacetime_data = validated_data['spacetime'] - instance.spacetime.day_of_week = DayOfWeekField.DAYS[validated_data['date'].weekday()] - instance.spacetime.location = spacetime_data['location'] - instance.spacetime.start_time = spacetime_data['start_time'] + instance.date = validated_data["date"] + spacetime_data = validated_data["spacetime"] + instance.spacetime.day_of_week = DayOfWeekField.DAYS[ + validated_data["date"].weekday() + ] + instance.spacetime.location = spacetime_data["location"] + instance.spacetime.start_time = spacetime_data["start_time"] instance.spacetime.duration = instance.overriden_spacetime.duration instance.spacetime.save() instance.save() @@ -276,23 +377,22 @@ def update(self, instance, validated_data): class Meta: model = Override fields = ("location", "start_time", "date", "overriden_spacetime") - extra_kwargs = {"overriden_spacetime": {'required': False}} + extra_kwargs = {"overriden_spacetime": {"required": False}} class MatcherSerializer(serializers.ModelSerializer): class Meta: model = Matcher - fields = ('id', 'course', 'assignment', 'is_open') + fields = ("id", "course", "assignment", "is_open") class MatcherSlotSerializer(serializers.ModelSerializer): class Meta: model = MatcherSlot - fields = ['id', 'matcher', 'times'] + fields = ["id", "matcher", "times"] class MatcherPreferenceSerializer(serializers.ModelSerializer): - class Meta: model = MatcherPreference - fields = ['slot', 'mentor', 'preference'] + fields = ["slot", "mentor", "preference"] diff --git a/csm_web/scheduler/tests/models/__init__.py b/csm_web/scheduler/tests/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/csm_web/scheduler/tests/models/test_attendance.py b/csm_web/scheduler/tests/models/test_attendance.py index 2275c315..e6f56913 100644 --- a/csm_web/scheduler/tests/models/test_attendance.py +++ b/csm_web/scheduler/tests/models/test_attendance.py @@ -1,38 +1,27 @@ -import pytest -from freezegun import freeze_time -from unittest import mock -import factory -import faker import datetime -from django.utils import timezone +import pytest from django.urls import reverse -from scheduler.models import ( - User, - Course, - Section, - Mentor, - Student, - Attendance, - SectionOccurrence, -) +from django.utils import timezone +from freezegun import freeze_time from scheduler.factories import ( - UserFactory, CourseFactory, - SectionFactory, MentorFactory, - StudentFactory, - AttendanceFactory, + SectionFactory, SpacetimeFactory, + UserFactory, ) +from scheduler.models import Attendance, SectionOccurrence, Student -import zoneinfo - -DEFAULT_TZ = zoneinfo.ZoneInfo(timezone.get_default_timezone().zone) +DEFAULT_TZ = timezone.get_default_timezone() -@pytest.fixture -def setup_section(db): +# avoid pylint warning redefining name in outer scope +@pytest.fixture(name="setup_section") +def fixture_setup_section(db): # pylint: disable=unused-argument + """ + Set up a mentor, student, and section for attendance testing + """ mentor_user = UserFactory( username="mentor_user", first_name="mentor", last_name="user" ) @@ -115,7 +104,11 @@ def setup_section(db): def test_attendance_add_student_on_day( client, setup_section, day, num_attendances_added ): - mentor, student_user, course, section = setup_section + """ + Check various section occurrence and attendance objects are created + when a student is added on a given day. + """ + _, student_user, _, section = setup_section with freeze_time(day): client.force_login(student_user) enroll_url = reverse("section-students", kwargs={"pk": section.pk}) @@ -181,7 +174,11 @@ def test_attendance_add_student_on_day( def test_attendance_drop_student_on_day( client, setup_section, day, num_attendances_left ): - mentor, student_user, course, section = setup_section + """ + Check that future attendances are deleted when a student is dropped, + and verify that past section occurrences and attendances are unchanged. + """ + _, student_user, _, section = setup_section # enroll student first with freeze_time(timezone.datetime(2020, 6, 1, 0, 0, 0, tzinfo=DEFAULT_TZ)): diff --git a/csm_web/scheduler/tests/models/test_spacetime.py b/csm_web/scheduler/tests/models/test_spacetime.py index 6c7839a3..d7e7d2d6 100644 --- a/csm_web/scheduler/tests/models/test_spacetime.py +++ b/csm_web/scheduler/tests/models/test_spacetime.py @@ -1,29 +1,27 @@ -import pytest -from freezegun import freeze_time import datetime -from django.db.models import Q -from django.utils import timezone +import pytest from django.urls import reverse -from scheduler.models import ( - Override, -) +from django.utils import timezone +from freezegun import freeze_time from scheduler.factories import ( - UserFactory, + CoordinatorFactory, CourseFactory, - SectionFactory, MentorFactory, - CoordinatorFactory, + SectionFactory, SpacetimeFactory, + UserFactory, ) -import zoneinfo - -DEFAULT_TZ = zoneinfo.ZoneInfo(timezone.get_default_timezone().zone) +DEFAULT_TZ = timezone.get_default_timezone() -@pytest.fixture -def setup_section(db): +# avoid pylint warning redefining name in outer scope +@pytest.fixture(name="setup_section") +def fixture_setup_section(db): # pylint: disable=unused-argument + """ + Set up a mentor, coordinator, and section for spacetime testing + """ mentor_user = UserFactory( username="mentor_user", first_name="mentor", last_name="user" ) @@ -142,6 +140,9 @@ def setup_section(db): ], ) def test_delete_spacetime(client, setup_section, day, spacetime_index): + """ + On deletion of spacetimes, ensure that future section occurrences are deleted. + """ section, _, coord, spacetimes = setup_section with freeze_time(day): @@ -154,10 +155,14 @@ def test_delete_spacetime(client, setup_section, day, spacetime_index): assert section.spacetimes.filter(pk=spacetime.pk).count() == 0 # make sure future section occurrences have been deleted - # FIX ME: Once issue #303 is resolved, this should search through the directly related section occurences - assert section.sectionoccurrence_set.filter( - date__gte=day, date__week_day=spacetime.day_number() - ).count() == 0 + # FIX ME: Once issue #303 is resolved, this should search + # through the directly related section occurences + assert ( + section.sectionoccurrence_set.filter( + date__gte=day, date__week_day=spacetime.day_number() + ).count() + == 0 + ) @pytest.mark.django_db @@ -165,34 +170,61 @@ def test_delete_spacetime(client, setup_section, day, spacetime_index): ["day", "override_date", "spacetime_index"], [ # on prior thursday, move tuesday section to following wednesday - (timezone.datetime(2020, 5, 21, 0, 0, 0, tzinfo=DEFAULT_TZ), - timezone.datetime(2020, 5, 27, 9, 0, 0, tzinfo=DEFAULT_TZ), 0), + ( + timezone.datetime(2020, 5, 21, 0, 0, 0, tzinfo=DEFAULT_TZ), + timezone.datetime(2020, 5, 27, 9, 0, 0, tzinfo=DEFAULT_TZ), + 0, + ), # on prior monday, change tuesday section location - (timezone.datetime(2020, 5, 25, 9, 0, 0, tzinfo=DEFAULT_TZ), - timezone.datetime(2020, 5, 26, 10, 0, 0, tzinfo=DEFAULT_TZ), 0), + ( + timezone.datetime(2020, 5, 25, 9, 0, 0, tzinfo=DEFAULT_TZ), + timezone.datetime(2020, 5, 26, 10, 0, 0, tzinfo=DEFAULT_TZ), + 0, + ), # on tuesday an hour before scheduled section, push tuesday section an hour later - (timezone.datetime(2020, 5, 26, 9, 0, 0, tzinfo=DEFAULT_TZ), - timezone.datetime(2020, 5, 26, 11, 0, 0, tzinfo=DEFAULT_TZ), 0), + ( + timezone.datetime(2020, 5, 26, 9, 0, 0, tzinfo=DEFAULT_TZ), + timezone.datetime(2020, 5, 26, 11, 0, 0, tzinfo=DEFAULT_TZ), + 0, + ), # on tuesday an hour after scheduled section, correct tuesday section to prior monday - (timezone.datetime(2020, 5, 26, 11, 0, 0, tzinfo=DEFAULT_TZ), - timezone.datetime(2020, 5, 25, 10, 0, 0, tzinfo=DEFAULT_TZ), 0), + ( + timezone.datetime(2020, 5, 26, 11, 0, 0, tzinfo=DEFAULT_TZ), + timezone.datetime(2020, 5, 25, 10, 0, 0, tzinfo=DEFAULT_TZ), + 0, + ), # on prior monday, move thursday section to prior wednesday - (timezone.datetime(2020, 5, 25, 9, 0, 0, tzinfo=DEFAULT_TZ), - timezone.datetime(2020, 5, 27, 10, 0, 0, tzinfo=DEFAULT_TZ), 1), + ( + timezone.datetime(2020, 5, 25, 9, 0, 0, tzinfo=DEFAULT_TZ), + timezone.datetime(2020, 5, 27, 10, 0, 0, tzinfo=DEFAULT_TZ), + 1, + ), # on prior wednesday, move thursday section to current time - (timezone.datetime(2020, 5, 27, 9, 0, 0, tzinfo=DEFAULT_TZ), - timezone.datetime(2020, 5, 27, 9, 0, 0, tzinfo=DEFAULT_TZ), 1), + ( + timezone.datetime(2020, 5, 27, 9, 0, 0, tzinfo=DEFAULT_TZ), + timezone.datetime(2020, 5, 27, 9, 0, 0, tzinfo=DEFAULT_TZ), + 1, + ), ], ids=[ "on prior thursday, move tuesday section to following wednesday", "on prior monday, change tuesday section location", - "on tuesday an hour before scheduled section, push tuesday section an hour later", - "on tuesday an hour after scheduled section, correct tuesday section to prior monday", + ( + "on tuesday an hour before scheduled section, push tuesday section an hour" + " later" + ), + ( + "on tuesday an hour after scheduled section, correct tuesday section to" + " prior monday" + ), "on prior monday, move thursday section to prior wednesday", "on prior wednesday, move thursday section to current time", ], ) def test_delete_override(client, setup_section, day, override_date, spacetime_index): + """ + Ensure that spacetime overrides can be deleted. + """ _, mentor, coord, spacetimes = setup_section with freeze_time(day): @@ -200,11 +232,15 @@ def test_delete_override(client, setup_section, day, override_date, spacetime_in spacetime = spacetimes[spacetime_index] override_url = reverse("spacetime-override", kwargs={"pk": spacetime.pk}) - response = client.put(override_url, content_type="application/json", data={ - "location": "location", - "start_time": override_date.timetz(), - "date": override_date.date(), - }) + client.put( + override_url, + content_type="application/json", + data={ + "location": "location", + "start_time": override_date.timetz(), + "date": override_date.date(), + }, + ) # refresh spacetime object spacetime.refresh_from_db() diff --git a/csm_web/scheduler/tests/models/test_word_of_the_day.py b/csm_web/scheduler/tests/models/test_word_of_the_day.py index 4e5efd4e..9389d3db 100644 --- a/csm_web/scheduler/tests/models/test_word_of_the_day.py +++ b/csm_web/scheduler/tests/models/test_word_of_the_day.py @@ -1,5 +1,4 @@ import datetime -import zoneinfo import pytest from django.urls import reverse @@ -14,7 +13,7 @@ ) from scheduler.models import Attendance, SectionOccurrence, Student -DEFAULT_TZ = zoneinfo.ZoneInfo(timezone.get_default_timezone().zone) +DEFAULT_TZ = timezone.get_default_timezone() # avoid pylint warning redefining name in outer scope diff --git a/csm_web/scheduler/tests/old_test_api.py b/csm_web/scheduler/tests/old_test_api.py deleted file mode 100644 index 4d2189fb..00000000 --- a/csm_web/scheduler/tests/old_test_api.py +++ /dev/null @@ -1,220 +0,0 @@ -from datetime import timedelta, time, datetime -from rest_framework import status -from rest_framework.test import APITestCase -from scheduler.models import Spacetime -from scheduler.serializers import get_profile_role -from scheduler.factories import ( - SpacetimeFactory, - CourseFactory, - SectionFactory, - StudentFactory, - MentorFactory, - OverrideFactory, - UserFactory -) - - -class ProfileListTest(APITestCase): - """ - Tests the /api/profiles endpoint, which lists all the profiles of a user. - Here's the expected fields for each response, since the formatter will make this no fun to read: - id: - section_id:
- section_spacetimes: [{ - start_time: "13:00:00" or other 24-hr string - day_of_week: "Monday" - time: "Monday 1:00-2:00 PM" or smth - duration: "01:00:00" - id: - location: "room number", - override: null - }, ...] - course: "CS61C" or whatever - course_id: - course_title: "Machine Structures" - role: "STUDENT" | "MENTOR" | "COORDINATOR" - """ - - endpoint = '/api/profiles/' - - def make_default_section(self, mentor=None): - return SectionFactory.create( - mentor=mentor, - course=CourseFactory.create(name='CS61C', title='Machine Structures'), - spacetimes=[ - SpacetimeFactory.create( - start_time=time(hour=13), - day_of_week='Monday', - duration=timedelta(hours=1), - location='Soda 1337' - ) - ] - ) - - def get_default_response(self, profile, section): - """ - This function is somewhat inflexible but it at least saves us from copy pasting dictionary - literals everywhere - It's probably not too hard to replace the fields with obj properties, but I have bigger - concerns at the moment - """ - return {'id': profile.pk, - 'section_id': section.pk, - 'section_spacetimes': [{'id': section.spacetimes.first().pk, - 'start_time': '13:00:00', - 'day_of_week': 'Monday', - 'duration': '01:00:00', - 'time': 'Monday 1:00-2:00 PM', - 'location': 'Soda 1337', - 'override': None}], - 'course': 'CS61C', - 'course_id': section.course.id, - 'course_title': 'Machine Structures', - 'role': get_profile_role(profile)} - - def setUp(self): - self.user = UserFactory.create() - self.client.force_authenticate(user=self.user) - - def test_simple_mentor(self): - mentor = MentorFactory.create(user=self.user) - section = self.make_default_section(mentor=mentor) - response = self.client.get(self.endpoint) - self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0], self.get_default_response(mentor, section)) - - def test_simple_student(self): - section = self.make_default_section() - student = StudentFactory.create(user=self.user, section=section) - response = self.client.get(self.endpoint) - self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0], self.get_default_response(student, section)) - - def test_mentor_and_student(self): - student_section = self.make_default_section() - student = StudentFactory.create(user=self.user, section=student_section) - mentor = MentorFactory.create(user=self.user) - mentor_section = SectionFactory.create( - mentor=mentor, - course=CourseFactory.create( - name='CS70', - title='Discrete Mathematics and Probability Theory' - ), - spacetimes=[ - SpacetimeFactory.create( - start_time=time(hour=11), - day_of_week='Tuesday', - duration=timedelta(hours=1.5), - location='Cory 7' - ), - SpacetimeFactory.create( - start_time=time(hour=11), - day_of_week='Thursday', - duration=timedelta(hours=1.5), - location='Cory 11' - ) - ] - ) - response = self.client.get(self.endpoint) - self.assertEqual(len(response.data), 2) - self.assertIn(self.get_default_response(student, student_section), response.data) - # CS 70 is a special case due to multiple spacetimes - self.assertIn({ - 'id': mentor.pk, - 'section_id': mentor_section.pk, - 'section_spacetimes': [ - { - 'id': mentor_section.spacetimes.all()[0].pk, - 'start_time': '11:00:00', - 'day_of_week': 'Tuesday', - 'duration': '01:30:00', - # IMPORTANT: note the AM here to avoid ambiguity - 'time': 'Tuesday 11:00 AM-12:30 PM', - 'location': 'Cory 7', - 'override': None - }, - { - 'id': mentor_section.spacetimes.all()[1].pk, - 'start_time': '11:00:00', - 'day_of_week': 'Thursday', - 'duration': '01:30:00', - 'time': 'Thursday 11:00 AM-12:30 PM', - 'location': 'Cory 11', - 'override': None - }, - ], - 'course': 'CS70', - 'course_id': mentor_section.course.id, - 'course_title': 'Discrete Mathematics and Probability Theory', 'role': "MENTOR" - }, response.data) - - def test_inactive_student(self): - section = self.make_default_section() - StudentFactory.create(user=self.user, section=section, active=False) - response = self.client.get(self.endpoint) - self.assertEqual(len(response.data), 0) - - -# class SpacetimeModifyTest(APITestCase): -# def setUp(self): -# self.user = UserFactory.create() -# self.client.force_authenticate(user=self.user) - -# def test_simple(self): -# mentor = MentorFactory.create(user=self.user) -# spacetime = SpacetimeFactory.create(start_time=time(hour=13), day_of_week='Monday', duration=timedelta(hours=1)) -# SectionFactory.create(mentor=mentor, course=CourseFactory.create( -# name='CS61C', title='Machine Structures'), spacetime=spacetime) -# new_time = time(hour=12) -# new_day = 'Tue' -# new_location = 'New Location!' -# response = self.client.put(f"/api/spacetimes/{spacetime.pk}/modify/", { -# "start_time": new_time, -# "day_of_week": new_day, -# "location": new_location, -# }) -# self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED, response.data) -# new_spacetime = Spacetime.objects.get(pk=spacetime.pk) -# self.assertEqual(new_spacetime.start_time, new_time) -# self.assertEqual(new_spacetime.location, new_location) -# self.assertEqual(new_spacetime.day_of_week, new_day) - - -# class OverrideTest(APITestCase): -# def setUp(self): -# self.user = UserFactory.create() -# section = SectionFactory.create(mentor=MentorFactory.create(user=self.user), course=CourseFactory.create()) -# self.client.force_authenticate(user=self.user) -# self.spacetime = section.spacetime - -# def test_create_override(self): -# override_time = time(hour=12) -# override_date = (datetime.now() + timedelta(days=1)).date() -# override_location = 'Soda 1337' -# data = {"start_time": override_time.strftime( -# "%H:%M:%S"), "date": override_date.isoformat(), "location": override_location} -# response = self.client.put(f"/api/spacetimes/{self.spacetime.pk}/override/", data, format='json') -# self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) -# self.spacetime.refresh_from_db() -# self.assertIsNotNone(self.spacetime.override) -# self.assertEqual(override_time, self.spacetime.override.spacetime.start_time) -# self.assertEqual(override_date, self.spacetime.override.date) -# self.assertEqual(override_location, self.spacetime.override.spacetime.location) -# self.assertEqual(self.spacetime.duration, self.spacetime.override.spacetime.duration) - -# def test_update_override(self): -# OverrideFactory.create(overriden_spacetime=self.spacetime) -# self.spacetime.refresh_from_db() -# self.assertIsNotNone(self.spacetime.override) -# override_time = time(hour=12) -# override_date = (datetime.now() + timedelta(days=1)).date() -# override_location = 'Soda 1337' -# data = {"start_time": override_time.strftime( -# "%H:%M:%S"), "date": override_date.isoformat(), "location": override_location} -# response = self.client.put(f"/api/spacetimes/{self.spacetime.pk}/override/", data, format='json') -# self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED, response.data) -# self.spacetime.refresh_from_db() -# self.assertEqual(override_time, self.spacetime.override.spacetime.start_time) -# self.assertEqual(override_date, self.spacetime.override.date) -# self.assertEqual(override_location, self.spacetime.override.spacetime.location) -# self.assertEqual(self.spacetime.duration, self.spacetime.override.spacetime.duration) diff --git a/csm_web/scheduler/tests/old_test_enroll_drop.py b/csm_web/scheduler/tests/old_test_enroll_drop.py deleted file mode 100644 index 8987b3aa..00000000 --- a/csm_web/scheduler/tests/old_test_enroll_drop.py +++ /dev/null @@ -1,208 +0,0 @@ -""" -Test cases to simulate simple enrollment and drop scenarios. -""" -import logging -from rest_framework import status -from scheduler.models import Student -from scheduler.tests import utils -from scheduler.factories import ( - UserFactory, - CourseFactory, - MentorFactory, - SectionFactory -) - - -def get_enroll_url_for(section): - return f"/sections/{section.id}/students" - - -def get_drop_url_for(student): - return f"/students/{student.id}/drop" - - -class DropTest(utils.APITestCase): - """ - Tests that a student is able to drop a section. - """ - - def setUp(self): - logging.disable(logging.CRITICAL) - self.my_course = CourseFactory.create() - self.other_course = CourseFactory.create() - self.user = UserFactory.create() - self.client = self.get_client_for(self.user) - - def count_student_profiles_in(self, user, course): - return Student.objects.filter( - user=user, - section__course=course, - active=True - ).count() - - def drop_and_should_succeed(self, student): - url = get_drop_url_for(student) - self.request(self.client.patch, url, exp_code=status.HTTP_204_NO_CONTENT) - - def test_simple(self): - """ - Tests that the student should be able to drop a section they just enrolled in. - """ - student = utils.enroll_user_as_student(self.user, SectionFactory.create(course=self.my_course)) - self.drop_and_should_succeed(student) - self.assertEqual(self.count_student_profiles_in(self.user, self.my_course), 0) - - def test_without_enroll(self): - """ - Tests that a student should be unable to drop a student that's not themselves. - """ - other_student = utils.enroll_user_as_student( - UserFactory.create(), - SectionFactory.create(course=self.my_course) - ) - url = get_drop_url_for(other_student) - self.request(self.client.patch, url, exp_code=status.HTTP_403_FORBIDDEN) - self.assertEqual(self.count_student_profiles_in(self.user, self.my_course), 0) - self.assertEqual(self.count_student_profiles_in(other_student.user, self.my_course), 1) - - def test_double_drop(self): - """ - Tests that dropping a section twice should fail the second call and make no DB changes. - """ - student = utils.enroll_user_as_student(self.user, SectionFactory.create(course=self.my_course)) - self.drop_and_should_succeed(student) - self.assertEqual(self.count_student_profiles_in(self.user, self.my_course), 0) - url = get_drop_url_for(student) - self.request(self.client.patch, url, exp_code=status.HTTP_403_FORBIDDEN) - self.assertEqual(self.count_student_profiles_in(self.user, self.my_course), 0) - - def test_with_other_sections(self): - """ - Tests that a student enrolled in multiple sections only drops the desired one. - """ - student1 = utils.enroll_user_as_student(self.user, SectionFactory.create(course=self.my_course)) - student2 = utils.enroll_user_as_student(self.user, SectionFactory.create(course=self.other_course)) - self.drop_and_should_succeed(student1) - self.assertEqual(self.count_student_profiles_in(self.user, self.my_course), 0) - self.assertEqual(self.count_student_profiles_in(self.user, self.other_course), 1) - self.drop_and_should_succeed(student2) - self.assertEqual(self.count_student_profiles_in(self.user, self.my_course), 0) - self.assertEqual(self.count_student_profiles_in(self.user, self.other_course), 0) - - -class EnrollmentTest(utils.APITestCase): - """ - Tests that a student can enroll in a section in these scenarios: - - The student is enrolled in/is a mentor for no other sections - - The student is already enrolled in a section in another course - - The student is a mentor for another course - - The section is one away from reaching capacity - - The student was previously enrolled in, but dropped a section in this course - Also tests that a student cannot enroll in these scenarios: - - The section is full - - The section is over capacity - - The student is already enrolled in a section in this course - - The student is a mentor in this course - """ - - def setUp(self): - logging.disable(logging.CRITICAL) - self.my_course = CourseFactory.create() - self.other_course = CourseFactory.create() - self.assertNotEqual(self.my_course, self.other_course) - self.user = UserFactory.create() - self.client = self.get_client_for(self.user) - - def count_student_profiles_in(self, course): - return Student.objects.filter( - user=self.user, - section__course=course, - active=True - ).count() - - def drop(self, student): - url = get_drop_url_for(student) - self.request(self.client.patch, url) - - def enroll_and_should_succeed(self, section): - url = get_enroll_url_for(section) - self.request(self.client.put, url, exp_code=status.HTTP_201_CREATED) - self.assertNotEqual(self.count_student_profiles_in(section.course), 0) - - def enroll_and_should_fail(self, section): - old_profile_count = self.count_student_profiles_in(section.course) - url = get_enroll_url_for(section) - request = self.request(self.client.put, url) - self.assertNotEqual(request.status_code, status.HTTP_201_CREATED) - new_profile_count = self.count_student_profiles_in(section.course) - self.assertEqual(new_profile_count, old_profile_count) - - def test_simple(self): - """ - Tests that enrollment succeeds when the student is enrolled in/is a mentor for no other - sections. - """ - section = SectionFactory.create(course=self.my_course) - self.enroll_and_should_succeed(section) - - def test_student_in_other_course(self): - """ - Tests that enrollment should succeed when the student is already enrolled in a section in - another course, but not this one. - """ - other_section = SectionFactory.create(course=self.other_course) - my_section = SectionFactory.create(course=self.my_course) - utils.enroll_user_as_student(self.user, other_section) - self.enroll_and_should_succeed(my_section) - - def test_mentor_in_other_course(self): - """ - Tests that enrollment should succeed when the student is a mentor for another course, but - not a student in this one. - """ - mentor = MentorFactory.create(user=self.user) - SectionFactory.create(course=self.other_course, mentor=mentor) - my_section = SectionFactory.create(course=self.my_course) - self.enroll_and_should_succeed(my_section) - - def test_last_spot(self): - """ - Tests that enrollment should succeed if the section is one away from capacity. - """ - section = SectionFactory.create(course=self.my_course, capacity=1) - self.enroll_and_should_succeed(section) - - def test_reenroll(self): - """ - Tests that a student that previously dropped a section is able to reenroll in this course. - """ - section = SectionFactory.create(course=self.my_course) - student = utils.enroll_user_as_student(self.user, section) - self.drop(student) - # New response should return 204 instead of 201 - url = get_enroll_url_for(section) - self.request(self.client.put, url, exp_code=status.HTTP_204_NO_CONTENT) - self.assertNotEqual(self.count_student_profiles_in(section.course), 0) - - def test_full_section(self): - """ - Tests that enrollment should not fail when the student attempts to enroll in a full section. - """ - section = SectionFactory.create(course=self.my_course, capacity=0) - self.enroll_and_should_fail(section) - - def test_already_enrolled(self): - """ - Tests that enrollment should fail if the user is already enrolled in a section in this course. - """ - section = SectionFactory.create(course=self.my_course) - self.enroll_and_should_succeed(section) - self.enroll_and_should_fail(section) - - def test_is_mentor(self): - """ - Tests that a mentor should be unable to enroll in a section in their course. - """ - mentor = MentorFactory.create(user=self.user) - section = SectionFactory.create(course=self.other_course, mentor=mentor) - self.enroll_and_should_fail(section) diff --git a/csm_web/scheduler/tests/old_test_management.py b/csm_web/scheduler/tests/old_test_management.py deleted file mode 100644 index 6462d5ba..00000000 --- a/csm_web/scheduler/tests/old_test_management.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Tests for management commands. -https://docs.djangoproject.com/en/3.0/topics/testing/tools/#topics-testing-management-commands -""" -from io import StringIO -from django.core.management import call_command -from django.test import TestCase -from scheduler.factories import ( - UserFactory, - CourseFactory, - MentorFactory, - SectionFactory, - StudentFactory -) - - -class TestSanity(TestCase): - """ - These tests just make sure that the command does not fail due to schema changes that weren't - accounted for. - """ - # (name, args) tuples - COMMANDS = ( - # ("checkintegrity", []), # Factories need updating to account for spacetime overlaps - # ("export_attendances", ["CS61A"]), - # ("export_course", ["CS61A"]), - # ("export_section", ["demo_user@berkeley.edu"]), - # ("triagedups", ["CS61A"]), - ) - - def setUp(self): - self.cs61a = CourseFactory.create(name="CS61A") - self.user = UserFactory.create(email="demo_user@berkeley.edu") - mentor = MentorFactory.create(user=self.user) - SectionFactory.create(course=self.cs61a, mentor=mentor) - # Create a bunch of random guys for the heck of it - for section in SectionFactory.create_batch(10, course=self.cs61a): - StudentFactory.create(section=section) - - def test_commands(self): - for name, args in TestSanity.COMMANDS: - out = StringIO() - call_command(name, *args, stdout=out) diff --git a/csm_web/scheduler/tests/old_test_models.py b/csm_web/scheduler/tests/old_test_models.py deleted file mode 100644 index e46b8727..00000000 --- a/csm_web/scheduler/tests/old_test_models.py +++ /dev/null @@ -1,133 +0,0 @@ -from django.test import TestCase -from django.utils import timezone -from rest_framework.serializers import ValidationError -from datetime import datetime, timedelta -from scheduler.models import Override, Spacetime, Student, DayOfWeekField, week_bounds -from scheduler.factories import ( - SpacetimeFactory, - OverrideFactory, - CourseFactory, - SectionFactory, - UserFactory -) - - -class SpacetimeTest(TestCase): - def setUp(self): - course = CourseFactory.create() - self.section = SectionFactory.create(course=course) - self.spacetime = SpacetimeFactory.create(section=self.section) - - def test_spacetime_without_override(self): - self.assertIsNone(self.spacetime.override) - - # def test_spacetime_with_override(self): - # override = OverrideFactory.create(overriden_spacetime=self.spacetime) - # self.assertEqual(override, self.spacetime.override) - - # def test_spacetime_with_expired_override(self): - # date = datetime.now() - timedelta(days=5) - # override = OverrideFactory.create( - # overriden_spacetime=self.spacetime, - # date=date, - # spacetime=SpacetimeFactory.create(day_of_week=DayOfWeekField.DAYS[date.weekday()]) - # ) - # self.assertTrue(override.is_expired()) - # self.assertIsNone(self.spacetime.override) - - def test_location_normalization(self): - spacetime = SpacetimeFactory.create(location=' Soda 372 ', section=self.section) - self.assertEqual(spacetime.location, 'Soda 372') - - -class CourseTest(TestCase): - def test_enrollment_start_after_end_validation(self): - with self.assertRaises(ValidationError): - CourseFactory.create( - enrollment_start=datetime.now(), - enrollment_end=(datetime.now() - timedelta(days=1)), - valid_until=(datetime.now() + timedelta(days=10)), - ) - - def test_valid_until_after_enrollment(self): - with self.assertRaises(ValidationError): - CourseFactory.create( - enrollment_start=datetime.now(), - enrollment_end=(datetime.now() + timedelta(days=1)), - valid_until=(datetime.now() - timedelta(days=10)), - ) - - -# class OverrideTest(TestCase): -# def setUp(self): -# course = CourseFactory.create() -# self.section = SectionFactory.create(course=course) -# self.spacetime = SpacetimeFactory.create(section=self.section) - -# def test_spacetime_override_self_validation(self): -# with self.assertRaises(ValidationError): -# Override.objects.create(spacetime=self.spacetime, -# overriden_spacetime=self.spacetime, date=datetime.now().date()) - -# def test_day_of_week_validation(self): -# override = OverrideFactory.create(overriden_spacetime=self.spacetime) -# with self.assertRaises(ValidationError): -# override.date -= timedelta(days=1) -# override.save() - - -class StudentAttendanceTest(TestCase): - """ - This test won't work on Mondays - """ - - def setUp(self): - self.now = timezone.now() - self.course = CourseFactory.create(enrollment_start=self.now - timedelta(weeks=1), enrollment_end=self.now + - timedelta(weeks=3), section_start=(self.now - timedelta(days=self.now.weekday())).date()) - self.user = UserFactory.create() - - def offset_by_days(self, k): - return DayOfWeekField.DAYS[(self.now.weekday() + k) % 7] - - def test_section_later_in_week(self): - section = SectionFactory.create(course=self.course, spacetimes=[SpacetimeFactory.create( - day_of_week=self.offset_by_days(1))]) - student = Student.objects.create(section=section, user=self.user) - self.assertEqual(student.attendance_set.count(), 1) - self.assertEqual(week_bounds(self.now.date()), week_bounds(student.attendance_set.first().date)) - - def test_section_both_later_in_week(self): - section = SectionFactory.create(course=self.course, spacetimes=[SpacetimeFactory.create( - day_of_week=self.offset_by_days(1)), SpacetimeFactory.create(day_of_week=self.offset_by_days(2))]) - student = Student.objects.create(section=section, user=self.user) - self.assertEqual(student.attendance_set.count(), 2) - self.assertEqual(student.attendance_set.first().date, (self.now + timedelta(days=1)).date()) - self.assertEqual(student.attendance_set.last().date, (self.now + timedelta(days=2)).date()) - - def test_section_earlier_in_week(self): - section = SectionFactory.create(course=self.course, spacetimes=[SpacetimeFactory.create( - day_of_week=self.offset_by_days(-1))]) - student = Student.objects.create(section=section, user=self.user) - self.assertFalse(student.attendance_set.exists()) - - def test_attendance_already_exists(self): - section = SectionFactory.create(course=self.course, spacetimes=[SpacetimeFactory.create( - day_of_week=self.offset_by_days(1))]) - student = Student.objects.create(section=section, user=self.user) - self.assertEqual(student.attendance_set.count(), 1) - new_section_same_day = SectionFactory.create(course=self.course, spacetimes=[SpacetimeFactory.create( - day_of_week=section.spacetimes.first().day_of_week)]) - student.section = new_section_same_day - student.save() - student.refresh_from_db() - self.assertEqual(student.attendance_set.count(), 1) - - def test_course_ended(self): - self.course.enrollment_end = self.now - timedelta(days=2) - self.course.valid_until = self.now - timedelta(days=1) - self.course.save() - section = SectionFactory.create(course=self.course, spacetimes=[SpacetimeFactory.create( - day_of_week=self.offset_by_days(1))]) - student = Student.objects.create(section=section, user=self.user) - self.assertFalse(student.attendance_set.exists()) diff --git a/csm_web/scheduler/tests/utils.py b/csm_web/scheduler/tests/utils.py deleted file mode 100644 index f2e3b525..00000000 --- a/csm_web/scheduler/tests/utils.py +++ /dev/null @@ -1,70 +0,0 @@ -from os import path -from django.test import TestCase -from rest_framework.test import APIClient - -from scheduler.factories import ( - CourseFactory, - UserFactory, - StudentFactory, - SectionFactory, -) - -COURSE_NAMES = ("CS88", "CS61A", "CS61B", "CS70", "CS61C", "EE16A", "EE16B") -BASE_PATH = "/api/" - -# ----- REQUEST UTILITIES ----- - - -def fail_msg(ep, resp): - return "Endpoint: {}\nResponse Content: {}".format(ep, resp.content) - - -class APITestCase(TestCase): - def get_client_for(self, user): - """Returns an APIClient object that is logged in as the provided user.""" - client = APIClient() - client.force_authenticate(user) - return client - - def request(self, method, endpoint, exp_code=None, data=None): - """ - Performs a request to the specified endpoint and returns the response object. - Also checks if the status code of the response is exp_code, if provided. - The method parameter should be a get/post/etc from an APIClient object. - """ - full_path = path.join(BASE_PATH, endpoint.strip("/")) - if full_path[-1] != "/": - full_path += "/" - resp = method(full_path, follow=True, data=data) - if exp_code is not None: - self.assertEqual(resp.status_code, exp_code, msg=fail_msg(full_path, resp)) - return resp - - -# ----- MODEL GENERATION ----- - - -def make_test_courses(): - """Creates course objects and persists them to database.""" - return [CourseFactory.create(name=name) for name in COURSE_NAMES] - - -def make_test_users(n): - """Creates N test users and persists them to database.""" - return UserFactory.create_batch(n) - - -def create_empty_section_for(mentor): - """ - Creates a section for MENTOR without populated students. - """ - return SectionFactory.create(course=mentor.course, mentor=mentor) - - -def enroll_user_as_student(user, section, save=True): - """ - Creates a student profile for USER, and assigns them to the given SECTION. - The student is saved to database. - Returns the created profile. - """ - return StudentFactory.create(user=user, section=section) diff --git a/csm_web/scheduler/tests/utils/__init__.py b/csm_web/scheduler/tests/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/csm_web/scheduler/utils/__init__.py b/csm_web/scheduler/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..27656f00 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1892 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "appnope" +version = "0.1.3" +description = "Disable App Nap on macOS >= 10.9" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, + {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, +] + +[[package]] +name = "asgiref" +version = "3.6.0" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "asgiref-3.6.0-py3-none-any.whl", hash = "sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac"}, + {file = "asgiref-3.6.0.tar.gz", hash = "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506"}, +] + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "astroid" +version = "2.12.13" +description = "An abstract syntax tree for Python with inference support." +category = "dev" +optional = false +python-versions = ">=3.7.2" +files = [ + {file = "astroid-2.12.13-py3-none-any.whl", hash = "sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907"}, + {file = "astroid-2.12.13.tar.gz", hash = "sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7"}, +] + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} +wrapt = {version = ">=1.11,<2", markers = "python_version < \"3.11\""} + +[[package]] +name = "asttokens" +version = "2.2.1" +description = "Annotate AST trees with source code positions" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"}, + {file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"}, +] + +[package.dependencies] +six = "*" + +[package.extras] +test = ["astroid", "pytest"] + +[[package]] +name = "attrs" +version = "22.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] + +[[package]] +name = "black" +version = "22.12.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "boto3" +version = "1.26.39" +description = "The AWS SDK for Python" +category = "main" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "boto3-1.26.39-py3-none-any.whl", hash = "sha256:9def744f663d556fc1d6222b8a4eebd426300ae2365cbeaa1d153b3e723b7156"}, + {file = "boto3-1.26.39.tar.gz", hash = "sha256:7319d89fd7f901eb6316398b2bffe3f719a483901c69705385ccff6f38feb291"}, +] + +[package.dependencies] +botocore = ">=1.29.39,<1.30.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.6.0,<0.7.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.29.39" +description = "Low-level, data-driven core of boto 3." +category = "main" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "botocore-1.29.39-py3-none-any.whl", hash = "sha256:3f48020d7d51bbd68e51c1fbf6ecac139579aa50378c0ca83d4f908313041be9"}, + {file = "botocore-1.29.39.tar.gz", hash = "sha256:c8f7c8de902e42b71dbf7e2205a40a1aab8b1fb37342d8fdff198cc1610c39c7"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = ">=1.25.4,<1.27" + +[package.extras] +crt = ["awscrt (==0.15.3)"] + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] + +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] + +[[package]] +name = "charset-normalizer" +version = "2.1.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] + +[package.extras] +unicode-backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cryptography" +version = "38.0.4" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70"}, + {file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c"}, + {file = "cryptography-38.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00"}, + {file = "cryptography-38.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0"}, + {file = "cryptography-38.0.4-cp36-abi3-win32.whl", hash = "sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744"}, + {file = "cryptography-38.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d"}, + {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353"}, + {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7"}, + {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd"}, + {file = "cryptography-38.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2"}, + {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b"}, + {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6"}, + {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876"}, + {file = "cryptography-38.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b"}, + {file = "cryptography-38.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285"}, + {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b"}, + {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083"}, + {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee"}, + {file = "cryptography-38.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9"}, + {file = "cryptography-38.0.4.tar.gz", hash = "sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools-rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + +[[package]] +name = "dill" +version = "0.3.6" +description = "serialize all of python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, + {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + +[[package]] +name = "distlib" +version = "0.3.6" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] + +[[package]] +name = "dj-database-url" +version = "1.2.0" +description = "Use Database URLs in your Django Application." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "dj-database-url-1.2.0.tar.gz", hash = "sha256:b23b15046cb38180e0c95207bcc90fe5e9dbde8eef16065907dd85cf4ca7036c"}, + {file = "dj_database_url-1.2.0-py3-none-any.whl", hash = "sha256:5c2993b91801c0f78a8b19e642b497b90831124cbade0c265900d4c1037b4730"}, +] + +[package.dependencies] +Django = ">=3.2" + +[[package]] +name = "django" +version = "4.1.4" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Django-4.1.4-py3-none-any.whl", hash = "sha256:0b223bfa55511f950ff741983d408d78d772351284c75e9f77d2b830b6b4d148"}, + {file = "Django-4.1.4.tar.gz", hash = "sha256:d38a4e108d2386cb9637da66a82dc8d0733caede4c83c4afdbda78af4214211b"}, +] + +[package.dependencies] +asgiref = ">=3.5.2,<4" +sqlparse = ">=0.2.2" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-csp" +version = "3.7" +description = "Django Content Security Policy support." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "django_csp-3.7-py2.py3-none-any.whl", hash = "sha256:01443a07723f9a479d498bd7bb63571aaa771e690f64bde515db6cdb76e8041a"}, + {file = "django_csp-3.7.tar.gz", hash = "sha256:01eda02ad3f10261c74131cdc0b5a6a62b7c7ad4fd017fbefb7a14776e0a9727"}, +] + +[package.dependencies] +Django = ">=1.8" + +[package.extras] +jinja2 = ["jinja2 (>=2.9.6)"] +tests = ["jinja2 (>=2.9.6)", "mock (==1.0.1)", "pep8 (==1.4.6)", "pytest (<4.0)", "pytest-django", "pytest-flakes (==1.0.1)", "pytest-pep8 (==1.0.6)", "six (==1.12.0)"] + +[[package]] +name = "django-extensions" +version = "3.2.1" +description = "Extensions for Django" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "django-extensions-3.2.1.tar.gz", hash = "sha256:2a4f4d757be2563cd1ff7cfdf2e57468f5f931cc88b23cf82ca75717aae504a4"}, + {file = "django_extensions-3.2.1-py3-none-any.whl", hash = "sha256:421464be390289513f86cb5e18eb43e5dc1de8b4c27ba9faa3b91261b0d67e09"}, +] + +[package.dependencies] +Django = ">=3.2" + +[[package]] +name = "django-heroku" +version = "0.3.1" +description = "This is a Django library for Heroku apps." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "django-heroku-0.3.1.tar.gz", hash = "sha256:6af4bc3ae4a9b55eaad6dbe5164918982d2762661aebc9f83d9fa49f6009514e"}, + {file = "django_heroku-0.3.1-py2.py3-none-any.whl", hash = "sha256:2bc690aab89eedbe01311752320a9a12e7548e3b0ed102681acc5736a41a4762"}, +] + +[package.dependencies] +dj-database-url = ">=0.5.0" +django = "*" +psycopg2 = "*" +whitenoise = "*" + +[[package]] +name = "django-postgres-extra" +version = "2.0.6" +description = "Bringing all of PostgreSQL's awesomeness to Django." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "django-postgres-extra-2.0.6.tar.gz", hash = "sha256:0ab6a54e0b24159d6fc2b110bca44c98e3b11471c6b236eb0b5c27e8912fd9e0"}, + {file = "django_postgres_extra-2.0.6-py3-none-any.whl", hash = "sha256:ee405f9d3560fbe9bd0bd4ad6c5a33f15a38ce50c34d9fe8cdf0b9f7f3409c9d"}, +] + +[package.dependencies] +Django = ">=2.0,<5.0" +python-dateutil = ">=2.8.0,<=3.0.0" + +[package.extras] +analysis = ["autoflake (==1.4)", "autopep8 (==1.6.0)", "black (==22.3.0)", "docformatter (==1.4)", "flake8 (==4.0.1)", "isort (==5.10.0)"] +docs = ["Sphinx (==2.2.0)", "docutils (<0.18)", "sphinx-rtd-theme (==0.4.3)"] +publish = ["build (==0.7.0)", "twine (==3.7.1)"] +test = ["coveralls (==3.3.0)", "dj-database-url (==0.5.0)", "freezegun (==1.1.0)", "psycopg2 (>=2.8.4,<3.0.0)", "pytest (==6.2.5)", "pytest-benchmark (==3.4.1)", "pytest-cov (==3.0.0)", "pytest-django (==4.4.0)", "snapshottest (==0.6.0)", "tox (==3.24.4)"] + +[[package]] +name = "django-storages" +version = "1.13.2" +description = "Support for many storage backends in Django" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "django-storages-1.13.2.tar.gz", hash = "sha256:cbadd15c909ceb7247d4ffc503f12a9bec36999df8d0bef7c31e57177d512688"}, + {file = "django_storages-1.13.2-py3-none-any.whl", hash = "sha256:31dc5a992520be571908c4c40d55d292660ece3a55b8141462b4e719aa38eab3"}, +] + +[package.dependencies] +Django = ">=3.2" + +[package.extras] +azure = ["azure-storage-blob (>=12.0.0)"] +boto3 = ["boto3 (>=1.4.4)"] +dropbox = ["dropbox (>=7.2.1)"] +google = ["google-cloud-storage (>=1.27.0)"] +libcloud = ["apache-libcloud"] +sftp = ["paramiko (>=1.10.0)"] + +[[package]] +name = "djangorestframework" +version = "3.14.0" +description = "Web APIs for Django, made easy." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"}, + {file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"}, +] + +[package.dependencies] +django = ">=3.0" +pytz = "*" + +[[package]] +name = "djangorestframework-camel-case" +version = "1.3.0" +description = "Camel case JSON support for Django REST framework." +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "djangorestframework-camel-case-1.3.0.tar.gz", hash = "sha256:df591362ffa448c8f0a354c56ae8a53fb7abbb15e222951d0c6f5f781633907e"}, +] + +[[package]] +name = "drf-nested-forms" +version = "1.1.7" +description = "A library that parses nested json or form data to python object" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "drf_nested_forms-1.1.7-py3-none-any.whl", hash = "sha256:c5c44e7e7ca2ab6eab160b5ad8cde33e29ac459132aca3f98040a5b8b0c848f1"}, + {file = "drf_nested_forms-1.1.7.tar.gz", hash = "sha256:49a3afda3b939e7afc963f9a0141f8ca00c5dc010f03b4b67ef71050ba079bd6"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.0" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, + {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "executing" +version = "1.2.0" +description = "Get the currently executing AST node of a frame, and other information" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"}, + {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"}, +] + +[package.extras] +tests = ["asttokens", "littleutils", "pytest", "rich"] + +[[package]] +name = "factory-boy" +version = "3.2.1" +description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "factory_boy-3.2.1-py2.py3-none-any.whl", hash = "sha256:eb02a7dd1b577ef606b75a253b9818e6f9eaf996d94449c9d5ebb124f90dc795"}, + {file = "factory_boy-3.2.1.tar.gz", hash = "sha256:a98d277b0c047c75eb6e4ab8508a7f81fb03d2cb21986f627913546ef7a2a55e"}, +] + +[package.dependencies] +Faker = ">=0.7.0" + +[package.extras] +dev = ["Django", "Pillow", "SQLAlchemy", "coverage", "flake8", "isort", "mongoengine", "tox", "wheel (>=0.32.0)", "zest.releaser[recommended]"] +doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] + +[[package]] +name = "faker" +version = "15.3.4" +description = "Faker is a Python package that generates fake data for you." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Faker-15.3.4-py3-none-any.whl", hash = "sha256:c2a2ff9dd8dfd991109b517ab98d5cb465e857acb45f6b643a0e284a9eb2cc76"}, + {file = "Faker-15.3.4.tar.gz", hash = "sha256:2d5443724f640ce07658ca8ca8bbd40d26b58914e63eec6549727869aa67e2cc"}, +] + +[package.dependencies] +python-dateutil = ">=2.4" + +[[package]] +name = "filelock" +version = "3.9.0" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, + {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, +] + +[package.extras] +docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "freezegun" +version = "1.2.2" +description = "Let your Python tests travel through time" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "freezegun-1.2.2-py3-none-any.whl", hash = "sha256:ea1b963b993cb9ea195adbd893a48d573fda951b0da64f60883d7e988b606c9f"}, + {file = "freezegun-1.2.2.tar.gz", hash = "sha256:cd22d1ba06941384410cd967d8a99d5ae2442f57dfafeff2fda5de8dc5c05446"}, +] + +[package.dependencies] +python-dateutil = ">=2.7" + +[[package]] +name = "gunicorn" +version = "20.1.0" +description = "WSGI HTTP Server for UNIX" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, + {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, +] + +[package.dependencies] +setuptools = ">=3.0" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "identify" +version = "2.5.11" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "identify-2.5.11-py2.py3-none-any.whl", hash = "sha256:e7db36b772b188099616aaf2accbee122949d1c6a1bac4f38196720d6f9f06db"}, + {file = "identify-2.5.11.tar.gz", hash = "sha256:14b7076b29c99b1b0b8b08e96d448c7b877a9b07683cd8cfda2ea06af85ffa1c"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] + +[[package]] +name = "ipython" +version = "8.7.0" +description = "IPython: Productive Interactive Computing" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ipython-8.7.0-py3-none-any.whl", hash = "sha256:352042ddcb019f7c04e48171b4dd78e4c4bb67bf97030d170e154aac42b656d9"}, + {file = "ipython-8.7.0.tar.gz", hash = "sha256:882899fe78d5417a0aa07f995db298fa28b58faeba2112d2e3a4c95fe14bb738"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=3.0.11,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" + +[package.extras] +all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.20)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.20)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] + +[[package]] +name = "isort" +version = "5.11.4" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"}, + {file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3,<0.5.0)"] +pipfile-deprecated-finder = ["pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "jedi" +version = "0.18.2" +description = "An autocompletion tool for Python that can be used for text editors." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.18.2-py2.py3-none-any.whl", hash = "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e"}, + {file = "jedi-0.18.2.tar.gz", hash = "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612"}, +] + +[package.dependencies] +parso = ">=0.8.0,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "lazy-object-proxy" +version = "1.8.0" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "lazy-object-proxy-1.8.0.tar.gz", hash = "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-win32.whl", hash = "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-win32.whl", hash = "sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win32.whl", hash = "sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-win32.whl", hash = "sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-win32.whl", hash = "sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0"}, + {file = "lazy_object_proxy-1.8.0-pp37-pypy37_pp73-any.whl", hash = "sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891"}, + {file = "lazy_object_proxy-1.8.0-pp38-pypy38_pp73-any.whl", hash = "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec"}, + {file = "lazy_object_proxy-1.8.0-pp39-pypy39_pp73-any.whl", hash = "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8"}, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] + +[[package]] +name = "networkx" +version = "2.8.8" +description = "Python package for creating and manipulating graphs and networks" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "networkx-2.8.8-py3-none-any.whl", hash = "sha256:e435dfa75b1d7195c7b8378c3859f0445cd88c6b0375c181ed66823a9ceb7524"}, + {file = "networkx-2.8.8.tar.gz", hash = "sha256:230d388117af870fce5647a3c52401fcf753e94720e6ea6b4197a5355648885e"}, +] + +[package.extras] +default = ["matplotlib (>=3.4)", "numpy (>=1.19)", "pandas (>=1.3)", "scipy (>=1.8)"] +developer = ["mypy (>=0.982)", "pre-commit (>=2.20)"] +doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.2)", "pydata-sphinx-theme (>=0.11)", "sphinx (>=5.2)", "sphinx-gallery (>=0.11)", "texext (>=0.6.6)"] +extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.9)", "sympy (>=1.10)"] +test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"] + +[[package]] +name = "nodeenv" +version = "1.7.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + +[[package]] +name = "packaging" +version = "22.0" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"}, + {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"}, +] + +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "pathspec" +version = "0.10.3" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, + {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, +] + +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] + +[[package]] +name = "platformdirs" +version = "2.6.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, + {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, +] + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "2.21.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "prompt-toolkit" +version = "3.0.36" +description = "Library for building powerful interactive command lines in Python" +category = "dev" +optional = false +python-versions = ">=3.6.2" +files = [ + {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, + {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psycopg2" +version = "2.9.5" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "psycopg2-2.9.5-cp310-cp310-win32.whl", hash = "sha256:d3ef67e630b0de0779c42912fe2cbae3805ebaba30cda27fea2a3de650a9414f"}, + {file = "psycopg2-2.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:4cb9936316d88bfab614666eb9e32995e794ed0f8f6b3b718666c22819c1d7ee"}, + {file = "psycopg2-2.9.5-cp311-cp311-win32.whl", hash = "sha256:093e3894d2d3c592ab0945d9eba9d139c139664dcf83a1c440b8a7aa9bb21955"}, + {file = "psycopg2-2.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:920bf418000dd17669d2904472efeab2b20546efd0548139618f8fa305d1d7ad"}, + {file = "psycopg2-2.9.5-cp36-cp36m-win32.whl", hash = "sha256:b9ac1b0d8ecc49e05e4e182694f418d27f3aedcfca854ebd6c05bb1cffa10d6d"}, + {file = "psycopg2-2.9.5-cp36-cp36m-win_amd64.whl", hash = "sha256:fc04dd5189b90d825509caa510f20d1d504761e78b8dfb95a0ede180f71d50e5"}, + {file = "psycopg2-2.9.5-cp37-cp37m-win32.whl", hash = "sha256:922cc5f0b98a5f2b1ff481f5551b95cd04580fd6f0c72d9b22e6c0145a4840e0"}, + {file = "psycopg2-2.9.5-cp37-cp37m-win_amd64.whl", hash = "sha256:1e5a38aa85bd660c53947bd28aeaafb6a97d70423606f1ccb044a03a1203fe4a"}, + {file = "psycopg2-2.9.5-cp38-cp38-win32.whl", hash = "sha256:f5b6320dbc3cf6cfb9f25308286f9f7ab464e65cfb105b64cc9c52831748ced2"}, + {file = "psycopg2-2.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:1a5c7d7d577e0eabfcf15eb87d1e19314c8c4f0e722a301f98e0e3a65e238b4e"}, + {file = "psycopg2-2.9.5-cp39-cp39-win32.whl", hash = "sha256:322fd5fca0b1113677089d4ebd5222c964b1760e361f151cbb2706c4912112c5"}, + {file = "psycopg2-2.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:190d51e8c1b25a47484e52a79638a8182451d6f6dff99f26ad9bd81e5359a0fa"}, + {file = "psycopg2-2.9.5.tar.gz", hash = "sha256:a5246d2e683a972e2187a8714b5c2cf8156c064629f9a9b1a873c1730d9e245a"}, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.5" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "psycopg2-binary-2.9.5.tar.gz", hash = "sha256:33e632d0885b95a8b97165899006c40e9ecdc634a529dca7b991eb7de4ece41c"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:0775d6252ccb22b15da3b5d7adbbf8cfe284916b14b6dc0ff503a23edb01ee85"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec46ed947801652c9643e0b1dc334cfb2781232e375ba97312c2fc256597632"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3520d7af1ebc838cc6084a3281145d5cd5bdd43fdef139e6db5af01b92596cb7"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cbc554ba47ecca8cd3396ddaca85e1ecfe3e48dd57dc5e415e59551affe568e"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:5d28ecdf191db558d0c07d0f16524ee9d67896edf2b7990eea800abeb23ebd61"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:b9c33d4aef08dfecbd1736ceab8b7b3c4358bf10a0121483e5cd60d3d308cc64"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:05b3d479425e047c848b9782cd7aac9c6727ce23181eb9647baf64ffdfc3da41"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1e491e6489a6cb1d079df8eaa15957c277fdedb102b6a68cfbf40c4994412fd0"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:9e32cedc389bcb76d9f24ea8a012b3cb8385ee362ea437e1d012ffaed106c17d"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:46850a640df62ae940e34a163f72e26aca1f88e2da79148e1862faaac985c302"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-win32.whl", hash = "sha256:3d790f84201c3698d1bfb404c917f36e40531577a6dda02e45ba29b64d539867"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:1764546ffeaed4f9428707be61d68972eb5ede81239b46a45843e0071104d0dd"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-macosx_10_9_universal2.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:426c2ae999135d64e6a18849a7d1ad0e1bd007277e4a8f4752eaa40a96b550ff"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cf1d44e710ca3a9ce952bda2855830fe9f9017ed6259e01fcd71ea6287565f5"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:024030b13bdcbd53d8a93891a2cf07719715724fc9fee40243f3bd78b4264b8f"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcda1c84a1c533c528356da5490d464a139b6e84eb77cc0b432e38c5c6dd7882"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:2ef892cabdccefe577088a79580301f09f2a713eb239f4f9f62b2b29cafb0577"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_24_ppc64le.whl", hash = "sha256:af0516e1711995cb08dc19bbd05bec7dbdebf4185f68870595156718d237df3e"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e72c91bda9880f097c8aa3601a2c0de6c708763ba8128006151f496ca9065935"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e67b3c26e9b6d37b370c83aa790bbc121775c57bfb096c2e77eacca25fd0233b"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5fc447058d083b8c6ac076fc26b446d44f0145308465d745fba93a28c14c9e32"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d892bfa1d023c3781a3cab8dd5af76b626c483484d782e8bd047c180db590e4c"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-win32.whl", hash = "sha256:2abccab84d057723d2ca8f99ff7b619285d40da6814d50366f61f0fc385c3903"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:bef7e3f9dc6f0c13afdd671008534be5744e0e682fb851584c8c3a025ec09720"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:6e63814ec71db9bdb42905c925639f319c80e7909fb76c3b84edc79dadef8d60"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:212757ffcecb3e1a5338d4e6761bf9c04f750e7d027117e74aa3cd8a75bb6fbd"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f8a9bcab7b6db2e3dbf65b214dfc795b4c6b3bb3af922901b6a67f7cb47d5f8"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:56b2957a145f816726b109ee3d4e6822c23f919a7d91af5a94593723ed667835"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:f95b8aca2703d6a30249f83f4fe6a9abf2e627aa892a5caaab2267d56be7ab69"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:70831e03bd53702c941da1a1ad36c17d825a24fbb26857b40913d58df82ec18b"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:dbc332beaf8492b5731229a881807cd7b91b50dbbbaf7fe2faf46942eda64a24"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:2d964eb24c8b021623df1c93c626671420c6efadbdb8655cb2bd5e0c6fa422ba"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:95076399ec3b27a8f7fa1cc9a83417b1c920d55cf7a97f718a94efbb96c7f503"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-win32.whl", hash = "sha256:3fc33295cfccad697a97a76dec3f1e94ad848b7b163c3228c1636977966b51e2"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-win_amd64.whl", hash = "sha256:02551647542f2bf89073d129c73c05a25c372fc0a49aa50e0de65c3c143d8bd0"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:63e318dbe52709ed10d516a356f22a635e07a2e34c68145484ed96a19b0c4c68"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7e518a0911c50f60313cb9e74a169a65b5d293770db4770ebf004245f24b5c5"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d38a4656e4e715d637abdf7296e98d6267df0cc0a8e9a016f8ba07e4aa3eeb"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:68d81a2fe184030aa0c5c11e518292e15d342a667184d91e30644c9d533e53e1"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:7ee3095d02d6f38bd7d9a5358fcc9ea78fcdb7176921528dd709cc63f40184f5"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:46512486be6fbceef51d7660dec017394ba3e170299d1dc30928cbedebbf103a"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b911dfb727e247340d36ae20c4b9259e4a64013ab9888ccb3cbba69b77fd9636"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:422e3d43b47ac20141bc84b3d342eead8d8099a62881a501e97d15f6addabfe9"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c5682a45df7d9642eff590abc73157c887a68f016df0a8ad722dcc0f888f56d7"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-win32.whl", hash = "sha256:b8104f709590fff72af801e916817560dbe1698028cd0afe5a52d75ceb1fce5f"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-win_amd64.whl", hash = "sha256:7b3751857da3e224f5629400736a7b11e940b5da5f95fa631d86219a1beaafec"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:043a9fd45a03858ff72364b4b75090679bd875ee44df9c0613dc862ca6b98460"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9ffdc51001136b699f9563b1c74cc1f8c07f66ef7219beb6417a4c8aaa896c28"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c15ba5982c177bc4b23a7940c7e4394197e2d6a424a2d282e7c236b66da6d896"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc85b3777068ed30aff8242be2813038a929f2084f69e43ef869daddae50f6ee"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:215d6bf7e66732a514f47614f828d8c0aaac9a648c46a831955cb103473c7147"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:7d07f552d1e412f4b4e64ce386d4c777a41da3b33f7098b6219012ba534fb2c2"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a0adef094c49f242122bb145c3c8af442070dc0e4312db17e49058c1702606d4"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:00475004e5ed3e3bf5e056d66e5dcdf41a0dc62efcd57997acd9135c40a08a50"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7d88db096fa19d94f433420eaaf9f3c45382da2dd014b93e4bf3215639047c16"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:902844f9c4fb19b17dfa84d9e2ca053d4a4ba265723d62ea5c9c26b38e0aa1e6"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-win32.whl", hash = "sha256:4e7904d1920c0c89105c0517dc7e3f5c20fb4e56ba9cdef13048db76947f1d79"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:a36a0e791805aa136e9cbd0ffa040d09adec8610453ee8a753f23481a0057af5"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:25382c7d174c679ce6927c16b6fbb68b10e56ee44b1acb40671e02d29f2fce7c"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9c38d3869238e9d3409239bc05bc27d6b7c99c2a460ea337d2814b35fb4fea1b"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c6527c8efa5226a9e787507652dd5ba97b62d29b53c371a85cd13f957fe4d42"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e59137cdb970249ae60be2a49774c6dfb015bd0403f05af1fe61862e9626642d"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:d4c7b3a31502184e856df1f7bbb2c3735a05a8ce0ade34c5277e1577738a5c91"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:b9a794cef1d9c1772b94a72eec6da144c18e18041d294a9ab47669bc77a80c1d"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5254cbd4f4855e11cebf678c1a848a3042d455a22a4ce61349c36aafd4c2267"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c5e65c6ac0ae4bf5bef1667029f81010b6017795dcb817ba5c7b8a8d61fab76f"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:74eddec4537ab1f701a1647214734bc52cee2794df748f6ae5908e00771f180a"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:01ad49d68dd8c5362e4bfb4158f2896dc6e0c02e87b8a3770fc003459f1a4425"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-win32.whl", hash = "sha256:937880290775033a743f4836aa253087b85e62784b63fd099ee725d567a48aa1"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:484405b883630f3e74ed32041a87456c5e0e63a8e3429aa93e8714c366d62bd1"}, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "pygments" +version = "2.13.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, + {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pyjwt" +version = "2.6.0" +description = "JSON Web Token implementation in Python" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"}, + {file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "pylint" +version = "2.15.9" +description = "python code static checker" +category = "dev" +optional = false +python-versions = ">=3.7.2" +files = [ + {file = "pylint-2.15.9-py3-none-any.whl", hash = "sha256:349c8cd36aede4d50a0754a8c0218b43323d13d5d88f4b2952ddfe3e169681eb"}, + {file = "pylint-2.15.9.tar.gz", hash = "sha256:18783cca3cfee5b83c6c5d10b3cdb66c6594520ffae61890858fe8d932e1c6b4"}, +] + +[package.dependencies] +astroid = ">=2.12.13,<=2.14.0-dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = {version = ">=0.2", markers = "python_version < \"3.11\""} +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +tomlkit = ">=0.10.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pylint-django" +version = "2.5.3" +description = "A Pylint plugin to help Pylint understand the Django web framework" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "pylint-django-2.5.3.tar.gz", hash = "sha256:0ac090d106c62fe33782a1d01bda1610b761bb1c9bf5035ced9d5f23a13d8591"}, + {file = "pylint_django-2.5.3-py3-none-any.whl", hash = "sha256:56b12b6adf56d548412445bd35483034394a1a94901c3f8571980a13882299d5"}, +] + +[package.dependencies] +pylint = ">=2.0,<3" +pylint-plugin-utils = ">=0.7" + +[package.extras] +for-tests = ["coverage", "django-tables2", "django-tastypie", "factory-boy", "pylint (>=2.13)", "pytest", "wheel"] +with-django = ["Django"] + +[[package]] +name = "pylint-plugin-utils" +version = "0.7" +description = "Utilities and helpers for writing Pylint plugins" +category = "dev" +optional = false +python-versions = ">=3.6.2" +files = [ + {file = "pylint-plugin-utils-0.7.tar.gz", hash = "sha256:ce48bc0516ae9415dd5c752c940dfe601b18fe0f48aa249f2386adfa95a004dd"}, + {file = "pylint_plugin_utils-0.7-py3-none-any.whl", hash = "sha256:b3d43e85ab74c4f48bb46ae4ce771e39c3a20f8b3d56982ab17aa73b4f98d535"}, +] + +[package.dependencies] +pylint = ">=1.7" + +[[package]] +name = "pytest" +version = "7.2.0" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, + {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, +] + +[package.dependencies] +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-django" +version = "4.5.2" +description = "A Django plugin for pytest." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"}, + {file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"}, +] + +[package.dependencies] +pytest = ">=5.4.0" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +testing = ["Django", "django-configurations (>=2.0)"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-social-auth" +version = "0.3.6" +description = "Python social authentication made simple." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "python-social-auth-0.3.6.tar.gz", hash = "sha256:6986220df76934aee15c54938a13ebe370a1976e043af01fa3bea417f6722e74"}, + {file = "python_social_auth-0.3.6-py2-none-any.whl", hash = "sha256:9aaad6f7bf83fd129620eadaab1e78e63200da61a5d8898fd1aa243686b36575"}, + {file = "python_social_auth-0.3.6-py3-none-any.whl", hash = "sha256:8aac8531f7dd4d1b9af606654532fef4f615cb437774a7a1f8dcdebe89c17664"}, +] + +[package.dependencies] +social-auth-core = "*" + +[package.extras] +cherrypy = ["social-auth-app-cherrypy"] +django = ["social-auth-app-django"] +django-mongoengine = ["social-auth-app-django-mongoengine"] +flask = ["social-auth-app-flask", "social-auth-app-flask-sqlalchemy"] +flask-mongoengine = ["social-auth-app-flask-mongoengine"] +flask-peewee = ["social-auth-app-flask-peewee"] +pyramid = ["social-auth-app-pyramid"] +tornado = ["social-auth-app-tornado"] +webpy = ["social-auth-app-webpy"] + +[[package]] +name = "python3-openid" +version = "3.2.0" +description = "OpenID support for modern servers and consumers." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "python3-openid-3.2.0.tar.gz", hash = "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf"}, + {file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"}, +] + +[package.dependencies] +defusedxml = "*" + +[package.extras] +mysql = ["mysql-connector-python"] +postgresql = ["psycopg2"] + +[[package]] +name = "pytz" +version = "2022.7" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2022.7-py2.py3-none-any.whl", hash = "sha256:93007def75ae22f7cd991c84e02d434876818661f8df9ad5df9e950ff4e52cfd"}, + {file = "pytz-2022.7.tar.gz", hash = "sha256:7ccfae7b4b2c067464a6733c6261673fdb8fd1be905460396b97a073e9fa683a"}, +] + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] + +[[package]] +name = "requests" +version = "2.28.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7, <4" +files = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-oauthlib" +version = "1.3.1" +description = "OAuthlib authentication support for Requests." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, + {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + +[[package]] +name = "s3transfer" +version = "0.6.0" +description = "An Amazon S3 Transfer Manager" +category = "main" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "s3transfer-0.6.0-py3-none-any.whl", hash = "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd"}, + {file = "s3transfer-0.6.0.tar.gz", hash = "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"}, +] + +[package.dependencies] +botocore = ">=1.12.36,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] + +[[package]] +name = "sentry-sdk" +version = "1.12.1" +description = "Python client for Sentry (https://sentry.io)" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "sentry-sdk-1.12.1.tar.gz", hash = "sha256:5bbe4b72de22f9ac1e67f2a4e6efe8fbd595bb59b7b223443f50fe5802a5551c"}, + {file = "sentry_sdk-1.12.1-py2.py3-none-any.whl", hash = "sha256:9f0b960694e2d8bb04db4ba6ac2a645040caef4e762c65937998ff06064f10d6"}, +] + +[package.dependencies] +certifi = "*" +urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""} + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +beam = ["apache-beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +chalice = ["chalice (>=1.16.0)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +fastapi = ["fastapi (>=0.79.0)"] +flask = ["blinker (>=1.1)", "flask (>=0.11)"] +httpx = ["httpx (>=0.16.0)"] +opentelemetry = ["opentelemetry-distro (>=0.350b0)"] +pure-eval = ["asttokens", "executing", "pure-eval"] +pymongo = ["pymongo (>=3.1)"] +pyspark = ["pyspark (>=2.4.4)"] +quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +starlette = ["starlette (>=0.19.1)"] +tornado = ["tornado (>=5)"] + +[[package]] +name = "setuptools" +version = "65.6.3" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, + {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "social-auth-app-django" +version = "5.0.0" +description = "Python Social Authentication, Django integration." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "social-auth-app-django-5.0.0.tar.gz", hash = "sha256:b6e3132ce087cdd6e1707aeb1b588be41d318408fcf6395435da0bc6fe9a9795"}, + {file = "social_auth_app_django-5.0.0-py3-none-any.whl", hash = "sha256:52241a25445a010ab1c108bafff21fc5522d5c8cd0d48a92c39c7371824b065d"}, +] + +[package.dependencies] +social-auth-core = ">=4.1.0" + +[[package]] +name = "social-auth-core" +version = "4.3.0" +description = "Python social authentication made simple." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "social-auth-core-4.3.0.tar.gz", hash = "sha256:4686f0e43cf12954216875a32e944847bb1dc69e7cd9573d16a9003bb05ca477"}, + {file = "social_auth_core-4.3.0-py3-none-any.whl", hash = "sha256:1e3440d104f743b02dfe258c9d4dba5b4065abf24b2f7eb362b47054d21797df"}, +] + +[package.dependencies] +cryptography = ">=1.4" +defusedxml = ">=0.5.0rc1" +oauthlib = ">=1.0.3" +PyJWT = ">=2.0.0" +python3-openid = ">=3.0.10" +requests = ">=2.9.1" +requests-oauthlib = ">=0.6.1" + +[package.extras] +all = ["cryptography (>=2.1.1)", "lxml (<4.7)", "python-jose (>=3.0.0)", "python3-saml (>=1.2.1)"] +allpy3 = ["cryptography (>=2.1.1)", "lxml (<4.7)", "python-jose (>=3.0.0)", "python3-saml (>=1.2.1)"] +azuread = ["cryptography (>=2.1.1)"] +openidconnect = ["python-jose (>=3.0.0)"] +saml = ["lxml (<4.7)", "python3-saml (>=1.2.1)"] + +[[package]] +name = "sqlparse" +version = "0.4.3" +description = "A non-validating SQL parser." +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"}, + {file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"}, +] + +[[package]] +name = "stack-data" +version = "0.6.2" +description = "Extract data from python stack frames and tracebacks for informative displays" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.2-py3-none-any.whl", hash = "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"}, + {file = "stack_data-0.6.2.tar.gz", hash = "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tomlkit" +version = "0.11.6" +description = "Style preserving TOML library" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, + {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, +] + +[[package]] +name = "traitlets" +version = "5.8.0" +description = "Traitlets Python configuration system" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "traitlets-5.8.0-py3-none-any.whl", hash = "sha256:c864831efa0ba6576d09b44884b34e41defc18c0d7e720b4a2d6698c842cab3e"}, + {file = "traitlets-5.8.0.tar.gz", hash = "sha256:6cc57d6dc28c85d5365961726ffd19b538739347749e13ebe34e03323a0e8f84"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] + +[[package]] +name = "typing-extensions" +version = "4.4.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, +] + +[[package]] +name = "tzdata" +version = "2022.7" +description = "Provider of IANA time zone data" +category = "main" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2022.7-py2.py3-none-any.whl", hash = "sha256:2b88858b0e3120792a3c0635c23daf36a7d7eeeca657c323da299d2094402a0d"}, + {file = "tzdata-2022.7.tar.gz", hash = "sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"}, +] + +[[package]] +name = "urllib3" +version = "1.26.13" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, + {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "virtualenv" +version = "20.17.1" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"}, + {file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"}, +] + +[package.dependencies] +distlib = ">=0.3.6,<1" +filelock = ">=3.4.1,<4" +platformdirs = ">=2.4,<3" + +[package.extras] +docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] +testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] + +[[package]] +name = "whitenoise" +version = "6.2.0" +description = "Radically simplified static file serving for WSGI applications" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "whitenoise-6.2.0-py3-none-any.whl", hash = "sha256:8e9c600a5c18bd17655ef668ad55b5edf6c24ce9bdca5bf607649ca4b1e8e2c2"}, + {file = "whitenoise-6.2.0.tar.gz", hash = "sha256:8fa943c6d4cd9e27673b70c21a07b0aa120873901e099cd46cab40f7cc96d567"}, +] + +[package.extras] +brotli = ["Brotli"] + +[[package]] +name = "wrapt" +version = "1.14.1" +description = "Module for decorators, wrappers and monkey patching." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, + {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, + {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, + {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, + {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, + {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, + {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, + {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, + {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, + {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, + {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, + {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, + {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, + {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, + {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, + {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "3.9.13" +content-hash = "ca6c4a7bb9b77811e1508074aed26ed8e3bc8467db9224a11b448cd763ddc7de" diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 00000000..5fcef8cd --- /dev/null +++ b/poetry.toml @@ -0,0 +1,3 @@ +[virtualenvs] +in-project = true +prefer-active-python = true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..fb1ffb3e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,78 @@ +### project info ### + +[tool.poetry] +name = "csm-web" +version = "0.1.0" +description = "" +authors = [] +readme = "README.md" + +### dependencies ### + +[tool.poetry.dependencies] +python = "3.9.13" +# AWS +boto3 = "^1.26.39" +# django +django = "^4.1.4" +django-csp = "^3.7" +django-extensions = "^3.2.1" +django-heroku = "^0.3.1" +django-postgres-extra = "^2.0.6" +django-storages = "^1.13.2" +djangorestframework = "^3.14.0" +djangorestframework-camel-case = "^1.3.0" +drf-nested-forms = "^1.1.7" +psycopg2-binary = "^2.9.5" +python-social-auth = "^0.3.6" +social-auth-app-django = "^5.0.0" +# heroku +gunicorn = "^20.1.0" +sentry-sdk = "^1.12.1" +# factories +factory-boy = "^3.2.1" +# misc +networkx = "^2.8.8" + +[tool.poetry.group.dev.dependencies] +# formatting/linting +black = "^22.12.0" +isort = "^5.11.4" +pre-commit = "^2.21.0" +pylint = "^2.15.9" +pylint-django = "^2.5.3" +# console +ipython = "^8.7.0" +# packages for testing +pytest = "^7.2.0" +pytest-django = "^4.5.2" +freezegun = "^1.2.2" + +### tools ### + +[tool.isort] +profile = "black" + +[tool.black] +preview = true + +[tool.pylint.MASTER] +load-plugins = "pylint_django" +# allow snake_case names of any length +variable-rgx = "^[a-z_][a-z0-9]*((_[a-z0-9]+)*)?$" +argument-rgx = "^[a-z_][a-z0-9]*((_[a-z0-9]+)*)?$" +# don't enforce module naming +module-naming-style = "any" +# ensure app imports are handled correctly +init-hook = "import sys; sys.path.insert(0, 'csm_web')" + + +[tool.pylint."MESSAGES CONTROL"] +# design: num. branches, num. lines, etc +# similarities: simiar chunks of code +# C0114: missing module docstring +# C0115: missing class docstring +disable = "design,similarities,C0114,C0115" + +[tool.pylint.PYLINT_DJANGO] +django-settings-module = "csm_web.settings" diff --git a/requirements.txt b/requirements.txt index 95dcfbc1..8b3cdb4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,54 +1,45 @@ -ansimarkup==1.4.0 -asgiref==3.4.1 -boto3==1.18.32 -botocore==1.21.32 -Brotli==1.0.9 -certifi==2022.12.7 -cffi==1.14.6 -chardet==4.0.0 -charset-normalizer==2.0.4 -colorama==0.4.4 -cryptography==39.0.1 -defusedxml==0.7.1 -dj-database-url==0.5.0 -Django==3.2.19 -django-csp==3.7 -django-extensions==3.1.3 -django-heroku==0.3.1 -django-postgres-extra==2.0.2 -django-storages==1.11.1 -djangorestframework==3.12.4 -djangorestframework-camel-case==1.2.0 -drf-nested-forms==1.1.6 -enforce==0.3.4 -factory-boy==3.2.0 -Faker==8.12.1 -freezegun==1.1.0 -gunicorn==20.1.0 -idna==3.2 -jmespath==0.10.0 -networkx==2.8.3 -oauthlib==3.2.2 -psycopg2==2.9.1 -psycopg2-binary==2.9.1 -pycparser==2.20 -Pygments==2.10.0 -PyJWT==2.4.0 -pytest-django==4.5.2 -python-dateutil==2.8.0 -python-social-auth==0.3.6 -python3-openid==3.2.0 -pytz==2021.1 -PyYAML==5.4.1 -requests==2.31.0 -requests-oauthlib==1.3.0 -s3transfer==0.5.0 -sentry-sdk==1.14.0 -six==1.16.0 -social-auth-app-django==5.0.0 -social-auth-core==4.1.0 -sqlparse==0.4.4 -text-unidecode==1.3 -urllib3==1.26.11 -whitenoise==5.3.0 -wrapt==1.12.1 +asgiref==3.6.0 ; python_full_version == "3.9.13" +boto3==1.26.39 ; python_full_version == "3.9.13" +botocore==1.29.39 ; python_full_version == "3.9.13" +certifi==2022.12.7 ; python_full_version == "3.9.13" +cffi==1.15.1 ; python_full_version == "3.9.13" +charset-normalizer==2.1.1 ; python_full_version == "3.9.13" +cryptography==38.0.4 ; python_full_version == "3.9.13" +defusedxml==0.7.1 ; python_full_version == "3.9.13" +dj-database-url==1.2.0 ; python_full_version == "3.9.13" +django-csp==3.7 ; python_full_version == "3.9.13" +django-extensions==3.2.1 ; python_full_version == "3.9.13" +django-heroku==0.3.1 ; python_full_version == "3.9.13" +django-postgres-extra==2.0.6 ; python_full_version == "3.9.13" +django-storages==1.13.2 ; python_full_version == "3.9.13" +django==4.1.4 ; python_full_version == "3.9.13" +djangorestframework-camel-case==1.3.0 ; python_full_version == "3.9.13" +djangorestframework==3.14.0 ; python_full_version == "3.9.13" +drf-nested-forms==1.1.7 ; python_full_version == "3.9.13" +factory-boy==3.2.1 ; python_full_version == "3.9.13" +faker==15.3.4 ; python_full_version == "3.9.13" +gunicorn==20.1.0 ; python_full_version == "3.9.13" +idna==3.4 ; python_full_version == "3.9.13" +jmespath==1.0.1 ; python_full_version == "3.9.13" +networkx==2.8.8 ; python_full_version == "3.9.13" +oauthlib==3.2.2 ; python_full_version == "3.9.13" +psycopg2-binary==2.9.5 ; python_full_version == "3.9.13" +psycopg2==2.9.5 ; python_full_version == "3.9.13" +pycparser==2.21 ; python_full_version == "3.9.13" +pyjwt==2.6.0 ; python_full_version == "3.9.13" +python-dateutil==2.8.2 ; python_full_version == "3.9.13" +python-social-auth==0.3.6 ; python_full_version == "3.9.13" +python3-openid==3.2.0 ; python_full_version == "3.9.13" +pytz==2022.7 ; python_full_version == "3.9.13" +requests-oauthlib==1.3.1 ; python_full_version == "3.9.13" +requests==2.28.1 ; python_full_version == "3.9.13" +s3transfer==0.6.0 ; python_full_version == "3.9.13" +sentry-sdk==1.12.1 ; python_full_version == "3.9.13" +setuptools==65.6.3 ; python_full_version == "3.9.13" +six==1.16.0 ; python_full_version == "3.9.13" +social-auth-app-django==5.0.0 ; python_full_version == "3.9.13" +social-auth-core==4.3.0 ; python_full_version == "3.9.13" +sqlparse==0.4.3 ; python_full_version == "3.9.13" +tzdata==2022.7 ; sys_platform == "win32" and python_full_version == "3.9.13" +urllib3==1.26.13 ; python_full_version == "3.9.13" +whitenoise==6.2.0 ; python_full_version == "3.9.13" diff --git a/setup.sh b/setup.sh index b9316207..232ea100 100755 --- a/setup.sh +++ b/setup.sh @@ -2,21 +2,48 @@ # Sets up environment variables and such, assuming requirements already installed. # Kevin Svetlitski, rev. Jonathan Shi +# check directory { [ ! -d 'csm_web' ] || [ ! -f 'package.json' ]; } && echo 'You are in the wrong directory!' 1>&2 && exit 1 +# should have virtual environment enabled [ -z "$VIRTUAL_ENV" ] && echo 'You must activate your virtualenv first!' 1>&2 && exit 1 -if ! ( command -v npm && command -v pip3 && command -v psql ) >/dev/null + +# check all required dependencies are installed +if ! ( command -v npm ) > /dev/null +then + echo 'You must install npm before running this script! (It is recommended to use nvm to manage npm versions; see https://github.com/nvm-sh/nvm#installing-and-updating)' 1>&2 + exit 1 +elif ! ( command -v python3 && command -v pip3 ) > /dev/null +then + echo 'You must have python3 and pip3 installed before running this script! (It is recommended to use pyenv to manage python versions; see https://github.com/pyenv/pyenv#installation)' 1>&2 + exit 1 +elif ! ( command -v poetry ) > /dev/null +then + echo 'You must have poetry installed before running this script! (See https://python-poetry.org/docs/#installation)' 1>&2 + exit 1 +elif ! ( command -v psql ) > /dev/null then - echo 'You must install npm, python3, pip, and postgres before running this script!' 1>&2 - exit 1 + echo 'You must have postgres installed before running this script! (See https://www.postgresql.org/download)' 1>&2 + exit 1 +fi + +# check python version +EXPECTED_PYTHON_VERSION=$(sed 's/[^0-9.]//g' runtime.txt) +ACTUAL_PYTHON_VERSION=$(python3 --version | sed 's/[^0-9.]//g') +if [[ "$EXPECTED_PYTHON_VERSION" != "$ACTUAL_PYTHON_VERSION" ]] +then + echo "Expected python version $EXPECTED_PYTHON_VERSION, got $ACTUAL_PYTHON_VERSION; make sure you have the right python version installed." 1>&2 + echo "It is recommended to use pyenv to manage python versions; see https://github.com/pyenv/pyenv#installation. make sure you've created the virtual environment with the correct python version." + echo "If your system Python version is not $EXPECTED_PYTHON_VERSION, make sure you created the virtual environment with the prefix: PYENV_VERSION=$EXPECTED_PYTHON_VERSION" + exit 1 fi echo 'Beginning setup, this may take a minute or so...' -sleep 1 # Give user time to read above message +sleep 1 # Give user time to read above message # Node and Python requirements -npm i +npm ci # The LDFLAGS are specified so that heroku's implicit psycopg2 (*not* binary) dependency will build successfully on macOS -LDFLAGS="-I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib" pip3 install --no-cache-dir -r requirements.txt +LDFLAGS="-I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib" poetry install --no-root --with=dev # Set up environment variables echo "Setting up environment variables..." @@ -31,19 +58,6 @@ export SECRET_KEY='temp' # set temporary secret key export SECRET_KEY=$(python3 csm_web/manage.py shell -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())") echo "SECRET_KEY='$SECRET_KEY'" >> .env -# Must be pulled from Heroku (but not critical to running tests etc.) -if ! command -v heroku 1>/dev/null -then - echo "Did not find Heroku CLI installation. OAUTH keys have not been set, so you will be unable to log in with your email." -elif heroku whoami 1>/dev/null -then - echo "Attempting to set OAUTH keys from Heroku config..." - heroku config:get SOCIAL_AUTH_GOOGLE_OAUTH2_KEY -s >> .env - heroku config:get SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET -s >> .env -else - echo "You are not logged into the Heroku CLI. OAUTH keys have not been set, so you will be unable to log in with your email." -fi - # Pull AWS S3 credentials if ! command -v aws 1>/dev/null then @@ -87,18 +101,13 @@ function run() { # Initialize for local development npm run dev source .env # need the relevant env variables for django, but can't count on the Heroku CLI being installed -python csm_web/manage.py migrate -python csm_web/manage.py createtestdata --yes +python3 csm_web/manage.py migrate +python3 csm_web/manage.py createtestdata --yes echo "Installing pre-commit hook..." -ln -s -f ../../.pre-commit.sh .git/hooks/pre-commit - -target_version=$(sed 's/[^0-9.]//g' runtime.txt) -installed_version=$(python3 --version | sed 's/[^0-9.]//g') -if [ "$target_version" != "$installed_version" ] -then - echo "You have python version $installed_version installed, but the expected version is $target_version. -This may cause problems." -fi +#ln -s -f ../../.pre-commit.sh .git/hooks/pre-commit +pre-commit install -echo "Done installing. Please reactivate your virtualenv before running any more commands." +echo +echo "Done installing." +echo "Please reactivate the virtual environment before running other commands."