rhiza 0.8.4__tar.gz → 0.8.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. {rhiza-0.8.4 → rhiza-0.8.6}/.github/workflows/rhiza_book.yml +4 -1
  2. {rhiza-0.8.4 → rhiza-0.8.6}/.github/workflows/rhiza_ci.yml +9 -8
  3. {rhiza-0.8.4 → rhiza-0.8.6}/.github/workflows/rhiza_deptry.yml +5 -8
  4. {rhiza-0.8.4 → rhiza-0.8.6}/.github/workflows/rhiza_release.yml +2 -2
  5. rhiza-0.8.6/.rhiza/.cfg.toml +31 -0
  6. {rhiza-0.8.4 → rhiza-0.8.6}/.rhiza/history +7 -4
  7. rhiza-0.8.6/.rhiza/requirements/README.md +27 -0
  8. rhiza-0.8.6/.rhiza/requirements/docs.txt +2 -0
  9. rhiza-0.8.6/.rhiza/requirements/marimo.txt +2 -0
  10. rhiza-0.8.6/.rhiza/requirements/tests.txt +4 -0
  11. rhiza-0.8.6/.rhiza/requirements/tools.txt +3 -0
  12. rhiza-0.8.6/.rhiza/scripts/install-dev-deps.sh +25 -0
  13. rhiza-0.8.6/.rhiza/utils/version_matrix.py +117 -0
  14. {rhiza-0.8.4 → rhiza-0.8.6}/Makefile +37 -15
  15. {rhiza-0.8.4 → rhiza-0.8.6}/PKG-INFO +2 -10
  16. {rhiza-0.8.4 → rhiza-0.8.6}/pyproject.toml +8 -15
  17. {rhiza-0.8.4 → rhiza-0.8.6}/tests/Makefile.tests +2 -2
  18. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_rhiza/test_makefile.py +5 -44
  19. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_rhiza/test_release_script.py +2 -3
  20. rhiza-0.8.6/tests/test_rhiza/test_requirements_folder.py +64 -0
  21. {rhiza-0.8.4 → rhiza-0.8.6}/uv.lock +464 -312
  22. rhiza-0.8.4/.rhiza/utils/version_matrix.py +0 -63
  23. rhiza-0.8.4/.rhiza/utils/version_max.py +0 -61
  24. {rhiza-0.8.4 → rhiza-0.8.6}/.editorconfig +0 -0
  25. {rhiza-0.8.4 → rhiza-0.8.6}/.github/ISSUE_TEMPLATE/assign_ui_implementation.md +0 -0
  26. {rhiza-0.8.4 → rhiza-0.8.6}/.github/workflows/rhiza_codeql.yml +0 -0
  27. {rhiza-0.8.4 → rhiza-0.8.6}/.github/workflows/rhiza_marimo.yml +0 -0
  28. {rhiza-0.8.4 → rhiza-0.8.6}/.github/workflows/rhiza_pre-commit.yml +0 -0
  29. {rhiza-0.8.4 → rhiza-0.8.6}/.github/workflows/rhiza_sync.yml +0 -0
  30. {rhiza-0.8.4 → rhiza-0.8.6}/.github/workflows/rhiza_validate.yml +0 -0
  31. {rhiza-0.8.4 → rhiza-0.8.6}/.gitignore +0 -0
  32. {rhiza-0.8.4 → rhiza-0.8.6}/.pre-commit-config.yaml +0 -0
  33. {rhiza-0.8.4 → rhiza-0.8.6}/.python-version +0 -0
  34. {rhiza-0.8.4 → rhiza-0.8.6}/.rhiza/docs/CONFIG.md +0 -0
  35. {rhiza-0.8.4 → rhiza-0.8.6}/.rhiza/docs/TOKEN_SETUP.md +0 -0
  36. {rhiza-0.8.4 → rhiza-0.8.6}/.rhiza/scripts/book.sh +0 -0
  37. {rhiza-0.8.4 → rhiza-0.8.6}/.rhiza/scripts/bump.sh +0 -0
  38. {rhiza-0.8.4 → rhiza-0.8.6}/.rhiza/scripts/customisations/build-extras.sh +0 -0
  39. {rhiza-0.8.4 → rhiza-0.8.6}/.rhiza/scripts/customisations/post-release.sh +0 -0
  40. {rhiza-0.8.4 → rhiza-0.8.6}/.rhiza/scripts/generate-coverage-badge.sh +0 -0
  41. {rhiza-0.8.4 → rhiza-0.8.6}/.rhiza/scripts/marimushka.sh +0 -0
  42. {rhiza-0.8.4 → rhiza-0.8.6}/.rhiza/scripts/release.sh +0 -0
  43. {rhiza-0.8.4 → rhiza-0.8.6}/.rhiza/scripts/update-readme-help.sh +0 -0
  44. {rhiza-0.8.4 → rhiza-0.8.6}/.rhiza/template.yml +0 -0
  45. {rhiza-0.8.4 → rhiza-0.8.6}/.rhiza.env +0 -0
  46. {rhiza-0.8.4 → rhiza-0.8.6}/CLI.md +0 -0
  47. {rhiza-0.8.4 → rhiza-0.8.6}/CODE_OF_CONDUCT.md +0 -0
  48. {rhiza-0.8.4 → rhiza-0.8.6}/CONTRIBUTING.md +0 -0
  49. {rhiza-0.8.4 → rhiza-0.8.6}/GETTING_STARTED.md +0 -0
  50. {rhiza-0.8.4 → rhiza-0.8.6}/LICENSE +0 -0
  51. {rhiza-0.8.4 → rhiza-0.8.6}/README.md +0 -0
  52. {rhiza-0.8.4 → rhiza-0.8.6}/USAGE.md +0 -0
  53. {rhiza-0.8.4 → rhiza-0.8.6}/book/Makefile.book +0 -0
  54. {rhiza-0.8.4 → rhiza-0.8.6}/book/README.md +0 -0
  55. {rhiza-0.8.4 → rhiza-0.8.6}/book/marimo/.gitkeep +0 -0
  56. {rhiza-0.8.4 → rhiza-0.8.6}/book/marimo/README.md +0 -0
  57. {rhiza-0.8.4 → rhiza-0.8.6}/book/marimo/rhiza.py +0 -0
  58. {rhiza-0.8.4 → rhiza-0.8.6}/book/minibook-templates/custom.html.jinja2 +0 -0
  59. {rhiza-0.8.4 → rhiza-0.8.6}/book/pdoc-templates/module.html.jinja2 +0 -0
  60. {rhiza-0.8.4 → rhiza-0.8.6}/presentation/Makefile.presentation +0 -0
  61. {rhiza-0.8.4 → rhiza-0.8.6}/presentation/README.md +0 -0
  62. {rhiza-0.8.4 → rhiza-0.8.6}/pytest.ini +0 -0
  63. {rhiza-0.8.4 → rhiza-0.8.6}/renovate.json +0 -0
  64. {rhiza-0.8.4 → rhiza-0.8.6}/ruff.toml +0 -0
  65. {rhiza-0.8.4 → rhiza-0.8.6}/src/rhiza/__init__.py +0 -0
  66. {rhiza-0.8.4 → rhiza-0.8.6}/src/rhiza/__main__.py +0 -0
  67. {rhiza-0.8.4 → rhiza-0.8.6}/src/rhiza/_templates/basic/__init__.py.jinja2 +0 -0
  68. {rhiza-0.8.4 → rhiza-0.8.6}/src/rhiza/_templates/basic/main.py.jinja2 +0 -0
  69. {rhiza-0.8.4 → rhiza-0.8.6}/src/rhiza/_templates/basic/pyproject.toml.jinja2 +0 -0
  70. {rhiza-0.8.4 → rhiza-0.8.6}/src/rhiza/cli.py +0 -0
  71. {rhiza-0.8.4 → rhiza-0.8.6}/src/rhiza/commands/__init__.py +0 -0
  72. {rhiza-0.8.4 → rhiza-0.8.6}/src/rhiza/commands/init.py +0 -0
  73. {rhiza-0.8.4 → rhiza-0.8.6}/src/rhiza/commands/materialize.py +0 -0
  74. {rhiza-0.8.4 → rhiza-0.8.6}/src/rhiza/commands/migrate.py +0 -0
  75. {rhiza-0.8.4 → rhiza-0.8.6}/src/rhiza/commands/uninstall.py +0 -0
  76. {rhiza-0.8.4 → rhiza-0.8.6}/src/rhiza/commands/validate.py +0 -0
  77. {rhiza-0.8.4 → rhiza-0.8.6}/src/rhiza/commands/welcome.py +0 -0
  78. {rhiza-0.8.4 → rhiza-0.8.6}/src/rhiza/models.py +0 -0
  79. {rhiza-0.8.4 → rhiza-0.8.6}/src/rhiza/subprocess_utils.py +0 -0
  80. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_cli_commands.py +0 -0
  81. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_commands/test_init.py +0 -0
  82. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_commands/test_materialize.py +0 -0
  83. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_commands/test_migrate.py +0 -0
  84. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_commands/test_uninstall.py +0 -0
  85. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_commands/test_validate.py +0 -0
  86. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_models.py +0 -0
  87. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_package.py +0 -0
  88. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_rhiza/README.md +0 -0
  89. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_rhiza/benchmarks/.gitignore +0 -0
  90. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_rhiza/benchmarks/README.md +0 -0
  91. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_rhiza/benchmarks/analyze_benchmarks.py +0 -0
  92. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_rhiza/conftest.py +0 -0
  93. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_rhiza/test_bump_script.py +0 -0
  94. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_rhiza/test_coverage_badge.py +0 -0
  95. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_rhiza/test_docstrings.py +0 -0
  96. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_rhiza/test_git_repo_fixture.py +0 -0
  97. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_rhiza/test_marimushka_script.py +0 -0
  98. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_rhiza/test_notebooks.py +0 -0
  99. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_rhiza/test_readme.py +0 -0
  100. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_rhiza/test_structure.py +0 -0
  101. {rhiza-0.8.4 → rhiza-0.8.6}/tests/test_rhiza/test_updatereadme_script.py +0 -0
@@ -50,7 +50,10 @@ jobs:
50
50
  run: |
51
51
  export UV_EXTRA_INDEX_URL="${{ secrets.uv-extra-index-url }}"
52
52
  # will just use .python-version?
53
- uv sync --all-extras --frozen
53
+ uv sync --all-extras --all-groups --frozen
54
+
55
+ - name: Install dev dependencies
56
+ run: bash .rhiza/scripts/install-dev-deps.sh
54
57
 
55
58
  - name: "Make the book"
56
59
  run: |
@@ -26,10 +26,15 @@ jobs:
26
26
  steps:
27
27
  - uses: actions/checkout@v6
28
28
 
29
+ - name: Install uv
30
+ uses: astral-sh/setup-uv@v7
31
+ with:
32
+ version: "0.9.21"
33
+
29
34
  - id: versions
30
35
  run: |
31
36
  # Generate Python versions JSON from the script
32
- JSON=$(python ./.rhiza/utils/version_matrix.py)
37
+ JSON=$(make -s version-matrix)
33
38
  echo "list=$JSON" >> "$GITHUB_OUTPUT"
34
39
 
35
40
  - name: Debug matrix
@@ -60,14 +65,10 @@ jobs:
60
65
  run: |
61
66
  export UV_EXTRA_INDEX_URL="${{ secrets.uv-extra-index-url }}"
62
67
  uv venv --python ${{ matrix.python-version }}
63
- uv sync --all-extras --frozen
68
+ uv sync --all-extras --all-groups --frozen
64
69
 
65
- - name: Install dependencies
66
- run: |
67
- if [ -f "tests/requirements.txt" ]; then
68
- uv pip install -r tests/requirements.txt
69
- fi
70
- uv pip install pytest
70
+ - name: Install dev dependencies
71
+ run: bash .rhiza/scripts/install-dev-deps.sh
71
72
 
72
73
  - name: Run tests
73
74
  run: uv run pytest tests
@@ -33,11 +33,8 @@ jobs:
33
33
  - uses: actions/checkout@v6
34
34
 
35
35
  - name: Run deptry
36
- run: |
37
- set -euo pipefail
38
- if [ -d "src" ]; then
39
- SOURCE_FOLDER="src"
40
- else
41
- SOURCE_FOLDER="."
42
- fi
43
- uvx deptry "$SOURCE_FOLDER"
36
+ run: make deptry
37
+ # NOTE: make deptry is good style because it encapsulates the folders to check
38
+ # (e.g. src and book/marimo) and keeps CI in sync with local development.
39
+ # Since we use a 'uv' container, the Makefile is optimized to use the
40
+ # pre-installed 'uv' and 'uvx' from the system PATH.
@@ -118,7 +118,7 @@ jobs:
118
118
  run: |
119
119
  export UV_EXTRA_INDEX_URL="${{ secrets.uv-extra-index-url }}"
120
120
  # will just use .python-version?
121
- uv sync --all-extras --frozen
121
+ uv sync --all-extras --all-groups --frozen
122
122
 
123
123
  - name: Verify version matches tag
124
124
  if: hashFiles('pyproject.toml') != ''
@@ -329,7 +329,7 @@ jobs:
329
329
  run: |
330
330
  export UV_EXTRA_INDEX_URL="${{ secrets.uv-extra-index-url }}"
331
331
  # will just use .python-version?
332
- uv sync --all-extras --frozen
332
+ uv sync --all-extras --all-groups --frozen
333
333
 
334
334
  - name: Set up Python
335
335
  uses: actions/setup-python@v6
@@ -0,0 +1,31 @@
1
+ [tool.bumpversion]
2
+ parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(?:-(?P<release>[a-z]+)\\.(?P<pre_n>\\d+))?(?:\\+build\\.(?P<build_n>\\d+))?"
3
+ serialize = ["{major}.{minor}.{patch}-{release}.{pre_n}+build.{build_n}", "{major}.{minor}.{patch}+build.{build_n}", "{major}.{minor}.{patch}-{release}.{pre_n}", "{major}.{minor}.{patch}"]
4
+ search = "{current_version}"
5
+ replace = "{new_version}"
6
+ regex = false
7
+ ignore_missing_version = false
8
+ ignore_missing_files = false
9
+ tag = false
10
+ sign_tags = false
11
+ tag_name = "v{new_version}"
12
+ tag_message = "Bump version: {current_version} → {new_version}"
13
+ allow_dirty = false
14
+ commit = false
15
+ message = "Chore: bump version {current_version} → {new_version}"
16
+ commit_args = ""
17
+
18
+ [tool.bumpversion.parts.release]
19
+ optional_value = "prod"
20
+ values = [
21
+ "dev",
22
+ "alpha",
23
+ "beta",
24
+ "rc",
25
+ "prod"
26
+ ]
27
+
28
+ [[tool.bumpversion.files]]
29
+ filename = "pyproject.toml"
30
+ search = 'version = "{current_version}"'
31
+ replace = 'version = "{new_version}"'
@@ -5,13 +5,10 @@
5
5
  #
6
6
  # Files under template control:
7
7
  .editorconfig
8
- .github/dependabot.yml
9
8
  .github/workflows/rhiza_book.yml
10
9
  .github/workflows/rhiza_ci.yml
11
10
  .github/workflows/rhiza_codeql.yml
12
11
  .github/workflows/rhiza_deptry.yml
13
- .github/workflows/rhiza_devcontainer.yml
14
- .github/workflows/rhiza_docker.yml
15
12
  .github/workflows/rhiza_marimo.yml
16
13
  .github/workflows/rhiza_pre-commit.yml
17
14
  .github/workflows/rhiza_release.yml
@@ -21,16 +18,21 @@
21
18
  .pre-commit-config.yaml
22
19
  .rhiza/docs/CONFIG.md
23
20
  .rhiza/docs/TOKEN_SETUP.md
21
+ .rhiza/requirements/README.md
22
+ .rhiza/requirements/docs.txt
23
+ .rhiza/requirements/marimo.txt
24
+ .rhiza/requirements/tests.txt
25
+ .rhiza/requirements/tools.txt
24
26
  .rhiza/scripts/book.sh
25
27
  .rhiza/scripts/bump.sh
26
28
  .rhiza/scripts/customisations/build-extras.sh
27
29
  .rhiza/scripts/customisations/post-release.sh
28
30
  .rhiza/scripts/generate-coverage-badge.sh
31
+ .rhiza/scripts/install-dev-deps.sh
29
32
  .rhiza/scripts/marimushka.sh
30
33
  .rhiza/scripts/release.sh
31
34
  .rhiza/scripts/update-readme-help.sh
32
35
  .rhiza/utils/version_matrix.py
33
- .rhiza/utils/version_max.py
34
36
  CODE_OF_CONDUCT.md
35
37
  CONTRIBUTING.md
36
38
  Makefile
@@ -60,5 +62,6 @@ tests/test_rhiza/test_marimushka_script.py
60
62
  tests/test_rhiza/test_notebooks.py
61
63
  tests/test_rhiza/test_readme.py
62
64
  tests/test_rhiza/test_release_script.py
65
+ tests/test_rhiza/test_requirements_folder.py
63
66
  tests/test_rhiza/test_structure.py
64
67
  tests/test_rhiza/test_updatereadme_script.py
@@ -0,0 +1,27 @@
1
+ # Requirements Folder
2
+
3
+ This folder contains the development dependencies for the Rhiza project, organized by purpose.
4
+
5
+ ## Files
6
+
7
+ - **tests.txt** - Testing dependencies (pytest, pytest-cov, pytest-html)
8
+ - **marimo.txt** - Marimo notebook dependencies
9
+ - **docs.txt** - Documentation generation dependencies (pdoc)
10
+ - **tools.txt** - Development tools (pre-commit, python-dotenv)
11
+
12
+ ## Usage
13
+
14
+ These requirements files are automatically installed by the `make install` command.
15
+
16
+ To install specific requirement files manually:
17
+
18
+ ```bash
19
+ uv pip install -r .rhiza/requirements/tests.txt
20
+ uv pip install -r .rhiza/requirements/marimo.txt
21
+ uv pip install -r .rhiza/requirements/docs.txt
22
+ uv pip install -r .rhiza/requirements/tools.txt
23
+ ```
24
+
25
+ ## CI/CD
26
+
27
+ GitHub Actions workflows automatically install these requirements as needed.
@@ -0,0 +1,2 @@
1
+ # Documentation dependencies for rhiza
2
+ pdoc>=16.0.0
@@ -0,0 +1,2 @@
1
+ # Marimo dependencies for rhiza
2
+ marimo>=0.18.0
@@ -0,0 +1,4 @@
1
+ # Test dependencies for rhiza
2
+ pytest==9.0.2
3
+ pytest-cov>=7.0.0
4
+ pytest-html>=4.1.1
@@ -0,0 +1,3 @@
1
+ # Development tool dependencies for rhiza
2
+ pre-commit==4.5.1
3
+ python-dotenv==1.2.1
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ # Install dev dependencies from .rhiza/requirements/*.txt files
3
+ # This script is used by GitHub Actions workflows to install development dependencies
4
+
5
+ set -euo pipefail
6
+
7
+ REQUIREMENTS_DIR=".rhiza/requirements"
8
+
9
+ if [ ! -d "$REQUIREMENTS_DIR" ]; then
10
+ echo "Warning: Requirements directory $REQUIREMENTS_DIR not found, skipping dev dependencies install" >&2
11
+ exit 0
12
+ fi
13
+
14
+ echo "Installing dev dependencies from $REQUIREMENTS_DIR"
15
+
16
+ shopt -s nullglob
17
+ for req_file in "$REQUIREMENTS_DIR"/*.txt; do
18
+ if [ -f "$req_file" ]; then
19
+ echo "Installing requirements from $req_file"
20
+ uv pip install -r "$req_file"
21
+ fi
22
+ done
23
+ shopt -u nullglob
24
+
25
+ echo "Dev dependencies installed successfully"
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env python3
2
+ """Emit the list of supported Python versions from pyproject.toml.
3
+
4
+ This helper is used in GitHub Actions to compute the test matrix.
5
+ """
6
+
7
+ import json
8
+ import re
9
+ import tomllib
10
+ from pathlib import Path
11
+
12
+ PYPROJECT = Path(__file__).resolve().parents[2] / "pyproject.toml"
13
+ CANDIDATES = ["3.11", "3.12", "3.13", "3.14"] # extend as needed
14
+
15
+
16
+ def parse_version(v: str) -> tuple[int, ...]:
17
+ """Parse a version string into a tuple of integers.
18
+
19
+ This is intentionally simple and only supports numeric components.
20
+ If a component contains non-numeric suffixes (e.g. '3.11.0rc1'),
21
+ the leading numeric portion will be used (e.g. '0rc1' -> 0). If a
22
+ component has no leading digits at all, a ValueError is raised.
23
+ """
24
+ parts: list[int] = []
25
+ for part in v.split("."):
26
+ match = re.match(r"\d+", part)
27
+ if not match:
28
+ msg = f"Invalid version component {part!r} in version {v!r}; expected a numeric prefix."
29
+ raise ValueError(msg)
30
+ parts.append(int(match.group(0)))
31
+ return tuple(parts)
32
+
33
+
34
+ def _check_operator(version_tuple: tuple[int, ...], op: str, spec_v_tuple: tuple[int, ...]) -> bool:
35
+ """Check if a version tuple satisfies an operator constraint."""
36
+ operators = {
37
+ ">=": lambda v, s: v >= s,
38
+ "<=": lambda v, s: v <= s,
39
+ ">": lambda v, s: v > s,
40
+ "<": lambda v, s: v < s,
41
+ "==": lambda v, s: v == s,
42
+ "!=": lambda v, s: v != s,
43
+ }
44
+ return operators[op](version_tuple, spec_v_tuple)
45
+
46
+
47
+ def satisfies(version: str, specifier: str) -> bool:
48
+ """Check if a version satisfies a comma-separated list of specifiers.
49
+
50
+ This is a simplified version of packaging.specifiers.SpecifierSet.
51
+ Supported operators: >=, <=, >, <, ==, !=
52
+ """
53
+ version_tuple = parse_version(version)
54
+
55
+ # Split by comma for multiple constraints
56
+ for spec in specifier.split(","):
57
+ spec = spec.strip()
58
+ # Match operator and version part
59
+ match = re.match(r"(>=|<=|>|<|==|!=)\s*([\d.]+)", spec)
60
+ if not match:
61
+ # If no operator, assume ==
62
+ if re.match(r"[\d.]+", spec):
63
+ if version_tuple != parse_version(spec):
64
+ return False
65
+ continue
66
+ raise ValueError(f"Invalid specifier: {spec}")
67
+
68
+ op, spec_v = match.groups()
69
+ spec_v_tuple = parse_version(spec_v)
70
+
71
+ if not _check_operator(version_tuple, op, spec_v_tuple):
72
+ return False
73
+
74
+ return True
75
+
76
+
77
+ def supported_versions() -> list[str]:
78
+ """Return all supported Python versions declared in pyproject.toml.
79
+
80
+ Reads project.requires-python, evaluates candidate versions against the
81
+ specifier, and returns the subset that satisfy the constraint, in ascending order.
82
+
83
+ Returns:
84
+ list[str]: The supported versions (e.g., ["3.11", "3.12"]).
85
+ """
86
+ # Load pyproject.toml using the tomllib standard library (Python 3.11+)
87
+ with PYPROJECT.open("rb") as f:
88
+ data = tomllib.load(f)
89
+
90
+ # Extract the requires-python field from project metadata
91
+ # This specifies the Python version constraint (e.g., ">=3.11")
92
+ spec_str = data.get("project", {}).get("requires-python")
93
+ if not spec_str:
94
+ msg = "pyproject.toml: missing 'project.requires-python'"
95
+ raise KeyError(msg)
96
+
97
+ # Filter candidate versions to find which ones satisfy the constraint
98
+ versions: list[str] = []
99
+ for v in CANDIDATES:
100
+ if satisfies(v, spec_str):
101
+ versions.append(v)
102
+
103
+ if not versions:
104
+ msg = f"pyproject.toml: no supported Python versions match '{spec_str}'"
105
+ raise ValueError(msg)
106
+
107
+ return versions
108
+
109
+
110
+ if __name__ == "__main__":
111
+ # Check if pyproject.toml exists in the expected location
112
+ # If it exists, use it to determine supported versions
113
+ # Otherwise, fall back to returning all candidates (for edge cases)
114
+ if PYPROJECT.exists():
115
+ print(json.dumps(supported_versions()))
116
+ else:
117
+ print(json.dumps(CANDIDATES))
@@ -34,8 +34,8 @@ RESET := \033[0m
34
34
  update-readme
35
35
 
36
36
  UV_INSTALL_DIR ?= ./bin
37
- UV_BIN := ${UV_INSTALL_DIR}/uv
38
- UVX_BIN := ${UV_INSTALL_DIR}/uvx
37
+ UV_BIN ?= $(shell command -v uv 2>/dev/null || echo ${UV_INSTALL_DIR}/uv)
38
+ UVX_BIN ?= $(shell command -v uvx 2>/dev/null || echo ${UV_INSTALL_DIR}/uvx)
39
39
  VENV ?= .venv
40
40
 
41
41
  export UV_NO_MODIFY_PATH := 1
@@ -54,11 +54,13 @@ install-uv: ## ensure uv/uvx is installed
54
54
  # Ensure the ${UV_INSTALL_DIR} folder exists
55
55
  @mkdir -p ${UV_INSTALL_DIR}
56
56
 
57
- # Install uv/uvx only if they are not already present
58
- @if [ -x "${UV_INSTALL_DIR}/uv" ] && [ -x "${UV_INSTALL_DIR}/uvx" ]; then \
57
+ # Install uv/uvx only if they are not already present in PATH or in the install dir
58
+ @if command -v uv >/dev/null 2>&1 && command -v uvx >/dev/null 2>&1; then \
59
+ :; \
60
+ elif [ -x "${UV_INSTALL_DIR}/uv" ] && [ -x "${UV_INSTALL_DIR}/uvx" ]; then \
59
61
  printf "${BLUE}[INFO] uv and uvx already installed in ${UV_INSTALL_DIR}, skipping.${RESET}\n"; \
60
62
  else \
61
- printf "${BLUE}[INFO] Installing uv and uvx...${RESET}\n"; \
63
+ printf "${BLUE}[INFO] Installing uv and uvx into ${UV_INSTALL_DIR}...${RESET}\n"; \
62
64
  if ! curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR="${UV_INSTALL_DIR}" sh >/dev/null 2>&1; then \
63
65
  printf "${RED}[ERROR] Failed to install uv${RESET}\n"; \
64
66
  exit 1; \
@@ -84,16 +86,11 @@ install: install-uv install-extras ## install
84
86
  printf "${BLUE}[INFO] Using existing virtual environment at ${VENV}, skipping creation${RESET}\n"; \
85
87
  fi
86
88
 
87
- # Check if there is requirements.txt file in the tests folder
88
- @if [ -f "tests/requirements.txt" ]; then \
89
- ${UV_BIN} pip install -r tests/requirements.txt || { printf "${RED}[ERROR] Failed to install test requirements${RESET}\n"; exit 1; }; \
90
- fi
91
-
92
89
  # Install the dependencies from pyproject.toml (if it exists)
93
90
  @if [ -f "pyproject.toml" ]; then \
94
91
  if [ -f "uv.lock" ]; then \
95
92
  printf "${BLUE}[INFO] Installing dependencies from lock file${RESET}\n"; \
96
- ${UV_BIN} sync --all-extras --frozen || { printf "${RED}[ERROR] Failed to install dependencies${RESET}\n"; exit 1; }; \
93
+ ${UV_BIN} sync --all-extras --all-groups --frozen || { printf "${RED}[ERROR] Failed to install dependencies${RESET}\n"; exit 1; }; \
97
94
  else \
98
95
  printf "${YELLOW}[WARN] uv.lock not found. Generating lock file and installing dependencies...${RESET}\n"; \
99
96
  ${UV_BIN} sync --all-extras || { printf "${RED}[ERROR] Failed to install dependencies${RESET}\n"; exit 1; }; \
@@ -102,6 +99,22 @@ install: install-uv install-extras ## install
102
99
  printf "${YELLOW}[WARN] No pyproject.toml found, skipping install${RESET}\n"; \
103
100
  fi
104
101
 
102
+ # Install dev dependencies from .rhiza/requirements/*.txt files
103
+ @if [ -d ".rhiza/requirements" ] && ls .rhiza/requirements/*.txt >/dev/null 2>&1; then \
104
+ for req_file in .rhiza/requirements/*.txt; do \
105
+ if [ -f "$$req_file" ]; then \
106
+ printf "${BLUE}[INFO] Installing requirements from $$req_file${RESET}\n"; \
107
+ ${UV_BIN} pip install -r "$$req_file" || { printf "${RED}[ERROR] Failed to install requirements from $$req_file${RESET}\n"; exit 1; }; \
108
+ fi; \
109
+ done; \
110
+ fi
111
+
112
+ # Check if there is requirements.txt file in the tests folder (legacy support)
113
+ @if [ -f "tests/requirements.txt" ]; then \
114
+ printf "${BLUE}[INFO] Installing requirements from tests/requirements.txt${RESET}\n"; \
115
+ ${UV_BIN} pip install -r tests/requirements.txt || { printf "${RED}[ERROR] Failed to install test requirements${RESET}\n"; exit 1; }; \
116
+ fi
117
+
105
118
  sync: ## sync with template repository as defined in .github/template.yml
106
119
  @if git remote get-url origin 2>/dev/null | grep -iq 'jebel-quant/rhiza'; then \
107
120
  printf "${BLUE}[INFO] Skipping sync in rhiza repository (no template.yml by design)${RESET}\n"; \
@@ -150,10 +163,16 @@ marimo: install ## fire up Marimo server
150
163
 
151
164
  ##@ Quality and Formatting
152
165
  deptry: install-uv ## Run deptry
153
- @if [ -d src ]; then \
154
- $(UVX_BIN) deptry src; \
155
- else \
156
- $(UVX_BIN) deptry .; \
166
+ @if [ -d ${SOURCE_FOLDER} ]; then \
167
+ $(UVX_BIN) deptry ${SOURCE_FOLDER}; \
168
+ fi
169
+
170
+ @if [ -d ${MARIMO_FOLDER} ]; then \
171
+ if [ -d ${SOURCE_FOLDER} ]; then \
172
+ $(UVX_BIN) deptry ${MARIMO_FOLDER} ${SOURCE_FOLDER} --ignore DEP004; \
173
+ else \
174
+ $(UVX_BIN) deptry ${MARIMO_FOLDER} --ignore DEP004; \
175
+ fi \
157
176
  fi
158
177
 
159
178
  fmt: install-uv ## check the pre-commit hooks and the linting
@@ -197,6 +216,9 @@ customisations: ## list available customisation scripts
197
216
  update-readme: ## update README.md with current Makefile help output
198
217
  @/bin/sh "${SCRIPTS_FOLDER}/update-readme-help.sh"
199
218
 
219
+ version-matrix: install-uv ## Emit the list of supported Python versions from pyproject.toml
220
+ @${UV_BIN} run .rhiza/utils/version_matrix.py
221
+
200
222
  # debugger tools
201
223
  custom-%: ## run a custom script (usage: make custom-scriptname)
202
224
  @SCRIPT="${CUSTOM_SCRIPTS_FOLDER}/$*.sh"; \
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rhiza
3
- Version: 0.8.4
3
+ Version: 0.8.6
4
4
  Summary: Reusable configuration templates for modern Python projects
5
5
  Project-URL: Homepage, https://github.com/jebel-quant/rhiza-cli
6
6
  Project-URL: Repository, https://github.com/jebel-quant/rhiza-cli
@@ -23,16 +23,8 @@ Requires-Dist: jinja2>=3.1.0
23
23
  Requires-Dist: loguru>=0.7.3
24
24
  Requires-Dist: pyyaml==6.0.3
25
25
  Requires-Dist: typer>=0.20.0
26
- Provides-Extra: dev
27
- Requires-Dist: marimo==0.18.4; extra == 'dev'
28
- Requires-Dist: pdoc>=16.0.0; extra == 'dev'
29
- Requires-Dist: pre-commit==4.5.1; extra == 'dev'
30
- Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
31
- Requires-Dist: pytest-html>=4.1.1; extra == 'dev'
32
- Requires-Dist: pytest==9.0.2; extra == 'dev'
33
- Requires-Dist: python-dotenv==1.2.1; extra == 'dev'
34
26
  Provides-Extra: tools
35
- Requires-Dist: rhiza-tools; extra == 'tools'
27
+ Requires-Dist: rhiza-tools>=0.1.2; extra == 'tools'
36
28
  Description-Content-Type: text/markdown
37
29
 
38
30
  <div align="center">
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "rhiza"
7
- version = "0.8.4"
7
+ version = "0.8.6"
8
8
  description = "Reusable configuration templates for modern Python projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -37,28 +37,21 @@ Homepage = "https://github.com/jebel-quant/rhiza-cli"
37
37
  Repository = "https://github.com/jebel-quant/rhiza-cli"
38
38
  Issues = "https://github.com/jebel-quant/rhiza/issues-cli"
39
39
 
40
-
41
40
  [project.optional-dependencies]
41
+ tools = ["rhiza-tools>=0.1.2"]
42
+
43
+ [dependency-groups]
42
44
  dev = [
43
- "pytest-cov>=7.0.0",
44
- "pytest-html>=4.1.1",
45
- "pytest==9.0.2",
46
- "pre-commit==4.5.1",
47
- "marimo==0.18.4",
48
- "pdoc>=16.0.0",
49
- "python-dotenv==1.2.1"
45
+ "numpy>=2.4",
46
+ "marimo>=0.18.0",
47
+ "pandas>=2.3",
48
+ "plotly>=6.5",
50
49
  ]
51
- tools = ["rhiza-tools"]
52
-
53
50
 
54
51
  [tool.hatch.build.targets.wheel]
55
52
  # Use src-layout: package code is under src/config
56
53
  packages = ["src/rhiza"]
57
54
 
58
- [tool.deptry]
59
- # see https://deptry.com/usage/#pep-621-dev-dependency-groups
60
- pep621_dev_dependency_groups = ["dev"]
61
-
62
55
  [tool.deptry.package_module_name_map]
63
56
  PyYAML = "yaml"
64
57
 
@@ -9,11 +9,11 @@ TESTS_FOLDER := tests
9
9
 
10
10
  ##@ Development and Testing
11
11
  test: install ## run all tests
12
- @if [ -d ${SOURCE_FOLDER} ] && [ -d ${TESTS_FOLDER} ]; then \
12
+ @if [ -d ${TESTS_FOLDER} ]; then \
13
13
  mkdir -p _tests/html-coverage _tests/html-report; \
14
14
  ${VENV}/bin/python -m pytest ${TESTS_FOLDER} --ignore=${TESTS_FOLDER}/benchmarks --cov=${SOURCE_FOLDER} --cov-report=term --cov-report=html:_tests/html-coverage --cov-report=json:_tests/coverage.json --html=_tests/html-report/report.html; \
15
15
  else \
16
- printf "${YELLOW}[WARN] Source folder ${SOURCE_FOLDER} or tests folder ${TESTS_FOLDER} not found, skipping tests${RESET}\n"; \
16
+ printf "${YELLOW}[WARN] Test folder ${TESTS_FOLDER} not found, skipping tests${RESET}\n"; \
17
17
  fi
18
18
 
19
19
  benchmark: install ## run performance benchmarks
@@ -34,12 +34,6 @@ def strip_ansi(text: str) -> str:
34
34
  return ansi_escape.sub("", text)
35
35
 
36
36
 
37
- @pytest.fixture
38
- def expected_uv_install_dir() -> str:
39
- """Get the expected UV_INSTALL_DIR from environment or default to ./bin."""
40
- return os.environ.get("UV_INSTALL_DIR", "./bin")
41
-
42
-
43
37
  @pytest.fixture(autouse=True)
44
38
  def setup_tmp_makefile(logger, root, tmp_path: Path):
45
39
  """Copy the Makefile and split Makefiles into a temp directory and chdir there.
@@ -136,23 +130,14 @@ class TestMakefile:
136
130
  assert "Targets:" in out
137
131
  assert "Bootstrap" in out or "Meta" in out # section headers
138
132
 
139
- def test_fmt_target_dry_run(self, logger, expected_uv_install_dir):
133
+ def test_fmt_target_dry_run(self, logger):
140
134
  """Fmt target should invoke pre-commit via uvx in dry-run output."""
141
135
  proc = run_make(logger, ["fmt"])
142
136
  out = proc.stdout
143
137
  # Check for uvx command with the configured path
144
- expected_uvx = f"{expected_uv_install_dir}/uvx"
145
- assert f"{expected_uvx} pre-commit run --all-files" in out
146
-
147
- def test_deptry_target_dry_run(self, logger, expected_uv_install_dir):
148
- """Deptry target should invoke deptry via uvx in dry-run output."""
149
- proc = run_make(logger, ["deptry"])
150
- out = proc.stdout
151
- # Check for uvx command with the configured path
152
- expected_uvx = f"{expected_uv_install_dir}/uvx"
153
- assert f"{expected_uvx} deptry ." in out
138
+ assert "uvx pre-commit run --all-files" in out
154
139
 
155
- def test_test_target_dry_run(self, logger, expected_uv_install_dir):
140
+ def test_test_target_dry_run(self, logger):
156
141
  """Test target should invoke pytest via uv with coverage and HTML outputs in dry-run output."""
157
142
  proc = run_make(logger, ["test"])
158
143
  out = proc.stdout
@@ -162,14 +147,13 @@ class TestMakefile:
162
147
  # expected_uv = f"{expected_uv_install_dir}/uv"
163
148
  # assert f"{expected_uv} run pytest" in out
164
149
 
165
- def test_book_target_dry_run(self, logger, expected_uv_install_dir):
150
+ def test_book_target_dry_run(self, logger):
166
151
  """Book target should run inline commands to assemble the book without go-task."""
167
152
  proc = run_make(logger, ["book"])
168
153
  out = proc.stdout
169
154
  # Expect marimushka export to install marimo and minibook to be invoked
170
155
  # Check for uvx command with the configured path
171
- expected_uvx = f"{expected_uv_install_dir}/uvx"
172
- assert f"{expected_uvx} minibook" in out
156
+ assert "uvx minibook" in out
173
157
 
174
158
  @pytest.mark.parametrize("target", ["book", "docs", "marimushka"])
175
159
  def test_book_related_targets_fallback_without_book_folder(self, logger, tmp_path, target):
@@ -193,29 +177,6 @@ class TestMakefile:
193
177
  out = strip_ansi(proc.stdout)
194
178
  assert "Value of UV_NO_MODIFY_PATH:\n1" in out
195
179
 
196
- def test_uv_install_dir_is_bin(self, logger, expected_uv_install_dir):
197
- """`UV_INSTALL_DIR` can be configured via environment variable or defaults to ./bin."""
198
- proc = run_make(logger, ["print-UV_INSTALL_DIR"], dry_run=False)
199
- out = strip_ansi(proc.stdout)
200
- # Check if UV_INSTALL_DIR is set in environment, otherwise expect default ./bin
201
- assert f"Value of UV_INSTALL_DIR:\n{expected_uv_install_dir}" in out
202
-
203
- def test_uv_bin_is_bin_uv(self, logger, expected_uv_install_dir):
204
- """`UV_BIN` is derived from UV_INSTALL_DIR environment variable or defaults to ./bin/uv."""
205
- proc = run_make(logger, ["print-UV_BIN"], dry_run=False)
206
- out = strip_ansi(proc.stdout)
207
- # Check if UV_INSTALL_DIR is set in environment, otherwise expect default ./bin
208
- expected_bin = f"{expected_uv_install_dir}/uv"
209
- assert f"Value of UV_BIN:\n{expected_bin}" in out
210
-
211
- def test_uvx_bin_is_bin_uvx(self, logger, expected_uv_install_dir):
212
- """`UVX_BIN` is derived from UV_INSTALL_DIR environment variable or defaults to ./bin/uvx."""
213
- proc = run_make(logger, ["print-UVX_BIN"], dry_run=False)
214
- out = strip_ansi(proc.stdout)
215
- # Check if UV_INSTALL_DIR is set in environment, otherwise expect default ./bin
216
- expected_bin = f"{expected_uv_install_dir}/uvx"
217
- assert f"Value of UVX_BIN:\n{expected_bin}" in out
218
-
219
180
  def test_script_folder_is_github_scripts(self, logger):
220
181
  """`SCRIPTS_FOLDER` should point to `.rhiza/scripts`."""
221
182
  proc = run_make(logger, ["print-SCRIPTS_FOLDER"], dry_run=False)
@@ -8,12 +8,11 @@ Tests call the script from a temporary clone and use a small mock `uv`
8
8
  to avoid external dependencies.
9
9
  """
10
10
 
11
- import shutil
12
11
  import subprocess
13
12
 
14
13
  # Get shell path once at module level
15
- SHELL = shutil.which("sh") or "/bin/sh"
16
- GIT = shutil.which("git") or "/usr/bin/git"
14
+ SHELL = "/bin/sh"
15
+ GIT = "/usr/bin/git"
17
16
 
18
17
 
19
18
  def test_release_creates_tag(git_repo):