From 905404182df528a3dcce0ba1474eb6c5c568b81a Mon Sep 17 00:00:00 2001 From: Joris Snellenburg Date: Sat, 8 Jun 2024 00:36:53 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=B7Add=20CI=20infrastructure=20files?= =?UTF-8?q?=20to=20run=20notebooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- action.yml | 20 +- .../.github/requirements_ci.txt | 7 + .../.github/workflows/integration_test.yml | 199 ++++++++++++++++++ requirements.txt | 1 + scripts/run_examples_notebooks.py | 145 +++++++++++++ 5 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 pyglotaran_examples/.github/requirements_ci.txt create mode 100644 pyglotaran_examples/.github/workflows/integration_test.yml create mode 100644 scripts/run_examples_notebooks.py diff --git a/action.yml b/action.yml index f77d5cd..2a3c697 100644 --- a/action.yml +++ b/action.yml @@ -25,6 +25,9 @@ outputs: example-list: description: "List of all possible example names to be used in a workflow matrix." value: ${{ steps.example-run.outputs.example-list }} + notebook-path: + description: "Path to evaluated notebook after running the example" + value: ${{ steps.example-notebook-run.outputs.notebook-path }} runs: using: "composite" @@ -62,7 +65,7 @@ runs: echo "::endgroup::" shell: bash - - name: Run example + - name: Run example scripts id: example-run run: | echo "::group:: Running ${{ inputs.example_name }}" @@ -78,6 +81,21 @@ runs: echo "::endgroup::" shell: bash + - name: Run example notebooks + id: example-notebook-run + run: | + echo "::group:: Running ${{ inputs.example_name }}" + if [ '${{ inputs.set_example_list }}' = 'false' ] + then + python pyglotaran-examples/scripts/run_examples_notebooks.py ${{ inputs.example_name }} 2>&1 + else + pip install yaargh papermill + python pyglotaran-examples/scripts/run_examples_notebooks.py set-gha-example-list-output + fi + + echo "::endgroup::" + shell: bash + - name: Save Examples commit sha if: inputs.set_example_list == 'false' run: | diff --git a/pyglotaran_examples/.github/requirements_ci.txt b/pyglotaran_examples/.github/requirements_ci.txt new file mode 100644 index 0000000..3f97edf --- /dev/null +++ b/pyglotaran_examples/.github/requirements_ci.txt @@ -0,0 +1,7 @@ + +pyglotaran>=0.3.0 +jupyterlab>=3.0.0 +matplotlib>=3.3.0 + +yaargh>=0.28.0 +papermill>=2.3.4 diff --git a/pyglotaran_examples/.github/workflows/integration_test.yml b/pyglotaran_examples/.github/workflows/integration_test.yml new file mode 100644 index 0000000..9bc8edf --- /dev/null +++ b/pyglotaran_examples/.github/workflows/integration_test.yml @@ -0,0 +1,199 @@ +name: "Run Examples" + +on: + push: + tags: + - v** + pull_request: + workflow_dispatch: + inputs: + pyglotaran_branch: + description: "pyglotaran branch/tag to run the examples against" + required: true + default: "main" + pyglotaran_examples_branch: + description: "pyglotaran-examples branch/tag to use" + required: true + default: "main" + +jobs: + create-example-list: + name: Create Example List + runs-on: ubuntu-latest + outputs: + example-list: ${{ steps.create-example-list.outputs.example-list }} + steps: + - name: Cloning pyglotaran-examples + if: ${{ github.event.inputs.pyglotaran_examples_branch }} == "" + uses: actions/checkout@v4 + with: + path: pyglotaran-examples + - name: Set example list output + id: create-example-list + uses: ./pyglotaran-examples + with: + example_name: set example list + set_example_list: true + + run-examples: + name: "Run Example: " + runs-on: ubuntu-latest + needs: [create-example-list] + strategy: + matrix: + example_name: ${{fromJson(needs.create-example-list.outputs.example-list)}} + fail-fast: false + steps: + - uses: actions/checkout@v4 + with: + repository: "glotaran/pyglotaran" + # If not provided (push and pull_request event) it uses the main branch + ref: ${{ github.event.inputs.pyglotaran_branch != '' && github.event.inputs.pyglotaran_branch || 'main'}} + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install pyglotaran + run: | + pip install wheel + pip install . + - name: Cloning pyglotaran-examples + if: ${{ github.event.inputs.pyglotaran_examples_branch }} == "" + uses: actions/checkout@v4 + with: + path: pyglotaran-examples + - id: example-run + uses: ./pyglotaran-examples + with: + example_name: ${{ matrix.example_name }} + examples_branch: ${{ github.event.inputs.pyglotaran_examples_branch }} + + - name: Upload Example Plots Artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: example-notebooks-${{ matrix.example_name }} + path: ${{ steps.example-run.outputs.notebook-path }} + + - name: Upload Example Results + uses: actions/upload-artifact@v4 + with: + name: example-results-${{ matrix.example_name }} + path: ~/pyglotaran_examples_results + + collect-artifacts: + if: always() + name: "Collect artifacts and reupload as bundel" + runs-on: ubuntu-latest + needs: [run-examples] + steps: + - name: Download Notebooks Artifacts + uses: actions/download-artifact@v4 + with: + path: example-notebooks + pattern: example-notebooks-* + merge-multiple: true + + - name: Upload Example Notebooks Artifact + uses: actions/upload-artifact@v4 + with: + name: example-notebooks + path: example-notebooks + overwrite: true + + - name: Delete Intermediate Notebooks artifacts + uses: GeekyEggo/delete-artifact@v5 + with: + name: example-notebooks-* + + - name: Download Result Artifacts + uses: actions/download-artifact@v4 + with: + path: example-results + pattern: example-results-* + merge-multiple: true + + - name: Upload Example Result Artifact + uses: actions/upload-artifact@v4 + with: + name: example-results + path: example-results + overwrite: true + + - name: Delete Intermediate Result artifacts + uses: GeekyEggo/delete-artifact@v5 + with: + name: example-results-* + + compare-results: + name: Compare Results + runs-on: ubuntu-latest + needs: [collect-artifacts] + steps: + - name: Checkout compare results + uses: actions/checkout@v4 + with: + repository: "glotaran/pyglotaran-examples" + ref: comparison-results + path: comparison-results + + - name: Download result artifact + uses: actions/download-artifact@v4 + with: + name: example-results + path: comparison-results-current + + - name: Show used versions for result creation + run: | + echo "::group:: ✔️ Compare-Results" + echo "✔️ pyglotaran-examples commit: $(< comparison-results/example_commit_sha.txt)" + echo "✔️ pyglotaran commit: $(< comparison-results/pyglotaran_commit_sha.txt)" + echo "::endgroup::" + echo "::group:: ♻️ Current-Results" + echo "♻️ pyglotaran-examples commit: $(< comparison-results-current/example_commit_sha.txt)" + echo "::endgroup::" + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Run result validator + uses: glotaran/pyglotaran-validation@main + with: + validation_name: pyglotaran-examples + + create-release: + name: "🚀 Create release assets and tag comparison-results branch" + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + runs-on: ubuntu-latest + needs: [compare-results] + steps: + - name: ⏬ Checkout compare results + uses: actions/checkout@v4 + with: + repository: "glotaran/pyglotaran-examples" + ref: comparison-results + + - name: Get tag name + id: tag + uses: devops-actions/action-get-tag@v1.0.2 + + - name: 📦 Create release asset + run: zip -r comparison-results-${{steps.tag.outputs.tag}}.zip . -x ".git/*" + + - name: 🚀⬆️ Upload Release Asset + uses: softprops/action-gh-release@v1 + with: + files: comparison-results-${{steps.tag.outputs.tag}}.zip + generate_release_notes: true + append_body: true + + - name: 📦 Create comparison-results tag + run: | + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config user.name 'github-actions[bot]' + NEW_VERSION="comparison-results-${{steps.tag.outputs.tag}}" + git tag -a $NEW_VERSION -m "Comparison results used with release ${{steps.tag.outputs.tag}}" + git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} + git push origin $NEW_VERSION diff --git a/requirements.txt b/requirements.txt index 4721654..f196561 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -r .github/requirements_ci.txt +yaargh git+https://github.com/glotaran/pyglotaran-extras.git diff --git a/scripts/run_examples_notebooks.py b/scripts/run_examples_notebooks.py new file mode 100644 index 0000000..4565aa7 --- /dev/null +++ b/scripts/run_examples_notebooks.py @@ -0,0 +1,145 @@ +from __future__ import annotations + +import json +import os +import warnings +from pathlib import Path + +import papermill as pm +import yaargh + +REPO_ROOT = Path(__file__).parent.parent +EXAMPLES_FOLDER = REPO_ROOT / "pyglotaran_examples" + + +def github_format_warning(message, category, filename, lineno, line=None): + return f"::warning file={filename},line={lineno}::{category.__name__}: {message}\n" + + +def run_notebook(notebook_path: Path) -> Path: + """Run notebook to update results.""" + print("\n", "#" * 80, sep="") + print("#", f"RUNNING: {notebook_path.name.upper()}".center(78), "#", sep="") + print("#" * 80, "\n") + if "GITHUB_OUTPUT" in os.environ: + warnings.formatwarning = github_format_warning + gh_output = Path(os.getenv("GITHUB_OUTPUT", "")) + with gh_output.open("a", encoding="utf8") as f: + f.writelines([f"notebook-path={notebook_path.as_posix()}"]) + print(f"Setting notebook-path output to {notebook_path.as_posix()}") + pm.execute_notebook(notebook_path, notebook_path, cwd=notebook_path.parent) + + +def fluorescence(): + """Run study_fluorescence/global_and_target_analysis.ipynb""" + return run_notebook(EXAMPLES_FOLDER / "study_fluorescence/global_and_target_analysis.ipynb") + + +def transient_absorption(): + """Runs study_transient_absorption/target_analysis.ipynb""" + return run_notebook(EXAMPLES_FOLDER / "study_transient_absorption/target_analysis.ipynb") + + +def transient_absorption_two_datasets(): + """Runs study_transient_absorption/two_dataset_analysis.ipynb""" + return run_notebook(EXAMPLES_FOLDER / "study_transient_absorption/two_dataset_analysis.ipynb") + + +def spectral_constraints(): + """Runs ex_spectral_constraints/ex_spectral_constraints.ipynb""" + return run_notebook(EXAMPLES_FOLDER / "ex_spectral_constraints/ex_spectral_constraints.ipynb") + + +def spectral_guidance(): + """Runs ex_spectral_guidance/ex_spectral_guidance.ipynb""" + return run_notebook(EXAMPLES_FOLDER / "ex_spectral_guidance/ex_spectral_guidance.ipynb") + + +def two_datasets(): + """Runs ex_two_datasets/ex_two_datasets.ipynb""" + return run_notebook(EXAMPLES_FOLDER / "ex_two_datasets/ex_two_datasets.ipynb") + + +def sim_3d_disp(): + """Runs test/simultaneous_analysis_3d_disp/sim_analysis_script_3d_disp.ipynb""" + return run_notebook( + EXAMPLES_FOLDER / "test/simultaneous_analysis_3d_disp/sim_analysis_script_3d_disp.ipynb" + ) + + +def sim_3d_nodisp(): + """Runs test/simultaneous_analysis_3d_nodisp/simultaneous_analysis_3d_nodisp.ipynb""" + return run_notebook( + EXAMPLES_FOLDER + / "test/simultaneous_analysis_3d_nodisp/simultaneous_analysis_3d_nodisp.ipynb" + ) + + +def sim_3d_weight(): + """Runs test/simultaneous_analysis_3d_weight/simultaneous_analysis_3d_weight.ipynb""" + return run_notebook( + EXAMPLES_FOLDER + / "test/simultaneous_analysis_3d_weight/simultaneous_analysis_3d_weight.ipynb" + ) + + +def sim_6d_disp(): + """Runs test/simultaneous_analysis_6d_disp/simultaneous_analysis_6d_disp.ipynb""" + return run_notebook( + EXAMPLES_FOLDER / "test/simultaneous_analysis_6d_disp/simultaneous_analysis_6d_disp.ipynb" + ) + + +def doas_beta(): + """Runs ex_doas_beta/ex_doas_beta.ipynb""" + return run_notebook(EXAMPLES_FOLDER / "ex_doas_beta/ex_doas_beta.ipynb") + + +all_funcs = [ + # fluorescence, + # transient_absorption, + # transient_absorption_two_datasets, + spectral_constraints, + # spectral_guidance, + # two_datasets, + sim_3d_disp, + # sim_3d_nodisp, + # sim_3d_weight, + # sim_6d_disp, + doas_beta, +] + + +def run_all(): + """Runs all examples.""" + errors = {} + for func in all_funcs: + try: + func() + except Exception as err: + errors[func.__name__] = err + if errors: + for func_name, err in errors.items(): + print("\n", "#" * 80, sep="") + print("#", f"Error running: {func_name.upper()}".center(78), "#", sep="") + print("#" * 80, "\n") + print(err) + print("Failed to run the following examples:") + for func_name in errors: + print(func_name) + + +def set_gha_example_list_output(): + """Export a list of all examples to an output github in github actions.""" + example_names = [func.__name__.replace("_", "-") for func in all_funcs] + gh_output = Path(os.getenv("GITHUB_OUTPUT", "")) + with gh_output.open("a", encoding="utf8") as f: + f.writelines([f"example-list={json.dumps(example_names)}"]) + + +parser = yaargh.ArghParser() +parser.add_commands([*all_funcs, run_all, set_gha_example_list_output]) + + +if __name__ == "__main__": + parser.dispatch()