diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml index 4308204b6..ec20d95cf 100644 --- a/.github/workflows/frontend-ci.yml +++ b/.github/workflows/frontend-ci.yml @@ -105,6 +105,9 @@ jobs: - name: Apply migrations to API server run: ./manage.py migrate + - name: Create any cache tables + run: ./manage.py createcachetable + - name: Install test data run: ./manage.py loaddata playwright diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 8645f271c..65349f538 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -19,6 +19,7 @@ This is the simplest configuration for developers to start with. 1. From VSCode, use `Ctrl-Shift-p` and run the command `Dev Containers: Reopen in Container`. 1. From the VSCode built-in terminal, run `./manage.py migrate`. +1. Run `docker compose run --rm django ./manage.py createcachetable` 1. From the VSCode built-in terminal, run `./manage.py createsuperuser --email $(git config user.email)` and follow the prompts. 1. From the VSCode built-in terminal, run `./manage.py create_dev_dandiset --owner $(git config user.email)` to create a dummy dandiset to start working with. @@ -37,6 +38,7 @@ This configuration also uses containers, but with Docker Compose instead of VSco ### Initial Setup 1. Install [Docker Compose](https://docs.docker.com/compose/install/) 1. Run `docker compose run --rm django ./manage.py migrate` +1. Run `docker compose run --rm django ./manage.py createcachetable` 1. Run `docker compose run --rm django ./manage.py createsuperuser --email $(git config user.email)` and follow the prompts to create your own user. This sets your username to your git email to ensure parity with how GitHub logins work. You can also replace the command substitution expression with a literal email address, or omit the `--email` option entirely to run the command in interactive mode. @@ -67,6 +69,7 @@ but allows developers to run Python code on their native system. 1. [Install `uv`](https://docs.astral.sh/uv/getting-started/installation/) 1. Run `export UV_ENV_FILE=./dev/.env.docker-compose-native` 1. Run `./manage.py migrate` +1. Run `docker compose run --rm django ./manage.py createcachetable` 1. Run `./manage.py createsuperuser --email $(git config user.email)` and follow the prompts. 1. Run `./manage.py create_dev_dandiset --owner $(git config user.email)` to create a dummy dandiset to start working with. diff --git a/dandiapi/api/tests/test_info.py b/dandiapi/api/tests/test_info.py index 2c02b0870..da82b8b2b 100644 --- a/dandiapi/api/tests/test_info.py +++ b/dandiapi/api/tests/test_info.py @@ -1,8 +1,10 @@ from __future__ import annotations from dandischema.conf import get_instance_config +import pytest +@pytest.mark.django_db def test_rest_info_instance_config_include_none(api_client): resp = api_client.get('/api/info/') assert resp.status_code == 200 diff --git a/dandiapi/api/tests/test_schema.py b/dandiapi/api/tests/test_schema.py index 370b835f3..29362a041 100644 --- a/dandiapi/api/tests/test_schema.py +++ b/dandiapi/api/tests/test_schema.py @@ -14,6 +14,7 @@ PublishedAsset, ], ) +@pytest.mark.django_db def test_schema_latest(api_client, model: CommonModel): """Test that the schema endpoints return valid schemas.""" resp = api_client.get('/api/schemas/', {'model': model.__name__}) @@ -30,6 +31,7 @@ def test_schema_latest(api_client, model: CommonModel): assert schema == expected_schema +@pytest.mark.django_db def test_schema_unsupported_model(api_client): """Test that the schema endpoint returns an error when passed invalid choice.""" resp = api_client.get('/api/schemas/', {'model': 'NotAValidModel'}) diff --git a/dandiapi/api/throttling.py b/dandiapi/api/throttling.py new file mode 100644 index 000000000..889fe8287 --- /dev/null +++ b/dandiapi/api/throttling.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from rest_framework.throttling import UserRateThrottle + + +# This is not currently used, but if we ever choose to rate limit logged-in users, +# this is how we can accomplish that, without applying it to admins. +class DandiUserRateThrottle(UserRateThrottle): + def get_cache_key(self, request, view): + # Don't rate limit admin users + if request.user and (request.user.is_staff or request.user.is_superuser): + return None + + return super().get_cache_key(request, view) diff --git a/dandiapi/settings/base.py b/dandiapi/settings/base.py index 5eae2f8fd..03ee6ca4a 100644 --- a/dandiapi/settings/base.py +++ b/dandiapi/settings/base.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging from pathlib import Path +import sys from typing import TYPE_CHECKING, cast from urllib.parse import urlunparse @@ -147,6 +148,24 @@ REST_FRAMEWORK['DEFAULT_PAGINATION_CLASS'] = 'dandiapi.api.views.pagination.DandiPagination' REST_FRAMEWORK['EXCEPTION_HANDLER'] = 'dandiapi.drf_utils.rewrap_django_core_exceptions' +# Throttling configuration +REST_FRAMEWORK['DEFAULT_THROTTLE_CLASSES'] = [ + 'rest_framework.throttling.AnonRateThrottle', +] +# By default, set request rate limit to a very high number, effectively disabling it. +# This is done to preserve the rate limiting behavior between dev and prod, +# without actually impeding developer experience. +REST_FRAMEWORK['DEFAULT_THROTTLE_RATES'] = { + 'anon': f'{sys.maxsize}/minute', +} + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', + 'LOCATION': 'dandi_cache_table', + } +} + REST_FRAMEWORK_EXTENSIONS = {'DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX': ''} # Clearing out the stock `SWAGGER_SETTINGS` variable causes a Django login diff --git a/dandiapi/settings/production.py b/dandiapi/settings/production.py index 523ec225e..a45e507a5 100644 --- a/dandiapi/settings/production.py +++ b/dandiapi/settings/production.py @@ -44,6 +44,12 @@ }, } +# In production, enable rate limiting for unauthenticated users +REST_FRAMEWORK['DEFAULT_THROTTLE_RATES'] = { + 'anon': '500/minute', +} + + DANDI_DEV_EMAIL: str = env.str('DJANGO_DANDI_DEV_EMAIL') DANDI_ADMIN_EMAIL: str = env.str('DJANGO_DANDI_ADMIN_EMAIL')