diff --git a/.github/actions/start-build/action.yml b/.github/actions/start-build/action.yml index ea4fa0ff66a..d1b124ef50e 100644 --- a/.github/actions/start-build/action.yml +++ b/.github/actions/start-build/action.yml @@ -27,8 +27,8 @@ runs: - name: Copy Settings shell: bash run: | - cp website/settings/local-travis.py website/settings/local.py - cp api/base/settings/local-travis.py api/base/settings/local.py + cp website/settings/local-ci.py website/settings/local.py + cp api/base/settings/local-ci.py api/base/settings/local.py mkdir -p ~/preprints touch ~/preprints/index.html - name: PIP install @@ -41,7 +41,7 @@ runs: shell: bash run: | # bumped psycopg to match requirements.txt, as otherwise build would fail - poetry run python3 -m invoke travis-addon-settings + poetry run python3 -m invoke ci-addon-settings pip uninstall uritemplate.py --yes # use yarn add --exact to match versions in yarn.lock w/o installing all deps yarn add --exact bower@^1.8.8 diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 7fe875888d9..b8ac8c6ad5b 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -39,7 +39,7 @@ jobs: postgres: image: postgres env: - POSTGRES_PASSWORD: postgres + POSTGRES_PASSWORD: ${{ env.OSF_DB_PASSWORD }} options: >- --health-cmd pg_isready --health-interval 10s @@ -52,7 +52,7 @@ jobs: - uses: actions/checkout@v2 - uses: ./.github/actions/start-build - name: Run tests - run: poetry run python3 -m invoke test-travis-addons -n 1 --junit + run: poetry run python3 -m invoke test-ci-addons -n 1 --junit - name: Upload report if: (github.event_name != 'pull_request') && (success() || failure()) # run this step even if previous step failed uses: ./.github/actions/gen-report @@ -66,7 +66,7 @@ jobs: postgres: image: postgres env: - POSTGRES_PASSWORD: postgres + POSTGRES_PASSWORD: ${{ env.OSF_DB_PASSWORD }} options: >- --health-cmd pg_isready --health-interval 10s @@ -79,7 +79,7 @@ jobs: - uses: actions/checkout@v2 - uses: ./.github/actions/start-build - name: Run tests - run: poetry run python3 -m invoke test-travis-website -n 1 --junit + run: poetry run python3 -m invoke test-ci-website -n 1 --junit - name: Upload report if: (github.event_name != 'pull_request') && (success() || failure()) # run this step even if previous step failed uses: ./.github/actions/gen-report @@ -93,7 +93,7 @@ jobs: postgres: image: postgres env: - POSTGRES_PASSWORD: postgres + POSTGRES_PASSWORD: ${{ env.OSF_DB_PASSWORD }} options: >- --health-cmd pg_isready --health-interval 10s @@ -108,7 +108,7 @@ jobs: - name: NVM & yarn install run: poetry run python3 -m invoke assets --dev - name: Run test - run: poetry run python3 -m invoke test-travis-api1-and-js -n 1 --junit + run: poetry run python3 -m invoke test-ci-api1-and-js -n 1 --junit - name: Upload report if: (github.event_name != 'pull_request') && (success() || failure()) # run this step even if previous step failed uses: ./.github/actions/gen-report @@ -122,7 +122,7 @@ jobs: postgres: image: postgres env: - POSTGRES_PASSWORD: postgres + POSTGRES_PASSWORD: ${{ env.OSF_DB_PASSWORD }} options: >- --health-cmd pg_isready --health-interval 10s @@ -135,7 +135,7 @@ jobs: - uses: actions/checkout@v2 - uses: ./.github/actions/start-build - name: Run tests - run: poetry run python3 -m invoke test-travis-api2 -n 1 --junit + run: poetry run python3 -m invoke test-ci-api2 -n 1 --junit - name: Upload report if: (github.event_name != 'pull_request') && (success() || failure()) # run this step even if previous step failed uses: ./.github/actions/gen-report @@ -150,7 +150,7 @@ jobs: image: postgres env: - POSTGRES_PASSWORD: postgres + POSTGRES_PASSWORD: ${{ env.OSF_DB_PASSWORD }} options: >- --health-cmd pg_isready --health-interval 10s @@ -163,7 +163,7 @@ jobs: - uses: actions/checkout@v2 - uses: ./.github/actions/start-build - name: Run tests - run: poetry run python3 -m invoke test-travis-api3-and-osf -n 1 --junit + run: poetry run python3 -m invoke test-ci-api3-and-osf -n 1 --junit - name: Upload report if: (github.event_name != 'pull_request') && (success() || failure()) # run this step even if previous step failed uses: ./.github/actions/gen-report diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0d5c765066d..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,185 +0,0 @@ -# Config file for automatic testing at travis-ci.org - -language: python - -python: - - "3.10" - -dist: trusty - -# TODO: uncomment when https://github.com/travis-ci/travis-ci/issues/8836 is resolved -# addons: -# chrome: stable - -os: linux - -cache: - yarn: true - pip: true - directories: - - $HOME/.cache - - node_modules - - website/static/vendor/bower_components - -env: - global: - - WHEELHOUSE="$HOME/.cache/wheelhouse" - - LIBXML2_DEB="libxml2-dbg_2.9.1+dfsg1-3ubuntu4.9_amd64.deb" - - POSTGRES_DEB="postgresql-9.6_9.6.3-1.pgdg12.4+1_amd64.deb" - - ELASTICSEARCH_ARCHIVE="elasticsearch-2.4.5.tar.gz" - - ELASTICSEARCH6_ARCHIVE="elasticsearch-6.3.1.tar.gz" - - LIBJEMALLOC_DEB="libjemalloc1_3.5.1-2_amd64.deb" - - LIBPCRE_DEB="libpcre3_8.31-2ubuntu2.3_amd64.deb" - # - VARNISH_DEB="varnish_4.1.0-1~trusty_amd64.deb" - - OSF_DB_PORT="54321" - # Workaround for travis bug: see https://github.com/travis-ci/travis-ci/issues/7940#issuecomment-311411559 - - BOTO_CONFIG=/dev/null - jobs: - - TEST_BUILD="addons" - - TEST_BUILD="website" - - TEST_BUILD="api1_and_js" - - TEST_BUILD="api2" - - TEST_BUILD="api3_and_osf" - -before_install: - # cache directories - - | - mkdir -p $HOME/.cache/downloads - mkdir -p $HOME/.cache/pip - mkdir -p $HOME/.cache/wheelhouse - mkdir -p $HOME/.cache/testmon - rm -rf node_modules ## TODO remove this later - # postgres - - | - cd $HOME/.cache/downloads - - if [ ! -f "$LIBXML2_DEB" ]; then - curl -SLO http://security.ubuntu.com/ubuntu/pool/main/libx/libxml2/$LIBXML2_DEB - fi - - if [ ! -f "$POSTGRES_DEB" ]; then - curl -SLO http://apt.postgresql.org/pub/repos/apt/pool/main/p/postgresql-9.6/$POSTGRES_DEB - fi - - dpkg -x $LIBXML2_DEB /tmp/libxml2 - dpkg -x $POSTGRES_DEB /tmp/postgres - - | - export LD_LIBRARY_PATH=/tmp/libxml2/usr/lib/x86_64-linux-gnu - /tmp/postgres/usr/lib/postgresql/9.6/bin/initdb /tmp/postgres/data --nosync -U postgres - sed -i -e 's/#fsync.*/fsync = off/' /tmp/postgres/data/postgresql.conf - sed -i -e 's/#synchronous_commit.*/synchronous_commit = off/' /tmp/postgres/data/postgresql.conf - sed -i -e 's/#full_page_writes.*/full_page_writes = off/' /tmp/postgres/data/postgresql.conf - /tmp/postgres/usr/lib/postgresql/9.6/bin/postgres -k /tmp -D /tmp/postgres/data -p 54321 > /dev/null & export POSTGRES_PID=$! - # elasticsearch - - | - cd $HOME/.cache/downloads - - if [ ! -f "$ELASTICSEARCH_ARCHIVE" ]; then - curl -SLO https://download.elasticsearch.org/elasticsearch/elasticsearch/$ELASTICSEARCH_ARCHIVE - fi - - if [ ! -f "$ELASTICSEARCH_ARCHIVE.sha1.txt" ]; then - curl -SLO https://download.elasticsearch.org/elasticsearch/elasticsearch/$ELASTICSEARCH_ARCHIVE.sha1.txt - fi - - sha1sum --check $ELASTICSEARCH_ARCHIVE.sha1.txt - - mkdir -p /tmp/elasticsearch - tar xzf $ELASTICSEARCH_ARCHIVE -C /tmp/elasticsearch --strip-components=1 - - /tmp/elasticsearch/bin/elasticsearch > /dev/null & export ELASTICSEARCH_PID=$! - # Wait for elasticsearch to come online - - |- - while true; do - sleep 5 - curl -sf http://localhost:9200/_cluster/health?wait_for_status=yellow - if [ $? -eq 0 ]; then - break - fi - done - - # elasticsearch6 - - | - - if [ ! -f "$ELASTICSEARCH6_ARCHIVE" ]; then - curl -SLO https://artifacts.elastic.co/downloads/elasticsearch/$ELASTICSEARCH6_ARCHIVE - fi - - if [ ! -f "$ELASTICSEARCH6_ARCHIVE.sha1.txt" ]; then - curl -SLO https://artifacts.elastic.co/downloads/elasticsearch/$ELASTICSEARCH6_ARCHIVE.sha1.txt - fi - - sha1sum --check $ELASTICSEARCH6_ARCHIVE.sha1.txt - - mkdir -p /tmp/elasticsearch6 - tar xzf $ELASTICSEARCH6_ARCHIVE -C /tmp/elasticsearch6 --strip-components=1 - - /tmp/elasticsearch6/bin/elasticsearch > /dev/null & export ELASTICSEARCH6_PID=$! - # Wait for elasticsearch to come online - - |- - while true; do - sleep 5 - curl -sf http://localhost:9201/_cluster/health?wait_for_status=yellow - if [ $? -eq 0 ]; then - break - fi - done - - -install: - - cd $TRAVIS_BUILD_DIR - - cp website/settings/local-travis.py website/settings/local.py - - cp api/base/settings/local-travis.py api/base/settings/local.py - - '[ -d $HOME/preprints ] || ( mkdir -p $HOME/preprints && touch $HOME/preprints/index.html )' - - - travis_retry pip install --upgrade pip - - travis_retry pip install invoke==0.13.0 - - travis_retry pip install flake8==7.0.0 --force-reinstall --upgrade - - travis_retry invoke wheelhouse --dev --addons - - - | - if [ "$TEST_BUILD" = "api1_and_js" ]; then - nvm install 8.6.0 - nvm use 8.6.0 - curl -o- -L https://yarnpkg.com/install.sh | bash - export PATH=$HOME/.yarn/bin:$PATH - travis_retry invoke assets --dev - fi - - - travis_retry invoke travis_addon_settings - # bumped psycopg to match requirements.txt, as otherwise build would fail - - travis_retry pip install psycopg2==2.9.9 --no-binary psycopg2 - - travis_retry invoke requirements --dev --addons - # Hack to fix package conflict between uritemplate and uritemplate.py (dependency of github3.py) - - pip uninstall uritemplate.py --yes - - pip install uritemplate.py==0.3.0 - -# Run Python tests (core and addon) and JS tests - -script: - - export COVERAGE=`if [ "$TRAVIS_BRANCH" == "master-w-coverage" ]; then echo "--coverage"; else echo ""; fi` - # Testmon will run for PRs, but will be disabled when merging into master or develop - - export TESTMON=`if [[ "$TRAVIS_PULL_REQUEST_BRANCH" == "" && "$TRAVIS_BRANCH" == "develop" || "$TRAVIS_PULL_REQUEST_BRANCH" == "" && "$TRAVIS_BRANCH" == "master" ]]; then echo ""; else echo "--testmon"; fi` - - export TESTMON_DATAFILE=$HOME/.cache/testmon/.testmondata_$TEST_BUILD - - invoke test_travis_$TEST_BUILD -n 1 $COVERAGE $TESTMON - -after_success: - - if [[ "$TRAVIS_BRANCH" == "master-w-coverage" ]]; then coveralls; fi - -before_cache: - # This ensures failed tests are removed from the cache so they can be re-tried. - - inv remove_failures_from_testmon --db-path=$HOME/.cache/testmon/.testmondata_$TEST_BUILD - - rm -Rf $HOME/.cache/pip/http - - rm -f $HOME/.cache/pip/log/debug.log - # exclude python requirements from github repo's - - rm -f $HOME/.cache/wheelhouse/modular_odm-*.whl - - rm -f $HOME/.cache/wheelhouse/mfr-*.whl - - rm -f $HOME/.cache/wheelhouse/responses-*.whl - - rm -f $HOME/.cache/wheelhouse/mendeley-*.whl - - rm -f $HOME/.cache/wheelhouse/feedparser-*.whl - # kill any running processes - - kill -9 $POSTGRES_PID - - kill -9 $ELASTICSEARCH_PID - - kill -9 $ELASTICSEARCH6_PID - -branches: - except: - - /^[0-9]/ diff --git a/CHANGELOG b/CHANGELOG index ea0a600ebd7..205d47de13c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,25 @@ We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO. +24.07.0 (2024-09-19) +==================== + +- Preprints Affiliation Project BE Release + +24.06.0 (2024-09-12) +==================== + +- Fix duplicate notifications for contributor-add failures +- Allow Read and Write contributors to view a project's draft registrations +- Change how files for withdrawn registrations are surfaced in the API +- Fix date displayed in citation for a registration +- New API endpoint /users/me/draft_preprints/ +- Add button to admin to move preprint from initial to pending +- Update language in notifications to indicate preprint resubmission +- Fix Preprint emails so they are sent as expected +- Fix ORCiD email by sending them after changes are committed to DB +- Remove references of TravisCI + 24.05.0 (2024-07-22) ==================== - Bump base python version from py3.6 to py3.12. diff --git a/README-docker-compose.md b/README-docker-compose.md index 9d8d6a2ba46..8e2fb5098ef 100644 --- a/README-docker-compose.md +++ b/README-docker-compose.md @@ -3,9 +3,7 @@ 1. Install the Docker Client - OSX: https://www.docker.com/products/docker#/mac - - Ubuntu - - docker: https://docs.docker.com/engine/installation/linux/ubuntulinux - - docker-compose: https://docs.docker.com/compose/install/ + - Ubuntu: https://docs.docker.com/engine/installation/linux/ubuntulinux - Windows: https://www.docker.com/products/docker#/windows 2. Grant the docker client additional resources (recommended minimums of 1 CPU, 8GB memory, 2GB swap, and 32GB disk image size) - OSX: https://docs.docker.com/docker-for-mac/#/preferences @@ -26,16 +24,21 @@ - Ubuntu - Add loopback alias - `sudo ifconfig lo:0 192.168.168.167 netmask 255.255.255.255 up` + ```bash + sudo ifconfig lo:0 192.168.168.167 netmask 255.255.255.255 up + ``` - For persistance, add to /etc/network/interfaces... Add lo:0 to auto line... - ```auto lo lo:0``` + ```bash + auto lo lo:0 + ``` Add stanza for lo:0... - ```iface lo:0 inet static - address 192.168.168.167 - netmask 255.255.255.255 - network 192.168.168.167 + ```bash + iface lo:0 inet static + address 192.168.168.167 + netmask 255.255.255.255 + network 192.168.168.167 ``` - If UFW enabled. Enable UFW forwarding. - https://docs.docker.com/engine/installation/linux/linux-postinstall/#allow-access-to-the-remote-api-through-a-firewall @@ -49,8 +52,10 @@ `sudo usermod -aG docker $USER` - In order to run OSF Preprints, raise fs.inotify.max_user_watches from default value - `echo fs.inotify.max_user_watches=131072 | sudo tee -a /etc/sysctl.conf` - `sudo sysctl -p` + ```bash + echo fs.inotify.max_user_watches=131072 | sudo tee -a /etc/sysctl.conf + sudo sysctl -p` + ``` - Windows - Install Microsoft Loopback Adapter (Windows 10 follow community comments as the driver was renamed) @@ -75,19 +80,22 @@ * _NOTE: After making changes to `Environment Variables` or `Volume Mounts` you will need to recreate the container(s)._ - - `$ docker-compose up --force-recreate --no-deps preprints` + ```bash + docker compose up --force-recreate --no-deps preprints + ``` 1. Application Settings - e.g. OSF & OSF API local.py - - `$ cp ./website/settings/local-dist.py ./website/settings/local.py` - - `$ cp ./api/base/settings/local-dist.py ./api/base/settings/local.py` - - `$ cp ./docker-compose-dist.override.yml ./docker-compose.override.yml` + ```bash + cp ./website/settings/local-dist.py ./website/settings/local.py + cp ./api/base/settings/local-dist.py ./api/base/settings/local.py + cp ./docker-compose-dist.override.yml ./docker-compose.override.yml + ``` For local tasks, (dev only) - `$ cp ./tasks/local-dist.py ./tasks/local.py` + ```bash + cp ./tasks/local-dist.py ./tasks/local.py + ``` 2. OPTIONAL (uncomment the below lines if you will use remote debugging) Environment variables (incl. remote debugging) - e.g. .docker-compose.env @@ -102,63 +110,63 @@ #### Special Instructions for Apple Chipset (M1, M2, etc.) and other ARM64 architecture - * _NOTE: The `elasticsearch`, `elasticsearch6`, and `sharejs` containers are incompatible with ARM64._ + * _NOTE: The default `elasticsearch`, `elasticsearch6`, and `sharejs` containers are incompatible with ARM64._ - - Running containers with docker-compose + - To run `elasticsearch6` on ARM64 architecture: - - Copy an ARM64-compatible configuration to `docker-compose.override.yml`: + - Copy `docker-compose-dist-arm64.override.yml` into your `docker-compose.override.yml` file - `$ cp ./docker-compose-dist-arm64.override.yml ./docker-compose.override.yml` + - Running containers with docker compose - In `webite/settings/local.py`, disable `SEARCH_ENGINE` ```python - # SEARCH_ENGINE = 'elastic' - SEARCH_ENGINE = None + # SEARCH_ENGINE = 'elastic' + SEARCH_ENGINE = None ``` - - Building the Docker image - - - If you wish to use an OSF image other than the latest `develop-arm64`: - - Build the image - ```bash - $ cd - $ git checkout - $ docker buildx build --platform linux/arm64 -t osf:-arm64 . - ``` - - In `docker-compose.override.yml`, replace any `quay.io/centerforopenscience/osf:develop-arm64` with the locally-tagged image above: - ```yml - image: osf:-arm64 - ``` - ## Application Runtime * _NOTE: Running docker containers detached (`-d`) will execute them in the background, if you would like to view/follow their console log output use the following command._ - - `$ docker-compose logs -f --tail 1000 web` + ```bash + docker compose logs -f --tail 1000 web + ``` 1. Application Environment - - `$ docker-compose up requirements mfr_requirements wb_requirements gv_requirements` + ```bash + docker compose up requirements mfr_requirements wb_requirements gv_requirements + ``` - _NOTE: When the various requirements installations are complete these containers will exit. You should only need to run these containers after pulling code that changes python requirements or if you update the python requirements._ + _NOTE: When the various requirements installations are complete these containers will exit. You should only need to run these containers after pulling code that changes python requirements or if you update the python requirements._ 2. Start Core Component Services (Detached) - - `$ docker-compose up -d elasticsearch postgres mongo rabbitmq` + + ```bash + docker compose up -d elasticsearch postgres mongo rabbitmq + ``` 3. Remove your existing node_modules and start the assets watcher (Detached) - - `$ rm -Rf ./node_modules` - - `$ docker-compose up -d assets` - - `$ docker-compose up -d admin_assets` + ```bash + rm -Rf ./node_modules + docker compose up -d assets + docker compose up -d admin_assets + ``` + + _NOTE: The first time the assets container is run it will take Webpack/NPM up to 15 minutes to compile resources. + When you see the BowerJS build occurring it is likely a safe time to move forward with starting the remaining + containers._ - _NOTE: The first time the assets container is run it will take Webpack/NPM up to 15 minutes to compile resources. - When you see the BowerJS build occurring it is likely a safe time to move forward with starting the remaining - containers._ 4. Start the Services (Detached) - - `$ docker-compose up -d mfr wb fakecas sharejs` + ```bash + docker compose up -d mfr wb fakecas sharejs + ``` 5. Run migrations and create preprint providers - When starting with an empty database you will need to run migrations and populate preprint providers. See the [Running arbitrary commands](#running-arbitrary-commands) section below for instructions. 6. Start the OSF Web, API Server, and Preprints (Detached) - - `$ docker-compose up -d worker web api admin preprints ember_osf_web gv` + ```bash + docker compose up -d worker web api admin preprints ember_osf_web gv + ``` 7. View the OSF at [http://localhost:5000](http://localhost:5000). @@ -167,52 +175,52 @@ - Once the requirements have all been installed, you can start the OSF in the background with ```bash - $ docker-compose up -d assets admin_assets mfr wb fakecas sharejs worker web api admin preprints ember_osf_web gv + docker compose up -d assets admin_assets mfr wb fakecas sharejs worker web api admin preprints ember_osf_web gv ``` - To view the logs for a given container: ```bash - $ docker-compose logs -f --tail 100 web + docker compose logs -f --tail 100 web ``` ### Helpful aliases - Start all containers ```bash - alias dcsa="docker-compose up -d assets admin_assets mfr wb fakecas sharejs worker elasticsearch elasticsearch6 web api admin preprints gv" + alias dcsa="docker compose up -d assets admin_assets mfr wb fakecas sharejs worker elasticsearch elasticsearch6 web api admin preprints gv" ``` - Shut down all containers ```bash - alias dchs="docker-compose down" + alias dchs="docker compose down" ``` - Attach to container logs - dcl . Ie. `dcl web` will log only the web container - ```bash - alias dcl="docker-compose logs -f --tail 100 " - ``` + ```bash + alias dcl="docker compose logs -f --tail 100 " + ``` - Run migrations (Starting a fresh database or changes to migrations) ```bash - alias dcm="docker-compose run --rm web python3 manage.py migrate" + alias dcm="docker compose run --rm web python3 manage.py migrate" ``` - Download requirements (Whenever the requirements change or first-time set-up) ```bash - alias dcreq="docker-compose up requirements mfr_requirements wb_requirements gv_requirements" + alias dcreq="docker compose up requirements mfr_requirements wb_requirements gv_requirements" ``` - Restart the containers - - `$ dcr `. Ie. `dcr web` will restart the web container + - `dcr `. Ie. `dcr web` will restart the web container ```bash - alias dcr="docker-compose restart -t 0 " + alias dcr="docker compose restart -t 0 " ``` - Start the OSF shell (Interactive python shell that allows working directly with the osf on a code level instead of a web level.) ```bash - alias dcosfs="docker-compose run --rm web python3 manage.py osf_shell" + alias dcosfs="docker compose run --rm web python3 manage.py osf_shell" ``` - List all these commands @@ -222,35 +230,60 @@ ## Running arbitrary commands -- View logs: `$ docker-compose logs -f --tail 100 ` +- View logs: + ```bash + docker compose logs -f --tail 100 + ``` - _NOTE: CTRL-c will exit_ - Run migrations: - After creating migrations, resetting your database, or starting on a fresh install you will need to run migrations to make the needed changes to database. This command looks at the migrations on disk and compares them to the list of migrations in the `django_migrations` database table and runs any migrations that have not been run. - - `docker-compose run --rm web python3 manage.py migrate` To run `osf` migrations - - `docker-compose run --rm gv python manage.py migrate` To run `gravyvalet(gv)` migrations + - To run `osf` migrations: + ```bash + docker compose run --rm web python3 manage.py migrate + ``` + - To run `gravyvalet(gv)` migrations: + ```bash + docker compose run --rm gv python manage.py migrate + ``` - Populate institutions: - After resetting your database or with a new install you will need to populate the table of institutions. **You must have run migrations first.** - - `docker-compose run --rm web python3 -m scripts.populate_institutions -e test -a` + ```bash + docker compose run --rm web python3 -m scripts.populate_institutions -e test -a + ``` - Populate preprint, registration, and collection providers: - After resetting your database or with a new install, the required providers and subjects will be created automatically **when you run migrations.** To create more: - - `docker-compose run --rm web python3 manage.py populate_fake_providers` + ```bash + docker compose run --rm web python3 manage.py populate_fake_providers + ``` - _NOTE: In case, you encounter error with missing data, when running the `'populate_fake_providers'` command. Fix this with 'update_taxonomies' command:_ - - `docker-compose run --rm web python3 -m scripts.update_taxonomies` + ```bash + docker compose run --rm web python3 -m scripts.update_taxonomies + ``` - Populate citation styles - Needed for api v2 citation style rendering. - - `docker-compose run --rm web python3 -m scripts.parse_citation_styles` + ```bash + docker compose run --rm web python3 -m scripts.parse_citation_styles + ``` - Start ember_osf_web - Needed for quickfiles feature: - - `docker-compose up -d ember_osf_web` + ```bash + docker compose up -d ember_osf_web + ``` - OPTIONAL: Register OAuth Scopes - Needed for things such as the ember-osf dummy app - - `docker-compose run --rm web python3 -m scripts.register_oauth_scopes` + ```bash + docker compose run --rm web python3 -m scripts.register_oauth_scopes + ``` - OPTIONAL: Create migrations: - After changing a model you will need to create migrations and apply them. Migrations are python code that changes either the structure or the data of a database. This will compare the django models on disk to the database, find the differences, and create migration code to change the database. If there are no changes this command is a noop. - - `docker-compose run --rm web python3 manage.py makemigrations` + ```bash + docker compose run --rm web python3 manage.py makemigrations + ``` - OPTIONAL: Destroy and recreate an empty database: - **WARNING**: This will delete all data in your database. - - `docker-compose run --rm web python3 manage.py reset_db --noinput` + ```bash + docker compose run --rm web python3 manage.py reset_db --noinput + ``` ## Application Debugging @@ -260,7 +293,7 @@ If you want to debug your changes by using print statements, you'll have to have 1. Edit your container configuration in docker-compose.mfr.env or docker-compose.mfr.env to include the new environment variable by uncommenting PYTHONUNBUFFERED=0 2. If you're using a container running Python 3 you can insert the following code prior to a print statement: - ``` + ```python import functools print = functools.partial(print, flush=True) ``` @@ -278,13 +311,13 @@ You should run the `web` and/or `api` container (depending on which codebase the ```bash # Kill the already-running web container -$ docker-compose kill web +docker compose kill web # Run a web container. App logs and breakpoints will show up here. -$ docker-compose run --rm --service-ports web +docker compose run --rm --service-ports web ``` -**IMPORTANT: While attached to the running app, CTRL-c will stop the container.** To detach from the container and leave it running, **use CTRL-p CTRL-q**. Use `docker attach` to re-attach to the container, passing the *container-name* (which you can get from `docker-compose ps`), e.g. `docker attach osf_web_run_1`. +**IMPORTANT: While attached to the running app, CTRL-c will stop the container.** To detach from the container and leave it running, **use CTRL-p CTRL-q**. Use `docker attach` to re-attach to the container, passing the *container-name* (which you can get from `docker compose ps`), e.g. `docker attach osf_web_run_1`. ### Remote Debugging with PyCharm @@ -300,37 +333,47 @@ $ docker-compose run --rm --service-ports web ## Application Tests - Run All Tests - - `$ docker-compose run --rm web invoke test` - -- Run OSF Specific Tests - - `$ docker-compose run --rm web invoke test_osf` + ```bash + docker compose run --rm web python3 -m pytest + ``` - Test a Specific Module - - `$ docker-compose run --rm web invoke test_module -m tests/test_conferences.py` + ```bash + docker compose run --rm web python3 -m pytest tests/test_conferences.py + ``` - Test a Specific Class - - `docker-compose run --rm web invoke test_module -m tests/test_conferences.py::TestProvisionNode` + ```bash + docker compose run --rm web python3 -m pytest tests/test_conferences.py::TestProvisionNode + ``` - Test a Specific Method - - `$ docker-compose run --rm web invoke test_module -m tests/test_conferences.py::TestProvisionNode::test_upload` - -- Test with Specific Parameters (1 cpu, capture stdout) - - `$ docker-compose run --rm web invoke test_module -m tests/test_conferences.py::TestProvisionNode::test_upload -n 1 --params '--capture=sys'` + ```bash + docker compose run --rm web python3 -m pytest tests/test_conferences.py::TestProvisionNode::test_upload + ``` ## Managing Container State Restart a container: - - `$ docker-compose restart -t 0 assets` +```bash +docker compose restart -t 0 assets +``` Recreate a container _(useful to ensure all environment variables/volume changes are in order)_: - - `$ docker-compose up --force-recreate --no-deps assets` + ```bash + docker compose up --force-recreate --no-deps assets + ``` Delete a container _(does not remove volumes)_: - - `$ docker-compose stop -t 0 assets` - - `$ docker-compose rm assets` + ```bash + docker compose stop -t 0 assets + docker compose rm assets + ``` List containers and status: - - `$ docker-compose ps` +```bash +docker compose ps +``` ### Backing up your database In certain cases, you may wish to remove all docker container images, but preserve a copy of the database used by your @@ -342,11 +385,13 @@ resetting docker. To back up your database, follow the following sequence of com ([as of this writing](https://github.com/CenterForOpenScience/osf.io/blob/ce1702cbc95eb7777e5aaf650658a9966f0e6b0c/docker-compose.yml#L53), Postgres 15) 2. Start postgres locally. This must be on a different port than the one used by [docker postgres](https://github.com/CenterForOpenScience/osf.io/blob/ce1702cbc95eb7777e5aaf650658a9966f0e6b0c/docker-compose.yml#L61). Eg, `pg_ctl -D /usr/local/var/postgres start -o "-p 5433"` -3. Verify that the postgres docker container is running (`docker-compose up -d postgres`) +3. Verify that the postgres docker container is running (`docker compose up -d postgres`) 4. Tell your local (non-docker) version of postgres to connect to (and back up) data from the instance in docker - (defaults to port 5432): - `pg_dump --username postgres --compress 9 --create --clean --format d --jobs 4 --host localhost --file ~/Desktop/osf_backup osf` for osf -5. The same can be done for `grayvalet`, just replace `osf` with `gravyvalet` (this applies for all following commands related to backups) + (defaults to port 5432). For `osf` run: + ```bash + pg_dump --username postgres --compress 9 --create --clean --format d --jobs 4 --host localhost --file ~/Desktop/osf_backup osf + ``` +6. The same can be done for `grayvalet`, just replace `osf` with `gravyvalet` (this applies for all following commands related to backups) (shorthand: `pg_dump -U postgres -Z 9 -C --c -Fd --j 4 -h localhost --f ~/Desktop/osf_backup osf`) @@ -357,12 +402,14 @@ resetting docker. To back up your database, follow the following sequence of com ``` 2. Delete a persistent storage volume: **WARNING: All postgres data will be destroyed.** - - `$ docker-compose stop -t 0 postgres` - - `$ docker-compose rm postgres` - - `$ docker volume rm osfio_postgres_data_vol` +```bash +docker compose stop -t 0 postgres +docker compose rm postgres +docker volume rm osfio_postgres_data_vol +``` 3. Starting a new postgres container. ```bash -docker-compose up -d postgres +docker compose up -d postgres ``` 4. Restoring the database from the dump file into the new postgres container. ```bash @@ -376,7 +423,7 @@ instructions on dropping postgres data volumes) When ready, run the restore command from a local terminal: ```bash -$ pg_restore --username postgres --clean --dbname osf --format d --jobs 4 --host localhost ~/Desktop/osf_backup +pg_restore --username postgres --clean --dbname osf --format d --jobs 4 --host localhost ~/Desktop/osf_backup ``` (shorthand) `pg_restore -U postgres -c -d osf -Fd -j 4 -h localhost ~/Desktop/osf_backup` @@ -386,29 +433,31 @@ $ pg_restore --username postgres --clean --dbname osf --format d --jobs 4 --host Resetting the Environment: **WARNING: All volumes and containers are destroyed** - - `$ docker-compose down -v` + - `docker compose down -v` Delete a persistent storage volume: **WARNING: All postgres data will be destroyed.** - - `$ docker-compose stop -t 0 postgres` - - `$ docker-compose rm postgres` - - `$ docker volume rm osfio_postgres_data_vol` + ```bash + docker compose stop -t 0 postgres + docker compose rm postgres + docker volume rm osfio_postgres_data_vol + ``` ## Updating ```bash -$ git stash # if you have any changes that need to be stashed -$ git pull upstream develop # (replace upstream with the name of your remote) -$ git stash pop # unstash changes +git stash # if you have any changes that need to be stashed +git pull upstream develop # (replace upstream with the name of your remote) +git stash pop # unstash changes # If you get an out of space error -$ docker image prune +docker image prune # Pull latest images -$ docker-compose pull +docker compose pull # It is recommended to run requirements only for services that require update, not to wear off local SSD more than needed -$ docker-compose up requirements mfr_requirements wb_requirements gv_requirements +docker compose up requirements mfr_requirements wb_requirements gv_requirements # Run db migrations -$ docker-compose run --rm web python3 manage.py migrate +docker compose run --rm web python3 manage.py migrate ``` ## Miscellaneous @@ -425,7 +474,7 @@ The issue is that docker containers run in unprivileged mode by default. For `docker run`, you can use `--privilege=true` to give the container extended privileges. You can also add or drop capabilities by using `cap-add` and `cap-drop`. Since Docker 1.12, there is no need to add `--security-opt seccomp=unconfined` because the seccomp profile will adjust to selected capabilities. ([Reference](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities)) -When using `docker-compose`, set `privileged: true` for individual containers in the `docker-compose.yml`. ([Reference](https://docs.docker.com/compose/compose-file/#domainname-hostname-ipc-mac_address-privileged-read_only-shm_size-stdin_open-tty-user-working_dir)) Here is an example for WaterButler: +When using `docker compose`, set `privileged: true` for individual containers in the `docker-compose.yml`. ([Reference](https://docs.docker.com/compose/compose-file/#domainname-hostname-ipc-mac_address-privileged-read_only-shm_size-stdin_open-tty-user-working_dir)) Here is an example for WaterButler: ```yml wb: diff --git a/addons/base/views.py b/addons/base/views.py index 0ffffc640cd..6f142db5781 100644 --- a/addons/base/views.py +++ b/addons/base/views.py @@ -647,6 +647,10 @@ def osfstoragefile_mark_viewed(self, auth, fileversion, file_node): @file_signals.file_viewed.connect def osfstoragefile_update_view_analytics(self, auth, fileversion, file_node): resource = file_node.target + user = getattr(auth, 'user', None) + if hasattr(resource, 'is_contributor_or_group_member') and resource.is_contributor_or_group_member(user): + # Don't record views by contributors + return enqueue_update_analytics( resource, file_node, @@ -658,6 +662,10 @@ def osfstoragefile_update_view_analytics(self, auth, fileversion, file_node): @file_signals.file_viewed.connect def osfstoragefile_viewed_update_metrics(self, auth, fileversion, file_node): resource = file_node.target + user = getattr(auth, 'user', None) + if hasattr(resource, 'is_contributor_or_group_member') and resource.is_contributor_or_group_member(user): + # Don't record views by contributors + return if waffle.switch_is_active(features.ELASTICSEARCH_METRICS) and isinstance(resource, Preprint): try: PreprintView.record_for_preprint( @@ -681,6 +689,10 @@ def osfstoragefile_downloaded_update_analytics(self, auth, fileversion, file_nod @file_signals.file_downloaded.connect def osfstoragefile_downloaded_update_metrics(self, auth, fileversion, file_node): resource = file_node.target + user = getattr(auth, 'user', None) + if hasattr(resource, 'is_contributor_or_group_member') and resource.is_contributor_or_group_member(user): + # Don't record downloads by contributors + return if waffle.switch_is_active(features.ELASTICSEARCH_METRICS) and isinstance(resource, Preprint): try: PreprintDownload.record_for_preprint( diff --git a/addons/dataverse/settings/local-travis.py b/addons/dataverse/settings/local-ci.py similarity index 100% rename from addons/dataverse/settings/local-travis.py rename to addons/dataverse/settings/local-ci.py diff --git a/admin/base/filters.py b/admin/base/filters.py index 19ba7fb8bce..f0107e9cc3c 100644 --- a/admin/base/filters.py +++ b/admin/base/filters.py @@ -1,5 +1,4 @@ from django import template -from django.utils.safestring import mark_safe import json @@ -8,4 +7,4 @@ @register.filter def jsonify(o): - return mark_safe(json.dumps(o)) + return json.dumps(o) diff --git a/admin/base/templatetags/filters.py b/admin/base/templatetags/filters.py index a3a72b9cd4a..38efad3a0fe 100644 --- a/admin/base/templatetags/filters.py +++ b/admin/base/templatetags/filters.py @@ -1,10 +1,9 @@ # h/t https://djangosnippets.org/snippets/1250/ from django import template -from django.utils.safestring import mark_safe import json register = template.Library() @register.filter def jsonify(o): - return mark_safe(json.dumps(o)) + return json.dumps(o) diff --git a/admin/management/views.py b/admin/management/views.py index 4323d7fd429..3bd675790dd 100644 --- a/admin/management/views.py +++ b/admin/management/views.py @@ -100,7 +100,6 @@ def post(self, request, *args, **kwargs): class DailyReportersGo(ManagementCommandPermissionView): def post(self, request, *args, **kwargs): - also_keen = bool(request.POST.get('also_send_to_keen', False)) report_date = request.POST.get('report_date', None) if report_date: report_date = isoparse(report_date).date() @@ -109,7 +108,6 @@ def post(self, request, *args, **kwargs): daily_reporters_go.apply_async(kwargs={ 'report_date': report_date, - 'also_send_to_keen': also_keen }) messages.success(request, 'Daily reporters going!') return redirect(reverse('management:commands')) diff --git a/admin/nodes/urls.py b/admin/nodes/urls.py index 6a918831f4c..5036b9dd06d 100644 --- a/admin/nodes/urls.py +++ b/admin/nodes/urls.py @@ -37,4 +37,5 @@ name='recalculate-node-storage'), re_path(r'^(?P[a-z0-9]+)/make_private/$', views.NodeMakePrivate.as_view(), name='make-private'), re_path(r'^(?P[a-z0-9]+)/make_public/$', views.NodeMakePublic.as_view(), name='make-public'), + re_path(r'^(?P[a-z0-9]+)/remove_notifications/$', views.NodeRemoveNotificationView.as_view(), name='node-remove-notifications'), ] diff --git a/admin/nodes/views.py b/admin/nodes/views.py index e7902956add..74b6b08feae 100644 --- a/admin/nodes/views.py +++ b/admin/nodes/views.py @@ -21,6 +21,7 @@ from admin.base.utils import change_embargo_date, validate_embargo_date from admin.base.views import GuidView from admin.base.forms import GuidForm +from admin.notifications.views import detect_duplicate_notifications, delete_selected_notifications from api.share.utils import update_share from api.caching.tasks import update_storage_usage_cache @@ -92,12 +93,30 @@ class NodeView(NodeMixin, GuidView): raise_exception = True def get_context_data(self, **kwargs): - return super().get_context_data(**{ + context = super().get_context_data(**kwargs) + node = self.get_object() + + detailed_duplicates = detect_duplicate_notifications(node_id=node.id) + + context.update({ 'SPAM_STATUS': SpamStatus, 'STORAGE_LIMITS': settings.StorageLimits, - 'node': kwargs.pop('object', self.get_object()), - }, **kwargs) + 'node': node, + 'duplicates': detailed_duplicates + }) + + return context + +class NodeRemoveNotificationView(View): + def post(self, request, *args, **kwargs): + selected_ids = request.POST.getlist('selected_notifications') + if selected_ids: + delete_selected_notifications(selected_ids) + messages.success(request, 'Selected notifications were successfully deleted.') + else: + messages.error(request, 'No notifications selected for deletion.') + return redirect('nodes:node', guid=kwargs.get('guid')) class NodeSearchView(PermissionRequiredMixin, FormView): """ Allows authorized users to search for a node by it's guid. diff --git a/admin/notifications/views.py b/admin/notifications/views.py new file mode 100644 index 00000000000..7a3a13a8df8 --- /dev/null +++ b/admin/notifications/views.py @@ -0,0 +1,30 @@ +from osf.models.notifications import NotificationSubscription +from django.db.models import Count + +def delete_selected_notifications(selected_ids): + NotificationSubscription.objects.filter(id__in=selected_ids).delete() + +def detect_duplicate_notifications(node_id=None): + query = NotificationSubscription.objects.values('_id').annotate(count=Count('_id')).filter(count__gt=1) + if node_id: + query = query.filter(node_id=node_id) + + detailed_duplicates = [] + for dup in query: + notifications = NotificationSubscription.objects.filter( + _id=dup['_id'] + ).order_by('created') + + for notification in notifications: + detailed_duplicates.append({ + 'id': notification.id, + '_id': notification._id, + 'event_name': notification.event_name, + 'created': notification.created, + 'count': dup['count'], + 'email_transactional': [u._id for u in notification.email_transactional.all()], + 'email_digest': [u._id for u in notification.email_digest.all()], + 'none': [u._id for u in notification.none.all()] + }) + + return detailed_duplicates diff --git a/admin/preprints/forms.py b/admin/preprints/forms.py index 15b0ba077ea..2ea91931018 100644 --- a/admin/preprints/forms.py +++ b/admin/preprints/forms.py @@ -1,9 +1,33 @@ from django import forms from osf.models import Preprint - +from osf.utils.workflows import ReviewStates class ChangeProviderForm(forms.ModelForm): class Meta: model = Preprint fields = ('provider',) + + +class MachineStateForm(forms.ModelForm): + class Meta: + model = Preprint + fields = ('machine_state',) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if not self.instance.is_public: + self.fields['machine_state'].widget.attrs['disabled'] = 'disabled' + else: + if self.instance.machine_state == ReviewStates.INITIAL.db_name: + self.fields['machine_state'].choices = [ + (ReviewStates.INITIAL.value, ReviewStates.INITIAL.value), + (ReviewStates.PENDING.value, ReviewStates.PENDING.value), + ] + else: + # Disabled Option you are on + self.fields['machine_state'].widget.attrs['disabled'] = 'disabled' + self.fields['machine_state'].choices = [ + (self.instance.machine_state.title(), self.instance.machine_state) + ] diff --git a/admin/preprints/urls.py b/admin/preprints/urls.py index ddbbc9c4a54..cec79891134 100644 --- a/admin/preprints/urls.py +++ b/admin/preprints/urls.py @@ -10,6 +10,8 @@ re_path(r'^known_ham$', views.PreprintKnownHamList.as_view(), name='known-ham'), re_path(r'^withdrawal_requests$', views.PreprintWithdrawalRequestList.as_view(), name='withdrawal-requests'), re_path(r'^(?P[a-z0-9]+)/$', views.PreprintView.as_view(), name='preprint'), + re_path(r'^(?P[a-z0-9]+)/change_provider/$', views.PreprintProviderChangeView.as_view(), name='preprint-provider'), + re_path(r'^(?P[a-z0-9]+)/machine_state/$', views.PreprintMachineStateView.as_view(), name='preprint-machine-state'), re_path(r'^(?P[a-z0-9]+)/reindex_share_preprint/$', views.PreprintReindexShare.as_view(), name='reindex-share-preprint'), re_path(r'^(?P[a-z0-9]+)/remove_user/(?P[a-z0-9]+)/$', views.PreprintRemoveContributorView.as_view(), diff --git a/admin/preprints/views.py b/admin/preprints/views.py index f8950c349c9..80f6da1f059 100644 --- a/admin/preprints/views.py +++ b/admin/preprints/views.py @@ -15,7 +15,7 @@ from admin.base.views import GuidView from admin.base.forms import GuidForm from admin.nodes.views import NodeRemoveContributorView -from admin.preprints.forms import ChangeProviderForm +from admin.preprints.forms import ChangeProviderForm, MachineStateForm from api.share.utils import update_share @@ -62,6 +62,21 @@ class PreprintView(PreprintMixin, GuidView): """ template_name = 'preprints/preprint.html' permission_required = ('osf.view_preprint', 'osf.change_preprint',) + + def get_context_data(self, **kwargs): + preprint = self.get_object() + return super().get_context_data(**{ + 'preprint': preprint, + 'SPAM_STATUS': SpamStatus, + 'change_provider_form': ChangeProviderForm(instance=preprint), + 'change_machine_state_form': MachineStateForm(instance=preprint), + }, **kwargs) + + +class PreprintProviderChangeView(PreprintMixin, GuidView): + """ Allows authorized users to view preprint info and change a preprint's provider. + """ + permission_required = ('osf.view_preprint', 'osf.change_preprint',) form_class = ChangeProviderForm def post(self, request, *args, **kwargs): @@ -79,13 +94,26 @@ def post(self, request, *args, **kwargs): return redirect(self.get_success_url()) - def get_context_data(self, **kwargs): + +class PreprintMachineStateView(PreprintMixin, GuidView): + """ Allows authorized users to view preprint info and change a preprint's machine_state. + """ + permission_required = ('osf.view_preprint', 'osf.change_preprint',) + form_class = MachineStateForm + + def post(self, request, *args, **kwargs): preprint = self.get_object() - return super().get_context_data(**{ - 'preprint': preprint, - 'SPAM_STATUS': SpamStatus, - 'form': ChangeProviderForm(instance=preprint), - }, **kwargs) + new_machine_state = request.POST.get('machine_state') + if new_machine_state and preprint.machine_state != new_machine_state: + preprint.machine_state = new_machine_state + try: + preprint.save() + except Exception as e: + messages.error(self.request, e.message) + + preprint.refresh_from_db() + + return redirect(self.get_success_url()) class PreprintSearchView(PermissionRequiredMixin, FormView): diff --git a/admin/templates/management/commands.html b/admin/templates/management/commands.html index 269ead1bd3d..91471394c71 100644 --- a/admin/templates/management/commands.html +++ b/admin/templates/management/commands.html @@ -89,11 +89,6 @@

Daily Reporters, Go!

(default: yesterday)
- - - (may result in duplicates) diff --git a/admin/templates/nodes/node.html b/admin/templates/nodes/node.html index 2a410881666..6ec71e2dfdc 100644 --- a/admin/templates/nodes/node.html +++ b/admin/templates/nodes/node.html @@ -104,6 +104,50 @@

{{ node.type|cut:'osf.'|title }}: {{ node.title }} + +

Duplicate Notifications

+ {% if duplicates %} +
+ {% csrf_token %} + + + + + + + + + + + + + + {% for notification in duplicates %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
SelectEvent NameCreatedCountEmail TransactionalEmail DigestNone
{{ notification.event_name }}{{ notification.created }}{{ notification.count }}{{ notification.email_transactional|join:", " }}{{ notification.email_digest|join:", " }}{{ notification.none|join:", " }}
No duplicate notifications found!
+ +
+ {% else %} +

No duplicate notifications found.

+ {% endif %} + + diff --git a/admin/templates/preprints/machine_state.html b/admin/templates/preprints/machine_state.html new file mode 100644 index 00000000000..0d133b037bb --- /dev/null +++ b/admin/templates/preprints/machine_state.html @@ -0,0 +1,22 @@ +{% load node_extras %} + + Machine State + +

{{ preprint.machine_state }}

+

{{ preprint.state }}

+ {% if perms.osf.change_preprint %} +
+ Change preprint machine_state + +
+
+
+ {% csrf_token %} + {{ change_machine_state_form.as_p }} + +
+
+
+ {% endif %} + + \ No newline at end of file diff --git a/admin/templates/preprints/preprint.html b/admin/templates/preprints/preprint.html index 4d96190339f..0b76a65951f 100644 --- a/admin/templates/preprints/preprint.html +++ b/admin/templates/preprints/preprint.html @@ -74,10 +74,6 @@

Preprint: {{ preprint.title }} Published {{ preprint.is_published }} - - Machine State - {{ preprint.machine_state }} - {% if preprint.is_published %} Date Published @@ -104,6 +100,7 @@

Preprint: {{ preprint.title }} {% endif %} {% include "preprints/provider.html" with preprint=preprint %} + {% include "preprints/machine_state.html" with preprint=preprint %} Subjects diff --git a/admin/templates/preprints/provider.html b/admin/templates/preprints/provider.html index 4d14d1faf03..4a640b997c7 100644 --- a/admin/templates/preprints/provider.html +++ b/admin/templates/preprints/provider.html @@ -9,9 +9,9 @@
-
+ {% csrf_token %} - {{ form.as_p }} + {{ change_provider_form.as_p }}
diff --git a/admin_tests/notifications/__init__.py b/admin_tests/notifications/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/admin_tests/notifications/test_views.py b/admin_tests/notifications/test_views.py new file mode 100644 index 00000000000..08ad695edd1 --- /dev/null +++ b/admin_tests/notifications/test_views.py @@ -0,0 +1,39 @@ +import pytest +from django.test import RequestFactory +from osf.models import OSFUser, NotificationSubscription, Node +from admin.notifications.views import ( + delete_selected_notifications, + detect_duplicate_notifications, +) +from tests.base import AdminTestCase + +pytestmark = pytest.mark.django_db + +class TestNotificationFunctions(AdminTestCase): + + def setUp(self): + super().setUp() + self.user = OSFUser.objects.create(username='admin', is_staff=True) + self.node = Node.objects.create(creator=self.user, title='Test Node') + self.request_factory = RequestFactory() + + def test_delete_selected_notifications(self): + notification1 = NotificationSubscription.objects.create(user=self.user, node=self.node, event_name='event1') + notification2 = NotificationSubscription.objects.create(user=self.user, node=self.node, event_name='event2') + notification3 = NotificationSubscription.objects.create(user=self.user, node=self.node, event_name='event3') + + delete_selected_notifications([notification1.id, notification2.id]) + + assert not NotificationSubscription.objects.filter(id__in=[notification1.id, notification2.id]).exists() + assert NotificationSubscription.objects.filter(id=notification3.id).exists() + + def test_detect_duplicate_notifications(self): + NotificationSubscription.objects.create(user=self.user, node=self.node, event_name='event1') + NotificationSubscription.objects.create(user=self.user, node=self.node, event_name='event1') + NotificationSubscription.objects.create(user=self.user, node=self.node, event_name='event2') + + duplicates = detect_duplicate_notifications() + + print(f"Detected duplicates: {duplicates}") + + assert len(duplicates) == 3, f"Expected 3 duplicates, but found {len(duplicates)}" diff --git a/admin_tests/preprints/test_views.py b/admin_tests/preprints/test_views.py index 2c9d46c48a0..3d06b2c2f86 100644 --- a/admin_tests/preprints/test_views.py +++ b/admin_tests/preprints/test_views.py @@ -57,7 +57,7 @@ class TestPreprintView: @pytest.fixture() def plain_view(self): - return views.PreprintView + return views.PreprintProviderChangeView @pytest.fixture() def view(self, req, plain_view): @@ -589,3 +589,63 @@ def test_approve_reject_on_list_view(self, withdrawal_request, admin, action, fi assert original_comment == withdrawal_request.target.withdrawal_justification else: assert not withdrawal_request.target.withdrawal_justification + + +@pytest.mark.urls('admin.base.urls') +@pytest.mark.django_db +class TestPreprintMachineStateView: + + @pytest.fixture() + def preprint(self): + return PreprintFactory() + + @pytest.fixture() + def user(self): + return AuthUserFactory() + + @pytest.fixture() + def admin_user(self): + admin_user = AuthUserFactory() + admin_user.is_admin = True + admin_user.save() + return admin_user + + @pytest.fixture() + def req(self, user): + req = RequestFactory().post('/fake_path') + req.user = user + return req + + @pytest.fixture() + def admin_req(self, admin_user): + req = RequestFactory().post('/fake_path') + req.user = admin_user + return req + + def test_post_changes_machine_state(self, admin_req, preprint): + new_state = 'new_state' + admin_req.POST = {'machine_state': new_state} + + view = setup_view(views.PreprintMachineStateView(), admin_req, guid=preprint._id) + response = view.post(admin_req) + + preprint.refresh_from_db() + assert preprint.machine_state == new_state + assert response.status_code == 302 + + def test_post_no_change_in_machine_state(self, admin_req, preprint): + current_state = preprint.machine_state + admin_req.POST = {'machine_state': current_state} + + view = setup_view(views.PreprintMachineStateView(), admin_req, guid=preprint._id) + response = view.post(admin_req) + + preprint.refresh_from_db() + assert preprint.machine_state == current_state + assert response.status_code == 302 + + def test_no_permission_raises_error(self, req, preprint): + request = RequestFactory().post(reverse('preprints:preprint-machine-state', kwargs={'guid': preprint._id})) + request.user = req.user + with pytest.raises(PermissionDenied): + views.PreprintMachineStateView.as_view()(request, guid=preprint._id) diff --git a/api/base/permissions.py b/api/base/permissions.py index ae2e45e0fcb..50f467a8512 100644 --- a/api/base/permissions.py +++ b/api/base/permissions.py @@ -8,8 +8,10 @@ from framework.auth import oauth_scopes from framework.auth.cas import CasResponse -from osf.models import ApiOAuth2Application, ApiOAuth2PersonalToken +from osf.models import ApiOAuth2Application, ApiOAuth2PersonalToken, Preprint +from osf.utils import permissions as osf_permissions from website.util.sanitize import is_iterable_but_not_string +from api.base.utils import get_user_auth # Implementation built on django-oauth-toolkit, but with more granular control over read+write permissions @@ -160,3 +162,17 @@ def has_object_permission(self, request, view, obj): obj = self.get_object(request, view, obj) return super().has_object_permission(request, view, obj) return Perm + + +class WriteOrPublicForRelationshipInstitutions(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + assert isinstance(obj, dict) + auth = get_user_auth(request) + resource = obj['self'] + + if request.method in permissions.SAFE_METHODS: + return resource.is_public or resource.can_view(auth) + else: + if isinstance(resource, Preprint): + return resource.can_edit(auth=auth) + return resource.has_permission(auth.user, osf_permissions.WRITE) diff --git a/api/base/settings/defaults.py b/api/base/settings/defaults.py index d74e744f787..136f7f48b6b 100644 --- a/api/base/settings/defaults.py +++ b/api/base/settings/defaults.py @@ -317,7 +317,7 @@ # django-elasticsearch-metrics ELASTICSEARCH_DSL = { 'default': { - 'hosts': os.environ.get('ELASTIC6_URI', '127.0.0.1:9201'), + 'hosts': osf_settings.ELASTIC6_URI, 'retry_on_timeout': True, }, } @@ -360,7 +360,7 @@ MAX_SIZE_OF_ES_QUERY = 10000 DEFAULT_ES_NULL_VALUE = 'N/A' -TRAVIS_ENV = False +CI_ENV = False CITATION_STYLES_REPO_URL = 'https://github.com/CenterForOpenScience/styles/archive/88e6ed31a91e9f5a480b486029cda97b535935d4.zip' DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' diff --git a/api/base/settings/local-travis.py b/api/base/settings/local-ci.py similarity index 90% rename from api/base/settings/local-travis.py rename to api/base/settings/local-ci.py index 8b98f743169..5dc5d6035f4 100644 --- a/api/base/settings/local-travis.py +++ b/api/base/settings/local-ci.py @@ -5,7 +5,7 @@ ENABLE_VARNISH = True ENABLE_ESI = False -OSF_DB_PASSWORD = 'postgres' +OSF_DB_PASSWORD = os.environ.get('OSF_DB_PASSWORD') SESSION_ENGINE = 'django.contrib.sessions.backends.db' @@ -25,4 +25,4 @@ ALLOWED_HOSTS.append('localhost') -TRAVIS_ENV = True +CI_ENV = True diff --git a/api/draft_registrations/permissions.py b/api/draft_registrations/permissions.py index 83bf44a612e..5232ee9d546 100644 --- a/api/draft_registrations/permissions.py +++ b/api/draft_registrations/permissions.py @@ -8,6 +8,8 @@ OSFUser, ) from api.nodes.permissions import ContributorDetailPermissions +from osf.utils.permissions import WRITE, ADMIN + class IsContributorOrAdminContributor(permissions.BasePermission): """ @@ -57,3 +59,34 @@ class DraftContributorDetailPermissions(ContributorDetailPermissions): def load_resource(self, context, view): return DraftRegistration.load(context['draft_id']) + + +class DraftRegistrationPermission(permissions.BasePermission): + """ + Check permissions for draft and node, Admin can create (POST) or edit (PATCH, PUT) to a DraftRegistration, but write + users can only edit them. Node permissions are inherited by the DraftRegistration when they are higher. + """ + acceptable_models = (DraftRegistration, AbstractNode) + + def has_object_permission(self, request, view, obj): + auth = get_user_auth(request) + + if not auth.user: + return False + + if request.method in permissions.SAFE_METHODS: + if isinstance(obj, DraftRegistration): + return obj.can_view(auth) + elif isinstance(obj, AbstractNode): + return obj.can_view(auth) + elif request.method == 'POST': # Only Admin can create a draft registration + if isinstance(obj, DraftRegistration): + return obj.is_contributor(auth.user) and obj.has_permission(auth.user, ADMIN) + elif isinstance(obj, AbstractNode): + return obj.has_permission(auth.user, ADMIN) + else: + if isinstance(obj, DraftRegistration): + return obj.is_contributor(auth.user) and obj.has_permission(auth.user, WRITE) + elif isinstance(obj, AbstractNode): + return obj.has_permission(auth.user, WRITE) + return False diff --git a/api/draft_registrations/serializers.py b/api/draft_registrations/serializers.py index 3f2db261c84..84d0c48423c 100644 --- a/api/draft_registrations/serializers.py +++ b/api/draft_registrations/serializers.py @@ -8,7 +8,6 @@ from api.nodes.serializers import ( DraftRegistrationLegacySerializer, DraftRegistrationDetailLegacySerializer, - update_institutions, get_license_details, NodeSerializer, NodeLicenseSerializer, @@ -18,6 +17,7 @@ NodeContributorDetailSerializer, RegistrationSchemaRelationshipField, ) +from api.institutions.utils import update_institutions from api.taxonomies.serializers import TaxonomizableSerializerMixin from osf.exceptions import DraftRegistrationStateError from osf.models import Node diff --git a/api/draft_registrations/views.py b/api/draft_registrations/views.py index 16443195492..30c583dd94a 100644 --- a/api/draft_registrations/views.py +++ b/api/draft_registrations/views.py @@ -6,7 +6,7 @@ from api.base.pagination import DraftRegistrationContributorPagination from api.draft_registrations.permissions import ( DraftContributorDetailPermissions, - IsContributorOrAdminContributor, + DraftRegistrationPermission, IsAdminContributor, ) from api.draft_registrations.serializers import ( @@ -50,9 +50,9 @@ def check_resource_permissions(self, resource): class DraftRegistrationList(NodeDraftRegistrationsList): permission_classes = ( - IsContributorOrAdminContributor, drf_permissions.IsAuthenticatedOrReadOnly, base_permissions.TokenHasScope, + DraftRegistrationPermission, ) view_category = 'draft_registrations' @@ -70,10 +70,9 @@ def get_queryset(self): # Returns DraftRegistrations for which a user is a contributor return user.draft_registrations_active - class DraftRegistrationDetail(NodeDraftRegistrationDetail, DraftRegistrationMixin): permission_classes = ( - ContributorOrPublic, + DraftRegistrationPermission, AdminDeletePermissions, drf_permissions.IsAuthenticatedOrReadOnly, base_permissions.TokenHasScope, diff --git a/api/files/serializers.py b/api/files/serializers.py index 20287c90454..e68845c4cd1 100644 --- a/api/files/serializers.py +++ b/api/files/serializers.py @@ -445,7 +445,6 @@ def to_representation(self, value): guid = Guid.load(view.kwargs['file_id']) if guid: data['data']['id'] = guid._id - return data diff --git a/api/files/views.py b/api/files/views.py index 4a4861f31ec..5a498fa7089 100644 --- a/api/files/views.py +++ b/api/files/views.py @@ -57,6 +57,9 @@ def get_file(self, check_permissions=True): if obj.target.creator.is_disabled: raise Gone(detail='This user has been deactivated and their quickfiles are no longer available.') + if getattr(obj.target, 'is_retracted', False): + raise Gone(detail='The requested file is no longer available.') + if check_permissions: # May raise a permission denied self.check_object_permissions(self.request, obj) diff --git a/api/institutions/authentication.py b/api/institutions/authentication.py index 0370201e077..a5588c2b034 100644 --- a/api/institutions/authentication.py +++ b/api/institutions/authentication.py @@ -25,6 +25,7 @@ from website.mails import send_mail, WELCOME_OSF4I, DUPLICATE_ACCOUNTS_OSF4I, ADD_SSO_EMAIL_OSF4I from website.settings import OSF_SUPPORT_EMAIL, DOMAIN +from website.util.metrics import institution_source_tag logger = logging.getLogger(__name__) @@ -388,6 +389,8 @@ def authenticate(self, request): sso_mail=sso_email, sso_department=department, ) + if is_created: + user.add_system_tag(institution_source_tag(secondary_institution._id)) # Storage region is only updated if the user is created via institutional SSO; the region will be set to the # institution's preferred one if the user's current region is not in the institution's default region list. diff --git a/api/institutions/serializers.py b/api/institutions/serializers.py index d19e4f7ff0c..f1124d896f8 100644 --- a/api/institutions/serializers.py +++ b/api/institutions/serializers.py @@ -129,7 +129,7 @@ def create(self, validated_data): if not node.has_permission(user, osf_permissions.WRITE): raise exceptions.PermissionDenied(detail='Write permission on node {} required'.format(node_dict['_id'])) if not node.is_affiliated_with_institution(inst): - node.add_affiliated_institution(inst, user, save=True) + node.add_affiliated_institution(inst, user) changes_flag = True if not changes_flag: @@ -174,7 +174,7 @@ def create(self, validated_data): if not registration.has_permission(user, osf_permissions.WRITE): raise exceptions.PermissionDenied(detail='Write permission on registration {} required'.format(registration_dict['_id'])) if not registration.is_affiliated_with_institution(inst): - registration.add_affiliated_institution(inst, user, save=True) + registration.add_affiliated_institution(inst, user) changes_flag = True if not changes_flag: @@ -292,3 +292,9 @@ def get_absolute_url(self, obj): 'version': 'v2', }, ) + + +class InstitutionRelated(JSONAPIRelationshipSerializer): + id = ser.CharField(source='_id', required=False, allow_null=True) + class Meta: + type_ = 'institutions' diff --git a/api/institutions/utils.py b/api/institutions/utils.py new file mode 100644 index 00000000000..3defb74b031 --- /dev/null +++ b/api/institutions/utils.py @@ -0,0 +1,58 @@ +from rest_framework import exceptions + +from api.base.serializers import relationship_diff +from osf.models import Institution +from osf.utils import permissions as osf_permissions + + +def get_institutions_to_add_remove(institutions, new_institutions): + diff = relationship_diff( + current_items={inst._id: inst for inst in institutions.all()}, + new_items={inst['_id']: inst for inst in new_institutions}, + ) + + insts_to_add = [] + for inst_id in diff['add']: + inst = Institution.load(inst_id) + if not inst: + raise exceptions.NotFound(detail=f'Institution with id "{inst_id}" was not found') + insts_to_add.append(inst) + + return insts_to_add, diff['remove'].values() + + +def update_institutions(resource, new_institutions, user, post=False): + add, remove = get_institutions_to_add_remove( + institutions=resource.affiliated_institutions, + new_institutions=new_institutions, + ) + + if not post: + for inst in remove: + if not user.is_affiliated_with_institution(inst) and not resource.has_permission(user, osf_permissions.ADMIN): + raise exceptions.PermissionDenied(detail=f'User needs to be affiliated with {inst.name}') + resource.remove_affiliated_institution(inst, user) + + for inst in add: + if not user.is_affiliated_with_institution(inst): + raise exceptions.PermissionDenied(detail=f'User needs to be affiliated with {inst.name}') + resource.add_affiliated_institution(inst, user) + + +def update_institutions_if_user_associated(resource, desired_institutions_data, user): + """Update institutions only if the user is associated with the institutions. Otherwise, raise an exception.""" + + desired_institutions = Institution.objects.filter(_id__in=[item['_id'] for item in desired_institutions_data]) + + # If a user wants to affiliate with a resource check that they have it. + for inst in desired_institutions: + if user.is_affiliated_with_institution(inst): + resource.add_affiliated_institution(inst, user) + else: + raise exceptions.PermissionDenied(detail=f'User needs to be affiliated with {inst.name}') + + # If a user doesn't include an affiliation they have, then remove it. + resource_institutions = resource.affiliated_institutions.all() + for inst in user.get_affiliated_institutions(): + if inst in resource_institutions and inst not in desired_institutions: + resource.remove_affiliated_institution(inst, user) diff --git a/api/nodes/permissions.py b/api/nodes/permissions.py index 3527d46504d..51145b8fd49 100644 --- a/api/nodes/permissions.py +++ b/api/nodes/permissions.py @@ -294,18 +294,6 @@ def has_object_permission(self, request, view, obj): return True -class WriteOrPublicForRelationshipInstitutions(permissions.BasePermission): - def has_object_permission(self, request, view, obj): - assert isinstance(obj, dict) - auth = get_user_auth(request) - node = obj['self'] - - if request.method in permissions.SAFE_METHODS: - return node.is_public or node.can_view(auth) - else: - return node.has_permission(auth.user, osf_permissions.WRITE) - - class ReadOnlyIfRegistration(permissions.BasePermission): """Makes PUT and POST forbidden for registrations.""" diff --git a/api/nodes/serializers.py b/api/nodes/serializers.py index b0ff4ec7323..5c86be774f8 100644 --- a/api/nodes/serializers.py +++ b/api/nodes/serializers.py @@ -7,11 +7,10 @@ ) from api.base.serializers import ( VersionedDateTimeField, HideIfRegistration, IDField, - JSONAPIRelationshipSerializer, JSONAPISerializer, LinksField, NodeFileHyperLinkField, RelationshipField, ShowIfVersion, TargetTypeField, TypeField, - WaterbutlerLink, relationship_diff, BaseAPISerializer, + WaterbutlerLink, BaseAPISerializer, HideIfWikiDisabled, ShowIfAdminScopeOrAnonymous, ValuesListField, TargetField, ) @@ -21,6 +20,7 @@ get_user_auth, is_truthy, ) from api.base.versioning import get_kebab_snake_case_field +from api.institutions.utils import update_institutions from api.taxonomies.serializers import TaxonomizableSerializerMixin from django.apps import apps from django.conf import settings @@ -34,7 +34,7 @@ from addons.osfstorage.models import Region from osf.exceptions import NodeStateError from osf.models import ( - Comment, DraftRegistration, ExternalAccount, Institution, + Comment, DraftRegistration, ExternalAccount, RegistrationSchema, AbstractNode, PrivateLink, Preprint, RegistrationProvider, OSFGroup, NodeLicense, DraftNode, Registration, Node, @@ -52,44 +52,6 @@ def to_internal_value(self, data): return self.get_object(data) -def get_institutions_to_add_remove(institutions, new_institutions): - diff = relationship_diff( - current_items={inst._id: inst for inst in institutions.all()}, - new_items={inst['_id']: inst for inst in new_institutions}, - ) - - insts_to_add = [] - for inst_id in diff['add']: - inst = Institution.load(inst_id) - if not inst: - raise exceptions.NotFound(detail=f'Institution with id "{inst_id}" was not found') - insts_to_add.append(inst) - - return insts_to_add, diff['remove'].values() - - -def update_institutions(node, new_institutions, user, post=False): - add, remove = get_institutions_to_add_remove( - institutions=node.affiliated_institutions, - new_institutions=new_institutions, - ) - - if not post: - for inst in remove: - if not user.is_affiliated_with_institution(inst) and not node.has_permission(user, osf_permissions.ADMIN): - raise exceptions.PermissionDenied( - detail=f'User needs to be affiliated with {inst.name}', - ) - node.remove_affiliated_institution(inst, user) - - for inst in add: - if not user.is_affiliated_with_institution(inst): - raise exceptions.PermissionDenied( - detail=f'User needs to be affiliated with {inst.name}', - ) - node.add_affiliated_institution(inst, user) - - class RegionRelationshipField(RelationshipField): def to_internal_value(self, data): @@ -1483,13 +1445,10 @@ def get_storage_addons_url(self, obj): }, ) -class InstitutionRelated(JSONAPIRelationshipSerializer): - id = ser.CharField(source='_id', required=False, allow_null=True) - class Meta: - type_ = 'institutions' - class NodeInstitutionsRelationshipSerializer(BaseAPISerializer): + from api.institutions.serializers import InstitutionRelated # Avoid circular import + data = ser.ListField(child=InstitutionRelated()) links = LinksField({ 'self': 'get_self_url', diff --git a/api/nodes/views.py b/api/nodes/views.py index bfc6a077800..0d659e2bd27 100644 --- a/api/nodes/views.py +++ b/api/nodes/views.py @@ -8,7 +8,7 @@ from django.db.models import F, Max, Q, Subquery from django.utils import timezone from django.contrib.contenttypes.models import ContentType -from rest_framework import generics, permissions as drf_permissions +from rest_framework import generics, permissions as drf_permissions, exceptions from rest_framework.exceptions import PermissionDenied, ValidationError, NotFound, MethodNotAllowed, NotAuthenticated from rest_framework.response import Response from rest_framework.status import HTTP_202_ACCEPTED, HTTP_204_NO_CONTENT @@ -60,6 +60,7 @@ WaterButlerMixin, ) from api.base.waffle_decorators import require_flag +from api.base.permissions import WriteOrPublicForRelationshipInstitutions from api.cedar_metadata_records.serializers import CedarMetadataRecordsListSerializer from api.cedar_metadata_records.utils import can_view_record from api.citations.utils import render_citation @@ -69,6 +70,7 @@ NodeCommentSerializer, ) from api.draft_registrations.serializers import DraftRegistrationSerializer, DraftRegistrationDetailSerializer +from api.draft_registrations.permissions import DraftRegistrationPermission from api.files.serializers import FileSerializer, OsfStorageFileSerializer from api.files import annotations as file_annotations from api.identifiers.serializers import NodeIdentifierSerializer @@ -78,7 +80,6 @@ from api.nodes.filters import NodesFilterMixin from api.nodes.permissions import ( IsAdmin, - IsAdminContributor, IsPublic, AdminOrPublic, WriteAdmin, @@ -90,7 +91,6 @@ NodeGroupDetailPermissions, IsContributorOrGroupMember, AdminDeletePermissions, - WriteOrPublicForRelationshipInstitutions, ExcludeWithdrawals, NodeLinksShowIfVersion, ReadOnlyIfWithdrawn, @@ -629,7 +629,7 @@ class NodeDraftRegistrationsList(JSONAPIBaseView, generics.ListCreateAPIView, No Use DraftRegistrationsList endpoint instead. """ permission_classes = ( - IsAdminContributor, + DraftRegistrationPermission, drf_permissions.IsAuthenticatedOrReadOnly, base_permissions.TokenHasScope, ) @@ -652,8 +652,11 @@ def get_serializer_class(self): # overrides ListCreateAPIView def get_queryset(self): + user = self.request.user node = self.get_node() - return node.draft_registrations_active + if user.is_anonymous: + raise exceptions.NotAuthenticated() + return user.draft_registrations_active.filter(branched_from=node) class NodeDraftRegistrationDetail(JSONAPIBaseView, generics.RetrieveUpdateDestroyAPIView, DraftMixin): @@ -663,9 +666,9 @@ class NodeDraftRegistrationDetail(JSONAPIBaseView, generics.RetrieveUpdateDestro Use DraftRegistrationDetail endpoint instead. """ permission_classes = ( + DraftRegistrationPermission, drf_permissions.IsAuthenticatedOrReadOnly, base_permissions.TokenHasScope, - IsAdminContributor, ) parser_classes = (JSONAPIMultipleRelationshipsParser, JSONAPIMultipleRelationshipsParserForRegularJSON) diff --git a/api/preprints/permissions.py b/api/preprints/permissions.py index 0d64f15662d..7bfaacdde3d 100644 --- a/api/preprints/permissions.py +++ b/api/preprints/permissions.py @@ -137,3 +137,30 @@ def has_object_permission(self, request, view, obj): raise exceptions.PermissionDenied(detail='Withdrawn preprints may not be edited') return True raise exceptions.NotFound + + +class PreprintInstitutionPermissionList(permissions.BasePermission): + """ + Custom permission class for checking access to a list of institutions + associated with a preprint. + + Permissions: + - Allows safe methods (GET, HEAD, OPTIONS) for public preprints. + - For private preprints, checks if the user has read permissions. + + Methods: + - has_object_permission: Raises MethodNotAllowed for non-safe methods and + checks if the user has the necessary permissions to access private preprints. + """ + def has_object_permission(self, request, view, obj): + if request.method not in permissions.SAFE_METHODS: + raise exceptions.MethodNotAllowed(method=request.method) + + if obj.is_public: + return True + + auth = get_user_auth(request) + if not auth.user: + return False + else: + return obj.has_permission(auth.user, osf_permissions.READ) diff --git a/api/preprints/serializers.py b/api/preprints/serializers.py index 3936292b6cb..97cc3f3fb7c 100644 --- a/api/preprints/serializers.py +++ b/api/preprints/serializers.py @@ -7,6 +7,7 @@ from api.base.exceptions import Conflict, JSONAPIException from api.base.serializers import ( + BaseAPISerializer, JSONAPISerializer, IDField, TypeField, @@ -35,10 +36,11 @@ NodeTagField, ) from api.base.metrics import MetricsSerializerMixin +from api.institutions.utils import update_institutions_if_user_associated from api.taxonomies.serializers import TaxonomizableSerializerMixin from framework.exceptions import PermissionsError from website.project import signals as project_signals -from osf.exceptions import NodeStateError +from osf.exceptions import NodeStateError, PreprintStateError from osf.models import ( BaseFileNode, Preprint, @@ -48,8 +50,6 @@ ) from osf.utils import permissions as osf_permissions -from osf.exceptions import PreprintStateError - class PrimaryFileRelationshipField(RelationshipField): def get_object(self, file_id): @@ -206,6 +206,16 @@ class PreprintSerializer(TaxonomizableSerializerMixin, MetricsSerializerMixin, J ), ) + affiliated_institutions = RelationshipField( + related_view='preprints:preprints-institutions', + related_view_kwargs={'preprint_id': '<_id>'}, + self_view='preprints:preprint-relationships-institutions', + self_view_kwargs={'preprint_id': '<_id>'}, + read_only=False, + required=False, + allow_null=True, + ) + links = LinksField( { 'self': 'get_preprint_url', @@ -359,59 +369,7 @@ def update(self, preprint, validated_data): preprint.custom_publication_citation = validated_data['custom_publication_citation'] or None save_preprint = True - if 'has_coi' in validated_data: - try: - preprint.update_has_coi(auth, validated_data['has_coi']) - except PreprintStateError as e: - raise exceptions.ValidationError(detail=str(e)) - - if 'conflict_of_interest_statement' in validated_data: - try: - preprint.update_conflict_of_interest_statement(auth, validated_data['conflict_of_interest_statement']) - except PreprintStateError as e: - raise exceptions.ValidationError(detail=str(e)) - - if 'has_data_links' in validated_data: - try: - preprint.update_has_data_links(auth, validated_data['has_data_links']) - except PreprintStateError as e: - raise exceptions.ValidationError(detail=str(e)) - - if 'why_no_data' in validated_data: - try: - preprint.update_why_no_data(auth, validated_data['why_no_data']) - except PreprintStateError as e: - raise exceptions.ValidationError(detail=str(e)) - - if 'data_links' in validated_data: - try: - preprint.update_data_links(auth, validated_data['data_links']) - except PreprintStateError as e: - raise exceptions.ValidationError(detail=str(e)) - - if 'has_prereg_links' in validated_data: - try: - preprint.update_has_prereg_links(auth, validated_data['has_prereg_links']) - except PreprintStateError as e: - raise exceptions.ValidationError(detail=str(e)) - - if 'why_no_prereg' in validated_data: - try: - preprint.update_why_no_prereg(auth, validated_data['why_no_prereg']) - except PreprintStateError as e: - raise exceptions.ValidationError(detail=str(e)) - - if 'prereg_links' in validated_data: - try: - preprint.update_prereg_links(auth, validated_data['prereg_links']) - except PreprintStateError as e: - raise exceptions.ValidationError(detail=str(e)) - - if 'prereg_link_info' in validated_data: - try: - preprint.update_prereg_link_info(auth, validated_data['prereg_link_info']) - except PreprintStateError as e: - raise exceptions.ValidationError(detail=str(e)) + self.handle_author_assertions(preprint, validated_data, auth) if published is not None: if not preprint.primary_file: @@ -438,6 +396,76 @@ def update(self, preprint, validated_data): return preprint + def handle_author_assertions(self, preprint, validated_data, auth): + author_assertions = { + 'has_coi', + 'conflict_of_interest_statement', + 'has_data_links', + 'why_no_data', + 'data_links', + 'why_no_prereg', + 'prereg_links', + 'has_prereg_links', + 'prereg_link_info', + } + if author_assertions & validated_data.keys(): + if not preprint.is_admin_contributor(auth.user): + raise exceptions.PermissionDenied('User must be admin to add author assertions') + + if 'has_coi' in validated_data: + try: + preprint.update_has_coi(auth, validated_data['has_coi']) + except PreprintStateError as e: + raise exceptions.ValidationError(detail=str(e)) + + if 'conflict_of_interest_statement' in validated_data: + try: + preprint.update_conflict_of_interest_statement(auth, validated_data['conflict_of_interest_statement']) + except PreprintStateError as e: + raise exceptions.ValidationError(detail=str(e)) + + if 'has_data_links' in validated_data: + try: + preprint.update_has_data_links(auth, validated_data['has_data_links']) + except PreprintStateError as e: + raise exceptions.ValidationError(detail=str(e)) + + if 'why_no_data' in validated_data: + try: + preprint.update_why_no_data(auth, validated_data['why_no_data']) + except PreprintStateError as e: + raise exceptions.ValidationError(detail=str(e)) + + if 'data_links' in validated_data: + try: + preprint.update_data_links(auth, validated_data['data_links']) + except PreprintStateError as e: + raise exceptions.ValidationError(detail=str(e)) + + if 'has_prereg_links' in validated_data: + try: + preprint.update_has_prereg_links(auth, validated_data['has_prereg_links']) + except PreprintStateError as e: + raise exceptions.ValidationError(detail=str(e)) + + if 'why_no_prereg' in validated_data: + try: + preprint.update_why_no_prereg(auth, validated_data['why_no_prereg']) + except PreprintStateError as e: + raise exceptions.ValidationError(detail=str(e)) + + if 'prereg_links' in validated_data: + try: + preprint.update_prereg_links(auth, validated_data['prereg_links']) + except PreprintStateError as e: + raise exceptions.ValidationError(detail=str(e)) + + if 'prereg_link_info' in validated_data: + try: + preprint.update_prereg_link_info(auth, validated_data['prereg_link_info']) + except PreprintStateError as e: + raise exceptions.ValidationError(detail=str(e)) + def set_field(self, func, val, auth, save=False): try: func(val, auth) @@ -447,6 +475,21 @@ def set_field(self, func, val, auth, save=False): raise exceptions.ValidationError(detail=str(e)) +class PreprintDraftSerializer(PreprintSerializer): + + def get_absolute_url(self, obj): + return absolute_reverse( + 'users:user-draft-preprints', + kwargs={ + 'preprint_id': obj._id, + 'version': self.context['request'].parser_context['kwargs']['version'], + }, + ) + + class Meta: + type_ = 'draft-preprints' + + class PreprintCreateSerializer(PreprintSerializer): # Overrides PreprintSerializer to make id nullable, adds `create` id = IDField(source='_id', required=False, allow_null=True) @@ -574,3 +617,39 @@ def update(self, instance, validated_data): links = LinksField({ 'self': 'get_self_url', }) + + +class PreprintsInstitutionsRelationshipSerializer(BaseAPISerializer): + from api.institutions.serializers import InstitutionRelated # Avoid circular import + data = ser.ListField(child=InstitutionRelated()) + + links = LinksField({ + 'self': 'get_self_url', + }) + + def get_self_url(self, obj): + return obj['self'].institutions_relationship_url + + class Meta: + type_ = 'institutions' + + def make_instance_obj(self, obj): + return { + 'data': obj.affiliated_institutions.all(), + 'self': obj, + } + + def update(self, instance, validated_data): + preprint = instance['self'] + user = self.context['request'].user + update_institutions_if_user_associated(preprint, validated_data['data'], user) + preprint.save() + return self.make_instance_obj(preprint) + + def create(self, validated_data): + instance = self.context['view'].get_object() + preprint = instance['self'] + user = self.context['request'].user + update_institutions_if_user_associated(preprint, validated_data['data'], user) + preprint.save() + return self.make_instance_obj(preprint) diff --git a/api/preprints/urls.py b/api/preprints/urls.py index f3de2782265..5971d61c7e5 100644 --- a/api/preprints/urls.py +++ b/api/preprints/urls.py @@ -20,4 +20,6 @@ re_path(r'^(?P\w+)/review_actions/$', views.PreprintActionList.as_view(), name=views.PreprintActionList.view_name), re_path(r'^(?P\w+)/requests/$', views.PreprintRequestListCreate.as_view(), name=views.PreprintRequestListCreate.view_name), re_path(r'^(?P\w+)/subjects/$', views.PreprintSubjectsList.as_view(), name=views.PreprintSubjectsList.view_name), + re_path(r'^(?P\w+)/institutions/$', views.PreprintInstitutionsList.as_view(), name=views.PreprintInstitutionsList.view_name), + re_path(r'^(?P\w+)/relationships/institutions/$', views.PreprintInstitutionsRelationship.as_view(), name=views.PreprintInstitutionsRelationship.view_name), ] diff --git a/api/preprints/views.py b/api/preprints/views.py index 2f6bbad0480..4c3c52f936f 100644 --- a/api/preprints/views.py +++ b/api/preprints/views.py @@ -6,7 +6,12 @@ from rest_framework import permissions as drf_permissions from framework.auth.oauth_scopes import CoreScopes -from osf.models import ReviewAction, Preprint, PreprintContributor +from osf.models import ( + ReviewAction, + Preprint, + PreprintContributor, + Institution, +) from osf.utils.requests import check_select_for_update from api.actions.permissions import ReviewActionPermission @@ -17,12 +22,13 @@ from api.base.views import JSONAPIBaseView, WaterButlerMixin from api.base.filters import ListFilterMixin, PreprintFilterMixin from api.base.parsers import ( - JSONAPIOnetoOneRelationshipParser, - JSONAPIOnetoOneRelationshipParserForRegularJSON, JSONAPIMultipleRelationshipsParser, JSONAPIMultipleRelationshipsParserForRegularJSON, + JSONAPIOnetoOneRelationshipParser, + JSONAPIOnetoOneRelationshipParserForRegularJSON, + JSONAPIRelationshipParser, + JSONAPIRelationshipParserForRegularJSON, ) - from api.base.utils import absolute_reverse, get_user_auth, get_object_or_error from api.base import permissions as base_permissions from api.citations.utils import render_citation @@ -35,15 +41,14 @@ PreprintStorageProviderSerializer, PreprintNodeRelationshipSerializer, PreprintContributorsCreateSerializer, + PreprintsInstitutionsRelationshipSerializer, ) from api.files.serializers import OsfStorageFileSerializer -from api.nodes.serializers import ( - NodeCitationStyleSerializer, -) - from api.identifiers.views import IdentifierList from api.identifiers.serializers import PreprintIdentifierSerializer +from api.institutions.serializers import InstitutionSerializer from api.nodes.views import NodeMixin, NodeContributorsList, NodeContributorDetail, NodeFilesList, NodeStorageProvidersList, NodeStorageProvider +from api.nodes.serializers import NodeCitationStyleSerializer from api.preprints.permissions import ( PreprintPublishedOrAdmin, PreprintPublishedOrWrite, @@ -51,10 +56,10 @@ AdminOrPublic, ContributorDetailPermissions, PreprintFilesPermissions, + PreprintInstitutionPermissionList, ) -from api.nodes.permissions import ( - ContributorOrPublic, -) +from api.nodes.permissions import ContributorOrPublic +from api.base.permissions import WriteOrPublicForRelationshipInstitutions from api.requests.permissions import PreprintRequestPermission from api.requests.serializers import PreprintRequestSerializer, PreprintRequestCreateSerializer from api.requests.views import PreprintRequestMixin @@ -62,6 +67,7 @@ from api.base.metrics import PreprintMetricsViewMixin from osf.metrics import PreprintDownload, PreprintView + class PreprintMixin(NodeMixin): serializer_class = PreprintSerializer preprint_lookup_url_kwarg = 'preprint_id' @@ -647,3 +653,60 @@ def get_default_queryset(self): def get_queryset(self): return self.get_queryset_from_request() + + +class PreprintInstitutionsList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin, PreprintMixin): + """The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/preprint_institutions_list). + """ + permission_classes = ( + drf_permissions.IsAuthenticatedOrReadOnly, + base_permissions.TokenHasScope, + PreprintInstitutionPermissionList, + ) + + required_read_scopes = [CoreScopes.PREPRINTS_READ, CoreScopes.INSTITUTION_READ] + required_write_scopes = [CoreScopes.NULL] + serializer_class = InstitutionSerializer + + model = Institution + view_category = 'preprints' + view_name = 'preprints-institutions' + + ordering = ('-id',) + + def get_resource(self): + return self.get_preprint() + + def get_queryset(self): + return self.get_resource().affiliated_institutions.all() + + +class PreprintInstitutionsRelationship(JSONAPIBaseView, generics.RetrieveUpdateAPIView, PreprintMixin): + """ """ + permission_classes = ( + drf_permissions.IsAuthenticatedOrReadOnly, + base_permissions.TokenHasScope, + WriteOrPublicForRelationshipInstitutions, + ) + required_read_scopes = [CoreScopes.PREPRINTS_READ] + required_write_scopes = [CoreScopes.PREPRINTS_WRITE] + serializer_class = PreprintsInstitutionsRelationshipSerializer + parser_classes = (JSONAPIRelationshipParser, JSONAPIRelationshipParserForRegularJSON) + + view_category = 'preprints' + view_name = 'preprint-relationships-institutions' + + def get_resource(self): + return self.get_preprint(check_object_permissions=False) + + def get_object(self): + preprint = self.get_resource() + obj = { + 'data': preprint.affiliated_institutions.all(), + 'self': preprint, + } + self.check_object_permissions(self.request, obj) + return obj + + def patch(self, *args, **kwargs): + raise MethodNotAllowed(self.request.method) diff --git a/api/providers/serializers.py b/api/providers/serializers.py index 91a13bb980d..ef89388e281 100644 --- a/api/providers/serializers.py +++ b/api/providers/serializers.py @@ -330,10 +330,11 @@ def create(self, validated_data): raise ValidationError('"full_name" is required when adding a moderator via email.') user = OSFUser.create_unregistered(full_name, email=address) user.add_unclaimed_record( - provider, referrer=auth.user, - given_name=full_name, email=address, + provider, + referrer=auth.user, + given_name=full_name, + email=address, ) - user.save() claim_url = user.get_claim_url(provider._id, external=True) context['claim_url'] = claim_url else: diff --git a/api/registrations/serializers.py b/api/registrations/serializers.py index 6e575bd33ad..f1676fc1ff7 100644 --- a/api/registrations/serializers.py +++ b/api/registrations/serializers.py @@ -19,7 +19,6 @@ NodeStorageProviderSerializer, NodeLicenseRelationshipField, NodeLinksSerializer, - update_institutions, NodeLicenseSerializer, NodeContributorsSerializer, RegistrationProviderRelationshipField, @@ -31,13 +30,13 @@ ShowIfVersion, VersionedDateTimeField, ValuesListField, HideIfWithdrawalOrWikiDisabled, ) +from api.institutions.utils import update_institutions from framework.auth.core import Auth from osf.exceptions import NodeStateError from osf.models import Node from osf.utils.registrations import strip_registered_meta_comments from osf.utils.workflows import ApprovalStates - class RegistrationSerializer(NodeSerializer): admin_only_editable_fields = [ 'custom_citation', diff --git a/api/users/serializers.py b/api/users/serializers.py index fd92914df05..5e8ca59d9cf 100644 --- a/api/users/serializers.py +++ b/api/users/serializers.py @@ -19,11 +19,10 @@ JSONAPIListField, ShowIfCurrentUser, ) -from api.base.utils import absolute_reverse, get_user_auth, is_deprecated, hashids -from api.base.utils import default_node_list_queryset +from api.base.utils import absolute_reverse, default_node_list_queryset, get_user_auth, is_deprecated, hashids from api.base.versioning import get_kebab_snake_case_field from api.nodes.serializers import NodeSerializer, RegionRelationshipField -from framework.auth.views import send_confirm_email +from framework.auth.views import send_confirm_email_async from osf.exceptions import ValidationValueError, ValidationError, BlockedEmailError from osf.models import Email, Node, OSFUser, Preprint, Registration from osf.models.provider import AbstractProviderGroupObjectPermission @@ -140,6 +139,14 @@ class UserSerializer(JSONAPISerializer): ), ) + draft_preprints = HideIfDisabled( + RelationshipField( + related_view='users:user-draft-preprints', + related_view_kwargs={'user_id': '<_id>'}, + related_meta={'count': 'get_draft_preprint_count'}, + ), + ) + emails = ShowIfCurrentUser( RelationshipField( related_view='users:user-emails', @@ -202,6 +209,11 @@ def get_preprint_count(self, obj): user_preprints_query = Preprint.objects.filter(_contributors__guids___id=obj._id).exclude(machine_state='initial') return Preprint.objects.can_view(user_preprints_query, auth_user, allow_contribs=False).count() + def get_draft_preprint_count(self, obj): + auth_user = get_user_auth(self.context['request']).user + user_preprints_query = Preprint.objects.filter(_contributors__guids___id=obj._id).filter(machine_state='initial') + return Preprint.objects.can_view(user_preprints_query, auth_user, allow_contribs=False).count() + def get_institutions_count(self, obj): if isinstance(obj, OSFUser): return obj.get_affiliated_institutions().count() @@ -610,7 +622,7 @@ def create(self, validated_data): token = user.add_unconfirmed_email(address) user.save() if CONFIRM_REGISTRATIONS_BY_EMAIL: - send_confirm_email(user, email=address) + send_confirm_email_async(user, email=address) user.email_last_sent = timezone.now() user.save() except ValidationError as e: diff --git a/api/users/urls.py b/api/users/urls.py index 8e273368e44..cf9bd0bb7b9 100644 --- a/api/users/urls.py +++ b/api/users/urls.py @@ -16,6 +16,7 @@ re_path(r'^(?P\w+)/nodes/$', views.UserNodes.as_view(), name=views.UserNodes.view_name), re_path(r'^(?P\w+)/groups/$', views.UserGroups.as_view(), name=views.UserGroups.view_name), re_path(r'^(?P\w+)/preprints/$', views.UserPreprints.as_view(), name=views.UserPreprints.view_name), + re_path(r'^(?P\w+)/draft_preprints/$', views.UserDraftPreprints.as_view(), name=views.UserDraftPreprints.view_name), re_path(r'^(?P\w+)/registrations/$', views.UserRegistrations.as_view(), name=views.UserRegistrations.view_name), re_path(r'^(?P\w+)/settings/$', views.UserSettings.as_view(), name=views.UserSettings.view_name), re_path(r'^(?P\w+)/quickfiles/$', views.UserQuickFiles.as_view(), name=views.UserQuickFiles.view_name), diff --git a/api/users/views.py b/api/users/views.py index 325619d517d..927b5dc2f9b 100644 --- a/api/users/views.py +++ b/api/users/views.py @@ -31,7 +31,7 @@ from api.nodes.serializers import DraftRegistrationLegacySerializer from api.nodes.utils import NodeOptimizationMixin from api.osf_groups.serializers import GroupSerializer -from api.preprints.serializers import PreprintSerializer +from api.preprints.serializers import PreprintSerializer, PreprintDraftSerializer from api.registrations import annotations as registration_annotations from api.registrations.serializers import RegistrationSerializer from api.resources import annotations as resource_annotations @@ -60,7 +60,7 @@ from django.http import JsonResponse from django.utils import timezone from framework.auth.core import get_user -from framework.auth.views import send_confirm_email +from framework.auth.views import send_confirm_email_async from framework.auth.oauth_scopes import CoreScopes, normalize_scopes from framework.auth.exceptions import ChangePasswordError from framework.utils import throttle_period_expired @@ -413,6 +413,36 @@ def get_queryset(self): return self.get_queryset_from_request() +class UserDraftPreprints(JSONAPIBaseView, generics.ListAPIView, UserMixin, PreprintFilterMixin): + """The documentation for this endpoint can be found [here](https://developer.osf.io/). + """ + + permission_classes = ( + drf_permissions.IsAuthenticatedOrReadOnly, + base_permissions.TokenHasScope, + CurrentUser, + ) + + ordering = ('-created') + + required_read_scopes = [CoreScopes.USERS_READ, CoreScopes.NODE_PREPRINTS_READ] + required_write_scopes = [CoreScopes.USERS_WRITE, CoreScopes.NODE_PREPRINTS_WRITE] + + serializer_class = PreprintDraftSerializer + view_category = 'users' + view_name = 'user-draft-preprints' + + def get_default_queryset(self): + user = self.get_user() + return user.preprints.filter( + machine_state='initial', + deleted__isnull=True, + ) + + def get_queryset(self): + return self.get_queryset_from_request() + + class UserInstitutions(JSONAPIBaseView, generics.ListAPIView, UserMixin): """The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/users_institutions_list). """ @@ -900,7 +930,7 @@ def get_object(self): if self.request.method == 'GET' and is_truthy(self.request.query_params.get('resend_confirmation')): if not confirmed and settings.CONFIRM_REGISTRATIONS_BY_EMAIL: if throttle_period_expired(user.email_last_sent, settings.SEND_EMAIL_THROTTLE): - send_confirm_email(user, email=address, renew=True) + send_confirm_email_async(user, email=address, renew=True) user.email_last_sent = timezone.now() user.save() diff --git a/api_tests/draft_registrations/views/test_draft_registration_detail.py b/api_tests/draft_registrations/views/test_draft_registration_detail.py index 18b00014f94..2106f87fb5a 100644 --- a/api_tests/draft_registrations/views/test_draft_registration_detail.py +++ b/api_tests/draft_registrations/views/test_draft_registration_detail.py @@ -2,10 +2,10 @@ from api.base.settings.defaults import API_BASE from api_tests.nodes.views.test_node_draft_registration_detail import ( - TestDraftRegistrationDetail, TestDraftRegistrationUpdate, TestDraftRegistrationPatch, TestDraftRegistrationDelete, + AbstractDraftRegistrationTestCase ) from osf.models import DraftNode, Node, NodeLicense, RegistrationSchema from osf.utils.permissions import ADMIN, READ, WRITE @@ -16,58 +16,34 @@ SubjectFactory, ProjectFactory, ) +from website.settings import API_DOMAIN @pytest.mark.django_db -class TestDraftRegistrationDetailEndpoint(TestDraftRegistrationDetail): +class TestDraftRegistrationDetailEndpoint(AbstractDraftRegistrationTestCase): @pytest.fixture() def url_draft_registrations(self, project_public, draft_registration): - return '/{}draft_registrations/{}/'.format( - API_BASE, draft_registration._id) - - # Overrides TestDraftRegistrationDetail - def test_admin_group_member_can_view(self, app, user, draft_registration, project_public, - schema, url_draft_registrations, group_mem): - - res = app.get(url_draft_registrations, auth=group_mem.auth, expect_errors=True) - assert res.status_code == 403 + return f'/{API_BASE}draft_registrations/{draft_registration._id}/' - def test_can_view_draft( - self, app, user_write_contrib, project_public, - user_read_contrib, user_non_contrib, - url_draft_registrations, group, group_mem): - - # test_read_only_contributor_can_view_draft - res = app.get( - url_draft_registrations, - auth=user_read_contrib.auth, - expect_errors=False) + def test_read_only_contributor_can_view_draft(self, app, user_read_contrib, url_draft_registrations): + res = app.get(url_draft_registrations, auth=user_read_contrib.auth) assert res.status_code == 200 - # test_read_write_contributor_can_view_draft - res = app.get( - url_draft_registrations, - auth=user_write_contrib.auth, - expect_errors=False) + def test_read_write_contributor_can_view_draft(self, app, user_write_contrib, url_draft_registrations): + res = app.get(url_draft_registrations, auth=user_write_contrib.auth) assert res.status_code == 200 - def test_cannot_view_draft( - self, app, project_public, - user_non_contrib, url_draft_registrations): - - # test_logged_in_non_contributor_cannot_view_draft - res = app.get( - url_draft_registrations, - auth=user_non_contrib.auth, - expect_errors=True) + def test_logged_in_non_contributor_cannot_view_draft(self, app, user_non_contrib, url_draft_registrations): + res = app.get(url_draft_registrations, auth=user_non_contrib.auth, expect_errors=True) assert res.status_code == 403 - # test_unauthenticated_user_cannot_view_draft + def test_unauthenticated_user_cannot_view_draft(self, app, url_draft_registrations): res = app.get(url_draft_registrations, expect_errors=True) assert res.status_code == 401 - def test_detail_view_returns_editable_fields(self, app, user, draft_registration, - url_draft_registrations, project_public): + def test_detail_view_returns_editable_fields( + self, app, user, draft_registration, url_draft_registrations, project_public + ): res = app.get(url_draft_registrations, auth=user.auth, expect_errors=True) attributes = res.json['data']['attributes'] @@ -77,7 +53,7 @@ def test_detail_view_returns_editable_fields(self, app, user, draft_registration assert attributes['category'] == project_public.category assert attributes['has_project'] - res.json['data']['links']['self'] == url_draft_registrations + assert res.json['data']['links']['self'] == f'{API_DOMAIN}{url_draft_registrations.lstrip("/")}' relationships = res.json['data']['relationships'] assert Node.load(relationships['branched_from']['data']['id']) == draft_registration.branched_from @@ -89,8 +65,7 @@ def test_detail_view_returns_editable_fields(self, app, user, draft_registration def test_detail_view_returns_editable_fields_no_specified_node(self, app, user): draft_registration = DraftRegistrationFactory(initiator=user, branched_from=None) - url = '/{}draft_registrations/{}/'.format( - API_BASE, draft_registration._id) + url = f'{API_DOMAIN}{API_BASE}draft_registrations/{draft_registration._id}/' res = app.get(url, auth=user.auth, expect_errors=True) attributes = res.json['data']['attributes'] @@ -101,7 +76,7 @@ def test_detail_view_returns_editable_fields_no_specified_node(self, app, user): assert attributes['node_license'] is None assert not attributes['has_project'] - res.json['data']['links']['self'] == url + assert res.json['data']['links']['self'] == url relationships = res.json['data']['relationships'] assert 'affiliated_institutions' in relationships @@ -112,16 +87,13 @@ def test_detail_view_returns_editable_fields_no_specified_node(self, app, user): res = app.get(draft_node_link, auth=user.auth) assert DraftNode.load(res.json['data']['id']) == draft_registration.branched_from - def test_draft_registration_perms_checked_on_draft_not_node(self, app, user, project_public, - draft_registration, url_draft_registrations): - - # Admin on node and draft + def test_admin_node_and_draft(self, app, user, project_public, draft_registration, url_draft_registrations): assert project_public.has_permission(user, ADMIN) is True assert draft_registration.has_permission(user, ADMIN) is True res = app.get(url_draft_registrations, auth=user.auth) assert res.status_code == 200 - # Admin on node but not draft + def test_admin_node_not_draft(self, app, user, project_public, draft_registration, url_draft_registrations): node_admin = AuthUserFactory() project_public.add_contributor(node_admin, ADMIN) assert project_public.has_permission(node_admin, ADMIN) is True @@ -129,7 +101,7 @@ def test_draft_registration_perms_checked_on_draft_not_node(self, app, user, pro res = app.get(url_draft_registrations, auth=node_admin.auth, expect_errors=True) assert res.status_code == 403 - # Admin on draft but not node + def test_admin_draft_not_node(self, app, user, project_public, draft_registration, url_draft_registrations): draft_admin = AuthUserFactory() draft_registration.add_contributor(draft_admin, ADMIN) assert project_public.has_permission(draft_admin, ADMIN) is False @@ -137,19 +109,66 @@ def test_draft_registration_perms_checked_on_draft_not_node(self, app, user, pro res = app.get(url_draft_registrations, auth=draft_admin.auth) assert res.status_code == 200 - # Overwrites TestDraftRegistrationDetail - def test_can_view_after_added( - self, app, schema, draft_registration, url_draft_registrations): - # Draft Registration permissions are no longer based on the branched from project + def test_write_node_and_draft(self, app, user, project_public, draft_registration, url_draft_registrations): + assert project_public.has_permission(user, WRITE) is True + assert draft_registration.has_permission(user, WRITE) is True + res = app.get(url_draft_registrations, auth=user.auth) + assert res.status_code == 200 + + def test_write_node_not_draft(self, app, user, project_public, draft_registration, url_draft_registrations): + node_admin = AuthUserFactory() + project_public.add_contributor(node_admin, WRITE) + assert project_public.has_permission(node_admin, WRITE) is True + assert draft_registration.has_permission(node_admin, WRITE) is False + res = app.get(url_draft_registrations, auth=node_admin.auth, expect_errors=True) + assert res.status_code == 403 + + def test_write_draft_not_node(self, app, user, project_public, draft_registration, url_draft_registrations): + draft_admin = AuthUserFactory() + draft_registration.add_contributor(draft_admin, WRITE) + assert project_public.has_permission(draft_admin, WRITE) is False + assert draft_registration.has_permission(draft_admin, WRITE) is True + res = app.get(url_draft_registrations, auth=draft_admin.auth) + assert res.status_code == 200 + + def test_read_node_and_draft(self, app, user, project_public, draft_registration, url_draft_registrations): + assert project_public.has_permission(user, READ) is True + assert draft_registration.has_permission(user, READ) is True + res = app.get(url_draft_registrations, auth=user.auth) + assert res.status_code == 200 + def test_read_node_not_draft(self, app, user, project_public, draft_registration, url_draft_registrations): + node_admin = AuthUserFactory() + project_public.add_contributor(node_admin, READ) + assert project_public.has_permission(node_admin, READ) is True + assert draft_registration.has_permission(node_admin, READ) is False + res = app.get(url_draft_registrations, auth=node_admin.auth, expect_errors=True) + assert res.status_code == 403 + + def test_read_draft_not_node(self, app, user, project_public, draft_registration, url_draft_registrations): + draft_admin = AuthUserFactory() + draft_registration.add_contributor(draft_admin, READ) + assert project_public.has_permission(draft_admin, READ) is False + assert draft_registration.has_permission(draft_admin, READ) is True + res = app.get(url_draft_registrations, auth=draft_admin.auth) + assert res.status_code == 200 + + def test_can_view_after_added(self, app, schema, draft_registration, url_draft_registrations): + """ + Ensure Draft Registration permissions are no longer based on the branched from project + """ user = AuthUserFactory() project = draft_registration.branched_from project.add_contributor(user, ADMIN) res = app.get(url_draft_registrations, auth=user.auth, expect_errors=True) assert res.status_code == 403 + draft_registration.add_contributor(user, ADMIN) + res = app.get(url_draft_registrations, auth=user.auth) + assert res.status_code == 200 - def test_current_permissions_field(self, app, user_read_contrib, - user_write_contrib, user, draft_registration, url_draft_registrations): + def test_current_permissions_field( + self, app, user_read_contrib, user_write_contrib, user, draft_registration, url_draft_registrations + ): res = app.get(url_draft_registrations, auth=user_read_contrib.auth, expect_errors=False) assert res.json['data']['attributes']['current_user_permissions'] == [READ] @@ -548,9 +567,8 @@ def test_write_contributor_can_update_draft( assert data['attributes']['registration_metadata'] == payload['data']['attributes']['registration_metadata'] -class TestDraftRegistrationDelete(TestDraftRegistrationDelete): +class TestDraftRegistrationDeleteDetail(TestDraftRegistrationDelete): @pytest.fixture() def url_draft_registrations(self, project_public, draft_registration): # Overrides TestDraftRegistrationDelete - return '/{}draft_registrations/{}/'.format( - API_BASE, draft_registration._id) + return f'/{API_BASE}draft_registrations/{draft_registration._id}/' diff --git a/api_tests/draft_registrations/views/test_draft_registration_list.py b/api_tests/draft_registrations/views/test_draft_registration_list.py index ba49520a174..1126af09ad3 100644 --- a/api_tests/draft_registrations/views/test_draft_registration_list.py +++ b/api_tests/draft_registrations/views/test_draft_registration_list.py @@ -2,20 +2,19 @@ import pytest from framework.auth.core import Auth -from api_tests.nodes.views.test_node_draft_registration_list import ( - TestDraftRegistrationList, - TestDraftRegistrationCreate -) +from django.utils import timezone +from api_tests.nodes.views.test_node_draft_registration_list import AbstractDraftRegistrationTestCase from api.base.settings.defaults import API_BASE from osf.migrations import ensure_invisible_and_inactive_schema -from osf.models import DraftRegistration, NodeLicense, RegistrationProvider +from osf.models import DraftRegistration, NodeLicense, RegistrationProvider, RegistrationSchema from osf_tests.factories import ( RegistrationFactory, CollectionFactory, ProjectFactory, AuthUserFactory, - InstitutionFactory + InstitutionFactory, + DraftRegistrationFactory, ) from osf.utils.permissions import READ, WRITE, ADMIN @@ -28,52 +27,143 @@ def invisible_and_inactive_schema(): @pytest.mark.django_db -class TestDraftRegistrationListNewWorkflow(TestDraftRegistrationList): +class TestDraftRegistrationListTopLevelEndpoint: + @pytest.fixture() - def url_draft_registrations(self, project_public): - return f'/{API_BASE}draft_registrations/?' + def url_draft_registrations(self): + return f'/{API_BASE}draft_registrations/' - # Overrides TestDraftRegistrationList - def test_osf_group_with_admin_permissions_can_view(self): - # DraftRegistration endpoints permissions are not calculated from the node - return + @pytest.fixture() + def user(self): + return AuthUserFactory() - # Overrides TestDraftRegistrationList - def test_cannot_view_draft_list( - self, app, user_write_contrib, project_public, - user_read_contrib, user_non_contrib, draft_registration, - url_draft_registrations, group, group_mem): + @pytest.fixture() + def user_admin_contrib(self): + return AuthUserFactory() + + @pytest.fixture() + def user_write_contrib(self): + return AuthUserFactory() + + @pytest.fixture() + def user_read_contrib(self): + return AuthUserFactory() + + @pytest.fixture() + def user_non_contrib(self): + return AuthUserFactory() + + @pytest.fixture() + def group_mem(self): + return AuthUserFactory() + + @pytest.fixture() + def project(self, user): + return ProjectFactory(creator=user) + + @pytest.fixture() + def schema(self): + return RegistrationSchema.objects.get(name='Open-Ended Registration', schema_version=3) - # test_read_only_contributor_can_view_draft_list + @pytest.fixture() + def draft_registration(self, user, project, schema, user_write_contrib, user_read_contrib, user_admin_contrib): + draft = DraftRegistrationFactory( + initiator=user, + registration_schema=schema, + branched_from=project + ) + draft.add_contributor(user_read_contrib, permissions=READ) + draft.add_contributor(user_write_contrib, permissions=WRITE) + draft.add_contributor(user_admin_contrib, permissions=ADMIN) + return draft + + def test_read_only_contributor_can_view_draft_list( + self, app, user_read_contrib, draft_registration, url_draft_registrations + ): res = app.get( url_draft_registrations, - auth=user_read_contrib.auth) + auth=user_read_contrib.auth + ) assert res.status_code == 200 assert len(res.json['data']) == 1 - # test_read_write_contributor_can_view_draft_list - res = app.get( - url_draft_registrations, - auth=user_write_contrib.auth) + def test_read_write_contributor_can_view_draft_list( + self, app, user_write_contrib, draft_registration, url_draft_registrations + ): + res = app.get(url_draft_registrations, auth=user_write_contrib.auth) assert res.status_code == 200 assert len(res.json['data']) == 1 - # test_logged_in_non_contributor_can_view_draft_list - res = app.get( - url_draft_registrations, - auth=user_non_contrib.auth, - expect_errors=True) + def test_admin_can_view_draft_list( + self, app, user_admin_contrib, draft_registration, schema, url_draft_registrations + ): + res = app.get(url_draft_registrations, auth=user_admin_contrib.auth) + + assert res.status_code == 200 + data = res.json['data'] + assert len(data) == 1 + + assert schema._id in data[0]['relationships']['registration_schema']['links']['related']['href'] + assert data[0]['id'] == draft_registration._id + assert data[0]['attributes']['registration_metadata'] == {} + + def test_logged_in_non_contributor_has_empty_list( + self, app, user_non_contrib, url_draft_registrations + ): + res = app.get(url_draft_registrations, auth=user_non_contrib.auth) assert res.status_code == 200 assert len(res.json['data']) == 0 - # test_unauthenticated_user_cannot_view_draft_list + def test_unauthenticated_user_cannot_view_draft_list(self, app, url_draft_registrations): res = app.get(url_draft_registrations, expect_errors=True) assert res.status_code == 401 + def test_logged_in_non_contributor_cannot_view_draft_list(self, app, user_non_contrib, url_draft_registrations): + res = app.get(url_draft_registrations, auth=user_non_contrib.auth) + assert res.status_code == 200 + assert len(res.json['data']) == 0 + + def test_deleted_draft_registration_does_not_show_up_in_draft_list(self, app, user, draft_registration, url_draft_registrations): + draft_registration.deleted = timezone.now() + draft_registration.save() + res = app.get(url_draft_registrations, auth=user.auth) + assert res.status_code == 200 + assert not res.json['data'] + + def test_draft_with_registered_node_does_not_show_up_in_draft_list( + self, app, user, project, draft_registration, url_draft_registrations + ): + registration = RegistrationFactory( + project=project, + draft_registration=draft_registration + ) + draft_registration.registered_node = registration + draft_registration.save() + res = app.get(url_draft_registrations, auth=user.auth) + assert res.status_code == 200 + assert not res.json['data'] + + def test_draft_with_deleted_registered_node_shows_up_in_draft_list( + self, app, user, project, draft_registration, schema, url_draft_registrations + ): + registration = RegistrationFactory(project=project, draft_registration=draft_registration) + draft_registration.registered_node = registration + draft_registration.save() + registration.deleted = timezone.now() + registration.save() + draft_registration.deleted = None + draft_registration.save() + res = app.get(url_draft_registrations, auth=user.auth) + assert res.status_code == 200 + data = res.json['data'] + assert len(data) == 1 + assert schema._id in data[0]['relationships']['registration_schema']['links']['related']['href'] + assert data[0]['id'] == draft_registration._id + assert data[0]['attributes']['registration_metadata'] == {} + -class TestDraftRegistrationCreateWithNode(TestDraftRegistrationCreate): +class TestDraftRegistrationCreateWithNode(AbstractDraftRegistrationTestCase): - # Overrides `url_draft_registrations` in `TestDraftRegistrationCreate` @pytest.fixture() def url_draft_registrations(self, project_public): return f'/{API_BASE}draft_registrations/?' @@ -125,29 +215,35 @@ def payload_alt(self, payload, provider_alt): new_payload['data']['relationships']['provider']['data']['id'] = provider_alt._id return new_payload - # Overrides TestDraftRegistrationList - def test_cannot_create_draft_errors(self, app, user, payload_alt, project_public, url_draft_registrations): - # test_cannot_create_draft_from_a_registration + def test_cannot_create_draft_from_a_registration(self, app, user, payload_alt, project_public, url_draft_registrations): registration = RegistrationFactory( - project=project_public, creator=user) + project=project_public, + creator=user + ) payload_alt['data']['relationships']['branched_from']['data']['id'] = registration._id res = app.post_json_api( - url_draft_registrations, payload_alt, auth=user.auth, - expect_errors=True) + url_draft_registrations, + payload_alt, + auth=user.auth, + expect_errors=True + ) assert res.status_code == 404 - # test_cannot_create_draft_from_deleted_node + def test_cannot_create_draft_from_deleted_node(self, app, user, payload_alt, project_public, url_draft_registrations): project = ProjectFactory(is_public=True, creator=user) project.is_deleted = True project.save() payload_alt['data']['relationships']['branched_from']['data']['id'] = project._id res = app.post_json_api( - url_draft_registrations, payload_alt, - auth=user.auth, expect_errors=True) + url_draft_registrations, + payload_alt, + auth=user.auth, + expect_errors=True + ) assert res.status_code == 410 assert res.json['errors'][0]['detail'] == 'The requested node is no longer available.' - # test_cannot_create_draft_from_collection + def test_cannot_create_draft_from_collection(self, app, user, payload_alt, project_public, url_draft_registrations): collection = CollectionFactory(creator=user) payload_alt['data']['relationships']['branched_from']['data']['id'] = collection._id res = app.post_json_api( @@ -155,8 +251,9 @@ def test_cannot_create_draft_errors(self, app, user, payload_alt, project_public expect_errors=True) assert res.status_code == 404 - def test_draft_registration_attributes_copied_from_node(self, app, project_public, - url_draft_registrations, user, payload_alt): + def test_draft_registration_attributes_copied_from_node( + self, app, project_public, url_draft_registrations, user, payload_alt + ): write_contrib = AuthUserFactory() read_contrib = AuthUserFactory() @@ -175,8 +272,9 @@ def test_draft_registration_attributes_copied_from_node(self, app, project_publi project_public.add_contributor(write_contrib, WRITE) project_public.add_contributor(read_contrib, READ) + # Only an admin can create a DraftRegistration res = app.post_json_api(url_draft_registrations, payload_alt, auth=write_contrib.auth, expect_errors=True) - assert res.status_code == 201 + assert res.status_code == 403 res = app.post_json_api(url_draft_registrations, payload_alt, auth=read_contrib.auth, expect_errors=True) assert res.status_code == 403 @@ -196,67 +294,55 @@ def test_draft_registration_attributes_copied_from_node(self, app, project_publi assert 'subjects' in relationships assert 'contributors' in relationships - def test_cannot_create_draft( - self, app, user_write_contrib, - user_read_contrib, user_non_contrib, - project_public, payload_alt, group, - url_draft_registrations, group_mem): - - # test_write_only_contributor_cannot_create_draft + def test_write_only_contributor_cannot_create_draft( + self, app, user_write_contrib, project_public, payload_alt, url_draft_registrations + ): assert user_write_contrib in project_public.contributors.all() res = app.post_json_api( url_draft_registrations, payload_alt, auth=user_write_contrib.auth, - expect_errors=True) - assert res.status_code == 201 + expect_errors=True + ) + assert res.status_code == 403 - # test_read_only_contributor_cannot_create_draft + def test_read_only_contributor_cannot_create_draft( + self, app, user_write_contrib, user_read_contrib, project_public, payload_alt, url_draft_registrations + ): assert user_read_contrib in project_public.contributors.all() res = app.post_json_api( url_draft_registrations, payload_alt, auth=user_read_contrib.auth, - expect_errors=True) + expect_errors=True + ) assert res.status_code == 403 - # test_non_authenticated_user_cannot_create_draft + def test_non_authenticated_user_cannot_create_draft( + self, app, user_write_contrib, payload_alt, group, url_draft_registrations + ): res = app.post_json_api( url_draft_registrations, - payload_alt, expect_errors=True) + payload_alt, + expect_errors=True + ) assert res.status_code == 401 - # test_logged_in_non_contributor_cannot_create_draft + def test_logged_in_non_contributor_cannot_create_draft( + self, app, user_non_contrib, payload_alt, url_draft_registrations + ): + res = app.post_json_api( url_draft_registrations, payload_alt, auth=user_non_contrib.auth, - expect_errors=True) + expect_errors=True + ) assert res.status_code == 403 - # test_group_admin_cannot_create_draft - res = app.post_json_api( - url_draft_registrations, - payload_alt, - auth=group_mem.auth, - expect_errors=True) - assert res.status_code == 201 - - # test_group_write_contrib_cannot_create_draft - project_public.remove_osf_group(group) - project_public.add_osf_group(group, WRITE) - res = app.post_json_api( - url_draft_registrations, - payload_alt, - auth=group_mem.auth, - expect_errors=True) - assert res.status_code == 201 - - def test_create_project_based_draft_does_not_email_initiator( - self, app, user, url_draft_registrations, payload): - post_url = url_draft_registrations + 'embed=branched_from&embed=initiator' + def test_create_project_based_draft_does_not_email_initiator(self, app, user, url_draft_registrations, payload): with mock.patch.object(mails, 'send_mail') as mock_send_mail: - app.post_json_api(post_url, payload, auth=user.auth) + app.post_json_api(f'{url_draft_registrations}?embed=branched_from&embed=initiator', payload, auth=user.auth) assert not mock_send_mail.called @@ -320,7 +406,7 @@ def test_affiliated_institutions_are_copied_from_user(self, app, user, url_draft assert list(draft_registration.affiliated_institutions.all()) == list(user.get_affiliated_institutions()) -class TestDraftRegistrationCreateWithoutNode(TestDraftRegistrationCreate): +class TestDraftRegistrationCreateWithoutNode(AbstractDraftRegistrationTestCase): @pytest.fixture() def url_draft_registrations(self): return f'/{API_BASE}draft_registrations/?' @@ -346,13 +432,14 @@ def test_admin_can_create_draft( assert draft.creator == user assert draft.has_permission(user, ADMIN) is True - def test_create_no_project_draft_emails_initiator( - self, app, user, url_draft_registrations, payload): - post_url = url_draft_registrations + 'embed=branched_from&embed=initiator' - + def test_create_no_project_draft_emails_initiator(self, app, user, url_draft_registrations, payload): # Intercepting the send_mail call from website.project.views.contributor.notify_added_contributor with mock.patch.object(mails, 'send_mail') as mock_send_mail: - resp = app.post_json_api(post_url, payload, auth=user.auth) + resp = app.post_json_api( + f'{url_draft_registrations}?embed=branched_from&embed=initiator', + payload, + auth=user.auth + ) assert mock_send_mail.called # Python 3.6 does not support mock.call_args.args/kwargs @@ -363,7 +450,9 @@ def test_create_no_project_draft_emails_initiator( assert mock_send_kwargs['user'] == user assert mock_send_kwargs['node'] == DraftRegistration.load(resp.json['data']['id']) - def test_create_draft_with_provider(self, app, user, url_draft_registrations, non_default_provider, payload_with_non_default_provider): + def test_create_draft_with_provider( + self, app, user, url_draft_registrations, non_default_provider, payload_with_non_default_provider + ): res = app.post_json_api(url_draft_registrations, payload_with_non_default_provider, auth=user.auth) assert res.status_code == 201 data = res.json['data'] @@ -373,14 +462,9 @@ def test_create_draft_with_provider(self, app, user, url_draft_registrations, no draft = DraftRegistration.load(data['id']) assert draft.provider == non_default_provider - # Overrides TestDraftRegistrationList - def test_cannot_create_draft( - self, app, user_write_contrib, - user_read_contrib, user_non_contrib, - project_public, payload, group, - url_draft_registrations, group_mem): - - # test_write_contrib (no node supplied, so any logged in user can create) + def test_write_contrib(self, app, user, project_public, payload, url_draft_registrations, user_write_contrib): + """(no node supplied, so any logged in user can create) + """ assert user_write_contrib in project_public.contributors.all() res = app.post_json_api( url_draft_registrations, @@ -388,7 +472,9 @@ def test_cannot_create_draft( auth=user_write_contrib.auth) assert res.status_code == 201 - # test_read_only (no node supplied, so any logged in user can create) + def test_read_only(self, app, user, url_draft_registrations, user_read_contrib, project_public, payload): + '''(no node supplied, so any logged in user can create) + ''' assert user_read_contrib in project_public.contributors.all() res = app.post_json_api( url_draft_registrations, @@ -396,24 +482,24 @@ def test_cannot_create_draft( auth=user_read_contrib.auth) assert res.status_code == 201 - # test_non_authenticated_user_cannot_create_draft + def test_non_authenticated_user_cannot_create_draft(self, app, user, url_draft_registrations, payload): res = app.post_json_api( url_draft_registrations, - payload, expect_errors=True) + payload, + expect_errors=True + ) assert res.status_code == 401 - # test_logged_in_non_contributor (no node supplied, so any logged in user can create) + def test_logged_in_non_contributor(self, app, user, url_draft_registrations, user_non_contrib, payload): + '''(no node supplied, so any logged in user can create) + ''' res = app.post_json_api( url_draft_registrations, payload, - auth=user_non_contrib.auth) + auth=user_non_contrib.auth + ) assert res.status_code == 201 - # Overrides TestDraftRegistrationList - def test_cannot_create_draft_errors(self): - # The original test assumes a node is being passed in - return - def test_draft_registration_attributes_not_copied_from_node(self, app, project_public, url_draft_registrations, user, payload): diff --git a/api_tests/files/views/test_file_detail.py b/api_tests/files/views/test_file_detail.py index 75224022929..a80b9319dae 100644 --- a/api_tests/files/views/test_file_detail.py +++ b/api_tests/files/views/test_file_detail.py @@ -31,6 +31,9 @@ SessionStore = import_module(django_conf_settings.SESSION_ENGINE).SessionStore +from addons.base.views import get_authenticated_resource +from framework.exceptions import HTTPError + # stolen from^W^Winspired by DRF # rest_framework.fields.DateTimeField.to_representation def _dt_to_iso8601(value): @@ -639,6 +642,10 @@ def file(self, root_node, user): }).save() return file + @pytest.fixture() + def file_url(self, file): + return f'/{API_BASE}files/{file._id}/' + def test_listing(self, app, user, file): file.create_version(user, { 'object': '0683m38e', @@ -705,6 +712,67 @@ def test_load_and_property(self, app, user, file): expect_errors=True, auth=user.auth, ).status_code == 405 + def test_retracted_registration_file(self, app, user, file_url, file): + resource = RegistrationFactory(is_public=True) + retraction = resource.retract_registration( + user=resource.creator, + justification='Justification for retraction', + save=True, + moderator_initiated=False + ) + + retraction.accept() + resource.save() + resource.refresh_from_db() + + file.target = resource + file.save() + + res = app.get(file_url, auth=user.auth, expect_errors=True) + assert res.status_code == 410 + + def test_retracted_file_returns_410(self, app, user, file_url, file): + resource = RegistrationFactory(is_public=True) + retraction = resource.retract_registration( + user=resource.creator, + justification='Justification for retraction', + save=True, + moderator_initiated=False + ) + + retraction.accept() + resource.save() + resource.refresh_from_db() + + file.target = resource + file.save() + + res = app.get(file_url, auth=user.auth, expect_errors=True) + assert res.status_code == 410 + + def test_get_authenticated_resource_retracted(self): + resource = RegistrationFactory(is_public=True) + + assert resource.is_retracted is False + + retraction = resource.retract_registration( + user=resource.creator, + justification='Justification for retraction', + save=True, + moderator_initiated=False + ) + + retraction.accept() + resource.save() + resource.refresh_from_db() + + assert resource.is_retracted is True + + with pytest.raises(HTTPError) as excinfo: + get_authenticated_resource(resource._id) + + assert excinfo.value.code == 410 + @pytest.mark.django_db class TestFileTagging: @@ -916,20 +984,20 @@ def test_withdrawn_preprint_files(self, app, file_url, preprint, user, other_use # Unauthenticated res = app.get(file_url, expect_errors=True) - assert res.status_code == 401 + assert res.status_code == 410 # Noncontrib res = app.get(file_url, auth=other_user.auth, expect_errors=True) - assert res.status_code == 403 + assert res.status_code == 410 # Write contributor preprint.add_contributor(other_user, WRITE, save=True) res = app.get(file_url, auth=other_user.auth, expect_errors=True) - assert res.status_code == 403 + assert res.status_code == 410 # Admin contrib res = app.get(file_url, auth=user.auth, expect_errors=True) - assert res.status_code == 403 + assert res.status_code == 410 @pytest.mark.django_db class TestShowAsUnviewed: diff --git a/api_tests/institutions/views/test_institution_auth.py b/api_tests/institutions/views/test_institution_auth.py index 40742424b91..670a6ee31b4 100644 --- a/api_tests/institutions/views/test_institution_auth.py +++ b/api_tests/institutions/views/test_institution_auth.py @@ -12,7 +12,7 @@ from framework.auth import signals, Auth from framework.auth.core import get_user -from framework.auth.views import send_confirm_email +from framework.auth.views import send_confirm_email_async from osf.models import OSFUser, InstitutionAffiliation, InstitutionStorageRegion from osf.models.institution import SsoFilterCriteriaAction @@ -204,6 +204,7 @@ def test_new_user_created(self, app, url_auth_institution, institution): assert user.fullname == 'Fake User' assert user.accepted_terms_of_service is None assert institution in user.get_affiliated_institutions() + assert f'source:institution|{institution._id}' in user.system_tags def test_existing_user_found_but_not_affiliated(self, app, institution, url_auth_institution): @@ -219,6 +220,7 @@ def test_existing_user_found_but_not_affiliated(self, app, institution, url_auth user.reload() assert user.fullname == 'Foo Bar' assert institution in user.get_affiliated_institutions() + assert f'source:institution|{institution._id}' not in user.system_tags def test_user_found_and_affiliated(self, app, institution, url_auth_institution): @@ -454,7 +456,7 @@ def test_user_external_unconfirmed(self, app, institution, url_auth_institution) assert user.external_identity # Send confirm email in order to add new email verifications - send_confirm_email( + send_confirm_email_async( user, user.username, external_id_provider=external_id_provider, @@ -811,6 +813,8 @@ def test_new_user_primary_only(self, app, url_auth_institution, assert user.accepted_terms_of_service is None assert institution_primary_type_1 in user.get_affiliated_institutions() assert institution_secondary_type_1 not in user.get_affiliated_institutions() + assert f'source:institution|{institution_primary_type_1._id}' in user.system_tags + assert f'source:institution|{institution_secondary_type_1._id}' not in user.system_tags def test_new_user_primary_and_secondary(self, app, url_auth_institution, institution_primary_type_1, institution_secondary_type_1): @@ -830,6 +834,8 @@ def test_new_user_primary_and_secondary(self, app, url_auth_institution, assert user assert user.fullname == 'Fake User' assert user.accepted_terms_of_service is None + assert f'source:institution|{institution_primary_type_1._id}' in user.system_tags + assert f'source:institution|{institution_secondary_type_1._id}' in user.system_tags assert institution_primary_type_1 in user.get_affiliated_institutions() assert institution_secondary_type_1 in user.get_affiliated_institutions() @@ -1059,6 +1065,7 @@ def test_selective_sso_allowed_new_user(self, app, url_auth_institution, institu assert user.fullname == 'Fake User' assert user.accepted_terms_of_service is None assert institution_selective_type_1 in user.get_affiliated_institutions() + assert f'source:institution|{institution_selective_type_1._id}' in user.system_tags def test_selective_sso_allowed_existing_user_not_affiliated(self, app, url_auth_institution, institution_selective_type_1): @@ -1147,6 +1154,7 @@ def test_selective_sso_allowed_new_user(self, app, url_auth_institution, institu assert user.fullname == 'Fake User' assert user.accepted_terms_of_service is None assert institution_selective_type_2 in user.get_affiliated_institutions() + assert f'source:institution|{institution_selective_type_2._id}' in user.system_tags def test_selective_sso_allowed_existing_user_not_affiliated(self, app, url_auth_institution, institution_selective_type_2): @@ -1240,6 +1248,7 @@ def test_new_user(self, app, url_auth_institution, institution): assert affiliation.sso_mail == sso_email assert affiliation.sso_identity == sso_identity assert affiliation.sso_department == department + assert f'source:institution|{institution._id}' in user.system_tags def test_existing_user_by_both_email_and_identity(self, app, url_auth_institution, institution): diff --git a/api_tests/institutions/views/test_institution_user_metric_list.py b/api_tests/institutions/views/test_institution_user_metric_list.py index 225f876e383..dfee4d178f5 100644 --- a/api_tests/institutions/views/test_institution_user_metric_list.py +++ b/api_tests/institutions/views/test_institution_user_metric_list.py @@ -218,7 +218,7 @@ def test_filter(self, app, url, admin, populate_counts): resp = app.get(f'{url}?filter[department]=Psychology dept', auth=admin.auth) assert resp.json['data'][0]['attributes']['department'] == 'Psychology dept' - @pytest.mark.skipif(settings.TRAVIS_ENV, reason='Non-deterministic fails on travis') + @pytest.mark.skipif(settings.CI_ENV, reason='Non-deterministic fails on CI') def test_sort_and_pagination(self, app, url, user, user2, user3, admin, populate_counts, populate_more_counts, institution): resp = app.get(f'{url}?sort=user_name&page[size]=1&page=2', auth=admin.auth) assert resp.status_code == 200 @@ -229,7 +229,7 @@ def test_sort_and_pagination(self, app, url, user, user2, user3, admin, populate assert resp.json['links']['meta']['total'] == 11 assert resp.json['data'][-1]['attributes']['user_name'] == 'Zedd' - @pytest.mark.skipif(settings.TRAVIS_ENV, reason='Non-deterministic fails on travis') + @pytest.mark.skipif(settings.CI_ENV, reason='Non-deterministic fails on CI') def test_filter_and_pagination(self, app, user, user2, user3, url, admin, populate_counts, populate_more_counts, institution): resp = app.get(f'{url}?page=2', auth=admin.auth) assert resp.json['links']['meta']['total'] == 11 @@ -238,7 +238,7 @@ def test_filter_and_pagination(self, app, user, user2, user3, url, admin, popula assert resp.json['links']['meta']['total'] == 1 assert resp.json['data'][0]['attributes']['user_name'] == 'Zedd' - @pytest.mark.skipif(settings.TRAVIS_ENV, reason='Non-deterministic fails on travis') + @pytest.mark.skipif(settings.CI_ENV, reason='Non-deterministic fails on CI') def test_filter_and_sort(self, app, url, user, user2, user3, admin, user4, populate_counts, populate_na_department, institution): """ Testing for bug where sorting and filtering would throw 502. diff --git a/api_tests/metrics/test_preprint_metrics.py b/api_tests/metrics/test_preprint_metrics.py index c3a24f4183e..57e31655c40 100644 --- a/api_tests/metrics/test_preprint_metrics.py +++ b/api_tests/metrics/test_preprint_metrics.py @@ -190,7 +190,7 @@ def test_preprint_list_with_metrics_fails(self, mock_timezone, app, user, base_u res = app.get(one_preprint_url, auth=other_non_admin_user.auth, expect_errors=True) assert res.status_code == 403 - @pytest.mark.skip('Return results will be entirely mocked so does not make a lot of sense to run on travis.') + @pytest.mark.skip('Return results will be entirely mocked so does not make a lot of sense to run on ci.') @mock.patch('api.metrics.utils.timezone.now') def test_preprint_with_metrics_succeeds(self, mock_timezone, app, user, base_url, preprint, other_user, preprint_no_results, metric_dates): diff --git a/api_tests/nodes/views/test_node_draft_registration_detail.py b/api_tests/nodes/views/test_node_draft_registration_detail.py index a4acf62be51..33e0a25b21a 100644 --- a/api_tests/nodes/views/test_node_draft_registration_detail.py +++ b/api_tests/nodes/views/test_node_draft_registration_detail.py @@ -9,41 +9,21 @@ AuthUserFactory, RegistrationFactory, ) -from osf.utils.permissions import WRITE, READ, ADMIN -from api_tests.nodes.views.test_node_draft_registration_list import DraftRegistrationTestCase +from osf.utils.permissions import ADMIN +from api_tests.nodes.views.test_node_draft_registration_list import AbstractDraftRegistrationTestCase +from framework.auth.core import Auth SCHEMA_VERSION = 2 @pytest.mark.django_db -class TestDraftRegistrationDetail(DraftRegistrationTestCase): - - @pytest.fixture() - def schema(self): - return RegistrationSchema.objects.get( - name='OSF-Standard Pre-Data Collection Registration', - schema_version=SCHEMA_VERSION) - - @pytest.fixture() - def draft_registration(self, user, project_public, schema): - return DraftRegistrationFactory( - initiator=user, - registration_schema=schema, - branched_from=project_public - ) - - @pytest.fixture() - def project_other(self, user): - return ProjectFactory(creator=user) +class TestDraftRegistrationDetail(AbstractDraftRegistrationTestCase): @pytest.fixture() def url_draft_registrations(self, project_public, draft_registration): - return '/{}nodes/{}/draft_registrations/{}/?{}'.format( - API_BASE, project_public._id, draft_registration._id, 'version=2.19') + return f'/{API_BASE}nodes/{project_public._id}/draft_registrations/{draft_registration._id}/?version=2.19' - def test_admin_can_view_draft( - self, app, user, draft_registration, project_public, - schema, url_draft_registrations, group_mem): + def test_node_admin_can_view_draft(self, app, user, draft_registration, schema, url_draft_registrations): res = app.get(url_draft_registrations, auth=user.auth) assert res.status_code == 200 data = res.json['data'] @@ -51,75 +31,55 @@ def test_admin_can_view_draft( assert data['id'] == draft_registration._id assert data['attributes']['registration_metadata'] == {} - def test_admin_group_member_can_view( - self, app, user, draft_registration, project_public, - schema, url_draft_registrations, group_mem): - - res = app.get(url_draft_registrations, auth=group_mem.auth) + def test_read_contributor_can_view_draft(self, app, user_read_contrib, url_draft_registrations): + """ + Note this is the Node permissions not DraftRegistration permission + """ + res = app.get(url_draft_registrations, auth=user_read_contrib.auth) assert res.status_code == 200 - def test_cannot_view_draft( - self, app, user_write_contrib, project_public, - user_read_contrib, user_non_contrib, - url_draft_registrations, group, group_mem): - - # test_read_only_contributor_cannot_view_draft - res = app.get( - url_draft_registrations, - auth=user_read_contrib.auth, - expect_errors=True) - assert res.status_code == 403 - - # test_read_write_contributor_cannot_view_draft - res = app.get( - url_draft_registrations, - auth=user_write_contrib.auth, - expect_errors=True) - assert res.status_code == 403 + def test_write_contributor_can_view_draft(self, app, user_write_contrib, url_draft_registrations): + """ + Note this is the Node permissions not DraftRegistration permission + """ + res = app.get(url_draft_registrations, auth=user_write_contrib.auth) + assert res.status_code == 200 - # test_logged_in_non_contributor_cannot_view_draft - res = app.get( - url_draft_registrations, - auth=user_non_contrib.auth, - expect_errors=True) - assert res.status_code == 403 + def test_logged_in_non_contributor_cannot_view_draft(self, app, user_non_contrib, url_draft_registrations): + res = app.get(url_draft_registrations, auth=user_non_contrib.auth, expect_errors=True) + assert res.status_code == 200 - # test_unauthenticated_user_cannot_view_draft + def test_unauthenticated_user_cannot_view_draft(self, app, url_draft_registrations): res = app.get(url_draft_registrations, expect_errors=True) assert res.status_code == 401 - # test_group_mem_read_cannot_view - project_public.remove_osf_group(group) - project_public.add_osf_group(group, READ) - res = app.get(url_draft_registrations, auth=group_mem.auth, expect_errors=True) - assert res.status_code == 403 - - def test_cannot_view_deleted_draft( - self, app, user, url_draft_registrations): + def test_cannot_view_deleted_draft(self, app, user, url_draft_registrations): res = app.delete_json_api(url_draft_registrations, auth=user.auth) assert res.status_code == 204 res = app.get( url_draft_registrations, auth=user.auth, - expect_errors=True) + expect_errors=True + ) assert res.status_code == 410 - def test_draft_must_be_branched_from_node_in_kwargs( - self, app, user, project_other, draft_registration): - url = '/{}nodes/{}/draft_registrations/{}/'.format( - API_BASE, project_other._id, draft_registration._id) - res = app.get(url, auth=user.auth, expect_errors=True) + def test_draft_must_be_branched_from_node_in_kwargs(self, app, user, project_other, draft_registration): + res = app.get( + f'/{API_BASE}nodes/{project_other._id}/draft_registrations/{draft_registration._id}/', + auth=user.auth, + expect_errors=True + ) assert res.status_code == 400 errors = res.json['errors'][0] assert errors['detail'] == 'This draft registration is not created from the given node.' def test_draft_registration_serializer_usage(self, app, user, project_public, draft_registration): # Tests the usage of DraftRegistrationDetailSerializer for version 2.20 - url_draft_registrations = '/{}nodes/{}/draft_registrations/{}/?{}'.format( - API_BASE, project_public._id, draft_registration._id, 'version=2.20') - - res = app.get(url_draft_registrations, auth=user.auth) + res = app.get( + f'/{API_BASE}nodes/{project_public._id}/draft_registrations/{draft_registration._id}/?version=2.20', + auth=user.auth + ) assert res.status_code == 200 data = res.json['data'] @@ -128,8 +88,7 @@ def test_draft_registration_serializer_usage(self, app, user, project_public, dr assert data['attributes']['description'] assert data['relationships']['affiliated_institutions'] - def test_can_view_after_added( - self, app, schema, draft_registration, url_draft_registrations): + def test_can_view_after_added(self, app, schema, draft_registration, url_draft_registrations): user = AuthUserFactory() project = draft_registration.branched_from project.add_contributor(user, ADMIN) @@ -138,21 +97,11 @@ def test_can_view_after_added( @pytest.mark.django_db -class TestDraftRegistrationUpdate(DraftRegistrationTestCase): - - @pytest.fixture() - def schema(self): - return RegistrationSchema.objects.get( - name='OSF-Standard Pre-Data Collection Registration', - schema_version=SCHEMA_VERSION) +class TestDraftRegistrationUpdate(AbstractDraftRegistrationTestCase): @pytest.fixture() - def draft_registration(self, user, project_public, schema): - return DraftRegistrationFactory( - initiator=user, - registration_schema=schema, - branched_from=project_public - ) + def url_draft_registrations(self, project_public, draft_registration): + return f'/{API_BASE}nodes/{project_public._id}/draft_registrations/{draft_registration._id}/?version=2.19' @pytest.fixture() def reg_schema(self): @@ -170,20 +119,13 @@ def draft_registration_prereg(self, user, project_public, reg_schema): ) @pytest.fixture() - def metadata_registration( - self, metadata, - draft_registration_prereg): + def metadata_registration(self, metadata, draft_registration_prereg): return metadata(draft_registration_prereg) @pytest.fixture() def project_other(self, user): return ProjectFactory(creator=user) - @pytest.fixture() - def url_draft_registrations(self, project_public, draft_registration): - return '/{}nodes/{}/draft_registrations/{}/?{}'.format( - API_BASE, project_public._id, draft_registration._id, 'version=2.19') - @pytest.fixture() def payload(self, draft_registration): return { @@ -267,12 +209,10 @@ def test_draft_must_be_branched_from_node( errors = res.json['errors'][0] assert errors['detail'] == 'This draft registration is not created from the given node.' - def test_cannot_update_draft( - self, app, user_write_contrib, project_public, - user_read_contrib, user_non_contrib, - payload, url_draft_registrations, group, group_mem): + def test_read_only_contributor_cannot_update_draft( + self, app, user_read_contrib, payload, url_draft_registrations, + ): - # test_read_only_contributor_cannot_update_draft res = app.put_json_api( url_draft_registrations, payload, @@ -280,37 +220,20 @@ def test_cannot_update_draft( expect_errors=True) assert res.status_code == 403 - # test_logged_in_non_contributor_cannot_update_draft + def test_logged_in_non_contributor_cannot_update_draft( + self, app, user_non_contrib, payload, url_draft_registrations, + ): res = app.put_json_api( url_draft_registrations, payload, auth=user_non_contrib.auth, - expect_errors=True) - assert res.status_code == 403 - - # test_unauthenticated_user_cannot_update_draft - res = app.put_json_api( - url_draft_registrations, - payload, expect_errors=True) - assert res.status_code == 401 - - # test_osf_group_member_admin_cannot_update_draft - res = app.put_json_api( - url_draft_registrations, - payload, expect_errors=True, - auth=group_mem.auth + expect_errors=True ) assert res.status_code == 403 - # test_osf_group_member_write_cannot_update_draft - project_public.remove_osf_group(group) - project_public.add_osf_group(group, WRITE) - res = app.put_json_api( - url_draft_registrations, - payload, expect_errors=True, - auth=group_mem.auth - ) - assert res.status_code == 403 + def test_unauthenticated_user_cannot_update_draft(self, app, payload, url_draft_registrations): + res = app.put_json_api(url_draft_registrations, payload, expect_errors=True) + assert res.status_code == 401 def test_registration_metadata_does_not_need_to_be_supplied( self, app, user, payload, url_draft_registrations): @@ -528,21 +451,7 @@ def test_multiple_choice_question_value_in_registration_responses_must_match_val @pytest.mark.django_db -class TestDraftRegistrationPatch(DraftRegistrationTestCase): - - @pytest.fixture() - def schema(self): - return RegistrationSchema.objects.get( - name='OSF-Standard Pre-Data Collection Registration', - schema_version=SCHEMA_VERSION) - - @pytest.fixture() - def draft_registration(self, user, project_public, schema): - return DraftRegistrationFactory( - initiator=user, - registration_schema=schema, - branched_from=project_public - ) +class TestDraftRegistrationPatch(AbstractDraftRegistrationTestCase): @pytest.fixture() def reg_schema(self): @@ -562,10 +471,6 @@ def draft_registration_prereg(self, user, project_public, reg_schema): def metadata_registration(self, metadata, draft_registration_prereg): return metadata(draft_registration_prereg) - @pytest.fixture() - def project_other(self, user): - return ProjectFactory(creator=user) - @pytest.fixture() def url_draft_registrations(self, project_public, draft_registration): return '/{}nodes/{}/draft_registrations/{}/?{}'.format( @@ -604,111 +509,69 @@ def test_admin_can_update_draft( assert schema._id in data['relationships']['registration_schema']['links']['related']['href'] assert data['attributes']['registration_metadata'] == payload['data']['attributes']['registration_metadata'] - def test_cannot_update_draft( - self, app, user_write_contrib, - user_read_contrib, user_non_contrib, - payload, url_draft_registrations, group_mem): - - # test_read_only_contributor_cannot_update_draft + def test_read_only_contributor_cannot_update_draft( + self, app, user_read_contrib, payload, url_draft_registrations + ): res = app.patch_json_api( url_draft_registrations, payload, auth=user_read_contrib.auth, - expect_errors=True) + expect_errors=True + ) assert res.status_code == 403 - # test_logged_in_non_contributor_cannot_update_draft + def test_logged_in_non_contributor_cannot_update_draft( + self, app, user_non_contrib, payload, url_draft_registrations + ): res = app.patch_json_api( url_draft_registrations, payload, auth=user_non_contrib.auth, - expect_errors=True) + expect_errors=True + ) assert res.status_code == 403 - # test_unauthenticated_user_cannot_update_draft + def test_unauthenticated_user_cannot_update_draft( + self, app, user_non_contrib, payload, url_draft_registrations + ): res = app.patch_json_api( url_draft_registrations, - payload, expect_errors=True) + payload, + expect_errors=True + ) assert res.status_code == 401 - # group admin cannot update draft - res = app.patch_json_api( - url_draft_registrations, - payload, - auth=group_mem.auth, - expect_errors=True) - assert res.status_code == 403 @pytest.mark.django_db -class TestDraftRegistrationDelete(DraftRegistrationTestCase): - - @pytest.fixture() - def schema(self): - return RegistrationSchema.objects.get( - name='OSF-Standard Pre-Data Collection Registration', - schema_version=SCHEMA_VERSION) - - @pytest.fixture() - def draft_registration(self, user, project_public, schema): - return DraftRegistrationFactory( - initiator=user, - registration_schema=schema, - branched_from=project_public - ) - - @pytest.fixture() - def project_other(self, user): - return ProjectFactory(creator=user) +class TestDraftRegistrationDelete(AbstractDraftRegistrationTestCase): @pytest.fixture() def url_draft_registrations(self, project_public, draft_registration): - return '/{}nodes/{}/draft_registrations/{}/?{}'.format( - API_BASE, project_public._id, draft_registration._id, 'version=2.19') + return f'/{API_BASE}nodes/{project_public._id}/draft_registrations/{draft_registration._id}/?version=2.19' def test_admin_can_delete_draft(self, app, user, url_draft_registrations, project_public): res = app.delete_json_api(url_draft_registrations, auth=user.auth) assert res.status_code == 204 - def test_cannot_delete_draft( - self, app, user_write_contrib, project_public, - user_read_contrib, user_non_contrib, - url_draft_registrations, group, group_mem): - - # test_read_only_contributor_cannot_delete_draft - res = app.delete_json_api( - url_draft_registrations, - auth=user_read_contrib.auth, - expect_errors=True) + def test_read_only_contributor_cannot_delete_draft(self, app, user_read_contrib, url_draft_registrations): + res = app.delete_json_api(url_draft_registrations, auth=user_read_contrib.auth, expect_errors=True) assert res.status_code == 403 - # test_read_write_contributor_cannot_delete_draft - res = app.delete_json_api( - url_draft_registrations, - auth=user_write_contrib.auth, - expect_errors=True) + def test_read_write_draft_contributor_cannot_delete_draft( + self, app, user_write_contrib, url_draft_registrations, project_public + ): + project_public.remove_contributor(user_write_contrib, Auth(user_write_contrib)) # Draft contributor only + res = app.delete_json_api(url_draft_registrations, auth=user_write_contrib.auth, expect_errors=True) assert res.status_code == 403 - # test_logged_in_non_contributor_cannot_delete_draft - res = app.delete_json_api( - url_draft_registrations, - auth=user_non_contrib.auth, - expect_errors=True) + def test_logged_in_non_contributor_cannot_delete_draft(self, app, user_non_contrib, url_draft_registrations): + res = app.delete_json_api(url_draft_registrations, auth=user_non_contrib.auth, expect_errors=True) assert res.status_code == 403 - # test_unauthenticated_user_cannot_delete_draft + def test_unauthenticated_user_cannot_delete_draft(self, app, url_draft_registrations): res = app.delete_json_api(url_draft_registrations, expect_errors=True) assert res.status_code == 401 - # test_group_member_admin_cannot_delete_draft - res = app.delete_json_api(url_draft_registrations, expect_errors=True, auth=group_mem.auth) - assert res.status_code == 403 - - # test_group_member_write_cannot_delete_draft - project_public.remove_osf_group(group) - project_public.add_osf_group(group, WRITE) - res = app.delete_json_api(url_draft_registrations, expect_errors=True, auth=group_mem.auth) - assert res.status_code == 403 - def test_draft_that_has_been_registered_cannot_be_deleted( self, app, user, project_public, draft_registration, url_draft_registrations): reg = RegistrationFactory(project=project_public) diff --git a/api_tests/nodes/views/test_node_draft_registration_list.py b/api_tests/nodes/views/test_node_draft_registration_list.py index 3a3c06f6c18..5e46b46b4c0 100644 --- a/api_tests/nodes/views/test_node_draft_registration_list.py +++ b/api_tests/nodes/views/test_node_draft_registration_list.py @@ -28,12 +28,16 @@ def invisible_and_inactive_schema(): @pytest.mark.django_db -class DraftRegistrationTestCase: +class AbstractDraftRegistrationTestCase: @pytest.fixture() def user(self): return AuthUserFactory() + @pytest.fixture() + def user_admin_contrib(self, user): + return AuthUserFactory() + @pytest.fixture() def user_write_contrib(self): return AuthUserFactory() @@ -55,7 +59,7 @@ def group(self, group_mem): return OSFGroupFactory(creator=group_mem) @pytest.fixture() - def project_public(self, user, user_write_contrib, user_read_contrib, group, group_mem): + def project_public(self, user, user_admin_contrib, user_write_contrib, user_read_contrib, group, group_mem): project_public = ProjectFactory(is_public=True, creator=user) project_public.add_contributor( user_write_contrib, @@ -63,11 +67,22 @@ def project_public(self, user, user_write_contrib, user_read_contrib, group, gro project_public.add_contributor( user_read_contrib, permissions=permissions.READ) + project_public.add_contributor( + user_admin_contrib, + permissions=permissions.ADMIN) project_public.save() project_public.add_osf_group(group, permissions.ADMIN) project_public.add_tag('hello', Auth(user), save=True) return project_public + @pytest.fixture() + def draft_registration(self, user, project_public, schema): + return DraftRegistrationFactory( + initiator=user, + registration_schema=schema, + branched_from=project_public + ) + @pytest.fixture() def metadata(self): def metadata(draft): @@ -90,15 +105,96 @@ def metadata(draft): return test_metadata return metadata + @pytest.fixture() + def schema(self): + return RegistrationSchema.objects.get( + name='OSF-Standard Pre-Data Collection Registration', + schema_version=SCHEMA_VERSION + ) + + @pytest.fixture() + def metaschema_open_ended(self): + return RegistrationSchema.objects.get( + name='Open-Ended Registration', + schema_version=OPEN_ENDED_SCHEMA_VERSION + ) + + @pytest.fixture() + def project_other(self, user): + return ProjectFactory(creator=user) + + @pytest.fixture() + def payload(self, metaschema_open_ended, provider): + return { + 'data': { + 'type': 'draft_registrations', + 'attributes': {}, + 'relationships': { + 'registration_schema': { + 'data': { + 'type': 'registration_schema', + 'id': metaschema_open_ended._id + } + }, + 'provider': { + 'data': { + 'type': 'registration-providers', + 'id': provider._id, + } + } + } + } + } + + @pytest.fixture() + def provider(self): + return RegistrationProvider.get_default() + + @pytest.fixture() + def non_default_provider(self, metaschema_open_ended): + non_default_provider = RegistrationProviderFactory() + non_default_provider.schemas.add(metaschema_open_ended) + non_default_provider.save() + return non_default_provider + + @pytest.fixture() + def payload_with_non_default_provider(self, metaschema_open_ended, non_default_provider): + return { + 'data': { + 'type': 'draft_registrations', + 'attributes': {}, + 'relationships': { + 'registration_schema': { + 'data': { + 'type': 'registration_schema', + 'id': metaschema_open_ended._id + } + }, + 'provider': { + 'data': { + 'type': 'registration-providers', + 'id': non_default_provider._id, + } + } + } + } + } + @pytest.mark.django_db -class TestDraftRegistrationList(DraftRegistrationTestCase): +class TestDraftRegistrationList(AbstractDraftRegistrationTestCase): + + @pytest.fixture() + def url_draft_registrations(self, project_public): + # Specifies version to test functionality when using DraftRegistrationLegacySerializer + return f'/{API_BASE}nodes/{project_public._id}/draft_registrations/?version=2.19' @pytest.fixture() def schema(self): return RegistrationSchema.objects.get( name='Open-Ended Registration', - schema_version=OPEN_ENDED_SCHEMA_VERSION) + schema_version=OPEN_ENDED_SCHEMA_VERSION + ) @pytest.fixture() def draft_registration(self, user, project_public, schema): @@ -108,15 +204,9 @@ def draft_registration(self, user, project_public, schema): branched_from=project_public ) - @pytest.fixture() - def url_draft_registrations(self, project_public): - # Specifies version to test functionality when using DraftRegistrationLegacySerializer - return '/{}nodes/{}/draft_registrations/?{}'.format( - API_BASE, project_public._id, 'version=2.19') - - def test_admin_can_view_draft_list( - self, app, user, draft_registration, project_public, - schema, url_draft_registrations): + def test_draft_admin_can_view_draft_list( + self, app, user, draft_registration, project_public, schema, url_draft_registrations + ): res = app.get(url_draft_registrations, auth=user.auth) assert res.status_code == 200 data = res.json['data'] @@ -126,54 +216,27 @@ def test_admin_can_view_draft_list( assert data[0]['id'] == draft_registration._id assert data[0]['attributes']['registration_metadata'] == {} - def test_osf_group_with_admin_permissions_can_view( - self, app, user, draft_registration, project_public, - schema, url_draft_registrations): - group_mem = AuthUserFactory() - group = OSFGroupFactory(creator=group_mem) - project_public.add_osf_group(group, permissions.ADMIN) - res = app.get(url_draft_registrations, auth=group_mem.auth, expect_errors=True) + def test_read_only_contributor_can_view_draft_list(self, app, user_read_contrib, url_draft_registrations): + res = app.get(url_draft_registrations, auth=user_read_contrib.auth) assert res.status_code == 200 - data = res.json['data'] - assert len(data) == 1 - assert schema._id in data[0]['relationships']['registration_schema']['links']['related']['href'] - - def test_cannot_view_draft_list( - self, app, user_write_contrib, project_public, - user_read_contrib, user_non_contrib, - url_draft_registrations, group, group_mem): - - # test_read_only_contributor_cannot_view_draft_list - res = app.get( - url_draft_registrations, - auth=user_read_contrib.auth, - expect_errors=True) - assert res.status_code == 403 - # test_read_write_contributor_cannot_view_draft_list - res = app.get( - url_draft_registrations, - auth=user_write_contrib.auth, - expect_errors=True) - assert res.status_code == 403 + def test_read_write_contributor_can_view_draft_list(self, app, user_write_contrib, url_draft_registrations): + res = app.get(url_draft_registrations, auth=user_write_contrib.auth) + assert res.status_code == 200 - # test_logged_in_non_contributor_cannot_view_draft_list - res = app.get( - url_draft_registrations, - auth=user_non_contrib.auth, - expect_errors=True) - assert res.status_code == 403 + def test_draft_contributor_not_project_contributor_can_view_draft_list(self, app, user_non_contrib, draft_registration, project_public, url_draft_registrations): + draft_registration.add_contributor(contributor=user_non_contrib, auth=Auth(draft_registration.initiator), save=True) + assert not project_public.is_contributor(user_non_contrib) + assert draft_registration.is_contributor(user_non_contrib) + res = app.get(url_draft_registrations, auth=user_non_contrib.auth) + assert res.status_code == 200 + data = res.json['data'] + assert len(data) == 1 - # test_unauthenticated_user_cannot_view_draft_list + def test_unauthenticated_user_cannot_view_draft_list(self, app, url_draft_registrations): res = app.get(url_draft_registrations, expect_errors=True) assert res.status_code == 401 - # test_osf_group_with_read_permissions - project_public.remove_osf_group(group) - project_public.add_osf_group(group, permissions.READ) - res = app.get(url_draft_registrations, auth=group_mem.auth, expect_errors=True) - assert res.status_code == 403 - def test_deleted_draft_registration_does_not_show_up_in_draft_list( self, app, user, draft_registration, url_draft_registrations): draft_registration.deleted = timezone.now() @@ -227,24 +290,11 @@ def test_draft_registration_serializer_usage(self, app, user, project_public, dr @pytest.mark.django_db -class TestDraftRegistrationCreate(DraftRegistrationTestCase): - - @pytest.fixture() - def provider(self): - return RegistrationProvider.get_default() - - @pytest.fixture() - def non_default_provider(self, metaschema_open_ended): - non_default_provider = RegistrationProviderFactory() - non_default_provider.schemas.add(metaschema_open_ended) - non_default_provider.save() - return non_default_provider +class TestDraftRegistrationCreate(AbstractDraftRegistrationTestCase): @pytest.fixture() - def metaschema_open_ended(self): - return RegistrationSchema.objects.get( - name='Open-Ended Registration', - schema_version=OPEN_ENDED_SCHEMA_VERSION) + def url_draft_registrations(self, project_public): + return f'/{API_BASE}nodes/{project_public._id}/draft_registrations/?version=2.19' @pytest.fixture() def payload(self, metaschema_open_ended, provider): @@ -269,37 +319,7 @@ def payload(self, metaschema_open_ended, provider): } } - @pytest.fixture() - def payload_with_non_default_provider(self, metaschema_open_ended, non_default_provider): - return { - 'data': { - 'type': 'draft_registrations', - 'attributes': {}, - 'relationships': { - 'registration_schema': { - 'data': { - 'type': 'registration_schema', - 'id': metaschema_open_ended._id - } - }, - 'provider': { - 'data': { - 'type': 'registration-providers', - 'id': non_default_provider._id, - } - } - } - } - } - - @pytest.fixture() - def url_draft_registrations(self, project_public): - return '/{}nodes/{}/draft_registrations/?{}'.format( - API_BASE, project_public._id, 'version=2.19') - - def test_type_is_draft_registrations( - self, app, user, metaschema_open_ended, - url_draft_registrations): + def test_type_is_draft_registrations(self, app, user, metaschema_open_ended, url_draft_registrations): draft_data = { 'data': { 'type': 'nodes', @@ -322,10 +342,13 @@ def test_type_is_draft_registrations( assert res.status_code == 409 def test_admin_can_create_draft( - self, app, user, project_public, url_draft_registrations, - payload, metaschema_open_ended): - url = f'{url_draft_registrations}&embed=branched_from&embed=initiator' - res = app.post_json_api(url, payload, auth=user.auth) + self, app, user, project_public, url_draft_registrations, payload, metaschema_open_ended + ): + res = app.post_json_api( + f'{url_draft_registrations}&embed=branched_from&embed=initiator', + payload, + auth=user.auth + ) assert res.status_code == 201 data = res.json['data'] assert metaschema_open_ended._id in data['relationships']['registration_schema']['links']['related']['href'] @@ -335,13 +358,10 @@ def test_admin_can_create_draft( assert data['embeds']['branched_from']['data']['id'] == project_public._id assert data['embeds']['initiator']['data']['id'] == user._id - def test_cannot_create_draft( - self, app, user_write_contrib, - user_read_contrib, user_non_contrib, - project_public, payload, group, - url_draft_registrations, group_mem): + def test_write_only_contributor_cannot_create_draft( + self, app, user_write_contrib, project_public, payload, url_draft_registrations + ): - # test_write_only_contributor_cannot_create_draft assert user_write_contrib in project_public.contributors.all() res = app.post_json_api( url_draft_registrations, @@ -350,7 +370,9 @@ def test_cannot_create_draft( expect_errors=True) assert res.status_code == 403 - # test_read_only_contributor_cannot_create_draft + def test_read_only_contributor_cannot_create_draft( + self, app, user_read_contrib, project_public, payload, url_draft_registrations + ): assert user_read_contrib in project_public.contributors.all() res = app.post_json_api( url_draft_registrations, @@ -359,13 +381,17 @@ def test_cannot_create_draft( expect_errors=True) assert res.status_code == 403 - # test_non_authenticated_user_cannot_create_draft + def test_non_authenticated_user_cannot_create_draft(self, app, user_read_contrib, payload, url_draft_registrations): res = app.post_json_api( url_draft_registrations, - payload, expect_errors=True) + payload, + expect_errors=True + ) assert res.status_code == 401 - # test_logged_in_non_contributor_cannot_create_draft + def test_logged_in_non_contributor_cannot_create_draft( + self, app, user_non_contrib, payload, url_draft_registrations + ): res = app.post_json_api( url_draft_registrations, payload, @@ -373,24 +399,6 @@ def test_cannot_create_draft( expect_errors=True) assert res.status_code == 403 - # test_group_admin_cannot_create_draft - res = app.post_json_api( - url_draft_registrations, - payload, - auth=group_mem.auth, - expect_errors=True) - assert res.status_code == 403 - - # test_group_write_contrib_cannot_create_draft - project_public.remove_osf_group(group) - project_public.add_osf_group(group, permissions.WRITE) - res = app.post_json_api( - url_draft_registrations, - payload, - auth=group_mem.auth, - expect_errors=True) - assert res.status_code == 403 - def test_schema_validation( self, app, user, provider, non_default_provider, payload, payload_with_non_default_provider, url_draft_registrations, metaschema_open_ended): # Schema validation for a default provider without defined schemas with any schema is tested by `test_admin_can_create_draft` diff --git a/api_tests/preprints/views/test_preprint_detail.py b/api_tests/preprints/views/test_preprint_detail.py index 7112a1a21e6..7e3b279c406 100644 --- a/api_tests/preprints/views/test_preprint_detail.py +++ b/api_tests/preprints/views/test_preprint_detail.py @@ -18,7 +18,6 @@ from osf.models import ( NodeLicense, PreprintContributor, - PreprintLog ) from osf.utils.permissions import WRITE from osf.utils.workflows import DefaultStates @@ -28,6 +27,7 @@ ProjectFactory, SubjectFactory, PreprintProviderFactory, + InstitutionFactory ) from website.settings import DOI_FORMAT, CROSSREF_URL @@ -58,6 +58,10 @@ class TestPreprintDetail: def preprint(self, user): return PreprintFactory(creator=user) + @pytest.fixture() + def institution(self): + return InstitutionFactory() + @pytest.fixture() def preprint_pre_mod(self, user): return PreprintFactory(reviews_workflow='pre-moderation', is_published=False, creator=user) @@ -215,6 +219,18 @@ def test_preprint_embed_identifiers(self, app, user, preprint, url): link = res.json['data']['relationships']['identifiers']['links']['related']['href'] assert f'{url}identifiers/' in link + def test_return_affiliated_institutions(self, app, user, preprint, institution, url): + """ + Confirmation test for the the new preprint affiliated institutions feature + """ + preprint.affiliated_institutions.add(institution) + res = app.get(url) + assert res.status_code == 200 + relationship_link = res.json['data']['relationships']['affiliated_institutions']['links']['related']['href'] + assert f'/v2/preprints/{preprint._id}/institutions/' in relationship_link + relationship_link = res.json['data']['relationships']['affiliated_institutions']['links']['self']['href'] + assert f'/v2/preprints/{preprint._id}/relationships/institutions/' in relationship_link + @pytest.mark.django_db class TestPreprintDelete: @@ -819,361 +835,6 @@ def test_update_preprint_task_called_on_api_update( assert mock_on_preprint_updated.called - def test_update_has_coi(self, app, user, preprint, url): - update_payload = build_preprint_update_payload( - preprint._id, - attributes={'has_coi': True} - ) - - contrib = AuthUserFactory() - preprint.add_contributor(contrib, READ) - res = app.patch_json_api(url, update_payload, auth=contrib.auth, expect_errors=True) - assert res.status_code == 403 - assert res.json['errors'][0]['detail'] == 'You do not have permission to perform this action.' - - res = app.patch_json_api(url, update_payload, auth=user.auth) - - assert res.status_code == 200 - assert res.json['data']['attributes']['has_coi'] - - preprint.reload() - assert preprint.has_coi - log = preprint.logs.first() - assert log.action == PreprintLog.UPDATE_HAS_COI - assert log.params == {'preprint': preprint._id, 'user': user._id, 'value': True} - - def test_update_conflict_of_interest_statement(self, app, user, preprint, url): - update_payload = build_preprint_update_payload( - preprint._id, - attributes={'conflict_of_interest_statement': 'Owns shares in Closed Science Corporation.'} - ) - contrib = AuthUserFactory() - preprint.add_contributor(contrib, READ) - res = app.patch_json_api(url, update_payload, auth=contrib.auth, expect_errors=True) - assert res.status_code == 403 - assert res.json['errors'][0]['detail'] == 'You do not have permission to perform this action.' - - preprint.has_coi = False - preprint.save() - res = app.patch_json_api(url, update_payload, auth=user.auth, expect_errors=True) - - assert res.status_code == 400 - assert res.json['errors'][0]['detail'] == 'You do not have the ability to edit a conflict of interest while the ' \ - 'has_coi field is set to false or unanswered' - - preprint.has_coi = True - preprint.save() - res = app.patch_json_api(url, update_payload, auth=user.auth) - - assert res.status_code == 200 - assert res.json['data']['attributes']['conflict_of_interest_statement'] ==\ - 'Owns shares in Closed Science Corporation.' - - preprint.reload() - assert preprint.conflict_of_interest_statement == 'Owns shares in Closed Science Corporation.' - log = preprint.logs.first() - assert log.action == PreprintLog.UPDATE_COI_STATEMENT - assert log.params == {'preprint': preprint._id, 'user': user._id} - - def test_update_has_data_links(self, app, user, preprint, url): - update_payload = build_preprint_update_payload(preprint._id, attributes={'has_data_links': 'available'}) - - contrib = AuthUserFactory() - preprint.add_contributor(contrib, READ) - res = app.patch_json_api(url, update_payload, auth=contrib.auth, expect_errors=True) - assert res.status_code == 403 - assert res.json['errors'][0]['detail'] == 'You do not have permission to perform this action.' - - res = app.patch_json_api(url, update_payload, auth=user.auth) - - assert res.status_code == 200 - assert res.json['data']['attributes']['has_data_links'] == 'available' - - preprint.reload() - assert preprint.has_data_links - log = preprint.logs.first() - assert log.action == PreprintLog.UPDATE_HAS_DATA_LINKS - assert log.params == {'value': 'available', 'user': user._id, 'preprint': preprint._id} - - def test_update_why_no_data(self, app, user, preprint, url): - update_payload = build_preprint_update_payload(preprint._id, attributes={'why_no_data': 'My dog ate it.'}) - - contrib = AuthUserFactory() - preprint.add_contributor(contrib, READ) - res = app.patch_json_api(url, update_payload, auth=contrib.auth, expect_errors=True) - assert res.status_code == 403 - assert res.json['errors'][0]['detail'] == 'You do not have permission to perform this action.' - - res = app.patch_json_api(url, update_payload, auth=user.auth, expect_errors=True) - - assert res.status_code == 400 - assert res.json['errors'][0]['detail'] == 'You cannot edit this statement while your data links availability' \ - ' is set to true or is unanswered.' - - preprint.has_data_links = 'no' - preprint.save() - res = app.patch_json_api(url, update_payload, auth=user.auth) - - assert res.status_code == 200 - assert res.json['data']['attributes']['why_no_data'] == 'My dog ate it.' - - preprint.reload() - assert preprint.why_no_data - log = preprint.logs.first() - assert log.action == PreprintLog.UPDATE_WHY_NO_DATA - assert log.params == {'user': user._id, 'preprint': preprint._id} - - def test_update_data_links(self, app, user, preprint, url): - data_links = ['http://www.JasonKelce.com', 'http://www.ItsTheWholeTeam.com/'] - update_payload = build_preprint_update_payload(preprint._id, attributes={'data_links': data_links}) - - contrib = AuthUserFactory() - preprint.add_contributor(contrib, READ) - res = app.patch_json_api(url, update_payload, auth=contrib.auth, expect_errors=True) - assert res.status_code == 403 - assert res.json['errors'][0]['detail'] == 'You do not have permission to perform this action.' - - preprint.has_data_links = 'no' - preprint.save() - res = app.patch_json_api(url, update_payload, auth=user.auth, expect_errors=True) - - assert res.status_code == 400 - assert res.json['errors'][0]['detail'] == 'You cannot edit this statement while your data links availability' \ - ' is set to false or is unanswered.' - - preprint.has_data_links = 'available' - preprint.save() - res = app.patch_json_api(url, update_payload, auth=user.auth) - - assert res.status_code == 200 - assert res.json['data']['attributes']['data_links'] == data_links - - preprint.reload() - assert preprint.data_links == data_links - log = preprint.logs.first() - assert log.action == PreprintLog.UPDATE_DATA_LINKS - assert log.params == {'user': user._id, 'preprint': preprint._id} - - update_payload = build_preprint_update_payload(preprint._id, attributes={'data_links': 'maformed payload'}) - res = app.patch_json_api(url, update_payload, auth=user.auth, expect_errors=True) - - assert res.status_code == 400 - assert res.json['errors'][0]['detail'] == 'Expected a list of items but got type "str".' - - def test_invalid_data_links(self, app, user, preprint, url): - preprint.has_data_links = 'available' - preprint.save() - - update_payload = build_preprint_update_payload(preprint._id, attributes={'data_links': ['thisaintright']}) - - res = app.patch_json_api(url, update_payload, auth=user.auth, expect_errors=True) - assert res.status_code == 400 - assert res.json['errors'][0]['detail'] == 'Enter a valid URL.' - - def test_update_has_prereg_links(self, app, user, preprint, url): - update_payload = build_preprint_update_payload(preprint._id, attributes={'has_prereg_links': 'available'}) - - contrib = AuthUserFactory() - preprint.add_contributor(contrib, READ) - res = app.patch_json_api(url, update_payload, auth=contrib.auth, expect_errors=True) - assert res.status_code == 403 - assert res.json['errors'][0]['detail'] == 'You do not have permission to perform this action.' - - res = app.patch_json_api(url, update_payload, auth=user.auth) - - assert res.status_code == 200 - assert res.json['data']['attributes']['has_prereg_links'] == 'available' - - preprint.reload() - assert preprint.has_prereg_links - log = preprint.logs.first() - assert log.action == PreprintLog.UPDATE_HAS_PREREG_LINKS - assert log.params == {'value': 'available', 'user': user._id, 'preprint': preprint._id} - - def test_invalid_prereg_links(self, app, user, preprint, url): - preprint.has_prereg_links = 'available' - preprint.save() - - update_payload = build_preprint_update_payload(preprint._id, attributes={'prereg_links': ['thisaintright']}) - - res = app.patch_json_api(url, update_payload, auth=user.auth, expect_errors=True) - - assert res.status_code == 400 - assert res.json['errors'][0]['detail'] == 'Enter a valid URL.' - - def test_no_data_links_clears_links(self, app, user, preprint, url): - preprint.has_data_links = 'available' - preprint.data_links = ['http://www.apple.com'] - preprint.save() - - update_payload = build_preprint_update_payload(preprint._id, attributes={'has_data_links': 'no'}) - - res = app.patch_json_api(url, update_payload, auth=user.auth) - - assert res.status_code == 200 - assert res.json['data']['attributes']['has_data_links'] == 'no' - assert res.json['data']['attributes']['data_links'] == [] - - def test_no_prereg_links_clears_links(self, app, user, preprint, url): - preprint.has_prereg_links = 'available' - preprint.prereg_links = ['http://example.com'] - preprint.prereg_link_info = 'prereg_analysis' - preprint.save() - - update_payload = build_preprint_update_payload(preprint._id, attributes={'has_prereg_links': 'no'}) - - res = app.patch_json_api(url, update_payload, auth=user.auth) - - assert res.status_code == 200 - assert res.json['data']['attributes']['has_prereg_links'] == 'no' - assert res.json['data']['attributes']['prereg_links'] == [] - assert not res.json['data']['attributes']['prereg_link_info'] - - def test_update_why_no_prereg(self, app, user, preprint, url): - update_payload = build_preprint_update_payload(preprint._id, attributes={'why_no_prereg': 'My dog ate it.'}) - - contrib = AuthUserFactory() - preprint.add_contributor(contrib, READ) - res = app.patch_json_api(url, update_payload, auth=contrib.auth, expect_errors=True) - assert res.status_code == 403 - assert res.json['errors'][0]['detail'] == 'You do not have permission to perform this action.' - - res = app.patch_json_api(url, update_payload, auth=user.auth, expect_errors=True) - - assert res.status_code == 400 - assert res.json['errors'][0]['detail'] == 'You cannot edit this statement while your prereg links availability' \ - ' is set to true or is unanswered.' - - preprint.has_prereg_links = False - preprint.save() - res = app.patch_json_api(url, update_payload, auth=user.auth) - - assert res.status_code == 200 - assert res.json['data']['attributes']['why_no_prereg'] == 'My dog ate it.' - - preprint.reload() - assert preprint.why_no_prereg - log = preprint.logs.first() - assert log.action == PreprintLog.UPDATE_WHY_NO_PREREG - assert log.params == {'user': user._id, 'preprint': preprint._id} - - def test_update_prereg_links(self, app, user, preprint, url): - - prereg_links = ['http://www.JasonKelce.com', 'http://www.ItsTheWholeTeam.com/'] - update_payload = build_preprint_update_payload(preprint._id, attributes={'prereg_links': prereg_links}) - - contrib = AuthUserFactory() - preprint.add_contributor(contrib, READ) - res = app.patch_json_api(url, update_payload, auth=contrib.auth, expect_errors=True) - assert res.status_code == 403 - assert res.json['errors'][0]['detail'] == 'You do not have permission to perform this action.' - - preprint.has_prereg_links = 'no' - preprint.save() - res = app.patch_json_api(url, update_payload, auth=user.auth, expect_errors=True) - - assert res.status_code == 400 - assert res.json['errors'][0]['detail'] == 'You cannot edit this field while your prereg links availability' \ - ' is set to false or is unanswered.' - - preprint.has_prereg_links = 'available' - preprint.save() - res = app.patch_json_api(url, update_payload, auth=user.auth) - - assert res.status_code == 200 - assert res.json['data']['attributes']['prereg_links'] == prereg_links - - preprint.reload() - assert preprint.prereg_links == prereg_links - log = preprint.logs.first() - assert log.action == PreprintLog.UPDATE_PREREG_LINKS - assert log.params == {'user': user._id, 'preprint': preprint._id} - - update_payload = build_preprint_update_payload(preprint._id, attributes={'prereg_links': 'maformed payload'}) - res = app.patch_json_api(url, update_payload, auth=user.auth, expect_errors=True) - - assert res.status_code == 400 - assert res.json['errors'][0]['detail'] == 'Expected a list of items but got type "str".' - - def test_update_prereg_link_info(self, app, user, preprint, url): - update_payload = build_preprint_update_payload( - preprint._id, - attributes={'prereg_link_info': 'prereg_designs'} - ) - - preprint.has_prereg_links = 'no' - preprint.save() - res = app.patch_json_api(url, update_payload, auth=user.auth, expect_errors=True) - - assert res.status_code == 400 - assert res.json['errors'][0]['detail'] == 'You cannot edit this field while your prereg links availability' \ - ' is set to false or is unanswered.' - - preprint.has_prereg_links = 'available' - preprint.save() - res = app.patch_json_api(url, update_payload, auth=user.auth) - - assert res.status_code == 200 - assert res.json['data']['attributes']['prereg_link_info'] == 'prereg_designs' - - preprint.reload() - assert preprint.prereg_link_info == 'prereg_designs' - log = preprint.logs.first() - assert log.action == PreprintLog.UPDATE_PREREG_LINKS_INFO - assert log.params == {'user': user._id, 'preprint': preprint._id} - - update_payload = build_preprint_update_payload( - preprint._id, - attributes={'prereg_link_info': 'maformed payload'} - ) - res = app.patch_json_api(url, update_payload, auth=user.auth, expect_errors=True) - - assert res.status_code == 400 - assert res.json['errors'][0]['detail'] == '"maformed payload" is not a valid choice.' - - def test_sloan_updates(self, app, user, preprint, url): - """ - - Tests to ensure updating a preprint with unchanged data does not create superfluous log statements. - - Tests to ensure various dependent fields can be updated in a single request. - """ - preprint.has_prereg_links = 'available' - preprint.prereg_links = ['http://no-sf.io'] - preprint.prereg_link_info = 'prereg_designs' - preprint.save() - - update_payload = build_preprint_update_payload( - preprint._id, - attributes={ - 'has_prereg_links': 'available', - 'prereg_link_info': 'prereg_designs', - 'prereg_links': ['http://osf.io'], # changing here should be only non-factory created log. - } - ) - app.patch_json_api(url, update_payload, auth=user.auth, expect_errors=True) - - # Any superfluous log statements? - logs = preprint.logs.all().values_list('action', 'params') - assert logs.count() == 3 # actions should be: 'subjects_updated', 'published', 'prereg_links_updated' - assert logs.latest() == ('prereg_links_updated', {'user': user._id, 'preprint': preprint._id}) - - # Can we set `has_prereg_links` to false and update `why_no_prereg` in a single request? - update_payload = build_preprint_update_payload( - preprint._id, - attributes={ - 'has_prereg_links': 'no', - 'why_no_prereg': 'My dog ate it.' - } - ) - res = app.patch_json_api(url, update_payload, auth=user.auth, expect_errors=True) - - assert res.status_code == 200 - assert res.json['data']['attributes']['has_prereg_links'] == 'no' - assert res.json['data']['attributes']['why_no_prereg'] == 'My dog ate it.' - - preprint.refresh_from_db() - assert preprint.has_prereg_links == 'no' - assert preprint.why_no_prereg == 'My dog ate it.' - @pytest.mark.django_db class TestPreprintUpdateSubjects(UpdateSubjectsMixin): diff --git a/api_tests/preprints/views/test_preprint_detail_author_assertions.py b/api_tests/preprints/views/test_preprint_detail_author_assertions.py new file mode 100644 index 00000000000..63dc8696d41 --- /dev/null +++ b/api_tests/preprints/views/test_preprint_detail_author_assertions.py @@ -0,0 +1,300 @@ +import pytest + +from osf.utils.permissions import READ, WRITE, ADMIN +from api.base.settings.defaults import API_BASE +from osf.models import PreprintLog +from osf_tests.factories import PreprintFactory, AuthUserFactory + + +def build_preprint_update_payload( + node_id, attributes=None, relationships=None, + jsonapi_type='preprints'): + payload = { + 'data': { + 'id': node_id, + 'type': jsonapi_type, + 'attributes': attributes, + 'relationships': relationships + } + } + return payload + + +@pytest.mark.django_db +@pytest.mark.enable_enqueue_task +class TestPreprintUpdateWithAuthorAssertion: + + @pytest.fixture() + def user(self): + return AuthUserFactory() + + @pytest.fixture() + def preprint(self, user): + """ + Creator is not admin permission + """ + preprint = PreprintFactory(creator=user) + admin = AuthUserFactory() + preprint.add_contributor(admin, ADMIN) + preprint.add_contributor(user, READ) + return preprint + + @pytest.fixture() + def url(self, preprint): + return f'/{API_BASE}preprints/{preprint._id}/' + + @pytest.fixture() + def read_contrib(self, preprint): + contrib = AuthUserFactory() + preprint.add_contributor(contrib, READ) + return contrib + + @pytest.fixture() + def write_contrib(self, preprint): + contrib = AuthUserFactory() + preprint.add_contributor(contrib, WRITE) + return contrib + + @pytest.fixture() + def admin_contrib(self, preprint): + contrib = AuthUserFactory() + preprint.add_contributor(contrib, ADMIN) + return contrib + + def assert_permission(self, app, url, contrib, attributes, expected_status): + update_payload = build_preprint_update_payload(node_id=contrib._id, attributes=attributes) + res = app.patch_json_api(url, update_payload, auth=contrib.auth, expect_errors=True) + assert res.status_code == expected_status + + # Testing permissions for updating has_coi + def test_update_has_coi_permission_denied(self, app, read_contrib, url): + self.assert_permission(app, url, read_contrib, {'has_coi': True}, 403) + + def test_update_has_coi_permission_granted_write(self, app, write_contrib, url): + self.assert_permission(app, url, write_contrib, {'has_coi': True}, 403) + + def test_update_has_coi_permission_granted_admin(self, app, admin_contrib, url): + self.assert_permission(app, url, admin_contrib, {'has_coi': True}, 200) + + def test_update_has_coi_permission_granted_creator(self, app, user, url): + self.assert_permission(app, url, user, {'has_coi': True}, 403) + + # Testing permissions for updating conflict_of_interest_statement + def test_update_conflict_of_interest_statement_permission_denied(self, app, read_contrib, url): + self.assert_permission(app, url, read_contrib, {'conflict_of_interest_statement': 'Test'}, 403) + + def test_update_conflict_of_interest_statement_permission_granted_write(self, app, write_contrib, preprint, url): + preprint.has_coi = True + preprint.save() + self.assert_permission(app, url, write_contrib, {'conflict_of_interest_statement': 'Test'}, 403) + + def test_update_conflict_of_interest_statement_permission_granted_admin(self, app, admin_contrib, preprint, url): + preprint.has_coi = True + preprint.save() + self.assert_permission(app, url, admin_contrib, {'conflict_of_interest_statement': 'Test'}, 200) + + def test_update_conflict_of_interest_statement_permission_granted_creator(self, app, user, preprint, url): + preprint.has_coi = True + preprint.save() + self.assert_permission(app, url, user, {'conflict_of_interest_statement': 'Test'}, 403) + + # Testing permissions for updating has_data_links + def test_update_has_data_links_permission_denied(self, app, read_contrib, url): + self.assert_permission(app, url, read_contrib, {'has_data_links': 'available'}, 403) + + def test_update_has_data_links_permission_granted_write(self, app, write_contrib, url): + self.assert_permission(app, url, write_contrib, {'has_data_links': 'available'}, 403) + + def test_update_has_data_links_permission_granted_admin(self, app, admin_contrib, url): + self.assert_permission(app, url, admin_contrib, {'has_data_links': 'available'}, 200) + + def test_update_has_data_links_permission_granted_creator(self, app, user, url): + self.assert_permission(app, url, user, {'has_data_links': 'available'}, 403) + + # Testing permissions for updating why_no_data + def test_update_why_no_data_permission_denied(self, app, read_contrib, url): + self.assert_permission(app, url, read_contrib, {'why_no_data': 'My dog ate it.'}, 403) + + def test_update_why_no_data_permission_granted_write(self, app, write_contrib, preprint, url): + preprint.has_data_links = 'no' + preprint.save() + self.assert_permission(app, url, write_contrib, {'why_no_data': 'My dog ate it.'}, 403) + + def test_update_why_no_data_permission_granted_admin(self, app, admin_contrib, preprint, url): + preprint.has_data_links = 'no' + preprint.save() + self.assert_permission(app, url, admin_contrib, {'why_no_data': 'My dog ate it.'}, 200) + + def test_update_why_no_data_permission_granted_creator(self, app, user, preprint, url): + preprint.has_data_links = 'no' + preprint.save() + self.assert_permission(app, url, user, {'why_no_data': 'My dog ate it.'}, 403) + + # Testing permissions for updating data_links + def test_update_data_links_permission_denied(self, app, read_contrib, url): + data_links = ['http://www.JasonKelce.com', 'http://www.ItsTheWholeTeam.com/'] + self.assert_permission(app, url, read_contrib, {'data_links': data_links}, 403) + + def test_update_data_links_permission_granted_write(self, app, write_contrib, preprint, url): + data_links = ['http://www.JasonKelce.com', 'http://www.ItsTheWholeTeam.com/'] + preprint.has_data_links = 'available' + preprint.save() + self.assert_permission(app, url, write_contrib, {'data_links': data_links}, 403) + + def test_update_data_links_permission_granted_admin(self, app, admin_contrib, preprint, url): + data_links = ['http://www.JasonKelce.com', 'http://www.ItsTheWholeTeam.com/'] + preprint.has_data_links = 'available' + preprint.save() + self.assert_permission(app, url, admin_contrib, {'data_links': data_links}, 200) + + def test_update_data_links_permission_granted_creator(self, app, user, preprint, url): + data_links = ['http://www.JasonKelce.com', 'http://www.ItsTheWholeTeam.com/'] + preprint.has_data_links = 'available' + preprint.save() + self.assert_permission(app, url, user, {'data_links': data_links}, 403) + + def test_update_data_links_invalid_payload(self, app, admin_contrib, url): + update_payload = build_preprint_update_payload(node_id=admin_contrib._id, attributes={'data_links': 'maformed payload'}) + res = app.patch_json_api(url, update_payload, auth=admin_contrib.auth, expect_errors=True) + assert res.status_code == 400 + assert res.json['errors'][0]['detail'] == 'Expected a list of items but got type "str".' + + def test_update_data_links_invalid_url(self, app, admin_contrib, preprint, url): + preprint.has_data_links = 'available' + preprint.save() + update_payload = build_preprint_update_payload(node_id=admin_contrib._id, attributes={'data_links': ['thisaintright']}) + res = app.patch_json_api(url, update_payload, auth=admin_contrib.auth, expect_errors=True) + assert res.status_code == 400 + assert res.json['errors'][0]['detail'] == 'Enter a valid URL.' + + # Testing permissions for updating has_prereg_links + def test_update_has_prereg_links_permission_denied(self, app, read_contrib, url): + self.assert_permission(app, url, read_contrib, {'has_prereg_links': 'available'}, 403) + + def test_update_has_prereg_links_permission_granted_write(self, app, write_contrib, url): + self.assert_permission(app, url, write_contrib, {'has_prereg_links': 'available'}, 403) + + def test_update_has_prereg_links_permission_granted_admin(self, app, admin_contrib, url): + self.assert_permission(app, url, admin_contrib, {'has_prereg_links': 'available'}, 200) + + def test_update_has_prereg_links_permission_granted_creator(self, app, user, url): + self.assert_permission(app, url, user, {'has_prereg_links': 'available'}, 403) + + # Testing permissions for updating prereg_links + def test_update_prereg_links_permission_denied(self, app, read_contrib, url): + prereg_links = ['http://www.JasonKelce.com', 'http://www.ItsTheWholeTeam.com/'] + self.assert_permission(app, url, read_contrib, {'prereg_links': prereg_links}, 403) + + def test_update_prereg_links_permission_granted_write(self, app, write_contrib, preprint, url): + prereg_links = ['http://www.JasonKelce.com', 'http://www.ItsTheWholeTeam.com/'] + preprint.has_prereg_links = 'available' + preprint.save() + self.assert_permission(app, url, write_contrib, {'prereg_links': prereg_links}, 403) + + def test_update_prereg_links_permission_granted_admin(self, app, admin_contrib, preprint, url): + prereg_links = ['http://www.JasonKelce.com', 'http://www.ItsTheWholeTeam.com/'] + preprint.has_prereg_links = 'available' + preprint.save() + self.assert_permission(app, url, admin_contrib, {'prereg_links': prereg_links}, 200) + + def test_update_prereg_links_permission_granted_creator(self, app, user, preprint, url): + prereg_links = ['http://www.JasonKelce.com', 'http://www.ItsTheWholeTeam.com/'] + preprint.has_prereg_links = 'available' + preprint.save() + self.assert_permission(app, url, user, {'prereg_links': prereg_links}, 403) + + def test_update_prereg_links_invalid_payload(self, app, admin_contrib, url): + update_payload = build_preprint_update_payload(node_id=admin_contrib._id, attributes={'prereg_links': 'maformed payload'}) + res = app.patch_json_api(url, update_payload, auth=admin_contrib.auth, expect_errors=True) + assert res.status_code == 400 + assert res.json['errors'][0]['detail'] == 'Expected a list of items but got type "str".' + + def test_update_prereg_links_invalid_url(self, app, admin_contrib, preprint, url): + preprint.has_prereg_links = 'available' + preprint.save() + update_payload = build_preprint_update_payload(node_id=admin_contrib._id, attributes={'prereg_links': ['thisaintright']}) + res = app.patch_json_api(url, update_payload, auth=admin_contrib.auth, expect_errors=True) + assert res.status_code == 400 + assert res.json['errors'][0]['detail'] == 'Enter a valid URL.' + + def test_update_prereg_link_info_fail_prereg_links(self, app, admin_contrib, preprint, url): + update_payload = build_preprint_update_payload(node_id=admin_contrib._id, attributes={'prereg_link_info': 'prereg_designs'}) + preprint.has_prereg_links = 'no' + preprint.save() + res = app.patch_json_api(url, update_payload, auth=admin_contrib.auth, expect_errors=True) + assert res.status_code == 400 + assert res.json['errors'][0]['detail'] == 'You cannot edit this field while your prereg links availability is set to false or is unanswered.' + + def test_update_prereg_link_info_success(self, app, admin_contrib, preprint, url): + update_payload = build_preprint_update_payload(node_id=admin_contrib._id, attributes={'prereg_link_info': 'prereg_designs'}) + preprint.has_prereg_links = 'available' + preprint.save() + res = app.patch_json_api(url, update_payload, auth=admin_contrib.auth) + assert res.status_code == 200 + assert res.json['data']['attributes']['prereg_link_info'] == 'prereg_designs' + preprint.reload() + assert preprint.prereg_link_info == 'prereg_designs' + log = preprint.logs.first() + assert log.action == PreprintLog.UPDATE_PREREG_LINKS_INFO + assert log.params == {'user': admin_contrib._id, 'preprint': preprint._id} + + def test_update_prereg_link_info_invalid_payload(self, app, admin_contrib, url): + update_payload = build_preprint_update_payload(node_id=admin_contrib._id, attributes={'prereg_link_info': 'maformed payload'}) + res = app.patch_json_api(url, update_payload, auth=admin_contrib.auth, expect_errors=True) + assert res.status_code == 400 + assert res.json['errors'][0]['detail'] == '"maformed payload" is not a valid choice.' + + def test_no_prereg_links_clears_links(self, app, admin_contrib, preprint, url): + preprint.has_prereg_links = 'available' + preprint.prereg_links = ['http://example.com'] + preprint.prereg_link_info = 'prereg_analysis' + preprint.save() + update_payload = build_preprint_update_payload(node_id=admin_contrib._id, attributes={'has_prereg_links': 'no'}) + res = app.patch_json_api(url, update_payload, auth=admin_contrib.auth) + assert res.status_code == 200 + assert res.json['data']['attributes']['has_prereg_links'] == 'no' + assert res.json['data']['attributes']['prereg_links'] == [] + assert not res.json['data']['attributes']['prereg_link_info'] + + def test_no_data_links_clears_links(self, app, admin_contrib, preprint, url): + preprint.has_data_links = 'available' + preprint.data_links = ['http://www.apple.com'] + preprint.save() + update_payload = build_preprint_update_payload(node_id=admin_contrib._id, attributes={'has_data_links': 'no'}) + res = app.patch_json_api(url, update_payload, auth=admin_contrib.auth) + assert res.status_code == 200 + assert res.json['data']['attributes']['has_data_links'] == 'no' + assert res.json['data']['attributes']['data_links'] == [] + + def test_sloan_updates(self, app, admin_contrib, preprint, url): + preprint.has_prereg_links = 'available' + preprint.prereg_links = ['http://no-sf.io'] + preprint.prereg_link_info = 'prereg_designs' + preprint.save() + update_payload = build_preprint_update_payload( + node_id=preprint._id, + attributes={ + 'has_prereg_links': 'available', + 'prereg_link_info': 'prereg_designs', + 'prereg_links': ['http://osf.io'], + } + ) + app.patch_json_api(url, update_payload, auth=admin_contrib.auth, expect_errors=True) + logs = preprint.logs.all().values_list('action', 'params') + assert logs.count() == 5 + assert logs.latest() == ('prereg_links_updated', {'user': admin_contrib._id, 'preprint': preprint._id}) + + update_payload = build_preprint_update_payload( + node_id=preprint._id, + attributes={ + 'has_prereg_links': 'no', + 'why_no_prereg': 'My dog ate it.' + } + ) + res = app.patch_json_api(url, update_payload, auth=admin_contrib.auth, expect_errors=True) + assert res.status_code == 200 + assert res.json['data']['attributes']['has_prereg_links'] == 'no' + assert res.json['data']['attributes']['why_no_prereg'] == 'My dog ate it.' + preprint.refresh_from_db() + assert preprint.has_prereg_links == 'no' + assert preprint.why_no_prereg == 'My dog ate it.' diff --git a/api_tests/preprints/views/test_preprint_institutions.py b/api_tests/preprints/views/test_preprint_institutions.py new file mode 100644 index 00000000000..a874ffa15bf --- /dev/null +++ b/api_tests/preprints/views/test_preprint_institutions.py @@ -0,0 +1,162 @@ +import pytest + +from api.base.settings.defaults import API_BASE +from osf_tests.factories import ( + PreprintFactory, + AuthUserFactory, + InstitutionFactory, +) +from osf.utils import permissions as osf_permissions + + +@pytest.mark.django_db +class TestPrivatePreprintInstitutionsList: + + @pytest.fixture() + def url(self, private_preprint): + return f'/{API_BASE}preprints/{private_preprint._id}/institutions/' + + @pytest.fixture() + def invalid_url(self): + return f'/{API_BASE}preprints/invalid_id/institutions/' + + @pytest.fixture() + def user(self): + return AuthUserFactory() + + @pytest.fixture() + def private_preprint(self): + preprint = PreprintFactory() + preprint.is_public = False + preprint.save() + return preprint + + @pytest.fixture() + def read_contrib(self, private_preprint): + user = AuthUserFactory() + private_preprint.add_permission(user, osf_permissions.READ) + return user + + @pytest.fixture() + def write_contrib(self, private_preprint): + user = AuthUserFactory() + private_preprint.add_permission(user, osf_permissions.WRITE) + return user + + @pytest.fixture() + def admin_contrib(self, private_preprint): + user = AuthUserFactory() + private_preprint.add_permission(user, osf_permissions.ADMIN) + return user + + @pytest.fixture() + def institution(self): + return InstitutionFactory() + + def test_preprint_institutions_no_auth(self, app, url): + res = app.get(url, expect_errors=True) + assert res.status_code == 401 + + def test_preprint_institutions_unauth(self, app, url, user, private_preprint): + res = app.get(url, auth=user.auth, expect_errors=True) + assert res.status_code == 403 + + def test_preprint_institutions_read(self, app, url, read_contrib, private_preprint, institution): + + res = app.get(url, auth=read_contrib.auth) + assert res.status_code == 200 + assert not res.json['data'] + + private_preprint.affiliated_institutions.add(institution) + res = app.get(url, auth=read_contrib.auth) + assert res.status_code == 200 + + assert res.json['data'][0]['id'] == institution._id + assert res.json['data'][0]['type'] == 'institutions' + + def test_preprint_institutions_write(self, app, url, write_contrib, private_preprint, institution): + + res = app.get(url, auth=write_contrib.auth) + assert res.status_code == 200 + + assert not res.json['data'] + + private_preprint.affiliated_institutions.add(institution) + res = app.get(url, auth=write_contrib.auth) + assert res.status_code == 200 + + assert res.json['data'][0]['id'] == institution._id + assert res.json['data'][0]['type'] == 'institutions' + + def test_preprint_institutions_admin(self, app, url, admin_contrib, private_preprint, institution): + + res = app.get(url, auth=admin_contrib.auth) + assert res.status_code == 200 + + assert not res.json['data'] + + private_preprint.affiliated_institutions.add(institution) + res = app.get(url, auth=admin_contrib.auth) + assert res.status_code == 200 + + assert res.json['data'][0]['id'] == institution._id + assert res.json['data'][0]['type'] == 'institutions' + + def test_invalid_preprint_id(self, app, invalid_url): + res = app.get(invalid_url, expect_errors=True) + assert res.status_code == 404 + + +@pytest.mark.django_db +class TestPublicPreprintInstitutionsList: + + @pytest.fixture() + def url(self, public_preprint): + return f'/{API_BASE}preprints/{public_preprint._id}/institutions/' + + @pytest.fixture() + def invalid_url(self): + return f'/{API_BASE}preprints/invalid_id/institutions/' + + @pytest.fixture() + def user(self): + return AuthUserFactory() + + @pytest.fixture() + def institution(self): + return InstitutionFactory() + + @pytest.fixture() + def public_preprint(self): + return PreprintFactory() + + @pytest.fixture() + def read_contrib(self, public_preprint): + user = AuthUserFactory() + public_preprint.add_permission(user, osf_permissions.READ) + return user + + def test_preprint_institutions_no_auth(self, app, url): + res = app.get(url) + assert res.status_code == 200 + + def test_preprint_institutions_unauth(self, app, url, user): + res = app.get(url, auth=user.auth) + assert res.status_code == 200 + + def test_preprint_institutions_read(self, app, url, read_contrib, public_preprint, institution): + + res = app.get(url, auth=read_contrib.auth) + assert res.status_code == 200 + assert not res.json['data'] + + public_preprint.affiliated_institutions.add(institution) + res = app.get(url, auth=read_contrib.auth) + assert res.status_code == 200 + + assert res.json['data'][0]['id'] == institution._id + assert res.json['data'][0]['type'] == 'institutions' + + def test_invalid_preprint_id(self, app, invalid_url): + res = app.get(invalid_url, expect_errors=True) + assert res.status_code == 404 diff --git a/api_tests/preprints/views/test_preprint_institutions_relationship.py b/api_tests/preprints/views/test_preprint_institutions_relationship.py new file mode 100644 index 00000000000..336ae2cd8ea --- /dev/null +++ b/api_tests/preprints/views/test_preprint_institutions_relationship.py @@ -0,0 +1,308 @@ +import pytest +from api.base.settings.defaults import API_BASE +from osf_tests.factories import PreprintFactory, AuthUserFactory, InstitutionFactory +from osf.utils.permissions import READ, WRITE, ADMIN + + +@pytest.mark.django_db +class TestPreprintInstitutionsRelationship: + """Test suite for managing preprint institution relationships.""" + + @pytest.fixture() + def user(self): + return AuthUserFactory() + + @pytest.fixture() + def institution_A(self): + return InstitutionFactory() + + @pytest.fixture() + def institution_B(self): + return InstitutionFactory() + + @pytest.fixture() + def institution_C(self): + return InstitutionFactory() + + @pytest.fixture() + def institution_D(self): + return InstitutionFactory() + + @pytest.fixture() + def institution_E(self): + return InstitutionFactory() + + @pytest.fixture() + def institution_F(self): + return InstitutionFactory() + + @pytest.fixture() + def admin_with_institutional_affiliation(self, institution_A, institution_B, institution_C, preprint): + user = AuthUserFactory() + preprint.add_contributor(user, permissions=ADMIN) + user.add_or_update_affiliated_institution(institution_A) + user.add_or_update_affiliated_institution(institution_B) + user.add_or_update_affiliated_institution(institution_C) + return user + + @pytest.fixture() + def write_user_with_institutional_affiliation(self, institution_B, institution_C, institution_D, preprint): + user = AuthUserFactory() + preprint.add_contributor(user, permissions=WRITE) + user.add_or_update_affiliated_institution(institution_B) + user.add_or_update_affiliated_institution(institution_C) + user.add_or_update_affiliated_institution(institution_D) + return user + + @pytest.fixture() + def read_user_with_institutional_affiliation(self, institution_C, institution_D, institution_F, preprint): + user = AuthUserFactory() + preprint.add_contributor(user, permissions=READ) + user.add_or_update_affiliated_institution(institution_C) + user.add_or_update_affiliated_institution(institution_D) + user.add_or_update_affiliated_institution(institution_F) + return user + + @pytest.fixture() + def no_auth_with_institutional_affiliation(self, institution): + user = AuthUserFactory() + user.add_or_update_affiliated_institution(institution) + user.save() + return user + + @pytest.fixture() + def admin_without_institutional_affiliation(self, preprint): + user = AuthUserFactory() + preprint.add_contributor(user, permissions=ADMIN) + return user + + @pytest.fixture() + def institutions(self): + return [InstitutionFactory() for _ in range(3)] + + @pytest.fixture() + def preprint(self): + return PreprintFactory() + + @pytest.fixture() + def url(self, preprint): + """Fixture that returns the URL for the preprint-institutions relationship endpoint.""" + return f'/{API_BASE}preprints/{preprint._id}/relationships/institutions/' + + def test_update_affiliated_institutions_add_unauthorized_user(self, app, user, url, institution_A): + """ + Test that unauthorized users cannot add institutions. + """ + update_institutions_payload = {'data': [{'type': 'institutions', 'id': institution_A._id}]} + res = app.put_json_api(url, update_institutions_payload, auth=user.auth, expect_errors=True) + assert res.status_code == 403 + + def test_update_affiliated_institutions_add_read_user(self, app, read_user_with_institutional_affiliation, url, institution_A): + """ + Test that read users cannot add institutions. + """ + update_institutions_payload = {'data': [{'type': 'institutions', 'id': institution_A._id}]} + res = app.put_json_api(url, update_institutions_payload, auth=read_user_with_institutional_affiliation.auth, expect_errors=True) + assert res.status_code == 403 + + def test_update_affiliated_institutions_add_write_user(self, app, write_user_with_institutional_affiliation, url, institution_A, institution_B): + """ + Test that write users cannot add institutions. + """ + update_institutions_payload = {'data': [{'type': 'institutions', 'id': institution_A._id}]} + res = app.put_json_api(url, update_institutions_payload, auth=write_user_with_institutional_affiliation.auth, expect_errors=True) + assert res.status_code == 403 + + update_institutions_payload = {'data': [{'type': 'institutions', 'id': institution_B._id}]} + res = app.put_json_api(url, update_institutions_payload, auth=write_user_with_institutional_affiliation.auth) + assert res.status_code == 200 + + def test_update_affiliated_institutions_add_admin_without_affiliation(self, app, admin_without_institutional_affiliation, url, institution_A): + """ + Test that admins without affiliation cannot add institutions. + """ + update_institutions_payload = {'data': [{'type': 'institutions', 'id': institution_A._id}]} + res = app.put_json_api(url, update_institutions_payload, auth=admin_without_institutional_affiliation.auth, expect_errors=True) + assert res.status_code == 403 + assert res.json['errors'][0]['detail'] == f'User needs to be affiliated with {institution_A.name}' + + def test_update_affiliated_institutions_add_admin_with_affiliation(self, app, admin_with_institutional_affiliation, preprint, url, institution_A): + """ + Test that admins with affiliation can add institutions. + """ + update_institutions_payload = {'data': [{'type': 'institutions', 'id': institution_A._id}]} + res = app.put_json_api(url, update_institutions_payload, auth=admin_with_institutional_affiliation.auth) + assert res.status_code == 200 + preprint.reload() + assert institution_A in preprint.affiliated_institutions.all() + + log = preprint.logs.latest() + assert log.action == 'affiliated_institution_added' + assert log.params['institution'] == {'id': institution_A._id, 'name': institution_A.name} + + def test_update_affiliated_institutions_remove_unauthorized_user(self, app, user, preprint, url, institution_A): + """ + Test that unauthorized users cannot remove institutions. + """ + preprint.affiliated_institutions.add(institution_A) + preprint.save() + update_institutions_payload = {'data': []} + res = app.put_json_api(url, update_institutions_payload, auth=user.auth, expect_errors=True) + assert res.status_code == 403 + + def test_update_affiliated_institutions_remove_read_user(self, app, read_user_with_institutional_affiliation, preprint, url, institution_A): + """ + Test that read users cannot remove institutions. + """ + preprint.affiliated_institutions.add(institution_A) + preprint.save() + update_institutions_payload = {'data': []} + res = app.put_json_api(url, update_institutions_payload, auth=read_user_with_institutional_affiliation.auth, expect_errors=True) + assert res.status_code == 403 + + def test_update_affiliated_institutions_remove_write_user(self, app, write_user_with_institutional_affiliation, preprint, url, institution_A): + """ + Test that write users cannot remove institutions. + """ + preprint.affiliated_institutions.add(institution_A) + preprint.save() + update_institutions_payload = {'data': []} + res = app.put_json_api(url, update_institutions_payload, auth=write_user_with_institutional_affiliation.auth) + assert res.status_code == 200 + preprint.reload() + assert institution_A in preprint.affiliated_institutions.all() + + def test_update_affiliated_institutions_remove_admin_without_affiliation(self, app, admin_without_institutional_affiliation, preprint, url, institution_A): + """ + Test that admins without affiliation cannot remove institutions. + """ + preprint.affiliated_institutions.add(institution_A) + preprint.save() + update_institutions_payload = {'data': []} + res = app.put_json_api(url, update_institutions_payload, auth=admin_without_institutional_affiliation.auth) + assert res.status_code == 200 + preprint.reload() + assert institution_A in preprint.affiliated_institutions.all() + + def test_update_affiliated_institutions_remove_admin_with_affiliation(self, app, admin_with_institutional_affiliation, preprint, url, institution_A): + """ + Test that admins with affiliation can remove institutions. + """ + preprint.affiliated_institutions.add(institution_A) + preprint.save() + update_institutions_payload = {'data': []} + res = app.put_json_api(url, update_institutions_payload, auth=admin_with_institutional_affiliation.auth) + assert res.status_code == 200 + preprint.reload() + assert institution_A not in preprint.affiliated_institutions.all() + + log = preprint.logs.latest() + assert log.action == 'affiliated_institution_removed' + assert log.params['institution'] == {'id': institution_A._id, 'name': institution_A.name} + + def test_preprint_institutions_list_get_unauthenticated(self, app, url): + """ + Test that unauthenticated users can retrieve the list of affiliated institutions for a preprint. + """ + res = app.get(url) + assert res.status_code == 200 + + def test_preprint_institutions_list_get_no_permissions(self, app, user, url): + """ + Test that users without permissions can retrieve the list of affiliated institutions for a preprint. + """ + res = app.get(url, auth=user.auth) + assert res.status_code == 200 + + def test_preprint_institutions_list_get_read_user(self, app, read_user_with_institutional_affiliation, preprint, url): + """ + Test that read users can retrieve the list of affiliated institutions for a preprint. + """ + preprint.is_public = False + preprint.save() + res = app.get(url, auth=read_user_with_institutional_affiliation.auth) + assert res.status_code == 200 + assert not res.json['data'] + + def test_preprint_institutions_list_get_write_user(self, app, write_user_with_institutional_affiliation, preprint, url): + """ + Test that write users can retrieve the list of affiliated institutions for a preprint. + """ + preprint.is_public = False + preprint.save() + res = app.get(url, auth=write_user_with_institutional_affiliation.auth) + assert res.status_code == 200 + assert not res.json['data'] + + def test_preprint_institutions_list_get_admin_without_affiliation(self, app, admin_without_institutional_affiliation, preprint, url): + """ + Test that admins without affiliation can retrieve the list of affiliated institutions for a preprint. + """ + preprint.is_public = False + preprint.save() + res = app.get(url, auth=admin_without_institutional_affiliation.auth) + assert res.status_code == 200 + assert not res.json['data'] + + def test_preprint_institutions_list_get_admin_with_affiliation(self, app, admin_with_institutional_affiliation, preprint, url, institution_A): + """ + Test that admins with affiliation can retrieve the list of affiliated institutions for a preprint. + """ + preprint.add_affiliated_institution(institution_A, admin_with_institutional_affiliation) + res = app.get(url, auth=admin_with_institutional_affiliation.auth) + assert res.status_code == 200 + assert res.json['data'][0]['id'] == institution_A._id + assert res.json['data'][0]['type'] == 'institutions' + + def test_post_affiliated_institutions(self, app, admin_with_institutional_affiliation, url, institutions): + """ + Test that POST method is not allowed for affiliated institutions. + """ + add_institutions_payload = {'data': [{'type': 'institutions', 'id': institution._id} for institution in institutions]} + res = app.post_json_api(url, add_institutions_payload, auth=admin_with_institutional_affiliation.auth, expect_errors=True) + assert res.status_code == 405 + + def test_patch_affiliated_institutions(self, app, admin_with_institutional_affiliation, url, institutions): + """ + Test that PATCH method is not allowed for affiliated institutions. + """ + add_institutions_payload = {'data': [{'type': 'institutions', 'id': institution._id} for institution in institutions]} + res = app.patch_json_api(url, add_institutions_payload, auth=admin_with_institutional_affiliation.auth, expect_errors=True) + assert res.status_code == 405 + + def test_delete_affiliated_institution(self, app, admin_with_institutional_affiliation, preprint, url, institution_A): + """ + Test that DELETE method is not allowed for affiliated institutions. + """ + preprint.affiliated_institutions.add(institution_A) + preprint.save() + res = app.delete_json_api(url, {'data': [{'type': 'institutions', 'id': institution_A._id}]}, auth=admin_with_institutional_affiliation.auth, expect_errors=True) + assert res.status_code == 405 + + def test_add_multiple_institutions_affiliations(self, app, admin_with_institutional_affiliation, preprint, url, institutions): + """ + Test that admins with multiple affiliations can add them to a preprint. + """ + for institution in institutions: + admin_with_institutional_affiliation.add_or_update_affiliated_institution(institution) + admin_with_institutional_affiliation.save() + add_institutions_payload = {'data': [{'type': 'institutions', 'id': institution._id} for institution in institutions]} + res = app.put_json_api(url, add_institutions_payload, auth=admin_with_institutional_affiliation.auth) + assert res.status_code == 200 + preprint.reload() + assert preprint.affiliated_institutions.all().count() == 3 + + def test_remove_only_institutions_affiliations_that_user_has(self, app, admin_with_institutional_affiliation, preprint, url, institutions, institution_A): + """ + Test that admins with multiple affiliations only remove their own affiliations, leaving others unchanged. + """ + preprint.affiliated_institutions.add(*institutions) + assert preprint.affiliated_institutions.all().count() == 3 + admin_with_institutional_affiliation.add_or_update_affiliated_institution(institutions[0]) + admin_with_institutional_affiliation.add_or_update_affiliated_institution(institutions[1]) + update_institution_payload = {'data': [{'type': 'institutions', 'id': institution_A._id}]} + res = app.put_json_api(url, update_institution_payload, auth=admin_with_institutional_affiliation.auth) + assert res.status_code == 200 + assert preprint.affiliated_institutions.all().count() == 2 + assert institution_A in preprint.affiliated_institutions.all() + assert institutions[2] in preprint.affiliated_institutions.all() diff --git a/api_tests/preprints/views/test_preprint_list.py b/api_tests/preprints/views/test_preprint_list.py index 81fdde05f79..5fd8f224a01 100644 --- a/api_tests/preprints/views/test_preprint_list.py +++ b/api_tests/preprints/views/test_preprint_list.py @@ -26,6 +26,7 @@ AuthUserFactory, SubjectFactory, PreprintProviderFactory, + InstitutionFactory, ) from tests.base import ApiTestCase, capture_signals from website.project import signals as project_signals @@ -145,6 +146,7 @@ class TestPreprintList(ApiTestCase): def setUp(self): super().setUp() self.user = AuthUserFactory() + self.institution = InstitutionFactory() self.preprint = PreprintFactory(creator=self.user) self.url = f'/{API_BASE}preprints/' @@ -184,6 +186,19 @@ def test_withdrawn_preprints_list(self): assert pp._id not in user_res_ids assert pp._id in mod_res_ids + def test_return_affiliated_institutions(self): + """ + Confirmation test for the the new preprint affiliated institutions feature + """ + self.preprint.affiliated_institutions.add(self.institution) + res = self.app.get(self.url) + assert len(res.json['data']) == 1 + assert res.status_code == 200 + assert res.content_type == 'application/vnd.api+json' + relationship_link = res.json['data'][0]['relationships']['affiliated_institutions']['links']['related']['href'] + assert f'/v2/preprints/{self.preprint._id}/institutions/' in relationship_link + relationship_link = res.json['data'][0]['relationships']['affiliated_institutions']['links']['self']['href'] + assert f'/v2/preprints/{self.preprint._id}/relationships/institutions/' in relationship_link class TestPreprintsListFiltering(PreprintsListFilteringMixin): diff --git a/api_tests/registrations/views/test_registration_list.py b/api_tests/registrations/views/test_registration_list.py index b9604b83501..4eca7295f7a 100644 --- a/api_tests/registrations/views/test_registration_list.py +++ b/api_tests/registrations/views/test_registration_list.py @@ -7,7 +7,7 @@ from api.base.settings.defaults import API_BASE from api.base.versioning import CREATE_REGISTRATION_FIELD_CHANGE_VERSION -from api_tests.nodes.views.test_node_draft_registration_list import DraftRegistrationTestCase +from api_tests.nodes.views.test_node_draft_registration_list import AbstractDraftRegistrationTestCase from api_tests.subjects.mixins import SubjectsFilterMixin from api_tests.registrations.filters.test_filters import RegistrationListFilteringMixin from api_tests.utils import create_test_file @@ -585,7 +585,7 @@ def url(self): return f'/{API_BASE}registrations/' -class TestNodeRegistrationCreate(DraftRegistrationTestCase): +class TestNodeRegistrationCreate(AbstractDraftRegistrationTestCase): """ Tests for creating registration through old workflow - POST NodeRegistrationList diff --git a/api_tests/users/views/test_user_detail.py b/api_tests/users/views/test_user_detail.py index d676bb8b7af..02a616bc4c4 100644 --- a/api_tests/users/views/test_user_detail.py +++ b/api_tests/users/views/test_user_detail.py @@ -122,6 +122,16 @@ def test_preprint_relationship(self, app, user_one): href_url = user_json['relationships']['preprints']['links']['related']['href'] assert preprint_url in href_url + def test_draft_preprint_relationship(self, app, user_one): + preprint_url = f'/{API_BASE}users/{user_one._id}/draft_preprints/' + res = app.get( + f'/{API_BASE}users/{user_one._id}/', + auth=user_one + ) + user_json = res.json['data'] + href_url = user_json['relationships']['draft_preprints']['links']['related']['href'] + assert preprint_url in href_url + def test_registrations_relationship(self, app, user_one): url = f'/{API_BASE}users/{user_one._id}/' registration_url = '/{}users/{}/registrations/'.format( diff --git a/api_tests/users/views/test_user_draft_preprint.py b/api_tests/users/views/test_user_draft_preprint.py new file mode 100644 index 00000000000..7d1aca575c6 --- /dev/null +++ b/api_tests/users/views/test_user_draft_preprint.py @@ -0,0 +1,160 @@ +import pytest +from osf.utils.permissions import WRITE +from osf_tests.factories import ( + PreprintFactory, + AuthUserFactory, + ProjectFactory, + SubjectFactory, + PreprintProviderFactory, +) +from api.base.settings.defaults import API_BASE +from django.utils import timezone + +@pytest.mark.django_db +class TestPreprintDraftList: + + @pytest.fixture() + def admin(self): + return AuthUserFactory() + + @pytest.fixture() + def write_contrib(self): + return AuthUserFactory() + + @pytest.fixture() + def non_contrib(self): + return AuthUserFactory() + + @pytest.fixture() + def public_project(self, admin): + return ProjectFactory(creator=admin, is_public=True) + + @pytest.fixture() + def private_project(self, admin): + return ProjectFactory(creator=admin, is_public=False) + + @pytest.fixture() + def subject(self): + return SubjectFactory() + + @pytest.fixture() + def provider(self): + return PreprintProviderFactory() + + @pytest.fixture() + def unpublished_preprint(self, admin, provider, subject, public_project): + return PreprintFactory( + creator=admin, + provider=provider, + is_published=False, + machine_state='initial' + ) + + @pytest.fixture() + def private_preprint(self, admin, provider, subject, private_project, write_contrib): + preprint = PreprintFactory( + creator=admin, + provider=provider, + is_published=True, + is_public=False, + machine_state='accepted' + ) + preprint.add_contributor(write_contrib, permissions=WRITE) + preprint.save() + return preprint + + @pytest.fixture() + def published_preprint(self, admin, provider, subject, write_contrib): + preprint = PreprintFactory( + creator=admin, + provider=provider, + is_published=True, + is_public=True, + machine_state='accepted' + ) + preprint.add_contributor(write_contrib, permissions=WRITE) + return preprint + + @pytest.fixture() + def abandoned_private_preprint(self, admin, provider, subject, private_project): + return PreprintFactory( + creator=admin, + provider=provider, + project=private_project, + is_published=False, + is_public=False, + machine_state='initial' + ) + + @pytest.fixture() + def abandoned_public_preprint(self, admin, provider, subject, public_project): + return PreprintFactory( + creator=admin, + provider=provider, + project=public_project, + is_published=False, + is_public=True, + machine_state='initial' + ) + + @pytest.fixture() + def deleted_preprint(self, admin, provider, subject, public_project): + preprint = PreprintFactory( + creator=admin, + provider=provider, + project=public_project, + is_published=False, + is_public=False, + machine_state='initial', + ) + preprint.deleted = timezone.now() + preprint.save() + return preprint + + def test_gets_preprint_drafts(self, app, admin, abandoned_public_preprint, abandoned_private_preprint, published_preprint): + res = app.get( + f'/{API_BASE}users/{admin._id}/draft_preprints/', + auth=admin.auth + ) + assert res.status_code == 200 + + ids = [each['id'] for each in res.json['data']] + assert abandoned_public_preprint._id in ids + assert abandoned_private_preprint._id in ids + assert published_preprint._id not in ids + + def test_anonymous_gets_401(self, app, admin): + res = app.get( + f'/{API_BASE}users/{admin._id}/draft_preprints/', + expect_errors=True + ) + assert res.status_code == 401 + + def test_get_preprints_non_contrib_gets_403(self, app, admin, non_contrib, abandoned_public_preprint, abandoned_private_preprint): + res = app.get( + f'/{API_BASE}users/{admin._id}/draft_preprints/', + auth=non_contrib.auth, + expect_errors=True + ) + assert res.status_code == 403 + + def test_get_projects_logged_in_as_write_user(self, app, admin, write_contrib, abandoned_public_preprint): + res = app.get( + f'/{API_BASE}users/{admin._id}/draft_preprints/', + auth=write_contrib.auth, + expect_errors=True + ) + assert res.status_code == 403 + + def test_deleted_drafts_excluded(self, app, admin, abandoned_public_preprint, abandoned_private_preprint, published_preprint, deleted_preprint): + res = app.get( + f'/{API_BASE}users/{admin._id}/draft_preprints/', + auth=admin.auth + ) + assert res.status_code == 200 + + ids = [each['id'] for each in res.json['data']] + assert abandoned_public_preprint._id in ids + assert abandoned_private_preprint._id in ids + assert published_preprint._id not in ids + assert deleted_preprint._id not in ids # Make sure deleted preprints are not listed diff --git a/api_tests/users/views/test_user_draft_registration_list.py b/api_tests/users/views/test_user_draft_registration_list.py index 272a9a73d9c..1f43cc3ee33 100644 --- a/api_tests/users/views/test_user_draft_registration_list.py +++ b/api_tests/users/views/test_user_draft_registration_list.py @@ -3,7 +3,7 @@ from api.base.settings.defaults import API_BASE from api.users.views import UserDraftRegistrations -from api_tests.nodes.views.test_node_draft_registration_list import DraftRegistrationTestCase +from api_tests.nodes.views.test_node_draft_registration_list import AbstractDraftRegistrationTestCase from api_tests.utils import only_supports_methods from osf.models import RegistrationSchema from osf_tests.factories import ( @@ -17,7 +17,11 @@ @pytest.mark.django_db -class TestDraftRegistrationList(DraftRegistrationTestCase): +class TestUserDraftRegistrationList(AbstractDraftRegistrationTestCase): + + @pytest.fixture() + def url_draft_registrations(self, project_public): + return f'/{API_BASE}users/me/draft_registrations/' @pytest.fixture() def other_admin(self, project_public): @@ -29,7 +33,8 @@ def other_admin(self, project_public): def schema(self): return RegistrationSchema.objects.get( name='Open-Ended Registration', - schema_version=SCHEMA_VERSION) + schema_version=SCHEMA_VERSION + ) @pytest.fixture() def draft_registration(self, user, project_public, schema): @@ -39,25 +44,12 @@ def draft_registration(self, user, project_public, schema): branched_from=project_public ) - @pytest.fixture() - def url_draft_registrations(self, project_public): - return f'/{API_BASE}users/me/draft_registrations/' - def test_unacceptable_methods(self): assert only_supports_methods(UserDraftRegistrations, ['GET']) - def test_view_permissions( - self, app, user, other_admin, draft_registration, - user_write_contrib, user_read_contrib, user_non_contrib, - schema, url_draft_registrations): - res = app.get(url_draft_registrations, auth=user.auth) - assert res.status_code == 200 - data = res.json['data'] - assert len(data) == 1 - assert schema._id in data[0]['relationships']['registration_schema']['links']['related']['href'] - assert data[0]['id'] == draft_registration._id - assert data[0]['attributes']['registration_metadata'] == {} - + def test_non_contrib_view_permissions( + self, app, user, other_admin, draft_registration, schema, url_draft_registrations + ): res = app.get(url_draft_registrations, auth=user.auth) assert res.status_code == 200 data = res.json['data'] @@ -66,25 +58,33 @@ def test_view_permissions( assert data[0]['id'] == draft_registration._id assert data[0]['attributes']['registration_metadata'] == {} - # test_read_only_contributor_can_view_draft_list + def test_read_only_contributor_can_view_draft_list( + self, app, draft_registration, user_read_contrib, url_draft_registrations + ): res = app.get( url_draft_registrations, - auth=user_read_contrib.auth) + auth=user_read_contrib.auth + ) assert len(res.json['data']) == 1 - # test_read_write_contributor_can_view_draft_list + def test_read_write_contributor_can_view_draft_list( + self, app, user, other_admin, draft_registration, user_write_contrib, url_draft_registrations + ): res = app.get( url_draft_registrations, - auth=user_write_contrib.auth) + auth=user_write_contrib.auth + ) assert len(res.json['data']) == 1 - # test_logged_in_non_contributor_cannot_view_draft_list + def test_logged_in_non_contributor_cannot_view_draft_list( + self, app, user, draft_registration, user_non_contrib, url_draft_registrations + ): res = app.get( url_draft_registrations, auth=user_non_contrib.auth) assert len(res.json['data']) == 0 - # test_unauthenticated_user_cannot_view_draft_list + def test_unauthenticated_user_cannot_view_draft_list(self, app, url_draft_registrations): res = app.get(url_draft_registrations, expect_errors=True) assert res.status_code == 401 @@ -133,15 +133,15 @@ def test_draft_with_deleted_registered_node_shows_up_in_draft_list( assert data[0]['id'] == draft_registration._id assert data[0]['attributes']['registration_metadata'] == {} - def test_cannot_access_other_users_draft_registration( - self, app, user, other_admin, project_public, - draft_registration, schema): - url = f'/{API_BASE}users/{user._id}/draft_registrations/' - res = app.get(url, auth=other_admin.auth, expect_errors=True) + def test_cannot_access_other_users_draft_registration(self, app, user, other_admin, draft_registration, schema): + res = app.get( + f'/{API_BASE}users/{user._id}/draft_registrations/', + auth=other_admin.auth, + expect_errors=True + ) assert res.status_code == 403 - def test_can_access_own_draft_registrations_with_guid( - self, app, user, draft_registration): + def test_can_access_own_draft_registrations_with_guid(self, app, user, draft_registration): url = f'/{API_BASE}users/{user._id}/draft_registrations/' res = app.get(url, auth=user.auth, expect_errors=True) assert res.status_code == 200 diff --git a/api_tests/users/views/test_user_settings.py b/api_tests/users/views/test_user_settings.py index d62b47405e7..039401afd3c 100644 --- a/api_tests/users/views/test_user_settings.py +++ b/api_tests/users/views/test_user_settings.py @@ -215,7 +215,7 @@ def test_unconfirmed_email_included(self, app, url, payload, user_one, unconfirm assert res.status_code == 200 assert unconfirmed_address in [result['attributes']['email_address'] for result in res.json['data']] - @mock.patch('api.users.serializers.send_confirm_email') + @mock.patch('api.users.serializers.send_confirm_email_async') def test_create_new_email_current_user(self, mock_send_confirm_mail, user_one, user_two, app, url, payload): new_email = 'hhh@wwe.test' payload['data']['attributes']['email_address'] = new_email @@ -228,7 +228,7 @@ def test_create_new_email_current_user(self, mock_send_confirm_mail, user_one, u assert new_email in user_one.unconfirmed_emails assert mock_send_confirm_mail.called - @mock.patch('api.users.serializers.send_confirm_email') + @mock.patch('api.users.serializers.send_confirm_email_async') def test_create_new_email_not_current_user(self, mock_send_confirm_mail, app, url, payload, user_one, user_two): new_email = 'HHH@wwe.test' payload['data']['attributes']['email_address'] = new_email @@ -238,7 +238,7 @@ def test_create_new_email_not_current_user(self, mock_send_confirm_mail, app, ur assert new_email not in user_one.unconfirmed_emails assert not mock_send_confirm_mail.called - @mock.patch('api.users.serializers.send_confirm_email') + @mock.patch('api.users.serializers.send_confirm_email_async') def test_create_email_already_exists(self, mock_send_confirm_mail, app, url, payload, user_one): new_email = 'hello@email.test' Email.objects.create(address=new_email, user=user_one) @@ -577,28 +577,28 @@ def test_updating_verified_for_merge(self, app, user_one, user_two, payload): assert res.json['data']['attributes']['confirmed'] is True assert res.json['data']['attributes']['is_merge'] is False - @mock.patch('api.users.views.send_confirm_email') - def test_resend_confirmation_email(self, mock_send_confirm_email, app, user_one, unconfirmed_url, confirmed_url): + @mock.patch('api.users.views.send_confirm_email_async') + def test_resend_confirmation_email(self, mock_send_confirm_email_async, app, user_one, unconfirmed_url, confirmed_url): url = f'{unconfirmed_url}?resend_confirmation=True' res = app.get(url, auth=user_one.auth) assert res.status_code == 202 - assert mock_send_confirm_email.called - call_count = mock_send_confirm_email.call_count + assert mock_send_confirm_email_async.called + call_count = mock_send_confirm_email_async.call_count # make sure setting false does not send confirm email url = f'{unconfirmed_url}?resend_confirmation=False' res = app.get(url, auth=user_one.auth) # should return 200 instead of 202 because nothing has been done assert res.status_code == 200 - assert mock_send_confirm_email.call_count + assert mock_send_confirm_email_async.call_count # make sure normal GET request does not re-send confirmation email res = app.get(unconfirmed_url, auth=user_one.auth) - assert mock_send_confirm_email.call_count == call_count + assert mock_send_confirm_email_async.call_count == call_count assert res.status_code == 200 # resend confirmation with confirmed email address does not send confirmation email url = f'{confirmed_url}?resend_confirmation=True' res = app.get(url, auth=user_one.auth) - assert mock_send_confirm_email.call_count == call_count + assert mock_send_confirm_email_async.call_count == call_count assert res.status_code == 200 diff --git a/conftest.py b/conftest.py index 1acfabbdbb5..2eb51df076e 100644 --- a/conftest.py +++ b/conftest.py @@ -124,7 +124,7 @@ def _test_speedups_disable(request, settings, _test_speedups): @pytest.fixture(scope='session') def setup_connections(): - connections.create_connection(hosts=['http://localhost:9201']) + connections.create_connection(hosts=[website_settings.ELASTIC6_URI]) @pytest.fixture(scope='function') diff --git a/docker-compose-dist-arm64.override.yml b/docker-compose-dist-arm64.override.yml index aad331ae1a7..cffa4bd8982 100644 --- a/docker-compose-dist-arm64.override.yml +++ b/docker-compose-dist-arm64.override.yml @@ -6,43 +6,6 @@ services: # OSF # ####### - requirements: - image: quay.io/centerforopenscience/osf:develop-arm64 + elasticsearch6: + image: quay.io/centerforopenscience/elasticsearch:es6-arm-6.3.1 platform: linux/arm64 - - assets: - image: quay.io/centerforopenscience/osf:develop-arm64 - platform: linux/arm64 - # Need to allocate tty to be able to call invoke for requirements task - tty: true - - admin_assets: - image: quay.io/centerforopenscience/osf:develop-arm64 - platform: linux/arm64 - # Need to allocate tty to be able to call invoke for requirements task - tty: true - - worker: - image: quay.io/centerforopenscience/osf:develop-arm64 - platform: linux/arm64 - # Need to allocate tty to be able to call invoke for requirements task - tty: true - - admin: - image: quay.io/centerforopenscience/osf:develop-arm64 - platform: linux/arm64 - # Need to allocate tty to be able to call invoke for requirements task - tty: true - - api: - image: quay.io/centerforopenscience/osf:develop-arm64 - platform: linux/arm64 - # Need to allocate tty to be able to call invoke for requirements task - tty: true - - web: - image: quay.io/centerforopenscience/osf:develop-arm64 - platform: linux/arm64 - # Need to allocate tty to be able to call invoke for requirements task - tty: true - diff --git a/framework/auth/__init__.py b/framework/auth/__init__.py index 02ae8787edb..73ca8dd06a8 100644 --- a/framework/auth/__init__.py +++ b/framework/auth/__init__.py @@ -12,6 +12,7 @@ from framework.celery_tasks.handlers import enqueue_task from framework.sessions import get_session, create_session from framework.sessions.utils import remove_session +from website.util.metrics import institution_source_tag __all__ = [ @@ -153,6 +154,7 @@ def get_or_create_institutional_user(fullname, sso_email, sso_identity, primary_ # Note: Institution users are created as confirmed with a strong and random password. Users don't need the # password since they sign in via SSO. They can reset their password to enable email/password login. user = OSFUser.create_confirmed(sso_email, str(uuid.uuid4()), fullname) + user.add_system_tag(institution_source_tag(primary_institution._id)) return user, True, None, None, sso_identity diff --git a/framework/auth/cas.py b/framework/auth/cas.py index be194e8093f..1084739fdc3 100644 --- a/framework/auth/cas.py +++ b/framework/auth/cas.py @@ -1,5 +1,4 @@ from furl import furl -from urllib.parse import unquote_plus from django.utils import timezone from rest_framework import status as http_status @@ -304,11 +303,11 @@ def make_response_from_ticket(ticket, service_url): f'CAS response - redirect existing external IdP login to verification key login: user=[{user._id}]', LogLevel.INFO ) - return redirect(get_logout_url(unquote_plus(get_login_url( + return redirect(get_logout_url(get_login_url( service_url, username=user.username, verification_key=user.verification_key - )))) + ))) # if user is authenticated by CAS print_cas_log(f'CAS response - finalizing authentication: user=[{user._id}]', LogLevel.INFO) diff --git a/framework/auth/views.py b/framework/auth/views.py index 1b3f5fc425c..e398a6db0a5 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -22,6 +22,7 @@ from framework.celery_tasks.handlers import enqueue_task from framework.exceptions import HTTPError from framework.flask import redirect # VOL-aware redirect +from framework.postcommit_tasks.handlers import enqueue_postcommit_task from framework.sessions.utils import remove_sessions_for_user from framework.sessions import get_session from framework.utils import throttle_period_expired @@ -800,7 +801,6 @@ def unconfirmed_email_add(auth=None): 'removed_email': json_body['address'] }, 200 - def send_confirm_email(user, email, renew=False, external_id_provider=None, external_id=None, destination=None): """ Sends `user` a confirmation to the given `email`. @@ -815,7 +815,6 @@ def send_confirm_email(user, email, renew=False, external_id_provider=None, exte :return: :raises: KeyError if user does not have a confirmation token for the given email. """ - confirmation_url = user.get_confirmation_url( email, external=True, @@ -872,6 +871,9 @@ def send_confirm_email(user, email, renew=False, external_id_provider=None, exte logo=logo if logo else settings.OSF_LOGO ) +def send_confirm_email_async(user, email, renew=False, external_id_provider=None, external_id=None, destination=None): + enqueue_postcommit_task(send_confirm_email, (user, email, renew, external_id_provider, external_id, destination), {}) + def register_user(**kwargs): """ @@ -942,7 +944,7 @@ def register_user(**kwargs): ) if settings.CONFIRM_REGISTRATIONS_BY_EMAIL: - send_confirm_email(user, email=user.username) + send_confirm_email_async(user, email=user.username) message = language.REGISTRATION_SUCCESS.format(email=user.username) return {'message': message} else: @@ -1096,7 +1098,7 @@ def external_login_email_post(): # 2. add unconfirmed email and send confirmation email user.add_unconfirmed_email(clean_email, external_identity=external_identity) user.save() - send_confirm_email( + send_confirm_email_async( user, clean_email, external_id_provider=external_id_provider, @@ -1126,7 +1128,7 @@ def external_login_email_post(): # TODO: [#OSF-6934] update social fields, verified social fields cannot be modified user.save() # 3. send confirmation email - send_confirm_email( + send_confirm_email_async( user, user.username, external_id_provider=external_id_provider, diff --git a/osf/management/commands/daily_reporters_go.py b/osf/management/commands/daily_reporters_go.py index d45f02fe54b..5c62e6fbaa6 100644 --- a/osf/management/commands/daily_reporters_go.py +++ b/osf/management/commands/daily_reporters_go.py @@ -2,11 +2,11 @@ import logging from django.core.management.base import BaseCommand +from django.db.utils import OperationalError from django.utils import timezone -from framework import sentry from framework.celery_tasks import app as celery_app -from osf.metrics.reporters import DAILY_REPORTERS +from osf.metrics.reporters import AllDailyReporters from website.app import init_app @@ -14,44 +14,39 @@ @celery_app.task(name='management.commands.daily_reporters_go') -def daily_reporters_go(also_send_to_keen=False, report_date=None, reporter_filter=None): +def daily_reporters_go(report_date=None, reporter_filter=None, **kwargs): init_app() # OSF-specific setup if report_date is None: # default to yesterday report_date = (timezone.now() - datetime.timedelta(days=1)).date() - errors = {} - for reporter_class in DAILY_REPORTERS: - if reporter_filter and (reporter_filter.lower() not in reporter_class.__name__.lower()): + for _reporter_key, _reporter_class in AllDailyReporters.__members__.items(): + if reporter_filter and (reporter_filter.lower() not in _reporter_class.__name__.lower()): continue - try: - reporter_class().run_and_record_for_date( - report_date=report_date, - also_send_to_keen=also_send_to_keen, - ) - except Exception as e: - errors[reporter_class.__name__] = repr(e) - logger.exception(e) - sentry.log_exception(e) - # continue with the next reporter - return errors + daily_reporter_go.apply_async(kwargs={ + 'reporter_key': _reporter_key, + 'report_date': report_date.isoformat(), + }) -def date_fromisoformat(date_str): - return datetime.datetime.strptime(date_str, '%Y-%m-%d').date() +@celery_app.task( + name='management.commands.daily_reporter_go', + autoretry_for=(OperationalError,), + max_retries=5, + retry_backoff=True, + bind=True, +) +def daily_reporter_go(task, reporter_key: str, report_date: str): + _reporter_class = AllDailyReporters[reporter_key].value + _parsed_date = datetime.date.fromisoformat(report_date) + _reporter_class().run_and_record_for_date(report_date=_parsed_date) class Command(BaseCommand): def add_arguments(self, parser): - parser.add_argument( - '--keen', - type=bool, - default=False, - help='also send reports to keen', - ) parser.add_argument( '--date', - type=date_fromisoformat, # in python 3.7+, could pass datetime.date.fromisoformat + type=datetime.date.fromisoformat, help='run for a specific date (default: yesterday)', ) parser.add_argument( @@ -62,7 +57,6 @@ def add_arguments(self, parser): def handle(self, *args, **options): errors = daily_reporters_go( report_date=options.get('date'), - also_send_to_keen=options['keen'], reporter_filter=options.get('filter'), ) for error_key, error_val in errors.items(): diff --git a/osf/management/commands/force_archive.py b/osf/management/commands/force_archive.py index b844f520526..725d53172fa 100644 --- a/osf/management/commands/force_archive.py +++ b/osf/management/commands/force_archive.py @@ -58,10 +58,13 @@ # Ignorable NodeLogs LOG_WHITELIST = { 'affiliated_institution_added', + 'category_updated', 'comment_added', 'comment_removed', 'comment_restored', 'comment_updated', + 'confirm_ham', + 'confirm_spam', 'contributor_added', 'contributor_removed', 'contributors_reordered', @@ -72,7 +75,10 @@ 'embargo_completed', 'embargo_initiated', 'embargo_terminated', + 'external_ids_added', 'file_tag_added', + 'flag_spam', + 'guid_metadata_updated', 'license_changed', 'made_contributor_invisible', 'made_private', @@ -80,6 +86,8 @@ 'made_wiki_private', 'made_wiki_public', 'node_removed', + 'node_access_requests_disabled', + 'node_access_requests_enabled', 'permissions_updated', 'pointer_created', 'pointer_removed', @@ -92,6 +100,7 @@ 'registration_initiated', 'retraction_approved', 'retraction_initiated', + 'subjects_updated', 'tag_added', 'tag_removed', 'wiki_deleted', diff --git a/osf/management/commands/monthly_reporters_go.py b/osf/management/commands/monthly_reporters_go.py index 74bd69da6ab..8f9854a722b 100644 --- a/osf/management/commands/monthly_reporters_go.py +++ b/osf/management/commands/monthly_reporters_go.py @@ -1,11 +1,11 @@ import logging from django.core.management.base import BaseCommand +from django.db.utils import OperationalError from django.utils import timezone -from framework import sentry from framework.celery_tasks import app as celery_app -from osf.metrics.reporters import MONTHLY_REPORTERS +from osf.metrics.reporters import AllMonthlyReporters from osf.metrics.utils import YearMonth from website.app import init_app @@ -28,17 +28,24 @@ def monthly_reporters_go(report_year=None, report_month=None): year=today.year if today.month > 1 else today.year - 1, month=today.month - 1 or MAXMONTH, ) + for _reporter_key in AllMonthlyReporters.__members__.keys(): + monthly_reporter_go.apply_async(kwargs={ + 'reporter_key': _reporter_key, + 'yearmonth': str(report_yearmonth), + }) - errors = {} - for reporter_class in MONTHLY_REPORTERS: - try: - reporter_class().run_and_record_for_month(report_yearmonth) - except Exception as e: - errors[reporter_class.__name__] = str(e) - logger.exception(e) - sentry.log_exception(e) - # continue with the next reporter - return errors + +@celery_app.task( + name='management.commands.monthly_reporter_go', + autoretry_for=(OperationalError,), + max_retries=5, + retry_backoff=True, + bind=True, +) +def monthly_reporter_go(task, reporter_key: str, yearmonth: str): + _reporter_class = AllMonthlyReporters[reporter_key].value + _parsed_yearmonth = YearMonth.from_str(yearmonth) + _reporter_class().run_and_record_for_month(_parsed_yearmonth) class Command(BaseCommand): diff --git a/osf/metrics/reporters/__init__.py b/osf/metrics/reporters/__init__.py index b7a0f5e5363..1f8e0fba862 100644 --- a/osf/metrics/reporters/__init__.py +++ b/osf/metrics/reporters/__init__.py @@ -1,3 +1,5 @@ +import enum + # from .active_users import ActiveUserReporter from .storage_addon_usage import StorageAddonUsageReporter from .download_count import DownloadCountReporter @@ -10,18 +12,17 @@ from .spam_count import SpamCountReporter -DAILY_REPORTERS = ( - # ActiveUserReporter, - DownloadCountReporter, - InstitutionSummaryReporter, - NewUserDomainReporter, - NodeCountReporter, - OsfstorageFileCountReporter, - PreprintCountReporter, - StorageAddonUsageReporter, - UserCountReporter, -) +class AllDailyReporters(enum.Enum): + # ACTIVE_USER = ActiveUserReporter + DOWNLOAD_COUNT = DownloadCountReporter + INSTITUTION_SUMMARY = InstitutionSummaryReporter + NEW_USER_DOMAIN = NewUserDomainReporter + NODE_COUNT = NodeCountReporter + OSFSTORAGE_FILE_COUNT = OsfstorageFileCountReporter + PREPRINT_COUNT = PreprintCountReporter + STORAGE_ADDON_USAGE = StorageAddonUsageReporter + USER_COUNT = UserCountReporter + -MONTHLY_REPORTERS = ( - SpamCountReporter, -) +class AllMonthlyReporters(enum.Enum): + SPAM_COUNT = SpamCountReporter diff --git a/osf/metrics/reporters/_base.py b/osf/metrics/reporters/_base.py index 94d35bbaad2..d3bf1722523 100644 --- a/osf/metrics/reporters/_base.py +++ b/osf/metrics/reporters/_base.py @@ -1,12 +1,6 @@ -from collections import defaultdict -from datetime import datetime import logging -import pytz - -from keen.client import KeenClient from osf.metrics.utils import YearMonth -from website.settings import KEEN as keen_settings logger = logging.getLogger(__name__) @@ -33,49 +27,10 @@ def report(self, report_date): """ raise NotImplementedError(f'{self.__name__} must implement `report`') - def keen_events_from_report(self, report): - """given one of this reporter's own reports, build equivalent keen events - (for back-compat; to be deleted once we don't need keen anymore) - - return a mapping from keen collection name to iterable of events - e.g. {'my_keen_collection': [event1, event2, ...]} - """ - raise NotImplementedError(f'{self.__name__} should probably implement keen_events_from_report') - - def run_and_record_for_date(self, report_date, *, also_send_to_keen=False): + def run_and_record_for_date(self, report_date): reports = self.report(report_date) # expecting each reporter to spit out only a handful of reports per day; # not bothering with bulk-create for report in reports: report.save() - - if also_send_to_keen: - self.send_to_keen(reports) - - def send_to_keen(self, reports): - keen_project = keen_settings['private']['project_id'] - write_key = keen_settings['private']['write_key'] - if not (keen_project and write_key): - logger.warning(f'keen not configured; not sending events for {self.__class__.__name__}') - return - - keen_events_by_collection = defaultdict(list) - for report in reports: - keen_event_timestamp = datetime( - report.report_date.year, - report.report_date.month, - report.report_date.day, - tzinfo=pytz.utc, - ) - - for collection_name, keen_events in self.keen_events_from_report(report).items(): - for event in keen_events: - event['keen'] = {'timestamp': keen_event_timestamp.isoformat()} - keen_events_by_collection[collection_name].extend(keen_events) - - client = KeenClient( - project_id=keen_project, - write_key=write_key, - ) - client.add_events(keen_events_by_collection) diff --git a/osf/metrics/reporters/download_count.py b/osf/metrics/reporters/download_count.py index f6ed14df198..f772722dc31 100644 --- a/osf/metrics/reporters/download_count.py +++ b/osf/metrics/reporters/download_count.py @@ -12,11 +12,3 @@ def report(self, date): report_date=date, ), ] - - def keen_events_from_report(self, report): - event = { - 'files': { - 'total': report.daily_file_downloads, - }, - } - return {'download_count_summary': [event]} diff --git a/osf/metrics/reporters/institution_summary.py b/osf/metrics/reporters/institution_summary.py index d51657e83b6..892e337aec4 100644 --- a/osf/metrics/reporters/institution_summary.py +++ b/osf/metrics/reporters/institution_summary.py @@ -93,17 +93,3 @@ def report(self, date): reports.append(report) return reports - - def keen_events_from_report(self, report): - event = { - 'institution': { - 'id': report.institution_id, - 'name': report.institution_name, - }, - 'users': report.users.to_dict(), - 'nodes': report.nodes.to_dict(), - 'projects': report.projects.to_dict(), - 'registered_nodes': report.registered_nodes.to_dict(), - 'registered_projects': report.registered_projects.to_dict(), - } - return {'institution_summary': [event]} diff --git a/osf/metrics/reporters/new_user_domain.py b/osf/metrics/reporters/new_user_domain.py index be28079e331..ec13aad860f 100644 --- a/osf/metrics/reporters/new_user_domain.py +++ b/osf/metrics/reporters/new_user_domain.py @@ -28,12 +28,3 @@ def report(self, date): ) for domain_name, count in domain_names.items() ] - - def keen_events_from_report(self, report): - events = [ - {'domain': report.domain_name, 'date': str(report.report_date)} - for _ in range(report.new_user_count) - ] - return { - 'user_domain_events': events, - } diff --git a/osf/metrics/reporters/node_count.py b/osf/metrics/reporters/node_count.py index d90a23fda0b..0a4120ca1f9 100644 --- a/osf/metrics/reporters/node_count.py +++ b/osf/metrics/reporters/node_count.py @@ -90,12 +90,3 @@ def report(self, date): ) return [report] - - def keen_events_from_report(self, report): - event = { - 'nodes': report.nodes.to_dict(), - 'projects': report.projects.to_dict(), - 'registered_nodes': report.registered_nodes.to_dict(), - 'registered_projects': report.registered_projects.to_dict(), - } - return {'node_summary': [event]} diff --git a/osf/metrics/reporters/osfstorage_file_count.py b/osf/metrics/reporters/osfstorage_file_count.py index 339838dce78..2f35e1e81fd 100644 --- a/osf/metrics/reporters/osfstorage_file_count.py +++ b/osf/metrics/reporters/osfstorage_file_count.py @@ -45,9 +45,3 @@ def report(self, date): ) return [report] - - def keen_events_from_report(self, report): - event = { - 'osfstorage_files_including_quickfiles': report.files.to_dict(), - } - return {'file_summary': [event]} diff --git a/osf/metrics/reporters/preprint_count.py b/osf/metrics/reporters/preprint_count.py index 319f72ae319..23f68bc7736 100644 --- a/osf/metrics/reporters/preprint_count.py +++ b/osf/metrics/reporters/preprint_count.py @@ -58,12 +58,3 @@ def report(self, date): logger.info('{} Preprints counted for the provider {}'.format(resp['hits']['total'], preprint_provider.name)) return reports - - def keen_events_from_report(self, report): - event = { - 'provider': { - 'name': report.provider_key, - 'total': report.preprint_count, - }, - } - return {'preprint_summary': [event]} diff --git a/osf/metrics/reporters/storage_addon_usage.py b/osf/metrics/reporters/storage_addon_usage.py index 242be243b57..704254795f0 100644 --- a/osf/metrics/reporters/storage_addon_usage.py +++ b/osf/metrics/reporters/storage_addon_usage.py @@ -167,23 +167,3 @@ def report(self, date): report_date=date, usage_by_addon=usage_by_addon, )] - - def keen_events_from_report(self, report): - events = [ - { - 'provider': { - 'name': addon_usage.addon_shortname, - }, - 'users': { - 'enabled': addon_usage.enabled_usersettings, - 'linked': addon_usage.linked_usersettings, - }, - 'nodes': { - 'connected': addon_usage.connected_nodesettings, - 'deleted': addon_usage.deleted_nodesettings, - 'disconnected': addon_usage.disconnected_nodesettings - }, - } - for addon_usage in report.usage_by_addon - ] - return {'addon_snapshot': events} diff --git a/osf/metrics/reporters/user_count.py b/osf/metrics/reporters/user_count.py index fc9f3d6df54..e0a61c7bb10 100644 --- a/osf/metrics/reporters/user_count.py +++ b/osf/metrics/reporters/user_count.py @@ -18,16 +18,3 @@ def report(self, report_date): ) return [report] - - def keen_events_from_report(self, report): - event = { - 'status': { - 'active': report.active, - 'deactivated': report.deactivated, - 'merged': report.merged, - 'new_users_daily': report.new_users_daily, - 'new_users_with_institution_daily': report.new_users_with_institution_daily, - 'unconfirmed': report.unconfirmed, - } - } - return {'user_summary': [event]} diff --git a/osf/migrations/0023_preprint_affiliated_institutions.py b/osf/migrations/0023_preprint_affiliated_institutions.py new file mode 100644 index 00000000000..cdfecc03858 --- /dev/null +++ b/osf/migrations/0023_preprint_affiliated_institutions.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.17 on 2024-07-18 19:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('osf', '0022_alter_abstractnode_subjects_alter_abstractnode_tags_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='preprint', + name='affiliated_institutions', + field=models.ManyToManyField(related_name='preprints', to='osf.Institution'), + ), + ] diff --git a/osf/models/mixins.py b/osf/models/mixins.py index 9328ec70b0b..9b8fea15fb0 100644 --- a/osf/models/mixins.py +++ b/osf/models/mixins.py @@ -300,7 +300,7 @@ class AffiliatedInstitutionMixin(models.Model): affiliated_institutions = models.ManyToManyField('Institution', related_name='nodes') - def add_affiliated_institution(self, inst, user, save=False, log=True): + def add_affiliated_institution(self, inst, user, log=True): if not user.is_affiliated_with_institution(inst): raise UserNotAffiliatedError(f'User is not affiliated with {inst.name}') if not self.is_affiliated_with_institution(inst): @@ -1470,22 +1470,29 @@ def add_unregistered_contributor(self, fullname, email, auth, send_email=None, # Create a new user record if you weren't passed an existing user contributor = existing_user if existing_user else OSFUser.create_unregistered(fullname=fullname, email=email) - contributor.add_unclaimed_record(self, referrer=auth.user, - given_name=fullname, email=email) try: - contributor.save() - except ValidationError: # User with same email already exists - contributor = get_user(email=email) - # Unregistered users may have multiple unclaimed records, so - # only raise error if user is registered. - if contributor.is_registered or self.is_contributor(contributor): - raise - contributor.add_unclaimed_record( - self, referrer=auth.user, given_name=fullname, email=email + self, + referrer=auth.user, + given_name=fullname, + email=email, ) - - contributor.save() + except ValidationError as e: + if 'Osf user with this Username already exists.' in e.message_dict.get('username'): + contributor = get_user(email=email) + # Unregistered users may have multiple unclaimed records, so + # only raise error if user is registered. + if contributor.is_registered or self.is_contributor(contributor): + raise + + contributor.add_unclaimed_record( + self, + referrer=auth.user, + given_name=fullname, + email=email, + ) + else: + raise e self.add_contributor( contributor, permissions=permissions, auth=auth, diff --git a/osf/models/node.py b/osf/models/node.py index d73e501b2c4..5fd96c1f0d5 100644 --- a/osf/models/node.py +++ b/osf/models/node.py @@ -701,8 +701,11 @@ def csl(self): # formats node information into CSL format for citation parsing if doi: csl['DOI'] = doi - if self.logs.exists(): - csl['issued'] = datetime_to_csl(self.logs.latest().date) + if self.registered_date: + csl['issued'] = datetime_to_csl(self.registered_date) + else: + if self.logs.exists(): + csl['issued'] = datetime_to_csl(self.logs.latest().date) return csl diff --git a/osf/models/osf_group.py b/osf/models/osf_group.py index f93bff12353..a9a6b3b6f56 100644 --- a/osf/models/osf_group.py +++ b/osf/models/osf_group.py @@ -233,8 +233,12 @@ def add_unregistered_member(self, fullname, email, auth, role=MEMBER): raise ValueError('User already exists.') else: user = OSFUser.create_unregistered(fullname=fullname, email=email) - user.add_unclaimed_record(self, referrer=auth.user, given_name=fullname, email=email) - user.save() + user.add_unclaimed_record( + self, + referrer=auth.user, + given_name=fullname, + email=email, + ) if role == MANAGER: self.make_manager(user, auth=auth) diff --git a/osf/models/preprint.py b/osf/models/preprint.py index d54eb000b03..467938e68af 100644 --- a/osf/models/preprint.py +++ b/osf/models/preprint.py @@ -25,7 +25,7 @@ from .provider import PreprintProvider from .preprintlog import PreprintLog from .contributor import PreprintContributor -from .mixins import ReviewableMixin, Taggable, Loggable, GuardianMixin +from .mixins import ReviewableMixin, Taggable, Loggable, GuardianMixin, AffiliatedInstitutionMixin from .validators import validate_doi from osf.utils.fields import NonNaiveDateTimeField from osf.utils.workflows import DefaultStates, ReviewStates @@ -108,7 +108,7 @@ def can_view(self, base_queryset=None, user=None, allow_contribs=True, public_on class Preprint(DirtyFieldsMixin, GuidMixin, IdentifierMixin, ReviewableMixin, BaseModel, TitleMixin, DescriptionMixin, - Loggable, Taggable, ContributorMixin, GuardianMixin, SpamOverrideMixin, TaxonomizableMixin): + Loggable, Taggable, ContributorMixin, GuardianMixin, SpamOverrideMixin, TaxonomizableMixin, AffiliatedInstitutionMixin): objects = PreprintManager() # Preprint fields that trigger a check to the spam filter on save @@ -141,6 +141,9 @@ class Preprint(DirtyFieldsMixin, GuidMixin, IdentifierMixin, ReviewableMixin, Ba ('not_applicable', 'Not applicable') ] + # overrides AffiliatedInstitutionMixin + affiliated_institutions = models.ManyToManyField('Institution', related_name='preprints') + provider = models.ForeignKey('osf.PreprintProvider', on_delete=models.SET_NULL, related_name='preprints', @@ -430,6 +433,10 @@ def display_absolute_url(self): def linked_nodes_self_url(self): return self.absolute_api_v2_url + 'relationships/node/' + @property + def institutions_relationship_url(self): + return self.absolute_api_v2_url + 'relationships/institutions/' + @property def admin_contributor_or_group_member_ids(self): # Overrides ContributorMixin diff --git a/osf/models/preprintlog.py b/osf/models/preprintlog.py index 2abbadec3d2..b846d851f7f 100644 --- a/osf/models/preprintlog.py +++ b/osf/models/preprintlog.py @@ -57,6 +57,8 @@ class PreprintLog(ObjectIDMixin, BaseModel): CONFIRM_HAM = 'confirm_ham' FLAG_SPAM = 'flag_spam' CONFIRM_SPAM = 'confirm_spam' + AFFILIATED_INSTITUTION_ADDED = 'affiliated_institution_added' + AFFILIATED_INSTITUTION_REMOVED = 'affiliated_institution_removed' actions = ([ DELETED, diff --git a/osf/models/spam.py b/osf/models/spam.py index c6d0a438f5c..993039b1fcf 100644 --- a/osf/models/spam.py +++ b/osf/models/spam.py @@ -200,7 +200,6 @@ def do_check_spam(self, author, author_email, content, request_headers): 'user_agent': request_headers.get('User-Agent'), 'referer': request_headers.get('Referer'), } - request_kwargs.update(request_headers) check_resource_for_domains_postcommit( self.guids.first()._id, diff --git a/osf/models/user.py b/osf/models/user.py index 103770c28df..4626beb3f60 100644 --- a/osf/models/user.py +++ b/osf/models/user.py @@ -59,7 +59,7 @@ from website import settings as website_settings from website import filters, mails from website.project import new_bookmark_collection -from website.util.metrics import OsfSourceTags +from website.util.metrics import OsfSourceTags, unregistered_created_source_tag from importlib import import_module from osf.utils.requests import get_headers_from_request @@ -1666,6 +1666,10 @@ def add_unclaimed_record(self, claim_origin, referrer, given_name, email=None): 'email': clean_email, } self.unclaimed_records[pid] = record + + self.save() # must save for PK to add system tags + self.add_system_tag(unregistered_created_source_tag(referrer_id)) + return record def get_unclaimed_record(self, project_id): diff --git a/osf/utils/notifications.py b/osf/utils/notifications.py index 004d0c57d30..92ea38fcf70 100644 --- a/osf/utils/notifications.py +++ b/osf/utils/notifications.py @@ -43,13 +43,19 @@ def notify_submit(resource, user, *args, **kwargs): ) -def notify_resubmit(resource, user, action, *args, **kwargs): +def notify_resubmit(resource, user, *args, **kwargs): context = get_email_template_context(resource) - reviews_signals.reviews_email.send( - creator=user, + context['referrer'] = user + context['resubmission'] = True + recipients = list(resource.contributors) + reviews_signals.reviews_email_submit.send( + recipients=recipients, context=context, - template='reviews_resubmission_confirmation', - action=action + template=mails.REVIEWS_RESUBMISSION_CONFIRMATION, + ) + reviews_signals.reviews_email_submit_moderators_notifications.send( + timestamp=timezone.now(), + context=context ) diff --git a/osf_tests/management_commands/test_reindex_es6.py b/osf_tests/management_commands/test_reindex_es6.py index 2e881b8f088..1031ba1f782 100644 --- a/osf_tests/management_commands/test_reindex_es6.py +++ b/osf_tests/management_commands/test_reindex_es6.py @@ -46,7 +46,7 @@ def url(self): return f'{settings.API_DOMAIN}_/metrics/preprints/downloads/' @pytest.mark.es - @pytest.mark.skipif(django_settings.TRAVIS_ENV, reason='Non-deterministic fails on travis') + @pytest.mark.skipif(django_settings.CI_ENV, reason='Non-deterministic fails on CI') def test_reindexing(self, app, url, preprint, user, admin, es6_client): preprint_download = PreprintDownload.record_for_preprint( preprint, diff --git a/osf_tests/metadata/test_osf_gathering.py b/osf_tests/metadata/test_osf_gathering.py index 52289367ccc..7bd72770aba 100644 --- a/osf_tests/metadata/test_osf_gathering.py +++ b/osf_tests/metadata/test_osf_gathering.py @@ -538,6 +538,7 @@ def test_gather_affiliated_institutions(self): institution_iri = URIRef(institution.ror_uri) self.user__admin.add_or_update_affiliated_institution(institution) self.project.add_affiliated_institution(institution, self.user__admin) + self.preprint.add_affiliated_institution(institution, self.user__admin) assert_triples(osf_gathering.gather_affiliated_institutions(self.projectfocus), { (self.projectfocus.iri, OSF.affiliation, institution_iri), (institution_iri, RDF.type, DCTERMS.Agent), @@ -559,6 +560,15 @@ def test_gather_affiliated_institutions(self): assert_triples(osf_gathering.gather_affiliated_institutions(self.registrationfocus), set()) # focus: file assert_triples(osf_gathering.gather_affiliated_institutions(self.filefocus), set()) + # focus: preprint + assert_triples(osf_gathering.gather_affiliated_institutions(self.preprintfocus), { + (self.preprintfocus.iri, OSF.affiliation, institution_iri), + (institution_iri, RDF.type, DCTERMS.Agent), + (institution_iri, RDF.type, FOAF.Organization), + (institution_iri, FOAF.name, Literal(institution.name)), + (institution_iri, DCTERMS.identifier, Literal(institution.identifier_domain)), + (institution_iri, DCTERMS.identifier, Literal(institution.ror_uri)), + }) def test_gather_funding(self): # focus: project diff --git a/osf_tests/test_institutional_affiliation.py b/osf_tests/test_institutional_affiliation.py new file mode 100644 index 00000000000..86bb6cebff6 --- /dev/null +++ b/osf_tests/test_institutional_affiliation.py @@ -0,0 +1,55 @@ +import pytest +from osf_tests.factories import ( + PreprintFactory, + UserFactory, + InstitutionFactory, +) +from osf.exceptions import UserNotAffiliatedError + + +@pytest.mark.django_db +class TestPreprintInstitutionalAffiliation: + """ + Tests for preprint model to handle updating InstitutionalAffiliationMixin + """ + + @pytest.fixture() + def institution(self): + return InstitutionFactory() + + @pytest.fixture() + def user(self, institution): + user = UserFactory() + user.add_or_update_affiliated_institution(institution) + return user + + @pytest.fixture() + def user_without_affiliation(self): + return UserFactory() + + @pytest.fixture() + def preprint(self, user): + preprint = PreprintFactory() + preprint.add_permission(user, 'admin') + return preprint + + def test_remove_nonexistent_affiliation(self, preprint, institution, user): + assert not preprint.remove_affiliated_institution(institution, user) + + def test_add_affiliated_institution_unaffiliated_user(self, preprint, institution, user_without_affiliation): + with pytest.raises(UserNotAffiliatedError): + preprint.add_affiliated_institution(institution, user_without_affiliation) + + assert not preprint.is_affiliated_with_institution(institution) + + def test_add_and_remove_affiliated_institution(self, preprint, institution, user): + preprint.add_affiliated_institution(institution, user) + assert preprint.is_affiliated_with_institution(institution) + + was_removed = preprint.remove_affiliated_institution(institution, user) + assert was_removed + assert not preprint.is_affiliated_with_institution(institution) + + def test_permission_errors_during_affiliation_update(self, preprint, institution, user_without_affiliation): + with pytest.raises(UserNotAffiliatedError): + preprint.add_affiliated_institution(institution, user_without_affiliation) diff --git a/osf_tests/test_reviewable.py b/osf_tests/test_reviewable.py index 6ded16a6df3..1d25ca4adac 100644 --- a/osf_tests/test_reviewable.py +++ b/osf_tests/test_reviewable.py @@ -4,6 +4,8 @@ from osf.models import Preprint from osf.utils.workflows import DefaultStates from osf_tests.factories import PreprintFactory, AuthUserFactory +from website import mails + @pytest.mark.django_db class TestReviewable: @@ -31,3 +33,29 @@ def test_state_changes(self, _): assert preprint.machine_state == DefaultStates.ACCEPTED.value from_db.refresh_from_db() assert from_db.machine_state == DefaultStates.ACCEPTED.value + + @mock.patch('website.reviews.listeners.mails.send_mail') + def test_reject_resubmission_sends_emails(self, send_mail): + user = AuthUserFactory() + preprint = PreprintFactory( + reviews_workflow='pre-moderation', + is_published=False + ) + assert preprint.machine_state == DefaultStates.INITIAL.value + assert not send_mail.call_count + + preprint.run_submit(user) + assert send_mail.call_count == 1 + assert preprint.machine_state == DefaultStates.PENDING.value + mail_template = send_mail.call_args[0][1] + assert mail_template == mails.REVIEWS_SUBMISSION_CONFIRMATION + + assert not user.notification_subscriptions.exists() + preprint.run_reject(user, 'comment') + assert preprint.machine_state == DefaultStates.REJECTED.value + + preprint.run_submit(user) # Resubmission alerts users and moderators + assert preprint.machine_state == DefaultStates.PENDING.value + mail_template = send_mail.call_args[0][1] + assert send_mail.call_count == 2 + assert mail_template == mails.REVIEWS_RESUBMISSION_CONFIRMATION diff --git a/osf_tests/test_user.py b/osf_tests/test_user.py index 46b89f46343..c5774823ccd 100644 --- a/osf_tests/test_user.py +++ b/osf_tests/test_user.py @@ -1189,6 +1189,7 @@ def test_add_unclaimed_record(self, unreg_user, unreg_moderator, email, referrer assert 'token' in data assert data['email'] == email assert data == unreg_user.get_unclaimed_record(project._primary_key) + assert f'source:unregistered_created|{referrer._id}' in unreg_user.system_tags # test_unreg_moderator data = unreg_moderator.unclaimed_records[provider._id] @@ -1197,6 +1198,7 @@ def test_add_unclaimed_record(self, unreg_user, unreg_moderator, email, referrer assert 'token' in data assert data['email'] == email assert data == unreg_moderator.get_unclaimed_record(provider._id) + assert f'source:unregistered_created|{referrer._id}' in unreg_user.system_tags def test_get_claim_url(self, unreg_user, unreg_moderator, project, provider): # test_unreg_contrib diff --git a/package.json b/package.json index e558bdbb72c..be5c3b44a30 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "OSF", - "version": "24.05.0", + "version": "24.07.0", "description": "Facilitating Open Science", "repository": "https://github.com/CenterForOpenScience/osf.io", "author": "Center for Open Science", diff --git a/poetry.lock b/poetry.lock index df2d3af5e08..8f0067d8d41 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,98 +2,113 @@ [[package]] name = "aiohappyeyeballs" -version = "2.3.5" +version = "2.4.0" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "aiohappyeyeballs-2.3.5-py3-none-any.whl", hash = "sha256:4d6dea59215537dbc746e93e779caea8178c866856a721c9c660d7a5a7b8be03"}, - {file = "aiohappyeyeballs-2.3.5.tar.gz", hash = "sha256:6fa48b9f1317254f122a07a131a86b71ca6946ca989ce6326fff54a99a920105"}, + {file = "aiohappyeyeballs-2.4.0-py3-none-any.whl", hash = "sha256:7ce92076e249169a13c2f49320d1967425eaf1f407522d707d59cac7628d62bd"}, + {file = "aiohappyeyeballs-2.4.0.tar.gz", hash = "sha256:55a1714f084e63d49639800f95716da97a1f173d46a16dfcfda0016abb93b6b2"}, ] [[package]] name = "aiohttp" -version = "3.10.1" +version = "3.10.5" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ - {file = "aiohttp-3.10.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:47b4c2412960e64d97258f40616efddaebcb34ff664c8a972119ed38fac2a62c"}, - {file = "aiohttp-3.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7dbf637f87dd315fa1f36aaed8afa929ee2c607454fb7791e74c88a0d94da59"}, - {file = "aiohttp-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c8fb76214b5b739ce59e2236a6489d9dc3483649cfd6f563dbf5d8e40dbdd57d"}, - {file = "aiohttp-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c577cdcf8f92862363b3d598d971c6a84ed8f0bf824d4cc1ce70c2fb02acb4a"}, - {file = "aiohttp-3.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:777e23609899cb230ad2642b4bdf1008890f84968be78de29099a8a86f10b261"}, - {file = "aiohttp-3.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b07286a1090483799599a2f72f76ac396993da31f6e08efedb59f40876c144fa"}, - {file = "aiohttp-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9db600a86414a9a653e3c1c7f6a2f6a1894ab8f83d11505247bd1b90ad57157"}, - {file = "aiohttp-3.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c3f1eb280008e51965a8d160a108c333136f4a39d46f516c64d2aa2e6a53f2"}, - {file = "aiohttp-3.10.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f5dd109a925fee4c9ac3f6a094900461a2712df41745f5d04782ebcbe6479ccb"}, - {file = "aiohttp-3.10.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8c81ff4afffef9b1186639506d70ea90888218f5ddfff03870e74ec80bb59970"}, - {file = "aiohttp-3.10.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:2a384dfbe8bfebd203b778a30a712886d147c61943675f4719b56725a8bbe803"}, - {file = "aiohttp-3.10.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b9fb6508893dc31cfcbb8191ef35abd79751db1d6871b3e2caee83959b4d91eb"}, - {file = "aiohttp-3.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:88596384c3bec644a96ae46287bb646d6a23fa6014afe3799156aef42669c6bd"}, - {file = "aiohttp-3.10.1-cp310-cp310-win32.whl", hash = "sha256:68164d43c580c2e8bf8e0eb4960142919d304052ccab92be10250a3a33b53268"}, - {file = "aiohttp-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:d6bbe2c90c10382ca96df33b56e2060404a4f0f88673e1e84b44c8952517e5f3"}, - {file = "aiohttp-3.10.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f6979b4f20d3e557a867da9d9227de4c156fcdcb348a5848e3e6190fd7feb972"}, - {file = "aiohttp-3.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03c0c380c83f8a8d4416224aafb88d378376d6f4cadebb56b060688251055cd4"}, - {file = "aiohttp-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c2b104e81b3c3deba7e6f5bc1a9a0e9161c380530479970766a6655b8b77c7c"}, - {file = "aiohttp-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b023b68c61ab0cd48bd38416b421464a62c381e32b9dc7b4bdfa2905807452a4"}, - {file = "aiohttp-3.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a07c76a82390506ca0eabf57c0540cf5a60c993c442928fe4928472c4c6e5e6"}, - {file = "aiohttp-3.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:41d8dab8c64ded1edf117d2a64f353efa096c52b853ef461aebd49abae979f16"}, - {file = "aiohttp-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:615348fab1a9ef7d0960a905e83ad39051ae9cb0d2837da739b5d3a7671e497a"}, - {file = "aiohttp-3.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:256ee6044214ee9d66d531bb374f065ee94e60667d6bbeaa25ca111fc3997158"}, - {file = "aiohttp-3.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7d5bb926805022508b7ddeaad957f1fce7a8d77532068d7bdb431056dc630cd"}, - {file = "aiohttp-3.10.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:028faf71b338f069077af6315ad54281612705d68889f5d914318cbc2aab0d50"}, - {file = "aiohttp-3.10.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:5c12310d153b27aa630750be44e79313acc4e864c421eb7d2bc6fa3429c41bf8"}, - {file = "aiohttp-3.10.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:de1a91d5faded9054957ed0a9e01b9d632109341942fc123947ced358c5d9009"}, - {file = "aiohttp-3.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9c186b270979fb1dee3ababe2d12fb243ed7da08b30abc83ebac3a928a4ddb15"}, - {file = "aiohttp-3.10.1-cp311-cp311-win32.whl", hash = "sha256:4a9ce70f5e00380377aac0e568abd075266ff992be2e271765f7b35d228a990c"}, - {file = "aiohttp-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:a77c79bac8d908d839d32c212aef2354d2246eb9deb3e2cb01ffa83fb7a6ea5d"}, - {file = "aiohttp-3.10.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:2212296cdb63b092e295c3e4b4b442e7b7eb41e8a30d0f53c16d5962efed395d"}, - {file = "aiohttp-3.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4dcb127ca3eb0a61205818a606393cbb60d93b7afb9accd2fd1e9081cc533144"}, - {file = "aiohttp-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb8b79a65332e1a426ccb6290ce0409e1dc16b4daac1cc5761e059127fa3d134"}, - {file = "aiohttp-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68cc24f707ed9cb961f6ee04020ca01de2c89b2811f3cf3361dc7c96a14bfbcc"}, - {file = "aiohttp-3.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cb54f5725b4b37af12edf6c9e834df59258c82c15a244daa521a065fbb11717"}, - {file = "aiohttp-3.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:51d03e948e53b3639ce4d438f3d1d8202898ec6655cadcc09ec99229d4adc2a9"}, - {file = "aiohttp-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:786299d719eb5d868f161aeec56d589396b053925b7e0ce36e983d30d0a3e55c"}, - {file = "aiohttp-3.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abda4009a30d51d3f06f36bc7411a62b3e647fa6cc935ef667e3e3d3a7dd09b1"}, - {file = "aiohttp-3.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:67f7639424c313125213954e93a6229d3a1d386855d70c292a12628f600c7150"}, - {file = "aiohttp-3.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e5a26d7aac4c0d8414a347da162696eea0629fdce939ada6aedf951abb1d745"}, - {file = "aiohttp-3.10.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:120548d89f14b76a041088b582454d89389370632ee12bf39d919cc5c561d1ca"}, - {file = "aiohttp-3.10.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f5293726943bdcea24715b121d8c4ae12581441d22623b0e6ab12d07ce85f9c4"}, - {file = "aiohttp-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1f8605e573ed6c44ec689d94544b2c4bb1390aaa723a8b5a2cc0a5a485987a68"}, - {file = "aiohttp-3.10.1-cp312-cp312-win32.whl", hash = "sha256:e7168782621be4448d90169a60c8b37e9b0926b3b79b6097bc180c0a8a119e73"}, - {file = "aiohttp-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fbf8c0ded367c5c8eaf585f85ca8dd85ff4d5b73fb8fe1e6ac9e1b5e62e11f7"}, - {file = "aiohttp-3.10.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:54b7f4a20d7cc6bfa4438abbde069d417bb7a119f870975f78a2b99890226d55"}, - {file = "aiohttp-3.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2fa643ca990323db68911b92f3f7a0ca9ae300ae340d0235de87c523601e58d9"}, - {file = "aiohttp-3.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8311d0d690487359fe2247ec5d2cac9946e70d50dced8c01ce9e72341c21151"}, - {file = "aiohttp-3.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222821c60b8f6a64c5908cb43d69c0ee978a1188f6a8433d4757d39231b42cdb"}, - {file = "aiohttp-3.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7b55d9ede66af7feb6de87ff277e0ccf6d51c7db74cc39337fe3a0e31b5872d"}, - {file = "aiohttp-3.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a95151a5567b3b00368e99e9c5334a919514f60888a6b6d2054fea5e66e527e"}, - {file = "aiohttp-3.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9e9171d2fe6bfd9d3838a6fe63b1e91b55e0bf726c16edf265536e4eafed19"}, - {file = "aiohttp-3.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a57e73f9523e980f6101dc9a83adcd7ac0006ea8bf7937ca3870391c7bb4f8ff"}, - {file = "aiohttp-3.10.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0df51a3d70a2bfbb9c921619f68d6d02591f24f10e9c76de6f3388c89ed01de6"}, - {file = "aiohttp-3.10.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:b0de63ff0307eac3961b4af74382d30220d4813f36b7aaaf57f063a1243b4214"}, - {file = "aiohttp-3.10.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8db9b749f589b5af8e4993623dbda6716b2b7a5fcb0fa2277bf3ce4b278c7059"}, - {file = "aiohttp-3.10.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6b14c19172eb53b63931d3e62a9749d6519f7c121149493e6eefca055fcdb352"}, - {file = "aiohttp-3.10.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cd57ad998e3038aa87c38fe85c99ed728001bf5dde8eca121cadee06ee3f637"}, - {file = "aiohttp-3.10.1-cp38-cp38-win32.whl", hash = "sha256:df31641e3f02b77eb3c5fb63c0508bee0fc067cf153da0e002ebbb0db0b6d91a"}, - {file = "aiohttp-3.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:93094eba50bc2ad4c40ff4997ead1fdcd41536116f2e7d6cfec9596a8ecb3615"}, - {file = "aiohttp-3.10.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:440954ddc6b77257e67170d57b1026aa9545275c33312357472504eef7b4cc0b"}, - {file = "aiohttp-3.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f9f8beed277488a52ee2b459b23c4135e54d6a819eaba2e120e57311015b58e9"}, - {file = "aiohttp-3.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8a8221a63602008550022aa3a4152ca357e1dde7ab3dd1da7e1925050b56863"}, - {file = "aiohttp-3.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a702bd3663b5cbf3916e84bf332400d24cdb18399f0877ca6b313ce6c08bfb43"}, - {file = "aiohttp-3.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1988b370536eb14f0ce7f3a4a5b422ab64c4e255b3f5d7752c5f583dc8c967fc"}, - {file = "aiohttp-3.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ccf1f0a304352c891d124ac1a9dea59b14b2abed1704aaa7689fc90ef9c5be1"}, - {file = "aiohttp-3.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc3ea6ef2a83edad84bbdb5d96e22f587b67c68922cd7b6f9d8f24865e655bcf"}, - {file = "aiohttp-3.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b47c125ab07f0831803b88aeb12b04c564d5f07a1c1a225d4eb4d2f26e8b5e"}, - {file = "aiohttp-3.10.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:21778552ef3d44aac3278cc6f6d13a6423504fa5f09f2df34bfe489ed9ded7f5"}, - {file = "aiohttp-3.10.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bde0693073fd5e542e46ea100aa6c1a5d36282dbdbad85b1c3365d5421490a92"}, - {file = "aiohttp-3.10.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:bf66149bb348d8e713f3a8e0b4f5b952094c2948c408e1cfef03b49e86745d60"}, - {file = "aiohttp-3.10.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:587237571a85716d6f71f60d103416c9df7d5acb55d96d3d3ced65f39bff9c0c"}, - {file = "aiohttp-3.10.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:bfe33cba6e127d0b5b417623c9aa621f0a69f304742acdca929a9fdab4593693"}, - {file = "aiohttp-3.10.1-cp39-cp39-win32.whl", hash = "sha256:9fbff00646cf8211b330690eb2fd64b23e1ce5b63a342436c1d1d6951d53d8dd"}, - {file = "aiohttp-3.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:5951c328f9ac42d7bce7a6ded535879bc9ae13032818d036749631fa27777905"}, - {file = "aiohttp-3.10.1.tar.gz", hash = "sha256:8b0d058e4e425d3b45e8ec70d49b402f4d6b21041e674798b1f91ba027c73f28"}, + {file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:18a01eba2574fb9edd5f6e5fb25f66e6ce061da5dab5db75e13fe1558142e0a3"}, + {file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:94fac7c6e77ccb1ca91e9eb4cb0ac0270b9fb9b289738654120ba8cebb1189c6"}, + {file = "aiohttp-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f1f1c75c395991ce9c94d3e4aa96e5c59c8356a15b1c9231e783865e2772699"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7acae3cf1a2a2361ec4c8e787eaaa86a94171d2417aae53c0cca6ca3118ff6"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94c4381ffba9cc508b37d2e536b418d5ea9cfdc2848b9a7fea6aebad4ec6aac1"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c31ad0c0c507894e3eaa843415841995bf8de4d6b2d24c6e33099f4bc9fc0d4f"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0912b8a8fadeb32ff67a3ed44249448c20148397c1ed905d5dac185b4ca547bb"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d93400c18596b7dc4794d48a63fb361b01a0d8eb39f28800dc900c8fbdaca91"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3c5e0d764a5c9aa5a62d99728c56d455310bcc288a79cab10157b3af426f"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d742c36ed44f2798c8d3f4bc511f479b9ceef2b93f348671184139e7d708042c"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:814375093edae5f1cb31e3407997cf3eacefb9010f96df10d64829362ae2df69"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8224f98be68a84b19f48e0bdc14224b5a71339aff3a27df69989fa47d01296f3"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9a487ef090aea982d748b1b0d74fe7c3950b109df967630a20584f9a99c0683"}, + {file = "aiohttp-3.10.5-cp310-cp310-win32.whl", hash = "sha256:d9ef084e3dc690ad50137cc05831c52b6ca428096e6deb3c43e95827f531d5ef"}, + {file = "aiohttp-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:66bf9234e08fe561dccd62083bf67400bdbf1c67ba9efdc3dac03650e97c6088"}, + {file = "aiohttp-3.10.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c6a4e5e40156d72a40241a25cc226051c0a8d816610097a8e8f517aeacd59a2"}, + {file = "aiohttp-3.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c634a3207a5445be65536d38c13791904fda0748b9eabf908d3fe86a52941cf"}, + {file = "aiohttp-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4aff049b5e629ef9b3e9e617fa6e2dfeda1bf87e01bcfecaf3949af9e210105e"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1942244f00baaacaa8155eca94dbd9e8cc7017deb69b75ef67c78e89fdad3c77"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04a1f2a65ad2f93aa20f9ff9f1b672bf912413e5547f60749fa2ef8a644e061"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f2bfc0032a00405d4af2ba27f3c429e851d04fad1e5ceee4080a1c570476697"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:424ae21498790e12eb759040bbb504e5e280cab64693d14775c54269fd1d2bb7"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:975218eee0e6d24eb336d0328c768ebc5d617609affaca5dbbd6dd1984f16ed0"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4120d7fefa1e2d8fb6f650b11489710091788de554e2b6f8347c7a20ceb003f5"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b90078989ef3fc45cf9221d3859acd1108af7560c52397ff4ace8ad7052a132e"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ba5a8b74c2a8af7d862399cdedce1533642fa727def0b8c3e3e02fcb52dca1b1"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:02594361128f780eecc2a29939d9dfc870e17b45178a867bf61a11b2a4367277"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4fc029e135859f533025bc82047334e24b0d489e75513144f25408ecaf058"}, + {file = "aiohttp-3.10.5-cp311-cp311-win32.whl", hash = "sha256:e1ca1ef5ba129718a8fc827b0867f6aa4e893c56eb00003b7367f8a733a9b072"}, + {file = "aiohttp-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:349ef8a73a7c5665cca65c88ab24abe75447e28aa3bc4c93ea5093474dfdf0ff"}, + {file = "aiohttp-3.10.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:305be5ff2081fa1d283a76113b8df7a14c10d75602a38d9f012935df20731487"}, + {file = "aiohttp-3.10.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3a1c32a19ee6bbde02f1cb189e13a71b321256cc1d431196a9f824050b160d5a"}, + {file = "aiohttp-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61645818edd40cc6f455b851277a21bf420ce347baa0b86eaa41d51ef58ba23d"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c225286f2b13bab5987425558baa5cbdb2bc925b2998038fa028245ef421e75"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ba01ebc6175e1e6b7275c907a3a36be48a2d487549b656aa90c8a910d9f3178"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8eaf44ccbc4e35762683078b72bf293f476561d8b68ec8a64f98cf32811c323e"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c43eb1ab7cbf411b8e387dc169acb31f0ca0d8c09ba63f9eac67829585b44f"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de7a5299827253023c55ea549444e058c0eb496931fa05d693b95140a947cb73"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4790f0e15f00058f7599dab2b206d3049d7ac464dc2e5eae0e93fa18aee9e7bf"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:44b324a6b8376a23e6ba25d368726ee3bc281e6ab306db80b5819999c737d820"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0d277cfb304118079e7044aad0b76685d30ecb86f83a0711fc5fb257ffe832ca"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:54d9ddea424cd19d3ff6128601a4a4d23d54a421f9b4c0fff740505813739a91"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f1c9866ccf48a6df2b06823e6ae80573529f2af3a0992ec4fe75b1a510df8a6"}, + {file = "aiohttp-3.10.5-cp312-cp312-win32.whl", hash = "sha256:dc4826823121783dccc0871e3f405417ac116055bf184ac04c36f98b75aacd12"}, + {file = "aiohttp-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:22c0a23a3b3138a6bf76fc553789cb1a703836da86b0f306b6f0dc1617398abc"}, + {file = "aiohttp-3.10.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7f6b639c36734eaa80a6c152a238242bedcee9b953f23bb887e9102976343092"}, + {file = "aiohttp-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29930bc2921cef955ba39a3ff87d2c4398a0394ae217f41cb02d5c26c8b1b77"}, + {file = "aiohttp-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f489a2c9e6455d87eabf907ac0b7d230a9786be43fbe884ad184ddf9e9c1e385"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:123dd5b16b75b2962d0fff566effb7a065e33cd4538c1692fb31c3bda2bfb972"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b98e698dc34966e5976e10bbca6d26d6724e6bdea853c7c10162a3235aba6e16"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3b9162bab7e42f21243effc822652dc5bb5e8ff42a4eb62fe7782bcbcdfacf6"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1923a5c44061bffd5eebeef58cecf68096e35003907d8201a4d0d6f6e387ccaa"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d55f011da0a843c3d3df2c2cf4e537b8070a419f891c930245f05d329c4b0689"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:afe16a84498441d05e9189a15900640a2d2b5e76cf4efe8cbb088ab4f112ee57"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8112fb501b1e0567a1251a2fd0747baae60a4ab325a871e975b7bb67e59221f"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e72589da4c90337837fdfe2026ae1952c0f4a6e793adbbfbdd40efed7c63599"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4d46c7b4173415d8e583045fbc4daa48b40e31b19ce595b8d92cf639396c15d5"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33e6bc4bab477c772a541f76cd91e11ccb6d2efa2b8d7d7883591dfb523e5987"}, + {file = "aiohttp-3.10.5-cp313-cp313-win32.whl", hash = "sha256:c58c6837a2c2a7cf3133983e64173aec11f9c2cd8e87ec2fdc16ce727bcf1a04"}, + {file = "aiohttp-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:38172a70005252b6893088c0f5e8a47d173df7cc2b2bd88650957eb84fcf5022"}, + {file = "aiohttp-3.10.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f6f18898ace4bcd2d41a122916475344a87f1dfdec626ecde9ee802a711bc569"}, + {file = "aiohttp-3.10.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5ede29d91a40ba22ac1b922ef510aab871652f6c88ef60b9dcdf773c6d32ad7a"}, + {file = "aiohttp-3.10.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:673f988370f5954df96cc31fd99c7312a3af0a97f09e407399f61583f30da9bc"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58718e181c56a3c02d25b09d4115eb02aafe1a732ce5714ab70326d9776457c3"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b38b1570242fbab8d86a84128fb5b5234a2f70c2e32f3070143a6d94bc854cf"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:074d1bff0163e107e97bd48cad9f928fa5a3eb4b9d33366137ffce08a63e37fe"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd31f176429cecbc1ba499d4aba31aaccfea488f418d60376b911269d3b883c5"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7384d0b87d4635ec38db9263e6a3f1eb609e2e06087f0aa7f63b76833737b471"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8989f46f3d7ef79585e98fa991e6ded55d2f48ae56d2c9fa5e491a6e4effb589"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:c83f7a107abb89a227d6c454c613e7606c12a42b9a4ca9c5d7dad25d47c776ae"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cde98f323d6bf161041e7627a5fd763f9fd829bcfcd089804a5fdce7bb6e1b7d"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:676f94c5480d8eefd97c0c7e3953315e4d8c2b71f3b49539beb2aa676c58272f"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2d21ac12dc943c68135ff858c3a989f2194a709e6e10b4c8977d7fcd67dfd511"}, + {file = "aiohttp-3.10.5-cp38-cp38-win32.whl", hash = "sha256:17e997105bd1a260850272bfb50e2a328e029c941c2708170d9d978d5a30ad9a"}, + {file = "aiohttp-3.10.5-cp38-cp38-win_amd64.whl", hash = "sha256:1c19de68896747a2aa6257ae4cf6ef59d73917a36a35ee9d0a6f48cff0f94db8"}, + {file = "aiohttp-3.10.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7e2fe37ac654032db1f3499fe56e77190282534810e2a8e833141a021faaab0e"}, + {file = "aiohttp-3.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5bf3ead3cb66ab990ee2561373b009db5bc0e857549b6c9ba84b20bc462e172"}, + {file = "aiohttp-3.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b2c16a919d936ca87a3c5f0e43af12a89a3ce7ccbce59a2d6784caba945b68b"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad146dae5977c4dd435eb31373b3fe9b0b1bf26858c6fc452bf6af394067e10b"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c5c6fa16412b35999320f5c9690c0f554392dc222c04e559217e0f9ae244b92"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95c4dc6f61d610bc0ee1edc6f29d993f10febfe5b76bb470b486d90bbece6b22"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da452c2c322e9ce0cfef392e469a26d63d42860f829026a63374fde6b5c5876f"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:898715cf566ec2869d5cb4d5fb4be408964704c46c96b4be267442d265390f32"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:391cc3a9c1527e424c6865e087897e766a917f15dddb360174a70467572ac6ce"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:380f926b51b92d02a34119d072f178d80bbda334d1a7e10fa22d467a66e494db"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce91db90dbf37bb6fa0997f26574107e1b9d5ff939315247b7e615baa8ec313b"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9093a81e18c45227eebe4c16124ebf3e0d893830c6aca7cc310bfca8fe59d857"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ee40b40aa753d844162dcc80d0fe256b87cba48ca0054f64e68000453caead11"}, + {file = "aiohttp-3.10.5-cp39-cp39-win32.whl", hash = "sha256:03f2645adbe17f274444953bdea69f8327e9d278d961d85657cb0d06864814c1"}, + {file = "aiohttp-3.10.5-cp39-cp39-win_amd64.whl", hash = "sha256:d17920f18e6ee090bdd3d0bfffd769d9f2cb4c8ffde3eb203777a3895c128862"}, + {file = "aiohttp-3.10.5.tar.gz", hash = "sha256:f071854b47d39591ce9a17981c46790acb30518e2f83dfca8db2dfa091178691"}, ] [package.dependencies] @@ -309,13 +324,13 @@ pyparsing = ">=2.0.3" [[package]] name = "billiard" -version = "4.2.0" +version = "4.2.1" description = "Python multiprocessing fork with improvements and bugfixes" optional = false python-versions = ">=3.7" files = [ - {file = "billiard-4.2.0-py3-none-any.whl", hash = "sha256:07aa978b308f334ff8282bd4a746e681b3513db5c9a514cbdd810cbbdc19714d"}, - {file = "billiard-4.2.0.tar.gz", hash = "sha256:9a3c3184cb275aa17a732f93f65b20c525d3d9f253722d26a82194803ade5a2c"}, + {file = "billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb"}, + {file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"}, ] [[package]] @@ -380,13 +395,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.156" +version = "1.34.162" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.156-py3-none-any.whl", hash = "sha256:c48f8c8996216dfdeeb0aa6d3c0f2c7ae25234766434a2ea3e57bdc08494bdda"}, - {file = "botocore-1.34.156.tar.gz", hash = "sha256:5d1478c41ab9681e660b3322432fe09c4055759c317984b7b8d3af9557ff769a"}, + {file = "botocore-1.34.162-py3-none-any.whl", hash = "sha256:2d918b02db88d27a75b48275e6fb2506e9adaaddbec1ffa6a8a0898b34e769be"}, + {file = "botocore-1.34.162.tar.gz", hash = "sha256:adc23be4fb99ad31961236342b7cbf3c0bfc62532cd02852196032e8c0d682f3"}, ] [package.dependencies] @@ -445,13 +460,13 @@ redis = ["redis (>=2.10.5)"] [[package]] name = "cachetools" -version = "5.4.0" +version = "5.5.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"}, - {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, ] [[package]] @@ -522,78 +537,78 @@ files = [ [[package]] name = "cffi" -version = "1.17.0" +version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ - {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, - {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, - {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, - {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, - {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, - {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, - {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, - {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, - {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, - {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, - {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, - {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, - {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, - {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, - {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, - {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] [package.dependencies] @@ -911,13 +926,13 @@ toml = ["tomli"] [[package]] name = "cron-descriptor" -version = "1.4.3" +version = "1.4.5" description = "A Python library that converts cron expressions into human readable strings." optional = false python-versions = "*" files = [ - {file = "cron_descriptor-1.4.3-py3-none-any.whl", hash = "sha256:a67ba21804983b1427ed7f3e1ec27ee77bf24c652b0430239c268c5ddfbf9dc0"}, - {file = "cron_descriptor-1.4.3.tar.gz", hash = "sha256:7b1a00d7d25d6ae6896c0da4457e790b98cba778398a3d48e341e5e0d33f0488"}, + {file = "cron_descriptor-1.4.5-py3-none-any.whl", hash = "sha256:736b3ae9d1a99bc3dbfc5b55b5e6e7c12031e7ba5de716625772f8b02dcd6013"}, + {file = "cron_descriptor-1.4.5.tar.gz", hash = "sha256:f51ce4ffc1d1f2816939add8524f206c376a42c87a5fca3091ce26725b3b1bca"}, ] [package.extras] @@ -1552,13 +1567,13 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "executing" -version = "2.0.1" +version = "2.1.0" description = "Get the currently executing AST node of a frame, and other information" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, - {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, + {file = "executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf"}, + {file = "executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab"}, ] [package.extras] @@ -1612,19 +1627,19 @@ sgmllib3k = "*" [[package]] name = "filelock" -version = "3.15.4" +version = "3.16.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, - {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] -typing = ["typing-extensions (>=4.8)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "flake8" @@ -1881,13 +1896,13 @@ test = ["betamax (>=0.5.1)", "betamax-matchers (>=0.3.0)", "pytest (>=7.0)", "py [[package]] name = "google-api-core" -version = "2.19.1" +version = "2.20.0" description = "Google API client core library" optional = false python-versions = ">=3.7" files = [ - {file = "google-api-core-2.19.1.tar.gz", hash = "sha256:f4695f1e3650b316a795108a76a1c416e6afb036199d1c1f1f110916df479ffd"}, - {file = "google_api_core-2.19.1-py3-none-any.whl", hash = "sha256:f12a9b8309b5e21d92483bbd47ce2c445861ec7d269ef6784ecc0ea8c1fa6125"}, + {file = "google_api_core-2.20.0-py3-none-any.whl", hash = "sha256:ef0591ef03c30bb83f79b3d0575c3f31219001fc9c5cf37024d08310aeffed8a"}, + {file = "google_api_core-2.20.0.tar.gz", hash = "sha256:f74dff1889ba291a4b76c5079df0711810e2d9da81abfdc99957bc961c1eb28f"}, ] [package.dependencies] @@ -2000,79 +2015,38 @@ protobuf = ["protobuf (<5.0.0dev)"] [[package]] name = "google-crc32c" -version = "1.5.0" +version = "1.6.0" description = "A python wrapper of the C library 'Google CRC32C'" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "google-crc32c-1.5.0.tar.gz", hash = "sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7"}, - {file = "google_crc32c-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13"}, - {file = "google_crc32c-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346"}, - {file = "google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65"}, - {file = "google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b"}, - {file = "google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02"}, - {file = "google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4"}, - {file = "google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e"}, - {file = "google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c"}, - {file = "google_crc32c-1.5.0-cp310-cp310-win32.whl", hash = "sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee"}, - {file = "google_crc32c-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289"}, - {file = "google_crc32c-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273"}, - {file = "google_crc32c-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298"}, - {file = "google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57"}, - {file = "google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438"}, - {file = "google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906"}, - {file = "google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183"}, - {file = "google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd"}, - {file = "google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c"}, - {file = "google_crc32c-1.5.0-cp311-cp311-win32.whl", hash = "sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709"}, - {file = "google_crc32c-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-win32.whl", hash = "sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740"}, - {file = "google_crc32c-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8"}, - {file = "google_crc32c-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a"}, - {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946"}, - {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a"}, - {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d"}, - {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a"}, - {file = "google_crc32c-1.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37"}, - {file = "google_crc32c-1.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894"}, - {file = "google_crc32c-1.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a"}, - {file = "google_crc32c-1.5.0-cp38-cp38-win32.whl", hash = "sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4"}, - {file = "google_crc32c-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c"}, - {file = "google_crc32c-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7"}, - {file = "google_crc32c-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d"}, - {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100"}, - {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9"}, - {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57"}, - {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210"}, - {file = "google_crc32c-1.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd"}, - {file = "google_crc32c-1.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96"}, - {file = "google_crc32c-1.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61"}, - {file = "google_crc32c-1.5.0-cp39-cp39-win32.whl", hash = "sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c"}, - {file = "google_crc32c-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93"}, + {file = "google_crc32c-1.6.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5bcc90b34df28a4b38653c36bb5ada35671ad105c99cfe915fb5bed7ad6924aa"}, + {file = "google_crc32c-1.6.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:d9e9913f7bd69e093b81da4535ce27af842e7bf371cde42d1ae9e9bd382dc0e9"}, + {file = "google_crc32c-1.6.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a184243544811e4a50d345838a883733461e67578959ac59964e43cca2c791e7"}, + {file = "google_crc32c-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:236c87a46cdf06384f614e9092b82c05f81bd34b80248021f729396a78e55d7e"}, + {file = "google_crc32c-1.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebab974b1687509e5c973b5c4b8b146683e101e102e17a86bd196ecaa4d099fc"}, + {file = "google_crc32c-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:50cf2a96da226dcbff8671233ecf37bf6e95de98b2a2ebadbfdf455e6d05df42"}, + {file = "google_crc32c-1.6.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f7a1fc29803712f80879b0806cb83ab24ce62fc8daf0569f2204a0cfd7f68ed4"}, + {file = "google_crc32c-1.6.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:40b05ab32a5067525670880eb5d169529089a26fe35dce8891127aeddc1950e8"}, + {file = "google_crc32c-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e4b426c3702f3cd23b933436487eb34e01e00327fac20c9aebb68ccf34117d"}, + {file = "google_crc32c-1.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51c4f54dd8c6dfeb58d1df5e4f7f97df8abf17a36626a217f169893d1d7f3e9f"}, + {file = "google_crc32c-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:bb8b3c75bd157010459b15222c3fd30577042a7060e29d42dabce449c087f2b3"}, + {file = "google_crc32c-1.6.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ed767bf4ba90104c1216b68111613f0d5926fb3780660ea1198fc469af410e9d"}, + {file = "google_crc32c-1.6.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:62f6d4a29fea082ac4a3c9be5e415218255cf11684ac6ef5488eea0c9132689b"}, + {file = "google_crc32c-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c87d98c7c4a69066fd31701c4e10d178a648c2cac3452e62c6b24dc51f9fcc00"}, + {file = "google_crc32c-1.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd5e7d2445d1a958c266bfa5d04c39932dc54093fa391736dbfdb0f1929c1fb3"}, + {file = "google_crc32c-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:7aec8e88a3583515f9e0957fe4f5f6d8d4997e36d0f61624e70469771584c760"}, + {file = "google_crc32c-1.6.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e2806553238cd076f0a55bddab37a532b53580e699ed8e5606d0de1f856b5205"}, + {file = "google_crc32c-1.6.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:bb0966e1c50d0ef5bc743312cc730b533491d60585a9a08f897274e57c3f70e0"}, + {file = "google_crc32c-1.6.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:386122eeaaa76951a8196310432c5b0ef3b53590ef4c317ec7588ec554fec5d2"}, + {file = "google_crc32c-1.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2952396dc604544ea7476b33fe87faedc24d666fb0c2d5ac971a2b9576ab871"}, + {file = "google_crc32c-1.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35834855408429cecf495cac67ccbab802de269e948e27478b1e47dfb6465e57"}, + {file = "google_crc32c-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:d8797406499f28b5ef791f339594b0b5fdedf54e203b5066675c406ba69d705c"}, + {file = "google_crc32c-1.6.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48abd62ca76a2cbe034542ed1b6aee851b6f28aaca4e6551b5599b6f3ef175cc"}, + {file = "google_crc32c-1.6.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18e311c64008f1f1379158158bb3f0c8d72635b9eb4f9545f8cf990c5668e59d"}, + {file = "google_crc32c-1.6.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05e2d8c9a2f853ff116db9706b4a27350587f341eda835f46db3c0a8c8ce2f24"}, + {file = "google_crc32c-1.6.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ca8145b060679ec9176e6de4f89b07363d6805bd4760631ef254905503598d"}, + {file = "google_crc32c-1.6.0.tar.gz", hash = "sha256:6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc"}, ] [package.extras] @@ -2098,13 +2072,13 @@ requests = ["requests (>=2.18.0,<3.0.0dev)"] [[package]] name = "googleapis-common-protos" -version = "1.63.2" +version = "1.65.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" files = [ - {file = "googleapis-common-protos-1.63.2.tar.gz", hash = "sha256:27c5abdffc4911f28101e635de1533fb4cfd2c37fbaa9174587c799fac90aa87"}, - {file = "googleapis_common_protos-1.63.2-py2.py3-none-any.whl", hash = "sha256:27a2499c7e8aff199665b22741997e485eccc8645aa9176c7c988e6fae507945"}, + {file = "googleapis_common_protos-1.65.0-py2.py3-none-any.whl", hash = "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63"}, + {file = "googleapis_common_protos-1.65.0.tar.gz", hash = "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0"}, ] [package.dependencies] @@ -2126,69 +2100,84 @@ files = [ [[package]] name = "greenlet" -version = "3.0.3" +version = "3.1.1" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" files = [ - {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, - {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, - {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, - {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, - {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, - {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, - {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, - {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, - {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, - {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, - {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, - {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, - {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, - {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, - {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, - {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, + {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, + {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, + {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, + {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, + {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, + {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, + {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, + {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"}, + {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"}, + {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"}, + {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"}, + {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"}, + {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"}, + {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"}, + {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"}, + {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, + {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, ] [package.extras] @@ -2225,13 +2214,13 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0 [[package]] name = "identify" -version = "2.6.0" +version = "2.6.1" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, - {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, + {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, + {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, ] [package.extras] @@ -2239,15 +2228,18 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.7" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "idutils" version = "1.2.1" @@ -2305,13 +2297,13 @@ ipython = {version = ">=7.31.1", markers = "python_version >= \"3.11\""} [[package]] name = "ipython" -version = "8.26.0" +version = "8.27.0" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.10" files = [ - {file = "ipython-8.26.0-py3-none-any.whl", hash = "sha256:e6b347c27bdf9c32ee9d31ae85defc525755a1869f14057e900675b9e8d6e6ff"}, - {file = "ipython-8.26.0.tar.gz", hash = "sha256:1cec0fbba8404af13facebe83d04436a7434c7400e59f47acf467c64abd0956c"}, + {file = "ipython-8.27.0-py3-none-any.whl", hash = "sha256:f68b3cb8bde357a5d7adc9598d57e22a45dfbea19eb6b98286fa3b288c9cd55c"}, + {file = "ipython-8.27.0.tar.gz", hash = "sha256:0b99a2dc9f15fd68692e898e5568725c6d49c527d36a9fb5960ffbdeaa82ff7e"}, ] [package.dependencies] @@ -2865,166 +2857,176 @@ resolved_reference = "be8a811fa6c3b105d9f5c656cabb6b1ba855ed5b" [[package]] name = "msgpack" -version = "1.0.8" +version = "1.1.0" description = "MessagePack serializer" optional = false python-versions = ">=3.8" files = [ - {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, - {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, - {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, - {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, - {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, - {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, - {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, - {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, - {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, - {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, - {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, - {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, - {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, - {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"}, + {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"}, + {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"}, + {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"}, + {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"}, + {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"}, + {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, + {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, + {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, + {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"}, + {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"}, + {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"}, + {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"}, + {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"}, + {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"}, + {file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"}, + {file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"}, + {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"}, + {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"}, + {file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"}, + {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"}, + {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"}, + {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"}, + {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"}, + {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"}, + {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"}, + {file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"}, + {file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"}, + {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, ] [[package]] name = "multidict" -version = "6.0.5" +version = "6.1.0" description = "multidict implementation" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, - {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, - {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, - {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, - {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, - {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, - {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, - {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, - {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, - {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, - {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, - {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, - {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, - {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, - {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, - {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, + {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, + {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, + {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, + {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, + {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, + {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, + {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, + {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, + {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, + {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, + {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, + {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, ] [[package]] @@ -3262,19 +3264,19 @@ xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.2.2" +version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "pluggy" @@ -3353,22 +3355,22 @@ testing = ["google-api-core (>=1.31.5)"] [[package]] name = "protobuf" -version = "5.27.3" +version = "5.28.2" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "protobuf-5.27.3-cp310-abi3-win32.whl", hash = "sha256:dcb307cd4ef8fec0cf52cb9105a03d06fbb5275ce6d84a6ae33bc6cf84e0a07b"}, - {file = "protobuf-5.27.3-cp310-abi3-win_amd64.whl", hash = "sha256:16ddf3f8c6c41e1e803da7abea17b1793a97ef079a912e42351eabb19b2cffe7"}, - {file = "protobuf-5.27.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:68248c60d53f6168f565a8c76dc58ba4fa2ade31c2d1ebdae6d80f969cdc2d4f"}, - {file = "protobuf-5.27.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:b8a994fb3d1c11156e7d1e427186662b64694a62b55936b2b9348f0a7c6625ce"}, - {file = "protobuf-5.27.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:a55c48f2a2092d8e213bd143474df33a6ae751b781dd1d1f4d953c128a415b25"}, - {file = "protobuf-5.27.3-cp38-cp38-win32.whl", hash = "sha256:043853dcb55cc262bf2e116215ad43fa0859caab79bb0b2d31b708f128ece035"}, - {file = "protobuf-5.27.3-cp38-cp38-win_amd64.whl", hash = "sha256:c2a105c24f08b1e53d6c7ffe69cb09d0031512f0b72f812dd4005b8112dbe91e"}, - {file = "protobuf-5.27.3-cp39-cp39-win32.whl", hash = "sha256:c84eee2c71ed83704f1afbf1a85c3171eab0fd1ade3b399b3fad0884cbcca8bf"}, - {file = "protobuf-5.27.3-cp39-cp39-win_amd64.whl", hash = "sha256:af7c0b7cfbbb649ad26132e53faa348580f844d9ca46fd3ec7ca48a1ea5db8a1"}, - {file = "protobuf-5.27.3-py3-none-any.whl", hash = "sha256:8572c6533e544ebf6899c360e91d6bcbbee2549251643d32c52cf8a5de295ba5"}, - {file = "protobuf-5.27.3.tar.gz", hash = "sha256:82460903e640f2b7e34ee81a947fdaad89de796d324bcbc38ff5430bcdead82c"}, + {file = "protobuf-5.28.2-cp310-abi3-win32.whl", hash = "sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d"}, + {file = "protobuf-5.28.2-cp310-abi3-win_amd64.whl", hash = "sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132"}, + {file = "protobuf-5.28.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7"}, + {file = "protobuf-5.28.2-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f"}, + {file = "protobuf-5.28.2-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f"}, + {file = "protobuf-5.28.2-cp38-cp38-win32.whl", hash = "sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0"}, + {file = "protobuf-5.28.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3"}, + {file = "protobuf-5.28.2-cp39-cp39-win32.whl", hash = "sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36"}, + {file = "protobuf-5.28.2-cp39-cp39-win_amd64.whl", hash = "sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276"}, + {file = "protobuf-5.28.2-py3-none-any.whl", hash = "sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece"}, + {file = "protobuf-5.28.2.tar.gz", hash = "sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0"}, ] [[package]] @@ -3420,24 +3422,24 @@ tests = ["pytest"] [[package]] name = "pyasn1" -version = "0.6.0" +version = "0.6.1" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" files = [ - {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"}, - {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, ] [[package]] name = "pyasn1-modules" -version = "0.4.0" +version = "0.4.1" description = "A collection of ASN.1-based protocols modules" optional = false python-versions = ">=3.8" files = [ - {file = "pyasn1_modules-0.4.0-py3-none-any.whl", hash = "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b"}, - {file = "pyasn1_modules-0.4.0.tar.gz", hash = "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6"}, + {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, + {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, ] [package.dependencies] @@ -3786,13 +3788,13 @@ test = ["coverage", "mypy", "ruff", "wheel"] [[package]] name = "pyparsing" -version = "3.1.2" +version = "3.1.4" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.6.8" files = [ - {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, - {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, + {file = "pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c"}, + {file = "pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032"}, ] [package.extras] @@ -4486,19 +4488,23 @@ tests = ["coverage[toml] (>=5.0.2)", "pytest"] [[package]] name = "setuptools" -version = "72.1.0" +version = "75.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-72.1.0-py3-none-any.whl", hash = "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1"}, - {file = "setuptools-72.1.0.tar.gz", hash = "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec"}, + {file = "setuptools-75.1.0-py3-none-any.whl", hash = "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2"}, + {file = "setuptools-75.1.0.tar.gz", hash = "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538"}, ] [package.extras] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] [[package]] name = "sgmllib3k" @@ -4523,13 +4529,13 @@ files = [ [[package]] name = "soupsieve" -version = "2.5" +version = "2.6" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" files = [ - {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, - {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, ] [[package]] @@ -4665,13 +4671,13 @@ test = ["pytest"] [[package]] name = "types-python-dateutil" -version = "2.9.0.20240316" +version = "2.9.0.20240906" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.8" files = [ - {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, - {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, + {file = "types-python-dateutil-2.9.0.20240906.tar.gz", hash = "sha256:9706c3b68284c25adffc47319ecc7947e5bb86b3773f843c73906fd598bc176e"}, + {file = "types_python_dateutil-2.9.0.20240906-py3-none-any.whl", hash = "sha256:27c8cc2d058ccb14946eebcaaa503088f4f6dbc4fb6093d3d456a49aef2753f6"}, ] [[package]] @@ -4735,13 +4741,13 @@ files = [ [[package]] name = "virtualenv" -version = "20.26.3" +version = "20.26.5" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, - {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, + {file = "virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6"}, + {file = "virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4"}, ] [package.dependencies] @@ -4792,13 +4798,13 @@ files = [ [[package]] name = "webob" -version = "1.8.7" +version = "1.8.8" description = "WSGI request and response object" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "WebOb-1.8.7-py2.py3-none-any.whl", hash = "sha256:73aae30359291c14fa3b956f8b5ca31960e420c28c1bec002547fb04928cf89b"}, - {file = "WebOb-1.8.7.tar.gz", hash = "sha256:b64ef5141be559cfade448f044fa45c2260351edcb6a8ef6b7e00c7dcef0c323"}, + {file = "WebOb-1.8.8-py2.py3-none-any.whl", hash = "sha256:b60ba63f05c0cf61e086a10c3781a41fcfe30027753a8ae6d819c77592ce83ea"}, + {file = "webob-1.8.8.tar.gz", hash = "sha256:2abc1555e118fc251e705fc6dc66c7f5353bb9fbfab6d20e22f1c02b4b71bcee"}, ] [package.extras] @@ -4807,13 +4813,13 @@ testing = ["coverage", "pytest (>=3.1.0)", "pytest-cov", "pytest-xdist"] [[package]] name = "webtest" -version = "3.0.0" +version = "3.0.1" description = "Helper to test WSGI applications" optional = false -python-versions = ">=3.6, <4" +python-versions = ">=3.7" files = [ - {file = "WebTest-3.0.0-py3-none-any.whl", hash = "sha256:2a001a9efa40d2a7e5d9cd8d1527c75f41814eb6afce2c3d207402547b1e5ead"}, - {file = "WebTest-3.0.0.tar.gz", hash = "sha256:54bd969725838d9861a9fa27f8d971f79d275d94ae255f5c501f53bb6d9929eb"}, + {file = "WebTest-3.0.1-py3-none-any.whl", hash = "sha256:b3bc75d020d0576ee93a5f149666045e58fe2400ea5f0c214d7430d7d213d0d0"}, + {file = "webtest-3.0.1.tar.gz", hash = "sha256:493b5c802f8948a65b5e3a1ad5b2524ee5e1ab60cd713d9a3da3b8da082c06fe"}, ] [package.dependencies] @@ -4822,7 +4828,7 @@ waitress = ">=0.8.5" WebOb = ">=1.2" [package.extras] -docs = ["Sphinx (>=1.8.1)", "docutils", "pylons-sphinx-themes (>=1.0.8)"] +docs = ["Sphinx (>=3.0.0)", "docutils", "pylons-sphinx-themes (>=1.0.8)"] tests = ["PasteDeploy", "WSGIProxy2", "coverage", "pyquery", "pytest", "pytest-cov"] [[package]] @@ -4889,101 +4895,103 @@ email = ["email-validator"] [[package]] name = "yarl" -version = "1.9.4" +version = "1.11.1" description = "Yet another URL library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, - {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, - {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, - {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, - {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, - {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, - {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, - {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, - {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, - {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, - {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, - {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, - {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, - {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, - {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, - {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, + {file = "yarl-1.11.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:400cd42185f92de559d29eeb529e71d80dfbd2f45c36844914a4a34297ca6f00"}, + {file = "yarl-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8258c86f47e080a258993eed877d579c71da7bda26af86ce6c2d2d072c11320d"}, + {file = "yarl-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2164cd9725092761fed26f299e3f276bb4b537ca58e6ff6b252eae9631b5c96e"}, + {file = "yarl-1.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08ea567c16f140af8ddc7cb58e27e9138a1386e3e6e53982abaa6f2377b38cc"}, + {file = "yarl-1.11.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:768ecc550096b028754ea28bf90fde071c379c62c43afa574edc6f33ee5daaec"}, + {file = "yarl-1.11.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2909fa3a7d249ef64eeb2faa04b7957e34fefb6ec9966506312349ed8a7e77bf"}, + {file = "yarl-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01a8697ec24f17c349c4f655763c4db70eebc56a5f82995e5e26e837c6eb0e49"}, + {file = "yarl-1.11.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e286580b6511aac7c3268a78cdb861ec739d3e5a2a53b4809faef6b49778eaff"}, + {file = "yarl-1.11.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4179522dc0305c3fc9782549175c8e8849252fefeb077c92a73889ccbcd508ad"}, + {file = "yarl-1.11.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:27fcb271a41b746bd0e2a92182df507e1c204759f460ff784ca614e12dd85145"}, + {file = "yarl-1.11.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f61db3b7e870914dbd9434b560075e0366771eecbe6d2b5561f5bc7485f39efd"}, + {file = "yarl-1.11.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c92261eb2ad367629dc437536463dc934030c9e7caca861cc51990fe6c565f26"}, + {file = "yarl-1.11.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d95b52fbef190ca87d8c42f49e314eace4fc52070f3dfa5f87a6594b0c1c6e46"}, + {file = "yarl-1.11.1-cp310-cp310-win32.whl", hash = "sha256:489fa8bde4f1244ad6c5f6d11bb33e09cf0d1d0367edb197619c3e3fc06f3d91"}, + {file = "yarl-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:476e20c433b356e16e9a141449f25161e6b69984fb4cdbd7cd4bd54c17844998"}, + {file = "yarl-1.11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:946eedc12895873891aaceb39bceb484b4977f70373e0122da483f6c38faaa68"}, + {file = "yarl-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:21a7c12321436b066c11ec19c7e3cb9aec18884fe0d5b25d03d756a9e654edfe"}, + {file = "yarl-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c35f493b867912f6fda721a59cc7c4766d382040bdf1ddaeeaa7fa4d072f4675"}, + {file = "yarl-1.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25861303e0be76b60fddc1250ec5986c42f0a5c0c50ff57cc30b1be199c00e63"}, + {file = "yarl-1.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4b53f73077e839b3f89c992223f15b1d2ab314bdbdf502afdc7bb18e95eae27"}, + {file = "yarl-1.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:327c724b01b8641a1bf1ab3b232fb638706e50f76c0b5bf16051ab65c868fac5"}, + {file = "yarl-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4307d9a3417eea87715c9736d050c83e8c1904e9b7aada6ce61b46361b733d92"}, + {file = "yarl-1.11.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48a28bed68ab8fb7e380775f0029a079f08a17799cb3387a65d14ace16c12e2b"}, + {file = "yarl-1.11.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:067b961853c8e62725ff2893226fef3d0da060656a9827f3f520fb1d19b2b68a"}, + {file = "yarl-1.11.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8215f6f21394d1f46e222abeb06316e77ef328d628f593502d8fc2a9117bde83"}, + {file = "yarl-1.11.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:498442e3af2a860a663baa14fbf23fb04b0dd758039c0e7c8f91cb9279799bff"}, + {file = "yarl-1.11.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:69721b8effdb588cb055cc22f7c5105ca6fdaa5aeb3ea09021d517882c4a904c"}, + {file = "yarl-1.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e969fa4c1e0b1a391f3fcbcb9ec31e84440253325b534519be0d28f4b6b533e"}, + {file = "yarl-1.11.1-cp311-cp311-win32.whl", hash = "sha256:7d51324a04fc4b0e097ff8a153e9276c2593106a811704025bbc1d6916f45ca6"}, + {file = "yarl-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:15061ce6584ece023457fb8b7a7a69ec40bf7114d781a8c4f5dcd68e28b5c53b"}, + {file = "yarl-1.11.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a4264515f9117be204935cd230fb2a052dd3792789cc94c101c535d349b3dab0"}, + {file = "yarl-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f41fa79114a1d2eddb5eea7b912d6160508f57440bd302ce96eaa384914cd265"}, + {file = "yarl-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:02da8759b47d964f9173c8675710720b468aa1c1693be0c9c64abb9d8d9a4867"}, + {file = "yarl-1.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9361628f28f48dcf8b2f528420d4d68102f593f9c2e592bfc842f5fb337e44fd"}, + {file = "yarl-1.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b91044952da03b6f95fdba398d7993dd983b64d3c31c358a4c89e3c19b6f7aef"}, + {file = "yarl-1.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74db2ef03b442276d25951749a803ddb6e270d02dda1d1c556f6ae595a0d76a8"}, + {file = "yarl-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e975a2211952a8a083d1b9d9ba26472981ae338e720b419eb50535de3c02870"}, + {file = "yarl-1.11.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aef97ba1dd2138112890ef848e17d8526fe80b21f743b4ee65947ea184f07a2"}, + {file = "yarl-1.11.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7915ea49b0c113641dc4d9338efa9bd66b6a9a485ffe75b9907e8573ca94b84"}, + {file = "yarl-1.11.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:504cf0d4c5e4579a51261d6091267f9fd997ef58558c4ffa7a3e1460bd2336fa"}, + {file = "yarl-1.11.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3de5292f9f0ee285e6bd168b2a77b2a00d74cbcfa420ed078456d3023d2f6dff"}, + {file = "yarl-1.11.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a34e1e30f1774fa35d37202bbeae62423e9a79d78d0874e5556a593479fdf239"}, + {file = "yarl-1.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:66b63c504d2ca43bf7221a1f72fbe981ff56ecb39004c70a94485d13e37ebf45"}, + {file = "yarl-1.11.1-cp312-cp312-win32.whl", hash = "sha256:a28b70c9e2213de425d9cba5ab2e7f7a1c8ca23a99c4b5159bf77b9c31251447"}, + {file = "yarl-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:17b5a386d0d36fb828e2fb3ef08c8829c1ebf977eef88e5367d1c8c94b454639"}, + {file = "yarl-1.11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1fa2e7a406fbd45b61b4433e3aa254a2c3e14c4b3186f6e952d08a730807fa0c"}, + {file = "yarl-1.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:750f656832d7d3cb0c76be137ee79405cc17e792f31e0a01eee390e383b2936e"}, + {file = "yarl-1.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b8486f322d8f6a38539136a22c55f94d269addb24db5cb6f61adc61eabc9d93"}, + {file = "yarl-1.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fce4da3703ee6048ad4138fe74619c50874afe98b1ad87b2698ef95bf92c96d"}, + {file = "yarl-1.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed653638ef669e0efc6fe2acb792275cb419bf9cb5c5049399f3556995f23c7"}, + {file = "yarl-1.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18ac56c9dd70941ecad42b5a906820824ca72ff84ad6fa18db33c2537ae2e089"}, + {file = "yarl-1.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:688654f8507464745ab563b041d1fb7dab5d9912ca6b06e61d1c4708366832f5"}, + {file = "yarl-1.11.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4973eac1e2ff63cf187073cd4e1f1148dcd119314ab79b88e1b3fad74a18c9d5"}, + {file = "yarl-1.11.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:964a428132227edff96d6f3cf261573cb0f1a60c9a764ce28cda9525f18f7786"}, + {file = "yarl-1.11.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6d23754b9939cbab02c63434776df1170e43b09c6a517585c7ce2b3d449b7318"}, + {file = "yarl-1.11.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c2dc4250fe94d8cd864d66018f8344d4af50e3758e9d725e94fecfa27588ff82"}, + {file = "yarl-1.11.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09696438cb43ea6f9492ef237761b043f9179f455f405279e609f2bc9100212a"}, + {file = "yarl-1.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:999bfee0a5b7385a0af5ffb606393509cfde70ecca4f01c36985be6d33e336da"}, + {file = "yarl-1.11.1-cp313-cp313-win32.whl", hash = "sha256:ce928c9c6409c79e10f39604a7e214b3cb69552952fbda8d836c052832e6a979"}, + {file = "yarl-1.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:501c503eed2bb306638ccb60c174f856cc3246c861829ff40eaa80e2f0330367"}, + {file = "yarl-1.11.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dae7bd0daeb33aa3e79e72877d3d51052e8b19c9025ecf0374f542ea8ec120e4"}, + {file = "yarl-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3ff6b1617aa39279fe18a76c8d165469c48b159931d9b48239065767ee455b2b"}, + {file = "yarl-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3257978c870728a52dcce8c2902bf01f6c53b65094b457bf87b2644ee6238ddc"}, + {file = "yarl-1.11.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f351fa31234699d6084ff98283cb1e852270fe9e250a3b3bf7804eb493bd937"}, + {file = "yarl-1.11.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8aef1b64da41d18026632d99a06b3fefe1d08e85dd81d849fa7c96301ed22f1b"}, + {file = "yarl-1.11.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7175a87ab8f7fbde37160a15e58e138ba3b2b0e05492d7351314a250d61b1591"}, + {file = "yarl-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba444bdd4caa2a94456ef67a2f383710928820dd0117aae6650a4d17029fa25e"}, + {file = "yarl-1.11.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ea9682124fc062e3d931c6911934a678cb28453f957ddccf51f568c2f2b5e05"}, + {file = "yarl-1.11.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8418c053aeb236b20b0ab8fa6bacfc2feaaf7d4683dd96528610989c99723d5f"}, + {file = "yarl-1.11.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:61a5f2c14d0a1adfdd82258f756b23a550c13ba4c86c84106be4c111a3a4e413"}, + {file = "yarl-1.11.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f3a6d90cab0bdf07df8f176eae3a07127daafcf7457b997b2bf46776da2c7eb7"}, + {file = "yarl-1.11.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:077da604852be488c9a05a524068cdae1e972b7dc02438161c32420fb4ec5e14"}, + {file = "yarl-1.11.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:15439f3c5c72686b6c3ff235279630d08936ace67d0fe5c8d5bbc3ef06f5a420"}, + {file = "yarl-1.11.1-cp38-cp38-win32.whl", hash = "sha256:238a21849dd7554cb4d25a14ffbfa0ef380bb7ba201f45b144a14454a72ffa5a"}, + {file = "yarl-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:67459cf8cf31da0e2cbdb4b040507e535d25cfbb1604ca76396a3a66b8ba37a6"}, + {file = "yarl-1.11.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:884eab2ce97cbaf89f264372eae58388862c33c4f551c15680dd80f53c89a269"}, + {file = "yarl-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a336eaa7ee7e87cdece3cedb395c9657d227bfceb6781295cf56abcd3386a26"}, + {file = "yarl-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87f020d010ba80a247c4abc335fc13421037800ca20b42af5ae40e5fd75e7909"}, + {file = "yarl-1.11.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:637c7ddb585a62d4469f843dac221f23eec3cbad31693b23abbc2c366ad41ff4"}, + {file = "yarl-1.11.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48dfd117ab93f0129084577a07287376cc69c08138694396f305636e229caa1a"}, + {file = "yarl-1.11.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e0ae31fb5ccab6eda09ba1494e87eb226dcbd2372dae96b87800e1dcc98804"}, + {file = "yarl-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f46f81501160c28d0c0b7333b4f7be8983dbbc161983b6fb814024d1b4952f79"}, + {file = "yarl-1.11.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:04293941646647b3bfb1719d1d11ff1028e9c30199509a844da3c0f5919dc520"}, + {file = "yarl-1.11.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:250e888fa62d73e721f3041e3a9abf427788a1934b426b45e1b92f62c1f68366"}, + {file = "yarl-1.11.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e8f63904df26d1a66aabc141bfd258bf738b9bc7bc6bdef22713b4f5ef789a4c"}, + {file = "yarl-1.11.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:aac44097d838dda26526cffb63bdd8737a2dbdf5f2c68efb72ad83aec6673c7e"}, + {file = "yarl-1.11.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:267b24f891e74eccbdff42241c5fb4f974de2d6271dcc7d7e0c9ae1079a560d9"}, + {file = "yarl-1.11.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6907daa4b9d7a688063ed098c472f96e8181733c525e03e866fb5db480a424df"}, + {file = "yarl-1.11.1-cp39-cp39-win32.whl", hash = "sha256:14438dfc5015661f75f85bc5adad0743678eefee266ff0c9a8e32969d5d69f74"}, + {file = "yarl-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:94d0caaa912bfcdc702a4204cd5e2bb01eb917fc4f5ea2315aa23962549561b0"}, + {file = "yarl-1.11.1-py3-none-any.whl", hash = "sha256:72bf26f66456baa0584eff63e44545c9f0eaed9b73cb6601b647c91f14c11f38"}, + {file = "yarl-1.11.1.tar.gz", hash = "sha256:1bb2d9e212fb7449b8fb73bc461b51eaa17cc8430b4a87d87be7b25052d92f53"}, ] [package.dependencies] @@ -5010,45 +5018,45 @@ test = ["zope.testrunner"] [[package]] name = "zope-interface" -version = "7.0.1" +version = "7.0.3" description = "Interfaces for Python" optional = false python-versions = ">=3.8" files = [ - {file = "zope.interface-7.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec4e87e6fdc511a535254daa122c20e11959ce043b4e3425494b237692a34f1c"}, - {file = "zope.interface-7.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51d5713e8e38f2d3ec26e0dfdca398ed0c20abda2eb49ffc15a15a23eb8e5f6d"}, - {file = "zope.interface-7.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea8d51e5eb29e57d34744369cd08267637aa5a0fefc9b5d33775ab7ff2ebf2e3"}, - {file = "zope.interface-7.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55bbcc74dc0c7ab489c315c28b61d7a1d03cf938cc99cc58092eb065f120c3a5"}, - {file = "zope.interface-7.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10ebac566dd0cec66f942dc759d46a994a2b3ba7179420f0e2130f88f8a5f400"}, - {file = "zope.interface-7.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:7039e624bcb820f77cc2ff3d1adcce531932990eee16121077eb51d9c76b6c14"}, - {file = "zope.interface-7.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03bd5c0db82237bbc47833a8b25f1cc090646e212f86b601903d79d7e6b37031"}, - {file = "zope.interface-7.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f52050c6a10d4a039ec6f2c58e5b3ade5cc570d16cf9d102711e6b8413c90e6"}, - {file = "zope.interface-7.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af0b33f04677b57843d529b9257a475d2865403300b48c67654c40abac2f9f24"}, - {file = "zope.interface-7.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696c2a381fc7876b3056711717dba5eddd07c2c9e5ccd50da54029a1293b6e43"}, - {file = "zope.interface-7.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f89a420cf5a6f2aa7849dd59e1ff0e477f562d97cf8d6a1ee03461e1eec39887"}, - {file = "zope.interface-7.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:b59deb0ddc7b431e41d720c00f99d68b52cb9bd1d5605a085dc18f502fe9c47f"}, - {file = "zope.interface-7.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52f5253cca1b35eaeefa51abd366b87f48f8714097c99b131ba61f3fdbbb58e7"}, - {file = "zope.interface-7.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88d108d004e0df25224de77ce349a7e73494ea2cb194031f7c9687e68a88ec9b"}, - {file = "zope.interface-7.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c203d82069ba31e1f3bc7ba530b2461ec86366cd4bfc9b95ec6ce58b1b559c34"}, - {file = "zope.interface-7.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3495462bc0438b76536a0e10d765b168ae636092082531b88340dc40dcd118"}, - {file = "zope.interface-7.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192b7a792e3145ed880ff6b1a206fdb783697cfdb4915083bfca7065ec845e60"}, - {file = "zope.interface-7.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:400d06c9ec8dbcc96f56e79376297e7be07a315605c9a2208720da263d44d76f"}, - {file = "zope.interface-7.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c1dff87b30fd150c61367d0e2cdc49bb55f8b9fd2a303560bbc24b951573ae1"}, - {file = "zope.interface-7.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f749ca804648d00eda62fe1098f229b082dfca930d8bad8386e572a6eafa7525"}, - {file = "zope.interface-7.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ec212037becf6d2f705b7ed4538d56980b1e7bba237df0d8995cbbed29961dc"}, - {file = "zope.interface-7.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d33cb526efdc235a2531433fc1287fcb80d807d5b401f9b801b78bf22df560dd"}, - {file = "zope.interface-7.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b419f2144e1762ab845f20316f1df36b15431f2622ebae8a6d5f7e8e712b413c"}, - {file = "zope.interface-7.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03f1452d5d1f279184d5bdb663a3dc39902d9320eceb63276240791e849054b6"}, - {file = "zope.interface-7.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ba4b3638d014918b918aa90a9c8370bd74a03abf8fcf9deb353b3a461a59a84"}, - {file = "zope.interface-7.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc0615351221926a36a0fbcb2520fb52e0b23e8c22a43754d9cb8f21358c33c0"}, - {file = "zope.interface-7.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:ce6cbb852fb8f2f9bb7b9cdca44e2e37bce783b5f4c167ff82cb5f5128163c8f"}, - {file = "zope.interface-7.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5566fd9271c89ad03d81b0831c37d46ae5e2ed211122c998637130159a120cf1"}, - {file = "zope.interface-7.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da0cef4d7e3f19c3bd1d71658d6900321af0492fee36ec01b550a10924cffb9c"}, - {file = "zope.interface-7.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f32ca483e6ade23c7caaee9d5ee5d550cf4146e9b68d2fb6c68bac183aa41c37"}, - {file = "zope.interface-7.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da21e7eec49252df34d426c2ee9cf0361c923026d37c24728b0fa4cc0599fd03"}, - {file = "zope.interface-7.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a8195b99e650e6f329ce4e5eb22d448bdfef0406404080812bc96e2a05674cb"}, - {file = "zope.interface-7.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:19c829d52e921b9fe0b2c0c6a8f9a2508c49678ee1be598f87d143335b6a35dc"}, - {file = "zope.interface-7.0.1.tar.gz", hash = "sha256:f0f5fda7cbf890371a59ab1d06512da4f2c89a6ea194e595808123c863c38eff"}, + {file = "zope.interface-7.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b9369671a20b8d039b8e5a1a33abd12e089e319a3383b4cc0bf5c67bd05fe7b"}, + {file = "zope.interface-7.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db6237e8fa91ea4f34d7e2d16d74741187e9105a63bbb5686c61fea04cdbacca"}, + {file = "zope.interface-7.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53d678bb1c3b784edbfb0adeebfeea6bf479f54da082854406a8f295d36f8386"}, + {file = "zope.interface-7.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3aa8fcbb0d3c2be1bfd013a0f0acd636f6ed570c287743ae2bbd467ee967154d"}, + {file = "zope.interface-7.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6195c3c03fef9f87c0dbee0b3b6451df6e056322463cf35bca9a088e564a3c58"}, + {file = "zope.interface-7.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:11fa1382c3efb34abf16becff8cb214b0b2e3144057c90611621f2d186b7e1b7"}, + {file = "zope.interface-7.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:af94e429f9d57b36e71ef4e6865182090648aada0cb2d397ae2b3f7fc478493a"}, + {file = "zope.interface-7.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dd647fcd765030638577fe6984284e0ebba1a1008244c8a38824be096e37fe3"}, + {file = "zope.interface-7.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bee1b722077d08721005e8da493ef3adf0b7908e0cd85cc7dc836ac117d6f32"}, + {file = "zope.interface-7.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2545d6d7aac425d528cd9bf0d9e55fcd47ab7fd15f41a64b1c4bf4c6b24946dc"}, + {file = "zope.interface-7.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d04b11ea47c9c369d66340dbe51e9031df2a0de97d68f442305ed7625ad6493"}, + {file = "zope.interface-7.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:064ade95cb54c840647205987c7b557f75d2b2f7d1a84bfab4cf81822ef6e7d1"}, + {file = "zope.interface-7.0.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3fcdc76d0cde1c09c37b7c6b0f8beba2d857d8417b055d4f47df9c34ec518bdd"}, + {file = "zope.interface-7.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3d4b91821305c8d8f6e6207639abcbdaf186db682e521af7855d0bea3047c8ca"}, + {file = "zope.interface-7.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35062d93bc49bd9b191331c897a96155ffdad10744ab812485b6bad5b588d7e4"}, + {file = "zope.interface-7.0.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c96b3e6b0d4f6ddfec4e947130ec30bd2c7b19db6aa633777e46c8eecf1d6afd"}, + {file = "zope.interface-7.0.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e0c151a6c204f3830237c59ee4770cc346868a7a1af6925e5e38650141a7f05"}, + {file = "zope.interface-7.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:3de1d553ce72868b77a7e9d598c9bff6d3816ad2b4cc81c04f9d8914603814f3"}, + {file = "zope.interface-7.0.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab985c566a99cc5f73bc2741d93f1ed24a2cc9da3890144d37b9582965aff996"}, + {file = "zope.interface-7.0.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d976fa7b5faf5396eb18ce6c132c98e05504b52b60784e3401f4ef0b2e66709b"}, + {file = "zope.interface-7.0.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a207c6b2c58def5011768140861a73f5240f4f39800625072ba84e76c9da0b"}, + {file = "zope.interface-7.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:382d31d1e68877061daaa6499468e9eb38eb7625d4369b1615ac08d3860fe896"}, + {file = "zope.interface-7.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c4316a30e216f51acbd9fb318aa5af2e362b716596d82cbb92f9101c8f8d2e7"}, + {file = "zope.interface-7.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01e6e58078ad2799130c14a1d34ec89044ada0e1495329d72ee0407b9ae5100d"}, + {file = "zope.interface-7.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799ef7a444aebbad5a145c3b34bff012b54453cddbde3332d47ca07225792ea4"}, + {file = "zope.interface-7.0.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3b7ce6d46fb0e60897d62d1ff370790ce50a57d40a651db91a3dde74f73b738"}, + {file = "zope.interface-7.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:f418c88f09c3ba159b95a9d1cfcdbe58f208443abb1f3109f4b9b12fd60b187c"}, + {file = "zope.interface-7.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:84f8794bd59ca7d09d8fce43ae1b571be22f52748169d01a13d3ece8394d8b5b"}, + {file = "zope.interface-7.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7d92920416f31786bc1b2f34cc4fc4263a35a407425319572cbf96b51e835cd3"}, + {file = "zope.interface-7.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e5913ec718010dc0e7c215d79a9683b4990e7026828eedfda5268e74e73e11"}, + {file = "zope.interface-7.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1eeeb92cb7d95c45e726e3c1afe7707919370addae7ed14f614e22217a536958"}, + {file = "zope.interface-7.0.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd32f30f40bfd8511b17666895831a51b532e93fc106bfa97f366589d3e4e0e"}, + {file = "zope.interface-7.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:5112c530fa8aa2108a3196b9c2f078f5738c1c37cfc716970edc0df0414acda8"}, + {file = "zope.interface-7.0.3.tar.gz", hash = "sha256:cd2690d4b08ec9eaf47a85914fe513062b20da78d10d6d789a792c0b20307fb1"}, ] [package.dependencies] diff --git a/pyproject.toml b/pyproject.toml index 6b51afc118d..051291fc333 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,6 @@ oauthlib = "3.2.2" requests-oauthlib = "1.3.1" sentry-sdk = {version= "2.2.0", extras = ["django", "flask", "celery"]} django-redis = "5.4.0" - # API requirements Django = "4.2.15" djangorestframework = "3.15.1" @@ -72,16 +71,13 @@ pyjwe = "1.0.0" cryptography = "42.0.5" jsonschema = "4.21.1" django-guardian = "2.4.0" - # Admin requirements -# fork to generate old webpack stats format so we don't have to upgrade admin's webpack django-webpack-loader = {git = "https://github.com/CenterForOpenScience/django-webpack-loader.git", rev = "af8438c2da909ec9f2188a6c07c9d2caad0f7e93"} # branch is feature/v1-webpack-stats django-sendgrid-v5 = "1.2.3" # metadata says python 3.10 not supported, but tests pass -# Analytics requirement +# Analytics requirements keen = "0.7.0" geoip2 = "4.7.0" - # OSF models django-typed-models = "0.14.0" django-storages = "1.14.3" @@ -89,29 +85,21 @@ google-cloud-storage = "2.16.0" # dependency of django-storages, hard-pin to ve django-dirtyfields = "1.9.2" django-extensions = "3.2.3" psycopg2 = "2.9.9" - # Reviews requirements transitions = "0.8.11" - # identifiers datacite = "1.1.3" - # metadata rdflib = "7.0.0" -packaging = "^24.0" - colorlog = "6.8.2" - # Metrics -# fork to pin installed version of elasticsearch-dsl django-elasticsearch-metrics = {git ="https://github.com/CenterForOpenScience/django-elasticsearch-metrics.git", rev = "f5b9312914154e213aa01731e934c593e3434269"} # branch is feature/pin-esdsl - # Impact Metrics CSV Export djangorestframework-csv = "3.0.2" gevent = "24.2.1" +packaging = "^24.0" [tool.poetry.group.dev.dependencies] -# Requirements that are used in the development environment only pytest = "7.4.4" pytest-socket = "0.7.0" pytest-xdist = "3.5.0" @@ -147,52 +135,33 @@ nplusone = "1.0.0" django-silk = "5.1.0" [tool.poetry.group.addons.dependencies] -## boa +# Requirements for the boa add-on boa-api = "0.1.14" -# Requirements for running asyncio in celery -asgiref = "3.7.2" -## box +# Requirements for running asyncio in celery, using 3.4.1 for Python 3.6 compatibility +asgiref = "3.7.2" boxsdk = "3.9.2" - -## dataverse -# new features & dependency updates +# Allow for optional timeout parameter. +# https://github.com/IQSS/dataverse-client-python/pull/27 dataverse = {git = "https://github.com/CenterForOpenScience/dataverse-client-python.git", rev="2b3827578048e6df3818f82381c7ea9a2395e526"} # branch is feature/dv-client-updates - -## dropbox dropbox = "11.36.2" -## github cachecontrol = "0.14.0" "github3.py" = "4.0.1" uritemplate = "4.1.1" - -## gitlab python-gitlab = "4.4.0" - -## mendeley # up-to-date with mendeley's master + add folder support and future dep updates mendeley = {git = "https://github.com/CenterForOpenScience/mendeley-python-sdk.git", rev="be8a811fa6c3b105d9f5c656cabb6b1ba855ed5b"} # branch is feature/osf-dep-updates - -## owncloud +# Requirements for the owncloud add-on pyocclient = "0.6.0" - -## s3 boto3 = "1.34.60" - -## twofactor pyotp = "2.9.0" - -## wiki -# needs pymongo, but already installed in deps -# was 4.6.3 in wiki requirements, but older version seems to work - -## zotero Pyzotero = "1.5.18" [tool.poetry.group.release.dependencies] -# Requirements to be installed on server deployments +# newrelic APM agent newrelic = "9.7.1" +# uwsgi uwsgi = "2.0.24" diff --git a/tasks/__init__.py b/tasks/__init__.py index d393d7fb1b6..180d7838126 100755 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -470,47 +470,47 @@ def remove_failures_from_testmon(ctx, db_path=None): conn = sqlite3.connect(db_path) tests_decached = conn.execute("delete from node where result <> '{}'").rowcount - ctx.run(f'echo {tests_decached} failures purged from travis cache') + ctx.run(f'echo {tests_decached} failures purged from ci cache') @task -def travis_setup(ctx): +def ci_setup(ctx): with open('package.json') as fobj: package_json = json.load(fobj) ctx.run('npm install @centerforopenscience/list-of-licenses@{}'.format(package_json['dependencies']['@centerforopenscience/list-of-licenses']), echo=True) @task -def test_travis_addons(ctx, numprocesses=None, coverage=False, testmon=False, junit=False): +def test_ci_addons(ctx, numprocesses=None, coverage=False, testmon=False, junit=False): """ - Run half of the tests to help travis go faster. + Run half of the tests to help ci go faster. """ - #travis_setup(ctx) + #ci_setup(ctx) syntax(ctx) test_addons(ctx, numprocesses=numprocesses, coverage=coverage, testmon=testmon, junit=junit) @task -def test_travis_website(ctx, numprocesses=None, coverage=False, testmon=False, junit=False): +def test_ci_website(ctx, numprocesses=None, coverage=False, testmon=False, junit=False): """ - Run other half of the tests to help travis go faster. + Run other half of the tests to help ci go faster. """ - #travis_setup(ctx) + #ci_setup(ctx) test_website(ctx, numprocesses=numprocesses, coverage=coverage, testmon=testmon, junit=junit) @task -def test_travis_api1_and_js(ctx, numprocesses=None, coverage=False, testmon=False, junit=False): - #travis_setup(ctx) +def test_ci_api1_and_js(ctx, numprocesses=None, coverage=False, testmon=False, junit=False): + #ci_setup(ctx) test_api1(ctx, numprocesses=numprocesses, coverage=coverage, testmon=testmon, junit=junit) @task -def test_travis_api2(ctx, numprocesses=None, coverage=False, testmon=False, junit=False): - #travis_setup(ctx) +def test_ci_api2(ctx, numprocesses=None, coverage=False, testmon=False, junit=False): + #ci_setup(ctx) test_api2(ctx, numprocesses=numprocesses, coverage=coverage, testmon=testmon, junit=junit) @task -def test_travis_api3_and_osf(ctx, numprocesses=None, coverage=False, testmon=False, junit=False): - #travis_setup(ctx) +def test_ci_api3_and_osf(ctx, numprocesses=None, coverage=False, testmon=False, junit=False): + #ci_setup(ctx) test_api3(ctx, numprocesses=numprocesses, coverage=coverage, testmon=testmon, junit=junit) @task @@ -559,13 +559,13 @@ def addon_requirements(ctx): @task -def travis_addon_settings(ctx): +def ci_addon_settings(ctx): for directory in os.listdir(settings.ADDON_PATH): path = os.path.join(settings.ADDON_PATH, directory, 'settings') if os.path.isdir(path): try: - open(os.path.join(path, 'local-travis.py')) - ctx.run('cp {path}/local-travis.py {path}/local.py'.format(path=path)) + open(os.path.join(path, 'local-ci.py')) + ctx.run('cp {path}/local-ci.py {path}/local.py'.format(path=path)) except OSError: pass diff --git a/tests/base.py b/tests/base.py index 1d2068189b2..2c36dd801eb 100644 --- a/tests/base.py +++ b/tests/base.py @@ -150,6 +150,9 @@ def setUp(self): class SearchTestCase(unittest.TestCase): def setUp(self): + if settings.SEARCH_ENGINE is None: + return + settings.ELASTIC_INDEX = uuid.uuid1().hex settings.ELASTIC_TIMEOUT = 60 @@ -163,7 +166,8 @@ def setUp(self): def tearDown(self): super().tearDown() - + if settings.SEARCH_ENGINE is None: + return from website.search import elastic_search elastic_search.delete_index(settings.ELASTIC_INDEX) diff --git a/tests/test_auth.py b/tests/test_auth.py index f59f1498a93..b59c1c065ab 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -135,10 +135,9 @@ def test_successful_external_login_cas_redirect(self, mock_service_validate, moc resp = cas.make_response_from_ticket(ticket, service_url) assert resp.status_code == 302, 'redirect to CAS login' assert quote_plus('/login?service=') in resp.location - - # the valid username will be double quoted as it is furl quoted in both get_login_url and get_logout_url in order - assert quote_plus(f'username={user.username}') in resp.location - assert quote_plus(f'verification_key={user.verification_key}') in resp.location + # the valid username and verification key should be double-quoted + assert quote_plus(f'username={quote_plus(user.username)}') in resp.location + assert quote_plus(f'verification_key={quote_plus(user.verification_key)}') in resp.location @mock.patch('framework.auth.cas.get_user_from_cas_resp') @mock.patch('framework.auth.cas.CasClient.service_validate') diff --git a/tests/test_views.py b/tests/test_views.py index 406ffb66ede..f1dbaa3285d 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -2611,7 +2611,7 @@ def test_claim_user_when_user_is_registered_with_orcid(self, mock_response_from_ assert res1.status_code == 302 res = self.app.resolve_redirect(self.app.get(url)) service_url = f'http://localhost{url}' - expected = cas.get_logout_url(service_url=unquote_plus(cas.get_login_url(service_url=service_url))) + expected = cas.get_logout_url(service_url=cas.get_login_url(service_url=service_url)) assert res1.location == expected # user logged in with orcid automatically becomes a contributor @@ -2631,7 +2631,7 @@ def test_claim_user_when_user_is_registered_with_orcid(self, mock_response_from_ # And the redirect URL must equal to the originial service URL assert res.status_code == 302 redirect_url = res.headers['Location'] - assert unquote_plus(redirect_url) == url + assert redirect_url == url # The response of this request is expected have the `Set-Cookie` header with OSF cookie. # And the cookie must belong to the ORCiD user. raw_set_cookie = res.headers['Set-Cookie'] @@ -3256,7 +3256,7 @@ def test_register_email_without_accepted_tos(self, _): user = OSFUser.objects.get(username=email) assert user.accepted_terms_of_service is None - @mock.patch('framework.auth.views.send_confirm_email') + @mock.patch('framework.auth.views.send_confirm_email_async') def test_register_scrubs_username(self, _): url = api_url_for('register_user') name = "Eunice O' \"Cornwallis\"" @@ -3438,8 +3438,8 @@ def test_register_after_being_invited_as_unreg_contributor(self, mock_update_sea assert new_user.check_password(password) assert new_user.fullname == real_name - @mock.patch('framework.auth.views.send_confirm_email') - def test_register_sends_user_registered_signal(self, mock_send_confirm_email): + @mock.patch('framework.auth.views.send_confirm_email_async') + def test_register_sends_user_registered_signal(self, mock_send_confirm_email_async): url = api_url_for('register_user') name, email, password = fake.name(), fake_email(), 'underpressure' with capture_signals() as mock_signals: @@ -3453,7 +3453,7 @@ def test_register_sends_user_registered_signal(self, mock_send_confirm_email): } ) assert mock_signals.signals_sent() == {auth.signals.user_registered, auth.signals.unconfirmed_user_created} - assert mock_send_confirm_email.called + assert mock_send_confirm_email_async.called @mock.patch('framework.auth.views.mails.send_mail') def test_resend_confirmation(self, send_mail: MagicMock): diff --git a/website/mails/mails.py b/website/mails/mails.py index 8f1c46b3310..da66ad8d083 100644 --- a/website/mails/mails.py +++ b/website/mails/mails.py @@ -459,6 +459,11 @@ def get_english_article(word): subject='Confirmation of your submission to ${provider_name}' ) +REVIEWS_RESUBMISSION_CONFIRMATION = Mail( + 'reviews_resubmission_confirmation', + subject='Confirmation of your submission to ${provider_name}' +) + ACCESS_REQUEST_SUBMITTED = Mail( 'access_request_submitted', subject='An OSF user has requested access to your ${node.project_or_component}' diff --git a/website/profile/views.py b/website/profile/views.py index 3e377157bb6..c4306b92125 100644 --- a/website/profile/views.py +++ b/website/profile/views.py @@ -14,7 +14,7 @@ from framework.auth.decorators import must_be_logged_in from framework.auth.decorators import must_be_confirmed from framework.auth.exceptions import ChangePasswordError -from framework.auth.views import send_confirm_email +from framework.auth.views import send_confirm_email_async from framework.auth.signals import ( user_account_merged, user_account_deactivated, @@ -83,7 +83,7 @@ def resend_confirmation(auth): # TODO: This setting is now named incorrectly. if settings.CONFIRM_REGISTRATIONS_BY_EMAIL: - send_confirm_email(user, email=address) + send_confirm_email_async(user, email=address) user.email_last_sent = timezone.now() user.save() @@ -166,7 +166,7 @@ def update_user(auth): if not throttle_period_expired(user.email_last_sent, settings.SEND_EMAIL_THROTTLE): raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data={'message_long': 'Too many requests. Please wait a while before adding an email to your account.'}) - send_confirm_email(user, email=address) + send_confirm_email_async(user, email=address) ############ # Username # diff --git a/website/project/views/contributor.py b/website/project/views/contributor.py index 593cb57816d..56b6802d2a6 100644 --- a/website/project/views/contributor.py +++ b/website/project/views/contributor.py @@ -1,5 +1,3 @@ -from urllib.parse import unquote_plus - from rest_framework import status as http_status from flask import request @@ -188,10 +186,12 @@ def deserialize_contributors(node, user_dicts, auth, validate=False): # Add unclaimed record if necessary if not contributor.is_registered: - contributor.add_unclaimed_record(node, referrer=auth.user, + contributor.add_unclaimed_record( + node, + referrer=auth.user, given_name=fullname, - email=email) - contributor.save() + email=email, + ) contribs.append({ 'user': contributor, @@ -667,7 +667,7 @@ def claim_user_registered(auth, node, **kwargs): current_user = auth.user current_session = get_session() - sign_out_url = cas.get_logout_url(service_url=unquote_plus(cas.get_login_url(service_url=request.url))) + sign_out_url = cas.get_logout_url(service_url=cas.get_login_url(service_url=request.url)) if not current_user: return redirect(sign_out_url) diff --git a/website/reviews/listeners.py b/website/reviews/listeners.py index aced08f644f..27a15c2c337 100644 --- a/website/reviews/listeners.py +++ b/website/reviews/listeners.py @@ -6,9 +6,12 @@ from website.settings import OSF_PREPRINTS_LOGO, OSF_REGISTRIES_LOGO, DOMAIN -# Handle email notifications including: update comment, accept, and reject of submission. @reviews_signals.reviews_email.connect def reviews_notification(self, creator, template, context, action): + """ + Handle email notifications including: update comment, accept, and reject of submission, but not initial submission + or resubmission. + """ # Avoid AppRegistryNotReady error from website.notifications.emails import notify_global_event recipients = list(action.target.contributors) @@ -25,9 +28,14 @@ def reviews_notification(self, creator, template, context, action): ) -# Handle email notifications for a new submission. @reviews_signals.reviews_email_submit.connect -def reviews_submit_notification(self, recipients, context): +def reviews_submit_notification(self, recipients, context, template=None): + """ + Handle email notifications for a new submission or a resubmission + """ + if not template: + template = mails.REVIEWS_SUBMISSION_CONFIRMATION + # Avoid AppRegistryNotReady error from website.notifications.emails import get_user_subscriptions @@ -51,15 +59,17 @@ def reviews_submit_notification(self, recipients, context): context['provider_name'] = context['reviewable'].provider.name mails.send_mail( recipient.username, - mails.REVIEWS_SUBMISSION_CONFIRMATION, + template, user=recipient, **context ) -# Handle email notifications to notify moderators of new submissions. @reviews_signals.reviews_email_submit_moderators_notifications.connect def reviews_submit_notification_moderators(self, timestamp, context): + """ + Handle email notifications to notify moderators of new submissions or resubmission. + """ # imports moved here to avoid AppRegistryNotReady error from osf.models import NotificationSubscription from website.profile.utils import get_profile_image_url @@ -87,7 +97,10 @@ def reviews_submit_notification_moderators(self, timestamp, context): context['message'] = f'submitted updates to "{resource.title}".' context['reviews_submission_url'] += f'&revisionId={revision_id}' else: - context['message'] = f'submitted "{resource.title}".' + if context.get('resubmission'): + context['message'] = f'resubmitted "{resource.title}".' + else: + context['message'] = f'submitted "{resource.title}".' # Get NotificationSubscription instance, which contains reference to all subscribers provider_subscription, created = NotificationSubscription.objects.get_or_create( diff --git a/website/settings/defaults.py b/website/settings/defaults.py index 8d951346ffa..07ca9d89c64 100644 --- a/website/settings/defaults.py +++ b/website/settings/defaults.py @@ -105,6 +105,7 @@ def parent_dir(path): SEARCH_ENGINE = 'elastic' # Can be 'elastic', or None ELASTIC_URI = '127.0.0.1:9200' +ELASTIC6_URI = os.environ.get('ELASTIC6_URI', '127.0.0.1:9201') ELASTIC_TIMEOUT = 10 ELASTIC_INDEX = 'website' ELASTIC_KWARGS = { @@ -619,7 +620,7 @@ class CeleryConfig: 'kwargs': {'dry_run': False}, }, 'clear_expired_sessions': { - 'task': 'management.commands.clear_expired_sessions', + 'task': 'osf.management.commands.clear_expired_sessions', 'schedule': crontab(minute=0, hour=5), # Daily 12 a.m 'kwargs': {'dry_run': False}, }, @@ -646,7 +647,6 @@ class CeleryConfig: 'daily_reporters_go': { 'task': 'management.commands.daily_reporters_go', 'schedule': crontab(minute=0, hour=6), # Daily 1:00 a.m. - 'kwargs': {'also_send_to_keen': True}, }, 'monthly_reporters_go': { 'task': 'management.commands.monthly_reporters_go', diff --git a/website/settings/local-travis.py b/website/settings/local-ci.py similarity index 98% rename from website/settings/local-travis.py rename to website/settings/local-ci.py index 0f3174f3ba9..582541525a9 100644 --- a/website/settings/local-travis.py +++ b/website/settings/local-ci.py @@ -10,7 +10,7 @@ import os DB_PORT = 5432 -OSF_DB_PASSWORD = 'postgres' +OSF_DB_PASSWORD = os.environ.get('OSF_DB_PASSWORD') DEV_MODE = True DEBUG_MODE = True # Sets app to debug mode, turns off template caching, etc. diff --git a/website/templates/emails/reviews_resubmission_confirmation.html.mako b/website/templates/emails/reviews_resubmission_confirmation.html.mako index a1ac664992d..23ce18781ba 100644 --- a/website/templates/emails/reviews_resubmission_confirmation.html.mako +++ b/website/templates/emails/reviews_resubmission_confirmation.html.mako @@ -1,39 +1,42 @@ -## -*- coding: utf-8 -*- -
-

Hello ${recipient.fullname},

-

- The ${document_type} - ${reviewable.title} - has been successfully re-submitted to ${reviewable.provider.name}. -

-

- ${reviewable.provider.name} has chosen to moderate their submissions using a - pre-moderation workflow, which means your submission is pending until accepted - by a moderator. +<%inherit file="notify_base.mako"/> +<%def name="content()"> +

+

+ Hello ${referrer.fullname}, +

+

+ The ${document_type} ${reviewable.title} has been successfully + resubmitted to ${reviewable.provider.name}. +

+

+ ${reviewable.provider.name} has chosen to moderate their submissions using a pre-moderation workflow, which + means your submission is pending until accepted by a moderator. % if not no_future_emails: You will receive a separate notification informing you of any status changes. % endif -

-

- You will ${'not receive ' if no_future_emails else 'be automatically subscribed to '}future - notification emails for this ${document_type}. -

-

- If you have been erroneously associated with "${reviewable.title}", then you - may visit the ${document_type}'s "Edit" page and remove yourself as a contributor. -

-

- For more information about ${reviewable.provider.name}, visit - ${provider_url} to learn more. To learn about the - Open Science Framework, visit https://osf.io/. -

-

For questions regarding submission criteria, please email ${provider_contact_email}

-
- Sincerely,
- Your ${reviewable.provider.name} and OSF teams -

- Center for Open Science
- 210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903 -

- Privacy Policy -
+

+

+ You will ${'not receive ' if no_future_emails else 'be automatically subscribed to '}future notification emails + for this ${document_type}. +

+

+ If you have been erroneously associated with "${reviewable.title}", then you may visit the ${document_type}'s + "Edit" page and remove yourself as a contributor. +

+

+ For more information about ${reviewable.provider.name}, visit ${provider_url} to + learn more. To learn about the Open Science Framework, visit . +

+

+ For questions regarding submission criteria, please email ${provider_contact_email} +

+
+ Sincerely, +
+ Your ${reviewable.provider.name} and OSF teams +

+ Center for Open Science
210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903 +

+ Privacy Policy +
+ diff --git a/website/util/metrics.py b/website/util/metrics.py index bb40dac9915..7324a410138 100644 --- a/website/util/metrics.py +++ b/website/util/metrics.py @@ -28,6 +28,14 @@ def campaign_source_tag(campaign_name): return f'source:campaign|{campaign_name}' +def unregistered_created_source_tag(reffer_id): + return f'source:unregistered_created|{reffer_id}' + + +def institution_source_tag(institution_id): + return f'source:institution|{institution_id}' + + def provider_claimed_tag(provider_id, service=None): if service: return f'claimed:provider|{service}|{provider_id}'