Repository: Project-MONAI/MONAI

Issue:
Add script API to get info of all the available bundles and details in the model-zoo
**Is your feature request related to a problem? Please describe.**
To help developers or users easily get the information of all the bundles in the model-zoo, it would be nice to add script APIs to get the manifest of bundles and details of the specific bundle.

Similar to the existing script APIs:
https://github.com/Project-MONAI/MONAI/blob/dev/monai/bundle/scripts.py#L154




Discussion:
According to our internal discussion, we planned to implement a new API in order to:
1) achieve all bundle names (from model zoo)
2) for a bundle, achieve its basic information, such as latest version and all versions, compressed zip file size.
3) for a bundle, get its inside information, such as references, model overview and license.

I will start to do 1) and 2) first, as for 3), I think it depends on the `README.md` and `license.txt` files inside a bundle. Therefore, to get the information, we may have the following ways:
1. grab from the model zoo repo (only the latest version).
2. grab from releases (need to download first, may take a lot of time).
3. for each bundle (all versions of all merged bundle), store its `README.md` and `license.txt files in some place.

Hi @Nic-Ma , @wyli , @zephyrie , could you please provide some advices of 3) ? Thanks!
Hi @yiheng-wang-nv ,

I think you can complete 1 & 2 first.
For 3, I think we don't need to support it so far?
And we should never access any file in the repo source code directly, source code is only used for development.
@wyli What do you think?

Thanks.



Resolving patch:
diff --git a/monai/bundle/__init__.py b/monai/bundle/__init__.py
--- a/monai/bundle/__init__.py
+++ b/monai/bundle/__init__.py
@@ -12,5 +12,16 @@
 from .config_item import ComponentLocator, ConfigComponent, ConfigExpression, ConfigItem, Instantiable
 from .config_parser import ConfigParser
 from .reference_resolver import ReferenceResolver
-from .scripts import ckpt_export, download, init_bundle, load, run, verify_metadata, verify_net_in_out
+from .scripts import (
+    ckpt_export,
+    download,
+    get_all_bundles_list,
+    get_bundle_info,
+    get_bundle_versions,
+    init_bundle,
+    load,
+    run,
+    verify_metadata,
+    verify_net_in_out,
+)
 from .utils import EXPR_KEY, ID_REF_KEY, ID_SEP_KEY, MACRO_KEY, load_bundle_config
diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py
--- a/monai/bundle/scripts.py
+++ b/monai/bundle/scripts.py
@@ -322,6 +322,115 @@ def load(
     return model
 
 
+def _get_all_bundles_info(repo: str = "Project-MONAI/model-zoo", tag: str = "hosting_storage_v1"):
+    if has_requests:
+        request_url = f"https://api.github.com/repos/{repo}/releases"
+        resp = requests_get(request_url)
+        resp.raise_for_status()
+    else:
+        raise ValueError("requests package is required, please install it.")
+    releases_list = json.loads(resp.text)
+    bundle_name_pattern = re.compile(r"_v\d*.")
+    bundles_info: Dict = {}
+
+    for release in releases_list:
+        if release["tag_name"] == tag:
+            for asset in release["assets"]:
+                asset_name = bundle_name_pattern.split(asset["name"])[0]
+                if asset_name not in bundles_info:
+                    bundles_info[asset_name] = {}
+                asset_version = asset["name"].split(f"{asset_name}_v")[-1].replace(".zip", "")
+                bundles_info[asset_name][asset_version] = {
+                    "id": asset["id"],
+                    "name": asset["name"],
+                    "size": asset["size"],
+                    "download_count": asset["download_count"],
+                    "browser_download_url": asset["browser_download_url"],
+                }
+            return bundles_info
+    return bundles_info
+
+
+def get_all_bundles_list(repo: str = "Project-MONAI/model-zoo", tag: str = "hosting_storage_v1"):
+    """
+    Get all bundles names (and the latest versions) that are stored in the release of specified repository
+    with the provided tag. The default values of arguments correspond to the release of MONAI model zoo.
+
+    Args:
+        repo: it should be in the form of "repo_owner/repo_name/".
+        tag: the tag name of the release.
+
+    Returns:
+        a list of tuple in the form of (bundle name, latest version).
+
+    """
+
+    bundles_info = _get_all_bundles_info(repo=repo, tag=tag)
+    bundles_list = []
+    for bundle_name in bundles_info.keys():
+        latest_version = sorted(bundles_info[bundle_name].keys())[-1]
+        bundles_list.append((bundle_name, latest_version))
+
+    return bundles_list
+
+
+def get_bundle_versions(bundle_name: str, repo: str = "Project-MONAI/model-zoo", tag: str = "hosting_storage_v1"):
+    """
+    Get the latest version, as well as all existing versions of a bundle that is stored in the release of specified
+    repository with the provided tag.
+
+    Args:
+        bundle_name: bundle name.
+        repo: it should be in the form of "repo_owner/repo_name/".
+        tag: the tag name of the release.
+
+    Returns:
+        a dictionary that contains the latest version and all versions of a bundle.
+
+    """
+
+    bundles_info = _get_all_bundles_info(repo=repo, tag=tag)
+    if bundle_name not in bundles_info:
+        raise ValueError(f"bundle: {bundle_name} is not existing.")
+    bundle_info = bundles_info[bundle_name]
+    all_versions = sorted(bundle_info.keys())
+
+    return {"latest_version": all_versions[-1], "all_versions": all_versions}
+
+
+def get_bundle_info(
+    bundle_name: str,
+    version: Optional[str] = None,
+    repo: str = "Project-MONAI/model-zoo",
+    tag: str = "hosting_storage_v1",
+):
+    """
+    Get all information (include "id", "name", "size", "download_count", "browser_download_url") of a bundle
+    with the specified bundle name and version.
+
+    Args:
+        bundle_name: bundle name.
+        version: version name of the target bundle, if None, the latest version will be used.
+        repo: it should be in the form of "repo_owner/repo_name/".
+        tag: the tag name of the release.
+
+    Returns:
+        a dictionary that contains the bundle's information.
+
+    """
+
+    bundles_info = _get_all_bundles_info(repo=repo, tag=tag)
+    if bundle_name not in bundles_info:
+        raise ValueError(f"bundle: {bundle_name} is not existing.")
+    bundle_info = bundles_info[bundle_name]
+    if version is None:
+        version = sorted(bundle_info.keys())[-1]
+    if version not in bundle_info:
+        raise ValueError(f"version: {version} of bundle: {bundle_name} is not existing.")
+
+    return bundle_info[version]
+
+
 def run(
     runner_id: Optional[Union[str, Sequence[str]]] = None,
     meta_file: Optional[Union[str, Sequence[str]]] = None,


Test patch:
diff --git a/tests/test_bundle_get_data.py b/tests/test_bundle_get_data.py
new file mode 100644
--- /dev/null
+++ b/tests/test_bundle_get_data.py
@@ -0,0 +1,54 @@
+# Copyright (c) MONAI Consortium
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+from parameterized import parameterized
+
+from monai.bundle import get_all_bundles_list, get_bundle_info, get_bundle_versions
+from tests.utils import skip_if_downloading_fails, skip_if_quick, skip_if_windows
+
+TEST_CASE_1 = [{"bundle_name": "brats_mri_segmentation"}]
+
+TEST_CASE_2 = [{"bundle_name": "spleen_ct_segmentation", "version": "0.1.0"}]
+
+
+@skip_if_windows
+class TestGetBundleData(unittest.TestCase):
+    @skip_if_quick
+    def test_get_all_bundles_list(self):
+        with skip_if_downloading_fails():
+            output = get_all_bundles_list()
+            self.assertTrue(isinstance(output, list))
+            self.assertTrue(isinstance(output[0], tuple))
+            self.assertTrue(len(output[0]) == 2)
+
+    @parameterized.expand([TEST_CASE_1])
+    @skip_if_quick
+    def test_get_bundle_versions(self, params):
+        with skip_if_downloading_fails():
+            output = get_bundle_versions(**params)
+            self.assertTrue(isinstance(output, dict))
+            self.assertTrue("latest_version" in output and "all_versions" in output)
+            self.assertTrue("0.1.0" in output["all_versions"])
+
+    @parameterized.expand([TEST_CASE_1, TEST_CASE_2])
+    @skip_if_quick
+    def test_get_bundle_info(self, params):
+        with skip_if_downloading_fails():
+            output = get_bundle_info(**params)
+            self.assertTrue(isinstance(output, dict))
+            for key in ["id", "name", "size", "download_count", "browser_download_url"]:
+                self.assertTrue(key in output)
+
+
+if __name__ == "__main__":
+    unittest.main()