diff --git a/ibpc_py/README.md b/ibpc_py/README.md
new file mode 100644
index 00000000..1fa0ded8
--- /dev/null
+++ b/ibpc_py/README.md
@@ -0,0 +1,38 @@
+# Industrial Bin Picking Challenge (IBPC)
+
+This is the python entrypoint for the Industrial Bin Picking Challenge
+
+## Usage
+
+Get a dataset
+`bpc fetch lm`
+
+
+Run tests against a dataset
+`bpc test <Pose Estimator Image Name> <datasetname> `
+
+`bpc test ibpc:pose_estimator lm`
+
+
+## Prerequisites
+
+
+### Install the package:
+
+In a virtualenv
+`pip install ibpc`
+
+Temporary before rocker release of https://github.com/osrf/rocker/pull/317/ 
+`pip uninstall rocker && pip install git+http://github.com/osrf/rocker.git@console_to_file`
+
+
+### Nvidia Docker (optoinal)
+Make sure nvidia_docker is installed if you want cuda. 
+
+## Release instructions
+
+```
+rm -rf dist/*
+python3 -m build --sdist .
+twine upload dist/*
+```
\ No newline at end of file
diff --git a/ibpc_py/setup.py b/ibpc_py/setup.py
new file mode 100644
index 00000000..08654a56
--- /dev/null
+++ b/ibpc_py/setup.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+
+import os
+import setuptools
+
+with open("README.md", "r") as fh:
+    long_description = fh.read()
+
+
+setuptools.setup(
+    name="ibpc",
+    version="0.0.2",
+    packages=["ibpc"],
+    package_dir={"": "src"},
+    # package_data={'ibpc': ['templates/*.em']},
+    author="Tully Foote",
+    author_email="tullyfoote@intrinsic.ai",
+    description="An entrypoint for the Industrial Bin Picking Challenge",
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+    url="https://bpc.opencv.org/",
+    license="Apache 2.0",
+    install_requires=[
+        "empy",
+        "rocker>=0.2.13",
+    ],
+    install_package_data=True,
+    zip_safe=False,
+    entry_points={
+        "console_scripts": [
+            "bpc = ibpc.ibpc:main",
+        ],
+    },
+)
diff --git a/ibpc_py/src/ibpc/__init__.py b/ibpc_py/src/ibpc/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ibpc.py b/ibpc_py/src/ibpc/ibpc.py
similarity index 50%
rename from ibpc.py
rename to ibpc_py/src/ibpc/ibpc.py
index 4c0475c1..f3943dd0 100644
--- a/ibpc.py
+++ b/ibpc_py/src/ibpc/ibpc.py
@@ -8,26 +8,78 @@
 from rocker.core import RockerExtensionManager
 from rocker.core import OPERATIONS_NON_INTERACTIVE
 
+from io import BytesIO
+from urllib.request import urlopen
+import urllib.request
+from zipfile import ZipFile
+
+
+def get_bop_template(modelname):
+    return f"https://huggingface.co/datasets/bop-benchmark/datasets/resolve/main/{modelname}/{modelname}"
+
+def get_ipb_template(modelname):
+    return f"https://huggingface.co/datasets/bop-benchmark/{modelname}/resolve/main/{modelname}"
+
+bop_suffixes = [
+    "_base.zip",
+    "_models.zip",
+    "_test_all.zip",
+    "_train_pbr.zip",
+]
+
+ipb_suffixes = [s for s in bop_suffixes]
+ipb_suffixes.append('_val.zip')
+ipb_suffixes.append('_test_all.z01')
+
+available_datasets = {
+    "ipb": (get_ipb_template("ipb"), ipb_suffixes),
+    "lm": (get_bop_template("lm"), bop_suffixes),
+    }
+
+def fetch_dataset(dataset, output_path):
+    (url_base, suffixes) = available_datasets[dataset]
+    for suffix in suffixes:
+
+        url = url_base + suffix
+        print(f"Downloading from url: {url}")
+        with urlopen(url) as zipurlfile:
+            with ZipFile(BytesIO(zipurlfile.read())) as zfile:
+                zfile.extractall(output_path)
+
 
 def main():
 
-    parser = argparse.ArgumentParser(
+    main_parser = argparse.ArgumentParser(
         description="The entry point for the Bin Picking Challenge",
         formatter_class=argparse.ArgumentDefaultsHelpFormatter,
     )
-    parser.add_argument("test_image")
-    parser.add_argument("dataset_directory")
-    parser.add_argument(
+    main_parser.add_argument(
         "-v", "--version", action="version", version="%(prog)s " + get_rocker_version()
     )
-    parser.add_argument("--debug-inside", action="store_true")
+
+    sub_parsers = main_parser.add_subparsers(title="test", dest="subparser_name")
+    test_parser = sub_parsers.add_parser("test")
+
+    test_parser.add_argument("estimator_image")
+    test_parser.add_argument("dataset")
+    test_parser.add_argument("--dataset_directory", action="store", default=".")
+    test_parser.add_argument("--debug-inside", action="store_true")
+
+    fetch_parser = sub_parsers.add_parser("fetch")
+    fetch_parser.add_argument("dataset", choices=available_datasets.keys())
+    fetch_parser.add_argument("--dataset-path", default=".")
 
     extension_manager = RockerExtensionManager()
     default_args = {"cuda": True, "network": "host"}
-    extension_manager.extend_cli_parser(parser, default_args)
+    # extension_manager.extend_cli_parser(test_parser, default_args)
 
-    args = parser.parse_args()
+    args = main_parser.parse_args()
     args_dict = vars(args)
+    if args.subparser_name == "fetch":
+        print(f"Fetching dataset {args_dict['dataset']} to {args_dict['dataset_path']}")
+        fetch_dataset(args_dict["dataset"], args_dict["dataset_path"])
+        print("Fetch complete")
+        return
 
     # Confirm dataset directory is absolute
     args_dict["dataset_directory"] = os.path.abspath(args_dict["dataset_directory"])
@@ -39,10 +91,13 @@ def main():
         "network": "host",
         "extension_blacklist": {},
         "operating_mode": OPERATIONS_NON_INTERACTIVE,
-        "env": [[f"BOP_PATH:/opt/ros/underlay/install/datasets"]],
+        "env": [
+            [f"BOP_PATH:/opt/ros/underlay/install/datasets/{args_dict['dataset']}"],
+            [f"DATASET_NAME:{args_dict['dataset']}"],
+        ],
         "console_output_file": "ibpc_test_output.log",
         "volume": [
-            [f"{args_dict['dataset_directory']}:/opt/ros/underlay/install/datasets/lm"]
+            [f"{args_dict['dataset_directory']}:/opt/ros/underlay/install/datasets"]
         ],
     }
     print("Buiding tester env")
@@ -59,11 +114,13 @@ def main():
         "extension_blacklist": {},
         "console_output_file": "ibpc_zenoh_output.log",
         "operating_mode": OPERATIONS_NON_INTERACTIVE,
+        "volume": [],
     }
+    zenoh_extensions = extension_manager.get_active_extensions(tester_args)
 
     print("Buiding zenoh env")
     dig_zenoh = DockerImageGenerator(
-        tester_extensions, tester_args, "eclipse/zenoh:1.1.1"
+        zenoh_extensions, zenoh_args, "eclipse/zenoh:1.1.1"
     )
     exit_code = dig_zenoh.build(**zenoh_args)
     if exit_code != 0:
@@ -76,22 +133,14 @@ def run_instance(dig_instance, args):
     tester_thread = threading.Thread(target=run_instance, args=(dig_zenoh, zenoh_args))
     tester_thread.start()
 
-    # TODO Redirect stdout
-    import time
-
-    time.sleep(3)
-
     tester_thread = threading.Thread(
         target=run_instance, args=(dig_tester, tester_args)
     )
     tester_thread.start()
-    # TODO Redirect stdout
 
-    import time
-
-    time.sleep(3)
-
-    dig = DockerImageGenerator(active_extensions, args_dict, args_dict["test_image"])
+    dig = DockerImageGenerator(
+        active_extensions, args_dict, args_dict["estimator_image"]
+    )
 
     exit_code = dig.build(**vars(args))
     if exit_code != 0:
@@ -104,7 +153,3 @@ def run_instance(dig_instance, args):
     result = dig.run(**args_dict)
     # TODO clean up threads here
     return result
-
-
-if __name__ == "__main__":
-    main()