From 8fcc8087e70caeda7740a33ae28281c9beac8fdb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 08:33:13 +0000 Subject: [PATCH 01/31] Initial plan From 5761edac2681db18153ea20988703a215b2a5c5b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 08:42:18 +0000 Subject: [PATCH 02/31] Update template-bundles.yml to be language-agnostic Co-authored-by: HarryCampion <40582604+HarryCampion@users.noreply.github.com> --- .rhiza/template-bundles.yml | 228 +++----------- README.md | 568 ++++++++++------------------------- docs/TEMPLATE_DEVELOPMENT.md | 385 ++++++++++++++++++++++++ pyproject.toml | 15 +- uv.lock | 4 +- 5 files changed, 610 insertions(+), 590 deletions(-) create mode 100644 docs/TEMPLATE_DEVELOPMENT.md diff --git a/.rhiza/template-bundles.yml b/.rhiza/template-bundles.yml index e29fdeb..49665d1 100644 --- a/.rhiza/template-bundles.yml +++ b/.rhiza/template-bundles.yml @@ -1,34 +1,34 @@ -# Rhiza Template Bundle Definitions +# Rhiza Core Template Bundle Definitions # -# This file defines template bundles - pre-configured sets of files that can be -# included in downstream projects by selecting templates instead of listing -# individual file paths. +# This file defines LANGUAGE-AGNOSTIC template bundles that can be inherited +# by language-specific Rhiza templates (rhiza, rhiza-go, etc.). # -# Usage in downstream projects (.rhiza/template.yml): +# Usage in language-specific templates (.rhiza/template.yml): +# +# repository: Jebel-Quant/rhiza-core +# ref: v0.1.0 # # templates: -# - tests +# - core +# - github # - docker -# - marimo -# -# Instead of manually listing: +# - lfs # -# include: | -# .rhiza/make.d/test.mk -# pytest.ini -# docker/Dockerfile -# ... +# Language-specific templates should define their own bundles for: +# - Testing frameworks (pytest, go test, cargo test, etc.) +# - Quality tools (ruff, golangci-lint, clippy, etc.) +# - Build systems (uv, cargo, go modules, npm, etc.) # Schema version for this bundles file format -version: "0.7.1" +version: "0.1.0" # Bundle Definitions bundles: # ============================================================================ - # CORE - Required infrastructure + # CORE - Required language-agnostic infrastructure # ============================================================================ core: - description: "Core Rhiza infrastructure" + description: "Core language-agnostic Rhiza infrastructure" required: true standalone: true files: @@ -39,49 +39,35 @@ bundles: - .rhiza/.gitignore - .rhiza/.rhiza-version - .rhiza/make.d/custom-env.mk - - .rhiza/make.d/docs.mk - .rhiza/make.d/custom-task.mk - - .rhiza/make.d/quality.mk - - .rhiza/make.d/bootstrap.mk - - .rhiza/make.d/releasing.mk - .rhiza/make.d/README.md - .rhiza/scripts - .rhiza/docs - .rhiza/assets - - .rhiza/requirements/README.md - - .rhiza/requirements/docs.txt - - .rhiza/requirements/tools.txt - # Root configuration files - - Makefile - - .pre-commit-config.yaml + # Root configuration files (language-agnostic) - .editorconfig - .gitignore - - .python-version - - ruff.toml - # Documentation files (user-facing docs) - - docs/SECURITY.md - - docs/ARCHITECTURE.md - - docs/CUSTOMIZATION.md - - docs/GLOSSARY.md - - docs/QUICK_REFERENCE.md - - docs/DEMO.md + # Documentation files + - docs/TEMPLATE_DEVELOPMENT.md + # ============================================================================ + # GITHUB - GitHub Actions and GitHub CLI helpers + # ============================================================================ github: - description: "GitHub Actions workflows for CI/CD" + description: "GitHub Actions workflows and GitHub CLI helpers (language-agnostic)" standalone: true requires: [core] files: + # Make targets for GitHub CLI - .rhiza/make.d/github.mk - .rhiza/make.d/agentic.mk - # Core GitHub Actions workflows + + # Core GitHub Actions workflows (language-agnostic) - .github/workflows/copilot-setup-steps.yml - .github/workflows/rhiza_validate.yml - .github/workflows/rhiza_sync.yml - - .github/workflows/rhiza_pre-commit.yml - - .github/workflows/rhiza_deptry.yml - - .github/workflows/rhiza_release.yml - .github/actions/configure-git-auth - .github/dependabot.yml - .github/copilot-instructions.md @@ -114,44 +100,19 @@ bundles: - CONTRIBUTING.md - CODE_OF_CONDUCT.md - # ============================================================================ - # DEVCONTAINER - VS Code DevContainer configuration - # ============================================================================ - devcontainer: - description: "VS Code DevContainer configuration for consistent development environments" - standalone: true - requires: [] - files: - # DevContainer configuration - - .devcontainer/devcontainer.json - - .devcontainer/bootstrap.sh - - # Documentation - - docs/DEVCONTAINER.md - - # GitHub Actions workflows - - .github/workflows/rhiza_devcontainer.yml - # ============================================================================ # DOCKER - Docker containerization support # ============================================================================ docker: - description: "Docker containerization support for building and running containers" + description: "Docker containerization support (language-agnostic make targets)" standalone: true requires: [] files: - # Docker configuration and files - - docker/Dockerfile - - docker/Dockerfile.dockerignore - - # Make targets + # Make targets (language-agnostic) - .rhiza/make.d/docker.mk - - # Documentation - - docs/DOCKER.md - - # GitHub Actions workflows - - .github/workflows/rhiza_docker.yml + + # Note: Dockerfile itself is language-specific and should be + # provided by rhiza- templates # ============================================================================ # LFS - Git Large File Storage support @@ -168,121 +129,30 @@ bundles: - .rhiza/docs/LFS.md # ============================================================================ - # PRESENTATION - Presentation building with reveal.js + # BOOK - Documentation book generation (language-agnostic framework) # ============================================================================ - presentation: - description: "Presentation building using reveal.js and Marimo" + book: + description: "Documentation book generation framework using minibook" standalone: true requires: [] - recommends: - - marimo # Presentations often use Marimo for interactive slides files: - - .rhiza/make.d/presentation.mk - - docs/PRESENTATION.md + # Book building configuration (language-agnostic) + - .rhiza/make.d/book.mk + - .rhiza/templates/minibook + + # Note: Language-specific templates should add their own + # documentation requirements and configurations # ============================================================================ - # GITLAB - GitLab CI/CD pipeline configuration + # RELEASE - Release automation and version management # ============================================================================ - gitlab: - description: "GitLab CI/CD pipeline configuration and workflows" + release: + description: "Release automation and version management (language-agnostic framework)" standalone: true requires: [core] - notes: | - GitLab workflows provide similar functionality to GitHub Actions. - Some workflows (like book, ci) may benefit from having their - corresponding feature templates (book, tests) also enabled. - files: - # Main GitLab CI configuration - - .gitlab-ci.yml - - # GitLab workflow files - - .gitlab/workflows/rhiza_book.yml - - .gitlab/workflows/rhiza_ci.yml - - .gitlab/workflows/rhiza_deptry.yml - - .gitlab/workflows/rhiza_pre-commit.yml - - .gitlab/workflows/rhiza_release.yml - - .gitlab/workflows/rhiza_renovate.yml - - .gitlab/workflows/rhiza_sync.yml - - .gitlab/workflows/rhiza_validate.yml - - # GitLab templates - - .gitlab/template - - # GitLab documentation - - .gitlab/COMPARISON.md - - .gitlab/README.md - - .gitlab/SUMMARY.md - - .gitlab/TESTING.md - - # ============================================================================ - # TESTS - Testing infrastructure with pytest, coverage, and type checking - # ============================================================================ - tests: - description: "Testing infrastructure with pytest, coverage, and type checking" - standalone: true - requires: [] - files: - # Make targets and configuration - - .rhiza/make.d/test.mk - - .rhiza/requirements/tests.txt - - pytest.ini - - # Core/generic test files - - .rhiza/tests - - # Benchmark and stress test infrastructure - - tests/benchmarks - - docs/TESTS.md - - # GitHub Actions workflows - - .github/workflows/rhiza_ci.yml - - .github/workflows/rhiza_security.yml - - .github/workflows/rhiza_codeql.yml - - .github/workflows/rhiza_benchmarks.yml - - # ============================================================================ - # MARIMO - Interactive Marimo notebooks - # ============================================================================ - marimo: - description: "Interactive Marimo notebooks for data exploration and documentation" - standalone: true - requires: [] files: - # Marimo configuration - - .rhiza/make.d/marimo.mk - - .rhiza/requirements/marimo.txt - - # Marimo notebooks directory - - book/marimo - - # Documentation - - docs/MARIMO.md - - # GitHub Actions workflows - - .github/workflows/rhiza_marimo.yml - - # ============================================================================ - # BOOK - Documentation book generation - # ============================================================================ - book: - description: | - Comprehensive documentation book generation combining: - - API documentation (pdoc) - - Test coverage reports - - Test results - - Interactive notebooks (if marimo is enabled) - standalone: false - requires: - - tests # Required: book needs test coverage and reports - recommends: - - marimo # Optional: book works better with notebook exports - files: - # Book building configuration - - .rhiza/make.d/book.mk - - .rhiza/templates/minibook - - # Documentation - - docs/BOOK.md - - # GitHub Actions workflows - - .github/workflows/rhiza_book.yml + # Release Make targets + - .rhiza/make.d/releasing.mk + + # Note: Language-specific release workflows (PyPI, Go modules, npm, etc.) + # should be provided by rhiza- templates diff --git a/README.md b/README.md index 1419b3e..de045f8 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,67 @@
-# Rhiza Logo Rhiza -![GitHub Release](https://img.shields.io/github/v/release/jebel-quant/rhiza?sort=semver&color=2FA4A9&label=rhiza) -![Synced with Rhiza](https://img.shields.io/badge/synced%20with-rhiza-2FA4A9?color=2FA4A9) +# Rhiza Logo Rhiza Core [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) -[![Python versions](https://img.shields.io/badge/Python-3.11%20•%203.12%20•%203.13%20•%203.14-blue?logo=python)](https://www.python.org/) -[![CI](https://github.com/Jebel-Quant/rhiza/actions/workflows/rhiza_ci.yml/badge.svg?event=push)](https://github.com/Jebel-Quant/rhiza/actions/workflows/rhiza_ci.yml) -[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg?logo=ruff)](https://github.com/astral-sh/ruff) -[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) -[![CodeFactor](https://www.codefactor.io/repository/github/jebel-quant/rhiza/badge)](https://www.codefactor.io/repository/github/jebel-quant/rhiza) - -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/jebel-quant/rhiza) +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/jebel-quant/rhiza-core) # Strong roots -Creating and maintaining technical harmony across repositories. +The foundational layer for language-agnostic Rhiza templates. -A collection of reusable configuration templates -for modern Python projects. -Save time and maintain consistency across your projects -with these pre-configured templates. +A collection of shared, reusable configuration templates and infrastructure +that powers modern project templates across multiple programming languages. -![Last Updated](https://img.shields.io/github/last-commit/jebel-quant/rhiza/main?label=Last%20updated&color=blue) +![Last Updated](https://img.shields.io/github/last-commit/jebel-quant/rhiza-core/main?label=Last%20updated&color=blue) In the original Greek, spelt **ῥίζα**, pronounced *ree-ZAH*, and having the literal meaning **root**.
-## 🌟 Why Rhiza? +## 🌟 Why Rhiza Core? + +**Rhiza Core** is the foundational layer that powers language-specific Rhiza templates. Instead of duplicating common infrastructure across [rhiza](https://github.com/Jebel-Quant/rhiza) (Python), [rhiza-go](https://github.com/Jebel-Quant/rhiza-go) (Go), and future language templates, Rhiza Core provides: + +- **Shared Makefile Infrastructure** - Language-agnostic build automation +- **GitHub Actions Workflows** - CI/CD templates that work across languages +- **Development Tools** - Common developer experience configurations +- **Release Automation** - Version management and release workflows +- **Documentation Templates** - Standardized project documentation + +Language-specific templates inherit from Rhiza Core and add their own language-specific tooling (like `uv` for Python or `go` toolchain for Go). This separation enables: -**Unlike traditional project templates** (like cookiecutter or copier) that generate a one-time snapshot of configuration files, **Rhiza provides living templates** that evolve with your project. Classic templates help you start a project, but once generated, your configuration drifts away from the template as best practices change. Rhiza takes a different approach: it enables **continuous synchronization**, allowing you to selectively pull template updates into your project over time through automated workflows. This means you can benefit from improvements to CI/CD workflows, linting rules, and development tooling without manually tracking upstream changes. Think of it as keeping your project's foundation fresh and aligned with modern practices, while maintaining full control over what gets updated. +- **Consistency** across all language templates +- **Reduced duplication** of common infrastructure +- **Centralized improvements** that benefit all language templates +- **Faster template development** for new languages + +### Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Rhiza Core │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Language-Agnostic Components │ │ +│ │ • Makefile infrastructure (.rhiza/make.d/) │ │ +│ │ • GitHub Actions workflows (.github/workflows/) │ │ +│ │ • Development configs (.editorconfig, etc.) │ │ +│ │ • Documentation templates │ │ +│ │ • Release automation scripts │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ▲ + │ inherits from + ┌───────────┴───────────┐ + │ │ + ┌───────────▼──────────┐ ┌─────────▼──────────┐ + │ rhiza (Python) │ │ rhiza-go (Go) │ + │ ┌────────────────┐ │ │ ┌──────────────┐ │ + │ │ Python-specific│ │ │ │ Go-specific │ │ + │ │ • uv/pip │ │ │ │ • go tooling │ │ + │ │ • ruff/pytest │ │ │ │ • golangci │ │ + │ │ • pdoc │ │ │ │ • goreleaser │ │ + │ └────────────────┘ │ │ └──────────────┘ │ + └──────────────────────┘ └────────────────────┘ +``` ### How It Works @@ -37,440 +69,167 @@ Rhiza uses a simple configuration file (`.rhiza/template.yml`) to control which ```yaml # .rhiza/template.yml -repository: Jebel-Quant/rhiza -ref: v0.7.1 +repository: Jebel-Quant/rhiza-core +ref: v0.1.0 include: | + .rhiza/make.d/github.mk + .rhiza/make.d/docker.mk .github/workflows/*.yml - .pre-commit-config.yaml - ruff.toml - pytest.ini - Makefile - + .editorconfig + exclude: | .rhiza/scripts/customisations/* ``` **What you're seeing:** -- **`repository`** - The upstream template source (**can be any repository, not just Rhiza!**) -- **`ref`** - Which version tag/branch to sync from (e.g., `v0.7.1` or `main`) -- **`include`** - File patterns to pull from the template (CI workflows, linting configs, etc.) +- **`repository`** - The upstream template source (rhiza-core for shared components) +- **`ref`** - Which version tag/branch to sync from (e.g., `v0.1.0` or `main`) +- **`include`** - File patterns to pull from the template - **`exclude`** - Paths to skip, protecting your customisations -> **💡 Automated Updates:** When using a version tag (e.g., `v0.7.1`) instead of a branch name, Renovate will automatically create pull requests to update the `ref` field when new versions are released. This keeps your templates up-to-date with minimal manual intervention. -> -> To enable this in your project, copy the [`regexManagers` configuration](renovate.json#L31-L40) from this repository's `renovate.json` file into your own Renovate configuration. See the linked configuration for the complete setup. - -When you run `uvx rhiza materialize` or trigger the automated sync workflow, Rhiza fetches only the files matching your `include` patterns, skips anything in `exclude`, and creates a clean diff for you to review. You stay in control of what updates and when. - -**💡 Pro Tip:** While you can use `Jebel-Quant/rhiza` directly, **we recommend creating your own template repository** using GitHub's "Use this template" button. This gives you a clean copy to customise for your organisation's specific needs and constraints—adjusting CI workflows, coding standards, or tooling choices—while still benefiting from Rhiza's sync mechanism. Your template repo becomes your team's source of truth, and you can selectively pull updates from upstream Rhiza when desired. - ## 📚 Table of Contents -- [Why Rhiza?](#-why-rhiza) -- [Quick Start](#-quick-start) -- [What You Get](#-what-you-get) -- [Integration Guide](#-integration-guide) -- [Available Tasks](#-available-tasks) -- [Advanced Topics](#-advanced-topics) -- [CI/CD Support](#-cicd-support) -- [Contributing to Rhiza](#-contributing-to-rhiza) - -## 🚀 Quick Start - -### For New Projects - -Create a new project with Rhiza templates: - -```bash -# Navigate to your project directory -cd /path/to/your/project - -# Initialise Rhiza configuration -uvx rhiza init - -# Edit .rhiza/template.yml to select desired templates -# Then materialize the templates -uvx rhiza materialize -``` - -### For Existing Projects - -Integrate Rhiza into an existing Python project: - -```bash -# Navigate to your repository -cd /path/to/your/project - -# Initialise and configure -uvx rhiza init - -# Review and edit .rhiza/template.yml -# Then apply templates -uvx rhiza materialize -``` - -See the [Integration Guide](#-integration-guide) for detailed instructions and options. - -### For Contributing to Rhiza - -If you want to develop Rhiza itself: - -```bash -# Clone the repository -git clone https://github.com/jebel-quant/rhiza.git -cd rhiza - -# Install dependencies -make install -``` - -## ✨ What You Get - -### Core Features - -- 🚀 **CI/CD Templates** - Ready-to-use GitHub Actions and GitLab CI workflows -- 🧪 **Testing Framework** - Comprehensive test setup with pytest -- 📚 **Documentation** - Automated documentation generation with pdoc and companion books -- 🔍 **Code Quality** - Linting with ruff, formatting, and dependency checking with deptry -- 📝 **Editor Configuration** - Cross-platform .editorconfig for consistent coding style -- 📊 **Marimo Integration** - Interactive notebook support for documentation and exploration -- 🎤 **Presentations** - Generate slides from Markdown using Marp -- 🐳 **Containerization** - Docker and Dev Container configurations - -### Available Templates - -This repository provides a curated set of reusable configuration templates: +- [Why Rhiza Core?](#-why-rhiza-core) +- [Architecture](#architecture) +- [What's Included](#-whats-included) +- [For Template Developers](#-for-template-developers) +- [For End Users](#-for-end-users) +- [Available Components](#-available-components) +- [Contributing](#-contributing) -#### 🌱 Core Project Configuration -- **.gitignore** - Sensible defaults for Python projects -- **.editorconfig** - Editor configuration to enforce consistent coding standards -- **ruff.toml** - Configuration for the Ruff linter and formatter -- **pytest.ini** - Configuration for the `pytest` testing framework -- **Makefile** - Task automation for common development workflows -- **CODE_OF_CONDUCT.md** - Code of conduct for open-source projects -- **CONTRIBUTING.md** - Contributing guidelines +## ✨ What's Included -#### 🔧 Developer Experience -- **.devcontainer/** - Development container setup (VS Code / Dev Containers) -- **.pre-commit-config.yaml** - Pre-commit hooks for code quality -- **docker/** - Example `Dockerfile` and `.dockerignore` +Rhiza Core provides the following language-agnostic components: -#### 🚀 CI/CD & Automation -- **.github/** - GitHub Actions workflows, scripts, and repository templates -- **.gitlab/** - GitLab CI/CD workflows (see [.gitlab/README.md](.gitlab/README.md)) +### Makefile Infrastructure (`.rhiza/make.d/`) -## 🧩 Integration Guide +Modular Makefile fragments that compose together: +- **`agentic.mk`** - AI agent workflows (GitHub Copilot, Claude) +- **`github.mk`** - GitHub CLI helpers (PRs, issues, workflows) +- **`lfs.mk`** - Git LFS support +- **`docker.mk`** - Docker build and run targets +- **`custom-env.mk`** - Environment variable customization +- **`custom-task.mk`** - Custom task hooks +- **`releasing.mk`** - Release automation framework +- **`book.mk`** - Documentation book building (via minibook) -Rhiza provides reusable configuration templates that you can integrate into your existing Python projects. +### Configuration Files -### Prerequisites +Universal editor and project settings: +- **`.editorconfig`** - Cross-editor formatting rules +- **`CODE_OF_CONDUCT.md`** - Community guidelines +- **`CONTRIBUTING.md`** - Contribution guidelines +- **`LICENSE`** - MIT License +- **Core `.gitignore`** - Common ignore patterns -- **Python 3.11+** - Ensure your project supports Python 3.11 or newer -- **Git** - Your project should be a Git repository -- **Backup** - Consider committing any uncommitted changes before integration +### GitHub Actions Workflows -### Automated Integration (Recommended) +CI/CD templates (in `.github/workflows/`): +- Template sync automation +- Validation workflows +- Release workflows (base) +- Pre-commit check workflows -The fastest way to integrate Rhiza: +### Documentation Templates -```bash -# Navigate to your repository -cd /path/to/your/project - -# Initialise configuration templates -uvx rhiza init - -# Edit .rhiza/template.yml to select desired templates -# Then materialize the templates -uvx rhiza materialize -``` +- README structure and examples +- Integration guides +- Customization documentation -**Options:** -- `--branch ` - Use a specific rhiza branch (default: main) -- `--help` - Show detailed usage information +## 🔧 For Template Developers -### Manual Integration (Selective Adoption) +If you're creating a new language-specific Rhiza template (like `rhiza-rust` or `rhiza-typescript`): -For cherry-picking specific templates or customising before integration: - -1. **Clone Rhiza** to a temporary location: +1. **Create your language template repository** ```bash - cd /tmp - git clone https://github.com/jebel-quant/rhiza.git + # Create new repo based on rhiza-core structure + git clone https://github.com/Jebel-Quant/rhiza-core.git rhiza- + cd rhiza- ``` -2. **Copy desired templates** to your project: - ```bash - cd /path/to/your/project - git checkout -b rhiza - mkdir -p .github/workflows .rhiza/scripts - cp /tmp/rhiza/.rhiza/template.yml .rhiza/template.yml - cp /tmp/rhiza/.rhiza/scripts/sync.sh .rhiza/scripts +2. **Configure to inherit from rhiza-core** + + Create `.rhiza/template.yml`: + ```yaml + repository: Jebel-Quant/rhiza-core + ref: v0.1.0 + + templates: + - core + - github + - docker ``` -3. **Run the sync script**: - ```bash - ./.rhiza/scripts/sync.sh - git status - git diff # Review changes - ``` - -4. **Commit and push** if satisfied with the changes - -### Automated Sync (Continuous Updates) +3. **Add language-specific components** + - Package manager integration + - Language-specific linters/formatters + - Testing frameworks + - Build tooling -Keep your templates up-to-date with automated sync workflows: +4. **Customize the bootstrap process** + + Override `.rhiza/make.d/bootstrap.mk` with language-specific setup -- Configure `.rhiza/template.yml` to define which templates to include/exclude -- The `.github/workflows/sync.yml` workflow runs on schedule or manually -- Creates pull requests with template updates +See [docs/TEMPLATE_DEVELOPMENT.md](docs/TEMPLATE_DEVELOPMENT.md) for detailed guidance. -For GitHub Token configuration and details, see the [GitHub Actions documentation](.github/README.md). +## 👤 For End Users -### What to Expect After Integration +If you want to use Rhiza templates in your project: -- **Automated CI/CD** - GitHub Actions workflows for testing, linting, and releases -- **Code Quality Tools** - Pre-commit hooks, ruff formatting, and pytest configuration -- **Task Automation** - Makefile with common development tasks -- **Dev Container** - Optional VS Code/Codespaces environment -- **Documentation** - Automated documentation generation +1. **Choose your language template:** + - Python: [rhiza](https://github.com/Jebel-Quant/rhiza) + - Go: [rhiza-go](https://github.com/Jebel-Quant/rhiza-go) -### Troubleshooting Integration +2. **Follow the language-specific quickstart guide** -- **Makefile conflicts**: Merge targets with existing build scripts -- **Pre-commit failures**: Run `make fmt` to fix formatting issues -- **Workflow failures**: Check Python version in `.python-version` and `pyproject.toml` -- **Dev container issues**: See [.devcontainer/README.md](.devcontainer/README.md) +You typically won't need to reference rhiza-core directly unless you're building custom templates or contributing improvements. -## 📋 Available Tasks +## 📋 Available Components -The project uses a [Makefile](Makefile) as the primary entry point for all tasks, powered by [uv](https://github.com/astral-sh/uv) for fast Python package management. +The project uses a modular Makefile system with language-agnostic targets. Language-specific templates inherit these and add their own tooling. -### Key Commands +### Core Make Targets (`.rhiza/make.d/`) ```bash -make install # Install dependencies and setup environment -make test # Run test suite with coverage -make fmt # Format and lint code -make sync # Sync with template repository -make release # Create and publish a new release -make marimo # Start Marimo notebook server -make book # Build documentation -``` - -Run `make help` for a complete list of 40+ available targets. - -
-Show all available targets - -```makefile - ____ _ _ - | _ \| |__ (_)______ _ - | |_) | '_ \| |_ / _\`| - | _ <| | | | |/ / (_| | - |_| \_\_| |_|_/___\__,_| - -Usage: - make - -Targets: - -Rhiza Workflows - sync sync with template repository as defined in .rhiza/template.yml - validate validate project structure against template repository as defined in .rhiza/template.yml - readme update README.md with current Makefile help output - -Bootstrap - install-uv ensure uv/uvx is installed - install install - clean Clean project artifacts and stale local branches - -Quality and Formatting - deptry Run deptry - fmt check the pre-commit hooks and the linting - -Releasing and Versioning - bump bump version - release create tag and push to remote with prompts - -Meta - help Display this help message - version-matrix Emit the list of supported Python versions from pyproject.toml - -Development and Testing - test run all tests - benchmark run performance benchmarks - -Documentation - docs create documentation with pdoc - book compile the companion book - -Marimo Notebooks - marimo-validate validate all Marimo notebooks can run - marimo fire up Marimo server - marimushka export Marimo notebooks to HTML - -Presentation - presentation generate presentation slides from PRESENTATION.md using Marp - presentation-pdf generate PDF presentation from PRESENTATION.md using Marp - presentation-serve serve presentation interactively with Marp - -Docker - docker-build build Docker image - docker-run run the Docker container - docker-clean remove Docker image - -Agentic Workflows - copilot open interactive prompt for copilot - analyse-repo run the analyser agent to update REPOSITORY_ANALYSIS.md - summarise-changes summarise changes since the most recent release/tag - install-copilot checks for copilot and prompts to install - -GitHub Helpers - gh-install check for gh cli existence and install extensions - view-prs list open pull requests - view-issues list open issues - failed-workflows list recent failing workflow runs - whoami check github auth status - -Custom Tasks - hello-rhiza a custom greeting task - post-install run custom logic after core install - +# GitHub helpers +make view-prs # List open pull requests +make view-issues # List open issues +make failed-workflows # Show recent CI failures +make whoami # Check GitHub auth status + +# AI Agent workflows +make analyse-repo # Run repository analyzer +make summarise-changes # Summarize changes since last release + +# Docker support +make docker-build # Build Docker image +make docker-run # Run Docker container + +# Documentation +make book # Build documentation book + +# Release automation +make bump # Bump version +make release # Create and publish release ``` -
+Run `make help` in a language-specific template for the complete list of available targets. -> **Note:** The help output is automatically generated from the Makefile. -> When you modify Makefile targets, run `make readme` to update this section, -> or the pre-commit hook will update it automatically. +## 🛠️ Contributing -## 🎯 Advanced Topics - -### Marimo Notebooks - -This project supports [Marimo](https://marimo.io/) notebooks for interactive documentation and exploration. - -```bash -make marimo # Start Marimo server -``` - -For configuration details including dependency management and pythonpath setup, see the [Marimo documentation](https://marimo.io/). - -### Presentations - -Generate presentation slides using [Marp](https://marp.app/): - -```bash -make presentation # Generate HTML slides -make presentation-pdf # Generate PDF slides -make presentation-serve # Serve with live reload -``` - -For detailed information about creating and customising presentations, see [presentation/README.md](presentation/README.md). - -### Documentation Examples - -README code blocks can be tested when tests are configured. - -```python -# Example code block -import math -print("Hello, World!") -print(1 + 1) -print(round(math.pi, 2)) -print(round(math.cos(math.pi/4.0), 2)) -``` - -```result -Hello, World! -2 -3.14 -0.71 -``` - -### Documentation Customisation - -For information on customising the look and feel of your documentation, see [book/README.md](book/README.md). - -### Python Version Management - -The `.python-version` file specifies the default Python version for local development. Tools like `uv` and `pyenv` automatically use this version. Simply update this file to change your local Python version. - -### Makefile Customisation - -Rhiza uses a modular Makefile system with extension points (hooks) for customisation. See [.rhiza/make.d/README.md](.rhiza/make.d/README.md) for the complete guide including: -- Extension points and hooks -- Custom target creation -- Module ordering conventions - -### Custom Build Scripts - -For system dependencies and custom build steps, see [docs/CUSTOMIZATION.md](docs/CUSTOMIZATION.md). - -### Private GitHub Packages - -Rhiza's template workflows automatically support private GitHub packages from the same organization. Simply add them to your `pyproject.toml`: - -**In `pyproject.toml`:** -```toml -[tool.uv.sources] -my-package = { git = "https://github.com/jebel-quant/my-package.git", rev = "v1.0.0" } -``` - -**Git authentication is already configured** in all Rhiza workflows (CI, book, release, etc.) using the default `GITHUB_TOKEN`, which automatically provides read access to repositories in the same organization. - -For custom workflows or local development setup, see [.rhiza/docs/PRIVATE_PACKAGES.md](.rhiza/docs/PRIVATE_PACKAGES.md). - -### Release Management - -For information on versioning, tagging, and publishing releases, see [.rhiza/docs/RELEASING.md](.rhiza/docs/RELEASING.md). - -### Dev Container - -This repository includes a template Dev Container configuration for seamless development in VS Code and GitHub Codespaces. See [.devcontainer/README.md](.devcontainer/README.md) for setup, configuration, and troubleshooting. - -For details about the VS Code extensions configured in the Dev Container, see [docs/VSCODE_EXTENSIONS.md](docs/VSCODE_EXTENSIONS.md). - -## 🔄 CI/CD Support - -### GitHub Actions - -The `.github/` directory contains comprehensive GitHub Actions workflows for: -- CI testing across multiple Python versions -- Pre-commit checks and code quality -- Dependency checking with deptry -- Documentation building -- Docker and devcontainer validation -- Release automation -- Template synchronization - -### GitLab CI/CD - -Rhiza provides GitLab CI/CD workflow configurations with feature parity to GitHub Actions. The `.gitlab/` directory includes workflows for CI, validation, dependency checking, documentation, sync, and releases. - -**Quick setup:** -```bash -cp -r .gitlab/ /path/to/your/project/ -cp .gitlab-ci.yml /path/to/your/project/ -``` - -For complete GitLab setup instructions, configuration variables, and troubleshooting, see **[.gitlab/README.md](.gitlab/README.md)**. - -## 🛠️ Contributing to Rhiza - -Contributions are welcome! To contribute to Rhiza itself (not using Rhiza in your project): +Contributions to Rhiza Core are welcome! This repository provides the foundation for all language-specific templates. +### To Contribute: 1. Fork the repository 2. Clone and setup: ```bash - git clone https://github.com/your-username/rhiza.git - cd rhiza + git clone https://github.com/your-username/rhiza-core.git + cd rhiza-core make install ``` 3. Create your feature branch (`git checkout -b feature/amazing-feature`) -4. Make your changes and test (`make test && make fmt`) +4. Make your changes and test 5. Commit your changes (`git commit -m 'Add some amazing feature'`) 6. Push to the branch (`git push origin feature/amazing-feature`) 7. Open a Pull Request @@ -481,10 +240,15 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines. This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +## 🔗 Related Projects + +- **[rhiza](https://github.com/Jebel-Quant/rhiza)** - Python template built on rhiza-core +- **[rhiza-go](https://github.com/Jebel-Quant/rhiza-go)** - Go template built on rhiza-core +- **[rhiza-cli](https://github.com/Jebel-Quant/rhiza-cli)** - CLI tool for Rhiza template management + ## 🙏 Acknowledgments - [GitHub Actions](https://github.com/features/actions) - For CI/CD capabilities -- [Marimo](https://marimo.io/) - For interactive notebooks - [UV](https://github.com/astral-sh/uv) - For fast Python package operations -- [Ruff](https://github.com/astral-sh/ruff) - For Python linting and formatting -- [Marp](https://marp.app/) - For presentation generation +- The open-source community for inspiration and best practices + diff --git a/docs/TEMPLATE_DEVELOPMENT.md b/docs/TEMPLATE_DEVELOPMENT.md new file mode 100644 index 0000000..e0bd8cd --- /dev/null +++ b/docs/TEMPLATE_DEVELOPMENT.md @@ -0,0 +1,385 @@ +# Template Development Guide + +This guide explains how to create a new language-specific Rhiza template that inherits from rhiza-core. + +## Overview + +Rhiza Core provides the language-agnostic foundation for all Rhiza templates. When creating a new language template (e.g., `rhiza-rust`, `rhiza-typescript`), you'll inherit the core infrastructure and add language-specific tooling. + +## Architecture + +``` +rhiza-core (language-agnostic) +├── .rhiza/make.d/ # Shared Makefile infrastructure +├── .github/workflows/ # Base CI/CD workflows +├── .editorconfig # Editor configuration +├── CODE_OF_CONDUCT.md # Community guidelines +└── CONTRIBUTING.md # Contribution guide + +rhiza- (language-specific) +├── .rhiza/ +│ ├── template.yml # Inherits from rhiza-core +│ └── make.d/ +│ ├── bootstrap.mk # Language-specific setup (override) +│ ├── quality.mk # Language-specific linting (override) +│ └── test.mk # Language-specific testing (override) +├── .github/workflows/ # Language-specific CI workflows +└── +``` + +## Step-by-Step Guide + +### 1. Create Your Template Repository + +```bash +# Clone rhiza-core as starting point +git clone https://github.com/Jebel-Quant/rhiza-core.git rhiza- +cd rhiza- + +# Set up as new repository +rm -rf .git +git init +git remote add origin https://github.com//rhiza-.git +``` + +### 2. Configure Template Inheritance + +Create or update `.rhiza/template.yml`: + +```yaml +# .rhiza/template.yml +repository: Jebel-Quant/rhiza-core +ref: v0.1.0 + +# Select which core bundles to include +templates: + - core # Core infrastructure (required) + - github # GitHub helpers + - docker # Docker support + - lfs # Git LFS support + +# Fine-grained file inclusion +include: | + .rhiza/make.d/github.mk + .rhiza/make.d/docker.mk + .rhiza/make.d/lfs.mk + .rhiza/make.d/agentic.mk + .rhiza/make.d/book.mk + .editorconfig + CODE_OF_CONDUCT.md + CONTRIBUTING.md + +# Exclude files you'll override +exclude: | + .rhiza/make.d/bootstrap.mk + .rhiza/make.d/quality.mk + .rhiza/make.d/test.mk +``` + +### 3. Override Language-Specific Makefiles + +#### Bootstrap (``.rhiza/make.d/bootstrap.mk`) + +Override with your language-specific setup: + +```makefile +## bootstrap.mk - Language-specific installation and setup + +.PHONY: install clean + +##@ Bootstrap + +install: ## Install dependencies + @printf "${BLUE}[INFO] Installing dependencies...${RESET}\n" + # Example for Rust + @cargo build + # Example for TypeScript + @npm install + # Example for Java + @mvn clean install + +clean: ## Clean build artifacts + @printf "${BLUE}[INFO] Cleaning artifacts...${RESET}\n" + # Example for Rust + @cargo clean + # Example for TypeScript + @rm -rf node_modules dist +``` + +#### Quality (``.rhiza/make.d/quality.mk`) + +Override with your language-specific linting: + +```makefile +## quality.mk - Language-specific code quality checks + +.PHONY: fmt lint + +##@ Quality and Formatting + +fmt: ## Format and lint code + @printf "${BLUE}[INFO] Formatting code...${RESET}\n" + # Example for Rust + @cargo fmt + @cargo clippy --fix --allow-dirty + # Example for TypeScript + @npm run lint:fix + # Example for Go + @gofmt -w . + @golangci-lint run --fix + +lint: ## Run linter (check-only) + @printf "${BLUE}[INFO] Linting code...${RESET}\n" + # Example for Rust + @cargo clippy -- -D warnings + # Example for TypeScript + @npm run lint +``` + +#### Testing (`.rhiza/make.d/test.mk`) + +Override with your language-specific testing: + +```makefile +## test.mk - Language-specific testing + +.PHONY: test test-unit test-integration + +##@ Development and Testing + +test: ## Run all tests + @printf "${BLUE}[INFO] Running tests...${RESET}\n" + # Example for Rust + @cargo test + # Example for TypeScript + @npm test + # Example for Go + @go test ./... + +test-unit: ## Run unit tests only + # Language-specific implementation + +test-integration: ## Run integration tests only + # Language-specific implementation +``` + +### 4. Add Language-Specific Configuration Files + +Add the configuration files specific to your language: + +**For Rust:** +- `Cargo.toml` +- `Cargo.lock` +- `rust-toolchain.toml` +- `.rustfmt.toml` +- `clippy.toml` + +**For TypeScript/JavaScript:** +- `package.json` +- `tsconfig.json` +- `.eslintrc.js` +- `.prettierrc` + +**For Go:** +- `go.mod` +- `go.sum` +- `.go-version` +- `.golangci.yml` + +**For Java:** +- `pom.xml` or `build.gradle` +- `checkstyle.xml` +- `.java-version` + +### 5. Create Language-Specific GitHub Actions + +Create CI workflows in `.github/workflows/`: + +```yaml +# .github/workflows/ci.yml +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + # Language-specific setup + - name: Setup + uses: actions/setup-@v4 + with: + -version: '' + + - name: Install dependencies + run: make install + + - name: Run linter + run: make lint + + - name: Run tests + run: make test +``` + +### 6. Update README + +Customize the README for your language: + +```markdown +# Rhiza + +A collection of reusable configuration templates for modern projects. + +## Features + +- -specific tooling integration +- CI/CD workflows for +- Testing framework setup +- Linting and formatting +- Inherits language-agnostic infrastructure from rhiza-core + +## Quick Start + +\`\`\`bash +cd /path/to/your/-project +uvx rhiza init +# Edit .rhiza/template.yml to point to rhiza- +uvx rhiza materialize +\`\`\` + +## What's Included + +- ** Build System** - [Tool name] configuration +- **Testing** - [Test framework] setup +- **Linting** - [Linter] configuration +- **Formatting** - [Formatter] setup +- **CI/CD** - GitHub Actions for + +Inherits from [rhiza-core](https://github.com/Jebel-Quant/rhiza-core): +- GitHub helpers +- Docker support +- Release automation +- Documentation tooling +``` + +### 7. Create Template Bundles + +Define template bundles in `.rhiza/template-bundles.yml`: + +```yaml +version: "0.1.0" + +bundles: + # Core language infrastructure + core: + description: "Core infrastructure" + required: true + files: + - .rhiza/make.d/bootstrap.mk + - .rhiza/make.d/quality.mk + - .rhiza/make.d/test.mk + - + + # Testing infrastructure + tests: + description: " testing setup" + files: + - .rhiza/make.d/test.mk + - tests/ + + # Language-specific features + : + description: " support" + files: + - +``` + +### 8. Testing Your Template + +1. **Create a test project:** + ```bash + mkdir /tmp/test--template + cd /tmp/test--template + git init + ``` + +2. **Configure to use your template:** + ```yaml + # .rhiza/template.yml + repository: /rhiza- + ref: main + + templates: + - core + - tests + ``` + +3. **Materialize and test:** + ```bash + uvx rhiza materialize + make install + make test + make lint + ``` + +## Best Practices + +### 1. Maintain Language-Agnostic Core + +Keep core infrastructure (GitHub helpers, Docker, etc.) in rhiza-core. Only override when absolutely necessary. + +### 2. Follow Naming Conventions + +- Use consistent target names: `install`, `test`, `lint`, `fmt` +- Keep make targets language-agnostic where possible +- Document all custom targets in help text + +### 3. Provide Good Defaults + +Configure sensible defaults for: +- Linting rules +- Testing frameworks +- CI/CD workflows +- Editor configurations + +### 4. Document Dependencies + +Clearly document: +- Required language version +- Build tool versions +- System dependencies +- Installation instructions + +### 5. Test Thoroughly + +- Test on multiple OS (Linux, macOS, Windows if applicable) +- Test with different language versions +- Validate CI/CD workflows +- Check template synchronization + +## Examples + +See existing templates: +- **[rhiza](https://github.com/Jebel-Quant/rhiza)** - Python template +- **[rhiza-go](https://github.com/Jebel-Quant/rhiza-go)** - Go template + +## Getting Help + +- Open an issue in [rhiza-core](https://github.com/Jebel-Quant/rhiza-core/issues) +- Check the [rhiza-core README](../README.md) +- Review existing language templates for patterns + +## Contributing + +When you've created a new language template: + +1. Add it to the [rhiza-core README](../README.md) under "Related Projects" +2. Consider contributing improvements back to rhiza-core +3. Share your template with the community! diff --git a/pyproject.toml b/pyproject.toml index 151e23a..6dfda31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,14 @@ [project] -name = "rhiza" -version = "0.8.0" -description = "Reusable configuration templates for modern Python projects" +name = "rhiza-core" +version = "0.1.0" +description = "Core reusable configuration templates for Rhiza language templates" readme = "README.md" requires-python = ">=3.11" license = { text = "MIT" } authors = [ { name = "Thomas Schmelzer" } ] -keywords = ["templates", "configuration", "ci", "ruff"] +keywords = ["templates", "configuration", "ci", "makefile", "devops", "core"] classifiers = [ "Private :: Do Not Upload", "Programming Language :: Python :: 3", @@ -20,6 +20,7 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Intended Audience :: Developers", "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Application Frameworks", ] dependencies = [] @@ -34,9 +35,9 @@ dev = [ ] [project.urls] -Homepage = "https://github.com/jebel-quant/rhiza" -Repository = "https://github.com/jebel-quant/rhiza" -Issues = "https://github.com/jebel-quant/rhiza/issues" +Homepage = "https://github.com/jebel-quant/rhiza-core" +Repository = "https://github.com/jebel-quant/rhiza-core" +Issues = "https://github.com/jebel-quant/rhiza-core/issues" [tool.deptry.package_module_name_map] marimo = "marimo" diff --git a/uv.lock b/uv.lock index fe37a86..e984663 100644 --- a/uv.lock +++ b/uv.lock @@ -564,8 +564,8 @@ wheels = [ ] [[package]] -name = "rhiza" -version = "0.8.0" +name = "rhiza-core" +version = "0.1.0" source = { virtual = "." } [package.dev-dependencies] From d0901ec0c30f0c20d3377e03a253a4f8e6d5a819 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 08:43:45 +0000 Subject: [PATCH 03/31] Add template.yml example and migration guide Co-authored-by: HarryCampion <40582604+HarryCampion@users.noreply.github.com> --- .rhiza/template.yml.example | 135 ++++++++++++++ docs/MIGRATION_GUIDE.md | 340 ++++++++++++++++++++++++++++++++++++ 2 files changed, 475 insertions(+) create mode 100644 .rhiza/template.yml.example create mode 100644 docs/MIGRATION_GUIDE.md diff --git a/.rhiza/template.yml.example b/.rhiza/template.yml.example new file mode 100644 index 0000000..6dd740a --- /dev/null +++ b/.rhiza/template.yml.example @@ -0,0 +1,135 @@ +# Example .rhiza/template.yml for Language-Specific Templates +# +# This file shows how to configure a language-specific Rhiza template +# (e.g., rhiza-python, rhiza-go, rhiza-rust) to inherit from rhiza-core. +# +# Copy this to your language template repository as `.rhiza/template.yml` +# and uncomment/adjust the sections you need. + +# ============================================================================ +# Repository Configuration +# ============================================================================ + +# Point to rhiza-core for shared infrastructure +repository: Jebel-Quant/rhiza-core + +# Use a specific version tag for stability (recommended) +# Or use 'main' for latest changes (useful during development) +ref: v0.1.0 + +# ============================================================================ +# Template Bundles (Recommended Approach) +# ============================================================================ + +# Select language-agnostic bundles from rhiza-core +templates: + - core # Required: Core infrastructure + - github # GitHub Actions and CLI helpers + - docker # Docker support (framework only) + - lfs # Git LFS support (if needed) + - legal # Legal and community files + - renovate # Renovate dependency automation + - book # Documentation book framework + - release # Release automation framework + +# ============================================================================ +# Fine-Grained File Inclusion (Alternative Approach) +# ============================================================================ + +# If you need more control, you can explicitly list files +# This approach gives you complete control over what gets inherited + +# include: | +# # Core infrastructure +# .rhiza/rhiza.mk +# .rhiza/.cfg.toml +# .rhiza/.env +# .rhiza/.gitignore +# .rhiza/.rhiza-version +# .rhiza/make.d/custom-env.mk +# .rhiza/make.d/custom-task.mk +# .rhiza/make.d/README.md +# +# # GitHub helpers +# .rhiza/make.d/github.mk +# .rhiza/make.d/agentic.mk +# .github/workflows/copilot-setup-steps.yml +# .github/workflows/rhiza_validate.yml +# .github/workflows/rhiza_sync.yml +# +# # Docker support +# .rhiza/make.d/docker.mk +# +# # Configuration files +# .editorconfig +# .gitignore +# +# # Legal and community +# LICENSE +# CODE_OF_CONDUCT.md +# CONTRIBUTING.md + +# ============================================================================ +# Exclusions +# ============================================================================ + +# Exclude files that your language template will override +exclude: | + # Language-specific makefiles you'll override + .rhiza/make.d/bootstrap.mk + .rhiza/make.d/quality.mk + .rhiza/make.d/test.mk + + # Language-specific configurations + .rhiza/requirements/ + + # Python-specific files (if not a Python template) + .python-version + pyproject.toml + pytest.ini + ruff.toml + + # Go-specific files (if not a Go template) + .go-version + go.mod + go.sum + .golangci.yml + + # Language-specific CI workflows + .github/workflows/rhiza_ci.yml + .github/workflows/rhiza_pre-commit.yml + .github/workflows/rhiza_deptry.yml + + # Language-specific scripts and customizations + .rhiza/scripts/customisations/ + +# ============================================================================ +# Notes for Language-Specific Templates +# ============================================================================ + +# Your language template should provide: +# +# 1. Language-Specific Makefiles: +# - .rhiza/make.d/bootstrap.mk (setup and install) +# - .rhiza/make.d/quality.mk (linting, formatting) +# - .rhiza/make.d/test.mk (testing framework) +# - .rhiza/make.d/docs.mk (if language-specific docs needed) +# +# 2. Language-Specific Configuration: +# - Build system files (Cargo.toml, go.mod, package.json, etc.) +# - Linter/formatter configs +# - Test framework configs +# - Version files (.python-version, .go-version, etc.) +# +# 3. Language-Specific CI/CD: +# - .github/workflows/rhiza_ci.yml +# - .github/workflows/rhiza_pre-commit.yml +# - .github/workflows/rhiza_release.yml (language-specific publishing) +# - .github/workflows/rhiza_deptry.yml (or equivalent) +# +# 4. Language-Specific Documentation: +# - docs/DEPENDENCIES.md +# - docs/TESTING.md +# - docs/QUICK_REFERENCE.md +# +# See docs/TEMPLATE_DEVELOPMENT.md for detailed guidance. diff --git a/docs/MIGRATION_GUIDE.md b/docs/MIGRATION_GUIDE.md new file mode 100644 index 0000000..8c71878 --- /dev/null +++ b/docs/MIGRATION_GUIDE.md @@ -0,0 +1,340 @@ +# Migration Guide: rhiza and rhiza-go to rhiza-core Inheritance + +This guide explains how to migrate existing `rhiza` (Python) and `rhiza-go` (Go) templates to inherit from `rhiza-core`. + +## Overview + +Previously, `rhiza` and `rhiza-go` were standalone template repositories containing both language-specific and language-agnostic components. With the introduction of `rhiza-core`, the shared infrastructure has been extracted into a common foundation. + +### Before (Standalone) + +``` +rhiza/ +├── .rhiza/make.d/ +│ ├── github.mk # Duplicated in rhiza-go +│ ├── docker.mk # Duplicated in rhiza-go +│ ├── lfs.mk # Duplicated in rhiza-go +│ ├── bootstrap.mk # Python-specific +│ ├── test.mk # Python-specific +│ └── quality.mk # Python-specific +└── ... + +rhiza-go/ +├── .rhiza/make.d/ +│ ├── github.mk # Duplicated from rhiza +│ ├── docker.mk # Duplicated from rhiza +│ ├── lfs.mk # Duplicated from rhiza +│ ├── bootstrap.mk # Go-specific +│ ├── test.mk # Go-specific +│ └── quality.mk # Go-specific +└── ... +``` + +### After (With Inheritance) + +``` +rhiza-core/ +├── .rhiza/make.d/ +│ ├── github.mk # Shared +│ ├── docker.mk # Shared framework +│ ├── lfs.mk # Shared +│ ├── agentic.mk # Shared +│ ├── book.mk # Shared framework +│ └── releasing.mk # Shared framework +└── ... + +rhiza/ +├── .rhiza/ +│ ├── template.yml # Points to rhiza-core +│ └── make.d/ +│ ├── bootstrap.mk # Python-specific (overrides) +│ ├── test.mk # Python-specific (overrides) +│ └── quality.mk # Python-specific (overrides) +└── ... + +rhiza-go/ +├── .rhiza/ +│ ├── template.yml # Points to rhiza-core +│ └── make.d/ +│ ├── bootstrap.mk # Go-specific (overrides) +│ ├── test.mk # Go-specific (overrides) +│ └── quality.mk # Go-specific (overrides) +└── ... +``` + +## Migration Steps + +### For rhiza (Python Template) + +#### 1. Create `.rhiza/template.yml` + +```yaml +# .rhiza/template.yml +repository: Jebel-Quant/rhiza-core +ref: v0.1.0 + +templates: + - core + - github + - docker + - lfs + - legal + - renovate + - book + - release + +exclude: | + # Keep Python-specific overrides + .rhiza/make.d/bootstrap.mk + .rhiza/make.d/test.mk + .rhiza/make.d/quality.mk + .rhiza/make.d/marimo.mk + .rhiza/make.d/presentation.mk + .rhiza/make.d/tutorial.mk + + # Keep Python-specific configurations + .python-version + pyproject.toml + pytest.ini + ruff.toml + .pre-commit-config.yaml + + # Keep Python-specific CI workflows + .github/workflows/rhiza_ci.yml + .github/workflows/rhiza_pre-commit.yml + .github/workflows/rhiza_deptry.yml + .github/workflows/rhiza_marimo.yml + .github/workflows/rhiza_benchmarks.yml + .github/workflows/rhiza_codeql.yml + .github/workflows/rhiza_security.yml + + # Keep Python-specific requirements + .rhiza/requirements/ +``` + +#### 2. Remove Duplicate Files + +After configuring template inheritance, remove files that will come from rhiza-core: + +```bash +# Remove shared makefiles (will come from rhiza-core) +git rm .rhiza/make.d/github.mk +git rm .rhiza/make.d/docker.mk +git rm .rhiza/make.d/lfs.mk +git rm .rhiza/make.d/agentic.mk +git rm .rhiza/make.d/custom-env.mk +git rm .rhiza/make.d/custom-task.mk + +# Remove shared GitHub Actions (will come from rhiza-core) +git rm .github/workflows/copilot-setup-steps.yml +git rm .github/workflows/rhiza_validate.yml +git rm .github/workflows/rhiza_sync.yml +git rm .github/actions/configure-git-auth -r + +# Keep .editorconfig, LICENSE, CODE_OF_CONDUCT.md, CONTRIBUTING.md +# as they may have Python-specific customizations +``` + +#### 3. Update Template Bundles + +Update `.rhiza/template-bundles.yml` to only include Python-specific bundles: + +```yaml +version: "0.8.0" + +bundles: + # Python-specific bundles only + tests: + description: "Python testing with pytest" + files: + - .rhiza/make.d/test.mk + - pytest.ini + - .rhiza/requirements/tests.txt + - .github/workflows/rhiza_ci.yml + - .github/workflows/rhiza_codeql.yml + - .github/workflows/rhiza_security.yml + + marimo: + description: "Marimo notebooks for Python" + files: + - .rhiza/make.d/marimo.mk + - .rhiza/requirements/marimo.txt + - .github/workflows/rhiza_marimo.yml + + # ... other Python-specific bundles +``` + +#### 4. Test the Migration + +```bash +# Sync from rhiza-core +uvx rhiza materialize + +# Verify shared files came from rhiza-core +git status + +# Test that everything still works +make install +make test +make lint +``` + +### For rhiza-go (Go Template) + +#### 1. Create `.rhiza/template.yml` + +```yaml +# .rhiza/template.yml +repository: Jebel-Quant/rhiza-core +ref: v0.1.0 + +templates: + - core + - github + - docker + - lfs + - legal + - renovate + - book + - release + +exclude: | + # Keep Go-specific overrides + .rhiza/make.d/bootstrap.mk + .rhiza/make.d/test.mk + .rhiza/make.d/quality.mk + .rhiza/make.d/security.mk + + # Keep Go-specific configurations + .go-version + go.mod + go.sum + .golangci.yml + .goreleaser.yml + .pre-commit-config.yaml + + # Keep Go-specific CI workflows + .github/workflows/rhiza_ci.yml + .github/workflows/rhiza_pre-commit.yml + .github/workflows/rhiza_release.yml + .github/workflows/rhiza_security.yml + + # Keep Go-specific scripts + install.sh + uninstall.sh +``` + +#### 2. Remove Duplicate Files + +```bash +# Remove shared makefiles (will come from rhiza-core) +git rm .rhiza/make.d/github.mk +git rm .rhiza/make.d/docker.mk +git rm .rhiza/make.d/lfs.mk +git rm .rhiza/make.d/agentic.mk +git rm .rhiza/make.d/custom-env.mk +git rm .rhiza/make.d/custom-task.mk + +# Remove shared GitHub Actions (will come from rhiza-core) +git rm .github/workflows/copilot-setup-steps.yml +git rm .github/workflows/rhiza_validate.yml +git rm .github/workflows/rhiza_sync.yml +git rm .github/actions/configure-git-auth -r +``` + +#### 3. Update Template Bundles + +Update `.rhiza/template-bundles.yml` to only include Go-specific bundles: + +```yaml +version: "0.2.0" + +bundles: + # Go-specific bundles only + tests: + description: "Go testing with go test" + files: + - .rhiza/make.d/test.mk + - .github/workflows/rhiza_ci.yml + + security: + description: "Go security scanning with gosec" + files: + - .rhiza/make.d/security.mk + - .github/workflows/rhiza_security.yml + + # ... other Go-specific bundles +``` + +#### 4. Test the Migration + +```bash +# Sync from rhiza-core +uvx rhiza materialize + +# Verify shared files came from rhiza-core +git status + +# Test that everything still works +make install +make test +make fmt +``` + +## Validation Checklist + +After migration, verify: + +- [ ] `.rhiza/template.yml` points to `rhiza-core` +- [ ] Shared makefiles are removed from your repo +- [ ] Language-specific makefiles remain (bootstrap.mk, test.mk, quality.mk) +- [ ] `make install` works correctly +- [ ] `make test` works correctly +- [ ] `make fmt` works correctly +- [ ] CI workflows pass +- [ ] `uvx rhiza materialize` successfully syncs from rhiza-core +- [ ] `uvx rhiza validate` passes + +## Benefits After Migration + +1. **Reduced Duplication**: Shared infrastructure is maintained in one place +2. **Easier Updates**: Bug fixes and improvements to shared components automatically available +3. **Consistency**: All language templates use the same GitHub helpers, Docker targets, etc. +4. **Faster Development**: New language templates can be created more quickly +5. **Clearer Separation**: Language-specific vs. language-agnostic concerns are clearly separated + +## Troubleshooting + +### Problem: Conflicts during sync + +**Solution**: Check your `exclude` patterns in `.rhiza/template.yml`. Ensure you're excluding files you want to keep. + +### Problem: Missing files after migration + +**Solution**: Verify the `templates` list in `.rhiza/template.yml` includes the bundles you need (e.g., `github`, `docker`). + +### Problem: CI workflows failing + +**Solution**: Ensure you've kept language-specific CI workflows in your exclude list. Check that language-specific dependencies are still configured correctly. + +### Problem: Make targets not working + +**Solution**: Verify that language-specific makefiles (bootstrap.mk, test.mk, quality.mk) are excluded and still present in your repository. + +## Getting Help + +- Check the [rhiza-core README](../README.md) +- Review [TEMPLATE_DEVELOPMENT.md](../docs/TEMPLATE_DEVELOPMENT.md) +- Open an issue in [rhiza-core issues](https://github.com/Jebel-Quant/rhiza-core/issues) +- See existing language templates for examples + +## Timeline + +**Recommended Migration Schedule:** + +1. **Week 1**: Create `.rhiza/template.yml` and test sync +2. **Week 2**: Remove duplicate files gradually, test after each change +3. **Week 3**: Update template-bundles.yml +4. **Week 4**: Final testing and validation + +Take your time and test thoroughly at each step. The migration can be done gradually without breaking existing functionality. From fe43b13cc2cd81779f8a4a91accd82dc455ed631 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 08:45:08 +0000 Subject: [PATCH 04/31] Address code review feedback: fix inconsistent comments and capitalization Co-authored-by: HarryCampion <40582604+HarryCampion@users.noreply.github.com> --- README.md | 2 +- docs/MIGRATION_GUIDE.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index de045f8..ef3e0bd 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## 🙏 Acknowledgments - [GitHub Actions](https://github.com/features/actions) - For CI/CD capabilities -- [UV](https://github.com/astral-sh/uv) - For fast Python package operations +- [uv](https://github.com/astral-sh/uv) - For fast Python package operations - The open-source community for inspiration and best practices diff --git a/docs/MIGRATION_GUIDE.md b/docs/MIGRATION_GUIDE.md index 8c71878..7e38a75 100644 --- a/docs/MIGRATION_GUIDE.md +++ b/docs/MIGRATION_GUIDE.md @@ -35,10 +35,10 @@ rhiza-go/ ``` rhiza-core/ ├── .rhiza/make.d/ -│ ├── github.mk # Shared +│ ├── github.mk # Shared infrastructure │ ├── docker.mk # Shared framework -│ ├── lfs.mk # Shared -│ ├── agentic.mk # Shared +│ ├── lfs.mk # Shared infrastructure +│ ├── agentic.mk # Shared infrastructure │ ├── book.mk # Shared framework │ └── releasing.mk # Shared framework └── ... From b42266c752db4434dfdfec3f549df728ebaf0002 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:00:34 +0000 Subject: [PATCH 05/31] Remove Python-specific makefiles, workflows, and files from rhiza-core This makes rhiza-core truly language-agnostic by removing: - Python-specific makefiles: bootstrap.mk, quality.mk, test.mk, docs.mk, marimo.mk, presentation.mk, tutorial.mk - Python-specific workflows: CI, deptry, pre-commit, benchmarks, marimo, devcontainer, book, codeql, security, docker, release - Python-specific configs: .python-version, pytest.ini, ruff.toml, .pre-commit-config.yaml - Python-specific directories: tests/, book/, docker/, .devcontainer/, .rhiza/requirements/, .rhiza/completions/, .rhiza/tests/ Keeping only language-agnostic components: - Makefiles: agentic.mk, github.mk, lfs.mk, docker.mk, custom-env.mk, custom-task.mk, releasing.mk, book.mk - Workflows: copilot-setup-steps.yml, rhiza_validate.yml, rhiza_sync.yml, renovate_rhiza_sync.yml - Configs: .editorconfig, .gitignore, LICENSE, CODE_OF_CONDUCT.md, CONTRIBUTING.md Co-authored-by: HarryCampion <40582604+HarryCampion@users.noreply.github.com> --- .devcontainer/bootstrap.sh | 38 -- .devcontainer/devcontainer.json | 54 -- .github/workflows/rhiza_benchmarks.yml | 87 --- .github/workflows/rhiza_book.yml | 82 --- .github/workflows/rhiza_ci.yml | 105 --- .github/workflows/rhiza_codeql.yml | 129 ---- .github/workflows/rhiza_deptry.yml | 45 -- .github/workflows/rhiza_devcontainer.yml | 137 ---- .github/workflows/rhiza_docker.yml | 108 --- .github/workflows/rhiza_marimo.yml | 108 --- .github/workflows/rhiza_pre-commit.yml | 52 -- .github/workflows/rhiza_release.yml | 449 ------------- .github/workflows/rhiza_security.yml | 49 -- .pre-commit-config.yaml | 67 -- .python-version | 1 - .rhiza/completions/README.md | 263 -------- .rhiza/completions/rhiza-completion.bash | 47 -- .rhiza/completions/rhiza-completion.zsh | 88 --- .rhiza/make.d/bootstrap.mk | 107 --- .rhiza/make.d/docs.mk | 96 --- .rhiza/make.d/marimo.mk | 67 -- .rhiza/make.d/presentation.mk | 70 -- .rhiza/make.d/quality.mk | 24 - .rhiza/make.d/test.mk | 115 ---- .rhiza/make.d/tutorial.mk | 101 --- .rhiza/requirements/README.md | 27 - .rhiza/requirements/docs.txt | 3 - .rhiza/requirements/marimo.txt | 2 - .rhiza/requirements/tests.txt | 15 - .rhiza/requirements/tools.txt | 7 - .rhiza/tests/README.md | 126 ---- .rhiza/tests/api/conftest.py | 91 --- .rhiza/tests/api/test_github_targets.py | 55 -- .rhiza/tests/api/test_makefile_api.py | 369 ---------- .rhiza/tests/api/test_makefile_targets.py | 291 -------- .rhiza/tests/conftest.py | 210 ------ .rhiza/tests/deps/test_dependency_health.py | 111 ---- .rhiza/tests/integration/test_book_targets.py | 150 ----- .rhiza/tests/integration/test_lfs.py | 182 ----- .rhiza/tests/integration/test_marimushka.py | 93 --- .../integration/test_notebook_execution.py | 96 --- .rhiza/tests/integration/test_sbom.py | 159 ----- .rhiza/tests/integration/test_test_mk.py | 53 -- .../integration/test_virtual_env_unexport.py | 37 -- .rhiza/tests/structure/test_lfs_structure.py | 135 ---- .rhiza/tests/structure/test_project_layout.py | 57 -- .rhiza/tests/structure/test_requirements.py | 51 -- .../tests/structure/test_template_bundles.py | 89 --- .rhiza/tests/sync/conftest.py | 95 --- .rhiza/tests/sync/test_docstrings.py | 123 ---- .rhiza/tests/sync/test_readme_validation.py | 122 ---- .rhiza/tests/sync/test_rhiza_version.py | 137 ---- .rhiza/tests/test_utils.py | 63 -- .rhiza/tests/utils/test_git_repo_fixture.py | 132 ---- book/marimo/notebooks/rhiza.py | 629 ------------------ docker/Dockerfile | 81 --- docker/Dockerfile.dockerignore | 17 - pytest.ini | 14 - ruff.toml | 130 ---- tests/benchmarks/conftest.py | 5 - tests/benchmarks/test_benchmarks.py | 59 -- tests/property/test_makefile_properties.py | 26 - 62 files changed, 6531 deletions(-) delete mode 100755 .devcontainer/bootstrap.sh delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .github/workflows/rhiza_benchmarks.yml delete mode 100644 .github/workflows/rhiza_book.yml delete mode 100644 .github/workflows/rhiza_ci.yml delete mode 100644 .github/workflows/rhiza_codeql.yml delete mode 100644 .github/workflows/rhiza_deptry.yml delete mode 100644 .github/workflows/rhiza_devcontainer.yml delete mode 100644 .github/workflows/rhiza_docker.yml delete mode 100644 .github/workflows/rhiza_marimo.yml delete mode 100644 .github/workflows/rhiza_pre-commit.yml delete mode 100644 .github/workflows/rhiza_release.yml delete mode 100644 .github/workflows/rhiza_security.yml delete mode 100644 .pre-commit-config.yaml delete mode 100644 .python-version delete mode 100644 .rhiza/completions/README.md delete mode 100644 .rhiza/completions/rhiza-completion.bash delete mode 100644 .rhiza/completions/rhiza-completion.zsh delete mode 100644 .rhiza/make.d/bootstrap.mk delete mode 100644 .rhiza/make.d/docs.mk delete mode 100644 .rhiza/make.d/marimo.mk delete mode 100644 .rhiza/make.d/presentation.mk delete mode 100644 .rhiza/make.d/quality.mk delete mode 100644 .rhiza/make.d/test.mk delete mode 100644 .rhiza/make.d/tutorial.mk delete mode 100644 .rhiza/requirements/README.md delete mode 100644 .rhiza/requirements/docs.txt delete mode 100644 .rhiza/requirements/marimo.txt delete mode 100644 .rhiza/requirements/tests.txt delete mode 100644 .rhiza/requirements/tools.txt delete mode 100644 .rhiza/tests/README.md delete mode 100644 .rhiza/tests/api/conftest.py delete mode 100644 .rhiza/tests/api/test_github_targets.py delete mode 100644 .rhiza/tests/api/test_makefile_api.py delete mode 100644 .rhiza/tests/api/test_makefile_targets.py delete mode 100644 .rhiza/tests/conftest.py delete mode 100644 .rhiza/tests/deps/test_dependency_health.py delete mode 100644 .rhiza/tests/integration/test_book_targets.py delete mode 100644 .rhiza/tests/integration/test_lfs.py delete mode 100644 .rhiza/tests/integration/test_marimushka.py delete mode 100644 .rhiza/tests/integration/test_notebook_execution.py delete mode 100644 .rhiza/tests/integration/test_sbom.py delete mode 100644 .rhiza/tests/integration/test_test_mk.py delete mode 100644 .rhiza/tests/integration/test_virtual_env_unexport.py delete mode 100644 .rhiza/tests/structure/test_lfs_structure.py delete mode 100644 .rhiza/tests/structure/test_project_layout.py delete mode 100644 .rhiza/tests/structure/test_requirements.py delete mode 100644 .rhiza/tests/structure/test_template_bundles.py delete mode 100644 .rhiza/tests/sync/conftest.py delete mode 100644 .rhiza/tests/sync/test_docstrings.py delete mode 100644 .rhiza/tests/sync/test_readme_validation.py delete mode 100644 .rhiza/tests/sync/test_rhiza_version.py delete mode 100644 .rhiza/tests/test_utils.py delete mode 100644 .rhiza/tests/utils/test_git_repo_fixture.py delete mode 100644 book/marimo/notebooks/rhiza.py delete mode 100644 docker/Dockerfile delete mode 100644 docker/Dockerfile.dockerignore delete mode 100644 pytest.ini delete mode 100644 ruff.toml delete mode 100644 tests/benchmarks/conftest.py delete mode 100644 tests/benchmarks/test_benchmarks.py delete mode 100644 tests/property/test_makefile_properties.py diff --git a/.devcontainer/bootstrap.sh b/.devcontainer/bootstrap.sh deleted file mode 100755 index 9239d2d..0000000 --- a/.devcontainer/bootstrap.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -set -euo pipefail -IFS=$'\n\t' - -# Read Python version from .python-version (single source of truth) -if [ -f ".python-version" ]; then - export PYTHON_VERSION=$(cat .python-version | tr -d '[:space:]') - echo "Using Python version from .python-version: $PYTHON_VERSION" -fi - -# Use INSTALL_DIR from environment or default to local bin -# In devcontainer, this is set to /home/vscode/.local/bin to avoid conflict with host -export INSTALL_DIR="${INSTALL_DIR:-./bin}" -export UV_BIN="${INSTALL_DIR}/uv" -export UVX_BIN="${INSTALL_DIR}/uvx" - -# Only remove existing binaries if we are installing to the default ./bin location -# and we want to force re-installation (e.g. if OS changed) -if [ "$INSTALL_DIR" = "./bin" ]; then - rm -f "$UV_BIN" "$UVX_BIN" -fi - -# Set UV environment variables to avoid prompts and warnings -export UV_VENV_CLEAR=1 -export UV_LINK_MODE=copy - -# Make UV environment variables persistent for all sessions -echo "export UV_LINK_MODE=copy" >> ~/.bashrc -echo "export UV_VENV_CLEAR=1" >> ~/.bashrc -echo "export PATH=\"$INSTALL_DIR:\$PATH\"" >> ~/.bashrc - -# Add to current PATH so subsequent commands can find uv -export PATH="$INSTALL_DIR:$PATH" - -make install - -# Install Marimo tool for notebook editing -"$UV_BIN" tool install marimo diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 5601a66..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "Python Dev (Rhiza)", - "image": "mcr.microsoft.com/devcontainers/python:3.14", - "hostRequirements": { - "cpus": 4 - }, - "features": { - "ghcr.io/devcontainers/features/github-cli:1": {}, - "ghcr.io/devcontainers/features/copilot-cli:1": {}, - "ghcr.io/devcontainers/features/node:1": {}, - "ghcr.io/devcontainers/features/docker-in-docker:2": { - "moby": false - } - }, - "mounts": [ - "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached" - ], - "containerEnv": { - "INSTALL_DIR": "/home/vscode/.local/bin" - }, - "forwardPorts": [8080, 2718], - "customizations": { - "vscode": { - "settings": { - "python.defaultInterpreterPath": ".venv/bin/python", - "python.testing.pytestEnabled": true, - "python.testing.unittestEnabled": false, - "python.testing.pytestArgs": ["."], - "python.terminal.activateEnvInCurrentTerminal": true, - "marimo.pythonPath": ".venv/bin/python", - "marimo.marimoPath": ".venv/bin/marimo" - }, - "extensions": [ - // Python Development - "ms-python.python", - "ms-python.vscode-pylance", - // Marimo/Notebooks - "marimo-team.vscode-marimo", - "marimo-ai.marimo-vscode", - // Linting and Formatting - "charliermarsh.ruff", - "tamasfe.even-better-toml", - // Build Tools - "ms-vscode.makefile-tools", - // AI Assistance - "github.copilot-chat", - "github.copilot", - "anthropic.claude-code" - ] - } - }, - "onCreateCommand": ".devcontainer/bootstrap.sh", - "remoteUser": "vscode" -} diff --git a/.github/workflows/rhiza_benchmarks.yml b/.github/workflows/rhiza_benchmarks.yml deleted file mode 100644 index c207b2a..0000000 --- a/.github/workflows/rhiza_benchmarks.yml +++ /dev/null @@ -1,87 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Workflow: Performance Benchmarks -# -# Purpose: Run performance benchmarks and detect regressions. -# -# Trigger: On push to main/master branches, PRs, and manual trigger. -# -# Regression Detection: -# - Compares against previous benchmark results stored in gh-pages branch -# - Alerts if performance degrades by more than 150% (configurable) -# - PRs will show a warning comment but not fail -# - Main branch updates the baseline for future comparisons - -name: "(RHIZA) BENCHMARKS" - -permissions: - contents: write - pull-requests: write - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - workflow_dispatch: - -jobs: - benchmark: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v6.0.2 - with: - lfs: true - - - name: Install uv - uses: astral-sh/setup-uv@v7.3.0 - with: - version: "0.10.2" - - - name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth - with: - token: ${{ secrets.GH_PAT }} - - - name: Run benchmarks - env: - UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} - run: | - make benchmark - - - name: Upload benchmark results - uses: actions/upload-artifact@v6.0.0 - if: always() - with: - name: benchmark-results - path: | - _benchmarks/benchmarks.json - _benchmarks/benchmarks.svg - _benchmarks/benchmarks.html - - # Regression detection using github-action-benchmark - # Stores benchmark history in gh-pages branch under /benchmarks - # Alerts if performance degrades by more than 150% of baseline - - name: Store benchmark result and check for regression - uses: benchmark-action/github-action-benchmark@v1 - # run this only if _benchmarks/benchmarks.json exists - if: hashFiles('_benchmarks/benchmarks.json') != '' - with: - tool: 'pytest' - output-file-path: _benchmarks/benchmarks.json - # Store benchmark data in gh-pages branch - gh-pages-branch: gh-pages - benchmark-data-dir-path: benchmarks - # Only update baseline on main branch push (not PRs) - auto-push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} - # Alert if performance degrades by more than 150% - alert-threshold: '150%' - # Post comment on PR if regression detected - comment-on-alert: ${{ github.event_name == 'pull_request' }} - # Fail workflow if regression detected (disabled for PRs to allow investigation) - fail-on-alert: ${{ github.event_name == 'push' }} - # GitHub token for pushing to gh-pages and commenting - github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/rhiza_book.yml b/.github/workflows/rhiza_book.yml deleted file mode 100644 index 3546de6..0000000 --- a/.github/workflows/rhiza_book.yml +++ /dev/null @@ -1,82 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Workflow: Book -# Purpose: This workflow builds and deploys comprehensive documentation for the project. -# It combines API documentation, test coverage reports, test results, and -# interactive notebooks into a single GitHub Pages site. -# -# Trigger: This workflow runs on every push to the main or master branch -# -# Components: -# - 📓 Process Marimo notebooks -# - 📖 Generate API documentation with pdoc -# - 🧪 Run tests and generate coverage reports -# - 🚀 Deploy combined documentation to GitHub Pages - -name: "(RHIZA) BOOK" - -on: - push: - branches: - - main - - master - -jobs: - book: - runs-on: "ubuntu-latest" - - environment: - name: github-pages # 👈 this is the critical missing piece - - permissions: - contents: read - pages: write # Permission to deploy to Pages - id-token: write # Permission to verify deployment origin - - steps: - # Check out the repository code - - uses: actions/checkout@v6.0.2 - with: - lfs: true - - - name: Install uv - uses: astral-sh/setup-uv@v7.3.0 - with: - version: "0.10.2" - - - 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 }} - 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 }} - run: | - make book - - # Step 5: 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.0.0 # Official GitHub Pages artifact upload action - with: - path: _book/ # Path to the directory containing all artifacts to deploy - - # Step 6: Deploy the packaged artifacts to GitHub Pages - # This step publishes the content to GitHub Pages - # The deployment is conditional based on whether the repository is a fork and the PUBLISH_COMPANION_BOOK variable is set - # If the repository is a fork, deployment is skipped to avoid unauthorised publishing - # 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.0.5 # Official GitHub Pages deployment action - continue-on-error: true diff --git a/.github/workflows/rhiza_ci.yml b/.github/workflows/rhiza_ci.yml deleted file mode 100644 index d51368e..0000000 --- a/.github/workflows/rhiza_ci.yml +++ /dev/null @@ -1,105 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Workflow: Continuous Integration -# -# Purpose: Run tests on multiple Python versions to ensure compatibility. -# -# Trigger: On push and pull requests to main/master branches. - -name: (RHIZA) CI - -permissions: - contents: read - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - -jobs: - generate-matrix: - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.versions.outputs.list }} - steps: - - uses: actions/checkout@v6.0.2 - with: - lfs: true - - - name: Install uv - uses: astral-sh/setup-uv@v7.3.0 - with: - version: "0.10.2" - - - name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth - with: - token: ${{ secrets.GH_PAT }} - - - id: versions - env: - UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} - run: | - # Generate Python versions JSON from the script - JSON=$(make -f .rhiza/rhiza.mk -s version-matrix) - echo "list=$JSON" >> "$GITHUB_OUTPUT" - - - name: Debug matrix - run: | - echo "Python versions: ${{ steps.versions.outputs.list }}" - - test: - needs: generate-matrix - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ${{ fromJson(needs.generate-matrix.outputs.matrix) }} - fail-fast: false - - steps: - - name: Checkout repository - uses: actions/checkout@v6.0.2 - with: - lfs: true - - - name: Install uv - uses: astral-sh/setup-uv@v7.3.0 - with: - version: "0.10.2" - python-version: ${{ matrix.python-version }} - - - name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth - with: - token: ${{ secrets.GH_PAT }} - - - name: Run tests - env: - UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} - run: | - make test - - - docs-coverage: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v6.0.2 - - - name: Install uv - uses: astral-sh/setup-uv@v7.3.0 - with: - version: "0.10.2" - - - name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth - with: - token: ${{ secrets.GH_PAT }} - - - name: Check docs coverage - env: - UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} - run: | - make docs-coverage diff --git a/.github/workflows/rhiza_codeql.yml b/.github/workflows/rhiza_codeql.yml deleted file mode 100644 index 0473d9d..0000000 --- a/.github/workflows/rhiza_codeql.yml +++ /dev/null @@ -1,129 +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.0.2 - - - name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth - with: - token: ${{ secrets.GH_PAT }} - # 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.32.3 - 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.32.3 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/rhiza_deptry.yml b/.github/workflows/rhiza_deptry.yml deleted file mode 100644 index 30220da..0000000 --- a/.github/workflows/rhiza_deptry.yml +++ /dev/null @@ -1,45 +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.30-bookworm - - steps: - - uses: actions/checkout@v6.0.2 - - - name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth - with: - token: ${{ secrets.GH_PAT }} - - - name: Run deptry - run: make 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_devcontainer.yml b/.github/workflows/rhiza_devcontainer.yml deleted file mode 100644 index 1beda4e..0000000 --- a/.github/workflows/rhiza_devcontainer.yml +++ /dev/null @@ -1,137 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Devcontainer CI Workflow -# -# Purpose: -# Validates that the devcontainer image builds successfully when devcontainer-related -# files are changed. This workflow does NOT publish/push the image to any registry. -# -# Trigger Conditions: -# - Push to any branch when files in .devcontainer/ change -# - Pull requests to main/master when files in .devcontainer/ change -# - Changes to this workflow file itself -# -# Image Configuration: -# - Registry: Defaults to ghcr.io (override with DEVCONTAINER_REGISTRY variable) -# - Image name: {registry}/{owner}/{repository}/devcontainer -# - Image tag: {branch}-{commit-sha} (e.g., main-abc123def456) -# - Config file: Always .devcontainer/devcontainer.json -# -# Safeguards: -# - Checks if .devcontainer/devcontainer.json exists before building -# - Skips gracefully with a warning if devcontainer.json is not found -# - Converts repository owner to lowercase for Docker compatibility -# -# Publishing: -# Publishing only happens during releases via rhiza_release.yml when the -# PUBLISH_DEVCONTAINER repository variable is set to "true". -# -# For repos without devcontainers: -# This workflow won't trigger unless .devcontainer/ files exist and are modified, -# or this workflow file itself is changed (in which case it skips gracefully). - - -name: (RHIZA) DEVCONTAINER - -on: - push: - branches: - - '**' - paths: - - ".devcontainer/**" - - ".github/workflows/rhiza_devcontainer.yml" - - pull_request: - branches: [ main, master ] - paths: - - ".devcontainer/**" - - ".github/workflows/rhiza_devcontainer.yml" - -permissions: - contents: read - packages: write - -jobs: - build: - name: Build Devcontainer Image - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v6.0.2 - - - name: Set registry - id: registry - run: | - REGISTRY="${{ vars.DEVCONTAINER_REGISTRY }}" - if [ -z "$REGISTRY" ]; then - REGISTRY="ghcr.io" - fi - echo "registry=$REGISTRY" >> "$GITHUB_OUTPUT" - - - name: Login to Container Registry - uses: docker/login-action@v3.7.0 - with: - registry: ${{ steps.registry.outputs.registry }} - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - # Additional gate to skip build if no devcontainer.json exists - - name: Check devcontainer exists - id: check - run: | - if [ ! -f ".devcontainer/devcontainer.json" ]; then - echo "exists=false" >> "$GITHUB_OUTPUT" - echo "::warning::No .devcontainer/devcontainer.json found, skipping build" - else - echo "exists=true" >> "$GITHUB_OUTPUT" - fi - # repository owner to lowercase for Docker image naming, as devcontainers/ci does not safeguard - - name: Get lowercase repository owner - id: repo_owner - run: echo "owner_lc=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" - - - name: Get Image Name - if: steps.check.outputs.exists == 'true' - id: image_name - run: | - # Check if custom name is provided, otherwise use default - if [ -z "${{ vars.DEVCONTAINER_IMAGE_NAME }}" ]; then - REPO_NAME_LC=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]') - # Sanitize repo name: replace invalid characters with hyphens - # Docker image names must match [a-z0-9]+([._-][a-z0-9]+)* - # Replace leading dots and multiple consecutive separators - REPO_NAME_SANITIZED=$(echo "$REPO_NAME_LC" | sed 's/^[._-]*//; s/[._-][._-]*/-/g') - IMAGE_NAME="$REPO_NAME_SANITIZED/devcontainer" - echo "Using default image name component: $IMAGE_NAME" - else - IMAGE_NAME="${{ vars.DEVCONTAINER_IMAGE_NAME }}" - echo "Using custom image name component: $IMAGE_NAME" - fi - - # Validate the image component matches [a-z0-9]+([._-][a-z0-9]+)* with optional / separators - if ! echo "$IMAGE_NAME" | grep -qE '^[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*$'; then - echo "::error::Invalid image name component: $IMAGE_NAME" - echo "::error::Each component must match [a-z0-9]+([._-][a-z0-9]+)* separated by /" - exit 1 - fi - - IMAGE_NAME="${{ steps.registry.outputs.registry }}/${{ steps.repo_owner.outputs.owner_lc }}/$IMAGE_NAME" - echo "✅ Final image name: $IMAGE_NAME" - echo "image_name=$IMAGE_NAME" >> "$GITHUB_OUTPUT" - - - name: Sanitize Image Tag - if: steps.check.outputs.exists == 'true' - id: sanitized_tag - run: | - SANITIZED_TAG=$(echo "${{ github.ref_name }}-${{ github.sha }}" | tr '/' '-') - echo "sanitized_tag=$SANITIZED_TAG" >> "$GITHUB_OUTPUT" - - - name: Build Devcontainer Image - uses: devcontainers/ci@v0.3 - if: steps.check.outputs.exists == 'true' - with: - configFile: .devcontainer/devcontainer.json - push: never - imageName: ${{ steps.image_name.outputs.image_name }} - imageTag: ${{ steps.sanitized_tag.outputs.sanitized_tag }} diff --git a/.github/workflows/rhiza_docker.yml b/.github/workflows/rhiza_docker.yml deleted file mode 100644 index 8e4ba9a..0000000 --- a/.github/workflows/rhiza_docker.yml +++ /dev/null @@ -1,108 +0,0 @@ -# GitHub Actions workflow: Lint Dockerfile with hadolint and build the image -# -# This workflow runs on pushes and pull requests. It performs three main tasks: -# - Lints docker/Dockerfile using hadolint (fails the job on lint errors) -# - Builds the container image with Docker Buildx to ensure the Dockerfile is valid -# - Scans the built image for security vulnerabilities using Trivy -# -# Notes: -# - The image is built locally for validation only; it is not pushed to any registry. -# - Vulnerability scanning fails the build on CRITICAL or HIGH severity issues. -# - Scan results are uploaded to GitHub Security and as artifacts. -name: (RHIZA) DOCKER - -permissions: - contents: read - security-events: write - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - -jobs: - lint_and_build: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v6.0.2 - - - name: Detect docker/Dockerfile presence - id: check_dockerfile - run: | - if [ -f docker/Dockerfile ]; then - echo "docker_present=true" >> "$GITHUB_OUTPUT" - else - echo "docker_present=false" >> "$GITHUB_OUTPUT" - fi - - - name: Skip notice (no docker/Dockerfile present) - if: ${{ steps.check_dockerfile.outputs.docker_present != 'true' }} - run: echo "No docker/Dockerfile found; skipping hadolint and image build." - - - name: Lint Dockerfile with hadolint - if: ${{ steps.check_dockerfile.outputs.docker_present == 'true' }} - uses: hadolint/hadolint-action@v3.3.0 - with: - dockerfile: docker/Dockerfile - # Fail on any error-level findings (default behavior). Adjust if needed: - # failure-threshold: error - - - name: Set up Docker Buildx - if: ${{ steps.check_dockerfile.outputs.docker_present == 'true' }} - uses: docker/setup-buildx-action@v3.12.0 - - - name: Read Python version from .python-version - if: ${{ steps.check_dockerfile.outputs.docker_present == 'true' }} - id: python_version - run: | - if [ -f .python-version ]; then - PYTHON_VERSION=$(tr -d '[:space:]' < .python-version) - echo "version=$PYTHON_VERSION" >> "$GITHUB_OUTPUT" - echo "Using Python version: $PYTHON_VERSION" - else - echo "version=3.12" >> "$GITHUB_OUTPUT" - echo "::warning::.python-version not found, using default 3.12" - fi - - - name: Build container image with Docker Buildx (validation only) - id: docker_build - if: ${{ steps.check_dockerfile.outputs.docker_present == 'true' }} - run: | - # Derive image tag from repository name and sanitize (remove '.' and '_') - REPO_NAME="${GITHUB_REPOSITORY##*/}" - REPO_NAME="${REPO_NAME//[._]/}" - docker buildx build \ - --file docker/Dockerfile \ - --build-arg PYTHON_VERSION=${{ steps.python_version.outputs.version }} \ - --tag "${REPO_NAME}:ci" \ - --load \ - . - echo "image_name=${REPO_NAME}:ci" >> "$GITHUB_OUTPUT" - - - name: Scan Docker Image with Trivy - uses: aquasecurity/trivy-action@0.34.0 - if: ${{ steps.check_dockerfile.outputs.docker_present == 'true' }} - with: - image-ref: '${{ steps.docker_build.outputs.image_name }}' - format: 'sarif' - output: 'trivy-results.sarif' - exit-code: '0' # Don't fail on vulnerabilities - rely on SARIF upload to GitHub Security - ignore-unfixed: true - vuln-type: 'os,library' - severity: 'CRITICAL,HIGH' - - - name: Upload Trivy scan results to GitHub Security - uses: github/codeql-action/upload-sarif@v4 - if: always() && steps.check_dockerfile.outputs.docker_present == 'true' - with: - sarif_file: 'trivy-results.sarif' - - - name: Upload Trivy scan results as artifact - uses: actions/upload-artifact@v6.0.0 - if: always() && steps.check_dockerfile.outputs.docker_present == 'true' - with: - name: trivy-docker-report - path: trivy-results.sarif diff --git a/.github/workflows/rhiza_marimo.yml b/.github/workflows/rhiza_marimo.yml deleted file mode 100644 index 9405e14..0000000 --- a/.github/workflows/rhiza_marimo.yml +++ /dev/null @@ -1,108 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Workflow: Marimo Notebooks -# -# Purpose: This workflow discovers and executes all Marimo notebooks in the -# repository. It builds a dynamic matrix to run each notebook in -# parallel to surface errors early and keep notebooks reproducible. -# -# Trigger: This workflow runs on every push and on pull requests to main/master -# branches (including from forks) -# -# Components: -# - 🔎 Discover notebooks in book/marimo -# - 🧪 Run each notebook in parallel using a matrix strategy -# - ✅ Fail-fast disabled to report all failing notebooks - -name: "(RHIZA) MARIMO" - -permissions: - contents: read - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - -jobs: - # Build a matrix of notebooks to test - list-notebooks: - runs-on: ubuntu-latest - outputs: - notebook-list: ${{ steps.notebooks.outputs.matrix }} - steps: - # Check out the repository code - - 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 - id: notebooks - run: | - # Extract MARIMO_FOLDER from the project configuration (via Makefile) - # shellcheck disable=SC2016 # Single quotes intentional - Make syntax, not shell expansion - NOTEBOOK_DIR=$(make -s -f Makefile -f - <<< 'print: ; @echo $(or $(MARIMO_FOLDER),marimo)' print) - - echo "Searching notebooks in: $NOTEBOOK_DIR" - # Check if directory exists - if [ ! -d "$NOTEBOOK_DIR" ]; then - echo "Directory $NOTEBOOK_DIR does not exist. Setting empty matrix." - echo "matrix=[]" >> "$GITHUB_OUTPUT" - exit 0 - fi - - # Find notebooks and handle empty results - if [ -z "$(find "$NOTEBOOK_DIR" -maxdepth 1 -name "*.py" 2>/dev/null)" ]; then - echo "No notebooks found in $NOTEBOOK_DIR. Setting empty matrix." - echo "matrix=[]" >> "$GITHUB_OUTPUT" - else - notebooks=$(find "$NOTEBOOK_DIR" -maxdepth 1 -name "*.py" -print0 | xargs -0 -n1 echo | jq -R -s -c 'split("\n")[:-1]') - echo "matrix=$notebooks" >> "$GITHUB_OUTPUT" - fi - shell: bash - - # Create one job per notebook using the matrix strategy for parallel execution - test-notebooks: - if: needs.list-notebooks.outputs.notebook-list != '[]' - runs-on: ubuntu-latest - needs: list-notebooks - strategy: - matrix: - notebook: ${{ fromJson(needs.list-notebooks.outputs.notebook-list) }} - # Don't fail the entire workflow if one notebook fails - fail-fast: false - name: Run notebook ${{ matrix.notebook }} - steps: - # Check out the repository code - - uses: actions/checkout@v6.0.2 - with: - lfs: true - - # Install uv/uvx - - name: Install uv - uses: astral-sh/setup-uv@v7.3.0 - with: - version: "0.10.2" - - - 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 - env: - UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} - run: | - uvx uv run "${{ matrix.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 - # ✅ This forces the notebook to explicitly handle dependencies (e.g., uv install ., or pip install inside the script). - # ✅ It’s a true integration smoke test. - # Benefits of this pattern - # Confirms the notebook can bootstrap itself in a fresh environment - # Catches missing uv install or pip steps early - # Ensures CI/other users can run the notebook without manual setup - shell: bash diff --git a/.github/workflows/rhiza_pre-commit.yml b/.github/workflows/rhiza_pre-commit.yml deleted file mode 100644 index df38fa0..0000000 --- a/.github/workflows/rhiza_pre-commit.yml +++ /dev/null @@ -1,52 +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 -# - 💾 Cache pre-commit environments to speed up runs - -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.0.2 - - - name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth - with: - token: ${{ secrets.GH_PAT }} - - # Cache pre-commit environments and hooks - - name: Cache pre-commit environments - uses: actions/cache@v5 - with: - path: ~/.cache/pre-commit - key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} - restore-keys: | - pre-commit-${{ runner.os }}- - - # Run pre-commit - - name: Run pre-commit - run: | - make fmt diff --git a/.github/workflows/rhiza_release.yml b/.github/workflows/rhiza_release.yml deleted file mode 100644 index 735bad5..0000000 --- a/.github/workflows/rhiza_release.yml +++ /dev/null @@ -1,449 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Release Workflow for Python Packages with Optional Devcontainer Publishing -# -# This workflow implements a secure, maintainable release pipeline with distinct phases: -# -# 📋 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. 📦 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" -# - Requires .devcontainer directory to exist -# - Uses the release tag (e.g., v1.2.3) as the image tag -# - Image name: {registry}/{owner}/{repository}/devcontainer -# - Registry defaults to ghcr.io (override with DEVCONTAINER_REGISTRY variable) -# - Config file: Always .devcontainer/devcontainer.json -# - Adds devcontainer image link to GitHub release notes -# -# 🚀 PyPI Publishing: -# - Skipped if no dist/ artifacts exist -# - Skipped if pyproject.toml contains "Private :: Do Not Upload" -# - Uses Trusted Publishing (OIDC) for PyPI (no stored credentials) -# - For custom feeds, use PYPI_REPOSITORY_URL and PYPI_TOKEN secrets -# - Adds PyPI/custom feed link to GitHub release notes -# -# 🔐 Security: -# - 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 (public repos only) -# - SBOM attestations generated for supply chain transparency (public repos only) -# -# 📄 Requirements: -# - pyproject.toml with top-level version field (for Python packages) -# - Package registered on PyPI as Trusted Publisher (for PyPI publishing) -# - PUBLISH_DEVCONTAINER variable set to "true" (for devcontainer publishing) -# - .devcontainer/devcontainer.json file (for devcontainer publishing) -# -# ✅ To Trigger: -# Create and push a version tag: -# git tag v1.2.3 -# git push origin v1.2.3 -# -# 🎯 For repos without Python packages: -# The workflow gracefully skips Python-related steps if pyproject.toml doesn't exist. -# -# 🎯 For repos without devcontainers: -# The workflow gracefully skips devcontainer steps if PUBLISH_DEVCONTAINER is not -# set to "true" or .devcontainer directory doesn't exist. - - -name: (RHIZA) RELEASE - -on: - push: - tags: - - 'v*' - -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 (public repos only) - -jobs: - tag: - name: Validate Tag - runs-on: ubuntu-latest - outputs: - tag: ${{ steps.set_tag.outputs.tag }} - steps: - - name: Checkout Code - uses: actions/checkout@v6.0.2 - with: - fetch-depth: 0 - - - name: Set Tag Variable - id: set_tag - run: | - TAG="${GITHUB_REF#refs/tags/}" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - - - name: Validate Release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ steps.set_tag.outputs.tag }} - run: | - if gh release view "$TAG" >/dev/null 2>&1; then - DRAFT_STATUS=$(gh release view "$TAG" --json isDraft --jq '.isDraft') - if [ "$DRAFT_STATUS" != "true" ]; then - echo "::error::Release '$TAG' already exists and is not a draft. Please use a new tag." - exit 1 - else - echo "::warning::Release '$TAG' exists as a draft. Will proceed to update it." - fi - fi - - build: - name: Build - runs-on: ubuntu-latest - needs: tag - steps: - - name: Checkout Code - uses: actions/checkout@v6.0.2 - with: - fetch-depth: 0 - - - name: Install uv - uses: astral-sh/setup-uv@v7.3.0 - with: - version: "0.10.2" - - - 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') != '' - run: | - TAG_VERSION="${{ needs.tag.outputs.tag }}" - 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'" - exit 1 - fi - echo "Version verified: $PROJECT_VERSION matches tag" - - - name: Detect buildable Python package - id: buildable - run: | - if [[ -f pyproject.toml ]] && grep -q '^\[build-system\]' pyproject.toml; then - echo "buildable=true" >> "$GITHUB_OUTPUT" - else - echo "buildable=false" >> "$GITHUB_OUTPUT" - fi - - - name: Build - if: steps.buildable.outputs.buildable == 'true' - run: | - printf "[INFO] Building package...\n" - uvx hatch 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 --of JSON -o sbom.cdx.json - uvx --from 'cyclonedx-bom>=7.0.0' cyclonedx-py environment --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-sbom@v3 - with: - subject-path: sbom.cdx.json - sbom-path: sbom.cdx.json - - - name: Upload SBOM artifacts - if: hashFiles('pyproject.toml') != '' - uses: actions/upload-artifact@v6.0.0 - with: - name: sbom - path: | - sbom.cdx.json - sbom.cdx.xml - - - name: Generate SLSA provenance attestations - if: steps.buildable.outputs.buildable == 'true' && github.event.repository.private == false - uses: actions/attest-build-provenance@v3 - with: - subject-path: dist/* - - - name: Upload dist artifact - if: steps.buildable.outputs.buildable == 'true' - uses: actions/upload-artifact@v6.0.0 - with: - name: dist - path: dist - - # Don't try to upload artifacts to GitHub Release at all - draft-release: - name: Draft GitHub Release - runs-on: ubuntu-latest - needs: [tag, build] - - steps: - - name: Download SBOM artifact - # Downloads sbom.cdx.json and sbom.cdx.xml into sbom/ directory - uses: actions/download-artifact@v6.0.0 - with: - name: sbom - path: sbom - continue-on-error: true - - - name: Create GitHub Release with artifacts - uses: softprops/action-gh-release@v2.5.0 - with: - tag_name: ${{ needs.tag.outputs.tag }} - name: ${{ needs.tag.outputs.tag }} - generate_release_notes: true - draft: true - files: | - sbom/* - - # Decide at step-level whether to publish - pypi: - name: Publish to PyPI - runs-on: ubuntu-latest - environment: release - needs: [tag, build, draft-release] - outputs: - should_publish: ${{ steps.check_dist.outputs.should_publish }} - - steps: - - name: Checkout Code - uses: actions/checkout@v6.0.2 - with: - fetch-depth: 0 - - - name: Download dist artifact - uses: actions/download-artifact@v6.0.0 - with: - name: dist - path: dist - # Continue if dist folder is not found. Don't fail the job. - continue-on-error: true - - - name: Check if dist contains artifacts and not marked as private - id: check_dist - run: | - if [[ ! -d dist ]]; then - echo "::warning::No folder dist/. Skipping PyPI publish." - echo "should_publish=false" >> "$GITHUB_OUTPUT" - else - if grep -R "Private :: Do Not Upload" pyproject.toml; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - else - echo "should_publish=true" >> "$GITHUB_OUTPUT" - fi - fi - cat "$GITHUB_OUTPUT" - - # this should not take place, as "Private :: Do Not Upload" set in pyproject.toml - # repository-url and password only used for custom feeds, not for PyPI with OIDC - - name: Publish to PyPI - if: ${{ steps.check_dist.outputs.should_publish == 'true' }} - uses: pypa/gh-action-pypi-publish@release/v1 - with: - packages-dir: dist/ - skip-existing: true - repository-url: ${{ vars.PYPI_REPOSITORY_URL }} - password: ${{ secrets.PYPI_TOKEN }} - - devcontainer: - name: Publish Devcontainer Image - runs-on: ubuntu-latest - environment: release - needs: [tag, build, draft-release] - outputs: - should_publish: ${{ steps.check_publish.outputs.should_publish }} - image_name: ${{ steps.image_name.outputs.image_name }} - steps: - - name: Checkout Code - uses: actions/checkout@v6.0.2 - with: - fetch-depth: 0 - - - name: Check if devcontainer should be published - id: check_publish - env: - PUBLISH_DEVCONTAINER: ${{ vars.PUBLISH_DEVCONTAINER }} - run: | - if [[ "$PUBLISH_DEVCONTAINER" == "true" ]] && [[ -d ".devcontainer" ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - echo "🚀 Will build and publish devcontainer image" - else - echo "should_publish=false" >> "$GITHUB_OUTPUT" - echo "⏭️ Skipping devcontainer publish (PUBLISH_DEVCONTAINER not true or .devcontainer missing)" - fi - - - name: Set registry - if: steps.check_publish.outputs.should_publish == 'true' - id: registry - run: | - REGISTRY="${{ vars.DEVCONTAINER_REGISTRY }}" - if [ -z "$REGISTRY" ]; then - REGISTRY="ghcr.io" - fi - echo "registry=$REGISTRY" >> "$GITHUB_OUTPUT" - - - name: Login to Container Registry - if: steps.check_publish.outputs.should_publish == 'true' - uses: docker/login-action@v3.7.0 - with: - registry: ${{ steps.registry.outputs.registry }} - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Get lowercase repository owner - if: steps.check_publish.outputs.should_publish == 'true' - id: repo_owner - run: echo "owner_lc=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" - - - name: Get Image Name - if: steps.check_publish.outputs.should_publish == 'true' - id: image_name - run: | - # Check if custom name is provided, otherwise use default - if [ -z "${{ vars.DEVCONTAINER_IMAGE_NAME }}" ]; then - REPO_NAME_LC=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]') - # Sanitize repo name: replace invalid characters with hyphens - # Docker image names must match [a-z0-9]+([._-][a-z0-9]+)* - # Replace leading dots and multiple consecutive separators - REPO_NAME_SANITIZED=$(echo "$REPO_NAME_LC" | sed 's/^[._-]*//; s/[._-][._-]*/-/g') - IMAGE_NAME="$REPO_NAME_SANITIZED/devcontainer" - echo "Using default image name component: $IMAGE_NAME" - else - IMAGE_NAME="${{ vars.DEVCONTAINER_IMAGE_NAME }}" - echo "Using custom image name component: $IMAGE_NAME" - fi - - # Validate the image component matches [a-z0-9]+([._-][a-z0-9]+)* with optional / separators - if ! echo "$IMAGE_NAME" | grep -qE '^[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*$'; then - echo "::error::Invalid image name component: $IMAGE_NAME" - echo "::error::Each component must match [a-z0-9]+([._-][a-z0-9]+)* separated by /" - exit 1 - fi - - IMAGE_NAME="${{ steps.registry.outputs.registry }}/${{ steps.repo_owner.outputs.owner_lc }}/$IMAGE_NAME" - echo "✅ Final image name: $IMAGE_NAME" - echo "image_name=$IMAGE_NAME" >> "$GITHUB_OUTPUT" - - - name: Build and Publish Devcontainer Image - if: steps.check_publish.outputs.should_publish == 'true' - uses: devcontainers/ci@v0.3 - with: - configFile: .devcontainer/devcontainer.json - push: always - imageName: ${{ steps.image_name.outputs.image_name }} - imageTag: ${{ needs.tag.outputs.tag }} - - finalise-release: - name: Finalise Release - runs-on: ubuntu-latest - needs: [tag, pypi, devcontainer] - if: needs.pypi.result == 'success' || needs.devcontainer.result == 'success' - steps: - - name: Checkout Code - uses: actions/checkout@v6.0.2 - with: - fetch-depth: 0 - - - name: Install uv - uses: astral-sh/setup-uv@v7.3.0 - with: - version: "0.10.2" - - - 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: Set up Python - uses: actions/setup-python@v6.2.0 - - - name: Generate Devcontainer Link - id: devcontainer_link - if: needs.devcontainer.outputs.should_publish == 'true' && needs.devcontainer.result == 'success' - run: | - FULL_IMAGE="${{ needs.devcontainer.outputs.image_name }}:${{ needs.tag.outputs.tag }}" - { - echo "message<> "$GITHUB_OUTPUT" - - - name: Generate PyPI Link - id: pypi_link - if: needs.pypi.outputs.should_publish == 'true' && needs.pypi.result == 'success' - run: | - 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} - - REPO_URL="${{ vars.PYPI_REPOSITORY_URL || '' }}" - if [ -z "$REPO_URL" ]; then - LINK="https://pypi.org/project/$PACKAGE_NAME/$VERSION/" - NAME="PyPI Package" - else - LINK="$REPO_URL" - NAME="Custom Feed Package" - fi - - { - echo "message<> "$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 }} - diff --git a/.github/workflows/rhiza_security.yml b/.github/workflows/rhiza_security.yml deleted file mode 100644 index 7ded477..0000000 --- a/.github/workflows/rhiza_security.yml +++ /dev/null @@ -1,49 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Workflow: Security -# -# Purpose: This workflow runs security scans on the project including: -# - pip-audit: Check for known vulnerabilities in dependencies -# - bandit: Static analysis for common security issues in Python code -# -# Trigger: This workflow runs on every push and on pull requests to main/master -# branches (including from forks) - -name: "(RHIZA) SECURITY" - -# Permissions: Only read access to repository contents is needed -permissions: - contents: read - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - -jobs: - security: - name: Security scanning - runs-on: ubuntu-latest - container: - image: ghcr.io/astral-sh/uv:0.9.30-bookworm - - steps: - - uses: actions/checkout@v6.0.2 - - - name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth - with: - token: ${{ secrets.GH_PAT }} - - - name: Run security scans - env: - UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} - run: make security - - - - name: Run typecheck - env: - UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} - run: make typecheck diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index fdacbf6..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,67 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 - hooks: - - id: check-toml - - id: check-yaml - args: ['--unsafe'] - - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.15.1' - hooks: - - id: ruff - args: [ --fix, --exit-non-zero-on-fix, --unsafe-fixes ] - - # Run the formatter - - id: ruff-format - - - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.47.0 - hooks: - - id: markdownlint - args: ["--disable", "MD013"] - - - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.36.1 - hooks: - - id: check-renovate - args: [ "--verbose" ] - - - id: check-github-workflows - args: ["--verbose"] - - - repo: https://github.com/rhysd/actionlint - rev: v1.7.11 - hooks: - - id: actionlint - - - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.25 - hooks: - - id: validate-pyproject - - - repo: https://github.com/PyCQA/bandit - rev: 1.9.3 - hooks: - - id: bandit - args: ["--skip", "B101", "--exclude", ".venv,tests,.rhiza/tests,.git,.pytest_cache", "-c", "pyproject.toml"] - - - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.10.2 - hooks: - - id: uv-lock - - - repo: https://github.com/Jebel-Quant/rhiza-hooks - rev: v0.3.0 # Use the latest release - hooks: - # Migrated from rhiza - - id: check-rhiza-workflow-names - - 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 deleted file mode 100644 index fdcfcfd..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.12 \ No newline at end of file diff --git a/.rhiza/completions/README.md b/.rhiza/completions/README.md deleted file mode 100644 index cb99e27..0000000 --- a/.rhiza/completions/README.md +++ /dev/null @@ -1,263 +0,0 @@ -# Shell Completion for Rhiza Make Targets - -This directory contains shell completion scripts for Bash and Zsh that provide tab-completion for make targets in Rhiza-based projects. - -## Features - -- ✅ Tab-complete all available make targets -- ✅ Show target descriptions in Zsh -- ✅ Complete common make variables (DRY_RUN, BUMP, ENV, etc.) -- ✅ Works with any Rhiza-based project -- ✅ Auto-discovers targets from Makefile and included .mk files - -## Installation - -### Bash - -#### Method 1: Source in your shell config - -Add to your `~/.bashrc` or `~/.bash_profile`: - -```bash -# Rhiza make completion -if [ -f /path/to/project/.rhiza/completions/rhiza-completion.bash ]; then - source /path/to/project/.rhiza/completions/rhiza-completion.bash -fi -``` - -Replace `/path/to/project` with the actual path to your Rhiza project. - -#### Method 2: System-wide installation - -```bash -# Copy to bash completion directory -sudo cp .rhiza/completions/rhiza-completion.bash /etc/bash_completion.d/rhiza - -# Reload completions -source /etc/bash_completion.d/rhiza -``` - -#### Method 3: User-local installation - -```bash -# Create local completion directory -mkdir -p ~/.local/share/bash-completion/completions - -# Copy completion script -cp .rhiza/completions/rhiza-completion.bash ~/.local/share/bash-completion/completions/make - -# Reload bash -source ~/.bashrc -``` - -### Zsh - -#### Method 1: User-local installation (Recommended) - -```bash -# Create completion directory -mkdir -p ~/.zsh/completion - -# Copy completion script -cp .rhiza/completions/rhiza-completion.zsh ~/.zsh/completion/_make - -# Add to ~/.zshrc (if not already present) -echo 'fpath=(~/.zsh/completion $fpath)' >> ~/.zshrc -echo 'autoload -U compinit && compinit' >> ~/.zshrc - -# Reload zsh -source ~/.zshrc -``` - -#### Method 2: Source directly - -Add to your `~/.zshrc`: - -```zsh -# Rhiza make completion -if [ -f /path/to/project/.rhiza/completions/rhiza-completion.zsh ]; then - source /path/to/project/.rhiza/completions/rhiza-completion.zsh -fi -``` - -#### Method 3: System-wide installation - -```bash -# Copy to system completion directory -sudo cp .rhiza/completions/rhiza-completion.zsh /usr/local/share/zsh/site-functions/_make - -# Reload zsh -exec zsh -``` - -## Usage - -Once installed, you can tab-complete make targets: - -```bash -# Tab-complete targets -make - -# Complete with prefix -make te # Expands to: make test - -# Complete variables -make BUMP= # Shows: patch, minor, major - -# Works with any target -make doc # Shows: docs, docker-build, docker-run, etc. -``` - -### Zsh Benefits - -In Zsh, you'll also see descriptions for targets: - -```bash -make -# Shows: -# test -- run all tests -# fmt -- check the pre-commit hooks and the linting -# install -- install -# docs -- create documentation with pdoc -# ... -``` - -## Common Variables - -The completion scripts understand these common variables: - -| Variable | Values | Description | -|----------|--------|-------------| -| `DRY_RUN` | `1` | Preview mode without making changes | -| `BUMP` | `patch`, `minor`, `major` | Version bump type | -| `ENV` | `dev`, `staging`, `prod` | Target environment | -| `COVERAGE_FAIL_UNDER` | (number) | Minimum coverage threshold | -| `PYTHON_VERSION` | (version) | Override Python version | - -Example usage: - -```bash -# Tab-complete after typing DRY_ -make DRY_ # Expands to: make DRY_RUN=1 - -# Tab-complete variable values -make BUMP= # Shows: patch minor major - -# Combine with targets -make bump BUMP= -``` - -## Troubleshooting - -### Bash: Completions not working - -1. Check if bash-completion is installed: - ```bash - # Debian/Ubuntu - sudo apt-get install bash-completion - - # macOS - brew install bash-completion@2 - ``` - -2. Ensure completion is enabled in your shell: - ```bash - # Add to ~/.bashrc if not present - if [ -f /etc/bash_completion ]; then - . /etc/bash_completion - fi - ``` - -3. Reload your shell configuration: - ```bash - source ~/.bashrc - ``` - -### Zsh: Completions not working - -1. Check if compinit is called in your `~/.zshrc`: - ```zsh - autoload -U compinit && compinit - ``` - -2. Clear the completion cache: - ```bash - rm -f ~/.zcompdump - compinit - ``` - -3. Ensure the script is in your fpath: - ```zsh - echo $fpath - ``` - -4. Reload your shell configuration: - ```zsh - source ~/.zshrc - ``` - -### No targets appearing - -1. Ensure you're in a directory with a Makefile: - ```bash - ls -la Makefile - ``` - -2. Test that make can parse the Makefile: - ```bash - make -qp 2>/dev/null | head - ``` - -3. Manually source the completion script to test: - ```bash - # Bash - source .rhiza/completions/rhiza-completion.bash - - # Zsh - source .rhiza/completions/rhiza-completion.zsh - ``` - -## Optional Aliases - -You can add shortcuts in your shell config: - -```bash -# Add to ~/.bashrc or ~/.zshrc -alias m='make' - -# For bash: -complete -F _rhiza_make_completion m - -# For zsh: -compdef _rhiza_make m -``` - -Then use: -```bash -m te # Expands to: m test -``` - -## Technical Details - -### How it works - -1. **Target Discovery**: Parses `make -qp` output to find all targets -2. **Description Extraction**: Looks for `##` comments after target names -3. **Variable Detection**: Includes common Makefile variables -4. **Dynamic Completion**: Regenerates list each time you tab - -### Performance - -- Completions are generated on-demand (when you press Tab) -- For large Makefiles (100+ targets), there may be a small delay -- Results are not cached to ensure targets are always current - -## See Also - -- [Tools Reference](../../docs/TOOLS_REFERENCE.md) - Complete command reference -- [Quick Reference](../../docs/QUICK_REFERENCE.md) - Quick command reference -- [Extending Rhiza](../../docs/EXTENDING_RHIZA.md) - How to add custom targets - ---- - -*Last updated: 2026-02-15* diff --git a/.rhiza/completions/rhiza-completion.bash b/.rhiza/completions/rhiza-completion.bash deleted file mode 100644 index 0f860da..0000000 --- a/.rhiza/completions/rhiza-completion.bash +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash -# Bash completion for Rhiza make targets -# -# Installation: -# Source this file in your ~/.bashrc or ~/.bash_profile: -# source /path/to/.rhiza/completions/rhiza-completion.bash -# -# Or copy to bash completion directory: -# sudo cp .rhiza/completions/rhiza-completion.bash /etc/bash_completion.d/rhiza -# - -_rhiza_make_completion() { - local cur prev opts - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - - # Check if we're in a directory with a Makefile - if [[ ! -f "Makefile" ]]; then - return 0 - fi - - # Extract make targets from Makefile and all included .mk files - # Looks for lines like: target: ## description - opts=$(make -qp 2>/dev/null | \ - awk -F':' '/^[a-zA-Z0-9][^$#\/\t=]*:([^=]|$)/ {split($1,A,/ /);for(i in A)print A[i]}' | \ - grep -v '^Makefile$' | \ - sort -u) - - # Add common make variables that can be overridden - local vars="DRY_RUN=1 BUMP=patch BUMP=minor BUMP=major ENV=dev ENV=staging ENV=prod" - opts="$opts $vars" - - # Generate completions - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 -} - -# Register the completion function for make command -complete -F _rhiza_make_completion make - -# Also complete for direct make invocation with path -complete -F _rhiza_make_completion ./Makefile - -# Helpful aliases (optional - uncomment if desired) -# alias m='make' -# complete -F _rhiza_make_completion m diff --git a/.rhiza/completions/rhiza-completion.zsh b/.rhiza/completions/rhiza-completion.zsh deleted file mode 100644 index 03931c9..0000000 --- a/.rhiza/completions/rhiza-completion.zsh +++ /dev/null @@ -1,88 +0,0 @@ -#compdef make -# Zsh completion for Rhiza make targets -# -# Installation: -# Add this file to your fpath and ensure compinit is called: -# -# Method 1 (User-local): -# mkdir -p ~/.zsh/completion -# cp .rhiza/completions/rhiza-completion.zsh ~/.zsh/completion/_make -# Add to ~/.zshrc: -# fpath=(~/.zsh/completion $fpath) -# autoload -U compinit && compinit -# -# Method 2 (Source directly): -# Add to ~/.zshrc: -# source /path/to/.rhiza/completions/rhiza-completion.zsh -# -# Method 3 (System-wide): -# sudo cp .rhiza/completions/rhiza-completion.zsh /usr/local/share/zsh/site-functions/_make -# - -_rhiza_make() { - local -a targets variables - - # Check if we're in a directory with a Makefile - if [[ ! -f "Makefile" ]]; then - return 0 - fi - - # Extract make targets with descriptions - # Format: target:description - targets=(${(f)"$( - make -qp 2>/dev/null | \ - awk -F':' ' - /^# Files/,/^# Finished Make data base/ { - if (/^[a-zA-Z0-9_-]+:.*##/) { - target=$1 - desc=$0 - sub(/^[^#]*## */, "", desc) - gsub(/^[ \t]+/, "", target) - print target ":" desc - } - } - ' | \ - grep -v '^Makefile:' | \ - sort -u - )"}) - - # Also get targets without descriptions - local -a plain_targets - plain_targets=(${(f)"$( - make -qp 2>/dev/null | \ - awk -F':' '/^[a-zA-Z0-9_-]+:([^=]|$)/ { - split($1,A,/ /) - for(i in A) print A[i] - }' | \ - grep -v '^Makefile$' | \ - sort -u - )"}) - - # Common make variables - variables=( - 'DRY_RUN=1:preview mode without making changes' - 'BUMP=patch:bump patch version' - 'BUMP=minor:bump minor version' - 'BUMP=major:bump major version' - 'ENV=dev:development environment' - 'ENV=staging:staging environment' - 'ENV=prod:production environment' - 'COVERAGE_FAIL_UNDER=:minimum coverage threshold' - 'PYTHON_VERSION=:override Python version' - ) - - # Combine all completions - local -a all_completions - all_completions=($targets $plain_targets $variables) - - # Show completions with descriptions - _describe 'make targets' all_completions -} - -# Register the completion function -compdef _rhiza_make make - -# Optional: Add completion for common aliases -# Uncomment these if you use these aliases -# alias m='make' -# compdef _rhiza_make m diff --git a/.rhiza/make.d/bootstrap.mk b/.rhiza/make.d/bootstrap.mk deleted file mode 100644 index 374c433..0000000 --- a/.rhiza/make.d/bootstrap.mk +++ /dev/null @@ -1,107 +0,0 @@ -## .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/make.d/docs.mk b/.rhiza/make.d/docs.mk deleted file mode 100644 index ad44503..0000000 --- a/.rhiza/make.d/docs.mk +++ /dev/null @@ -1,96 +0,0 @@ -## docs.mk - Documentation generation targets -# This file is included by the main Makefile. -# It provides targets for generating API documentation using pdoc -# and building/serving MkDocs documentation sites. - -# Declare phony targets (they don't produce files) -.PHONY: docs mkdocs mkdocs-serve mkdocs-build - -# Default output directory for MkDocs (HTML site) -MKDOCS_OUTPUT ?= _mkdocs - -# MkDocs config file location -MKDOCS_CONFIG ?= docs/mkdocs.yml - -# Default pdoc template directory (can be overridden) -PDOC_TEMPLATE_DIR ?= book/pdoc-templates - -##@ 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 'mkdocs-build' target builds the MkDocs documentation site. -# 1. Checks if the mkdocs.yml config file exists. -# 2. Cleans up any previous output. -# 3. Builds the static site using mkdocs with material theme. -mkdocs-build:: install-uv ## build MkDocs documentation site - @printf "${BLUE}[INFO] Building MkDocs site...${RESET}\n" - @if [ -f "$(MKDOCS_CONFIG)" ]; then \ - rm -rf "$(MKDOCS_OUTPUT)"; \ - MKDOCS_OUTPUT_ABS="$$(pwd)/$(MKDOCS_OUTPUT)"; \ - ${UVX_BIN} --with mkdocs-material --with "pymdown-extensions>=10.0" mkdocs build \ - -f "$(MKDOCS_CONFIG)" \ - -d "$$MKDOCS_OUTPUT_ABS"; \ - else \ - printf "${YELLOW}[WARN] $(MKDOCS_CONFIG) not found, skipping MkDocs build${RESET}\n"; \ - fi - -# The 'mkdocs-serve' target serves the documentation with live reload. -# Useful for local development and previewing changes. -mkdocs-serve: install-uv ## serve MkDocs site with live reload - @if [ -f "$(MKDOCS_CONFIG)" ]; then \ - ${UVX_BIN} --with mkdocs-material --with "pymdown-extensions>=10.0" mkdocs serve \ - -f "$(MKDOCS_CONFIG)"; \ - else \ - printf "${RED}[ERROR] $(MKDOCS_CONFIG) not found${RESET}\n"; \ - exit 1; \ - fi - -# Convenience alias -mkdocs: mkdocs-serve ## alias for mkdocs-serve diff --git a/.rhiza/make.d/marimo.mk b/.rhiza/make.d/marimo.mk deleted file mode 100644 index 2597573..0000000 --- a/.rhiza/make.d/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/.rhiza/make.d/presentation.mk b/.rhiza/make.d/presentation.mk deleted file mode 100644 index d5fe8db..0000000 --- a/.rhiza/make.d/presentation.mk +++ /dev/null @@ -1,70 +0,0 @@ -## presentation.mk - Presentation targets -# This file is included by the main Makefile - -# Declare phony targets (they don't produce files) -.PHONY: presentation presentation-pdf presentation-serve - -##@ Presentation -presentation:: ## generate presentation slides from PRESENTATION.md using Marp - @printf "${BLUE}[INFO] Checking for Marp CLI...${RESET}\n" - @if ! command -v marp >/dev/null 2>&1; then \ - if command -v npm >/dev/null 2>&1; then \ - printf "${YELLOW}[WARN] Marp CLI not found. Installing with npm...${RESET}\n"; \ - npm install -g @marp-team/marp-cli || { \ - printf "${RED}[ERROR] Failed to install Marp CLI. Please install manually:${RESET}\n"; \ - printf "${BLUE} npm install -g @marp-team/marp-cli${RESET}\n"; \ - exit 1; \ - }; \ - else \ - printf "${RED}[ERROR] npm not found. Please install Node.js and npm first.${RESET}\n"; \ - printf "${BLUE} See: https://nodejs.org/${RESET}\n"; \ - printf "${BLUE} Then run: npm install -g @marp-team/marp-cli${RESET}\n"; \ - exit 1; \ - fi; \ - fi - @printf "${BLUE}[INFO] Generating HTML presentation...${RESET}\n" - @marp PRESENTATION.md -o presentation.html - @printf "${GREEN}[SUCCESS] Presentation generated: presentation.html${RESET}\n" - @printf "${BLUE}[TIP] Open presentation.html in a browser to view slides${RESET}\n" - -presentation-pdf:: ## generate PDF presentation from PRESENTATION.md using Marp - @printf "${BLUE}[INFO] Checking for Marp CLI...${RESET}\n" - @if ! command -v marp >/dev/null 2>&1; then \ - if command -v npm >/dev/null 2>&1; then \ - printf "${YELLOW}[WARN] Marp CLI not found. Installing with npm...${RESET}\n"; \ - npm install -g @marp-team/marp-cli || { \ - printf "${RED}[ERROR] Failed to install Marp CLI. Please install manually:${RESET}\n"; \ - printf "${BLUE} npm install -g @marp-team/marp-cli${RESET}\n"; \ - exit 1; \ - }; \ - else \ - printf "${RED}[ERROR] npm not found. Please install Node.js and npm first.${RESET}\n"; \ - printf "${BLUE} See: https://nodejs.org/${RESET}\n"; \ - printf "${BLUE} Then run: npm install -g @marp-team/marp-cli${RESET}\n"; \ - exit 1; \ - fi; \ - fi - @printf "${BLUE}[INFO] Generating PDF presentation...${RESET}\n" - @marp PRESENTATION.md -o presentation.pdf --allow-local-files - @printf "${GREEN}[SUCCESS] Presentation generated: presentation.pdf${RESET}\n" - -presentation-serve:: ## serve presentation interactively with Marp - @printf "${BLUE}[INFO] Checking for Marp CLI...${RESET}\n" - @if ! command -v marp >/dev/null 2>&1; then \ - if command -v npm >/dev/null 2>&1; then \ - printf "${YELLOW}[WARN] Marp CLI not found. Installing with npm...${RESET}\n"; \ - npm install -g @marp-team/marp-cli || { \ - printf "${RED}[ERROR] Failed to install Marp CLI. Please install manually:${RESET}\n"; \ - printf "${BLUE} npm install -g @marp-team/marp-cli${RESET}\n"; \ - exit 1; \ - }; \ - else \ - printf "${RED}[ERROR] npm not found. Please install Node.js and npm first.${RESET}\n"; \ - printf "${BLUE} See: https://nodejs.org/${RESET}\n"; \ - printf "${BLUE} Then run: npm install -g @marp-team/marp-cli${RESET}\n"; \ - exit 1; \ - fi; \ - fi - @printf "${BLUE}[INFO] Starting Marp server...${RESET}\n" - @printf "${GREEN}[INFO] Press Ctrl+C to stop the server${RESET}\n" - @marp -s . diff --git a/.rhiza/make.d/quality.mk b/.rhiza/make.d/quality.mk deleted file mode 100644 index 6271309..0000000 --- a/.rhiza/make.d/quality.mk +++ /dev/null @@ -1,24 +0,0 @@ -## .rhiza/make.d/quality.mk - Quality and Formatting -# This file provides targets for code quality checks, linting, and formatting. - -# Declare phony targets (they don't produce files) -.PHONY: all deptry fmt - -##@ Quality and Formatting -all: fmt deptry test docs-coverage security 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 diff --git a/.rhiza/make.d/test.mk b/.rhiza/make.d/test.mk deleted file mode 100644 index 945af34..0000000 --- a/.rhiza/make.d/test.mk +++ /dev/null @@ -1,115 +0,0 @@ -## Makefile.tests - Testing and benchmarking targets -# This file is included by the main Makefile. -# It provides targets for running the test suite with coverage and -# executing performance benchmarks. - -# Declare phony targets (they don't produce files) -.PHONY: test benchmark typecheck security docs-coverage hypothesis-test - -# Default directory for tests -TESTS_FOLDER := tests - -# Minimum coverage percent for tests to pass -# (Can be overridden in local.mk or via environment variable) -COVERAGE_FAIL_UNDER ?= 90 - -##@ Development and Testing - -# The 'test' target runs the complete test suite. -# 1. Cleans up any previous test results in _tests/. -# 2. Creates directories for HTML coverage and test reports. -# 3. Invokes pytest via the local virtual environment. -# 4. Generates terminal output, HTML coverage, JSON coverage, and HTML test reports. -test: install ## run all tests - @rm -rf _tests; - - if [ -z "$$(find ${TESTS_FOLDER} -name 'test_*.py' -o -name '*_test.py' 2>/dev/null)" ]; then \ - printf "${YELLOW}[WARN] No test files found in ${TESTS_FOLDER}, skipping tests.${RESET}\n"; \ - exit 0; \ - fi; \ - mkdir -p _tests/html-coverage _tests/html-report; \ - if [ -d ${SOURCE_FOLDER} ]; then \ - ${UV_BIN} run pytest \ - --ignore=${TESTS_FOLDER}/benchmarks \ - --cov=${SOURCE_FOLDER} \ - --cov-report=term \ - --cov-report=html:_tests/html-coverage \ - --cov-fail-under=$(COVERAGE_FAIL_UNDER) \ - --cov-report=json:_tests/coverage.json \ - --html=_tests/html-report/report.html; \ - else \ - printf "${YELLOW}[WARN] Source folder ${SOURCE_FOLDER} not found, running tests without coverage${RESET}\n"; \ - ${UV_BIN} run pytest \ - --ignore=${TESTS_FOLDER}/benchmarks \ - --html=_tests/html-report/report.html; \ - fi - -# The 'typecheck' target runs static type analysis using ty. -# 1. Checks if the source directory exists. -# 2. Runs ty on the source folder. -typecheck: install ## run ty type checking - @if [ -d ${SOURCE_FOLDER} ]; then \ - printf "${BLUE}[INFO] Running ty type checking...${RESET}\n"; \ - ${UV_BIN} run ty check ${SOURCE_FOLDER}; \ - else \ - printf "${YELLOW}[WARN] Source folder ${SOURCE_FOLDER} not found, skipping typecheck${RESET}\n"; \ - fi - -# The 'security' target performs security vulnerability scans. -# 1. Runs pip-audit to check for known vulnerabilities in dependencies. -# 2. Runs bandit to find common security issues in the source code. -security: install ## run security scans (pip-audit and bandit) - @printf "${BLUE}[INFO] Running pip-audit for dependency vulnerabilities...${RESET}\n" - @${UVX_BIN} pip-audit - @printf "${BLUE}[INFO] Running bandit security scan...${RESET}\n" - @${UVX_BIN} bandit -r ${SOURCE_FOLDER} -ll -q -c pyproject.toml - -# The 'benchmark' target runs performance benchmarks using pytest-benchmark. -# 1. Installs benchmarking dependencies (pytest-benchmark, pygal). -# 2. Executes benchmarks found in the benchmarks/ subfolder. -# 3. Generates histograms and JSON results. -# 4. Runs a post-analysis script to process the results. -benchmark: install ## run performance benchmarks - @if [ -d "${TESTS_FOLDER}/benchmarks" ]; then \ - printf "${BLUE}[INFO] Running performance benchmarks...${RESET}\n"; \ - ${UV_BIN} pip install pytest-benchmark==5.2.3 pygal==3.1.0; \ - mkdir -p _tests/benchmarks; \ - ${UV_BIN} run pytest "${TESTS_FOLDER}/benchmarks/" \ - --benchmark-only \ - --benchmark-histogram=_tests/benchmarks/histogram \ - --benchmark-json=_tests/benchmarks/results.json; \ - ${UVX_BIN} "rhiza-tools>=0.2.3" analyze-benchmarks --benchmarks-json _tests/benchmarks/results.json --output-html _tests/benchmarks/report.html; \ - else \ - printf "${YELLOW}[WARN] Benchmarks folder not found, skipping benchmarks${RESET}\n"; \ - fi - -# The 'docs-coverage' target checks documentation coverage using interrogate. -# 1. Checks if SOURCE_FOLDER exists. -# 2. Runs interrogate on the source folder with verbose output. -docs-coverage: install ## check documentation coverage with interrogate - @if [ -d "${SOURCE_FOLDER}" ]; then \ - printf "${BLUE}[INFO] Checking documentation coverage in ${SOURCE_FOLDER}...${RESET}\n"; \ - ${UV_BIN} run interrogate -vv ${SOURCE_FOLDER}; \ - else \ - printf "${YELLOW}[WARN] Source folder ${SOURCE_FOLDER} not found, skipping docs-coverage${RESET}\n"; \ - fi - -# The 'hypothesis-test' target runs property-based tests using Hypothesis. -# 1. Checks if hypothesis tests exist in the tests directory. -# 2. Runs pytest with hypothesis-specific settings and statistics. -# 3. Generates detailed hypothesis examples and statistics. -hypothesis-test: install ## run property-based tests with Hypothesis - @if [ -z "$$(find ${TESTS_FOLDER} -name 'test_*.py' -o -name '*_test.py' 2>/dev/null)" ]; then \ - printf "${YELLOW}[WARN] No test files found in ${TESTS_FOLDER}, skipping hypothesis tests.${RESET}\n"; \ - exit 0; \ - fi; \ - printf "${BLUE}[INFO] Running Hypothesis property-based tests...${RESET}\n"; \ - mkdir -p _tests/hypothesis; \ - ${UV_BIN} run pytest \ - --ignore=${TESTS_FOLDER}/benchmarks \ - -v \ - --hypothesis-show-statistics \ - --hypothesis-seed=0 \ - -m "hypothesis or property" \ - --tb=short \ - --html=_tests/hypothesis/report.html \ No newline at end of file diff --git a/.rhiza/make.d/tutorial.mk b/.rhiza/make.d/tutorial.mk deleted file mode 100644 index 73d5d93..0000000 --- a/.rhiza/make.d/tutorial.mk +++ /dev/null @@ -1,101 +0,0 @@ -## .rhiza/make.d/tutorial.mk - Interactive Tutorial - -.PHONY: tutorial - -##@ Learning and Onboarding -tutorial: ## interactive tutorial with guided walkthrough of key features - @printf "${BLUE}" - @printf "╔════════════════════════════════════════════════════════════════════════════╗\n" - @printf "║ Welcome to the Rhiza Interactive Tutorial! ║\n" - @printf "║ ║\n" - @printf "║ This tutorial will guide you through the key features of Rhiza. ║\n" - @printf "║ Follow along and try the commands as we go! ║\n" - @printf "╚════════════════════════════════════════════════════════════════════════════╝\n" - @printf "${RESET}\n" - @sleep 2 - @printf "${BOLD}📚 Lesson 1: Understanding Rhiza${RESET}\n" - @printf "Rhiza is a living template system for Python projects.\n" - @printf "Unlike traditional templates, Rhiza keeps your project synchronized\n" - @printf "with best practices through continuous updates.\n\n" - @printf "${YELLOW}Press Enter to continue...${RESET}\n" && read -r - @printf "\n${BOLD}🔧 Lesson 2: Essential Commands${RESET}\n" - @printf "Let's explore the most important make targets:\n\n" - @printf "${GREEN} make help${RESET} - Show all available commands\n" - @printf "${GREEN} make install${RESET} - Install dependencies and set up environment\n" - @printf "${GREEN} make test${RESET} - Run all tests with coverage\n" - @printf "${GREEN} make fmt${RESET} - Format and lint your code\n\n" - @printf "${YELLOW}Try running '${GREEN}make help${YELLOW}' now to see all commands.${RESET}\n" - @printf "${YELLOW}Press Enter when done...${RESET}\n" && read -r - @printf "\n${BOLD}🏗️ Lesson 3: Project Structure${RESET}\n" - @printf "Key directories in a Rhiza project:\n\n" - @printf "${GREEN}.rhiza/${RESET} - Template-managed files (do not edit directly)\n" - @printf "${GREEN}.rhiza/make.d/${RESET} - Modular Makefile components (auto-loaded)\n" - @printf "${GREEN}Makefile${RESET} - Your project-specific customizations\n" - @printf "${GREEN}local.mk${RESET} - Developer-local overrides (not committed)\n" - @printf "${GREEN}pyproject.toml${RESET} - Python project configuration\n\n" - @printf "${YELLOW}Press Enter to continue...${RESET}\n" && read -r - @printf "\n${BOLD}🔄 Lesson 4: Template Synchronization${RESET}\n" - @printf "Rhiza keeps your project up-to-date with template changes:\n\n" - @printf "${GREEN} make sync${RESET} - Sync with upstream template\n" - @printf "${GREEN} make summarise-sync${RESET} - Preview sync changes without applying\n" - @printf "${GREEN} make validate${RESET} - Validate project structure\n\n" - @printf "Configuration is in ${GREEN}.rhiza/template.yml${RESET}\n\n" - @printf "${YELLOW}Press Enter to continue...${RESET}\n" && read -r - @printf "\n${BOLD}🎨 Lesson 5: Customization${RESET}\n" - @printf "You can extend Rhiza without breaking template sync:\n\n" - @printf "1. ${BOLD}Add custom targets${RESET} in your root Makefile:\n" - @printf " ${GREEN}my-task: ## Description\n" - @printf " @echo 'Custom task'${RESET}\n\n" - @printf "2. ${BOLD}Use hooks${RESET} to inject logic into standard workflows:\n" - @printf " ${GREEN}post-install::\n" - @printf " @echo 'Additional setup'${RESET}\n\n" - @printf "3. ${BOLD}Create local overrides${RESET} in local.mk (not committed)\n\n" - @printf "${YELLOW}See docs/CUSTOMIZATION.md and docs/EXTENDING_RHIZA.md for details.${RESET}\n" - @printf "${YELLOW}Press Enter to continue...${RESET}\n" && read -r - @printf "\n${BOLD}🧪 Lesson 6: Development Workflow${RESET}\n" - @printf "Typical development cycle:\n\n" - @printf "1. ${GREEN}make install${RESET} - Set up your environment (first time)\n" - @printf "2. ${GREEN}# Write code...${RESET}\n" - @printf "3. ${GREEN}make test${RESET} - Run tests to verify changes\n" - @printf "4. ${GREEN}make fmt${RESET} - Format code before committing\n" - @printf "5. ${GREEN}git commit${RESET} - Commit your changes\n\n" - @printf "${YELLOW}Press Enter to continue...${RESET}\n" && read -r - @printf "\n${BOLD}📦 Lesson 7: Dependency Management${RESET}\n" - @printf "Rhiza uses uv for fast, reliable dependency management:\n\n" - @printf "${GREEN} uv add package-name${RESET} - Add a new dependency\n" - @printf "${GREEN} uv remove package-name${RESET} - Remove a dependency\n" - @printf "${GREEN} uv sync${RESET} - Sync dependencies with lock file\n" - @printf "${GREEN} uv run python${RESET} - Run Python in virtual environment\n" - @printf "${GREEN} uv run pytest${RESET} - Run pytest\n\n" - @printf "${YELLOW}Never call .venv/bin/python directly - always use 'uv run'!${RESET}\n" - @printf "${YELLOW}Press Enter to continue...${RESET}\n" && read -r - @printf "\n${BOLD}🚀 Lesson 8: Releasing${RESET}\n" - @printf "Streamlined version management and releases:\n\n" - @printf "${GREEN} make publish${RESET} - Bump version, create tag, and push (all-in-one)\n" - @printf "${GREEN} make bump${RESET} - Bump version (prompts for major/minor/patch)\n" - @printf "${GREEN} make release${RESET} - Create and push release tag\n" - @printf "${GREEN} make release-status${RESET} - Show release workflow status\n\n" - @printf "Version is in ${GREEN}pyproject.toml${RESET} and tags are prefixed with 'v'\n\n" - @printf "${YELLOW}Press Enter to continue...${RESET}\n" && read -r - @printf "\n${BOLD}📚 Lesson 9: Documentation${RESET}\n" - @printf "Generate and view documentation:\n\n" - @printf "${GREEN} make docs${RESET} - Generate API documentation with pdoc\n" - @printf "${GREEN} make book${RESET} - Build companion book\n" - @printf "${GREEN} make mkdocs-serve${RESET} - Serve MkDocs site with live reload\n\n" - @printf "${YELLOW}Press Enter to continue...${RESET}\n" && read -r - @printf "\n${BOLD}🤖 Lesson 10: AI-Powered Workflows${RESET}\n" - @printf "Rhiza includes AI-assisted development tools:\n\n" - @printf "${GREEN} make copilot${RESET} - Open GitHub Copilot CLI\n" - @printf "${GREEN} make claude${RESET} - Open Claude Code interactive prompt\n" - @printf "${GREEN} make analyse-repo${RESET} - AI analysis of repository structure\n" - @printf "${GREEN} make summarise-changes${RESET} - Summarize changes since last release\n\n" - @printf "${YELLOW}Press Enter to continue...${RESET}\n" && read -r - @printf "\n${BOLD}✨ Tutorial Complete!${RESET}\n\n" - @printf "You've learned the essentials of working with Rhiza.\n\n" - @printf "${BOLD}Next steps:${RESET}\n" - @printf " • Read ${GREEN}docs/TOOLS_REFERENCE.md${RESET} for command quick reference\n" - @printf " • Read ${GREEN}docs/EXTENDING_RHIZA.md${RESET} for customization patterns\n" - @printf " • Read ${GREEN}docs/CUSTOMIZATION.md${RESET} for advanced topics\n" - @printf " • Try ${GREEN}make help${RESET} to see all available commands\n" - @printf " • Check ${GREEN}README.md${RESET} for project-specific information\n\n" - @printf "${BLUE}Happy coding with Rhiza! 🌱${RESET}\n\n" diff --git a/.rhiza/requirements/README.md b/.rhiza/requirements/README.md deleted file mode 100644 index 4feff9a..0000000 --- a/.rhiza/requirements/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Requirements Folder - -This folder contains the development dependencies for the Rhiza project, organized by purpose. - -## Files - -- **tests.txt** - Testing dependencies (pytest, pytest-cov, pytest-html) -- **marimo.txt** - Marimo notebook dependencies -- **docs.txt** - Documentation generation dependencies (pdoc) -- **tools.txt** - Development tools (pre-commit, python-dotenv) - -## Usage - -These requirements files are automatically installed by the `make install` command. - -To install specific requirement files manually: - -```bash -uv pip install -r .rhiza/requirements/tests.txt -uv pip install -r .rhiza/requirements/marimo.txt -uv pip install -r .rhiza/requirements/docs.txt -uv pip install -r .rhiza/requirements/tools.txt -``` - -## CI/CD - -GitHub Actions workflows automatically install these requirements as needed. diff --git a/.rhiza/requirements/docs.txt b/.rhiza/requirements/docs.txt deleted file mode 100644 index 7d12b67..0000000 --- a/.rhiza/requirements/docs.txt +++ /dev/null @@ -1,3 +0,0 @@ -# Documentation dependencies for rhiza -pdoc>=16.0.0 -interrogate>=1.7.0 diff --git a/.rhiza/requirements/marimo.txt b/.rhiza/requirements/marimo.txt deleted file mode 100644 index 032c872..0000000 --- a/.rhiza/requirements/marimo.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Marimo dependencies for rhiza -marimo>=0.18.0 diff --git a/.rhiza/requirements/tests.txt b/.rhiza/requirements/tests.txt deleted file mode 100644 index b475152..0000000 --- a/.rhiza/requirements/tests.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Test dependencies for rhiza -pytest>=8.0 -python-dotenv>=1.0 -pytest-cov>=6.0 -pytest-html>=4.0 -pytest-mock>=3.0 -PyYAML>=6.0 -defusedxml>=0.7.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 deleted file mode 100644 index ed3e3ca..0000000 --- a/.rhiza/requirements/tools.txt +++ /dev/null @@ -1,7 +0,0 @@ -# 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 -ty==0.0.17 diff --git a/.rhiza/tests/README.md b/.rhiza/tests/README.md deleted file mode 100644 index 697bdad..0000000 --- a/.rhiza/tests/README.md +++ /dev/null @@ -1,126 +0,0 @@ -# Rhiza Test Suite - -This directory contains the comprehensive test suite for the Rhiza project. - -## Test Organization - -Tests are organized into purpose-driven subdirectories: - -### `structure/` -Static assertions about file and directory presence. These tests verify that the repository contains the expected files, directories, and configuration structure without executing any subprocesses. - -- `test_project_layout.py` — Validates root-level files and directories -- `test_requirements.py` — Validates `.rhiza/requirements/` structure - -### `api/` -Makefile target validation via dry-runs. These tests verify that Makefile targets are properly defined and would execute the expected commands. - -- `test_makefile_targets.py` — Core Makefile targets (install, test, fmt, etc.) -- `test_makefile_api.py` — Makefile API (delegation, extension, hooks, overrides) -- `test_github_targets.py` — GitHub-specific Makefile targets - -### `integration/` -Tests requiring sandboxed git repositories or subprocess execution. These tests verify end-to-end workflows. - -- `test_release.py` — Release script functionality -- `test_book_targets.py` — Documentation book build targets -- `test_marimushka.py` — Marimushka target execution -- `test_notebook_execution.py` — Marimo notebook execution validation - -### `sync/` -Template sync, workflows, versioning, and content validation tests. These tests ensure that template synchronization and content validation work correctly. - -- `test_rhiza_version.py` — Version reading and workflow validation -- `test_readme_validation.py` — README code block execution and validation -- `test_docstrings.py` — Doctest validation across source modules - -### `utils/` -Tests for utility code and test infrastructure. These tests validate the testing framework itself and utility scripts. - -- `test_git_repo_fixture.py` — Validates the `git_repo` fixture - -### `deps/` -Dependency validation tests. These tests ensure that project dependencies are correctly specified and healthy. - -- `test_dependency_health.py` — Validates pyproject.toml and requirements files - -## Running Tests - -### Run all tests -```bash -uv run pytest .rhiza/tests/ -# or -make test -``` - -### Run tests from a specific category -```bash -uv run pytest .rhiza/tests/structure/ -uv run pytest .rhiza/tests/api/ -uv run pytest .rhiza/tests/integration/ -uv run pytest .rhiza/tests/sync/ -uv run pytest .rhiza/tests/utils/ -uv run pytest .rhiza/tests/deps/ -``` - -### Run a specific test file -```bash -uv run pytest .rhiza/tests/structure/test_project_layout.py -``` - -### Run with verbose output -```bash -uv run pytest .rhiza/tests/ -v -``` - -### Run with coverage -```bash -uv run pytest .rhiza/tests/ --cov -``` - -## Fixtures - -### Root-level fixtures (`conftest.py`) -- `root` — Repository root path (session-scoped) -- `logger` — Configured logger instance (session-scoped) -- `git_repo` — Sandboxed git repository (function-scoped) - -### Category-specific fixtures -- `api/conftest.py` — `setup_tmp_makefile`, `run_make`, `setup_rhiza_git_repo` -- `sync/conftest.py` — `setup_sync_env` - -## Writing Tests - -### Conventions -- Use descriptive test names that explain what is being tested -- Group related tests in classes when appropriate -- Use appropriate fixtures for setup/teardown -- Add docstrings to test modules and complex test functions -- Use `pytest.mark.skip` for tests that depend on optional features - -### Import Patterns -```python -# Import shared helpers from test_utils -from test_utils import strip_ansi, run_make, setup_rhiza_git_repo - -# Import from local category conftest (for fixtures and category-specific helpers) -from api.conftest import SPLIT_MAKEFILES, setup_tmp_makefile - -# Note: Fixtures defined in conftest.py are automatically available in tests -# and don't need to be explicitly imported -``` - -## Test Coverage - -The test suite aims for high coverage across: -- Configuration validation (structure, dependencies) -- Makefile target correctness (api) -- End-to-end workflows (integration) -- Template synchronization (sync) -- Utility code (utils) - -## Notes - -- Benchmarks are located in `tests/benchmarks/` and run via `make benchmark` -- Integration tests use sandboxed git repositories to avoid affecting the working tree -- All Makefile tests use dry-run mode (`make -n`) to avoid side effects diff --git a/.rhiza/tests/api/conftest.py b/.rhiza/tests/api/conftest.py deleted file mode 100644 index 9cb5aef..0000000 --- a/.rhiza/tests/api/conftest.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Shared fixtures for Makefile API tests. - -This conftest provides: -- setup_tmp_makefile: Copies Makefile and split files to temp dir for isolated testing -- run_make: Helper to execute make commands with dry-run support (imported from test_utils) -- setup_rhiza_git_repo: Initialize a git repo configured as rhiza origin (imported from test_utils) -- SPLIT_MAKEFILES: List of split Makefile paths -""" - -from __future__ import annotations - -import os -import shutil -import sys -from pathlib import Path - -import pytest - -tests_root = Path(__file__).resolve().parents[1] -if str(tests_root) not in sys.path: - sys.path.insert(0, str(tests_root)) - -from test_utils import run_make, setup_rhiza_git_repo, strip_ansi # noqa: E402, F401 - -# Split Makefile paths that are included in the main Makefile -# These are now located in .rhiza/make.d/ directory -SPLIT_MAKEFILES = [ - ".rhiza/rhiza.mk", - ".rhiza/make.d/bootstrap.mk", - ".rhiza/make.d/quality.mk", - ".rhiza/make.d/releasing.mk", - ".rhiza/make.d/test.mk", - ".rhiza/make.d/book.mk", - ".rhiza/make.d/marimo.mk", - ".rhiza/make.d/presentation.mk", - ".rhiza/make.d/github.mk", - ".rhiza/make.d/agentic.mk", - ".rhiza/make.d/docker.mk", - ".rhiza/make.d/docs.mk", -] - - -@pytest.fixture(autouse=True) -def setup_tmp_makefile(logger, root, tmp_path: Path): - """Copy the Makefile and split Makefiles into a temp directory and chdir there. - - We rely on `make -n` so that no real commands are executed. - This fixture consolidates setup for both basic Makefile tests and GitHub targets. - """ - logger.debug("Setting up temporary Makefile test dir: %s", tmp_path) - - # Copy the main Makefile into the temporary working directory - shutil.copy(root / "Makefile", tmp_path / "Makefile") - - # Copy core Rhiza Makefiles - (tmp_path / ".rhiza").mkdir(exist_ok=True) - shutil.copy(root / ".rhiza" / "rhiza.mk", tmp_path / ".rhiza" / "rhiza.mk") - - # Copy .python-version file for PYTHON_VERSION variable - if (root / ".python-version").exists(): - shutil.copy(root / ".python-version", tmp_path / ".python-version") - - # Copy .rhiza/.env if it exists (needed for GitHub targets and other configuration) - if (root / ".rhiza" / ".env").exists(): - shutil.copy(root / ".rhiza" / ".env", tmp_path / ".rhiza" / ".env") - else: - # Create a minimal, deterministic .rhiza/.env for tests so they don't - # depend on the developer's local configuration which may vary. - env_content = "SCRIPTS_FOLDER=.rhiza/scripts\nCUSTOM_SCRIPTS_FOLDER=.rhiza/customisations/scripts\n" - (tmp_path / ".rhiza" / ".env").write_text(env_content) - - logger.debug("Copied Makefile from %s to %s", root / "Makefile", tmp_path / "Makefile") - - # Copy split Makefiles if they exist (maintaining directory structure) - for split_file in SPLIT_MAKEFILES: - source_path = root / split_file - if source_path.exists(): - dest_path = tmp_path / split_file - dest_path.parent.mkdir(parents=True, exist_ok=True) - shutil.copy(source_path, dest_path) - logger.debug("Copied %s to %s", source_path, dest_path) - - # Move into tmp directory for isolation - old_cwd = Path.cwd() - os.chdir(tmp_path) - logger.debug("Changed working directory to %s", tmp_path) - try: - yield - finally: - os.chdir(old_cwd) - logger.debug("Restored working directory to %s", old_cwd) diff --git a/.rhiza/tests/api/test_github_targets.py b/.rhiza/tests/api/test_github_targets.py deleted file mode 100644 index c3b1941..0000000 --- a/.rhiza/tests/api/test_github_targets.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Tests for the GitHub Makefile targets using safe dry-runs. - -These tests validate that the .github/github.mk targets are correctly exposed -and emit the expected commands without actually executing them. -""" - -from __future__ import annotations - -# Import run_make from local conftest (setup_tmp_makefile is autouse) -from api.conftest import run_make - - -def test_gh_targets_exist(logger): - """Verify that GitHub targets are listed in help.""" - result = run_make(logger, ["help"], dry_run=False) - output = result.stdout - - expected_targets = ["gh-install", "view-prs", "view-issues", "failed-workflows", "whoami"] - - for target in expected_targets: - assert target in output, f"Target {target} not found in help output" - - -def test_gh_install_dry_run(logger): - """Verify gh-install target dry-run.""" - result = run_make(logger, ["gh-install"]) - # In dry-run, we expect to see the shell commands that would be executed. - # Since the recipe uses @if, make -n might verify the syntax or show the command if not silenced. - # However, with -s (silent), make -n might not show much for @ commands unless they are echoed. - # But we mainly want to ensure it runs without error. - assert result.returncode == 0 - - -def test_view_prs_dry_run(logger): - """Verify view-prs target dry-run.""" - result = run_make(logger, ["view-prs"]) - assert result.returncode == 0 - - -def test_view_issues_dry_run(logger): - """Verify view-issues target dry-run.""" - result = run_make(logger, ["view-issues"]) - assert result.returncode == 0 - - -def test_failed_workflows_dry_run(logger): - """Verify failed-workflows target dry-run.""" - result = run_make(logger, ["failed-workflows"]) - assert result.returncode == 0 - - -def test_whoami_dry_run(logger): - """Verify whoami target dry-run.""" - result = run_make(logger, ["whoami"]) - assert result.returncode == 0 diff --git a/.rhiza/tests/api/test_makefile_api.py b/.rhiza/tests/api/test_makefile_api.py deleted file mode 100644 index 8096006..0000000 --- a/.rhiza/tests/api/test_makefile_api.py +++ /dev/null @@ -1,369 +0,0 @@ -"""Tests for the new Makefile API structure (Wrapper + Makefile.rhiza).""" - -import os -import shutil -import subprocess # nosec -from pathlib import Path - -import pytest - -# Get absolute paths for executables to avoid S607 warnings from CodeFactor/Bandit -GIT = shutil.which("git") or "/usr/bin/git" - -# Files required for the API test environment -REQUIRED_FILES = [ - "Makefile", - "pyproject.toml", - "README.md", # is needed to do uv sync, etc. -] - -# Folders to copy recursively -REQUIRED_FOLDERS = [ - ".rhiza", -] - -OPTIONAL_FOLDERS = [ - "tests", # for tests/tests.mk - "docker", # for docker/docker.mk, if referenced - "book", - "presentation", -] - - -@pytest.fixture -def setup_api_env(logger, root, tmp_path: Path): - """Set up the Makefile API test environment in a temp folder.""" - logger.debug("Setting up Makefile API test env in: %s", tmp_path) - - # Copy files - for filename in REQUIRED_FILES: - src = root / filename - if src.exists(): - shutil.copy(src, tmp_path / filename) - else: - pytest.fail(f"Required file {filename} not found in root") - - # Copy required directories - for folder in REQUIRED_FOLDERS: - src = root / folder - if src.exists(): - dest = tmp_path / folder - if dest.exists(): - shutil.rmtree(dest) - shutil.copytree(src, dest) - else: - pytest.fail(f"Required folder {folder} not found in root") - - # Copy optional directories - for folder in OPTIONAL_FOLDERS: - src = root / folder - if src.exists(): - dest = tmp_path / folder - if dest.exists(): - shutil.rmtree(dest) - shutil.copytree(src, dest) - - # Create .rhiza/make.d and ensure no local.mk exists initially - (tmp_path / ".rhiza" / "make.d").mkdir(parents=True, exist_ok=True) - if (tmp_path / "local.mk").exists(): - (tmp_path / "local.mk").unlink() - - # Initialize git repo for rhiza tools (required for sync/validate) - subprocess.run([GIT, "init"], cwd=tmp_path, check=True, capture_output=True) # nosec - # Configure git user for commits if needed (some rhiza checks might need commits) - subprocess.run([GIT, "config", "user.email", "you@example.com"], cwd=tmp_path, check=True, capture_output=True) # nosec - subprocess.run([GIT, "config", "user.name", "Rhiza Test"], cwd=tmp_path, check=True, capture_output=True) # nosec - # Add origin remote to simulate being in the rhiza repo (triggers the skip logic in rhiza.mk) - subprocess.run( - [GIT, "remote", "add", "origin", "https://github.com/jebel-quant/rhiza.git"], - cwd=tmp_path, - check=True, - capture_output=True, - ) # nosec - - # Move to tmp dir - old_cwd = Path.cwd() - os.chdir(tmp_path) - try: - yield tmp_path - finally: - os.chdir(old_cwd) - - -# Import run_make from local conftest -from api.conftest import run_make # noqa: E402 - - -def test_api_delegation(logger, setup_api_env): - """Test that 'make help' works and delegates to .rhiza/rhiza.mk.""" - result = run_make(logger, ["help"], dry_run=False) - assert result.returncode == 0 - # "Rhiza Workflows" is a section in .rhiza/rhiza.mk - assert "Rhiza Workflows" in result.stdout - - # Core targets from .rhiza/make.d/ should be available - assert "test" in result.stdout or "install" in result.stdout - - -def test_minimal_setup_works(logger, setup_api_env): - """Test that make works even if optional folders (tests, docker, etc.) are missing.""" - # Remove optional folders - for folder in OPTIONAL_FOLDERS: - p = setup_api_env / folder - if p.exists(): - shutil.rmtree(p) - - # Also remove files that might be copied if they were in the root? - # Just mainly folders. - - # Run make help - result = run_make(logger, ["help"], dry_run=False) - assert result.returncode == 0 - - # Check that core rhiza targets exist - assert "Rhiza Workflows" in result.stdout - assert "sync" in result.stdout - - # Note: docker-build and other targets from .rhiza/make.d/ are always present - # but they gracefully skip if their respective folders/files don't exist. - # This is by design - targets are always available but handle missing resources. - - -def test_extension_mechanism(logger, setup_api_env): - """Test that custom targets can be added in the root Makefile.""" - # Add a custom target to the root Makefile (before include line) - makefile = setup_api_env / "Makefile" - original = makefile.read_text() - # Insert custom target before the include line - new_content = ( - """.PHONY: custom-target -custom-target: - @echo "Running custom target" - -""" - + original - ) - makefile.write_text(new_content) - - result = run_make(logger, ["custom-target"], dry_run=False) - assert result.returncode == 0 - assert "Running custom target" in result.stdout - - -def test_local_override(logger, setup_api_env): - """Test that local.mk is included and can match targets.""" - local_file = setup_api_env / "local.mk" - local_file.write_text(""" -.PHONY: local-target -local-target: - @echo "Running local target" -""") - - result = run_make(logger, ["local-target"], dry_run=False) - assert result.returncode == 0 - assert "Running local target" in result.stdout - - -def test_local_override_pre_hook(logger, setup_api_env): - """Test using local.mk to override a pre-hook.""" - local_file = setup_api_env / "local.mk" - # We override pre-sync to print a marker (using double-colon to match rhiza.mk) - local_file.write_text(""" -pre-sync:: - @echo "[[LOCAL_PRE_SYNC]]" -""") - - # Run sync in dry-run. - # Note: Makefile.rhiza defines pre-sync as empty rule (or with @:). - # Make warns if we redefine a target unless it's a double-colon rule or we are careful. - # But usually the last one loaded wins or they merge if double-colon. - # The current definition in Makefile.rhiza is `pre-sync: ; @echo ...` or similar. - # Wait, I defined it as `pre-sync: ; @:` (single colon). - # So redefining it in local.mk (which is included AFTER) might trigger a warning but should work. - - result = run_make(logger, ["sync"], dry_run=False) - # We might expect a warning about overriding commands for target `pre-sync` - # checking stdout/stderr for the marker - - assert "[[LOCAL_PRE_SYNC]]" in result.stdout - - -def test_hook_execution_order(logger, setup_api_env): - """Define hooks in root Makefile and verify execution order.""" - # Add hooks to root Makefile (before include line) - makefile = setup_api_env / "Makefile" - original = makefile.read_text() - new_content = ( - """pre-sync:: - @echo "STARTING_SYNC" - -post-sync:: - @echo "FINISHED_SYNC" - -""" - + original - ) - makefile.write_text(new_content) - - result = run_make(logger, ["sync"], dry_run=False) - assert result.returncode == 0 - output = result.stdout - - # Check that markers are present - assert "STARTING_SYNC" in output - assert "FINISHED_SYNC" in output - - # Check order: STARTING_SYNC comes before FINISHED_SYNC - start_index = output.find("STARTING_SYNC") - finish_index = output.find("FINISHED_SYNC") - assert start_index < finish_index - - -def test_override_core_target(logger, setup_api_env): - """Verify that the root Makefile can override a core target (with warning).""" - # Override 'fmt' which is defined in quality.mk - # Add override AFTER the include line so it takes precedence - makefile = setup_api_env / "Makefile" - original = makefile.read_text() - new_content = ( - original - + """ -fmt: - @echo "CUSTOM_FMT" -""" - ) - makefile.write_text(new_content) - - result = run_make(logger, ["fmt"], dry_run=False) - assert result.returncode == 0 - # It should run the custom one because it's defined after the include - assert "CUSTOM_FMT" in result.stdout - - # We expect a warning on stderr about overriding - assert "warning: overriding" in result.stderr.lower() - assert "fmt" in result.stderr.lower() - - -def test_global_variable_override(logger, setup_api_env): - """Test that global variables can be overridden in the root Makefile. - - This tests the pattern documented in CUSTOMIZATION.md: - Set variables before the include line to override defaults. - """ - # Add variable override to root Makefile (before include line) - makefile = setup_api_env / "Makefile" - original = makefile.read_text() - new_content = ( - """# Override default coverage threshold (defaults to 90) -COVERAGE_FAIL_UNDER := 42 -export COVERAGE_FAIL_UNDER - -""" - + original - ) - makefile.write_text(new_content) - - result = run_make(logger, ["print-COVERAGE_FAIL_UNDER"], dry_run=False) - assert result.returncode == 0 - assert "42" in result.stdout - - -def test_pre_install_hook(logger, setup_api_env): - """Test that pre-install hooks are executed before install. - - This tests the hook pattern documented in CUSTOMIZATION.md. - """ - makefile = setup_api_env / "Makefile" - original = makefile.read_text() - new_content = ( - """pre-install:: - @echo "[[PRE_INSTALL_HOOK]]" - -""" - + original - ) - makefile.write_text(new_content) - - # Run install in dry-run mode to avoid actual installation - result = run_make(logger, ["install"], dry_run=True) - assert result.returncode == 0 - # In dry-run mode, the echo command is printed (not executed) - assert "PRE_INSTALL_HOOK" in result.stdout - - -def test_post_install_hook(logger, setup_api_env): - """Test that post-install hooks are executed after install. - - This tests the hook pattern documented in CUSTOMIZATION.md. - """ - makefile = setup_api_env / "Makefile" - original = makefile.read_text() - new_content = ( - """post-install:: - @echo "[[POST_INSTALL_HOOK]]" - -""" - + original - ) - makefile.write_text(new_content) - - # Run install in dry-run mode - result = run_make(logger, ["install"], dry_run=True) - assert result.returncode == 0 - assert "POST_INSTALL_HOOK" in result.stdout - - -def test_multiple_hooks_accumulate(logger, setup_api_env): - """Test that multiple hook definitions accumulate rather than override. - - This is a key feature of double-colon rules: the root Makefile and - local.mk can both add to the same hook without conflicts. - """ - # Add hook in root Makefile - makefile = setup_api_env / "Makefile" - original = makefile.read_text() - new_content = ( - """pre-sync:: - @echo "[[HOOK_A]]" - -""" - + original - ) - makefile.write_text(new_content) - - # Add another hook in local.mk - (setup_api_env / "local.mk").write_text("""pre-sync:: - @echo "[[HOOK_B]]" -""") - - result = run_make(logger, ["sync"], dry_run=False) - assert result.returncode == 0 - # Both hooks should be present - assert "[[HOOK_A]]" in result.stdout - assert "[[HOOK_B]]" in result.stdout - - -def test_variable_override_before_include(logger, setup_api_env): - """Test that variables set before include take precedence. - - Variables defined in the root Makefile before the include line - should be available throughout the build. - """ - # Set a variable and use it in a target (before include) - makefile = setup_api_env / "Makefile" - original = makefile.read_text() - new_content = ( - """MY_CUSTOM_VAR := hello - -.PHONY: show-var -show-var: - @echo "MY_VAR=$(MY_CUSTOM_VAR)" - -""" - + original - ) - makefile.write_text(new_content) - - result = run_make(logger, ["show-var"], dry_run=False) - assert result.returncode == 0 - assert "MY_VAR=hello" in result.stdout diff --git a/.rhiza/tests/api/test_makefile_targets.py b/.rhiza/tests/api/test_makefile_targets.py deleted file mode 100644 index 302aea2..0000000 --- a/.rhiza/tests/api/test_makefile_targets.py +++ /dev/null @@ -1,291 +0,0 @@ -"""Tests for the Makefile targets and help output using safe dry-runs. - -This file and its associated tests flow down via a SYNC action from the jebel-quant/rhiza repository -(https://github.com/jebel-quant/rhiza). - -These tests validate that the Makefile exposes expected targets and emits -the correct commands without actually executing them, by invoking `make -n` -(dry-run). We also pass `-s` to reduce noise in CI logs. This approach keeps -tests fast, portable, and free of side effects like network or environment -changes. -""" - -from __future__ import annotations - -import os - -import pytest -from api.conftest import SPLIT_MAKEFILES, run_make, setup_rhiza_git_repo, strip_ansi - - -def assert_uvx_command_uses_version(output: str, tmp_path, command_fragment: str): - """Assert uvx command uses .python-version when present, else fallback checks.""" - python_version_file = tmp_path / ".python-version" - if python_version_file.exists(): - python_version = python_version_file.read_text().strip() - assert f"uvx -p {python_version} {command_fragment}" in output - else: - assert "uvx -p" in output - assert command_fragment in output - - -class TestMakefile: - """Smoke tests for Makefile help and common targets using make -n.""" - - def test_default_goal_is_help(self, logger): - """Default goal should render the help index with known targets.""" - proc = run_make(logger) - out = proc.stdout - assert "Usage:" in out - assert "Targets:" in out - # ensure a few known targets appear in the help index - for target in ["install", "fmt", "deptry", "test", "help"]: - assert target in out - - def test_help_target(self, logger): - """Explicit `make help` prints usage, targets, and section headers.""" - proc = run_make(logger, ["help"]) - out = proc.stdout - assert "Usage:" in out - assert "Targets:" in out - assert "Bootstrap" in out or "Meta" in out # section headers - - def test_fmt_target_dry_run(self, logger, tmp_path): - """Fmt target should invoke pre-commit via uvx with Python version in dry-run output.""" - # Create clean environment without PYTHON_VERSION so Makefile reads from .python-version - env = os.environ.copy() - env.pop("PYTHON_VERSION", None) - - proc = run_make(logger, ["fmt"], env=env) - out = proc.stdout - assert_uvx_command_uses_version(out, tmp_path, "pre-commit run --all-files") - - def test_deptry_target_dry_run(self, logger, tmp_path): - """Deptry target should invoke deptry via uvx with Python version in dry-run output.""" - # Create a mock SOURCE_FOLDER directory so the deptry command runs - source_folder = tmp_path / "src" - source_folder.mkdir(exist_ok=True) - - # Update .env to set SOURCE_FOLDER - env_file = tmp_path / ".rhiza" / ".env" - env_content = env_file.read_text() - env_content += "\nSOURCE_FOLDER=src\n" - env_file.write_text(env_content) - - # Create clean environment without PYTHON_VERSION so Makefile reads from .python-version - env = os.environ.copy() - env.pop("PYTHON_VERSION", None) - - proc = run_make(logger, ["deptry"], env=env) - - out = proc.stdout - assert_uvx_command_uses_version(out, tmp_path, "deptry src") - - def test_typecheck_target_dry_run(self, logger, tmp_path): - """Typecheck target should invoke ty via uv run in dry-run output.""" - # Create a mock SOURCE_FOLDER directory so the typecheck command runs - source_folder = tmp_path / "src" - source_folder.mkdir(exist_ok=True) - - # Update .env to set SOURCE_FOLDER - env_file = tmp_path / ".rhiza" / ".env" - env_content = env_file.read_text() - env_content += "\nSOURCE_FOLDER=src\n" - env_file.write_text(env_content) - - proc = run_make(logger, ["typecheck"]) - out = proc.stdout - # Check for uv run command - assert "uv run ty check src" in out - - def test_test_target_dry_run(self, logger): - """Test target should invoke pytest via uv with coverage and HTML outputs in dry-run output.""" - proc = run_make(logger, ["test"]) - out = proc.stdout - # Expect key steps - assert "mkdir -p _tests/html-coverage _tests/html-report" in out - # Check for uv command running pytest - assert "uv run pytest" in out - - def test_test_target_without_source_folder(self, logger, tmp_path): - """Test target should run without coverage when SOURCE_FOLDER doesn't exist.""" - # Update .env to set SOURCE_FOLDER to a non-existent directory - env_file = tmp_path / ".rhiza" / ".env" - env_content = env_file.read_text() - env_content += "\nSOURCE_FOLDER=nonexistent_src\n" - env_file.write_text(env_content) - - # Create tests folder - tests_folder = tmp_path / "tests" - tests_folder.mkdir(exist_ok=True) - - proc = run_make(logger, ["test"]) - out = proc.stdout - # Should see warning about missing source folder - assert "if [ -d nonexistent_src ]" in out - # Should still run pytest but without coverage flags - assert "uv run pytest" in out - assert "--html=_tests/html-report/report.html" in out - - def test_python_version_defaults_to_3_13_if_missing(self, logger, tmp_path): - """`PYTHON_VERSION` should default to `3.13` if .python-version is missing.""" - # Ensure .python-version does not exist - python_version_file = tmp_path / ".python-version" - if python_version_file.exists(): - python_version_file.unlink() - - # Create clean environment without PYTHON_VERSION - env = os.environ.copy() - env.pop("PYTHON_VERSION", None) - - proc = run_make(logger, ["print-PYTHON_VERSION"], dry_run=False, env=env) - out = strip_ansi(proc.stdout) - assert "Value of PYTHON_VERSION:\n3.13" in out - - def test_uv_no_modify_path_is_exported(self, logger): - """`UV_NO_MODIFY_PATH` should be set to `1` in the Makefile.""" - proc = run_make(logger, ["print-UV_NO_MODIFY_PATH"], dry_run=False) - out = strip_ansi(proc.stdout) - assert "Value of UV_NO_MODIFY_PATH:\n1" in out - - def test_script_folder_is_github_scripts(self, logger): - """`SCRIPTS_FOLDER` should point to `.rhiza/scripts`.""" - proc = run_make(logger, ["print-SCRIPTS_FOLDER"], dry_run=False) - out = strip_ansi(proc.stdout) - assert "Value of SCRIPTS_FOLDER:\n.rhiza/scripts" in out - - def test_that_target_coverage_is_configurable(self, logger): - """Test target should respond to COVERAGE_FAIL_UNDER variable.""" - # Default case: ensure the flag is present - proc = run_make(logger, ["test"]) - assert "--cov-fail-under=" in proc.stdout - - # Override case: ensure the flag takes the specific value - proc_override = run_make(logger, ["test", "COVERAGE_FAIL_UNDER=42"]) - assert "--cov-fail-under=42" in proc_override.stdout - - -class TestMakefileRootFixture: - """Tests for root fixture usage in Makefile tests.""" - - def test_makefile_exists_at_root(self, root): - """Makefile should exist at repository root.""" - makefile = root / "Makefile" - assert makefile.exists() - assert makefile.is_file() - - def test_makefile_contains_targets(self, root): - """Makefile should contain expected targets (including split files).""" - makefile = root / "Makefile" - content = makefile.read_text() - - # Read split Makefiles as well - for split_file in SPLIT_MAKEFILES: - split_path = root / split_file - if split_path.exists(): - content += "\n" + split_path.read_text() - - expected_targets = ["install", "fmt", "test", "deptry", "help"] - for target in expected_targets: - assert f"{target}:" in content or f".PHONY: {target}" in content - - def test_validate_target_skips_in_rhiza_repo(self, logger): - """Validate target should skip execution in rhiza repository.""" - setup_rhiza_git_repo() - - proc = run_make(logger, ["validate"], dry_run=False) - # out = strip_ansi(proc.stdout) - # assert "[INFO] Skipping validate in rhiza repository" in out - assert proc.returncode == 0 - - def test_sync_target_skips_in_rhiza_repo(self, logger): - """Sync target should skip execution in rhiza repository.""" - setup_rhiza_git_repo() - - proc = run_make(logger, ["sync"], dry_run=False) - # out = strip_ansi(proc.stdout) - # assert "[INFO] Skipping sync in rhiza repository" in out - assert proc.returncode == 0 - - -class TestMakeBump: - """Tests for the 'make bump' target.""" - - @pytest.fixture - def mock_bin(self, tmp_path): - """Create mock uv and uvx scripts in ./bin.""" - bin_dir = tmp_path / "bin" - bin_dir.mkdir(exist_ok=True) - - uv = bin_dir / "uv" - uv.write_text('#!/bin/sh\necho "[MOCK] uv $@"\n') - uv.chmod(0o755) - - # Mock uvx to simulate version bump if arguments match - uvx = bin_dir / "uvx" - uvx_script = """#!/usr/bin/env python3 -import sys -import re -from pathlib import Path - -args = sys.argv[1:] -print(f"[MOCK] uvx {' '.join(args)}") - -# Check if this is the bump command: "rhiza-tools>=0.3.3" bump -if "bump" in args: - # Simulate bumping version in pyproject.toml - pyproject = Path("pyproject.toml") - if pyproject.exists(): - content = pyproject.read_text() - # Simple regex replacement for version - # Assuming version = "0.1.0" -> "0.1.1" - new_content = re.sub(r'version = "([0-9.]+)"', lambda m: f'version = "{m.group(1)[:-1]}{int(m.group(1)[-1]) + 1}"', content) - pyproject.write_text(new_content) - print(f"[MOCK] Bumped version in {pyproject}") -""" # noqa: E501 - uvx.write_text(uvx_script) - uvx.chmod(0o755) - - return bin_dir - - def test_bump_execution(self, logger, mock_bin, tmp_path): - """Test 'make bump' execution with mocked tools and verify version change.""" - # Create dummy pyproject.toml with initial version - pyproject = tmp_path / "pyproject.toml" - pyproject.write_text('version = "0.1.0"\n[project]\nname = "test"\n') - - uv_bin = mock_bin / "uv" - uvx_bin = mock_bin / "uvx" - - # Run make bump with dry_run=False to actually execute the shell commands - result = run_make(logger, ["bump", f"UV_BIN={uv_bin}", f"UVX_BIN={uvx_bin}"], dry_run=False) - - # Verify that the mock tools were called - assert "[MOCK] uvx rhiza-tools>=0.3.3 bump" in result.stdout - assert "[MOCK] uv lock" in result.stdout - - # Verify that 'make install' was called (which calls uv sync) - assert "[MOCK] uv sync" in result.stdout - - # Verify that the version was actually bumped by our mock - new_content = pyproject.read_text() - assert 'version = "0.1.1"' in new_content - - def test_bump_no_pyproject(self, logger, mock_bin, tmp_path): - """Test 'make bump' execution without pyproject.toml.""" - # Ensure pyproject.toml does not exist - pyproject = tmp_path / "pyproject.toml" - if pyproject.exists(): - pyproject.unlink() - - uv_bin = mock_bin / "uv" - uvx_bin = mock_bin / "uvx" - - result = run_make(logger, ["bump", f"UV_BIN={uv_bin}", f"UVX_BIN={uvx_bin}"], dry_run=False) - - # Check for warning message - assert "No pyproject.toml found, skipping bump" in result.stdout - - # Ensure bump commands are NOT executed - assert "[MOCK] uvx" not in result.stdout - assert "[MOCK] uv lock" not in result.stdout diff --git a/.rhiza/tests/conftest.py b/.rhiza/tests/conftest.py deleted file mode 100644 index b2a1c02..0000000 --- a/.rhiza/tests/conftest.py +++ /dev/null @@ -1,210 +0,0 @@ -"""Pytest configuration and fixtures for setting up a mock git repository with versioning. - -This file and its associated tests flow down via a SYNC action from the jebel-quant/rhiza repository -(https://github.com/jebel-quant/rhiza). - -Provides test fixtures for testing git-based workflows and version management. -""" - -import logging -import os -import pathlib -import shutil -import subprocess # nosec B404 -import sys - -import pytest - -tests_root = pathlib.Path(__file__).resolve().parent -if str(tests_root) not in sys.path: - sys.path.insert(0, str(tests_root)) - -from test_utils import GIT # noqa: E402 - -MOCK_MAKE_SCRIPT = """#!/usr/bin/env python3 -import sys - -if len(sys.argv) > 1 and sys.argv[1] == "help": - print("Mock Makefile Help") - print("target: ## Description") -""" - -MOCK_UV_SCRIPT = """#!/usr/bin/env python3 -import sys -import re - -try: - from packaging.version import parse, InvalidVersion - HAS_PACKAGING = True -except ImportError: - HAS_PACKAGING = False - -def get_version(): - with open("pyproject.toml", "r") as f: - content = f.read() - match = re.search(r'version = "(.*?)"', content) - return match.group(1) if match else "0.0.0" - -def set_version(new_version): - with open("pyproject.toml", "r") as f: - content = f.read() - new_content = re.sub(r'version = ".*?"', f'version = "{new_version}"', content) - with open("pyproject.toml", "w") as f: - f.write(new_content) - -def bump_version(current, bump_type): - major, minor, patch = map(int, current.split('.')) - if bump_type == "major": - return f"{major + 1}.0.0" - elif bump_type == "minor": - return f"{major}.{minor + 1}.0" - elif bump_type == "patch": - return f"{major}.{minor}.{patch + 1}" - return current - -def main(): - args = sys.argv[1:] - if not args: - sys.exit(1) - - if args[0] != "version": - # It might be a uvx call if we use the same script, but let's keep them separate or handle it here. - # For now, let's assume this is only for uv version commands as per original design. - sys.exit(1) - - # uv version --short - if "--short" in args and "--bump" not in args: - print(get_version()) - return - - # uv version --bump --dry-run --short - if "--bump" in args and "--dry-run" in args and "--short" in args: - bump_idx = args.index("--bump") + 1 - bump_type = args[bump_idx] - current = get_version() - print(bump_version(current, bump_type)) - return - - # uv version --bump (actual update) - if "--bump" in args and "--dry-run" not in args: - bump_idx = args.index("--bump") + 1 - bump_type = args[bump_idx] - current = get_version() - new_ver = bump_version(current, bump_type) - set_version(new_ver) - return - - # uv version --dry-run - if len(args) >= 2 and not args[1].startswith("-") and "--dry-run" in args: - version = args[1] - if HAS_PACKAGING: - try: - parse(version) - except InvalidVersion: - sys.exit(1) - else: - # Simple validation: must start with a digit - if not re.match(r"^\\d", version): - sys.exit(1) - # Just exit 0 if valid - return - - # uv version (actual update) - if len(args) == 2 and not args[1].startswith("-"): - set_version(args[1]) - return - -if __name__ == "__main__": - main() -""" - - -@pytest.fixture(scope="session") -def root(): - """Return the repository root directory as a pathlib.Path. - - Used by tests to locate files and scripts relative to the project root. - """ - return pathlib.Path(__file__).parent.parent.parent - - -@pytest.fixture(scope="session") -def logger(): - """Provide a session-scoped logger for tests. - - Returns: - logging.Logger: Logger configured for the test session. - """ - return logging.getLogger(__name__) - - -@pytest.fixture -def git_repo(root, tmp_path, monkeypatch): - """Sets up a remote bare repo and a local clone with necessary files.""" - remote_dir = tmp_path / "remote.git" - local_dir = tmp_path / "local" - - # 1. Create bare remote - remote_dir.mkdir() - subprocess.run([GIT, "init", "--bare", str(remote_dir)], check=True) # nosec B603 - # Ensure the remote's default HEAD points to master for predictable behavior - subprocess.run([GIT, "symbolic-ref", "HEAD", "refs/heads/master"], cwd=remote_dir, check=True) # nosec B603 - - # 2. Clone to local - subprocess.run([GIT, "clone", str(remote_dir), str(local_dir)], check=True) # nosec B603 - - # Use monkeypatch to safely change cwd for the duration of the test - monkeypatch.chdir(local_dir) - - # Ensure local default branch is 'master' to match test expectations - subprocess.run([GIT, "checkout", "-b", "master"], check=True) # nosec B603 - - # Create pyproject.toml - with open("pyproject.toml", "w") as f: - f.write('[project]\nname = "test-project"\nversion = "0.1.0"\n') - - # Create dummy uv.lock - with open("uv.lock", "w") as f: - f.write("") - - # Create bin/uv mock - bin_dir = local_dir / "bin" - bin_dir.mkdir() - - uv_path = bin_dir / "uv" - with open(uv_path, "w") as f: - f.write(MOCK_UV_SCRIPT) - uv_path.chmod(0o755) - - make_path = bin_dir / "make" - with open(make_path, "w") as f: - f.write(MOCK_MAKE_SCRIPT) - make_path.chmod(0o755) - - # Ensure our bin comes first on PATH so 'uv' resolves to mock - monkeypatch.setenv("PATH", f"{bin_dir}:{os.environ.get('PATH', '')}") - - # Copy core Rhiza Makefiles - (local_dir / ".rhiza").mkdir(parents=True, exist_ok=True) - shutil.copy(root / ".rhiza" / "rhiza.mk", local_dir / ".rhiza" / "rhiza.mk") - shutil.copy(root / "Makefile", local_dir / "Makefile") - - # Copy .rhiza/make.d/ directory (contains split makefiles) - make_d_src = root / ".rhiza" / "make.d" - if make_d_src.is_dir(): - make_d_dst = local_dir / ".rhiza" / "make.d" - shutil.copytree(make_d_src, make_d_dst, dirs_exist_ok=True) - - book_src = root / "book" - book_dst = local_dir / "book" - if book_src.is_dir(): - shutil.copytree(book_src, book_dst, dirs_exist_ok=True) - - # Commit and push initial state - subprocess.run([GIT, "config", "user.email", "test@example.com"], check=True) # nosec B603 - subprocess.run([GIT, "config", "user.name", "Test User"], check=True) # nosec B603 - subprocess.run([GIT, "add", "."], check=True) # nosec B603 - subprocess.run([GIT, "commit", "-m", "Initial commit"], check=True) # nosec B603 - subprocess.run([GIT, "push", "origin", "master"], check=True) # nosec B603 - - return local_dir diff --git a/.rhiza/tests/deps/test_dependency_health.py b/.rhiza/tests/deps/test_dependency_health.py deleted file mode 100644 index e3de07d..0000000 --- a/.rhiza/tests/deps/test_dependency_health.py +++ /dev/null @@ -1,111 +0,0 @@ -"""Dependency health tests — validate requirements files and pyproject.toml content.""" - -import re -import tomllib - - -def test_pyproject_has_requires_python(root): - """Verify that pyproject.toml declares requires-python in [project].""" - pyproject_path = root / "pyproject.toml" - assert pyproject_path.exists(), "pyproject.toml not found" - - with pyproject_path.open("rb") as f: - pyproject = tomllib.load(f) - - assert "project" in pyproject, "[project] section missing from pyproject.toml" - assert "requires-python" in pyproject["project"], "requires-python missing from [project] section" - - requires_python = pyproject["project"]["requires-python"] - assert isinstance(requires_python, str), "requires-python must be a string" - assert requires_python.strip(), "requires-python cannot be empty" - - -def test_requirements_files_are_valid_pip_specifiers(root): - """Verify that all lines in requirements files are valid pip requirement specifiers.""" - requirements_dir = root / ".rhiza" / "requirements" - assert requirements_dir.exists(), ".rhiza/requirements directory not found" - - # Pattern for valid requirement specifier (simplified check) - # Matches: package, package>=1.0, package[extra], git+https://... - valid_specifier_pattern = re.compile( - r"^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?|git\+https?://)", - re.IGNORECASE, - ) - - for req_file in requirements_dir.glob("*.txt"): - if req_file.name == "README.md": - continue - - with req_file.open() as f: - for line_num, line in enumerate(f, start=1): - line = line.strip() - - # Skip empty lines and comments - if not line or line.startswith("#"): - continue - - # Basic validation: line should start with a valid package name or git URL - assert valid_specifier_pattern.match(line), ( - f"{req_file.name}:{line_num} - Invalid requirement specifier: {line}" - ) - - -def test_no_duplicate_packages_across_requirements(root): - """Verify that no package appears in multiple requirements files.""" - requirements_dir = root / ".rhiza" / "requirements" - assert requirements_dir.exists(), ".rhiza/requirements directory not found" - - # Known packages that intentionally appear in multiple files - # python-dotenv is used by both test infrastructure and development tools - allowed_duplicates = {"python-dotenv"} - - # Map of package name (lowercase) to list of files it appears in - package_locations = {} - - # Pattern to extract package name from requirement line - # Matches the package name before any version specifier, extra, or URL fragment - package_name_pattern = re.compile(r"^([a-zA-Z0-9][a-zA-Z0-9._-]*)", re.IGNORECASE) - - for req_file in requirements_dir.glob("*.txt"): - if req_file.name == "README.md": - continue - - with req_file.open() as f: - for line in f: - line = line.strip() - - # Skip empty lines and comments - if not line or line.startswith("#"): - continue - - # Extract package name - match = package_name_pattern.match(line) - if match: - package_name = match.group(1).lower() - - if package_name not in package_locations: - package_locations[package_name] = [] - - package_locations[package_name].append(req_file.name) - - # Find duplicates (excluding allowed ones) - duplicates = { - pkg: files for pkg, files in package_locations.items() if len(files) > 1 and pkg not in allowed_duplicates - } - - if duplicates: - duplicate_list = [f"{pkg} ({', '.join(files)})" for pkg, files in duplicates.items()] - msg = f"Packages found in multiple requirements files: {', '.join(duplicate_list)}" - raise AssertionError(msg) - - -def test_dotenv_in_test_requirements(root): - """Verify that python-dotenv is listed in tests.txt (test suite depends on it).""" - tests_req_path = root / ".rhiza" / "requirements" / "tests.txt" - assert tests_req_path.exists(), "tests.txt not found" - - with tests_req_path.open() as f: - content = f.read().lower() - - # Check for python-dotenv (case-insensitive) - assert "python-dotenv" in content, "python-dotenv not found in tests.txt (required by test suite)" diff --git a/.rhiza/tests/integration/test_book_targets.py b/.rhiza/tests/integration/test_book_targets.py deleted file mode 100644 index be875c0..0000000 --- a/.rhiza/tests/integration/test_book_targets.py +++ /dev/null @@ -1,150 +0,0 @@ -"""Tests for book-related Makefile targets and their resilience.""" - -import shutil -import subprocess # nosec - -import pytest - -MAKE = shutil.which("make") or "/usr/bin/make" - - -@pytest.fixture -def book_makefile(git_repo): - """Return the book.mk path or skip tests if missing.""" - makefile = git_repo / ".rhiza" / "make.d" / "book.mk" - if not makefile.exists(): - pytest.skip("book.mk not found, skipping test") - return makefile - - -def test_no_book_folder(git_repo, book_makefile): - """Test that make targets work gracefully when book folder is missing. - - Now that book-related targets are defined in .rhiza/make.d/, they are always - available but check internally for the existence of the book folder. - Using dry-run (-n) to test the target logic without actually executing. - """ - if (git_repo / "book").exists(): - shutil.rmtree(git_repo / "book") - assert not (git_repo / "book").exists() - - # Targets are now always defined via .rhiza/make.d/ - # Use dry-run to verify they exist and can be parsed - for target in ["book", "docs", "marimushka"]: - result = subprocess.run([MAKE, "-n", target], cwd=git_repo, capture_output=True, text=True) # nosec - # Target should exist (not "no rule to make target") - assert "no rule to make target" not in result.stderr.lower(), ( - f"Target {target} should be defined in .rhiza/make.d/" - ) - - -def test_book_folder_but_no_mk(git_repo, book_makefile): - """Test behavior when book folder exists but is empty. - - With the new architecture, targets are always defined in .rhiza/make.d/book.mk, - so they should exist regardless of the book folder contents. - """ - # ensure book folder exists but is empty - if (git_repo / "book").exists(): - shutil.rmtree(git_repo / "book") - # create an empty book folder - (git_repo / "book").mkdir() - - # assert the book folder exists - assert (git_repo / "book").exists() - # assert the git_repo / "book" folder is empty - assert not list((git_repo / "book").iterdir()) - - # Targets are now always defined via .rhiza/make.d/ - # Use dry-run to verify they exist and can be parsed - for target in ["book", "docs", "marimushka"]: - result = subprocess.run([MAKE, "-n", target], cwd=git_repo, capture_output=True, text=True) # nosec - # Target should exist (not "no rule to make target") - assert "no rule to make target" not in result.stderr.lower(), ( - f"Target {target} should be defined in .rhiza/make.d/" - ) - - -def test_book_folder(git_repo, book_makefile): - """Test that .rhiza/make.d/book.mk defines the expected phony targets.""" - content = book_makefile.read_text() - - # get the list of phony targets from the Makefile - phony_targets = [line.strip() for line in content.splitlines() if line.startswith(".PHONY:")] - if not phony_targets: - pytest.skip("No .PHONY targets found in book.mk") - - # Collect all targets from all .PHONY lines - all_targets = set() - for phony_line in phony_targets: - targets = phony_line.split(":")[1].strip().split() - all_targets.update(targets) - - expected_targets = {"book", "marimushka", "mkdocs-build"} - assert expected_targets.issubset(all_targets), ( - f"Expected phony targets to include {expected_targets}, got {all_targets}" - ) - - -def test_book_without_logo_file(git_repo, book_makefile): - """Test that book target works when LOGO_FILE is not set or empty. - - The build should succeed gracefully without a logo, and the generated - HTML template should hide the logo element via onerror handler. - """ - makefile = git_repo / "Makefile" - if not makefile.exists(): - pytest.skip("Makefile not found") - - # Read current Makefile content - content = makefile.read_text() - - # Remove or comment out LOGO_FILE if present - lines = content.splitlines() - new_lines = [] - for line in lines: - if line.strip().startswith("LOGO_FILE"): - # Comment out the line - new_lines.append(f"# {line}") - else: - new_lines.append(line) - makefile.write_text("\n".join(new_lines)) - - # Dry-run the book target - it should still be valid - result = subprocess.run([MAKE, "-n", "book"], cwd=git_repo, capture_output=True, text=True) # nosec - assert "no rule to make target" not in result.stderr.lower(), "book target should work without LOGO_FILE" - # Should not have errors about missing logo variable - assert result.returncode == 0, f"Dry-run failed: {result.stderr}" - - -def test_book_with_missing_logo_file(git_repo, book_makefile): - """Test that book target warns when LOGO_FILE points to non-existent file. - - The build should succeed but emit a warning about the missing logo. - """ - makefile = git_repo / "Makefile" - if not makefile.exists(): - pytest.skip("Makefile not found") - - # Read current Makefile content and set LOGO_FILE to non-existent path - content = makefile.read_text() - lines = content.splitlines() - new_lines = [] - logo_set = False - for line in lines: - if line.strip().startswith("LOGO_FILE"): - new_lines.append("LOGO_FILE=nonexistent/path/logo.svg") - logo_set = True - else: - new_lines.append(line) - if not logo_set: - # Insert LOGO_FILE before the include line - for i, line in enumerate(new_lines): - if line.strip().startswith("include"): - new_lines.insert(i, "LOGO_FILE=nonexistent/path/logo.svg") - break - makefile.write_text("\n".join(new_lines)) - - # Dry-run should still succeed - result = subprocess.run([MAKE, "-n", "book"], cwd=git_repo, capture_output=True, text=True) # nosec - assert result.returncode == 0, f"Dry-run failed with missing logo: {result.stderr}" diff --git a/.rhiza/tests/integration/test_lfs.py b/.rhiza/tests/integration/test_lfs.py deleted file mode 100644 index b722a6b..0000000 --- a/.rhiza/tests/integration/test_lfs.py +++ /dev/null @@ -1,182 +0,0 @@ -"""Tests for Git LFS Makefile targets. - -This file and its associated tests flow down via a SYNC action from the jebel-quant/rhiza repository -(https://github.com/jebel-quant/rhiza). - -Tests the lfs-install, lfs-pull, lfs-track, and lfs-status targets. -""" - -import shutil -import subprocess # nosec - -import pytest - -# Get make command once at module level -MAKE = shutil.which("make") or "/usr/bin/make" - - -@pytest.fixture -def lfs_makefile(git_repo): - """Return the lfs.mk path or skip tests if missing.""" - makefile = git_repo / ".rhiza" / "make.d" / "lfs.mk" - if not makefile.exists(): - pytest.skip("lfs.mk not found, skipping test") - return makefile - - -@pytest.fixture -def lfs_install_dry_run(git_repo, lfs_makefile): - """Run lfs-install in dry-run mode and return the result.""" - return subprocess.run( # nosec - [MAKE, "-n", "lfs-install"], - cwd=git_repo, - capture_output=True, - text=True, - ) - - -def test_lfs_targets_exist(git_repo, logger, lfs_makefile): - """Test that all LFS targets are defined in the Makefile.""" - result = subprocess.run( - [MAKE, "help"], - cwd=git_repo, - capture_output=True, - text=True, - ) # nosec - - assert result.returncode == 0 - assert "lfs-install" in result.stdout - assert "lfs-pull" in result.stdout - assert "lfs-track" in result.stdout - assert "lfs-status" in result.stdout - assert "Git LFS" in result.stdout - - -def test_lfs_install_dry_run(lfs_install_dry_run): - """Test lfs-install target in dry-run mode.""" - assert lfs_install_dry_run.returncode == 0 - # Check that the command includes OS detection - assert "uname -s" in lfs_install_dry_run.stdout - assert "uname -m" in lfs_install_dry_run.stdout - - -def test_lfs_install_macos_logic(lfs_install_dry_run): - """Test that lfs-install generates correct logic for macOS.""" - assert lfs_install_dry_run.returncode == 0 - # Verify macOS installation logic is present - assert "Darwin" in lfs_install_dry_run.stdout - assert "darwin-arm64" in lfs_install_dry_run.stdout - assert "darwin-amd64" in lfs_install_dry_run.stdout - assert ".local/bin" in lfs_install_dry_run.stdout - assert "curl" in lfs_install_dry_run.stdout - assert "github.com/git-lfs/git-lfs/releases" in lfs_install_dry_run.stdout - - -def test_lfs_install_linux_logic(lfs_install_dry_run): - """Test that lfs-install generates correct logic for Linux.""" - assert lfs_install_dry_run.returncode == 0 - # Verify Linux installation logic is present - assert "Linux" in lfs_install_dry_run.stdout - assert "apt-get update" in lfs_install_dry_run.stdout - assert "apt-get install" in lfs_install_dry_run.stdout - assert "git-lfs" in lfs_install_dry_run.stdout - - -def test_lfs_pull_target(git_repo, logger, lfs_makefile): - """Test lfs-pull target in dry-run mode.""" - result = subprocess.run( - [MAKE, "-n", "lfs-pull"], - cwd=git_repo, - capture_output=True, - text=True, - ) # nosec - - assert result.returncode == 0 - assert "git lfs pull" in result.stdout - - -def test_lfs_track_target(git_repo, logger, lfs_makefile): - """Test lfs-track target in dry-run mode.""" - result = subprocess.run( - [MAKE, "-n", "lfs-track"], - cwd=git_repo, - capture_output=True, - text=True, - ) # nosec - - assert result.returncode == 0 - assert "git lfs track" in result.stdout - - -def test_lfs_status_target(git_repo, logger, lfs_makefile): - """Test lfs-status target in dry-run mode.""" - result = subprocess.run( - [MAKE, "-n", "lfs-status"], - cwd=git_repo, - capture_output=True, - text=True, - ) # nosec - - assert result.returncode == 0 - assert "git lfs status" in result.stdout - - -def test_lfs_install_error_handling(lfs_install_dry_run): - """Test that lfs-install includes error handling.""" - assert lfs_install_dry_run.returncode == 0 - # Verify error handling is present - assert "ERROR" in lfs_install_dry_run.stdout - assert "exit 1" in lfs_install_dry_run.stdout - - -def test_lfs_install_uses_github_api(lfs_install_dry_run): - """Test that lfs-install uses GitHub API for version detection.""" - assert lfs_install_dry_run.returncode == 0 - - -def test_lfs_install_sudo_handling(lfs_install_dry_run): - """Test that lfs-install handles sudo correctly on Linux.""" - assert lfs_install_dry_run.returncode == 0 - # Verify sudo logic is present - assert "sudo" in lfs_install_dry_run.stdout - assert "id -u" in lfs_install_dry_run.stdout - - -@pytest.mark.skipif( - not shutil.which("git-lfs"), - reason="git-lfs not installed", -) -def test_lfs_actual_execution_status(git_repo, logger, lfs_makefile): - """Test actual execution of lfs-status (requires git-lfs to be installed).""" - # Initialize git-lfs in the test repo - subprocess.run(["git", "lfs", "install"], cwd=git_repo, capture_output=True) # nosec - - result = subprocess.run( - [MAKE, "lfs-status"], - cwd=git_repo, - capture_output=True, - text=True, - ) # nosec - - # Should succeed even if no LFS files are tracked - assert result.returncode == 0 - - -@pytest.mark.skipif( - not shutil.which("git-lfs"), - reason="git-lfs not installed", -) -def test_lfs_actual_execution_track(git_repo, logger, lfs_makefile): - """Test actual execution of lfs-track (requires git-lfs to be installed).""" - # Initialize git-lfs in the test repo - subprocess.run(["git", "lfs", "install"], cwd=git_repo, capture_output=True) # nosec - - result = subprocess.run( - [MAKE, "lfs-track"], - cwd=git_repo, - capture_output=True, - text=True, - ) # nosec - - # Should succeed even if no patterns are tracked - assert result.returncode == 0 diff --git a/.rhiza/tests/integration/test_marimushka.py b/.rhiza/tests/integration/test_marimushka.py deleted file mode 100644 index 6abdaa9..0000000 --- a/.rhiza/tests/integration/test_marimushka.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Tests for the marimushka Makefile target using a sandboxed environment. - -This file and its associated tests flow down via a SYNC action from the jebel-quant/rhiza repository -(https://github.com/jebel-quant/rhiza). - -Provides test fixtures for testing git-based workflows and version management. -""" - -import os -import shutil -import subprocess # nosec - -import pytest - -# Get shell path and make command once at module level -SHELL = shutil.which("sh") or "/bin/sh" -MAKE = shutil.which("make") or "/usr/bin/make" - - -def test_marimushka_target_success(git_repo): - """Test successful execution of the marimushka Makefile target.""" - # only run this test if the marimo folder is present - if not (git_repo / "book" / "marimo").exists(): - pytest.skip("marimo folder not found, skipping test") - - # Setup directories in the git repo - marimo_folder = git_repo / "book" / "marimo" / "notebooks" - marimo_folder.mkdir(parents=True, exist_ok=True) - (marimo_folder / "notebook.py").touch() - - output_folder = git_repo / "_marimushka" - - # Run the make target - env = os.environ.copy() - env["MARIMO_FOLDER"] = "book/marimo/notebooks" - env["MARIMUSHKA_OUTPUT"] = "_marimushka" - - # Create dummy bin/uv and bin/uvx if they don't exist - (git_repo / "bin").mkdir(exist_ok=True) - (git_repo / "bin" / "uv").touch() - (git_repo / "bin" / "uv").chmod(0o755) - (git_repo / "bin" / "uvx").touch() - (git_repo / "bin" / "uvx").chmod(0o755) - - # Put our bin on the PATH so 'command -v uvx' finds it in the test - env["PATH"] = f"{git_repo}/bin:{env.get('PATH', '')}" - - # In tests, we don't want to actually run marimushka as it's not installed in the mock env - # But we want to test that the Makefile logic works. - # We can mock the marimushka CLI call by creating a script that generates the expected files. - with open(git_repo / "bin" / "marimushka", "w") as f: - f.write( - f"#!/bin/sh\nmkdir -p {output_folder}/notebooks\n" - f"touch {output_folder}/index.html\n" - f"touch {output_folder}/notebooks/notebook.html\n" - ) - (git_repo / "bin" / "marimushka").chmod(0o755) - - # Override UVX_BIN to use our mock marimushka CLI - env["UVX_BIN"] = str(git_repo / "bin" / "marimushka") - - result = subprocess.run([MAKE, "marimushka"], env=env, cwd=git_repo, capture_output=True, text=True) # nosec - - assert result.returncode == 0 - assert "Exporting notebooks" in result.stdout - assert (output_folder / "index.html").exists() - assert (output_folder / "notebooks" / "notebook.html").exists() - - -def test_marimushka_no_python_files(git_repo): - """Test marimushka target behavior when MARIMO_FOLDER has no python files.""" - if not (git_repo / "book" / "marimo").exists(): - pytest.skip("marimo folder not found, skipping test") - - marimo_folder = git_repo / "book" / "marimo" / "notebooks" - marimo_folder.mkdir(parents=True, exist_ok=True) - - # Delete all .py files in the marimo folder - for file in marimo_folder.glob("*.py"): - file.unlink() - - # No .py files created - - output_folder = git_repo / "_marimushka" - - env = os.environ.copy() - env["MARIMO_FOLDER"] = "book/marimo/notebooks" - env["MARIMUSHKA_OUTPUT"] = "_marimushka" - - result = subprocess.run([MAKE, "marimushka"], env=env, cwd=git_repo, capture_output=True, text=True) # nosec - - assert result.returncode == 0 - assert (output_folder / "index.html").exists() diff --git a/.rhiza/tests/integration/test_notebook_execution.py b/.rhiza/tests/integration/test_notebook_execution.py deleted file mode 100644 index 12036f9..0000000 --- a/.rhiza/tests/integration/test_notebook_execution.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Tests for Marimo notebooks.""" - -import shutil -import subprocess # nosec -from pathlib import Path - -import pytest -from dotenv import dotenv_values - -# Read .rhiza/.env at collection time (no environment side-effects). -# dotenv_values returns a dict of key -> value (or None for missing). -RHIZA_ENV_PATH = Path(".rhiza/.env") - - -def collect_marimo_notebooks(env_path: Path = RHIZA_ENV_PATH): - """Return a sorted list of notebook script Paths discovered from .rhiza/.env. - - - Reads MARIMO_FOLDER from .rhiza/.env (if present), otherwise falls back to "marimo". - - Returns [] if the folder does not exist. - """ - values = {} - if env_path.exists(): - values = dotenv_values(env_path) - - marimo_folder = values.get("MARIMO_FOLDER", "book/marimo/notebooks") - marimo_path = Path(marimo_folder) - - if not marimo_path.exists(): - return [] - - # Return sorted list for stable ordering - return sorted(marimo_path.glob("*.py")) - - -# Collect notebook paths at import/collection time so pytest.parametrize can use them. -NOTEBOOK_PATHS = collect_marimo_notebooks() - - -def test_notebooks_discovered(): - """At least one notebook should be discovered for parametrized tests to run.""" - if not NOTEBOOK_PATHS: - pytest.skip("No Marimo notebooks found — check MARIMO_FOLDER in .rhiza/.env") - - -@pytest.mark.parametrize("notebook_path", NOTEBOOK_PATHS, ids=lambda p: p.name) -def test_notebook_execution(notebook_path: Path): - """Test if a Marimo notebook can be executed without errors. - - We use 'marimo export html' which executes the notebook cells and - reports if any cells failed. - """ - # Determine uvx command: prefer local ./bin/uvx, then fall back to uvx on PATH. - local_uvx = Path("bin/uvx") - - if local_uvx.exists() and local_uvx.is_file(): - uvx_cmd = str(local_uvx.resolve()) # Use absolute path - else: - uvx_cmd = shutil.which("uvx") - if uvx_cmd is None: - pytest.skip("uvx not found (neither ./bin/uvx nor uvx on PATH); skipping marimo notebook tests") - - cmd = [ - uvx_cmd, - "--with", - "typing_extensions", # Workaround: marimo's starlette dep needs this - "marimo", - "export", - "html", - "--sandbox", - str(notebook_path.name), - "-o", - "/dev/null", # We don't need the actual HTML output - ] - - result = subprocess.run(cmd, capture_output=True, text=True, cwd=notebook_path.parent) # nosec - - # Ensure process exit code indicates success - assert result.returncode == 0, ( - f"Marimo export returned non-zero for {notebook_path.name}:\n" - f"stdout:\n{result.stdout}\n\nstderr:\n{result.stderr}" - ) - - # Check stdout/stderr for known failure messages (case-insensitive) - combined_output = (result.stdout or "") + "\n" + (result.stderr or "") - lower_output = combined_output.lower() - - failure_keywords = [ - "cells failed to execute", - "marimoexceptionraisederror", - "Couldn't parse requirement", # sandbox setup fails because of invalid dependencies section in the notebook - ] - for kw in failure_keywords: - assert kw.lower() not in lower_output, ( - f"Notebook {notebook_path.name} reported cell failures (found keyword '{kw}'):\n" - f"stdout:\n{result.stdout}\n\nstderr:\n{result.stderr}" - ) diff --git a/.rhiza/tests/integration/test_sbom.py b/.rhiza/tests/integration/test_sbom.py deleted file mode 100644 index 46b2a6f..0000000 --- a/.rhiza/tests/integration/test_sbom.py +++ /dev/null @@ -1,159 +0,0 @@ -"""Integration test for SBOM generation using cyclonedx-bom. - -This file and its associated tests flow down via a SYNC action from the jebel-quant/rhiza repository -(https://github.com/jebel-quant/rhiza). - -Tests the SBOM (Software Bill of Materials) generation workflow to ensure -the cyclonedx-bom tool works correctly with uvx. -""" - -import subprocess # nosec B404 - - -def test_sbom_generation_json(git_repo, logger): - """Test that SBOM generation works in JSON format.""" - # Run the SBOM generation command for JSON - result = subprocess.run( # nosec B603 - [ - "uvx", - "--from", - "cyclonedx-bom>=7.0.0", - "cyclonedx-py", - "environment", - "--of", - "JSON", - "-o", - "sbom.cdx.json", - ], - cwd=git_repo, - capture_output=True, - text=True, - check=False, - ) - - logger.info("SBOM JSON stdout: %s", result.stdout) - logger.info("SBOM JSON stderr: %s", result.stderr) - - # Verify command succeeded - assert result.returncode == 0, f"SBOM JSON generation failed: {result.stderr}" - - # Verify output file exists - sbom_file = git_repo / "sbom.cdx.json" - assert sbom_file.exists(), "SBOM JSON file was not created" - assert sbom_file.stat().st_size > 0, "SBOM JSON file is empty" - - # Verify it's valid JSON - import json - - with open(sbom_file) as f: - sbom_data = json.load(f) - - # Basic CycloneDX structure validation - assert "bomFormat" in sbom_data, "SBOM missing bomFormat field" - assert sbom_data["bomFormat"] == "CycloneDX", "SBOM has incorrect bomFormat" - assert "components" in sbom_data, "SBOM missing components field" - - -def test_sbom_generation_xml(git_repo, logger): - """Test that SBOM generation works in XML format.""" - # Run the SBOM generation command for XML - result = subprocess.run( # nosec B603 - [ - "uvx", - "--from", - "cyclonedx-bom>=7.0.0", - "cyclonedx-py", - "environment", - "--of", - "XML", - "-o", - "sbom.cdx.xml", - ], - cwd=git_repo, - capture_output=True, - text=True, - check=False, - ) - - logger.info("SBOM XML stdout: %s", result.stdout) - logger.info("SBOM XML stderr: %s", result.stderr) - - # Verify command succeeded - assert result.returncode == 0, f"SBOM XML generation failed: {result.stderr}" - - # Verify output file exists - sbom_file = git_repo / "sbom.cdx.xml" - assert sbom_file.exists(), "SBOM XML file was not created" - assert sbom_file.stat().st_size > 0, "SBOM XML file is empty" - - # Verify it's valid XML with CycloneDX structure - import defusedxml.ElementTree - - tree = defusedxml.ElementTree.parse(sbom_file) - root = tree.getroot() - - # Check for CycloneDX namespace - assert "cyclonedx" in root.tag.lower(), "SBOM XML root is not CycloneDX" - # Check for components element - components = root.find(".//{*}components") - assert components is not None, "SBOM XML missing components element" - - -def test_sbom_command_syntax(git_repo, logger): - """Test that the uvx command syntax is correct (no npm-style @^version).""" - # This test verifies that we're using the correct syntax - # Bad: uvx cyclonedx-bom@^7.0.0 - # Good: uvx --from 'cyclonedx-bom>=7.0.0' cyclonedx-py - - # Try the old (incorrect) syntax - should fail - result_bad = subprocess.run( # nosec B603 - [ - "uvx", - "cyclonedx-bom@^7.0.0", - "environment", - "--of", - "JSON", - "-o", - "sbom.test.json", - ], - cwd=git_repo, - capture_output=True, - text=True, - check=False, - ) - - logger.info("Bad syntax stdout: %s", result_bad.stdout) - logger.info("Bad syntax stderr: %s", result_bad.stderr) - - # Old syntax should fail - assert result_bad.returncode != 0, "Old npm-style syntax should not work" - - # Try the new (correct) syntax - should succeed - result_good = subprocess.run( # nosec B603 - [ - "uvx", - "--from", - "cyclonedx-bom>=7.0.0", - "cyclonedx-py", - "environment", - "--of", - "JSON", - "-o", - "sbom.test.json", - ], - cwd=git_repo, - capture_output=True, - text=True, - check=False, - ) - - logger.info("Good syntax stdout: %s", result_good.stdout) - logger.info("Good syntax stderr: %s", result_good.stderr) - - # New syntax should succeed - assert result_good.returncode == 0, f"Correct syntax failed: {result_good.stderr}" - - # Cleanup - test_file = git_repo / "sbom.test.json" - if test_file.exists(): - test_file.unlink() diff --git a/.rhiza/tests/integration/test_test_mk.py b/.rhiza/tests/integration/test_test_mk.py deleted file mode 100644 index 83102bd..0000000 --- a/.rhiza/tests/integration/test_test_mk.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Integration test for .rhiza/make.d/test.mk to verify that it handles the case of missing test files correctly.""" - -from test_utils import run_make - - -def test_missing_tests_warning(git_repo, logger): - """Test that missing tests trigger a warning but do not fail (exit 0).""" - # 1. Setup a minimal Makefile in the test repo - # We include .rhiza/make.d/test.mk but mock the 'install' dependency - # and provide color variables used in the script. - makefile_content = r""" -YELLOW := \033[33m -RED := \033[31m -RESET := \033[0m - -# Define folders expected by test.mk -TESTS_FOLDER := tests -SOURCE_FOLDER := src -VENV := .venv - -# Mock install to avoid actual installation in test -install: - @echo "Mock install" - -# Include the target under test -include .rhiza/make.d/test.mk -""" - (git_repo / "Makefile").write_text(makefile_content, encoding="utf-8") - - # 2. Ensure 'tests' folder exists but is empty/has no python test files - tests_dir = git_repo / "tests" - if tests_dir.exists(): - import shutil - - shutil.rmtree(tests_dir) - tests_dir.mkdir() - - # 3. Run 'make test' - # We use dry_run=False so the shell commands in the recipe actually execute. - # The 'check=False' allows us to assert the return code ourselves, - # though we expect 0 now. - result = run_make(logger, ["test"], check=False, dry_run=False) - - # 4. output for debugging - logger.info("make stdout: %s", result.stdout) - logger.info("make stderr: %s", result.stderr) - - # 5. Verify results - assert result.returncode == 0, "make test should exit with 0 when no tests found" - - # The warning message matches what we put in test.mk - # "No test files found in {TESTS_FOLDER}, skipping tests" - assert "No test files found in tests, skipping tests" in result.stdout diff --git a/.rhiza/tests/integration/test_virtual_env_unexport.py b/.rhiza/tests/integration/test_virtual_env_unexport.py deleted file mode 100644 index fae30bb..0000000 --- a/.rhiza/tests/integration/test_virtual_env_unexport.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Integration test to verify VIRTUAL_ENV is unset for uv commands.""" - -import os - -from test_utils import run_make - - -def test_virtual_env_not_exported(git_repo, logger): - """Test that VIRTUAL_ENV is not exported to child processes when set in the environment.""" - # 1. Setup a minimal Makefile that includes rhiza.mk - makefile_content = r""" -# Include rhiza.mk which has 'unexport VIRTUAL_ENV' -include .rhiza/rhiza.mk - -# Create a test target that checks if VIRTUAL_ENV is exported -.PHONY: test-env -test-env: - @echo "VIRTUAL_ENV in shell: '$$VIRTUAL_ENV'" -""" - (git_repo / "Makefile").write_text(makefile_content, encoding="utf-8") - - # 2. Set VIRTUAL_ENV in the environment (simulating an activated venv) - env = os.environ.copy() - env["VIRTUAL_ENV"] = "/some/absolute/path/.venv" - - # 3. Run 'make test-env' with VIRTUAL_ENV set - result = run_make(logger, ["test-env"], check=True, dry_run=False, env=env) - - # 4. Output for debugging - logger.info("make stdout: %s", result.stdout) - logger.info("make stderr: %s", result.stderr) - - # 5. Verify that VIRTUAL_ENV is empty in the shell (not exported) - # The output should contain "VIRTUAL_ENV in shell: ''" - assert "VIRTUAL_ENV in shell: ''" in result.stdout, ( - f"VIRTUAL_ENV should be empty in shell commands, but got: {result.stdout}" - ) diff --git a/.rhiza/tests/structure/test_lfs_structure.py b/.rhiza/tests/structure/test_lfs_structure.py deleted file mode 100644 index 038ccc4..0000000 --- a/.rhiza/tests/structure/test_lfs_structure.py +++ /dev/null @@ -1,135 +0,0 @@ -"""Tests for Git LFS template structure and files. - -This file and its associated tests flow down via a SYNC action from the jebel-quant/rhiza repository -(https://github.com/jebel-quant/rhiza). - -Verifies that LFS-related files and configurations are present. -""" - -import pytest - - -@pytest.fixture -def lfs_makefile(root): - """Fixture that returns the path to lfs.mk if it exists, else skips.""" - path = root / ".rhiza" / "make.d" / "lfs.mk" - if not path.exists(): - pytest.skip("lfs.mk not found, skipping LFS tests") - return path - - -class TestLFSTemplateStructure: - """Tests for LFS template file structure.""" - - def test_lfs_makefile_exists(self, lfs_makefile): - """LFS makefile should exist in make.d directory.""" - assert lfs_makefile.exists() - - def test_lfs_documentation_exists(self, root, lfs_makefile): - """LFS documentation should exist.""" - lfs_doc = root / ".rhiza" / "docs" / "LFS.md" - assert lfs_doc.exists(), "LFS.md documentation not found" - - def test_lfs_makefile_has_targets(self, lfs_makefile): - """LFS makefile should define all expected targets.""" - content = lfs_makefile.read_text() - - required_targets = [ - "lfs-install:", - "lfs-pull:", - "lfs-track:", - "lfs-status:", - ] - - for target in required_targets: - assert target in content, f"Target {target} not found in lfs.mk" - - def test_lfs_makefile_has_phony_declarations(self, lfs_makefile): - """LFS makefile should declare targets as phony.""" - content = lfs_makefile.read_text() - - assert ".PHONY:" in content - assert "lfs-install" in content - assert "lfs-pull" in content - assert "lfs-track" in content - assert "lfs-status" in content - - def test_lfs_makefile_has_help_comments(self, lfs_makefile): - """LFS makefile should have help comments for targets.""" - content = lfs_makefile.read_text() - - # Check for ##@ section header - assert "##@ Git LFS" in content - - # Check for target descriptions - assert "##" in content - - def test_lfs_documentation_has_sections(self, root, lfs_makefile): - """LFS documentation should have all expected sections.""" - lfs_doc = root / ".rhiza" / "docs" / "LFS.md" - # Since test_lfs_documentation_exists checks existence, we assume it exists if passed - if not lfs_doc.exists(): - pytest.skip("LFS.md not found") - - content = lfs_doc.read_text() - - expected_sections = [ - "# Git LFS", - "## Overview", - "## Available Make Targets", - "## Typical Workflow", - "## CI/CD Integration", - "## Troubleshooting", - ] - - for section in expected_sections: - assert section in content, f"Section '{section}' not found in LFS.md" - - def test_lfs_documentation_describes_all_targets(self, root, lfs_makefile): - """LFS documentation should describe all make targets.""" - lfs_doc = root / ".rhiza" / "docs" / "LFS.md" - if not lfs_doc.exists(): - pytest.skip("LFS.md not found") - - content = lfs_doc.read_text() - - targets = [ - "lfs-install", - "lfs-pull", - "lfs-track", - "lfs-status", - ] - - for target in targets: - assert target in content, f"Target {target} not documented in LFS.md" - - def test_lfs_makefile_cross_platform_support(self, lfs_makefile): - """LFS makefile should support multiple platforms.""" - content = lfs_makefile.read_text() - - # Check for OS detection - assert "uname -s" in content - assert "Darwin" in content - assert "Linux" in content - - # Check for architecture detection (macOS) - assert "uname -m" in content - assert "arm64" in content - assert "amd64" in content - - def test_lfs_makefile_error_handling(self, lfs_makefile): - """LFS makefile should include error handling.""" - content = lfs_makefile.read_text() - - # Check for error messages - assert "ERROR" in content - assert "exit 1" in content - - def test_lfs_makefile_uses_color_variables(self, lfs_makefile): - """LFS makefile should use standard color variables.""" - content = lfs_makefile.read_text() - - # Check for color variable usage - assert "BLUE" in content - assert "RED" in content - assert "RESET" in content diff --git a/.rhiza/tests/structure/test_project_layout.py b/.rhiza/tests/structure/test_project_layout.py deleted file mode 100644 index 1ceac5f..0000000 --- a/.rhiza/tests/structure/test_project_layout.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Tests for the root pytest fixture that yields the repository root Path. - -This file and its associated tests flow down via a SYNC action from the jebel-quant/rhiza repository -(https://github.com/jebel-quant/rhiza). - -This module ensures the fixture resolves to the true project root and that -expected files/directories exist, enabling other tests to locate resources -reliably. -""" - -import pytest - - -class TestRootFixture: - """Tests for the root fixture that provides repository root path.""" - - def test_root_resolves_correctly_from_nested_location(self, root): - """Root should correctly resolve to repository root from .rhiza/tests/.""" - conftest_path = root / ".rhiza" / "tests" / "conftest.py" - assert conftest_path.exists() - - def test_root_contains_expected_directories(self, root): - """Root should contain all expected project directories.""" - required_dirs = [".rhiza"] - optional_dirs = ["src", "tests", "book"] # src/ is optional (rhiza itself doesn't have one) - - for dirname in required_dirs: - assert (root / dirname).exists(), f"Required directory {dirname} not found" - - # Check that at least one CI directory exists (.github or .gitlab) - ci_dirs = [".github", ".gitlab"] - if not any((root / ci_dir).exists() for ci_dir in ci_dirs): - pytest.fail(f"At least one CI directory from {ci_dirs} must exist") - - for dirname in optional_dirs: - if not (root / dirname).exists(): - pytest.skip(f"Optional directory {dirname} not present in this project") - - def test_root_contains_expected_files(self, root): - """Root should contain all expected configuration files.""" - required_files = [ - "pyproject.toml", - "README.md", - "Makefile", - ] - optional_files = [ - "ruff.toml", - ".gitignore", - ".editorconfig", - ] - - for filename in required_files: - assert (root / filename).exists(), f"Required file {filename} not found" - - for filename in optional_files: - if not (root / filename).exists(): - pytest.skip(f"Optional file {filename} not present in this project") diff --git a/.rhiza/tests/structure/test_requirements.py b/.rhiza/tests/structure/test_requirements.py deleted file mode 100644 index 1bf9d04..0000000 --- a/.rhiza/tests/structure/test_requirements.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Tests for the .rhiza/requirements folder structure. - -This test ensures that the requirements folder exists and contains the expected -requirement files for development dependencies. -""" - -from typing import ClassVar - - -class TestRequirementsFolder: - """Tests for the .rhiza/requirements folder structure.""" - - # Expected requirements files - EXPECTED_REQUIREMENTS_FILES: ClassVar[list[str]] = [ - # "tests.txt", # may not be present in all repositories - # "marimo.txt", # may not be present in all repositories - "docs.txt", - "tools.txt", - ] - - def test_requirements_folder_exists(self, root): - """Requirements folder should exist in .rhiza directory.""" - requirements_dir = root / ".rhiza" / "requirements" - assert requirements_dir.exists(), ".rhiza/requirements directory should exist" - assert requirements_dir.is_dir(), ".rhiza/requirements should be a directory" - - def test_requirements_files_exist(self, root): - """All expected requirements files should exist.""" - requirements_dir = root / ".rhiza" / "requirements" - for filename in self.EXPECTED_REQUIREMENTS_FILES: - filepath = requirements_dir / filename - assert filepath.exists(), f"{filename} should exist in requirements folder" - assert filepath.is_file(), f"{filename} should be a file" - - def test_requirements_files_not_empty(self, root): - """Requirements files should not be empty.""" - requirements_dir = root / ".rhiza" / "requirements" - for filename in self.EXPECTED_REQUIREMENTS_FILES: - filepath = requirements_dir / filename - content = filepath.read_text() - # Filter out comments and empty lines - lines = [line.strip() for line in content.splitlines() if line.strip() and not line.strip().startswith("#")] - assert len(lines) > 0, f"{filename} should contain at least one dependency" - - def test_readme_exists_in_requirements_folder(self, root): - """README.md should exist in requirements folder.""" - readme_path = root / ".rhiza" / "requirements" / "README.md" - assert readme_path.exists(), "README.md should exist in requirements folder" - assert readme_path.is_file(), "README.md should be a file" - content = readme_path.read_text() - assert len(content) > 0, "README.md should not be empty" diff --git a/.rhiza/tests/structure/test_template_bundles.py b/.rhiza/tests/structure/test_template_bundles.py deleted file mode 100644 index a25f662..0000000 --- a/.rhiza/tests/structure/test_template_bundles.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Tests to validate that all files/folders referenced in template-bundles.yml exist. - -This file and its associated tests flow down via a SYNC action from the jebel-quant/rhiza repository -(https://github.com/jebel-quant/rhiza). - -This module ensures that template bundle definitions in .rhiza/template-bundles.yml -reference only files and folders that actually exist in the repository. -""" - -import pytest -import yaml - - -class TestTemplateBundles: - """Tests for template-bundles.yml validation.""" - - @pytest.fixture - def bundles_file(self, root): - """Return the path to template-bundles.yml.""" - return root / ".rhiza" / "template-bundles.yml" - - @pytest.fixture - def bundles_data(self, bundles_file): - """Load and parse the template-bundles.yml file.""" - if not bundles_file.exists(): - pytest.skip("template-bundles.yml does not exist in this project") - - with open(bundles_file) as f: - data = yaml.safe_load(f) - - if not data or "bundles" not in data: - pytest.fail("Invalid template-bundles.yml format - missing 'bundles' key") - - return data - - def test_bundles_file_exists_or_skip(self, bundles_file): - """Test that template-bundles.yml exists, or skip if not present.""" - if not bundles_file.exists(): - pytest.skip("template-bundles.yml does not exist in this project") - - def test_all_bundle_files_exist(self, root, bundles_data): - """Test that all files referenced in template-bundles.yml exist.""" - bundles = bundles_data["bundles"] - all_missing = [] - total_files = 0 - - # Check each bundle - for bundle_name, bundle_config in bundles.items(): - if "files" not in bundle_config: - continue - - files = bundle_config["files"] - - for file_path in files: - total_files += 1 - path = root / file_path - - if not path.exists(): - all_missing.append((bundle_name, file_path)) - - # Report results - if all_missing: - error_msg = f"\nValidation failed: {len(all_missing)} of {total_files} files/folders are missing:\n\n" - for bundle_name, file_path in all_missing: - error_msg += f" [{bundle_name}] {file_path}\n" - pytest.fail(error_msg) - - def test_each_bundle_files_exist(self, root, bundles_data): - """Test that files exist for each individual bundle.""" - bundles = bundles_data["bundles"] - - for bundle_name, bundle_config in bundles.items(): - if "files" not in bundle_config: - continue - - files = bundle_config["files"] - missing_in_bundle = [] - - for file_path in files: - path = root / file_path - - if not path.exists(): - missing_in_bundle.append(file_path) - - if missing_in_bundle: - error_msg = f"\nBundle '{bundle_name}' has {len(missing_in_bundle)} missing path(s):\n" - for missing in missing_in_bundle: - error_msg += f" - {missing}\n" - pytest.fail(error_msg) diff --git a/.rhiza/tests/sync/conftest.py b/.rhiza/tests/sync/conftest.py deleted file mode 100644 index 383c8b2..0000000 --- a/.rhiza/tests/sync/conftest.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Shared fixtures and helpers for sync tests. - -Provides environment setup for template sync, workflow versioning, -and content validation tests. -""" - -from __future__ import annotations - -import os -import shutil -import sys -from pathlib import Path - -import pytest - -tests_root = Path(__file__).resolve().parents[1] -if str(tests_root) not in sys.path: - sys.path.insert(0, str(tests_root)) - -from test_utils import run_make, setup_rhiza_git_repo, strip_ansi # noqa: E402, F401 - - -@pytest.fixture(autouse=True) -def setup_sync_env(logger, root, tmp_path: Path): - """Set up a temporary environment for sync tests with Makefile, templates, and git. - - This fixture creates a complete test environment with: - - Makefile and rhiza.mk configuration - - .rhiza-version file and .env configuration - - template.yml and pyproject.toml - - Initialized git repository (configured as rhiza origin) - - src/ and tests/ directories to satisfy validate target - """ - logger.debug("Setting up sync test environment: %s", tmp_path) - - # Copy the main Makefile into the temporary working directory - shutil.copy(root / "Makefile", tmp_path / "Makefile") - - # Copy core Rhiza Makefiles and version file - (tmp_path / ".rhiza").mkdir(exist_ok=True) - shutil.copy(root / ".rhiza" / "rhiza.mk", tmp_path / ".rhiza" / "rhiza.mk") - - # Copy split Makefiles from make.d directory - split_makefiles = [ - "bootstrap.mk", - "quality.mk", - "releasing.mk", - "test.mk", - "book.mk", - "marimo.mk", - "presentation.mk", - "github.mk", - "agentic.mk", - "docker.mk", - "docs.mk", - ] - (tmp_path / ".rhiza" / "make.d").mkdir(parents=True, exist_ok=True) - for mk_file in split_makefiles: - source_path = root / ".rhiza" / "make.d" / mk_file - if source_path.exists(): - shutil.copy(source_path, tmp_path / ".rhiza" / "make.d" / mk_file) - - # Copy .rhiza-version if it exists - if (root / ".rhiza" / ".rhiza-version").exists(): - shutil.copy(root / ".rhiza" / ".rhiza-version", tmp_path / ".rhiza" / ".rhiza-version") - - # Create a minimal, deterministic .rhiza/.env for tests - env_content = "SCRIPTS_FOLDER=.rhiza/scripts\nCUSTOM_SCRIPTS_FOLDER=.rhiza/customisations/scripts\n" - (tmp_path / ".rhiza" / ".env").write_text(env_content) - - logger.debug("Copied Makefile from %s to %s", root / "Makefile", tmp_path / "Makefile") - - # Create a minimal .rhiza/template.yml - (tmp_path / ".rhiza" / "template.yml").write_text("repository: Jebel-Quant/rhiza\nref: v0.7.1\n") - - # Sort out pyproject.toml - (tmp_path / "pyproject.toml").write_text('[project]\nname = "test-project"\nversion = "0.1.0"\n') - - # Move into tmp directory for isolation - old_cwd = Path.cwd() - os.chdir(tmp_path) - logger.debug("Changed working directory to %s", tmp_path) - - # Initialize a git repo so that commands checking for it (like materialize) don't fail validation - setup_rhiza_git_repo() - - # Create src and tests directories to satisfy validate - (tmp_path / "src").mkdir(exist_ok=True) - (tmp_path / "tests").mkdir(exist_ok=True) - - try: - yield - finally: - os.chdir(old_cwd) - logger.debug("Restored working directory to %s", old_cwd) diff --git a/.rhiza/tests/sync/test_docstrings.py b/.rhiza/tests/sync/test_docstrings.py deleted file mode 100644 index 231e57b..0000000 --- a/.rhiza/tests/sync/test_docstrings.py +++ /dev/null @@ -1,123 +0,0 @@ -"""Tests for module docstrings using doctest. - -This file and its associated tests flow down via a SYNC action from the jebel-quant/rhiza repository -(https://github.com/jebel-quant/rhiza). - -Automatically discovers all packages and runs doctests for each. -""" - -from __future__ import annotations - -import doctest -import importlib -import warnings -from pathlib import Path - -import pytest -from dotenv import dotenv_values - -# Read .rhiza/.env at collection time (no environment side-effects). -RHIZA_ENV_PATH = Path(".rhiza/.env") - - -def _iter_modules_from_path(logger, package_path: Path, src_path: Path): - """Recursively find all Python modules in a directory.""" - for path in package_path.rglob("*.py"): - if path.name == "__init__.py": - module_path = path.parent.relative_to(src_path) - else: - module_path = path.relative_to(src_path).with_suffix("") - - # Convert path to module name in an OS-independent way - module_name = ".".join(module_path.parts) - - try: - yield importlib.import_module(module_name) - except ImportError as e: - warnings.warn(f"Could not import {module_name}: {e}", stacklevel=2) - logger.warning("Could not import module %s: %s", module_name, e) - continue - - -def _find_packages(src_path: Path): - """Find all packages in the source path, including those nested under namespace packages.""" - for init_file in src_path.rglob("__init__.py"): - package_dir = init_file.parent - # Only yield top-level packages (those whose parent doesn't have __init__.py or is src_path) - parent = package_dir.parent - if parent == src_path or not (parent / "__init__.py").exists(): - yield package_dir - - -def test_doctests(logger, root, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]): - """Run doctests for each package directory.""" - values = dotenv_values(root / RHIZA_ENV_PATH) if (root / RHIZA_ENV_PATH).exists() else {} - source_folder = values.get("SOURCE_FOLDER", "src") - src_path = root / source_folder - - logger.info("Starting doctest discovery in: %s", src_path) - if not src_path.exists(): - logger.info("Source directory not found: %s — skipping doctests", src_path) - pytest.skip(f"Source directory not found: {src_path}") - - # Add source path to sys.path with automatic cleanup - monkeypatch.syspath_prepend(str(src_path)) - logger.debug("Prepended to sys.path: %s", src_path) - - total_tests = 0 - total_failures = 0 - failed_modules = [] - - # Find all packages in the source path (supports namespace packages) - for package_dir in _find_packages(src_path): - if package_dir.is_dir() and (package_dir / "__init__.py").exists(): - # Import the package - package_name = package_dir.name - logger.info("Discovered package: %s", package_name) - try: - modules = list(_iter_modules_from_path(logger, package_dir, src_path)) - logger.debug("%d module(s) found in package %s", len(modules), package_name) - - for module in modules: - logger.debug("Running doctests for module: %s", module.__name__) - # Disable pytest's stdout capture during doctest to avoid interference - with capsys.disabled(): - results = doctest.testmod( - module, - verbose=False, - optionflags=(doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE), - ) - total_tests += results.attempted - - if results.failed: - logger.warning( - "Doctests failed for %s: %d/%d failed", - module.__name__, - results.failed, - results.attempted, - ) - total_failures += results.failed - failed_modules.append((module.__name__, results.failed, results.attempted)) - else: - logger.debug("Doctests passed for %s (%d test(s))", module.__name__, results.attempted) - - except ImportError as e: - warnings.warn(f"Could not import package {package_name}: {e}", stacklevel=2) - logger.warning("Could not import package %s: %s", package_name, e) - continue - - if failed_modules: - formatted = "\n".join(f" {name}: {failed}/{attempted} failed" for name, failed, attempted in failed_modules) - msg = ( - f"Doctest summary: {total_tests} tests across {len(failed_modules)} module(s)\n" - f"Failures: {total_failures}\n" - f"Failed modules:\n{formatted}" - ) - logger.error("%s", msg) - assert total_failures == 0, msg - else: - logger.info("Doctest summary: %d tests, 0 failures", total_tests) - - if total_tests == 0: - logger.info("No doctests were found in any module — skipping") - pytest.skip("No doctests were found in any module") diff --git a/.rhiza/tests/sync/test_readme_validation.py b/.rhiza/tests/sync/test_readme_validation.py deleted file mode 100644 index 29f1b0e..0000000 --- a/.rhiza/tests/sync/test_readme_validation.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Tests for README code examples. - -This file and its associated tests flow down via a SYNC action from the jebel-quant/rhiza repository -(https://github.com/jebel-quant/rhiza). - -This module extracts Python code and expected result blocks from README.md, -executes the code, and verifies the output matches the documented result. -""" - -import re -import subprocess -import sys - -import pytest - -# Regex for Python code blocks -CODE_BLOCK = re.compile(r"```python\n(.*?)```", re.DOTALL) - -RESULT = re.compile(r"```result\n(.*?)```", re.DOTALL) - -# Regex for Bash code blocks -BASH_BLOCK = re.compile(r"```bash\n(.*?)```", re.DOTALL) - -# Bash executable used for syntax checking; subprocess.run below is trusted (noqa: S603). -BASH = "bash" - - -def test_readme_runs(logger, root): - """Execute README code blocks and compare output to documented results.""" - readme = root / "README.md" - logger.info("Reading README from %s", readme) - readme_text = readme.read_text(encoding="utf-8") - code_blocks = CODE_BLOCK.findall(readme_text) - result_blocks = RESULT.findall(readme_text) - logger.info("Found %d code block(s) and %d result block(s) in README", len(code_blocks), len(result_blocks)) - - code = "".join(code_blocks) # merged code - expected = "".join(result_blocks) # merged results - - # Trust boundary: we execute Python snippets sourced from README.md in this repo. - # The README is part of the trusted repository content and reviewed in PRs. - logger.debug("Executing README code via %s -c ...", sys.executable) - result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, cwd=root) # nosec - - stdout = result.stdout - logger.debug("Execution finished with return code %d", result.returncode) - if result.stderr: - logger.debug("Stderr from README code:\n%s", result.stderr) - logger.debug("Stdout from README code:\n%s", stdout) - - assert result.returncode == 0, f"README code exited with {result.returncode}. Stderr:\n{result.stderr}" - logger.info("README code executed successfully; comparing output to expected result") - assert stdout.strip() == expected.strip() - logger.info("README code output matches expected result") - - -class TestReadmeTestEdgeCases: - """Edge cases for README code block testing.""" - - def test_readme_file_exists_at_root(self, root): - """README.md should exist at repository root.""" - readme = root / "README.md" - assert readme.exists() - assert readme.is_file() - - def test_readme_is_readable(self, root): - """README.md should be readable with UTF-8 encoding.""" - readme = root / "README.md" - content = readme.read_text(encoding="utf-8") - assert len(content) > 0 - assert isinstance(content, str) - - def test_readme_code_is_syntactically_valid(self, root): - """Python code blocks in README should be syntactically valid.""" - readme = root / "README.md" - content = readme.read_text(encoding="utf-8") - code_blocks = re.findall(r"\`\`\`python\n(.*?)\`\`\`", content, re.DOTALL) - - for i, code in enumerate(code_blocks): - try: - compile(code, f"", "exec") - except SyntaxError as e: - pytest.fail(f"Code block {i} has syntax error: {e}") - - -class TestReadmeBashFragments: - """Tests for bash code fragments in README.""" - - def test_bash_blocks_basic_syntax(self, root, logger): - """Bash code blocks should have basic valid syntax (can be parsed by bash -n).""" - readme = root / "README.md" - content = readme.read_text(encoding="utf-8") - bash_blocks = BASH_BLOCK.findall(content) - - logger.info("Found %d bash code block(s) in README", len(bash_blocks)) - - for i, code in enumerate(bash_blocks): - # Skip directory tree representations and other non-executable blocks - if any(marker in code for marker in ["├──", "└──", "│"]): - logger.info("Skipping bash block %d (directory tree representation)", i) - continue - - # Skip blocks that are primarily comments or documentation - lines = [line.strip() for line in code.split("\n") if line.strip()] - non_comment_lines = [line for line in lines if not line.startswith("#")] - if not non_comment_lines: - logger.info("Skipping bash block %d (only comments)", i) - continue - - logger.debug("Checking bash block %d:\n%s", i, code) - - # Use bash -n to check syntax without executing - # Trust boundary: we use bash -n which only parses without executing - result = subprocess.run( # nosec - [BASH, "-n"], - input=code, - capture_output=True, - text=True, - ) - - if result.returncode != 0: - pytest.fail(f"Bash block {i} has syntax errors:\nCode:\n{code}\nError:\n{result.stderr}") diff --git a/.rhiza/tests/sync/test_rhiza_version.py b/.rhiza/tests/sync/test_rhiza_version.py deleted file mode 100644 index 4f139dd..0000000 --- a/.rhiza/tests/sync/test_rhiza_version.py +++ /dev/null @@ -1,137 +0,0 @@ -"""Tests for the .rhiza-version file and related Makefile functionality. - -This file and its associated tests flow down via a SYNC action from the jebel-quant/rhiza repository -(https://github.com/jebel-quant/rhiza). - -These tests validate: -- Reading RHIZA_VERSION from .rhiza/.rhiza-version -- The summarise-sync Makefile target -- Version usage in sync and validate targets -""" - -from __future__ import annotations - -# Import from local conftest -from sync.conftest import run_make, strip_ansi - - -class TestRhizaVersion: - """Tests for RHIZA_VERSION variable and .rhiza-version file.""" - - def test_rhiza_version_exists_in_file(self, root): - """The .rhiza/.rhiza-version file should exist and contain a version.""" - version_file = root / ".rhiza" / ".rhiza-version" - assert version_file.exists() - assert version_file.is_file() - - content = version_file.read_text().strip() - assert len(content) > 0 - # Check it looks like a version (e.g., "0.9.0") - assert content[0].isdigit() - - def test_rhiza_version_exported_in_makefile(self, logger): - """RHIZA_VERSION should be exported and readable.""" - proc = run_make(logger, ["print-RHIZA_VERSION"], dry_run=False) - out = strip_ansi(proc.stdout) - # The output should contain the version value - assert "Value of RHIZA_VERSION:" in out - # Should have a version number - assert any(char.isdigit() for char in out) - - def test_rhiza_version_defaults_to_0_9_0_without_file(self, logger, tmp_path): - """RHIZA_VERSION should default to 0.10.2 if .rhiza-version doesn't exist.""" - # Remove the .rhiza-version file - version_file = tmp_path / ".rhiza" / ".rhiza-version" - if version_file.exists(): - version_file.unlink() - - # Clear RHIZA_VERSION from environment to test the default value - import os - import subprocess - - env = os.environ.copy() - env.pop("RHIZA_VERSION", None) - - cmd = ["/usr/bin/make", "-s", "print-RHIZA_VERSION"] - logger.info("Running command: %s", " ".join(cmd)) - proc = subprocess.run(cmd, capture_output=True, text=True, env=env) - out = strip_ansi(proc.stdout) - assert "Value of RHIZA_VERSION:\n0.10.2" in out - - def test_rhiza_version_used_in_sync_target(self, logger): - """Sync target should use RHIZA_VERSION from .rhiza-version.""" - proc = run_make(logger, ["sync"]) - out = proc.stdout - # Check that rhiza>= is used with the version variable - assert 'uvx "rhiza>=' in out or "rhiza>=" in out - - def test_rhiza_version_used_in_validate_target(self, logger): - """Validate target should use RHIZA_VERSION from .rhiza-version.""" - proc = run_make(logger, ["validate"]) - out = proc.stdout - # Check that rhiza>= is used with the version variable - assert 'uvx "rhiza>=' in out or "rhiza>=" in out - - -class TestSummariseSync: - """Tests for the summarise-sync Makefile target.""" - - def test_summarise_sync_target_exists(self, logger): - """The summarise-sync target should be available.""" - proc = run_make(logger, ["help"]) - out = proc.stdout - # Check that summarise-sync appears in help - assert "summarise-sync" in out - - def test_summarise_sync_dry_run(self, logger): - """Summarise-sync target should invoke rhiza summarise in dry-run output.""" - proc = run_make(logger, ["summarise-sync"]) - out = proc.stdout - # Check for uvx command with rhiza summarise - assert "uvx" in out - assert "rhiza" in out - assert "summarise" in out - - def test_summarise_sync_uses_rhiza_version(self, logger): - """Summarise-sync target should use RHIZA_VERSION from .rhiza-version.""" - proc = run_make(logger, ["summarise-sync"]) - out = proc.stdout - # Check that rhiza>= is used with the version - assert 'uvx "rhiza>=' in out or "rhiza>=" in out - - def test_summarise_sync_skips_in_rhiza_repo(self, logger): - """Summarise-sync target should skip execution in rhiza repository.""" - # setup_rhiza_git_repo() is already called by fixture - - proc = run_make(logger, ["summarise-sync"], dry_run=True) - # Should succeed but skip the actual summarise - assert proc.returncode == 0 - # Verify the skip message is in the output - assert "Skipping summarise-sync in rhiza repository" in proc.stdout - - def test_summarise_sync_requires_install_uv(self, logger): - """Summarise-sync should ensure uv is installed first.""" - proc = run_make(logger, ["summarise-sync"]) - out = proc.stdout - # The output should show that install-uv is called - # This might be implicit via the dependency chain - assert "rhiza" in out - - def test_workflow_uvx_command_format(self, logger): - """Test that the uvx command format matches workflow expectations.""" - # This test validates the command format used in both Makefile and workflow - proc = run_make(logger, ["sync"]) - out = proc.stdout - - # The format should be: uvx "rhiza>=VERSION" materialize --force . - assert 'uvx "rhiza>=' in out - assert "materialize --force" in out - - def test_workflow_summarise_command_format(self, logger): - """Test that the summarise command format matches workflow expectations.""" - proc = run_make(logger, ["summarise-sync"]) - out = proc.stdout - - # The format should be: uvx "rhiza>=VERSION" summarise . - assert 'uvx "rhiza>=' in out - assert "summarise" in out diff --git a/.rhiza/tests/test_utils.py b/.rhiza/tests/test_utils.py deleted file mode 100644 index d90db5e..0000000 --- a/.rhiza/tests/test_utils.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Shared test utilities. - -Helper functions used across the test suite. Extracted from conftest.py to avoid -relative imports and __init__.py requirements in test directories. - -This file and its associated utilities flow down via a SYNC action from the -jebel-quant/rhiza repository (https://github.com/jebel-quant/rhiza). -""" - -import re -import shutil -import subprocess # nosec B404 - -# Get absolute paths for executables to avoid S607 warnings -GIT = shutil.which("git") or "/usr/bin/git" -MAKE = shutil.which("make") or "/usr/bin/make" - - -def strip_ansi(text: str) -> str: - """Strip ANSI escape sequences from text.""" - ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") - return ansi_escape.sub("", text) - - -def run_make( - logger, args: list[str] | None = None, check: bool = True, dry_run: bool = True, env: dict[str, str] | None = None -) -> subprocess.CompletedProcess: - """Run `make` with optional arguments and return the completed process. - - Args: - logger: Logger used to emit diagnostic messages during the run - args: Additional arguments for make - check: If True, raise on non-zero return code - dry_run: If True, use -n to avoid executing commands - env: Optional environment variables to pass to the subprocess - """ - cmd = [MAKE] - if args: - cmd.extend(args) - # Use -s to reduce noise, -n to avoid executing commands - flags = "-sn" if dry_run else "-s" - cmd.insert(1, flags) - logger.info("Running command: %s", " ".join(cmd)) - result = subprocess.run(cmd, capture_output=True, text=True, env=env) # nosec B603 - logger.debug("make exited with code %d", result.returncode) - if result.stdout: - logger.debug("make stdout (truncated to 500 chars):\n%s", result.stdout[:500]) - if result.stderr: - logger.debug("make stderr (truncated to 500 chars):\n%s", result.stderr[:500]) - if check and result.returncode != 0: - msg = f"make failed with code {result.returncode}:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}" - raise AssertionError(msg) - return result - - -def setup_rhiza_git_repo(): - """Initialize a git repository and set remote to rhiza.""" - subprocess.run([GIT, "init"], check=True, capture_output=True) # nosec B603 - subprocess.run( # nosec B603 - [GIT, "remote", "add", "origin", "https://github.com/jebel-quant/rhiza"], - check=True, - capture_output=True, - ) diff --git a/.rhiza/tests/utils/test_git_repo_fixture.py b/.rhiza/tests/utils/test_git_repo_fixture.py deleted file mode 100644 index f8165b3..0000000 --- a/.rhiza/tests/utils/test_git_repo_fixture.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Tests for the git_repo pytest fixture that creates a mock Git repository. - -This file and its associated tests flow down via a SYNC action from the jebel-quant/rhiza repository -(https://github.com/jebel-quant/rhiza). - -This module validates the temporary repository structure, git initialization, -mocked tool executables, environment variables, and basic git configuration the -fixture is expected to provide for integration-style tests. -""" - -import os -import shutil -import subprocess -from pathlib import Path - -# Get absolute path for git to avoid S607 warnings -GIT = shutil.which("git") or "/usr/bin/git" - - -class TestGitRepoFixture: - """Tests for the git_repo fixture that sets up a mock git repository.""" - - def test_git_repo_creates_temporary_directory(self, git_repo): - """Git repo fixture should create a temporary directory.""" - assert git_repo.exists() - assert git_repo.is_dir() - - def test_git_repo_contains_pyproject_toml(self, git_repo): - """Git repo should contain a pyproject.toml file.""" - pyproject = git_repo / "pyproject.toml" - assert pyproject.exists() - content = pyproject.read_text() - assert 'name = "test-project"' in content - assert 'version = "0.1.0"' in content - - def test_git_repo_contains_uv_lock(self, git_repo): - """Git repo should contain a uv.lock file.""" - assert (git_repo / "uv.lock").exists() - - def test_git_repo_has_bin_directory_with_mocks(self, git_repo): - """Git repo should have bin directory with mock tools.""" - bin_dir = git_repo / "bin" - assert bin_dir.exists() - assert (bin_dir / "uv").exists() - - def test_git_repo_mock_tools_are_executable(self, git_repo): - """Mock tools should be executable.""" - for tool in ["uv"]: - tool_path = git_repo / "bin" / tool - assert os.access(tool_path, os.X_OK), f"{tool} is not executable" - - def test_git_repo_is_initialized(self, git_repo): - """Git repo should be properly initialized.""" - result = subprocess.run( - [GIT, "rev-parse", "--git-dir"], - cwd=git_repo, - capture_output=True, - text=True, - ) - assert result.returncode == 0 - assert ".git" in result.stdout - - def test_git_repo_has_master_branch(self, git_repo): - """Git repo should be on master branch.""" - result = subprocess.run( - [GIT, "branch", "--show-current"], - cwd=git_repo, - capture_output=True, - text=True, - ) - assert result.returncode == 0 - assert result.stdout.strip() == "master" - - def test_git_repo_has_initial_commit(self, git_repo): - """Git repo should have an initial commit.""" - result = subprocess.run( - [GIT, "log", "--oneline"], - cwd=git_repo, - capture_output=True, - text=True, - ) - assert result.returncode == 0 - assert "Initial commit" in result.stdout - - def test_git_repo_has_remote_configured(self, git_repo): - """Git repo should have origin remote configured.""" - result = subprocess.run( - [GIT, "remote", "-v"], - cwd=git_repo, - capture_output=True, - text=True, - ) - assert result.returncode == 0 - assert "origin" in result.stdout - - def test_git_repo_user_config_is_set(self, git_repo): - """Git repo should have user.email and user.name configured.""" - email = subprocess.check_output( - [GIT, "config", "user.email"], - cwd=git_repo, - text=True, - ).strip() - name = subprocess.check_output( - [GIT, "config", "user.name"], - cwd=git_repo, - text=True, - ).strip() - assert email == "test@example.com" - assert name == "Test User" - - def test_git_repo_working_tree_is_clean(self, git_repo): - """Git repo should start with a clean working tree.""" - result = subprocess.run( - [GIT, "status", "--porcelain"], - cwd=git_repo, - capture_output=True, - text=True, - ) - assert result.returncode == 0 - assert result.stdout.strip() == "" - - def test_git_repo_changes_current_directory(self, git_repo): - """Git repo fixture should change to the temporary directory.""" - current_dir = Path.cwd() - assert current_dir == git_repo - - def test_git_repo_modifies_path_environment(self, git_repo): - """Git repo fixture should prepend bin directory to PATH.""" - path_env = os.environ.get("PATH", "") - bin_dir = str(git_repo / "bin") - assert bin_dir in path_env - assert path_env.startswith(bin_dir) diff --git a/book/marimo/notebooks/rhiza.py b/book/marimo/notebooks/rhiza.py deleted file mode 100644 index 8167c6c..0000000 --- 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/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index fd786fb..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,81 +0,0 @@ -# ================================ -# Build Arguments -# ================================ -# Python version from .python-version (single source of truth) -# MUST be passed via --build-arg PYTHON_VERSION=$(cat .python-version) -# Docker cannot read files before FROM, so the build process must pass this. -# Use: make docker-build (reads .python-version automatically) -ARG PYTHON_VERSION=3.12 - -# ================================ -# Stage 1: Builder -# ================================ -FROM ghcr.io/astral-sh/uv:0.9.24-python${PYTHON_VERSION}-bookworm-slim AS builder - -WORKDIR /app - -# Create non-root user -RUN useradd -m app_user && chown app_user:app_user /app - -# Use non-root user -USER app_user - -# Copy application code if the folder src exists -COPY --chown=app_user:app_user pyproject.toml uv.lock README.md src* ./ - -# Install dependencies into .venv (no dev deps) -RUN uv sync --frozen --no-dev --no-cache && \ - # Install the package itself (non-editable, so src/ is not required afterwards) \ - uv pip install --no-deps . - -# Remove source to shrink image -RUN rm -rf ./src/ ~/.cache/ && \ - rm -rf /app/.venv/CACHEDIR.TAG && \ - rm -rf /app/.venv/bin/activate* && \ - rm -rf /app/.venv/bin/deactivate.bat && \ - rm -rf /app/.venv/bin/pydoc.bat && \ - rm -rf /app/.venv/.gitignore /app/.venv/.lock && \ - find /app/.venv -path "*/site-packages/*dist-info" -type d -exec rm -rf {} + && \ - find /app/.venv -path "*/site-packages/*egg-info" -type d -exec rm -rf {} + && \ - find /app/.venv -name "_virtualenv.*" -delete && \ - find /app/.venv -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true && \ - find /app/.venv -name "*.pyc" -delete - - -# ================================ -# Stage 2: Runtime -# ================================ -FROM python:${PYTHON_VERSION}-slim AS runtime - -WORKDIR /app - -# Install small system tools and update packages to fix security vulnerabilities -# Fixes CVE-2025-38248, CVE-2025-40170, CVE-2025-68800, CVE-2025-68811, CVE-2025-71085 -# CVE-2025-71116, CVE-2026-22984, CVE-2026-22990, CVE-2026-23001, CVE-2026-23010 -# CVE-2026-23050, CVE-2026-23054, CVE-2026-23074, CVE-2026-23084, CVE-2026-23097 -RUN apt-get update && \ - apt-get install -y --no-install-recommends nano=8.* tree=2.* && \ - apt-get upgrade -y linux-libc-dev && \ - rm -rf /var/lib/apt/lists/* - -# Update pip and setuptools to fix security vulnerabilities -# CVE-2025-8869 (pip: missing checks on symbolic link extraction) -# CVE-2026-1703 (pip: information disclosure via path traversal) -# CVE-2026-23949 (jaraco.context: path traversal) -# CVE-2026-24049 (wheel: privilege escalation) -RUN pip install --no-cache-dir --upgrade pip==26.0 && \ - pip install --no-cache-dir setuptools==82.0.0 - -# Create non-root user -RUN useradd -m app_user && chown app_user:app_user /app - -# Copy stripped .venv and app from builder -COPY --from=builder --chown=app_user:app_user /app/.venv /app/.venv - -# Set virtual environment PATH -ENV PATH="/app/.venv/bin:$PATH" -ENV PYTHONDONTWRITEBYTECODE=1 - -# Switch to non-root -USER app_user - diff --git a/docker/Dockerfile.dockerignore b/docker/Dockerfile.dockerignore deleted file mode 100644 index 18b491a..0000000 --- a/docker/Dockerfile.dockerignore +++ /dev/null @@ -1,17 +0,0 @@ -# This Dockerfile.dockerignore file is located in the docker/ folder for better organization. -# A symlink at the repository root (Dockerfile.dockerignore -> docker/Dockerfile.dockerignore) allows -# Docker to find it when building with the root directory as the build context. -# See: https://stackoverflow.com/questions/28097064/dockerignore-ignore-everything-except-a-file-and-the-dockerfile - -# Ignore Everything -** - -# but don't ignore those files/folders -!pyproject.toml -!uv.lock -!README.md -!src/ - -# In those folders make sure those files are not included -**/__pycache__ -**/*.pyc diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index d82d333..0000000 --- a/pytest.ini +++ /dev/null @@ -1,14 +0,0 @@ -[pytest] -testpaths = tests -# Enable live logs on console -log_cli = true -# Show DEBUG+ messages -log_cli_level = DEBUG -log_cli_format = %(asctime)s %(levelname)s %(name)s: %(message)s -log_cli_date_format = %H:%M:%S -# Show extra summary info for skipped/failed tests -addopts = -ra -# Register custom markers -markers = - stress: marks tests as stress tests (deselect with '-m "not stress"') - property: marks tests as property-based tests diff --git a/ruff.toml b/ruff.toml deleted file mode 100644 index e5b3910..0000000 --- a/ruff.toml +++ /dev/null @@ -1,130 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Maximum line length for the entire project -line-length = 120 -# Target Python version -target-version = "py311" - -# Exclude directories with Jinja template variables in their names -exclude = ["**/[{][{]*/", "**/*[}][}]*/"] - -[lint] -# Available rule sets in Ruff: -# A: flake8-builtins - Check for python builtins being used as variables or parameters -# B: flake8-bugbear - Find likely bugs and design problems -# C4: flake8-comprehensions - Helps write better list/set/dict comprehensions -# D: pydocstyle - Check docstring style -# E: pycodestyle errors - PEP 8 style guide -# ERA: eradicate - Find commented out code -# F: pyflakes - Detect logical errors -# I: isort - Sort imports -# N: pep8-naming - Check PEP 8 naming conventions -# PT: flake8-pytest-style - Check pytest best practices -# RUF: Ruff-specific rules -# S: flake8-bandit - Find security issues -# SIM: flake8-simplify - Simplify code -# T10: flake8-debugger - Check for debugger imports and calls -# UP: pyupgrade - Upgrade syntax for newer Python -# W: pycodestyle warnings - PEP 8 style guide warnings -# ANN: flake8-annotations - Type annotation checks -# ARG: flake8-unused-arguments - Unused arguments -# BLE: flake8-blind-except - Check for blind except statements -# COM: flake8-commas - Trailing comma enforcement -# DTZ: flake8-datetimez - Ensure timezone-aware datetime objects -# EM: flake8-errmsg - Check error message strings -# FBT: flake8-boolean-trap - Boolean argument checks -# ICN: flake8-import-conventions - Import convention enforcement -# ISC: flake8-implicit-str-concat - Implicit string concatenation -# NPY: NumPy-specific rules -# PD: pandas-specific rules -# PGH: pygrep-hooks - Grep-based checks -# PIE: flake8-pie - Miscellaneous rules -# PL: Pylint rules -# Q: flake8-quotes - Quotation style enforcement -# RSE: flake8-raise - Raise statement checks -# RET: flake8-return - Return statement checks -# SLF: flake8-self - Check for self references -# TCH: flake8-type-checking - Type checking imports -# TID: flake8-tidy-imports - Import tidying -# TRY: flake8-try-except-raise - Try/except/raise checks -# YTT: flake8-2020 - Python 2020+ compatibility - -select = [ - "D", # pydocstyle - Check docstring style - "E", # pycodestyle errors - PEP 8 style guide - "F", # pyflakes - Detect logical errors - "I", # isort - Sort imports - "N", # pep8-naming - Check PEP 8 naming conventions - "W", # pycodestyle warnings - PEP 8 style guide warnings - "UP", # pyupgrade - Upgrade syntax for newer Python -] - -# Ensure docstrings are required for magic methods (e.g., __init__) and -# private/underscored modules by explicitly selecting the relevant pydocstyle -# rules in addition to the D set. This makes the intent clear and future-proof -# if the default pydocstyle selection changes. -extend-select = [ - "D105", # pydocstyle - Require docstrings for magic methods - "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 - "PT", # flake8-pytest-style - Check pytest best practices - "RUF", # Ruff-specific rules - "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 - "ICN", # flake8-import-conventions - Import convention enforcement - #"PIE", # flake8-pie - Miscellaneous rules - #"PL", # Pylint rules -] - -# Resolve incompatible pydocstyle rules: prefer D211 and D212 over D203 and D213 -ignore = [ - "D203", # one-blank-line-before-class (conflicts with D211) - "D213", # multi-line-summary-second-line (conflicts with D212) -] - -[lint.pydocstyle] -convention = "google" - -# Formatting configuration -[format] -# Use double quotes for strings -quote-style = "double" -# Use spaces for indentation -indent-style = "space" -# Automatically detect and use the appropriate line ending -line-ending = "auto" - -# File-specific rule exceptions -[lint.per-file-ignores] -# 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 -] -"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 -"**/marimo/**/*.py" = [ - "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 in utility scripts - "TRY003", # Allow long exception messages in utility scripts -] diff --git a/tests/benchmarks/conftest.py b/tests/benchmarks/conftest.py deleted file mode 100644 index a3c07de..0000000 --- a/tests/benchmarks/conftest.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Pytest configuration for benchmark tests. - -This file can be used to add custom fixtures or configuration -for your benchmark tests. -""" diff --git a/tests/benchmarks/test_benchmarks.py b/tests/benchmarks/test_benchmarks.py deleted file mode 100644 index e7a6f0f..0000000 --- a/tests/benchmarks/test_benchmarks.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Example benchmark tests. - -This file contains simple example benchmark tests that demonstrate -how to use pytest-benchmark. These are placeholder tests that you -should replace with your own meaningful benchmarks. - -Uses pytest-benchmark to measure and compare execution times. -""" - -from __future__ import annotations - - -class TestExampleBenchmarks: - """Example benchmark tests demonstrating basic usage.""" - - def test_string_concatenation(self, benchmark): - """Example: Benchmark string concatenation.""" - - def concatenate_strings(): - result = "" - for i in range(100): - result += str(i) - return result - - result = benchmark(concatenate_strings) - assert len(result) > 0 - - def test_list_comprehension(self, benchmark): - """Example: Benchmark list comprehension.""" - - def create_list(): - return [i * 2 for i in range(1000)] - - result = benchmark(create_list) - assert len(result) == 1000 - - def test_dictionary_operations(self, benchmark): - """Example: Benchmark dictionary operations.""" - - def dictionary_ops(): - data = {} - for i in range(100): - data[f"key_{i}"] = i * 2 - return sum(data.values()) - - result = benchmark(dictionary_ops) - assert result > 0 - - def test_simple_computation(self, benchmark): - """Example: Benchmark simple computation.""" - - def compute_sum(): - total = 0 - for i in range(1000): - total += i - return total - - result = benchmark(compute_sum) - assert result == sum(range(1000)) diff --git a/tests/property/test_makefile_properties.py b/tests/property/test_makefile_properties.py deleted file mode 100644 index 8d32a3c..0000000 --- a/tests/property/test_makefile_properties.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Property-based tests using Hypothesis. - -This module currently exercises generic Python behavior (for example, list sorting) -rather than any project Makefile targets or operations. - -Uses Hypothesis to generate test cases that verify behavior across a wide range of inputs. -""" - -from __future__ import annotations - -import itertools -from collections import Counter - -import pytest -from hypothesis import given -from hypothesis import strategies as st - - -@pytest.mark.property -@given(st.lists(st.integers() | st.floats(allow_nan=False, allow_infinity=False))) -def test_sort_correctness_using_properties(lst): - """Verify that sorted() correctly orders lists and preserves all elements.""" - result = sorted(lst) - # Use Counter to ensure multiplicities (duplicates) are preserved - assert Counter(lst) == Counter(result) - assert all(a <= b for a, b in itertools.pairwise(result)) From b97f7fadc76bdb587b1feadc885c52c358679ce3 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 12:47:05 +0400 Subject: [PATCH 06/31] Delete .claude directory --- .claude/plan.md | 340 ----------------------------------- .claude/quality.md | 431 --------------------------------------------- 2 files changed, 771 deletions(-) delete mode 100644 .claude/plan.md delete mode 100644 .claude/quality.md diff --git a/.claude/plan.md b/.claude/plan.md deleted file mode 100644 index b1abb10..0000000 --- a/.claude/plan.md +++ /dev/null @@ -1,340 +0,0 @@ -# Rhiza Quality Improvement Plan: Path to 10/10 - -**Current Score**: 9.9/10 -**Target Score**: 10/10 -**Date**: 2026-02-15 -**Last Updated**: 2026-02-15 - ---- - -## Executive Summary - -This plan outlines the roadmap to achieve a perfect 10/10 quality score across all categories. Currently, 4 categories need improvement: - -| Category | Current | Target | Gap | Priority | Status | -|----------|---------|--------|-----|----------|--------| -| Security | 9.5/10 | 10/10 | 0.5 | High | In Progress | -| Architecture | 10/10 | 10/10 | 0.0 | - | ✅ **COMPLETED** | -| Developer Experience | 10/10 | 10/10 | 0.0 | - | ✅ **COMPLETED** | -| Maintainability | 9/10 | 10/10 | 1.0 | Medium | Pending | -| Shell Scripts | 9.5/10 | 10/10 | 0.5 | Low | Pending | - -**Estimated Timeline**: 3-4 weeks -**Estimated Effort**: 40-50 hours -**Progress**: 30 hours completed (Architecture: 9h, Developer Experience: 21h) -**Remaining**: 20-25 hours - ---- - -## 1. Security: 9.5/10 → 10/10 - -**Current Weakness**: Some bandit rules disabled in tests (S101 for asserts, S603 for subprocess - both acceptable in test context) - -### Strategy -While the disabled rules are contextually appropriate for tests, we can demonstrate even stronger security posture by: - -1. **Add explicit security justification comments** in test files -2. **Implement security-focused test cases** to validate that disabled rules don't mask real issues -3. **Create security testing documentation** explaining the rationale for test exceptions - -### Action Items - -| Task | Description | Effort | Impact | -|------|-------------|--------|--------| -| Document security exceptions | Add inline comments in conftest.py and test files explaining why S101/S603/S607 are safe in test context | 2h | High | -| Add security test suite | Create `tests/security/test_security_patterns.py` to validate no real security issues exist | 4h | High | -| Security testing guide | Add `docs/SECURITY_TESTING.md` documenting our security testing approach | 2h | Medium | -| SAST baseline | Generate and commit a bandit baseline file showing zero findings in production code | 1h | Medium | - -**Total Effort**: 9 hours -**Success Criteria**: Security score reaches 10/10 by demonstrating comprehensive security testing approach - ---- - -## 2. Architecture: 9/10 → 10/10 ✅ **COMPLETED** - -**Previous Weakness**: Deep directory nesting in some areas (`.rhiza/make.d/`, `.rhiza/utils/`) - -### Strategy -The directory nesting serves a functional purpose (modularity), but we can improve discoverability and documentation. - -### Action Items - -| Task | Description | Effort | Impact | Status | -|------|-------------|--------|--------|--------| -| Architecture visualization | Create Mermaid diagram showing `.rhiza/` directory structure and dependencies | 3h | High | ✅ Done (#694) | -| Navigation aids | Add README.md files in `.rhiza/make.d/` and `.rhiza/utils/` explaining organization | 2h | High | ✅ Done (#694) | -| Naming conventions guide | Document the naming and organization patterns in `docs/ARCHITECTURE.md` | 2h | Medium | ✅ Done (#694) | -| Index file | Create `.rhiza/INDEX.md` as a quick reference to all utilities and makefiles | 2h | Medium | ✅ Done (#694) | - -**Total Effort**: 9 hours (Completed) -**Success Criteria**: ✅ Architecture score reached 10/10 by improving navigability without changing structure - -**Completed Deliverables** (PR #694): -- **8 comprehensive Mermaid diagrams** in docs/ARCHITECTURE.md: - - System overview and component interactions - - Makefile hierarchy and auto-loading - - Hook system and extension points - - Release pipeline and workflow triggers - - Template sync flow - - Directory structure with dependencies - - .rhiza/ internal organization - - CI/CD workflow triggers and Python execution model -- **Comprehensive naming conventions** (330+ lines in docs/ARCHITECTURE.md): - - Makefile naming conventions (lowercase-with-hyphens) - - Target naming patterns (verb-noun format) - - Variable naming (SCREAMING_SNAKE_CASE) - - Hook naming (pre-/post- with double-colons) - - Documentation naming (SCREAMING_SNAKE_CASE.md) - - Workflow naming (rhiza_feature.yml) - - Template bundle naming -- **Quick reference index** (.rhiza/INDEX.md - 155 lines): - - Complete directory structure overview - - Makefile catalog with sizes and purposes - - Requirements files reference - - Test suite organization - - Key make targets - - Template bundles reference -- **Makefile cookbook** (.rhiza/make.d/README.md - 127 lines): - - 5 copy-paste recipes for common tasks - - Hook usage examples - - Customization patterns - - File organization reference - -**Alternative Strategy** (if restructuring is preferred): -- Flatten `.rhiza/utils/` → `.rhiza/scripts/` with clearer naming -- Consolidate `.rhiza/make.d/*.mk` into fewer, more cohesive modules -- **Effort**: 15-20 hours | **Risk**: Higher (requires testing all make targets) - ---- - -## 3. Developer Experience: 9/10 → 10/10 ✅ **COMPLETED** - -**Previous Weaknesses**: -- Learning curve for .rhiza/make.d/ extension system -- Multiple tools to understand (uv, make, git) - -### Strategy -Improve onboarding and provide better tooling support. - -### Action Items - -| Task | Description | Effort | Impact | Status | -|------|-------------|--------|--------|--------| -| Interactive tutorial | Create `make tutorial` target with guided walkthrough of key features | 4h | High | ✅ Done (#696) | -| Tool cheat sheet | Add `docs/TOOLS_REFERENCE.md` with quick reference for uv, make, git commands | 3h | High | ✅ Done (#696) | -| Extension system guide | Create `docs/EXTENDING_RHIZA.md` with examples and best practices | 4h | High | ✅ Done (#696) | -| Makefile autocomplete | Add shell completion scripts for bash/zsh (complete make targets) | 4h | Medium | ✅ Done (#696) | -| VSCode extensions documentation | Document all 11 VSCode extensions in devcontainer | 3h | High | ✅ Done (#690) | -| Dependency version rationale | Document rationale for each dependency constraint | 3h | High | ✅ Done (#687) | -| VSCode tasks integration | Enhance `.vscode/tasks.json` with all common make targets | 2h | Medium | Deferred | -| Video walkthrough | Record 5-minute quickstart video (screen capture with voiceover) | 3h | Medium | Deferred | -| IntelliJ run configurations | Add `.idea/runConfigurations/` XML files for common tasks | 2h | Low | Deferred | - -**Total Effort**: 28 hours (21h completed, 7h deferred) -**Success Criteria**: ✅ Developer Experience reached 10/10 with comprehensive onboarding and tooling support - -**Completed Deliverables** (PR #696, #694, #690, #687): -- ✅ **Interactive tutorial system** (PR #696 - tutorial.mk, 101 lines): - - 10 comprehensive lessons covering essential concepts - - Step-by-step walkthrough with hands-on exercises - - Covers project structure, template sync, customization, hooks, workflows - - Color-coded output with clear progression -- ✅ **Shell completion system** (PR #696 - .rhiza/completions/, 398 lines): - - Bash completion (47 lines) with auto-discovery - - Zsh completion (88 lines) with target descriptions - - Comprehensive setup guide (263 lines) - - Completes targets and common make variables -- ✅ **Tools reference guide** (PR #696 - docs/TOOLS_REFERENCE.md, 820 lines): - - Essential commands quick reference - - Comprehensive make, uv, and git command catalog - - Testing, quality, and documentation workflows - - Release management and troubleshooting -- ✅ **Extension guide** (PR #696 - docs/EXTENDING_RHIZA.md, 915 lines): - - 8 available hooks with detailed use cases - - Custom target patterns and examples - - Variable and environment customization - - 20+ real-world examples with best practices -- ✅ **VSCode extensions documentation** (PR #690 - docs/VSCODE_EXTENSIONS.md, 215 lines): - - Detailed description of all 11 pre-configured extensions - - Purpose, features, and rationale for each extension - - Integration and usage tips -- ✅ **Dependency version rationale** (PR #687 - docs/DEPENDENCIES.md, 222 lines): - - Philosophy behind version constraints - - Detailed rationale for each dependency - - Security, stability, and compatibility considerations -- ✅ **Makefile cookbook** (PR #694 - .rhiza/make.d/README.md, 127 lines): - - Copy-paste recipes for common tasks - - Hook usage examples and patterns - ---- - -## 4. Maintainability: 9/10 → 10/10 - -**Current Weakness**: Few TODO comments for roadmap visibility - -### Strategy -Implement a structured approach to tracking technical debt and future improvements. - -### Action Items - -| Task | Description | Effort | Impact | -|------|-------------|--------|--------| -| ROADMAP.md creation | Create comprehensive roadmap document with planned features and improvements | 3h | High | -| TODO scanning automation | Add `make todos` target to search and report all TODO/FIXME/HACK comments | 2h | High | -| Technical debt tracking | Create `docs/TECHNICAL_DEBT.md` documenting known limitations and future work | 3h | High | -| GitHub project board | Set up project board for tracking enhancements and roadmap items | 2h | Medium | -| Changelog automation | Enhance changelog generation with PR categorization and automatic updates | 3h | Medium | - -**Total Effort**: 13 hours -**Success Criteria**: Maintainability reaches 10/10 with clear roadmap visibility and technical debt tracking - ---- - -## 5. Shell Scripts: 9.5/10 → 10/10 - -**Current Weakness**: Errors cause immediate exit vs. offering recovery options (by design for automation) - -### Strategy -While `set -euo pipefail` is best practice for automation, we can add graceful degradation where appropriate. - -### Action Items - -| Task | Description | Effort | Impact | -|------|-------------|--------|--------| -| Add recovery options | Enhance `.devcontainer/bootstrap.sh` with fallback options for failed installations | 3h | Medium | -| Dry-run mode | Add `--dry-run` flag to session hooks for testing without side effects | 2h | Medium | -| Error messaging improvements | Add more descriptive error messages with suggested remediation steps | 2h | High | -| Shell script testing | Add `tests/shell/test_scripts.sh` with bats-core for shell script unit tests | 4h | High | -| Shell documentation | Create `docs/SHELL_SCRIPTS.md` documenting each script's purpose and error handling | 2h | Medium | - -**Total Effort**: 13 hours -**Success Criteria**: Shell Scripts reach 10/10 with improved error recovery and comprehensive testing - ---- - -## Implementation Plan - -### Phase 1: Quick Wins (Week 1) - 20 hours ✅ **PARTIALLY COMPLETED** -Focus on documentation and low-hanging fruit: -- ✅ **Security exception documentation** - Not yet done -- ✅ **Architecture navigation aids** - COMPLETED (#694: .rhiza/make.d/README.md, .rhiza/INDEX.md) -- ✅ **Architecture visualization** - COMPLETED (#694: 8 Mermaid diagrams in docs/ARCHITECTURE.md) -- ✅ **Naming conventions guide** - COMPLETED (#694: comprehensive guide in docs/ARCHITECTURE.md) -- ✅ **VSCode extensions documentation** - COMPLETED (#690: docs/VSCODE_EXTENSIONS.md) -- ✅ **Dependency version rationale** - COMPLETED (#687: docs/DEPENDENCIES.md) -- ⏳ Tool cheat sheet - Not yet done -- ⏳ ROADMAP.md creation - Not yet done -- ⏳ TODO scanning automation - Not yet done -- ⏳ Error messaging improvements - Not yet done - -**Expected Score After Phase 1**: 9.8/10 -**Actual Score**: 9.8/10 ✅ **ACHIEVED** - -### Phase 2: Enhanced Tooling (Week 2) - 15 hours ✅ **COMPLETED** -Improve developer experience: -- ✅ **Interactive tutorial** (`make tutorial`) - COMPLETED (#696) -- ✅ **Extension system guide** - COMPLETED (#696: docs/EXTENDING_RHIZA.md) -- ✅ **Tools reference** - COMPLETED (#696: docs/TOOLS_REFERENCE.md) -- ✅ **Shell completions** - COMPLETED (#696: bash + zsh) -- ⏳ VSCode tasks integration - Deferred -- ⏳ Shell script testing - Pending - -**Expected Score After Phase 2**: 9.9/10 -**Actual Score**: 9.9/10 ✅ **ACHIEVED** - -### Phase 3: Polish & Validation (Week 3) - 15 hours -Complete remaining items: -- ✅ Security test suite -- ✅ Architecture visualization -- ✅ Technical debt tracking -- ✅ Shell script dry-run mode -- ✅ Video walkthrough - -**Expected Score After Phase 3**: 10/10 - ---- - -## Risk Assessment - -| Risk | Impact | Likelihood | Mitigation | -|------|--------|------------|------------| -| Time overrun due to scope creep | Medium | Medium | Stick to defined action items, track hours | -| Breaking changes during refactoring | High | Low | Comprehensive testing before/after changes | -| Community resistance to changes | Low | Low | Document rationale, maintain backward compatibility | -| Insufficient testing of new features | Medium | Low | Add tests for all new documentation/tooling | - ---- - -## Success Metrics - -### Quantitative -- ✅ Security score: 10/10 -- ✅ Architecture score: 10/10 -- ✅ Developer Experience score: 10/10 -- ✅ Maintainability score: 10/10 -- ✅ Shell Scripts score: 10/10 -- ✅ Overall score: 10/10 - -### Qualitative -- ✅ Onboarding time for new contributors reduced by 50% -- ✅ Zero confusion about directory structure (based on contributor feedback) -- ✅ Clear roadmap visibility for all stakeholders -- ✅ Comprehensive security testing documentation -- ✅ Enhanced shell script error handling with recovery options - ---- - -## Resources Required - -- **Developer Time**: 66 hours (split across 3-4 weeks) -- **Tools**: bats-core (shell testing), screen recording software -- **Review Time**: 4-6 hours for code review and documentation review - ---- - -## Conclusion - -This plan provides a clear path to achieving a perfect 10/10 quality score. The improvements focus on: - -1. **Documentation** - Making the existing excellence more visible and accessible -2. **Developer Experience** - Reducing onboarding friction and improving tooling -3. **Transparency** - Clear roadmap and technical debt tracking -4. **Robustness** - Enhanced error handling and security testing - -The repository has progressed from 9.7/10 to **9.8/10** (enterprise-grade), with Architecture reaching a perfect 10/10 score. These improvements represent the final polish to reach perfection. All enhancements maintain backward compatibility and build on the existing solid foundation. - -## Progress Update (2026-02-15) - -### Major Achievements ✅ - -1. **Architecture: 9/10 → 10/10** (PR #694) ✅ **PERFECT SCORE** - - 8 comprehensive Mermaid diagrams in docs/ARCHITECTURE.md - - Complete naming conventions guide (330+ lines) - - .rhiza/INDEX.md quick reference (155 lines) - - .rhiza/make.d/README.md cookbook (127 lines) - -2. **Developer Experience: 9/10 → 10/10** (PR #696, #694, #690, #687) ✅ **PERFECT SCORE** - - Interactive tutorial system (tutorial.mk, 101 lines) - - Shell completions for bash and zsh (398 lines total) - - Tools reference guide (docs/TOOLS_REFERENCE.md, 820 lines) - - Extension guide (docs/EXTENDING_RHIZA.md, 915 lines) - - VSCode extensions documented (docs/VSCODE_EXTENSIONS.md, 215 lines) - - Dependency version rationale (docs/DEPENDENCIES.md, 222 lines) - - Makefile cookbook (.rhiza/make.d/README.md, 127 lines) - -3. **Overall Score: 9.7/10 → 9.8/10 → 9.9/10** - - 30 hours of planned work completed (60% of total plan) - - Two categories achieved perfect 10/10 scores - - Only 2 categories remaining below 10/10 - -### Remaining Work - -To reach 10/10, focus on: -- **Security**: Document exceptions, add security test suite (9h) -- **Maintainability**: ROADMAP.md, TODO tracking, technical debt documentation (13h) -- **Shell Scripts**: Recovery options, dry-run mode, comprehensive testing (13h) - -**Total Remaining**: ~35 hours across 3 categories - -**Next Steps**: Continue with Phase 3 (Polish & Validation) focusing on Security, Maintainability, and Shell Scripts improvements. diff --git a/.claude/quality.md b/.claude/quality.md deleted file mode 100644 index b726509..0000000 --- a/.claude/quality.md +++ /dev/null @@ -1,431 +0,0 @@ -# Repository Quality Scoring - -**Repository**: Rhiza -**Assessment Date**: 2026-02-15 -**Version Analyzed**: 0.7.5 -**Overall Score**: 9.9/10 - ---- - -## Score Summary - -| Category | Score | Weight | Weighted | -|----------|-------|--------|----------| -| Code Quality | 10/10 | 10% | 1.00 | -| Testing | 10/10 | 15% | 1.50 | -| Documentation | 10/10 | 10% | 1.00 | -| CI/CD | 10/10 | 15% | 1.50 | -| Security | 9.5/10 | 10% | 0.95 | -| Architecture | 10/10 | 10% | 1.00 | -| Dependency Management | 10/10 | 10% | 1.00 | -| Developer Experience | 10/10 | 10% | 1.00 | -| Maintainability | 9/10 | 5% | 0.45 | -| Shell Scripts | 9.5/10 | 5% | 0.475 | -| **Overall** | **9.9/10** | 100% | **9.875** | - -**Quality Tier**: Enterprise-Grade / Production-Ready - ---- - -## Detailed Assessment - -### 1. Code Quality: 10/10 - -**Strengths**: -- Comprehensive Ruff configuration with 15 actively enforced rule sets (D, E, F, I, N, W, UP, D105, D107, B, C4, SIM, PT, RUF, S, TRY, ICN) -- **Security (S) and simplicity (SIM) rules now enabled** (PR #678) -- Google-style docstrings enforced via pydocstyle rules with explicit magic method coverage -- Strong type annotations encouraged with `from __future__ import annotations` pattern -- ty type checker integrated for static type analysis (replaced mypy) -- 120-character line length with consistent formatting -- Modern Python syntax enforced via pyupgrade rules (Python 3.11+) -- Import sorting via isort integration -- PEP 8 naming conventions enforced -- **Per-file exceptions refactored to be targeted and justified** (PR #678) - -**Weaknesses**: -- None significant - ---- - -### 2. Testing: 10/10 - -**Strengths**: -- 22 dedicated test files with 121 test functions and methods -- Multiple test types: unit, integration, doctest, README code execution, benchmarks, **property-based tests** -- **Property-based testing with Hypothesis** (tests/property/test_makefile_properties.py) -- Sophisticated fixtures in conftest.py for git repository mocking -- README code blocks validated via test_readme.py -- Release script tested with mock git environments -- Multi-Python version testing (3.11, 3.12, 3.13, 3.14) -- 90% coverage threshold enforced via `--cov-fail-under=90` -- Benchmark regression detection via pytest-benchmark - -**Strengths (continued)**: -- Property-based testing with Hypothesis via `make hypothesis-test` -- Tests in `tests/property/` directory with active `.hypothesis` cache -- Hypothesis strategies for testing across wide range of inputs - -**Weaknesses**: -- No load/stress testing - ---- - -### 3. Documentation: 10/10 - -**Strengths**: -- Comprehensive README.md (18KB) with quick start, features, integration guide -- Architecture documentation with Mermaid diagrams (docs/ARCHITECTURE.md) -- Glossary of terms (docs/GLOSSARY.md) -- Quick reference card (docs/QUICK_REFERENCE.md) -- Customization guide (docs/CUSTOMIZATION.md) -- Release process guide (.rhiza/docs/RELEASING.md) -- Security policy (SECURITY.md) -- Contributing guidelines (CONTRIBUTING.md) -- Code of conduct (CODE_OF_CONDUCT.md) -- Auto-generated API docs via pdoc -- Interactive Marimo notebooks -- Testing documentation (docs/TESTS.md) -- Docker documentation (docs/DOCKER.md) -- Devcontainer documentation (docs/DEVCONTAINER.md) -- Marimo notebooks documentation (docs/MARIMO.md) -- Presentation materials (docs/PRESENTATION.md) -- **GitHub Pages deployment configured** (rhiza_book.yml) with MkDocs Material theme -- **Automated documentation publishing** on every push to main - -**Strengths (continued)**: -- External documentation hosted on GitHub Pages with MkDocs -- Combined documentation site includes API docs (pdoc), coverage reports, test results, and notebooks -- Material for MkDocs theme with dark/light mode toggle -- Automated deployment via rhiza_book.yml workflow - -**Weaknesses**: -- None - ---- - -### 4. CI/CD: 10/10 - -**Strengths**: -- 15 GitHub Actions workflows covering all development phases: - - `rhiza_ci.yml` - Multi-Python testing with dynamic matrix (includes ty type checking) - - `rhiza_codeql.yml` - CodeQL security scanning - - `rhiza_security.yml` - pip-audit + bandit - - `rhiza_deptry.yml` - Dependency hygiene - - `rhiza_pre-commit.yml` - Hook validation - - `rhiza_release.yml` - Multi-phase release pipeline - - `rhiza_benchmarks.yml` - Performance regression detection - - `rhiza_book.yml` - Documentation + GitHub Pages - - `rhiza_docker.yml` - Container building - - `rhiza_devcontainer.yml` - Dev container validation - - `rhiza_marimo.yml` - Notebook validation - - `rhiza_sync.yml` - Template synchronization - - `rhiza_validate.yml` - Structure validation - - `copilot-setup-steps.yml` - Copilot/agentic workflow setup - - `renovate_rhiza_sync.yml` - Automated renovate sync -- OIDC trusted publishing (no stored PyPI credentials) -- Dynamic Python version matrix from pyproject.toml -- Minimal permissions model -- fail-fast: false for complete test coverage - -**Strengths (continued)**: -- Manual approval gates via GitHub environments (`environment: release`) -- Requires explicit approval before PyPI publishing -- Protection against accidental releases - -**Weaknesses**: -- None significant - ---- - -### 5. Security: 9.5/10 - -**Strengths**: -- Comprehensive SECURITY.md with vulnerability reporting process -- Response SLAs defined (48h acknowledgment, 7d assessment, 30d resolution) -- Multiple security scanners: - - CodeQL for semantic analysis - - Bandit for Python security patterns (S rules now enforced) - - pip-audit for dependency vulnerabilities - - actionlint with shellcheck for workflow/script validation - - **Trivy container vulnerability scanning** for Docker images (rhiza_docker.yml) -- **SBOM generation in release workflow** (CycloneDX JSON + XML formats) -- **SBOM attestations** for supply chain transparency (public repos) -- OIDC trusted publishing (no stored credentials) -- SLSA provenance attestations -- Locked dependencies via uv.lock (1013 lines) -- Renovate for automated security updates -- **Environment-based deployment protection** (release environment for PyPI publishing) - -**Strengths (continued)**: -- SBOM generation in both JSON and XML formats using CycloneDX -- SBOM attestation via GitHub's attest-sbom action -- SBOM artifacts uploaded to GitHub releases -- Comprehensive SBOM test suite in `.rhiza/tests/integration/test_sbom.py` -- Container image scanning with Trivy for CRITICAL and HIGH vulnerabilities -- Trivy results uploaded to GitHub Security (SARIF format) and as artifacts -- Vulnerability scanning integrated into rhiza_docker.yml workflow - -**Weaknesses**: -- Some bandit rules disabled in tests (S101 for asserts, S603 for subprocess - both acceptable in test context) - ---- - -### 6. Architecture: 10/10 - -**Strengths**: -- Modular Makefile system (.rhiza/rhiza.mk + .rhiza/make.d/*.mk) -- Extension hooks (pre-install, post-install, pre-release, etc.) -- Clear separation of concerns: - - Core config in .rhiza/ - - Tests in tests/test_rhiza/ - - Docs in book/ and docs/ - - Workflows in .github/workflows/ -- Configuration as code (pyproject.toml, ruff.toml, pytest.ini) -- Minimal root Makefile (12 lines) delegating to .rhiza/rhiza.mk -- Reusable Python utilities in .rhiza/utils/ with proper exception handling -- Unified interface: everything steered through `make` and `uv` commands -- Agentic workflow support with copilot and claude targets -- **Comprehensive architecture documentation** with Mermaid diagrams (docs/ARCHITECTURE.md) -- **Quick reference index** (.rhiza/INDEX.md) cataloging all components -- **Navigation aids** (.rhiza/make.d/README.md) with recipes and patterns -- **Naming conventions guide** documenting all organizational patterns - -**Strengths (continued)**: -- Architecture visualization with 8 detailed Mermaid diagrams: - - System overview and component interactions - - Makefile hierarchy and auto-loading - - Hook system and extension points - - Release pipeline and workflow triggers - - Template sync flow and directory structure - - .rhiza/ internal organization and dependencies -- Comprehensive naming conventions documented: - - Makefile naming (lowercase-with-hyphens) - - Target naming (verb-noun pattern) - - Variable naming (SCREAMING_SNAKE_CASE) - - Hook naming (pre-/post- pattern with double-colons) - - Documentation naming (SCREAMING_SNAKE_CASE.md) - - Workflow naming (rhiza_feature.yml) -- Complete index file (.rhiza/INDEX.md) providing: - - Directory structure overview - - Makefile catalog with sizes and purposes - - Requirements and test suite organization - - Key make targets reference - - Links to all related documentation -- Makefile cookbook (.rhiza/make.d/README.md) with: - - Copy-paste recipes for common tasks - - Hook usage examples - - Customization patterns - - File organization reference - -**Weaknesses**: -- None - ---- - -### 7. Dependency Management: 10/10 - -**Strengths**: -- uv.lock file (1013 lines) ensuring reproducible builds -- Modern uv package manager -- Zero production dependencies (template system only) -- Isolated dev dependencies with strict version bounds: - - marimo>=0.18.0,<1.0 - - numpy>=2.4.0,<3.0 - - plotly>=6.5.0,<7.0 - - pandas>=3,<3.1 -- Deptry integration for dependency hygiene -- Renovate automation for updates (pep621, pre-commit, github-actions, dockerfile) -- Lock file committed for reproducibility -- Python version specified in .python-version and pyproject.toml -- **Daily Renovate schedule** ("every night") ensures prompt security patches and updates -- Dependency version choices documented with clear rationale - -**Weaknesses**: -- None - ---- - -### 8. Developer Experience: 10/10 - -**Strengths**: -- 50+ Makefile targets with auto-generated help -- Single entry point: `make install` and `make help` -- .editorconfig for cross-IDE consistency -- 17 pre-commit hooks for local validation -- GitHub Codespaces support with .devcontainer -- Colored output in scripts (BLUE, RED, YELLOW) -- Quick start guide in README -- UV auto-installation via `make install-uv` -- Agentic workflow integration (make copilot, make claude) -- **Interactive tutorial** (`make tutorial`) - comprehensive guided walkthrough -- **Shell completions** for bash and zsh with target descriptions -- **Tools reference** (docs/TOOLS_REFERENCE.md) - 820-line quick reference guide -- **Extension guide** (docs/EXTENDING_RHIZA.md) - 915-line comprehensive customization guide -- **VSCode extensions fully documented** (docs/VSCODE_EXTENSIONS.md) -- **Dependency version rationale documented** (docs/DEPENDENCIES.md) - -**Strengths (continued)**: -- Interactive tutorial system (tutorial.mk, 101 lines): - - 10 guided lessons covering essential concepts - - Step-by-step walkthrough of key features - - Hands-on exercises and best practices - - Covers structure, sync, customization, and workflows -- Shell completion system (.rhiza/completions/): - - Bash completion (47 lines) with target discovery - - Zsh completion (88 lines) with descriptions - - Auto-discovers targets from all .mk files - - Completes common make variables (DRY_RUN, BUMP, ENV) - - Comprehensive setup guide (263 lines) -- Tools reference guide (docs/TOOLS_REFERENCE.md, 820 lines): - - Essential commands quick reference - - Comprehensive make command catalog - - UV package manager guide - - Git workflows and best practices - - Testing, quality, and documentation commands - - Release management procedures - - AI-powered workflow integration - - Troubleshooting section -- Extension guide (docs/EXTENDING_RHIZA.md, 915 lines): - - 8 available makefile hooks with use cases - - Custom target patterns and examples - - Variable and environment customization - - Template bundle creation - - 20+ real-world examples - - Best practices and troubleshooting -- VSCode devcontainer with 11 pre-configured extensions: - - Python development (ms-python.python, Pylance) - - Marimo notebooks (marimo-team.vscode-marimo, marimo-ai.marimo-vscode) - - Code quality (charliermarsh.ruff, editorconfig.editorconfig, tamasfe.even-better-toml) - - Git integration (mhutchie.git-graph) - - Documentation (bierner.markdown-mermaid, yzhang.markdown-all-in-one) - - Testing (littlefoxteam.vscode-python-test-adapter) -- Comprehensive documentation ecosystem: - - docs/VSCODE_EXTENSIONS.md (215 lines) - extension details - - docs/DEPENDENCIES.md (222 lines) - dependency rationale - - docs/QUICK_REFERENCE.md - command reference - - .rhiza/INDEX.md - component catalog - - .rhiza/make.d/README.md - cookbook with recipes - -**Weaknesses**: -- None - ---- - -### 9. Maintainability: 9/10 - -**Strengths**: -- Descriptive naming (version_matrix.py, check_workflow_names.py) -- Custom exception classes (RhizaError, VersionSpecifierError, PyProjectError) -- Consistent Google-style docstrings with Args, Returns, Raises -- Active maintenance (recent commits within days) -- Semantic commit messages with PR references -- Configuration-driven behavior via template.yml and pyproject.toml -- POSIX-compliant shell scripts validated with shellcheck - -**Weaknesses**: -- Few TODO comments for roadmap visibility - ---- - -### 10. Shell Scripts: 9.5/10 - -**Strengths**: -- Minimal and focused: Only 3 shell scripts (92 total lines) - - `.devcontainer/bootstrap.sh` (44 lines) - environment setup - - `.github/hooks/session-start.sh` (27 lines) - validation hook - - `.github/hooks/session-end.sh` (21 lines) - quality gates hook -- Strict error handling with `set -euo pipefail` (fail on error, undefined variables, pipe failures) -- Proper error handling with meaningful messages -- Well-commented for their complexity level with clear explanations -- Shellcheck validation via actionlint workflow -- Clear, focused responsibilities per script -- Environment variable management with sensible defaults -- Proper PATH handling and binary detection - -**Weaknesses**: -- Errors cause immediate exit vs. offering recovery options (by design for automation) - ---- - -## Improvement Recommendations - -### Completed Improvements ✅ - -| Improvement | Impact | Effort | Status | -|-------------|--------|--------|--------| -| ~~Add SBOM generation to release workflow~~ | Supply chain transparency | Medium | ✅ Done (rhiza_release.yml) | -| Container image scanning for devcontainer | Security completeness | Low | ⏳ Branch exists, needs merge | -| ~~Manual approval gate for PyPI publishing~~ | Release safety | Low | ✅ Environment protection available | - -| Improvement | Status | Implementation Details | -|-------------|--------|----------------------| -| SBOM generation in release workflow | ✅ Complete | CycloneDX JSON/XML with GitHub attestation | -| Container image scanning | ✅ Complete | Trivy scanning in rhiza_docker.yml with SARIF upload | -| Manual approval gate for PyPI publishing | ✅ Complete | GitHub environments with release approval gate | -| Property-based testing with Hypothesis | ✅ Complete | `make hypothesis-test` with tests/property/ directory | -| External documentation hosting | ✅ Complete | GitHub Pages with MkDocs and Material theme | - -### Remaining Opportunities - -### Low Priority - -| Improvement | Impact | Effort | Status | -|-------------|--------|--------|--------| -| ~~VSCode extension documentation~~ | DX improvement | Low | ✅ Done (11 extensions in devcontainer.json + docs/DEVCONTAINER.md) | -| ~~More frequent Renovate schedule~~ | Freshness | Low | ✅ Done (daily "every night") | -| ~~Document dependency version rationale~~ | Clarity | Low | ✅ Done (rationale documented) | - -### Recently Completed (PR #678 and related) - -| Improvement | Impact | Date | -|-------------|--------|------| -| Enable Security (S) linting rules | Code security | 2026-02-15 | -| Enable Simplicity (SIM) linting rules | Code quality | 2026-02-15 | -| Refactor per-file exceptions | Maintainability | 2026-02-15 | -| Add Trivy Docker scanning | Container security | 2026-02-11 | -| SBOM generation with attestations | Supply chain | 2026-02-11 | -| Property-based testing framework | Test depth | 2026-02-11 | -| GitHub Pages documentation | Accessibility | 2026-02-11 | - ---- - -## Conclusion - -Rhiza demonstrates **enterprise-grade engineering** with particular excellence in: - -1. **Automation**: 15 CI/CD workflows, 50+ make targets, 17 pre-commit hooks -2. **Testing**: Comprehensive suite with innovative techniques (README testing, mock git repos, property-based testing with Hypothesis) -3. **Security**: Multi-layer protection with OIDC, CodeQL, bandit, pip-audit, SBOM generation with attestation, Trivy container scanning -4. **Dependency Management**: Zero runtime deps, locked builds, automated updates -5. **Developer Experience**: Unified Makefile interface, sensible defaults, Codespaces support, agentic workflows -6. **Type Safety**: ty type checker integration replacing mypy for modern Python type checking -7. **Documentation**: Comprehensive docs hosted on GitHub Pages with MkDocs, API docs, coverage reports - -**Recent Improvements**: -- All high/medium/low priority recommendations from previous assessment have been completed -- Code Quality score improved from 9/10 to 10/10 (Security and Simplicity linting enabled via PR #678) -- Security score improved from 9/10 to 9.5/10 (SBOM generation with attestation + Trivy container scanning) -- Documentation score improved from 9/10 to 10/10 (GitHub Pages deployment with MkDocs Material theme) -- Shell Scripts score improved from 9/10 to 9.5/10 (verification of minimal, well-documented scripts) -- Architecture score improved from 9/10 to 10/10 (comprehensive documentation with Mermaid diagrams, INDEX.md, naming conventions) -- Developer Experience score improved from 9/10 to 10/10 (interactive tutorial, shell completions, comprehensive guides) -- Overall score improved from 9.4/10 → 9.6/10 → 9.7/10 → 9.8/10 → 9.9/10 - -**Additional Completions**: -- Property-based testing framework with Hypothesis -- Daily Renovate schedule for prompt security patches ("every night") -- Dependency version rationale documented (docs/DEPENDENCIES.md) - #687 -- VSCode extensions fully documented (docs/VSCODE_EXTENSIONS.md) - #690 -- Architecture visualization with 8 Mermaid diagrams (docs/ARCHITECTURE.md) - #694 -- Quick reference index (.rhiza/INDEX.md) - #694 -- Makefile cookbook with recipes (.rhiza/make.d/README.md) - #694 -- Comprehensive naming conventions guide - #694 -- Interactive tutorial system (`make tutorial`) - #696 -- Shell completions for bash and zsh - #696 -- Comprehensive tools reference (docs/TOOLS_REFERENCE.md, 820 lines) - #696 -- Extensive extension guide (docs/EXTENDING_RHIZA.md, 915 lines) - #696 - -The repository serves as an exemplary template for Python projects, demonstrating how to balance standardization with extensibility through its living template architecture. - -**Verdict**: Production-ready, suitable for enterprise adoption as a project template foundation. Continuously improving with excellent security posture and comprehensive automation. From d36d14153c469d1fdbb8b684bdbe829ba6710d77 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 12:48:01 +0400 Subject: [PATCH 07/31] Delete REPOSITORY_ANALYSIS.md --- REPOSITORY_ANALYSIS.md | 402 ----------------------------------------- 1 file changed, 402 deletions(-) delete mode 100644 REPOSITORY_ANALYSIS.md diff --git a/REPOSITORY_ANALYSIS.md b/REPOSITORY_ANALYSIS.md deleted file mode 100644 index a38f6d4..0000000 --- a/REPOSITORY_ANALYSIS.md +++ /dev/null @@ -1,402 +0,0 @@ -# Rhiza Repository Analysis - -**Repository**: Jebel-Quant/rhiza -**Analysis Date**: December 21, 2025 -**Overall Rating**: 9.0/10 - ---- - -## Executive Summary - -Rhiza is a sophisticated, well-engineered collection of reusable configuration templates for modern Python projects. The repository demonstrates exceptional attention to detail, comprehensive automation, and professional development practices. It serves as both a practical toolset and an exemplary template for Python project structure. - ---- - -## Overall Score: 9.0/10 - -### Score Breakdown - -| Category | Score | Weight | -|----------|-------|--------| -| Code Quality & Architecture | 9.5/10 | 25% | -| Documentation | 9.0/10 | 20% | -| Testing & CI/CD | 9.5/10 | 20% | -| Developer Experience | 9.0/10 | 15% | -| Maintainability | 8.5/10 | 10% | -| Innovation & Usefulness | 9.0/10 | 10% | - ---- - -## Detailed Analysis - -### 1. Code Quality & Architecture (9.5/10) - -#### Strengths: -- **Minimal Core Implementation**: The `src/rhiza/__init__.py` is intentionally minimal, focusing on templates rather than code -- **Excellent Tooling Configuration**: - - Comprehensive `ruff.toml` with well-documented rule selections - - Proper `.editorconfig` for consistent cross-editor formatting - - Pre-commit hooks covering YAML, TOML, Markdown, and GitHub workflows -- **POSIX-Compliant Shell Scripts**: All `.sh` scripts use `#!/bin/sh` with proper error handling (`set -e`, `set -euo pipefail`) -- **Script Quality**: The release, bump, and book scripts are well-structured with: - - Color-coded output for better UX - - Comprehensive error checking - - Interactive prompts with safeguards - - Clear usage documentation - -#### Areas for Improvement: -- **Limited Python Code**: While intentional, there's minimal Python code to evaluate (~1947 lines total) -- **Script Comments**: Some scripts could benefit from more inline comments explaining complex logic - ---- - -### 2. Documentation (9.0/10) - -#### Strengths: -- **Outstanding README**: - - Clear, comprehensive, and well-structured - - Excellent use of badges for status indicators - - Detailed installation and usage instructions - - Multiple integration methods (automated and manual) - - Beautiful formatting with icons and visual hierarchy -- **Auto-Generated Help**: The Makefile help is auto-synced to README via pre-commit hook -- **Template Documentation**: Each configuration file includes header comments explaining its purpose -- **Contributing Guide**: Clear, welcoming, with specific examples -- **Code of Conduct**: Professional and inclusive -- **Etymology**: Nice touch explaining the Greek origin of "rhiza" (root) - -#### Areas for Improvement: -- **API Documentation**: While `make docs` generates API docs with pdoc, there's minimal API to document -- **Architecture Decision Records**: No ADRs documenting why certain design decisions were made -- **Troubleshooting Section**: While present, could be expanded with more common issues -- **Video/Visual Tutorials**: No screencasts or diagrams showing the sync workflow in action - ---- - -### 3. Testing & CI/CD (9.5/10) - -#### Strengths: -- **Comprehensive Test Suite**: - - 1,291 lines of test code across 10 test files - - Tests for scripts (bump, release) - - Tests for Makefile targets (including marimushka) - - Tests for README code blocks - - Structural validation tests -- **Multi-Version CI Matrix**: - - Dynamic Python version matrix (3.11-3.14) - - Configurable via repository variables - - Smart matrix generation script -- **Multiple CI Workflows**: 10 different workflows covering: - - Continuous Integration (CI) - - Pre-commit checks - - Dependency checking (deptry) - - Documentation building (book) - - Marimo notebooks - - Docker validation - - Devcontainer validation - - Release automation - - Template synchronization -- **Release Automation**: - - Sophisticated bump/release process - - OIDC-based PyPI publishing (no stored credentials) - - Conditional devcontainer publishing - - Draft releases with artifact links -- **Pre-commit Hooks**: - - ruff (linting and formatting) - - markdownlint - - check-jsonschema - - actionlint for GitHub Actions - - validate-pyproject - - Custom README updater - -#### Areas for Improvement: -- **Test Coverage Metrics**: While pytest-cov is configured, no coverage badges or requirements -- **Integration Tests**: Limited integration testing of the sync workflow -- **Performance Tests**: No benchmarking of sync operations -- **Security Scanning**: No CodeQL or security-focused workflows - ---- - -### 4. Developer Experience (9.0/10) - -#### Strengths: -- **Exceptional Makefile**: - - Well-organized with section headers - - Color-coded output for better UX - - Comprehensive targets (18 targets) - - Self-documenting help system - - Chained dependencies -- **Modern Tooling**: - - Uses `uv` for fast package management - - Hatch for building - - Marimo for interactive notebooks - - minibook for companion documentation -- **Dev Container Support**: - - Fully configured VS Code dev container - - GitHub Codespaces ready - - SSH agent forwarding - - Marimo integration - - Auto-bootstrapping -- **Quick Start**: `make install` handles everything -- **Customization Hooks**: - - `build-extras.sh` for custom dependencies - - `post-release.sh` for post-release tasks - - Template exclusion mechanism -- **Multiple Integration Methods**: Automated injection script and manual cherry-picking - -#### Areas for Improvement: -- **First-Time Setup**: No check for system dependencies (curl, git, etc.) -- **IDE Configuration**: No `.vscode/settings.json` with recommended extensions -- **Dependency Caching**: Makefile doesn't cache dependency installations efficiently -- **Windows Support**: Unclear if shell scripts work on Windows (WSL recommended?) - ---- - -### 5. Maintainability (8.5/10) - -#### Strengths: -- **Consistent Style**: All files follow established patterns -- **Version Control**: Semantic versioning, clear tagging strategy -- **Automated Updates**: Sync workflow keeps templates up to date -- **Template Configuration**: `.github/template.yml` as single source of truth -- **Modular Scripts**: Scripts are focused and single-purpose -- **Git Hygiene**: Good `.gitignore`, clean history (though only 2 commits visible) - -#### Areas for Improvement: -- **Limited Git History**: Only 2 commits make it hard to assess evolution -- **No CHANGELOG**: No automated changelog generation -- **Dependency Updates**: No Renovate/Dependabot configuration visible -- **Breaking Changes**: No clear strategy for handling breaking changes in templates -- **Versioning Strategy**: Unclear how template versions relate to repository version - ---- - -### 6. Innovation & Usefulness (9.0/10) - -#### Strengths: -- **Novel Approach**: Combining templates with sync automation is elegant -- **Practical**: Solves a real pain point in Python project setup -- **Comprehensive**: Not just CI/CD, but complete project structure -- **Self-Hosting**: Uses itself (rhiza in pyproject.toml, sync workflow) -- **Marimo Integration**: Early adoption of modern notebook technology -- **OIDC Publishing**: Modern, secure PyPI publishing without tokens -- **Flexible Adoption**: Can use all or pick specific templates -- **Dynamic Python Versioning**: Smart approach to Python version management - -#### Areas for Improvement: -- **Language Support**: Python-only (understandable given focus) -- **Template Variations**: No variants for different project types (CLI vs library vs web) -- **Metrics Dashboard**: No way to see which templates are most popular -- **Community Templates**: No mechanism for community-contributed templates - ---- - -## Key Strengths - -### 1. **Professional Engineering Practices** -The repository demonstrates industry-leading practices: -- Comprehensive CI/CD with 10 different workflows -- Pre-commit hooks catching issues before they reach CI -- OIDC-based publishing eliminating credential management -- Multi-version testing matrix - -### 2. **Outstanding Documentation** -The README is exemplary: -- Clear value proposition -- Multiple integration paths -- Extensive troubleshooting -- Auto-synchronized with actual code - -### 3. **Developer-Centric Design** -Everything is optimized for developer productivity: -- `make install` and you're running -- Dev containers for immediate environment -- Interactive scripts with sensible defaults -- Color-coded output for clarity - -### 4. **Modern Tooling** -Leverages cutting-edge Python ecosystem: -- `uv` for blazing-fast package management -- Marimo for interactive notebooks -- Ruff for ultra-fast linting -- Hatch for building - -### 5. **Self-Referential Architecture** -The repository uses itself for its own infrastructure, demonstrating confidence in the templates and providing a living example. - -### 6. **Automation Excellence** -- Auto-syncing templates from upstream -- Auto-updating README from Makefile -- Auto-publishing on tag -- Auto-testing documentation code blocks - ---- - -## Key Weaknesses & Recommendations - -### 1. **Limited Git History** (Priority: Low) -**Issue**: Only 2 commits visible, making it hard to understand evolution. -**Impact**: Can't assess how the project responds to issues/PRs. -**Recommendation**: This appears to be a recent start or squashed history. Normal going forward. - -### 2. **No Security Scanning** (Priority: High) -**Issue**: No CodeQL, Snyk, or similar security scanning. -**Impact**: Vulnerabilities in dependencies or scripts could go unnoticed. -**Recommendation**: -```yaml -# Add to .github/workflows/security.yml (partial example) -jobs: - security: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: github/codeql-action/init@v3 - with: - languages: python - - uses: github/codeql-action/analyze@v3 -``` - -### 3. **Missing Dependency Management** (Priority: Medium) -**Issue**: No Renovate or Dependabot configuration visible. -**Impact**: Dependencies (especially GitHub Actions) could become outdated. -**Recommendation**: -```json -// Add .github/renovate.json -{ - "extends": ["config:recommended"], - "schedule": ["before 4am on Monday"] -} -``` - -### 4. **No CHANGELOG** (Priority: Medium) -**Issue**: No automated changelog generation. -**Impact**: Users don't know what changed between versions. -**Recommendation**: -- Use conventional commits -- Add changelog generation to release workflow -- Consider using `git-cliff` or similar - -### 5. **Test Coverage Not Tracked** (Priority: Low) -**Issue**: pytest-cov configured but no coverage requirements or badges. -**Impact**: Could accidentally reduce test coverage. -**Recommendation**: -```yaml -# Add to CI workflow -- name: Check coverage - run: | - uv run pytest --cov=src --cov-report=term --cov-fail-under=80 -``` - -### 6. **Limited Template Variations** (Priority: Low) -**Issue**: One-size-fits-all approach for all Python projects. -**Impact**: May include unnecessary files for simple projects. -**Recommendation**: -- Consider project profiles (minimal, standard, full) -- Allow template variants via configuration - -### 7. **Windows Support Unclear** (Priority: Medium) -**Issue**: Shell scripts may not work on Windows without WSL. -**Impact**: Windows users might struggle with local development. -**Recommendation**: -- Document Windows requirements explicitly -- Consider PowerShell alternatives for scripts -- Test in Windows dev container - -### 8. **No Architecture Decision Records** (Priority: Low) -**Issue**: No ADRs explaining design decisions. -**Impact**: Future maintainers won't understand "why" choices were made. -**Recommendation**: -```markdown -# Add docs/adr/0001-use-uv-for-package-management.md -## Context -We needed fast, reliable Python package management... -``` - ---- - -## Comparison to Industry Standards - -### Excellent Practices Found: -- [x] MIT License (permissive, business-friendly) -- [x] Code of Conduct (inclusive community) -- [x] Contributing guidelines (clear process) -- [x] CI/CD with multiple workflows -- [x] Pre-commit hooks (catching issues early) -- [x] Dev container support (consistent environments) -- [x] Semantic versioning -- [x] OIDC publishing (secure, no tokens) -- [x] Multi-version testing - -### Missing Best Practices: -- [ ] Security scanning (CodeQL, Snyk) -- [ ] Dependency updates automation (Renovate/Dependabot) -- [ ] CHANGELOG.md -- [ ] Issue/PR templates -- [ ] GitHub Discussions enabled -- [ ] Project roadmap -- [ ] Architecture Decision Records -- [~] Limited commit history - ---- - -## Recommendations by Priority - -### High Priority (Implement Soon) -1. **Add Security Scanning**: Implement CodeQL for Python and GitHub Actions -2. **Enable Renovate/Dependabot**: Keep dependencies current automatically -3. **Add Issue/PR Templates**: Improve community contributions - -### Medium Priority (Next Quarter) -4. **Generate CHANGELOG**: Automate changelog from conventional commits -5. **Add Coverage Requirements**: Enforce minimum test coverage -6. **Document Windows Support**: Clarify requirements and provide guidance -7. **Create ADRs**: Document key architectural decisions - -### Low Priority (Nice to Have) -8. **Template Variations**: Create minimal/standard/full profiles -9. **Visual Documentation**: Add diagrams explaining sync workflow -10. **Performance Benchmarks**: Track sync operation speed -11. **Community Templates**: Allow external template contributions - ---- - -## Conclusion - -Rhiza is an **exceptional repository** that serves as both a practical tool and a best-practices showcase for Python projects. It demonstrates professional software engineering with comprehensive automation, excellent documentation, and thoughtful developer experience design. - -The 9.0/10 rating reflects: -- **Near-perfect execution** of its intended purpose -- **Industry-leading practices** in CI/CD, documentation, and automation -- **Minor gaps** in security scanning, dependency management, and changelog -- **Room for enhancement** in template variations and community features - -This repository could serve as a **gold standard template** for other Python projects. The few weaknesses identified are relatively minor and easily addressable. The self-referential architecture (using Rhiza to manage Rhiza) demonstrates high confidence in the tooling. - -### Final Verdict: 9.0/10 - Excellent - -**Strongly Recommended** for: -- Teams starting new Python projects -- Organizations seeking standardization -- Developers wanting modern Python project structure -- Anyone tired of configuring CI/CD from scratch - ---- - -## Appendix: Metrics Summary - -| Metric | Value | -|--------|-------| -| Total Python LOC | ~1,947 | -| Test Python LOC | ~1,291 | -| GitHub Workflows | 10 | -| Makefile Targets | 18 | -| Pre-commit Hooks | 9 | -| Python Versions Supported | 4 (3.11-3.14) | -| Documentation Pages | 7+ | -| Shell Scripts | 6 | -| Test Files | 10 | -| Commits (visible) | 2 | - ---- - -*This analysis was conducted by thoroughly reviewing the repository structure, code quality, documentation, CI/CD workflows, testing infrastructure, and developer experience. Recommendations are based on industry best practices and modern software engineering standards.* From e7c248c8b42c8ad94b2ea0f5054f182c3850e92f Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 12:59:18 +0400 Subject: [PATCH 08/31] Remove dev dependencies from pyproject.toml Removed dependency groups and related comments from pyproject.toml. --- pyproject.toml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6dfda31..cfec824 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,24 +24,7 @@ classifiers = [ ] dependencies = [] -[dependency-groups] -dev = [ - "marimo>=0.18.0,<1.0", # Reactive notebook runtime for book/ examples (v0.18+ for streaming, <1.0 for API stability) - "numpy>=2.4.0,<3.0", # Numerical computing for notebook data analysis (v2.4+ includes 2.0 improvements, <3.0 avoids breaking changes) - "plotly>=6.5.0,<7.0", # Interactive visualization in notebooks (v6.5+ mature features, <7.0 for API stability) - "pandas>=3,<3.1", # Data manipulation for notebook examples (v3+ for modern APIs, <3.1 conservative after major release) - "pyyaml>=6.0,<7.0", # YAML parsing for validation scripts (v6+ for security fixes, <7.0 for API stability) - # See docs/DEPENDENCIES.md for detailed version rationale -] - [project.urls] Homepage = "https://github.com/jebel-quant/rhiza-core" Repository = "https://github.com/jebel-quant/rhiza-core" Issues = "https://github.com/jebel-quant/rhiza-core/issues" - -[tool.deptry.package_module_name_map] -marimo = "marimo" -numpy = "numpy" -plotly = "plotly" -pandas = "pandas" -pyyaml = "yaml" From b1d201b840923bcb480c575e20327a6eec2209f5 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 13:00:19 +0400 Subject: [PATCH 09/31] Delete .rhiza/scripts directory --- .rhiza/scripts/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .rhiza/scripts/.gitkeep diff --git a/.rhiza/scripts/.gitkeep b/.rhiza/scripts/.gitkeep deleted file mode 100644 index e69de29..0000000 From e1f68a2adbc26a668c2187dc3cacbcf4dc85b760 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 20:43:21 +0400 Subject: [PATCH 10/31] deflate uv.lock --- uv.lock | 694 -------------------------------------------------------- 1 file changed, 694 deletions(-) diff --git a/uv.lock b/uv.lock index e984663..203388e 100644 --- a/uv.lock +++ b/uv.lock @@ -10,701 +10,7 @@ resolution-markers = [ "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] -[[package]] -name = "anyio" -version = "4.12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, -] - -[[package]] -name = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "docutils" -version = "0.22.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "itsdangerous" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, -] - -[[package]] -name = "jedi" -version = "0.19.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "parso" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, -] - -[[package]] -name = "loro" -version = "1.10.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/27/ea6f3298fc87ea5f2d60ebfbca088e7d9b2ceb3993f67c83bfb81778ec01/loro-1.10.3.tar.gz", hash = "sha256:68184ab1c2ab94af6ad4aaba416d22f579cabee0b26cbb09a1f67858207bbce8", size = 68833, upload-time = "2025-12-09T10:14:06.644Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/bb/61f36aac7981f84ffba922ac1220505365df3e064bc91c015790bff92007/loro-1.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7ee0e1c9a6d0e4a1df4f1847d3b31cef8088860c1193442f131936d084bd3fe1", size = 3254532, upload-time = "2025-12-09T10:11:31.215Z" }, - { url = "https://files.pythonhosted.org/packages/15/28/5708da252eb6be90131338b104e5030c9b815c41f9e97647391206bec092/loro-1.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7225471b29a892a10589d7cf59c70b0e4de502fa20da675e9aaa1060c7703ae", size = 3055231, upload-time = "2025-12-09T10:11:16.111Z" }, - { url = "https://files.pythonhosted.org/packages/16/b6/68c350a39fd96f24c55221f883230aa83db0bb5f5d8e9776ccdb25ea1f7b/loro-1.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc04a714e0a604e191279501fa4d2db3b39cee112275f31e87d95ecfbafdfb6c", size = 3286945, upload-time = "2025-12-09T10:08:12.633Z" }, - { url = "https://files.pythonhosted.org/packages/23/af/8245b8a20046423e035cd17de9811ab1b27fc9e73425394c34387b41cc13/loro-1.10.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:375c888a4ddf758b034eb6ebd093348547d17364fae72aa7459d1358e4843b1f", size = 3349533, upload-time = "2025-12-09T10:08:46.754Z" }, - { url = "https://files.pythonhosted.org/packages/cc/8c/d764c60914e45a2b8c562e01792172e3991430103c019cc129d56c24c868/loro-1.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2020d9384a426e91a7d38c9d0befd42e8ad40557892ed50d47aad79f8d92b654", size = 3704622, upload-time = "2025-12-09T10:09:25.068Z" }, - { url = "https://files.pythonhosted.org/packages/54/cc/ebdbdf0b1c7a223fe84fc0de78678904ed6424b426f90b98503b95b1dff9/loro-1.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95afacd832dce152700c2bc643f7feb27d5611fc97b5141684b5831b22845380", size = 3416659, upload-time = "2025-12-09T10:09:59.107Z" }, - { url = "https://files.pythonhosted.org/packages/fa/bc/db7f3fc619483b60c03d85b4f9bb5812b2229865b574c8802b46a578f545/loro-1.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c95868bcf6361d700e215f33a88b8f51d7bc3ae7bbe3d35998148932e23d3fa", size = 3345007, upload-time = "2025-12-09T10:10:53.327Z" }, - { url = "https://files.pythonhosted.org/packages/91/65/bcd3b1d3a3615e679177c1256f2e0ff7ee242c3d5d1b9cb725b0ec165b51/loro-1.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68f5c7fad09d8937ef4b55e7dd4a0f9f175f026369b3f55a5b054d3513f6846d", size = 3687874, upload-time = "2025-12-09T10:10:31.674Z" }, - { url = "https://files.pythonhosted.org/packages/3a/e4/0d51e2da2ae6143bfd03f7127b9daf58a3f8dae9d5ca7740ccba63a04de4/loro-1.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:740bb548139d71eccd6317f3df40a0dc5312e98bbb2be09a6e4aaddcaf764206", size = 3467200, upload-time = "2025-12-09T10:11:47.994Z" }, - { url = "https://files.pythonhosted.org/packages/06/99/ada2baeaf6496e34962fe350cd41129e583219bf4ce5e680c37baa0613a8/loro-1.10.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c756a6ee37ed851e9cf91e5fedbc68ca21e05969c4e2ec6531c15419a4649b58", size = 3618468, upload-time = "2025-12-09T10:12:24.182Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/83335935959c5e3946e02b748af71d801412b2aa3876f870beae1cd56d4d/loro-1.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3553390518e188c055b56bcbae76bf038329f9c3458cb1d69068c55b3f8f49f1", size = 3666852, upload-time = "2025-12-09T10:12:59.117Z" }, - { url = "https://files.pythonhosted.org/packages/9f/53/1bd455b3254afa35638d617e06c65a22e604b1fae2f494abb9a621c8e69b/loro-1.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0885388c0c2b53f5140229921bd64c7838827e3101a05d4d53346191ba76b15d", size = 3556829, upload-time = "2025-12-09T10:13:34.002Z" }, - { url = "https://files.pythonhosted.org/packages/66/30/6f48726ef50f911751c6b69d7fa81482cac70d4ed817216f846776fec28c/loro-1.10.3-cp311-cp311-win32.whl", hash = "sha256:764b68c4ff0411399c9cf936d8b6db1161ec445388ff2944a25bbdeb2bbac15c", size = 2723776, upload-time = "2025-12-09T10:14:27.261Z" }, - { url = "https://files.pythonhosted.org/packages/69/39/0b08203d94a6f200bbfefa8025a1b825c8cfb30e8cc8b2a1224629150d08/loro-1.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:9e583e6aabd6f9b2bdf3ff3f6e0de10c3f7f8ab9d4c05c01a9ecca309c969017", size = 2950529, upload-time = "2025-12-09T10:14:08.857Z" }, - { url = "https://files.pythonhosted.org/packages/dd/b6/cfbf8088e8ca07d66e6c1eccde42e00bd61708f28e8ea0936f9582306323/loro-1.10.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:028948b48dcc5c2127f974dae4ad466ab69f0d1eeaf367a8145eb6501fb988f2", size = 3239592, upload-time = "2025-12-09T10:11:32.505Z" }, - { url = "https://files.pythonhosted.org/packages/78/e4/7b614260bf16c5e33c0bea6ac47ab0284efd21f89f2e5e4e15cd93bead40/loro-1.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5253b8f436d90412b373c583f22ac9539cfb495bf88f78d4bb41daafef0830b7", size = 3045107, upload-time = "2025-12-09T10:11:17.481Z" }, - { url = "https://files.pythonhosted.org/packages/ae/17/0a78ec341ca69d376629ff2a1b9b3511ee7dd54f2b018616ef03328024f7/loro-1.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14be8a5539d49468c94d65742355dbe79745123d78bf769a23e53bf9b60dd46a", size = 3292720, upload-time = "2025-12-09T10:08:14.027Z" }, - { url = "https://files.pythonhosted.org/packages/d4/9b/f36a4654508e9b8ddbe08a62a0ce8b8e7fd511a39b161821917530cffd8e/loro-1.10.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91b2b9139dfc5314a0197132a53b6673fddb63738380a522d12a05cec7ad76b4", size = 3353260, upload-time = "2025-12-09T10:08:48.251Z" }, - { url = "https://files.pythonhosted.org/packages/b4/0e/7d441ddecc7695153dbe68af4067d62e8d7607fce3747a184878456a91f6/loro-1.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:247897288911c712ee7746965573299fc23ce091e94456da8da371e6adae30f4", size = 3712354, upload-time = "2025-12-09T10:09:26.38Z" }, - { url = "https://files.pythonhosted.org/packages/1c/33/10e66bb84599e61df124f76c00c5398eb59cbb6f69755f81c40f65a18344/loro-1.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:835abc6025eb5b6a0fe22c808472affc95e9a661b212400cfd88ba186b0d304c", size = 3422926, upload-time = "2025-12-09T10:10:00.347Z" }, - { url = "https://files.pythonhosted.org/packages/b2/70/00dc4246d9f3c69ecbb9bc36d5ad1a359884464a44711c665cb0afb1e9de/loro-1.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e660853617fc29e71bb7b796e6f2c21f7722c215f593a89e95cd4d8d5a32aca0", size = 3353092, upload-time = "2025-12-09T10:10:55.786Z" }, - { url = "https://files.pythonhosted.org/packages/19/37/60cc0353c5702e1e469b5d49d1762e782af5d5bd5e7c4e8c47556335b4c6/loro-1.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8059063cab57ca521012ed315a454784c20b0a86653e9014795e804e0a333659", size = 3687798, upload-time = "2025-12-09T10:10:33.253Z" }, - { url = "https://files.pythonhosted.org/packages/88/c4/4db1887eb08dfbb305d9424fdf1004c0edf147fd53ab0aaf64a90450567a/loro-1.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9748359343b5fd7019ab3c2d1d583a0c13c633a4dd21d75e50e3815ab479f493", size = 3474451, upload-time = "2025-12-09T10:11:49.489Z" }, - { url = "https://files.pythonhosted.org/packages/d8/66/10d2e00c43b05f56e96e62100f86a1261f8bbd6422605907f118a752fe61/loro-1.10.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:def7c9c2e16ad5470c9c56f096ac649dd4cd42d5936a32bb0817509a92d82467", size = 3621647, upload-time = "2025-12-09T10:12:25.536Z" }, - { url = "https://files.pythonhosted.org/packages/47/f0/ef8cd6654b09a03684195c650b1fba00f42791fa4844ea400d94030c5615/loro-1.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:34b223fab58591a823f439d9a13d1a1ddac18dc4316866503c588ae8a9147cb1", size = 3667946, upload-time = "2025-12-09T10:13:00.711Z" }, - { url = "https://files.pythonhosted.org/packages/bb/5d/960b62bf85c38d6098ea067438f037a761958f3a17ba674db0cf316b0f60/loro-1.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d5fa4baceb248d771897b76d1426c7656176e82e770f6790940bc3e3812436d", size = 3565866, upload-time = "2025-12-09T10:13:35.401Z" }, - { url = "https://files.pythonhosted.org/packages/8f/d4/0d499a5e00df13ce497263aef2494d9de9e9d1f11d8ab68f89328203befb/loro-1.10.3-cp312-cp312-win32.whl", hash = "sha256:f25ab769b84a5fbeb1f9a1111f5d28927eaeaa8f5d2d871e237f80eaca5c684e", size = 2720785, upload-time = "2025-12-09T10:14:28.79Z" }, - { url = "https://files.pythonhosted.org/packages/1a/9b/2b5be23f1da4cf20c6ce213cfffc66bdab2ea012595abc9e3383103793d0/loro-1.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:3b73b7a3a32e60c3424fc7deaf8b127af7580948e27d8bbe749e3f43508aa0a2", size = 2954650, upload-time = "2025-12-09T10:14:10.235Z" }, - { url = "https://files.pythonhosted.org/packages/75/67/8467cc1c119149ada86903b67ce10fc4b47fb6eb2a8ca5f94c0938fd010f/loro-1.10.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:380ef692c5272e8b607be2ee6a8eef5113e65dc38e6739526c30e3db6abc3fbc", size = 3239527, upload-time = "2025-12-09T10:11:33.884Z" }, - { url = "https://files.pythonhosted.org/packages/bc/3b/d1a01af3446cb98890349215bea7e71ba49dc3e50ffbfb90c5649657a8b8/loro-1.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed966ce6ff1fb3787b3f6c4ed6dd036baa5fb738b84a466a5e764f2ab534ccc2", size = 3044767, upload-time = "2025-12-09T10:11:18.777Z" }, - { url = "https://files.pythonhosted.org/packages/6b/93/37f891fa46767001ae2518697fb01fc187497e3a5238fe28102be626055d/loro-1.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d7c8d2f3d88578fdf69845a9ae16fc5ea3ac54aa838a6bf43a24ce11908220", size = 3292648, upload-time = "2025-12-09T10:08:15.404Z" }, - { url = "https://files.pythonhosted.org/packages/6c/67/82273eeba2416b0410595071eda1eefcdf4072c014d44d2501b660aa7145/loro-1.10.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62283c345bfeedef19c8a6d029cd8830e5d2c20b5fb45975d8a70a8a30a7944b", size = 3353181, upload-time = "2025-12-09T10:08:50.144Z" }, - { url = "https://files.pythonhosted.org/packages/82/33/894dccf132bece82168dfbe61fad25a13ed89d18f20649f99e87c38f9228/loro-1.10.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1e7e6ae091179fa5f0fca1f8612fde20236ee0a678744bf51ff7d26103ea04f", size = 3712583, upload-time = "2025-12-09T10:09:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b7/99292729d8b271bcc4bff5faa20b33e4c749173af4c9cb9d34880ae3b4c8/loro-1.10.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6abc6de4876aa205498cef52a002bc38662fbd8d742351ea0f535479208b8b1c", size = 3421491, upload-time = "2025-12-09T10:10:01.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/fb/188b808ef1d9b6d842d53969b99a16afb1b71f04739150959c8946345d0e/loro-1.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acbbfd24cf28a71bbdad8544852e9bbba0ba8535f8221f8859b2693555fa8356", size = 3352623, upload-time = "2025-12-09T10:10:57.361Z" }, - { url = "https://files.pythonhosted.org/packages/53/cc/e2d008cc24bddcf05d1a15b8907a73b1731921ab40897f73a3385fdd274a/loro-1.10.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5faf4ebbe8ca39605024f16dbbbde354365f4e2dcfda82c753797461b504bbd3", size = 3687687, upload-time = "2025-12-09T10:10:34.453Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b6/4251822674230027103caa4fd46a1e83c4d676500074e7ab297468bf8f40/loro-1.10.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e049c21b292c4ff992b23a98812840735db84620721c10ae7f047a921202d090", size = 3474316, upload-time = "2025-12-09T10:11:51.207Z" }, - { url = "https://files.pythonhosted.org/packages/c4/54/ecff3ec08d814f3b9ec1c78a14ecf2e7ff132a71b8520f6aa6ad1ace0056/loro-1.10.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:20e8dacfb827c1f7ffb73e127029d7995a9ab2c3b7b7bc3ecc91d22ee32d78d0", size = 3622069, upload-time = "2025-12-09T10:12:27.059Z" }, - { url = "https://files.pythonhosted.org/packages/ac/84/c1b8251000f46df5f4d043af8c711bdbff9818727d26429378e0f3a5115e/loro-1.10.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1b743c1c4f93f5b4f0e12efbb352d26e9f80bcbf20f45d9c70f3d0b522f42060", size = 3667722, upload-time = "2025-12-09T10:13:02.012Z" }, - { url = "https://files.pythonhosted.org/packages/ef/13/c5c02776f4ad52c6361b95e1d7396c29071533cef45e3861a2e35745be27/loro-1.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:446d67bc9e28036a5a5e03526d28a1559ef2a47b3ccad6b07820dae123cc3697", size = 3564952, upload-time = "2025-12-09T10:13:37.227Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f1/63d4bc63a1521a9b577f6d13538ec4790865584fdf87569d5af943792406/loro-1.10.3-cp313-cp313-win32.whl", hash = "sha256:45d7d8ec683599897695bb714771baccabc1b4c4a412283cc39787c7a59f7ff0", size = 2720952, upload-time = "2025-12-09T10:14:30.17Z" }, - { url = "https://files.pythonhosted.org/packages/29/3c/65c8b0b7f96c9b4fbd458867cf91f30fcd58ac25449d8ba9303586061671/loro-1.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:a42bf73b99b07fed11b65feb0a5362b33b19de098f2235848687f4c41204830e", size = 2953768, upload-time = "2025-12-09T10:14:11.965Z" }, - { url = "https://files.pythonhosted.org/packages/4e/e9/f6a242f61aa4d8b56bd11fa467be27d416401d89cc3244b58651a3a44c88/loro-1.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4866325b154aeebcd34be106c7597acf150c374481ac3c12035a1af715ac0f01", size = 3289791, upload-time = "2025-12-09T10:08:16.926Z" }, - { url = "https://files.pythonhosted.org/packages/a7/81/8f5f4d6805658c654264e99467f3f46facdbb2062cbf86743768ee4b942a/loro-1.10.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ea7b8849660a28ce8cd90a82db4f76c23453836fcbc88f5767feaaf8739045e2", size = 3348007, upload-time = "2025-12-09T10:08:53.305Z" }, - { url = "https://files.pythonhosted.org/packages/c3/15/bba0fad18ec5561a140e9781fd2b38672210b52e847d207c57ae85379efd/loro-1.10.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e82cdaf9a5892557d3167e07ed5093f87dfa31ef860a63b0eac6c0c2f435705", size = 3707937, upload-time = "2025-12-09T10:09:29.165Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b2/5519c92bd4f9cde068dc60ba35d7f3e4f8cce41e7bf39febd4fb08908e97/loro-1.10.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7ee99e5dc844fb20fca830906a0d721022ad1c37aad0b1a440c4ecb98d0c02f", size = 3416744, upload-time = "2025-12-09T10:10:02.956Z" }, - { url = "https://files.pythonhosted.org/packages/81/ba/92d97c27582c0ce12bb83df19b9e080c0dfe95068966296a4fa2279c0477/loro-1.10.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:153c297672ad98d0fe6ff8985decf1e64528ad1dd01ae1452bb83bdeb31f858f", size = 3470978, upload-time = "2025-12-09T10:11:52.707Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8b/acb39b0e74af1c317d3121e75a4bc5bc77d7fda5a79c60399746486f60d9/loro-1.10.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:0ed72f8c6a5f521252ee726954055339abba3fcf00404fb4b5c2da168f0cce79", size = 3615039, upload-time = "2025-12-09T10:12:28.631Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c3/154e3361e5ef42012f6842dbd93f8fbace6eec06517b5a4a9f8c4a46e873/loro-1.10.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f612ab17acdac16c0139e63ff45b33175ebfb22e61a60eb7929a4583389348d6", size = 3663731, upload-time = "2025-12-09T10:13:03.557Z" }, - { url = "https://files.pythonhosted.org/packages/c6/dd/a283cf5b1c957e0bbc67503a10e17606a8f8c87f51d3cf3d83dc3a0ac88a/loro-1.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f2741db05c79f3618c954bac90f4572d28c01c243884453f379e9a8738f93d81", size = 3558807, upload-time = "2025-12-09T10:13:38.926Z" }, - { url = "https://files.pythonhosted.org/packages/8d/4a/a5340b6fdf4cd34d758bed23bd1f64063b3b1b41ff4ecc94ee39259ee9a7/loro-1.10.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:623cf7df17626aa55bc6ca54e89177dbe71a5f1c293e102d6153f43991a1a041", size = 3213589, upload-time = "2025-12-09T10:11:35.377Z" }, - { url = "https://files.pythonhosted.org/packages/00/93/5164e93a77e365a92def77c1258386daef233516a29fb674a3b9d973b8b8/loro-1.10.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d8e715d475f32a1462969aca27eeb3f998f309182978f55bc37ce5c515d92e90", size = 3029557, upload-time = "2025-12-09T10:11:20.076Z" }, - { url = "https://files.pythonhosted.org/packages/6c/30/94592d7c01f480ce99e1783b0d9203eb20ba2eab42575dabd384e3c9d1fa/loro-1.10.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61e012a80e8c9fe248b9d0a76e91664c9479a72d976eaeed78f87b15b5d1d732", size = 3282335, upload-time = "2025-12-09T10:08:18.168Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a8/7ae3c0b955aa638fa7dbd2d194c7759749a0d0d96a94805d5dec9b30eaea/loro-1.10.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:686ece56756acbaf80c986848915e9126a29a06d7a62209747e3ef1efc0bd8f6", size = 3333071, upload-time = "2025-12-09T10:08:55.314Z" }, - { url = "https://files.pythonhosted.org/packages/f7/10/151edebdb2bca626ad50911b761164ced16984b25b0b37b34b674ded8b29/loro-1.10.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aa821c8871deca98f4605eb0c40fb26bcf82bd29c9e7fa33b183516c5395b11", size = 3698226, upload-time = "2025-12-09T10:09:30.474Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ac/02a490e38466506b1003df4910d2a8ae582265023dae9e2217c98b56ea3f/loro-1.10.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:507d34137adb4148f79e1da7f89a21a4aab18565621a5dc2b389773fe98ac25b", size = 3407322, upload-time = "2025-12-09T10:10:04.199Z" }, - { url = "https://files.pythonhosted.org/packages/81/db/da51f2bcad81ca3733bc21e83f3b6752446436b565b90f5c350ad227ad01/loro-1.10.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91d3b2e187ccfe2b14118a6e5617266fedcdf3435f6fa0a3db7b4afce8afa687", size = 3330268, upload-time = "2025-12-09T10:10:58.61Z" }, - { url = "https://files.pythonhosted.org/packages/4e/af/50d136c83d504a3a1f4ad33a6bf38b6933985a82741302255cf446a5f7ad/loro-1.10.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0016f834fd1626710081334400aed8494380b55ef131f7133d21c3bd22d892a", size = 3673582, upload-time = "2025-12-09T10:10:35.849Z" }, - { url = "https://files.pythonhosted.org/packages/63/4d/53288aae777218e05c43af9c080652bcdbbc8d97c031607eedd3fc15617d/loro-1.10.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:71c4275dca5a8a86219d60545d4f60e081b4af44b490ac912c0481906934bfc6", size = 3463731, upload-time = "2025-12-09T10:11:54.102Z" }, - { url = "https://files.pythonhosted.org/packages/75/01/2389f26ffe8bc3ffe48a0a578f610dd49c709bbcf0d5d2642c6e2b52f490/loro-1.10.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:490f12571b2ed1a8eaf1edd3a7fffc55adac5010b1875fe1bb9e9af9a3907c38", size = 3602334, upload-time = "2025-12-09T10:12:30.082Z" }, - { url = "https://files.pythonhosted.org/packages/a7/16/07b64af13f5fcea025e003ca27bbd6f748217abbd4803dad88ea0900526c/loro-1.10.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a374a43cadaa48528a5411496481df9ae52bf01e513f4509e37d6c986f199c0e", size = 3657896, upload-time = "2025-12-09T10:13:04.86Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/4050770d7675ceced71651fe76971d5c27456b7098c0de03a4ecdbb0a02d/loro-1.10.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1a93b2ee59f1fa8d98dd552211fd5693551893b34c1dd2ba0324806d6d14022f", size = 3544339, upload-time = "2025-12-09T10:13:40.396Z" }, - { url = "https://files.pythonhosted.org/packages/c9/21/67e27cb404c968fc19a841d5c6277f13a17c69a56f49e3c15ea1c92a28eb/loro-1.10.3-cp314-cp314-win32.whl", hash = "sha256:baa863e3d869422e3320e822c0b1f87f5dc44cda903d1bd3b7a16f8413ce3d92", size = 2706731, upload-time = "2025-12-09T10:14:31.604Z" }, - { url = "https://files.pythonhosted.org/packages/08/54/6770cf36aeb994489375e9ab9c01201e70ab7cc286fa97e907aa41b1bae6/loro-1.10.3-cp314-cp314-win_amd64.whl", hash = "sha256:f10ed3ca89485f942b8b2de796ed9783edb990e7e570605232de77489e9f3548", size = 2933563, upload-time = "2025-12-09T10:14:13.805Z" }, - { url = "https://files.pythonhosted.org/packages/24/f5/eb089fd25eb428709dbe79fd4d36b82a00572aa54badd1dff62511a38fe3/loro-1.10.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b4d049efb1953aebfc16fa0b445ff5a37d4d08a1ab93f3b5a577a454b7a5ded", size = 3282369, upload-time = "2025-12-09T10:08:20.011Z" }, - { url = "https://files.pythonhosted.org/packages/30/d7/692cb87c908f6a8af6cbfc10ebab69e16780e3796e11454c2b481b5c3817/loro-1.10.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56ecad7fbac58aa8bee52bb261a764aeef6c7b39c20f0d69e8fad908ab2ca7d8", size = 3332530, upload-time = "2025-12-09T10:08:57.07Z" }, - { url = "https://files.pythonhosted.org/packages/54/46/ed3afbf749288b6f70f3b859a6762538818bf6a557ca873b07d6b036946b/loro-1.10.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d8d1be349d08b3a95592c6a17b80b1ea6aef892b1b8e2b93b540062d04e34e0", size = 3702599, upload-time = "2025-12-09T10:09:31.779Z" }, - { url = "https://files.pythonhosted.org/packages/fe/30/6cb616939c12bfe96a71a01a6e3551febf1c34bf9de114fafadbcfb65064/loro-1.10.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ec0a0b9bc4e32c46f14710062ec5b536c72110318aaf85632a4f8b37e9a470a", size = 3404412, upload-time = "2025-12-09T10:10:05.448Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/3d4006d3333589f9158ac6d403979bf5c985be8b461b18e7a2ea23b05414/loro-1.10.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c5d4437987f7a4a4ff5927f39d0f43ded5b34295dfb0a3c8e150687e25c3d6b8", size = 3462948, upload-time = "2025-12-09T10:11:55.405Z" }, - { url = "https://files.pythonhosted.org/packages/41/30/c640ccd3e570b08770a9f459decc2d8e7ceefdc34ac28a745418fb9cb5ba/loro-1.10.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:86d4f0c631ca274ad2fa2c0bdb8e1e141882d94339b7284a8bef5bf73fa6957d", size = 3599851, upload-time = "2025-12-09T10:12:31.759Z" }, - { url = "https://files.pythonhosted.org/packages/59/8f/062ea50554c47ae30e98b1f0442a458c0edecc6d4edc7fcfc4d901734dd0/loro-1.10.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:15e03084ff1b472e14623183ed6e1e43e0f717c2112697beda5e69b5bd0ff236", size = 3655558, upload-time = "2025-12-09T10:13:06.529Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f5/c7dd8cdbd57454b23d89799c22cd42b6d2dda283cd87d7b198dc424a462c/loro-1.10.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42d6a5ce5bc518eaa682413e82d597299650eeb03e8bc39341752d6e0d22503e", size = 3541282, upload-time = "2025-12-09T10:13:42.189Z" }, - { url = "https://files.pythonhosted.org/packages/43/1a/49e864102721e0e15a4e4c56d7f2dddad5cd589c2d0aceafe14990513583/loro-1.10.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16ca42e991589ea300b59da9e98940d5ddda76275fe4363b1f1e079d244403a1", size = 3284236, upload-time = "2025-12-09T10:08:25.836Z" }, - { url = "https://files.pythonhosted.org/packages/e9/c6/d46b433105d8002e4c90248c07f00cd2c8ea76f1048cc5f35b733be96723/loro-1.10.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9ca16dae359397aa7772891bb3967939ffda8da26e0b392d331b506e16afc78", size = 3348996, upload-time = "2025-12-09T10:09:03.951Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f3/e918c7b396c547b22a7ab3cff1b570c5ce94293f0dcb17cd96cbe6ba2d50/loro-1.10.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d87cfc0a6e119c1c8cfa93078f5d012e557c6b75edcd0977da58ec46d28dc242", size = 3701875, upload-time = "2025-12-09T10:09:37.924Z" }, - { url = "https://files.pythonhosted.org/packages/4c/67/140ecb65b4f436099ad674fbe7502378156f43b737cb43f5fd76c42a0da8/loro-1.10.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4541ed987306c51e718f51196fd2b2d05e87b323da5d850b37900d2e8ac6aae6", size = 3412283, upload-time = "2025-12-09T10:10:10.946Z" }, - { url = "https://files.pythonhosted.org/packages/d0/93/b7b41cf8b3e591b7191494e12be24cbb101f137fe82f0a24ed7934bbacf3/loro-1.10.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce0b0a500e08b190038380d4593efcb33c98ed4282cc8347ca6ce55d05cbdf6e", size = 3340580, upload-time = "2025-12-09T10:11:02.956Z" }, - { url = "https://files.pythonhosted.org/packages/94/19/fdc9ea9ce6510147460200c90164a84c22b0cc9e33f7dd5c0d5f76484314/loro-1.10.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:987dbcb42b4b8d2c799660a6d8942e53ae346f51d51c9ad7ef5d7e640422fe4a", size = 3680924, upload-time = "2025-12-09T10:10:39.877Z" }, - { url = "https://files.pythonhosted.org/packages/40/61/548491499394fe02e7451b0d7367f7eeed32f0f6dd8f1826be8b4c329f28/loro-1.10.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:f876d477cb38c6c623c4ccb5dc4b7041dbeff04167bf9c19fa461d57a3a1b916", size = 3465033, upload-time = "2025-12-09T10:12:03.122Z" }, - { url = "https://files.pythonhosted.org/packages/26/68/d8bebb6b583fe5a3dc4da32c9070964548e3ca1d524f383c71f9becf4197/loro-1.10.3-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:641c8445bd1e4181b5b28b75a0bc544ef51f065b15746e8714f90e2e029b5202", size = 3616740, upload-time = "2025-12-09T10:12:38.187Z" }, - { url = "https://files.pythonhosted.org/packages/52/9b/8f8ecc85eb925122a79348eb77ff7109a7ee41ee7d1a282122be2daff378/loro-1.10.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:a6ab6244472402b8d1f4f77e5210efa44dfa4914423cafcfcbd09232ea8bbff0", size = 3661160, upload-time = "2025-12-09T10:13:12.513Z" }, - { url = "https://files.pythonhosted.org/packages/79/3c/e884d06859f9a9fc64afd21c426b9d681af0856181c1fe66571a65d35ef7/loro-1.10.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ae4c765671ee7d7618962ec11cb3bb471965d9b88c075166fe383263235d58d6", size = 3553653, upload-time = "2025-12-09T10:13:47.917Z" }, -] - -[[package]] -name = "marimo" -version = "0.19.11" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "docutils" }, - { name = "itsdangerous" }, - { name = "jedi" }, - { name = "loro" }, - { name = "markdown" }, - { name = "msgspec" }, - { name = "narwhals" }, - { name = "packaging" }, - { name = "psutil" }, - { name = "pygments" }, - { name = "pymdown-extensions" }, - { name = "pyyaml" }, - { name = "starlette" }, - { name = "tomlkit" }, - { name = "uvicorn" }, - { name = "websockets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/31/aa12298afce0c7cf6bc0d8ffe664b4cfb295451f9029ca7a461c6e30390d/marimo-0.19.11.tar.gz", hash = "sha256:1f65a19f9dced82ae2d041dbe31b3f47ba16993f6411d41ba526b3b322abd948", size = 38203731, upload-time = "2026-02-12T18:37:49.536Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/d4/817cfdcf9636fc75036b9e0f7aa458c0bedec420d79768005ccfcf7fc9d8/marimo-0.19.11-py3-none-any.whl", hash = "sha256:4f98b677c89cada69c330e38f0747b4fca73cedb6f81b66832df3035ab8b6cff", size = 38609582, upload-time = "2026-02-12T18:37:53.954Z" }, -] - -[[package]] -name = "markdown" -version = "3.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, -] - -[[package]] -name = "msgspec" -version = "0.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/9c/bfbd12955a49180cbd234c5d29ec6f74fe641698f0cd9df154a854fc8a15/msgspec-0.20.0.tar.gz", hash = "sha256:692349e588fde322875f8d3025ac01689fead5901e7fb18d6870a44519d62a29", size = 317862, upload-time = "2025-11-24T03:56:28.934Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/59/fdcb3af72f750a8de2bcf39d62ada70b5eb17b06d7f63860e0a679cb656b/msgspec-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:09e0efbf1ac641fedb1d5496c59507c2f0dc62a052189ee62c763e0aae217520", size = 193345, upload-time = "2025-11-24T03:55:20.613Z" }, - { url = "https://files.pythonhosted.org/packages/5a/15/3c225610da9f02505d37d69a77f4a2e7daae2a125f99d638df211ba84e59/msgspec-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23ee3787142e48f5ee746b2909ce1b76e2949fbe0f97f9f6e70879f06c218b54", size = 186867, upload-time = "2025-11-24T03:55:22.4Z" }, - { url = "https://files.pythonhosted.org/packages/81/36/13ab0c547e283bf172f45491edfdea0e2cecb26ae61e3a7b1ae6058b326d/msgspec-0.20.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81f4ac6f0363407ac0465eff5c7d4d18f26870e00674f8fcb336d898a1e36854", size = 215351, upload-time = "2025-11-24T03:55:23.958Z" }, - { url = "https://files.pythonhosted.org/packages/6b/96/5c095b940de3aa6b43a71ec76275ac3537b21bd45c7499b5a17a429110fa/msgspec-0.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb4d873f24ae18cd1334f4e37a178ed46c9d186437733351267e0a269bdf7e53", size = 219896, upload-time = "2025-11-24T03:55:25.356Z" }, - { url = "https://files.pythonhosted.org/packages/98/7a/81a7b5f01af300761087b114dafa20fb97aed7184d33aab64d48874eb187/msgspec-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b92b8334427b8393b520c24ff53b70f326f79acf5f74adb94fd361bcff8a1d4e", size = 220389, upload-time = "2025-11-24T03:55:26.99Z" }, - { url = "https://files.pythonhosted.org/packages/70/c0/3d0cce27db9a9912421273d49eab79ce01ecd2fed1a2f1b74af9b445f33c/msgspec-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:562c44b047c05cc0384e006fae7a5e715740215c799429e0d7e3e5adf324285a", size = 223348, upload-time = "2025-11-24T03:55:28.311Z" }, - { url = "https://files.pythonhosted.org/packages/89/5e/406b7d578926b68790e390d83a1165a9bfc2d95612a1a9c1c4d5c72ea815/msgspec-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:d1dcc93a3ce3d3195985bfff18a48274d0b5ffbc96fa1c5b89da6f0d9af81b29", size = 188713, upload-time = "2025-11-24T03:55:29.553Z" }, - { url = "https://files.pythonhosted.org/packages/47/87/14fe2316624ceedf76a9e94d714d194cbcb699720b210ff189f89ca4efd7/msgspec-0.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:aa387aa330d2e4bd69995f66ea8fdc87099ddeedf6fdb232993c6a67711e7520", size = 174229, upload-time = "2025-11-24T03:55:31.107Z" }, - { url = "https://files.pythonhosted.org/packages/d9/6f/1e25eee957e58e3afb2a44b94fa95e06cebc4c236193ed0de3012fff1e19/msgspec-0.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2aba22e2e302e9231e85edc24f27ba1f524d43c223ef5765bd8624c7df9ec0a5", size = 196391, upload-time = "2025-11-24T03:55:32.677Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ee/af51d090ada641d4b264992a486435ba3ef5b5634bc27e6eb002f71cef7d/msgspec-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:716284f898ab2547fedd72a93bb940375de9fbfe77538f05779632dc34afdfde", size = 188644, upload-time = "2025-11-24T03:55:33.934Z" }, - { url = "https://files.pythonhosted.org/packages/49/d6/9709ee093b7742362c2934bfb1bbe791a1e09bed3ea5d8a18ce552fbfd73/msgspec-0.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:558ed73315efa51b1538fa8f1d3b22c8c5ff6d9a2a62eff87d25829b94fc5054", size = 218852, upload-time = "2025-11-24T03:55:35.575Z" }, - { url = "https://files.pythonhosted.org/packages/5c/a2/488517a43ccf5a4b6b6eca6dd4ede0bd82b043d1539dd6bb908a19f8efd3/msgspec-0.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:509ac1362a1d53aa66798c9b9fd76872d7faa30fcf89b2fba3bcbfd559d56eb0", size = 224937, upload-time = "2025-11-24T03:55:36.859Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e8/49b832808aa23b85d4f090d1d2e48a4e3834871415031ed7c5fe48723156/msgspec-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1353c2c93423602e7dea1aa4c92f3391fdfc25ff40e0bacf81d34dbc68adb870", size = 222858, upload-time = "2025-11-24T03:55:38.187Z" }, - { url = "https://files.pythonhosted.org/packages/9f/56/1dc2fa53685dca9c3f243a6cbecd34e856858354e455b77f47ebd76cf5bf/msgspec-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb33b5eb5adb3c33d749684471c6a165468395d7aa02d8867c15103b81e1da3e", size = 227248, upload-time = "2025-11-24T03:55:39.496Z" }, - { url = "https://files.pythonhosted.org/packages/5a/51/aba940212c23b32eedce752896205912c2668472ed5b205fc33da28a6509/msgspec-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:fb1d934e435dd3a2b8cf4bbf47a8757100b4a1cfdc2afdf227541199885cdacb", size = 190024, upload-time = "2025-11-24T03:55:40.829Z" }, - { url = "https://files.pythonhosted.org/packages/41/ad/3b9f259d94f183daa9764fef33fdc7010f7ecffc29af977044fa47440a83/msgspec-0.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:00648b1e19cf01b2be45444ba9dc961bd4c056ffb15706651e64e5d6ec6197b7", size = 175390, upload-time = "2025-11-24T03:55:42.05Z" }, - { url = "https://files.pythonhosted.org/packages/8a/d1/b902d38b6e5ba3bdddbec469bba388d647f960aeed7b5b3623a8debe8a76/msgspec-0.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c1ff8db03be7598b50dd4b4a478d6fe93faae3bd54f4f17aa004d0e46c14c46", size = 196463, upload-time = "2025-11-24T03:55:43.405Z" }, - { url = "https://files.pythonhosted.org/packages/57/b6/eff0305961a1d9447ec2b02f8c73c8946f22564d302a504185b730c9a761/msgspec-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f6532369ece217fd37c5ebcfd7e981f2615628c21121b7b2df9d3adcf2fd69b8", size = 188650, upload-time = "2025-11-24T03:55:44.761Z" }, - { url = "https://files.pythonhosted.org/packages/99/93/f2ec1ae1de51d3fdee998a1ede6b2c089453a2ee82b5c1b361ed9095064a/msgspec-0.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9a1697da2f85a751ac3cc6a97fceb8e937fc670947183fb2268edaf4016d1ee", size = 218834, upload-time = "2025-11-24T03:55:46.441Z" }, - { url = "https://files.pythonhosted.org/packages/28/83/36557b04cfdc317ed8a525c4993b23e43a8fbcddaddd78619112ca07138c/msgspec-0.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fac7e9c92eddcd24c19d9e5f6249760941485dff97802461ae7c995a2450111", size = 224917, upload-time = "2025-11-24T03:55:48.06Z" }, - { url = "https://files.pythonhosted.org/packages/8f/56/362037a1ed5be0b88aced59272442c4b40065c659700f4b195a7f4d0ac88/msgspec-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f953a66f2a3eb8d5ea64768445e2bb301d97609db052628c3e1bcb7d87192a9f", size = 222821, upload-time = "2025-11-24T03:55:49.388Z" }, - { url = "https://files.pythonhosted.org/packages/92/75/fa2370ec341cedf663731ab7042e177b3742645c5dd4f64dc96bd9f18a6b/msgspec-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:247af0313ae64a066d3aea7ba98840f6681ccbf5c90ba9c7d17f3e39dbba679c", size = 227227, upload-time = "2025-11-24T03:55:51.125Z" }, - { url = "https://files.pythonhosted.org/packages/f1/25/5e8080fe0117f799b1b68008dc29a65862077296b92550632de015128579/msgspec-0.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:67d5e4dfad52832017018d30a462604c80561aa62a9d548fc2bd4e430b66a352", size = 189966, upload-time = "2025-11-24T03:55:52.458Z" }, - { url = "https://files.pythonhosted.org/packages/79/b6/63363422153937d40e1cb349c5081338401f8529a5a4e216865decd981bf/msgspec-0.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:91a52578226708b63a9a13de287b1ec3ed1123e4a088b198143860c087770458", size = 175378, upload-time = "2025-11-24T03:55:53.721Z" }, - { url = "https://files.pythonhosted.org/packages/bb/18/62dc13ab0260c7d741dda8dc7f481495b93ac9168cd887dda5929880eef8/msgspec-0.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:eead16538db1b3f7ec6e3ed1f6f7c5dec67e90f76e76b610e1ffb5671815633a", size = 196407, upload-time = "2025-11-24T03:55:55.001Z" }, - { url = "https://files.pythonhosted.org/packages/dd/1d/b9949e4ad6953e9f9a142c7997b2f7390c81e03e93570c7c33caf65d27e1/msgspec-0.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:703c3bb47bf47801627fb1438f106adbfa2998fe586696d1324586a375fca238", size = 188889, upload-time = "2025-11-24T03:55:56.311Z" }, - { url = "https://files.pythonhosted.org/packages/1e/19/f8bb2dc0f1bfe46cc7d2b6b61c5e9b5a46c62298e8f4d03bbe499c926180/msgspec-0.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6cdb227dc585fb109305cee0fd304c2896f02af93ecf50a9c84ee54ee67dbb42", size = 219691, upload-time = "2025-11-24T03:55:57.908Z" }, - { url = "https://files.pythonhosted.org/packages/b8/8e/6b17e43f6eb9369d9858ee32c97959fcd515628a1df376af96c11606cf70/msgspec-0.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27d35044dd8818ac1bd0fedb2feb4fbdff4e3508dd7c5d14316a12a2d96a0de0", size = 224918, upload-time = "2025-11-24T03:55:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/1c/db/0e833a177db1a4484797adba7f429d4242585980b90882cc38709e1b62df/msgspec-0.20.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4296393a29ee42dd25947981c65506fd4ad39beaf816f614146fa0c5a6c91ae", size = 223436, upload-time = "2025-11-24T03:56:00.716Z" }, - { url = "https://files.pythonhosted.org/packages/c3/30/d2ee787f4c918fd2b123441d49a7707ae9015e0e8e1ab51aa7967a97b90e/msgspec-0.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:205fbdadd0d8d861d71c8f3399fe1a82a2caf4467bc8ff9a626df34c12176980", size = 227190, upload-time = "2025-11-24T03:56:02.371Z" }, - { url = "https://files.pythonhosted.org/packages/ff/37/9c4b58ff11d890d788e700b827db2366f4d11b3313bf136780da7017278b/msgspec-0.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:7dfebc94fe7d3feec6bc6c9df4f7e9eccc1160bb5b811fbf3e3a56899e398a6b", size = 193950, upload-time = "2025-11-24T03:56:03.668Z" }, - { url = "https://files.pythonhosted.org/packages/e9/4e/cab707bf2fa57408e2934e5197fc3560079db34a1e3cd2675ff2e47e07de/msgspec-0.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:2ad6ae36e4a602b24b4bf4eaf8ab5a441fec03e1f1b5931beca8ebda68f53fc0", size = 179018, upload-time = "2025-11-24T03:56:05.038Z" }, - { url = "https://files.pythonhosted.org/packages/4c/06/3da3fc9aaa55618a8f43eb9052453cfe01f82930bca3af8cea63a89f3a11/msgspec-0.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f84703e0e6ef025663dd1de828ca028774797b8155e070e795c548f76dde65d5", size = 200389, upload-time = "2025-11-24T03:56:06.375Z" }, - { url = "https://files.pythonhosted.org/packages/83/3b/cc4270a5ceab40dfe1d1745856951b0a24fd16ac8539a66ed3004a60c91e/msgspec-0.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c83fc24dd09cf1275934ff300e3951b3adc5573f0657a643515cc16c7dee131", size = 193198, upload-time = "2025-11-24T03:56:07.742Z" }, - { url = "https://files.pythonhosted.org/packages/cd/ae/4c7905ac53830c8e3c06fdd60e3cdcfedc0bbc993872d1549b84ea21a1bd/msgspec-0.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f13ccb1c335a124e80c4562573b9b90f01ea9521a1a87f7576c2e281d547f56", size = 225973, upload-time = "2025-11-24T03:56:09.18Z" }, - { url = "https://files.pythonhosted.org/packages/d9/da/032abac1de4d0678d99eaeadb1323bd9d247f4711c012404ba77ed6f15ca/msgspec-0.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17c2b5ca19f19306fc83c96d85e606d2cc107e0caeea85066b5389f664e04846", size = 229509, upload-time = "2025-11-24T03:56:10.898Z" }, - { url = "https://files.pythonhosted.org/packages/69/52/fdc7bdb7057a166f309e0b44929e584319e625aaba4771b60912a9321ccd/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d931709355edabf66c2dd1a756b2d658593e79882bc81aae5964969d5a291b63", size = 230434, upload-time = "2025-11-24T03:56:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/cb/fe/1dfd5f512b26b53043884e4f34710c73e294e7cc54278c3fe28380e42c37/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:565f915d2e540e8a0c93a01ff67f50aebe1f7e22798c6a25873f9fda8d1325f8", size = 231758, upload-time = "2025-11-24T03:56:13.765Z" }, - { url = "https://files.pythonhosted.org/packages/97/f6/9ba7121b8e0c4e0beee49575d1dbc804e2e72467692f0428cf39ceba1ea5/msgspec-0.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:726f3e6c3c323f283f6021ebb6c8ccf58d7cd7baa67b93d73bfbe9a15c34ab8d", size = 206540, upload-time = "2025-11-24T03:56:15.029Z" }, - { url = "https://files.pythonhosted.org/packages/c8/3e/c5187de84bb2c2ca334ab163fcacf19a23ebb1d876c837f81a1b324a15bf/msgspec-0.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:93f23528edc51d9f686808a361728e903d6f2be55c901d6f5c92e44c6d546bfc", size = 183011, upload-time = "2025-11-24T03:56:16.442Z" }, -] - -[[package]] -name = "narwhals" -version = "2.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/6f/713be67779028d482c6e0f2dde5bc430021b2578a4808c1c9f6d7ad48257/narwhals-2.16.0.tar.gz", hash = "sha256:155bb45132b370941ba0396d123cf9ed192bf25f39c4cea726f2da422ca4e145", size = 618268, upload-time = "2026-02-02T10:31:00.545Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/cc/7cb74758e6df95e0c4e1253f203b6dd7f348bf2f29cf89e9210a2416d535/narwhals-2.16.0-py3-none-any.whl", hash = "sha256:846f1fd7093ac69d63526e50732033e86c30ea0026a44d9b23991010c7d1485d", size = 443951, upload-time = "2026-02-02T10:30:58.635Z" }, -] - -[[package]] -name = "numpy" -version = "2.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, - { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, - { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, - { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, - { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, - { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, - { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, - { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, - { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, - { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, - { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, - { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, - { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, - { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, - { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, - { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, - { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, - { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, - { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, - { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, - { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, - { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, - { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, - { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, - { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, - { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, - { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, - { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, - { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, - { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, - { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, - { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, - { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, - { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, - { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, - { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, - { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, - { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, - { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, - { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, - { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, - { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, - { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, - { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, - { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, - { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, - { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, - { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, - { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, - { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, - { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, - { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, - { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, - { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, - { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, - { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, -] - -[[package]] -name = "packaging" -version = "26.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, -] - -[[package]] -name = "pandas" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "python-dateutil" }, - { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/1e/b184654a856e75e975a6ee95d6577b51c271cd92cb2b020c9378f53e0032/pandas-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d64ce01eb9cdca96a15266aa679ae50212ec52757c79204dbc7701a222401850", size = 10313247, upload-time = "2026-01-21T15:50:15.775Z" }, - { url = "https://files.pythonhosted.org/packages/dd/5e/e04a547ad0f0183bf151fd7c7a477468e3b85ff2ad231c566389e6cc9587/pandas-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:613e13426069793aa1ec53bdcc3b86e8d32071daea138bbcf4fa959c9cdaa2e2", size = 9913131, upload-time = "2026-01-21T15:50:18.611Z" }, - { url = "https://files.pythonhosted.org/packages/a2/93/bb77bfa9fc2aba9f7204db807d5d3fb69832ed2854c60ba91b4c65ba9219/pandas-3.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0192fee1f1a8e743b464a6607858ee4b071deb0b118eb143d71c2a1d170996d5", size = 10741925, upload-time = "2026-01-21T15:50:21.058Z" }, - { url = "https://files.pythonhosted.org/packages/62/fb/89319812eb1d714bfc04b7f177895caeba8ab4a37ef6712db75ed786e2e0/pandas-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b853319dec8d5e0c8b875374c078ef17f2269986a78168d9bd57e49bf650ae", size = 11245979, upload-time = "2026-01-21T15:50:23.413Z" }, - { url = "https://files.pythonhosted.org/packages/a9/63/684120486f541fc88da3862ed31165b3b3e12b6a1c7b93be4597bc84e26c/pandas-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:707a9a877a876c326ae2cb640fbdc4ef63b0a7b9e2ef55c6df9942dcee8e2af9", size = 11756337, upload-time = "2026-01-21T15:50:25.932Z" }, - { url = "https://files.pythonhosted.org/packages/39/92/7eb0ad232312b59aec61550c3c81ad0743898d10af5df7f80bc5e5065416/pandas-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:afd0aa3d0b5cda6e0b8ffc10dbcca3b09ef3cbcd3fe2b27364f85fdc04e1989d", size = 12325517, upload-time = "2026-01-21T15:50:27.952Z" }, - { url = "https://files.pythonhosted.org/packages/51/27/bf9436dd0a4fc3130acec0828951c7ef96a0631969613a9a35744baf27f6/pandas-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:113b4cca2614ff7e5b9fee9b6f066618fe73c5a83e99d721ffc41217b2bf57dd", size = 9881576, upload-time = "2026-01-21T15:50:30.149Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2b/c618b871fce0159fd107516336e82891b404e3f340821853c2fc28c7830f/pandas-3.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c14837eba8e99a8da1527c0280bba29b0eb842f64aa94982c5e21227966e164b", size = 9140807, upload-time = "2026-01-21T15:50:32.308Z" }, - { url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" }, - { url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" }, - { url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" }, - { url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" }, - { url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" }, - { url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" }, - { url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" }, - { url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" }, - { url = "https://files.pythonhosted.org/packages/6f/fa/7f0ac4ca8877c57537aaff2a842f8760e630d8e824b730eb2e859ffe96ca/pandas-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b78d646249b9a2bc191040988c7bb524c92fa8534fb0898a0741d7e6f2ffafa6", size = 10307129, upload-time = "2026-01-21T15:50:52.877Z" }, - { url = "https://files.pythonhosted.org/packages/6f/11/28a221815dcea4c0c9414dfc845e34a84a6a7dabc6da3194498ed5ba4361/pandas-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bc9cba7b355cb4162442a88ce495e01cb605f17ac1e27d6596ac963504e0305f", size = 9850201, upload-time = "2026-01-21T15:50:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/ba/da/53bbc8c5363b7e5bd10f9ae59ab250fc7a382ea6ba08e4d06d8694370354/pandas-3.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c9a1a149aed3b6c9bf246033ff91e1b02d529546c5d6fb6b74a28fea0cf4c70", size = 10354031, upload-time = "2026-01-21T15:50:57.463Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a3/51e02ebc2a14974170d51e2410dfdab58870ea9bcd37cda15bd553d24dc4/pandas-3.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95683af6175d884ee89471842acfca29172a85031fccdabc35e50c0984470a0e", size = 10861165, upload-time = "2026-01-21T15:50:59.32Z" }, - { url = "https://files.pythonhosted.org/packages/a5/fe/05a51e3cac11d161472b8297bd41723ea98013384dd6d76d115ce3482f9b/pandas-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1fbbb5a7288719e36b76b4f18d46ede46e7f916b6c8d9915b756b0a6c3f792b3", size = 11359359, upload-time = "2026-01-21T15:51:02.014Z" }, - { url = "https://files.pythonhosted.org/packages/ee/56/ba620583225f9b85a4d3e69c01df3e3870659cc525f67929b60e9f21dcd1/pandas-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e8b9808590fa364416b49b2a35c1f4cf2785a6c156935879e57f826df22038e", size = 11912907, upload-time = "2026-01-21T15:51:05.175Z" }, - { url = "https://files.pythonhosted.org/packages/c9/8c/c6638d9f67e45e07656b3826405c5cc5f57f6fd07c8b2572ade328c86e22/pandas-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:98212a38a709feb90ae658cb6227ea3657c22ba8157d4b8f913cd4c950de5e7e", size = 9732138, upload-time = "2026-01-21T15:51:07.569Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bf/bd1335c3bf1770b6d8fed2799993b11c4971af93bb1b729b9ebbc02ca2ec/pandas-3.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:177d9df10b3f43b70307a149d7ec49a1229a653f907aa60a48f1877d0e6be3be", size = 9033568, upload-time = "2026-01-21T15:51:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/8e/c6/f5e2171914d5e29b9171d495344097d54e3ffe41d2d85d8115baba4dc483/pandas-3.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2713810ad3806767b89ad3b7b69ba153e1c6ff6d9c20f9c2140379b2a98b6c98", size = 10741936, upload-time = "2026-01-21T15:51:11.693Z" }, - { url = "https://files.pythonhosted.org/packages/51/88/9a0164f99510a1acb9f548691f022c756c2314aad0d8330a24616c14c462/pandas-3.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:15d59f885ee5011daf8335dff47dcb8a912a27b4ad7826dc6cbe809fd145d327", size = 10393884, upload-time = "2026-01-21T15:51:14.197Z" }, - { url = "https://files.pythonhosted.org/packages/e0/53/b34d78084d88d8ae2b848591229da8826d1e65aacf00b3abe34023467648/pandas-3.0.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24e6547fb64d2c92665dd2adbfa4e85fa4fd70a9c070e7cfb03b629a0bbab5eb", size = 10310740, upload-time = "2026-01-21T15:51:16.093Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d3/bee792e7c3d6930b74468d990604325701412e55d7aaf47460a22311d1a5/pandas-3.0.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48ee04b90e2505c693d3f8e8f524dab8cb8aaf7ddcab52c92afa535e717c4812", size = 10700014, upload-time = "2026-01-21T15:51:18.818Z" }, - { url = "https://files.pythonhosted.org/packages/55/db/2570bc40fb13aaed1cbc3fbd725c3a60ee162477982123c3adc8971e7ac1/pandas-3.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66f72fb172959af42a459e27a8d8d2c7e311ff4c1f7db6deb3b643dbc382ae08", size = 11323737, upload-time = "2026-01-21T15:51:20.784Z" }, - { url = "https://files.pythonhosted.org/packages/bc/2e/297ac7f21c8181b62a4cccebad0a70caf679adf3ae5e83cb676194c8acc3/pandas-3.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4a4a400ca18230976724a5066f20878af785f36c6756e498e94c2a5e5d57779c", size = 11771558, upload-time = "2026-01-21T15:51:22.977Z" }, - { url = "https://files.pythonhosted.org/packages/0a/46/e1c6876d71c14332be70239acce9ad435975a80541086e5ffba2f249bcf6/pandas-3.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:940eebffe55528074341a5a36515f3e4c5e25e958ebbc764c9502cfc35ba3faa", size = 10473771, upload-time = "2026-01-21T15:51:25.285Z" }, - { url = "https://files.pythonhosted.org/packages/c0/db/0270ad9d13c344b7a36fa77f5f8344a46501abf413803e885d22864d10bf/pandas-3.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:597c08fb9fef0edf1e4fa2f9828dd27f3d78f9b8c9b4a748d435ffc55732310b", size = 10312075, upload-time = "2026-01-21T15:51:28.5Z" }, - { url = "https://files.pythonhosted.org/packages/09/9f/c176f5e9717f7c91becfe0f55a52ae445d3f7326b4a2cf355978c51b7913/pandas-3.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:447b2d68ac5edcbf94655fe909113a6dba6ef09ad7f9f60c80477825b6c489fe", size = 9900213, upload-time = "2026-01-21T15:51:30.955Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e7/63ad4cc10b257b143e0a5ebb04304ad806b4e1a61c5da25f55896d2ca0f4/pandas-3.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debb95c77ff3ed3ba0d9aa20c3a2f19165cc7956362f9873fce1ba0a53819d70", size = 10428768, upload-time = "2026-01-21T15:51:33.018Z" }, - { url = "https://files.pythonhosted.org/packages/9e/0e/4e4c2d8210f20149fd2248ef3fff26623604922bd564d915f935a06dd63d/pandas-3.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fedabf175e7cd82b69b74c30adbaa616de301291a5231138d7242596fc296a8d", size = 10882954, upload-time = "2026-01-21T15:51:35.287Z" }, - { url = "https://files.pythonhosted.org/packages/c6/60/c9de8ac906ba1f4d2250f8a951abe5135b404227a55858a75ad26f84db47/pandas-3.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:412d1a89aab46889f3033a386912efcdfa0f1131c5705ff5b668dda88305e986", size = 11430293, upload-time = "2026-01-21T15:51:37.57Z" }, - { url = "https://files.pythonhosted.org/packages/a1/69/806e6637c70920e5787a6d6896fd707f8134c2c55cd761e7249a97b7dc5a/pandas-3.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e979d22316f9350c516479dd3a92252be2937a9531ed3a26ec324198a99cdd49", size = 11952452, upload-time = "2026-01-21T15:51:39.618Z" }, - { url = "https://files.pythonhosted.org/packages/cb/de/918621e46af55164c400ab0ef389c9d969ab85a43d59ad1207d4ddbe30a5/pandas-3.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:083b11415b9970b6e7888800c43c82e81a06cd6b06755d84804444f0007d6bb7", size = 9851081, upload-time = "2026-01-21T15:51:41.758Z" }, - { url = "https://files.pythonhosted.org/packages/91/a1/3562a18dd0bd8c73344bfa26ff90c53c72f827df119d6d6b1dacc84d13e3/pandas-3.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:5db1e62cb99e739fa78a28047e861b256d17f88463c76b8dafc7c1338086dca8", size = 9174610, upload-time = "2026-01-21T15:51:44.312Z" }, - { url = "https://files.pythonhosted.org/packages/ce/26/430d91257eaf366f1737d7a1c158677caaf6267f338ec74e3a1ec444111c/pandas-3.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:697b8f7d346c68274b1b93a170a70974cdc7d7354429894d5927c1effdcccd73", size = 10761999, upload-time = "2026-01-21T15:51:46.899Z" }, - { url = "https://files.pythonhosted.org/packages/ec/1a/954eb47736c2b7f7fe6a9d56b0cb6987773c00faa3c6451a43db4beb3254/pandas-3.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cb3120f0d9467ed95e77f67a75e030b67545bcfa08964e349252d674171def2", size = 10410279, upload-time = "2026-01-21T15:51:48.89Z" }, - { url = "https://files.pythonhosted.org/packages/20/fc/b96f3a5a28b250cd1b366eb0108df2501c0f38314a00847242abab71bb3a/pandas-3.0.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33fd3e6baa72899746b820c31e4b9688c8e1b7864d7aec2de7ab5035c285277a", size = 10330198, upload-time = "2026-01-21T15:51:51.015Z" }, - { url = "https://files.pythonhosted.org/packages/90/b3/d0e2952f103b4fbef1ef22d0c2e314e74fc9064b51cee30890b5e3286ee6/pandas-3.0.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8942e333dc67ceda1095227ad0febb05a3b36535e520154085db632c40ad084", size = 10728513, upload-time = "2026-01-21T15:51:53.387Z" }, - { url = "https://files.pythonhosted.org/packages/76/81/832894f286df828993dc5fd61c63b231b0fb73377e99f6c6c369174cf97e/pandas-3.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:783ac35c4d0fe0effdb0d67161859078618b1b6587a1af15928137525217a721", size = 11345550, upload-time = "2026-01-21T15:51:55.329Z" }, - { url = "https://files.pythonhosted.org/packages/34/a0/ed160a00fb4f37d806406bc0a79a8b62fe67f29d00950f8d16203ff3409b/pandas-3.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:125eb901e233f155b268bbef9abd9afb5819db74f0e677e89a61b246228c71ac", size = 11799386, upload-time = "2026-01-21T15:51:57.457Z" }, - { url = "https://files.pythonhosted.org/packages/36/c8/2ac00d7255252c5e3cf61b35ca92ca25704b0188f7454ca4aec08a33cece/pandas-3.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b86d113b6c109df3ce0ad5abbc259fe86a1bd4adfd4a31a89da42f84f65509bb", size = 10873041, upload-time = "2026-01-21T15:52:00.034Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" }, -] - -[[package]] -name = "parso" -version = "0.8.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, -] - -[[package]] -name = "plotly" -version = "6.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "narwhals" }, - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e3/4f/8a10a9b9f5192cb6fdef62f1d77fa7d834190b2c50c0cd256bd62879212b/plotly-6.5.2.tar.gz", hash = "sha256:7478555be0198562d1435dee4c308268187553cc15516a2f4dd034453699e393", size = 7015695, upload-time = "2026-01-14T21:26:51.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/67/f95b5460f127840310d2187f916cf0023b5875c0717fdf893f71e1325e87/plotly-6.5.2-py3-none-any.whl", hash = "sha256:91757653bd9c550eeea2fa2404dba6b85d1e366d54804c340b2c874e5a7eb4a4", size = 9895973, upload-time = "2026-01-14T21:26:47.135Z" }, -] - -[[package]] -name = "psutil" -version = "7.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, - { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, - { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, - { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, - { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, - { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, - { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, - { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, - { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, - { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, - { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, - { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, - { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, - { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, - { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, - { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, - { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, - { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, -] - -[[package]] -name = "pygments" -version = "2.19.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, -] - -[[package]] -name = "pymdown-extensions" -version = "10.21" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/63/06673d1eb6d8f83c0ea1f677d770e12565fb516928b4109c9e2055656a9e/pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5", size = 853363, upload-time = "2026-02-15T20:44:06.748Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f", size = 268877, upload-time = "2026-02-15T20:44:05.464Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - [[package]] name = "rhiza-core" version = "0.1.0" source = { virtual = "." } - -[package.dev-dependencies] -dev = [ - { name = "marimo" }, - { name = "numpy" }, - { name = "pandas" }, - { name = "plotly" }, - { name = "pyyaml" }, -] - -[package.metadata] - -[package.metadata.requires-dev] -dev = [ - { name = "marimo", specifier = ">=0.18.0,<1.0" }, - { name = "numpy", specifier = ">=2.4.0,<3.0" }, - { name = "pandas", specifier = ">=3,<3.1" }, - { name = "plotly", specifier = ">=6.5.0,<7.0" }, - { name = "pyyaml", specifier = ">=6.0,<7.0" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "starlette" -version = "0.52.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, -] - -[[package]] -name = "tomlkit" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "tzdata" -version = "2025.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, -] - -[[package]] -name = "uvicorn" -version = "0.40.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, -] - -[[package]] -name = "websockets" -version = "16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, - { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, - { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, - { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, - { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, - { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, - { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, - { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, - { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, - { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, - { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, - { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, -] From c18065f1b5d2f0bb34e2c25b3fb1bb34311db6fb Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 20:45:13 +0400 Subject: [PATCH 11/31] Remove Codespaces badge from README Removed GitHub Codespaces badge from README. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index ef3e0bd..c28adb7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ # Rhiza Logo Rhiza Core [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/jebel-quant/rhiza-core) # Strong roots The foundational layer for language-agnostic Rhiza templates. From 754a4b5fb125d50f93d38e0ee08db7183384adeb Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 21:29:39 +0400 Subject: [PATCH 12/31] Remove scripts directory from template bundles Removed the scripts directory from template bundles. --- .rhiza/template-bundles.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.rhiza/template-bundles.yml b/.rhiza/template-bundles.yml index 49665d1..91ab8a5 100644 --- a/.rhiza/template-bundles.yml +++ b/.rhiza/template-bundles.yml @@ -41,7 +41,6 @@ bundles: - .rhiza/make.d/custom-env.mk - .rhiza/make.d/custom-task.mk - .rhiza/make.d/README.md - - .rhiza/scripts - .rhiza/docs - .rhiza/assets From c7541c789499a20b112e9fa154562def355e41b6 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 21:30:35 +0400 Subject: [PATCH 13/31] Delete .gitlab directory --- .gitlab/COMPARISON.md | 256 --------------- .gitlab/README.md | 293 ------------------ .gitlab/SUMMARY.md | 207 ------------- .gitlab/TESTING.md | 288 ----------------- .../template/marimo_job_template.yml.jinja | 20 -- .gitlab/workflows/rhiza_book.yml | 50 --- .gitlab/workflows/rhiza_ci.yml | 41 --- .gitlab/workflows/rhiza_deptry.yml | 21 -- .gitlab/workflows/rhiza_pre-commit.yml | 26 -- .gitlab/workflows/rhiza_release.yml | 200 ------------ .gitlab/workflows/rhiza_renovate.yml | 62 ---- .gitlab/workflows/rhiza_sync.yml | 81 ----- .gitlab/workflows/rhiza_validate.yml | 24 -- 13 files changed, 1569 deletions(-) delete mode 100644 .gitlab/COMPARISON.md delete mode 100644 .gitlab/README.md delete mode 100644 .gitlab/SUMMARY.md delete mode 100644 .gitlab/TESTING.md delete mode 100644 .gitlab/template/marimo_job_template.yml.jinja delete mode 100644 .gitlab/workflows/rhiza_book.yml delete mode 100644 .gitlab/workflows/rhiza_ci.yml delete mode 100644 .gitlab/workflows/rhiza_deptry.yml delete mode 100644 .gitlab/workflows/rhiza_pre-commit.yml delete mode 100644 .gitlab/workflows/rhiza_release.yml delete mode 100644 .gitlab/workflows/rhiza_renovate.yml delete mode 100644 .gitlab/workflows/rhiza_sync.yml delete mode 100644 .gitlab/workflows/rhiza_validate.yml diff --git a/.gitlab/COMPARISON.md b/.gitlab/COMPARISON.md deleted file mode 100644 index fac0fcc..0000000 --- a/.gitlab/COMPARISON.md +++ /dev/null @@ -1,256 +0,0 @@ -# GitHub Actions vs GitLab CI Comparison - -This document provides a side-by-side comparison of GitHub Actions and GitLab CI implementations for the rhiza project. - -## Workflow Mapping - -| Feature | GitHub Actions | GitLab CI | Status | -|---------|----------------|-----------|--------| -| Main Config | `.github/workflows/*.yml` | `.gitlab-ci.yml` + `.gitlab/workflows/*.yml` | ✅ Complete | -| CI Testing | `rhiza_ci.yml` | `rhiza_ci.yml` | ✅ Complete | -| Validation | `rhiza_validate.yml` | `rhiza_validate.yml` | ✅ Complete | -| Dependencies | `rhiza_deptry.yml` | `rhiza_deptry.yml` | ✅ Complete | -| Pre-commit | `rhiza_pre-commit.yml` | `rhiza_pre-commit.yml` | ✅ Complete | -| Documentation | `rhiza_book.yml` | `rhiza_book.yml` | ✅ Complete | -| Sync | `rhiza_sync.yml` | `rhiza_sync.yml` | ✅ Complete | -| Release | `rhiza_release.yml` | `rhiza_release.yml` | ✅ Complete | - -## Syntax Differences - -### Triggers - -**GitHub Actions:** -```yaml -on: - push: - pull_request: - branches: [main, master] -``` - -**GitLab CI:** -```yaml -rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - if: $CI_COMMIT_BRANCH -``` - -### Jobs and Steps - -**GitHub Actions:** -```yaml -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Run tests - run: pytest tests -``` - -**GitLab CI:** -```yaml -test: - stage: test - image: python:3.12 - script: - - pytest tests -``` - -### Matrix Strategy - -**GitHub Actions:** -```yaml -strategy: - matrix: - python-version: ${{ fromJson(needs.generate-matrix.outputs.matrix) }} -``` - -**GitLab CI:** -```yaml -parallel: - matrix: - - PYTHON_VERSION: ["3.11", "3.12", "3.13"] -``` - -**Note:** GitLab CI has limited dynamic matrix support. Workaround: use child pipelines or iterate in script. - -### Artifacts - -**GitHub Actions:** -```yaml -- uses: actions/upload-artifact@v6 - with: - name: dist - path: dist -``` - -**GitLab CI:** -```yaml -artifacts: - paths: - - dist/ - expire_in: 1 day -``` - -### Container Images - -**GitHub Actions:** -```yaml -jobs: - test: - runs-on: ubuntu-latest - container: - image: python:3.12 -``` - -**GitLab CI:** -```yaml -test: - image: python:3.12 -``` - -### Secrets and Variables - -**GitHub Actions:** -```yaml -env: - TOKEN: ${{ secrets.PYPI_TOKEN }} - CUSTOM_VAR: ${{ vars.CUSTOM_VAR }} -``` - -**GitLab CI:** -```yaml -script: - - echo $PYPI_TOKEN - - echo $CUSTOM_VAR -``` - -## Feature Comparison - -### 1. Python Matrix Testing (CI) - -| Feature | GitHub Actions | GitLab CI | -|---------|----------------|-----------| -| Dynamic matrix | ✅ Full support | ⚠️ Limited (static matrix used) | -| Parallel execution | ✅ Yes | ✅ Yes | -| Git LFS | ✅ Yes | ✅ Yes | -| UV integration | ✅ Yes | ✅ Yes | - -### 2. Documentation (Book) - -| Feature | GitHub Actions | GitLab CI | -|---------|----------------|-----------| -| Build | ✅ `make book` | ✅ `make book` | -| Output directory | `_book/` | `public/` (required) | -| Deployment | GitHub Pages | GitLab Pages | -| Deploy action | `actions/deploy-pages@v4` | Job named `pages` | - -### 3. Release - -| Feature | GitHub Actions | GitLab CI | -|---------|----------------|-----------| -| PyPI auth | ✅ OIDC Trusted Publishing | ⚠️ Token-based | -| Release creation | `softprops/action-gh-release` | GitLab Releases API | -| Version validation | ✅ Yes | ✅ Yes | -| Draft releases | ✅ Yes | ✅ Yes (via API) | - -### 4. Sync - -| Feature | GitHub Actions | GitLab CI | -|---------|----------------|-----------| -| Template sync | ✅ `rhiza materialize` | ✅ `rhiza materialize` | -| PR/MR creation | ✅ Automatic | ⚠️ Manual (API call needed) | -| Token requirement | PAT_TOKEN | PAT_TOKEN | -| Scheduling | ✅ Cron syntax | ✅ Pipeline schedules | - -## Platform-Specific Features - -### GitHub Actions Only - -1. **OIDC Authentication:** Passwordless authentication with PyPI and cloud providers -2. **Action Marketplace:** Reusable actions from the community -3. **Job summaries:** Rich markdown summaries in the UI -4. **Environments:** Built-in environment protection rules - -### GitLab CI Only - -1. **Stages:** Explicit pipeline stages (`.pre`, `build`, `test`, `deploy`, `.post`) -2. **Job templates:** Reusable job definitions with `extends` -3. **Child pipelines:** Dynamic pipeline generation -4. **Auto DevOps:** Automatic CI/CD configuration - -## Migration Considerations - -### Easy Migrations -- ✅ Basic CI/CD pipelines -- ✅ Docker-based workflows -- ✅ Artifact handling -- ✅ Environment variables -- ✅ Scheduled pipelines - -### Moderate Effort -- ⚠️ Dynamic matrix strategies (use child pipelines) -- ⚠️ Marketplace actions (reimplement with scripts) -- ⚠️ Complex conditionals (restructure with rules) - -### Challenging Migrations -- ❌ OIDC-based authentication (use tokens) -- ❌ GitHub-specific APIs (use GitLab APIs) -- ❌ GitHub Apps (use GitLab integrations) - -## Testing Status - -| Workflow | YAML Valid | Logic Verified | Notes | -|----------|------------|----------------|-------| -| CI | ✅ | ⏳ | Needs test with actual Python matrix | -| Validate | ✅ | ⏳ | Skips in rhiza repo | -| Deptry | ✅ | ⏳ | Needs test with dependencies | -| Pre-commit | ✅ | ⏳ | Needs test with hooks | -| Book | ✅ | ⏳ | Needs GitLab Pages setup | -| Sync | ✅ | ⏳ | Needs PAT_TOKEN | -| Release | ✅ | ⏳ | Needs PYPI_TOKEN | - -Legend: -- ✅ Complete -- ⏳ Pending (ready for testing) -- ❌ Not tested - -## Recommendations - -### For New Projects -1. Choose platform based on primary hosting (GitHub vs GitLab) -2. Consider OIDC requirements (GitHub has better support) -3. Evaluate marketplace actions vs custom scripts - -### For Migration -1. Start with simple workflows (CI, pre-commit) -2. Test thoroughly in a fork/mirror -3. Configure all required variables -4. Update documentation links -5. Train team on platform differences - -### For Dual Support -1. Maintain both workflow sets (as done here) -2. Keep feature parity -3. Document platform-specific differences -4. Test changes on both platforms - -## Resources - -- **GitHub Actions Docs:** https://docs.github.com/en/actions -- **GitLab CI Docs:** https://docs.gitlab.com/ee/ci/ -- **Rhiza GitHub:** https://github.com/jebel-quant/rhiza -- **Migration Guide:** `.gitlab/README.md` -- **Testing Guide:** `.gitlab/TESTING.md` - -## Summary - -Most GitHub Actions workflows (7 of 10) have been converted to GitLab CI with equivalent functionality. The main differences are: - -1. **Syntax:** Different trigger and job definitions -2. **Authentication:** Token-based instead of OIDC -3. **Matrix:** Static instead of dynamic -4. **Pages:** Different output directory requirements -5. **APIs:** Platform-specific endpoints - -Both platforms are fully supported and provide equivalent functionality for the rhiza project. diff --git a/.gitlab/README.md b/.gitlab/README.md deleted file mode 100644 index b0523a7..0000000 --- a/.gitlab/README.md +++ /dev/null @@ -1,293 +0,0 @@ -# GitLab CI/CD Workflows for Rhiza - -This directory contains GitLab CI/CD workflow configurations that mirror the functionality of the GitHub Actions workflows in `.github/workflows/`. - -## Structure - -``` -.gitlab/ -├── workflows/ -│ ├── rhiza_ci.yml # Continuous Integration - Python matrix testing -│ ├── rhiza_validate.yml # Rhiza configuration validation -│ ├── rhiza_deptry.yml # Dependency checking -│ ├── rhiza_pre-commit.yml # Pre-commit hooks -│ ├── rhiza_book.yml # Documentation building (GitLab Pages) -│ ├── rhiza_sync.yml # Template synchronization -│ └── rhiza_release.yml # Release workflow -├── template/ # GitLab CI job templates -│ └── marimo_job_template.yml.jinja -└── README.md # This file - -.gitlab-ci.yml # Main GitLab CI configuration (includes all workflows) -``` - -## Workflows - -### 1. CI (`rhiza_ci.yml`) -**Purpose:** Run tests on multiple Python versions to ensure compatibility. - -**Trigger:** -- On push to any branch -- On merge requests to main/master - -**Key Features:** -- Dynamic Python version matrix generation -- Tests on Python 3.11, 3.12, 3.13 -- Git LFS support -- UV package manager for dependency management - -**Equivalent GitHub Action:** `.github/workflows/rhiza_ci.yml` - ---- - -### 2. Validate (`rhiza_validate.yml`) -**Purpose:** Validate Rhiza configuration against template. - -**Trigger:** -- On push to any branch -- On merge requests to main/master - -**Key Features:** -- Skips validation in the rhiza repository itself -- Uses uvx for ephemeral environment - -**Equivalent GitHub Action:** `.github/workflows/rhiza_validate.yml` - ---- - -### 3. Deptry (`rhiza_deptry.yml`) -**Purpose:** Check for missing and obsolete dependencies. - -**Trigger:** -- On push to any branch -- On merge requests to main/master - -**Key Features:** -- Automatic source folder detection -- Identifies unused dependencies - -**Equivalent GitHub Action:** `.github/workflows/rhiza_deptry.yml` - ---- - -### 4. Pre-commit (`rhiza_pre-commit.yml`) -**Purpose:** Run pre-commit checks for code quality. - -**Trigger:** -- On push to any branch -- On merge requests to main/master - -**Key Features:** -- Runs all pre-commit hooks -- UV environment setup - -**Equivalent GitHub Action:** `.github/workflows/rhiza_pre-commit.yml` - ---- - -### 5. Book (`rhiza_book.yml`) -**Purpose:** Build and deploy documentation to GitLab Pages. - -**Trigger:** -- On push to main/master branch - -**Key Features:** -- Combines API docs, test coverage, and notebooks -- Deploys to GitLab Pages -- Controlled by `PUBLISH_COMPANION_BOOK` variable - -**Equivalent GitHub Action:** `.github/workflows/rhiza_book.yml` - -**GitLab-specific:** Outputs to `public/` directory for GitLab Pages. - ---- - -### 6. Sync (`rhiza_sync.yml`) -**Purpose:** Synchronize repository with its template. - -**Trigger:** -- Scheduled (can be set in GitLab) -- Manual trigger -- Web pipeline trigger - -**Key Features:** -- Template materialization with rhiza -- Automatic branch creation -- Manual merge request creation - -**Equivalent GitHub Action:** `.github/workflows/rhiza_sync.yml` - -**GitLab-specific:** Requires Project/Group Access Token (PAT_TOKEN) for workflow modifications. - ---- - -### 7. Release (`rhiza_release.yml`) -**Purpose:** Create releases and publish packages to PyPI. - -**Trigger:** -- On version tags (e.g., `v1.2.3`) - -**Key Features:** -- Version validation -- Python package building with Hatch -- PyPI publishing with twine -- GitLab release creation - -**Equivalent GitHub Action:** `.github/workflows/rhiza_release.yml` - -**GitLab-specific:** -- Uses GitLab Releases API instead of GitHub Releases -- Uses PYPI_TOKEN instead of OIDC Trusted Publishing - ---- - -## Key Differences from GitHub Actions - -### 1. **Syntax and Structure** -- **GitHub Actions:** Uses `jobs` and `steps` with `uses` for actions -- **GitLab CI:** Uses `jobs` with `script` and `before_script` sections - -### 2. **Triggers** -- **GitHub Actions:** `on: push`, `on: pull_request` -- **GitLab CI:** `rules` with conditions like `if: $CI_PIPELINE_SOURCE == "merge_request_event"` - -### 3. **Artifacts and Caching** -- **GitHub Actions:** `actions/upload-artifact@v6`, `actions/download-artifact@v7` -- **GitLab CI:** `artifacts: paths:` and automatic artifact passing between stages - -### 4. **Container Images** -- **GitHub Actions:** `runs-on: ubuntu-latest` with `uses: actions/setup-python` -- **GitLab CI:** `image: python:3.12` or specific Docker images - -### 5. **Matrix Strategy** -- **GitHub Actions:** Built-in `strategy.matrix` with dynamic values from JSON -- **GitLab CI:** `parallel.matrix` with limited dynamic support (workaround: child pipelines) - -### 6. **Pages Deployment** -- **GitHub Actions:** `actions/deploy-pages@v4` -- **GitLab CI:** Job named `pages` with `artifacts: paths: [public]` - -### 7. **Secrets and Variables** -- **GitHub Actions:** `secrets.*` and `vars.*` -- **GitLab CI:** `$CI_VARIABLE_NAME` or protected/masked variables - -### 8. **Release Management** -- **GitHub Actions:** `softprops/action-gh-release@v2` -- **GitLab CI:** GitLab Releases API with `curl` commands - -### 9. **Authentication** -- **GitHub Actions:** OIDC Trusted Publishing for PyPI, `GITHUB_TOKEN` for registry -- **GitLab CI:** Token-based authentication with `PYPI_TOKEN`, `CI_JOB_TOKEN` for registry - -### 10. **Conditional Execution** -- **GitHub Actions:** `if:` conditions at job/step level -- **GitLab CI:** `rules:` at job level, `when:` for manual/conditional execution - ---- - -## Configuration Variables - -These variables can be set in GitLab CI/CD settings (Settings > CI/CD > Variables): - -| Variable | Default | Description | -|----------|---------|-------------| -| `UV_EXTRA_INDEX_URL` | `""` | Extra index URL for UV package manager | -| `PYPI_REPOSITORY_URL` | `""` | Custom PyPI repository URL (empty = pypi.org) | -| `PYPI_TOKEN` | N/A | **Secret** - PyPI authentication token | -| `PUBLISH_COMPANION_BOOK` | `true` | Whether to publish documentation | -| `CREATE_MR` | `true` | Whether to create merge request on sync | -| `PAT_TOKEN` | N/A | **Secret** - Project/Group Access Token for sync | - -### Setting Variables - -1. Navigate to your GitLab project -2. Go to **Settings > CI/CD > Variables** -3. Click **Add variable** -4. Enter the variable name and value -5. Mark as **Protected** for production variables -6. Mark as **Masked** for sensitive values - ---- - -## Testing GitLab CI Locally - -You can validate the GitLab CI configuration syntax using: - -```bash -# Install GitLab CI Lint tool -curl --header "PRIVATE-TOKEN: " \ - "https://gitlab.com/api/v4/projects//ci/lint" \ - --data-urlencode "content@.gitlab-ci.yml" -``` - -Or use the GitLab UI: -1. Go to **CI/CD > Pipelines** -2. Click **CI Lint** button (or go to `/ci/lint`) -3. Paste your `.gitlab-ci.yml` content -4. Click **Validate** - ---- - -## Migration Checklist - -When migrating from GitHub Actions to GitLab CI: - -- [ ] Set required CI/CD variables (especially secrets like `PYPI_TOKEN`) -- [ ] Configure Project/Group Access Token for `PAT_TOKEN` (if using sync) -- [ ] Enable GitLab Pages in project settings (if using book) -- [ ] Configure scheduled pipelines for sync workflow -- [ ] Update any repository-specific configurations -- [ ] Test each workflow individually -- [ ] Verify release workflow with a test tag -- [ ] Update documentation links - ---- - -## Troubleshooting - -### Common Issues - -1. **Pipeline fails with "permission denied"** - - Check if required variables are set - - Verify token permissions - -2. **Pages deployment doesn't work** - - Ensure job is named `pages` - - Verify artifacts are in `public/` directory - - Check if GitLab Pages is enabled - -3. **Matrix jobs don't run in parallel** - - GitLab CI has limitations on dynamic matrices - - Consider using child pipelines for true parallelism - -4. **Release workflow fails** - - Verify `PYPI_TOKEN` is set - - Check tag format (must start with `v`) - - Ensure version in pyproject.toml matches tag - ---- - -## Support - -For issues specific to: -- **GitLab CI syntax:** Refer to [GitLab CI/CD Documentation](https://docs.gitlab.com/ee/ci/) -- **Rhiza workflows:** See main repository README -- **Workflow behavior:** Compare with corresponding GitHub Actions workflows - ---- - -## Contributing - -When adding or modifying workflows: - -1. Update both `.gitlab/workflows/*.yml` and `.github/workflows/*.yml` -2. Keep feature parity between GitHub Actions and GitLab CI -3. Document any platform-specific differences -4. Test changes in a fork before merging -5. Update this README with new workflows or variables - ---- - -## License - -These workflows are part of the jebel-quant/rhiza repository and follow the same license. diff --git a/.gitlab/SUMMARY.md b/.gitlab/SUMMARY.md deleted file mode 100644 index fd9ea98..0000000 --- a/.gitlab/SUMMARY.md +++ /dev/null @@ -1,207 +0,0 @@ -# GitLab CI/CD Implementation Summary - -## Overview - -This implementation provides complete GitLab CI/CD equivalents for all GitHub Actions workflows in the rhiza repository. The workflows maintain feature parity with GitHub Actions while adapting to GitLab CI's specific capabilities and constraints. - -## What Was Created - -### Directory Structure -``` -.gitlab-ci.yml # Main configuration file -.gitlab/ -├── README.md # Comprehensive documentation (10.7 KB) -├── TESTING.md # Testing guide (9.9 KB) -├── COMPARISON.md # Platform comparison (7.3 KB) -├── SUMMARY.md # This file -├── validate.sh # Validation script -└── workflows/ # Individual workflow definitions - ├── rhiza_ci.yml (1.3 KB) - Python matrix testing - ├── rhiza_validate.yml (0.7 KB) - Config validation - ├── rhiza_deptry.yml (0.9 KB) - Dependency check - ├── rhiza_pre-commit.yml (0.9 KB) - Code quality - ├── rhiza_book.yml (1.7 KB) - Documentation - ├── rhiza_sync.yml (2.8 KB) - Template sync - └── rhiza_release.yml (9.4 KB) - Release pipeline - -GITLAB_CI.md # Quick start guide (3.6 KB) -``` - -**Total:** 14 files, 56+ KB of configuration and documentation - -### Workflow Coverage - -| # | Workflow | Status | Lines | Features | -|---|----------|--------|-------|----------| -| 1 | CI Testing | ✅ Complete | 50 | Python matrix, parallel tests, Git LFS | -| 2 | Validation | ✅ Complete | 24 | Rhiza config validation, conditional skip | -| 3 | Deptry | ✅ Complete | 28 | Dependency checking, auto source detection | -| 4 | Pre-commit | ✅ Complete | 29 | All hooks, UV environment | -| 5 | Book/Pages | ✅ Complete | 52 | GitLab Pages, docs building | -| 6 | Sync | ✅ Complete | 82 | Template sync, branch creation | -| 7 | Release | ✅ Complete | 287 | PyPI, GitLab releases | - -**Total:** 675 lines of CI/CD configuration - -## Key Features - -### ✅ Current Coverage -- 7 of 10 GitHub Actions workflows have GitLab CI equivalents -- Same functionality where implemented, adapted for GitLab CI syntax -- Maintained compatibility with existing project structure - -### ✅ Comprehensive Documentation -- **README.md**: Full workflow documentation with examples -- **TESTING.md**: Step-by-step testing guide for each workflow -- **COMPARISON.md**: Side-by-side platform comparison -- **GITLAB_CI.md**: Quick start guide for users -- **SUMMARY.md**: Implementation overview (this file) - -### ✅ Platform Adaptations -- GitLab Pages integration (public/ directory) -- GitLab Container Registry support -- GitLab Releases API integration -- Token-based authentication (vs OIDC) -- Rule-based triggers (vs on:) - -### ✅ Quality Assurance -- YAML syntax validation (all included YAML files valid) -- Validation script for automated checks -- Workflow count verification (7/10 currently matched) -- Documentation completeness check - -## Platform Differences Addressed - -### 1. Dynamic Matrix Strategy -**Challenge:** GitLab CI has limited dynamic matrix support. -**Solution:** Static matrix for Python versions; documented workaround using child pipelines. - -### 2. Authentication -**Challenge:** GitLab doesn't support OIDC-based PyPI publishing. -**Solution:** Token-based authentication with PYPI_TOKEN variable. - -### 3. Pages Deployment -**Challenge:** Different deployment mechanisms. -**Solution:** Job named `pages` with artifacts in `public/` directory. - -### 4. Merge Request Creation -**Challenge:** Sync workflow needs to create merge requests. -**Solution:** Documented manual/API approach; branch auto-pushed. - -### 5. Release Management -**Challenge:** Different API for release creation. -**Solution:** GitLab Releases API integration with curl commands. - -## Validation Results - -``` -✅ YAML Syntax: All 8 YAML files valid -✅ Workflow Count: 7/10 matched with GitHub Actions -✅ Documentation: All 4 docs present -✅ Structure: Complete directory hierarchy -✅ Validation Script: Working and executable -``` - -## Configuration Requirements - -### Required Secrets (for full functionality) -- `PYPI_TOKEN` - PyPI publishing (releases) -- `PAT_TOKEN` - Workflow modifications (sync) - -### Optional Variables -- `UV_EXTRA_INDEX_URL` - Package index -- `PYPI_REPOSITORY_URL` - Custom PyPI feed -- `PUBLISH_COMPANION_BOOK` - Enable documentation - -## Testing Status - -| Category | Status | Details | -|----------|--------|---------| -| YAML Syntax | ✅ Validated | All 8 YAML files pass YAML parsing | -| Structure | ✅ Complete | All workflows and docs present | -| Logic | ⏳ Ready | Awaiting GitLab repository setup | -| Integration | ⏳ Ready | Requires CI/CD variable configuration | - -## Next Steps for Users - -1. **Setup Repository** - - Create GitLab repository (mirror/fork) - - Enable GitLab Pages (if using book workflow) - -2. **Configure Variables** - - Set required secrets (PYPI_TOKEN, PAT_TOKEN) - - Configure optional variables as needed - - Test with protected/masked settings - -3. **Test Workflows** - - Push to test branch (triggers CI, marimo, etc.) - - Push to main (triggers book/pages) - - Manual trigger (triggers sync) - - Push version tag (triggers release) - -4. **Verify Deployments** - - Check GitLab Pages URL - - Verify container images in registry - - Confirm PyPI package uploads - -## Maintenance Guidelines - -### When Adding New Workflows -1. Create equivalent files in both `.github/workflows/` and `.gitlab/workflows/` -2. Update `.gitlab-ci.yml` to include new workflow -3. Document in `.gitlab/README.md` -4. Add testing instructions to `.gitlab/TESTING.md` -5. Update comparison in `.gitlab/COMPARISON.md` - -### When Modifying Existing Workflows -1. Keep both GitHub and GitLab versions in sync -2. Document any platform-specific differences -3. Test on both platforms if possible -4. Update relevant documentation - -### Version Updates -- UV version: Update in all workflow files (currently 0.9.18) -- Python versions: Update matrix in CI workflow -- Docker versions: Update base images as needed - -## Success Metrics - -- ✅ 7/10 workflows converted -- ✅ 100% YAML validation pass rate -- ✅ 56+ KB of documentation -- ✅ 675 lines of CI/CD configuration -- ✅ Automated validation script -- ✅ Feature parity with GitHub Actions - -## Support Resources - -- **Quick Start:** `GITLAB_CI.md` -- **Full Docs:** `.gitlab/README.md` -- **Testing:** `.gitlab/TESTING.md` -- **Comparison:** `.gitlab/COMPARISON.md` -- **Validation:** `.gitlab/validate.sh` - -## Contributing - -This implementation is designed to be: -- **Maintainable:** Clear structure and documentation -- **Testable:** Validation script and testing guide -- **Extensible:** Easy to add new workflows -- **Documented:** Comprehensive guides for all use cases - -When contributing, please maintain feature parity between GitHub Actions and GitLab CI, and update all relevant documentation. - -## Conclusion - -This implementation successfully provides complete GitLab CI/CD coverage for the rhiza project. All GitHub Actions workflows have equivalent GitLab CI implementations with comprehensive documentation and testing guides. - -The workflows are production-ready and only require: -1. GitLab repository setup -2. CI/CD variable configuration -3. Initial testing to verify environment-specific settings - ---- - -**Implementation Date:** December 26, 2024 -**Status:** ✅ Complete and Ready for Testing -**Validation:** ✅ All checks passed diff --git a/.gitlab/TESTING.md b/.gitlab/TESTING.md deleted file mode 100644 index 8bbcd93..0000000 --- a/.gitlab/TESTING.md +++ /dev/null @@ -1,288 +0,0 @@ -# GitLab CI/CD Testing Guide - -This document provides instructions for testing the GitLab CI/CD workflows. - -## Prerequisites - -1. A GitLab account (gitlab.com or self-hosted) -2. A GitLab repository (can be a mirror/fork of the GitHub repository) -3. Access to GitLab CI/CD settings - -## Testing Approach - -Since this is the rhiza repository itself, comprehensive testing requires: -1. Creating a test project/fork in GitLab -2. Configuring CI/CD variables -3. Triggering each workflow type - -## Quick Validation - -### 1. YAML Syntax Validation - -All workflow files have been validated for YAML syntax: - -```bash -# Validate all YAML files -for file in .gitlab-ci.yml .gitlab/workflows/*.yml; do - python3 -c "import yaml; yaml.safe_load(open('$file'))" && \ - echo "✅ $file is valid YAML" -done -``` - -**Result:** All 8 files (1 main + 7 workflows) are valid YAML. - -### 2. GitLab CI Lint - -You can validate the configuration using GitLab's CI Lint tool: - -**Option A: Web UI** -1. Go to your GitLab project -2. Navigate to **CI/CD > Pipelines** -3. Click **CI Lint** button (or visit `/-/ci/lint`) -4. Paste the contents of `.gitlab-ci.yml` -5. Click **Validate** - -**Option B: API** (requires GitLab access token) -```bash -curl --header "PRIVATE-TOKEN: " \ - "https://gitlab.com/api/v4/projects//ci/lint" \ - --form "content=@.gitlab-ci.yml" -``` - -## Workflow-Specific Testing - -### 1. CI Workflow (`rhiza_ci.yml`) - -**Test trigger:** Push to any branch or create merge request - -**Expected behavior:** -- Generates Python version matrix (3.11, 3.12, 3.13) -- Runs tests in parallel for each Python version -- Uses UV for dependency management -- Supports Git LFS - -**Manual test:** -```bash -# Push to a test branch -git checkout -b test-gitlab-ci -git push origin test-gitlab-ci -``` - -**Success criteria:** -- Pipeline shows 3 test jobs (one per Python version) -- All tests pass -- Git LFS files are downloaded correctly - ---- - -### 2. Validate Workflow (`rhiza_validate.yml`) - -**Test trigger:** Push to any branch or create merge request - -**Expected behavior:** -- Skips in the rhiza repository itself -- Runs `rhiza validate .` in downstream projects - -**Manual test:** -This workflow is designed for repositories that use rhiza as a template, not for rhiza itself. - -**Success criteria:** -- Job skips in rhiza repository -- Would run and validate in downstream projects - ---- - -### 3. Deptry Workflow (`rhiza_deptry.yml`) - -**Test trigger:** Push to any branch or create merge request - -**Expected behavior:** -- Checks for missing/obsolete dependencies -- Automatically detects source folder (`src/` or `.`) -- Reports unused dependencies - -**Manual test:** -```bash -# Run deptry locally -uvx deptry src/ -``` - -**Success criteria:** -- Dependency check completes -- No critical dependency issues found - ---- - -### 4. Pre-commit Workflow (`rhiza_pre-commit.yml`) - -**Test trigger:** Push to any branch or create merge request - -**Expected behavior:** -- Runs all pre-commit hooks -- Checks code formatting, linting, etc. - -**Manual test:** -```bash -# Run pre-commit locally -uv run pre-commit run --all-files -``` - -**Success criteria:** -- All pre-commit checks pass -- No formatting or linting issues - ---- - -### 5. Book Workflow (`rhiza_book.yml`) - -**Test trigger:** Push to `main` or `master` branch - -**Expected behavior:** -- Builds comprehensive documentation -- Combines API docs, tests, coverage, notebooks -- Deploys to GitLab Pages (public/ directory) - -**Manual test:** -```bash -# Build book locally -make book -ls -la _book/ -``` - -**Success criteria:** -- Documentation builds successfully -- GitLab Pages deployment succeeds -- Pages are accessible at `https://.gitlab.io//` - -**Configuration needed:** -- Enable GitLab Pages in project settings -- Set `PUBLISH_COMPANION_BOOK=true` (default) - ---- - -### 6. Sync Workflow (`rhiza_sync.yml`) - -**Test trigger:** Manual pipeline, scheduled pipeline, or web trigger - -**Expected behavior:** -- Syncs repository with template -- Creates a new branch -- Commits changes -- Optionally creates merge request - -**Manual test:** -```bash -# Trigger manually from GitLab UI -# CI/CD > Pipelines > Run pipeline -``` - -**Success criteria:** -- Template synchronization completes -- New branch created if changes detected -- No changes if already in sync - -**Configuration needed:** -- Set `PAT_TOKEN` for workflow modifications -- Set `CREATE_MR=true` to auto-create merge requests - ---- - -### 7. Release Workflow (`rhiza_release.yml`) - -**Test trigger:** Push a version tag (e.g., `v1.0.0`) - -**Expected behavior:** -- Validates tag format -- Builds Python package -- Creates GitLab release -- Publishes to PyPI (if configured) -- Finalizes release with links - -**Manual test:** -```bash -# Create and push a test tag -git tag v0.0.1-test -git push origin v0.0.1-test -``` - -**Success criteria:** -- Version matches pyproject.toml -- Package builds successfully -- GitLab release created -- PyPI upload succeeds (if PYPI_TOKEN set) - -**Configuration needed:** -- Set `PYPI_TOKEN` for PyPI publishing -- Optionally set `PYPI_REPOSITORY_URL` for custom feed - ---- - -## Required CI/CD Variables - -Set these in GitLab project settings (Settings > CI/CD > Variables): - -### Secrets (Protected & Masked) -- `PYPI_TOKEN` - PyPI authentication token (for releases) -- `PAT_TOKEN` - Project/Group Access Token (for sync workflow) - -### Configuration Variables -- `UV_EXTRA_INDEX_URL` - Extra index URL for UV (optional) -- `PYPI_REPOSITORY_URL` - Custom PyPI URL (optional) -- `PUBLISH_COMPANION_BOOK` - Publish documentation (default: true) -- `CREATE_MR` - Auto-create merge requests (default: true) - -## Complete Testing Checklist - -- [x] Validate YAML syntax for all workflow files -- [ ] Set up test GitLab repository -- [ ] Configure required CI/CD variables -- [ ] Test CI workflow (push to branch) -- [ ] Test Validate workflow (in downstream project) -- [ ] Test Deptry workflow -- [ ] Test Pre-commit workflow -- [ ] Test Book workflow (push to main) -- [ ] Test Sync workflow (manual trigger) -- [ ] Test Release workflow (push version tag) -- [ ] Verify GitLab Pages deployment -- [ ] Verify container registry images -- [ ] Verify PyPI package upload (test PyPI) - -## Known Limitations - -1. **Dynamic Matrix:** GitLab CI has limited support for dynamic parallel matrices. Where dynamic matrices are needed, consider generating jobs via child pipelines or iterate within a job. - -2. **Merge Request Creation:** The Sync workflow doesn't automatically create merge requests via the API (would require additional setup with GitLab CLI or API calls). - -3. **OIDC Publishing:** GitLab CI doesn't support OIDC-based PyPI publishing like GitHub Actions. Token-based authentication is used instead. - -## Troubleshooting - -### Pipeline doesn't start -- Check if `.gitlab-ci.yml` is in the root directory -- Verify YAML syntax is valid -- Check pipeline rules match the trigger condition - -### Permission errors -- Ensure required variables are set -- Check if tokens have correct scopes -- Verify project permissions - -### GitLab Pages not deploying -- Ensure job is named `pages` -- Verify artifacts are in `public/` directory -- Check if GitLab Pages is enabled in project settings -- Ensure pipeline runs on default branch - -## Next Steps - -1. **Create test repository:** Fork or mirror rhiza to GitLab -2. **Configure variables:** Set all required CI/CD variables -3. **Test incrementally:** Test each workflow type individually -4. **Document issues:** Report any platform-specific issues -5. **Iterate:** Fix issues and retest - -## Support - -- **GitLab CI/CD Docs:** https://docs.gitlab.com/ee/ci/ -- **GitLab API Docs:** https://docs.gitlab.com/ee/api/ -- **Rhiza Repository:** https://github.com/jebel-quant/rhiza diff --git a/.gitlab/template/marimo_job_template.yml.jinja b/.gitlab/template/marimo_job_template.yml.jinja deleted file mode 100644 index 1026916..0000000 --- a/.gitlab/template/marimo_job_template.yml.jinja +++ /dev/null @@ -1,20 +0,0 @@ -marimo_test_{{JOB_NAME}}: - stage: test - image: ghcr.io/astral-sh/uv:0.9.18-python3.12-bookworm - variables: - GIT_LFS_SKIP_SMUDGE: "0" - before_script: - - apt-get update && apt-get install -y git-lfs - - git lfs install - - git lfs pull - script: - - echo "🚀 Running notebook: {{NOTEBOOK}}" - - folder="results/$(basename "{{NOTEBOOK}}" .py)" - - mkdir -p "$folder" - - export NOTEBOOK_OUTPUT_FOLDER="$folder" - - uv run "{{NOTEBOOK}}" - artifacts: - paths: - - results/ - expire_in: 1 week - allow_failure: false diff --git a/.gitlab/workflows/rhiza_book.yml b/.gitlab/workflows/rhiza_book.yml deleted file mode 100644 index 0d1cc47..0000000 --- a/.gitlab/workflows/rhiza_book.yml +++ /dev/null @@ -1,50 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Workflow: Book (GitLab CI) -# -# Purpose: This workflow builds and deploys comprehensive documentation for the project. -# It combines API documentation, test coverage reports, test results, and -# interactive notebooks into a single GitLab Pages site. -# -# Trigger: This workflow runs on every push to the main or master branch -# -# Components: -# - 📓 Process Marimo notebooks -# - 📖 Generate API documentation with pdoc -# - 🧪 Run tests and generate coverage reports -# - 🚀 Deploy combined documentation to GitLab Pages - -pages: - stage: deploy - needs: [] - image: ghcr.io/astral-sh/uv:0.9.30-bookworm - variables: - GIT_LFS_SKIP_SMUDGE: "0" - before_script: - - apt-get update && apt-get install -y git-lfs make - - git lfs pull - - export UV_EXTRA_INDEX_URL="${UV_EXTRA_INDEX_URL}" - script: - - | - # Check if PUBLISH_COMPANION_BOOK is set (defaults to allowing deployment) - if [ "${PUBLISH_COMPANION_BOOK:-true}" = "false" ]; then - echo "PUBLISH_COMPANION_BOOK is set to false, skipping deployment" - exit 0 - fi - - # Build the book - make book - - # GitLab Pages expects artifacts in the "public" directory - # Move _book to public for GitLab Pages - mv _book public - - # Create .nojekyll file if not present - touch public/.nojekyll - artifacts: - paths: - - public - expire_in: 30 days - rules: - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master" diff --git a/.gitlab/workflows/rhiza_ci.yml b/.gitlab/workflows/rhiza_ci.yml deleted file mode 100644 index 2798398..0000000 --- a/.gitlab/workflows/rhiza_ci.yml +++ /dev/null @@ -1,41 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Workflow: Continuous Integration (GitLab CI) -# -# Purpose: Run tests on multiple Python versions to ensure compatibility. -# -# Trigger: On push and merge requests to main/master branches. - -ci:test: - stage: test - image: ghcr.io/astral-sh/uv:0.9.30-bookworm - parallel: - matrix: - - PYTHON_VERSION: ["3.11", "3.12", "3.13", "3.14"] - variables: - GIT_LFS_SKIP_SMUDGE: "0" - before_script: - - apt-get update && apt-get install -y git-lfs - - git lfs pull - script: - - echo "Testing with Python $PYTHON_VERSION" - - export UV_EXTRA_INDEX_URL="${UV_EXTRA_INDEX_URL}" - - uv venv --python ${PYTHON_VERSION} - - make -f .rhiza/rhiza.mk test - - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - if: $CI_COMMIT_BRANCH - -ci:docs-coverage: - stage: test - image: ghcr.io/astral-sh/uv:0.9.30-bookworm - script: - - export UV_EXTRA_INDEX_URL="${UV_EXTRA_INDEX_URL}" - - uv venv - - make -f .rhiza/rhiza.mk docs-coverage - - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - if: $CI_COMMIT_BRANCH diff --git a/.gitlab/workflows/rhiza_deptry.yml b/.gitlab/workflows/rhiza_deptry.yml deleted file mode 100644 index e2f15f1..0000000 --- a/.gitlab/workflows/rhiza_deptry.yml +++ /dev/null @@ -1,21 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Workflow: Deptry (GitLab CI) -# -# 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 merge requests to main/master -# branches (including from forks) - -deptry:check: - stage: test - needs: [] - image: ghcr.io/astral-sh/uv:0.9.30-bookworm - script: - - make deptry - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - if: $CI_COMMIT_BRANCH diff --git a/.gitlab/workflows/rhiza_pre-commit.yml b/.gitlab/workflows/rhiza_pre-commit.yml deleted file mode 100644 index bd89c44..0000000 --- a/.gitlab/workflows/rhiza_pre-commit.yml +++ /dev/null @@ -1,26 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Workflow: Pre-commit (GitLab CI) -# -# 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 merge requests to main/master -# branches (including from forks) -# -# Components: -# - 🔍 Run pre-commit checks using reusable action - -pre-commit:check: - stage: test - needs: [] - image: ghcr.io/astral-sh/uv:0.9.30-bookworm - script: - - make fmt - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - if: $CI_COMMIT_BRANCH - diff --git a/.gitlab/workflows/rhiza_release.yml b/.gitlab/workflows/rhiza_release.yml deleted file mode 100644 index c26e3ca..0000000 --- a/.gitlab/workflows/rhiza_release.yml +++ /dev/null @@ -1,200 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Release Workflow for Python Packages (GitLab CI) -# -# This workflow implements a secure, maintainable release pipeline with distinct phases: -# -# 📋 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 GitLab release with build artifacts -# 4. 🚀 Publish to PyPI - Publish package using token or custom feed -# 5. ✅ Finalize Release - Publish the GitLab release with links -# -# 🚀 PyPI Publishing: -# - Skipped if no dist/ artifacts exist -# - Skipped if pyproject.toml contains "Private :: Do Not Upload" -# - Uses PYPI_TOKEN for authentication -# - For custom feeds, use PYPI_REPOSITORY_URL and PYPI_TOKEN variables -# -# 📄 Requirements: -# - pyproject.toml with top-level version field (for Python packages) -# - PYPI_TOKEN variable for PyPI publishing -# -# ✅ To Trigger: -# Create and push a version tag: -# git tag v1.2.3 -# git push origin v1.2.3 - -workflow: - rules: - - if: $CI_COMMIT_TAG =~ /^v.*/ - -variables: - RELEASE_TAG: $CI_COMMIT_TAG - -.release_validate_tag: - stage: .pre - image: alpine:latest - script: - - apk add --no-cache git curl jq - - | - TAG="$CI_COMMIT_TAG" - echo "Validating tag: $TAG" - - # Check if release already exists (using GitLab API) - RELEASE_EXISTS=$(curl --header "PRIVATE-TOKEN: $CI_JOB_TOKEN" \ - "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${TAG}" \ - -s -o /dev/null -w "%{http_code}") - - if [ "$RELEASE_EXISTS" = "200" ]; then - echo "❌ Release '$TAG' already exists. Please use a new tag." - exit 1 - fi - - echo "✅ Tag validation passed" - artifacts: - reports: - dotenv: release.env - -release:build: - stage: build - needs: - - job: .release_validate_tag - optional: true - image: ghcr.io/astral-sh/uv:0.9.30-bookworm - before_script: - - git fetch --tags - script: - - | - export UV_EXTRA_INDEX_URL="${UV_EXTRA_INDEX_URL}" - uv python install $(cat .python-version) - uv sync --all-extras --all-groups --frozen - - # Verify version matches tag - if [ -f "pyproject.toml" ]; then - TAG_VERSION="${CI_COMMIT_TAG#v}" - PROJECT_VERSION=$(uv version --short) - - if [ "$PROJECT_VERSION" != "$TAG_VERSION" ]; then - echo "❌ Version mismatch: pyproject.toml has '$PROJECT_VERSION' but tag is '$TAG_VERSION'" - exit 1 - fi - echo "✅ Version verified: $PROJECT_VERSION matches tag" - fi - - # Detect buildable Python package - if [ -f "pyproject.toml" ] && grep -q '^\[build-system\]' pyproject.toml; then - echo "📦 Building package..." - uvx hatch build - else - echo "⏭️ No buildable package detected, skipping build" - mkdir -p dist # Create empty dist directory - fi - artifacts: - paths: - - dist/ - expire_in: 1 day - -release:draft: - stage: deploy - needs: - - job: release:build - image: alpine:latest - before_script: - - apk add --no-cache curl jq - script: - - | - TAG="$CI_COMMIT_TAG" - - # Create draft release using GitLab API - RELEASE_NOTES="Release $TAG" - - curl --request POST \ - --header "PRIVATE-TOKEN: $CI_JOB_TOKEN" \ - --data "tag_name=$TAG" \ - --data "name=$TAG" \ - --data "description=$RELEASE_NOTES" \ - "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases" - - echo "✅ Draft release created" - -release:pypi: - stage: deploy - needs: - - job: release:build - artifacts: true - - job: release:draft - image: ghrc.io/astral-uv/uv:0.9.28-bookworm - before_scri: - - uv python install $(cat .python-version) - - uv pip install twine - script: - - | - # Check if dist contains artifacts and not marked as private - if [ ! -d "dist" ] || [ -z "$(ls -A dist)" ]; then - echo "⚠️ No dist/ artifacts found. Skipping PyPI publish." - exit 0 - fi - - if grep -R "Private :: Do Not Upload" pyproject.toml; then - echo "⏭️ Package marked as private. Skipping PyPI publish." - exit 0 - fi - - # Publish to PyPI - if [ -n "$PYPI_REPOSITORY_URL" ]; then - echo "📦 Publishing to custom repository: $PYPI_REPOSITORY_URL" - twine upload --repository-url "$PYPI_REPOSITORY_URL" \ - --username __token__ --password "$PYPI_TOKEN" \ - --skip-existing dist/* - else - echo "📦 Publishing to PyPI" - twine upload --username __token__ --password "$PYPI_TOKEN" \ - --skip-existing dist/* - fi - - echo "✅ Published to PyPI" - rules: - - if: $CI_COMMIT_TAG =~ /^v.*/ - when: on_success - allow_failure: false - -release:finalize: - stage: .post - needs: - - job: release:build - - job: release:pypi - optional: true - image: alpine:latest - before_script: - - apk add --no-cache curl jq python3 - script: - - | - TAG="$CI_COMMIT_TAG" - RELEASE_BODY="" - - # Add PyPI link if published - if [ -f "pyproject.toml" ]; then - PACKAGE_NAME=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['name'])" 2>/dev/null || echo "") - if [ -n "$PACKAGE_NAME" ]; then - VERSION="${TAG#v}" - if [ -z "$PYPI_REPOSITORY_URL" ]; then - RELEASE_BODY="${RELEASE_BODY}\n\n### PyPI Package\n\n[$PACKAGE_NAME](https://pypi.org/project/$PACKAGE_NAME/$VERSION/)" - else - RELEASE_BODY="${RELEASE_BODY}\n\n### Custom Feed Package\n\n[$PACKAGE_NAME]($PYPI_REPOSITORY_URL)" - fi - fi - fi - - # Update release with additional information - curl --request PUT \ - --header "PRIVATE-TOKEN: $CI_JOB_TOKEN" \ - --data "description=Release $TAG$RELEASE_BODY" \ - "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${TAG}" - - echo "✅ Release finalized" - rules: - - if: $CI_COMMIT_TAG =~ /^v.*/ - when: on_success diff --git a/.gitlab/workflows/rhiza_renovate.yml b/.gitlab/workflows/rhiza_renovate.yml deleted file mode 100644 index 3c65103..0000000 --- a/.gitlab/workflows/rhiza_renovate.yml +++ /dev/null @@ -1,62 +0,0 @@ -# Renovate - Automated dependency updates -# -# This workflow runs Renovate to check for dependency updates and create -# merge requests automatically. -# -# Required variables: -# - RENOVATE_TOKEN: GitLab Project/Group Access Token with api, read_repository, -# write_repository scopes. Must be a Project Access Token (not PAT). -# -# Triggers: -# - Scheduled pipelines (set up in GitLab CI/CD > Schedules) -# - Manual trigger via web UI -# - -renovate: - stage: .pre - image: renovate/renovate:latest - variables: - # GitLab platform configuration - RENOVATE_PLATFORM: gitlab - RENOVATE_ENDPOINT: $CI_API_V4_URL - # Git author for commits (required for Project Access Tokens) - RENOVATE_GIT_AUTHOR: "Renovate Bot " - # Note: GITHUB_COM_TOKEN is set via CI/CD variables (Settings > CI/CD > Variables) - # Logging - set to debug for troubleshooting - LOG_LEVEL: info - script: - - | - if [ -z "$RENOVATE_TOKEN" ]; then - echo "Error: RENOVATE_TOKEN is not set" - echo "Please create a Project Access Token with api, read_repository, write_repository scopes" - echo "and add it as a CI/CD variable named RENOVATE_TOKEN" - exit 1 - fi - - | - echo "Running Renovate for $CI_PROJECT_PATH" - echo "GitLab API endpoint: $CI_API_V4_URL" - echo "Token length: ${#RENOVATE_TOKEN}" - echo "Token prefix: $(echo $RENOVATE_TOKEN | cut -c1-10)..." - - | - echo "GITHUB_COM_TOKEN length: ${#GITHUB_COM_TOKEN}" - echo "GITHUB_COM_TOKEN prefix: $(echo $GITHUB_COM_TOKEN | cut -c1-4)" - - | - echo "Testing GitHub token with curl..." - curl -s -o /dev/null -w "GitHub API Status: %{http_code}\n" \ - -H "Authorization: token $GITHUB_COM_TOKEN" \ - "https://api.github.com/rate_limit" - - | - echo "Testing GitLab token with curl..." - curl -s -o /dev/null -w "HTTP Status: %{http_code}\n" \ - --header "PRIVATE-TOKEN: $RENOVATE_TOKEN" \ - "$CI_API_V4_URL/version" - - renovate --autodiscover=false "$CI_PROJECT_PATH" - rules: - # Run on scheduled pipelines - - if: $CI_PIPELINE_SOURCE == "schedule" && $RENOVATE_ENABLED != "false" - when: always - # Allow manual trigger from web UI - - if: $CI_PIPELINE_SOURCE == "web" && $RENOVATE_RUN == "true" - when: always - # Never run on regular pushes or MRs - - when: never diff --git a/.gitlab/workflows/rhiza_sync.yml b/.gitlab/workflows/rhiza_sync.yml deleted file mode 100644 index 6758d14..0000000 --- a/.gitlab/workflows/rhiza_sync.yml +++ /dev/null @@ -1,81 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Workflow: Rhiza Sync (GitLab CI) -# -# Purpose: This workflow synchronizes the repository with its template. -# -# IMPORTANT: When workflow files (.gitlab-ci.yml or .gitlab/workflows/*.yml) are modified, -# a Project Access Token or Group Access Token with 'write_repository' scope is required. -# The token must be set as a CI/CD variable named PAT_TOKEN. -# -# Trigger: Manual trigger via pipeline or scheduled - -sync:template: - stage: deploy - image: ghcr.io/astral-sh/uv:0.9.30-bookworm - before_script: - - apt-get update && apt-get install -y git - - git config --global user.name "GitLab CI" - - git config --global user.email "gitlab-ci@gitlab.com" - script: - - | - # Don't run this in rhiza itself - if [ "$CI_PROJECT_PATH" = "jebel-quant/rhiza" ]; then - echo "Skipping sync in rhiza repository itself" - exit 0 - fi - - # Check PAT_TOKEN configuration - if [ -z "$PAT_TOKEN" ]; then - echo "⚠️ PAT_TOKEN variable is not configured." - echo "⚠️ If this sync modifies workflow files, the push will fail." - echo "⚠️ Please configure a Project/Group Access Token with write_repository scope." - TOKEN="${CI_JOB_TOKEN}" - else - echo "✓ PAT_TOKEN is configured." - TOKEN="${PAT_TOKEN}" - fi - - # Define sync branch name - BRANCH_NAME="rhiza/${CI_PIPELINE_ID}" - - # Fetch all branches - git fetch origin - - # Create and checkout sync branch - git checkout -b "$BRANCH_NAME" - - # Sync template - uvx rhiza materialize --force . - - # Check if there are changes - git add -A - - if git diff --cached --quiet; then - echo "No changes detected, skipping commit and push" - exit 0 - fi - - # Commit changes - git commit -m "chore: Update via rhiza" - - # Construct push URL with token - PUSH_URL=$(echo "$CI_REPOSITORY_URL" | sed "s|gitlab-ci-token:[^@]*@|oauth2:${TOKEN}@|") - - # Push changes - git push "$PUSH_URL" "$BRANCH_NAME" - - # Create merge request if CREATE_MR is true (default) - if [ "${CREATE_MR:-true}" = "true" ]; then - echo "Creating merge request..." - # Note: This requires gitlab CLI or API call - # For now, just push the branch - MR can be created manually or via GitLab API - echo "Branch $BRANCH_NAME pushed. Please create a merge request manually." - echo "Or use GitLab API to create MR automatically." - fi - rules: - - if: $CI_PIPELINE_SOURCE == "schedule" - - if: $CI_PIPELINE_SOURCE == "web" - - when: manual - allow_failure: false diff --git a/.gitlab/workflows/rhiza_validate.yml b/.gitlab/workflows/rhiza_validate.yml deleted file mode 100644 index b2eafb8..0000000 --- a/.gitlab/workflows/rhiza_validate.yml +++ /dev/null @@ -1,24 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Workflow: Rhiza Validate (GitLab CI) -# -# Purpose: Validates Rhiza configuration -# -# Trigger: This workflow runs on every push and on merge requests to main/master - -validate:rhiza: - stage: test - needs: [] - image: ghcr.io/astral-sh/uv:0.9.30-bookworm - script: - - | - # Don't run this in rhiza itself. Rhiza has no template.yml file. - if [ "$CI_PROJECT_PATH" = "jebel-quant/rhiza" ]; then - echo "Skipping validation in rhiza repository itself" - exit 0 - fi - uvx "rhiza>=0.8.0" validate . - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - if: $CI_COMMIT_BRANCH From 47e156ce8f63664b821f8a5b8f054e7124a12c47 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 21:31:15 +0400 Subject: [PATCH 14/31] Delete .gitlab-ci.yml --- .gitlab-ci.yml | 123 ------------------------------------------------- 1 file changed, 123 deletions(-) delete mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 2dd1332..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,123 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# GitLab CI/CD Main Configuration -# -# This file includes all individual workflow files from .gitlab/workflows/ -# and defines the pipeline structure for the rhiza project. -# -# Workflows included: -# - CI: Run tests on multiple Python versions -# - VALIDATE: Validate Rhiza configuration -# - DEPTRY: Check dependencies -# - PRE-COMMIT: Run pre-commit checks -# - BOOK: Build and deploy documentation -# - SYNC: Synchronize with template repository -# - RELEASE: Release workflow for tags - -# Define pipeline stages -stages: - - .pre # Pre-pipeline jobs (matrix generation, validation) - - build # Build jobs - - test # Test jobs (CI, marimo, validate, deptry, pre-commit) - - deploy # Deployment jobs (pages, sync) - - .post # Post-pipeline jobs (finalization) - -# Default settings for all jobs -default: - # Retry failed jobs once - retry: - max: 1 - when: - - runner_system_failure - - stuck_or_timeout_failure - -# Include individual workflow files -# These files are organised by purpose and mirror the GitHub Actions structure - -# CI/CD Testing Workflows -include: - # Continuous Integration - Python matrix testing - - local: '.gitlab/workflows/rhiza_ci.yml' - rules: - - if: $CI_COMMIT_TAG - when: never - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - if: $CI_COMMIT_BRANCH - - # Rhiza Validation - Validate project structure - - local: '.gitlab/workflows/rhiza_validate.yml' - rules: - - if: $CI_COMMIT_TAG - when: never - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - if: $CI_COMMIT_BRANCH - - # Deptry - Check dependencies - - local: '.gitlab/workflows/rhiza_deptry.yml' - rules: - - if: $CI_COMMIT_TAG - when: never - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - if: $CI_COMMIT_BRANCH - - # Pre-commit - Code quality checks - - local: '.gitlab/workflows/rhiza_pre-commit.yml' - rules: - - if: $CI_COMMIT_TAG - when: never - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - if: $CI_COMMIT_BRANCH - -# Deployment Workflows - # Book - Build and deploy documentation to GitLab Pages - - local: '.gitlab/workflows/rhiza_book.yml' - rules: - - if: $CI_COMMIT_TAG - when: never - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - - if: $CI_COMMIT_BRANCH == "main" - - if: $CI_COMMIT_BRANCH == "master" - - # Sync - Synchronize with template repository - - local: '.gitlab/workflows/rhiza_sync.yml' - rules: - # Automatic sync for scheduled pipelines - - if: '$CI_PIPELINE_SOURCE == "schedule"' - when: always - - # Automatic sync when triggered from the UI - - if: '$CI_PIPELINE_SOURCE == "web"' - when: always - - - when: never - - -# Release Workflow - # Release - Create releases and publish packages - - local: '.gitlab/workflows/rhiza_release.yml' - rules: - - if: $CI_COMMIT_TAG =~ /^v.*/ - -# Variables that can be overridden per project -# These are the same variables used in GitHub Actions workflows -variables: - # UV package manager configuration - UV_EXTRA_INDEX_URL: "" - - # PyPI configuration - PYPI_REPOSITORY_URL: "" - - # Documentation configuration - PUBLISH_COMPANION_BOOK: "true" - - # Sync configuration - CREATE_MR: "true" - -# Job templates for reusable configuration -.python_base: - image: ghcr.io/astral-sh/uv:0.9.30-bookworm - before_script: - - export UV_EXTRA_INDEX_URL="${UV_EXTRA_INDEX_URL}" - - uv python install $(cat .python-version) - - uv sync --all-extras --all-groups --frozen From cc191408452ca5629ea9be748f3347c8e3aa9ceb Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 21:53:35 +0400 Subject: [PATCH 15/31] Delete pyproject.toml --- pyproject.toml | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index cfec824..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,30 +0,0 @@ -[project] -name = "rhiza-core" -version = "0.1.0" -description = "Core reusable configuration templates for Rhiza language templates" -readme = "README.md" -requires-python = ">=3.11" -license = { text = "MIT" } -authors = [ - { name = "Thomas Schmelzer" } -] -keywords = ["templates", "configuration", "ci", "makefile", "devops", "core"] -classifiers = [ - "Private :: Do Not Upload", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "License :: OSI Approved :: MIT License", - "Intended Audience :: Developers", - "Topic :: Software Development :: Build Tools", - "Topic :: Software Development :: Libraries :: Application Frameworks", -] -dependencies = [] - -[project.urls] -Homepage = "https://github.com/jebel-quant/rhiza-core" -Repository = "https://github.com/jebel-quant/rhiza-core" -Issues = "https://github.com/jebel-quant/rhiza-core/issues" From 6215b64d304f6b5af2d4c0d3f4e55600bebffb50 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 21:53:46 +0400 Subject: [PATCH 16/31] Delete uv.lock --- uv.lock | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 uv.lock diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 203388e..0000000 --- a/uv.lock +++ /dev/null @@ -1,16 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.11" -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] - -[[package]] -name = "rhiza-core" -version = "0.1.0" -source = { virtual = "." } From 5d0c2a38ecb25dbd180611d433c454f8d19dc37e Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 22:10:55 +0400 Subject: [PATCH 17/31] Establish rhiza-core as language-agnostic foundation for rhiza and rhiza-go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix rhiza.mk header to reference rhiza-core (was incorrectly referencing rhiza) - Fix sync/validate/summarise-sync self-skip guard: replace fragile repo-name grep with `[ ! -f ".rhiza/template.yml" ]` check — correctly skips in repos without a template.yml, including rhiza-core itself - Remove Python-specific `version-matrix` target from rhiza.mk - Update ARCHITECTURE.md: remove Python-specific diagrams, add 3-tier inheritance model (rhiza-core → rhiza/rhiza-go → user-projects), update directory structure and workflow diagrams to reflect rhiza-core's actual contents Co-Authored-By: Claude Sonnet 4.6 --- .rhiza/rhiza.mk | 24 +++--- docs/ARCHITECTURE.md | 195 ++++++++++++++++--------------------------- 2 files changed, 84 insertions(+), 135 deletions(-) diff --git a/.rhiza/rhiza.mk b/.rhiza/rhiza.mk index 68dd38a..64bbfbf 100644 --- a/.rhiza/rhiza.mk +++ b/.rhiza/rhiza.mk @@ -1,7 +1,7 @@ -## Makefile for jebel-quant/rhiza -# (https://github.com/jebel-quant/rhiza) +## Makefile for jebel-quant/rhiza-core +# (https://github.com/jebel-quant/rhiza-core) # -# Purpose: Developer tasks using uv/uvx (install, test, docs, marimushka, book). +# Purpose: Language-agnostic infrastructure tasks (sync, validate, release, docs). # Lines with `##` after a target are parsed into help text, # and lines starting with `##@` create section headers in the help output. # @@ -33,8 +33,7 @@ RESET := \033[0m readme \ summarise-sync \ sync \ - validate \ - version-matrix + validate # we need absolute paths! INSTALL_DIR ?= $(abspath ./bin) @@ -93,8 +92,8 @@ print-logo: sync: pre-sync ## sync with 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 sync in rhiza repository (no template.yml by design)${RESET}\n"; \ + @if [ ! -f ".rhiza/template.yml" ]; then \ + printf "${BLUE}[INFO] Skipping sync: no .rhiza/template.yml found${RESET}\n"; \ else \ $(MAKE) install-uv; \ ${UVX_BIN} "rhiza>=$(RHIZA_VERSION)" materialize --force .; \ @@ -102,8 +101,8 @@ sync: pre-sync ## sync with template repository as defined in .rhiza/template.ym @$(MAKE) post-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"; \ + @if [ ! -f ".rhiza/template.yml" ]; then \ + printf "${BLUE}[INFO] Skipping summarise-sync: no .rhiza/template.yml found${RESET}\n"; \ else \ $(MAKE) install-uv; \ ${UVX_BIN} "rhiza>=$(RHIZA_VERSION)" summarise .; \ @@ -117,8 +116,8 @@ rhiza-test: install ## run rhiza's own tests (if any) fi 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"; \ + @if [ ! -f ".rhiza/template.yml" ]; then \ + printf "${BLUE}[INFO] Skipping validate: no .rhiza/template.yml found${RESET}\n"; \ else \ $(MAKE) install-uv; \ ${UVX_BIN} "rhiza>=$(RHIZA_VERSION)" validate .; \ @@ -137,9 +136,6 @@ help: print-logo ## Display this help message +@awk 'BEGIN {FS = ":.*##"; printf ""} /^[a-zA-Z_-]+:.*?##/ { printf " $(BLUE)%-20s$(RESET) %s\n", $$1, $$2 } /^##@/ { printf "\n$(BOLD)%s$(RESET)\n", substr($$0, 5) }' $(MAKEFILE_LIST) +@printf "\n" -version-matrix: install-uv ## Emit the list of supported Python versions from pyproject.toml - @${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" @printf "${BOLD}Value of $*:${RESET}\n" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2dd7bea..670d4cc 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -15,33 +15,26 @@ flowchart TB rhizamk[rhiza.mk
Core Logic] maked[make.d/*.mk
Extensions] scripts[scripts/
Shell Scripts] - reqs[requirements/
Dependencies] template[template-bundles.yml
Bundle Config] end subgraph Config["Configuration"] - pyproject[pyproject.toml] - ruff[ruff.toml] - precommit[.pre-commit-config.yaml] editorconfig[.editorconfig] + gitignore[.gitignore] end subgraph CI["GitHub Actions"] - ci[CI Workflow] - release[Release Workflow] - security[Security Workflow] + validate[Validate Workflow] sync[Sync Workflow] + release[Release Workflow] end make --> rhizamk local -.-> rhizamk rhizamk --> maked rhizamk --> scripts - maked --> reqs - maked --> pyproject - ci --> make + validate --> make release --> make - security --> make sync --> template ``` @@ -131,24 +124,30 @@ flowchart TD ## Template Sync Flow ```mermaid -flowchart LR - upstream[Upstream Rhiza
jebel-quant/rhiza] -->|template.yml| sync[make sync] - sync -->|updates| downstream[Downstream Project] +flowchart TD + subgraph tier1["Tier 1: rhiza-core"] + core[jebel-quant/rhiza-core
Language-agnostic infrastructure] + corebundles[template-bundles.yml
core, github, docker, lfs, book, release, ...] + end - subgraph Synced["Synced Files"] - workflows[.github/workflows/] - rhiza[.rhiza/] - configs[Config Files] + subgraph tier2["Tier 2: Language Templates"] + rhizapy[jebel-quant/rhiza
Python template] + rhizago[jebel-quant/rhiza-go
Go template] + pyconfig[template.yml → rhiza-core
+ Python-specific bundles] + goconfig[template.yml → rhiza-core
+ Go-specific bundles] end - subgraph Preserved["Preserved"] - localmk[local.mk] - src[src/] - tests[tests/] + subgraph tier3["Tier 3: User Projects"] + proj1[futures, taipan, ...] + proj2[go-services, ...] end - sync --> Synced - downstream --> Preserved + core -->|make sync materializes files| rhizapy + core -->|make sync materializes files| rhizago + rhizapy -->|make sync| proj1 + rhizago -->|make sync| proj2 + + note1["Files are physically written into
each tier on sync — not resolved
dynamically at build time"] ``` ## Directory Structure @@ -159,35 +158,30 @@ flowchart TD root --> rhiza[.rhiza/] root --> github[.github/] - root --> src[src/] - root --> tests[tests/] root --> docs[docs/] - root --> book[book/] rhiza --> rhizamk[rhiza.mk] rhiza --> maked[make.d/] - rhiza --> scripts[scripts/] - rhiza --> reqs[requirements/] - rhiza --> rtests[tests/] rhiza --> rdocs[docs/] rhiza --> templates[templates/] rhiza --> assets[assets/] + rhiza --> bundles[template-bundles.yml] github --> workflows[workflows/] - workflows --> ci[rhiza_ci.yml] - workflows --> release[rhiza_release.yml] - workflows --> security[rhiza_security.yml] - workflows --> more[... 11 more] + github --> actions[actions/] + workflows --> validate[rhiza_validate.yml] + workflows --> sync[rhiza_sync.yml] + workflows --> copilot[copilot-setup-steps.yml] + workflows --> renovate[renovate_rhiza_sync.yml] maked --> agentic[agentic.mk] maked --> book[book.mk] - maked --> bootstrap[bootstrap.mk] maked --> docker[docker.mk] - maked --> docs_mk[docs.mk] maked --> github_mk[github.mk] - maked --> marimo[marimo.mk] - maked --> test[test.mk] - maked --> more_mk[... 6 more] + maked --> lfs[lfs.mk] + maked --> releasing[releasing.mk] + maked --> custom_env[custom-env.mk] + maked --> custom_task[custom-task.mk] ``` ## .rhiza/ Directory Structure and Dependencies @@ -196,76 +190,49 @@ flowchart TD flowchart TB subgraph rhiza[".rhiza/ Directory"] direction TB - + subgraph core["Core Files"] - rhizamk[rhiza.mk
Core Logic - 153 lines] + rhizamk[rhiza.mk
Core Logic] cfg[.cfg.toml
Configuration] env[.env
Environment] version[.rhiza-version
Version] bundles[template-bundles.yml
Bundle Definitions] end - - subgraph maked["make.d/ (14 files, ~41KB)"] + + subgraph maked["make.d/ (language-agnostic)"] direction LR agentic[agentic.mk
AI Agents] - bootstrap[bootstrap.mk
Installation] - test[test.mk
Testing] book_mk[book.mk
Documentation] docker_mk[docker.mk
Containers] - quality[quality.mk
Code Quality] + github_mk[github.mk
GitHub CLI] + lfs_mk[lfs.mk
Git LFS] releasing[releasing.mk
Releases] - more[...] - end - - subgraph requirements["requirements/ (4 files)"] - direction LR - tests_txt[tests.txt
pytest, coverage] - marimo_txt[marimo.txt
notebooks] - docs_txt[docs.txt
pdoc] - tools_txt[tools.txt
pre-commit] - end - - subgraph tests_dir["tests/ (23 files)"] - direction LR - api[api/
Makefile Tests] - integration[integration/
E2E Tests] - structure[structure/
Layout Tests] - sync[sync/
Sync Tests] - deps[deps/
Dependency Tests] + custom_env[custom-env.mk
Env Example] + custom_task[custom-task.mk
Task Example] end - + subgraph other["Other Directories"] direction LR - docs_dir[docs/
7 MD files] + docs_dir[docs/
Infrastructure docs] templates_dir[templates/
minibook] assets_dir[assets/
Logo] scripts_dir[scripts/
Utilities] end end - + subgraph project["Project Files"] Makefile[Makefile
Entry Point] - pyproject[pyproject.toml
Dependencies] - ruff_toml[ruff.toml
Linting] - pytest_ini[pytest.ini
Test Config] - python_version[.python-version
Python 3.13] + pyproject[pyproject.toml
Package metadata] + editorconfig[.editorconfig] + python_version[.python-version
for uv tooling] end - + Makefile -->|includes| rhizamk rhizamk -->|auto-loads| maked - maked -->|reads| pyproject - maked -->|reads| python_version - test -->|uses| pytest_ini - test -->|installs| tests_txt - book_mk -->|installs| docs_txt - book_mk -->|uses| marimo_txt - quality -->|uses| ruff_toml - bootstrap -->|installs| tools_txt - tests_dir -->|validates| core - tests_dir -->|validates| maked + book_mk -->|builds| templates_dir ``` -## CI/CD Workflow Triggers +## CI/CD Workflow Triggers (rhiza-core) ```mermaid flowchart TD @@ -278,53 +245,40 @@ flowchart TD end subgraph Workflows - ci[CI] - security[Security] - codeql[CodeQL] + validate[rhiza_validate] + sync[rhiza_sync] release[Release] - deptry[Deptry] - precommit[Pre-commit] + renovate[renovate_rhiza_sync] end - push --> ci - push --> security - push --> codeql - pr --> ci - pr --> deptry - pr --> precommit - schedule --> security - manual --> ci + push --> validate + pr --> validate + schedule --> sync + manual --> sync tag --> release + schedule --> renovate ``` -## Python Execution Model +## Tool Execution Model + +rhiza-core uses `uv`/`uvx` to run rhiza-cli tooling. This is true even for language templates that are not Python — `uv` is a build-time dependency of the template management toolchain, not of the project being templated. ```mermaid flowchart LR - subgraph Commands - make[make test] - direct[Direct Python] - end - - subgraph UV["uv Layer"] - uv_run[uv run] - uvx[uvx] + subgraph MakeTargets + sync[make sync] + validate[make validate] + release[make release] end - subgraph Tools - pytest[pytest] - ruff[ruff] - hatch[hatch] + subgraph UVX["uvx (ephemeral)"] + rhizacli[rhiza CLI
materialize / validate / summarise] + rhizatools[rhiza-tools
bump / release / update-readme] end - make --> uv_run - uv_run --> pytest - uv_run --> ruff - uvx --> hatch - - direct -.->|Never| pytest - - style direct stroke-dasharray: 5 5 + sync --> rhizacli + validate --> rhizacli + release --> rhizatools ``` ## Naming Conventions and Organization Patterns @@ -478,10 +432,9 @@ GitHub Actions workflows use the pattern `rhiza_.yml`: ### 1. Single Source of Truth -- **Python version**: `.python-version` file (not hardcoded) -- **Rhiza version**: `.rhiza/.rhiza-version` file -- **Dependencies**: `pyproject.toml` (not duplicated in makefiles) -- **Bundle definitions**: `template-bundles.yml` (not scattered) +- **Rhiza version**: `.rhiza/.rhiza-version` file (controls which rhiza-cli version is used for sync/validate) +- **Bundle definitions**: `template-bundles.yml` (defines what language templates can inherit) +- **Language-specific versions** (in consuming templates, not here): `.python-version`, `.go-version`, etc. ### 2. Auto-Loading Pattern From 61aba3c4422b46381a43ad872591fb745a80ae1b Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 22:14:31 +0400 Subject: [PATCH 18/31] Delete .github/workflows/rhiza_validate.yml --- .github/workflows/rhiza_validate.yml | 42 ---------------------------- 1 file changed, 42 deletions(-) delete mode 100644 .github/workflows/rhiza_validate.yml diff --git a/.github/workflows/rhiza_validate.yml b/.github/workflows/rhiza_validate.yml deleted file mode 100644 index f7ff097..0000000 --- a/.github/workflows/rhiza_validate.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: (RHIZA) VALIDATE - -permissions: - contents: read - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - -jobs: - validation: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v6.0.2 - with: - lfs: true - - - name: Install uv - uses: astral-sh/setup-uv@v7.3.0 - with: - version: "0.10.2" - - - name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth - with: - token: ${{ secrets.GH_PAT }} - - - name: Validate Rhiza config - # don't run this in rhiza itself. Rhiza has no template.yml file. - if: ${{ github.repository != 'jebel-quant/rhiza' }} - shell: bash - run: | - uvx "rhiza>=0.8.0" validate . - - - name: Run Rhiza Tests - shell: bash - run: | - make rhiza-test From d9f9fcc6a0ee72562acf9be1a69a5f178e8c33a4 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 22:25:00 +0400 Subject: [PATCH 19/31] Fix stale repo reference in copilot-setup-steps.yml Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/copilot-setup-steps.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 3164c3a..ba4149c 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -1,5 +1,5 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). +# This file is part of the jebel-quant/rhiza-core repository +# (https://github.com/jebel-quant/rhiza-core). # # Workflow: Copilot Setup Steps # From aec17dfff98c10af3815ead12583a2ce6842a1cc Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 22:36:42 +0400 Subject: [PATCH 20/31] Add 'install' to phony targets in rhiza.mk --- .rhiza/rhiza.mk | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.rhiza/rhiza.mk b/.rhiza/rhiza.mk index 64bbfbf..0eb46a3 100644 --- a/.rhiza/rhiza.mk +++ b/.rhiza/rhiza.mk @@ -19,6 +19,7 @@ RESET := \033[0m # Declare phony targets (they don't produce files) .PHONY: \ help \ + install \ post-bump \ post-install \ post-release \ @@ -75,7 +76,7 @@ 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 validate readme pre-sync post-sync pre-validate post-validate install # Hook targets (double-colon rules allow multiple definitions) # Note: pre-install/post-install are defined in bootstrap.mk @@ -84,6 +85,7 @@ pre-sync:: ; @: post-sync:: ; @: pre-validate:: ; @: post-validate:: ; @: +install:: ; @: ##@ Rhiza Workflows From 8094ad595d92cc9c67aa2367e6e05ecf85c68225 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 22:39:35 +0400 Subject: [PATCH 21/31] Add GitHub Actions workflow for Rhiza validation --- .github/workflows/rhiza_validate.yml | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/rhiza_validate.yml diff --git a/.github/workflows/rhiza_validate.yml b/.github/workflows/rhiza_validate.yml new file mode 100644 index 0000000..dd39cae --- /dev/null +++ b/.github/workflows/rhiza_validate.yml @@ -0,0 +1,40 @@ +name: (RHIZA) VALIDATE + +permissions: + contents: read + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + validation: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.2 + with: + lfs: true + + - name: Install uv + uses: astral-sh/setup-uv@v7.3.0 + with: + version: "0.10.2" + + - name: Configure git auth for private packages + uses: ./.github/actions/configure-git-auth + with: + token: ${{ secrets.GH_PAT }} + + - name: Validate Rhiza config + shell: bash + run: | + make validate + + - name: Run Rhiza Tests + shell: bash + run: | + make rhiza-test From 8ea6fe50185af8174653516227822085d27a6655 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 22:40:06 +0400 Subject: [PATCH 22/31] Remove Rhiza tests step from validation workflow Removed the step to run Rhiza tests from the workflow. --- .github/workflows/rhiza_validate.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/rhiza_validate.yml b/.github/workflows/rhiza_validate.yml index dd39cae..39c9226 100644 --- a/.github/workflows/rhiza_validate.yml +++ b/.github/workflows/rhiza_validate.yml @@ -33,8 +33,3 @@ jobs: shell: bash run: | make validate - - - name: Run Rhiza Tests - shell: bash - run: | - make rhiza-test From 2011cd4a8d4cbd0f89557c4e517fcef62a2753c6 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 23:13:39 +0400 Subject: [PATCH 23/31] feat(book): add default no-op test target Adds a double-colon test:: rule so book:: can depend on test without failing when no language-specific mk file defines a test target. Co-Authored-By: Claude Sonnet 4.6 --- .rhiza/make.d/book.mk | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.rhiza/make.d/book.mk b/.rhiza/make.d/book.mk index 715db7f..b4f0054 100644 --- a/.rhiza/make.d/book.mk +++ b/.rhiza/make.d/book.mk @@ -4,7 +4,7 @@ # and compiling a companion book (minibook). # Declare phony targets (they don't produce files) -.PHONY: marimushka mkdocs-build book +.PHONY: marimushka mkdocs-build book test # Define a default no-op marimushka target that will be used # when book/marimo/marimo.mk doesn't exist or doesn't define marimushka @@ -17,6 +17,11 @@ marimushka:: install-uv > "${MARIMUSHKA_OUTPUT}/index.html"; \ fi +# Define a default no-op test target that will be used +# when no language-specific mk file defines test +test:: + @printf "${BLUE}[INFO] No test target defined, skipping tests${RESET}\n" + # Define a default no-op mkdocs-build target that will be used # when .rhiza/make.d/docs.mk doesn't exist or doesn't define mkdocs-build mkdocs-build:: install-uv From 1bfaa906875e0d4cd8d4bcdccb3c92c6ededef17 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 23:16:39 +0400 Subject: [PATCH 24/31] feat(book): add default no-op install-uv target Co-Authored-By: Claude Sonnet 4.6 --- .rhiza/make.d/book.mk | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.rhiza/make.d/book.mk b/.rhiza/make.d/book.mk index b4f0054..981b996 100644 --- a/.rhiza/make.d/book.mk +++ b/.rhiza/make.d/book.mk @@ -4,7 +4,7 @@ # and compiling a companion book (minibook). # Declare phony targets (they don't produce files) -.PHONY: marimushka mkdocs-build book test +.PHONY: marimushka mkdocs-build book test install-uv # Define a default no-op marimushka target that will be used # when book/marimo/marimo.mk doesn't exist or doesn't define marimushka @@ -17,6 +17,11 @@ marimushka:: install-uv > "${MARIMUSHKA_OUTPUT}/index.html"; \ fi +# Define a default no-op install-uv target that will be used +# when no language-specific mk file defines install-uv +install-uv:: + @printf "${BLUE}[INFO] No install-uv target defined, skipping${RESET}\n" + # Define a default no-op test target that will be used # when no language-specific mk file defines test test:: From 234b77fdbbc4920a937e555ec10dacd749d06e04 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 23:21:07 +0400 Subject: [PATCH 25/31] book workflow --- .github/workflows/rhiza_book.yml | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/workflows/rhiza_book.yml diff --git a/.github/workflows/rhiza_book.yml b/.github/workflows/rhiza_book.yml new file mode 100644 index 0000000..f7a1e59 --- /dev/null +++ b/.github/workflows/rhiza_book.yml @@ -0,0 +1,74 @@ +# This file is part of the jebel-quant/rhiza repository +# (https://github.com/jebel-quant/rhiza). +# +# Workflow: Book +# Purpose: This workflow builds and deploys comprehensive documentation for the project. +# It combines API documentation, test coverage reports, test results, and +# interactive notebooks into a single GitHub Pages site. +# +# Trigger: This workflow runs on every push to the main or master branch +# +# Components: +# - 📓 Process Marimo notebooks +# - 📖 Generate API documentation with pdoc +# - 🧪 Run tests and generate coverage reports +# - 🚀 Deploy combined documentation to GitHub Pages + +name: "(RHIZA) BOOK" + +on: + push: + branches: + - main + - master + +jobs: + book: + runs-on: "ubuntu-latest" + + environment: + name: github-pages # 👈 this is the critical missing piece + + permissions: + contents: read + pages: write # Permission to deploy to Pages + id-token: write # Permission to verify deployment origin + + steps: + # Check out the repository code + - uses: actions/checkout@v6.0.2 + with: + lfs: true + + - name: Install uv + uses: astral-sh/setup-uv@v7.3.0 + with: + version: "0.10.4" + + - name: Configure git auth for private packages + uses: ./.github/actions/configure-git-auth + with: + token: ${{ secrets.GH_PAT }} + + - name: "Make the book" + env: + UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} + run: | + make book + + # Step 5: 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.0.0 # Official GitHub Pages artifact upload action + with: + path: _book/ # Path to the directory containing all artifacts to deploy + + # Step 6: Deploy the packaged artifacts to GitHub Pages + # This step publishes the content to GitHub Pages + # The deployment is conditional based on whether the repository is a fork and the PUBLISH_COMPANION_BOOK variable is set + # If the repository is a fork, deployment is skipped to avoid unauthorised publishing + # 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.0.5 # Official GitHub Pages deployment action + continue-on-error: true From 1b611b2cd60922d185535d0fa07b3883f1abf1bf Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 22 Feb 2026 23:37:22 +0400 Subject: [PATCH 26/31] Implement mkdocs-build target for MkDocs site Added mkdocs-build target to build MkDocs documentation site with checks for config file and cleanup. --- .rhiza/make.d/book.mk | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/.rhiza/make.d/book.mk b/.rhiza/make.d/book.mk index 981b996..ff7fde9 100644 --- a/.rhiza/make.d/book.mk +++ b/.rhiza/make.d/book.mk @@ -3,6 +3,29 @@ # It provides targets for exporting Marimo notebooks to HTML (marimushka) # and compiling a companion book (minibook). +# Default output directory for MkDocs (HTML site) +MKDOCS_OUTPUT ?= _mkdocs + +# MkDocs config file location +MKDOCS_CONFIG ?= docs/mkdocs.yml + +# The 'mkdocs-build' target builds the MkDocs documentation site. +# 1. Checks if the mkdocs.yml config file exists. +# 2. Cleans up any previous output. +# 3. Builds the static site using mkdocs with material theme. +mkdocs-build: ## build MkDocs documentation site + @printf "${BLUE}[INFO] Building MkDocs site...${RESET}\n" + @if [ -f "$(MKDOCS_CONFIG)" ]; then \ + rm -rf "$(MKDOCS_OUTPUT)"; \ + MKDOCS_OUTPUT_ABS="$$(pwd)/$(MKDOCS_OUTPUT)"; \ + ${UVX_BIN} --with mkdocs-material --with "pymdown-extensions>=10.0" mkdocs build \ + -f "$(MKDOCS_CONFIG)" \ + -d "$$MKDOCS_OUTPUT_ABS"; \ + else \ + printf "${YELLOW}[WARN] $(MKDOCS_CONFIG) not found, skipping MkDocs build${RESET}\n"; \ + fi + + # Declare phony targets (they don't produce files) .PHONY: marimushka mkdocs-build book test install-uv @@ -27,13 +50,6 @@ install-uv:: test:: @printf "${BLUE}[INFO] No test target defined, skipping tests${RESET}\n" -# Define a default no-op mkdocs-build target that will be used -# when .rhiza/make.d/docs.mk doesn't exist or doesn't define mkdocs-build -mkdocs-build:: install-uv - @if [ ! -f "docs/mkdocs.yml" ]; then \ - printf "${BLUE}[INFO] No mkdocs.yml found, skipping MkDocs${RESET}\n"; \ - fi - # Default output directory for Marimushka (HTML exports of notebooks) MARIMUSHKA_OUTPUT ?= _marimushka From 6a9b5654ab4f9435724d57bf5635bfcc8bb6fc2b Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 24 Feb 2026 23:10:37 +0400 Subject: [PATCH 27/31] Add bootstrap.mk for MkDocs documentation build --- .rhiza/make.d/bootstrap.mk | 116 +++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 .rhiza/make.d/bootstrap.mk diff --git a/.rhiza/make.d/bootstrap.mk b/.rhiza/make.d/bootstrap.mk new file mode 100644 index 0000000..f001261 --- /dev/null +++ b/.rhiza/make.d/bootstrap.mk @@ -0,0 +1,116 @@ +## book.mk - Documentation book targets +# This file is included by the main Makefile. +# For Go projects, the book target compiles documentation from +# Go doc output, test coverage reports, and test results. + +# Declare phony targets (they don't produce files) +.PHONY: book mkdocs-build + +# Default output directory for MkDocs +MKDOCS_OUTPUT ?= _mkdocs + +# MkDocs config file location +MKDOCS_CONFIG ?= docs/mkdocs.yml + +# Book configuration +BOOK_TITLE ?= $(shell basename $(PWD)) +BOOK_SUBTITLE ?= Go Project Documentation +BOOK_TEMPLATE ?= .rhiza/templates/minibook/custom.html.jinja2 + +##@ Book + +# Build MkDocs documentation site +mkdocs-build:: install-uv ## build mkdocs documentation site + @printf "${BLUE}[INFO] Building MkDocs site...${RESET}\n" + @if [ -f "$(MKDOCS_CONFIG)" ]; then \ + rm -rf "$(MKDOCS_OUTPUT)"; \ + MKDOCS_OUTPUT_ABS="$$(pwd)/$(MKDOCS_OUTPUT)"; \ + $(UVX_BIN) --from "mkdocs<2" --with "mkdocs-material<9.6" --with "pymdown-extensions>=10.0" mkdocs build \ + -f "$(MKDOCS_CONFIG)" \ + -d "$$MKDOCS_OUTPUT_ABS"; \ + else \ + printf "${YELLOW}[WARN] $(MKDOCS_CONFIG) not found, skipping MkDocs build${RESET}\n"; \ + fi + +# ---------------------------- +# Book sections (declarative) +# ---------------------------- +# format: +# name | source index | book-relative index | source dir | book dir + +# Module path for external API link +GO_MODULE ?= $(shell grep '^module ' go.mod | awk '{print $$2}') + +# Optional: set to your project's official documentation URL to include it in the book navigation. +# e.g. OFFICIAL_DOCS_URL = https://myproject.example.com +OFFICIAL_DOCS_URL ?= + +BOOK_SECTIONS := \ + "Official Documentation|$(MKDOCS_OUTPUT)/index.html|docs/index.html|$(MKDOCS_OUTPUT)|docs" \ + "Test Report|test-report.html|tests/index.html|test-report.html|tests" \ + "Coverage|coverage.html|coverage/index.html|coverage.html|coverage" + +# The 'book' target assembles documentation from available sources. +# 1. Aggregates Go documentation, coverage reports, and test results into _book. +# 2. Uses minibook to create a unified documentation site. +book:: test docs mkdocs-build ## compile the companion documentation book + @printf "${BLUE}[INFO] Building combined documentation...${RESET}\n" + @rm -rf _book && mkdir -p _book + + @printf "{\n" > _book/links.json + @printf ' "API": "./docs/API/index.html"' >> _book/links.json + @if [ -n "$(OFFICIAL_DOCS_URL)" ]; then \ + printf ",\n" >> _book/links.json; \ + printf ' "Official Docs": "%s"' "$(OFFICIAL_DOCS_URL)" >> _book/links.json; \ + fi + @first=0; \ + for entry in $(BOOK_SECTIONS); do \ + name=$${entry%%|*}; \ + rest=$${entry#*|}; \ + src_index=$${rest%%|*}; rest=$${rest#*|}; \ + book_index=$${rest%%|*}; rest=$${rest#*|}; \ + src_path=$${rest%%|*}; book_dir=$${rest#*|}; \ + if [ -f "$$src_index" ]; then \ + printf "${BLUE}[INFO] Adding $$name...${RESET}\n"; \ + mkdir -p "_book/$$book_dir"; \ + if [ -d "$$src_path" ]; then \ + cp -r "$$src_path/"* "_book/$$book_dir/"; \ + else \ + cp "$$src_path" "_book/$$book_index"; \ + fi; \ + 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; \ + if [ -n "$(LOGO_FILE)" ]; then \ + if [ -f "$(LOGO_FILE)" ]; then \ + cp "$(LOGO_FILE)" "_book/logo$$(echo $(LOGO_FILE) | sed 's/.*\(\.[^.]*\)$$/\1/')"; \ + printf "${BLUE}[INFO] Copying logo: $(LOGO_FILE)${RESET}\n"; \ + else \ + printf "${YELLOW}[WARN] Logo file $(LOGO_FILE) not found, skipping${RESET}\n"; \ + fi; \ + 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" + @printf "${BLUE}[INFO] Documentation book generated in _book/${RESET}\n" From 986b7e5bb521ba29083890862f0c02658ed8f89b Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 24 Feb 2026 23:23:14 +0400 Subject: [PATCH 28/31] Refactor bootstrap.mk for development setup and cleaning --- .rhiza/make.d/bootstrap.mk | 152 ++++++++++++------------------------- 1 file changed, 47 insertions(+), 105 deletions(-) diff --git a/.rhiza/make.d/bootstrap.mk b/.rhiza/make.d/bootstrap.mk index f001261..0aac99c 100644 --- a/.rhiza/make.d/bootstrap.mk +++ b/.rhiza/make.d/bootstrap.mk @@ -1,116 +1,58 @@ -## book.mk - Documentation book targets -# This file is included by the main Makefile. -# For Go projects, the book target compiles documentation from -# Go doc output, test coverage reports, and test results. +## .rhiza/make.d/bootstrap.mk - Bootstrap and Installation +# This file provides non-Go targets for setting up the development environment, +# installing dependencies, and cleaning project artifacts. +# Go-specific targets (install-go, install, build) are in bootstrap-go.mk. # Declare phony targets (they don't produce files) -.PHONY: book mkdocs-build - -# Default output directory for MkDocs -MKDOCS_OUTPUT ?= _mkdocs - -# MkDocs config file location -MKDOCS_CONFIG ?= docs/mkdocs.yml - -# Book configuration -BOOK_TITLE ?= $(shell basename $(PWD)) -BOOK_SUBTITLE ?= Go Project Documentation -BOOK_TEMPLATE ?= .rhiza/templates/minibook/custom.html.jinja2 - -##@ Book - -# Build MkDocs documentation site -mkdocs-build:: install-uv ## build mkdocs documentation site - @printf "${BLUE}[INFO] Building MkDocs site...${RESET}\n" - @if [ -f "$(MKDOCS_CONFIG)" ]; then \ - rm -rf "$(MKDOCS_OUTPUT)"; \ - MKDOCS_OUTPUT_ABS="$$(pwd)/$(MKDOCS_OUTPUT)"; \ - $(UVX_BIN) --from "mkdocs<2" --with "mkdocs-material<9.6" --with "pymdown-extensions>=10.0" mkdocs build \ - -f "$(MKDOCS_CONFIG)" \ - -d "$$MKDOCS_OUTPUT_ABS"; \ +.PHONY: install-uv 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 "${YELLOW}[WARN] $(MKDOCS_CONFIG) not found, skipping MkDocs build${RESET}\n"; \ + 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 -# ---------------------------- -# Book sections (declarative) -# ---------------------------- -# format: -# name | source index | book-relative index | source dir | book dir +clean: ## Clean project artifacts and stale local branches + @printf "%bCleaning project...%b\n" "$(BLUE)" "$(RESET)" -# Module path for external API link -GO_MODULE ?= $(shell grep '^module ' go.mod | awk '{print $$2}') + # Clean Go build cache and test cache + @$(GO_BIN) clean -cache -testcache -modcache || true -# Optional: set to your project's official documentation URL to include it in the book navigation. -# e.g. OFFICIAL_DOCS_URL = https://myproject.example.com -OFFICIAL_DOCS_URL ?= + # Remove ignored files/directories, but keep .env files, tested with futures project + @git clean -d -X -f \ + -e '!.env' \ + -e '!.env.*' -BOOK_SECTIONS := \ - "Official Documentation|$(MKDOCS_OUTPUT)/index.html|docs/index.html|$(MKDOCS_OUTPUT)|docs" \ - "Test Report|test-report.html|tests/index.html|test-report.html|tests" \ - "Coverage|coverage.html|coverage/index.html|coverage.html|coverage" + # Remove build artifacts + @rm -rf \ + dist \ + build \ + coverage.out \ + coverage.html \ + test-output.json \ + test-report.xml \ + test-report.html \ + *.test \ + *.prof -# The 'book' target assembles documentation from available sources. -# 1. Aggregates Go documentation, coverage reports, and test results into _book. -# 2. Uses minibook to create a unified documentation site. -book:: test docs mkdocs-build ## compile the companion documentation book - @printf "${BLUE}[INFO] Building combined documentation...${RESET}\n" - @rm -rf _book && mkdir -p _book + @printf "%bRemoving local branches with no remote counterpart...%b\n" "$(BLUE)" "$(RESET)" - @printf "{\n" > _book/links.json - @printf ' "API": "./docs/API/index.html"' >> _book/links.json - @if [ -n "$(OFFICIAL_DOCS_URL)" ]; then \ - printf ",\n" >> _book/links.json; \ - printf ' "Official Docs": "%s"' "$(OFFICIAL_DOCS_URL)" >> _book/links.json; \ - fi - @first=0; \ - for entry in $(BOOK_SECTIONS); do \ - name=$${entry%%|*}; \ - rest=$${entry#*|}; \ - src_index=$${rest%%|*}; rest=$${rest#*|}; \ - book_index=$${rest%%|*}; rest=$${rest#*|}; \ - src_path=$${rest%%|*}; book_dir=$${rest#*|}; \ - if [ -f "$$src_index" ]; then \ - printf "${BLUE}[INFO] Adding $$name...${RESET}\n"; \ - mkdir -p "_book/$$book_dir"; \ - if [ -d "$$src_path" ]; then \ - cp -r "$$src_path/"* "_book/$$book_dir/"; \ - else \ - cp "$$src_path" "_book/$$book_index"; \ - fi; \ - 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; \ - if [ -n "$(LOGO_FILE)" ]; then \ - if [ -f "$(LOGO_FILE)" ]; then \ - cp "$(LOGO_FILE)" "_book/logo$$(echo $(LOGO_FILE) | sed 's/.*\(\.[^.]*\)$$/\1/')"; \ - printf "${BLUE}[INFO] Copying logo: $(LOGO_FILE)${RESET}\n"; \ - else \ - printf "${YELLOW}[WARN] Logo file $(LOGO_FILE) not found, skipping${RESET}\n"; \ - fi; \ - 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" + @git fetch --prune - @touch "_book/.nojekyll" - @printf "${BLUE}[INFO] Documentation book generated in _book/${RESET}\n" + @git branch -vv | awk '/: gone]/{print $$1}' | xargs -r git branch -D From 0fb40df8daebef09e212c12c03092740de1ba561 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 24 Feb 2026 23:24:19 +0400 Subject: [PATCH 29/31] Refactor book.mk for improved documentation targets --- .rhiza/make.d/book.mk | 119 ++++++++++++++++-------------------------- 1 file changed, 45 insertions(+), 74 deletions(-) diff --git a/.rhiza/make.d/book.mk b/.rhiza/make.d/book.mk index ff7fde9..6f86296 100644 --- a/.rhiza/make.d/book.mk +++ b/.rhiza/make.d/book.mk @@ -1,113 +1,83 @@ -## book.mk - Book-building targets +## book.mk - Documentation book targets # This file is included by the main Makefile. -# It provides targets for exporting Marimo notebooks to HTML (marimushka) -# and compiling a companion book (minibook). +# For Go projects, the book target compiles documentation from +# Go doc output, test coverage reports, and test results. -# Default output directory for MkDocs (HTML site) +# Declare phony targets (they don't produce files) +.PHONY: book mkdocs-build + +# Default output directory for MkDocs MKDOCS_OUTPUT ?= _mkdocs # MkDocs config file location MKDOCS_CONFIG ?= docs/mkdocs.yml -# The 'mkdocs-build' target builds the MkDocs documentation site. -# 1. Checks if the mkdocs.yml config file exists. -# 2. Cleans up any previous output. -# 3. Builds the static site using mkdocs with material theme. -mkdocs-build: ## build MkDocs documentation site +# Book configuration +BOOK_TITLE ?= $(shell basename $(PWD)) +BOOK_SUBTITLE ?= Project Documentation +BOOK_TEMPLATE ?= .rhiza/templates/minibook/custom.html.jinja2 + +##@ Book + +# Build MkDocs documentation site +mkdocs-build:: install-uv ## build mkdocs documentation site @printf "${BLUE}[INFO] Building MkDocs site...${RESET}\n" @if [ -f "$(MKDOCS_CONFIG)" ]; then \ rm -rf "$(MKDOCS_OUTPUT)"; \ MKDOCS_OUTPUT_ABS="$$(pwd)/$(MKDOCS_OUTPUT)"; \ - ${UVX_BIN} --with mkdocs-material --with "pymdown-extensions>=10.0" mkdocs build \ + $(UVX_BIN) --from "mkdocs<2" --with "mkdocs-material<9.6" --with "pymdown-extensions>=10.0" mkdocs build \ -f "$(MKDOCS_CONFIG)" \ -d "$$MKDOCS_OUTPUT_ABS"; \ else \ printf "${YELLOW}[WARN] $(MKDOCS_CONFIG) not found, skipping MkDocs build${RESET}\n"; \ fi - -# Declare phony targets (they don't produce files) -.PHONY: marimushka mkdocs-build book test install-uv - -# 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 - -# Define a default no-op install-uv target that will be used -# when no language-specific mk file defines install-uv -install-uv:: - @printf "${BLUE}[INFO] No install-uv target defined, skipping${RESET}\n" - -# Define a default no-op test target that will be used -# when no language-specific mk file defines test -test:: - @printf "${BLUE}[INFO] No test target defined, skipping tests${RESET}\n" - -# Default output directory for Marimushka (HTML exports of notebooks) -MARIMUSHKA_OUTPUT ?= _marimushka - -# Default output directory for MkDocs -MKDOCS_OUTPUT ?= _mkdocs - # ---------------------------- # 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" \ - "Official Documentation|_mkdocs/index.html|docs/index.html|_mkdocs|docs" +# Module path for external API link +GO_MODULE ?= $(shell grep '^module ' go.mod | awk '{print $$2}') -##@ Book +# Optional: set to your project's official documentation URL to include it in the book navigation. +# e.g. OFFICIAL_DOCS_URL = https://myproject.example.com +OFFICIAL_DOCS_URL ?= -# The 'book' target assembles the final documentation book. -# 1. Aggregates API docs, coverage, test reports, notebooks, and MkDocs site into _book. -# 2. Generates links.json to define the book structure. -# 3. Uses 'minibook' to compile the final HTML site. -book:: test docs marimushka mkdocs-build ## compile the companion book +BOOK_SECTIONS := \ + "Official Documentation|$(MKDOCS_OUTPUT)/index.html|docs/index.html|$(MKDOCS_OUTPUT)|docs" \ + "Test Report|test-report.html|tests/index.html|test-report.html|tests" \ + "Coverage|coverage.html|coverage/index.html|coverage.html|coverage" + +# The 'book' target assembles documentation from available sources. +# 1. Aggregates Go documentation, coverage reports, and test results into _book. +# 2. Uses minibook to create a unified documentation site. +book:: test docs mkdocs-build ## compile the companion documentation book @printf "${BLUE}[INFO] Building combined documentation...${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; \ + @printf ' "API": "./docs/API/index.html"' >> _book/links.json + @if [ -n "$(OFFICIAL_DOCS_URL)" ]; then \ + printf ",\n" >> _book/links.json; \ + printf ' "Official Docs": "%s"' "$(OFFICIAL_DOCS_URL)" >> _book/links.json; \ + fi + @first=0; \ 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#*|}; \ + src_path=$${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 [ -d "$$src_path" ]; then \ + cp -r "$$src_path/"* "_book/$$book_dir/"; \ + else \ + cp "$$src_path" "_book/$$book_index"; \ + fi; \ if [ $$first -eq 0 ]; then \ printf ",\n" >> _book/links.json; \ fi; \ @@ -129,7 +99,7 @@ json.dump(badge, open('_book/tests/coverage-badge.json', 'w'))"; \ fi; \ if [ -n "$(LOGO_FILE)" ]; then \ if [ -f "$(LOGO_FILE)" ]; then \ - cp "$(LOGO_FILE)" "_book/logo$$(echo $(LOGO_FILE) | sed 's/.*\./\./')"; \ + cp "$(LOGO_FILE)" "_book/logo$$(echo $(LOGO_FILE) | sed 's/.*\(\.[^.]*\)$$/\1/')"; \ printf "${BLUE}[INFO] Copying logo: $(LOGO_FILE)${RESET}\n"; \ else \ printf "${YELLOW}[WARN] Logo file $(LOGO_FILE) not found, skipping${RESET}\n"; \ @@ -143,3 +113,4 @@ json.dump(badge, open('_book/tests/coverage-badge.json', 'w'))"; \ --output "_book" @touch "_book/.nojekyll" + @printf "${BLUE}[INFO] Documentation book generated in _book/${RESET}\n" From 20f455f5f4248693c5546be12085d4df8ff9c5c8 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 24 Feb 2026 23:25:19 +0400 Subject: [PATCH 30/31] Disable branch restrictions for push events Comment out branch specifications for push events. --- .github/workflows/rhiza_book.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rhiza_book.yml b/.github/workflows/rhiza_book.yml index f7a1e59..c221938 100644 --- a/.github/workflows/rhiza_book.yml +++ b/.github/workflows/rhiza_book.yml @@ -18,9 +18,9 @@ name: "(RHIZA) BOOK" on: push: - branches: - - main - - master + #branches: + # - main + # - master jobs: book: From 82d8df9c405cf4d060b2987b9d77a22efe5d42c4 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 24 Feb 2026 23:28:20 +0400 Subject: [PATCH 31/31] Enable workflow for main and master branches --- .github/workflows/rhiza_book.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rhiza_book.yml b/.github/workflows/rhiza_book.yml index c221938..f7a1e59 100644 --- a/.github/workflows/rhiza_book.yml +++ b/.github/workflows/rhiza_book.yml @@ -18,9 +18,9 @@ name: "(RHIZA) BOOK" on: push: - #branches: - # - main - # - master + branches: + - main + - master jobs: book: