diff --git a/.github/renovate.json b/.github/renovate.json index 9b85094..26d9ddf 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -7,6 +7,7 @@ ":pinVersions" ], "enabledManagers": [ - "github-actions" + "pip_requirements", + "github-actions" ] } diff --git a/.github/scripts/build.py b/.github/scripts/build.py deleted file mode 100755 index b936554..0000000 --- a/.github/scripts/build.py +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env python3 -""" -Build script for marimo notebooks. - -This script exports marimo notebooks to HTML/WebAssembly format and generates -an index.html file that lists all the notebooks. It handles both regular notebooks -(from the notebooks/ directory) and apps (from the apps/ directory). - -The script can be run from the command line with optional arguments: - python .github/scripts/build.py [--output-dir OUTPUT_DIR] - -The exported files will be placed in the specified output directory (default: _site). -""" - -import os -import subprocess -import argparse -from typing import List, Optional, Dict, Any, Union -from pathlib import Path - - -def export_html_wasm(notebook_path: str, output_dir: str, as_app: bool = False) -> bool: - """Export a single marimo notebook to HTML/WebAssembly format. - - This function takes a marimo notebook (.py file) and exports it to HTML/WebAssembly format. - If as_app is True, the notebook is exported in "run" mode with code hidden, suitable for - applications. Otherwise, it's exported in "edit" mode, suitable for interactive notebooks. - - Args: - notebook_path (str): Path to the marimo notebook (.py file) to export - output_dir (str): Directory where the exported HTML file will be saved - as_app (bool, optional): Whether to export as an app (run mode) or notebook (edit mode). - Defaults to False. - - Returns: - bool: True if export succeeded, False otherwise - """ - # Convert .py extension to .html for the output file - output_path: str = notebook_path.replace(".py", ".html") - - # Base command for marimo export - cmd: List[str] = ["marimo", "export", "html-wasm"] - - # Configure export mode based on whether it's an app or a notebook - if as_app: - print(f"Exporting {notebook_path} to {output_path} as app") - cmd.extend(["--mode", "run", "--no-show-code"]) # Apps run in "run" mode with hidden code - else: - print(f"Exporting {notebook_path} to {output_path} as notebook") - cmd.extend(["--mode", "edit"]) # Notebooks run in "edit" mode - - try: - # Create full output path and ensure directory exists - output_file: str = os.path.join(output_dir, output_path) - os.makedirs(os.path.dirname(output_file), exist_ok=True) - - # Add notebook path and output file to command - cmd.extend([notebook_path, "-o", output_file]) - - # Run marimo export command - print(cmd) - subprocess.run(cmd, capture_output=True, text=True, check=True) - return True - except subprocess.CalledProcessError as e: - # Handle marimo export errors - print(f"Error exporting {notebook_path}:") - print(e.stderr) - return False - except Exception as e: - # Handle unexpected errors - print(f"Unexpected error exporting {notebook_path}: {e}") - return False - - -def generate_index(all_notebooks: List[str], output_dir: str) -> None: - """Generate an index.html file that lists all the notebooks. - - This function creates an HTML index page that displays links to all the exported - notebooks. The index page includes the marimo logo and displays each notebook - with a formatted title and a link to open it. - - Args: - all_notebooks (List[str]): List of paths to all the notebooks - output_dir (str): Directory where the index.html file will be saved - - Returns: - None - """ - print("Generating index.html") - - # Create the full path for the index.html file - index_path: str = os.path.join(output_dir, "index.html") - - # Ensure the output directory exists - os.makedirs(output_dir, exist_ok=True) - - try: - # Open the index.html file for writing - with open(index_path, "w") as f: - # Write the HTML header and page structure - f.write( - """ - - - - - marimo - - - -
- marimo -
-
-""" - ) - # Process each notebook and create a card for it - for notebook in all_notebooks: - # Extract the notebook name from the path and remove the .py extension - notebook_name: str = notebook.split("/")[-1].replace(".py", "") - - # Format the display name by replacing underscores with spaces and capitalizing - display_name: str = notebook_name.replace("_", " ").title() - - # Write the HTML for the notebook card - f.write( - f'
\n' - f'

{display_name}

\n' - f'
\n' - f' Open Notebook\n' - f"
\n" - f"
\n" - ) - # Write the HTML footer - f.write( - """
- -""" - ) - except IOError as e: - # Handle file I/O errors - print(f"Error generating index.html: {e}") - - -def main() -> None: - """Main function to build marimo notebooks. - - This function: - 1. Parses command line arguments - 2. Finds all marimo notebooks in the 'notebooks' and 'apps' directories - 3. Exports each notebook to HTML/WebAssembly format - 4. Generates an index.html file that lists all the notebooks - - Command line arguments: - --output-dir: Directory where the exported files will be saved (default: _site) - - Returns: - None - """ - # Set up command line argument parsing - parser: argparse.ArgumentParser = argparse.ArgumentParser(description="Build marimo notebooks") - parser.add_argument( - "--output-dir", default="_site", help="Output directory for built files" - ) - args: argparse.Namespace = parser.parse_args() - - # Initialize empty list to store all notebook paths - all_notebooks: List[str] = [] - - # Look for notebooks in both the notebooks/ and apps/ directories - for directory in ["notebooks", "apps"]: - dir_path: Path = Path(directory) - if not dir_path.exists(): - print(f"Warning: Directory not found: {dir_path}") - continue - - # Find all Python files recursively in the directory - all_notebooks.extend(str(path) for path in dir_path.rglob("*.py")) - - # Exit if no notebooks were found - if not all_notebooks: - print("No notebooks found!") - return - - # Export each notebook to HTML/WebAssembly format - # Files in the apps/ directory are exported as apps (run mode) - # Files in the notebooks/ directory are exported as notebooks (edit mode) - for nb in all_notebooks: - export_html_wasm(nb, args.output_dir, as_app=nb.startswith("apps/")) - - # Generate the index.html file that lists all notebooks - generate_index(all_notebooks, args.output_dir) - - -if __name__ == "__main__": - main() diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 73dca23..adeeb7b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -26,25 +26,9 @@ jobs: # Check out the repository code - uses: actions/checkout@v4 - # Install uv package manager for faster Python package installation - - name: ๐Ÿš€ Install uv - uses: astral-sh/setup-uv@v6 - - # Set up Python environment - - name: ๐Ÿ Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.12 # Use Python 3.12 for the build - - # Install marimo and other required dependencies - - name: ๐Ÿ“ฆ Install dependencies - run: | - uv pip install marimo - - # Run the build script to export notebooks to WebAssembly - - name: ๐Ÿ› ๏ธ Export notebooks + - name: Run the dryrun command run: | - python .github/scripts/build.py # This script exports all notebooks to the _site directory + make dryrun # Upload the generated site as an artifact for the deploy job - name: ๐Ÿ“ค Upload artifact @@ -52,6 +36,8 @@ jobs: with: path: _site # Directory containing the built site + + # The deploy job publishes the built site to GitHub Pages deploy: needs: build # This job depends on the build job completing successfully @@ -72,3 +58,7 @@ jobs: - name: ๐Ÿš€ Deploy to GitHub Pages id: deployment # ID used to reference this step's outputs uses: actions/deploy-pages@v4 # GitHub's official Pages deployment action + + - name: Display address + run: | + echo ${{ steps.deployment.outputs.page_url }} diff --git a/.github/workflows/index_template.html b/.github/workflows/index_template.html new file mode 100644 index 0000000..ad7e91a --- /dev/null +++ b/.github/workflows/index_template.html @@ -0,0 +1,242 @@ + + + + + + marimo WebAssembly + GitHub Pages Template + + + + +
+
+ +

๐Ÿš€ marimo WebAssembly + GitHub Pages Template

+

Interactive Python notebooks exported to WebAssembly and deployed to GitHub Pages

+
+ Python 3.12+ + Marimo + GitHub Pages +
+
+
+ +
+ +

Notebooks

+

Interactive notebooks in edit mode - you can modify and experiment with the code

+
+ + +
+
fibonacci
+
+

Interactive Fibonacci sequence calculator with a slider to adjust the number of sequence elements.

+ Open Notebook +
+
+ + +
+
penguins
+
+

Interactive data analysis of the Palmer Penguins dataset using Polars and marimo.

+ Open Notebook +
+
+ +
+ +

Apps

+

Interactive applications in run mode - code is hidden for a clean user interface

+
+ + +
+
charts
+
+

Interactive data visualization dashboard with Altair charts.

+ Open App +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5d79d07..c4ab7a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ _site/ +__marimo__ +tmp # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4f1cb06 --- /dev/null +++ b/Makefile @@ -0,0 +1,64 @@ +# This file contains commands for setting up the environment, formatting code, +# building the book, and other maintenance tasks. + +.DEFAULT_GOAL := help + +# Install uv and uvx +uv: +uv: + @which uv > /dev/null || (curl -LsSf https://astral.sh/uv/install.sh | sh > /dev/null 2>&1) + +# Display help information about available make targets +.PHONY: help +help: ## Display this help screen + @echo -e "\033[1mAvailable commands:\033[0m" + @grep -E '^[a-z.A-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}' | sort + +# Install and run Marimo for interactive notebooks +.PHONY: notebook +notebook: uv ## Export one notebook with NOTEBOOK=... + @mkdir -p _site/notebooks/$(NOTEBOOK) + @uvx marimo export html-wasm --sandbox notebooks/$(NOTEBOOK).py --mode edit -o _site/notebooks/$(NOTEBOOK) + +# Export all notebooks to HTML-WASM +.PHONY: all-notebooks +all-notebooks: ## Export all notebooks to HTML-WASM + @for notebook in notebooks/*.py; do \ + filename=$$(basename "$$notebook" .py); \ + echo "Exporting $$notebook to HTML-WASM..."; \ + echo $$filename; \ + $(MAKE) notebook NOTEBOOK=$$filename; \ + done + +# Install and run Marimo for interactive notebooks +.PHONY: app +app: uv ## Export one app with APP=... + @mkdir -p _site/apps/$(APP) + @uvx marimo export html-wasm --sandbox apps/$(APP).py --mode run --no-show-code -o _site/apps/$(APP) + +# Export all apps to HTML-WASM +.PHONY: all-apps +all-apps: ## Export all apps to HTML-WASM + @for app in apps/*.py; do \ + filename=$$(basename "$$app" .py); \ + echo "Exporting $$app to HTML-WASM..."; \ + echo $$filename; \ + $(MAKE) app APP=$$filename; \ + done + +# Build the website locally (dry run) without deploying to GitHub Pages +.PHONY: dryrun +dryrun: ## Build the website locally using the same process as CI/CD but publish to a local folder + @echo "Building website locally (dry run)..." + @mkdir -p _site/assets + + $(MAKE) all-notebooks + $(MAKE) all-apps + + @echo "Copying assets..." + @cp -r apps/public/logo.png _site/assets/logo.png + + @echo "Generating index.html..." + @cp .github/workflows/index_template.html _site/index.html + + @echo "Website built successfully! Open _site/index.html in your browser to view it." diff --git a/README.md b/README.md index dc2630d..7c2b5a6 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,103 @@ -# marimo WebAssembly + GitHub Pages Template +# ๐Ÿš€ marimo WebAssembly + GitHub Pages Template -This template repository demonstrates how to export [marimo](https://marimo.io) notebooks to WebAssembly and deploy them to GitHub Pages. +[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) +[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) +[![GitHub Pages](https://img.shields.io/badge/GitHub%20Pages-Deployed-green?logo=github)](https://pages.github.com/) +[![Marimo](https://img.shields.io/badge/marimo-0.13.15+-orange.svg)](https://marimo.io) +[![GitHub Actions](https://img.shields.io/badge/GitHub%20Actions-Enabled-2088FF?logo=github-actions&logoColor=white)](https://github.com/features/actions) +[![UV](https://img.shields.io/badge/UV-Package%20Manager-blueviolet)](https://github.com/astral-sh/uv) + +This template repository demonstrates how to export [marimo](https://marimo.io) notebooks to WebAssembly and +deploy them to GitHub Pages. Marimo is an interactive Python notebook that combines the flexibility +of a notebook with the reproducibility of a script. + +## ๐Ÿงฐ Prerequisites + +- Python 3.12 or higher +- GitHub account (for deployment) ## ๐Ÿ“š Included Examples -- `apps/charts.py`: Interactive data visualization with Altair -- `notebooks/fibonacci.py`: Interactive Fibonacci sequence calculator -- `notebooks/penguins.py`: Interactive data analysis with Polars and marimo +- **`apps/charts.py`**: Interactive data visualization dashboard with Altair charts. This app is exported in "run" mode, which hides the code and presents a clean user interface. +- **`notebooks/fibonacci.py`**: Interactive Fibonacci sequence calculator with a slider to adjust the number of sequence elements. This notebook is exported in "edit" mode, allowing users to modify the code. +- **`notebooks/penguins.py`**: Interactive data analysis of the Palmer Penguins dataset using Polars and marimo. Demonstrates data loading, filtering, and visualization. + +## ๐Ÿš€ Deployment -## ๐Ÿš€ Usage +### Automatic Deployment with GitHub Actions 1. Fork this repository 2. Add your marimo files to the `notebooks/` or `apps/` directory - 1. `notebooks/` notebooks are exported with `--mode edit` - 2. `apps/` notebooks are exported with `--mode run` -3. Push to main branch +3. Push to the main branch 4. Go to repository **Settings > Pages** and change the "Source" dropdown to "GitHub Actions" 5. GitHub Actions will automatically build and deploy to Pages -## Including data or assets +### How the Deployment Works -To include data or assets in your notebooks, add them to the `public/` directory. +- Notebooks in the `notebooks/` directory are exported with `--mode edit` (users can modify the code) +- Notebooks in the `apps/` directory are exported with `--mode run --no-show-code` (code is hidden, only UI is shown) +- The GitHub Actions workflow creates an index page and deploys everything to GitHub Pages -For example, the `apps/charts.py` notebook loads an image asset from the `public/` directory. +## ๐Ÿ’ป Local Development -```markdown - +### Setting Up the Environment + +This project uses [uv](https://github.com/astral-sh/uv) for package management. The Makefile includes commands to help you get started: + +```bash +# Install uv package manager +make uv + +# Run a specific notebook in edit mode +make notebook NOTEBOOK=fibonacci + +# Run a specific app in edit mode +make app APP=charts + +# Build the entire website locally (dry run) +make dryrun ``` -And the `notebooks/penguins.py` notebook loads a CSV dataset from the `public/` directory. +The `dryrun` command builds the entire website locally using the same process as the CI/CD pipeline but publishes it to a local `_site` folder instead of deploying to GitHub Pages. This is useful for previewing the complete website before pushing changes. + +### Creating New Notebooks + +1. Add your Python file to either the `notebooks/` or `apps/` directory +2. Include the necessary dependencies at the top of your file: ```python -import polars as pl -df = pl.read_csv(mo.notebook_location() / "public" / "penguins.csv") +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "marimo>=0.13.15", +# # Add other dependencies here +# ] +# /// ``` -## ๐Ÿงช Testing +## ๐Ÿ“ Including Data or Assets -To test the export process, run `.github/scripts/build.py` from the root directory. +To include data or assets in your notebooks, add them to the `public/` directory within either the `notebooks/` or `apps/` directory. -```bash -python .github/scripts/build.py +### Examples: + +#### Loading an image: + +```markdown + ``` -This will export all notebooks in a folder called `_site/` in the root directory. Then to serve the site, run: +#### Loading a CSV dataset: -```bash -python -m http.server -d _site +```python +import polars as pl +df = pl.read_csv(mo.notebook_location() / "public" / "penguins.csv") ``` -This will serve the site at `http://localhost:8000`. +## ๐Ÿค Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## ๐Ÿ“„ License + +This project is licensed under the terms included in the [LICENSE](LICENSE) file. diff --git a/apps/charts.py b/apps/charts.py index e9d6a88..5b8f022 100644 --- a/apps/charts.py +++ b/apps/charts.py @@ -1,20 +1,26 @@ +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "marimo==0.13.15", +# "altair==4.2.0", +# "pandas==2.3.0", +# "numpy==2.3.0" +# ] +# /// import marimo __generated_with = "0.10.9" app = marimo.App(width="medium") - -@app.cell -def _(): +with app.setup: import numpy as np import altair as alt import pandas as pd import marimo as mo - return alt, mo, np, pd @app.cell -def _(mo): +def _(): mo.md( """ # Interactive Data Visualization @@ -29,7 +35,7 @@ def _(mo): @app.cell -def _(alt, mo, np, pd): +def _(): # Create sample data data = pd.DataFrame({"x": np.arange(100), "y": np.random.normal(0, 1, 100)}) @@ -43,7 +49,7 @@ def _(alt, mo, np, pd): ) ) chart - return chart, data + return chart @app.cell diff --git a/notebooks/fibonacci.py b/notebooks/fibonacci.py index 5997136..c4c8d1d 100644 --- a/notebooks/fibonacci.py +++ b/notebooks/fibonacci.py @@ -1,11 +1,19 @@ +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "marimo==0.13.15", +# ] +# /// import marimo __generated_with = "0.10.6" app = marimo.App() - +with app.setup: + import marimo as mo + @app.cell -def _(mo): +def _(): mo.md( r""" # Fibonacci Calculator @@ -17,37 +25,25 @@ def _(mo): @app.cell -def _(mo): +def _(): # Create an interactive slider n = mo.ui.slider(1, 100, value=50, label="Number of Fibonacci numbers") n - return (n,) + return n @app.cell -def _(fibonacci, mo, n): +def _(n): fib = fibonacci(n.value) mo.md(", ".join([str(f) for f in fib])) - return (fib,) - - -@app.cell -def _(): - # Generate Fibonacci sequence - def fibonacci(n): - sequence = [0, 1] - for i in range(2, n): - sequence.append(sequence[i - 1] + sequence[i - 2]) - return sequence - return (fibonacci,) - - -@app.cell -def _(): - import numpy as np - import marimo as mo - return mo, np - + +@app.function +def fibonacci(n): + sequence = [0, 1] + for i in range(2, n): + sequence.append(sequence[i - 1] + sequence[i - 2]) + return sequence + if __name__ == "__main__": app.run() diff --git a/notebooks/penguins.py b/notebooks/penguins.py index 064a562..dd01ba6 100644 --- a/notebooks/penguins.py +++ b/notebooks/penguins.py @@ -1,19 +1,27 @@ +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "marimo==0.13.15", +# "polars==1.30.0", +# "altair==4.2.0", +# "pandas==2.3.0", +# ] +# /// import marimo -__generated_with = "0.10.9" +__generated_with = "0.13.5" app = marimo.App(width="medium") - -@app.cell -def _(): - import polars as pl +with app.setup: import marimo as mo + import polars as pl import altair as alt - return alt, mo, pl + import pandas as pd + file = mo.notebook_location() / "public" / "penguins.csv" @app.cell(hide_code=True) -def _(mo): +def _(): mo.md( """ # Palmer Penguins Analysis @@ -25,15 +33,20 @@ def _(mo): @app.cell -def _(mo, pl): +def _(): # Read the penguins dataset - df = pl.read_csv(str(mo.notebook_location() / "public" / "penguins.csv")) + df = pl.read_csv(str(file)) df.head() return (df,) +@app.cell +def _(): + # Try to avoid reading the file with pandas + _df = pd.read_csv(str(file)) + return @app.cell -def _(df, mo): +def _(df): # Basic statistics mo.md(f""" ### Dataset Overview @@ -49,13 +62,13 @@ def _(df, mo): @app.cell(hide_code=True) -def _(mo): +def _(): mo.md(r"""### Species Distribution""") return @app.cell -def _(alt, df, mo): +def _(df): # Create species distribution chart species_chart = mo.ui.altair_chart( alt.Chart(df) @@ -70,13 +83,13 @@ def _(alt, df, mo): @app.cell(hide_code=True) -def _(mo): +def _(): mo.md(r"""### Bill Dimensions Analysis""") return @app.cell -def _(alt, df, mo): +def _(df): # Scatter plot of bill dimensions scatter = mo.ui.altair_chart( alt.Chart(df)