diff --git a/.gitignore b/.gitignore index 63dc29fef..301b915d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ staticfiles/ +/.ruff_cache/ # Created by https://www.gitignore.io/api/django # Edit at https://www.gitignore.io/?templates=django @@ -123,4 +124,4 @@ dmypy.json # End of https://www.gitignore.io/api/django # Editor settings -.vscode +.vscode diff --git a/dandiapi/__init__.py b/dandiapi/__init__.py index f35d40bf1..aabca329d 100644 --- a/dandiapi/__init__.py +++ b/dandiapi/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from importlib.metadata import version # This project module is imported for us when Django starts. To ensure that Celery app is always diff --git a/dandiapi/analytics/apps.py b/dandiapi/analytics/apps.py index 59aaa07c4..42d602b34 100644 --- a/dandiapi/analytics/apps.py +++ b/dandiapi/analytics/apps.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.apps import AppConfig diff --git a/dandiapi/analytics/migrations/0001_initial_v2.py b/dandiapi/analytics/migrations/0001_initial_v2.py index 11d72d202..6ee97e473 100644 --- a/dandiapi/analytics/migrations/0001_initial_v2.py +++ b/dandiapi/analytics/migrations/0001_initial_v2.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import django.core.validators from django.db import migrations, models diff --git a/dandiapi/analytics/models.py b/dandiapi/analytics/models.py index 39aed4edc..40b2996d2 100644 --- a/dandiapi/analytics/models.py +++ b/dandiapi/analytics/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.core.validators import RegexValidator from django.db import models diff --git a/dandiapi/analytics/tasks/__init__.py b/dandiapi/analytics/tasks/__init__.py index 8b86ad383..2f3037c0c 100644 --- a/dandiapi/analytics/tasks/__init__.py +++ b/dandiapi/analytics/tasks/__init__.py @@ -1,6 +1,8 @@ +from __future__ import annotations + from collections import Counter -from collections.abc import Generator from pathlib import Path +from typing import TYPE_CHECKING from celery.app import shared_task from celery.utils.log import get_task_logger @@ -15,6 +17,9 @@ from dandiapi.api.models.asset import AssetBlob, EmbargoedAssetBlob from dandiapi.api.storage import get_boto_client, get_embargo_storage, get_storage +if TYPE_CHECKING: + from collections.abc import Generator + logger = get_task_logger(__name__) # should be one of the DANDI_DANDISETS_*_LOG_BUCKET_NAME settings @@ -77,14 +82,14 @@ def process_s3_log_file_task(bucket: LogBucket, s3_log_key: str) -> None: return s3 = get_boto_client(get_storage() if not embargoed else get_embargo_storage()) - BlobModel = AssetBlob if not embargoed else EmbargoedAssetBlob + BlobModel = AssetBlob if not embargoed else EmbargoedAssetBlob # noqa: N806 data = s3.get_object(Bucket=bucket, Key=s3_log_key) download_counts = Counter() for log_entry in s3logparse.parse_log_lines( line.decode('utf8') for line in data['Body'].iter_lines() ): - if log_entry.operation == 'REST.GET.OBJECT' and log_entry.status_code == 200: + if log_entry.operation == 'REST.GET.OBJECT' and log_entry.status_code == 200: # noqa: PLR2004 download_counts.update({log_entry.s3_key: 1}) with transaction.atomic(): diff --git a/dandiapi/analytics/tests/test_download_counts.py b/dandiapi/analytics/tests/test_download_counts.py index 0eab45809..4b1f6a480 100644 --- a/dandiapi/analytics/tests/test_download_counts.py +++ b/dandiapi/analytics/tests/test_download_counts.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.conf import settings import pytest diff --git a/dandiapi/api/__init__.py b/dandiapi/api/__init__.py index 282a42b22..0fa5df86a 100644 --- a/dandiapi/api/__init__.py +++ b/dandiapi/api/__init__.py @@ -1,3 +1,4 @@ -# flake8: noqa +from __future__ import annotations + # TODO: remove this after migration is complete -import dandiapi.api.user_migration +import dandiapi.api.user_migration # noqa: F401 diff --git a/dandiapi/api/admin.py b/dandiapi/api/admin.py index ee4a78077..06abb2e85 100644 --- a/dandiapi/api/admin.py +++ b/dandiapi/api/admin.py @@ -1,4 +1,7 @@ +from __future__ import annotations + import csv +from typing import TYPE_CHECKING from allauth.socialaccount.models import SocialAccount from django.contrib import admin, messages @@ -8,7 +11,6 @@ from django.db.models.aggregates import Count from django.db.models.query import Prefetch, QuerySet from django.forms.models import BaseInlineFormSet -from django.http.request import HttpRequest from django.http.response import HttpResponse from django.urls import reverse from django.utils.html import format_html @@ -26,6 +28,9 @@ from dandiapi.api.views.users import social_account_to_dict from dandiapi.zarr.tasks import ingest_dandiset_zarrs +if TYPE_CHECKING: + from django.http.request import HttpRequest + admin.site.site_header = 'DANDI Admin' admin.site.site_title = 'DANDI Admin' diff --git a/dandiapi/api/apps.py b/dandiapi/api/apps.py index bc6ef64b0..93d3f29d6 100644 --- a/dandiapi/api/apps.py +++ b/dandiapi/api/apps.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import re @@ -14,7 +16,7 @@ class PublishConfig(AppConfig): verbose_name = 'DANDI: Publish' @staticmethod - def _get_sentry_performance_sample_rate(*args, **kwargs) -> float: + def _get_sentry_performance_sample_rate(*args, **kwargs) -> float: # noqa: ARG004 from dandiapi.api.models.asset import Asset from dandiapi.api.models.dandiset import Dandiset from dandiapi.api.models.version import Version @@ -44,7 +46,8 @@ def is_noisy(): return 0.001 if is_noisy() else 0.01 def ready(self): - import dandiapi.api.checks # noqa: F401 + # RUF100 is caused by https://github.com/astral-sh/ruff/issues/60 + import dandiapi.api.checks # noqa: F401, RUF100 import dandiapi.api.signals # noqa: F401 if hasattr(settings, 'SENTRY_DSN'): diff --git a/dandiapi/api/checks.py b/dandiapi/api/checks.py index f53861126..a3339490e 100644 --- a/dandiapi/api/checks.py +++ b/dandiapi/api/checks.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.conf import settings from django.core.checks import Error, register diff --git a/dandiapi/api/doi.py b/dandiapi/api/doi.py index 0a549edc4..92f272d54 100644 --- a/dandiapi/api/doi.py +++ b/dandiapi/api/doi.py @@ -1,9 +1,13 @@ +from __future__ import annotations + import logging +from typing import TYPE_CHECKING from django.conf import settings import requests -from dandiapi.api.models import Version +if TYPE_CHECKING: + from dandiapi.api.models import Version # All of the required DOI configuration settings DANDI_DOI_SETTINGS = [ diff --git a/dandiapi/api/garbage.py b/dandiapi/api/garbage.py index 6efae1ecd..9ca9f48b7 100644 --- a/dandiapi/api/garbage.py +++ b/dandiapi/api/garbage.py @@ -1,11 +1,16 @@ +from __future__ import annotations + from datetime import timedelta +from typing import TYPE_CHECKING from django.db.models import Exists, OuterRef -from django.db.models.query import QuerySet from django.utils import timezone from dandiapi.api.models import Asset, Version +if TYPE_CHECKING: + from django.db.models.query import QuerySet + # How long after the last modification things are eligible for deletion STALE_TIME_INTERVAL = timedelta(days=7) diff --git a/dandiapi/api/mail.py b/dandiapi/api/mail.py index a85389136..d786b3201 100644 --- a/dandiapi/api/mail.py +++ b/dandiapi/api/mail.py @@ -1,14 +1,18 @@ from __future__ import annotations -from collections.abc import Iterable import logging +from typing import TYPE_CHECKING -from allauth.socialaccount.models import SocialAccount from django.conf import settings -from django.contrib.auth.models import User from django.core import mail from django.template.loader import render_to_string +if TYPE_CHECKING: + from collections.abc import Iterable + + from allauth.socialaccount.models import SocialAccount + from django.contrib.auth.models import User + logger = logging.getLogger(__name__) BASE_RENDER_CONTEXT = { diff --git a/dandiapi/api/management/commands/cleanup_blobs.py b/dandiapi/api/management/commands/cleanup_blobs.py index bec0976f7..4051616f8 100644 --- a/dandiapi/api/management/commands/cleanup_blobs.py +++ b/dandiapi/api/management/commands/cleanup_blobs.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.conf import settings import djclick as click from storages.backends.s3 import S3Storage diff --git a/dandiapi/api/management/commands/collect_garbage.py b/dandiapi/api/management/commands/collect_garbage.py index a2878363f..51bf64249 100644 --- a/dandiapi/api/management/commands/collect_garbage.py +++ b/dandiapi/api/management/commands/collect_garbage.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import djclick as click from dandiapi.api.garbage import stale_assets diff --git a/dandiapi/api/management/commands/create_dev_dandiset.py b/dandiapi/api/management/commands/create_dev_dandiset.py index a07bfdc40..a2819c2cb 100644 --- a/dandiapi/api/management/commands/create_dev_dandiset.py +++ b/dandiapi/api/management/commands/create_dev_dandiset.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from uuid import uuid4 from django.conf import settings diff --git a/dandiapi/api/management/commands/depose_placeholder.py b/dandiapi/api/management/commands/depose_placeholder.py index 8b1c9501a..651916218 100644 --- a/dandiapi/api/management/commands/depose_placeholder.py +++ b/dandiapi/api/management/commands/depose_placeholder.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.contrib.auth.models import User import djclick as click diff --git a/dandiapi/api/management/commands/depose_placeholders.py b/dandiapi/api/management/commands/depose_placeholders.py index 8fa80c923..1fd7d5cfd 100644 --- a/dandiapi/api/management/commands/depose_placeholders.py +++ b/dandiapi/api/management/commands/depose_placeholders.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.contrib.auth.models import User import djclick as click diff --git a/dandiapi/api/management/commands/ingest_asset_paths.py b/dandiapi/api/management/commands/ingest_asset_paths.py index 42c99fa0f..ae3fe0a54 100644 --- a/dandiapi/api/management/commands/ingest_asset_paths.py +++ b/dandiapi/api/management/commands/ingest_asset_paths.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import djclick as click from dandiapi.api.asset_paths import add_version_asset_paths diff --git a/dandiapi/api/management/commands/list_placeholders.py b/dandiapi/api/management/commands/list_placeholders.py index b85ef42a2..86f462af7 100644 --- a/dandiapi/api/management/commands/list_placeholders.py +++ b/dandiapi/api/management/commands/list_placeholders.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.contrib.auth.models import User import djclick as click diff --git a/dandiapi/api/management/commands/migrate_published_version_metadata.py b/dandiapi/api/management/commands/migrate_published_version_metadata.py index 6b331f951..df648acc4 100644 --- a/dandiapi/api/management/commands/migrate_published_version_metadata.py +++ b/dandiapi/api/management/commands/migrate_published_version_metadata.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from difflib import ndiff from pprint import pformat @@ -14,7 +16,8 @@ @click.argument('to_version') def migrate_published_version_metadata(*, dandiset: str, published_version: str, to_version: str): click.echo( - f'Migrating published version {dandiset}/{published_version} metadata to version {to_version}' # noqa: E501 + f'Migrating published version {dandiset}/{published_version} ' + f'metadata to version {to_version}' ) version = Version.objects.exclude(version='draft').get( dandiset=dandiset, version=published_version @@ -23,7 +26,7 @@ def migrate_published_version_metadata(*, dandiset: str, published_version: str, try: metanew = migrate(metadata, to_version=to_version, skip_validation=False) - except Exception as e: + except Exception as e: # noqa: BLE001 click.echo(f'Failed to migrate {dandiset}/{published_version}') click.echo(e) raise click.Abort from e diff --git a/dandiapi/api/management/commands/migrate_version_metadata.py b/dandiapi/api/management/commands/migrate_version_metadata.py index 89f91ce15..9b5417509 100644 --- a/dandiapi/api/management/commands/migrate_version_metadata.py +++ b/dandiapi/api/management/commands/migrate_version_metadata.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dandischema import migrate import djclick as click @@ -15,7 +17,7 @@ def migrate_version_metadata(*, to_version: str): try: metanew = migrate(metadata, to_version=to_version, skip_validation=True) - except Exception as e: + except Exception as e: # noqa: BLE001 click.echo(f'Failed to migrate {version.dandiset.identifier}/{version.version}') click.echo(e) continue diff --git a/dandiapi/api/management/commands/revalidate.py b/dandiapi/api/management/commands/revalidate.py index 3c9008bb7..e324c8090 100644 --- a/dandiapi/api/management/commands/revalidate.py +++ b/dandiapi/api/management/commands/revalidate.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import djclick as click from dandiapi.api.models import Asset, Version diff --git a/dandiapi/api/manifests.py b/dandiapi/api/manifests.py index 87c04adf4..a67403abe 100644 --- a/dandiapi/api/manifests.py +++ b/dandiapi/api/manifests.py @@ -1,6 +1,8 @@ +from __future__ import annotations + from contextlib import contextmanager import tempfile -from typing import IO, Any, Generator, Iterable +from typing import IO, TYPE_CHECKING, Any from urllib.parse import urlparse, urlunparse from django.conf import settings @@ -11,6 +13,9 @@ from dandiapi.api.models import Asset, AssetBlob, Version from dandiapi.api.storage import create_s3_storage +if TYPE_CHECKING: + from collections.abc import Generator, Iterable + def _s3_url(path: str) -> str: """Turn an object path into a fully qualified S3 URL.""" @@ -68,7 +73,7 @@ def _streaming_file_upload(path: str) -> Generator[IO[bytes], None, None]: # Piggyback on the AssetBlob storage since we want to store manifests in the same bucket storage = AssetBlob.blob.field.storage - storage._save(path, File(outfile)) + storage._save(path, File(outfile)) # noqa: SLF001 def _yaml_dump_sequence_from_generator(stream: IO[bytes], generator: Iterable[Any]) -> None: diff --git a/dandiapi/api/migrations/0001_initial_v2.py b/dandiapi/api/migrations/0001_initial_v2.py index c7e66029a..351eb82aa 100644 --- a/dandiapi/api/migrations/0001_initial_v2.py +++ b/dandiapi/api/migrations/0001_initial_v2.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import uuid from django.conf import settings diff --git a/dandiapi/api/migrations/0001_stagingapplication.py b/dandiapi/api/migrations/0001_stagingapplication.py index fba013cfa..86953d76b 100644 --- a/dandiapi/api/migrations/0001_stagingapplication.py +++ b/dandiapi/api/migrations/0001_stagingapplication.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.conf import settings from django.db import migrations, models import django.db.models.deletion diff --git a/dandiapi/api/migrations/0002_asset_zarr.py b/dandiapi/api/migrations/0002_asset_zarr.py index 6c8f5371e..49f2f2186 100644 --- a/dandiapi/api/migrations/0002_asset_zarr.py +++ b/dandiapi/api/migrations/0002_asset_zarr.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import migrations, models import django.db.models.deletion diff --git a/dandiapi/api/migrations/0003_default_oauth_application.py b/dandiapi/api/migrations/0003_default_oauth_application.py index 3c1414d8d..fb65f036c 100644 --- a/dandiapi/api/migrations/0003_default_oauth_application.py +++ b/dandiapi/api/migrations/0003_default_oauth_application.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import migrations from django.db.models import Q from oauth2_provider.settings import oauth2_settings diff --git a/dandiapi/api/migrations/0004_merge.py b/dandiapi/api/migrations/0004_merge.py index 5a1f3336a..56e2312ce 100644 --- a/dandiapi/api/migrations/0004_merge.py +++ b/dandiapi/api/migrations/0004_merge.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import migrations diff --git a/dandiapi/api/migrations/0005_null_charfield.py b/dandiapi/api/migrations/0005_null_charfield.py new file mode 100644 index 000000000..f5341b658 --- /dev/null +++ b/dandiapi/api/migrations/0005_null_charfield.py @@ -0,0 +1,65 @@ +# Generated by Django 4.1.13 on 2023-11-11 04:25 +from __future__ import annotations + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0004_merge'), + ] + + operations = [ + migrations.AlterField( + model_name='assetblob', + name='sha256', + field=models.CharField( + blank=True, + default=None, + max_length=64, + null=True, + validators=[django.core.validators.RegexValidator('^[0-9a-f]{64}$')], + ), + ), + migrations.AlterField( + model_name='embargoedassetblob', + name='sha256', + field=models.CharField( + blank=True, + default=None, + max_length=64, + null=True, + validators=[django.core.validators.RegexValidator('^[0-9a-f]{64}$')], + ), + ), + migrations.AlterField( + model_name='embargoedupload', + name='etag', + field=models.CharField( + blank=True, + db_index=True, + default=None, + max_length=40, + null=True, + validators=[django.core.validators.RegexValidator('^[0-9a-f]{32}(-[1-9][0-9]*)?$')], + ), + ), + migrations.AlterField( + model_name='upload', + name='etag', + field=models.CharField( + blank=True, + db_index=True, + default=None, + max_length=40, + null=True, + validators=[django.core.validators.RegexValidator('^[0-9a-f]{32}(-[1-9][0-9]*)?$')], + ), + ), + migrations.AlterField( + model_name='version', + name='doi', + field=models.CharField(blank=True, default=None, max_length=64, null=True), + ), + ] diff --git a/dandiapi/api/models/__init__.py b/dandiapi/api/models/__init__.py index c3eb3fc44..fdc3bd2b5 100644 --- a/dandiapi/api/models/__init__.py +++ b/dandiapi/api/models/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .asset import Asset, AssetBlob, EmbargoedAssetBlob from .asset_paths import AssetPath, AssetPathRelation from .dandiset import Dandiset diff --git a/dandiapi/api/models/asset.py b/dandiapi/api/models/asset.py index a61ff99b7..b967d9b5c 100644 --- a/dandiapi/api/models/asset.py +++ b/dandiapi/api/models/asset.py @@ -27,7 +27,7 @@ from .version import Version ASSET_CHARS_REGEX = r'[A-z0-9(),&\s#+~_=-]' -ASSET_PATH_REGEX = fr'^({ASSET_CHARS_REGEX}?\/?\.?{ASSET_CHARS_REGEX})+$' +ASSET_PATH_REGEX = rf'^({ASSET_CHARS_REGEX}?\/?\.?{ASSET_CHARS_REGEX})+$' ASSET_COMPUTED_FIELDS = [ 'id', 'path', @@ -58,8 +58,9 @@ class BaseAssetBlob(TimeStampedModel): ETAG_REGEX = r'[0-9a-f]{32}(-[1-9][0-9]*)?' blob_id = models.UUIDField(unique=True) - sha256 = models.CharField( + sha256 = models.CharField( # noqa: DJ001 null=True, + default=None, blank=True, max_length=64, validators=[RegexValidator(f'^{SHA256_REGEX}$')], diff --git a/dandiapi/api/models/metadata.py b/dandiapi/api/models/metadata.py index 6a45d22b9..2aec442d5 100644 --- a/dandiapi/api/models/metadata.py +++ b/dandiapi/api/models/metadata.py @@ -1,6 +1,11 @@ -import datetime +from __future__ import annotations + +from typing import TYPE_CHECKING from uuid import uuid4 +if TYPE_CHECKING: + import datetime + class PublishableMetadataMixin: def published_by(self, now: datetime.datetime): diff --git a/dandiapi/api/models/oauth.py b/dandiapi/api/models/oauth.py index 70bfec800..5493d92d2 100644 --- a/dandiapi/api/models/oauth.py +++ b/dandiapi/api/models/oauth.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from fnmatch import fnmatch from django.core.exceptions import ValidationError diff --git a/dandiapi/api/models/upload.py b/dandiapi/api/models/upload.py index a5b3fac51..220e61b92 100644 --- a/dandiapi/api/models/upload.py +++ b/dandiapi/api/models/upload.py @@ -26,8 +26,9 @@ class BaseUpload(models.Model): # This is the key used to generate the object key, and the primary identifier for the upload. upload_id = models.UUIDField(unique=True, default=uuid4, db_index=True) - etag = models.CharField( + etag = models.CharField( # noqa: DJ001 null=True, + default=None, blank=True, max_length=40, validators=[RegexValidator(f'^{ETAG_REGEX}$')], @@ -43,7 +44,7 @@ class Meta: @staticmethod @abstractmethod - def object_key(upload_id, *, dandiset: Dandiset): # noqa: N805 + def object_key(upload_id, *, dandiset: Dandiset): pass @classmethod @@ -83,8 +84,11 @@ class Upload(BaseUpload): blob = models.FileField(blank=True, storage=get_storage, upload_to=get_storage_prefix) dandiset = models.ForeignKey(Dandiset, related_name='uploads', on_delete=models.CASCADE) + def __str__(self) -> str: + return self.upload_id + @staticmethod - def object_key(upload_id, *, dandiset: Dandiset | None = None): + def object_key(upload_id, *, dandiset: Dandiset | None = None): # noqa: ARG004 upload_id = str(upload_id) return ( f'{settings.DANDI_DANDISETS_BUCKET_PREFIX}' @@ -109,6 +113,9 @@ class EmbargoedUpload(BaseUpload): Dandiset, related_name='embargoed_uploads', on_delete=models.CASCADE ) + def __str__(self) -> str: + return self.upload_id + @staticmethod def object_key(upload_id, *, dandiset: Dandiset): upload_id = str(upload_id) diff --git a/dandiapi/api/models/user.py b/dandiapi/api/models/user.py index cc90c2005..b3c324dae 100644 --- a/dandiapi/api/models/user.py +++ b/dandiapi/api/models/user.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.contrib.auth.models import User from django.db import models from django_extensions.db.models import TimeStampedModel diff --git a/dandiapi/api/models/version.py b/dandiapi/api/models/version.py index 0cf3f728d..cac103586 100644 --- a/dandiapi/api/models/version.py +++ b/dandiapi/api/models/version.py @@ -36,7 +36,7 @@ class Status(models.TextChoices): max_length=13, validators=[RegexValidator(f'^{VERSION_REGEX}$')], ) - doi = models.CharField(max_length=64, null=True, blank=True) + doi = models.CharField(max_length=64, null=True, default=None, blank=True) # noqa: DJ001 """Track the validation status of this version, without considering assets""" status = models.CharField( max_length=10, @@ -185,7 +185,10 @@ def _populate_metadata(self): 'version': self.version, 'id': f'DANDI:{self.dandiset.identifier}/{self.version}', 'repository': settings.DANDI_WEB_APP_URL, - 'url': f'{settings.DANDI_WEB_APP_URL}/dandiset/{self.dandiset.identifier}/{self.version}', # noqa + 'url': ( + f'{settings.DANDI_WEB_APP_URL}/dandiset/' + f'{self.dandiset.identifier}/{self.version}' + ), 'dateCreated': self.dandiset.created.isoformat(), } diff --git a/dandiapi/api/permissions.py b/dandiapi/api/permissions.py index 5af54f7db..61b961b09 100644 --- a/dandiapi/api/permissions.py +++ b/dandiapi/api/permissions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from rest_framework.permissions import SAFE_METHODS, BasePermission, IsAuthenticated from dandiapi.api.models.user import UserMetadata diff --git a/dandiapi/api/services/asset/__init__.py b/dandiapi/api/services/asset/__init__.py index d1b1a34ac..8cedcd739 100644 --- a/dandiapi/api/services/asset/__init__.py +++ b/dandiapi/api/services/asset/__init__.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from django.db import transaction from dandiapi.api.asset_paths import add_asset_paths, delete_asset_paths, get_conflicting_paths @@ -10,7 +14,9 @@ DraftDandisetNotModifiableError, ZarrArchiveBelongsToDifferentDandisetError, ) -from dandiapi.zarr.models import ZarrArchive + +if TYPE_CHECKING: + from dandiapi.zarr.models import ZarrArchive def _create_asset( @@ -37,7 +43,7 @@ def _create_asset( return asset -def change_asset( +def change_asset( # noqa: PLR0913 *, user, asset: Asset, @@ -134,7 +140,7 @@ def add_asset_to_version( asset_blob = None else: embargoed_asset_blob = None - asset_blob = asset_blob + asset_blob = asset_blob # noqa: PLW0127 with transaction.atomic(): asset = _create_asset( diff --git a/dandiapi/api/services/asset/exceptions.py b/dandiapi/api/services/asset/exceptions.py index 0bd06d91d..3c6413fc9 100644 --- a/dandiapi/api/services/asset/exceptions.py +++ b/dandiapi/api/services/asset/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from rest_framework import status from dandiapi.api.services.exceptions import DandiError diff --git a/dandiapi/api/services/dandiset/__init__.py b/dandiapi/api/services/dandiset/__init__.py index 32a8d31b9..0cf80d82c 100644 --- a/dandiapi/api/services/dandiset/__init__.py +++ b/dandiapi/api/services/dandiset/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import transaction from dandiapi.api.models.dandiset import Dandiset diff --git a/dandiapi/api/services/dandiset/exceptions.py b/dandiapi/api/services/dandiset/exceptions.py index c5df12e27..a71088c1b 100644 --- a/dandiapi/api/services/dandiset/exceptions.py +++ b/dandiapi/api/services/dandiset/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from rest_framework import status from dandiapi.api.services.exceptions import DandiError @@ -7,7 +9,7 @@ class DandisetAlreadyExistsError(DandiError): http_status_code = status.HTTP_400_BAD_REQUEST -class UnauthorizedEmbargoAccess(DandiError): +class UnauthorizedEmbargoAccessError(DandiError): http_status_code = status.HTTP_401_UNAUTHORIZED message = ( 'Authentication credentials must be provided when attempting to access embargoed dandisets' diff --git a/dandiapi/api/services/embargo/__init__.py b/dandiapi/api/services/embargo/__init__.py index 05b6cf4a2..9b3e4425b 100644 --- a/dandiapi/api/services/embargo/__init__.py +++ b/dandiapi/api/services/embargo/__init__.py @@ -1,6 +1,8 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from django.conf import settings -from django.contrib.auth.models import User -from django.db.models import QuerySet from dandiapi.api.copy import copy_object_multipart from dandiapi.api.models import Asset, AssetBlob, Dandiset, Upload, Version @@ -9,6 +11,10 @@ from .exceptions import AssetNotEmbargoedError, DandisetNotEmbargoedError +if TYPE_CHECKING: + from django.contrib.auth.models import User + from django.db.models import QuerySet + def _unembargo_asset(asset: Asset): """Unembargo an asset by copying its blob to the public bucket.""" diff --git a/dandiapi/api/services/embargo/exceptions.py b/dandiapi/api/services/embargo/exceptions.py index 1955b49d6..736dedfb7 100644 --- a/dandiapi/api/services/embargo/exceptions.py +++ b/dandiapi/api/services/embargo/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from rest_framework import status from dandiapi.api.services.exceptions import DandiError diff --git a/dandiapi/api/services/exceptions.py b/dandiapi/api/services/exceptions.py index 777dc432e..386869c13 100644 --- a/dandiapi/api/services/exceptions.py +++ b/dandiapi/api/services/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from rest_framework import status diff --git a/dandiapi/api/services/metadata/__init__.py b/dandiapi/api/services/metadata/__init__.py index 2b05d01cb..022ee3379 100644 --- a/dandiapi/api/services/metadata/__init__.py +++ b/dandiapi/api/services/metadata/__init__.py @@ -1,19 +1,25 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from celery.utils.log import get_task_logger import dandischema.exceptions from dandischema.metadata import aggregate_assets_summary, validate from django.conf import settings from django.db import transaction from django.utils import timezone -import jsonschema.exceptions from dandiapi.api.models import Asset, Version from dandiapi.api.services.metadata.exceptions import ( AssetHasBeenPublishedError, VersionHasBeenPublishedError, - VersionMetadataConcurrentlyModified, + VersionMetadataConcurrentlyModifiedError, ) from dandiapi.api.services.publish import _build_publishable_version_from_draft +if TYPE_CHECKING: + import jsonschema.exceptions + logger = get_task_logger(__name__) @@ -96,7 +102,7 @@ def version_aggregate_assets_summary(version: Version) -> None: ) if updated_count == 0: logger.info('Skipped updating assetsSummary for version %s', version.id) - raise VersionMetadataConcurrentlyModified + raise VersionMetadataConcurrentlyModifiedError def validate_version_metadata(*, version: Version) -> None: @@ -110,10 +116,11 @@ def _build_validatable_version_metadata(version: Version) -> dict: metadata_for_validation[ 'id' - ] = f'DANDI:{publishable_version.dandiset.identifier}/{publishable_version.version}' # noqa - metadata_for_validation[ - 'url' - ] = f'{settings.DANDI_WEB_APP_URL}/dandiset/{publishable_version.dandiset.identifier}/{publishable_version.version}' # noqa + ] = f'DANDI:{publishable_version.dandiset.identifier}/{publishable_version.version}' + metadata_for_validation['url'] = ( + f'{settings.DANDI_WEB_APP_URL}/dandiset/' + f'{publishable_version.dandiset.identifier}/{publishable_version.version}' + ) metadata_for_validation['doi'] = '10.80507/dandi.123456/0.123456.1234' metadata_for_validation['assetsSummary'] = { 'schemaKey': 'AssetsSummary', diff --git a/dandiapi/api/services/metadata/exceptions.py b/dandiapi/api/services/metadata/exceptions.py index 012b81842..1aeb365f0 100644 --- a/dandiapi/api/services/metadata/exceptions.py +++ b/dandiapi/api/services/metadata/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from rest_framework import status from dandiapi.api.services.exceptions import DandiError @@ -13,6 +15,6 @@ class VersionHasBeenPublishedError(DandiError): message = 'This version has been published and cannot be modified.' -class VersionMetadataConcurrentlyModified(DandiError): +class VersionMetadataConcurrentlyModifiedError(DandiError): http_status_code = status.HTTP_500_INTERNAL_SERVER_ERROR message = 'The metadata for this version has been modified since the request began.' diff --git a/dandiapi/api/services/publish/__init__.py b/dandiapi/api/services/publish/__init__.py index 289e5de1e..a41ead487 100644 --- a/dandiapi/api/services/publish/__init__.py +++ b/dandiapi/api/services/publish/__init__.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import datetime +from typing import TYPE_CHECKING from dandischema.metadata import aggregate_assets_summary, validate -from django.contrib.auth.models import User from django.db import transaction -from django.db.models import QuerySet from more_itertools import ichunked from dandiapi.api import doi @@ -20,6 +21,10 @@ ) from dandiapi.api.tasks import write_manifest_files +if TYPE_CHECKING: + from django.contrib.auth.models import User + from django.db.models import QuerySet + def publish_asset(*, asset: Asset) -> None: with transaction.atomic(): @@ -120,7 +125,7 @@ def _publish_dandiset(dandiset_id: int) -> None: new_version.save() # Bulk create the join table rows to optimize linking assets to new_version - AssetVersions = Version.assets.through + AssetVersions = Version.assets.through # noqa: N806 # Add a new many-to-many association directly to any already published assets already_published_assets: QuerySet[Asset] = old_version.assets.filter(published=True) diff --git a/dandiapi/api/services/publish/exceptions.py b/dandiapi/api/services/publish/exceptions.py index a0a3cb95f..f2310d9c1 100644 --- a/dandiapi/api/services/publish/exceptions.py +++ b/dandiapi/api/services/publish/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from rest_framework import status from dandiapi.api.services.exceptions import DandiError diff --git a/dandiapi/api/services/version/metadata.py b/dandiapi/api/services/version/metadata.py index 576540a25..ed3f10011 100644 --- a/dandiapi/api/services/version/metadata.py +++ b/dandiapi/api/services/version/metadata.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dandischema.models import Dandiset as PydanticDandiset from django.conf import settings diff --git a/dandiapi/api/signals.py b/dandiapi/api/signals.py index 1eed71f00..095a03117 100644 --- a/dandiapi/api/signals.py +++ b/dandiapi/api/signals.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from allauth.account.signals import user_signed_up from corsheaders.signals import check_request_enabled from django.conf import settings diff --git a/dandiapi/api/storage.py b/dandiapi/api/storage.py index 64a4f12d6..ff0de3702 100644 --- a/dandiapi/api/storage.py +++ b/dandiapi/api/storage.py @@ -1,9 +1,8 @@ from __future__ import annotations -from collections.abc import Iterator from datetime import timedelta import hashlib -from typing import Any +from typing import TYPE_CHECKING, Any from urllib.parse import urlsplit, urlunsplit import boto3 @@ -19,6 +18,9 @@ from s3_file_field._multipart_s3 import S3MultipartManager from storages.backends.s3 import S3Storage +if TYPE_CHECKING: + from collections.abc import Iterator + class ChecksumCalculatorFile: """File-like object that calculates the checksum of everything written to it.""" @@ -61,7 +63,7 @@ class DandiMinioMultipartManager(DandiMultipartMixin, MinioMultipartManager): """A custom multipart manager for passing ACL information.""" def _create_upload_id(self, object_key: str, content_type: str) -> str: - return self._client._create_multipart_upload( + return self._client._create_multipart_upload( # noqa: SLF001 bucket_name=self._bucket_name, object_name=object_key, headers={ @@ -312,9 +314,9 @@ def get_boto_client(storage: Storage | None = None): def get_storage_params(storage: Storage): if isinstance(storage, MinioStorage): return { - 'endpoint_url': storage.client._base_url._url.geturl(), - 'access_key': storage.client._provider.retrieve().access_key, - 'secret_key': storage.client._provider.retrieve().secret_key, + 'endpoint_url': storage.client._base_url._url.geturl(), # noqa: SLF001 + 'access_key': storage.client._provider.retrieve().access_key, # noqa: SLF001 + 'secret_key': storage.client._provider.retrieve().secret_key, # noqa: SLF001 } return { diff --git a/dandiapi/api/tasks/__init__.py b/dandiapi/api/tasks/__init__.py index 5332da639..80f8ef87c 100644 --- a/dandiapi/api/tasks/__init__.py +++ b/dandiapi/api/tasks/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from celery import shared_task from celery.utils.log import get_task_logger diff --git a/dandiapi/api/tasks/scheduled.py b/dandiapi/api/tasks/scheduled.py index f2402b915..e1cff81b7 100644 --- a/dandiapi/api/tasks/scheduled.py +++ b/dandiapi/api/tasks/scheduled.py @@ -3,12 +3,13 @@ This module is imported from celery.py in a post-app-load hook. """ -from collections.abc import Iterable +from __future__ import annotations + from datetime import timedelta import time +from typing import TYPE_CHECKING from celery import shared_task -from celery.app.base import Celery from celery.schedules import crontab from celery.utils.log import get_task_logger from django.conf import settings @@ -21,7 +22,7 @@ from dandiapi.api.models import UserMetadata, Version from dandiapi.api.models.asset import Asset from dandiapi.api.services.metadata import version_aggregate_assets_summary -from dandiapi.api.services.metadata.exceptions import VersionMetadataConcurrentlyModified +from dandiapi.api.services.metadata.exceptions import VersionMetadataConcurrentlyModifiedError from dandiapi.api.tasks import ( validate_asset_metadata_task, validate_version_metadata_task, @@ -29,6 +30,11 @@ ) from dandiapi.zarr.models import ZarrArchiveStatus +if TYPE_CHECKING: + from collections.abc import Iterable + + from celery.app.base import Celery + logger = get_task_logger(__name__) @@ -46,7 +52,7 @@ def throttled_iterator(iterable: Iterable, max_per_second: int = 100) -> Iterabl @shared_task( soft_time_limit=60, - autoretry_for=(VersionMetadataConcurrentlyModified,), + autoretry_for=(VersionMetadataConcurrentlyModifiedError,), retry_backoff=True, ) def aggregate_assets_summary_task(version_id: int): diff --git a/dandiapi/api/tests/factories.py b/dandiapi/api/tests/factories.py index d8451c38d..c0626d2be 100644 --- a/dandiapi/api/tests/factories.py +++ b/dandiapi/api/tests/factories.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime import hashlib diff --git a/dandiapi/api/tests/fuzzy.py b/dandiapi/api/tests/fuzzy.py index 0b95e458e..739f9872b 100644 --- a/dandiapi/api/tests/fuzzy.py +++ b/dandiapi/api/tests/fuzzy.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import re diff --git a/dandiapi/api/tests/test_asset.py b/dandiapi/api/tests/test_asset.py index 23fda2f4a..f1f18ccb3 100644 --- a/dandiapi/api/tests/test_asset.py +++ b/dandiapi/api/tests/test_asset.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import os.path from uuid import uuid4 @@ -197,7 +199,7 @@ def test_asset_full_metadata(draft_asset_factory): 'contentUrl': [download_url, blob_url], 'contentSize': asset.blob.size, 'digest': asset.blob.digest, - '@context': f'https://raw.githubusercontent.com/dandi/schema/master/releases/{settings.DANDI_SCHEMA_VERSION}/context.json', # noqa: E501 + '@context': f'https://raw.githubusercontent.com/dandi/schema/master/releases/{settings.DANDI_SCHEMA_VERSION}/context.json', } @@ -225,7 +227,7 @@ def test_asset_full_metadata_zarr(draft_asset_factory, zarr_archive): 'digest': asset.digest, # This should be injected on all zarr assets 'encodingFormat': 'application/x-zarr', - '@context': f'https://raw.githubusercontent.com/dandi/schema/master/releases/{settings.DANDI_SCHEMA_VERSION}/context.json', # noqa: E501 + '@context': f'https://raw.githubusercontent.com/dandi/schema/master/releases/{settings.DANDI_SCHEMA_VERSION}/context.json', } diff --git a/dandiapi/api/tests/test_asset_paths.py b/dandiapi/api/tests/test_asset_paths.py index 00fb41ea2..3d8f5b1ff 100644 --- a/dandiapi/api/tests/test_asset_paths.py +++ b/dandiapi/api/tests/test_asset_paths.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db.models import Q, QuerySet import pytest diff --git a/dandiapi/api/tests/test_auth.py b/dandiapi/api/tests/test_auth.py index 354a7f56d..24cd3a9f5 100644 --- a/dandiapi/api/tests/test_auth.py +++ b/dandiapi/api/tests/test_auth.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from rest_framework.authtoken.models import Token diff --git a/dandiapi/api/tests/test_checksum.py b/dandiapi/api/tests/test_checksum.py index df83bc1e5..15236ee0d 100644 --- a/dandiapi/api/tests/test_checksum.py +++ b/dandiapi/api/tests/test_checksum.py @@ -1,7 +1,12 @@ +from __future__ import annotations + import hashlib +from typing import TYPE_CHECKING from django.core.files.base import ContentFile -from django.core.files.storage import Storage + +if TYPE_CHECKING: + from django.core.files.storage import Storage def test_checksum(faker, storage: Storage): diff --git a/dandiapi/api/tests/test_create_dev_dandiset.py b/dandiapi/api/tests/test_create_dev_dandiset.py index 5af53da92..6a738bf50 100644 --- a/dandiapi/api/tests/test_create_dev_dandiset.py +++ b/dandiapi/api/tests/test_create_dev_dandiset.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from dandiapi.api.management.commands.create_dev_dandiset import create_dev_dandiset diff --git a/dandiapi/api/tests/test_dandiset.py b/dandiapi/api/tests/test_dandiset.py index 5b0cabb38..5495b97d4 100644 --- a/dandiapi/api/tests/test_dandiset.py +++ b/dandiapi/api/tests/test_dandiset.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from django.conf import settings @@ -375,7 +377,7 @@ def test_dandiset_rest_create(api_client, user): assert dandiset.draft_version.metadata == { **metadata, 'manifestLocation': [ - f'{settings.DANDI_API_URL}/api/dandisets/{dandiset.identifier}/versions/draft/assets/' # noqa: E501 + f'{settings.DANDI_API_URL}/api/dandisets/{dandiset.identifier}/versions/draft/assets/' ], 'name': name, 'identifier': DANDISET_SCHEMA_ID_RE, @@ -387,7 +389,7 @@ def test_dandiset_rest_create(api_client, user): f'{user.last_name}, {user.first_name} ({year}) {name} ' f'(Version draft) [Data set]. DANDI archive. {url}' ), - '@context': f'https://raw.githubusercontent.com/dandi/schema/master/releases/{settings.DANDI_SCHEMA_VERSION}/context.json', # noqa: E501 + '@context': f'https://raw.githubusercontent.com/dandi/schema/master/releases/{settings.DANDI_SCHEMA_VERSION}/context.json', 'schemaVersion': settings.DANDI_SCHEMA_VERSION, 'schemaKey': 'Dandiset', 'access': [{'schemaKey': 'AccessRequirements', 'status': 'dandi:OpenAccess'}], @@ -468,7 +470,7 @@ def test_dandiset_rest_create_with_identifier(api_client, admin_user): assert dandiset.draft_version.metadata == { **metadata, 'manifestLocation': [ - f'{settings.DANDI_API_URL}/api/dandisets/{dandiset.identifier}/versions/draft/assets/' # noqa: E501 + f'{settings.DANDI_API_URL}/api/dandisets/{dandiset.identifier}/versions/draft/assets/' ], 'name': name, 'identifier': f'DANDI:{identifier}', @@ -480,7 +482,7 @@ def test_dandiset_rest_create_with_identifier(api_client, admin_user): f'{admin_user.last_name}, {admin_user.first_name} ({year}) {name} ' f'(Version draft) [Data set]. DANDI archive. {url}' ), - '@context': f'https://raw.githubusercontent.com/dandi/schema/master/releases/{settings.DANDI_SCHEMA_VERSION}/context.json', # noqa: E501 + '@context': f'https://raw.githubusercontent.com/dandi/schema/master/releases/{settings.DANDI_SCHEMA_VERSION}/context.json', 'schemaVersion': settings.DANDI_SCHEMA_VERSION, 'schemaKey': 'Dandiset', 'access': [{'schemaKey': 'AccessRequirements', 'status': 'dandi:OpenAccess'}], @@ -575,7 +577,7 @@ def test_dandiset_rest_create_with_contributor(api_client, admin_user): assert dandiset.draft_version.metadata == { **metadata, 'manifestLocation': [ - f'{settings.DANDI_API_URL}/api/dandisets/{dandiset.identifier}/versions/draft/assets/' # noqa: E501 + f'{settings.DANDI_API_URL}/api/dandisets/{dandiset.identifier}/versions/draft/assets/' ], 'name': name, 'identifier': f'DANDI:{identifier}', @@ -586,7 +588,7 @@ def test_dandiset_rest_create_with_contributor(api_client, admin_user): 'citation': ( f'Jane Doe ({year}) {name} ' f'(Version draft) [Data set]. DANDI archive. {url}' ), - '@context': f'https://raw.githubusercontent.com/dandi/schema/master/releases/{settings.DANDI_SCHEMA_VERSION}/context.json', # noqa: E501 + '@context': f'https://raw.githubusercontent.com/dandi/schema/master/releases/{settings.DANDI_SCHEMA_VERSION}/context.json', 'schemaVersion': settings.DANDI_SCHEMA_VERSION, 'schemaKey': 'Dandiset', 'access': [{'schemaKey': 'AccessRequirements', 'status': 'dandi:OpenAccess'}], @@ -665,7 +667,7 @@ def test_dandiset_rest_create_embargoed(api_client, user): assert dandiset.draft_version.metadata == { **metadata, 'manifestLocation': [ - f'{settings.DANDI_API_URL}/api/dandisets/{dandiset.identifier}/versions/draft/assets/' # noqa: E501 + f'{settings.DANDI_API_URL}/api/dandisets/{dandiset.identifier}/versions/draft/assets/' ], 'name': name, 'identifier': DANDISET_SCHEMA_ID_RE, @@ -677,7 +679,7 @@ def test_dandiset_rest_create_embargoed(api_client, user): f'{user.last_name}, {user.first_name} ({year}) {name} ' f'(Version draft) [Data set]. DANDI archive. {url}' ), - '@context': f'https://raw.githubusercontent.com/dandi/schema/master/releases/{settings.DANDI_SCHEMA_VERSION}/context.json', # noqa: E501 + '@context': f'https://raw.githubusercontent.com/dandi/schema/master/releases/{settings.DANDI_SCHEMA_VERSION}/context.json', 'schemaVersion': settings.DANDI_SCHEMA_VERSION, 'schemaKey': 'Dandiset', 'access': [{'schemaKey': 'AccessRequirements', 'status': 'dandi:EmbargoedAccess'}], diff --git a/dandiapi/api/tests/test_embargo.py b/dandiapi/api/tests/test_embargo.py index b6e911832..191733c8a 100644 --- a/dandiapi/api/tests/test_embargo.py +++ b/dandiapi/api/tests/test_embargo.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from dandiapi.api.models import Dandiset @@ -54,7 +56,7 @@ def embargo_status(request): ), ( 'get', - '/api/dandisets/{dandiset.identifier}/versions/draft/assets/{asset.asset_id}/validation/', # noqa: E501 + '/api/dandisets/{dandiset.identifier}/versions/draft/assets/{asset.asset_id}/validation/', ), ], ) diff --git a/dandiapi/api/tests/test_garbage.py b/dandiapi/api/tests/test_garbage.py index 866e497b5..0a191cef9 100644 --- a/dandiapi/api/tests/test_garbage.py +++ b/dandiapi/api/tests/test_garbage.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from datetime import timedelta from django.utils import timezone diff --git a/dandiapi/api/tests/test_info.py b/dandiapi/api/tests/test_info.py index f1eec871c..ce4c7eb0f 100644 --- a/dandiapi/api/tests/test_info.py +++ b/dandiapi/api/tests/test_info.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.conf import settings from dandiapi import __version__ diff --git a/dandiapi/api/tests/test_manifests.py b/dandiapi/api/tests/test_manifests.py index 9db4b6019..a148479f3 100644 --- a/dandiapi/api/tests/test_manifests.py +++ b/dandiapi/api/tests/test_manifests.py @@ -1,6 +1,9 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from django.conf import settings from django.core.files.base import ContentFile -from django.core.files.storage import Storage import pytest from rest_framework.renderers import JSONRenderer from rest_framework_yaml.renderers import YAMLRenderer @@ -14,6 +17,9 @@ ) from dandiapi.api.models import AssetBlob, Version +if TYPE_CHECKING: + from django.core.files.storage import Storage + @pytest.mark.django_db() def test_write_dandiset_jsonld(storage: Storage, version: Version): diff --git a/dandiapi/api/tests/test_permission.py b/dandiapi/api/tests/test_permission.py index 4f8b88196..14ea28dc2 100644 --- a/dandiapi/api/tests/test_permission.py +++ b/dandiapi/api/tests/test_permission.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from rest_framework.permissions import SAFE_METHODS @@ -54,7 +56,7 @@ ), ( 'get', - '/api/dandisets/{dandiset.identifier}/versions/draft/assets/{asset.asset_id}/validation/', # noqa: E501 + '/api/dandisets/{dandiset.identifier}/versions/draft/assets/{asset.asset_id}/validation/', False, ), # Zarrs diff --git a/dandiapi/api/tests/test_search.py b/dandiapi/api/tests/test_search.py index edd334553..bd5511774 100644 --- a/dandiapi/api/tests/test_search.py +++ b/dandiapi/api/tests/test_search.py @@ -1,5 +1,11 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + import pytest -from rest_framework.test import APIClient + +if TYPE_CHECKING: + from rest_framework.test import APIClient @pytest.mark.django_db() diff --git a/dandiapi/api/tests/test_stats.py b/dandiapi/api/tests/test_stats.py index c4fffc759..5de16f0c8 100644 --- a/dandiapi/api/tests/test_stats.py +++ b/dandiapi/api/tests/test_stats.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/dandiapi/api/tests/test_tasks.py b/dandiapi/api/tests/test_tasks.py index d5dcb3056..15cdea730 100644 --- a/dandiapi/api/tests/test_tasks.py +++ b/dandiapi/api/tests/test_tasks.py @@ -1,18 +1,23 @@ +from __future__ import annotations + import datetime import hashlib +from typing import TYPE_CHECKING from django.conf import settings -from django.contrib.auth.models import User -from django.core.files.storage import Storage from guardian.shortcuts import assign_perm import pytest -from rest_framework.test import APIClient from dandiapi.api import tasks from dandiapi.api.models import Asset, AssetBlob, EmbargoedAssetBlob, Version from .fuzzy import URN_RE, UTC_ISO_TIMESTAMP_RE +if TYPE_CHECKING: + from django.contrib.auth.models import User + from django.core.files.storage import Storage + from rest_framework.test import APIClient + @pytest.mark.django_db() def test_calculate_checksum_task(storage: Storage, asset_blob_factory): @@ -317,7 +322,7 @@ def test_publish_task( }, 'datePublished': UTC_ISO_TIMESTAMP_RE, 'manifestLocation': [ - f'http://{settings.MINIO_STORAGE_ENDPOINT}/test-dandiapi-dandisets/test-prefix/dandisets/{draft_version.dandiset.identifier}/{published_version.version}/assets.yaml', # noqa: E501 + f'http://{settings.MINIO_STORAGE_ENDPOINT}/test-dandiapi-dandisets/test-prefix/dandisets/{draft_version.dandiset.identifier}/{published_version.version}/assets.yaml', ], 'identifier': f'DANDI:{draft_version.dandiset.identifier}', 'version': published_version.version, diff --git a/dandiapi/api/tests/test_unembargo.py b/dandiapi/api/tests/test_unembargo.py index b7177d112..8f98d4775 100644 --- a/dandiapi/api/tests/test_unembargo.py +++ b/dandiapi/api/tests/test_unembargo.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import math import os diff --git a/dandiapi/api/tests/test_upload.py b/dandiapi/api/tests/test_upload.py index 764b0ff1b..6bd3abf49 100644 --- a/dandiapi/api/tests/test_upload.py +++ b/dandiapi/api/tests/test_upload.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import uuid @@ -175,9 +177,9 @@ def test_upload_initialize_embargo(api_client, user, dandiset_factory): assert not Upload.objects.all().exists() upload = EmbargoedUpload.objects.get(upload_id=resp.data['upload_id']) upload_id = str(upload.upload_id) - assert ( - upload.blob.name - == f'test-embargo-prefix/{dandiset.identifier}/blobs/{upload_id[:3]}/{upload_id[3:6]}/{upload_id}' # noqa: E501 + assert upload.blob.name == ( + f'test-embargo-prefix/{dandiset.identifier}/blobs/' + f'{upload_id[:3]}/{upload_id[3:6]}/{upload_id}' ) diff --git a/dandiapi/api/tests/test_users.py b/dandiapi/api/tests/test_users.py index 35f698e48..feac3af91 100644 --- a/dandiapi/api/tests/test_users.py +++ b/dandiapi/api/tests/test_users.py @@ -1,24 +1,26 @@ from __future__ import annotations import json -from typing import Any +from typing import TYPE_CHECKING, Any -from allauth.socialaccount.models import SocialAccount -from django.contrib.auth.models import User -from django.core.mail.message import EmailMessage -from django.test.client import Client from django.urls.base import reverse import pytest from pytest_django.asserts import assertContains from rest_framework.exceptions import ErrorDetail -from rest_framework.response import Response -from rest_framework.test import APIClient from dandiapi.api.mail import ADMIN_EMAIL from dandiapi.api.models import UserMetadata from dandiapi.api.views.auth import COLLECT_USER_NAME_QUESTIONS, NEW_USER_QUESTIONS, QUESTIONS from dandiapi.api.views.users import user_to_dict +if TYPE_CHECKING: + from allauth.socialaccount.models import SocialAccount + from django.contrib.auth.models import User + from django.core.mail.message import EmailMessage + from django.test.client import Client + from rest_framework.response import Response + from rest_framework.test import APIClient + def serialize_social_account(social_account): return { diff --git a/dandiapi/api/tests/test_version.py b/dandiapi/api/tests/test_version.py index 87ec5300c..142ebe23a 100644 --- a/dandiapi/api/tests/test_version.py +++ b/dandiapi/api/tests/test_version.py @@ -5,15 +5,15 @@ from typing import TYPE_CHECKING from django.conf import settings -from django.contrib.auth.models import User from freezegun import freeze_time from guardian.shortcuts import assign_perm import pytest from dandiapi.api.services.metadata import version_aggregate_assets_summary -from dandiapi.api.services.metadata.exceptions import VersionMetadataConcurrentlyModified +from dandiapi.api.services.metadata.exceptions import VersionMetadataConcurrentlyModifiedError if TYPE_CHECKING: + from django.contrib.auth.models import User from rest_framework.test import APIClient from dandiapi.api import tasks @@ -72,7 +72,7 @@ def test_draft_version_metadata_computed(draft_version: Version): expected_metadata = { **original_metadata, 'manifestLocation': [ - f'{settings.DANDI_API_URL}/api/dandisets/{draft_version.dandiset.identifier}/versions/draft/assets/' # noqa: E501 + f'{settings.DANDI_API_URL}/api/dandisets/{draft_version.dandiset.identifier}/versions/draft/assets/' ], 'name': draft_version.name, 'identifier': f'DANDI:{draft_version.dandiset.identifier}', @@ -84,7 +84,7 @@ def test_draft_version_metadata_computed(draft_version: Version): ), 'repository': settings.DANDI_WEB_APP_URL, 'dateCreated': draft_version.dandiset.created.isoformat(), - '@context': f'https://raw.githubusercontent.com/dandi/schema/master/releases/{settings.DANDI_SCHEMA_VERSION}/context.json', # noqa: E501 + '@context': f'https://raw.githubusercontent.com/dandi/schema/master/releases/{settings.DANDI_SCHEMA_VERSION}/context.json', 'assetsSummary': { 'numberOfBytes': 0, 'numberOfFiles': 0, @@ -117,14 +117,17 @@ def test_published_version_metadata_computed(published_version: Version): 'identifier': f'DANDI:{published_version.dandiset.identifier}', 'version': published_version.version, 'id': f'DANDI:{published_version.dandiset.identifier}/{published_version.version}', - 'doi': f'10.80507/dandi.{published_version.dandiset.identifier}/{published_version.version}', # noqa: E501 + 'doi': ( + f'10.80507/dandi.' + f'{published_version.dandiset.identifier}/{published_version.version}' + ), 'url': ( f'{settings.DANDI_WEB_APP_URL}/dandiset/' f'{published_version.dandiset.identifier}/{published_version.version}' ), 'repository': settings.DANDI_WEB_APP_URL, 'dateCreated': published_version.dandiset.created.isoformat(), - '@context': f'https://raw.githubusercontent.com/dandi/schema/master/releases/{settings.DANDI_SCHEMA_VERSION}/context.json', # noqa: E501 + '@context': f'https://raw.githubusercontent.com/dandi/schema/master/releases/{settings.DANDI_SCHEMA_VERSION}/context.json', 'assetsSummary': { 'numberOfBytes': 0, 'numberOfFiles': 0, @@ -140,10 +143,13 @@ def test_published_version_metadata_computed(published_version: Version): def test_version_metadata_citation_draft(draft_version): name = draft_version.metadata['name'].rstrip('.') year = datetime.datetime.now(datetime.UTC).year - url = f'{settings.DANDI_WEB_APP_URL}/dandiset/{draft_version.dandiset.identifier}/{draft_version.version}' # noqa: E501 + url = ( + f'{settings.DANDI_WEB_APP_URL}/dandiset/' + f'{draft_version.dandiset.identifier}/{draft_version.version}' + ) assert ( draft_version.metadata['citation'] - == f'{name} ({year}). (Version {draft_version.version}) [Data set]. DANDI archive. {url}' # noqa: E501 + == f'{name} ({year}). (Version {draft_version.version}) [Data set]. DANDI archive. {url}' ) @@ -152,9 +158,9 @@ def test_version_metadata_citation_published(published_version): name = published_version.metadata['name'].rstrip('.') year = datetime.datetime.now(datetime.UTC).year url = f'https://doi.org/{published_version.doi}' - assert ( - published_version.metadata['citation'] - == f'{name} ({year}). (Version {published_version.version}) [Data set]. DANDI archive. {url}' # noqa: E501 + assert published_version.metadata['citation'] == ( + f'{name} ({year}). (Version {published_version.version}) [Data set]. ' + f'DANDI archive. {url}' ) @@ -325,7 +331,7 @@ def test_version_publish_version(draft_version, asset): 'dateCreated': UTC_ISO_TIMESTAMP_RE, 'datePublished': UTC_ISO_TIMESTAMP_RE, 'manifestLocation': [ - f'http://{settings.MINIO_STORAGE_ENDPOINT}/test-dandiapi-dandisets/test-prefix/dandisets/{publish_version.dandiset.identifier}/{publish_version.version}/assets.yaml', # noqa: E501 + f'http://{settings.MINIO_STORAGE_ENDPOINT}/test-dandiapi-dandisets/test-prefix/dandisets/{publish_version.dandiset.identifier}/{publish_version.version}/assets.yaml', ], 'identifier': f'DANDI:{publish_version.dandiset.identifier}', 'version': publish_version.version, @@ -370,7 +376,7 @@ def test_version_aggregate_assets_summary_metadata_modified( # Modify the metadata passed to the function so that it's mismatched version.metadata['foo'] = 'bar' - with pytest.raises(VersionMetadataConcurrentlyModified): + with pytest.raises(VersionMetadataConcurrentlyModifiedError): version_aggregate_assets_summary(version) @@ -543,7 +549,7 @@ def test_version_rest_update(api_client, user, draft_version): **new_metadata, 'schemaVersion': settings.DANDI_SCHEMA_VERSION, 'manifestLocation': [ - f'{settings.DANDI_API_URL}/api/dandisets/{draft_version.dandiset.identifier}/versions/draft/assets/' # noqa: E501 + f'{settings.DANDI_API_URL}/api/dandisets/{draft_version.dandiset.identifier}/versions/draft/assets/' ], 'name': new_name, 'identifier': f'DANDI:{draft_version.dandiset.identifier}', diff --git a/dandiapi/api/user_migration.py b/dandiapi/api/user_migration.py index 628039a92..51c804c16 100644 --- a/dandiapi/api/user_migration.py +++ b/dandiapi/api/user_migration.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from allauth.account.signals import user_logged_in diff --git a/dandiapi/api/views/__init__.py b/dandiapi/api/views/__init__.py index a38cefef3..7254ba465 100644 --- a/dandiapi/api/views/__init__.py +++ b/dandiapi/api/views/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .asset import AssetViewSet, NestedAssetViewSet from .auth import auth_token_view, authorize_view, user_questionnaire_form_view from .dandiset import DandisetViewSet diff --git a/dandiapi/api/views/asset.py b/dandiapi/api/views/asset.py index aed2815b2..8bb97b2cb 100644 --- a/dandiapi/api/views/asset.py +++ b/dandiapi/api/views/asset.py @@ -23,10 +23,10 @@ MinioStorage = type('FakeMinioStorage', (), {}) import os.path +from typing import TYPE_CHECKING from django.conf import settings from django.db import transaction -from django.db.models import QuerySet from django.http import HttpResponse, HttpResponseRedirect from django.utils.decorators import method_decorator from django_filters import rest_framework as filters @@ -58,6 +58,9 @@ AssetValidationSerializer, ) +if TYPE_CHECKING: + from django.db.models import QuerySet + class AssetFilter(filters.FilterSet): path = filters.CharFilter(lookup_expr='istartswith') diff --git a/dandiapi/api/views/auth.py b/dandiapi/api/views/auth.py index b5cfcf611..a7884957d 100644 --- a/dandiapi/api/views/auth.py +++ b/dandiapi/api/views/auth.py @@ -2,11 +2,10 @@ import json from json.decoder import JSONDecodeError +from typing import TYPE_CHECKING from django.conf import settings -from django.contrib.auth.models import User from django.db import transaction -from django.http import HttpRequest, HttpResponse from django.http.response import Http404, HttpResponseBase, HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.urls import reverse @@ -16,7 +15,6 @@ from rest_framework.authtoken.models import Token from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated -from rest_framework.request import Request from rest_framework.response import Response from dandiapi.api.mail import ( @@ -27,6 +25,11 @@ from dandiapi.api.models import UserMetadata from dandiapi.api.permissions import IsApproved +if TYPE_CHECKING: + from django.contrib.auth.models import User + from django.http import HttpRequest, HttpResponse + from rest_framework.request import Request + @swagger_auto_schema( methods=['GET', 'POST'], diff --git a/dandiapi/api/views/common.py b/dandiapi/api/views/common.py index 44c8996fe..d1391aa63 100644 --- a/dandiapi/api/views/common.py +++ b/dandiapi/api/views/common.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from drf_yasg import openapi from rest_framework.pagination import PageNumberPagination diff --git a/dandiapi/api/views/dandiset.py b/dandiapi/api/views/dandiset.py index 787836be0..040395aef 100644 --- a/dandiapi/api/views/dandiset.py +++ b/dandiapi/api/views/dandiset.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from allauth.socialaccount.models import SocialAccount from django.contrib.auth.models import User from django.db.models import Count, Max, OuterRef, Subquery, Sum @@ -13,7 +17,6 @@ from rest_framework.decorators import action from rest_framework.exceptions import NotAuthenticated, PermissionDenied from rest_framework.generics import get_object_or_404 -from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ValidationError from rest_framework.viewsets import ReadOnlyModelViewSet @@ -22,7 +25,7 @@ from dandiapi.api.mail import send_ownership_change_emails from dandiapi.api.models import Dandiset, Version from dandiapi.api.services.dandiset import create_dandiset, delete_dandiset -from dandiapi.api.services.dandiset.exceptions import UnauthorizedEmbargoAccess +from dandiapi.api.services.dandiset.exceptions import UnauthorizedEmbargoAccessError from dandiapi.api.services.embargo import unembargo_dandiset from dandiapi.api.views.common import DANDISET_PK_PARAM, DandiPagination from dandiapi.api.views.serializers import ( @@ -37,6 +40,9 @@ ) from dandiapi.search.models import AssetSearch +if TYPE_CHECKING: + from rest_framework.request import Request + class DandisetFilterBackend(filters.OrderingFilter): ordering_fields = ['id', 'name', 'modified', 'size'] @@ -122,7 +128,7 @@ def get_queryset(self): # Return early if attempting to access embargoed data without authentication if show_embargoed and not self.request.user.is_authenticated: - raise UnauthorizedEmbargoAccess() + raise UnauthorizedEmbargoAccessError if not show_draft: # Only include dandisets that have more than one version, i.e. published dandisets. diff --git a/dandiapi/api/views/dashboard.py b/dandiapi/api/views/dashboard.py index 642b0f9c5..6a595e086 100644 --- a/dandiapi/api/views/dashboard.py +++ b/dandiapi/api/views/dashboard.py @@ -1,4 +1,7 @@ -from allauth.socialaccount.models import SocialAccount +from __future__ import annotations + +from typing import TYPE_CHECKING + from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.models import User @@ -13,6 +16,9 @@ from dandiapi.api.models import Asset, AssetBlob, Upload, UserMetadata, Version from dandiapi.api.views.users import social_account_to_dict +if TYPE_CHECKING: + from allauth.socialaccount.models import SocialAccount + class DashboardMixin(LoginRequiredMixin, UserPassesTestMixin): def test_func(self): diff --git a/dandiapi/api/views/info.py b/dandiapi/api/views/info.py index b6e4bd86c..5561a78a4 100644 --- a/dandiapi/api/views/info.py +++ b/dandiapi/api/views/info.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.conf import settings from drf_yasg.utils import no_body, swagger_auto_schema from rest_framework import serializers diff --git a/dandiapi/api/views/root.py b/dandiapi/api/views/root.py index e68f011b8..b39da5311 100644 --- a/dandiapi/api/views/root.py +++ b/dandiapi/api/views/root.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.conf import settings from django.shortcuts import render diff --git a/dandiapi/api/views/serializers.py b/dandiapi/api/views/serializers.py index f28a39fe8..c7b99f422 100644 --- a/dandiapi/api/views/serializers.py +++ b/dandiapi/api/views/serializers.py @@ -1,5 +1,6 @@ -from collections import OrderedDict -from typing import Any +from __future__ import annotations + +from typing import TYPE_CHECKING, Any from django.conf import settings from django.contrib.auth.validators import UnicodeUsernameValidator @@ -10,6 +11,9 @@ from dandiapi.api.models import Asset, AssetBlob, AssetPath, Dandiset, Version from dandiapi.search.models import AssetSearch +if TYPE_CHECKING: + from collections import OrderedDict + def extract_contact_person(version: Version) -> str: """Extract a version's contact person from its metadata.""" diff --git a/dandiapi/api/views/stats.py b/dandiapi/api/views/stats.py index cf3c2a296..7175dd00d 100644 --- a/dandiapi/api/views/stats.py +++ b/dandiapi/api/views/stats.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.contrib.auth.models import User from django.views.decorators.cache import cache_page from rest_framework.decorators import api_view diff --git a/dandiapi/api/views/upload.py b/dandiapi/api/views/upload.py index 89ba37be5..fb00f12e7 100644 --- a/dandiapi/api/views/upload.py +++ b/dandiapi/api/views/upload.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING from django.db import transaction from django.http.response import Http404, HttpResponseBase @@ -11,7 +12,6 @@ from rest_framework.decorators import api_view, parser_classes, permission_classes from rest_framework.exceptions import ValidationError from rest_framework.parsers import JSONParser -from rest_framework.request import Request from rest_framework.response import Response from s3_file_field._multipart import TransferredPart, TransferredParts @@ -21,6 +21,9 @@ from dandiapi.api.tasks import calculate_sha256 from dandiapi.api.views.serializers import AssetBlobSerializer +if TYPE_CHECKING: + from rest_framework.request import Request + supported_digests = {'dandi:dandi-etag': 'etag', 'dandi:sha2-256': 'sha256'} diff --git a/dandiapi/api/views/users.py b/dandiapi/api/views/users.py index c4e6beccf..27362b8c1 100644 --- a/dandiapi/api/views/users.py +++ b/dandiapi/api/views/users.py @@ -2,22 +2,25 @@ import logging import re +from typing import TYPE_CHECKING from allauth.socialaccount.models import SocialAccount from django.contrib.auth.models import User from django.db.models import OuterRef, Q, Subquery -from django.http.response import HttpResponseBase from drf_yasg.utils import swagger_auto_schema from rest_framework.decorators import api_view, parser_classes, permission_classes from rest_framework.parsers import JSONParser from rest_framework.permissions import IsAuthenticated -from rest_framework.request import Request from rest_framework.response import Response from dandiapi.api.models import UserMetadata from dandiapi.api.permissions import IsApproved from dandiapi.api.views.serializers import UserDetailSerializer, UserSerializer +if TYPE_CHECKING: + from django.http.response import HttpResponseBase + from rest_framework.request import Request + logger = logging.getLogger(__name__) diff --git a/dandiapi/api/views/version.py b/dandiapi/api/views/version.py index 5c8ce119a..b94592414 100644 --- a/dandiapi/api/views/version.py +++ b/dandiapi/api/views/version.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import transaction from django.utils.decorators import method_decorator from django_filters import rest_framework as filters diff --git a/dandiapi/asgi.py b/dandiapi/asgi.py index bf8afa87a..bec2a9522 100644 --- a/dandiapi/asgi.py +++ b/dandiapi/asgi.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import configurations.importer diff --git a/dandiapi/celery.py b/dandiapi/celery.py index 4ca1d3d72..91e3ba7be 100644 --- a/dandiapi/celery.py +++ b/dandiapi/celery.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from celery import Celery, signals diff --git a/dandiapi/conftest.py b/dandiapi/conftest.py index 79e2ffb2d..4af84693b 100644 --- a/dandiapi/conftest.py +++ b/dandiapi/conftest.py @@ -1,10 +1,11 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from django.conf import settings -from django.core.files.storage import Storage -from minio_storage.storage import MinioStorage import pytest from pytest_factoryboy import register from rest_framework.test import APIClient -from storages.backends.s3 import S3Storage from dandiapi.api.storage import create_s3_storage from dandiapi.api.tests.factories import ( @@ -23,6 +24,11 @@ from dandiapi.zarr.tests.factories import ZarrArchiveFactory from dandiapi.zarr.tests.utils import upload_zarr_file +if TYPE_CHECKING: + from django.core.files.storage import Storage + from minio_storage.storage import MinioStorage + from storages.backends.s3 import S3Storage + register(PublishedAssetFactory, _name='published_asset') register(DraftAssetFactory, _name='draft_asset') register(AssetBlobFactory) @@ -84,7 +90,7 @@ def authenticated_api_client(user) -> APIClient: # storage fixtures are copied from django-s3-file-field test fixtures -def base_s3_storage_factory(bucket_name: str) -> 'S3Storage': +def base_s3_storage_factory(bucket_name: str) -> S3Storage: return create_s3_storage(bucket_name) @@ -109,12 +115,12 @@ def embargoed_minio_storage_factory() -> MinioStorage: @pytest.fixture() -def s3_storage() -> 'S3Storage': +def s3_storage() -> S3Storage: return s3_storage_factory() @pytest.fixture() -def embargoed_s3_storage() -> 'S3Storage': +def embargoed_s3_storage() -> S3Storage: return s3_storage_factory() diff --git a/dandiapi/drf_utils.py b/dandiapi/drf_utils.py index ddb80f9e0..8aeb3da12 100644 --- a/dandiapi/drf_utils.py +++ b/dandiapi/drf_utils.py @@ -1,9 +1,14 @@ +from __future__ import annotations + from django.core.exceptions import ( PermissionDenied as DjangoPermissionDenied, +) +from django.core.exceptions import ( ValidationError as DjangoValidationError, ) from django.http import Http404 -from rest_framework import exceptions as drf_exceptions, status +from rest_framework import exceptions as drf_exceptions +from rest_framework import status from rest_framework.response import Response from rest_framework.serializers import as_serializer_error from rest_framework.views import exception_handler diff --git a/dandiapi/search/apps.py b/dandiapi/search/apps.py index a83b47ccc..099c61ef4 100644 --- a/dandiapi/search/apps.py +++ b/dandiapi/search/apps.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.apps import AppConfig diff --git a/dandiapi/search/migrations/0001_initial.py b/dandiapi/search/migrations/0001_initial.py index 7a327a543..34ef2fae5 100644 --- a/dandiapi/search/migrations/0001_initial.py +++ b/dandiapi/search/migrations/0001_initial.py @@ -1,5 +1,5 @@ # Generated by Django 4.1.4 on 2023-03-14 22:52 -# flake8: noqa +from __future__ import annotations from django.contrib.postgres.operations import TrigramExtension from django.db import migrations, models @@ -12,7 +12,7 @@ VERSION_TABLE = Version._meta.db_table VERSION_ASSET_TABLE = Asset.versions.through._meta.db_table -raw_sql = f''' +raw_sql = f""" CREATE MATERIALIZED VIEW asset_search AS SELECT DISTINCT {VERSION_TABLE}.dandiset_id AS dandiset_id, @@ -27,13 +27,18 @@ CREATE UNIQUE INDEX idx_asset_search_dandiset_id_asset_id ON asset_search (dandiset_id, asset_id); -CREATE INDEX idx_asset_search_asset_size ON asset_search (asset_size); -CREATE INDEX idx_asset_search_measurement_technique ON asset_search USING gin ((asset_metadata->'measurementTechnique')); -CREATE INDEX asset_search_metadata_species_name_idx ON asset_search ((asset_metadata #> '{{wasAttributedTo,0,species,name}}')); -CREATE INDEX asset_search_metadata_genotype_name_idx ON asset_search ((asset_metadata #> '{{wasAttributedTo,0,genotype,name}}')); +CREATE INDEX idx_asset_search_asset_size + ON asset_search (asset_size); +CREATE INDEX idx_asset_search_measurement_technique + ON asset_search USING gin ((asset_metadata->'measurementTechnique')); +CREATE INDEX asset_search_metadata_species_name_idx + ON asset_search ((asset_metadata #> '{{wasAttributedTo,0,species,name}}')); +CREATE INDEX asset_search_metadata_genotype_name_idx + ON asset_search ((asset_metadata #> '{{wasAttributedTo,0,genotype,name}}')); -CREATE INDEX idx_asset_search_encoding_format ON asset_search USING gin (UPPER(asset_metadata->>'encodingFormat') gin_trgm_ops); -''' +CREATE INDEX idx_asset_search_encoding_format + ON asset_search USING gin (UPPER(asset_metadata->>'encodingFormat') gin_trgm_ops); +""" # noqa: S608 class Migration(migrations.Migration): @@ -48,7 +53,10 @@ class Migration(migrations.Migration): name='AssetSearch', fields=[ ('dandiset_id', models.PositiveBigIntegerField()), - ('asset_id', models.PositiveBigIntegerField(primary_key=True, serialize=False)), + ( + 'asset_id', + models.PositiveBigIntegerField(primary_key=True, serialize=False), + ), ('asset_metadata', models.JSONField()), ('asset_size', models.PositiveBigIntegerField()), ], diff --git a/dandiapi/search/models.py b/dandiapi/search/models.py index 6be39ac03..41eb4f68c 100644 --- a/dandiapi/search/models.py +++ b/dandiapi/search/models.py @@ -1,12 +1,16 @@ from __future__ import annotations -from django.contrib.auth.models import User +from typing import TYPE_CHECKING + from django.db import models from django.db.models import OuterRef, Q, Subquery from guardian.shortcuts import get_objects_for_user from dandiapi.api.models import Dandiset +if TYPE_CHECKING: + from django.contrib.auth.models import User + class AssetSearchManager(models.Manager): def visible_to(self, user: User) -> models.QuerySet[AssetSearch]: diff --git a/dandiapi/search/tests/test_permissions.py b/dandiapi/search/tests/test_permissions.py index bf9f82578..1de98f343 100644 --- a/dandiapi/search/tests/test_permissions.py +++ b/dandiapi/search/tests/test_permissions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import connection import pytest diff --git a/dandiapi/search/tests/test_views.py b/dandiapi/search/tests/test_views.py index 01b5593fc..5f777d765 100644 --- a/dandiapi/search/tests/test_views.py +++ b/dandiapi/search/tests/test_views.py @@ -1,5 +1,11 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + import pytest -from rest_framework.test import APIClient + +if TYPE_CHECKING: + from rest_framework.test import APIClient @pytest.mark.django_db() diff --git a/dandiapi/search/views/__init__.py b/dandiapi/search/views/__init__.py index 055a3473c..ebe19a85d 100644 --- a/dandiapi/search/views/__init__.py +++ b/dandiapi/search/views/__init__.py @@ -1,5 +1,7 @@ -from django.contrib.auth.models import User -from django.db.models.query import QuerySet +from __future__ import annotations + +from typing import TYPE_CHECKING + from django.http.response import JsonResponse from drf_yasg.utils import swagger_auto_schema from rest_framework import serializers @@ -8,6 +10,10 @@ from dandiapi.search.models import AssetSearch +if TYPE_CHECKING: + from django.contrib.auth.models import User + from django.db.models.query import QuerySet + class SearchSerializer(serializers.Serializer): def to_queryset(self, user: User) -> QuerySet[AssetSearch]: diff --git a/dandiapi/settings.py b/dandiapi/settings.py index 51b898e2a..5550e116e 100644 --- a/dandiapi/settings.py +++ b/dandiapi/settings.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from pathlib import Path diff --git a/dandiapi/swagger.py b/dandiapi/swagger.py index ac10510f0..8cdc144ef 100644 --- a/dandiapi/swagger.py +++ b/dandiapi/swagger.py @@ -1,5 +1,7 @@ # Separate from settings.py due to being a somewhat of a premature import there. # See https://github.com/dandi/dandi-api/pull/482#issuecomment-901250541 . +from __future__ import annotations + from drf_yasg.inspectors import SwaggerAutoSchema diff --git a/dandiapi/urls.py b/dandiapi/urls.py index b6f889b85..32267b165 100644 --- a/dandiapi/urls.py +++ b/dandiapi/urls.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.conf import settings from django.contrib import admin from django.urls import include, path, re_path, register_converter diff --git a/dandiapi/wsgi.py b/dandiapi/wsgi.py index a42b8a6a3..607460f4a 100644 --- a/dandiapi/wsgi.py +++ b/dandiapi/wsgi.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import configurations.importer diff --git a/dandiapi/zarr/admin.py b/dandiapi/zarr/admin.py index 690f07569..b0fba4eda 100644 --- a/dandiapi/zarr/admin.py +++ b/dandiapi/zarr/admin.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.contrib import admin, messages from django.utils.translation import ngettext diff --git a/dandiapi/zarr/apps.py b/dandiapi/zarr/apps.py index c849386eb..8b4b30035 100644 --- a/dandiapi/zarr/apps.py +++ b/dandiapi/zarr/apps.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.apps import AppConfig diff --git a/dandiapi/zarr/management/commands/ingest_dandiset_zarrs.py b/dandiapi/zarr/management/commands/ingest_dandiset_zarrs.py index 92f6adecb..78da99ae5 100644 --- a/dandiapi/zarr/management/commands/ingest_dandiset_zarrs.py +++ b/dandiapi/zarr/management/commands/ingest_dandiset_zarrs.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import djclick as click from dandiapi.zarr.tasks import ingest_dandiset_zarrs as _ingest_dandiset_zarrs diff --git a/dandiapi/zarr/management/commands/ingest_zarr_archive.py b/dandiapi/zarr/management/commands/ingest_zarr_archive.py index 0ba14ed4f..3bae54d3d 100644 --- a/dandiapi/zarr/management/commands/ingest_zarr_archive.py +++ b/dandiapi/zarr/management/commands/ingest_zarr_archive.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import djclick as click from dandiapi.zarr.tasks import ingest_zarr_archive as _ingest_zarr_archive diff --git a/dandiapi/zarr/migrations/0001_initial_v2.py b/dandiapi/zarr/migrations/0001_initial_v2.py index 8c5c46835..b50facfe6 100644 --- a/dandiapi/zarr/migrations/0001_initial_v2.py +++ b/dandiapi/zarr/migrations/0001_initial_v2.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import uuid from django.db import migrations, models diff --git a/dandiapi/zarr/migrations/0002_null_charfield.py b/dandiapi/zarr/migrations/0002_null_charfield.py new file mode 100644 index 000000000..782703bd1 --- /dev/null +++ b/dandiapi/zarr/migrations/0002_null_charfield.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.13 on 2023-11-11 04:25 +from __future__ import annotations + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('zarr', '0001_initial_v2'), + ] + + operations = [ + migrations.AlterField( + model_name='embargoedzarrarchive', + name='checksum', + field=models.CharField(blank=True, default=None, max_length=512, null=True), + ), + migrations.AlterField( + model_name='zarrarchive', + name='checksum', + field=models.CharField(blank=True, default=None, max_length=512, null=True), + ), + ] diff --git a/dandiapi/zarr/models.py b/dandiapi/zarr/models.py index 33a0dfdec..d558e9b92 100644 --- a/dandiapi/zarr/models.py +++ b/dandiapi/zarr/models.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from pathlib import Path +from typing import TYPE_CHECKING from urllib.parse import urlparse, urlunparse from uuid import uuid4 @@ -13,6 +13,9 @@ from dandiapi.api.models import Dandiset from dandiapi.api.storage import get_embargo_storage, get_storage +if TYPE_CHECKING: + from pathlib import Path + logger = logging.Logger(name=__name__) @@ -54,7 +57,7 @@ class Meta: name = models.CharField(max_length=512) file_count = models.BigIntegerField(default=0) size = models.BigIntegerField(default=0) - checksum = models.CharField(max_length=512, null=True, default=None) + checksum = models.CharField(max_length=512, null=True, default=None, blank=True) # noqa: DJ001 status = models.CharField( max_length=max(len(choice[0]) for choice in ZarrArchiveStatus.choices), choices=ZarrArchiveStatus.choices, @@ -102,7 +105,10 @@ class ZarrArchive(BaseZarrArchive): def s3_path(self, zarr_path: str | Path): """Generate a full S3 object path from a path in this zarr_archive.""" - return f'{settings.DANDI_DANDISETS_BUCKET_PREFIX}{settings.DANDI_ZARR_PREFIX_NAME}/{self.zarr_id}/{zarr_path}' # noqa: E501 + return ( + f'{settings.DANDI_DANDISETS_BUCKET_PREFIX}{settings.DANDI_ZARR_PREFIX_NAME}/' + f'{self.zarr_id}/{zarr_path}' + ) class EmbargoedZarrArchive(BaseZarrArchive): @@ -113,4 +119,7 @@ class EmbargoedZarrArchive(BaseZarrArchive): def s3_path(self, zarr_path: str | Path): """Generate a full S3 object path from a path in this zarr_archive.""" - return f'{settings.DANDI_DANDISETS_EMBARGO_BUCKET_PREFIX}{settings.DANDI_ZARR_PREFIX_NAME}/{self.dandiset.identifier}/{self.zarr_id}/{zarr_path}' # noqa: E501 + return ( + f'{settings.DANDI_DANDISETS_EMBARGO_BUCKET_PREFIX}{settings.DANDI_ZARR_PREFIX_NAME}/' + f'{self.dandiset.identifier}/{self.zarr_id}/{zarr_path}' + ) diff --git a/dandiapi/zarr/tests/factories.py b/dandiapi/zarr/tests/factories.py index 844b6e5f1..d536e3152 100644 --- a/dandiapi/zarr/tests/factories.py +++ b/dandiapi/zarr/tests/factories.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import factory from dandiapi.api.tests.factories import DandisetFactory diff --git a/dandiapi/zarr/tests/test_ingest_zarr_archive.py b/dandiapi/zarr/tests/test_ingest_zarr_archive.py index 54bd06068..c14c3b8b6 100644 --- a/dandiapi/zarr/tests/test_ingest_zarr_archive.py +++ b/dandiapi/zarr/tests/test_ingest_zarr_archive.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.conf import settings from guardian.shortcuts import assign_perm import pytest diff --git a/dandiapi/zarr/tests/test_zarr.py b/dandiapi/zarr/tests/test_zarr.py index e568d7c19..dfbc651d4 100644 --- a/dandiapi/zarr/tests/test_zarr.py +++ b/dandiapi/zarr/tests/test_zarr.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.conf import settings from guardian.shortcuts import assign_perm import pytest @@ -432,7 +434,7 @@ def test_zarr_file_list(api_client, storage, zarr_archive: ZarrArchive, zarr_fil ) assert resp.status_code == 302 assert resp.headers['Location'].startswith( - f'http://{settings.MINIO_STORAGE_ENDPOINT}/test-dandiapi-dandisets/test-prefix/test-zarr/{zarr_archive.zarr_id}/foo/bar/a.txt?' # noqa: E501 + f'http://{settings.MINIO_STORAGE_ENDPOINT}/test-dandiapi-dandisets/test-prefix/test-zarr/{zarr_archive.zarr_id}/foo/bar/a.txt?' ) @@ -445,5 +447,5 @@ def test_zarr_explore_head(api_client, storage, zarr_archive: ZarrArchive): resp = api_client.head(f'/api/zarr/{zarr_archive.zarr_id}/files/', {'prefix': filepath}) assert resp.status_code == 302 assert resp.headers['Location'].startswith( - f'http://{settings.MINIO_STORAGE_ENDPOINT}/test-dandiapi-dandisets/test-prefix/test-zarr/{zarr_archive.zarr_id}/{filepath}?' # noqa: E501 + f'http://{settings.MINIO_STORAGE_ENDPOINT}/test-dandiapi-dandisets/test-prefix/test-zarr/{zarr_archive.zarr_id}/{filepath}?' ) diff --git a/dandiapi/zarr/tests/test_zarr_upload.py b/dandiapi/zarr/tests/test_zarr_upload.py index 2cf4bdc5a..fbb29ddc1 100644 --- a/dandiapi/zarr/tests/test_zarr_upload.py +++ b/dandiapi/zarr/tests/test_zarr_upload.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from guardian.shortcuts import assign_perm import pytest from zarr_checksum.checksum import EMPTY_CHECKSUM diff --git a/dandiapi/zarr/tests/utils.py b/dandiapi/zarr/tests/utils.py index 27133ee56..a4412284a 100644 --- a/dandiapi/zarr/tests/utils.py +++ b/dandiapi/zarr/tests/utils.py @@ -1,12 +1,17 @@ +from __future__ import annotations + import hashlib import os from pathlib import Path +from typing import TYPE_CHECKING import faker from zarr_checksum.generators import ZarrArchiveFile from dandiapi.api.storage import get_boto_client -from dandiapi.zarr.models import ZarrArchive + +if TYPE_CHECKING: + from dandiapi.zarr.models import ZarrArchive def upload_zarr_file(zarr_archive: ZarrArchive, path: str | None = None, size: int = 100): diff --git a/dandiapi/zarr/views/__init__.py b/dandiapi/zarr/views/__init__.py index 7d780e066..8c5a60585 100644 --- a/dandiapi/zarr/views/__init__.py +++ b/dandiapi/zarr/views/__init__.py @@ -2,9 +2,9 @@ import logging from pathlib import Path +from typing import TYPE_CHECKING from django.db import IntegrityError, transaction -from django.db.models.query import QuerySet from django.http import HttpResponseRedirect from drf_yasg.utils import no_body, swagger_auto_schema from rest_framework import serializers, status @@ -21,6 +21,9 @@ from dandiapi.zarr.models import ZarrArchive, ZarrArchiveStatus from dandiapi.zarr.tasks import ingest_zarr_archive +if TYPE_CHECKING: + from django.db.models.query import QuerySet + logger = logging.getLogger(__name__) diff --git a/manage.py b/manage.py index 27eb1a701..37dd76dfe 100755 --- a/manage.py +++ b/manage.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +from __future__ import annotations + import os import sys diff --git a/pyproject.toml b/pyproject.toml index 07d473f5d..40e898a78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,17 +1,3 @@ -[tool.black] -line-length = 100 -skip-string-normalization = true -target-version = ["py311"] -exclude='\.eggs|\.git|\.mypy_cache|\.tox|\.venv|_build|buck-out|build|dist' - -[tool.isort] -profile = "black" -line_length = 100 -# Sort by name, don't cluster "from" vs "import" -force_sort_within_sections = true -# Combines "as" imports on the same line -combine_as_imports = true - [tool.mypy] ignore_missing_imports = true show_error_codes = true @@ -28,3 +14,78 @@ exclude = [ # [tool.django-stubs] # django_settings_module = "dandiapi.settings" + +[tool.ruff] +line-length = 100 +target-version = "py311" +select = ["ALL"] +ignore = [ + # Incompatible with formatter + # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "COM812", # missing-trailing-comma + "COM819", # prohibited-trailing-comma + "D206", # indent-with-spaces + "D300", # triple-single-quotes + "E111", # indentation-with-invalid-multiple + "E114", # indentation-with-invalid-multiple-comment + "E117", # over-indented + "ISC001", # single-line-implicit-string-concatenation + "ISC002", # multi-line-implicit-string-concatenation + "Q", # flake8-quotes + "W191", # tab-indentation + + "A003", # Class attribute is shadowing a Python builtin + "ANN", # flake8-annotations + "ARG001", # Unused function argument + "ARG002", # Unused method argument + "D1", # Missing docstring + "EM101", # Exception must not use a string literal, assign to variable first + "EM102", # Exception must not use an f-string literal, assign to variable first + "ERA001", # Found commented-out code + "FIX", # flake8-fixme + "TD002", # Missing author in TODO + "TD003", # Missing issue link on the line following this TODO + "TRY003", # Avoid specifying long messages outside the exception class + + # Try to fix upstream + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + + # Fix in DANDI codebase. PR that will fix this: + # https://github.com/dandi/dandi-archive/pull/1782 + "PTH119", # TODO: re-enable this when it's fixed +] + +[tool.ruff.per-file-ignores] +"scripts/**" = [ + "INP001", # File is part of an implicit namespace package +] +"**/migrations/**" = [ + "N806", # Variable in function should be lowercase + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` +] +"**/management/commands/**" = [ + "INP001", # File is part of an implicit namespace package +] +"**/tests/**" = [ + "DJ007", # Do not use `__all__` + "DJ008", # Model does not define `__str__` method + "PLR0913", # Too many arguments to function call + "PLR2004", # Magic value used in comparison + "S", # flake8-bandit + "SLF001", # Private member accessed +] + +[tool.ruff.format] +quote-style = "single" + +[tool.ruff.lint.flake8-self] +extend-ignore-names = ["_base_manager", "_default_manager", "_meta"] + +[tool.ruff.lint.isort] +# Sort by name, don't cluster "from" vs "import" +force-sort-within-sections = true +# Deferred annotations allows TCH rules to move more imports into TYPE_CHECKING blocks +required-imports = ["from __future__ import annotations"] + +[tool.ruff.lint.pydocstyle] +convention = "pep257" diff --git a/scripts/delete_from_versioned_bucket.py b/scripts/delete_from_versioned_bucket.py index e6595e80b..7528be7b0 100644 --- a/scripts/delete_from_versioned_bucket.py +++ b/scripts/delete_from_versioned_bucket.py @@ -11,6 +11,8 @@ You will need an AWS CLI profile set up with permission to delete objects from the desired bucket. See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html """ +from __future__ import annotations + from concurrent.futures import ThreadPoolExecutor, as_completed import boto3 @@ -31,7 +33,7 @@ def delete_version(bucket, thing): @click.option( '--profile', prompt='AWS profile name', - help='The AWS profile to use. See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html', # noqa: E501 + help='The AWS profile to use. See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html', ) @click.option('--bucket', prompt='Bucket', help='The bucket to delete from') @click.option('--prefix', prompt='Path to delete', help='The prefix to delete all files under') diff --git a/scripts/papertrail.py b/scripts/papertrail.py index 72f4c2570..068dc07bd 100755 --- a/scripts/papertrail.py +++ b/scripts/papertrail.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 """Script that will export all desired papertrail log files.""" +from __future__ import annotations + from datetime import datetime import os from pathlib import Path @@ -31,7 +33,7 @@ @click.option( '-o', '--out', 'output_file', help='The output file', default='logs.tsv.gz', show_default=True ) -def cli(*, start, end, force, amend, output_file): +def cli(*, start, end, force, amend, output_file): # noqa: C901 if PAPERTRAIL_TOKEN is None: raise ClickException( 'Must set the PAPERTRAIL_APIKEY environment variable. ' diff --git a/setup.py b/setup.py index b53f1f84b..1245f9ffc 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pathlib import Path from setuptools import find_namespace_packages, setup diff --git a/tox.ini b/tox.ini index 0197f71cd..5b4304efb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,6 @@ [tox] +# Don't use "min_version", to ensure Tox 3 respects this +minversion = 4 envlist = lint, type, @@ -6,20 +8,22 @@ envlist = check-migrations, [testenv:lint] -skipsdist = true -skip_install = true +package = skip deps = codespell~=2.0 - flake8 - flake8-black >= 0.2.4 - flake8-bugbear - flake8-docstrings - flake8-isort - flake8-quotes - pep8-naming + ruff commands = - flake8 --config=tox.ini {posargs:.} - codespell {posargs:.} + ruff check + ruff format --check + codespell . + +[testenv:format] +package = skip +deps = + ruff +commands = + ruff check --fix-only + ruff format [testenv:type] deps = @@ -29,16 +33,6 @@ extras = commands = mypy {posargs:dandiapi/} -[testenv:format] -skipsdist = true -skip_install = true -deps = - black >= 23.1.0 - isort -commands = - isort {posargs:.} - black {posargs:.} - [testenv:test] passenv = DJANGO_CELERY_BROKER_URL @@ -75,27 +69,6 @@ extras = commands = {envpython} ./manage.py makemigrations --check --dry-run -[flake8] -max-line-length = 100 -show-source = True -ignore = - # closing bracket does not match indentation of opening bracket’s line - E123 - # whitespace before ':' - E203, - # line break before binary operator - W503, - # Missing docstring in * - D10, - # variables should be lowercased - N806, - # exceptions need Error in their name - N818 -extend-exclude = - build, - dist, - venvs, - [pytest] DJANGO_SETTINGS_MODULE = dandiapi.settings DJANGO_CONFIGURATION = TestingConfiguration