From 30450c9582233dbdebbda2901cbd07e8183701a6 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Fri, 30 Jan 2026 05:36:03 -0500 Subject: [PATCH 1/4] Enhance API client error messages Improve error messages in dandiapi.py to provide clearer guidance: - Explain mutually exclusive api_url/dandi_instance parameters - Add hint to verify Dandiset ID and check permissions - Reference 'dandi ls' command for listing assets - Include version context in "No asset at path" errors These improvements help users quickly troubleshoot API access issues. Co-Authored-By: Claude Sonnet 4.5 --- dandi/dandiapi.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/dandi/dandiapi.py b/dandi/dandiapi.py index f133d32dd..74a7aed1e 100644 --- a/dandi/dandiapi.py +++ b/dandi/dandiapi.py @@ -435,7 +435,11 @@ def __init__( dandi_instance = get_instance(instance_name) api_url = dandi_instance.api elif dandi_instance is not None: - raise ValueError("api_url and dandi_instance are mutually exclusive") + raise ValueError( + "api_url and dandi_instance are mutually exclusive. " + "Use either 'api_url' to specify a custom API URL, " + "or 'dandi_instance' to use a registered DANDI instance, but not both." + ) else: dandi_instance = get_instance(api_url) super().__init__(api_url) @@ -562,7 +566,11 @@ def get_dandiset( self, self.get(f"/dandisets/{dandiset_id}/") ) except HTTP404Error: - raise NotFoundError(f"No such Dandiset: {dandiset_id!r}") + raise NotFoundError( + f"No such Dandiset: {dandiset_id!r}. " + "Verify the Dandiset ID is correct and that you have access. " + f"View available Dandisets at {self.dandi_instance.gui}." + ) if version_id is not None and version_id != d.version_id: if version_id == DRAFT: return d.for_version(d.draft_version) @@ -732,7 +740,11 @@ def get_asset(self, asset_id: str) -> BaseRemoteAsset: try: info = self.get(f"/assets/{asset_id}/info/") except HTTP404Error: - raise NotFoundError(f"No such asset: {asset_id!r}") + raise NotFoundError( + f"No such asset: {asset_id!r}. " + "Verify the asset ID is correct. " + "Use 'dandi ls' to list available assets." + ) metadata = info.pop("metadata", None) return BaseRemoteAsset.from_base_data(self, info, metadata) @@ -1306,7 +1318,11 @@ def get_asset_by_path(self, path: str) -> RemoteAsset: a for a in self.get_assets_with_path_prefix(path) if a.path == path ) except ValueError: - raise NotFoundError(f"No asset at path {path!r}") + raise NotFoundError( + f"No asset at path {path!r} in version {self.version_id}. " + "Verify the path is correct and the asset exists in this version. " + "Use 'dandi ls' to list available assets." + ) else: return asset From 236c36bd371370459d854feb2a0cab2d8461b70c Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:38:49 -0500 Subject: [PATCH 2/4] Apply suggestion from @CodyCBakerPhD --- dandi/dandiapi.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dandi/dandiapi.py b/dandi/dandiapi.py index 74a7aed1e..4f976b9be 100644 --- a/dandi/dandiapi.py +++ b/dandi/dandiapi.py @@ -569,7 +569,6 @@ def get_dandiset( raise NotFoundError( f"No such Dandiset: {dandiset_id!r}. " "Verify the Dandiset ID is correct and that you have access. " - f"View available Dandisets at {self.dandi_instance.gui}." ) if version_id is not None and version_id != d.version_id: if version_id == DRAFT: From 6ec1f72428ca034f89b85286e15bceb5e1750c82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 04:22:18 +0000 Subject: [PATCH 3/4] Initial plan From 51dbd8feb13e40ae93985d84072784d6b878a1b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 04:40:01 +0000 Subject: [PATCH 4/4] Update test expectations for enhanced error messages --- dandi/tests/test_dandiarchive.py | 42 ++++++++++++++++++++++++++------ dandi/tests/test_delete.py | 5 +++- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/dandi/tests/test_dandiarchive.py b/dandi/tests/test_dandiarchive.py index cc5d988a4..e17b3f099 100644 --- a/dandi/tests/test_dandiarchive.py +++ b/dandi/tests/test_dandiarchive.py @@ -588,11 +588,17 @@ def test_get_nonexistent_dandiset( parsed_url.get_dandiset(client) # No error with pytest.raises(NotFoundError) as excinfo: parsed_url.get_dandiset(client, lazy=False) - assert str(excinfo.value) == "No such Dandiset: '999999'" + assert str(excinfo.value) == ( + "No such Dandiset: '999999'. " + "Verify the Dandiset ID is correct and that you have access. " + ) assert list(parsed_url.get_assets(client)) == [] with pytest.raises(NotFoundError) as excinfo: next(parsed_url.get_assets(client, strict=True)) - assert str(excinfo.value) == "No such Dandiset: '999999'" + assert str(excinfo.value) == ( + "No such Dandiset: '999999'. " + "Verify the Dandiset ID is correct and that you have access. " + ) @pytest.mark.parametrize("version", ["draft", "0.999999.9999"]) @@ -608,7 +614,10 @@ def test_get_nonexistent_dandiset_asset_id( assert list(parsed_url.get_assets(client)) == [] with pytest.raises(NotFoundError) as excinfo: next(parsed_url.get_assets(client, strict=True)) - assert str(excinfo.value) == "No such Dandiset: '999999'" + assert str(excinfo.value) == ( + "No such Dandiset: '999999'. " + "Verify the Dandiset ID is correct and that you have access. " + ) def test_get_dandiset_nonexistent_asset_id(text_dandiset: SampleDandiset) -> None: @@ -635,7 +644,11 @@ def test_get_nonexistent_asset_id(local_dandi_api: DandiAPI) -> None: assert list(parsed_url.get_assets(client)) == [] with pytest.raises(NotFoundError) as excinfo: next(parsed_url.get_assets(client, strict=True)) - assert str(excinfo.value) == "No such asset: '00000000-0000-0000-0000-000000000000'" + assert str(excinfo.value) == ( + "No such asset: '00000000-0000-0000-0000-000000000000'. " + "Verify the asset ID is correct. " + "Use 'dandi ls' to list available assets." + ) @pytest.mark.parametrize("version_suffix", ["", "@draft", "@0.999999.9999"]) @@ -648,7 +661,10 @@ def test_get_nonexistent_dandiset_asset_path( assert list(parsed_url.get_assets(client)) == [] with pytest.raises(NotFoundError) as excinfo: next(parsed_url.get_assets(client, strict=True)) - assert str(excinfo.value) == "No such Dandiset: '999999'" + assert str(excinfo.value) == ( + "No such Dandiset: '999999'. " + "Verify the Dandiset ID is correct and that you have access. " + ) def test_get_nonexistent_asset_path(text_dandiset: SampleDandiset) -> None: @@ -661,7 +677,11 @@ def test_get_nonexistent_asset_path(text_dandiset: SampleDandiset) -> None: assert list(parsed_url.get_assets(client)) == [] with pytest.raises(NotFoundError) as excinfo: next(parsed_url.get_assets(client, strict=True)) - assert str(excinfo.value) == "No asset at path 'does/not/exist'" + assert str(excinfo.value) == ( + "No asset at path 'does/not/exist' in version draft. " + "Verify the path is correct and the asset exists in this version. " + "Use 'dandi ls' to list available assets." + ) @pytest.mark.parametrize("version_suffix", ["", "@draft", "@0.999999.9999"]) @@ -677,7 +697,10 @@ def test_get_nonexistent_dandiset_asset_folder( assert list(parsed_url.get_assets(client)) == [] with pytest.raises(NotFoundError) as excinfo: next(parsed_url.get_assets(client, strict=True)) - assert str(excinfo.value) == "No such Dandiset: '999999'" + assert str(excinfo.value) == ( + "No such Dandiset: '999999'. " + "Verify the Dandiset ID is correct and that you have access. " + ) def test_get_nonexistent_asset_folder(text_dandiset: SampleDandiset) -> None: @@ -706,7 +729,10 @@ def test_get_nonexistent_dandiset_asset_prefix( assert list(parsed_url.get_assets(client)) == [] with pytest.raises(NotFoundError) as excinfo: next(parsed_url.get_assets(client, strict=True)) - assert str(excinfo.value) == "No such Dandiset: '999999'" + assert str(excinfo.value) == ( + "No such Dandiset: '999999'. " + "Verify the Dandiset ID is correct and that you have access. " + ) def test_get_nonexistent_asset_prefix(text_dandiset: SampleDandiset) -> None: diff --git a/dandi/tests/test_delete.py b/dandi/tests/test_delete.py index 0bd0d2b9d..9a6ff8dd1 100644 --- a/dandi/tests/test_delete.py +++ b/dandi/tests/test_delete.py @@ -252,7 +252,10 @@ def test_delete_nonexistent_dandiset( devel_debug=True, force=True, ) - assert str(excinfo.value) == "No such Dandiset: '999999'" + assert str(excinfo.value) == ( + "No such Dandiset: '999999'. " + "Verify the Dandiset ID is correct and that you have access. " + ) delete_spy.assert_not_called()