Repository: dandi/dandi-cli

Issue:
ls: provide ability to list individual asset from a download url
ATM:
```
$> dandi ls -f json_pp https://api.dandiarchive.org/api/dandisets/000003/versions/draft/assets/0a748f90-d497-4a9c-822e-9c63811db412/download/
2021-04-06 12:20:16,926 [    INFO] Logs saved in /home/yoh/.cache/dandi-cli/log/20210406162015Z-1963932.log
Error: Response for getting https://api.dandiarchive.org/api/dandisets/000003/versions/draft/assets/0a748f90-d497-4a9c-822e-9c63811db412/download/ to redirect returned 403. Please verify that it is a URL related to dandiarchive and supported by dandi client

```
although URL is valid and wget follows redirect nicely.  Might be that dandi-api does not handle HEAD request correctly while we are trying to get redirect url (so something to file and issue or submit a PR about to dandi-api).  But I do not think in this case we should even try to follow any redirect since all pertinent information is in the URL and we just need to parse it and request metadata record for the asset

Discussion:

Patch:
diff --git a/dandi/dandiarchive.py b/dandi/dandiarchive.py
index 3729a092..81c0f6b8 100644
--- a/dandi/dandiarchive.py
+++ b/dandi/dandiarchive.py
@@ -51,7 +51,7 @@ def navigate_url(url):
             asset_id["version"] = r["most_recent_version"]["version"]
         args = (asset_id["dandiset_id"], asset_id["version"])
         kwargs["include_metadata"] = True
-        if asset_id.get("location"):
+        if asset_id.get("location") or asset_id.get("asset_id"):
             with client.session():
                 dandiset = client.get_dandiset(*args)
                 if asset_type == "folder":
@@ -59,8 +59,11 @@ def navigate_url(url):
                         *args, path=asset_id["location"]
                     )
                 elif asset_type == "item":
-                    asset = client.get_asset_bypath(*args, asset_id["location"])
-                    assets = [asset] if asset is not None else []
+                    if "location" in asset_id:
+                        asset = client.get_asset_bypath(*args, asset_id["location"])
+                        assets = [asset] if asset is not None else []
+                    else:
+                        assets = [client.get_asset(*args, asset_id["asset_id"])]
                 else:
                     raise NotImplementedError(
                         f"Do not know how to handle asset type {asset_type} with location"
@@ -183,6 +186,16 @@ class _dandi_url_parser:
             {"server_type": "api"},
             "https://<server>[/api]/dandisets/<dandiset id>[/versions[/<version>]]",
         ),
+        (
+            re.compile(
+                rf"{server_grp}(?P<asset_type>dandiset)s/{dandiset_id_grp}"
+                r"/versions/(?P<version>[.0-9]{5,}|draft)"
+                r"/assets/(?P<asset_id>[^/]+)(/(download/?)?)?"
+            ),
+            {"server_type": "api"},
+            "https://<server>[/api]/dandisets/<dandiset id>/versions/<version>"
+            "/assets/<asset id>[/download]",
+        ),
         # But for drafts files navigator it is a bit different beast and there
         # could be no versions, only draft
         # https://deploy-preview-341--gui-dandiarchive-org.netlify.app/#/dandiset/000027/draft/files?_id=5f176583f63d62e1dbd06943&_modelType=folder
@@ -353,6 +366,7 @@ class _dandi_url_parser:
         dandiset_id = groups.get("dandiset_id")
         version = groups.get("version")
         location = groups.get("location")
+        asset_key = groups.get("asset_id")
         if location:
             location = urlunquote(location)
             # ATM carries leading '/' which IMHO is not needed/misguiding somewhat, so
@@ -371,6 +385,9 @@ class _dandi_url_parser:
             else:
                 asset_type = "item"
             asset_ids["location"] = location
+        elif asset_key:
+            asset_type = "item"
+            asset_ids["asset_id"] = asset_key
         # TODO: remove whenever API supports "draft" and this type of url
         if groups.get("id"):
             assert version == "draft"


Test patch:
diff --git a/dandi/tests/test_dandiarchive.py b/dandi/tests/test_dandiarchive.py
index 6c760e6d..f4df0c63 100644
--- a/dandi/tests/test_dandiarchive.py
+++ b/dandi/tests/test_dandiarchive.py
@@ -129,6 +129,17 @@ def test_parse_girder_url(url, asset_type, asset_id):
         #         "folder_id": "5f176583f63d62e1dbd06943",
         #     },
         # )
+        (
+            "https://api.dandiarchive.org/api/dandisets/000003/versions/draft"
+            "/assets/0a748f90-d497-4a9c-822e-9c63811db412/download/",
+            "dandi-api",
+            "item",
+            {
+                "dandiset_id": "000003",
+                "version": "draft",
+                "asset_id": "0a748f90-d497-4a9c-822e-9c63811db412",
+            },
+        ),
     ],
 )
 def test_parse_api_url(url, instance, asset_type, asset_id):