From 8809fcfffd740499d0f03102c28d406c2cd31b93 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Thu, 16 Apr 2026 06:49:03 +0400 Subject: [PATCH 1/5] chore: prepare rhiza sync infrastructure Update rhiza tooling to use sync command (replacing deprecated materialize), bump .rhiza-version to 0.12.1, and add bootstrap.mk with install-uv target. Co-Authored-By: Claude Sonnet 4.6 --- .rhiza/.rhiza-version | 2 +- .rhiza/make.d/bootstrap.mk | 107 +++++++++++++++++++++ .rhiza/rhiza.mk | 189 ++++++------------------------------- .rhiza/template.yml | 37 ++++---- 4 files changed, 154 insertions(+), 181 deletions(-) create mode 100644 .rhiza/make.d/bootstrap.mk diff --git a/.rhiza/.rhiza-version b/.rhiza/.rhiza-version index 899f24fc..34a83616 100644 --- a/.rhiza/.rhiza-version +++ b/.rhiza/.rhiza-version @@ -1 +1 @@ -0.9.0 \ No newline at end of file +0.12.1 diff --git a/.rhiza/make.d/bootstrap.mk b/.rhiza/make.d/bootstrap.mk new file mode 100644 index 00000000..374c433a --- /dev/null +++ b/.rhiza/make.d/bootstrap.mk @@ -0,0 +1,107 @@ +## .rhiza/make.d/bootstrap.mk - Bootstrap and Installation +# This file provides targets for setting up the development environment, +# installing dependencies, and cleaning project artifacts. + +# Declare phony targets (they don't produce files) +.PHONY: install-uv install clean pre-install post-install + +# Hook targets (double-colon rules allow multiple definitions) +pre-install:: ; @: +post-install:: ; @: + +##@ Bootstrap +install-uv: ## ensure uv/uvx is installed + # Ensure the ${INSTALL_DIR} folder exists + @mkdir -p ${INSTALL_DIR} + + # Install uv/uvx only if they are not already present in PATH or in the install dir + @if command -v uv >/dev/null 2>&1 && command -v uvx >/dev/null 2>&1; then \ + :; \ + elif [ -x "${INSTALL_DIR}/uv" ] && [ -x "${INSTALL_DIR}/uvx" ]; then \ + printf "${BLUE}[INFO] uv and uvx already installed in ${INSTALL_DIR}, skipping.${RESET}\n"; \ + else \ + printf "${BLUE}[INFO] Installing uv and uvx into ${INSTALL_DIR}...${RESET}\n"; \ + if ! curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR="${INSTALL_DIR}" sh >/dev/null 2>&1; then \ + printf "${RED}[ERROR] Failed to install uv${RESET}\n"; \ + exit 1; \ + fi; \ + fi + +install: pre-install install-uv ## install + # Create the virtual environment only if it doesn't exist + @if [ ! -d "${VENV}" ]; then \ + ${UV_BIN} venv $(if $(PYTHON_VERSION),--python $(PYTHON_VERSION)) ${VENV} || { printf "${RED}[ERROR] Failed to create virtual environment${RESET}\n"; exit 1; }; \ + else \ + printf "${BLUE}[INFO] Using existing virtual environment at ${VENV}, skipping creation${RESET}\n"; \ + fi + + # Install the dependencies from pyproject.toml (if it exists) + @if [ -f "pyproject.toml" ]; then \ + if [ -f "uv.lock" ]; then \ + if ! ${UV_BIN} lock --check >/dev/null 2>&1; then \ + printf "${YELLOW}[WARN] uv.lock is out of sync with pyproject.toml${RESET}\n"; \ + printf "${YELLOW} Run 'uv sync' to update your lock file and environment${RESET}\n"; \ + printf "${YELLOW} Or run 'uv lock' to update only the lock file${RESET}\n"; \ + exit 1; \ + fi; \ + printf "${BLUE}[INFO] Installing dependencies from lock file${RESET}\n"; \ + ${UV_BIN} sync --all-extras --all-groups --frozen || { printf "${RED}[ERROR] Failed to install dependencies${RESET}\n"; exit 1; }; \ + else \ + printf "${YELLOW}[WARN] uv.lock not found. Generating lock file and installing dependencies...${RESET}\n"; \ + ${UV_BIN} sync --all-extras || { printf "${RED}[ERROR] Failed to install dependencies${RESET}\n"; exit 1; }; \ + fi; \ + else \ + printf "${YELLOW}[WARN] No pyproject.toml found, skipping install${RESET}\n"; \ + fi + + # Install dev dependencies from .rhiza/requirements/*.txt files + @if [ -d ".rhiza/requirements" ] && ls .rhiza/requirements/*.txt >/dev/null 2>&1; then \ + for req_file in .rhiza/requirements/*.txt; do \ + if [ -f "$$req_file" ]; then \ + printf "${BLUE}[INFO] Installing requirements from $$req_file${RESET}\n"; \ + ${UV_BIN} pip install -r "$$req_file" || { printf "${RED}[ERROR] Failed to install requirements from $$req_file${RESET}\n"; exit 1; }; \ + fi; \ + done; \ + fi + + # Check if there is requirements.txt file in the tests folder (legacy support) + @if [ -f "tests/requirements.txt" ]; then \ + printf "${BLUE}[INFO] Installing requirements from tests/requirements.txt${RESET}\n"; \ + ${UV_BIN} pip install -r tests/requirements.txt || { printf "${RED}[ERROR] Failed to install test requirements${RESET}\n"; exit 1; }; \ + fi + + # Install pre-commit hooks + @if [ -f ".pre-commit-config.yaml" ]; then \ + printf "${BLUE}[INFO] Installing pre-commit hooks...${RESET}\n"; \ + ${UVX_BIN} -p ${PYTHON_VERSION} pre-commit install || { printf "${YELLOW}[WARN] Failed to install pre-commit hooks${RESET}\n"; }; \ + fi + + @$(MAKE) post-install + + # Display success message with activation instructions + @printf "\n${GREEN}[SUCCESS] Installation complete!${RESET}\n\n" + @printf "${BLUE}To activate the virtual environment, run:${RESET}\n" + @printf "${YELLOW} source ${VENV}/bin/activate${RESET}\n\n" + +clean: ## Clean project artifacts and stale local branches + @printf "%bCleaning project...%b\n" "$(BLUE)" "$(RESET)" + + # Remove ignored files/directories, but keep .env files, tested with futures project + @git clean -d -X -f \ + -e '!.env' \ + -e '!.env.*' + + # Remove build & test artifacts + @rm -rf \ + dist \ + build \ + *.egg-info \ + .coverage \ + .pytest_cache \ + .benchmarks + + @printf "%bRemoving local branches with no remote counterpart...%b\n" "$(BLUE)" "$(RESET)" + + @git fetch --prune + + @git branch -vv | awk '/: gone]/{print $$1}' | xargs -r git branch -D diff --git a/.rhiza/rhiza.mk b/.rhiza/rhiza.mk index a03538b3..25282eaf 100644 --- a/.rhiza/rhiza.mk +++ b/.rhiza/rhiza.mk @@ -1,7 +1,7 @@ ## Makefile for jebel-quant/rhiza # (https://github.com/jebel-quant/rhiza) # -# Purpose: Developer tasks using uv/uvx (install, test, docs, marimushka, book). +# Purpose: Developer tasks using uv/uvx (install, test, book). # Lines with `##` after a target are parsed into help text, # and lines starting with `##@` create section headers in the help output. # @@ -18,14 +18,7 @@ RESET := \033[0m # Declare phony targets (they don't produce files) .PHONY: \ - bump \ - clean \ - deptry \ - fmt \ - mypy \ help \ - install \ - install-uv \ post-bump \ post-install \ post-release \ @@ -36,10 +29,10 @@ RESET := \033[0m pre-release \ pre-sync \ pre-validate \ - release \ - sync \ + print-logo \ + readme \ summarise-sync \ - update-readme \ + sync \ validate \ version-matrix @@ -54,29 +47,21 @@ PYTHON_VERSION ?= $(shell cat .python-version 2>/dev/null || echo "3.13") export PYTHON_VERSION # Read Rhiza version from .rhiza/.rhiza-version (single source of truth for rhiza-tools) -RHIZA_VERSION ?= $(shell cat .rhiza/.rhiza-version 2>/dev/null || echo "0.9.0") +RHIZA_VERSION ?= $(shell cat .rhiza/.rhiza-version 2>/dev/null || echo "0.10.2") export RHIZA_VERSION export UV_NO_MODIFY_PATH := 1 export UV_VENV_CLEAR := 1 +# Unset VIRTUAL_ENV to prevent uv from warning about path mismatches +# when a virtual environment is already activated in the shell +unexport VIRTUAL_ENV + # Load .rhiza/.env (if present) and export its variables so recipes see them. -include .rhiza/.env -# Include split Makefiles --include tests/tests.mk --include book/book.mk --include book/marimo/marimo.mk --include presentation/presentation.mk --include docker/docker.mk --include .github/agents/agentic.mk -# .rhiza/rhiza.mk is INLINED below --include .github/github.mk - - - # ============================================================================== -# Rhiza Core Actions (formerly .rhiza/rhiza.mk) +# Rhiza Core # ============================================================================== # RHIZA_LOGO definition @@ -91,19 +76,15 @@ endef export RHIZA_LOGO # Declare phony targets for Rhiza Core -.PHONY: print-logo sync validate readme pre-sync post-sync pre-validate post-validate +.PHONY: print-logo sync sync-experimental materialize validate readme pre-sync post-sync pre-validate post-validate # Hook targets (double-colon rules allow multiple definitions) +# Note: pre-install/post-install are defined in bootstrap.mk +# Note: pre-bump/post-bump/pre-release/post-release are defined in releasing.mk pre-sync:: ; @: post-sync:: ; @: pre-validate:: ; @: post-validate:: ; @: -pre-install:: ; @: -post-install:: ; @: -pre-release:: ; @: -post-release:: ; @: -pre-bump:: ; @: -post-bump:: ; @: ##@ Rhiza Workflows @@ -116,152 +97,42 @@ sync: pre-sync ## sync with template repository as defined in .rhiza/template.ym printf "${BLUE}[INFO] Skipping sync in rhiza repository (no template.yml by design)${RESET}\n"; \ else \ $(MAKE) install-uv; \ - ${UVX_BIN} "rhiza>=$(RHIZA_VERSION)" materialize --force .; \ + ${UVX_BIN} "rhiza==$(RHIZA_VERSION)" sync .; \ fi @$(MAKE) post-sync +materialize: ## [DEPRECATED] use 'make sync' instead — materialize --force is now sync + @printf "${YELLOW}[WARN] 'make materialize' is deprecated and will be removed in a future release.${RESET}\n" + @printf "${YELLOW}[WARN] Please use 'make sync' instead (e.g. 'materialize --force' is now 'make sync').${RESET}\n" + @$(MAKE) sync + summarise-sync: install-uv ## summarise differences created by sync with template repository @if git remote get-url origin 2>/dev/null | grep -iqE 'jebel-quant/rhiza(\.git)?$$'; then \ printf "${BLUE}[INFO] Skipping summarise-sync in rhiza repository (no template.yml by design)${RESET}\n"; \ else \ $(MAKE) install-uv; \ - ${UVX_BIN} "rhiza>=$(RHIZA_VERSION)" summarise .; \ + ${UVX_BIN} "rhiza==$(RHIZA_VERSION)" summarise .; \ + fi + +rhiza-test: install ## run rhiza's own tests (if any) + @if [ -d ".rhiza/tests" ]; then \ + ${UV_BIN} run pytest .rhiza/tests; \ + else \ + printf "${YELLOW}[WARN] No .rhiza/tests directory found, skipping rhiza-tests${RESET}\n"; \ fi -validate: pre-validate ## validate project structure against template repository as defined in .rhiza/template.yml +validate: pre-validate rhiza-test ## validate project structure against template repository as defined in .rhiza/template.yml @if git remote get-url origin 2>/dev/null | grep -iqE 'jebel-quant/rhiza(\.git)?$$'; then \ printf "${BLUE}[INFO] Skipping validate in rhiza repository (no template.yml by design)${RESET}\n"; \ else \ $(MAKE) install-uv; \ - ${UVX_BIN} "rhiza>=$(RHIZA_VERSION)" validate .; \ + ${UVX_BIN} "rhiza==$(RHIZA_VERSION)" validate .; \ fi @$(MAKE) post-validate readme: install-uv ## update README.md with current Makefile help output @${UVX_BIN} "rhiza-tools>=0.2.0" update-readme -# ============================================================================== -# End Rhiza Core Actions -# ============================================================================== - -##@ Bootstrap -install-uv: ## ensure uv/uvx is installed - # Ensure the ${INSTALL_DIR} folder exists - @mkdir -p ${INSTALL_DIR} - - # Install uv/uvx only if they are not already present in PATH or in the install dir - @if command -v uv >/dev/null 2>&1 && command -v uvx >/dev/null 2>&1; then \ - :; \ - elif [ -x "${INSTALL_DIR}/uv" ] && [ -x "${INSTALL_DIR}/uvx" ]; then \ - printf "${BLUE}[INFO] uv and uvx already installed in ${INSTALL_DIR}, skipping.${RESET}\n"; \ - else \ - printf "${BLUE}[INFO] Installing uv and uvx into ${INSTALL_DIR}...${RESET}\n"; \ - if ! curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR="${INSTALL_DIR}" sh >/dev/null 2>&1; then \ - printf "${RED}[ERROR] Failed to install uv${RESET}\n"; \ - exit 1; \ - fi; \ - fi - -install: pre-install install-uv ## install - # Create the virtual environment only if it doesn't exist - @if [ ! -d "${VENV}" ]; then \ - ${UV_BIN} venv $(if $(PYTHON_VERSION),--python $(PYTHON_VERSION)) ${VENV} || { printf "${RED}[ERROR] Failed to create virtual environment${RESET}\n"; exit 1; }; \ - else \ - printf "${BLUE}[INFO] Using existing virtual environment at ${VENV}, skipping creation${RESET}\n"; \ - fi - - # Install the dependencies from pyproject.toml (if it exists) - @if [ -f "pyproject.toml" ]; then \ - if [ -f "uv.lock" ]; then \ - printf "${BLUE}[INFO] Installing dependencies from lock file${RESET}\n"; \ - ${UV_BIN} sync --all-extras --all-groups --frozen || { printf "${RED}[ERROR] Failed to install dependencies${RESET}\n"; exit 1; }; \ - else \ - printf "${YELLOW}[WARN] uv.lock not found. Generating lock file and installing dependencies...${RESET}\n"; \ - ${UV_BIN} sync --all-extras || { printf "${RED}[ERROR] Failed to install dependencies${RESET}\n"; exit 1; }; \ - fi; \ - else \ - printf "${YELLOW}[WARN] No pyproject.toml found, skipping install${RESET}\n"; \ - fi - - # Install dev dependencies from .rhiza/requirements/*.txt files - @if [ -d ".rhiza/requirements" ] && ls .rhiza/requirements/*.txt >/dev/null 2>&1; then \ - for req_file in .rhiza/requirements/*.txt; do \ - if [ -f "$$req_file" ]; then \ - printf "${BLUE}[INFO] Installing requirements from $$req_file${RESET}\n"; \ - ${UV_BIN} pip install -r "$$req_file" || { printf "${RED}[ERROR] Failed to install requirements from $$req_file${RESET}\n"; exit 1; }; \ - fi; \ - done; \ - fi - - # Check if there is requirements.txt file in the tests folder (legacy support) - @if [ -f "tests/requirements.txt" ]; then \ - printf "${BLUE}[INFO] Installing requirements from tests/requirements.txt${RESET}\n"; \ - ${UV_BIN} pip install -r tests/requirements.txt || { printf "${RED}[ERROR] Failed to install test requirements${RESET}\n"; exit 1; }; \ - fi - @$(MAKE) post-install - -clean: ## Clean project artifacts and stale local branches - @printf "%bCleaning project...%b\n" "$(BLUE)" "$(RESET)" - - # Remove ignored files/directories, but keep .env files, tested with futures project - @git clean -d -X -f \ - -e '!.env' \ - -e '!.env.*' - - # Remove build & test artifacts - @rm -rf \ - dist \ - build \ - *.egg-info \ - .coverage \ - .pytest_cache \ - .benchmarks - - @printf "%bRemoving local branches with no remote counterpart...%b\n" "$(BLUE)" "$(RESET)" - - @git fetch --prune - - @git branch -vv | awk '/: gone]/{print $$1}' | xargs -r git branch -D - -##@ Quality and Formatting -deptry: install-uv ## Run deptry - @if [ -d ${SOURCE_FOLDER} ]; then \ - $(UVX_BIN) -p ${PYTHON_VERSION} deptry ${SOURCE_FOLDER}; \ - fi - - @if [ -d ${MARIMO_FOLDER} ]; then \ - if [ -d ${SOURCE_FOLDER} ]; then \ - $(UVX_BIN) -p ${PYTHON_VERSION} deptry ${MARIMO_FOLDER} ${SOURCE_FOLDER} --ignore DEP004; \ - else \ - $(UVX_BIN) -p ${PYTHON_VERSION} deptry ${MARIMO_FOLDER} --ignore DEP004; \ - fi \ - fi - -fmt: install-uv ## check the pre-commit hooks and the linting - @${UVX_BIN} -p ${PYTHON_VERSION} pre-commit run --all-files - -mypy: install-uv ## run mypy analysis - @if [ -d ${SOURCE_FOLDER} ]; then \ - ${UVX_BIN} -p ${PYTHON_VERSION} mypy ${SOURCE_FOLDER} --strict --config-file=pyproject.toml; \ - fi - -##@ Releasing and Versioning -bump: pre-bump ## bump version - @if [ -f "pyproject.toml" ]; then \ - $(MAKE) install; \ - ${UVX_BIN} "rhiza[tools]>=0.8.6" tools bump; \ - printf "${BLUE}[INFO] Updating uv.lock file...${RESET}\n"; \ - ${UV_BIN} lock; \ - else \ - printf "${YELLOW}[WARN] No pyproject.toml found, skipping bump${RESET}\n"; \ - fi - @$(MAKE) post-bump - -release: pre-release install-uv ## create tag and push to remote with prompts - @UV_BIN="${UV_BIN}" /bin/sh ".rhiza/scripts/release.sh" - @$(MAKE) post-release - - ##@ Meta help: print-logo ## Display this help message @@ -272,7 +143,7 @@ help: print-logo ## Display this help message +@printf "\n" version-matrix: install-uv ## Emit the list of supported Python versions from pyproject.toml - @${UV_BIN} run .rhiza/utils/version_matrix.py + @${UVX_BIN} "rhiza-tools>=0.2.2" version-matrix print-% : ## print the value of a variable (usage: make print-VARIABLE) @printf "${BLUE}[INFO] Printing value of variable '$*':${RESET}\n" diff --git a/.rhiza/template.yml b/.rhiza/template.yml index 1fb7976d..6a7d0141 100644 --- a/.rhiza/template.yml +++ b/.rhiza/template.yml @@ -1,22 +1,17 @@ -template-repository: jebel-quant/rhiza -template-branch: main -include: -- .github/workflows -- .editorconfig -- .gitignore -- .pre-commit-config.yaml -- CODE_OF_CONDUCT.md -- CONTRIBUTING.md -- Makefile -- ruff.toml -- .rhiza -- book -- renovate.json -exclude: -- .github/workflows/rhiza_docker.yml -- .github/workflows/rhiza_devcontainer.yml -- .github/workflows/rhiza_ci.yml -- .github/workflows/rhiza_security.yml -- .github/dependabot.yml -- .rhiza/docs +template-repository: "jebel-quant/rhiza" +template-branch: "v0.9.5" + +templates: + - github + - marimo +exclude: + - .rhiza/docs/ + - .rhiza/make.d/agentic.mk + - .rhiza/make.d/gh-aw.mk + - .rhiza/make.d/github.mk + - .rhiza/make.d/custom-env.mk + - .rhiza/make.d/custom-task.mk + - .rhiza/make.d/README.md + - .github/agents/ + - docs/adr From 2f7b64063a2017cab7e166b8e3238ed011e5f655 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Thu, 16 Apr 2026 06:50:06 +0400 Subject: [PATCH 2/5] chore: sync with rhiza template v0.9.5 Applied rhiza template sync from jebel-quant/rhiza@v0.9.5. Added new GitHub templates, workflows, hooks, and make.d files; removed deprecated workflows and orphaned files. Co-Authored-By: Claude Sonnet 4.6 --- .github/DISCUSSION_TEMPLATE/q-and-a.yml | 25 + .github/ISSUE_TEMPLATE/bug_report.yml | 57 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 41 ++ .github/actions/configure-git-auth/README.md | 80 +++ .github/actions/configure-git-auth/action.yml | 21 + .github/copilot-instructions.md | 181 +++++ .github/dependabot.yml | 80 +++ .github/hooks/hooks.json | 21 + .github/hooks/session-end.sh | 36 + .github/hooks/session-start.sh | 37 ++ .github/secret_scanning.yml | 20 + .github/semgrep.yml | 84 +++ .github/workflows/copilot-setup-steps.yml | 50 ++ .github/workflows/rhiza_book.yml | 33 +- .github/workflows/rhiza_codeql.yml | 125 ---- .github/workflows/rhiza_deptry.yml | 40 -- .github/workflows/rhiza_marimo.yml | 32 +- .github/workflows/rhiza_mypy.yml | 34 - .github/workflows/rhiza_pre-commit.yml | 37 -- .github/workflows/rhiza_release.yml | 173 +++-- .github/workflows/rhiza_sync.yml | 114 +++- .github/workflows/rhiza_validate.yml | 27 - .github/workflows/rhiza_weekly.yml | 118 ++++ .gitignore | 24 + .pre-commit-config.yaml | 41 +- .python-version | 1 + .rhiza/.cfg.toml | 12 +- .rhiza/.env | 13 +- .rhiza/assets/rhiza-logo.svg | 81 +++ .rhiza/make.d/01-custom-env.mk | 9 - .rhiza/make.d/10-custom-task.mk | 12 - .rhiza/make.d/book.mk | 99 +++ .rhiza/make.d/marimo.mk | 42 ++ .rhiza/make.d/quality.mk | 51 ++ .rhiza/make.d/releasing.mk | 50 ++ .rhiza/requirements/README.md | 6 +- .rhiza/requirements/docs.txt | 4 +- .rhiza/requirements/tests.txt | 12 - .rhiza/requirements/tools.txt | 3 +- .rhiza/scripts/check_workflow_names.py | 73 -- .rhiza/scripts/release.sh | 276 -------- .rhiza/template.lock | 64 ++ .rhiza/utils/version_matrix.py | 152 ----- CODE_OF_CONDUCT.md | 27 - CONTRIBUTING.md | 93 --- Makefile | 41 ++ book/README.md | 67 -- book/book.mk | 166 ----- book/marimo/marimo.mk | 67 -- book/marimo/notebooks/rhiza.py | 629 ------------------ book/minibook-templates/custom.html.jinja2 | 210 ------ docs/assets/rhiza-logo.svg | 81 +++ .../README.md => docs/development/MARIMO.md | 10 +- docs/index.md | 2 + docs/mkdocs-base.yml | 71 ++ renovate.json | 29 - ruff.toml | 40 +- 57 files changed, 1798 insertions(+), 2226 deletions(-) create mode 100644 .github/DISCUSSION_TEMPLATE/q-and-a.yml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/actions/configure-git-auth/README.md create mode 100644 .github/actions/configure-git-auth/action.yml create mode 100644 .github/copilot-instructions.md create mode 100644 .github/dependabot.yml create mode 100644 .github/hooks/hooks.json create mode 100755 .github/hooks/session-end.sh create mode 100755 .github/hooks/session-start.sh create mode 100644 .github/secret_scanning.yml create mode 100644 .github/semgrep.yml create mode 100644 .github/workflows/copilot-setup-steps.yml delete mode 100644 .github/workflows/rhiza_codeql.yml delete mode 100644 .github/workflows/rhiza_deptry.yml delete mode 100644 .github/workflows/rhiza_mypy.yml delete mode 100644 .github/workflows/rhiza_pre-commit.yml delete mode 100644 .github/workflows/rhiza_validate.yml create mode 100644 .github/workflows/rhiza_weekly.yml create mode 100644 .python-version create mode 100644 .rhiza/assets/rhiza-logo.svg delete mode 100644 .rhiza/make.d/01-custom-env.mk delete mode 100644 .rhiza/make.d/10-custom-task.mk create mode 100644 .rhiza/make.d/book.mk create mode 100644 .rhiza/make.d/marimo.mk create mode 100644 .rhiza/make.d/quality.mk create mode 100644 .rhiza/make.d/releasing.mk delete mode 100644 .rhiza/requirements/tests.txt delete mode 100644 .rhiza/scripts/check_workflow_names.py delete mode 100755 .rhiza/scripts/release.sh create mode 100644 .rhiza/template.lock delete mode 100755 .rhiza/utils/version_matrix.py delete mode 100644 CODE_OF_CONDUCT.md delete mode 100644 CONTRIBUTING.md delete mode 100644 book/README.md delete mode 100644 book/book.mk delete mode 100644 book/marimo/marimo.mk delete mode 100644 book/marimo/notebooks/rhiza.py delete mode 100644 book/minibook-templates/custom.html.jinja2 create mode 100644 docs/assets/rhiza-logo.svg rename book/marimo/README.md => docs/development/MARIMO.md (95%) create mode 100644 docs/index.md create mode 100644 docs/mkdocs-base.yml delete mode 100644 renovate.json diff --git a/.github/DISCUSSION_TEMPLATE/q-and-a.yml b/.github/DISCUSSION_TEMPLATE/q-and-a.yml new file mode 100644 index 00000000..c83b0a7d --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/q-and-a.yml @@ -0,0 +1,25 @@ +title: "[Question] " +labels: ["question"] +body: + - type: markdown + attributes: + value: | + Welcome! Use this space to ask questions, share how your use-case, or explore ideas with the community. + + - type: textarea + id: question + attributes: + label: Your Question or Topic + description: What would you like to discuss? + validations: + required: true + + - type: textarea + id: context + attributes: + label: Context + description: Any relevant code, configuration, or background that helps frame your question. + placeholder: | + ```python + # your code snippet here + ``` diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..060ab290 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,57 @@ +name: Bug Report +description: Report a bug or unexpected behaviour +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report a bug. Please fill out the sections below. + + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of what the bug is. + placeholder: What happened? + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: Minimal steps to reproduce the behaviour. + placeholder: | + 1. Run `make ...` + 2. See error + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behaviour + description: What did you expect to happen? + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Environment + description: | + Relevant versions and system info. Run `make info` if available. + placeholder: | + - OS: macOS 14 / Ubuntu 24.04 / Windows 11 + - Python: 3.13.x + - rhiza version: + validations: + required: false + + - type: textarea + id: context + attributes: + label: Additional context + description: Logs, screenshots, or anything else that may be helpful. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..1b0de2d0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,41 @@ +name: Feature Request +description: Suggest a new feature or enhancement +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for proposing a feature. Please align with the team before investing significant effort. + + - type: textarea + id: problem + attributes: + label: Problem / motivation + description: What problem does this solve? Why is it valuable? + placeholder: As a contributor I find it hard to ... because ... + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed solution + description: Describe the solution you have in mind. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Other approaches you have considered and why you ruled them out. + validations: + required: false + + - type: textarea + id: context + attributes: + label: Additional context + description: Links, mockups, prior art, or anything else that may be helpful. + validations: + required: false diff --git a/.github/actions/configure-git-auth/README.md b/.github/actions/configure-git-auth/README.md new file mode 100644 index 00000000..4b6faeb7 --- /dev/null +++ b/.github/actions/configure-git-auth/README.md @@ -0,0 +1,80 @@ +# Configure Git Auth for Private Packages + +This composite action configures git to use token authentication for private GitHub packages. + +## Usage + +Add this step before installing dependencies that include private GitHub packages: + +```yaml +- name: Configure git auth for private packages + uses: ./.github/actions/configure-git-auth + with: + token: ${{ secrets.GH_PAT }} +``` + +The `GH_PAT` secret should be a Personal Access Token with `repo` scope. + +## What It Does + +This action runs: + +```bash +git config --global url."https://@github.com/".insteadOf "https://github.com/" +``` + +This tells git to automatically inject the token into all HTTPS GitHub URLs, enabling access to private repositories. + +## When to Use + +Use this action when your project has dependencies defined in `pyproject.toml` like: + +```toml +[tool.uv.sources] +private-package = { git = "https://github.com/your-org/private-package.git", rev = "v1.0.0" } +``` + +## Token Requirements + +By default, this action will use the workflow’s built-in `GITHUB_TOKEN` (`github.token`) if no `token` input is provided or if the provided value is empty (it uses `inputs.token || github.token` internally). + +The `GITHUB_TOKEN` is usually sufficient when: + +- installing dependencies hosted in the **same repository** as the workflow, or +- accessing **public** repositories. + +The default `GITHUB_TOKEN` typically does **not** have permission to read other private repositories, even within the same organization. For that scenario, you should create a Personal Access Token (PAT) with `repo` scope and store it as `secrets.GH_PAT`, then pass it to the action via the `token` input. + +If you configure the step as in the example (`token: ${{ secrets.GH_PAT }}`) and `secrets.GH_PAT` is not defined, GitHub Actions passes an empty string to the action. The composite action then falls back to `github.token`, so the configuration step itself still succeeds. However, any subsequent step that tries to access private repositories that are not covered by the permissions of `GITHUB_TOKEN` will fail with an authentication error. +## Example Workflow + +```yaml +name: CI + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Configure git auth for private packages + uses: ./.github/actions/configure-git-auth + with: + token: ${{ secrets.GH_PAT }} + + - name: Install dependencies + run: uv sync --frozen + + - name: Run tests + run: uv run pytest +``` + +## See Also + +- [PRIVATE_PACKAGES.md](../../../.rhiza/docs/PRIVATE_PACKAGES.md) - Complete guide to using private packages +- [TOKEN_SETUP.md](../../../.rhiza/docs/TOKEN_SETUP.md) - Setting up Personal Access Tokens diff --git a/.github/actions/configure-git-auth/action.yml b/.github/actions/configure-git-auth/action.yml new file mode 100644 index 00000000..d4d898fb --- /dev/null +++ b/.github/actions/configure-git-auth/action.yml @@ -0,0 +1,21 @@ +name: 'Configure Git Auth for Private Packages' +description: 'Configure git to use token authentication for private GitHub packages' + +inputs: + token: + description: 'GitHub token to use for authentication' + required: false + +runs: + using: composite + steps: + - name: Configure git authentication + shell: bash + env: + GH_TOKEN: ${{ inputs.token || github.token }} + run: | + # Configure git to use token authentication for GitHub URLs + # This allows uv/pip to install private packages from GitHub + git config --global url."https://${GH_TOKEN}@github.com/".insteadOf "https://github.com/" + + echo "✓ Git configured to use token authentication for GitHub" diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..ec0580fc --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,181 @@ +# Rhiza Copilot Instructions + +You are working in a project that utilises the `rhiza` framework. Rhiza is a collection of reusable +configuration templates and tooling designed to standardise and streamline modern Python development. + +As a Rhiza-based project, this workspace adheres to specific conventions for structure, dependency management, and automation. + +## Development Environment + +The project uses `make` and `uv` for development tasks. UV handles all dependency and Python version management automatically. + +### Prerequisites + +- **Git**: Required for version control +- **Make**: Command runner for all development tasks +- **curl**: Required for installing uv (usually pre-installed on most systems) + +**Note**: Python is NOT a prerequisite. UV will automatically download and install the correct Python version (specified in `.python-version`) when you run `make install`. + +### Environment Setup + +Setting up your environment is simple: + +```bash +make install +``` + +This single command handles everything: +1. Installs `uv` package manager (to `./bin/uv` if not already in PATH) +2. Downloads and installs the correct Python version from `.python-version` (currently 3.13) +3. Creates a `.venv` virtual environment with that Python version +4. Installs all project dependencies from `pyproject.toml` + +### Verifying Installation + +After installation completes, verify everything works: + +```bash +make test # Should run successfully +``` + +### Environment Variables + +UV automatically uses these environment variables (set by the bootstrap process): +- `UV_LINK_MODE=copy`: Ensures proper dependency linking across filesystems +- `UV_VENV_CLEAR=1`: Clears existing venv on reinstall to avoid conflicts + +### Common Development Commands + +- **Install Dependencies**: `make install` (full setup: uv, Python, venv, dependencies) +- **Run Tests**: `make test` (runs `pytest` with coverage) +- **Format Code**: `make fmt` (runs `ruff format` and `ruff check --fix`) +- **Check Dependencies**: `make deptry` (checks for missing/unused dependencies) +- **Marimo Notebooks**: `make marimo` (starts the Marimo notebook server) +- **Build Documentation**: `make book` (builds the documentation book) +- **Clean Environment**: `make clean` (removes build artifacts and stale branches) + +### Troubleshooting + +- **Installation fails**: Check internet connectivity (UV needs to download Python and packages) +- **Python version issues**: The `.python-version` file is the single source of truth. UV uses this automatically. +- **Pre-commit failures**: Run `make fmt` to auto-fix most formatting issues +- **Stale environment**: Run `make clean` followed by `make install` to start fresh + +### Important Notes for Agents + +- **Virtual Environment Activation**: Most `make` commands automatically handle virtual environment activation. Manual activation is rarely needed. +- **Python Version**: The repository specifies Python 3.13 in `.python-version`. UV installs this automatically. +- **All Commands Through Make**: Always use `make` targets rather than running tools directly to ensure consistency. +- **When a `make` target exists, use it**: Do not replace `make test`, `make fmt`, `make deptry`, etc. with direct tool commands. +- **For Python commands without a `make` target, use `uv run`**: Run Python and Python tooling via `uv run `. +- **Never call the interpreter directly from `.venv`**: Do **not** use `.venv/bin/python`, `.venv/bin/pytest`, etc. + +### Command Execution Policy (Strict) + +Use these rules in order: + +1. If there is an appropriate `make` target, use the `make` target. +2. If no `make` target exists and you must run Python code/tooling, use `uv run ...`. +3. Do not invoke binaries from `.venv/bin` directly. + +Examples: + +- ✅ `make test` +- ✅ `make fmt` +- ✅ `uv run pytest` +- ✅ `uv run python -m pytest tests/property/test_makefile_properties.py` +- ✅ `uv run python scripts/some_script.py` +- ❌ `.venv/bin/python -m pytest` +- ❌ `.venv/bin/pytest` + +### Customizing Setup with Hooks + +The Makefile provides hooks for customizing the setup process. Add these to the root `Makefile`: + +```makefile +# Run before make install +pre-install:: + @echo "Installing system dependencies..." + @command -v graphviz || brew install graphviz + +# Run after make install +post-install:: + @echo "Running custom setup..." + @./scripts/custom-setup.sh +``` + +**Available hooks:** +- `pre-install` / `post-install`: Runs around `make install` +- `pre-sync` / `post-sync`: Runs around template synchronization +- `pre-validate` / `post-validate`: Runs around validation +- `pre-release` / `post-release`: Runs around releases + +**Note**: Use double-colon syntax (`::`) for hooks to allow multiple definitions. See `.rhiza/make.d/README.md` for more details. + +### Cloud/CI Environment Setup + +The Copilot coding agent environment is automatically configured via official GitHub mechanisms: + +- **`.github/workflows/copilot-setup-steps.yml`**: Runs before the agent starts. Installs uv, configures git auth for private packages, and runs `make install` to set up a deterministic environment. +- **`.github/hooks/hooks.json`**: Defines session lifecycle hooks: + - `sessionStart`: Validates the environment is correctly set up (uv available, .venv exists) + - `sessionEnd`: Runs `make fmt` and `make test` as quality gates after the agent finishes work + +These files must exist on the default branch. The agent does not need to run any setup commands manually. + +For DevContainers and Codespaces, the `.devcontainer/` configuration and `bootstrap.sh` handle setup automatically. + +## Project Structure + +- `src/`: Source code +- `tests/`: Tests (pytest) +- `assets/`: Static assets +- `book/`: Documentation source +- `docker/`: Docker configuration +- `.rhiza/`: Rhiza-specific scripts and configurations + +## Coding Standards + +- **Style**: Follow PEP 8. Use `make fmt` to enforce style. +- **Testing**: Write tests in `tests/` using `pytest`. Ensure high coverage. +- **Documentation**: Document code using docstrings. +- **Dependencies**: Manage dependencies in `pyproject.toml`. Use `uv add` to add dependencies. + +## Workflow + +1. **Setup**: Run `make install` to set up the environment. +2. **Develop**: Write code in `src/` and tests in `tests/`. +3. **Test**: Run `make test` to verify changes. +4. **Format**: Run `make fmt` before committing. +5. **Verify**: Run `make deptry` to check dependencies. + +## GitHub Agentic Workflows (gh-aw) + +This repository uses GitHub Agentic Workflows for AI-driven automation. +Agentic workflow files are Markdown files in `.github/workflows/` with +`.lock.yml` compiled counterparts. + +**Key Commands:** +- `make gh-aw-compile` or `gh aw compile` — Compile workflow `.md` files to `.lock.yml` +- `make gh-aw-run WORKFLOW=` or `gh aw run ` — Run a specific workflow locally +- `make gh-aw-status` — Check status of all agentic workflows +- `make gh-aw-setup` — Configure secrets and engine for first-time setup + +**Important Rules:** +- **Never edit `.lock.yml` files directly** — Always edit the `.md` source and recompile +- Workflows must be compiled before they can run in GitHub Actions +- After editing any `.md` workflow, always run `make gh-aw-compile` and commit both files + +**Available Starter Workflows:** +- `daily-repo-status.md` — Daily repository health reports +- `ci-doctor.md` — Automatic CI failure diagnosis +- `issue-triage.md` — Automatic issue classification and labeling + +## Key Files + +- `Makefile`: Main entry point for tasks. +- `pyproject.toml`: Project configuration and dependencies. +- `.devcontainer/bootstrap.sh`: Bootstrap script for dev containers. +- `.github/workflows/copilot-setup-steps.yml`: Agent environment setup (runs before agent starts). +- `.github/hooks/hooks.json`: Agent session hooks (quality gates). diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..1da7ad10 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,80 @@ +# This file is part of the jebel-quant/rhiza repository +# (https://github.com/jebel-quant/rhiza). +# +# Configuration: Dependabot +# +# Purpose: Automate dependency updates for Python packages, GitHub Actions, and Docker images. +# +# Documentation: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + # Python dependencies using uv + - package-ecosystem: "uv" + directory: "/" + schedule: + interval: "weekly" + day: "tuesday" + time: "09:00" + timezone: "Asia/Dubai" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "python" + groups: + python-dependencies: + patterns: + - "*" + update-types: + - "patch" + - "minor" + commit-message: + prefix: "chore(deps)" + prefix-development: "chore(deps-dev)" + include: "scope" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "tuesday" + time: "09:00" + timezone: "Asia/Dubai" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "github-actions" + groups: + github-actions: + patterns: + - "*" + update-types: + - "patch" + - "minor" + + commit-message: + prefix: "chore(deps)" + include: "scope" + + # Docker + #- package-ecosystem: "docker" + # directory: "/docker" + # schedule: + # interval: "weekly" + # day: "tuesday" + # time: "09:00" + # timezone: "Asia/Dubai" + # open-pull-requests-limit: 10 + # labels: + # - "dependencies" + # - "docker" + # commit-message: + # prefix: "chore(deps)" + # include: "scope" diff --git a/.github/hooks/hooks.json b/.github/hooks/hooks.json new file mode 100644 index 00000000..6afa9448 --- /dev/null +++ b/.github/hooks/hooks.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "hooks": { + "sessionStart": [ + { + "type": "command", + "bash": ".github/hooks/session-start.sh", + "cwd": ".", + "timeoutSec": 30 + } + ], + "sessionEnd": [ + { + "type": "command", + "bash": ".github/hooks/session-end.sh", + "cwd": ".", + "timeoutSec": 120 + } + ] + } +} diff --git a/.github/hooks/session-end.sh b/.github/hooks/session-end.sh new file mode 100755 index 00000000..14e58557 --- /dev/null +++ b/.github/hooks/session-end.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -euo pipefail + +# Session End Hook +# Runs quality gates after the agent finishes work. + +echo "[copilot-hook] Running post-work quality gates..." + +# Format code +echo "[copilot-hook] Formatting code..." +if ! make fmt; then + echo "[copilot-hook] [ERROR] Formatting check failed" + echo "[copilot-hook] [INFO] Remediation: Review the formatting errors above" + echo "[copilot-hook] [INFO] Common fixes:" + echo "[copilot-hook] - Run 'make fmt' locally to see detailed errors" + echo "[copilot-hook] - Check for syntax errors in modified files" + echo "[copilot-hook] - Ensure all files follow project style guidelines" + exit 1 +fi +echo "[copilot-hook] [OK] Code formatting passed" + +# Run tests +echo "[copilot-hook] Running tests..." +if ! make test; then + echo "[copilot-hook] [ERROR] Tests failed" + echo "[copilot-hook] [INFO] Remediation: Review the test failures above" + echo "[copilot-hook] [INFO] Common fixes:" + echo "[copilot-hook] - Run 'make test' locally to see detailed output" + echo "[copilot-hook] - Check if new code broke existing functionality" + echo "[copilot-hook] - Verify test assertions match expected behavior" + echo "[copilot-hook] - Review test logs in _tests/ directory" + exit 1 +fi +echo "[copilot-hook] [OK] Tests passed" + +echo "[copilot-hook] [OK] All quality gates passed" diff --git a/.github/hooks/session-start.sh b/.github/hooks/session-start.sh new file mode 100755 index 00000000..17e0f730 --- /dev/null +++ b/.github/hooks/session-start.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -euo pipefail + +# Session Start Hook +# Validates that the environment is correctly set up before the agent begins work. +# The virtual environment should already be activated via copilot-setup-steps.yml. + +echo "[copilot-hook] Validating environment..." + +# Verify uv is available +if ! command -v uv >/dev/null 2>&1 && [ ! -x "./bin/uv" ]; then + echo "[copilot-hook] [ERROR] uv not found" + echo "[copilot-hook] [INFO] Remediation: Run 'make install' to set up the environment" + echo "[copilot-hook] [INFO] Alternative: Ensure uv is in PATH or ./bin/uv exists" + exit 1 +fi +echo "[copilot-hook] [OK] uv is available" + +# Verify virtual environment exists +if [ ! -d ".venv" ]; then + echo "[copilot-hook] [ERROR] .venv not found" + echo "[copilot-hook] [INFO] Remediation: Run 'make install' to create the virtual environment" + echo "[copilot-hook] [INFO] Details: The .venv directory should contain Python dependencies" + exit 1 +fi +echo "[copilot-hook] [OK] Virtual environment exists" + +# Verify virtual environment is on PATH (activated via copilot-setup-steps.yml) +if ! command -v python >/dev/null 2>&1 || [[ "$(command -v python)" != *".venv"* ]]; then + echo "[copilot-hook] [WARN] .venv/bin is not on PATH" + echo "[copilot-hook] [INFO] Note: The agent may not use the correct Python version" + echo "[copilot-hook] [INFO] Remediation: Ensure .venv/bin is added to PATH before running the agent" +else + echo "[copilot-hook] [OK] Virtual environment is activated" +fi + +echo "[copilot-hook] [OK] Environment validated successfully" diff --git a/.github/secret_scanning.yml b/.github/secret_scanning.yml new file mode 100644 index 00000000..fdfd809c --- /dev/null +++ b/.github/secret_scanning.yml @@ -0,0 +1,20 @@ +# This file is part of the jebel-quant/rhiza repository +# (https://github.com/jebel-quant/rhiza). +# +# Configuration: GitHub Secret Scanning +# +# Purpose: Configure GitHub secret scanning to identify and prevent accidental +# exposure of secrets, credentials, and tokens in the repository. +# +# Note: Secret scanning must be enabled in repository Settings > +# Security > Code security and analysis > Secret scanning. +# +# Documentation: https://docs.github.com/en/code-security/secret-scanning/configuring-secret-scanning-for-your-repository + +paths-ignore: + # Ignore test fixtures that may contain example/fake secrets + - ".rhiza/tests/**" + - "tests/**" + # Ignore documentation that references example tokens/keys + - "docs/**/*.md" + - "book/**" diff --git a/.github/semgrep.yml b/.github/semgrep.yml new file mode 100644 index 00000000..a5cc97c6 --- /dev/null +++ b/.github/semgrep.yml @@ -0,0 +1,84 @@ +rules: + # ---------------------------- + # Security + # ---------------------------- + + - id: numpy-load-allow-pickle + patterns: + - pattern: np.load($PATH, ...) + - pattern-not: np.load($PATH, ..., allow_pickle=False, ...) + message: | + np.load() may deserialize arbitrary objects when allow_pickle=True + (default in older NumPy versions). This can execute malicious code. + Pass allow_pickle=False unless explicitly required. + languages: [python] + severity: ERROR + metadata: + category: security + cwe: "CWE-502: Deserialization of Untrusted Data" + + # ---------------------------- + # Randomness / Reproducibility + # ---------------------------- + + - id: numpy-random-seed + pattern: np.random.seed(...) + message: | + np.random.seed() sets global state and is not thread-safe. + Prefer a local Generator via np.random.default_rng(). + languages: [python] + severity: WARNING + metadata: + category: best-practice + + - id: numpy-global-random-state + patterns: + - pattern-either: + - pattern: np.random.rand(...) + - pattern: np.random.randn(...) + - pattern: np.random.randint(...) + - pattern: np.random.random(...) + - pattern: np.random.normal(...) + - pattern: np.random.uniform(...) + - pattern: np.random.choice(...) + - pattern: np.random.shuffle(...) + message: | + Global np.random.* usage is not thread-safe and can harm reproducibility. + Prefer a local Generator: + rng = np.random.default_rng(seed) + rng.method(...) + languages: [python] + severity: INFO + metadata: + category: best-practice + + # ---------------------------- + # Numerical Stability + # ---------------------------- + + - id: numpy-avoid-inv + pattern: np.linalg.inv($X) + message: | + Avoid explicit matrix inversion. Use np.linalg.solve(A, b) + for better numerical stability and performance. + languages: [python] + severity: WARNING + metadata: + category: numerical-stability + + # ---------------------------- + # Deprecated / Dangerous APIs + # ---------------------------- + + - id: numpy-matrix-deprecated + patterns: + - pattern-either: + - pattern: np.matrix(...) + - pattern: np.mat(...) + message: | + np.matrix is deprecated and scheduled for removal. Use np.array + or np.ndarray instead. It has different operator semantics (*). + languages: [python] + severity: WARNING + metadata: + category: maintainability diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000..c54de383 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,50 @@ +# This file is part of the jebel-quant/rhiza repository +# (https://github.com/jebel-quant/rhiza). +# +# Workflow: Copilot Setup Steps +# +# Purpose: Preconfigure the development environment before the Copilot +# coding agent begins working. This ensures the agent always +# has a deterministic, fully working environment. +# +# Reference: https://docs.github.com/en/copilot/customizing-copilot/customizing-the-development-environment-for-copilot-coding-agent + +name: "(RHIZA) AGENT SETUP" + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + copilot-setup-steps: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v6.0.2 + with: + lfs: true + + - name: Install uv + uses: astral-sh/setup-uv@v8.0.0 + with: + version: "0.11.6" + + - name: Configure git auth for private packages + uses: ./.github/actions/configure-git-auth + with: + token: ${{ secrets.GH_PAT }} + + - name: Install dependencies + env: + UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} + run: make install + + - name: Activate virtual environment + run: echo "${{ github.workspace }}/.venv/bin" >> "$GITHUB_PATH" diff --git a/.github/workflows/rhiza_book.yml b/.github/workflows/rhiza_book.yml index f8c85942..2d7612d4 100644 --- a/.github/workflows/rhiza_book.yml +++ b/.github/workflows/rhiza_book.yml @@ -36,33 +36,48 @@ jobs: steps: # Check out the repository code - - uses: actions/checkout@v6 + - uses: actions/checkout@v6.0.2 with: lfs: true - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@v8.0.0 with: - version: "0.9.27" + version: "0.11.6" + + - name: Configure git auth for private packages + uses: ./.github/actions/configure-git-auth + with: + token: ${{ secrets.GH_PAT }} - name: "Sync the virtual environment for ${{ github.repository }}" shell: bash env: - UV_EXTRA_INDEX_URL: ${{ secrets.uv-extra-index-url }} + UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} run: | # will just use .python-version? uv sync --all-extras --all-groups --frozen - name: "Make the book" env: - UV_EXTRA_INDEX_URL: ${{ secrets.uv-extra-index-url }} + UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} run: | - make -f .rhiza/rhiza.mk book + make book + + # Step 5a: Upload the book as a downloadable workflow artifact + # This allows anyone to download the built documentation directly from + # GitHub Actions without needing to run a full local build. + - name: Upload book as workflow artifact + uses: actions/upload-artifact@v7.0.1 + with: + name: book + path: _book/ + retention-days: 30 - # Step 5: Package all artifacts for GitHub Pages deployment + # Step 5b: Package all artifacts for GitHub Pages deployment # This prepares the combined outputs for deployment by creating a single artifact - name: Upload static files as artifact - uses: actions/upload-pages-artifact@v4 # Official GitHub Pages artifact upload action + uses: actions/upload-pages-artifact@v5.0.0 # Official GitHub Pages artifact upload action with: path: _book/ # Path to the directory containing all artifacts to deploy @@ -73,5 +88,5 @@ jobs: # If PUBLISH_COMPANION_BOOK is not set, it defaults to allowing deployment - name: Deploy to GitHub Pages if: ${{ !github.event.repository.fork && (vars.PUBLISH_COMPANION_BOOK == 'true' || vars.PUBLISH_COMPANION_BOOK == '') }} - uses: actions/deploy-pages@v4 # Official GitHub Pages deployment action + uses: actions/deploy-pages@v5.0.0 # Official GitHub Pages deployment action continue-on-error: true diff --git a/.github/workflows/rhiza_codeql.yml b/.github/workflows/rhiza_codeql.yml deleted file mode 100644 index aca6b23b..00000000 --- a/.github/workflows/rhiza_codeql.yml +++ /dev/null @@ -1,125 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -# ******** IMPORTANT: GitHub Advanced Security Required ******** -# CodeQL is FREE for public repositories, but requires GitHub Advanced Security -# (part of GitHub Enterprise) for private repositories. -# -# This workflow automatically: -# - Runs on public repositories -# - Skips on private repositories (unless Advanced Security is available) -# -# To control this behavior, set the CODEQL_ENABLED repository variable: -# - Set to 'true' to force enable (if you have Advanced Security on private repos) -# - Set to 'false' to disable entirely -# - Leave unset for automatic behavior (recommended) -# -# For more information, see docs/CUSTOMIZATION.md -# -name: "(RHIZA) CODEQL" - -on: - push: - branches: [ "main", "master" ] - pull_request: - branches: [ "main", "master" ] - schedule: - - cron: '27 1 * * 1' - -jobs: - analyze: - name: Analyze (${{ matrix.language }}) - # Runner size impacts CodeQL analysis time. To learn more, please see: - # - https://gh.io/recommended-hardware-resources-for-running-codeql - # - https://gh.io/supported-runners-and-hardware-resources - # - https://gh.io/using-larger-runners (GitHub.com only) - # Consider using larger runners or machines with greater resources for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - # CodeQL requires GitHub Advanced Security (part of GitHub Enterprise). - # For users without Enterprise license: - # - Public repositories: CodeQL is available for free - # - Private repositories: Requires GitHub Advanced Security - # To disable this workflow, set CODEQL_ENABLED repository variable to 'false' - # To enable this workflow for private repos with Advanced Security, set CODEQL_ENABLED to 'true' - if: | - vars.CODEQL_ENABLED == 'true' || - (vars.CODEQL_ENABLED != 'false' && github.event.repository.visibility == 'public') - permissions: - # required for all workflows - security-events: write - - # required to fetch internal or private CodeQL packs - packages: read - - # only required for workflows in private repositories - actions: read - contents: read - - strategy: - fail-fast: false - matrix: - include: - - language: actions - build-mode: none - - language: python - build-mode: none - # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' - # Use `c-cpp` to analyze code written in C, C++ or both - # Use 'java-kotlin' to analyze code written in Java, Kotlin or both - # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both - # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, - # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. - # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how - # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - # Add any setup steps before running the `github/codeql-action/init` action. - # This includes steps like installing compilers or runtimes (`actions/setup-node` - # or others). This is typically only required for manual builds. - # - name: Setup runtime (example) - # uses: actions/setup-example@v1 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - name: Run manual build steps - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/rhiza_deptry.yml b/.github/workflows/rhiza_deptry.yml deleted file mode 100644 index eefc2223..00000000 --- a/.github/workflows/rhiza_deptry.yml +++ /dev/null @@ -1,40 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Workflow: Deptry -# -# Purpose: This workflow identifies missing and obsolete dependencies in the project. -# It helps maintain a clean dependency tree by detecting unused packages and -# implicit dependencies that should be explicitly declared. -# -# Trigger: This workflow runs on every push and on pull requests to main/master -# branches (including from forks) - -name: "(RHIZA) DEPTRY" - -# Permissions: Only read access to repository contents is needed -permissions: - contents: read - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - -jobs: - deptry: - name: Check dependencies with deptry - runs-on: ubuntu-latest - container: - image: ghcr.io/astral-sh/uv:0.9.27-python3.12-trixie - - steps: - - uses: actions/checkout@v6 - - - name: Run deptry - run: make -f .rhiza/rhiza.mk deptry - # NOTE: make deptry is good style because it encapsulates the folders to check - # (e.g. src and book/marimo) and keeps CI in sync with local development. - # Since we use a 'uv' container, the Makefile is optimised to use the - # pre-installed 'uv' and 'uvx' from the system PATH. diff --git a/.github/workflows/rhiza_marimo.yml b/.github/workflows/rhiza_marimo.yml index 67dfda97..cf1677e8 100644 --- a/.github/workflows/rhiza_marimo.yml +++ b/.github/workflows/rhiza_marimo.yml @@ -34,7 +34,7 @@ jobs: notebook-list: ${{ steps.notebooks.outputs.matrix }} steps: # Check out the repository code - - uses: actions/checkout@v6 + - uses: actions/checkout@v6.0.2 # Find all Python files in the marimo folder and create a matrix for parallel execution - name: Find notebooks and build matrix @@ -75,20 +75,34 @@ jobs: name: Run notebook ${{ matrix.notebook }} steps: # Check out the repository code - - uses: actions/checkout@v6 + - uses: actions/checkout@v6.0.2 with: lfs: true # Install uv/uvx - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@v8.0.0 with: - version: "0.9.27" + version: "0.11.6" + + - name: Configure git auth for private packages + uses: ./.github/actions/configure-git-auth + with: + token: ${{ secrets.GH_PAT }} # Execute the notebook with the appropriate runner based on its content - name: Run notebook + id: artefact-folder + env: + UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} run: | - uvx uv run "${{ matrix.notebook }}" + notebook="${{ matrix.notebook }}" + notebook_stem="$(basename "$notebook" .py)" + artefact_folder="results/${notebook_stem}" + mkdir -p "${artefact_folder}" + echo "name=${notebook_stem}" >> "$GITHUB_OUTPUT" + export NOTEBOOK_OUTPUT_FOLDER="${artefact_folder}" + uvx uv run "$notebook" # uvx → creates a fresh ephemeral environment # uv run → runs the notebook as a script in that ephemeral env # No project packages are pre-installed @@ -99,3 +113,11 @@ jobs: # Catches missing uv install or pip steps early # Ensures CI/other users can run the notebook without manual setup shell: bash + + - name: Upload notebook artefacts + if: always() + uses: actions/upload-artifact@v7.0.1 + with: + name: notebook-artefacts-${{ steps.artefact-folder.outputs.name }} + path: results/${{ steps.artefact-folder.outputs.name }}/ + if-no-files-found: ignore diff --git a/.github/workflows/rhiza_mypy.yml b/.github/workflows/rhiza_mypy.yml deleted file mode 100644 index 011e558d..00000000 --- a/.github/workflows/rhiza_mypy.yml +++ /dev/null @@ -1,34 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Workflow: Mypy -# -# Purpose: Run static type checking with mypy in strict mode to ensure -# type safety across the codebase. -# -# Trigger: On push and pull requests to main/master branches. - -name: "(RHIZA) MYPY" - -permissions: - contents: read - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - -jobs: - mypy: - name: Static type checking with mypy - runs-on: ubuntu-latest - container: - image: ghcr.io/astral-sh/uv:0.9.27-python3.12-trixie - - steps: - - uses: actions/checkout@v6 - - # to brutal for now - # - name: Run mypy - # run: make -f .rhiza/rhiza.mk mypy diff --git a/.github/workflows/rhiza_pre-commit.yml b/.github/workflows/rhiza_pre-commit.yml deleted file mode 100644 index 4190d4d9..00000000 --- a/.github/workflows/rhiza_pre-commit.yml +++ /dev/null @@ -1,37 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Workflow: Pre-commit -# -# Purpose: This workflow runs pre-commit checks to ensure code quality -# and consistency across the codebase. It helps catch issues -# like formatting errors, linting issues, and other code quality -# problems before they are merged. -# -# Trigger: This workflow runs on every push and on pull requests to main/master -# branches (including from forks) -# -# Components: -# - 🔍 Run pre-commit checks using reusable action - -name: "(RHIZA) PRE-COMMIT" -permissions: - contents: read - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - -jobs: - pre-commit: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - - # Run pre-commit - - name: Run pre-commit - run: | - make -f .rhiza/rhiza.mk fmt diff --git a/.github/workflows/rhiza_release.yml b/.github/workflows/rhiza_release.yml index 7b1abf73..3146205c 100644 --- a/.github/workflows/rhiza_release.yml +++ b/.github/workflows/rhiza_release.yml @@ -7,11 +7,19 @@ # # 📋 Pipeline Phases: # 1. 🔍 Validate Tag - Check tag format and ensure release doesn't already exist -# 2. 🏗️ Build - Build Python package with Hatch (if [build-system] is defined in pyproject.toml -# 3. 📝 Draft Release - Create draft GitHub release with build artifacts -# 4. 🚀 Publish to PyPI - Publish package using OIDC or custom feed -# 5. 🐳 Publish Devcontainer - Build and publish devcontainer image (conditional) -# 6. ✅ Finalize Release - Publish the GitHub release with links +# 2. 🏗️ Build - Build Python package with Hatch (if [build-system] is defined in pyproject.toml) +# 3. 📦 Generate SBOM - Create Software Bill of Materials (CycloneDX format) +# 4. 📝 Draft Release - Create draft GitHub release with build artifacts and SBOM +# 5. 🚀 Publish to PyPI - Publish package using OIDC or custom feed +# 6. 🐳 Publish Devcontainer - Build and publish devcontainer image (conditional) +# 7. ✅ Finalize Release - Publish the GitHub release with links +# +# 📦 SBOM Generation: +# - Generated using CycloneDX format (industry standard for software supply chain security) +# - Creates both JSON and XML formats for maximum compatibility +# - SBOM attestations are created and stored (public repos only) +# - Attached to GitHub releases for transparency and compliance +# - Skipped if pyproject.toml doesn't exist # # 🐳 Devcontainer Publishing: # - Only occurs when PUBLISH_DEVCONTAINER repository variable is set to "true" @@ -33,7 +41,8 @@ # - No PyPI credentials stored; relies on Trusted Publishing via GitHub OIDC # - For custom feeds, PYPI_TOKEN secret is used with default username __token__ # - Container registry uses GITHUB_TOKEN for authentication -# - SLSA provenance attestations generated for build artifacts (supply chain security) +# - SLSA provenance attestations generated for build artifacts (public repos only) +# - SBOM attestations generated for supply chain transparency (public repos only) # # 📄 Requirements: # - pyproject.toml with top-level version field (for Python packages) @@ -65,7 +74,7 @@ permissions: contents: write # Needed to create releases id-token: write # Needed for OIDC authentication with PyPI packages: write # Needed to publish devcontainer image - attestations: write # Needed for SLSA provenance attestations + attestations: write # Needed for SLSA provenance attestations (public repos only) jobs: tag: @@ -75,7 +84,7 @@ jobs: tag: ${{ steps.set_tag.outputs.tag }} steps: - name: Checkout Code - uses: actions/checkout@v6 + uses: actions/checkout@v6.0.2 with: fetch-depth: 0 @@ -106,21 +115,19 @@ jobs: needs: tag steps: - name: Checkout Code - uses: actions/checkout@v6 + uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@v8.0.0 with: - version: "0.9.27" + version: "0.11.6" - - name: "Sync the virtual environment for ${{ github.repository }}" - shell: bash - run: | - export UV_EXTRA_INDEX_URL="${{ secrets.uv-extra-index-url }}" - # will just use .python-version? - uv sync --all-extras --all-groups --frozen + - name: Configure git auth for private packages + uses: ./.github/actions/configure-git-auth + with: + token: ${{ secrets.GH_PAT }} - name: Verify version matches tag if: hashFiles('pyproject.toml') != '' @@ -129,11 +136,16 @@ jobs: TAG_VERSION=${TAG_VERSION#v} PROJECT_VERSION=$(uv version --short) - if [[ "$PROJECT_VERSION" != "$TAG_VERSION" ]]; then - echo "::error::Version mismatch: pyproject.toml has '$PROJECT_VERSION' but tag is '$TAG_VERSION'" + # Normalize tag version to PEP 440 format for comparison. + # Tags use semver format (e.g., 0.11.1-beta.1) while uv version --short + # returns PEP 440 normalized format (e.g., 0.11.1b1). + NORMALIZED_TAG=$(uv run --with packaging --no-project python3 -c "from packaging.version import Version; print(Version('$TAG_VERSION'))") + + if [[ "$PROJECT_VERSION" != "$NORMALIZED_TAG" ]]; then + echo "::error::Version mismatch: pyproject.toml has '$PROJECT_VERSION' but tag is '$NORMALIZED_TAG' (from tag '$TAG_VERSION')" exit 1 fi - echo "Version verified: $PROJECT_VERSION matches tag" + echo "Version verified: $PROJECT_VERSION matches tag (normalized: $NORMALIZED_TAG)" - name: Detect buildable Python package id: buildable @@ -148,17 +160,58 @@ jobs: if: steps.buildable.outputs.buildable == 'true' run: | printf "[INFO] Building package...\n" - uvx hatch build + uv build + + - name: Install Python for SBOM generation + if: hashFiles('pyproject.toml') != '' + uses: actions/setup-python@v6.2.0 + with: + python-version-file: .python-version + + - name: Sync environment for SBOM generation + if: hashFiles('pyproject.toml') != '' + run: | + export UV_EXTRA_INDEX_URL="${{ secrets.UV_EXTRA_INDEX_URL }}" + uv sync --all-extras --all-groups --frozen + + - name: Generate SBOM (CycloneDX) + if: hashFiles('pyproject.toml') != '' + run: | + printf "[INFO] Generating SBOM in CycloneDX format...\n" + # Note: uvx caches the tool environment, so the second call is fast + uvx --from 'cyclonedx-bom>=7.0.0' cyclonedx-py environment --pyproject pyproject.toml --of JSON -o sbom.cdx.json + uvx --from 'cyclonedx-bom>=7.0.0' cyclonedx-py environment --pyproject pyproject.toml --of XML -o sbom.cdx.xml + printf "[INFO] SBOM generation complete\n" + printf "Generated files:\n" + ls -lh sbom.cdx.* + + - name: Attest SBOM + # Attest only the JSON format as it's the canonical machine-readable format. + # The XML format is provided for compatibility but doesn't need separate attestation. + if: hashFiles('pyproject.toml') != '' && github.event.repository.private == false + uses: actions/attest@v4.1.0 + with: + subject-path: sbom.cdx.json + sbom-path: sbom.cdx.json + + - name: Upload SBOM artifacts + if: hashFiles('pyproject.toml') != '' + uses: actions/upload-artifact@v7.0.1 + with: + name: sbom + path: | + sbom.cdx.json + sbom.cdx.xml - name: Generate SLSA provenance attestations - if: steps.buildable.outputs.buildable == 'true' - uses: actions/attest-build-provenance@v3 + if: steps.buildable.outputs.buildable == 'true' && github.event.repository.private == false + uses: actions/attest-build-provenance@v4 with: subject-path: dist/* - name: Upload dist artifact if: steps.buildable.outputs.buildable == 'true' - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7.0.1 with: name: dist path: dist @@ -170,13 +223,34 @@ jobs: needs: [tag, build] steps: + - name: Checkout Code + uses: actions/checkout@v6.0.2 + with: + fetch-depth: 0 + + - name: Install uv + uses: astral-sh/setup-uv@v8.0.0 + + - name: Generate release notes with git-cliff + run: uvx git-cliff --latest --output RELEASE_NOTES.md + + - name: Download SBOM artifact + # Downloads sbom.cdx.json and sbom.cdx.xml into sbom/ directory + uses: actions/download-artifact@v8.0.1 + with: + name: sbom + path: sbom + continue-on-error: true + - name: Create GitHub Release with artifacts - uses: softprops/action-gh-release@v2.5.0 + uses: ncipollo/release-action@v1.21.0 with: - tag_name: ${{ needs.tag.outputs.tag }} + tag: ${{ needs.tag.outputs.tag }} name: ${{ needs.tag.outputs.tag }} - generate_release_notes: true + bodyFile: RELEASE_NOTES.md draft: true + allowUpdates: true + artifacts: "sbom/*" # Decide at step-level whether to publish pypi: @@ -189,12 +263,12 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v6 + uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Download dist artifact - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8.0.1 with: name: dist path: dist @@ -237,7 +311,7 @@ jobs: image_name: ${{ steps.image_name.outputs.image_name }} steps: - name: Checkout Code - uses: actions/checkout@v6 + uses: actions/checkout@v6.0.2 with: fetch-depth: 0 @@ -266,7 +340,7 @@ jobs: - name: Login to Container Registry if: steps.check_publish.outputs.should_publish == 'true' - uses: docker/login-action@v3 + uses: docker/login-action@v4.1.0 with: registry: ${{ steps.registry.outputs.registry }} username: ${{ github.repository_owner }} @@ -322,24 +396,24 @@ jobs: if: needs.pypi.result == 'success' || needs.devcontainer.result == 'success' steps: - name: Checkout Code - uses: actions/checkout@v6 + uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@v8.0.0 with: - version: "0.9.27" + version: "0.11.6" - name: "Sync the virtual environment for ${{ github.repository }}" shell: bash run: | - export UV_EXTRA_INDEX_URL="${{ secrets.uv-extra-index-url }}" + export UV_EXTRA_INDEX_URL="${{ secrets.uv_extra_index_url }}" # will just use .python-version? uv sync --all-extras --all-groups --frozen - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v6.2.0 - name: Generate Devcontainer Link id: devcontainer_link @@ -358,7 +432,7 @@ jobs: id: pypi_link if: needs.pypi.outputs.should_publish == 'true' && needs.pypi.result == 'success' run: | - PACKAGE_NAME=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['name'])") + PACKAGE_NAME=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['name'])") VERSION="${{ needs.tag.outputs.tag }}" VERSION=${VERSION#v} @@ -380,12 +454,21 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Publish Release - uses: softprops/action-gh-release@v2.5.0 - with: - tag_name: ${{ needs.tag.outputs.tag }} - draft: false - append_body: true - body: | - ${{ steps.devcontainer_link.outputs.message }} - ${{ steps.pypi_link.outputs.message }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ needs.tag.outputs.tag }} + DEVCONTAINER_MSG: ${{ steps.devcontainer_link.outputs.message }} + PYPI_MSG: ${{ steps.pypi_link.outputs.message }} + run: | + # Get existing auto-generated release notes (gracefully handle missing release) + EXISTING=$(gh release view "$TAG" --json body --jq '.body // ""' 2>/dev/null || echo "") + + # Append links to release body + { + printf '%s' "$EXISTING" + [ -n "$DEVCONTAINER_MSG" ] && printf '\n\n%s' "$DEVCONTAINER_MSG" + [ -n "$PYPI_MSG" ] && printf '\n\n%s' "$PYPI_MSG" + } > /tmp/notes.md + + gh release edit "$TAG" --draft=false --notes-file /tmp/notes.md diff --git a/.github/workflows/rhiza_sync.yml b/.github/workflows/rhiza_sync.yml index ea218eaf..2da8d0ed 100644 --- a/.github/workflows/rhiza_sync.yml +++ b/.github/workflows/rhiza_sync.yml @@ -1,32 +1,112 @@ name: (RHIZA) SYNC -# This workflow synchronizes the repository with its template. -# IMPORTANT: When workflow files (.github/workflows/rhiza_*.yml) are modified, -# a Personal Access Token (PAT) with 'workflow' scope is required. -# The PAT_TOKEN secret must be set in repository secrets. -# See .github/rhiza/TOKEN_SETUP.md for setup instructions. +# Synchronizes the repository with its rhiza template. +# - On Renovate/rhiza branch push: auto-commits synced files directly to the branch. +# - On schedule/dispatch: opens a pull request with the synced changes. +# +# IMPORTANT: A PAT with 'workflow' scope (PAT_TOKEN) is required when workflow +# files are modified. See .rhiza/docs/TOKEN_SETUP.md for setup instructions. permissions: contents: write pull-requests: write on: + push: + branches: + - 'renovate/jebel-quant-rhiza-**' + - 'rhiza/**' + paths: + - '.rhiza/template.yml' + schedule: + - cron: '0 0 * * 1' # Weekly on Monday workflow_dispatch: inputs: create-pr: description: "Create a pull request" type: boolean default: true - schedule: - - cron: '0 0 * * 1' # Weekly on Monday jobs: - sync: - if: ${{ github.repository != 'jebel-quant/rhiza' }} + sync-direct: + name: Sync and commit (Renovate) + if: github.event_name == 'push' && github.repository != 'jebel-quant/rhiza' runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v6.0.2 + with: + ref: ${{ github.ref }} + token: ${{ secrets.PAT_TOKEN || github.token }} + + - name: Check PAT_TOKEN configuration + shell: bash + env: + PAT_TOKEN: ${{ secrets.PAT_TOKEN }} + run: | + if [ -z "$PAT_TOKEN" ]; then + echo "::warning::PAT_TOKEN secret is not configured." + echo "::warning::If this sync modifies workflow files, the push will fail." + echo "::warning::See .rhiza/docs/TOKEN_SETUP.md for setup instructions." + else + echo "✓ PAT_TOKEN is configured." + fi + + - name: Install uv + uses: astral-sh/setup-uv@v8.0.0 + + - name: Get Rhiza version + id: rhiza-version + run: | + VERSION=$(cat .rhiza/.rhiza-version 2>/dev/null || echo "0.9.0") + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + + - name: Sync rhiza template + id: sync + run: | + set -euo pipefail + + RHIZA_VERSION="${{ steps.rhiza-version.outputs.version }}" + + echo "Running rhiza sync with version >=${RHIZA_VERSION}" + uvx "rhiza>=${RHIZA_VERSION}" sync . + + if git diff --quiet; then + echo "No changes detected after template sync" + echo "changes=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "Template changes detected" + echo "changes=true" >> "$GITHUB_OUTPUT" + + - name: Commit and push changes + if: steps.sync.outputs.changes == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --global url."https://x-access-token:${{ secrets.PAT_TOKEN || github.token }}@github.com/".insteadOf "https://github.com/" + + git add -A + git commit -m "$(cat <<'EOF' + chore: sync rhiza template files + + Automatically synced template files after updating .rhiza/template.yml + + Co-Authored-By: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> + EOF + )" + + git push + + sync-pr: + name: Sync and open PR (scheduled/manual) + if: github.event_name != 'push' && github.repository != 'jebel-quant/rhiza' + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.2 with: token: ${{ secrets.PAT_TOKEN || github.token }} fetch-depth: 0 @@ -44,13 +124,13 @@ jobs: if [ -z "$PAT_TOKEN" ]; then echo "::warning::PAT_TOKEN secret is not configured." echo "::warning::If this sync modifies workflow files, the push will fail." - echo "::warning::See .github/TOKEN_SETUP.md for setup instructions." + echo "::warning::See .rhiza/docs/TOKEN_SETUP.md for setup instructions." else echo "✓ PAT_TOKEN is configured." fi - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@v8.0.0 - name: Get Rhiza version id: rhiza-version @@ -66,7 +146,7 @@ jobs: RHIZA_VERSION="${{ steps.rhiza-version.outputs.version }}" - uvx "rhiza>=${RHIZA_VERSION}" materialize --force . + uvx "rhiza>=${RHIZA_VERSION}" sync . git add -A @@ -82,19 +162,19 @@ jobs: git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config --global url."https://x-access-token:${{ secrets.PAT_TOKEN }}@github.com/".insteadOf "https://github.com/" + git config --global url."https://x-access-token:${{ secrets.PAT_TOKEN || github.token }}@github.com/".insteadOf "https://github.com/" git commit -m "chore: Update via rhiza" - + - name: Create pull request if: > (github.event_name == 'schedule' || inputs.create-pr == true) && steps.sync.outputs.changes_detected == 'true' - uses: peter-evans/create-pull-request@v8 + uses: peter-evans/create-pull-request@v8.1.1 with: token: ${{ secrets.PAT_TOKEN || github.token }} base: ${{ github.event.repository.default_branch }} branch: ${{ steps.branch.outputs.name }} delete-branch: true title: "chore: Sync with rhiza" - body-path: ${{ runner.temp }}/pr-description.md \ No newline at end of file + body-path: ${{ runner.temp }}/pr-description.md diff --git a/.github/workflows/rhiza_validate.yml b/.github/workflows/rhiza_validate.yml deleted file mode 100644 index d6f4640c..00000000 --- a/.github/workflows/rhiza_validate.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: (RHIZA) VALIDATE - -permissions: - contents: read - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - -jobs: - validation: - runs-on: ubuntu-latest - # don't run this in rhiza itself. Rhiza has no template.yml file. - if: ${{ github.repository != 'jebel-quant/rhiza' }} - container: - image: ghcr.io/astral-sh/uv:0.9.27-python3.12-trixie - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Validate Rhiza config - shell: bash - run: | - uvx "rhiza>=0.8.0" validate . diff --git a/.github/workflows/rhiza_weekly.yml b/.github/workflows/rhiza_weekly.yml new file mode 100644 index 00000000..589dc9c5 --- /dev/null +++ b/.github/workflows/rhiza_weekly.yml @@ -0,0 +1,118 @@ +name: "(RHIZA) WEEKLY" + +# Runs weekly checks that are too slow or noisy for every push: +# +# dep-compat-test — Resolves all dependencies fresh (ignoring the lockfile) +# and runs the full test suite. Catches newly-released +# packages that break compatibility before Renovate picks +# them up. Runs on schedule/dispatch only. +# +# link-check — Verifies that all hyperlinks in README.md are reachable. +# Runs on schedule/dispatch only. + +permissions: + contents: read + +on: + #push: + # branches: [main] + # paths: [README.md] + #pull_request: + # paths: [README.md] + schedule: + - cron: "0 8 * * 1" # Every Monday at 08:00 UTC + workflow_dispatch: + +jobs: + dep-compat-test: + name: Test with latest compatible dependencies + runs-on: ubuntu-latest + #if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.2 + with: + lfs: true + + - name: Install uv + uses: astral-sh/setup-uv@v8.0.0 + with: + version: "0.11.6" + + - name: Configure git auth for private packages + uses: ./.github/actions/configure-git-auth + with: + token: ${{ secrets.GH_PAT }} + + - name: Resolve and install latest compatible dependencies + env: + UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} + run: | + # --upgrade ignores the committed lockfile and resolves the newest + # versions that satisfy pyproject.toml constraints. + uv sync --upgrade + + - name: Show resolved package versions + run: | + echo "=== Installed package versions ===" + uv pip list + + - name: Run tests + env: + UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} + run: make test + + semgrep: + name: Semgrep (numpy) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6.0.2 + + - name: Install uv + uses: astral-sh/setup-uv@v8.0.0 + with: + version: "0.11.6" + + - name: Configure git auth for private packages + uses: ./.github/actions/configure-git-auth + with: + token: ${{ secrets.GH_PAT }} + + - name: Run Semgrep + env: + UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} + run: make semgrep + + pip-audit: + name: Dependency vulnerability scan + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6.0.2 + + - name: Install uv + uses: astral-sh/setup-uv@v8.0.0 + with: + version: "0.11.6" + + - name: Run pip-audit + run: uvx pip-audit + + link-check: + name: Check links in README.md + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6.0.2 + + - name: Check links in README.md + uses: lycheeverse/lychee-action@v2 + with: + args: >- + --verbose + --no-progress + --accept 200,206,429 + README.md + fail: true diff --git a/.gitignore b/.gitignore index c0c30e2e..0dc346a2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,10 @@ .idea .venv .ruff_cache +.ty_cache + +# HTML outputs from docstring examples (e.g. report.save("output/...")) +output/ ### Don't expose API keys, etc. .env @@ -11,9 +15,23 @@ __marimo__ _tests _book _pdoc +docs/notebooks/*.html +docs/reports +docs/notebooks.md +docs/reports.md _marimushka +_mkdocs _benchmarks _jupyter +_site + +# LaTeX build artifacts +docs/paper/*.aux +docs/paper/*.fdb_latexmk +docs/paper/*.fls +docs/paper/*.log +docs/paper/*.out +docs/paper/*.toc # temp file used by Junie .output.txt @@ -74,9 +92,13 @@ coverage.json *.cover *.py,cover .hypothesis/ +.benchmarks/ .pytest_cache/ cover/ +# Security scanning baselines (regenerate as needed) +.bandit-baseline.json + # Translations *.mo *.pot @@ -97,3 +119,5 @@ cython_debug/ # Makefile local.mk +.bandit-baseline.json + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 07bb360e..774f3124 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,9 +7,11 @@ repos: hooks: - id: check-toml - id: check-yaml + args: ['--unsafe'] + exclude: ^recipe/meta\.yaml$ - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.14.14' + rev: 'v0.15.10' hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix, --unsafe-fixes ] @@ -18,13 +20,13 @@ repos: - id: ruff-format - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.47.0 + rev: v0.48.0 hooks: - id: markdownlint args: ["--disable", "MD013"] - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.36.1 + rev: 0.37.1 hooks: - id: check-renovate args: [ "--verbose" ] @@ -33,33 +35,34 @@ repos: args: ["--verbose"] - repo: https://github.com/rhysd/actionlint - rev: v1.7.10 + rev: v1.7.12 hooks: - id: actionlint - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.24.1 + rev: v0.25 hooks: - id: validate-pyproject - repo: https://github.com/PyCQA/bandit - rev: 1.9.3 + rev: 1.9.4 hooks: - id: bandit - args: ["--skip", "B101", "--exclude", ".venv,tests,.git,.pytest_cache"] + args: ["--skip", "B101", "--exclude", ".venv,tests,.rhiza/tests,.git,.pytest_cache", "-c", "pyproject.toml"] - - repo: local + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.11.6 hooks: - - id: update-readme-help - name: Update README with Makefile help output - entry: make readme - language: system - files: '^Makefile$' - pass_filenames: false + - id: uv-lock + - repo: https://github.com/Jebel-Quant/rhiza-hooks + rev: v0.3.2 # Use the latest release + hooks: + # Migrated from rhiza - id: check-rhiza-workflow-names - name: Check and Fix Rhiza workflow names - entry: python .rhiza/scripts/check_workflow_names.py - language: python - additional_dependencies: [PyYAML] - files: ^\.github/workflows/rhiza_.*\.ya?ml$ + - id: update-readme-help + # Additional utility hooks + - id: check-rhiza-config + - id: check-makefile-targets + - id: check-python-version-consistency + # - id: check-template-bundles diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..fdcfcfdf --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 \ No newline at end of file diff --git a/.rhiza/.cfg.toml b/.rhiza/.cfg.toml index 65660d1d..c96b1dd1 100644 --- a/.rhiza/.cfg.toml +++ b/.rhiza/.cfg.toml @@ -1,12 +1,12 @@ [tool.bumpversion] -parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(?:-(?P[a-z]+)\\.(?P\\d+))?(?:\\+build\\.(?P\\d+))?" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(?:[-]?(?P[a-z]+)[\\.]?(?P\\d+))?(?:\\+build\\.(?P\\d+))?" serialize = ["{major}.{minor}.{patch}-{release}.{pre_n}+build.{build_n}", "{major}.{minor}.{patch}+build.{build_n}", "{major}.{minor}.{patch}-{release}.{pre_n}", "{major}.{minor}.{patch}"] search = "{current_version}" replace = "{new_version}" regex = false ignore_missing_version = false ignore_missing_files = false -tag = false +tag = true sign_tags = false tag_name = "v{new_version}" tag_message = "Bump version: {current_version} → {new_version}" @@ -14,13 +14,16 @@ allow_dirty = false commit = true message = "Chore: bump version {current_version} → {new_version}" commit_args = "" +pre_commit_hooks = ["uv sync", "git add uv.lock"] # Ensure uv.lock is updated [tool.bumpversion.parts.release] optional_value = "prod" values = [ "dev", "alpha", + "a", # PEP 440 short form for alpha "beta", + "b", # PEP 440 short form for beta "rc", "prod" ] @@ -29,8 +32,3 @@ values = [ filename = "pyproject.toml" search = 'version = "{current_version}"' replace = 'version = "{new_version}"' - -[[tool.bumpversion.files]] -filename = "uv.lock" -search = 'version = "{current_version}"' -replace = 'version = "{new_version}"' \ No newline at end of file diff --git a/.rhiza/.env b/.rhiza/.env index 47753265..47ca6b05 100644 --- a/.rhiza/.env +++ b/.rhiza/.env @@ -1,13 +1,2 @@ -MARIMO_FOLDER=book/marimo/notebooks +MARIMO_FOLDER=docs/notebooks SOURCE_FOLDER=src -SCRIPTS_FOLDER=.rhiza/scripts - -# Book-specific variables -BOOK_TITLE=Project Documentation -BOOK_SUBTITLE=Generated by minibook -PDOC_TEMPLATE_DIR=book/pdoc-templates -BOOK_TEMPLATE=book/minibook-templates/custom.html.jinja2 -DOCFORMAT=google - -# Agentic-specific variables -DEFAULT_AI_MODEL=gpt-4.1 diff --git a/.rhiza/assets/rhiza-logo.svg b/.rhiza/assets/rhiza-logo.svg new file mode 100644 index 00000000..ff1c9f57 --- /dev/null +++ b/.rhiza/assets/rhiza-logo.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.rhiza/make.d/01-custom-env.mk b/.rhiza/make.d/01-custom-env.mk deleted file mode 100644 index 69c34c6f..00000000 --- a/.rhiza/make.d/01-custom-env.mk +++ /dev/null @@ -1,9 +0,0 @@ -## .rhiza/make.d/01-custom-env.mk - Custom Environment Configuration -# This file example shows how to set variables for the project. - -# Custom variables for this repository -PROJECT_NAME_EXTRA := Rhiza Platform -LOG_LEVEL ?= INFO - -# Overriding core variables (be careful) -# VENV := .venv_custom diff --git a/.rhiza/make.d/10-custom-task.mk b/.rhiza/make.d/10-custom-task.mk deleted file mode 100644 index 7c0ae036..00000000 --- a/.rhiza/make.d/10-custom-task.mk +++ /dev/null @@ -1,12 +0,0 @@ -## .rhiza/make.d/10-custom-task.mk - Custom Repository Tasks -# This file example shows how to add new targets. - -.PHONY: hello-rhiza - -##@ Custom Tasks -hello-rhiza: ## a custom greeting task - @printf "${GREEN}[INFO] Hello from the customised Rhiza project!${RESET}\n" - -# Adding logic to existing hooks -post-install:: ## run custom logic after core install - @printf "${BLUE}[INFO] Running custom post-install steps...${RESET}\n" diff --git a/.rhiza/make.d/book.mk b/.rhiza/make.d/book.mk new file mode 100644 index 00000000..28c77f60 --- /dev/null +++ b/.rhiza/make.d/book.mk @@ -0,0 +1,99 @@ +## book.mk - Book-building targets (MkDocs-based) + +.PHONY: book mkdocs-build test benchmark stress hypothesis-test _book-reports _book-notebooks mkdocs-serve mkdocs + +# No-op stubs — overridden by test.mk / bench.mk when present +test:: ; @: +benchmark:: ; @: +stress:: ; @: +hypothesis-test:: ; @: + +BOOK_OUTPUT ?= _book + +# Additional uvx --with packages to inject into mkdocs build and serve. +# Projects can extend the package list without editing this template, e.g.: +# MKDOCS_EXTRA_PACKAGES = --with "mkdocs-graphviz" +MKDOCS_EXTRA_PACKAGES ?= + +# Detect mkdocs config: prefer root-level, fall back to docs/mkdocs-base.yml +_MKDOCS_CFG := $(if $(wildcard mkdocs.yml),mkdocs.yml,$(if $(wildcard docs/mkdocs-base.yml),docs/mkdocs-base.yml,)) + +##@ Book + +_book-reports: test benchmark stress hypothesis-test + @mkdir -p docs/reports + @for src_dir in \ + "_tests/html-coverage:reports/coverage" \ + "_tests/html-report:reports/test-report" \ + "_tests/benchmarks:reports/benchmarks" \ + "_tests/stress:reports/stress" \ + "_tests/hypothesis:reports/hypothesis"; do \ + src=$${src_dir%%:*}; dest=docs/$${src_dir#*:}; \ + if [ -d "$$src" ] && [ -n "$$(ls -A "$$src" 2>/dev/null)" ]; then \ + printf "${BLUE}[INFO] Copying $$src -> $$dest${RESET}\n"; \ + mkdir -p "$$dest"; cp -r "$$src/." "$$dest/"; \ + else \ + printf "${YELLOW}[WARN] $$src not found, skipping${RESET}\n"; \ + fi; \ + done + @printf "# Reports\n\n" > docs/reports.md + @[ -f "docs/reports/test-report/report.html" ] && echo "- [Test Report](reports/test-report/report.html)" >> docs/reports.md || true + @[ -f "docs/reports/hypothesis/report.html" ] && echo "- [Hypothesis Report](reports/hypothesis/report.html)" >> docs/reports.md || true + @[ -f "docs/reports/benchmarks/report.html" ] && echo "- [Benchmarks](reports/benchmarks/report.html)" >> docs/reports.md || true + @[ -f "docs/reports/stress/report.html" ] && echo "- [Stress Report](reports/stress/report.html)" >> docs/reports.md || true + @[ -f "docs/reports/coverage/index.html" ] && echo "- [Coverage Report](reports/coverage/index.html)" >> docs/reports.md || true + +_book-notebooks: + @if [ -d "$(MARIMO_FOLDER)" ]; then \ + for nb in $(MARIMO_FOLDER)/*.py; do \ + name=$$(basename "$$nb" .py); \ + printf "${BLUE}[INFO] Exporting $$nb${RESET}\n"; \ + abs_output="$$(pwd)/docs/notebooks/$$name.html"; \ + mkdir -p docs/notebooks; \ + (cd "$$(dirname "$$nb")" && ${UV_BIN} run marimo export html --sandbox "$$(basename "$$nb")" -o "$$abs_output"); \ + done; \ + printf "# Marimo Notebooks\n\n" > docs/notebooks.md; \ + for html in docs/notebooks/*.html; do \ + name=$$(basename "$$html" .html); \ + echo "- [$$name]($$name.html)" >> docs/notebooks.md; \ + done; \ + fi + +book:: _book-reports _book-notebooks ## compile the companion book via MkDocs + @if [ -n "$(_MKDOCS_CFG)" ]; then \ + rm -rf "$(BOOK_OUTPUT)"; \ + ${UVX_BIN} --with "mkdocs-material<10.0" --with "pymdown-extensions>=10.0" --with "mkdocs<2.0" $(MKDOCS_EXTRA_PACKAGES) mkdocs build \ + -f "$(_MKDOCS_CFG)" \ + -d "$$(pwd)/$(BOOK_OUTPUT)"; \ + else \ + printf "${YELLOW}[WARN] No mkdocs config found, skipping MkDocs build${RESET}\n"; \ + fi + @mkdir -p "$(BOOK_OUTPUT)" + @touch "$(BOOK_OUTPUT)/.nojekyll" + @printf "${GREEN}[SUCCESS] Book built at $(BOOK_OUTPUT)/${RESET}\n" + @tree $(BOOK_OUTPUT) + +mkdocs-build: install-uv ## build MkDocs documentation site + @if [ -n "$(_MKDOCS_CFG)" ]; then \ + rm -rf "$(BOOK_OUTPUT)"; \ + ${UVX_BIN} --with "mkdocs-material<10.0" --with "pymdown-extensions>=10.0" --with "mkdocs<2.0" $(MKDOCS_EXTRA_PACKAGES) mkdocs build \ + -f "$(_MKDOCS_CFG)" \ + -d "$$(pwd)/$(BOOK_OUTPUT)"; \ + else \ + printf "${RED}[ERROR] No mkdocs config found${RESET}\n"; \ + exit 1; \ + fi + @mkdir -p "$(BOOK_OUTPUT)" + @touch "$(BOOK_OUTPUT)/.nojekyll" + @printf "${GREEN}[SUCCESS] Docs built at $(BOOK_OUTPUT)/${RESET}\n" + +mkdocs-serve: install-uv ## serve MkDocs site with live reload + @if [ -n "$(_MKDOCS_CFG)" ]; then \ + ${UVX_BIN} --with "mkdocs-material<10.0" --with "pymdown-extensions>=10.0" --with "mkdocs<2.0" $(MKDOCS_EXTRA_PACKAGES) mkdocs serve \ + -f "$(_MKDOCS_CFG)"; \ + else \ + printf "${RED}[ERROR] No mkdocs config found${RESET}\n"; \ + exit 1; \ + fi + +mkdocs: mkdocs-serve ## alias for mkdocs-serve diff --git a/.rhiza/make.d/marimo.mk b/.rhiza/make.d/marimo.mk new file mode 100644 index 00000000..36ae74ca --- /dev/null +++ b/.rhiza/make.d/marimo.mk @@ -0,0 +1,42 @@ +## Makefile.marimo - Marimo notebook targets +# This file is included by the main Makefile + +# Declare phony targets (they don't produce files) +.PHONY: marimo-validate marimo + +##@ Marimo Notebooks +marimo-validate: install ## validate all Marimo notebooks can run + @printf "${BLUE}[INFO] Validating all notebooks in ${MARIMO_FOLDER}...${RESET}\n" + @if [ ! -d "${MARIMO_FOLDER}" ]; then \ + printf "${YELLOW}[WARN] Directory '${MARIMO_FOLDER}' does not exist. Skipping validation.${RESET}\n"; \ + else \ + failed=0; \ + for notebook in ${MARIMO_FOLDER}/*.py; do \ + if [ -f "$$notebook" ]; then \ + notebook_name=$$(basename "$$notebook"); \ + notebook_stem=$$(basename "$$notebook" .py); \ + artefact_folder="results/$$notebook_stem"; \ + mkdir -p "$$artefact_folder"; \ + printf "${BLUE}[INFO] Validating $$notebook_name (artefacts → $$artefact_folder)...${RESET}\n"; \ + if NOTEBOOK_OUTPUT_FOLDER="$$artefact_folder" ${UV_BIN} run "$$notebook" > /dev/null 2>&1; then \ + printf "${GREEN}[SUCCESS] $$notebook_name is valid${RESET}\n"; \ + else \ + printf "${RED}[ERROR] $$notebook_name failed validation${RESET}\n"; \ + failed=$$((failed + 1)); \ + fi; \ + fi; \ + done; \ + if [ $$failed -eq 0 ]; then \ + printf "${GREEN}[SUCCESS] All notebooks validated successfully${RESET}\n"; \ + else \ + printf "${RED}[ERROR] $$failed notebook(s) failed validation${RESET}\n"; \ + exit 1; \ + fi; \ + fi + +marimo: install ## fire up Marimo server + @if [ ! -d "${MARIMO_FOLDER}" ]; then \ + printf " ${YELLOW}[WARN] Marimo folder '${MARIMO_FOLDER}' not found, skipping start${RESET}\n"; \ + else \ + ${UV_BIN} run --no-project --with marimo --directory "${MARIMO_FOLDER}" marimo edit --no-token --headless; \ + fi diff --git a/.rhiza/make.d/quality.mk b/.rhiza/make.d/quality.mk new file mode 100644 index 00000000..32007f33 --- /dev/null +++ b/.rhiza/make.d/quality.mk @@ -0,0 +1,51 @@ +## .rhiza/make.d/quality.mk - Quality and Formatting +# This file provides targets for code quality checks, linting, and formatting. + +# Configurable list of licenses that fail the compliance scan (semicolon-separated) +LICENSE_FAIL_ON ?= GPL;LGPL;AGPL + +# Declare phony targets (they don't produce files) +.PHONY: all deptry fmt license todos suppression-audit + +##@ Quality and Formatting +all: fmt deptry test docs-coverage security license typecheck rhiza-test ## run all CI targets locally + +deptry: install-uv ## Run deptry + @if [ -d ${SOURCE_FOLDER} ]; then \ + $(UVX_BIN) -p ${PYTHON_VERSION} deptry ${SOURCE_FOLDER}; \ + fi + + @if [ -d ${MARIMO_FOLDER} ]; then \ + if [ -d ${SOURCE_FOLDER} ]; then \ + $(UVX_BIN) -p ${PYTHON_VERSION} deptry ${MARIMO_FOLDER} ${SOURCE_FOLDER} --ignore DEP004; \ + else \ + $(UVX_BIN) -p ${PYTHON_VERSION} deptry ${MARIMO_FOLDER} --ignore DEP004; \ + fi \ + fi + +fmt: install-uv ## check the pre-commit hooks and the linting + @${UVX_BIN} -p ${PYTHON_VERSION} pre-commit run --all-files + +todos: ## search and report all TODO/FIXME/HACK comments in the codebase + @printf "${BLUE}[INFO] Searching for TODO, FIXME, and HACK comments...${RESET}\n" + @printf "${BOLD}Found the following items:${RESET}\n\n" + @find . -type f \( -name "*.py" -o -name "*.mk" -o -name "*.sh" -o -name "*.md" -o -name "*.yml" -o -name "*.yaml" \) \ + -not -path "./.venv/*" \ + -not -path "./.git/*" \ + -not -path "./node_modules/*" \ + -not -path "./.tox/*" \ + -not -path "./build/*" \ + -not -path "./dist/*" \ + -print0 | xargs -0 grep -nHE "(TODO|FIXME|HACK):" 2>/dev/null | \ + grep -v "make todos" | \ + awk -F: '{ printf "${YELLOW}%s${RESET}:${GREEN}%s${RESET}: %s\n", $$1, $$2, substr($$0, index($$0,$$3)) }' || \ + printf "${GREEN}[SUCCESS] No TODO/FIXME/HACK comments found!${RESET}\n" + @printf "\n${BLUE}[INFO] Search complete.${RESET}\n" + +suppression-audit: ## scan codebase for inline suppressions and report (grade, detail, histogram) + @printf "${BLUE}[INFO] Running suppression audit...${RESET}\n" + @${UV_BIN} run python .rhiza/utils/suppression_audit.py + +license: install ## run license compliance scan (fail on GPL, LGPL, AGPL) + @printf "${BLUE}[INFO] Running license compliance scan...${RESET}\n" + @${UV_BIN} run --with pip-licenses pip-licenses --fail-on="${LICENSE_FAIL_ON}" diff --git a/.rhiza/make.d/releasing.mk b/.rhiza/make.d/releasing.mk new file mode 100644 index 00000000..fc6d57a6 --- /dev/null +++ b/.rhiza/make.d/releasing.mk @@ -0,0 +1,50 @@ +## .rhiza/make.d/releasing.mk - Releasing and Versioning +# This file provides targets for version bumping and release management. + +# Declare phony targets (they don't produce files) +.PHONY: bump release publish release-status pre-bump post-bump pre-release post-release + +# Hook targets (double-colon rules allow multiple definitions) +pre-bump:: ; @: +post-bump:: ; @: +pre-release:: ; @: +post-release:: ; @: + +# DRY_RUN support: pass DRY_RUN=1 to preview changes without applying them +_DRY_RUN_FLAG := $(if $(DRY_RUN),--dry-run,) +_VERSION=0.3.3 + +##@ Releasing and Versioning +bump: pre-bump ## bump version of the project (supports DRY_RUN=1) + @if [ -f "pyproject.toml" ]; then \ + $(MAKE) install; \ + PATH="$(abspath ${VENV})/bin:$$PATH" ${UVX_BIN} "rhiza-tools>=$(_VERSION)" bump $(_DRY_RUN_FLAG); \ + if [ -z "$(DRY_RUN)" ]; then \ + printf "${BLUE}[INFO] Checking uv.lock file...${RESET}\n"; \ + ${UV_BIN} lock; \ + fi; \ + else \ + printf "${YELLOW}[WARN] No pyproject.toml found, skipping bump${RESET}\n"; \ + fi + @$(MAKE) post-bump + +release: pre-release install-uv ## create tag and push to remote repository triggering release workflow (supports DRY_RUN=1) + ${UVX_BIN} "rhiza-tools>=$(_VERSION)" release $(_DRY_RUN_FLAG); + @$(MAKE) post-release + +publish: pre-release install-uv ## bump version, create tag and push in one step (supports DRY_RUN=1) + ${UVX_BIN} "rhiza-tools>=$(_VERSION)" release --with-bump $(_DRY_RUN_FLAG); + @$(MAKE) post-release + +release-status: ## show release workflow status and latest release information +ifeq ($(FORGE_TYPE),github) + @{ $(MAKE) --no-print-directory workflow-status; printf "\n"; $(MAKE) --no-print-directory latest-release; } 2>&1 | $${PAGER:-less -R} +else ifeq ($(FORGE_TYPE),gitlab) + @printf "${YELLOW}[WARN] GitLab detected — release-status is not yet supported for GitLab repositories.${RESET}\n" + @printf "${BLUE}[INFO] Please check your pipeline status in the GitLab UI.${RESET}\n" +else + @printf "${RED}[ERROR] Could not detect forge type (.github/workflows/ or .gitlab-ci.yml not found)${RESET}\n" +endif + + + diff --git a/.rhiza/requirements/README.md b/.rhiza/requirements/README.md index 4feff9a7..5c7a818e 100644 --- a/.rhiza/requirements/README.md +++ b/.rhiza/requirements/README.md @@ -4,10 +4,10 @@ This folder contains the development dependencies for the Rhiza project, organiz ## Files -- **tests.txt** - Testing dependencies (pytest, pytest-cov, pytest-html) +- **tests.txt** - Testing dependencies (pytest, pytest-cov, pytest-html, pytest-mock, PyYAML, defusedxml, hypothesis, pytest-benchmark, pygal) - **marimo.txt** - Marimo notebook dependencies -- **docs.txt** - Documentation generation dependencies (pdoc) -- **tools.txt** - Development tools (pre-commit, python-dotenv) +- **docs.txt** - Documentation generation dependencies (pdoc, interrogate, mkdocs, mkdocs-material, mkdocstrings) +- **tools.txt** - Development tools (pre-commit, python-dotenv, typer, ty) ## Usage diff --git a/.rhiza/requirements/docs.txt b/.rhiza/requirements/docs.txt index 7d12b67f..1b9d3921 100644 --- a/.rhiza/requirements/docs.txt +++ b/.rhiza/requirements/docs.txt @@ -1,3 +1,5 @@ # Documentation dependencies for rhiza -pdoc>=16.0.0 interrogate>=1.7.0 +mkdocs>=1.6.0 +mkdocs-material>=9.5.0 +mkdocstrings[python]>=0.25.0 diff --git a/.rhiza/requirements/tests.txt b/.rhiza/requirements/tests.txt deleted file mode 100644 index fe03710f..00000000 --- a/.rhiza/requirements/tests.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Test dependencies for rhiza -pytest>=8.0 -pytest-cov>=6.0 -pytest-html>=4.0 -pytest-mock>=3.0 - -# For property-based testing -hypothesis>=6.150.0 - -# For benchmarks -pytest-benchmark>=5.2.3 -pygal>=3.1.0 diff --git a/.rhiza/requirements/tools.txt b/.rhiza/requirements/tools.txt index cb101c4f..262ffc01 100644 --- a/.rhiza/requirements/tools.txt +++ b/.rhiza/requirements/tools.txt @@ -1,6 +1,7 @@ # Development tool dependencies for rhiza pre-commit==4.5.1 python-dotenv==1.2.1 + # for now needed until rhiza-tools is finished typer==0.21.1 -mypy==1.19.1 +ty==0.0.18 diff --git a/.rhiza/scripts/check_workflow_names.py b/.rhiza/scripts/check_workflow_names.py deleted file mode 100644 index e4a838d1..00000000 --- a/.rhiza/scripts/check_workflow_names.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -"""Script to ensure GitHub Actions workflows have the (RHIZA) prefix.""" - -import sys - -import yaml - - -def check_file(filepath): - """Check if the workflow file has the correct name prefix and update if needed. - - Args: - filepath: Path to the workflow file. - - Returns: - bool: True if file is correct, False if it was updated or has errors. - """ - with open(filepath) as f: - try: - content = yaml.safe_load(f) - except yaml.YAMLError as exc: - print(f"Error parsing YAML {filepath}: {exc}") - return False - - if not isinstance(content, dict): - # Empty file or not a dict - return True - - name = content.get("name") - if not name: - print(f"Error: {filepath} missing 'name' field.") - return False - - if not name.startswith("(RHIZA) "): - print(f"Updating {filepath}: name '{name}' -> '(RHIZA) {name}'") - - # Read file lines to perform replacement while preserving comments - with open(filepath) as f_read: - lines = f_read.readlines() - - with open(filepath, "w") as f_write: - replaced = False - for line in lines: - # Replace only the top-level name field (assumes it starts at beginning of line) - if not replaced and line.startswith("name:"): - # Check if this line corresponds to the extracted name. - # Simple check: does it contain reasonable parts of the name? - # Or just blinding replace top-level name: - # We'll use quotes to be safe - f_write.write(f'name: "(RHIZA) {name}"\n') - replaced = True - else: - f_write.write(line) - - return False # Fail so pre-commit knows files were modified - - return True - - -def main(): - """Execute the script.""" - files = sys.argv[1:] - failed = False - for f in files: - if not check_file(f): - failed = True - - if failed: - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/.rhiza/scripts/release.sh b/.rhiza/scripts/release.sh deleted file mode 100755 index 6ba0f218..00000000 --- a/.rhiza/scripts/release.sh +++ /dev/null @@ -1,276 +0,0 @@ -#!/bin/sh -# Release script -# - Creates a git tag based on the current version in pyproject.toml -# - Pushes the tag to remote to trigger the release workflow -# - Performs checks (branch, upstream status, clean working tree) -# -# This script is POSIX-sh compatible and follows the style of other scripts -# in this repository. It uses uv to read the current version. - -set -eu - -UV_BIN=${UV_BIN:-./bin/uv} -DRY_RUN="" - -BLUE="\033[36m" -RED="\033[31m" -GREEN="\033[32m" -YELLOW="\033[33m" -RESET="\033[0m" - -# Parse command-line arguments -show_usage() { - printf "Usage: %s [OPTIONS]\n\n" "$0" - printf "Description:\n" - printf " Create tag and push to remote (with prompts)\n\n" - printf "Options:\n" - printf " -n, --dry-run Show what would be done without making changes\n" - printf " -h, --help Show this help message\n\n" - printf "Examples:\n" - printf " %s (create tag and push with prompts)\n" "$0" - printf " %s --dry-run (simulate release without changes)\n" "$0" -} - -while [ $# -gt 0 ]; do - case "$1" in - -n|--dry-run) - DRY_RUN="true" - shift - ;; - -h|--help) - show_usage - exit 0 - ;; - -*) - printf "%b[ERROR] Unknown option: %s%b\n" "$RED" "$1" "$RESET" - show_usage - exit 1 - ;; - *) - printf "%b[ERROR] Unknown argument: %s%b\n" "$RED" "$1" "$RESET" - show_usage - exit 1 - ;; - esac -done - -# Check if pyproject.toml exists -if [ ! -f "pyproject.toml" ]; then - printf "%b[ERROR] pyproject.toml not found in current directory%b\n" "$RED" "$RESET" - exit 1 -fi - -# Check if uv is available -if [ ! -x "$UV_BIN" ]; then - printf "%b[ERROR] uv not found at %s. Run 'make install-uv' first.%b\n" "$RED" "$UV_BIN" "$RESET" - exit 1 -fi - -# Helper function to prompt user to continue -# In dry-run mode, automatically continues without prompting -prompt_continue() { - _pc_message="$1" - if [ -n "$DRY_RUN" ]; then - printf "\n%b[DRY-RUN] %s Would prompt to continue%b\n" "$YELLOW" "$_pc_message" "$RESET" - return 0 - fi - printf "\n%b[PROMPT] %s Continue? [y/N] %b" "$YELLOW" "$_pc_message" "$RESET" - read -r _pc_answer - case "$_pc_answer" in - [Yy]*) - return 0 - ;; - *) - printf "%b[INFO] Aborted by user%b\n" "$YELLOW" "$RESET" - exit 0 - ;; - esac -} - -# Helper function to prompt user for yes/no -# In dry-run mode, automatically returns yes -prompt_yes_no() { - _pyn_message="$1" - if [ -n "$DRY_RUN" ]; then - printf "\n%b[DRY-RUN] %s Would prompt yes/no%b\n" "$YELLOW" "$_pyn_message" "$RESET" - return 0 - fi - printf "\n%b[PROMPT] %s [y/N] %b" "$YELLOW" "$_pyn_message" "$RESET" - read -r _pyn_answer - case "$_pyn_answer" in - [Yy]*) - return 0 - ;; - *) - return 1 - ;; - esac -} - -# Function: Release - create tag and push (with prompts) -do_release() { - # Get the current version from pyproject.toml - CURRENT_VERSION=$("$UV_BIN" version --short 2>/dev/null) - if [ -z "$CURRENT_VERSION" ]; then - printf "%b[ERROR] Could not determine version from pyproject.toml%b\n" "$RED" "$RESET" - exit 1 - fi - - TAG="v$CURRENT_VERSION" - - # Get current branch - CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) - if [ -z "$CURRENT_BRANCH" ]; then - printf "%b[ERROR] Could not determine current branch%b\n" "$RED" "$RESET" - exit 1 - fi - - # Determine default branch - DEFAULT_BRANCH=$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5) - if [ -z "$DEFAULT_BRANCH" ]; then - printf "%b[ERROR] Could not determine default branch from remote%b\n" "$RED" "$RESET" - exit 1 - fi - - # Warn if not on default branch - if [ "$CURRENT_BRANCH" != "$DEFAULT_BRANCH" ]; then - printf "%b[WARN] You are on branch '%s' but the default branch is '%s'%b\n" "$YELLOW" "$CURRENT_BRANCH" "$DEFAULT_BRANCH" "$RESET" - printf "%b[WARN] Releases are typically created from the default branch.%b\n" "$YELLOW" "$RESET" - prompt_continue "Proceed with release from '$CURRENT_BRANCH'?" - fi - - printf "%b[INFO] Current version: %s%b\n" "$BLUE" "$CURRENT_VERSION" "$RESET" - printf "%b[INFO] Tag to create: %s%b\n" "$BLUE" "$TAG" "$RESET" - - # Check if there are uncommitted changes - if [ -n "$(git status --porcelain)" ]; then - printf "%b[ERROR] You have uncommitted changes:%b\n" "$RED" "$RESET" - git status --short - printf "\n%b[ERROR] Please commit or stash your changes before releasing.%b\n" "$RED" "$RESET" - exit 1 - fi - - # Check if branch is up-to-date with remote - # This prevents releasing from an out-of-sync branch which could miss commits or conflict - printf "%b[INFO] Checking remote status...%b\n" "$BLUE" "$RESET" - git fetch origin >/dev/null 2>&1 - # Get the upstream tracking branch (e.g., origin/main) - UPSTREAM=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null) - if [ -z "$UPSTREAM" ]; then - printf "%b[ERROR] No upstream branch configured for %s%b\n" "$RED" "$CURRENT_BRANCH" "$RESET" - exit 1 - fi - - # Compare local, remote, and merge-base commits to determine sync status - # LOCAL: current commit on local branch - # REMOTE: current commit on remote tracking branch - # BASE: most recent common ancestor between local and remote - LOCAL=$(git rev-parse @) - REMOTE=$(git rev-parse "$UPSTREAM") - BASE=$(git merge-base @ "$UPSTREAM") - - # Use git revision comparison to detect branch status - if [ "$LOCAL" != "$REMOTE" ]; then - if [ "$LOCAL" = "$BASE" ]; then - # Local is behind remote (need to pull) - printf "%b[ERROR] Your branch is behind '%s'. Please pull changes.%b\n" "$RED" "$UPSTREAM" "$RESET" - exit 1 - elif [ "$REMOTE" = "$BASE" ]; then - # Local is ahead of remote (need to push) - printf "%b[WARN] Your branch is ahead of '%s'.%b\n" "$YELLOW" "$UPSTREAM" "$RESET" - printf "Unpushed commits:\n" - git log --oneline --graph --decorate "$UPSTREAM..HEAD" - prompt_continue "Push changes to remote before releasing?" - if [ -n "$DRY_RUN" ]; then - printf "%b[DRY-RUN] Would run: git push origin %s%b\n" "$YELLOW" "$CURRENT_BRANCH" "$RESET" - else - git push origin "$CURRENT_BRANCH" - fi - else - # Branches have diverged (need to merge or rebase) - printf "%b[ERROR] Your branch has diverged from '%s'. Please reconcile.%b\n" "$RED" "$UPSTREAM" "$RESET" - exit 1 - fi - fi - - # Check if tag already exists locally - SKIP_TAG_CREATE="" - if git rev-parse "$TAG" >/dev/null 2>&1; then - printf "%b[WARN] Tag '%s' already exists locally%b\n" "$YELLOW" "$TAG" "$RESET" - prompt_continue "Tag exists. Skip tag creation and proceed to push?" - SKIP_TAG_CREATE="true" - fi - - # Check if tag already exists on remote - if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then - printf "%b[ERROR] Tag '%s' already exists on remote%b\n" "$RED" "$TAG" "$RESET" - printf "The release for version %s has already been published.\n" "$CURRENT_VERSION" - exit 1 - fi - - # Step 1: Create the tag (if it doesn't exist) - if [ -z "$SKIP_TAG_CREATE" ]; then - printf "\n%b=== Step 1: Create Tag ===%b\n" "$BLUE" "$RESET" - printf "Creating tag '%s' for version %s\n" "$TAG" "$CURRENT_VERSION" - prompt_continue "" - - # Check if GPG signing is configured for git commits/tags - # If user.signingkey is set or commit.gpgsign is true, create a signed tag - # Signed tags provide cryptographic verification of release authenticity - if git config --get user.signingkey >/dev/null 2>&1 || [ "$(git config --get commit.gpgsign)" = "true" ]; then - printf "%b[INFO] GPG signing is enabled. Creating signed tag.%b\n" "$BLUE" "$RESET" - if [ -n "$DRY_RUN" ]; then - printf "%b[DRY-RUN] Would run: git tag -s %s -m \"Release %s\"%b\n" "$YELLOW" "$TAG" "$TAG" "$RESET" - else - git tag -s "$TAG" -m "Release $TAG" - fi - else - printf "%b[INFO] GPG signing is not enabled. Creating unsigned tag.%b\n" "$BLUE" "$RESET" - if [ -n "$DRY_RUN" ]; then - printf "%b[DRY-RUN] Would run: git tag -a %s -m \"Release %s\"%b\n" "$YELLOW" "$TAG" "$TAG" "$RESET" - else - git tag -a "$TAG" -m "Release $TAG" - fi - fi - if [ -n "$DRY_RUN" ]; then - printf "%b[DRY-RUN] Tag '%s' would be created locally%b\n" "$YELLOW" "$TAG" "$RESET" - else - printf "%b[SUCCESS] Tag '%s' created locally%b\n" "$GREEN" "$TAG" "$RESET" - fi - fi - - # Step 2: Push the tag to remote - printf "\n%b=== Step 2: Push Tag to Remote ===%b\n" "$BLUE" "$RESET" - printf "Pushing tag '%s' to origin will trigger the release workflow.\n" "$TAG" - - # Show what commits are in this tag compared to the last tag - # This helps users understand what changes are included in the release - LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - if [ -n "$LAST_TAG" ] && [ "$LAST_TAG" != "$TAG" ]; then - # Count commits between last tag and current tag - COMMIT_COUNT=$(git rev-list "$LAST_TAG..$TAG" --count 2>/dev/null || echo "0") - printf "Commits since %s: %s\n" "$LAST_TAG" "$COMMIT_COUNT" - fi - - prompt_continue "" - - # Push only the specific tag (not all tags) to trigger the release workflow - # Extract repository name from remote URL for constructing GitHub Actions link - # Converts git@github.com:user/repo.git or https://github.com/user/repo.git to user/repo - REPO_URL=$(git remote get-url origin | sed 's/.*github.com[:/]\(.*\)\.git/\1/') - - if [ -n "$DRY_RUN" ]; then - printf "%b[DRY-RUN] Would run: git push origin refs/tags/%s%b\n" "$YELLOW" "$TAG" "$RESET" - printf "\n%b[DRY-RUN] Release tag %s would be pushed to remote%b\n" "$YELLOW" "$TAG" "$RESET" - printf "%b[DRY-RUN] This would trigger the release workflow%b\n" "$YELLOW" "$RESET" - printf "%b[INFO] Monitor progress at: https://github.com/%s/actions%b\n" "$BLUE" "$REPO_URL" "$RESET" - else - git push origin "refs/tags/$TAG" - printf "\n%b[SUCCESS] Release tag %s pushed to remote!%b\n" "$GREEN" "$TAG" "$RESET" - printf "%b[INFO] The release workflow will now be triggered automatically.%b\n" "$BLUE" "$RESET" - printf "%b[INFO] Monitor progress at: https://github.com/%s/actions%b\n" "$BLUE" "$REPO_URL" "$RESET" - fi -} - -# Main execution logic -do_release diff --git a/.rhiza/template.lock b/.rhiza/template.lock new file mode 100644 index 00000000..6e08fe14 --- /dev/null +++ b/.rhiza/template.lock @@ -0,0 +1,64 @@ +sha: dccf929ce3bcbaf01ddf1240ac16354e0cf76bb1 +repo: jebel-quant/rhiza +host: github +ref: v0.9.5 +include: [] +exclude: +- .rhiza/docs/ +- .rhiza/make.d/agentic.mk +- .rhiza/make.d/gh-aw.mk +- .rhiza/make.d/github.mk +- .rhiza/make.d/custom-env.mk +- .rhiza/make.d/custom-task.mk +- .rhiza/make.d/README.md +- .github/agents/ +- docs/adr +templates: +- github +- marimo +files: +- .editorconfig +- .github/DISCUSSION_TEMPLATE/q-and-a.yml +- .github/ISSUE_TEMPLATE/bug_report.yml +- .github/ISSUE_TEMPLATE/feature_request.yml +- .github/actions/configure-git-auth/README.md +- .github/actions/configure-git-auth/action.yml +- .github/copilot-instructions.md +- .github/dependabot.yml +- .github/hooks/hooks.json +- .github/hooks/session-end.sh +- .github/hooks/session-start.sh +- .github/secret_scanning.yml +- .github/semgrep.yml +- .github/workflows/copilot-setup-steps.yml +- .github/workflows/rhiza_book.yml +- .github/workflows/rhiza_marimo.yml +- .github/workflows/rhiza_release.yml +- .github/workflows/rhiza_sync.yml +- .github/workflows/rhiza_weekly.yml +- .gitignore +- .pre-commit-config.yaml +- .python-version +- .rhiza/.cfg.toml +- .rhiza/.env +- .rhiza/.gitignore +- .rhiza/.rhiza-version +- .rhiza/assets/rhiza-logo.svg +- .rhiza/make.d/book.mk +- .rhiza/make.d/bootstrap.mk +- .rhiza/make.d/marimo.mk +- .rhiza/make.d/quality.mk +- .rhiza/make.d/releasing.mk +- .rhiza/requirements/README.md +- .rhiza/requirements/docs.txt +- .rhiza/requirements/marimo.txt +- .rhiza/requirements/tools.txt +- .rhiza/rhiza.mk +- Makefile +- docs/assets/rhiza-logo.svg +- docs/development/MARIMO.md +- docs/index.md +- docs/mkdocs-base.yml +- ruff.toml +synced_at: '2026-04-16T02:49:17Z' +strategy: merge diff --git a/.rhiza/utils/version_matrix.py b/.rhiza/utils/version_matrix.py deleted file mode 100755 index 08ef655a..00000000 --- a/.rhiza/utils/version_matrix.py +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env python3 -"""Emit the list of supported Python versions from pyproject.toml. - -This helper is used in GitHub Actions to compute the test matrix. -""" - -import json -import re -import tomllib -from pathlib import Path - -PYPROJECT = Path(__file__).resolve().parents[2] / "pyproject.toml" -CANDIDATES = ["3.11", "3.12", "3.13", "3.14"] # extend as needed - - -class RhizaError(Exception): - """Base exception for Rhiza-related errors.""" - - -class VersionSpecifierError(RhizaError): - """Raised when a version string or specifier is invalid.""" - - -class PyProjectError(RhizaError): - """Raised when there are issues with pyproject.toml configuration.""" - - -def parse_version(v: str) -> tuple[int, ...]: - """Parse a version string into a tuple of integers. - - This is intentionally simple and only supports numeric components. - If a component contains non-numeric suffixes (e.g. '3.11.0rc1'), - the leading numeric portion will be used (e.g. '0rc1' -> 0). If a - component has no leading digits at all, a VersionSpecifierError is raised. - - Args: - v: Version string to parse (e.g., "3.11", "3.11.0rc1"). - - Returns: - Tuple of integers representing the version. - - Raises: - VersionSpecifierError: If a version component has no numeric prefix. - """ - parts: list[int] = [] - for part in v.split("."): - match = re.match(r"\d+", part) - if not match: - msg = f"Invalid version component {part!r} in version {v!r}; expected a numeric prefix." - raise VersionSpecifierError(msg) - parts.append(int(match.group(0))) - return tuple(parts) - - -def _check_operator(version_tuple: tuple[int, ...], op: str, spec_v_tuple: tuple[int, ...]) -> bool: - """Check if a version tuple satisfies an operator constraint.""" - operators = { - ">=": lambda v, s: v >= s, - "<=": lambda v, s: v <= s, - ">": lambda v, s: v > s, - "<": lambda v, s: v < s, - "==": lambda v, s: v == s, - "!=": lambda v, s: v != s, - } - return operators[op](version_tuple, spec_v_tuple) - - -def satisfies(version: str, specifier: str) -> bool: - """Check if a version satisfies a comma-separated list of specifiers. - - This is a simplified version of packaging.specifiers.SpecifierSet. - Supported operators: >=, <=, >, <, ==, != - - Args: - version: Version string to check (e.g., "3.11"). - specifier: Comma-separated specifier string (e.g., ">=3.11,<3.14"). - - Returns: - True if the version satisfies all specifiers, False otherwise. - - Raises: - VersionSpecifierError: If the specifier format is invalid. - """ - version_tuple = parse_version(version) - - # Split by comma for multiple constraints - for spec in specifier.split(","): - spec = spec.strip() - # Match operator and version part - match = re.match(r"(>=|<=|>|<|==|!=)\s*([\d.]+)", spec) - if not match: - # If no operator, assume == - if re.match(r"[\d.]+", spec): - if version_tuple != parse_version(spec): - return False - continue - msg = f"Invalid specifier {spec!r}; expected format like '>=3.11' or '3.11'" - raise VersionSpecifierError(msg) - - op, spec_v = match.groups() - spec_v_tuple = parse_version(spec_v) - - if not _check_operator(version_tuple, op, spec_v_tuple): - return False - - return True - - -def supported_versions() -> list[str]: - """Return all supported Python versions declared in pyproject.toml. - - Reads project.requires-python, evaluates candidate versions against the - specifier, and returns the subset that satisfy the constraint, in ascending order. - - Returns: - list[str]: The supported versions (e.g., ["3.11", "3.12"]). - - Raises: - PyProjectError: If requires-python is missing or no candidates match. - """ - # Load pyproject.toml using the tomllib standard library (Python 3.11+) - with PYPROJECT.open("rb") as f: - data = tomllib.load(f) - - # Extract the requires-python field from project metadata - # This specifies the Python version constraint (e.g., ">=3.11") - spec_str = data.get("project", {}).get("requires-python") - if not spec_str: - msg = "pyproject.toml: missing 'project.requires-python'" - raise PyProjectError(msg) - - # Filter candidate versions to find which ones satisfy the constraint - versions: list[str] = [] - for v in CANDIDATES: - if satisfies(v, spec_str): - versions.append(v) - - if not versions: - msg = f"pyproject.toml: no supported Python versions match '{spec_str}'. Evaluated candidates: {CANDIDATES}" - raise PyProjectError(msg) - - return versions - - -if __name__ == "__main__": - # Check if pyproject.toml exists in the expected location - # If it exists, use it to determine supported versions - # Otherwise, fall back to returning all candidates (for edge cases) - if PYPROJECT.exists(): - print(json.dumps(supported_versions())) - else: - print(json.dumps(CANDIDATES)) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 41f0e9c2..00000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,27 +0,0 @@ -# Code of Conduct - -## Purpose - -This code of conduct outlines expectations for behaviour when participating in the project with the aim of fostering a constructive and professional engineering environment. - -## Expected Behaviour - -Participants are expected to: - -- Be respectful in communication and collaboration. -- Focus on technical topics and project goals. -- Accept constructive feedback gracefully and offer it in kind. -- Assume good intentions from others and engage with a problem-solving mindset. -- Use appropriate language and conduct in all community spaces. - -## Scope - -This Code of Conduct applies in all project spaces—online and offline—and also applies when individuals are representing the project in public spaces. - -## Enforcement - -Project maintainers are responsible for enforcing the code and may take proportionate corrective action in response to unexpected behaviour. - -## Attribution - -Adapted from the [Contributor Covenant](https://www.contributor-covenant.org), with modifications for brevity and neutrality. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index d8603155..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,93 +0,0 @@ -# Contributing - -This document is a guide to contributing to the project. - -We welcome all contributions. You don't need to be an expert -to help out. - -## Checklist - -Contributions are made through -[pull requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests). -Before sending a pull request, make sure you do the following: - -- Run `make fmt` to make sure your code adheres to our [coding style](#code-style) -and all tests pass. -- [Write unit tests](#writing-unit-tests) for new functionality added. - -## Building from source - -You'll need to build the project locally to start editing code. -To install from source, clone the repository from GitHub, -navigate to its root, and run the following command: - -```bash -make install -``` - -## Contributing code - -To contribute to the project, send us pull requests. -For those new to contributing, check out GitHub's -[guide](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests). - -Once you've made your pull request, a member of the -development team will assign themselves to review it. -You might have a few -back-and-forths with your reviewer before it is accepted, -which is completely normal. -Your pull request will trigger continuous integration tests -for many different -Python versions and different platforms. If these tests start failing, -please -fix your code and send another commit, which will re-trigger the tests. - -If you'd like to add a new feature, please propose your -change in a GitHub issue to make sure -that your priorities align with ours. - -If you'd like to contribute code but don't know where to start, -try one of the -following: - -- Read the source and enhance the documentation, - or address TODOs -- Browse the open issues, - and look for the issues tagged "help wanted". - -## Code style - -We use ruff to enforce our Python coding style. -Before sending us a pull request, navigate to the project -root and run - -```bash -make fmt -``` - -to make sure that your changes abide by our style conventions. -Please fix any errors that are reported before sending -the pull request. - -## Writing unit tests - -Most code changes will require new unit tests. -Even bug fixes require unit tests, -since the presence of bugs usually indicates insufficient tests. -When adding tests, try to find a file in which your tests should belong; -if you're testing a new feature, you might want to create a new test file. - -We use the popular Python [pytest](https://docs.pytest.org/en/) framework for our -tests. - -## Running unit tests - -We use `pytest` to run our unit tests. -To run all unit tests run the following command: - -```bash -make test -``` - -Please make sure that your change doesn't cause any -of the unit tests to fail. diff --git a/Makefile b/Makefile index 0a1aac7f..24239e6f 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,49 @@ ## Makefile (repo-owned) # Keep this file small. It can be edited without breaking template sync. +DOCFORMAT=google +DEFAULT_AI_MODEL=claude-sonnet-4.5 +LOGO_FILE=.rhiza/assets/rhiza-logo.svg +GH_AW_ENGINE ?= copilot # Default AI engine for gh-aw workflows (copilot, claude, or codex) + # Always include the Rhiza API (template-managed) include .rhiza/rhiza.mk # Optional: developer-local extensions (not committed) -include local.mk + +# Wire typecheck into make validate +post-validate:: + @$(MAKE) typecheck + +## Custom targets + +.PHONY: adr +adr: install-gh-aw ## Create a new Architecture Decision Record (ADR) using AI assistance + @echo "Creating a new ADR..." + @echo "This will trigger the adr-create workflow." + @echo "" + @read -p "Enter ADR title (e.g., 'Use PostgreSQL for data storage'): " title; \ + echo ""; \ + read -p "Enter brief context (optional, press Enter to skip): " context; \ + echo ""; \ + if [ -z "$$title" ]; then \ + echo "Error: Title is required"; \ + exit 1; \ + fi; \ + if [ -z "$$context" ]; then \ + gh workflow run adr-create.md -f title="$$title"; \ + else \ + gh workflow run adr-create.md -f title="$$title" -f context="$$context"; \ + fi; \ + echo ""; \ + echo "✅ ADR creation workflow triggered!"; \ + echo ""; \ + echo "The workflow will:"; \ + echo " 1. Generate the next ADR number"; \ + echo " 2. Create a comprehensive ADR document"; \ + echo " 3. Update the ADR index"; \ + echo " 4. Open a pull request for review"; \ + echo ""; \ + echo "Check workflow status: gh run list --workflow=adr-create.md"; \ + echo "View latest run: gh run view" diff --git a/book/README.md b/book/README.md deleted file mode 100644 index 81273af2..00000000 --- a/book/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Project Book and Documentation - -This directory contains the source and templates for generating the Rhiza companion book and API documentation. - -## Structure - -- `marimo/`: Interactive [Marimo](https://marimo.io/) notebooks that are included in the book. -- `minibook-templates/`: Jinja2 templates for the minibook generation. -- `pdoc-templates/`: Custom templates for [pdoc](https://pdoc.dev/) API documentation. -- `book.mk`: Specialised Makefile for building the book and documentation. - -## Building the Book - -You can build the complete documentation book using the main project Makefile: - -```bash -make book -``` - -This process involves: -1. Exporting Marimo notebooks to HTML. -2. Generating API documentation from the source code. -3. Combining them into a cohesive "book" structure. - -## Documentation Customisation - -You can customise the look and feel of your documentation by providing your own templates. - -### API Documentation (pdoc) - -The `make docs` command checks for a directory at `book/pdoc-templates`. If found, it uses the templates within that directory to generate the API documentation. - -To customise the API docs: -1. Create the directory: `mkdir -p book/pdoc-templates` -2. Add your Jinja2 templates (e.g., `module.html.jinja2`) to this directory. - -See the [pdoc documentation](https://pdoc.dev/docs/pdoc.html#templates) for more details on templating. - -### Project Logo - -The documentation generation supports embedding a project logo in the sidebar. - -**Default Behavior:** -By default, the build looks for `assets/rhiza-logo.svg`. - -**Customization:** -You can change the logo by setting the `LOGO_FILE` variable in your project's `Makefile` or `local.mk`. - -```makefile -# Example: Use a custom PNG logo -LOGO_FILE := assets/my-company-logo.png -``` - -To disable the logo entirely, set the variable to an empty string: - -```makefile -# Example: Disable logo -LOGO_FILE := -``` - -### Companion Book (minibook) - -The `make book` command checks for a template at `book/minibook-templates/custom.html.jinja2`. If found, it uses this template for the minibook generation. - -To customise the book: -1. Create the directory: `mkdir -p book/minibook-templates` -2. Create your custom template at `book/minibook-templates/custom.html.jinja2`. diff --git a/book/book.mk b/book/book.mk deleted file mode 100644 index 5c2fc17b..00000000 --- a/book/book.mk +++ /dev/null @@ -1,166 +0,0 @@ -## book.mk - Documentation and book-building targets -# This file is included by the main Makefile. -# It provides targets for generating API documentation (pdoc), -# exporting Marimo notebooks to HTML (marimushka), and compiling -# a companion book (minibook). - -# Declare phony targets (they don't produce files) -.PHONY: docs marimushka book - -# Define a default no-op marimushka target that will be used -# when book/marimo/marimo.mk doesn't exist or doesn't define marimushka -marimushka:: install-uv - @if [ ! -d "book/marimo" ]; then \ - printf "${BLUE}[INFO] No Marimo directory found, creating placeholder${RESET}\n"; \ - mkdir -p "${MARIMUSHKA_OUTPUT}"; \ - printf '%s\n' 'Marimo Notebooks' \ - '

Marimo Notebooks

No notebooks found.

' \ - > "${MARIMUSHKA_OUTPUT}/index.html"; \ - fi - -# Default output directory for Marimushka (HTML exports of notebooks) -MARIMUSHKA_OUTPUT ?= _marimushka - -# Logo file for pdoc (relative to project root). -# 1. Defaults to the Rhiza logo if present. -# 2. Can be overridden in Makefile or local.mk (e.g. LOGO_FILE := my-logo.png) -# 3. If set to empty string, no logo will be used. -LOGO_FILE ?= assets/rhiza-logo.svg - -# ---------------------------- -# Book sections (declarative) -# ---------------------------- -# format: -# name | source index | book-relative index | source dir | book dir - -BOOK_SECTIONS := \ - "API|_pdoc/index.html|pdoc/index.html|_pdoc|pdoc" \ - "Coverage|_tests/html-coverage/index.html|tests/html-coverage/index.html|_tests/html-coverage|tests/html-coverage" \ - "Test Report|_tests/html-report/report.html|tests/html-report/report.html|_tests/html-report|tests/html-report" \ - "Notebooks|_marimushka/index.html|marimushka/index.html|_marimushka|marimushka" - - -##@ Documentation - -# The 'docs' target generates API documentation using pdoc. -# 1. Identifies Python packages within the source folder. -# 2. Detects the docformat (google, numpy, or sphinx) from ruff.toml or defaults to google. -# 3. Installs pdoc and generates HTML documentation in _pdoc. -docs:: install ## create documentation with pdoc - # Clean up previous docs - rm -rf _pdoc; - - @if [ -d "${SOURCE_FOLDER}" ]; then \ - PKGS=""; for d in "${SOURCE_FOLDER}"/*; do [ -d "$$d" ] && PKGS="$$PKGS $$(basename "$$d")"; done; \ - if [ -z "$$PKGS" ]; then \ - printf "${YELLOW}[WARN] No packages found under ${SOURCE_FOLDER}, skipping docs${RESET}\n"; \ - else \ - TEMPLATE_ARG=""; \ - if [ -d "${PDOC_TEMPLATE_DIR}" ]; then \ - TEMPLATE_ARG="-t ${PDOC_TEMPLATE_DIR}"; \ - printf "${BLUE}[INFO] Using pdoc templates from ${PDOC_TEMPLATE_DIR}${RESET}\n"; \ - fi; \ - DOCFORMAT="$(DOCFORMAT)"; \ - if [ -z "$$DOCFORMAT" ]; then \ - if [ -f "ruff.toml" ]; then \ - DOCFORMAT=$$(${UV_BIN} run python -c "import tomllib; print(tomllib.load(open('ruff.toml', 'rb')).get('lint', {}).get('pydocstyle', {}).get('convention', ''))"); \ - fi; \ - if [ -z "$$DOCFORMAT" ]; then \ - DOCFORMAT="google"; \ - fi; \ - printf "${BLUE}[INFO] Detected docformat: $$DOCFORMAT${RESET}\n"; \ - else \ - printf "${BLUE}[INFO] Using provided docformat: $$DOCFORMAT${RESET}\n"; \ - fi; \ - LOGO_ARG=""; \ - if [ -n "$(LOGO_FILE)" ]; then \ - if [ -f "$(LOGO_FILE)" ]; then \ - MIME=$$(file --mime-type -b "$(LOGO_FILE)"); \ - DATA=$$(base64 < "$(LOGO_FILE)" | tr -d '\n'); \ - LOGO_ARG="--logo data:$$MIME;base64,$$DATA"; \ - printf "${BLUE}[INFO] Embedding logo: $(LOGO_FILE)${RESET}\n"; \ - else \ - printf "${YELLOW}[WARN] Logo file $(LOGO_FILE) not found, skipping${RESET}\n"; \ - fi; \ - fi; \ - ${UV_BIN} pip install pdoc && \ - PYTHONPATH="${SOURCE_FOLDER}" ${UV_BIN} run pdoc --docformat $$DOCFORMAT --output-dir _pdoc $$TEMPLATE_ARG $$LOGO_ARG $$PKGS; \ - fi; \ - else \ - printf "${YELLOW}[WARN] Source folder ${SOURCE_FOLDER} not found, skipping docs${RESET}\n"; \ - fi - -# The 'book' target assembles the final documentation book. -# 1. Aggregates API docs, coverage, test reports, and notebooks into _book. -# 2. Generates links.json to define the book structure. -# 3. Uses 'minibook' to compile the final HTML site. -book:: install-uv ## compile the companion book - @printf "${BLUE}[INFO] Building combined documentation...${RESET}\n" - - # Run docs but don't fail book if docs fail or are unavailable - @$(MAKE) docs || printf "${YELLOW}[WARN] Docs failed or unavailable, continuing with book generation${RESET}\n" - - # Run marimushka but don't fail book if marimushka fails or is unavailable - @$(MAKE) marimushka || printf "${YELLOW}[WARN] Marimushka failed or unavailable, continuing with book generation${RESET}\n" - - # Run tests but don't fail book if tests fail or are unavailable - @$(MAKE) test || printf "${YELLOW}[WARN] Tests failed or unavailable, continuing with book generation${RESET}\n" - - @rm -rf _book && mkdir -p _book - - @if [ -f "_tests/coverage.json" ]; then \ - printf "${BLUE}[INFO] Generating coverage badge JSON...${RESET}\n"; \ - mkdir -p _book/tests; \ - ${UV_BIN} run python -c "\ -import json; \ -data = json.load(open('_tests/coverage.json')); \ -pct = int(data['totals']['percent_covered']); \ -color = 'brightgreen' if pct >= 90 else 'green' if pct >= 80 else 'yellow' if pct >= 70 else 'orange' if pct >= 60 else 'red'; \ -badge = {'schemaVersion': 1, 'label': 'coverage', 'message': f'{pct}%', 'color': color}; \ -json.dump(badge, open('_book/tests/coverage-badge.json', 'w'))"; \ - printf "${BLUE}[INFO] Coverage badge JSON:${RESET}\n"; \ - cat _book/tests/coverage-badge.json; \ - printf "\n"; \ - else \ - printf "${YELLOW}[WARN] No coverage.json found, skipping badge generation${RESET}\n"; \ - fi - - @printf "{\n" > _book/links.json - @first=1; \ - for entry in $(BOOK_SECTIONS); do \ - name=$${entry%%|*}; \ - rest=$${entry#*|}; \ - src_index=$${rest%%|*}; rest=$${rest#*|}; \ - book_index=$${rest%%|*}; rest=$${rest#*|}; \ - src_dir=$${rest%%|*}; book_dir=$${rest#*|}; \ - if [ -f "$$src_index" ]; then \ - printf "${BLUE}[INFO] Adding $$name...${RESET}\n"; \ - mkdir -p "_book/$$book_dir"; \ - cp -r "$$src_dir/"* "_book/$$book_dir"; \ - if [ $$first -eq 0 ]; then \ - printf ",\n" >> _book/links.json; \ - fi; \ - printf " \"%s\": \"./%s\"" "$$name" "$$book_index" >> _book/links.json; \ - first=0; \ - else \ - printf "${YELLOW}[WARN] Missing $$name, skipping${RESET}\n"; \ - fi; \ - done; \ - printf "\n}\n" >> _book/links.json - - @printf "${BLUE}[INFO] Generated links.json:${RESET}\n" - @cat _book/links.json - - @TEMPLATE_ARG=""; \ - if [ -f "$(BOOK_TEMPLATE)" ]; then \ - TEMPLATE_ARG="--template $(BOOK_TEMPLATE)"; \ - printf "${BLUE}[INFO] Using book template $(BOOK_TEMPLATE)${RESET}\n"; \ - fi; \ - "$(UVX_BIN)" minibook \ - --title "$(BOOK_TITLE)" \ - --subtitle "$(BOOK_SUBTITLE)" \ - $$TEMPLATE_ARG \ - --links "$$(python3 -c 'import json;print(json.dumps(json.load(open("_book/links.json"))))')" \ - --output "_book" - - @touch "_book/.nojekyll" diff --git a/book/marimo/marimo.mk b/book/marimo/marimo.mk deleted file mode 100644 index 25975735..00000000 --- a/book/marimo/marimo.mk +++ /dev/null @@ -1,67 +0,0 @@ -## Makefile.marimo - Marimo notebook targets -# This file is included by the main Makefile - -# Declare phony targets (they don't produce files) -.PHONY: marimo-validate marimo marimushka - -##@ Marimo Notebooks -marimo-validate: install ## validate all Marimo notebooks can run - @printf "${BLUE}[INFO] Validating all notebooks in ${MARIMO_FOLDER}...${RESET}\n" - @if [ ! -d "${MARIMO_FOLDER}" ]; then \ - printf "${YELLOW}[WARN] Directory '${MARIMO_FOLDER}' does not exist. Skipping validation.${RESET}\n"; \ - else \ - failed=0; \ - for notebook in ${MARIMO_FOLDER}/*.py; do \ - if [ -f "$$notebook" ]; then \ - notebook_name=$$(basename "$$notebook"); \ - printf "${BLUE}[INFO] Validating $$notebook_name...${RESET}\n"; \ - if ${UV_BIN} run "$$notebook" > /dev/null 2>&1; then \ - printf "${GREEN}[SUCCESS] $$notebook_name is valid${RESET}\n"; \ - else \ - printf "${RED}[ERROR] $$notebook_name failed validation${RESET}\n"; \ - failed=$$((failed + 1)); \ - fi; \ - fi; \ - done; \ - if [ $$failed -eq 0 ]; then \ - printf "${GREEN}[SUCCESS] All notebooks validated successfully${RESET}\n"; \ - else \ - printf "${RED}[ERROR] $$failed notebook(s) failed validation${RESET}\n"; \ - exit 1; \ - fi; \ - fi - -marimo: install ## fire up Marimo server - @if [ ! -d "${MARIMO_FOLDER}" ]; then \ - printf " ${YELLOW}[WARN] Marimo folder '${MARIMO_FOLDER}' not found, skipping start${RESET}\n"; \ - else \ - ${UV_BIN} run --with marimo marimo edit --no-token --headless "${MARIMO_FOLDER}"; \ - fi - -# The 'marimushka' target exports Marimo notebooks (.py files) to static HTML. -# 1. Detects notebooks in the MARIMO_FOLDER. -# 2. Converts them using 'marimushka export'. -# 3. Generates a placeholder index.html if no notebooks are found. -marimushka:: install-uv ## export Marimo notebooks to HTML - # Clean up previous marimushka output - rm -rf "${MARIMUSHKA_OUTPUT}"; - - @printf "${BLUE}[INFO] Exporting notebooks from ${MARIMO_FOLDER}...${RESET}\n" - @if [ ! -d "${MARIMO_FOLDER}" ]; then \ - printf "${YELLOW}[WARN] Directory '${MARIMO_FOLDER}' does not exist. Skipping marimushka.${RESET}\n"; \ - else \ - mkdir -p "${MARIMUSHKA_OUTPUT}"; \ - if ! ls "${MARIMO_FOLDER}"/*.py >/dev/null 2>&1; then \ - printf "${YELLOW}[WARN] No Python files found in '${MARIMO_FOLDER}'.${RESET}\n"; \ - printf '%s\n' 'Marimo Notebooks' \ - '

Marimo Notebooks

No notebooks found.

' \ - > "${MARIMUSHKA_OUTPUT}/index.html"; \ - else \ - CURRENT_DIR=$$(pwd); \ - OUTPUT_DIR="$$CURRENT_DIR/${MARIMUSHKA_OUTPUT}"; \ - cd "${MARIMO_FOLDER}" && \ - UVX_DIR=$$(dirname "$$(command -v uvx || echo "${INSTALL_DIR}/uvx")") && \ - "${UVX_BIN}" "marimushka>=0.1.9" export --notebooks "." --output "$$OUTPUT_DIR" --bin-path "$$UVX_DIR" && \ - cd "$$CURRENT_DIR"; \ - fi; \ - fi diff --git a/book/marimo/notebooks/rhiza.py b/book/marimo/notebooks/rhiza.py deleted file mode 100644 index 8167c6c9..00000000 --- a/book/marimo/notebooks/rhiza.py +++ /dev/null @@ -1,629 +0,0 @@ -"""Marimo Showcase Notebook - Demonstrating Key Features. - -This notebook showcases the most useful features of Marimo, including: -- Interactive UI elements (sliders, dropdowns, text inputs) -- Reactive programming (automatic cell updates) -- Data visualisation with popular libraries -- Markdown and LaTeX support -- Layout components (columns, tabs, accordions) -- Forms and user input handling -- Dynamic content generation - -Run this notebook with: marimo edit rhiza.py -Or in the rhiza project: make marimo -""" - -# /// script -# requires-python = ">=3.11" -# dependencies = [ -# "marimo==0.18.4", -# "numpy>=1.24.0", -# "plotly>=5.18.0", -# "pandas>=2.0.0", -# ] -# /// - -import marimo - -__generated_with = "0.18.4" -app = marimo.App(width="medium") - -with app.setup: - import marimo as mo - import numpy as np - import pandas as pd - import plotly.graph_objects as go - - -@app.cell -def cell_02(): - """Render the showcase introduction Markdown content.""" - mo.md( - r""" - # 🎨 Marimo Showcase - - Welcome to the **Marimo Showcase Notebook**! This interactive notebook demonstrates - the most powerful and useful features of [Marimo](https://marimo.io/). - - **Marimo** is a reactive Python notebook that combines the best of Jupyter notebooks - with the power of reactive programming. Every cell automatically updates when its - dependencies change, creating a seamless interactive experience. - - ## Why Marimo? - - - ✨ **Reactive by default** - No manual cell re-runs - - 🎯 **Pure Python** - Notebooks are `.py` files - - 🔄 **Reproducible** - Consistent execution order - - 🎨 **Rich UI elements** - Beautiful interactive components - - 📦 **Version control friendly** - Easy to diff and merge - """ - ) - - -@app.cell -def cell_03(): - """Render a horizontal rule to separate sections.""" - mo.md(r"""---""") - - -@app.cell -def cell_04(): - """Introduce the Interactive UI Elements section.""" - mo.md( - r""" - ## 🎚️ Interactive UI Elements - - Marimo provides rich UI components that automatically trigger reactive updates. - """ - ) - - -@app.cell -def cell_05(): - """Create and display a numeric slider UI component.""" - # Slider for numeric input - slider = mo.ui.slider(start=0, stop=100, value=50, label="Adjust the value:", show_value=True) - slider - return (slider,) - - -@app.cell -def cell_06(slider): - """Display the current slider value reactively.""" - mo.md( - f""" - The slider value is: **{slider.value}** - - This text updates automatically when you move the slider! ✨ - """ - ) - - -@app.cell -def cell_07(): - """Create and display a dropdown for language selection.""" - # Dropdown for selection - dropdown = mo.ui.dropdown( - options=["Python", "JavaScript", "Rust", "Go", "TypeScript"], - value="Python", - label="Choose your favorite language:", - ) - dropdown - return (dropdown,) - - -@app.cell -def cell_08(dropdown): - """Display the currently selected language from the dropdown.""" - mo.md( - f""" - You selected: **{dropdown.value}** 🎉 - - Great choice! {dropdown.value} is an excellent programming language. - """ - ) - - -@app.cell -def cell_09(): - """Create and display a text input field for the user's name.""" - # Text input - text_input = mo.ui.text(value="Marimo", label="Enter your name:", placeholder="Type something...") - text_input - return (text_input,) - - -@app.cell -def cell_10(text_input): - """Display a personalized greeting using the current text input value.""" - mo.md(f"""Hello, **{text_input.value}**! 👋""") - - -@app.cell -def cell_11(): - """Render a horizontal rule to visually separate sections.""" - mo.md(r"""---""") - - -@app.cell -def cell_12(): - """Introduce the Data Visualisation section.""" - mo.md( - r""" - ## 📊 Data Visualisation - - Marimo works seamlessly with popular visualisation libraries like Plotly, - Altair, and Matplotlib. Let's create interactive plots! - """ - ) - - -@app.cell -def cell_14(): - """Create sliders for wave frequency and amplitude controls for the plot.""" - # Interactive controls for the plot - frequency_slider = mo.ui.slider(start=1, stop=10, value=2, label="Wave frequency:", show_value=True) - - amplitude_slider = mo.ui.slider(start=1, stop=5, value=1, label="Wave amplitude:", show_value=True) - - mo.vstack([frequency_slider, amplitude_slider]) - return amplitude_slider, frequency_slider - - -@app.cell -def cell_15(amplitude_slider, frequency_slider): - """Build a reactive Plotly sine wave based on the slider values.""" - # Generate reactive plot based on slider values - x = np.linspace(0, 4 * np.pi, 1000) - y = amplitude_slider.value * np.sin(frequency_slider.value * x) - - fig = go.Figure() - fig.add_trace(go.Scatter(x=x, y=y, mode="lines", line={"color": "#2FA4A9", "width": 2}, name="Sine Wave")) - - fig.update_layout( - title=f"Sine Wave: y = {amplitude_slider.value} × sin({frequency_slider.value}x)", - xaxis_title="x", - yaxis_title="y", - template="plotly_white", - height=400, - showlegend=False, - ) - - fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor="rgba(128,128,128,0.2)") - fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor="rgba(128,128,128,0.2)") - - mo.vstack( - [ - mo.md( - f""" - ### Interactive Sine Wave - - Adjust the sliders above to change the wave properties! - - Current parameters: - - Frequency: {frequency_slider.value} - - Amplitude: {amplitude_slider.value} - """ - ), - mo.ui.plotly(fig), - ] - ) - return fig, x, y - - -@app.cell -def cell_16(): - """Render a horizontal rule to visually separate sections.""" - mo.md(r"""---""") - - -@app.cell -def cell_17(): - """Introduce the DataFrames section.""" - mo.md( - r""" - ## 📋 Working with DataFrames - - Marimo provides excellent support for working with Pandas DataFrames. - """ - ) - - -@app.cell -def cell_18(): - """Create a sample Pandas DataFrame for use in subsequent cells.""" - # Create sample data - data = pd.DataFrame( - { - "Product": ["Widget A", "Widget B", "Widget C", "Widget D", "Widget E"], - "Sales": [250, 180, 420, 350, 290], - "Revenue": [5000, 3600, 8400, 7000, 5800], - "Rating": [4.5, 4.2, 4.8, 4.6, 4.3], - } - ) - return data - - -@app.cell -def cell_19(): - """Render introductory text for the sample sales dataset.""" - mo.md( - r""" - ### Sample Sales Data - - Here's our dataset displayed as an interactive table: - """ - ) - - -@app.cell -def cell_20(data): - """Display the sample dataset as an interactive table.""" - # Display as interactive table - mo.ui.table(data) - - -@app.cell -def cell_21(data): - """Render a Plotly bar chart showing sales by product.""" - # Create a bar chart with Plotly - colours = ["#2FA4A9", "#3FB5BA", "#4FC6CB", "#5FD7DC", "#6FE8ED"] - - fig_bar = go.Figure() - fig_bar.add_trace( - go.Bar( - x=data["Product"], - y=data["Sales"], - marker_color=colours, - text=data["Sales"], - textposition="auto", - ) - ) - - fig_bar.update_layout( - title="Sales by Product", - xaxis_title="Product", - yaxis_title="Sales", - template="plotly_white", - height=500, - showlegend=False, - ) - - mo.ui.plotly(fig_bar) - return colours, fig_bar - - -@app.cell -def cell_22(): - """Render a horizontal rule to visually separate sections.""" - mo.md(r"""---""") - - -@app.cell -def cell_23(): - """Introduce the layout components section.""" - mo.md( - r""" - ## 🎯 Layout Components - - Marimo provides powerful layout primitives to organise your content. - """ - ) - - -@app.cell -def cell_24(): - """Demonstrate a two-column layout with left and right content.""" - # Using columns for side-by-side layout - left_content = mo.md( - r""" - ### Left Column - - This is the left side of a two-column layout. - - - Feature 1 - - Feature 2 - - Feature 3 - """ - ) - - right_content = mo.md( - r""" - ### Right Column - - This is the right side of a two-column layout. - - - Benefit A - - Benefit B - - Benefit C - """ - ) - - mo.hstack([left_content, right_content], justify="space-between") - return left_content, right_content - - -@app.cell -def cell_25(): - """Demonstrate tabs with Introduction, Details, and Summary content.""" - # Using tabs for organised content - tab1 = mo.md( - r""" - ## Tab 1: Introduction - - This is the content of the first tab. Tabs are great for organising - related content without cluttering the interface. - - **Key points:** - - Clean organisation - - Reduced clutter - - Easy navigation - """ - ) - - tab2 = mo.md( - r""" - ## Tab 2: Details - - Here's more detailed information in the second tab. - - You can include any content here: - - Code examples - - Visualisations - - Interactive elements - """ - ) - - tab3 = mo.md( - r""" - ## Tab 3: Summary - - The final tab with summary information. - - Tabs are perfect for: - 1. Step-by-step guides - 2. Different views of data - 3. Organising complex notebooks - """ - ) - - mo.ui.tabs({"Introduction": tab1, "Details": tab2, "Summary": tab3}) - return tab1, tab2, tab3 - - -@app.cell -def cell_26(): - """Render a horizontal rule to visually separate sections.""" - mo.md(r"""---""") - - -@app.cell -def cell_27(): - """Introduce the forms and user input section.""" - mo.md( - r""" - ## 📝 Forms and User Input - - Marimo forms allow you to batch multiple inputs and submit them together. - """ - ) - - -@app.cell -def cell_28(): - """Build and display a multi-input form for collecting user information.""" - # Create a form with multiple inputs - form = mo.ui.dictionary( - { - "name": mo.ui.text(label="Your name:", placeholder="Enter name"), - "age": mo.ui.slider(start=18, stop=100, value=25, label="Your age:"), - "email": mo.ui.text(label="Email:", placeholder="email@example.com"), - "subscribe": mo.ui.checkbox(label="Subscribe to newsletter"), - "interests": mo.ui.multiselect( - options=["Data Science", "Machine Learning", "Web Development", "DevOps"], label="Your interests:" - ), - } - ) - mo.vstack([mo.md("### User Information Form"), form]) - return (form,) - - -@app.cell -def cell_29(form): - """Display current form values reactively as the user edits the form.""" - # Display form values - updates reactively as you type/change values - if form.value and any(form.value.values()): - interests_text = ", ".join(form.value["interests"]) if form.value["interests"] else "None selected" - - mo.md( - f""" - ### Current Form Values ✅ - - The values update automatically as you interact with the form! - - - **Name:** {form.value["name"] or "(not entered)"} - - **Age:** {form.value["age"]} - - **Email:** {form.value["email"] or "(not entered)"} - - **Newsletter:** {"Subscribed ✅" if form.value["subscribe"] else "Not subscribed"} - - **Interests:** {interests_text} - - Notice how the values update reactively as you change them! - """ - ) - else: - mo.md("*The form values will appear here as you interact with them.*") - - -@app.cell -def cell_30(): - """Render a horizontal rule to visually separate sections.""" - mo.md(r"""---""") - - -@app.cell -def cell_31(): - """Introduce the Markdown and LaTeX support section.""" - mo.md( - r""" - ## 🎓 Markdown & LaTeX Support - - Marimo has excellent support for rich text formatting using Markdown and LaTeX. - """ - ) - - -@app.cell -def cell_32(): - """Render rich Markdown with LaTeX equations, code blocks, and formatting examples.""" - mo.md( - r""" - ### Mathematical Equations - - You can write beautiful equations using LaTeX: - - **Pythagorean theorem:** - - $$a^2 + b^2 = c^2$$ - - **Euler's identity:** - - $$e^{i\pi} + 1 = 0$$ - - **Quadratic formula:** - - $$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$$ - - **Inline math:** The famous $E = mc^2$ equation by Einstein. - - ### Code Blocks - - You can also include syntax-highlighted code: - - ```python - def fibonacci(n): - if n <= 1: - return n - return fibonacci(n-1) + fibonacci(n-2) - ``` - - ### Rich Formatting - - - **Bold text** - - *Italic text* - - ~~Strikethrough text~~ - - `Inline code` - - [Links](https://marimo.io/) - - > This is a blockquote with important information! - """ - ) - - -@app.cell -def cell_33(): - """Render a horizontal rule to visually separate sections.""" - mo.md(r"""---""") - - -@app.cell -def cell_34(): - """Introduce the Advanced Features section.""" - mo.md( - r""" - ## 🎪 Advanced Features - - Here are some more advanced Marimo features worth exploring. - """ - ) - - -@app.cell -def cell_35(): - """Render an informational callout about Marimo notebooks being plain Python files.""" - # Callout boxes for important information - mo.callout( - mo.md( - r""" - ### 💡 Pro Tip - - Marimo notebooks are **just Python files**! This means: - - Easy version control with Git - - Standard code review workflows - - No hidden JSON metadata - - Compatible with all Python tools - """ - ), - kind="info", - ) - - -@app.cell -def cell_36(): - """Display an accordion with notes on reactivity, performance, and dependencies.""" - # Accordion for collapsible content - mo.accordion( - { - "🔍 Click to learn about Reactive Programming": mo.md( - r""" - Marimo uses **reactive programming** to automatically track dependencies - between cells. When you change a value in one cell, all dependent cells - automatically update! - - This eliminates the common notebook problem of running cells out of order. - """ - ), - "🚀 Click to learn about Performance": mo.md( - r""" - Marimo only re-runs cells that are affected by changes, making it - efficient even for large notebooks. This intelligent execution means - you get fast feedback without wasting computation. - """ - ), - "📦 Click to learn about Dependencies": mo.md( - r""" - You can specify dependencies right in the notebook using inline metadata. - This makes notebooks self-contained and reproducible, as seen in the - header of this notebook! - """ - ), - } - ) - - -@app.cell -def cell_37(): - """Render a horizontal rule to visually separate sections.""" - mo.md(r"""---""") - - -@app.cell -def cell_38(): - """Render the conclusion section of the Marimo showcase notebook.""" - mo.md( - r""" - ## 🎉 Conclusion - - This notebook has demonstrated many of Marimo's most useful features: - - ✅ **Interactive UI elements** - Sliders, dropdowns, text inputs, and more - ✅ **Reactive programming** - Automatic cell updates when dependencies change - ✅ **Data visualisation** - Seamless integration with Plotly, Matplotlib, etc. - ✅ **Layout components** - Columns, tabs, accordions for organising content - ✅ **Forms** - Batched input collection with submission - ✅ **Rich formatting** - Markdown and LaTeX support - ✅ **Pure Python** - Notebooks are version-control friendly `.py` files - - ### Next Steps - - To learn more about Marimo: - - Visit the [official documentation](https://docs.marimo.io/) - - Explore the [example gallery](https://marimo.io/examples) - - Join the [community Discord](https://discord.gg/JE7nhX6mD8) - - **Happy exploring! 🚀** - """ - ) - - -if __name__ == "__main__": - app.run() diff --git a/book/minibook-templates/custom.html.jinja2 b/book/minibook-templates/custom.html.jinja2 deleted file mode 100644 index eda66e9b..00000000 --- a/book/minibook-templates/custom.html.jinja2 +++ /dev/null @@ -1,210 +0,0 @@ - - - - - - {{ title }} - - - - - - -
-
- - -
- Logo -
- - -
-
-

{{ title }}

- {% if description %} -
{{ description }}
- {% endif %} -
-
- - - - - - Home Repo - -
-
- -
- {% for name, url in links %} - - {% endfor %} -
- - -
-
- - \ No newline at end of file diff --git a/docs/assets/rhiza-logo.svg b/docs/assets/rhiza-logo.svg new file mode 100644 index 00000000..ff1c9f57 --- /dev/null +++ b/docs/assets/rhiza-logo.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/book/marimo/README.md b/docs/development/MARIMO.md similarity index 95% rename from book/marimo/README.md rename to docs/development/MARIMO.md index d9eb024c..df69cce7 100644 --- a/book/marimo/README.md +++ b/docs/development/MARIMO.md @@ -33,14 +33,14 @@ From the repository root: make marimo ``` -This will start the Marimo server and open all notebooks in the `book/marimo` directory. +This will start the Marimo server and open all notebooks in the `docs/notebooks` directory. ### Running a Specific Notebook To run a single notebook: ```bash -marimo edit book/marimo/rhiza.py +marimo edit docs/notebooks/rhiza.py ``` ### Using uv (Recommended) @@ -48,7 +48,7 @@ marimo edit book/marimo/rhiza.py The notebooks include inline dependency metadata, making them self-contained: ```bash -uv run book/marimo/rhiza.py +uv run docs/notebooks/rhiza.py ``` This will automatically install the required dependencies and run the notebook. @@ -100,7 +100,7 @@ To create a new Marimo notebook: 1. Create a new `.py` file in this directory: ```bash - marimo edit book/marimo/my_notebook.py + marimo edit docs/notebooks/my_notebook.py ``` 2. Add inline metadata at the top: @@ -118,7 +118,7 @@ To create a new Marimo notebook: 4. Test it runs in a clean environment: ```bash - uv run book/marimo/my_notebook.py + uv run docs/notebooks/my_notebook.py ``` 5. Commit and push - the CI will validate it automatically diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..0e0b6b49 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,2 @@ +--8<-- "README.md" + diff --git a/docs/mkdocs-base.yml b/docs/mkdocs-base.yml new file mode 100644 index 00000000..6d7143d9 --- /dev/null +++ b/docs/mkdocs-base.yml @@ -0,0 +1,71 @@ +# docs/mkdocs-base.yml — Base MkDocs configuration for rhiza-based projects. +# +# USAGE (standalone) +# Build or serve this file directly if you have no root-level mkdocs.yml: +# uvx --with mkdocs-material mkdocs serve -f docs/mkdocs-base.yml +# The rhiza build system (make book) will pick this file up automatically +# as a fallback when no root-level mkdocs.yml is found. +# +# USAGE (with INHERIT) +# To extend this config from a root-level mkdocs.yml, add the following +# at the top of your mkdocs.yml and override only what you need: +# +# INHERIT: docs/mkdocs-base.yml +# +# site_name: My Project +# site_url: https://example.github.io/my-project/ +# repo_url: https://github.com/example/my-project +# repo_name: example/my-project +# +# docs_dir: docs # always set this explicitly — base uses docs_dir: . which +# # resolves relative to docs/mkdocs-base.yml, not mkdocs.yml +# +# nav: # nav is fully replaced — not merged — by the child config +# - Home: index.md +# - Guide: guide.md +# +# Any key you omit in mkdocs.yml is inherited from this file. +# The 'nav' key is always fully replaced when defined in the child. + +site_name: Rhiza +docs_dir: . +site_dir: _mkdocs + +theme: + name: material + features: + - navigation.tabs + - navigation.sections + - navigation.top + - search.suggest + - search.highlight + logo: assets/rhiza-logo.svg + favicon: assets/rhiza-logo.svg + +plugins: + - search + +markdown_extensions: + - md_in_html + - pymdownx.highlight + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.snippets: + base_path: ["."] + - admonition + - toc: + permalink: true + toc_depth: 3 + +extra_javascript: + - https://unpkg.com/mermaid@11.4.0/dist/mermaid.esm.min.mjs + +# nav is overwritten completely in mkdocs.yml +nav: + - Home: index.md + - Notebooks: notebooks.md + - Reports: reports.md + - Paper: paper/rhiza.pdf diff --git a/renovate.json b/renovate.json deleted file mode 100644 index 2d59f362..00000000 --- a/renovate.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "extends": [ - "config:recommended", - ":enablePreCommit", - ":automergeMinor", - ":dependencyDashboard", - ":maintainLockFilesWeekly", - ":semanticCommits", - ":pinDevDependencies" - ], - "enabledManagers": [ - "pep621", - "pre-commit", - "github-actions", - "devcontainer", - "dockerfile" - ], - "timezone": "Asia/Dubai", - "schedule": [ - "before 10am on tuesday" - ], - "packageRules": [ - { - "matchManagers": ["pep621"], - "matchPackageNames": ["python"], - "enabled": false - } - ] -} diff --git a/ruff.toml b/ruff.toml index f99a9933..4563178d 100644 --- a/ruff.toml +++ b/ruff.toml @@ -69,10 +69,10 @@ extend-select = [ "D107", # pydocstyle - Require docstrings for __init__ "B", # flake8-bugbear - Find likely bugs and design problems "C4", # flake8-comprehensions - Better list/set/dict comprehensions - #"SIM", # flake8-simplify - Simplify code + "SIM", # flake8-simplify - Simplify code "PT", # flake8-pytest-style - Check pytest best practices "RUF", # Ruff-specific rules - #"S", # flake8-bandit - Find security issues + "S", # flake8-bandit - Find security issues #"ERA", # eradicate - Find commented out code #"T10", # flake8-debugger - Check for debugger imports and calls "TRY", # flake8-try-except-raise - Try/except/raise checks @@ -101,25 +101,31 @@ line-ending = "auto" # File-specific rule exceptions [lint.per-file-ignores] -"tests/**/*.py" = [ +# Test files - allow assert statements and subprocess calls for testing +"**/tests/**/*.py" = [ "S101", # Allow assert statements in tests "S603", # Allow subprocess calls without shell=False check + "S607", # Allow starting processes with partial paths in tests "PLW1510", # Allow subprocess without explicit check parameter - "ERA001", # Allow commented out code - "PLR2004", # Allow magic values - "RUF002", # Allow ambiguous unicode - "RUF012", # Allow mutable class attributes ] -# this will also work book/marimo or test/resources/marimo ... -"**/marimo/**/*.py" = [ - "N803", # Allow non-lowercase variable names - "S101", # Allow assert statements - "PLC0415", # Allow imports not at top-level - "B018", # Allow useless expressions - "RUF001", # Allow ambiguous unicode - "RUF002", # Allow ambiguous unicode +"tests/**/*.py" = [ + "ERA001", # Allow commented out code in project tests + "PLR2004", # Allow magic values in project tests + "RUF002", # Allow ambiguous unicode in project tests + "RUF012", # Allow mutable class attributes in project tests +] +# Marimo notebooks - allow flexible coding patterns for interactive exploration +"**/notebooks/*.py" = [ + "D100", # No module docstring - marimo requires `import marimo` as the first statement + "N803", # Allow non-lowercase variable names in notebooks + "S101", # Allow assert statements in notebooks + "PLC0415", # Allow imports not at top-level in notebooks + "B018", # Allow useless expressions in notebooks + "RUF001", # Allow ambiguous unicode in notebooks + "RUF002", # Allow ambiguous unicode in notebooks ] +# Internal utility scripts - specific exceptions for internal tooling ".rhiza/utils/*.py" = [ - "PLW2901", # Allow loop variable overwriting - "TRY003", # Allow long exception messages + "PLW2901", # Allow loop variable overwriting in utility scripts + "TRY003", # Allow long exception messages in utility scripts ] From 05f0476cf635350a724f684a5306c827509ddad1 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Thu, 16 Apr 2026 06:54:15 +0400 Subject: [PATCH 3/5] Remove the README from make.d --- .rhiza/make.d/README.md | 82 ----------------------------------------- 1 file changed, 82 deletions(-) delete mode 100644 .rhiza/make.d/README.md diff --git a/.rhiza/make.d/README.md b/.rhiza/make.d/README.md deleted file mode 100644 index 6f80875a..00000000 --- a/.rhiza/make.d/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# Makefile Cookbook - -This directory (`.rhiza/make.d/`) is the designated place for **repository-specific build logic**. Any `.mk` file added here is automatically absorbed by the main Makefile. - -Use this cookbook to find copy-paste patterns for common development needs. - -## 🥘 Recipes - -### 1. Add a Simple Task -**Goal**: Run a script with `make train-model`. - -Create `.rhiza/make.d/50-model.mk`: -```makefile -##@ Machine Learning -train: ## Train the model using local data - @echo "Training model..." - @uv run python scripts/train.py -``` - -### 2. Inject Code into Standard Workflows (Hooks) -**Goal**: Apply task after `make sync`. - -Create `.rhiza/make.d/90-hooks.mk`: -```makefile -post-sync:: - @echo "Applying something..." -``` -*Note: Use double-colons (`::`) for hooks to avoid conflicts.* - -### 3. Define Global Variables -**Goal**: Set a default timeout for all test runs. - -Create `.rhiza/make.d/01-config.mk`: -```makefile -# Override default timeout (defaults to 60s) -export TEST_TIMEOUT := 120 -``` - -### 4. Create a Private Shortcut -**Goal**: Create a command that only exists on my machine (not committed). - -Do not use `.rhiza/make.d/`. Instead, create a `local.mk` in the project root: -```makefile -deploy-dev: - @./scripts/deploy-to-my-sandbox.sh -``` - -### 5. Install System Dependencies -**Goal**: Ensure `graphviz` is installed for Marimo notebooks using a hook. - -Create `.rhiza/make.d/20-dependencies.mk`: -```makefile -pre-install:: - @if ! command -v dot >/dev/null 2>&1; then \ - echo "Graphviz not found. Installing..."; \ - if command -v brew >/dev/null 2>&1; then \ - brew install graphviz; \ - elif command -v apt-get >/dev/null 2>&1; then \ - sudo apt-get install -y graphviz; \ - else \ - echo "Please install graphviz manually."; \ - exit 1; \ - fi \ - fi -``` - ---- - -## ℹ️ Reference - -### Execution Order -Files are loaded alphabetically. We use numeric prefixes to ensure dependencies resolve correctly: -- `00-19`: Configuration & Variables -- `20-79`: Custom Tasks & Rules -- `80-99`: Hooks & Lifecycle logic - -### Available Hooks -- `pre-install` / `post-install`: Runs around `make install`. -- `pre-sync` / `post-sync`: Runs around repository synchronization. -- `pre-validate` / `post-validate`: Runs around validation checks. -- `pre-release` / `post-release`: Runs around release process. -- `pre-bump` / `post-bump`: Runs around version bumping. From 3865e437e426c5ff8484cdeb25a40846445737c4 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Thu, 16 Apr 2026 06:58:27 +0400 Subject: [PATCH 4/5] moving documents into place --- book/docs/index.md | 1 - {book/docs => docs}/paper/1310.3396.pdf | Bin {book/docs => docs}/paper/SSRN-id2695101.pdf | Bin {book/docs => docs}/talk/abstract.txt | 0 {book/docs => docs}/talk/bio.txt | 0 {book/docs => docs}/talk/title.txt | 0 6 files changed, 1 deletion(-) delete mode 120000 book/docs/index.md rename {book/docs => docs}/paper/1310.3396.pdf (100%) rename {book/docs => docs}/paper/SSRN-id2695101.pdf (100%) rename {book/docs => docs}/talk/abstract.txt (100%) rename {book/docs => docs}/talk/bio.txt (100%) rename {book/docs => docs}/talk/title.txt (100%) diff --git a/book/docs/index.md b/book/docs/index.md deleted file mode 120000 index fe840054..00000000 --- a/book/docs/index.md +++ /dev/null @@ -1 +0,0 @@ -../../README.md \ No newline at end of file diff --git a/book/docs/paper/1310.3396.pdf b/docs/paper/1310.3396.pdf similarity index 100% rename from book/docs/paper/1310.3396.pdf rename to docs/paper/1310.3396.pdf diff --git a/book/docs/paper/SSRN-id2695101.pdf b/docs/paper/SSRN-id2695101.pdf similarity index 100% rename from book/docs/paper/SSRN-id2695101.pdf rename to docs/paper/SSRN-id2695101.pdf diff --git a/book/docs/talk/abstract.txt b/docs/talk/abstract.txt similarity index 100% rename from book/docs/talk/abstract.txt rename to docs/talk/abstract.txt diff --git a/book/docs/talk/bio.txt b/docs/talk/bio.txt similarity index 100% rename from book/docs/talk/bio.txt rename to docs/talk/bio.txt diff --git a/book/docs/talk/title.txt b/docs/talk/title.txt similarity index 100% rename from book/docs/talk/title.txt rename to docs/talk/title.txt From 0617001b9eecde69a271faf868f824ba4fb87c4a Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Thu, 16 Apr 2026 06:59:53 +0400 Subject: [PATCH 5/5] reduced Makefile --- Makefile | 42 ------------------------------------------ 1 file changed, 42 deletions(-) diff --git a/Makefile b/Makefile index 24239e6f..b5fd62f1 100644 --- a/Makefile +++ b/Makefile @@ -1,49 +1,7 @@ ## Makefile (repo-owned) # Keep this file small. It can be edited without breaking template sync. -DOCFORMAT=google -DEFAULT_AI_MODEL=claude-sonnet-4.5 LOGO_FILE=.rhiza/assets/rhiza-logo.svg -GH_AW_ENGINE ?= copilot # Default AI engine for gh-aw workflows (copilot, claude, or codex) # Always include the Rhiza API (template-managed) include .rhiza/rhiza.mk - -# Optional: developer-local extensions (not committed) --include local.mk - -# Wire typecheck into make validate -post-validate:: - @$(MAKE) typecheck - -## Custom targets - -.PHONY: adr -adr: install-gh-aw ## Create a new Architecture Decision Record (ADR) using AI assistance - @echo "Creating a new ADR..." - @echo "This will trigger the adr-create workflow." - @echo "" - @read -p "Enter ADR title (e.g., 'Use PostgreSQL for data storage'): " title; \ - echo ""; \ - read -p "Enter brief context (optional, press Enter to skip): " context; \ - echo ""; \ - if [ -z "$$title" ]; then \ - echo "Error: Title is required"; \ - exit 1; \ - fi; \ - if [ -z "$$context" ]; then \ - gh workflow run adr-create.md -f title="$$title"; \ - else \ - gh workflow run adr-create.md -f title="$$title" -f context="$$context"; \ - fi; \ - echo ""; \ - echo "✅ ADR creation workflow triggered!"; \ - echo ""; \ - echo "The workflow will:"; \ - echo " 1. Generate the next ADR number"; \ - echo " 2. Create a comprehensive ADR document"; \ - echo " 3. Update the ADR index"; \ - echo " 4. Open a pull request for review"; \ - echo ""; \ - echo "Check workflow status: gh run list --workflow=adr-create.md"; \ - echo "View latest run: gh run view"