You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2021/09/15 20:21:49 UTC

[airavata-django-portal] branch master updated (42c8bea -> 90d08ed)

This is an automated email from the ASF dual-hosted git repository.

machristie pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git.


    from 42c8bea  Merge branch 'staging'
     new 1924b7b  AIRAVATA-3497 JS utility code for loading an input or output file
     new e415a97  AIRAVATA-3497 Utility for downloading a data product
     new e1bf066  AIRAVATA-3497 tutorial: updating how to download output file with ExperimentUtils
     new 9763265  AIRAVATA-3497 Updated tutorial to use async/await syntax
     new 90d08ed  Merge branch 'airavata-3497'

The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../django_airavata_api/js/models/Experiment.js    |   8 +
 .../js/utils/ExperimentUtils.js                    |  81 ++++++-
 docs/tutorial/custom_ui_tutorial.md                | 257 ++++++++++-----------
 3 files changed, 207 insertions(+), 139 deletions(-)

[airavata-django-portal] 05/05: Merge branch 'airavata-3497'

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 90d08ed100357f64c123cec9cc15a1e54d303381
Merge: 42c8bea 9763265
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Wed Sep 15 16:21:28 2021 -0400

    Merge branch 'airavata-3497'

 .../django_airavata_api/js/models/Experiment.js    |   8 +
 .../js/utils/ExperimentUtils.js                    |  81 ++++++-
 docs/tutorial/custom_ui_tutorial.md                | 257 ++++++++++-----------
 3 files changed, 207 insertions(+), 139 deletions(-)

[airavata-django-portal] 01/05: AIRAVATA-3497 JS utility code for loading an input or output file

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 1924b7b596b8d6d273fadcc7829f2fee019d5339
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Fri Aug 27 17:10:01 2021 -0400

    AIRAVATA-3497 JS utility code for loading an input or output file
---
 .../js/utils/ExperimentUtils.js                    | 80 +++++++++++++++++++++-
 1 file changed, 78 insertions(+), 2 deletions(-)

diff --git a/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js b/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
index be91f0a..c8a7954 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
@@ -22,7 +22,9 @@ const createExperiment = async function ({
       applicationName
     );
   } else {
-    throw new Error("Either applicationInterfaceId or applicationId or applicationName is required");
+    throw new Error(
+      "Either applicationInterfaceId or applicationId or applicationName is required"
+    );
   }
   const applicationModuleId = applicationInterface.applicationModuleId;
   let computeResourceId = null;
@@ -170,8 +172,82 @@ const loadWorkspacePreferences = async function () {
   return await services.WorkspacePreferencesService.get();
 };
 
-export { createExperiment };
+const loadExperiment = async function (experimentId) {
+  return await services.ExperimentService.retrieve({ lookup: experimentId });
+};
+
+const readDataProduct = async function (
+  dataProductURI,
+  { bodyType = "text" } = {}
+) {
+  return await fetch(
+    `/sdk/download?data-product-uri=${encodeURIComponent(dataProductURI)}`,
+    {
+      credentials: "same-origin",
+    }
+  ).then((r) => {
+    if (r.status === 404) {
+      return null;
+    }
+    if (!r.ok) {
+      throw new Error(r.statusText);
+    }
+    return r[bodyType]();
+  });
+};
+
+const readExperimentDataObject = async function (
+  experimentId,
+  name,
+  dataType,
+  { bodyType = "text" } = {}
+) {
+  if (dataType !== "input" && dataType !== "output") {
+    throw new Error("dataType should be one of 'input' or 'output'");
+  }
+  const experiment = await loadExperiment(experimentId);
+  const dataObjectsField =
+    dataType === "input" ? "experimentInputs" : "experimentOutputs";
+  const dataObject = experiment[dataObjectsField].find(
+    (dataObj) => dataObj.name === name
+  );
+  if (dataObject.value && dataObject.type.isFileValueType) {
+    const downloads = dataObject.value
+      .split(",")
+      .map((dp) => readDataProduct(dp, { bodyType }));
+    if (downloads.length === 1) {
+      return await downloads[0];
+    } else {
+      return await Promise.all(downloads);
+    }
+  }
+  return null;
+};
+
+const readInputFile = async function (
+  experimentId,
+  inputName,
+  { bodyType = "text" } = {}
+) {
+  return await readExperimentDataObject(experimentId, inputName, "input", {
+    bodyType,
+  });
+};
+
+const readOutputFile = async function (
+  experimentId,
+  outputName,
+  { bodyType = "text" } = {}
+) {
+  return await readExperimentDataObject(experimentId, outputName, "output", {
+    bodyType,
+  });
+};
+
+export { createExperiment, readInputFile, readOutputFile };
 
 export default {
   createExperiment,
+  readInputFile,
+  readOutputFile,
 };

[airavata-django-portal] 04/05: AIRAVATA-3497 Updated tutorial to use async/await syntax

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 976326541e7342c3fcb4b8905f52e06dd9a4cf75
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Wed Sep 15 16:15:39 2021 -0400

    AIRAVATA-3497 Updated tutorial to use async/await syntax
---
 docs/tutorial/custom_ui_tutorial.md | 170 +++++++++++++++++++-----------------
 1 file changed, 90 insertions(+), 80 deletions(-)

diff --git a/docs/tutorial/custom_ui_tutorial.md b/docs/tutorial/custom_ui_tutorial.md
index 67dfa6a..deb9745 100644
--- a/docs/tutorial/custom_ui_tutorial.md
+++ b/docs/tutorial/custom_ui_tutorial.md
@@ -1317,29 +1317,29 @@ Now we'll use the `AiravataAPI` library to load the user's recent experiments.
    end of the _scripts_ block in `hello.html`:
 
 ```javascript
-// ...
+    // ...
     // STARTING HERE
     const appInterfaceId = "Echo_23d67491-1bef-47bd-a0f5-faf069e09773";
 
-    function loadExperiments() {
+    async function loadExperiments() {
 
-        return services.ExperimentSearchService
+        const data = await services.ExperimentSearchService
             .list({limit: 5,
                 [models.ExperimentSearchFields.USER_NAME.name]: session.Session.username,
                 [models.ExperimentSearchFields.APPLICATION_ID.name]: appInterfaceId,
-            })
-            .then(data => {
-                $('#experiment-list').empty();
-                data.results.forEach((exp, index) => {
-                    $('#experiment-list').append(
-                    `<tr>
-                        <td>${exp.name}</td>
-                        <td>${exp.executionId}</td>
-                        <td>${exp.creationTime}</td>
-                        <td>${exp.experimentStatus.name}</td>
-                        <td id="output_${index}"></td>
-                    </tr>`);
-                });
+            });
+
+        $('#experiment-list').empty();
+        data.results.forEach(async (expSummary, index) => {
+
+            $('#experiment-list').append(
+            `<tr>
+                <td>${expSummary.name}</td>
+                <td>${expSummary.executionId}</td>
+                <td>${expSummary.creationTime}</td>
+                <td>${expSummary.experimentStatus.name}</td>
+                <td id="output_${index}"></td>
+            </tr>`);
         });
     }
 
@@ -1367,26 +1367,31 @@ then examine it line by line to see what it is doing.
 // ...
 
 // STARTING HERE
-    $("#run-button").click((e) => {
-        const greeting = $("#greeting-select").val();
+    async function submitExperiment(greeting) {
         // Construct experiment object
-        utils.ExperimentUtils.createExperiment({
+        const experimentData = await utils.ExperimentUtils.createExperiment({
             applicationInterfaceId: appInterfaceId,
             computeResourceName: "example-vc.jetstream-cloud.org",
             experimentName: "Echo " + greeting,
             experimentInputs: {
                 "Input-to-Echo": greeting
             }
-        }).then(experiment=> {
-            // Save experiment
-            return services.ExperimentService.create({ data: experiment });
-        }).then(experiment => {
-            // Launch experiment
-            return services.ExperimentService.launch({
-                lookup: experiment.experimentId,
-            });
-        })
-    });
+        });
+        // Save experiment
+        const experiment = await services.ExperimentService.create({ data: experimentData });
+        // Launch experiment
+        await services.ExperimentService.launch({ lookup: experiment.experimentId });
+    }
+
+    async function runClickHandler() {
+        const greeting = $("#greeting-select").val();
+        await submitExperiment(greeting);
+        // Reload experiments to see the new one
+        loadExperiments();
+    }
+
+    $("#run-button").click(runClickHandler);
+
 // ENDING HERE
 
 </script>
@@ -1398,12 +1403,18 @@ then examine it line by line to see what it is doing.
    handler to the _Run_ button that gets the selected greeting value:
 
 ```javascript
-$("#run-button").click((e) => {
+async function runClickHandler() {
     const greeting = $("#greeting-select").val();
-});
+    await submitExperiment(greeting);
+    // Reload experiments to see the new one
+    loadExperiments();
+}
+
+$("#run-button").click(runClickHandler);
 ```
 
-3. Now the code constructs an experiment object using the utility function
+3. Next, the `submitExperiment` function is called. This code constructs an
+   experiment object using the utility function
    `utils.ExperimentUtils.createExperiment`. In Airavata, Experiments are
    created from Application Interface descriptions, so we'll first pass the
    `applicationInterfaceId`. We already have the `appInterfaceId` in the code
@@ -1429,7 +1440,7 @@ $("#run-button").click((e) => {
 
 ```javascript
 // Construct experiment object
-utils.ExperimentUtils.createExperiment({
+const experimentData = await utils.ExperimentUtils.createExperiment({
     applicationInterfaceId: appInterfaceId,
     computeResourceName: "example-vc.jetstream-cloud.org",
     experimentName: "Echo " + greeting,
@@ -1439,9 +1450,9 @@ utils.ExperimentUtils.createExperiment({
 });
 ```
 
-4. The `createExperiment` function does a few more things behind the scenes and
-   once we run it we can take a look at the REST API calls it makes. In summary
-   `createExperiment`:
+4. The `utils.ExperimentUtils.createExperiment` function does a few more things
+   behind the scenes and once we run it we can take a look at the REST API calls
+   it makes. In summary `utils.ExperimentUtils.createExperiment`:
 
     - loads the Application Interface
     - loads the compute resource ID
@@ -1460,16 +1471,13 @@ utils.ExperimentUtils.createExperiment({
    (`ExperimentService.create`) and then launch it (`ExperimentService.launch`).
 
 ```javascript
-        // ...
-        }).then(experiment=> {
-            // Save experiment
-            return services.ExperimentService.create({ data: experiment });
-        }).then(experiment => {
-            // Launch experiment
-            return services.ExperimentService.launch({
-                lookup: experiment.experimentId,
-            });
-        })
+// ...
+// Save experiment
+const experiment = await services.ExperimentService.create({
+    data: experimentData,
+});
+// Launch experiment
+await services.ExperimentService.launch({ lookup: experiment.experimentId });
 ```
 
 Now that we can launch the experiment we can go ahead and give it a try.
@@ -1491,10 +1499,10 @@ We'll read the STDOUT file and display that in our experiment listing table.
 
 1. What we need to do is get the identifier for the experiment's STDOUT file. In
    Airavata, this identifier is called the _Data Product ID_. The experiment
-   metadata includes a list of output files and the `value` of each one is that file's
-   Data Product ID. For each `exp` we can use the `ExperimentService` to load
-   this metadata for the experiment, find the STDOUT output object and get its
-   value, which is the _Data Product ID_.
+   metadata includes a list of output files and the `value` of each one is that
+   file's Data Product ID. For each `exp` we can use the `ExperimentService` to
+   load this metadata for the experiment, find the STDOUT output object and get
+   its value, which is the _Data Product ID_.
 
 ```javascript
 if (exp.experimentStatus === models.ExperimentState.COMPLETED) {
@@ -1532,42 +1540,44 @@ if (expSummary.experimentStatus === models.ExperimentState.COMPLETED) {
    `loadExperiments` function:
 
 ```javascript
-function loadExperiments() {
-    return services.ExperimentSearchService.list({
+async function loadExperiments() {
+    const data = await services.ExperimentSearchService.list({
         limit: 5,
         [models.ExperimentSearchFields.USER_NAME.name]:
             session.Session.username,
         [models.ExperimentSearchFields.APPLICATION_ID.name]: appInterfaceId,
-    }).then((data) => {
-        $("#experiment-list").empty();
-        data.results.forEach((exp, index) => {
-            $("#experiment-list").append(
-                `<tr>
-                            <td>${exp.name}</td>
-                            <td>${exp.executionId}</td>
-                            <td>${exp.creationTime}</td>
-                            <td>${exp.experimentStatus.name}</td>
-                            <td id="output_${index}"></td>
-                        </tr>`
-            );
+    });
 
-            // STARTING HERE
-            // If experiment has finished, download and display the stdout file contents
-            if (expSummary.experimentStatus === models.ExperimentState.COMPLETED) {
-                const experiment = await services.ExperimentService.retrieve({
-                    lookup: expSummary.experimentId
-                });
-                const stdoutInput = experiment.getExperimentOutput('Echo-STDOUT');
-                const dataProductURI = stdoutInput.value;
-                try {
-                    const stdout = await utils.ExperimentUtils.readDataProduct(dataProductURI);
-                    $(`#output_${index}`).text(stdout);
-                } catch (error) {
-                    $(`#output_${index}`).text("N/A");
-                }
+    $("#experiment-list").empty();
+    data.results.forEach(async (expSummary, index) => {
+        $("#experiment-list").append(
+            `<tr>
+                <td>${expSummary.name}</td>
+                <td>${expSummary.executionId}</td>
+                <td>${expSummary.creationTime}</td>
+                <td>${expSummary.experimentStatus.name}</td>
+                <td id="output_${index}"></td>
+            </tr>`
+        );
+
+        // STARTING HERE
+        // If experiment has finished, download and display the stdout file contents
+        if (expSummary.experimentStatus === models.ExperimentState.COMPLETED) {
+            const experiment = await services.ExperimentService.retrieve({
+                lookup: expSummary.experimentId,
+            });
+            const stdoutInput = experiment.getExperimentOutput("Echo-STDOUT");
+            const dataProductURI = stdoutInput.value;
+            try {
+                const stdout = await utils.ExperimentUtils.readDataProduct(
+                    dataProductURI
+                );
+                $(`#output_${index}`).text(stdout);
+            } catch (error) {
+                $(`#output_${index}`).text("N/A");
             }
-            // ENDING HERE
-        });
+        }
+        // ENDING HERE
     });
 }
 ```

[airavata-django-portal] 03/05: AIRAVATA-3497 tutorial: updating how to download output file with ExperimentUtils

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit e1bf0669403fbe3a9155827c8a8b7df6640cd328
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Wed Sep 8 16:39:01 2021 -0400

    AIRAVATA-3497 tutorial: updating how to download output file with ExperimentUtils
---
 docs/tutorial/custom_ui_tutorial.md | 123 ++++++++++++++----------------------
 1 file changed, 48 insertions(+), 75 deletions(-)

diff --git a/docs/tutorial/custom_ui_tutorial.md b/docs/tutorial/custom_ui_tutorial.md
index 9299432..67dfa6a 100644
--- a/docs/tutorial/custom_ui_tutorial.md
+++ b/docs/tutorial/custom_ui_tutorial.md
@@ -1428,15 +1428,15 @@ $("#run-button").click((e) => {
     the name of the experiment and the experiment's input values.
 
 ```javascript
-        // Construct experiment object
-        utils.ExperimentUtils.createExperiment({
-            applicationInterfaceId: appInterfaceId,
-            computeResourceName: "example-vc.jetstream-cloud.org",
-            experimentName: "Echo " + greeting,
-            experimentInputs: {
-                "Input-to-Echo": greeting
-            }
-        })
+// Construct experiment object
+utils.ExperimentUtils.createExperiment({
+    applicationInterfaceId: appInterfaceId,
+    computeResourceName: "example-vc.jetstream-cloud.org",
+    experimentName: "Echo " + greeting,
+    experimentInputs: {
+        "Input-to-Echo": greeting,
+    },
+});
 ```
 
 4. The `createExperiment` function does a few more things behind the scenes and
@@ -1490,54 +1490,40 @@ bonjour
 We'll read the STDOUT file and display that in our experiment listing table.
 
 1. What we need to do is get the identifier for the experiment's STDOUT file. In
-   Airavata, this identifier is called the _Data Product ID_. Once we have that
-   we can get the DataProduct object which has the files metadata, including a
-   `downloadURL`. For each `exp` we can use the `FullExperimentService` to get
-   these details like so:
+   Airavata, this identifier is called the _Data Product ID_. The experiment
+   metadata includes a list of output files and the `value` of each one is that file's
+   Data Product ID. For each `exp` we can use the `ExperimentService` to load
+   this metadata for the experiment, find the STDOUT output object and get its
+   value, which is the _Data Product ID_.
 
 ```javascript
 if (exp.experimentStatus === models.ExperimentState.COMPLETED) {
-    services.FullExperimentService.retrieve({ lookup: exp.experimentId }).then(
-        (fullDetails) => {
-            const stdoutDataProductId =
-                fullDetails.experiment.experimentOutputs.find(
-                    (o) => o.name === "Echo-STDOUT"
-                ).value;
-            const stdoutDataProduct = fullDetails.outputDataProducts.find(
-                (dp) => dp.productUri === stdoutDataProductId
-            );
-            if (stdoutDataProduct && stdoutDataProduct.downloadURL) {
-                return fetch(stdoutDataProduct.downloadURL, {
-                    credentials: "same-origin",
-                }).then((result) => result.text());
-            }
-        }
-    );
+    const experiment = await services.ExperimentService.retrieve({
+        lookup: expSummary.experimentId,
+    });
+    const stdoutInput = experiment.getExperimentOutput("Echo-STDOUT");
+    const dataProductURI = stdoutInput.value;
 }
 ```
 
-2. Then we'll simply display the value in the table.
+2. Then we'll simply download the file and display the value in the table. We'll
+   use `ExperimentUtils.readDataProduct()` to download the file.
 
 ```javascript
-if (exp.experimentStatus === models.ExperimentState.COMPLETED) {
-    services.FullExperimentService.retrieve({ lookup: exp.experimentId })
-        .then((fullDetails) => {
-            const stdoutDataProductId =
-                fullDetails.experiment.experimentOutputs.find(
-                    (o) => o.name === "Echo-STDOUT"
-                ).value;
-            const stdoutDataProduct = fullDetails.outputDataProducts.find(
-                (dp) => dp.productUri === stdoutDataProductId
-            );
-            if (stdoutDataProduct && stdoutDataProduct.downloadURL) {
-                return fetch(stdoutDataProduct.downloadURL, {
-                    credentials: "same-origin",
-                }).then((result) => result.text());
-            }
-        })
-        .then((text) => {
-            $(`#output_${index}`).text(text);
-        });
+if (expSummary.experimentStatus === models.ExperimentState.COMPLETED) {
+    const experiment = await services.ExperimentService.retrieve({
+        lookup: expSummary.experimentId,
+    });
+    const stdoutInput = experiment.getExperimentOutput("Echo-STDOUT");
+    const dataProductURI = stdoutInput.value;
+    try {
+        const stdout = await utils.ExperimentUtils.readDataProduct(
+            dataProductURI
+        );
+        $(`#output_${index}`).text(stdout);
+    } catch (error) {
+        $(`#output_${index}`).text("N/A");
+    }
 }
 ```
 
@@ -1566,32 +1552,19 @@ function loadExperiments() {
             );
 
             // STARTING HERE
-            // If experiment has finished, load full details, then parse the stdout file
-            if (exp.experimentStatus === models.ExperimentState.COMPLETED) {
-                services.FullExperimentService.retrieve({
-                    lookup: exp.experimentId,
-                })
-                    .then((fullDetails) => {
-                        const stdoutDataProductId =
-                            fullDetails.experiment.experimentOutputs.find(
-                                (o) => o.name === "Echo-STDOUT"
-                            ).value;
-                        const stdoutDataProduct =
-                            fullDetails.outputDataProducts.find(
-                                (dp) => dp.productUri === stdoutDataProductId
-                            );
-                        if (
-                            stdoutDataProduct &&
-                            stdoutDataProduct.downloadURL
-                        ) {
-                            return fetch(stdoutDataProduct.downloadURL, {
-                                credentials: "same-origin",
-                            }).then((result) => result.text());
-                        }
-                    })
-                    .then((text) => {
-                        $(`#output_${index}`).text(text);
-                    });
+            // If experiment has finished, download and display the stdout file contents
+            if (expSummary.experimentStatus === models.ExperimentState.COMPLETED) {
+                const experiment = await services.ExperimentService.retrieve({
+                    lookup: expSummary.experimentId
+                });
+                const stdoutInput = experiment.getExperimentOutput('Echo-STDOUT');
+                const dataProductURI = stdoutInput.value;
+                try {
+                    const stdout = await utils.ExperimentUtils.readDataProduct(dataProductURI);
+                    $(`#output_${index}`).text(stdout);
+                } catch (error) {
+                    $(`#output_${index}`).text("N/A");
+                }
             }
             // ENDING HERE
         });

[airavata-django-portal] 02/05: AIRAVATA-3497 Utility for downloading a data product

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit e415a978653ea2c0ac8021a839e77170506604d5
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Sep 2 17:36:02 2021 -0400

    AIRAVATA-3497 Utility for downloading a data product
---
 .../apps/api/static/django_airavata_api/js/models/Experiment.js   | 8 ++++++++
 .../api/static/django_airavata_api/js/utils/ExperimentUtils.js    | 3 ++-
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/Experiment.js b/django_airavata/apps/api/static/django_airavata_api/js/models/Experiment.js
index 4e79c16..7169687 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/Experiment.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/Experiment.js
@@ -167,6 +167,14 @@ export default class Experiment extends BaseModel {
     }
   }
 
+  getExperimentInput(inputName) {
+    return this.experimentInputs.find(inp => inp.name === inputName);
+  }
+
+  getExperimentOutput(outputName) {
+    return this.experimentOutputs.find(out => out.name === outputName);
+  }
+
   _collectInputValues() {
     const result = {};
     this.experimentInputs.forEach((inp) => {
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js b/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
index c8a7954..4e70df5 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/utils/ExperimentUtils.js
@@ -244,10 +244,11 @@ const readOutputFile = async function (
   });
 };
 
-export { createExperiment, readInputFile, readOutputFile };
+export { createExperiment, readInputFile, readOutputFile, readDataProduct };
 
 export default {
   createExperiment,
   readInputFile,
   readOutputFile,
+  readDataProduct,
 };