Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .copier-answers.resonant.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
_commit: v0.41.0
_commit: v0.47.1
_src_path: gh:kitware-resonant/cookiecutter-resonant
core_app_name: api
include_example_code: false
project_name: DANDI Archive
project_slug: dandiapi
python_package_name: dandiapi
site_domain: api.dandiarchive.org
use_asgi: false
21 changes: 14 additions & 7 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,28 @@ insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8

[*.py]
indent_size = 4
max_line_length = 100
[*.{css,pcss}]
indent_size = 2

[*.html]
indent_size = 2

[*.css]
[*.ini]
indent_size = 4

[*.{js,ts,vue}]
indent_size = 2
max_line_length = 100

[{*.yml,*.yaml}]
[*.{json,jsonc,json5}]
indent_size = 2

[*.ini]
[*.py]
indent_size = 4
max_line_length = 100

[*.toml]
indent_size = 2

[*.sh]
[*.{yml,yaml}]
indent_size = 2
Empty file.
Empty file.
19 changes: 6 additions & 13 deletions dandiapi/api/management/commands/cleanup_blobs.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,28 @@
from __future__ import annotations

from django.conf import settings
from django.core.files.storage import default_storage
import djclick as click
from storages.backends.s3 import S3Storage

from dandiapi.api.models.upload import AssetBlob

BUCKET = settings.DANDI_DANDISETS_BUCKET_NAME


def s3_client():
storage = S3Storage(bucket_name=BUCKET)
return storage.connection.meta.client


@click.command()
@click.option('--delete', is_flag=True, default=False)
def cleanup_blobs(*, delete: bool):
client = s3_client()
client = default_storage.s3_client
bucket_name = default_storage.bucket_name
# Ignore pagination for now, hopefully there aren't enough objects to matter
objs = client.list_object_versions(Bucket=BUCKET, Prefix='dev/')
objs = client.list_object_versions(Bucket=bucket_name, Prefix='dev/')
for version in objs['Versions']:
if not AssetBlob.objects.filter(etag=version['ETag'][1:-1]).exists():
click.echo(f'Deleting version {version["Key"]}')
if delete:
client.delete_object(
Bucket=BUCKET, Key=version['Key'], VersionId=version['VersionId']
Bucket=bucket_name, Key=version['Key'], VersionId=version['VersionId']
)
for delete_marker in objs['DeleteMarkers']:
click.echo(f'Deleting delete marker {delete_marker["Key"]}')
if delete:
client.delete_object(
Bucket=BUCKET, Key=delete_marker['Key'], VersionId=delete_marker['VersionId']
Bucket=bucket_name, Key=delete_marker['Key'], VersionId=delete_marker['VersionId']
)
3 changes: 2 additions & 1 deletion dandiapi/api/management/commands/createsuperuser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
from typing import TYPE_CHECKING

from django.db.models.signals import post_init, post_save
from resonant_settings.allauth_support.management.commands import createsuperuser

from dandiapi.api.models.user import UserMetadata

from resonant_settings.allauth_support.management.commands import createsuperuser

if TYPE_CHECKING:
from resonant_settings.allauth_support.createsuperuser import EmailAsUsernameProxyUser

Expand Down
6 changes: 3 additions & 3 deletions dandiapi/api/migrations/0001_default_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.db.migrations.state import StateApps


def update_default_site(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor):
def update_default_site(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
Site = apps.get_model('sites', 'Site')

# A default site object may or may not exist.
Expand All @@ -27,7 +27,7 @@ def update_default_site(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
)


def rollback_default_site(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor):
def rollback_default_site(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
Site = apps.get_model('sites', 'Site')

# This is the initial value of the default site object, as populated by the sites app.
Expand All @@ -42,5 +42,5 @@ class Migration(migrations.Migration):
]

operations = [
migrations.RunPython(update_default_site, rollback_default_site),
migrations.RunPython(update_default_site, rollback_default_site, elidable=False),
]
3 changes: 1 addition & 2 deletions dandiapi/api/services/embargo/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import logging
from typing import TYPE_CHECKING

from django.conf import settings
from django.core.files.storage import default_storage
from django.db.models import Q
from more_itertools import chunked
Expand Down Expand Up @@ -32,7 +31,7 @@ def _delete_object_tags(blob: str):
def _delete_zarr_object_tags(zarr: str):
paginator = ZarrArchive.storage.s3_client.get_paginator('list_objects_v2')
pages = paginator.paginate(
Bucket=settings.DANDI_DANDISETS_BUCKET_NAME, Prefix=zarr_s3_path(zarr_id=zarr)
Bucket=default_storage.bucket_name, Prefix=zarr_s3_path(zarr_id=zarr)
)

# Constant low thread number to limit memory usage, as each thread
Expand Down
7 changes: 5 additions & 2 deletions dandiapi/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from corsheaders.defaults import default_headers
import django_stubs_ext
from environ import Env

from resonant_settings.allauth import *
from resonant_settings.celery import *
from resonant_settings.django import *
Expand All @@ -30,6 +31,8 @@

ROOT_URLCONF = 'dandiapi.urls'

WSGI_APPLICATION = 'dandiapi.wsgi.application'

INSTALLED_APPS = [
# Install local apps first, to ensure any overridden resources are found first
'dandiapi.api.apps.ApiConfig',
Expand Down Expand Up @@ -79,6 +82,7 @@
# Add username middleware after authentication to capture username for gunicorn access logs
'dandiapi.api.middleware.GunicornUsernameMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.contrib.sites.middleware.CurrentSiteMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'allauth.account.middleware.AccountMiddleware',
]
Expand All @@ -90,7 +94,7 @@
'default': {
**env.db_url('DJANGO_DATABASE_URL', engine='django.db.backends.postgresql'),
'CONN_MAX_AGE': timedelta(minutes=10).total_seconds(),
}
},
}
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

Expand All @@ -102,7 +106,6 @@
'BACKEND': 'whitenoise.storage.CompressedStaticFilesStorage',
},
}
DANDI_DANDISETS_BUCKET_NAME: str

STATIC_ROOT = BASE_DIR / 'staticfiles'
# Django staticfiles auto-creates any intermediate directories, but do so here to prevent warnings.
Expand Down
9 changes: 4 additions & 5 deletions dandiapi/settings/development.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
from .base import *

# Import these afterwards, to override
from resonant_settings.development.celery import * # isort: skip
from resonant_settings.development.debug_toolbar import * # isort: skip
from resonant_settings.development.celery import *
from resonant_settings.development.debug_toolbar import *

INSTALLED_APPS += [
'debug_toolbar',
'django_browser_reload',
]
# Force WhiteNoice to serve static files, even when using 'manage.py runserver_plus'
# Force WhiteNoise to serve static files, even when using "manage.py runserver_plus"
staticfiles_index = INSTALLED_APPS.index('django.contrib.staticfiles')
INSTALLED_APPS.insert(staticfiles_index, 'whitenoise.runserver_nostatic')

Expand All @@ -33,7 +33,7 @@
# to add new settings as individual feature flags.
DEBUG = True

SECRET_KEY = 'insecure-secret' # noqa: S105
SECRET_KEY = 'insecure-secret'

# This is typically only overridden when running from Docker.
INTERNAL_IPS = InternalIPS(env.list('DJANGO_INTERNAL_IPS', cast=str, default=['127.0.0.1']))
Expand All @@ -56,7 +56,6 @@
'media_url': _storage_media_url,
},
}
DANDI_DANDISETS_BUCKET_NAME = STORAGES['default']['OPTIONS']['bucket_name'] # TODO: remove

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Expand Down
2 changes: 1 addition & 1 deletion dandiapi/settings/heroku_production.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# Provided by https://github.com/ianpurvis/heroku-buildpack-version
os.environ['DJANGO_SENTRY_RELEASE'] = os.environ['SOURCE_VERSION']

from .production import * # isort: skip
from .production import *

# This needs to be set by the HTTPS terminating reverse proxy.
# Heroku and Render automatically set this.
Expand Down
17 changes: 7 additions & 10 deletions dandiapi/settings/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@
from .base import *

# Import these afterwards, to override
from resonant_settings.production.email import * # isort: skip
from resonant_settings.production.https import * # isort: skip

WSGI_APPLICATION = 'dandiapi.wsgi.application'
from resonant_settings.production.email import *
from resonant_settings.production.https import *

SECRET_KEY: str = env.str('DJANGO_SECRET_KEY')

Expand All @@ -27,18 +25,17 @@
# Only allow GitHub auth on production, no username/password
SOCIALACCOUNT_ONLY = True

# This only needs to be defined in production. Testing will add 'testserver'. In development
# (specifically when DEBUG is True), 'localhost' and '127.0.0.1' will be added.
# This only needs to be defined in production. Testing will add "testserver". In development
# (specifically when DEBUG is True), "localhost" and "127.0.0.1" will be added.
ALLOWED_HOSTS: list[str] = env.list('DJANGO_ALLOWED_HOSTS', cast=str)

DANDI_DANDISETS_BUCKET_NAME: str = env.str('DJANGO_STORAGE_BUCKET_NAME')
STORAGES['default'] = {
'BACKEND': 'dandiapi.storage.DandiS3Storage',
'OPTIONS': {
'region_name': env.str('AWS_DEFAULT_REGION'),
'access_key': env.str('AWS_ACCESS_KEY_ID'),
'secret_key': env.str('AWS_SECRET_ACCESS_KEY'),
'bucket_name': DANDI_DANDISETS_BUCKET_NAME,
'bucket_name': env.str('DJANGO_STORAGE_BUCKET_NAME'),
'querystring_expire': int(timedelta(hours=6).total_seconds()),
'max_memory_size': 5 * 1024 * 1024,
},
Expand All @@ -53,8 +50,8 @@
DANDI_DEV_EMAIL: str = env.str('DJANGO_DANDI_DEV_EMAIL')
DANDI_ADMIN_EMAIL: str = env.str('DJANGO_DANDI_ADMIN_EMAIL')

# sentry_sdk is able to directly use environment variables like 'SENTRY_DSN', but prefix them
# with 'DJANGO_' to avoid conflicts with other Sentry-using services.
# sentry_sdk is able to directly use environment variables like "SENTRY_DSN", but prefix them
# with "DJANGO_" to avoid conflicts with other Sentry-using services.
sentry_sdk.init(
dsn=env.str('DJANGO_SENTRY_DSN', default=None),
environment=env.str('DJANGO_SENTRY_ENVIRONMENT', default=None),
Expand Down
7 changes: 4 additions & 3 deletions dandiapi/settings/testing.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations

from secrets import randbelow

from .base import *

SECRET_KEY = 'insecure-secret' # noqa: S105
SECRET_KEY = 'insecure-secret'

# Use a fast, insecure hasher to speed up tests
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
Expand All @@ -14,11 +16,10 @@
'endpoint_url': f'{_minio_url.scheme}://{_minio_url.hostname}:{_minio_url.port}',
'access_key': _minio_url.username,
'secret_key': _minio_url.password,
'bucket_name': 'test-django-storage',
'bucket_name': f'test-django-storage-{randbelow(1_000_000):06d}',
'querystring_expire': int(timedelta(hours=6).total_seconds()),
},
}
DANDI_DANDISETS_BUCKET_NAME = 'test-django-storage'

# Testing will set EMAIL_BACKEND to use the memory backend

Expand Down
Empty file.
Empty file.
8 changes: 5 additions & 3 deletions gunicorn.conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

bind = f'0.0.0.0:{os.environ.get("PORT", "8000")}'

# Explicitly set the timeout to 5 seconds less than the Heroku request timeout so
# that gunicorn can gracefully shut down the worker if a request times out.
timeout = 25
# Set `graceful_timeout` to shorter than the 30 second limit imposed by Heroku restarts
graceful_timeout = 25

# Set `timeout` to shorter than the 30 second limit imposed by the Heroku router
timeout = 15

# Add the username to the access log (set by Django middleware)
access_log_format = AccessLogFormat.default + ' <username:%({x-request-username}o)s>'
Loading
Loading