Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
51fd7f9
feat: have the `/info/` endpoint provide instance config info
candleindark Jun 4, 2025
70c1afb
test: update `test_rest_info()`
candleindark Jul 8, 2025
689faaa
tmp: temporarily set the dandischema dependency to the devendorize br…
candleindark Aug 21, 2025
d963767
test: provide env var to set schema config instance name
candleindark Oct 4, 2025
bfa9b08
feat: make dandischema instance config available through Django settings
candleindark Oct 4, 2025
80c99ba
feat: provide instance name through dandischema instance config
candleindark Oct 3, 2025
ca72a6d
feat: provide instance name through dandischema instance config
candleindark Oct 4, 2025
5acd73e
Merge branch 'master' into rf-jsonschema-runtime-instance-config
candleindark Oct 16, 2025
ead592c
feat: replace `"DANDI` with instance name in `DandisetList` vue compo…
candleindark Oct 8, 2025
4e6c527
ci: add DJANGO_DANDI_INSTANCE_NAME env var to frontend CI
candleindark Oct 8, 2025
4ba8be9
feat: replace hardcoded RRID with one set in schema instance config
candleindark Oct 8, 2025
e487003
ci: add instance identifier to backend CI env
candleindark Oct 8, 2025
4ccc663
feat: replace the use of hardcoded license
candleindark Oct 8, 2025
ee161a2
feat: use dandiarchive's version in producing `PublishActivity` metadata
candleindark Oct 9, 2025
72e56a5
feat: vendorize software name in `PublishActivity` metadata
candleindark Oct 9, 2025
9043b2e
test: update tests for the `PublishActivity` metadata changes
candleindark Oct 9, 2025
eb7d9d6
style: replace use of `dandiapi.__version__` with `importlib.metadata…
candleindark Oct 16, 2025
8007762
rf: remove outdated patching of JSON schema
candleindark Oct 16, 2025
421f756
Merge branch 'rf-jsonschema-runtime-instance-config' into vendor-conf…
candleindark Oct 16, 2025
064b5ad
style: replace use of `dandiapi.__version__` with `importlib.metadata…
candleindark Oct 16, 2025
fe5084e
rf: import `dandischema.conf.get_instance_config` as `get_schema_inst…
candleindark Oct 16, 2025
1c82a67
feat: call avoid using `dandiapi/settings/base.py` to store schema in…
candleindark Oct 17, 2025
1d46c87
revert: revert naming of API service in generated asset metadata
candleindark Oct 17, 2025
fb97bbd
Always use first sorted license value in create_dev_dandiset
jjnesbitt Oct 30, 2025
7fccf48
Don't rename `get_instance_config` to `get_schema_instance_config`
jjnesbitt Nov 3, 2025
ffabf98
Don't use global variables for instance config
jjnesbitt Nov 3, 2025
35b16fd
Reuse identical schema object for tests
jjnesbitt Nov 3, 2025
c8b3f10
Require DJANGO_DANDI_INSTANCE_IDENTIFIER env var
jjnesbitt Nov 3, 2025
386e705
Use dummy instance config values for local dev and CI
jjnesbitt Nov 5, 2025
f8c4dd2
Revert change of version value in PublishableMetadataMixin
jjnesbitt Nov 4, 2025
62a641d
Update tests regarding Pydantic and None values
jjnesbitt Nov 5, 2025
943fd62
Require DJANGO_DANDI_DOI_API_PREFIX env var
jjnesbitt Nov 5, 2025
173504d
Use schema vendorization for DOIs
jjnesbitt Nov 5, 2025
0a59286
Use schema vendorization for id/identifier
jjnesbitt Nov 5, 2025
7616dca
Revert unnecessary change to `$route.query`
jjnesbitt Nov 5, 2025
54b996c
Use function to supply default license value
jjnesbitt Nov 5, 2025
4ebd692
Merge branch 'master' into vendor-configurable
jjnesbitt Nov 6, 2025
aad2338
Fix linting errors
jjnesbitt Nov 6, 2025
78f180b
Fix malformed license field in create_dev_dandiset
jjnesbitt Nov 6, 2025
fa08c1e
Move `get_default_license` to conftest.py and rename
jjnesbitt Nov 6, 2025
ca9a214
Merge branch 'master' into vendor-configurable
jjnesbitt Nov 6, 2025
67ca98a
Merge branch 'master' into vendor-configurable
jjnesbitt Nov 17, 2025
155b8b9
Set dandischema dependency to 0.12.0
jjnesbitt Nov 21, 2025
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
2 changes: 2 additions & 0 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ jobs:
DJANGO_DANDI_WEB_APP_URL: http://localhost:8085
DJANGO_DANDI_API_URL: http://localhost:8000
DJANGO_DANDI_JUPYTERHUB_URL: https://hub.dandiarchive.org/
DJANGO_DANDI_INSTANCE_NAME: DANDI
DJANGO_DANDI_INSTANCE_IDENTIFIER: "RRID:SCR_017571"
steps:
- uses: actions/checkout@v5
with:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/frontend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ jobs:
DJANGO_DANDI_WEB_APP_URL: http://localhost:8085
DJANGO_DANDI_API_URL: http://localhost:8000
DJANGO_DANDI_JUPYTERHUB_URL: https://hub.dandiarchive.org/
DJANGO_DANDI_INSTANCE_NAME: DANDI
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@waxlamp Sorry for accidentally deleting your comment about this line you posted earlier.

Your question was regarding why other environment variables are not included to set dandischema.conf.Config.

There are five fields in dandischema.conf.Config to set. The DJANGO_DANDI_INSTANCE_NAME env var sets instance_name. There is already DJANGO_DANDI_WEB_APP_URL env var that sets instance_url. licenses defaults to the value for DANDI Archive. The remaining fields to be set are instance_identifier and doi_prefix, and they can be set by DJANGO_DANDI_INSTANCE_IDENTIFIER and DJANGO_DANDI_DOI_API_PREFIX env var respectively. However, the frond end code doesn't seem to require those values.

If you want to input set the remaining field, you may want to pick from those in https://github.com/dandi/dandi-infrastructure/pull/224/files#diff-1a87a9189dbcd06318fab560eeb95c27d4c7dd16ef4574ffdf368cb2b63d7e30.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though this is the frontend CI, it still runs the application, and we should be consistent in how the application is invoked.

I pushed a commit that makes the DJANGO_DANDI_INSTANCE_IDENTIFIER env var required in the Django app, and as such, also added that env var here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yarikoptic Is making DJANGO_DANDI_INSTANCE_IDENTIFIER required an issue if one of your goals is to support people running DANDI instances in isolation without support for publishing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO it should not be required, as we would just default to the adhoc one and instance should just rely on what schema tells it to be.
But overall I think it is fine as long as the infrastructure changes are up to date (or beyond) with the server for which they are intended for. So infrastructure could/should just introduce them before (ie. now) even adopting a new version of the dandi-archive which requires it. And for DOI we have other more DOI-specific settings to enable or disable it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no default for the instance identifier, the default you're referring to is for the instance name.

Regardless, the reason for requiring these values be specified is not for the sake of dandi-schema, it's for the sake of operating the archive. We rely on those values in various parts of the code base, and to make those values optional would introduce buggy and uncertain behavior in multiple spots.

They need not be set to "real world" values, they can be set to dummy values, as is done in the development setup. Any downstream DANDI instances can change these value to whatever they like, or make them optional by modifying the settings files (and accepting the consequences).


# Web client env vars
VITE_APP_DANDI_API_ROOT: http://localhost:8000/api/
Expand Down
6 changes: 5 additions & 1 deletion dandiapi/api/management/commands/create_dev_dandiset.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from uuid import uuid4

from dandischema.conf import get_instance_config as get_schema_instance_config
from django.conf import settings
from django.contrib.auth.models import User
from django.core.files.uploadedfile import SimpleUploadedFile
Expand Down Expand Up @@ -30,9 +31,12 @@
def create_dev_dandiset(*, name: str, email: str, num_extra_owners: int):
owner = User.objects.get(email=email)

# The licenses field is a set, sort values to ensure consistent behavior
licenses = sorted(x.value for x in get_schema_instance_config().licenses)

version_metadata = {
'description': 'An informative description',
'license': ['spdx:CC0-1.0'],
'license': licenses[0],
}
dandiset, draft_version = create_open_dandiset(
user=owner, version_name=name, version_metadata=version_metadata
Expand Down
14 changes: 11 additions & 3 deletions dandiapi/api/models/metadata.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
from __future__ import annotations

import importlib
from typing import TYPE_CHECKING
from uuid import uuid4

from dandischema.conf import get_instance_config as get_schema_instance_config

if TYPE_CHECKING:
import datetime

_SCHEMA_INSTANCE_CONFIG = get_schema_instance_config()


class PublishableMetadataMixin:
@classmethod
def published_by(cls, now: datetime.datetime):
instance_name = _SCHEMA_INSTANCE_CONFIG.instance_name
instance_identifier = _SCHEMA_INSTANCE_CONFIG.instance_identifier

return {
'id': uuid4().urn,
'name': 'DANDI publish',
Expand All @@ -20,10 +28,10 @@ def published_by(cls, now: datetime.datetime):
'wasAssociatedWith': [
{
'id': uuid4().urn,
'identifier': 'RRID:SCR_017571',
'name': 'DANDI API',
**({'identifier': instance_identifier} if instance_identifier else {}),
'name': f'{instance_name} API',
# TODO: version the API
'version': '0.1.0',
'version': importlib.metadata.version('dandiapi'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one though indeed would be "an improvement" so let's keep the library version here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this change does not relate to vendorization, let's revert this for now and file an issue to discuss it further before we decide whether to readopt the change.

'schemaKey': 'Software',
}
],
Expand Down
9 changes: 7 additions & 2 deletions dandiapi/api/models/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
from typing import TypedDict

from dandischema.conf import get_instance_config as get_schema_instance_config
from dandischema.models import AccessType
from django.conf import settings
from django.contrib.postgres.indexes import HashIndex
Expand All @@ -19,6 +20,8 @@

logger = logging.getLogger(__name__)

_SCHEMA_INSTANCE_CONFIG = get_schema_instance_config()


class VersionAssetValidationError(TypedDict):
field: str
Expand Down Expand Up @@ -237,9 +240,11 @@ def _populate_metadata(self):
),
'manifestLocation': manifest_location(self),
'name': self.name,
'identifier': f'DANDI:{self.dandiset.identifier}',
'identifier': (f'{_SCHEMA_INSTANCE_CONFIG.instance_name}:{self.dandiset.identifier}'),
'version': self.version,
'id': f'DANDI:{self.dandiset.identifier}/{self.version}',
'id': (
f'{_SCHEMA_INSTANCE_CONFIG.instance_name}:{self.dandiset.identifier}/{self.version}'
),
'repository': settings.DANDI_WEB_APP_URL,
'url': (
f'{settings.DANDI_WEB_APP_URL}/dandiset/{self.dandiset.identifier}/{self.version}'
Expand Down
4 changes: 3 additions & 1 deletion dandiapi/api/services/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import TYPE_CHECKING

from celery.utils.log import get_task_logger
from dandischema.conf import get_instance_config as get_schema_instance_config
import dandischema.exceptions
from dandischema.metadata import aggregate_assets_summary, validate
from django.conf import settings
Expand Down Expand Up @@ -116,7 +117,8 @@ def _build_validatable_version_metadata(version: Version) -> dict:
metadata_for_validation = publishable_version.metadata

metadata_for_validation['id'] = (
f'DANDI:{publishable_version.dandiset.identifier}/{publishable_version.version}'
f'{get_schema_instance_config().instance_name}:'
f'{publishable_version.dandiset.identifier}/{publishable_version.version}'
)
metadata_for_validation['url'] = (
f'{settings.DANDI_WEB_APP_URL}/dandiset/'
Expand Down
13 changes: 10 additions & 3 deletions dandiapi/api/tests/test_asset.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations

import importlib
import json
from uuid import uuid4

from dandischema.conf import get_instance_config as get_schema_instance_config
from dandischema.models import AccessType
from django.conf import settings
from django.db.utils import IntegrityError
Expand All @@ -25,6 +27,8 @@

from .fuzzy import HTTP_URL_RE, TIMESTAMP_RE, URN_RE, UTC_ISO_TIMESTAMP_RE, UUID_RE

_SCHEMA_INSTANCE_CONFIG = get_schema_instance_config()

# Model tests


Expand Down Expand Up @@ -112,6 +116,9 @@ def test_publish_asset(draft_asset: Asset):
published_asset = draft_asset
published_asset.refresh_from_db()

instance_name = _SCHEMA_INSTANCE_CONFIG.instance_name
instance_identifier = _SCHEMA_INSTANCE_CONFIG.instance_identifier

assert published_asset.blob == draft_blob
assert published_asset.full_metadata == {
**draft_metadata,
Expand All @@ -124,10 +131,10 @@ def test_publish_asset(draft_asset: Asset):
'wasAssociatedWith': [
{
'id': URN_RE,
'identifier': 'RRID:SCR_017571',
'name': 'DANDI API',
**({'identifier': instance_identifier} if instance_identifier else {}),
'name': f'{instance_name} API',
# TODO: version the API
'version': '0.1.0',
'version': importlib.metadata.version('dandiapi'),
'schemaKey': 'Software',
}
],
Expand Down
17 changes: 16 additions & 1 deletion dandiapi/api/tests/test_info.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from __future__ import annotations

from unittest.mock import ANY

from dandischema.conf import Config
from dandischema.conf import get_instance_config as get_schema_instance_config
from django.conf import settings

from dandiapi.api.views.info import get_schema_url
Expand All @@ -8,13 +12,18 @@


def test_rest_info(api_client):
instance_config = get_schema_instance_config()

resp = api_client.get('/api/info/')
assert resp.status_code == 200

# Get the expected schema URL
schema_url = get_schema_url()

assert resp.json() == {
resp_json = resp.json()

assert resp_json == {
'instance_config': ANY,
'schema_version': settings.DANDI_SCHEMA_VERSION,
'schema_url': schema_url,
# Matches setuptools_scm's versioning scheme "no-guess-dev"
Expand All @@ -27,3 +36,9 @@ def test_rest_info(api_client):
'jupyterhub': {'url': settings.DANDI_JUPYTERHUB_URL},
},
}

# Verify that the instance config can be reconstituted from the info published by the API
reconstituted_instance_config = Config.model_validate(resp_json['instance_config'])
assert reconstituted_instance_config == instance_config, (
"Instance config can't be reconstituted from API response that publishes it"
)
20 changes: 14 additions & 6 deletions dandiapi/api/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import datetime
import hashlib
import importlib
from pathlib import Path
from typing import TYPE_CHECKING

from dandischema.conf import get_instance_config as get_schema_instance_config
from django.conf import settings
from django.core.files.storage import default_storage
from django.forms.models import model_to_dict
Expand All @@ -27,6 +29,9 @@
from rest_framework.test import APIClient


_SCHEMA_INSTANCE_CONFIG = get_schema_instance_config()


@pytest.mark.django_db
def test_calculate_checksum_task(asset_blob_factory):
asset_blob = asset_blob_factory(blob__data=b'known-content', size=13, sha256=None)
Expand Down Expand Up @@ -371,6 +376,9 @@ def test_publish_task(

published_version = draft_version.dandiset.versions.latest('created')

instance_name = _SCHEMA_INSTANCE_CONFIG.instance_name
instance_identifier = _SCHEMA_INSTANCE_CONFIG.instance_identifier

assert published_version.metadata == {
**draft_version.metadata,
'publishedBy': {
Expand All @@ -381,10 +389,10 @@ def test_publish_task(
'wasAssociatedWith': [
{
'id': URN_RE,
'identifier': 'RRID:SCR_017571',
'name': 'DANDI API',
**({'identifier': instance_identifier} if instance_identifier else {}),
'name': f'{instance_name} API',
# TODO: version the API
'version': '0.1.0',
'version': importlib.metadata.version('dandiapi'),
'schemaKey': 'Software',
}
],
Expand Down Expand Up @@ -441,9 +449,9 @@ def test_publish_task(
'wasAssociatedWith': [
{
'id': URN_RE,
'identifier': 'RRID:SCR_017571',
'name': 'DANDI API',
'version': '0.1.0',
**({'identifier': instance_identifier} if instance_identifier else {}),
'name': f'{instance_name} API',
'version': importlib.metadata.version('dandiapi'),
'schemaKey': 'Software',
}
],
Expand Down
13 changes: 10 additions & 3 deletions dandiapi/api/tests/test_version.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from __future__ import annotations

import datetime
import importlib
from time import sleep
from typing import TYPE_CHECKING

from dandischema.conf import get_instance_config as get_schema_instance_config
from dandischema.models import AccessType
from django.conf import settings
from freezegun import freeze_time
Expand All @@ -27,6 +29,8 @@

from .fuzzy import HTTP_URL_RE, TIMESTAMP_RE, URN_RE, UTC_ISO_TIMESTAMP_RE, VERSION_ID_RE

_SCHEMA_INSTANCE_CONFIG = get_schema_instance_config()


@freeze_time()
@pytest.mark.django_db
Expand Down Expand Up @@ -291,6 +295,9 @@ def test_version_publish_version(draft_version, asset):
publish_version.doi = fake_doi
publish_version.save()

instance_name = _SCHEMA_INSTANCE_CONFIG.instance_name
instance_identifier = _SCHEMA_INSTANCE_CONFIG.instance_identifier

assert publish_version.dandiset == draft_version.dandiset
assert publish_version.metadata == {
**draft_version.metadata,
Expand All @@ -302,10 +309,10 @@ def test_version_publish_version(draft_version, asset):
'wasAssociatedWith': [
{
'id': URN_RE,
'identifier': 'RRID:SCR_017571',
'name': 'DANDI API',
**({'identifier': instance_identifier} if instance_identifier else {}),
'name': f'{instance_name} API',
# TODO: version the API
'version': '0.1.0',
'version': importlib.metadata.version('dandiapi'),
'schemaKey': 'Software',
}
],
Expand Down
3 changes: 2 additions & 1 deletion dandiapi/api/views/dandiset.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import TYPE_CHECKING

from allauth.socialaccount.models import SocialAccount
from dandischema.conf import get_instance_config as get_schema_instance_config
from django.contrib.auth.models import User
from django.contrib.postgres.lookups import Unaccent
from django.db import transaction
Expand Down Expand Up @@ -425,7 +426,7 @@ def create(self, request: Request):
identifier = None
if 'identifier' in serializer.validated_data['metadata']:
identifier = serializer.validated_data['metadata']['identifier']
identifier = identifier.removeprefix('DANDI:')
identifier = identifier.removeprefix(f'{get_schema_instance_config().instance_name}:')

try:
identifier = int(identifier)
Expand Down
13 changes: 13 additions & 0 deletions dandiapi/api/views/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import importlib.metadata
from urllib.parse import ParseResult, urlencode, urlparse, urlunparse

from dandischema.conf import get_instance_config as get_schema_instance_config
from django.conf import settings
from django.urls import reverse
from drf_yasg.utils import no_body, swagger_auto_schema
from rest_framework import serializers
from rest_framework.decorators import api_view
from rest_framework.response import Response

_INSTANCE_CONFIG = get_schema_instance_config()


def get_schema_url():
"""Get the URL for the schema based on current server deployment."""
Expand Down Expand Up @@ -50,6 +53,9 @@ def __init__(self, *args, **kwargs):
}
)

# Instance Configuration
instance_config = serializers.JSONField()

# Schema
schema_version = serializers.CharField()
schema_url = serializers.URLField()
Expand All @@ -71,6 +77,13 @@ def info_view(request):
api_url = f'{settings.DANDI_API_URL}/api'
serializer = ApiInfoSerializer(
data={
'instance_config': _INSTANCE_CONFIG.model_dump(
# Not excluding any `None` value fields in this object because the `None` values are
# needed to reconstitute a `dandischema.conf.Config` instance properly in any
# receiving client since a corresponding environment variable to a field is used to
# set the value of the field if the argument for a particular field is not provided.
mode='json'
),
'schema_version': settings.DANDI_SCHEMA_VERSION,
'schema_url': get_schema_url(),
'version': importlib.metadata.version('dandiapi'),
Expand Down
1 change: 0 additions & 1 deletion dandiapi/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@
DANDI_SCHEMA_VERSION: str = env.str(
'DJANGO_DANDI_SCHEMA_VERSION', default=_DEFAULT_DANDI_SCHEMA_VERSION
)

DANDI_ZARR_PREFIX_NAME: str = env.str('DJANGO_DANDI_ZARR_PREFIX_NAME', default='zarr')

# Required environment variables
Expand Down
1 change: 1 addition & 0 deletions dev/.env.docker-compose-native
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ DJANGO_MINIO_STORAGE_URL=http://minioAccessKey:minioSecretKey@localhost:9000/dan
DJANGO_DANDI_WEB_APP_URL=http://localhost:8085
DJANGO_DANDI_API_URL=http://localhost:8000
DJANGO_DANDI_JUPYTERHUB_URL=https://hub.dandiarchive.org/
DJANGO_DANDI_INSTANCE_NAME=DANDI
Loading
Loading