rhiza 0.3.0__tar.gz → 0.5.0__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.
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/actions/setup-project/action.yml +2 -24
- rhiza-0.5.0/.github/copilot-instructions.md +349 -0
- rhiza-0.5.0/.github/scripts/sync.sh +310 -0
- rhiza-0.5.0/.github/template.yml +16 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/deptry.yml +1 -1
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/marimo.yml +1 -1
- rhiza-0.5.0/.github/workflows/structure.yml +73 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/sync.yml +1 -1
- rhiza-0.5.0/CLI.md +311 -0
- rhiza-0.5.0/PKG-INFO +773 -0
- rhiza-0.5.0/README.md +740 -0
- rhiza-0.5.0/USAGE.md +638 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/pyproject.toml +17 -5
- rhiza-0.5.0/src/rhiza/__init__.py +9 -0
- rhiza-0.5.0/src/rhiza/__main__.py +10 -0
- rhiza-0.5.0/src/rhiza/cli.py +115 -0
- rhiza-0.5.0/src/rhiza/commands/__init__.py +5 -0
- rhiza-0.5.0/src/rhiza/commands/init.py +66 -0
- rhiza-0.5.0/src/rhiza/commands/materialize.py +140 -0
- rhiza-0.5.0/src/rhiza/commands/validate.py +140 -0
- rhiza-0.5.0/src/rhiza/models.py +103 -0
- rhiza-0.5.0/tests/test_cli_commands.py +112 -0
- rhiza-0.5.0/tests/test_commands/test_init.py +105 -0
- rhiza-0.5.0/tests/test_commands/test_materialize.py +342 -0
- rhiza-0.5.0/tests/test_commands/test_validate.py +356 -0
- rhiza-0.5.0/tests/test_models.py +183 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/uv.lock +92 -4
- rhiza-0.3.0/.devcontainer/bootstrap.sh +0 -38
- rhiza-0.3.0/.devcontainer/devcontainer.json +0 -48
- rhiza-0.3.0/.github/workflows/devcontainer.yml +0 -137
- rhiza-0.3.0/.github/workflows/docker.yml +0 -63
- rhiza-0.3.0/PKG-INFO +0 -709
- rhiza-0.3.0/README.md +0 -679
- rhiza-0.3.0/assets/rhiza-logo.svg +0 -81
- rhiza-0.3.0/book/marimo/df.py +0 -37
- rhiza-0.3.0/book/minibook-templates/custom.html.jinja2 +0 -210
- rhiza-0.3.0/book/pdoc-templates/module.html.jinja2 +0 -19
- rhiza-0.3.0/docker/Dockerfile +0 -64
- rhiza-0.3.0/docker/Dockerfile.dockerignore +0 -17
- rhiza-0.3.0/docker/README.md +0 -31
- {rhiza-0.3.0 → rhiza-0.5.0}/.editorconfig +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/renovate.json +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/scripts/book.sh +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/scripts/bump.sh +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/scripts/customisations/build-extras.sh +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/scripts/customisations/post-release.sh +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/scripts/marimushka.sh +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/scripts/release.sh +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/scripts/update-readme-help.sh +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/book.yml +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/ci.yml +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/pre-commit.yml +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/release.yml +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/scripts/version_matrix.py +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/scripts/version_max.py +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.gitignore +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/.pre-commit-config.yaml +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/CODE_OF_CONDUCT.md +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/CONTRIBUTING.md +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/LICENSE +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/Makefile +0 -0
- {rhiza-0.3.0/src/rhiza → rhiza-0.5.0/book/marimo}/.gitkeep +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/pytest.ini +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/ruff.toml +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/README.md +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/conftest.py +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_bump_script.py +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_docstrings.py +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_git_repo_fixture.py +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_makefile.py +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_marimushka_script.py +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_readme.py +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_release_script.py +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_structure.py +0 -0
- {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_updatereadme_script.py +0 -0
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
# Action: Setup Project (composite action)
|
|
5
5
|
#
|
|
6
6
|
# Purpose: Bootstrap a Python project in GitHub Actions by:
|
|
7
|
-
# - Installing Task, uv, and uvx
|
|
8
|
-
# - Optionally rendering the repository from a Copier template for tests
|
|
7
|
+
# - Installing Task, uv, and uvx
|
|
9
8
|
# - Detecting presence of pyproject.toml and exposing it as an output
|
|
10
9
|
# - Creating a virtual environment with uv and syncing dependencies
|
|
11
10
|
#
|
|
@@ -43,28 +42,7 @@ runs:
|
|
|
43
42
|
- name: Install uv
|
|
44
43
|
uses: astral-sh/setup-uv@v7
|
|
45
44
|
with:
|
|
46
|
-
version: "0.9.
|
|
47
|
-
|
|
48
|
-
- name: Render the project
|
|
49
|
-
if: hashFiles('tests/resources/render.yml') != ''
|
|
50
|
-
shell: bash
|
|
51
|
-
run: |
|
|
52
|
-
pip install copier
|
|
53
|
-
|
|
54
|
-
# Render the current folder in-place
|
|
55
|
-
copier copy . . \
|
|
56
|
-
--data-file ./tests/resources/render.yml \
|
|
57
|
-
--force \
|
|
58
|
-
--overwrite \
|
|
59
|
-
--quiet
|
|
60
|
-
|
|
61
|
-
# Delete all remaining .jinja files
|
|
62
|
-
find . -type f -name "*.jinja" -exec rm -f {} +
|
|
63
|
-
|
|
64
|
-
# Delete all folders that still contain {{ ... }}
|
|
65
|
-
find . -depth -type d -name "*{{*}}*" -exec rm -rf {} +
|
|
66
|
-
|
|
67
|
-
ls -R
|
|
45
|
+
version: "0.9.18"
|
|
68
46
|
|
|
69
47
|
- name: Check for pyproject.toml
|
|
70
48
|
id: check_pyproject
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# GitHub Copilot Instructions for rhiza-cli
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
Rhiza is a command-line interface (CLI) tool for managing reusable configuration templates for modern Python projects. It provides commands for initializing, validating, and materializing configuration templates across projects.
|
|
6
|
+
|
|
7
|
+
**Repository:** <https://github.com/jebel-quant/rhiza-cli>
|
|
8
|
+
|
|
9
|
+
## Technology Stack
|
|
10
|
+
|
|
11
|
+
- **Language:** Python 3.11+ (supports 3.11, 3.12, 3.13, 3.14)
|
|
12
|
+
- **Package Manager:** uv (fast Python package installer and resolver)
|
|
13
|
+
- **CLI Framework:** Typer
|
|
14
|
+
- **Testing:** pytest with coverage reporting
|
|
15
|
+
- **Linting/Formatting:** Ruff
|
|
16
|
+
- **Build System:** Hatchling
|
|
17
|
+
- **Pre-commit Hooks:** YAML/TOML validation, Ruff, markdownlint, actionlint
|
|
18
|
+
|
|
19
|
+
## Project Structure
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
rhiza-cli/
|
|
23
|
+
├── src/rhiza/ # Main source code
|
|
24
|
+
│ ├── cli.py # CLI entry points (Typer app)
|
|
25
|
+
│ └── commands/ # Command implementations
|
|
26
|
+
├── tests/ # Test suite
|
|
27
|
+
├── book/ # Documentation and Marimo notebooks
|
|
28
|
+
├── .github/ # GitHub workflows and scripts
|
|
29
|
+
├── pyproject.toml # Project configuration
|
|
30
|
+
├── ruff.toml # Linting configuration
|
|
31
|
+
└── Makefile # Development tasks
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Coding Standards
|
|
35
|
+
|
|
36
|
+
### Python Style
|
|
37
|
+
|
|
38
|
+
- **Line length:** Maximum 120 characters
|
|
39
|
+
- **Quotes:** Use double quotes for strings
|
|
40
|
+
- **Indentation:** 4 spaces (no tabs)
|
|
41
|
+
- **Docstrings:** Google style convention (required for all public modules, classes, and functions)
|
|
42
|
+
- **Type hints:** Not strictly enforced but encouraged
|
|
43
|
+
- **Import sorting:** Automatic via isort (part of Ruff)
|
|
44
|
+
|
|
45
|
+
### Linting Rules
|
|
46
|
+
|
|
47
|
+
The project uses Ruff with the following rule sets:
|
|
48
|
+
|
|
49
|
+
- **D** (pydocstyle): Docstring style enforcement
|
|
50
|
+
- **E** (pycodestyle): PEP 8 style guide errors
|
|
51
|
+
- **F** (pyflakes): Logical error detection
|
|
52
|
+
- **I** (isort): Import sorting
|
|
53
|
+
- **N** (pep8-naming): PEP 8 naming conventions
|
|
54
|
+
- **W** (pycodestyle): PEP 8 warnings
|
|
55
|
+
- **UP** (pyupgrade): Modern Python syntax
|
|
56
|
+
|
|
57
|
+
**Exception:** Tests allow assert statements (S101 ignored in tests/)
|
|
58
|
+
|
|
59
|
+
### Docstring Requirements
|
|
60
|
+
|
|
61
|
+
- All public modules, classes, functions, and methods must have docstrings
|
|
62
|
+
- Use Google docstring convention
|
|
63
|
+
- Include magic methods like `__init__` (D105, D107 enforced)
|
|
64
|
+
- Use multi-line format with summary line, then blank line, then details
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
def my_function(arg1: str, arg2: int) -> bool:
|
|
70
|
+
"""Short summary of what the function does.
|
|
71
|
+
|
|
72
|
+
Longer description if needed. Explain complex behavior,
|
|
73
|
+
side effects, or important context.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
arg1: Description of arg1
|
|
77
|
+
arg2: Description of arg2
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Description of return value (bool)
|
|
81
|
+
"""
|
|
82
|
+
return True
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Development Workflow
|
|
86
|
+
|
|
87
|
+
### Setup
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
make install # Install dependencies with uv
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Common Commands
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
make fmt # Run linters and formatters (pre-commit)
|
|
97
|
+
make test # Run tests with coverage
|
|
98
|
+
make docs # Generate documentation with pdoc
|
|
99
|
+
make clean # Clean build artifacts
|
|
100
|
+
make help # Show all available commands
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Testing
|
|
104
|
+
|
|
105
|
+
- Use pytest for all tests
|
|
106
|
+
- Place tests in `tests/` directory
|
|
107
|
+
- Test files should match pattern `test_*.py`
|
|
108
|
+
- Aim for good coverage of new code
|
|
109
|
+
- Run tests with `make test` before submitting changes
|
|
110
|
+
|
|
111
|
+
### Pre-commit Hooks
|
|
112
|
+
|
|
113
|
+
The project uses pre-commit hooks that run automatically on commit:
|
|
114
|
+
|
|
115
|
+
- YAML/TOML validation
|
|
116
|
+
- Ruff linting and formatting
|
|
117
|
+
- Markdown linting (MD013 disabled for long lines)
|
|
118
|
+
- GitHub workflow validation
|
|
119
|
+
- Renovate config validation
|
|
120
|
+
- README.md auto-update with Makefile help
|
|
121
|
+
|
|
122
|
+
## Architecture Notes
|
|
123
|
+
|
|
124
|
+
### CLI Structure
|
|
125
|
+
|
|
126
|
+
The CLI uses Typer for command definitions. Commands are thin wrappers in `cli.py` that delegate to implementations in `rhiza.commands.*`:
|
|
127
|
+
|
|
128
|
+
- `init`: Initialize or validate `.github/template.yml`
|
|
129
|
+
- `materialize` (alias `inject`): Apply templates to a target repository
|
|
130
|
+
- `validate`: Validate template configuration
|
|
131
|
+
|
|
132
|
+
### Command Implementation Pattern
|
|
133
|
+
|
|
134
|
+
1. Command defined in `src/rhiza/cli.py` using Typer decorators
|
|
135
|
+
2. Implementation logic in `src/rhiza/commands/*.py`
|
|
136
|
+
3. Commands use `loguru` for logging
|
|
137
|
+
4. Use `Path` from `pathlib` for file operations
|
|
138
|
+
|
|
139
|
+
## Best Practices
|
|
140
|
+
|
|
141
|
+
1. **Minimal changes:** Make surgical, focused changes
|
|
142
|
+
2. **Type hints:** Use when they improve clarity
|
|
143
|
+
3. **Error handling:** Use appropriate exceptions, log errors clearly
|
|
144
|
+
4. **Documentation:** Update docstrings when changing function signatures
|
|
145
|
+
5. **Tests:** Add tests for new functionality
|
|
146
|
+
6. **Imports:** Keep imports organized (isort handles this automatically)
|
|
147
|
+
7. **File headers:** Include repository attribution comment at top of new files:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
# This file is part of the jebel-quant/rhiza repository
|
|
151
|
+
# (https://github.com/jebel-quant/rhiza).
|
|
152
|
+
#
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Dependencies
|
|
156
|
+
|
|
157
|
+
### Core Dependencies
|
|
158
|
+
|
|
159
|
+
See `pyproject.toml` for exact versions. Key dependencies:
|
|
160
|
+
|
|
161
|
+
- `typer` - CLI framework
|
|
162
|
+
- `loguru` - Logging
|
|
163
|
+
- `PyYAML` - YAML parsing
|
|
164
|
+
|
|
165
|
+
### Development Dependencies
|
|
166
|
+
|
|
167
|
+
See `pyproject.toml` for complete list. Key dev dependencies:
|
|
168
|
+
|
|
169
|
+
- `pytest`, `pytest-cov`, `pytest-html` - Testing
|
|
170
|
+
- `pre-commit` - Git hooks
|
|
171
|
+
- `marimo` - Notebook support
|
|
172
|
+
- `pdoc` - Documentation generation
|
|
173
|
+
|
|
174
|
+
## Common Patterns
|
|
175
|
+
|
|
176
|
+
### Path Handling
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from pathlib import Path
|
|
180
|
+
|
|
181
|
+
target = Path(".") # Use Path objects, not strings
|
|
182
|
+
if target.exists():
|
|
183
|
+
# Do something
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Logging
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from loguru import logger
|
|
190
|
+
|
|
191
|
+
logger.info("Starting operation")
|
|
192
|
+
logger.error("Something went wrong")
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### CLI Arguments
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
import typer
|
|
199
|
+
|
|
200
|
+
@app.command()
|
|
201
|
+
def my_command(
|
|
202
|
+
target: Path = typer.Argument(
|
|
203
|
+
default=Path("."),
|
|
204
|
+
exists=True,
|
|
205
|
+
help="Description"
|
|
206
|
+
),
|
|
207
|
+
):
|
|
208
|
+
"""Command docstring."""
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Security Considerations
|
|
212
|
+
|
|
213
|
+
- **No secrets in code:** Never commit API keys, passwords, or sensitive data
|
|
214
|
+
- **Path traversal:** Always use `Path.resolve()` to normalize paths and prevent directory traversal attacks
|
|
215
|
+
- **Input validation:** Validate all user inputs, especially file paths and command arguments
|
|
216
|
+
- **YAML parsing:** Use safe YAML loading (PyYAML uses safe loading by default)
|
|
217
|
+
- **File permissions:** Be mindful of file permissions when creating files
|
|
218
|
+
|
|
219
|
+
## Error Handling Patterns
|
|
220
|
+
|
|
221
|
+
### Exception Handling
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from loguru import logger
|
|
225
|
+
from pathlib import Path
|
|
226
|
+
|
|
227
|
+
def safe_operation(path: Path):
|
|
228
|
+
"""Safe operation with proper error handling."""
|
|
229
|
+
try:
|
|
230
|
+
# Normalize path to prevent traversal
|
|
231
|
+
path = path.resolve()
|
|
232
|
+
|
|
233
|
+
if not path.exists():
|
|
234
|
+
logger.error(f"Path does not exist: {path}")
|
|
235
|
+
raise FileNotFoundError(f"Path not found: {path}")
|
|
236
|
+
|
|
237
|
+
# Perform operation
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
except PermissionError as e:
|
|
241
|
+
logger.error(f"Permission denied: {e}")
|
|
242
|
+
raise
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.error(f"Unexpected error: {e}")
|
|
245
|
+
raise
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### CLI Exit Codes
|
|
249
|
+
|
|
250
|
+
Use Typer's `Exit` for non-zero exit codes on errors:
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
import typer
|
|
254
|
+
|
|
255
|
+
if not success:
|
|
256
|
+
raise typer.Exit(code=1)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Common Tasks
|
|
260
|
+
|
|
261
|
+
### Adding a New Command
|
|
262
|
+
|
|
263
|
+
1. Create a new file in `src/rhiza/commands/` (e.g., `newcommand.py`)
|
|
264
|
+
2. Implement the command logic with proper docstrings
|
|
265
|
+
3. Add a wrapper in `src/rhiza/cli.py` using Typer decorators
|
|
266
|
+
4. Add tests in `tests/` for the new command
|
|
267
|
+
5. Update documentation if needed
|
|
268
|
+
|
|
269
|
+
Example:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
# In src/rhiza/commands/newcommand.py
|
|
273
|
+
from pathlib import Path
|
|
274
|
+
from loguru import logger
|
|
275
|
+
|
|
276
|
+
def my_new_command(target: Path):
|
|
277
|
+
"""Execute the new command.
|
|
278
|
+
|
|
279
|
+
Parameters
|
|
280
|
+
----------
|
|
281
|
+
target:
|
|
282
|
+
Path to the target directory.
|
|
283
|
+
"""
|
|
284
|
+
target = target.resolve()
|
|
285
|
+
logger.info(f"Running new command on: {target}")
|
|
286
|
+
# Implementation here
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
# In src/rhiza/cli.py
|
|
291
|
+
from rhiza.commands.newcommand import my_new_command
|
|
292
|
+
|
|
293
|
+
@app.command()
|
|
294
|
+
def newcommand(
|
|
295
|
+
target: Path = typer.Argument(
|
|
296
|
+
default=Path("."),
|
|
297
|
+
exists=True,
|
|
298
|
+
file_okay=False,
|
|
299
|
+
dir_okay=True,
|
|
300
|
+
help="Target directory"
|
|
301
|
+
),
|
|
302
|
+
):
|
|
303
|
+
"""Short description of the command."""
|
|
304
|
+
my_new_command(target)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Running the CLI in Development
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
# Install in editable mode
|
|
311
|
+
make install
|
|
312
|
+
|
|
313
|
+
# Run the CLI
|
|
314
|
+
uv run rhiza --help
|
|
315
|
+
uv run rhiza init
|
|
316
|
+
uv run rhiza materialize --branch main
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Troubleshooting
|
|
320
|
+
|
|
321
|
+
### Common Issues
|
|
322
|
+
|
|
323
|
+
**Import errors after adding dependencies:**
|
|
324
|
+
- Run `make install` to sync dependencies
|
|
325
|
+
- Ensure `pyproject.toml` is updated with new dependencies
|
|
326
|
+
|
|
327
|
+
**Linting failures:**
|
|
328
|
+
- Run `make fmt` to auto-fix most issues
|
|
329
|
+
- Check `ruff.toml` for configured rules
|
|
330
|
+
- Ensure docstrings follow Google convention
|
|
331
|
+
|
|
332
|
+
**Test failures:**
|
|
333
|
+
- Run `make test` to see detailed output
|
|
334
|
+
- Check test coverage report in `_tests/html-coverage/`
|
|
335
|
+
- Ensure new code has corresponding tests
|
|
336
|
+
|
|
337
|
+
**Pre-commit hook failures:**
|
|
338
|
+
- Run `make fmt` to fix formatting issues
|
|
339
|
+
- Check `.pre-commit-config.yaml` for hook configuration
|
|
340
|
+
- Install hooks with `uv run pre-commit install`
|
|
341
|
+
|
|
342
|
+
## When Making Changes
|
|
343
|
+
|
|
344
|
+
1. Run `make fmt` to ensure code follows style guidelines
|
|
345
|
+
2. Run `make test` to verify tests pass
|
|
346
|
+
3. Update docstrings if changing public APIs
|
|
347
|
+
4. Add tests for new functionality
|
|
348
|
+
5. Keep changes focused and minimal
|
|
349
|
+
6. Follow existing code patterns and conventions
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Sync configuration files from template repository
|
|
3
|
+
# - Reads configuration from .github/template.yml
|
|
4
|
+
# - Downloads specified files from the template repository
|
|
5
|
+
# - Copies them to the current repository
|
|
6
|
+
#
|
|
7
|
+
# This script is POSIX-sh compatible and provides manual sync capability
|
|
8
|
+
# for repositories that use jebel-quant/rhiza as a template.
|
|
9
|
+
|
|
10
|
+
set -e
|
|
11
|
+
|
|
12
|
+
BLUE="\033[36m"
|
|
13
|
+
RED="\033[31m"
|
|
14
|
+
GREEN="\033[32m"
|
|
15
|
+
YELLOW="\033[33m"
|
|
16
|
+
RESET="\033[0m"
|
|
17
|
+
|
|
18
|
+
TEMPLATE_CONFIG=".github/template.yml"
|
|
19
|
+
TEMP_DIR="/tmp/rhiza-sync-$$"
|
|
20
|
+
|
|
21
|
+
show_usage() {
|
|
22
|
+
printf "Usage: %s [OPTIONS]\n\n" "$0"
|
|
23
|
+
printf "Sync configuration files from the template repository.\n\n"
|
|
24
|
+
printf "Options:\n"
|
|
25
|
+
printf " -h, --help Show this help message\n"
|
|
26
|
+
printf " --dry-run Show what would be synced without making changes\n\n"
|
|
27
|
+
printf "Configuration:\n"
|
|
28
|
+
printf " Reads from %s to determine:\n" "$TEMPLATE_CONFIG"
|
|
29
|
+
printf " - template-repository: Source repository (e.g., 'jebel-quant/rhiza')\n"
|
|
30
|
+
printf " - template-branch: Branch to sync from (e.g., 'main')\n"
|
|
31
|
+
printf " - include: Files/directories to sync\n"
|
|
32
|
+
printf " - exclude: Files/directories to skip (optional)\n\n"
|
|
33
|
+
printf "Example %s:\n" "$TEMPLATE_CONFIG"
|
|
34
|
+
printf " template-repository: \"jebel-quant/rhiza\"\n"
|
|
35
|
+
printf " template-branch: \"main\"\n"
|
|
36
|
+
printf " include: |\n"
|
|
37
|
+
printf " .github\n"
|
|
38
|
+
printf " Makefile\n"
|
|
39
|
+
printf " ruff.toml\n"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
DRY_RUN=""
|
|
43
|
+
while [ $# -gt 0 ]; do
|
|
44
|
+
case "$1" in
|
|
45
|
+
-h|--help)
|
|
46
|
+
show_usage
|
|
47
|
+
exit 0
|
|
48
|
+
;;
|
|
49
|
+
--dry-run)
|
|
50
|
+
DRY_RUN="true"
|
|
51
|
+
shift
|
|
52
|
+
;;
|
|
53
|
+
*)
|
|
54
|
+
printf "%b[ERROR] Unknown option: %s%b\n" "$RED" "$1" "$RESET"
|
|
55
|
+
show_usage
|
|
56
|
+
exit 1
|
|
57
|
+
;;
|
|
58
|
+
esac
|
|
59
|
+
done
|
|
60
|
+
|
|
61
|
+
# Check if template.yml exists
|
|
62
|
+
if [ ! -f "$TEMPLATE_CONFIG" ]; then
|
|
63
|
+
printf "%b[ERROR] Template configuration not found: %s%b\n" "$RED" "$TEMPLATE_CONFIG" "$RESET"
|
|
64
|
+
printf "\nThis repository is not configured for template syncing.\n"
|
|
65
|
+
printf "Create %s with the following content:\n\n" "$TEMPLATE_CONFIG"
|
|
66
|
+
printf " template-repository: \"jebel-quant/rhiza\"\n"
|
|
67
|
+
printf " template-branch: \"main\"\n"
|
|
68
|
+
printf " include: |\n"
|
|
69
|
+
printf " .github\n"
|
|
70
|
+
printf " Makefile\n"
|
|
71
|
+
printf " ruff.toml\n"
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Parse template.yml to extract configuration
|
|
76
|
+
# This is a simple parser that works with basic YAML structure
|
|
77
|
+
printf "%b[INFO] Reading configuration from %s...%b\n" "$BLUE" "$TEMPLATE_CONFIG" "$RESET"
|
|
78
|
+
|
|
79
|
+
TEMPLATE_REPO=""
|
|
80
|
+
TEMPLATE_BRANCH=""
|
|
81
|
+
INCLUDE_LIST=""
|
|
82
|
+
EXCLUDE_LIST=""
|
|
83
|
+
|
|
84
|
+
# Simple YAML parser
|
|
85
|
+
in_multiline=""
|
|
86
|
+
|
|
87
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
88
|
+
# Skip comments and empty lines
|
|
89
|
+
case "$line" in
|
|
90
|
+
\#*|"") continue ;;
|
|
91
|
+
esac
|
|
92
|
+
|
|
93
|
+
# Check if line starts with spaces by looking at first character
|
|
94
|
+
first_char=$(printf '%s' "$line" | cut -c1)
|
|
95
|
+
|
|
96
|
+
if [ "$first_char" = " " ] || [ "$first_char" = "$(printf '\t')" ]; then
|
|
97
|
+
# This is indented content (part of a multiline block)
|
|
98
|
+
if [ -n "$in_multiline" ]; then
|
|
99
|
+
# Remove leading spaces
|
|
100
|
+
item=$(echo "$line" | sed 's/^[[:space:]]*//')
|
|
101
|
+
if [ -n "$item" ]; then
|
|
102
|
+
if [ "$in_multiline" = "include" ]; then
|
|
103
|
+
INCLUDE_LIST="${INCLUDE_LIST}${item}
|
|
104
|
+
"
|
|
105
|
+
elif [ "$in_multiline" = "exclude" ]; then
|
|
106
|
+
EXCLUDE_LIST="${EXCLUDE_LIST}${item}
|
|
107
|
+
"
|
|
108
|
+
fi
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
111
|
+
else
|
|
112
|
+
# This is a key line (not indented)
|
|
113
|
+
case "$line" in
|
|
114
|
+
template-repository:*)
|
|
115
|
+
TEMPLATE_REPO=$(echo "$line" | sed 's/^template-repository:[[:space:]]*//' | sed 's/"//g' | sed "s/'//g")
|
|
116
|
+
in_multiline=""
|
|
117
|
+
;;
|
|
118
|
+
template-branch:*)
|
|
119
|
+
TEMPLATE_BRANCH=$(echo "$line" | sed 's/^template-branch:[[:space:]]*//' | sed 's/"//g' | sed "s/'//g")
|
|
120
|
+
in_multiline=""
|
|
121
|
+
;;
|
|
122
|
+
include:*)
|
|
123
|
+
in_multiline="include"
|
|
124
|
+
;;
|
|
125
|
+
exclude:*)
|
|
126
|
+
in_multiline="exclude"
|
|
127
|
+
;;
|
|
128
|
+
esac
|
|
129
|
+
fi
|
|
130
|
+
done < "$TEMPLATE_CONFIG"
|
|
131
|
+
|
|
132
|
+
# Validate required fields
|
|
133
|
+
if [ -z "$TEMPLATE_REPO" ]; then
|
|
134
|
+
printf "%b[ERROR] template-repository not found in %s%b\n" "$RED" "$TEMPLATE_CONFIG" "$RESET"
|
|
135
|
+
exit 1
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
if [ -z "$TEMPLATE_BRANCH" ]; then
|
|
139
|
+
printf "%b[WARN] template-branch not specified, using 'main'%b\n" "$YELLOW" "$RESET"
|
|
140
|
+
TEMPLATE_BRANCH="main"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
if [ -z "$INCLUDE_LIST" ]; then
|
|
144
|
+
printf "%b[ERROR] include list is empty in %s%b\n" "$RED" "$TEMPLATE_CONFIG" "$RESET"
|
|
145
|
+
exit 1
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
printf "%b[INFO] Template repository: %s%b\n" "$GREEN" "$TEMPLATE_REPO" "$RESET"
|
|
149
|
+
printf "%b[INFO] Template branch: %s%b\n" "$GREEN" "$TEMPLATE_BRANCH" "$RESET"
|
|
150
|
+
printf "%b[INFO] Files to sync:%b\n" "$GREEN" "$RESET"
|
|
151
|
+
echo "$INCLUDE_LIST" | while IFS= read -r item; do
|
|
152
|
+
[ -n "$item" ] && printf " - %s\n" "$item"
|
|
153
|
+
done || true
|
|
154
|
+
|
|
155
|
+
if [ -n "$EXCLUDE_LIST" ]; then
|
|
156
|
+
printf "%b[INFO] Files to exclude:%b\n" "$YELLOW" "$RESET"
|
|
157
|
+
echo "$EXCLUDE_LIST" | while IFS= read -r item; do
|
|
158
|
+
[ -n "$item" ] && printf " - %s\n" "$item"
|
|
159
|
+
done || true
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
if [ -n "$DRY_RUN" ]; then
|
|
163
|
+
printf "\n%b[DRY RUN] No changes will be made%b\n" "$YELLOW" "$RESET"
|
|
164
|
+
exit 0
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# Create temporary directory
|
|
168
|
+
mkdir -p "$TEMP_DIR"
|
|
169
|
+
trap 'rm -rf "$TEMP_DIR"' EXIT INT TERM
|
|
170
|
+
|
|
171
|
+
# Backup this script to avoid being overwritten during sync
|
|
172
|
+
SELF_SCRIPT=".github/scripts/sync.sh"
|
|
173
|
+
if [ -f "$SELF_SCRIPT" ]; then
|
|
174
|
+
cp "$SELF_SCRIPT" "$TEMP_DIR/sync.sh.bak"
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
# Clone the template repository
|
|
178
|
+
printf "\n%b[INFO] Cloning template repository...%b\n" "$BLUE" "$RESET"
|
|
179
|
+
REPO_URL="https://github.com/${TEMPLATE_REPO}.git"
|
|
180
|
+
|
|
181
|
+
if ! git clone --depth 1 --branch "$TEMPLATE_BRANCH" "$REPO_URL" "$TEMP_DIR/template" 2>/dev/null; then
|
|
182
|
+
printf "%b[ERROR] Failed to clone template repository from %s%b\n" "$RED" "$REPO_URL" "$RESET"
|
|
183
|
+
exit 1
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
# Function to check if a file path should be excluded
|
|
187
|
+
is_file_excluded() {
|
|
188
|
+
file_path="$1"
|
|
189
|
+
if [ -z "$EXCLUDE_LIST" ]; then
|
|
190
|
+
return 1 # Not excluded (false)
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
while IFS= read -r exclude_item || [ -n "$exclude_item" ]; do
|
|
194
|
+
[ -z "$exclude_item" ] && continue
|
|
195
|
+
if [ "$file_path" = "$exclude_item" ]; then
|
|
196
|
+
return 0 # Is excluded (true)
|
|
197
|
+
fi
|
|
198
|
+
done <<EOF_EXCLUDE_CHECK
|
|
199
|
+
$EXCLUDE_LIST
|
|
200
|
+
EOF_EXCLUDE_CHECK
|
|
201
|
+
|
|
202
|
+
return 1 # Not excluded (false)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Copy files from template to current directory
|
|
206
|
+
printf "%b[INFO] Syncing files...%b\n" "$BLUE" "$RESET"
|
|
207
|
+
|
|
208
|
+
synced_count=0
|
|
209
|
+
skipped_count=0
|
|
210
|
+
# Track whether .github (containing this script) was synced and whether a direct self update was requested
|
|
211
|
+
synced_dotgithub="false"
|
|
212
|
+
deferred_self_update="false"
|
|
213
|
+
|
|
214
|
+
# Use here-document instead of pipeline to avoid subshell
|
|
215
|
+
while IFS= read -r item || [ -n "$item" ]; do
|
|
216
|
+
[ -z "$item" ] && continue
|
|
217
|
+
|
|
218
|
+
# Check if this item is in the exclude list
|
|
219
|
+
if is_file_excluded "$item"; then
|
|
220
|
+
printf " %b[SKIP]%b %s (excluded)\n" "$YELLOW" "$RESET" "$item"
|
|
221
|
+
skipped_count=$((skipped_count + 1))
|
|
222
|
+
continue
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
# Defer updating this script to the very end to avoid mid-run overwrite
|
|
226
|
+
if [ "$item" = ".github/scripts/sync.sh" ]; then
|
|
227
|
+
deferred_self_update="true"
|
|
228
|
+
printf " %b[DEFER]%b %s (will update at end)\n" "$YELLOW" "$RESET" "$item"
|
|
229
|
+
continue
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
src_path="$TEMP_DIR/template/$item"
|
|
233
|
+
dest_path="./$item"
|
|
234
|
+
|
|
235
|
+
if [ -e "$src_path" ]; then
|
|
236
|
+
# Create parent directory if needed
|
|
237
|
+
dest_dir=$(dirname "$dest_path")
|
|
238
|
+
mkdir -p "$dest_dir"
|
|
239
|
+
|
|
240
|
+
# Copy the file or directory
|
|
241
|
+
if [ -d "$src_path" ]; then
|
|
242
|
+
# Ensure destination directory exists
|
|
243
|
+
mkdir -p "$dest_path"
|
|
244
|
+
# Copy contents of the source directory into the destination directory
|
|
245
|
+
# to avoid nesting (e.g., .github/.github or tests/tests)
|
|
246
|
+
cp -R "$src_path"/. "$dest_path"/
|
|
247
|
+
# Mark if we synced the .github directory so we can safely update sync.sh at the end
|
|
248
|
+
if [ "$item" = ".github" ]; then
|
|
249
|
+
synced_dotgithub="true"
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
# Remove excluded files from the copied directory
|
|
253
|
+
if [ -n "$EXCLUDE_LIST" ]; then
|
|
254
|
+
while IFS= read -r exclude_item || [ -n "$exclude_item" ]; do
|
|
255
|
+
[ -z "$exclude_item" ] && continue
|
|
256
|
+
# Check if the excluded item is a child of the current item
|
|
257
|
+
# e.g., if item=".github" and exclude_item=".github/workflows/docker.yml"
|
|
258
|
+
case "$exclude_item" in
|
|
259
|
+
"$item"/*)
|
|
260
|
+
# This is a nested file that should be excluded
|
|
261
|
+
excluded_file_path="./$exclude_item"
|
|
262
|
+
if [ -e "$excluded_file_path" ]; then
|
|
263
|
+
rm -rf "$excluded_file_path"
|
|
264
|
+
printf " %b[EXCLUDE]%b %s (removed from synced directory)\n" "$YELLOW" "$RESET" "$exclude_item"
|
|
265
|
+
fi
|
|
266
|
+
;;
|
|
267
|
+
esac
|
|
268
|
+
done <<EOF_EXCLUDE_NESTED
|
|
269
|
+
$EXCLUDE_LIST
|
|
270
|
+
EOF_EXCLUDE_NESTED
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
# If we just synced the .github directory, restore this script immediately to avoid mid-run overwrite issues
|
|
274
|
+
if [ "$item" = ".github" ] && [ -f "$TEMP_DIR/sync.sh.bak" ]; then
|
|
275
|
+
cp "$TEMP_DIR/sync.sh.bak" "$SELF_SCRIPT"
|
|
276
|
+
fi
|
|
277
|
+
printf " %b[SYNC]%b %s (directory contents)\n" "$GREEN" "$RESET" "$item"
|
|
278
|
+
else
|
|
279
|
+
cp "$src_path" "$dest_path"
|
|
280
|
+
printf " %b[SYNC]%b %s\n" "$GREEN" "$RESET" "$item"
|
|
281
|
+
fi
|
|
282
|
+
synced_count=$((synced_count + 1))
|
|
283
|
+
else
|
|
284
|
+
printf " %b[WARN]%b %s (not found in template)\n" "$YELLOW" "$RESET" "$item"
|
|
285
|
+
skipped_count=$((skipped_count + 1))
|
|
286
|
+
fi
|
|
287
|
+
done <<EOF_INCLUDE
|
|
288
|
+
$INCLUDE_LIST
|
|
289
|
+
EOF_INCLUDE
|
|
290
|
+
|
|
291
|
+
# Finalize self-update of sync.sh if applicable
|
|
292
|
+
TEMPLATE_SELF_SH="$TEMP_DIR/template/.github/scripts/sync.sh"
|
|
293
|
+
if [ -f "$TEMPLATE_SELF_SH" ] && { [ "$deferred_self_update" = "true" ] || [ "$synced_dotgithub" = "true" ]; }; then
|
|
294
|
+
if is_file_excluded ".github/scripts/sync.sh"; then
|
|
295
|
+
printf " %b[SKIP]%b .github/scripts/sync.sh (excluded from final update)\n" "$YELLOW" "$RESET"
|
|
296
|
+
else
|
|
297
|
+
cp "$TEMPLATE_SELF_SH" "$SELF_SCRIPT"
|
|
298
|
+
chmod +x "$SELF_SCRIPT" 2>/dev/null || true
|
|
299
|
+
printf " %b[SYNC]%b .github/scripts/sync.sh (finalized)\n" "$GREEN" "$RESET"
|
|
300
|
+
fi
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
printf "\n%b[INFO] Sync complete!%b\n" "$GREEN" "$RESET"
|
|
304
|
+
printf " Synced: %d files/directories\n" "$synced_count"
|
|
305
|
+
if [ "$skipped_count" -gt 0 ]; then
|
|
306
|
+
printf " Skipped: %d files/directories\n" "$skipped_count"
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
printf "\n%b[INFO] Review the changes with: git status%b\n" "$BLUE" "$RESET"
|
|
310
|
+
printf "%b[INFO] Commit the changes with: git add . && git commit -m 'chore: sync template files'%b\n" "$BLUE" "$RESET"
|