Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Docs

on:
push:
branches: [main]
paths:
- "entries/**"
- "docs/**"
- "scripts/build_index.py"
- ".github/workflows/docs.yml"
pull_request:
paths:
- "entries/**"
- "docs/**"
- "scripts/build_index.py"
- ".github/workflows/docs.yml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: docs-${{ github.ref }}
# Cancel superseded PR builds, but never interrupt a Pages deployment.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
cache-dependency-path: docs/requirements.txt
- run: pip install -r docs/requirements.txt
# Notebooks aren't executed (nb_execution_mode = "off"), so no data or
# UXarray install is needed; this only renders committed outputs.
- run: sphinx-build -b html --keep-going docs docs/_build/html
- uses: actions/upload-pages-artifact@v3
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
with:
path: docs/_build/html

deploy:
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ __pycache__/
*.zarr/
*.h5
*.hdf5

# Docs build output (docs/entries/ is generated from entries/)
docs/_build/
docs/entries/
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,11 @@ You can validate your entry locally:
pip install pyyaml
python scripts/build_index.py --check
```

To preview how your entry looks on the community site (notebooks are rendered from their
committed outputs, never executed):

```bash
pip install -r docs/requirements.txt
sphinx-build -b html docs docs/_build/html # then open docs/_build/html/index.html
```
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ user community. Here you'll find demonstration notebooks from the team, notebook
together to ask questions or report behavior, workshop and tutorial material, and anything
else that comes out of working together.

Everything is organized by date. Browse the full catalog in **[INDEX.md](INDEX.md)**.
Everything is organized by date. Browse the full catalog in **[INDEX.md](INDEX.md)**, or read
the notebooks with their rendered outputs on the
**[community site](https://uxarray.github.io/uxarray-community/)**.

## Layout

Expand Down
42 changes: 42 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Sphinx configuration for the UXarray Community site.

Entry pages are generated from entries/ at build time (see generate.py). Notebooks are
never executed: most entries need data that only exists on HPC systems, so the outputs
committed by their authors are what gets rendered.
"""

import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
import generate # noqa: E402

generate.main()

project = "UXarray Community"
author = "UXarray developers and community"
copyright = "UXarray developers"

extensions = ["myst_nb", "sphinx_copybutton"]

# README content is folded into each entry's generated index.md.
exclude_patterns = ["_build", "**.ipynb_checkpoints", "entries/*/README.md"]

# Render committed outputs as-is; never run notebooks during the build.
nb_execution_mode = "off"

myst_enable_extensions = ["colon_fence", "dollarmath", "amsmath"]
myst_heading_anchors = 3

# Contributor notebooks often skip heading levels, and HoloViews adds helper outputs
# (application/vnd.holoviews_*) next to the HTML/JS that actually renders the plot.
suppress_warnings = ["myst.header", "mystnb.unknown_mime_type"]

html_theme = "sphinx_book_theme"
html_title = "UXarray Community"
html_theme_options = {
"repository_url": "https://github.com/UXARRAY/uxarray-community",
"use_repository_button": True,
"use_issues_button": True,
"home_page_in_toc": True,
}
112 changes: 112 additions & 0 deletions docs/generate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Generate the site's entry pages from entries/ before Sphinx reads the sources.

Each entry folder is copied to docs/entries/<slug>/ and given an index.md built from
its README metadata, README body, and notebooks. docs/entries/index.md is the catalog.
Everything under docs/entries/ is regenerated on every build and is git-ignored.
"""

from __future__ import annotations

import shutil
import sys
from pathlib import Path

DOCS = Path(__file__).resolve().parent
ROOT = DOCS.parent
OUT = DOCS / "entries"

sys.path.insert(0, str(ROOT / "scripts"))
from build_index import ENTRIES, FRONT_MATTER_RE, cell, collect # noqa: E402

IGNORE = shutil.ignore_patterns(".ipynb_checkpoints", "__pycache__", ".DS_Store")


def entry_page(entry: dict, folder: Path) -> str:
readme = (folder / "README.md").read_text(encoding="utf-8")
body = FRONT_MATTER_RE.sub("", readme, count=1).strip()

authors = ", ".join(
f"[@{a.lstrip('@')}](https://github.com/{a.lstrip('@')})" for a in entry["authors"]
)
facts = [
f"**Date:** {entry['date'].isoformat()}",
f"**Type:** {entry['type']}",
f"**Authors:** {authors}",
f"**UXarray:** {entry['uxarray_version']}",
]
if entry["tags"]:
facts.append("**Tags:** " + " ".join(f"`{t}`" for t in entry["tags"]))

lines = [f"# {entry['title']}", ""]
if entry["status"] == "superseded":
lines += [
":::{warning}",
"This entry is superseded. See its description for the newer approach.",
":::",
"",
]
lines += [" · ".join(facts), "", body, ""]

notebooks = sorted(
p.relative_to(folder).with_suffix("").as_posix()
for p in folder.rglob("*.ipynb")
if ".ipynb_checkpoints" not in p.parts
)
if notebooks:
lines += ["```{toctree}", ":maxdepth: 1", ":titlesonly:", ":caption: Notebooks", "", *notebooks, "```", ""]

# Other Markdown files (e.g. data.md) are pages too; keep them out of the sidebar.
others = sorted(
p.relative_to(folder).with_suffix("").as_posix()
for p in folder.rglob("*.md")
if p.name != "README.md" and ".ipynb_checkpoints" not in p.parts
)
if others:
lines += ["```{toctree}", ":hidden:", "", *others, "```", ""]
return "\n".join(lines)


def catalog(entries: list[dict]) -> str:
lines = ["# Catalog", "", f"{len(entries)} entries, newest first.", ""]
entries = sorted(entries, key=lambda e: (e["date"], e["path"]), reverse=True)
for year in sorted({e["date"].year for e in entries}, reverse=True):
lines += [
f"## {year}",
"",
"| Date | Title | Type | Authors | UXarray |",
"|------|-------|------|---------|---------|",
]
for e in (e for e in entries if e["date"].year == year):
slug = Path(e["path"]).name
title = f"[{cell(e['title'])}]({slug}/index.md)"
if e["status"] == "superseded":
title = f"~~{title}~~ (superseded)"
authors = ", ".join(f"@{a.lstrip('@')}" for a in e["authors"])
lines.append(
f"| {e['date'].isoformat()} | {title} | {e['type']} | {authors} "
f"| {cell(e['uxarray_version'])} |"
)
lines.append("")
slugs = [f"{Path(e['path']).name}/index" for e in entries]
lines += ["```{toctree}", ":hidden:", "", *slugs, "```", ""]
return "\n".join(lines)


def main() -> None:
entries, errors = collect()
for err in errors:
print(f"warning: {err}", file=sys.stderr)

if OUT.exists():
shutil.rmtree(OUT)
OUT.mkdir()
for entry in entries:
src = ENTRIES / Path(entry["path"]).name
dst = OUT / src.name
shutil.copytree(src, dst, ignore=IGNORE)
(dst / "index.md").write_text(entry_page(entry, src), encoding="utf-8")
(OUT / "index.md").write_text(catalog(entries), encoding="utf-8")


if __name__ == "__main__":
main()
21 changes: 21 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# UXarray Community

A shared space between the [UXarray](https://uxarray.readthedocs.io) team and its user
community: demonstration notebooks from the team, notebooks users put together to ask
questions or report behavior, workshop and tutorial material, and anything else that comes
out of working together.

Browse everything in the **[Catalog](entries/index.md)**, organized by date.

Notebooks are shown with the outputs their authors committed. Many of them run only on HPC
systems where the data lives, so each entry describes its data and pins its own
environment if you want to run it yourself.

Want to add an entry or ask a question with a notebook? See the
[contributing guide](https://github.com/UXARRAY/uxarray-community/blob/main/CONTRIBUTING.md).

```{toctree}
:hidden:

entries/index
```
5 changes: 5 additions & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
sphinx
myst-nb
sphinx-book-theme
sphinx-copybutton
pyyaml
2 changes: 1 addition & 1 deletion scripts/build_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def collect() -> tuple[list[dict], list[str]]:
entries, errors = [], []
if not ENTRIES.is_dir():
return entries, errors
for path in sorted(p for p in ENTRIES.iterdir() if p.is_dir()):
for path in sorted(p for p in ENTRIES.iterdir() if p.is_dir() and not p.name.startswith(".")):
entry, errs = load_entry(path)
errors.extend(errs)
if entry:
Expand Down
Loading