From 00e671996e17e097bca526ae032e37123f3cd72d Mon Sep 17 00:00:00 2001 From: Douglas Cerna Date: Mon, 6 Jan 2025 08:25:52 -0600 Subject: [PATCH] Drop Python 3.8 support --- .github/workflows/test.yml | 1 - .pre-commit-config.yaml | 8 +- pyproject.toml | 4 +- requirements-dev.in | 5 +- requirements-dev.txt | 93 ++++++++++--------- requirements.in | 9 +- requirements.txt | 70 +++++++------- .../administration/migrations/0001_initial.py | 5 +- storage_service/common/backends.py | 11 +-- storage_service/common/helpers.py | 5 +- storage_service/common/startup.py | 4 +- .../locations/models/async_manager.py | 3 +- storage_service/locations/models/space.py | 3 +- .../storage_service/settings/base.py | 4 +- tests/administration/test_users.py | 13 ++- tests/integration/test_integration.py | 49 +++++----- tests/integration/test_oidc_auth.py | 7 +- tests/locations/test_archipelago.py | 69 +++++++------- tests/locations/test_gpg.py | 3 +- 19 files changed, 172 insertions(+), 194 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef90759fe..4d21b5644 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,6 @@ jobs: "22.04", ] python-version: [ - "3.8", "3.9", "3.10", "3.11", diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cdd27cdcf..abd9750ec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.0 + rev: v0.8.5 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -22,7 +22,7 @@ repos: - eslint-plugin-prettier@5.1.3 - prettier@3.0.3 - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.42.0 + rev: v0.43.0 hooks: - id: markdownlint exclude: | @@ -39,7 +39,7 @@ repos: install/README\.md ) - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.12.1 + rev: v1.14.1 hooks: - id: mypy additional_dependencies: @@ -47,7 +47,7 @@ repos: - types-python-dateutil - pytest - repo: https://github.com/tcort/markdown-link-check - rev: v3.12.2 + rev: v3.13.6 hooks: - id: markdown-link-check stages: [manual] diff --git a/pyproject.toml b/pyproject.toml index 32cdbb9c3..bd70f046a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ build-backend = "setuptools.build_meta" [project] name="archivematica-storage-service" description="Django based webapp for managing storage in an Archivematica installation" +requires-python = ">=3.9" authors = [ {name = "Artefactual Systems Inc.", email = "info@artefactual.com"}, ] @@ -105,12 +106,11 @@ ignore_errors = false legacy_tox_ini = """ [tox] skipsdist = True - envlist = linting, py{38,39,310,311,312}, migrations + envlist = linting, py{39,310,311,312}, migrations skip_missing_interpreters = true [gh-actions] python = - 3.8: py38 3.9: py39, migrations 3.10: py310 3.11: py311 diff --git a/requirements-dev.in b/requirements-dev.in index 699b7fa50..bbe636863 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -1,6 +1,7 @@ # Test dependencies go here. -r requirements.txt +coverage pip-tools pytest-cov pytest-django @@ -13,7 +14,3 @@ tox # gevent dependency in requirements.txt. # See https://github.com/microsoft/playwright-python/issues/2190 git+https://github.com/microsoft/playwright-python.git@d9cdfbb1e178b6770625e9f857139aff77516af0#egg=playwright - -# These dependencies dropped support for Python 3.8, so pinning them for now. -coverage==7.6.1 -pytest-randomly==3.15.0 diff --git a/requirements-dev.txt b/requirements-dev.txt index e981db5f6..444127af5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements-dev.txt requirements-dev.in # -agentarchives==0.9.0 +agentarchives==0.10.0 # via -r requirements.txt asgiref==3.8.1 # via @@ -12,9 +12,9 @@ asgiref==3.8.1 # django bagit==1.8.1 # via -r requirements.txt -boto3==1.35.49 +boto3==1.35.92 # via -r requirements.txt -botocore==1.35.49 +botocore==1.35.92 # via # -r requirements.txt # boto3 @@ -25,7 +25,7 @@ build==1.2.2.post1 # via pip-tools cachetools==5.5.0 # via tox -certifi==2024.8.30 +certifi==2024.12.14 # via # -r requirements.txt # requests @@ -35,19 +35,19 @@ cffi==1.17.1 # cryptography chardet==5.2.0 # via tox -charset-normalizer==3.4.0 +charset-normalizer==3.4.1 # via # -r requirements.txt # requests -click==8.1.7 +click==8.1.8 # via pip-tools colorama==0.4.6 # via tox -coverage[toml]==7.6.1 +coverage[toml]==7.6.10 # via # -r requirements-dev.in # pytest-cov -cryptography==43.0.3 +cryptography==44.0.0 # via # -r requirements.txt # josepy @@ -61,9 +61,9 @@ debtcollector==3.0.0 # python-keystoneclient distlib==0.3.9 # via virtualenv -dj-database-url==2.2.0 +dj-database-url==2.3.0 # via -r requirements.txt -django==4.2.16 +django==4.2.17 # via # -r requirements.txt # dj-database-url @@ -72,7 +72,7 @@ django==4.2.16 # django-csp # jsonfield # mozilla-django-oidc -django-auth-ldap==5.0.0 +django-auth-ldap==5.1.0 # via -r requirements.txt django-cas-ng==5.0.1 # via -r requirements.txt @@ -82,7 +82,7 @@ django-prometheus==2.3.1 # via -r requirements.txt django-shibboleth-remoteuser @ git+https://github.com/artefactual-labs/django-shibboleth-remoteuser.git@f08a7864d6130416c352981ccf318fff0fd5be58 # via -r requirements.txt -django-tastypie==0.14.7 +django-tastypie==0.15.0 # via -r requirements.txt exceptiongroup==1.2.2 # via pytest @@ -90,7 +90,7 @@ filelock==3.16.1 # via # tox # virtualenv -gevent==24.2.1 +gevent==24.11.1 # via -r requirements.txt greenlet==3.1.1 # via @@ -111,7 +111,7 @@ importlib-metadata==8.5.0 # via # build # pytest-randomly -importlib-resources==6.4.5 +importlib-resources==6.5.2 # via -r requirements.txt iniconfig==2.0.0 # via pytest @@ -131,7 +131,7 @@ josepy==1.14.0 # mozilla-django-oidc jsonfield==3.1.0 # via -r requirements.txt -keystoneauth1==5.8.0 +keystoneauth1==5.9.1 # via # -r requirements.txt # python-keystoneclient @@ -141,7 +141,7 @@ lxml==5.3.0 # metsrw # python-cas # sword2 -metsrw==0.5.1 +metsrw==0.6.0 # via -r requirements.txt mozilla-django-oidc==4.0.1 # via -r requirements.txt @@ -149,7 +149,7 @@ msgpack==1.1.0 # via # -r requirements.txt # oslo-serialization -mysqlclient==2.2.5 +mysqlclient==2.2.6 # via # -r requirements.txt # agentarchives @@ -158,34 +158,30 @@ netaddr==1.3.0 # -r requirements.txt # oslo-config # oslo-utils -netifaces==0.11.0 - # via - # -r requirements.txt - # oslo-utils os-service-types==1.7.0 # via # -r requirements.txt # keystoneauth1 -oslo-config==9.6.0 +oslo-config==9.7.0 # via # -r requirements.txt # python-keystoneclient -oslo-i18n==6.4.0 +oslo-i18n==6.5.0 # via # -r requirements.txt # oslo-config # oslo-utils # python-keystoneclient -oslo-serialization==5.5.0 +oslo-serialization==5.6.0 # via # -r requirements.txt # python-keystoneclient -oslo-utils==7.3.0 +oslo-utils==8.0.0 # via # -r requirements.txt # oslo-serialization # python-keystoneclient -packaging==24.1 +packaging==24.2 # via # -r requirements.txt # build @@ -218,10 +214,14 @@ pluggy==1.5.0 # via # pytest # tox -prometheus-client==0.21.0 +prometheus-client==0.21.1 # via # -r requirements.txt # django-prometheus +psutil==6.1.1 + # via + # -r requirements.txt + # oslo-utils pyasn1==0.6.1 # via # -r requirements.txt @@ -237,11 +237,11 @@ pycparser==2.22 # cffi pyee==12.0.0 # via playwright -pyopenssl==24.2.1 +pyopenssl==24.3.0 # via # -r requirements.txt # josepy -pyparsing==3.1.4 +pyparsing==3.2.1 # via # -r requirements.txt # httplib2 @@ -252,7 +252,7 @@ pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==8.3.3 +pytest==8.3.4 # via # -r requirements-dev.in # pytest-base-url @@ -262,13 +262,13 @@ pytest==8.3.3 # pytest-randomly pytest-base-url==2.1.0 # via pytest-playwright -pytest-cov==5.0.0 +pytest-cov==6.0.0 # via -r requirements-dev.in pytest-django==4.9.0 # via -r requirements-dev.in -pytest-playwright==0.5.2 +pytest-playwright==0.6.2 # via -r requirements-dev.in -pytest-randomly==3.15.0 +pytest-randomly==3.16.0 # via -r requirements-dev.in python-cas==1.6.0 # via @@ -315,20 +315,20 @@ rfc3986==2.0.0 # via # -r requirements.txt # oslo-config -s3transfer==0.10.3 +s3transfer==0.10.4 # via # -r requirements.txt # boto3 -six==1.16.0 +six==1.17.0 # via # -r requirements.txt # python-cas # python-dateutil -sqlparse==0.5.1 +sqlparse==0.5.3 # via # -r requirements.txt # django -stevedore==5.3.0 +stevedore==5.4.0 # via # -r requirements.txt # keystoneauth1 @@ -338,7 +338,7 @@ sword2 @ git+https://github.com/artefactual-labs/python-client-sword2.git@619ee4 # via -r requirements.txt text-unidecode==1.3 # via python-slugify -tomli==2.0.2 +tomli==2.2.1 # via # build # coverage @@ -353,6 +353,7 @@ typing-extensions==4.12.2 # -r requirements.txt # asgiref # dj-database-url + # keystoneauth1 # pyee # tox tzdata==2024.2 @@ -365,17 +366,17 @@ urllib3==1.26.20 # -r requirements.txt # botocore # requests -virtualenv==20.27.0 +virtualenv==20.28.1 # via tox -wheel==0.44.0 +wheel==0.45.1 # via pip-tools -whitenoise==6.7.0 +whitenoise==6.8.2 # via -r requirements.txt -wrapt==1.16.0 +wrapt==1.17.0 # via # -r requirements.txt # debtcollector -zipp==3.20.2 +zipp==3.21.0 # via # -r requirements.txt # importlib-metadata @@ -384,15 +385,15 @@ zope-event==5.0 # via # -r requirements.txt # gevent -zope-interface==7.1.1 +zope-interface==7.2 # via # -r requirements.txt # gevent # The following packages are considered to be unsafe in a requirements file: -pip==24.2 +pip==24.3.1 # via pip-tools -setuptools==75.2.0 +setuptools==75.6.0 # via # -r requirements.txt # pip-tools diff --git a/requirements.in b/requirements.in index ce3439f5c..2492ad37e 100644 --- a/requirements.in +++ b/requirements.in @@ -5,6 +5,8 @@ brotli Django>=4.2,<5 django-csp django-tastypie +dj-database-url +gevent gunicorn importlib_resources jsonfield @@ -23,15 +25,10 @@ django-prometheus # LDAP support python-ldap +django-auth-ldap # CAS authentication django-cas-ng # Required for OpenID Connect authentication mozilla-django-oidc - -# These dependencies dropped support for Python 3.8, so pinning them for now. -dj-database-url==2.2.0 -django-auth-ldap==5.0.0 -gevent==24.2.1 -pyparsing==3.1.4 diff --git a/requirements.txt b/requirements.txt index 3e46541d8..6a92a1b37 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,27 +4,27 @@ # # pip-compile --allow-unsafe --output-file=requirements.txt requirements.in # -agentarchives==0.9.0 +agentarchives==0.10.0 # via -r requirements.in asgiref==3.8.1 # via django bagit==1.8.1 # via -r requirements.in -boto3==1.35.49 +boto3==1.35.92 # via -r requirements.in -botocore==1.35.49 +botocore==1.35.92 # via # boto3 # s3transfer brotli==1.1.0 # via -r requirements.in -certifi==2024.8.30 +certifi==2024.12.14 # via requests cffi==1.17.1 # via cryptography -charset-normalizer==3.4.0 +charset-normalizer==3.4.1 # via requests -cryptography==43.0.3 +cryptography==44.0.0 # via # josepy # mozilla-django-oidc @@ -34,9 +34,9 @@ debtcollector==3.0.0 # oslo-config # oslo-utils # python-keystoneclient -dj-database-url==2.2.0 +dj-database-url==2.3.0 # via -r requirements.in -django==4.2.16 +django==4.2.17 # via # -r requirements.in # dj-database-url @@ -45,7 +45,7 @@ django==4.2.16 # django-csp # jsonfield # mozilla-django-oidc -django-auth-ldap==5.0.0 +django-auth-ldap==5.1.0 # via -r requirements.in django-cas-ng==5.0.1 # via -r requirements.in @@ -55,9 +55,9 @@ django-prometheus==2.3.1 # via -r requirements.in django-shibboleth-remoteuser @ git+https://github.com/artefactual-labs/django-shibboleth-remoteuser.git@f08a7864d6130416c352981ccf318fff0fd5be58 # via -r requirements.in -django-tastypie==0.14.7 +django-tastypie==0.15.0 # via -r requirements.in -gevent==24.2.1 +gevent==24.11.1 # via -r requirements.in greenlet==3.1.1 # via gevent @@ -67,7 +67,7 @@ httplib2==0.22.0 # via sword2 idna==3.10 # via requests -importlib-resources==6.4.5 +importlib-resources==6.5.2 # via -r requirements.in iso8601==2.1.0 # via @@ -81,7 +81,7 @@ josepy==1.14.0 # via mozilla-django-oidc jsonfield==3.1.0 # via -r requirements.in -keystoneauth1==5.8.0 +keystoneauth1==5.9.1 # via python-keystoneclient lxml==5.3.0 # via @@ -89,36 +89,34 @@ lxml==5.3.0 # metsrw # python-cas # sword2 -metsrw==0.5.1 +metsrw==0.6.0 # via -r requirements.in mozilla-django-oidc==4.0.1 # via -r requirements.in msgpack==1.1.0 # via oslo-serialization -mysqlclient==2.2.5 +mysqlclient==2.2.6 # via agentarchives netaddr==1.3.0 # via # oslo-config # oslo-utils -netifaces==0.11.0 - # via oslo-utils os-service-types==1.7.0 # via keystoneauth1 -oslo-config==9.6.0 +oslo-config==9.7.0 # via python-keystoneclient -oslo-i18n==6.4.0 +oslo-i18n==6.5.0 # via # oslo-config # oslo-utils # python-keystoneclient -oslo-serialization==5.5.0 +oslo-serialization==5.6.0 # via python-keystoneclient -oslo-utils==7.3.0 +oslo-utils==8.0.0 # via # oslo-serialization # python-keystoneclient -packaging==24.1 +packaging==24.2 # via # gunicorn # oslo-utils @@ -131,10 +129,12 @@ pbr==6.1.0 # oslo-serialization # python-keystoneclient # stevedore -prometheus-client==0.21.0 +prometheus-client==0.21.1 # via # -r requirements.in # django-prometheus +psutil==6.1.1 + # via oslo-utils pyasn1==0.6.1 # via # pyasn1-modules @@ -143,11 +143,10 @@ pyasn1-modules==0.4.1 # via python-ldap pycparser==2.22 # via cffi -pyopenssl==24.2.1 +pyopenssl==24.3.0 # via josepy -pyparsing==3.1.4 +pyparsing==3.2.1 # via - # -r requirements.in # httplib2 # oslo-utils python-cas==1.6.0 @@ -184,15 +183,15 @@ requests==2.32.3 # python-swiftclient rfc3986==2.0.0 # via oslo-config -s3transfer==0.10.3 +s3transfer==0.10.4 # via boto3 -six==1.16.0 +six==1.17.0 # via # python-cas # python-dateutil -sqlparse==0.5.1 +sqlparse==0.5.3 # via django -stevedore==5.3.0 +stevedore==5.4.0 # via # keystoneauth1 # oslo-config @@ -203,6 +202,7 @@ typing-extensions==4.12.2 # via # asgiref # dj-database-url + # keystoneauth1 tzdata==2024.2 # via # oslo-serialization @@ -211,19 +211,19 @@ urllib3==1.26.20 # via # botocore # requests -whitenoise==6.7.0 +whitenoise==6.8.2 # via -r requirements.in -wrapt==1.16.0 +wrapt==1.17.0 # via debtcollector -zipp==3.20.2 +zipp==3.21.0 # via importlib-resources zope-event==5.0 # via gevent -zope-interface==7.1.1 +zope-interface==7.2 # via gevent # The following packages are considered to be unsafe in a requirements file: -setuptools==75.2.0 +setuptools==75.6.0 # via # zope-event # zope-interface diff --git a/storage_service/administration/migrations/0001_initial.py b/storage_service/administration/migrations/0001_initial.py index fde0e05ee..46028f432 100644 --- a/storage_service/administration/migrations/0001_initial.py +++ b/storage_service/administration/migrations/0001_initial.py @@ -1,12 +1,9 @@ -from typing import List -from typing import Tuple - from django.db import migrations from django.db import models class Migration(migrations.Migration): - dependencies: List[Tuple[str, str]] = [] + dependencies: list[tuple[str, str]] = [] operations = [ migrations.CreateModel( diff --git a/storage_service/common/backends.py b/storage_service/common/backends.py index 38e092713..91e2f540f 100644 --- a/storage_service/common/backends.py +++ b/storage_service/common/backends.py @@ -1,6 +1,5 @@ import json from typing import Any -from typing import Dict from administration import roles from django.conf import settings @@ -66,8 +65,8 @@ def authenticate(self, request, **kwargs): return super().authenticate(request, **kwargs) def get_userinfo( - self, access_token: str, id_token: str, verified_id: Dict[str, Any] - ) -> Dict[str, Any]: + self, access_token: str, id_token: str, verified_id: dict[str, Any] + ) -> dict[str, Any]: """Extract user details from JSON web tokens. It returns a dict of user details that will be applied directly to the @@ -82,7 +81,7 @@ def decode_token(token: str) -> Any: access_info = decode_token(access_token) id_info = decode_token(id_token) - info: Dict[str, Any] = {} + info: dict[str, Any] = {} for oidc_attr, user_attr in settings.OIDC_ACCESS_ATTRIBUTE_MAP.items(): if oidc_attr in access_info: @@ -94,14 +93,14 @@ def decode_token(token: str) -> Any: return info - def create_user(self, user_info: Dict[str, Any]) -> User: + def create_user(self, user_info: dict[str, Any]) -> User: user = super().create_user(user_info) for attr, value in user_info.items(): setattr(user, attr, value) self.set_user_role(user) return user - def update_user(self, user: User, user_info: Dict[str, Any]) -> User: + def update_user(self, user: User, user_info: dict[str, Any]) -> User: self.set_user_role(user) return user diff --git a/storage_service/common/helpers.py b/storage_service/common/helpers.py index 87f418770..6692c140b 100644 --- a/storage_service/common/helpers.py +++ b/storage_service/common/helpers.py @@ -1,7 +1,6 @@ +from collections.abc import Iterable from os import environ from typing import Any -from typing import Dict -from typing import Iterable from django.core.exceptions import ImproperlyConfigured @@ -21,7 +20,7 @@ def is_true(env_str: str) -> bool: def get_oidc_secondary_providers( oidc_secondary_provider_names: Iterable[str], -) -> Dict[str, Dict[str, str]]: +) -> dict[str, dict[str, str]]: providers = {} for provider_name in oidc_secondary_provider_names: diff --git a/storage_service/common/startup.py b/storage_service/common/startup.py index dd3c5c181..8c4f47bbb 100644 --- a/storage_service/common/startup.py +++ b/storage_service/common/startup.py @@ -2,8 +2,6 @@ import logging import os import pathlib -from typing import Dict -from typing import List from typing import Optional from typing import Union @@ -85,7 +83,7 @@ def populate_default_locations(space_path: pathlib.Path) -> None: LOGGER.info("Multiple default Spaces exist, done default setup.") return - default_locations: List[Dict[str, Union[str, pathlib.Path, bool, None]]] = [ + default_locations: list[dict[str, Union[str, pathlib.Path, bool, None]]] = [ { "purpose": locations_models.Location.TRANSFER_SOURCE, "relative_path": "home", diff --git a/storage_service/locations/models/async_manager.py b/storage_service/locations/models/async_manager.py index 4f336266a..90ec6db39 100644 --- a/storage_service/locations/models/async_manager.py +++ b/storage_service/locations/models/async_manager.py @@ -14,7 +14,6 @@ import threading import time import traceback -from typing import List from django.utils import timezone @@ -48,7 +47,7 @@ def __init__(self): class AsyncManager: - running_tasks: List[Async] = [] + running_tasks: list[Async] = [] lock = threading.Lock() @staticmethod diff --git a/storage_service/locations/models/space.py b/storage_service/locations/models/space.py index c38ce07d4..b29fc1407 100644 --- a/storage_service/locations/models/space.py +++ b/storage_service/locations/models/space.py @@ -8,7 +8,6 @@ import subprocess import tempfile import uuid -from typing import Set from common import fields from common import utils @@ -113,7 +112,7 @@ class Space(models.Model): GPG = "GPG" S3 = "S3" # These will not be displayed in the Space Create GUI (see locations/forms.py) - BETA_PROTOCOLS: Set[str] = set() + BETA_PROTOCOLS: set[str] = set() OBJECT_STORAGE = { ARCHIPELAGO, DATAVERSE, diff --git a/storage_service/storage_service/settings/base.py b/storage_service/storage_service/settings/base.py index bd2a218b2..5a76479fa 100644 --- a/storage_service/storage_service/settings/base.py +++ b/storage_service/storage_service/settings/base.py @@ -6,8 +6,6 @@ from pathlib import Path from sys import path from typing import Any -from typing import Dict -from typing import List from common.helpers import get_oidc_secondary_providers from common.helpers import is_true @@ -159,7 +157,7 @@ def _get_settings_from_file(path): # ######## TEMPLATE CONFIGURATION -TEMPLATES: List[Dict[str, Any]] = [ +TEMPLATES: list[dict[str, Any]] = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [str(SITE_ROOT / "templates")], diff --git a/tests/administration/test_users.py b/tests/administration/test_users.py index f534a630e..cd51fa764 100644 --- a/tests/administration/test_users.py +++ b/tests/administration/test_users.py @@ -1,7 +1,6 @@ import hmac import uuid from hashlib import sha1 -from typing import Type from unittest import mock import pytest @@ -64,7 +63,7 @@ def test_create_user_as_admin( def test_create_user_as_non_admin( admin_client: Client, settings: pytest_django.fixtures.SettingsWrapper, - django_user_model: Type[User], + django_user_model: type[User], ) -> None: """Only administrators are allowed to create new users.""" as_reader(django_user_model.objects.get(username="admin")) @@ -87,7 +86,7 @@ def test_create_user_as_non_admin( @pytest.fixture -def user(django_user_model: Type[User]) -> User: +def user(django_user_model: type[User]) -> User: return django_user_model.objects.create_user( username="test", password="ck61Qc873.KxoZ5G", email="test@example.com" ) @@ -121,7 +120,7 @@ def test_edit_user_promote_as_manager( def test_edit_user_promotion_requires_admin( admin_client: Client, settings: pytest_django.fixtures.SettingsWrapper, - django_user_model: Type[User], + django_user_model: type[User], user: User, ) -> None: """Only administrators are allowed to promote/demote users.""" @@ -144,7 +143,7 @@ def test_edit_user_promotion_requires_admin( @pytest.fixture -def admin_user_apikey(admin_client: Client, django_user_model: Type[User]) -> ApiKey: +def admin_user_apikey(admin_client: Client, django_user_model: type[User]) -> ApiKey: return ApiKey.objects.create(user=django_user_model.objects.get(username="admin")) @@ -152,7 +151,7 @@ def admin_user_apikey(admin_client: Client, django_user_model: Type[User]) -> Ap def test_user_edit_view_updates_password( admin_client: Client, settings: pytest_django.fixtures.SettingsWrapper, - django_user_model: Type[User], + django_user_model: type[User], admin_user_apikey: ApiKey, ) -> None: user = django_user_model.objects.get(username="admin") @@ -179,7 +178,7 @@ def test_user_edit_view_updates_password( def test_user_edit_view_regenerates_api_key( admin_client: Client, settings: pytest_django.fixtures.SettingsWrapper, - django_user_model: Type[User], + django_user_model: type[User], admin_user_apikey: ApiKey, ) -> None: user = django_user_model.objects.get(username="admin") diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index fd998abc6..b237d9047 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -16,9 +16,6 @@ import tarfile import uuid from pathlib import Path -from typing import Dict -from typing import List -from typing import Tuple from typing import Union import pytest @@ -38,25 +35,25 @@ TagName = str Attribute = str Value = str -Element = Tuple[Attribute, Value] +Element = tuple[Attribute, Value] -PremisAgent = Tuple[ +PremisAgent = tuple[ TagName, - Dict[str, str], - Tuple[TagName, Element, Element], + dict[str, str], + tuple[TagName, Element, Element], Element, Element, ] -PremisEvent = Tuple[ +PremisEvent = tuple[ TagName, - Dict[str, str], - Tuple[TagName, Element, Element], + dict[str, str], + tuple[TagName, Element, Element], Element, Element, Element, - Tuple[TagName, Tuple[TagName, Element]], - Tuple[TagName, Element, Element], + tuple[TagName, tuple[TagName, Element]], + tuple[TagName, Element, Element], ] FIXTURES_DIR = Path(__file__).parent / "fixtures" @@ -75,26 +72,26 @@ class Client: def __init__(self, admin_client: TestClient) -> None: self.admin_client = admin_client - def add_space(self, data: Dict[str, Union[str, bool]]) -> HttpResponse: + def add_space(self, data: dict[str, Union[str, bool]]) -> HttpResponse: return self.admin_client.post( "/api/v2/space/", json.dumps(data), content_type="application/json" ) - def add_pipeline(self, data: Dict[str, Union[str, bool]]) -> HttpResponse: + def add_pipeline(self, data: dict[str, Union[str, bool]]) -> HttpResponse: return self.admin_client.post( "/api/v2/pipeline/", json.dumps(data), content_type="application/json" ) - def get_pipelines(self, data: Dict[str, str]) -> HttpResponse: + def get_pipelines(self, data: dict[str, str]) -> HttpResponse: return self.admin_client.get("/api/v2/pipeline/", data) - def add_location(self, data: Dict[str, Union[str, List[str]]]) -> HttpResponse: + def add_location(self, data: dict[str, Union[str, list[str]]]) -> HttpResponse: return self.admin_client.post( "/api/v2/location/", json.dumps(data), content_type="application/json" ) def set_location( - self, location_id: uuid.UUID, data: Dict[str, str] + self, location_id: uuid.UUID, data: dict[str, str] ) -> HttpResponse: return self.admin_client.post( f"/api/v2/location/{location_id}/", @@ -102,13 +99,13 @@ def set_location( content_type="application/json", ) - def get_locations(self, data: Dict[str, str]) -> HttpResponse: + def get_locations(self, data: dict[str, str]) -> HttpResponse: return self.admin_client.get("/api/v2/location/", data) def add_file( self, file_id: uuid.UUID, - data: Dict[str, Union[str, int, List[PremisEvent], List[PremisAgent]]], + data: dict[str, Union[str, int, list[PremisEvent], list[PremisAgent]]], ) -> HttpResponse: return self.admin_client.put( f"/api/v2/file/{file_id}/", @@ -126,7 +123,7 @@ def check_fixity(self, file_id: uuid.UUID) -> HttpResponse: return self.admin_client.get(f"/api/v2/file/{file_id}/check_fixity/") def request_aip_recovery( - self, file_id: uuid.UUID, data: Dict[str, Union[str, int]] + self, file_id: uuid.UUID, data: dict[str, Union[str, int]] ) -> HttpResponse: return self.admin_client.post( f"/api/v2/file/{file_id}/recover_aip/", @@ -205,7 +202,7 @@ class StorageScenario: PIPELINE_UUID = uuid.UUID("00000b87-1655-4b7e-bbf8-344b317da334") PACKAGE_UUID = uuid.UUID("5658e603-277b-4292-9b58-20bf261c8f88") - SPACES: Dict[str, Dict[str, Union[str, bool]]] = { + SPACES: dict[str, dict[str, Union[str, bool]]] = { Space.S3: { "access_protocol": Space.S3, "path": "", @@ -268,8 +265,8 @@ def register_pipeline(self) -> None: assert resp.status_code == 201 def _adjust_space_data( - self, data: Dict[str, Union[str, bool]] - ) -> Dict[str, Union[str, bool]]: + self, data: dict[str, Union[str, bool]] + ) -> dict[str, Union[str, bool]]: for attr in ["path", "staging_path"]: if ( (value := data.get(attr) is not None) @@ -563,14 +560,14 @@ def copy_fixture_to_aip_recovery_location(self) -> None: self.copy_fixture(aip_recovery_location_path) - def request_aip_recovery(self, data: Dict[str, Union[str, int]]) -> HttpResponse: + def request_aip_recovery(self, data: dict[str, Union[str, int]]) -> HttpResponse: return self.client.request_aip_recovery(self.PACKAGE_UUID, data) def approve_aip_recovery_request(self, event_id: int) -> HttpResponse: return self.client.approve_aip_recovery_request(event_id) def recover_aip(self) -> None: - data: Dict[str, Union[str, int]] = { + data: dict[str, Union[str, int]] = { "event_reason": "Delete please!", "pipeline": str(self.PIPELINE_UUID), "user_id": 1, @@ -709,7 +706,7 @@ def test_aip_recovery_handles_recovery_copy_setup_error( scenario.assert_stored() scenario.corrupt_package() - data: Dict[str, Union[str, int]] = { + data: dict[str, Union[str, int]] = { "event_reason": "Delete please!", "pipeline": str(scenario.PIPELINE_UUID), "user_id": 1, diff --git a/tests/integration/test_oidc_auth.py b/tests/integration/test_oidc_auth.py index 0dd6fd207..3103d5d00 100644 --- a/tests/integration/test_oidc_auth.py +++ b/tests/integration/test_oidc_auth.py @@ -1,6 +1,5 @@ import os -from typing import Generator -from typing import Type +from collections.abc import Generator import pytest from django.contrib.auth.models import Group @@ -33,7 +32,7 @@ def recreate_user_groups( @pytest.fixture -def user(django_user_model: Type[User]) -> User: +def user(django_user_model: type[User]) -> User: user = django_user_model.objects.create( username="foobar", email="foobar@example.com", @@ -50,7 +49,7 @@ def user(django_user_model: Type[User]) -> User: def test_oidc_backend_creates_local_user( page: Page, live_server: LiveServer, - django_user_model: Type[User], + django_user_model: type[User], ) -> None: page.goto(live_server.url) diff --git a/tests/locations/test_archipelago.py b/tests/locations/test_archipelago.py index 9b9815efd..fc678599d 100644 --- a/tests/locations/test_archipelago.py +++ b/tests/locations/test_archipelago.py @@ -31,19 +31,17 @@ def test_move_from_storage_service(archipelago_space): destination_path = "/path/to/destination" package = Mock(uuid="package_uuid") - with patch("os.path.basename") as mock_basename, patch( - "os.path.exists" - ) as mock_exists, patch( - "locations.models.Archipelago._get_metadata" - ) as mock_get_metadata, patch( - "locations.models.Archipelago.extract_title_from_mets_xml" - ) as mock_extract_title, patch( - "locations.models.Archipelago._upload_file" - ) as mock_upload_file, patch( - "locations.models.Archipelago.get_dc_metadata" - ) as mock_get_dc_metadata, patch( - "locations.models.Archipelago._upload_metadata" - ) as mock_upload_metadata: + with ( + patch("os.path.basename") as mock_basename, + patch("os.path.exists") as mock_exists, + patch("locations.models.Archipelago._get_metadata") as mock_get_metadata, + patch( + "locations.models.Archipelago.extract_title_from_mets_xml" + ) as mock_extract_title, + patch("locations.models.Archipelago._upload_file") as mock_upload_file, + patch("locations.models.Archipelago.get_dc_metadata") as mock_get_dc_metadata, + patch("locations.models.Archipelago._upload_metadata") as mock_upload_metadata, + ): mock_basename.return_value = "filename" mock_exists.return_value = True mock_get_metadata.return_value = "mets_xml" @@ -70,17 +68,16 @@ def test_move_from_storage_service_no_source(archipelago_space): package = Mock(uuid="package_uuid") expected_result = None - with patch("os.path.basename") as mock_basename, patch( - "os.path.exists" - ) as mock_exists, patch( - "locations.models.Archipelago._get_metadata" - ) as mock_get_metadata, patch( - "locations.models.Archipelago.extract_title_from_mets_xml" - ) as mock_extract_title, patch( - "locations.models.Archipelago._upload_file" - ) as mock_upload_file, patch( - "locations.models.Archipelago.get_dc_metadata" - ) as mock_get_dc_metadata: + with ( + patch("os.path.basename") as mock_basename, + patch("os.path.exists") as mock_exists, + patch("locations.models.Archipelago._get_metadata") as mock_get_metadata, + patch( + "locations.models.Archipelago.extract_title_from_mets_xml" + ) as mock_extract_title, + patch("locations.models.Archipelago._upload_file") as mock_upload_file, + patch("locations.models.Archipelago.get_dc_metadata") as mock_get_dc_metadata, + ): mock_basename.return_value = "filename" mock_exists.return_value = False mock_get_metadata.return_value = "mets_xml" @@ -100,8 +97,9 @@ def test_upload_file(archipelago_space): source_path = "/path/to/test.zip" expected_fid = "12345" - with patch("requests.post") as mock_post, patch( - "builtins.open", mock_open(read_data="data") + with ( + patch("requests.post") as mock_post, + patch("builtins.open", mock_open(read_data="data")), ): mock_response = mock_post.return_value mock_response.status_code = 201 @@ -131,8 +129,9 @@ def test_upload_file_error(archipelago_space): filename = "test.zip" source_path = "/path/to/test.zip" - with patch("requests.post") as mock_post, patch( - "builtins.open", mock_open(read_data="data") + with ( + patch("requests.post") as mock_post, + patch("builtins.open", mock_open(read_data="data")), ): mock_response = mock_post.return_value mock_response.status_code = 500 @@ -212,9 +211,10 @@ def test_upload_metadata(archipelago_space): strawberry = '{"key": "value"}' title = "Test Title" - with patch("requests.post") as mock_post, patch( - "locations.models.archipelago.LOGGER.info" - ) as mock_logger_info: + with ( + patch("requests.post") as mock_post, + patch("locations.models.archipelago.LOGGER.info") as mock_logger_info, + ): mock_response = mock_post.return_value mock_response.status_code = 201 mock_response.raise_for_status.return_value = None @@ -268,9 +268,10 @@ def test_upload_metadata_error(archipelago_space): archipelago_user = "username" archipelago_password = "password" - with patch("requests.post") as mock_post, patch( - "locations.models.archipelago.LOGGER.error" - ) as mock_logger_error: + with ( + patch("requests.post") as mock_post, + patch("locations.models.archipelago.LOGGER.error") as mock_logger_error, + ): mock_response = mock_post.return_value mock_response.status_code = 500 mock_response.text = "Internal Server Error" diff --git a/tests/locations/test_gpg.py b/tests/locations/test_gpg.py index ad879d7b2..ebbea8270 100644 --- a/tests/locations/test_gpg.py +++ b/tests/locations/test_gpg.py @@ -4,7 +4,6 @@ import pathlib from collections import namedtuple from typing import Any -from typing import Dict from unittest import mock import pytest @@ -40,7 +39,7 @@ ) ) ] -BROWSE_FAIL_DICT: Dict[str, Any] = {"directories": [], "entries": [], "properties": {}} +BROWSE_FAIL_DICT: dict[str, Any] = {"directories": [], "entries": [], "properties": {}} FakeGPGRet = namedtuple("FakeGPGRet", "ok status stderr")