diff --git a/dandi/dandiapi.py b/dandi/dandiapi.py index 61566425b..42e311c95 100644 --- a/dandi/dandiapi.py +++ b/dandi/dandiapi.py @@ -70,6 +70,20 @@ lgr = get_logger() +def set_asset_schema_key(metadata: dict[str, Any]) -> None: + """ + Set ``schemaKey`` to ``"Asset"`` on metadata bound for the server as asset metadata. + + However the metadata was gathered (e.g. as a ``BareAsset``), if it is to be uploaded + to the server to populate, in part, an ``Asset`` instance, set the ``schemaKey`` of + the metadata to ``"Asset"`` here. The server expects the uploaded metadata to carry + ``"Asset"`` as its ``schemaKey``. + + The metadata is modified in place. + """ + metadata["schemaKey"] = "Asset" + + class AssetType(Enum): """ .. versionadded:: 0.36.0 @@ -1993,6 +2007,7 @@ def set_raw_metadata(self, metadata: dict[str, Any]) -> None: Set the metadata for the asset on the server to the given value and update the `RemoteBlobAsset` in place. """ + set_asset_schema_key(metadata) data = self.client.put( self.api_path, json={"metadata": metadata, "blob_id": self.blob} ) @@ -2016,6 +2031,7 @@ def set_raw_metadata(self, metadata: dict[str, Any]) -> None: Set the metadata for the asset on the server to the given value and update the `RemoteZarrAsset` in place. """ + set_asset_schema_key(metadata) data = self.client.put( self.api_path, json={"metadata": metadata, "zarr_id": self.zarr} ) diff --git a/dandi/files/bases.py b/dandi/files/bases.py index 381ee51cf..3577e605f 100644 --- a/dandi/files/bases.py +++ b/dandi/files/bases.py @@ -25,7 +25,12 @@ import requests import dandi -from dandi.dandiapi import RemoteAsset, RemoteDandiset, RESTFullAPIClient +from dandi.dandiapi import ( + RemoteAsset, + RemoteDandiset, + RESTFullAPIClient, + set_asset_schema_key, +) from dandi.metadata.core import get_default_metadata from dandi.misctypes import DUMMY_DANDI_ETAG, Digest, LocalReadableFile, P from dandi.utils import post_upload_size_check, pre_upload_size_check, yaml_load @@ -363,6 +368,7 @@ def iter_upload( from dandi.support.digests import get_dandietag asset_path = metadata.setdefault("path", self.path) + set_asset_schema_key(metadata) client = dandiset.client yield {"status": "calculating etag"} etagger = get_dandietag(self.filepath) diff --git a/dandi/files/zarr.py b/dandi/files/zarr.py index 14bac02c5..8f5a4aa0e 100644 --- a/dandi/files/zarr.py +++ b/dandi/files/zarr.py @@ -38,6 +38,7 @@ RemoteZarrAsset, RemoteZarrEntry, RESTFullAPIClient, + set_asset_schema_key, ) from dandi.exceptions import UploadError from dandi.metadata.core import get_default_metadata @@ -580,6 +581,7 @@ def iter_upload( `RemoteAsset`. """ asset_path = metadata.setdefault("path", self.path) + set_asset_schema_key(metadata) client = dandiset.client lgr.debug("%s: Producing asset", asset_path) yield {"status": "producing asset"} diff --git a/dandi/tests/data/metadata/metadata2asset.json b/dandi/tests/data/metadata/metadata2asset.json index 60a3a9005..267b72c14 100644 --- a/dandi/tests/data/metadata/metadata2asset.json +++ b/dandi/tests/data/metadata/metadata2asset.json @@ -1,6 +1,6 @@ { "id": "dandiasset:0b0a1a0b-e3ea-4cf6-be94-e02c830d54be", - "schemaKey": "Asset", + "schemaKey": "ANYTHING GOOD NOW -- `schemaKey` of a `BareAsset` instance will be adjusted to the default value of the `schemaKey` field in the Pydantic `BareAsset` model", "schemaVersion": "0.4.1", "keywords": [ "test", diff --git a/dandi/tests/data/metadata/metadata2asset_3.json b/dandi/tests/data/metadata/metadata2asset_3.json index 44b809a69..abc20e3b7 100644 --- a/dandi/tests/data/metadata/metadata2asset_3.json +++ b/dandi/tests/data/metadata/metadata2asset_3.json @@ -1,6 +1,6 @@ { "id": "dandiasset:0b0a1a0b-e3ea-4cf6-be94-e02c830d54be", - "schemaKey": "Asset", + "schemaKey": "ANYTHING GOOD NOW -- `schemaKey` of a `BareAsset` instance will be adjusted to the default value of the `schemaKey` field in the Pydantic `BareAsset` model", "schemaVersion": "0.4.1", "keywords": [ "test", diff --git a/dandi/tests/data/metadata/metadata2asset_cellline.json b/dandi/tests/data/metadata/metadata2asset_cellline.json index 286eb0b1f..b777d27c3 100644 --- a/dandi/tests/data/metadata/metadata2asset_cellline.json +++ b/dandi/tests/data/metadata/metadata2asset_cellline.json @@ -1,6 +1,6 @@ { "id": "dandiasset:0b0a1a0b-e3ea-4cf6-be94-e02c830d54be", - "schemaKey": "Asset", + "schemaKey": "ANYTHING GOOD NOW -- `schemaKey` of a `BareAsset` instance will be adjusted to the default value of the `schemaKey` field in the Pydantic `BareAsset` model", "schemaVersion": "0.4.1", "keywords": [ "test", diff --git a/dandi/tests/data/metadata/metadata2asset_simple1.json b/dandi/tests/data/metadata/metadata2asset_simple1.json index 931779db5..a28b4eb4b 100644 --- a/dandi/tests/data/metadata/metadata2asset_simple1.json +++ b/dandi/tests/data/metadata/metadata2asset_simple1.json @@ -1,6 +1,6 @@ { "id": "dandiasset:bfc23fb6192b41c083a7257e09a3702b", - "schemaKey": "Asset", + "schemaKey": "ANYTHING GOOD NOW -- `schemaKey` of a `BareAsset` instance will be adjusted to the default value of the `schemaKey` field in the Pydantic `BareAsset` model", "schemaVersion": "0.4.1", "keywords": [ "keyword1", diff --git a/dandi/tests/test_metadata.py b/dandi/tests/test_metadata.py index 00520d08f..c168af852 100644 --- a/dandi/tests/test_metadata.py +++ b/dandi/tests/test_metadata.py @@ -418,11 +418,22 @@ def test_prepare_metadata(filename: str, metadata: dict[str, Any]) -> None: with (METADATA_DIR / filename).open() as fp: data_as_dict = json.load(fp) data_as_dict["schemaVersion"] = DANDI_SCHEMA_VERSION + # `prepare_metadata()` returns a `BareAsset`, so the top-level `schemaKey` + # of `data` is the default of the `schemaKey` field of `BareAsset` of the + # installed dandischema: "Asset" historically, but "BareAsset" once + # https://github.com/dandi/dandi-schema/pull/419 lands, which pins + # `BareAsset.schemaKey` to its own class name. The JSON files store a + # placeholder for this field, so set it to the current default here to keep + # the comparison independent of the installed dandischema version. + data_as_dict["schemaKey"] = BareAsset.model_fields["schemaKey"].default assert data == data_as_dict + + # `data_as_dict` is a BareAsset; the following lines turn it into an Asset + # instance so that `validate()` below checks it against the Asset class: set + # its `schemaKey`, and add the Asset-only fields `identifier` and (as of + # schema-0.5.0, https://github.com/dandi/dandischema/pull/52) `contentUrl`. + data_as_dict["schemaKey"] = "Asset" data_as_dict["identifier"] = "0b0a1a0b-e3ea-4cf6-be94-e02c830d54be" - # as of schema-0.5.0 (https://github.com/dandi/dandischema/pull/52) - # contentUrl is required, and validate below would map into Asset, - # due to schemaKey if Version(DANDI_SCHEMA_VERSION) >= Version("0.5.0"): data_as_dict["contentUrl"] = ["http://example.com"] validate(data_as_dict) @@ -1135,7 +1146,6 @@ def test_nwb2asset(simple2_nwb: Path) -> None: # Classes with ANY_AWARE_DATETIME fields need to be constructed with # model_construct() assert nwb2asset(simple2_nwb, digest=DUMMY_DANDI_ETAG) == BareAsset.model_construct( - schemaKey="Asset", schemaVersion=DANDI_SCHEMA_VERSION, keywords=["keyword1", "keyword 2"], access=[ @@ -1218,7 +1228,6 @@ def test_nwb2asset_remote_asset(nwb_dandiset: SampleDandiset) -> None: # Classes with ANY_AWARE_DATETIME fields need to be constructed with # model_construct() assert nwb2asset(r, digest=digest) == BareAsset.model_construct( - schemaKey="Asset", schemaVersion=DANDI_SCHEMA_VERSION, keywords=["keyword1", "keyword 2"], access=[ diff --git a/dandi/tests/test_upload.py b/dandi/tests/test_upload.py index 577d99d11..cda476474 100644 --- a/dandi/tests/test_upload.py +++ b/dandi/tests/test_upload.py @@ -50,6 +50,10 @@ def test_upload_download( (dspath / parent).mkdir() copyfile(nwb_file, dspath / parent / name) new_dandiset.upload() + # The uploaded asset's metadata is derived from a ``BareAsset``, but it must + # be uploaded and stored with a ``schemaKey`` of `"Asset"` + (asset,) = d.get_assets() + assert asset.get_raw_metadata()["schemaKey"] == "Asset" download(d.version_api_url, tmp_path) assert list_paths(tmp_path) == [ tmp_path / d.identifier / dandiset_metadata_file,