BoilerWorks
Initial commit — Boilerworks CLI v0.1.0
Commit
0cb4a5e6ba26b36ed54ace7bf4f3cc00aa4e141090a85272a24c52c8cb850248
32 files changed
+35
+8
+23
+13
+23
+40
+2
+26
+82
+26
+160
+93
+3
+43
+69
+106
+354
+89
+69
+164
+212
+306
+62
+27
+104
+103
+306
+146
+70
+183
+177
+575
+
.github/ISSUE_TEMPLATE/bug_report.md
+
.github/ISSUE_TEMPLATE/config.yml
+
.github/ISSUE_TEMPLATE/feature_request.md
+
.github/dependabot.yml
+
.github/pull_request_template.md
+
.github/workflows/ci.yml
+
.gitignore
+
AGENTS.md
+
CLAUDE.md
+
Makefile
+
README.md
+
boilerworks.yaml.example
+
boilerworks/__init__.py
+
boilerworks/bootstrap.py
+
boilerworks/cli.py
+
boilerworks/console.py
+
boilerworks/generator.py
+
boilerworks/manifest.py
+
boilerworks/registry.py
+
boilerworks/renderer.py
+
boilerworks/wizard.py
+
data/templates.yaml
+
pyproject.toml
+
tests/conftest.py
+
tests/test_cli.py
+
tests/test_console.py
+
tests/test_generator.py
+
tests/test_manifest.py
+
tests/test_registry.py
+
tests/test_renderer.py
+
tests/test_wizard.py
+
uv.lock
| --- a/.github/ISSUE_TEMPLATE/bug_report.md | ||
| +++ b/.github/ISSUE_TEMPLATE/bug_report.md | ||
| @@ -0,0 +1,35 @@ | ||
| 1 | +--- | |
| 2 | +name: Bug report | |
| 3 | +about: Something isn't working | |
| 4 | +title: 'bug: ' | |
| 5 | +labels: bug | |
| 6 | +assignees: '' | |
| 7 | +--- | |
| 8 | + | |
| 9 | +## Describe the bug | |
| 10 | + | |
| 11 | +A clear description of what the bug is. | |
| 12 | + | |
| 13 | +## To reproduce | |
| 14 | + | |
| 15 | +Steps to reproduce the behavior: | |
| 16 | + | |
| 17 | +1. Run `boilerworks ...` | |
| 18 | +2. See error | |
| 19 | + | |
| 20 | +## Expected behavior | |
| 21 | + | |
| 22 | +What you expected to happen. | |
| 23 | + | |
| 24 | +## Environment | |
| 25 | + | |
| 26 | +- OS: | |
| 27 | +- Python version: | |
| 28 | +- Boilerworks version: (run `boilerworks --version`) | |
| 29 | +- Template: | |
| 30 | + | |
| 31 | +## Error output | |
| 32 | + | |
| 33 | +``` | |
| 34 | +paste error output here | |
| 35 | +``` |
| --- a/.github/ISSUE_TEMPLATE/bug_report.md | |
| +++ b/.github/ISSUE_TEMPLATE/bug_report.md | |
| @@ -0,0 +1,35 @@ | |
| --- a/.github/ISSUE_TEMPLATE/bug_report.md | |
| +++ b/.github/ISSUE_TEMPLATE/bug_report.md | |
| @@ -0,0 +1,35 @@ | |
| 1 | --- |
| 2 | name: Bug report |
| 3 | about: Something isn't working |
| 4 | title: 'bug: ' |
| 5 | labels: bug |
| 6 | assignees: '' |
| 7 | --- |
| 8 | |
| 9 | ## Describe the bug |
| 10 | |
| 11 | A clear description of what the bug is. |
| 12 | |
| 13 | ## To reproduce |
| 14 | |
| 15 | Steps to reproduce the behavior: |
| 16 | |
| 17 | 1. Run `boilerworks ...` |
| 18 | 2. See error |
| 19 | |
| 20 | ## Expected behavior |
| 21 | |
| 22 | What you expected to happen. |
| 23 | |
| 24 | ## Environment |
| 25 | |
| 26 | - OS: |
| 27 | - Python version: |
| 28 | - Boilerworks version: (run `boilerworks --version`) |
| 29 | - Template: |
| 30 | |
| 31 | ## Error output |
| 32 | |
| 33 | ``` |
| 34 | paste error output here |
| 35 | ``` |
| --- a/.github/ISSUE_TEMPLATE/config.yml | ||
| +++ b/.github/ISSUE_TEMPLATE/config.yml | ||
| @@ -0,0 +1,8 @@ | ||
| 1 | +blank_issues_enabled: false | |
| 2 | +contact_links: | |
| 3 | + - name: Documentation | |
| 4 | + url: https://boilerworks.dev | |
| 5 | + about: Read the docs | |
| 6 | + - name: Discussions | |
| 7 | + url: https://github.com/ConflictHQ/boilerworks/discussions | |
| 8 | + about: Ask questions and share ideas |
| --- a/.github/ISSUE_TEMPLATE/config.yml | |
| +++ b/.github/ISSUE_TEMPLATE/config.yml | |
| @@ -0,0 +1,8 @@ | |
| --- a/.github/ISSUE_TEMPLATE/config.yml | |
| +++ b/.github/ISSUE_TEMPLATE/config.yml | |
| @@ -0,0 +1,8 @@ | |
| 1 | blank_issues_enabled: false |
| 2 | contact_links: |
| 3 | - name: Documentation |
| 4 | url: https://boilerworks.dev |
| 5 | about: Read the docs |
| 6 | - name: Discussions |
| 7 | url: https://github.com/ConflictHQ/boilerworks/discussions |
| 8 | about: Ask questions and share ideas |
| --- a/.github/ISSUE_TEMPLATE/feature_request.md | ||
| +++ b/.github/ISSUE_TEMPLATE/feature_request.md | ||
| @@ -0,0 +1,23 @@ | ||
| 1 | +--- | |
| 2 | +name: Feature request | |
| 3 | +about: Suggest an improvement or new template | |
| 4 | +title: 'feat: ' | |
| 5 | +labels: enhancement | |
| 6 | +assignees: '' | |
| 7 | +--- | |
| 8 | + | |
| 9 | +## What problem does this solve? | |
| 10 | + | |
| 11 | +A clear description of the problem or gap. | |
| 12 | + | |
| 13 | +## Proposed solution | |
| 14 | + | |
| 15 | +What you'd like to see. Be specific. | |
| 16 | + | |
| 17 | +## Alternatives considered | |
| 18 | + | |
| 19 | +Other approaches you've thought about. | |
| 20 | + | |
| 21 | +## Additional context | |
| 22 | + | |
| 23 | +Any other context, screenshots, or references. |
| --- a/.github/ISSUE_TEMPLATE/feature_request.md | |
| +++ b/.github/ISSUE_TEMPLATE/feature_request.md | |
| @@ -0,0 +1,23 @@ | |
| --- a/.github/ISSUE_TEMPLATE/feature_request.md | |
| +++ b/.github/ISSUE_TEMPLATE/feature_request.md | |
| @@ -0,0 +1,23 @@ | |
| 1 | --- |
| 2 | name: Feature request |
| 3 | about: Suggest an improvement or new template |
| 4 | title: 'feat: ' |
| 5 | labels: enhancement |
| 6 | assignees: '' |
| 7 | --- |
| 8 | |
| 9 | ## What problem does this solve? |
| 10 | |
| 11 | A clear description of the problem or gap. |
| 12 | |
| 13 | ## Proposed solution |
| 14 | |
| 15 | What you'd like to see. Be specific. |
| 16 | |
| 17 | ## Alternatives considered |
| 18 | |
| 19 | Other approaches you've thought about. |
| 20 | |
| 21 | ## Additional context |
| 22 | |
| 23 | Any other context, screenshots, or references. |
+13
| --- a/.github/dependabot.yml | ||
| +++ b/.github/dependabot.yml | ||
| @@ -0,0 +1,13 @@ | ||
| 1 | +version: 2 | |
| 2 | +updates: | |
| 3 | + - package-ecosystem: "pip" | |
| 4 | + directory: "/" | |
| 5 | + schedule: | |
| 6 | + interval: "weekly" | |
| 7 | + open-pull-requests-limit: 10 | |
| 8 | + | |
| 9 | + - package-ecosystem: "github-actions" | |
| 10 | + directory: "/" | |
| 11 | + schedule: | |
| 12 | + interval: "weekly" | |
| 13 | + open-pull-requests-limit: 5 |
| --- a/.github/dependabot.yml | |
| +++ b/.github/dependabot.yml | |
| @@ -0,0 +1,13 @@ | |
| --- a/.github/dependabot.yml | |
| +++ b/.github/dependabot.yml | |
| @@ -0,0 +1,13 @@ | |
| 1 | version: 2 |
| 2 | updates: |
| 3 | - package-ecosystem: "pip" |
| 4 | directory: "/" |
| 5 | schedule: |
| 6 | interval: "weekly" |
| 7 | open-pull-requests-limit: 10 |
| 8 | |
| 9 | - package-ecosystem: "github-actions" |
| 10 | directory: "/" |
| 11 | schedule: |
| 12 | interval: "weekly" |
| 13 | open-pull-requests-limit: 5 |
| --- a/.github/pull_request_template.md | ||
| +++ b/.github/pull_request_template.md | ||
| @@ -0,0 +1,23 @@ | ||
| 1 | +## Summary | |
| 2 | + | |
| 3 | +<!-- What does this PR do? 1-3 bullet points. --> | |
| 4 | + | |
| 5 | +- | |
| 6 | +- | |
| 7 | + | |
| 8 | +## Related issue | |
| 9 | + | |
| 10 | +Closes # | |
| 11 | + | |
| 12 | +## Test plan | |
| 13 | + | |
| 14 | +<!-- How was this tested? --> | |
| 15 | + | |
| 16 | +- [ ] `make lint` passes | |
| 17 | +- [ ] `make test` passes with ≥ 80% coverage | |
| 18 | +- [ ] `uv run boilerworks --help` works | |
| 19 | +- [ ] Manually tested the changed command(s) | |
| 20 | + | |
| 21 | +## Notes | |
| 22 | + | |
| 23 | +<!-- Anything reviewers should know? Breaking changes? Migration steps? --> |
| --- a/.github/pull_request_template.md | |
| +++ b/.github/pull_request_template.md | |
| @@ -0,0 +1,23 @@ | |
| --- a/.github/pull_request_template.md | |
| +++ b/.github/pull_request_template.md | |
| @@ -0,0 +1,23 @@ | |
| 1 | ## Summary |
| 2 | |
| 3 | <!-- What does this PR do? 1-3 bullet points. --> |
| 4 | |
| 5 | - |
| 6 | - |
| 7 | |
| 8 | ## Related issue |
| 9 | |
| 10 | Closes # |
| 11 | |
| 12 | ## Test plan |
| 13 | |
| 14 | <!-- How was this tested? --> |
| 15 | |
| 16 | - [ ] `make lint` passes |
| 17 | - [ ] `make test` passes with ≥ 80% coverage |
| 18 | - [ ] `uv run boilerworks --help` works |
| 19 | - [ ] Manually tested the changed command(s) |
| 20 | |
| 21 | ## Notes |
| 22 | |
| 23 | <!-- Anything reviewers should know? Breaking changes? Migration steps? --> |
+40
| --- a/.github/workflows/ci.yml | ||
| +++ b/.github/workflows/ci.yml | ||
| @@ -0,0 +1,40 @@ | ||
| 1 | +name: CI | |
| 2 | + | |
| 3 | +on: | |
| 4 | + push: | |
| 5 | + branches: [main] | |
| 6 | + pull_request: | |
| 7 | + branches: [main] | |
| 8 | + | |
| 9 | +jobs: | |
| 10 | + lint: | |
| 11 | + name: Lint | |
| 12 | + runs-on: ubuntu-latest | |
| 13 | + steps: | |
| 14 | + - uses: actions/checkout@v4 | |
| 15 | + -4uses: astral-sh/setup-uv@version: "latest" | |
| 16 | + - name: Install dependencies | |
| 17 | + run: uv sync | |
| 18 | + Ruff check | |
| 19 | + Y@4Y,N:name: Ruff format check7@8f,1: l@53,A:name: Test1g@9F,1:4P@Aw,2M:version: "latest" | |
| 20 | + - name: Install dependencies | |
| 21 | + run: uv sync | |
| 22 | + - name: Run tests | |
| 23 | + run: uv run pytest | |
| 24 | + | |
| 25 | + build: | |
| 26 | + name: Build1g@9F,1:4P@Aw,j:version: "latest" | |
| 27 | + - name: Build package | |
| 28 | +8@BB,2v:run: uv build | |
| 29 | + - name: Verify package | |
| 30 | + run: | | |
| 31 | + ls -la dist/ | |
| 32 | + uv run python -c "import boilerworks; print(boilerworks.__version__)" | |
| 33 | + | |
| 34 | + audit: | |
| 35 | + name: Audit1g@9F,1:4P@Aw,2N:version: "latest" | |
| 36 | + - name: Install pip-audit | |
| 37 | + run: uv tool install pip-audit | |
| 38 | + - name: Run audit | |
| 39 | + run: uv run pip-audit || true | |
| 40 | +2W2YhL; |
| --- a/.github/workflows/ci.yml | |
| +++ b/.github/workflows/ci.yml | |
| @@ -0,0 +1,40 @@ | |
| --- a/.github/workflows/ci.yml | |
| +++ b/.github/workflows/ci.yml | |
| @@ -0,0 +1,40 @@ | |
| 1 | name: CI |
| 2 | |
| 3 | on: |
| 4 | push: |
| 5 | branches: [main] |
| 6 | pull_request: |
| 7 | branches: [main] |
| 8 | |
| 9 | jobs: |
| 10 | lint: |
| 11 | name: Lint |
| 12 | runs-on: ubuntu-latest |
| 13 | steps: |
| 14 | - uses: actions/checkout@v4 |
| 15 | -4uses: astral-sh/setup-uv@version: "latest" |
| 16 | - name: Install dependencies |
| 17 | run: uv sync |
| 18 | Ruff check |
| 19 | Y@4Y,N:name: Ruff format check7@8f,1: l@53,A:name: Test1g@9F,1:4P@Aw,2M:version: "latest" |
| 20 | - name: Install dependencies |
| 21 | run: uv sync |
| 22 | - name: Run tests |
| 23 | run: uv run pytest |
| 24 | |
| 25 | build: |
| 26 | name: Build1g@9F,1:4P@Aw,j:version: "latest" |
| 27 | - name: Build package |
| 28 | 8@BB,2v:run: uv build |
| 29 | - name: Verify package |
| 30 | run: | |
| 31 | ls -la dist/ |
| 32 | uv run python -c "import boilerworks; print(boilerworks.__version__)" |
| 33 | |
| 34 | audit: |
| 35 | name: Audit1g@9F,1:4P@Aw,2N:version: "latest" |
| 36 | - name: Install pip-audit |
| 37 | run: uv tool install pip-audit |
| 38 | - name: Run audit |
| 39 | run: uv run pip-audit || true |
| 40 | 2W2YhL; |
+2
| --- a/.gitignore | ||
| +++ b/.gitignore | ||
| @@ -0,0 +1,2 @@ | ||
| 1 | +.idea/ | |
| 2 | +backend/backend.iml |
| --- a/.gitignore | |
| +++ b/.gitignore | |
| @@ -0,0 +1,2 @@ | |
| --- a/.gitignore | |
| +++ b/.gitignore | |
| @@ -0,0 +1,2 @@ | |
| 1 | .idea/ |
| 2 | backend/backend.iml |
+26
| --- a/AGENTS.md | ||
| +++ b/AGENTS.md | ||
| @@ -0,0 +1,26 @@ | ||
| 1 | +# Agents — Boilerworks CLI | |
| 2 | + | |
| 3 | +This file is for Cursor, Windsurf, and generic AI coding agents. | |
| 4 | + | |
| 5 | +**Read `CLAUDE.md` for the full stack summary and conventions.** | |
| 6 | + | |
| 7 | +## Quick orientation | |
| 8 | + | |
| 9 | +- This is the `boilerworks` Python CLI package, not a web app | |
| 10 | +- Entry point: `boilerworks/cli.py` → `main()` Click group | |
| 11 | +- Template data: `data/templates.yaml` (26 templates) | |
| 12 | +- Manifest model: `boilerworks/manifest.py` → `BoilerworksManifest` | |
| 13 | +- Registry: `boilerworks/registry.py` → `Registry` | |
| 14 | +- Renderer: `boilerworks/renderer.py` → `render_directory`, `build_replacements` | |
| 15 | + | |
| 16 | +## Before writing code | |
| 17 | + | |
| 18 | +1. Run `uv run boilerworks --help` to verify the CLI works | |
| 19 | +2. Run `make lint` to check for style issues | |
| 20 | +3. Run `make test` to ensure tests pass | |
| 21 | + | |
| 22 | +## After writing code | |
| 23 | + | |
| 24 | +1. `make format` — fix style issues | |
| 25 | +2. `make lint` — verify zero violations | |
| 26 | +3. `make test` — verify tests pass and coverage ≥ 80% |
| --- a/AGENTS.md | |
| +++ b/AGENTS.md | |
| @@ -0,0 +1,26 @@ | |
| --- a/AGENTS.md | |
| +++ b/AGENTS.md | |
| @@ -0,0 +1,26 @@ | |
| 1 | # Agents — Boilerworks CLI |
| 2 | |
| 3 | This file is for Cursor, Windsurf, and generic AI coding agents. |
| 4 | |
| 5 | **Read `CLAUDE.md` for the full stack summary and conventions.** |
| 6 | |
| 7 | ## Quick orientation |
| 8 | |
| 9 | - This is the `boilerworks` Python CLI package, not a web app |
| 10 | - Entry point: `boilerworks/cli.py` → `main()` Click group |
| 11 | - Template data: `data/templates.yaml` (26 templates) |
| 12 | - Manifest model: `boilerworks/manifest.py` → `BoilerworksManifest` |
| 13 | - Registry: `boilerworks/registry.py` → `Registry` |
| 14 | - Renderer: `boilerworks/renderer.py` → `render_directory`, `build_replacements` |
| 15 | |
| 16 | ## Before writing code |
| 17 | |
| 18 | 1. Run `uv run boilerworks --help` to verify the CLI works |
| 19 | 2. Run `make lint` to check for style issues |
| 20 | 3. Run `make test` to ensure tests pass |
| 21 | |
| 22 | ## After writing code |
| 23 | |
| 24 | 1. `make format` — fix style issues |
| 25 | 2. `make lint` — verify zero violations |
| 26 | 3. `make test` — verify tests pass and coverage ≥ 80% |
+82
| --- a/CLAUDE.md | ||
| +++ b/CLAUDE.md | ||
| @@ -0,0 +1,82 @@ | ||
| 1 | +# Claude — Boilerworks CLI | |
| 2 | + | |
| 3 | +Primary conventions doc: read this file, then the source code. | |
| 4 | + | |
| 5 | +This repo is the **Boilerworks CLI** — a Python package published to PyPI as `boilerworks`. | |
| 6 | +It is NOT a web application. It is a command-line tool built with Click, Questionary, and Rich. | |
| 7 | + | |
| 8 | +--- | |
| 9 | + | |
| 10 | +## Stack | |
| 11 | + | |
| 12 | +- **Language**: Python 3.12+ | |
| 13 | +- **CLI framework**: Click 8+ | |
| 14 | +- **Interactive prompts**: Questionary 2+ | |
| 15 | +- **Output**: Rich 13+ (tables, panels, progress bars) | |
| 16 | +- **Manifest validation**: Pydantic v2 | |
| 17 | +- **Template rendering**: string replacement (not Jinja2) | |
| 18 | +- **Git operations**: subprocess (git CLI) + GitPython | |
| 19 | +- **Config**: PyYAML 6+ | |
| 20 | +- **Package manager**: uv (not pip) | |
| 21 | +- **Lint + format**: Ruff (not flake8/black/isort) | |
| 22 | +- **Tests**: pytest with coverage | |
| 23 | + | |
| 24 | +## Package layout | |
| 25 | + | |
| 26 | +``` | |
| 27 | +boilerworks/ # Python package | |
| 28 | + __init__.py # __version__ = "0.1.0" | |
| 29 | + cli.py # Click group: setup, init, bootstrap, list | |
| 30 | + wizard.py # Questionary prompts → boilerworks.yaml | |
| 31 | + generator.py # Clone → render → wire → git init | |
| 32 | + bootstrap.py # Terraform stub (v2) | |
| 33 | + manifest.py # Pydantic models for boilerworks.yaml | |
| 34 | + registry.py # Load + query templates.yaml | |
| 35 | + renderer.py # String replacement in cloned files | |
| 36 | + console.py # Rich output helpers | |
| 37 | +data/ | |
| 38 | + templates.yaml # All 26 templates with metadata | |
| 39 | +tests/ | |
| 40 | + conftest.py | |
| 41 | + test_cli.py | |
| 42 | + test_manifest.py | |
| 43 | + test_registry.py | |
| 44 | + test_renderer.py | |
| 45 | + test_generator.py | |
| 46 | + test_console.py | |
| 47 | + test_wizard.py | |
| 48 | +``` | |
| 49 | + | |
| 50 | +## Running locally | |
| 51 | + | |
| 52 | +```bash | |
| 53 | +uv sync # install deps | |
| 54 | +uv run boilerworks --help # verify install | |
| 55 | +make lint # ruff check + format --check | |
| 56 | +make test # pytest with coverage | |
| 57 | +make format # ruff fix + format | |
| 58 | +``` | |
| 59 | + | |
| 60 | +## Adding a template | |
| 61 | + | |
| 62 | +Edit `data/templates.yaml`. Add an entry following the existing schema. | |
| 63 | +Run `make test` — `test_registry.py` will catch count mismatches. | |
| 64 | + | |
| 65 | +## Coding standards | |
| 66 | + | |
| 67 | +- Fully typed: all function signatures have type hints | |
| 68 | +- Line length: 120 (ruff config in pyproject.toml) | |
| 69 | +- `ruff check . && ruff format .` after every change | |
| 70 | +- pytest coverage ≥ 80% | |
| 71 | +- No TODOs, no stubs (bootstrap is intentionally a v2 stub — document it clearly) | |
| 72 | +- No co-authorship messages in commits | |
| 73 | + | |
| 74 | +## Common patterns | |
| 75 | + | |
| 76 | +**Adding a CLI option**: edit `boilerworks/cli.py`, add `@click.option(...)` decorator | |
| 77 | + | |
| 78 | +**Adding a manifest field**: edit `boilerworks/manifest.py` (BoilerworksManifest model) | |
| 79 | + | |
| 80 | +**Adding a renderer rule**: edit `boilerworks/renderer.py` (`build_replacements` or `_SKIP_*`) | |
| 81 | + | |
| 82 | +**Adding a template to the catalogue**: edit `data/templates.yaml` |
| --- a/CLAUDE.md | |
| +++ b/CLAUDE.md | |
| @@ -0,0 +1,82 @@ | |
| --- a/CLAUDE.md | |
| +++ b/CLAUDE.md | |
| @@ -0,0 +1,82 @@ | |
| 1 | # Claude — Boilerworks CLI |
| 2 | |
| 3 | Primary conventions doc: read this file, then the source code. |
| 4 | |
| 5 | This repo is the **Boilerworks CLI** — a Python package published to PyPI as `boilerworks`. |
| 6 | It is NOT a web application. It is a command-line tool built with Click, Questionary, and Rich. |
| 7 | |
| 8 | --- |
| 9 | |
| 10 | ## Stack |
| 11 | |
| 12 | - **Language**: Python 3.12+ |
| 13 | - **CLI framework**: Click 8+ |
| 14 | - **Interactive prompts**: Questionary 2+ |
| 15 | - **Output**: Rich 13+ (tables, panels, progress bars) |
| 16 | - **Manifest validation**: Pydantic v2 |
| 17 | - **Template rendering**: string replacement (not Jinja2) |
| 18 | - **Git operations**: subprocess (git CLI) + GitPython |
| 19 | - **Config**: PyYAML 6+ |
| 20 | - **Package manager**: uv (not pip) |
| 21 | - **Lint + format**: Ruff (not flake8/black/isort) |
| 22 | - **Tests**: pytest with coverage |
| 23 | |
| 24 | ## Package layout |
| 25 | |
| 26 | ``` |
| 27 | boilerworks/ # Python package |
| 28 | __init__.py # __version__ = "0.1.0" |
| 29 | cli.py # Click group: setup, init, bootstrap, list |
| 30 | wizard.py # Questionary prompts → boilerworks.yaml |
| 31 | generator.py # Clone → render → wire → git init |
| 32 | bootstrap.py # Terraform stub (v2) |
| 33 | manifest.py # Pydantic models for boilerworks.yaml |
| 34 | registry.py # Load + query templates.yaml |
| 35 | renderer.py # String replacement in cloned files |
| 36 | console.py # Rich output helpers |
| 37 | data/ |
| 38 | templates.yaml # All 26 templates with metadata |
| 39 | tests/ |
| 40 | conftest.py |
| 41 | test_cli.py |
| 42 | test_manifest.py |
| 43 | test_registry.py |
| 44 | test_renderer.py |
| 45 | test_generator.py |
| 46 | test_console.py |
| 47 | test_wizard.py |
| 48 | ``` |
| 49 | |
| 50 | ## Running locally |
| 51 | |
| 52 | ```bash |
| 53 | uv sync # install deps |
| 54 | uv run boilerworks --help # verify install |
| 55 | make lint # ruff check + format --check |
| 56 | make test # pytest with coverage |
| 57 | make format # ruff fix + format |
| 58 | ``` |
| 59 | |
| 60 | ## Adding a template |
| 61 | |
| 62 | Edit `data/templates.yaml`. Add an entry following the existing schema. |
| 63 | Run `make test` — `test_registry.py` will catch count mismatches. |
| 64 | |
| 65 | ## Coding standards |
| 66 | |
| 67 | - Fully typed: all function signatures have type hints |
| 68 | - Line length: 120 (ruff config in pyproject.toml) |
| 69 | - `ruff check . && ruff format .` after every change |
| 70 | - pytest coverage ≥ 80% |
| 71 | - No TODOs, no stubs (bootstrap is intentionally a v2 stub — document it clearly) |
| 72 | - No co-authorship messages in commits |
| 73 | |
| 74 | ## Common patterns |
| 75 | |
| 76 | **Adding a CLI option**: edit `boilerworks/cli.py`, add `@click.option(...)` decorator |
| 77 | |
| 78 | **Adding a manifest field**: edit `boilerworks/manifest.py` (BoilerworksManifest model) |
| 79 | |
| 80 | **Adding a renderer rule**: edit `boilerworks/renderer.py` (`build_replacements` or `_SKIP_*`) |
| 81 | |
| 82 | **Adding a template to the catalogue**: edit `data/templates.yaml` |
A
Makefile
+26
| --- a/Makefile | ||
| +++ b/Makefile | ||
| @@ -0,0 +1,26 @@ | ||
| 1 | +.PHONY: lint format test build install clean | |
| 2 | + | |
| 3 | +lint: | |
| 4 | + uv run ruff check . | |
| 5 | + uv run ruff format --check . | |
| 6 | + | |
| 7 | +format: | |
| 8 | + uv run ruff check --fix . | |
| 9 | + uv run ruff format . | |
| 10 | + | |
| 11 | +test: | |
| 12 | + uv run pytest | |
| 13 | + | |
| 14 | +test-fast: | |
| 15 | + uv run pytest -x --no-cov | |
| 16 | + | |
| 17 | +build: | |
| 18 | + uv build | |
| 19 | + | |
| 20 | +install: | |
| 21 | + uv sync | |
| 22 | + | |
| 23 | +clean: | |
| 24 | + rm -rf dist/ .pytest_cache/ .coverage htmlcov/ __pycache__ | |
| 25 | + find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true | |
| 26 | + find . -type f -name "*.pyc" -delete |
| --- a/Makefile | |
| +++ b/Makefile | |
| @@ -0,0 +1,26 @@ | |
| --- a/Makefile | |
| +++ b/Makefile | |
| @@ -0,0 +1,26 @@ | |
| 1 | .PHONY: lint format test build install clean |
| 2 | |
| 3 | lint: |
| 4 | uv run ruff check . |
| 5 | uv run ruff format --check . |
| 6 | |
| 7 | format: |
| 8 | uv run ruff check --fix . |
| 9 | uv run ruff format . |
| 10 | |
| 11 | test: |
| 12 | uv run pytest |
| 13 | |
| 14 | test-fast: |
| 15 | uv run pytest -x --no-cov |
| 16 | |
| 17 | build: |
| 18 | uv build |
| 19 | |
| 20 | install: |
| 21 | uv sync |
| 22 | |
| 23 | clean: |
| 24 | rm -rf dist/ .pytest_cache/ .coverage htmlcov/ __pycache__ |
| 25 | find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true |
| 26 | find . -type f -name "*.pyc" -delete |
+160
| --- a/README.md | ||
| +++ b/README.md | ||
| @@ -0,0 +1,160 @@ | ||
| 1 | +# Boilerworks | |
| 2 | + | |
| 3 | +**Production-ready project templates — assembled in seconds.** | |
| 4 | + | |
| 5 | +Boilerworks is a CLI that clones and configures any of 26 opinionated, best-of-breed project templates. Stop re-solving auth, permissions, Docker, CI, and admin panels from scratch. Pick a stack, run `boilerworks init`, and get straight to your business logic. | |
| 6 | + | |
| 7 | +``` | |
| 8 | +pip install boilerworks | |
| 9 | +boilerworks setup # interactive wizard → writes boilerworks.yaml | |
| 10 | +boilerworks init # clone + configure the template | |
| 11 | +cd my-project | |
| 12 | +docker compose up -d # full stack running in seconds | |
| 13 | +``` | |
| 14 | + | |
| 15 | +--- | |
| 16 | + | |
| 17 | +## Installation | |
| 18 | + | |
| 19 | +```bash | |
| 20 | +pip install boilerworks | |
| 21 | +# or with uv: | |
| 22 | +uv tool install boilerworks | |
| 23 | +``` | |
| 24 | + | |
| 25 | +Requires Python 3.12+. | |
| 26 | + | |
| 27 | +--- | |
| 28 | + | |
| 29 | +## Quick Start | |
| 30 | + | |
| 31 | +### 1. Run the setup wizard | |
| 32 | + | |
| 33 | +```bash | |
| 34 | +boilerworks setup | |
| 35 | +``` | |
| 36 | + | |
| 37 | +Answer 13 questions about your project (name, template, cloud, compliance, etc.) and a `boilerworks.yaml` manifest is written to the current directory. | |
| 38 | + | |
| 39 | +### 2. Generate the project | |
| 40 | + | |
| 41 | +```bash | |
| 42 | +boilerworks init | |
| 43 | +``` | |
| 44 | + | |
| 45 | +Boilerworks clones the selected template, replaces all `boilerworks` references with your project name, and runs `git init` to give you a clean starting commit. | |
| 46 | + | |
| 47 | +### 3. Start developing | |
| 48 | + | |
| 49 | +```bash | |
| 50 | +cd my-project | |
| 51 | +docker compose up -d | |
| 52 | +# Visit http://localhost:3000 | |
| 53 | +``` | |
| 54 | + | |
| 55 | +One command. Full stack. No manual setup. | |
| 56 | + | |
| 57 | +--- | |
| 58 | + | |
| 59 | +## Template Catalogue | |
| 60 | + | |
| 61 | +26 templates across three sizes: | |
| 62 | + | |
| 63 | +| Size | Description | Auth | Deploy Target | | |
| 64 | +|------|-------------|------|---------------| | |
| 65 | +| **Full** | Apps with users | Session auth, permissions, org management | VPS, containers, Kubernetes | | |
| 66 | +| **Micro** | API services | API-key auth | VPS, containers | | |
| 67 | +| **Edge** | Serverless apps | Flexible | Cloudflare Workers / Pages | | |
| 68 | + | |
| 69 | +### Full Templates (15) | |
| 70 | + | |
| 71 | +| Statusular](https://github|---------/boilerworks-django-nextjs) | Django 5 | Next.js 16 | | |
| 72 | +| [nestjs-nextjs](https://github.com/ConflictHQ/boilerworks-nestjs-nextjs) | NestJS 11 | Next.js 16 | | |
| 73 | +| [rails-hotwire](https://github.com/ConflictHQ/boilerworks-rails-hotwire) | Rails 8 | Hotwire + Tailwind | | |
| 74 | +| [rails-nextjs](https://github.com/ConflictHQ/boilerworks-rails-nextjs) | Rails 8 | Next.js 16 | | |
| 75 | +| [spring-angular](https://github.com/ConflictHQ/boilerworks-spring-angular) | Spring Boot 3 | Angular 19 | | |
| 76 | +| [go-nextjs](https://github.com/ConflictHQ/boilerworks-go-nextjs) | Go + Chi | Next.js 16 | | |
| 77 | +| [phoenix-liveview](https://github.com/ConflictHQ/boilerworks-phoenix-liveview) | Phoenix 1.7 | LiveView | | |
| 78 | +| [laravel-vue](https://github.com/ConflictHQ/boilerworks-laravel-vue) | Laravel 12 | Inertia + Vue 3 | | |
| 79 | +| [django-htmx](https://github.com/ConflictHQ/boilerworks-django-htmx) | Django 5 | HTMX + Alpine.js | | |
| 80 | +| [fastapi-nextjs](https://github.com/ConflictHQ/boilerworks-fastapi-nextjs) | FastAPI | Next.js 16 | | |
| 81 | +| [spring-nextjs](https://github.com/ConflictHQ/boilerworks-spring-nextjs) | Spring Boot 3 | Next.js 16 | | |
| 82 | +| [laravel-livewire](https://github.com/ConflictHQ/boilerworks-laravel-livewire) | Laravel 12 | Livewire 3 | | |
| 83 | +| [go-htmx](https://github.com/ConflictHQ/boilerworks-go-htmx) | Go + Chi | HTMX + Templ | | |
| 84 | +| [fastapi-htmx](https://github.com/ConflictHQ/boilerworks-fastapi-htmx) | FastAPI | HTMX + Alpine.js | | |
| 85 | +| [saleor-nextjs](https://github.com/ConflictHQ/boilerworks-saleor-nextjs) | Saleor (Django) | Next.js 16 | | |
| 86 | + | |
| 87 | +### Micro Templates (6) | |
| 88 | + | |
| 89 | +| Name | Backend | | |
| 90 | +|------|---------| | |
| 91 | +| [django-micro](https://github.com/ConflictHQ/boilerworks-django-micro) | Django 5 (DRF/Ninja) | | |
| 92 | +| [fastapi-micro](https://github.com/ConflictHQ/boilerworks-fastapi-micro) | FastAPI | | |
| 93 | +| [nestjs-micro](https://github.com/ConflictHQ/boilerworks-nestjs-micro) | NestJS 11 | | |
| 94 | +| [go-micro](https://github.com/ConflictHQ/boilerworks-go-micro) | Go + Chi | | |
| 95 | +| [rust-micro](https://github.com/ConflictHQ/boilerworks-rust-micro) | Axum (Rust) | | |
| 96 | +| [cherrypy-micro](https://github.com/ConflictHQ/boilerworks-cherrypy-micro) | CherryPy | | |
| 97 | + | |
| 98 | +### Edge Templates (5) | |
| 99 | + | |
| 100 | +| Name | Framework | | |
| 101 | +|------|-----------| | |
| 102 | +| [sveltekit-full](https://github.com/ConflictHQ/boilerworks-sveltekit-full) | SvelteKit | | |
| 103 | +| [remix-full](https://github.com/ConflictHQ/boilerworks-remix-full) | Remix | | |
| 104 | +| [hono-micro](https://github.com/ConflictHQ/boilerworks-hono-micro) | Hono (Cloudflare Workers) | | |
| 105 | +| [nuxt-full](https://github.com/ConflictHQ/boilerworks-nuxt-full) | Nuxt 4 | | |
| 106 | +| [astro-site](https://github.com/ConflictHQ/boilerworks-astro-site) | Astro | | |
| 107 | + | |
| 108 | +--- | |
| 109 | + | |
| 110 | +## Commands | |
| 111 | + | |
| 112 | +```bash | |
| 113 | +boilerworks --help # show all commands | |
| 114 | +boilerworks list # show all 26 templates | |
| 115 | +boilerworks list --size micro # filter by size | |
| 116 | +boilerworks list --language python # filter by language | |
| 117 | +boilerworks setup # interactive wizard | |
| 118 | +boilerworks init # generate project from boilerworks.yaml | |
| 119 | +boilerworks init --dry-run # preview what would happen | |
| 120 | +boilerworks init --manifest ./path/to/boilerworks.yaml | |
| 121 | +boilerworks init --output /path/to/output | |
| 122 | +``` | |
| 123 | + | |
| 124 | +--- | |
| 125 | + | |
| 126 | +## boilerworks.yaml | |
| 127 | + | |
| 128 | +The manifest file describes your project. Generated by `boilerworks setup`, editable by hand. | |
| 129 | + | |
| 130 | +```yaml | |
| 131 | +project: my-app | |
| 132 | +family: django-nextjs | |
| 133 | +size: full | |
| 134 | +topology: standard | |
| 135 | +cloud: aws | |
| 136 | +ops: true | |
| 137 | +region: us-east-1 | |
| 138 | +domain: myapp.com | |
| 139 | +mobile: false | |
| 140 | +web_presence: false | |
| 141 | +compliance: | |
| 142 | + - soc2 | |
| 143 | +servicesonflictil: ses | |
| 144 | + cache: redis | |
| 145 | +data: | |
| 146 | + database: postgres | |
| 147 | + migrations: true | |
| 148 | + seedonflict LLC. | |
| 149 | + ✅ Done | | |
| 150 | +| nestjs-nextjs | NestJS 11 | Next.js 16 | ✅ Done | | |
| 151 | +| django-htmxrrent directory. | |
| 152 | + | |
| 153 | +### 2. Generat 🔨 Building | | |
| 154 | +| saleor-nextjs**Micro** | API services | API-ke 🔨 Building | | |
| 155 | +| rails-hotwire | Rails 8 | Hotwire | 🔨 Building | | |
| 156 | +| laravel-vue | Laravel 11ce, etc.) and a `boi 🔨 Building | | |
| 157 | +| fastapi-nextjslerworks` references with 🔨 Building | | |
| 158 | +| fastapi-htmx-|------|---------------| | |
| 159 | +| **F 📋 Planned | | |
| 160 | +| spring-ang |
| --- a/README.md | |
| +++ b/README.md | |
| @@ -0,0 +1,160 @@ | |
| --- a/README.md | |
| +++ b/README.md | |
| @@ -0,0 +1,160 @@ | |
| 1 | # Boilerworks |
| 2 | |
| 3 | **Production-ready project templates — assembled in seconds.** |
| 4 | |
| 5 | Boilerworks is a CLI that clones and configures any of 26 opinionated, best-of-breed project templates. Stop re-solving auth, permissions, Docker, CI, and admin panels from scratch. Pick a stack, run `boilerworks init`, and get straight to your business logic. |
| 6 | |
| 7 | ``` |
| 8 | pip install boilerworks |
| 9 | boilerworks setup # interactive wizard → writes boilerworks.yaml |
| 10 | boilerworks init # clone + configure the template |
| 11 | cd my-project |
| 12 | docker compose up -d # full stack running in seconds |
| 13 | ``` |
| 14 | |
| 15 | --- |
| 16 | |
| 17 | ## Installation |
| 18 | |
| 19 | ```bash |
| 20 | pip install boilerworks |
| 21 | # or with uv: |
| 22 | uv tool install boilerworks |
| 23 | ``` |
| 24 | |
| 25 | Requires Python 3.12+. |
| 26 | |
| 27 | --- |
| 28 | |
| 29 | ## Quick Start |
| 30 | |
| 31 | ### 1. Run the setup wizard |
| 32 | |
| 33 | ```bash |
| 34 | boilerworks setup |
| 35 | ``` |
| 36 | |
| 37 | Answer 13 questions about your project (name, template, cloud, compliance, etc.) and a `boilerworks.yaml` manifest is written to the current directory. |
| 38 | |
| 39 | ### 2. Generate the project |
| 40 | |
| 41 | ```bash |
| 42 | boilerworks init |
| 43 | ``` |
| 44 | |
| 45 | Boilerworks clones the selected template, replaces all `boilerworks` references with your project name, and runs `git init` to give you a clean starting commit. |
| 46 | |
| 47 | ### 3. Start developing |
| 48 | |
| 49 | ```bash |
| 50 | cd my-project |
| 51 | docker compose up -d |
| 52 | # Visit http://localhost:3000 |
| 53 | ``` |
| 54 | |
| 55 | One command. Full stack. No manual setup. |
| 56 | |
| 57 | --- |
| 58 | |
| 59 | ## Template Catalogue |
| 60 | |
| 61 | 26 templates across three sizes: |
| 62 | |
| 63 | | Size | Description | Auth | Deploy Target | |
| 64 | |------|-------------|------|---------------| |
| 65 | | **Full** | Apps with users | Session auth, permissions, org management | VPS, containers, Kubernetes | |
| 66 | | **Micro** | API services | API-key auth | VPS, containers | |
| 67 | | **Edge** | Serverless apps | Flexible | Cloudflare Workers / Pages | |
| 68 | |
| 69 | ### Full Templates (15) |
| 70 | |
| 71 | | Statusular](https://github|---------/boilerworks-django-nextjs) | Django 5 | Next.js 16 | |
| 72 | | [nestjs-nextjs](https://github.com/ConflictHQ/boilerworks-nestjs-nextjs) | NestJS 11 | Next.js 16 | |
| 73 | | [rails-hotwire](https://github.com/ConflictHQ/boilerworks-rails-hotwire) | Rails 8 | Hotwire + Tailwind | |
| 74 | | [rails-nextjs](https://github.com/ConflictHQ/boilerworks-rails-nextjs) | Rails 8 | Next.js 16 | |
| 75 | | [spring-angular](https://github.com/ConflictHQ/boilerworks-spring-angular) | Spring Boot 3 | Angular 19 | |
| 76 | | [go-nextjs](https://github.com/ConflictHQ/boilerworks-go-nextjs) | Go + Chi | Next.js 16 | |
| 77 | | [phoenix-liveview](https://github.com/ConflictHQ/boilerworks-phoenix-liveview) | Phoenix 1.7 | LiveView | |
| 78 | | [laravel-vue](https://github.com/ConflictHQ/boilerworks-laravel-vue) | Laravel 12 | Inertia + Vue 3 | |
| 79 | | [django-htmx](https://github.com/ConflictHQ/boilerworks-django-htmx) | Django 5 | HTMX + Alpine.js | |
| 80 | | [fastapi-nextjs](https://github.com/ConflictHQ/boilerworks-fastapi-nextjs) | FastAPI | Next.js 16 | |
| 81 | | [spring-nextjs](https://github.com/ConflictHQ/boilerworks-spring-nextjs) | Spring Boot 3 | Next.js 16 | |
| 82 | | [laravel-livewire](https://github.com/ConflictHQ/boilerworks-laravel-livewire) | Laravel 12 | Livewire 3 | |
| 83 | | [go-htmx](https://github.com/ConflictHQ/boilerworks-go-htmx) | Go + Chi | HTMX + Templ | |
| 84 | | [fastapi-htmx](https://github.com/ConflictHQ/boilerworks-fastapi-htmx) | FastAPI | HTMX + Alpine.js | |
| 85 | | [saleor-nextjs](https://github.com/ConflictHQ/boilerworks-saleor-nextjs) | Saleor (Django) | Next.js 16 | |
| 86 | |
| 87 | ### Micro Templates (6) |
| 88 | |
| 89 | | Name | Backend | |
| 90 | |------|---------| |
| 91 | | [django-micro](https://github.com/ConflictHQ/boilerworks-django-micro) | Django 5 (DRF/Ninja) | |
| 92 | | [fastapi-micro](https://github.com/ConflictHQ/boilerworks-fastapi-micro) | FastAPI | |
| 93 | | [nestjs-micro](https://github.com/ConflictHQ/boilerworks-nestjs-micro) | NestJS 11 | |
| 94 | | [go-micro](https://github.com/ConflictHQ/boilerworks-go-micro) | Go + Chi | |
| 95 | | [rust-micro](https://github.com/ConflictHQ/boilerworks-rust-micro) | Axum (Rust) | |
| 96 | | [cherrypy-micro](https://github.com/ConflictHQ/boilerworks-cherrypy-micro) | CherryPy | |
| 97 | |
| 98 | ### Edge Templates (5) |
| 99 | |
| 100 | | Name | Framework | |
| 101 | |------|-----------| |
| 102 | | [sveltekit-full](https://github.com/ConflictHQ/boilerworks-sveltekit-full) | SvelteKit | |
| 103 | | [remix-full](https://github.com/ConflictHQ/boilerworks-remix-full) | Remix | |
| 104 | | [hono-micro](https://github.com/ConflictHQ/boilerworks-hono-micro) | Hono (Cloudflare Workers) | |
| 105 | | [nuxt-full](https://github.com/ConflictHQ/boilerworks-nuxt-full) | Nuxt 4 | |
| 106 | | [astro-site](https://github.com/ConflictHQ/boilerworks-astro-site) | Astro | |
| 107 | |
| 108 | --- |
| 109 | |
| 110 | ## Commands |
| 111 | |
| 112 | ```bash |
| 113 | boilerworks --help # show all commands |
| 114 | boilerworks list # show all 26 templates |
| 115 | boilerworks list --size micro # filter by size |
| 116 | boilerworks list --language python # filter by language |
| 117 | boilerworks setup # interactive wizard |
| 118 | boilerworks init # generate project from boilerworks.yaml |
| 119 | boilerworks init --dry-run # preview what would happen |
| 120 | boilerworks init --manifest ./path/to/boilerworks.yaml |
| 121 | boilerworks init --output /path/to/output |
| 122 | ``` |
| 123 | |
| 124 | --- |
| 125 | |
| 126 | ## boilerworks.yaml |
| 127 | |
| 128 | The manifest file describes your project. Generated by `boilerworks setup`, editable by hand. |
| 129 | |
| 130 | ```yaml |
| 131 | project: my-app |
| 132 | family: django-nextjs |
| 133 | size: full |
| 134 | topology: standard |
| 135 | cloud: aws |
| 136 | ops: true |
| 137 | region: us-east-1 |
| 138 | domain: myapp.com |
| 139 | mobile: false |
| 140 | web_presence: false |
| 141 | compliance: |
| 142 | - soc2 |
| 143 | servicesonflictil: ses |
| 144 | cache: redis |
| 145 | data: |
| 146 | database: postgres |
| 147 | migrations: true |
| 148 | seedonflict LLC. |
| 149 | ✅ Done | |
| 150 | | nestjs-nextjs | NestJS 11 | Next.js 16 | ✅ Done | |
| 151 | | django-htmxrrent directory. |
| 152 | |
| 153 | ### 2. Generat 🔨 Building | |
| 154 | | saleor-nextjs**Micro** | API services | API-ke 🔨 Building | |
| 155 | | rails-hotwire | Rails 8 | Hotwire | 🔨 Building | |
| 156 | | laravel-vue | Laravel 11ce, etc.) and a `boi 🔨 Building | |
| 157 | | fastapi-nextjslerworks` references with 🔨 Building | |
| 158 | | fastapi-htmx-|------|---------------| |
| 159 | | **F 📋 Planned | |
| 160 | | spring-ang |
+93
| --- a/boilerworks.yaml.example | ||
| +++ b/boilerworks.yaml.example | ||
| @@ -0,0 +1,93 @@ | ||
| 1 | +# boilerworks.yaml — project manifest | |
| 2 | +# | |
| 3 | +# Generated by `boilerworks setup`. Edit by hand or regenerate at any time. | |
| 4 | +# All fields are validated against the registry and manifest schema. | |
| 5 | +# Run `boilerworks init` to generate the project from this file. | |
| 6 | + | |
| 7 | +# ── Required ────────────────────────────────────────────────────────────────── | |
| 8 | + | |
| 9 | +# Slug format: lowercase, start with a letter, letters/digits/hyphens only | |
| 10 | +project: my-app | |
| 11 | + | |
| 12 | +# Template family name. Run `boilerworks list` to see all options. | |
| 13 | +family: django-nextjs | |
| 14 | + | |
| 15 | +# Template size: full | micro | edge | |
| 16 | +size: full | |
| 17 | + | |
| 18 | +# ── Topology ────────────────────────────────────────────────────────────────── | |
| 19 | + | |
| 20 | +# standard: monorepo with web app and ops (default) | |
| 21 | +# api-only: backend only, no frontend | |
| 22 | +# omni: standard + mobile (v2) | |
| 23 | +topology: standard | |
| 24 | + | |
| 25 | +# ── Cloud ───────────────────────────────────────────────────────────────────── | |
| 26 | + | |
| 27 | +# Cloud provider: aws | gcp | azure | null | |
| 28 | +cloud: aws | |
| 29 | + | |
| 30 | +# Cloud region (required if cloud is set) | |
| 31 | +region: us-east-1 | |
| 32 | + | |
| 33 | +# ── Domain ──────────────────────────────────────────────────────────────────── | |
| 34 | + | |
| 35 | +# Your production domain (optional) | |
| 36 | +domain: myapp.com | |
| 37 | + | |
| 38 | +# ── Optional add-ons ────────────────────────────────────────────────────────── | |
| 39 | + | |
| 40 | +# Include a mobile app template (Full size only) | |
| 41 | +mobile: false | |
| 42 | + | |
| 43 | +# Include a web presence / marketing site template (Full size only) | |
| 44 | +web_presence: false | |
| 45 | + | |
| 46 | +# ── Compliance ──────────────────────────────────────────────────────────────── | |
| 47 | + | |
| 48 | +# One or more: soc2 | hipaa | pci-dss | gdpr | |
| 49 | +compliance: | |
| 50 | + - soc2 | |
| 51 | + | |
| 52 | +# ── Services ────────────────────────────────────────────────────────────────── | |
| 53 | + | |
| 54 | +services: | |
| 55 | + # Transactional email: ses | sendgrid | mailgun | null | |
| 56 | + email: ses | |
| 57 | + | |
| 58 | + # File storage: s3 | gcs | azure-blob | null | |
| 59 | + storage: s3 | |
| 60 | + | |
| 61 | + # Full-text search: opensearch | meilisearch | null | |
| 62 | + search: opensearch | |
| 63 | + | |
| 64 | + # Cache / session store: redis | memcached | null | |
| 65 | + cache: redis | |
| 66 | + | |
| 67 | +# ── Data ────────────────────────────────────────────────────────────────────── | |
| 68 | + | |
| 69 | +data: | |
| 70 | + # Primary database: postgres | mysql | sqlite | |
| 71 | + database: postgres | |
| 72 | + | |
| 73 | + # Run migrations on first boot | |
| 74 | + migrations: true | |
| 75 | + | |
| 76 | + # Load seed data on first boot | |
| 77 | + seed_data: true | |
| 78 | + | |
| 79 | +# ── Testing ─────────────────────────────────────────────────────────────────── | |
| 80 | + | |
| 81 | +testing: | |
| 82 | + # End-to-end framework: playwright | cypress | null | |
| 83 | + e2e: playwright | |
| 84 | + | |
| 85 | + # Unit tests | |
| 86 | + unit: true | |
| 87 | + | |
| 88 | + # Integration tests | |
| 89 | + integration: true | |
| 90 | + | |
| 91 | +# ── Template versions (auto-managed, do not edit manually) ──────────────────── | |
| 92 | + | |
| 93 | +template_versions: {} |
| --- a/boilerworks.yaml.example | |
| +++ b/boilerworks.yaml.example | |
| @@ -0,0 +1,93 @@ | |
| --- a/boilerworks.yaml.example | |
| +++ b/boilerworks.yaml.example | |
| @@ -0,0 +1,93 @@ | |
| 1 | # boilerworks.yaml — project manifest |
| 2 | # |
| 3 | # Generated by `boilerworks setup`. Edit by hand or regenerate at any time. |
| 4 | # All fields are validated against the registry and manifest schema. |
| 5 | # Run `boilerworks init` to generate the project from this file. |
| 6 | |
| 7 | # ── Required ────────────────────────────────────────────────────────────────── |
| 8 | |
| 9 | # Slug format: lowercase, start with a letter, letters/digits/hyphens only |
| 10 | project: my-app |
| 11 | |
| 12 | # Template family name. Run `boilerworks list` to see all options. |
| 13 | family: django-nextjs |
| 14 | |
| 15 | # Template size: full | micro | edge |
| 16 | size: full |
| 17 | |
| 18 | # ── Topology ────────────────────────────────────────────────────────────────── |
| 19 | |
| 20 | # standard: monorepo with web app and ops (default) |
| 21 | # api-only: backend only, no frontend |
| 22 | # omni: standard + mobile (v2) |
| 23 | topology: standard |
| 24 | |
| 25 | # ── Cloud ───────────────────────────────────────────────────────────────────── |
| 26 | |
| 27 | # Cloud provider: aws | gcp | azure | null |
| 28 | cloud: aws |
| 29 | |
| 30 | # Cloud region (required if cloud is set) |
| 31 | region: us-east-1 |
| 32 | |
| 33 | # ── Domain ──────────────────────────────────────────────────────────────────── |
| 34 | |
| 35 | # Your production domain (optional) |
| 36 | domain: myapp.com |
| 37 | |
| 38 | # ── Optional add-ons ────────────────────────────────────────────────────────── |
| 39 | |
| 40 | # Include a mobile app template (Full size only) |
| 41 | mobile: false |
| 42 | |
| 43 | # Include a web presence / marketing site template (Full size only) |
| 44 | web_presence: false |
| 45 | |
| 46 | # ── Compliance ──────────────────────────────────────────────────────────────── |
| 47 | |
| 48 | # One or more: soc2 | hipaa | pci-dss | gdpr |
| 49 | compliance: |
| 50 | - soc2 |
| 51 | |
| 52 | # ── Services ────────────────────────────────────────────────────────────────── |
| 53 | |
| 54 | services: |
| 55 | # Transactional email: ses | sendgrid | mailgun | null |
| 56 | email: ses |
| 57 | |
| 58 | # File storage: s3 | gcs | azure-blob | null |
| 59 | storage: s3 |
| 60 | |
| 61 | # Full-text search: opensearch | meilisearch | null |
| 62 | search: opensearch |
| 63 | |
| 64 | # Cache / session store: redis | memcached | null |
| 65 | cache: redis |
| 66 | |
| 67 | # ── Data ────────────────────────────────────────────────────────────────────── |
| 68 | |
| 69 | data: |
| 70 | # Primary database: postgres | mysql | sqlite |
| 71 | database: postgres |
| 72 | |
| 73 | # Run migrations on first boot |
| 74 | migrations: true |
| 75 | |
| 76 | # Load seed data on first boot |
| 77 | seed_data: true |
| 78 | |
| 79 | # ── Testing ─────────────────────────────────────────────────────────────────── |
| 80 | |
| 81 | testing: |
| 82 | # End-to-end framework: playwright | cypress | null |
| 83 | e2e: playwright |
| 84 | |
| 85 | # Unit tests |
| 86 | unit: true |
| 87 | |
| 88 | # Integration tests |
| 89 | integration: true |
| 90 | |
| 91 | # ── Template versions (auto-managed, do not edit manually) ──────────────────── |
| 92 | |
| 93 | template_versions: {} |
| --- a/boilerworks/__init__.py | ||
| +++ b/boilerworks/__init__.py | ||
| @@ -0,0 +1,3 @@ | ||
| 1 | +"""Boilerworks — production-ready project templates.""" | |
| 2 | + | |
| 3 | +__version__ = "0.1.0" |
| --- a/boilerworks/__init__.py | |
| +++ b/boilerworks/__init__.py | |
| @@ -0,0 +1,3 @@ | |
| --- a/boilerworks/__init__.py | |
| +++ b/boilerworks/__init__.py | |
| @@ -0,0 +1,3 @@ | |
| 1 | """Boilerworks — production-ready project templates.""" |
| 2 | |
| 3 | __version__ = "0.1.0" |
+43
| --- a/boilerworks/bootstrap.py | ||
| +++ b/boilerworks/bootstrap.py | ||
| @@ -0,0 +1,43 @@ | ||
| 1 | +"""Bootstrap command — infrastructure layer orchestration (v2 stub).""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +from pathlib import Path | |
| 6 | + | |
| 7 | +from rich.panel import Panel | |
| 8 | + | |
| 9 | +from boilerworks.console import console, print_info, print_warning | |
| 10 | + | |
| 11 | +_LAYERS = [ | |
| 12 | + ("1", "Foundation", "VPC, subnets, security groups, IAM roles"), | |
| 13 | + ("2", "Data", "RDS (Postgres), ElastiCache (Redis), S3 buckets"), | |
| 14 | + ("3", "Compute", "ECS/GKE cluster, service definitions, autoscaling"), | |
| 15 | + ("4", "Delivery", "Load balancer, TLS certificates, CDN, DNS"), | |
| 16 | + ("5", "Observability", "Metrics, logs, alerts, dashboards"), | |
| 17 | +] | |
| 18 | + | |
| 19 | +_V2_NOTICE = ( | |
| 20 | + "Infrastructure bootstrapping is coming in v2.\n" | |
| 21 | + "For now, follow the ops template README to provision infrastructure manually.\n\n" | |
| 22 | + "Ops template: [bold]https://github.com/ConflictHQ/boilerworks-opscode[/bold]" | |
| 23 | +) | |
| 24 | + | |
| 25 | + | |
| 26 | +def run_bootstrap(ops_dir: str | None = None, dry_run: bool = True) -> None: | |
| 27 | + """Show the infrastructure bootstrap execution plan.""" | |
| 28 | + if ops_dir: | |
| 29 | + ops_path = Path(ops_dir) | |
| 30 | + if not ops_path.exists(): | |
| 31 | + print_warning(f"Ops directory not found: {ops_path}") | |
| 32 | + else: | |
| 33 | + print_info("No --ops-dir specified; showing generic plan") | |
| 34 | + | |
| 35 | + # Build the plan panel | |
| 36 | + plan_lines = ["[bold]Execution plan — 5 layers[/bold]\n"] | |
| 37 | + for layer_num, layer_name, layer_desc in _LAYERS: | |
| 38 | + plan_lines.append(f" [cyan]{layer_num}.[/cyan] [bold]{layer_name}[/bold]") | |
| 39 | + plan_lines.append(f" {layer_desc}") | |
| 40 | + plan_lines.append("") | |
| 41 | + | |
| 42 | + console.print(Panel("\n".join(plan_lines).rstrip(), title="[bold]Bootstrap Plan[/bold]", border_style="cyan")) | |
| 43 | + console.print(Panel(_V2_NOTICE, title="[bold yellow]v1 — Stub[/bold yellow]", border_style="yellow")) |
| --- a/boilerworks/bootstrap.py | |
| +++ b/boilerworks/bootstrap.py | |
| @@ -0,0 +1,43 @@ | |
| --- a/boilerworks/bootstrap.py | |
| +++ b/boilerworks/bootstrap.py | |
| @@ -0,0 +1,43 @@ | |
| 1 | """Bootstrap command — infrastructure layer orchestration (v2 stub).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from pathlib import Path |
| 6 | |
| 7 | from rich.panel import Panel |
| 8 | |
| 9 | from boilerworks.console import console, print_info, print_warning |
| 10 | |
| 11 | _LAYERS = [ |
| 12 | ("1", "Foundation", "VPC, subnets, security groups, IAM roles"), |
| 13 | ("2", "Data", "RDS (Postgres), ElastiCache (Redis), S3 buckets"), |
| 14 | ("3", "Compute", "ECS/GKE cluster, service definitions, autoscaling"), |
| 15 | ("4", "Delivery", "Load balancer, TLS certificates, CDN, DNS"), |
| 16 | ("5", "Observability", "Metrics, logs, alerts, dashboards"), |
| 17 | ] |
| 18 | |
| 19 | _V2_NOTICE = ( |
| 20 | "Infrastructure bootstrapping is coming in v2.\n" |
| 21 | "For now, follow the ops template README to provision infrastructure manually.\n\n" |
| 22 | "Ops template: [bold]https://github.com/ConflictHQ/boilerworks-opscode[/bold]" |
| 23 | ) |
| 24 | |
| 25 | |
| 26 | def run_bootstrap(ops_dir: str | None = None, dry_run: bool = True) -> None: |
| 27 | """Show the infrastructure bootstrap execution plan.""" |
| 28 | if ops_dir: |
| 29 | ops_path = Path(ops_dir) |
| 30 | if not ops_path.exists(): |
| 31 | print_warning(f"Ops directory not found: {ops_path}") |
| 32 | else: |
| 33 | print_info("No --ops-dir specified; showing generic plan") |
| 34 | |
| 35 | # Build the plan panel |
| 36 | plan_lines = ["[bold]Execution plan — 5 layers[/bold]\n"] |
| 37 | for layer_num, layer_name, layer_desc in _LAYERS: |
| 38 | plan_lines.append(f" [cyan]{layer_num}.[/cyan] [bold]{layer_name}[/bold]") |
| 39 | plan_lines.append(f" {layer_desc}") |
| 40 | plan_lines.append("") |
| 41 | |
| 42 | console.print(Panel("\n".join(plan_lines).rstrip(), title="[bold]Bootstrap Plan[/bold]", border_style="cyan")) |
| 43 | console.print(Panel(_V2_NOTICE, title="[bold yellow]v1 — Stub[/bold yellow]", border_style="yellow")) |
+69
| --- a/boilerworks/cli.py | ||
| +++ b/boilerworks/cli.py | ||
| @@ -0,0 +1,69 @@ | ||
| 1 | +"""Boilerworks CLI — main entry point.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import click | |
| 6 | + | |
| 7 | +from boilerworks import __version__ | |
| 8 | + | |
| 9 | + | |
| 10 | +@click.group() | |
| 11 | +@click.version_option(__version__, prog_name="boilerworks") | |
| 12 | +def main() -> None: | |
| 13 | + """Boilerworks — production-ready project templates.""" | |
| 14 | + | |
| 15 | + | |
| 16 | +@main.command() | |
| 17 | +def setup() -> None: | |
| 18 | + """Interactive wizard → writes boilerworks.yaml.""" | |
| 19 | + from boilerworks.wizard import run_wizard | |
| 20 | + | |
| 21 | + run_wizard() | |
| 22 | + | |
| 23 | + | |
| 24 | +@main.command(name="init") | |
| 25 | +@click.option("--manifest", "manifest_path", default=None, help="Path to boilerworks.yaml") | |
| 26 | +@click.option("--output", "output_dir", default=".", show_default=True, help="Output directory for generated project") | |
| 27 | +@click.option("--dry-run", is_flag=True, default=False, help="Print what would happen without doing it") | |
| 28 | +def init_command(manifest_path: str | None, output_dir: str, dry_run: bool) -> None: | |
| 29 | + """Clone a template and configure it from boilerworks.yaml.""" | |
| 30 | + from boilerworks.generator import generate_from_manifest | |
| 31 | + | |
| 32 | + generate_from_manifest(manifest_path=manifest_path, output_dir=output_dir, dry_run=dry_run) | |
| 33 | + | |
| 34 | + | |
| 35 | +@main.command() | |
| 36 | +@click.option("--ops-dir", default=None, help="Path to ops directory (default: ../{project}-ops)") | |
| 37 | +@click.option("--dry-run", is_flag=True, default=True, help="Show execution plan (v1 only supports dry-run)") | |
| 38 | +def bootstrap(ops_dir: str | None, dry_run: bool) -> None: | |
| 39 | + """Show infrastructure bootstrap plan (Terraform — v2).""" | |
| 40 | + from boilerworks.bootstrap import run_bootstrap | |
| 41 | + | |
| 42 | + run_bootstrap(ops_dir=ops_dir, dry_run=dry_run) | |
| 43 | + | |
| 44 | + | |
| 45 | +@main.command(name="list") | |
| 46 | +@click.option("--size", type=click.Choice(["full", "micro", "edge"]), default=None, help="Filter by template size") | |
| 47 | +@click.option( | |
| 48 | + "--language", | |
| 49 | + type=click.Choice(["python", "typescript", "ruby", "php", "java", "go", "elixir", "rust", "svelte"]), | |
| 50 | + default=None, | |
| 51 | + help="Filter by primary language", | |
| 52 | +) | |
| 53 | +@click.option("--status", type=click.Choice(["done", "building", "planned"]), default=None, help="Filter by status") | |
| 54 | +def list_command(size: str | None, language: str | None, status: str | None) -> None: | |
| 55 | + """List all available templates.""" | |
| 56 | + from boilerworks.console import print_template_table | |
| 57 | + from boilerworks.registry import Registry | |
| 58 | + | |
| 59 | + registry = Registry() | |
| 60 | + templates = registry.list_all() | |
| 61 | + | |
| 62 | + if size: | |
| 63 | + templates = [t for t in templates if t.size == size] | |
| 64 | + if language: | |
| 65 | + templates = [t for t in templates if t.language == language] | |
| 66 | + if status: | |
| 67 | + templates = [t for t in templates if t.status == status] | |
| 68 | + | |
| 69 | + print_template_table(templates) |
| --- a/boilerworks/cli.py | |
| +++ b/boilerworks/cli.py | |
| @@ -0,0 +1,69 @@ | |
| --- a/boilerworks/cli.py | |
| +++ b/boilerworks/cli.py | |
| @@ -0,0 +1,69 @@ | |
| 1 | """Boilerworks CLI — main entry point.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import click |
| 6 | |
| 7 | from boilerworks import __version__ |
| 8 | |
| 9 | |
| 10 | @click.group() |
| 11 | @click.version_option(__version__, prog_name="boilerworks") |
| 12 | def main() -> None: |
| 13 | """Boilerworks — production-ready project templates.""" |
| 14 | |
| 15 | |
| 16 | @main.command() |
| 17 | def setup() -> None: |
| 18 | """Interactive wizard → writes boilerworks.yaml.""" |
| 19 | from boilerworks.wizard import run_wizard |
| 20 | |
| 21 | run_wizard() |
| 22 | |
| 23 | |
| 24 | @main.command(name="init") |
| 25 | @click.option("--manifest", "manifest_path", default=None, help="Path to boilerworks.yaml") |
| 26 | @click.option("--output", "output_dir", default=".", show_default=True, help="Output directory for generated project") |
| 27 | @click.option("--dry-run", is_flag=True, default=False, help="Print what would happen without doing it") |
| 28 | def init_command(manifest_path: str | None, output_dir: str, dry_run: bool) -> None: |
| 29 | """Clone a template and configure it from boilerworks.yaml.""" |
| 30 | from boilerworks.generator import generate_from_manifest |
| 31 | |
| 32 | generate_from_manifest(manifest_path=manifest_path, output_dir=output_dir, dry_run=dry_run) |
| 33 | |
| 34 | |
| 35 | @main.command() |
| 36 | @click.option("--ops-dir", default=None, help="Path to ops directory (default: ../{project}-ops)") |
| 37 | @click.option("--dry-run", is_flag=True, default=True, help="Show execution plan (v1 only supports dry-run)") |
| 38 | def bootstrap(ops_dir: str | None, dry_run: bool) -> None: |
| 39 | """Show infrastructure bootstrap plan (Terraform — v2).""" |
| 40 | from boilerworks.bootstrap import run_bootstrap |
| 41 | |
| 42 | run_bootstrap(ops_dir=ops_dir, dry_run=dry_run) |
| 43 | |
| 44 | |
| 45 | @main.command(name="list") |
| 46 | @click.option("--size", type=click.Choice(["full", "micro", "edge"]), default=None, help="Filter by template size") |
| 47 | @click.option( |
| 48 | "--language", |
| 49 | type=click.Choice(["python", "typescript", "ruby", "php", "java", "go", "elixir", "rust", "svelte"]), |
| 50 | default=None, |
| 51 | help="Filter by primary language", |
| 52 | ) |
| 53 | @click.option("--status", type=click.Choice(["done", "building", "planned"]), default=None, help="Filter by status") |
| 54 | def list_command(size: str | None, language: str | None, status: str | None) -> None: |
| 55 | """List all available templates.""" |
| 56 | from boilerworks.console import print_template_table |
| 57 | from boilerworks.registry import Registry |
| 58 | |
| 59 | registry = Registry() |
| 60 | templates = registry.list_all() |
| 61 | |
| 62 | if size: |
| 63 | templates = [t for t in templates if t.size == size] |
| 64 | if language: |
| 65 | templates = [t for t in templates if t.language == language] |
| 66 | if status: |
| 67 | templates = [t for t in templates if t.status == status] |
| 68 | |
| 69 | print_template_table(templates) |
+106
| --- a/boilerworks/console.py | ||
| +++ b/boilerworks/console.py | ||
| @@ -0,0 +1,106 @@ | ||
| 1 | +"""Rich output helpers for the Boilerworks CLI.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +from rich.console import Console | |
| 6 | +from rich.panel import Panel | |
| 7 | +from rich.table import Table | |
| 8 | +from rich.text import Text | |
| 9 | + | |
| 10 | +from boilerworks.registry import TemplateInfo | |
| 11 | + | |
| 12 | +console = Console() | |
| 13 | + | |
| 14 | + | |
| 15 | +_STATUS_STYLE: dict[str, str] = { | |
| 16 | + "done": "bold green", | |
| 17 | + "building": "bold yellow", | |
| 18 | + "planned": "dim", | |
| 19 | +} | |
| 20 | + | |
| 21 | +_SIZE_STYLE: dict[str, str] = { | |
| 22 | + "full": "cyan", | |
| 23 | + "micro": "magenta", | |
| 24 | + "edge": "blue", | |
| 25 | +} | |
| 26 | + | |
| 27 | + | |
| 28 | +def _status_badge(status: str) -> Text: | |
| 29 | + style = _STATUS_STYLE.get(status, "") | |
| 30 | + labels = {"done": "● done", "building": "◐ building", "planned": "○ planned"} | |
| 31 | + return Text(labels.get(status, status), style=style) | |
| 32 | + | |
| 33 | + | |
| 34 | +def print_template_table(templates: list[TemplateInfo]) -> None: | |
| 35 | + """Print a Rich table of templates.""" | |
| 36 | + if not templates: | |
| 37 | + console.print("[yellow]No templates match your filters.[/yellow]") | |
| 38 | + return | |
| 39 | + | |
| 40 | + table = Table( | |
| 41 | + title=f"Boilerworks Templates ({len(templates)})", | |
| 42 | + show_lines=False, | |
| 43 | + header_style="bold white", | |
| 44 | + border_style="dim", | |
| 45 | + expand=False, | |
| 46 | + ) | |
| 47 | + | |
| 48 | + table.add_column("Name", style="bold", min_width=20) | |
| 49 | + table.add_column("Size", min_width=6) | |
| 50 | + table.add_column("Language", min_width=11) | |
| 51 | + table.add_column("Backend", min_width=16) | |
| 52 | + table.add_column("Frontend", min_width=16) | |
| 53 | + table.add_column("Status", min_width=13) | |
| 54 | + table.add_column("Description", min_width=30) | |
| 55 | + | |
| 56 | + for t in templates: | |
| 57 | + size_text = Text(t.size, style=_SIZE_STYLE.get(t.size, "")) | |
| 58 | + table.add_row( | |
| 59 | + t.name, | |
| 60 | + size_text, | |
| 61 | + t.language, | |
| 62 | + t.backend, | |
| 63 | + t.frontend if t.frontend else "—", | |
| 64 | + _status_badge(t.status), | |
| 65 | + t.description, | |
| 66 | + ) | |
| 67 | + | |
| 68 | + console.print(table) | |
| 69 | + | |
| 70 | + | |
| 71 | +def print_template_detail(template: TemplateInfo) -> None: | |
| 72 | + """Print a Rich panel with full template details.""" | |
| 73 | + lines = [ | |
| 74 | + f"[bold]Name:[/bold] {template.name}", | |
| 75 | + f"[bold]Repo:[/bold] {template.repo}", | |
| 76 | + f"[bold]Size:[/bold] {template.size}", | |
| 77 | + f"[bold]Language:[/bold] {template.language}", | |
| 78 | + f"[bold]Backend:[/bold] {template.backend}", | |
| 79 | + f"[bold]Frontend:[/bold] {template.frontend or '—'}", | |
| 80 | + f"[bold]Status:[/bold] {template.status}", | |
| 81 | + f"[bold]Topologies:[/bold] {', '.join(template.topologies)}", | |
| 82 | + "", | |
| 83 | + f"[bold]Best for:[/bold] {template.best_for}", | |
| 84 | + f"[bold]Description:[/bold] {template.description}", | |
| 85 | + ] | |
| 86 | + console.print(Panel("\n".join(lines), title=f"[bold cyan]{template.name}[/bold cyan]", border_style="cyan")) | |
| 87 | + | |
| 88 | + | |
| 89 | +def print_success(message: str) -> None: | |
| 90 | + """Print a success message.""" | |
| 91 | + console.print(f"[bold green]✓[/bold green] {message}") | |
| 92 | + | |
| 93 | + | |
| 94 | +def print_error(message: str) -> None: | |
| 95 | + """Print an error message.""" | |
| 96 | + console.print(f"[bold red]✗[/bold red] {message}") | |
| 97 | + | |
| 98 | + | |
| 99 | +def print_info(message: str) -> None: | |
| 100 | + """Print an informational message.""" | |
| 101 | + console.print(f"[dim]→[/dim] {message}") | |
| 102 | + | |
| 103 | + | |
| 104 | +def print_warning(message: str) -> None: | |
| 105 | + """Print a warning message.""" | |
| 106 | + console.print(f"[bold yellow]![/bold yellow] {message}") |
| --- a/boilerworks/console.py | |
| +++ b/boilerworks/console.py | |
| @@ -0,0 +1,106 @@ | |
| --- a/boilerworks/console.py | |
| +++ b/boilerworks/console.py | |
| @@ -0,0 +1,106 @@ | |
| 1 | """Rich output helpers for the Boilerworks CLI.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from rich.console import Console |
| 6 | from rich.panel import Panel |
| 7 | from rich.table import Table |
| 8 | from rich.text import Text |
| 9 | |
| 10 | from boilerworks.registry import TemplateInfo |
| 11 | |
| 12 | console = Console() |
| 13 | |
| 14 | |
| 15 | _STATUS_STYLE: dict[str, str] = { |
| 16 | "done": "bold green", |
| 17 | "building": "bold yellow", |
| 18 | "planned": "dim", |
| 19 | } |
| 20 | |
| 21 | _SIZE_STYLE: dict[str, str] = { |
| 22 | "full": "cyan", |
| 23 | "micro": "magenta", |
| 24 | "edge": "blue", |
| 25 | } |
| 26 | |
| 27 | |
| 28 | def _status_badge(status: str) -> Text: |
| 29 | style = _STATUS_STYLE.get(status, "") |
| 30 | labels = {"done": "● done", "building": "◐ building", "planned": "○ planned"} |
| 31 | return Text(labels.get(status, status), style=style) |
| 32 | |
| 33 | |
| 34 | def print_template_table(templates: list[TemplateInfo]) -> None: |
| 35 | """Print a Rich table of templates.""" |
| 36 | if not templates: |
| 37 | console.print("[yellow]No templates match your filters.[/yellow]") |
| 38 | return |
| 39 | |
| 40 | table = Table( |
| 41 | title=f"Boilerworks Templates ({len(templates)})", |
| 42 | show_lines=False, |
| 43 | header_style="bold white", |
| 44 | border_style="dim", |
| 45 | expand=False, |
| 46 | ) |
| 47 | |
| 48 | table.add_column("Name", style="bold", min_width=20) |
| 49 | table.add_column("Size", min_width=6) |
| 50 | table.add_column("Language", min_width=11) |
| 51 | table.add_column("Backend", min_width=16) |
| 52 | table.add_column("Frontend", min_width=16) |
| 53 | table.add_column("Status", min_width=13) |
| 54 | table.add_column("Description", min_width=30) |
| 55 | |
| 56 | for t in templates: |
| 57 | size_text = Text(t.size, style=_SIZE_STYLE.get(t.size, "")) |
| 58 | table.add_row( |
| 59 | t.name, |
| 60 | size_text, |
| 61 | t.language, |
| 62 | t.backend, |
| 63 | t.frontend if t.frontend else "—", |
| 64 | _status_badge(t.status), |
| 65 | t.description, |
| 66 | ) |
| 67 | |
| 68 | console.print(table) |
| 69 | |
| 70 | |
| 71 | def print_template_detail(template: TemplateInfo) -> None: |
| 72 | """Print a Rich panel with full template details.""" |
| 73 | lines = [ |
| 74 | f"[bold]Name:[/bold] {template.name}", |
| 75 | f"[bold]Repo:[/bold] {template.repo}", |
| 76 | f"[bold]Size:[/bold] {template.size}", |
| 77 | f"[bold]Language:[/bold] {template.language}", |
| 78 | f"[bold]Backend:[/bold] {template.backend}", |
| 79 | f"[bold]Frontend:[/bold] {template.frontend or '—'}", |
| 80 | f"[bold]Status:[/bold] {template.status}", |
| 81 | f"[bold]Topologies:[/bold] {', '.join(template.topologies)}", |
| 82 | "", |
| 83 | f"[bold]Best for:[/bold] {template.best_for}", |
| 84 | f"[bold]Description:[/bold] {template.description}", |
| 85 | ] |
| 86 | console.print(Panel("\n".join(lines), title=f"[bold cyan]{template.name}[/bold cyan]", border_style="cyan")) |
| 87 | |
| 88 | |
| 89 | def print_success(message: str) -> None: |
| 90 | """Print a success message.""" |
| 91 | console.print(f"[bold green]✓[/bold green] {message}") |
| 92 | |
| 93 | |
| 94 | def print_error(message: str) -> None: |
| 95 | """Print an error message.""" |
| 96 | console.print(f"[bold red]✗[/bold red] {message}") |
| 97 | |
| 98 | |
| 99 | def print_info(message: str) -> None: |
| 100 | """Print an informational message.""" |
| 101 | console.print(f"[dim]→[/dim] {message}") |
| 102 | |
| 103 | |
| 104 | def print_warning(message: str) -> None: |
| 105 | """Print a warning message.""" |
| 106 | console.print(f"[bold yellow]![/bold yellow] {message}") |
+354
| --- a/boilerworks/generator.py | ||
| +++ b/boilerworks/generator.py | ||
| @@ -0,0 +1,354 @@ | ||
| 1 | +"""Project generator — clone → render → rename → git init.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import shutil | |
| 6 | +import subprocess | |
| 7 | +import sys | |
| 8 | +from pathlib import Path | |
| 9 | + | |
| 10 | +from rich.progress import Progress, SpinnerColumn, TextColumn | |
| 11 | + | |
| 12 | +from boilerworks.console import console, print_error, print_info, print_success | |
| 13 | +from boilerworks.manifest import BoilerworksManifest | |
| 14 | +from boilerworks.registry import Registry | |
| 15 | +from boilerworks.renderer import build_replacements, rename_boilerworks_paths, render_directory | |
| 16 | + | |
| 17 | +_OPS_REPO = "ConflictHQ/boilerworks-opscode" | |
| 18 | + | |
| 19 | +_NEXT_STEPS_TEMPLATE = """[bold]Project created at:[/bold] {project_dir} | |
| 20 | + | |
| 21 | +[bold]Next steps:[/bold] | |
| 22 | + cd {project} | |
| 23 | + docker compose up -d | |
| 24 | + # Visit http://localhost:3000 | |
| 25 | + | |
| 26 | +[bold]Documentation:[/bold] | |
| 27 | + bootstrap.md — conventions | |
| 28 | + CLAUDE.md — AI agent guide | |
| 29 | +""" | |
| 30 | + | |
| 31 | +_NEXT_STEPS_OPS_STANDARD = """[bold]Infrastructure repo:[/bold] {ops_dir} | |
| 32 | + | |
| 33 | +[bold]Infrastructure next steps:[/bold] | |
| 34 | + cd {ops_name} | |
| 35 | + # Edit {cloud}/config.env to review settings | |
| 36 | + ./run.sh bootstrap {cloud} dev | |
| 37 | + ./run.sh plan {cloud} dev | |
| 38 | + | |
| 39 | +[bold]Documentation:[/bold] | |
| 40 | + bootstrap.md — Terraform conventions and setup | |
| 41 | +""" | |
| 42 | + | |
| 43 | +_NEXT_STEPS_OPS_OMNI = """[bold]Infrastructure (ops/) is inside the app repo[/bold] | |
| 44 | + | |
| 45 | +[bold]Infrastructure next steps:[/bold] | |
| 46 | + cd {project}/ops | |
| 47 | + # Edit {cloud}/config.env to review settings | |
| 48 | + ./run.sh bootstrap {cloud} dev | |
| 49 | + ./run.sh plan {cloud} dev | |
| 50 | + | |
| 51 | +[bold]Documentation:[/bold] | |
| 52 | + ops/bootstrap.md — Terraform conventions and setup | |
| 53 | +""" | |
| 54 | + | |
| 55 | + | |
| 56 | +def _clone_repo(repo: str, dest: Path) -> None: | |
| 57 | + """Clone repo to dest. Tries SSH first, falls back to HTTPS.""" | |
| 58 | + ssh_url = f"[email protected]:{repo}.git" | |
| 59 | + https_url = f"https://github.com/{repo}.git" | |
| 60 | + | |
| 61 | + result = subprocess.run( | |
| 62 | + ["git", "clone", "--depth", "1", ssh_url, str(dest)], | |
| 63 | + capture_output=True, | |
| 64 | + text=True, | |
| 65 | + ) | |
| 66 | + if result.returncode != 0: | |
| 67 | + # SSH failed — try HTTPS | |
| 68 | + result = subprocess.run( | |
| 69 | + ["git", "clone", "--depth", "1", https_url, str(dest)], | |
| 70 | + capture_output=True, | |
| 71 | + text=True, | |
| 72 | + ) | |
| 73 | + if result.returncode != 0: | |
| 74 | + raise RuntimeError( | |
| 75 | + f"Failed to clone {repo}.\n" | |
| 76 | + f"SSH error: {result.stderr.strip()}\n" | |
| 77 | + "Ensure you have GitHub access (SSH key or gh auth login)." | |
| 78 | + ) | |
| 79 | + | |
| 80 | + | |
| 81 | +def _remove_git_dir(project_dir: Path) -> None: | |
| 82 | + git_dir = project_dir / ".git" | |
| 83 | + if git_dir.exists(): | |
| 84 | + shutil.rmtree(git_dir) | |
| 85 | + | |
| 86 | + | |
| 87 | +def _git_init(project_dir: Path, family: str) -> None: | |
| 88 | + """Initialise a fresh git repo and make the initial commit.""" | |
| 89 | + subprocess.run(["git", "init"], cwd=project_dir, capture_output=True) | |
| 90 | + subprocess.run(["git", "add", "."], cwd=project_dir, capture_output=True) | |
| 91 | + subprocess.run( | |
| 92 | + ["git", "commit", "-m", f"Initial project from boilerworks-{family}"], | |
| 93 | + cwd=project_dir, | |
| 94 | + capture_output=True, | |
| 95 | + ) | |
| 96 | + | |
| 97 | + | |
| 98 | +def _git_add_commit(project_dir: Path, message: str) -> None: | |
| 99 | + """Stage all and commit in an existing repo.""" | |
| 100 | + subprocess.run(["git", "add", "."], cwd=project_dir, capture_output=True) | |
| 101 | + subprocess.run( | |
| 102 | + ["git", "commit", "-m", message], | |
| 103 | + cwd=project_dir, | |
| 104 | + capture_output=True, | |
| 105 | + ) | |
| 106 | + | |
| 107 | + | |
| 108 | +def _write_ops_config(ops_dir: Path, cloud: str, project: str, region: str | None, domain: str | None) -> None: | |
| 109 | + """Write {cloud}/config.env with project-specific values.""" | |
| 110 | + config_path = ops_dir / cloud / "config.env" | |
| 111 | + if not config_path.exists(): | |
| 112 | + return | |
| 113 | + | |
| 114 | + region_default = {"aws": "us-east-1", "gcp": "us-central1", "azure": "eastus"}.get(cloud, "us-east-1") | |
| 115 | + effective_region = region or region_default | |
| 116 | + | |
| 117 | + lines = [ | |
| 118 | + "# -----------------------------------------------------------------------------", | |
| 119 | + f"# {project} — {cloud.upper()} Configuration", | |
| 120 | + "#", | |
| 121 | + "# Shared configuration for all environments. Sourced by run.sh and bootstrap.sh.", | |
| 122 | + "# Review and update before running bootstrap.", | |
| 123 | + "# -----------------------------------------------------------------------------", | |
| 124 | + "", | |
| 125 | + "# Project slug — used in all resource naming", | |
| 126 | + f'PROJECT="{project}"', | |
| 127 | + "", | |
| 128 | + f"# {cloud.upper()} region", | |
| 129 | + ] | |
| 130 | + | |
| 131 | + if cloud == "aws": | |
| 132 | + lines.append(f'AWS_REGION="{effective_region}"') | |
| 133 | + elif cloud == "gcp": | |
| 134 | + lines.append(f'GCP_REGION="{effective_region}"') | |
| 135 | + elif cloud == "azure": | |
| 136 | + lines.append(f'AZURE_REGION="{effective_region}"') | |
| 137 | + | |
| 138 | + lines += [ | |
| 139 | + "", | |
| 140 | + "# Owner tag", | |
| 141 | + f'OWNER="{project}"', | |
| 142 | + ] | |
| 143 | + | |
| 144 | + if domain: | |
| 145 | + lines += [ | |
| 146 | + "", | |
| 147 | + "# Domain", | |
| 148 | + f'DOMAIN="{domain}"', | |
| 149 | + ] | |
| 150 | + | |
| 151 | + config_path.write_text("\n".join(lines) + "\n", encoding="utf-8") | |
| 152 | + | |
| 153 | + | |
| 154 | +def _clone_and_render_ops( | |
| 155 | + project: str, | |
| 156 | + cloud: str, | |
| 157 | + region: str | None, | |
| 158 | + domain: str | None, | |
| 159 | + dest: Path, | |
| 160 | + progress: Progress, | |
| 161 | +) -> None: | |
| 162 | + """Clone boilerworks-opscode, render, rename, and configure for this project.""" | |
| 163 | + task = progress.add_task(f"Cloning {_OPS_REPO}…", total=None) | |
| 164 | + try: | |
| 165 | + _clone_repo(_OPS_REPO, dest) | |
| 166 | + except RuntimeError as exc: | |
| 167 | + progress.stop() | |
| 168 | + print_error(str(exc)) | |
| 169 | + sys.exit(1) | |
| 170 | + progress.remove_task(task) | |
| 171 | + | |
| 172 | + task = progress.add_task("Removing .git/ from ops…", total=None) | |
| 173 | + _remove_git_dir(dest) | |
| 174 | + progress.remove_task(task) | |
| 175 | + | |
| 176 | + task = progress.add_task("Applying ops substitutions…", total=None) | |
| 177 | + replacements = build_replacements(project) | |
| 178 | + render_directory(dest, replacements) | |
| 179 | + progress.remove_task(task) | |
| 180 | + | |
| 181 | + task = progress.add_task("Renaming ops paths…", total=None) | |
| 182 | + rename_boilerworks_paths(dest, project) | |
| 183 | + progress.remove_task(task) | |
| 184 | + | |
| 185 | + task = progress.add_task("Configuring ops for your project…", total=None) | |
| 186 | + _write_ops_config(dest, cloud, project, region, domain) | |
| 187 | + progress.remove_task(task) | |
| 188 | + | |
| 189 | + | |
| 190 | +def _dry_run_plan(manifest: BoilerworksManifest, output_dir: Path) -> None: | |
| 191 | + """Print what would happen without doing it.""" | |
| 192 | + registry = Registry() | |
| 193 | + template = registry.get_by_name(manifest.family) | |
| 194 | + repo = template.repo if template else f"ConflictHQ/boilerworks-{manifest.family}" | |
| 195 | + project_dir = output_dir / manifest.project | |
| 196 | + | |
| 197 | + console.print("\n[bold]Dry run — no files will be written[/bold]\n") | |
| 198 | + steps = [ | |
| 199 | + f"[dim]1.[/dim] Clone [cyan]{repo}[/cyan]", | |
| 200 | + "[dim]2.[/dim] Remove .git/ from cloned directory", | |
| 201 | + f"[dim]3.[/dim] Replace all 'boilerworks' → '[bold]{manifest.project}[/bold]' (case-variant)", | |
| 202 | + "[dim]4.[/dim] Rename files/dirs containing 'boilerworks'", | |
| 203 | + "[dim]5.[/dim] Update CLAUDE.md and README.md headers", | |
| 204 | + f"[dim]6.[/dim] git init + initial commit in [bold]{project_dir}[/bold]", | |
| 205 | + ] | |
| 206 | + if manifest.ops and manifest.cloud: | |
| 207 | + ops_dest = f"{project_dir}/ops/" if manifest.topology == "omni" else str(output_dir / f"{manifest.project}-ops") | |
| 208 | + steps += [ | |
| 209 | + f"[dim]7.[/dim] Clone [cyan]{_OPS_REPO}[/cyan] → [bold]{ops_dest}[/bold]", | |
| 210 | + f"[dim]8.[/dim] Render + rename ops files (boilerworks → {manifest.project})", | |
| 211 | + f"[dim]9.[/dim] Write [cyan]{manifest.cloud}/config.env[/cyan] (project, region, domain)", | |
| 212 | + ] | |
| 213 | + if manifest.topology == "omni": | |
| 214 | + steps.append("[dim]10.[/dim] Recommit app repo to include ops/") | |
| 215 | + else: | |
| 216 | + steps.append(f"[dim]10.[/dim] git init ops repo in [bold]{ops_dest}[/bold]") | |
| 217 | + | |
| 218 | + if manifest.mobile: | |
| 219 | + steps.append(f"[dim]{len(steps) + 1}.[/dim] Clone mobile template") | |
| 220 | + | |
| 221 | + for step in steps: | |
| 222 | + console.print(f" {step}") | |
| 223 | + console.print(f"\n[dim]Output directory:[/dim] {project_dir}") | |
| 224 | + | |
| 225 | + | |
| 226 | +def generate_from_manifest( | |
| 227 | + manifest_path: str | None, | |
| 228 | + output_dir: str = ".", | |
| 229 | + dry_run: bool = False, | |
| 230 | +) -> None: | |
| 231 | + """Entry point called from the CLI.""" | |
| 232 | + manifest_file = Path(manifest_path) if manifest_path else Path("boilerworks.yaml") | |
| 233 | + | |
| 234 | + if not manifest_file.exists(): | |
| 235 | + print_error(f"Manifest not found: {manifest_file}") | |
| 236 | + print_info("Run [bold]boilerworks setup[/bold] first to create boilerworks.yaml") | |
| 237 | + sys.exit(1) | |
| 238 | + | |
| 239 | + try: | |
| 240 | + manifest = BoilerworksManifest.from_file(manifest_file) | |
| 241 | + except Exception as exc: | |
| 242 | + print_error(f"Invalid manifest: {exc}") | |
| 243 | + sys.exit(1) | |
| 244 | + | |
| 245 | + out = Path(output_dir).resolve() | |
| 246 | + | |
| 247 | + if dry_run: | |
| 248 | + _dry_run_plan(manifest, out) | |
| 249 | + return | |
| 250 | + | |
| 251 | + _generate(manifest, out) | |
| 252 | + | |
| 253 | + | |
| 254 | +def _generate(manifest: BoilerworksManifest, output_dir: Path) -> None: | |
| 255 | + registry = Registry() | |
| 256 | + template = registry.get_by_name(manifest.family) | |
| 257 | + if template is None: | |
| 258 | + print_error(f"Unknown template family: {manifest.family}") | |
| 259 | + sys.exit(1) | |
| 260 | + | |
| 261 | + project_dir = output_dir / manifest.project | |
| 262 | + | |
| 263 | + if project_dir.exists(): | |
| 264 | + print_error(f"Directory already exists: {project_dir}") | |
| 265 | + print_info("Delete it first or choose a different output directory.") | |
| 266 | + sys.exit(1) | |
| 267 | + | |
| 268 | + replacements = build_replacements(manifest.project) | |
| 269 | + | |
| 270 | + with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True) as progress: | |
| 271 | + # ── App template ────────────────────────────────────────────────────── | |
| 272 | + task = progress.add_task(f"Cloning {template.repo}…", total=None) | |
| 273 | + try: | |
| 274 | + _clone_repo(template.repo, project_dir) | |
| 275 | + except RuntimeError as exc: | |
| 276 | + progress.stop() | |
| 277 | + print_error(str(exc)) | |
| 278 | + sys.exit(1) | |
| 279 | + progress.remove_task(task) | |
| 280 | + | |
| 281 | + task = progress.add_task("Removing .git/…", total=None) | |
| 282 | + _remove_git_dir(project_dir) | |
| 283 | + progress.remove_task(task) | |
| 284 | + | |
| 285 | + task = progress.add_task("Applying template substitutions…", total=None) | |
| 286 | + render_directory(project_dir, replacements) | |
| 287 | + progress.remove_task(task) | |
| 288 | + | |
| 289 | + task = progress.add_task("Renaming paths…", total=None) | |
| 290 | + rename_boilerworks_paths(project_dir, manifest.project) | |
| 291 | + progress.remove_task(task) | |
| 292 | + | |
| 293 | + # ── Ops (infra-as-code) ─────────────────────────────────────────────── | |
| 294 | + ops_dir: Path | None = None | |
| 295 | + if manifest.ops and manifest.cloud: | |
| 296 | + if manifest.topology == "omni": | |
| 297 | + # Ops lives inside the app repo | |
| 298 | + ops_dir = project_dir / "ops" | |
| 299 | + _clone_and_render_ops( | |
| 300 | + manifest.project, manifest.cloud, manifest.region, manifest.domain, ops_dir, progress | |
| 301 | + ) | |
| 302 | + # git init + commit includes ops/ | |
| 303 | + task = progress.add_task("Initialising git repository (app + ops)…", total=None) | |
| 304 | + _git_init(project_dir, manifest.family) | |
| 305 | + progress.remove_task(task) | |
| 306 | + else: | |
| 307 | + # Standard: app and ops are sibling repos | |
| 308 | + task = progress.add_task("Initialising app git repository…", total=None) | |
| 309 | + _git_init(project_dir, manifest.family) | |
| 310 | + progress.remove_task(task) | |
| 311 | + | |
| 312 | + ops_dir = output_dir / f"{manifest.project}-ops" | |
| 313 | + if ops_dir.exists(): | |
| 314 | + print_error(f"Ops directory already exists: {ops_dir}") | |
| 315 | + print_info("Delete it first or choose a different output directory.") | |
| 316 | + sys.exit(1) | |
| 317 | + _clone_and_render_ops( | |
| 318 | + manifest.project, manifest.cloud, manifest.region, manifest.domain, ops_dir, progress | |
| 319 | + ) | |
| 320 | + task = progress.add_task("Initialising ops git repository…", total=None) | |
| 321 | + _git_init(ops_dir, "opscode") | |
| 322 | + progress.remove_task(task) | |
| 323 | + else: | |
| 324 | + # No ops — just init app | |
| 325 | + task = progress.add_task("Initialising git repository…", total=None) | |
| 326 | + _git_init(project_dir, manifest.family) | |
| 327 | + progress.remove_task(task) | |
| 328 | + | |
| 329 | + print_success(f"Project [bold]{manifest.project}[/bold] created at [bold]{project_dir}[/bold]") | |
| 330 | + | |
| 331 | + from rich.panel import Panel | |
| 332 | + | |
| 333 | + next_steps = _NEXT_STEPS_TEMPLATE.format(project=manifest.project, project_dir=project_dir) | |
| 334 | + | |
| 335 | + if manifest.ops and manifest.cloud and ops_dir is not None: | |
| 336 | + if manifest.topology == "omni": | |
| 337 | + next_steps += _NEXT_STEPS_OPS_OMNI.format( | |
| 338 | + project=manifest.project, | |
| 339 | + cloud=manifest.cloud, | |
| 340 | + ) | |
| 341 | + else: | |
| 342 | + next_steps += _NEXT_STEPS_OPS_STANDARD.format( | |
| 343 | + ops_dir=ops_dir, | |
| 344 | + ops_name=f"{manifest.project}-ops", | |
| 345 | + cloud=manifest.cloud, | |
| 346 | + ) | |
| 347 | + | |
| 348 | + console.print( | |
| 349 | + Panel( | |
| 350 | + next_steps.strip(), | |
| 351 | + title="[bold green]Done![/bold green]", | |
| 352 | + border_style="green", | |
| 353 | + ) | |
| 354 | + ) |
| --- a/boilerworks/generator.py | |
| +++ b/boilerworks/generator.py | |
| @@ -0,0 +1,354 @@ | |
| --- a/boilerworks/generator.py | |
| +++ b/boilerworks/generator.py | |
| @@ -0,0 +1,354 @@ | |
| 1 | """Project generator — clone → render → rename → git init.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import shutil |
| 6 | import subprocess |
| 7 | import sys |
| 8 | from pathlib import Path |
| 9 | |
| 10 | from rich.progress import Progress, SpinnerColumn, TextColumn |
| 11 | |
| 12 | from boilerworks.console import console, print_error, print_info, print_success |
| 13 | from boilerworks.manifest import BoilerworksManifest |
| 14 | from boilerworks.registry import Registry |
| 15 | from boilerworks.renderer import build_replacements, rename_boilerworks_paths, render_directory |
| 16 | |
| 17 | _OPS_REPO = "ConflictHQ/boilerworks-opscode" |
| 18 | |
| 19 | _NEXT_STEPS_TEMPLATE = """[bold]Project created at:[/bold] {project_dir} |
| 20 | |
| 21 | [bold]Next steps:[/bold] |
| 22 | cd {project} |
| 23 | docker compose up -d |
| 24 | # Visit http://localhost:3000 |
| 25 | |
| 26 | [bold]Documentation:[/bold] |
| 27 | bootstrap.md — conventions |
| 28 | CLAUDE.md — AI agent guide |
| 29 | """ |
| 30 | |
| 31 | _NEXT_STEPS_OPS_STANDARD = """[bold]Infrastructure repo:[/bold] {ops_dir} |
| 32 | |
| 33 | [bold]Infrastructure next steps:[/bold] |
| 34 | cd {ops_name} |
| 35 | # Edit {cloud}/config.env to review settings |
| 36 | ./run.sh bootstrap {cloud} dev |
| 37 | ./run.sh plan {cloud} dev |
| 38 | |
| 39 | [bold]Documentation:[/bold] |
| 40 | bootstrap.md — Terraform conventions and setup |
| 41 | """ |
| 42 | |
| 43 | _NEXT_STEPS_OPS_OMNI = """[bold]Infrastructure (ops/) is inside the app repo[/bold] |
| 44 | |
| 45 | [bold]Infrastructure next steps:[/bold] |
| 46 | cd {project}/ops |
| 47 | # Edit {cloud}/config.env to review settings |
| 48 | ./run.sh bootstrap {cloud} dev |
| 49 | ./run.sh plan {cloud} dev |
| 50 | |
| 51 | [bold]Documentation:[/bold] |
| 52 | ops/bootstrap.md — Terraform conventions and setup |
| 53 | """ |
| 54 | |
| 55 | |
| 56 | def _clone_repo(repo: str, dest: Path) -> None: |
| 57 | """Clone repo to dest. Tries SSH first, falls back to HTTPS.""" |
| 58 | ssh_url = f"[email protected]:{repo}.git" |
| 59 | https_url = f"https://github.com/{repo}.git" |
| 60 | |
| 61 | result = subprocess.run( |
| 62 | ["git", "clone", "--depth", "1", ssh_url, str(dest)], |
| 63 | capture_output=True, |
| 64 | text=True, |
| 65 | ) |
| 66 | if result.returncode != 0: |
| 67 | # SSH failed — try HTTPS |
| 68 | result = subprocess.run( |
| 69 | ["git", "clone", "--depth", "1", https_url, str(dest)], |
| 70 | capture_output=True, |
| 71 | text=True, |
| 72 | ) |
| 73 | if result.returncode != 0: |
| 74 | raise RuntimeError( |
| 75 | f"Failed to clone {repo}.\n" |
| 76 | f"SSH error: {result.stderr.strip()}\n" |
| 77 | "Ensure you have GitHub access (SSH key or gh auth login)." |
| 78 | ) |
| 79 | |
| 80 | |
| 81 | def _remove_git_dir(project_dir: Path) -> None: |
| 82 | git_dir = project_dir / ".git" |
| 83 | if git_dir.exists(): |
| 84 | shutil.rmtree(git_dir) |
| 85 | |
| 86 | |
| 87 | def _git_init(project_dir: Path, family: str) -> None: |
| 88 | """Initialise a fresh git repo and make the initial commit.""" |
| 89 | subprocess.run(["git", "init"], cwd=project_dir, capture_output=True) |
| 90 | subprocess.run(["git", "add", "."], cwd=project_dir, capture_output=True) |
| 91 | subprocess.run( |
| 92 | ["git", "commit", "-m", f"Initial project from boilerworks-{family}"], |
| 93 | cwd=project_dir, |
| 94 | capture_output=True, |
| 95 | ) |
| 96 | |
| 97 | |
| 98 | def _git_add_commit(project_dir: Path, message: str) -> None: |
| 99 | """Stage all and commit in an existing repo.""" |
| 100 | subprocess.run(["git", "add", "."], cwd=project_dir, capture_output=True) |
| 101 | subprocess.run( |
| 102 | ["git", "commit", "-m", message], |
| 103 | cwd=project_dir, |
| 104 | capture_output=True, |
| 105 | ) |
| 106 | |
| 107 | |
| 108 | def _write_ops_config(ops_dir: Path, cloud: str, project: str, region: str | None, domain: str | None) -> None: |
| 109 | """Write {cloud}/config.env with project-specific values.""" |
| 110 | config_path = ops_dir / cloud / "config.env" |
| 111 | if not config_path.exists(): |
| 112 | return |
| 113 | |
| 114 | region_default = {"aws": "us-east-1", "gcp": "us-central1", "azure": "eastus"}.get(cloud, "us-east-1") |
| 115 | effective_region = region or region_default |
| 116 | |
| 117 | lines = [ |
| 118 | "# -----------------------------------------------------------------------------", |
| 119 | f"# {project} — {cloud.upper()} Configuration", |
| 120 | "#", |
| 121 | "# Shared configuration for all environments. Sourced by run.sh and bootstrap.sh.", |
| 122 | "# Review and update before running bootstrap.", |
| 123 | "# -----------------------------------------------------------------------------", |
| 124 | "", |
| 125 | "# Project slug — used in all resource naming", |
| 126 | f'PROJECT="{project}"', |
| 127 | "", |
| 128 | f"# {cloud.upper()} region", |
| 129 | ] |
| 130 | |
| 131 | if cloud == "aws": |
| 132 | lines.append(f'AWS_REGION="{effective_region}"') |
| 133 | elif cloud == "gcp": |
| 134 | lines.append(f'GCP_REGION="{effective_region}"') |
| 135 | elif cloud == "azure": |
| 136 | lines.append(f'AZURE_REGION="{effective_region}"') |
| 137 | |
| 138 | lines += [ |
| 139 | "", |
| 140 | "# Owner tag", |
| 141 | f'OWNER="{project}"', |
| 142 | ] |
| 143 | |
| 144 | if domain: |
| 145 | lines += [ |
| 146 | "", |
| 147 | "# Domain", |
| 148 | f'DOMAIN="{domain}"', |
| 149 | ] |
| 150 | |
| 151 | config_path.write_text("\n".join(lines) + "\n", encoding="utf-8") |
| 152 | |
| 153 | |
| 154 | def _clone_and_render_ops( |
| 155 | project: str, |
| 156 | cloud: str, |
| 157 | region: str | None, |
| 158 | domain: str | None, |
| 159 | dest: Path, |
| 160 | progress: Progress, |
| 161 | ) -> None: |
| 162 | """Clone boilerworks-opscode, render, rename, and configure for this project.""" |
| 163 | task = progress.add_task(f"Cloning {_OPS_REPO}…", total=None) |
| 164 | try: |
| 165 | _clone_repo(_OPS_REPO, dest) |
| 166 | except RuntimeError as exc: |
| 167 | progress.stop() |
| 168 | print_error(str(exc)) |
| 169 | sys.exit(1) |
| 170 | progress.remove_task(task) |
| 171 | |
| 172 | task = progress.add_task("Removing .git/ from ops…", total=None) |
| 173 | _remove_git_dir(dest) |
| 174 | progress.remove_task(task) |
| 175 | |
| 176 | task = progress.add_task("Applying ops substitutions…", total=None) |
| 177 | replacements = build_replacements(project) |
| 178 | render_directory(dest, replacements) |
| 179 | progress.remove_task(task) |
| 180 | |
| 181 | task = progress.add_task("Renaming ops paths…", total=None) |
| 182 | rename_boilerworks_paths(dest, project) |
| 183 | progress.remove_task(task) |
| 184 | |
| 185 | task = progress.add_task("Configuring ops for your project…", total=None) |
| 186 | _write_ops_config(dest, cloud, project, region, domain) |
| 187 | progress.remove_task(task) |
| 188 | |
| 189 | |
| 190 | def _dry_run_plan(manifest: BoilerworksManifest, output_dir: Path) -> None: |
| 191 | """Print what would happen without doing it.""" |
| 192 | registry = Registry() |
| 193 | template = registry.get_by_name(manifest.family) |
| 194 | repo = template.repo if template else f"ConflictHQ/boilerworks-{manifest.family}" |
| 195 | project_dir = output_dir / manifest.project |
| 196 | |
| 197 | console.print("\n[bold]Dry run — no files will be written[/bold]\n") |
| 198 | steps = [ |
| 199 | f"[dim]1.[/dim] Clone [cyan]{repo}[/cyan]", |
| 200 | "[dim]2.[/dim] Remove .git/ from cloned directory", |
| 201 | f"[dim]3.[/dim] Replace all 'boilerworks' → '[bold]{manifest.project}[/bold]' (case-variant)", |
| 202 | "[dim]4.[/dim] Rename files/dirs containing 'boilerworks'", |
| 203 | "[dim]5.[/dim] Update CLAUDE.md and README.md headers", |
| 204 | f"[dim]6.[/dim] git init + initial commit in [bold]{project_dir}[/bold]", |
| 205 | ] |
| 206 | if manifest.ops and manifest.cloud: |
| 207 | ops_dest = f"{project_dir}/ops/" if manifest.topology == "omni" else str(output_dir / f"{manifest.project}-ops") |
| 208 | steps += [ |
| 209 | f"[dim]7.[/dim] Clone [cyan]{_OPS_REPO}[/cyan] → [bold]{ops_dest}[/bold]", |
| 210 | f"[dim]8.[/dim] Render + rename ops files (boilerworks → {manifest.project})", |
| 211 | f"[dim]9.[/dim] Write [cyan]{manifest.cloud}/config.env[/cyan] (project, region, domain)", |
| 212 | ] |
| 213 | if manifest.topology == "omni": |
| 214 | steps.append("[dim]10.[/dim] Recommit app repo to include ops/") |
| 215 | else: |
| 216 | steps.append(f"[dim]10.[/dim] git init ops repo in [bold]{ops_dest}[/bold]") |
| 217 | |
| 218 | if manifest.mobile: |
| 219 | steps.append(f"[dim]{len(steps) + 1}.[/dim] Clone mobile template") |
| 220 | |
| 221 | for step in steps: |
| 222 | console.print(f" {step}") |
| 223 | console.print(f"\n[dim]Output directory:[/dim] {project_dir}") |
| 224 | |
| 225 | |
| 226 | def generate_from_manifest( |
| 227 | manifest_path: str | None, |
| 228 | output_dir: str = ".", |
| 229 | dry_run: bool = False, |
| 230 | ) -> None: |
| 231 | """Entry point called from the CLI.""" |
| 232 | manifest_file = Path(manifest_path) if manifest_path else Path("boilerworks.yaml") |
| 233 | |
| 234 | if not manifest_file.exists(): |
| 235 | print_error(f"Manifest not found: {manifest_file}") |
| 236 | print_info("Run [bold]boilerworks setup[/bold] first to create boilerworks.yaml") |
| 237 | sys.exit(1) |
| 238 | |
| 239 | try: |
| 240 | manifest = BoilerworksManifest.from_file(manifest_file) |
| 241 | except Exception as exc: |
| 242 | print_error(f"Invalid manifest: {exc}") |
| 243 | sys.exit(1) |
| 244 | |
| 245 | out = Path(output_dir).resolve() |
| 246 | |
| 247 | if dry_run: |
| 248 | _dry_run_plan(manifest, out) |
| 249 | return |
| 250 | |
| 251 | _generate(manifest, out) |
| 252 | |
| 253 | |
| 254 | def _generate(manifest: BoilerworksManifest, output_dir: Path) -> None: |
| 255 | registry = Registry() |
| 256 | template = registry.get_by_name(manifest.family) |
| 257 | if template is None: |
| 258 | print_error(f"Unknown template family: {manifest.family}") |
| 259 | sys.exit(1) |
| 260 | |
| 261 | project_dir = output_dir / manifest.project |
| 262 | |
| 263 | if project_dir.exists(): |
| 264 | print_error(f"Directory already exists: {project_dir}") |
| 265 | print_info("Delete it first or choose a different output directory.") |
| 266 | sys.exit(1) |
| 267 | |
| 268 | replacements = build_replacements(manifest.project) |
| 269 | |
| 270 | with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True) as progress: |
| 271 | # ── App template ────────────────────────────────────────────────────── |
| 272 | task = progress.add_task(f"Cloning {template.repo}…", total=None) |
| 273 | try: |
| 274 | _clone_repo(template.repo, project_dir) |
| 275 | except RuntimeError as exc: |
| 276 | progress.stop() |
| 277 | print_error(str(exc)) |
| 278 | sys.exit(1) |
| 279 | progress.remove_task(task) |
| 280 | |
| 281 | task = progress.add_task("Removing .git/…", total=None) |
| 282 | _remove_git_dir(project_dir) |
| 283 | progress.remove_task(task) |
| 284 | |
| 285 | task = progress.add_task("Applying template substitutions…", total=None) |
| 286 | render_directory(project_dir, replacements) |
| 287 | progress.remove_task(task) |
| 288 | |
| 289 | task = progress.add_task("Renaming paths…", total=None) |
| 290 | rename_boilerworks_paths(project_dir, manifest.project) |
| 291 | progress.remove_task(task) |
| 292 | |
| 293 | # ── Ops (infra-as-code) ─────────────────────────────────────────────── |
| 294 | ops_dir: Path | None = None |
| 295 | if manifest.ops and manifest.cloud: |
| 296 | if manifest.topology == "omni": |
| 297 | # Ops lives inside the app repo |
| 298 | ops_dir = project_dir / "ops" |
| 299 | _clone_and_render_ops( |
| 300 | manifest.project, manifest.cloud, manifest.region, manifest.domain, ops_dir, progress |
| 301 | ) |
| 302 | # git init + commit includes ops/ |
| 303 | task = progress.add_task("Initialising git repository (app + ops)…", total=None) |
| 304 | _git_init(project_dir, manifest.family) |
| 305 | progress.remove_task(task) |
| 306 | else: |
| 307 | # Standard: app and ops are sibling repos |
| 308 | task = progress.add_task("Initialising app git repository…", total=None) |
| 309 | _git_init(project_dir, manifest.family) |
| 310 | progress.remove_task(task) |
| 311 | |
| 312 | ops_dir = output_dir / f"{manifest.project}-ops" |
| 313 | if ops_dir.exists(): |
| 314 | print_error(f"Ops directory already exists: {ops_dir}") |
| 315 | print_info("Delete it first or choose a different output directory.") |
| 316 | sys.exit(1) |
| 317 | _clone_and_render_ops( |
| 318 | manifest.project, manifest.cloud, manifest.region, manifest.domain, ops_dir, progress |
| 319 | ) |
| 320 | task = progress.add_task("Initialising ops git repository…", total=None) |
| 321 | _git_init(ops_dir, "opscode") |
| 322 | progress.remove_task(task) |
| 323 | else: |
| 324 | # No ops — just init app |
| 325 | task = progress.add_task("Initialising git repository…", total=None) |
| 326 | _git_init(project_dir, manifest.family) |
| 327 | progress.remove_task(task) |
| 328 | |
| 329 | print_success(f"Project [bold]{manifest.project}[/bold] created at [bold]{project_dir}[/bold]") |
| 330 | |
| 331 | from rich.panel import Panel |
| 332 | |
| 333 | next_steps = _NEXT_STEPS_TEMPLATE.format(project=manifest.project, project_dir=project_dir) |
| 334 | |
| 335 | if manifest.ops and manifest.cloud and ops_dir is not None: |
| 336 | if manifest.topology == "omni": |
| 337 | next_steps += _NEXT_STEPS_OPS_OMNI.format( |
| 338 | project=manifest.project, |
| 339 | cloud=manifest.cloud, |
| 340 | ) |
| 341 | else: |
| 342 | next_steps += _NEXT_STEPS_OPS_STANDARD.format( |
| 343 | ops_dir=ops_dir, |
| 344 | ops_name=f"{manifest.project}-ops", |
| 345 | cloud=manifest.cloud, |
| 346 | ) |
| 347 | |
| 348 | console.print( |
| 349 | Panel( |
| 350 | next_steps.strip(), |
| 351 | title="[bold green]Done![/bold green]", |
| 352 | border_style="green", |
| 353 | ) |
| 354 | ) |
+89
| --- a/boilerworks/manifest.py | ||
| +++ b/boilerworks/manifest.py | ||
| @@ -0,0 +1,89 @@ | ||
| 1 | +"""Pydantic models for boilerworks.yaml manifest.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import re | |
| 6 | +from pathlib import Path | |
| 7 | +from typing import Literal | |
| 8 | + | |
| 9 | +import yaml | |
| 10 | +from pydantic import BaseModel, Field, field_validator, model_validator | |
| 11 | + | |
| 12 | +_SLUG_RE = re.compile(r"^[a-z][a-z0-9-]*$") | |
| 13 | + | |
| 14 | + | |
| 15 | +class ServicesConfig(BaseModel): | |
| 16 | + email: Literal["ses", "sendgrid", "mailgun", "none"] | None = None | |
| 17 | + storage: Literal["s3", "gcs", "azure-blob", "none"] | None = None | |
| 18 | + search: Literal["opensearch", "meilisearch", "none"] | None = None | |
| 19 | + cache: Literal["redis", "memcached", "none"] | None = "redis" | |
| 20 | + | |
| 21 | + | |
| 22 | +class DataConfig(BaseModel): | |
| 23 | + database: Literal["postgres", "mysql", "sqlite"] = "postgres" | |
| 24 | + migrations: bool = True | |
| 25 | + seed_data: bool = True | |
| 26 | + | |
| 27 | + | |
| 28 | +class TestingConfig(BaseModel): | |
| 29 | + e2e: Literal["playwright", "cypress", "none"] | None = None | |
| 30 | + unit: bool = True | |
| 31 | + integration: bool = True | |
| 32 | + | |
| 33 | + | |
| 34 | +class BoilerworksManifest(BaseModel): | |
| 35 | + project: str | |
| 36 | + family: str | |
| 37 | + size: Literal["full", "micro", "edge"] | |
| 38 | + topology: Literal["standard", "omni", "api-only"] = "standard" | |
| 39 | + cloud: Literal["aws", "gcp", "azure"] | None = None | |
| 40 | + ops: bool = False | |
| 41 | + region: str | None = None | |
| 42 | + domain: str | None = None | |
| 43 | + mobile: bool = False | |
| 44 | + web_presence: bool = False | |
| 45 | + compliance: list[str] = Field(default_factory=list) | |
| 46 | + services: ServicesConfig = Field(default_factory=ServicesConfig) | |
| 47 | + data: DataConfig = Field(default_factory=DataConfig) | |
| 48 | + testing: TestingConfig = Field(default_factory=TestingConfig) | |
| 49 | + template_versions: dict[str, str] = Field(default_factory=dict) | |
| 50 | + | |
| 51 | + @field_validator("project") | |
| 52 | + @classmethod | |
| 53 | + def validate_project_slug(cls, v: str) -> str: | |
| 54 | + if not _SLUG_RE.match(v): | |
| 55 | + raise ValueError( | |
| 56 | + f"project name '{v}' must be lowercase, start with a letter, " | |
| 57 | + "and contain only letters, digits, and hyphens" | |
| 58 | + ) | |
| 59 | + return v | |
| 60 | + | |
| 61 | + @model_validator(mode="after") | |
| 62 | + def validate_family_in_registry(self) -> BoilerworksManifest: | |
| 63 | + from boilerworks.registry import Registry | |
| 64 | + | |
| 65 | + registry = Registry() | |
| 66 | + if registry.get_by_name(self.family) is None: | |
| 67 | + valid = ", ".join(sorted(registry.names())) | |
| 68 | + raise ValueError(f"unknown template family '{self.family}'. Valid families: {valid}") | |
| 69 | + return self | |
| 70 | + | |
| 71 | + def to_yaml(self) -> str: | |
| 72 | + """Serialise the manifest to a YAML string.""" | |
| 73 | + data = self.model_dump(exclude_none=False) | |
| 74 | + return yaml.dump(data, default_flow_style=False, sort_keys=False, allow_unicode=True) | |
| 75 | + | |
| 76 | + @classmethod | |
| 77 | + def from_yaml(cls, text: str) -> BoilerworksManifest: | |
| 78 | + """Parse a manifest from a YAML string.""" | |
| 79 | + data = yaml.safe_load(text) | |
| 80 | + return cls(**data) | |
| 81 | + | |
| 82 | + @classmethod | |
| 83 | + def from_file(cls, path: str | Path) -> BoilerworksManifest: | |
| 84 | + """Load a manifest from a file path.""" | |
| 85 | + return cls.from_yaml(Path(path).read_text()) | |
| 86 | + | |
| 87 | + def to_file(self, path: str | Path) -> None: | |
| 88 | + """Write the manifest to a file.""" | |
| 89 | + Path(path).write_text(self.to_yaml()) |
| --- a/boilerworks/manifest.py | |
| +++ b/boilerworks/manifest.py | |
| @@ -0,0 +1,89 @@ | |
| --- a/boilerworks/manifest.py | |
| +++ b/boilerworks/manifest.py | |
| @@ -0,0 +1,89 @@ | |
| 1 | """Pydantic models for boilerworks.yaml manifest.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import re |
| 6 | from pathlib import Path |
| 7 | from typing import Literal |
| 8 | |
| 9 | import yaml |
| 10 | from pydantic import BaseModel, Field, field_validator, model_validator |
| 11 | |
| 12 | _SLUG_RE = re.compile(r"^[a-z][a-z0-9-]*$") |
| 13 | |
| 14 | |
| 15 | class ServicesConfig(BaseModel): |
| 16 | email: Literal["ses", "sendgrid", "mailgun", "none"] | None = None |
| 17 | storage: Literal["s3", "gcs", "azure-blob", "none"] | None = None |
| 18 | search: Literal["opensearch", "meilisearch", "none"] | None = None |
| 19 | cache: Literal["redis", "memcached", "none"] | None = "redis" |
| 20 | |
| 21 | |
| 22 | class DataConfig(BaseModel): |
| 23 | database: Literal["postgres", "mysql", "sqlite"] = "postgres" |
| 24 | migrations: bool = True |
| 25 | seed_data: bool = True |
| 26 | |
| 27 | |
| 28 | class TestingConfig(BaseModel): |
| 29 | e2e: Literal["playwright", "cypress", "none"] | None = None |
| 30 | unit: bool = True |
| 31 | integration: bool = True |
| 32 | |
| 33 | |
| 34 | class BoilerworksManifest(BaseModel): |
| 35 | project: str |
| 36 | family: str |
| 37 | size: Literal["full", "micro", "edge"] |
| 38 | topology: Literal["standard", "omni", "api-only"] = "standard" |
| 39 | cloud: Literal["aws", "gcp", "azure"] | None = None |
| 40 | ops: bool = False |
| 41 | region: str | None = None |
| 42 | domain: str | None = None |
| 43 | mobile: bool = False |
| 44 | web_presence: bool = False |
| 45 | compliance: list[str] = Field(default_factory=list) |
| 46 | services: ServicesConfig = Field(default_factory=ServicesConfig) |
| 47 | data: DataConfig = Field(default_factory=DataConfig) |
| 48 | testing: TestingConfig = Field(default_factory=TestingConfig) |
| 49 | template_versions: dict[str, str] = Field(default_factory=dict) |
| 50 | |
| 51 | @field_validator("project") |
| 52 | @classmethod |
| 53 | def validate_project_slug(cls, v: str) -> str: |
| 54 | if not _SLUG_RE.match(v): |
| 55 | raise ValueError( |
| 56 | f"project name '{v}' must be lowercase, start with a letter, " |
| 57 | "and contain only letters, digits, and hyphens" |
| 58 | ) |
| 59 | return v |
| 60 | |
| 61 | @model_validator(mode="after") |
| 62 | def validate_family_in_registry(self) -> BoilerworksManifest: |
| 63 | from boilerworks.registry import Registry |
| 64 | |
| 65 | registry = Registry() |
| 66 | if registry.get_by_name(self.family) is None: |
| 67 | valid = ", ".join(sorted(registry.names())) |
| 68 | raise ValueError(f"unknown template family '{self.family}'. Valid families: {valid}") |
| 69 | return self |
| 70 | |
| 71 | def to_yaml(self) -> str: |
| 72 | """Serialise the manifest to a YAML string.""" |
| 73 | data = self.model_dump(exclude_none=False) |
| 74 | return yaml.dump(data, default_flow_style=False, sort_keys=False, allow_unicode=True) |
| 75 | |
| 76 | @classmethod |
| 77 | def from_yaml(cls, text: str) -> BoilerworksManifest: |
| 78 | """Parse a manifest from a YAML string.""" |
| 79 | data = yaml.safe_load(text) |
| 80 | return cls(**data) |
| 81 | |
| 82 | @classmethod |
| 83 | def from_file(cls, path: str | Path) -> BoilerworksManifest: |
| 84 | """Load a manifest from a file path.""" |
| 85 | return cls.from_yaml(Path(path).read_text()) |
| 86 | |
| 87 | def to_file(self, path: str | Path) -> None: |
| 88 | """Write the manifest to a file.""" |
| 89 | Path(path).write_text(self.to_yaml()) |
+69
| --- a/boilerworks/registry.py | ||
| +++ b/boilerworks/registry.py | ||
| @@ -0,0 +1,69 @@ | ||
| 1 | +"""Template registry — loads and queries templates.yaml.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +from pathlib import Path | |
| 6 | +from typing import Literal | |
| 7 | + | |
| 8 | +import yaml | |
| 9 | +from pydantic import BaseModel, Field | |
| 10 | + | |
| 11 | +_DATA_DIR = Path(__file__).parent / "data" | |
| 12 | + | |
| 13 | + | |
| 14 | +class TemplateInfo(BaseModel): | |
| 15 | + """A single template entry from templates.yaml.""" | |
| 16 | + | |
| 17 | + name: str | |
| 18 | + repo: str | |
| 19 | + size: Literal["full", "micro", "edge"] | |
| 20 | + language: str | |
| 21 | + backend: str | |
| 22 | + frontend: str | |
| 23 | + status: Literal["done", "building", "planned"] | |
| 24 | + description: str | |
| 25 | + topologies: list[str] = Field(default_factory=list) | |
| 26 | + best_for: str = "" | |
| 27 | + | |
| 28 | + | |
| 29 | +class Registry: | |
| 30 | + """Loads templates.yaml and provides query methods.""" | |
| 31 | + | |
| 32 | + def __init__(self, yaml_path: Path | None = None) -> None: | |
| 33 | + path = yaml_path or (_DATA_DIR / "templates.yaml") | |
| 34 | + raw: list[dict] = yaml.safe_load(path.read_text()) | |
| 35 | + self._templates: list[TemplateInfo] = [TemplateInfo(**entry) for entry in raw] | |
| 36 | + | |
| 37 | + def list_all(self) -> list[TemplateInfo]: | |
| 38 | + """Return all templates.""" | |
| 39 | + return list(self._templates) | |
| 40 | + | |
| 41 | + def filter_by_size(self, size: str) -> list[TemplateInfo]: | |
| 42 | + """Return templates matching the given size.""" | |
| 43 | + return [t for t in self._templates if t.size == size] | |
| 44 | + | |
| 45 | + def filter_by_language(self, language: str) -> list[TemplateInfo]: | |
| 46 | + """Return templates matching the given language.""" | |
| 47 | + return [t for t in self._templates if t.language == language] | |
| 48 | + | |
| 49 | + def filter_by_status(self, status: str) -> list[TemplateInfo]: | |
| 50 | + """Return templates matching the given status.""" | |
| 51 | + return [t for t in self._templates if t.status == status] | |
| 52 | + | |
| 53 | + def get_by_name(self, name: str) -> TemplateInfo | None: | |
| 54 | + """Return the template with the given name, or None.""" | |
| 55 | + for t in self._templates: | |
| 56 | + if t.name == name: | |
| 57 | + return t | |
| 58 | + return None | |
| 59 | + | |
| 60 | + def search(self, query: str) -> list[TemplateInfo]: | |
| 61 | + """Return templates whose name, description, or best_for contains the query (case-insensitive).""" | |
| 62 | + q = query.lower() | |
| 63 | + return [ | |
| 64 | + t for t in self._templates if q in t.name.lower() or q in t.description.lower() or q in t.best_for.lower() | |
| 65 | + ] | |
| 66 | + | |
| 67 | + def names(self) -> list[str]: | |
| 68 | + """Return all template names.""" | |
| 69 | + return [t.name for t in self._templates] |
| --- a/boilerworks/registry.py | |
| +++ b/boilerworks/registry.py | |
| @@ -0,0 +1,69 @@ | |
| --- a/boilerworks/registry.py | |
| +++ b/boilerworks/registry.py | |
| @@ -0,0 +1,69 @@ | |
| 1 | """Template registry — loads and queries templates.yaml.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from pathlib import Path |
| 6 | from typing import Literal |
| 7 | |
| 8 | import yaml |
| 9 | from pydantic import BaseModel, Field |
| 10 | |
| 11 | _DATA_DIR = Path(__file__).parent / "data" |
| 12 | |
| 13 | |
| 14 | class TemplateInfo(BaseModel): |
| 15 | """A single template entry from templates.yaml.""" |
| 16 | |
| 17 | name: str |
| 18 | repo: str |
| 19 | size: Literal["full", "micro", "edge"] |
| 20 | language: str |
| 21 | backend: str |
| 22 | frontend: str |
| 23 | status: Literal["done", "building", "planned"] |
| 24 | description: str |
| 25 | topologies: list[str] = Field(default_factory=list) |
| 26 | best_for: str = "" |
| 27 | |
| 28 | |
| 29 | class Registry: |
| 30 | """Loads templates.yaml and provides query methods.""" |
| 31 | |
| 32 | def __init__(self, yaml_path: Path | None = None) -> None: |
| 33 | path = yaml_path or (_DATA_DIR / "templates.yaml") |
| 34 | raw: list[dict] = yaml.safe_load(path.read_text()) |
| 35 | self._templates: list[TemplateInfo] = [TemplateInfo(**entry) for entry in raw] |
| 36 | |
| 37 | def list_all(self) -> list[TemplateInfo]: |
| 38 | """Return all templates.""" |
| 39 | return list(self._templates) |
| 40 | |
| 41 | def filter_by_size(self, size: str) -> list[TemplateInfo]: |
| 42 | """Return templates matching the given size.""" |
| 43 | return [t for t in self._templates if t.size == size] |
| 44 | |
| 45 | def filter_by_language(self, language: str) -> list[TemplateInfo]: |
| 46 | """Return templates matching the given language.""" |
| 47 | return [t for t in self._templates if t.language == language] |
| 48 | |
| 49 | def filter_by_status(self, status: str) -> list[TemplateInfo]: |
| 50 | """Return templates matching the given status.""" |
| 51 | return [t for t in self._templates if t.status == status] |
| 52 | |
| 53 | def get_by_name(self, name: str) -> TemplateInfo | None: |
| 54 | """Return the template with the given name, or None.""" |
| 55 | for t in self._templates: |
| 56 | if t.name == name: |
| 57 | return t |
| 58 | return None |
| 59 | |
| 60 | def search(self, query: str) -> list[TemplateInfo]: |
| 61 | """Return templates whose name, description, or best_for contains the query (case-insensitive).""" |
| 62 | q = query.lower() |
| 63 | return [ |
| 64 | t for t in self._templates if q in t.name.lower() or q in t.description.lower() or q in t.best_for.lower() |
| 65 | ] |
| 66 | |
| 67 | def names(self) -> list[str]: |
| 68 | """Return all template names.""" |
| 69 | return [t.name for t in self._templates] |
+164
| --- a/boilerworks/renderer.py | ||
| +++ b/boilerworks/renderer.py | ||
| @@ -0,0 +1,164 @@ | ||
| 1 | +"""File renderer — string replacement across a cloned template directory.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import os | |
| 6 | +from pathlib import Path | |
| 7 | + | |
| 8 | +# Directories to skip entirely during rendering | |
| 9 | +_SKIP_DIRS: frozenset[str] = frozenset( | |
| 10 | + [".git", "node_modules", "vendor", "__pycache__", "_build", "deps", "target", ".venv"] | |
| 11 | +) | |
| 12 | + | |
| 13 | +# Binary/lock file extensions to skip (don't attempt text replacement) | |
| 14 | +_SKIP_EXTENSIONS: frozenset[str] = frozenset( | |
| 15 | + [ | |
| 16 | + ".png", | |
| 17 | + ".jpg", | |
| 18 | + ".jpeg", | |
| 19 | + ".gif", | |
| 20 | + ".ico", | |
| 21 | + ".woff", | |
| 22 | + ".woff2", | |
| 23 | + ".ttf", | |
| 24 | + ".eot", | |
| 25 | + ".lock", | |
| 26 | + ".pyc", | |
| 27 | + ".pdf", | |
| 28 | + ".zip", | |
| 29 | + ".tar", | |
| 30 | + ".gz", | |
| 31 | + ".bz2", | |
| 32 | + ".7z", | |
| 33 | + ".exe", | |
| 34 | + ".bin", | |
| 35 | + ".so", | |
| 36 | + ".dylib", | |
| 37 | + ".dll", | |
| 38 | + ".wasm", | |
| 39 | + ".db", | |
| 40 | + ".sqlite", | |
| 41 | + ".sqlite3", | |
| 42 | + ] | |
| 43 | +) | |
| 44 | + | |
| 45 | + | |
| 46 | +def build_replacements(project: str) -> dict[str, str]: | |
| 47 | + """Build the standard case-variant replacement map from a project name. | |
| 48 | + | |
| 49 | + E.g. project='my-app' produces: | |
| 50 | + 'boilerworks' → 'my-app' | |
| 51 | + 'Boilerworks' → 'My-App' | |
| 52 | + 'BOILERWORKS' → 'MY-APP' | |
| 53 | + 'boilerworks_' → 'my_app_' (underscore variant for Python identifiers) | |
| 54 | + """ | |
| 55 | + # underscore variant (Python module names) | |
| 56 | + project_under = project.replace("-", "_") | |
| 57 | + project_title = project.replace("-", " ").title().replace(" ", "-") | |
| 58 | + project_upper = project.upper() | |
| 59 | + | |
| 60 | + return { | |
| 61 | + "boilerworks_": f"{project_under}_", | |
| 62 | + "_boilerworks": f"_{project_under}", | |
| 63 | + "BOILERWORKS": project_upper, | |
| 64 | + "Boilerworks": project_title, | |
| 65 | + "boilerworks": project, | |
| 66 | + } | |
| 67 | + | |
| 68 | + | |
| 69 | +def render_file(path: Path, replacements: dict[str, str]) -> bool: | |
| 70 | + """Apply string replacements to a single file in-place. | |
| 71 | + | |
| 72 | + Returns True if the file was modified, False if skipped or unchanged. | |
| 73 | + """ | |
| 74 | + if path.suffix.lower() in _SKIP_EXTENSIONS: | |
| 75 | + return False | |
| 76 | + | |
| 77 | + try: | |
| 78 | + original = path.read_text(encoding="utf-8", errors="strict") | |
| 79 | + except (UnicodeDecodeError, PermissionError): | |
| 80 | + # Binary or unreadable file — skip | |
| 81 | + return False | |
| 82 | + | |
| 83 | + modified = original | |
| 84 | + for old, new in replacements.items(): | |
| 85 | + modified = modified.replace(old, new) | |
| 86 | + | |
| 87 | + if modified == original: | |
| 88 | + return False | |
| 89 | + | |
| 90 | + path.write_text(modified, encoding="utf-8") | |
| 91 | + return True | |
| 92 | + | |
| 93 | + | |
| 94 | +def render_directory( | |
| 95 | + root: Path, | |
| 96 | + replacements: dict[str, str], | |
| 97 | + skip_dirs: frozenset[str] | None = None, | |
| 98 | + skip_extensions: frozenset[str] | None = None, | |
| 99 | +) -> list[Path]: | |
| 100 | + """Apply replacements to all eligible files under root. | |
| 101 | + | |
| 102 | + Returns the list of files that were modified. | |
| 103 | + """ | |
| 104 | + skip_d = skip_dirs if skip_dirs is not None else _SKIP_DIRS | |
| 105 | + skip_e = skip_extensions if skip_extensions is not None else _SKIP_EXTENSIONS | |
| 106 | + | |
| 107 | + modified: list[Path] = [] | |
| 108 | + | |
| 109 | + for dirpath, dirnames, filenames in os.walk(root): | |
| 110 | + # Prune excluded dirs in-place so os.walk doesn't recurse into them | |
| 111 | + dirnames[:] = [d for d in dirnames if d not in skip_d] | |
| 112 | + | |
| 113 | + for filename in filenames: | |
| 114 | + filepath = Path(dirpath) / filename | |
| 115 | + if filepath.suffix.lower() in skip_e: | |
| 116 | + continue | |
| 117 | + try: | |
| 118 | + was_changed = render_file(filepath, replacements) | |
| 119 | + except OSError: | |
| 120 | + continue | |
| 121 | + if was_changed: | |
| 122 | + modified.append(filepath) | |
| 123 | + | |
| 124 | + return modified | |
| 125 | + | |
| 126 | + | |
| 127 | +def rename_boilerworks_paths(root: Path, project: str) -> list[tuple[Path, Path]]: | |
| 128 | + """Rename any files or directories containing 'boilerworks' in their name. | |
| 129 | + | |
| 130 | + Walks the tree bottom-up so child renames happen before parents. | |
| 131 | + Returns list of (old_path, new_path) pairs. | |
| 132 | + """ | |
| 133 | + project_under = project.replace("-", "_") | |
| 134 | + renames: list[tuple[Path, Path]] = [] | |
| 135 | + | |
| 136 | + for dirpath, dirnames, filenames in os.walk(root, topdown=False): | |
| 137 | + current_dir = Path(dirpath) | |
| 138 | + | |
| 139 | + # Rename files | |
| 140 | + for filename in filenames: | |
| 141 | + if "boilerworks" in filename.lower(): | |
| 142 | + old = current_dir / filename | |
| 143 | + new_name = filename.replace("boilerworks", project).replace("BOILERWORKS", project.upper()) | |
| 144 | + new_name = new_name.replace("Boilerworks", project.replace("-", " ").title().replace(" ", "-")) | |
| 145 | + # Also handle underscore variant | |
| 146 | + new_name = new_name.replace("boilerworks_", f"{project_under}_") | |
| 147 | + new = current_dir / new_name | |
| 148 | + if old != new: | |
| 149 | + old.rename(new) | |
| 150 | + renames.append((old, new)) | |
| 151 | + | |
| 152 | + # Rename subdirectories | |
| 153 | + for dirname in dirnames: | |
| 154 | + if "boilerworks" in dirname.lower(): | |
| 155 | + old = current_dir / dirname | |
| 156 | + new_dirname = dirname.replace("boilerworks", project).replace("BOILERWORKS", project.upper()) | |
| 157 | + new_dirname = new_dirname.replace("Boilerworks", project.replace("-", " ").title().replace(" ", "-")) | |
| 158 | + new_dirname = new_dirname.replace("boilerworks_", f"{project_under}_") | |
| 159 | + new = current_dir / new_dirname | |
| 160 | + if old != new and old.exists(): | |
| 161 | + old.rename(new) | |
| 162 | + renames.append((old, new)) | |
| 163 | + | |
| 164 | + return renames |
| --- a/boilerworks/renderer.py | |
| +++ b/boilerworks/renderer.py | |
| @@ -0,0 +1,164 @@ | |
| --- a/boilerworks/renderer.py | |
| +++ b/boilerworks/renderer.py | |
| @@ -0,0 +1,164 @@ | |
| 1 | """File renderer — string replacement across a cloned template directory.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import os |
| 6 | from pathlib import Path |
| 7 | |
| 8 | # Directories to skip entirely during rendering |
| 9 | _SKIP_DIRS: frozenset[str] = frozenset( |
| 10 | [".git", "node_modules", "vendor", "__pycache__", "_build", "deps", "target", ".venv"] |
| 11 | ) |
| 12 | |
| 13 | # Binary/lock file extensions to skip (don't attempt text replacement) |
| 14 | _SKIP_EXTENSIONS: frozenset[str] = frozenset( |
| 15 | [ |
| 16 | ".png", |
| 17 | ".jpg", |
| 18 | ".jpeg", |
| 19 | ".gif", |
| 20 | ".ico", |
| 21 | ".woff", |
| 22 | ".woff2", |
| 23 | ".ttf", |
| 24 | ".eot", |
| 25 | ".lock", |
| 26 | ".pyc", |
| 27 | ".pdf", |
| 28 | ".zip", |
| 29 | ".tar", |
| 30 | ".gz", |
| 31 | ".bz2", |
| 32 | ".7z", |
| 33 | ".exe", |
| 34 | ".bin", |
| 35 | ".so", |
| 36 | ".dylib", |
| 37 | ".dll", |
| 38 | ".wasm", |
| 39 | ".db", |
| 40 | ".sqlite", |
| 41 | ".sqlite3", |
| 42 | ] |
| 43 | ) |
| 44 | |
| 45 | |
| 46 | def build_replacements(project: str) -> dict[str, str]: |
| 47 | """Build the standard case-variant replacement map from a project name. |
| 48 | |
| 49 | E.g. project='my-app' produces: |
| 50 | 'boilerworks' → 'my-app' |
| 51 | 'Boilerworks' → 'My-App' |
| 52 | 'BOILERWORKS' → 'MY-APP' |
| 53 | 'boilerworks_' → 'my_app_' (underscore variant for Python identifiers) |
| 54 | """ |
| 55 | # underscore variant (Python module names) |
| 56 | project_under = project.replace("-", "_") |
| 57 | project_title = project.replace("-", " ").title().replace(" ", "-") |
| 58 | project_upper = project.upper() |
| 59 | |
| 60 | return { |
| 61 | "boilerworks_": f"{project_under}_", |
| 62 | "_boilerworks": f"_{project_under}", |
| 63 | "BOILERWORKS": project_upper, |
| 64 | "Boilerworks": project_title, |
| 65 | "boilerworks": project, |
| 66 | } |
| 67 | |
| 68 | |
| 69 | def render_file(path: Path, replacements: dict[str, str]) -> bool: |
| 70 | """Apply string replacements to a single file in-place. |
| 71 | |
| 72 | Returns True if the file was modified, False if skipped or unchanged. |
| 73 | """ |
| 74 | if path.suffix.lower() in _SKIP_EXTENSIONS: |
| 75 | return False |
| 76 | |
| 77 | try: |
| 78 | original = path.read_text(encoding="utf-8", errors="strict") |
| 79 | except (UnicodeDecodeError, PermissionError): |
| 80 | # Binary or unreadable file — skip |
| 81 | return False |
| 82 | |
| 83 | modified = original |
| 84 | for old, new in replacements.items(): |
| 85 | modified = modified.replace(old, new) |
| 86 | |
| 87 | if modified == original: |
| 88 | return False |
| 89 | |
| 90 | path.write_text(modified, encoding="utf-8") |
| 91 | return True |
| 92 | |
| 93 | |
| 94 | def render_directory( |
| 95 | root: Path, |
| 96 | replacements: dict[str, str], |
| 97 | skip_dirs: frozenset[str] | None = None, |
| 98 | skip_extensions: frozenset[str] | None = None, |
| 99 | ) -> list[Path]: |
| 100 | """Apply replacements to all eligible files under root. |
| 101 | |
| 102 | Returns the list of files that were modified. |
| 103 | """ |
| 104 | skip_d = skip_dirs if skip_dirs is not None else _SKIP_DIRS |
| 105 | skip_e = skip_extensions if skip_extensions is not None else _SKIP_EXTENSIONS |
| 106 | |
| 107 | modified: list[Path] = [] |
| 108 | |
| 109 | for dirpath, dirnames, filenames in os.walk(root): |
| 110 | # Prune excluded dirs in-place so os.walk doesn't recurse into them |
| 111 | dirnames[:] = [d for d in dirnames if d not in skip_d] |
| 112 | |
| 113 | for filename in filenames: |
| 114 | filepath = Path(dirpath) / filename |
| 115 | if filepath.suffix.lower() in skip_e: |
| 116 | continue |
| 117 | try: |
| 118 | was_changed = render_file(filepath, replacements) |
| 119 | except OSError: |
| 120 | continue |
| 121 | if was_changed: |
| 122 | modified.append(filepath) |
| 123 | |
| 124 | return modified |
| 125 | |
| 126 | |
| 127 | def rename_boilerworks_paths(root: Path, project: str) -> list[tuple[Path, Path]]: |
| 128 | """Rename any files or directories containing 'boilerworks' in their name. |
| 129 | |
| 130 | Walks the tree bottom-up so child renames happen before parents. |
| 131 | Returns list of (old_path, new_path) pairs. |
| 132 | """ |
| 133 | project_under = project.replace("-", "_") |
| 134 | renames: list[tuple[Path, Path]] = [] |
| 135 | |
| 136 | for dirpath, dirnames, filenames in os.walk(root, topdown=False): |
| 137 | current_dir = Path(dirpath) |
| 138 | |
| 139 | # Rename files |
| 140 | for filename in filenames: |
| 141 | if "boilerworks" in filename.lower(): |
| 142 | old = current_dir / filename |
| 143 | new_name = filename.replace("boilerworks", project).replace("BOILERWORKS", project.upper()) |
| 144 | new_name = new_name.replace("Boilerworks", project.replace("-", " ").title().replace(" ", "-")) |
| 145 | # Also handle underscore variant |
| 146 | new_name = new_name.replace("boilerworks_", f"{project_under}_") |
| 147 | new = current_dir / new_name |
| 148 | if old != new: |
| 149 | old.rename(new) |
| 150 | renames.append((old, new)) |
| 151 | |
| 152 | # Rename subdirectories |
| 153 | for dirname in dirnames: |
| 154 | if "boilerworks" in dirname.lower(): |
| 155 | old = current_dir / dirname |
| 156 | new_dirname = dirname.replace("boilerworks", project).replace("BOILERWORKS", project.upper()) |
| 157 | new_dirname = new_dirname.replace("Boilerworks", project.replace("-", " ").title().replace(" ", "-")) |
| 158 | new_dirname = new_dirname.replace("boilerworks_", f"{project_under}_") |
| 159 | new = current_dir / new_dirname |
| 160 | if old != new and old.exists(): |
| 161 | old.rename(new) |
| 162 | renames.append((old, new)) |
| 163 | |
| 164 | return renames |
+212
| --- a/boilerworks/wizard.py | ||
| +++ b/boilerworks/wizard.py | ||
| @@ -0,0 +1,212 @@ | ||
| 1 | +"""Interactive setup wizard — collects answers, writes boilerworks.yaml.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import re | |
| 6 | +import sys | |
| 7 | +from pathlib import Path | |
| 8 | + | |
| 9 | +import questionary | |
| 10 | +from rich.panel import Panel | |
| 11 | + | |
| 12 | +from boilerworks.console import console, print_success | |
| 13 | +from boilerworks.manifest import BoilerworksManifest, DataConfig, ServicesConfig, TestingConfig | |
| 14 | +from boilerworks.registry import Registry, TemplateInfo | |
| 15 | + | |
| 16 | +_SLUG_RE = re.compile(r"^[a-z][a-z0-9-]*$") | |
| 17 | + | |
| 18 | +_SELECTION_GUIDE = """[bold]How to pick a template size:[/bold] | |
| 19 | + | |
| 20 | + [cyan]Full[/cyan] — Apps with users (login, permissions, org management) | |
| 21 | + [magenta]Micro[/magenta] — API-key services, microservices, workers | |
| 22 | + [blue]Edge[/blue] — Cloudflare Workers / Pages, globally distributed | |
| 23 | + | |
| 24 | +[bold]When to choose Full vs Micro:[/bold] | |
| 25 | + If it has users logging in → Full | |
| 26 | + If it's a service with API keys → Micro | |
| 27 | + If it needs global edge scale → Edge | |
| 28 | +""" | |
| 29 | + | |
| 30 | + | |
| 31 | +def _validate_slug(text: str) -> bool | str: | |
| 32 | + if not text: | |
| 33 | + return "Project name is required" | |
| 34 | + if not _SLUG_RE.match(text): | |
| 35 | + return "Must be lowercase, start with a letter, letters/digits/hyphens only (e.g. my-project)" | |
| 36 | + return True | |
| 37 | + | |
| 38 | + | |
| 39 | +def _template_choices(templates: list[TemplateInfo]) -> list[questionary.Choice]: | |
| 40 | + choices = [] | |
| 41 | + current_lang = None | |
| 42 | + for t in templates: | |
| 43 | + if t.language != current_lang: | |
| 44 | + current_lang = t.language | |
| 45 | + choices.append(questionary.Separator(f"── {t.language} ──")) | |
| 46 | + status_icon = {"done": "●", "building": "◐", "planned": "○"}.get(t.status, "?") | |
| 47 | + label = f"{status_icon} {t.name:<24} {t.description}" | |
| 48 | + choices.append(questionary.Choice(title=label, value=t.name)) | |
| 49 | + return choices | |
| 50 | + | |
| 51 | + | |
| 52 | +def run_wizard(output_path: str | Path = "boilerworks.yaml") -> None: | |
| 53 | + """Walk through the 13-step wizard and write boilerworks.yaml.""" | |
| 54 | + registry = Registry() | |
| 55 | + console.print( | |
| 56 | + Panel( | |
| 57 | + "[bold cyan]Boilerworks Setup Wizard[/bold cyan]\nAnswers are saved to [bold]boilerworks.yaml[/bold]", | |
| 58 | + border_style="cyan", | |
| 59 | + ) | |
| 60 | + ) | |
| 61 | + | |
| 62 | + # ── Step 1: Project name ───────────────────────────────────────────────── | |
| 63 | + project = questionary.text( | |
| 64 | + "Project name (slug format, e.g. my-app):", | |
| 65 | + validate=_validate_slug, | |
| 66 | + ).ask() | |
| 67 | + if project is None: | |
| 68 | + console.print("[yellow]Cancelled.[/yellow]") | |
| 69 | + sys.exit(0) | |
| 70 | + | |
| 71 | + # ── Step 2: Template size ───────────────────────────────────────────────── | |
| 72 | + console.print(Panel(_SELECTION_GUIDE, title="Template Guide", border_style="dim")) | |
| 73 | + size = questionary.select( | |
| 74 | + "Template size:", | |
| 75 | + choices=["full", "micro", "edge"], | |
| 76 | + ).ask() | |
| 77 | + if size is None: | |
| 78 | + console.print("[yellow]Cancelled.[/yellow]") | |
| 79 | + sys.exit(0) | |
| 80 | + | |
| 81 | + # ── Step 3: Template family ─────────────────────────────────────────────── | |
| 82 | + filtered = registry.filter_by_size(size) | |
| 83 | + family = questionary.select( | |
| 84 | + "Template family:", | |
| 85 | + choices=_template_choices(filtered), | |
| 86 | + ).ask() | |
| 87 | + if family is None: | |
| 88 | + console.print("[yellow]Cancelled.[/yellow]") | |
| 89 | + sys.exit(0) | |
| 90 | + | |
| 91 | + template = registry.get_by_name(family) | |
| 92 | + | |
| 93 | + # ── Step 4: Topology ────────────────────────────────────────────────────── | |
| 94 | + available_topologies = template.topologies if template else ["standard"] | |
| 95 | + if len(available_topologies) > 1: | |
| 96 | + topology = questionary.select( | |
| 97 | + "Deployment topology:", | |
| 98 | + choices=available_topologies, | |
| 99 | + ).ask() | |
| 100 | + else: | |
| 101 | + topology = available_topologies[0] | |
| 102 | + console.print(f" [dim]Topology: {topology} (only option for this template)[/dim]") | |
| 103 | + if topology is None: | |
| 104 | + topology = "standard" | |
| 105 | + | |
| 106 | + # ── Step 5: Cloud provider ──────────────────────────────────────────────── | |
| 107 | + cloud_answer = questionary.select( | |
| 108 | + "Cloud provider:", | |
| 109 | + choices=["aws", "gcp", "azure", "none"], | |
| 110 | + ).ask() | |
| 111 | + cloud = cloud_answer if cloud_answer != "none" else None | |
| 112 | + | |
| 113 | + # ── Step 5b: Ops (infra-as-code) ───────────────────────────────────────── | |
| 114 | + ops = False | |
| 115 | + if cloud: | |
| 116 | + ops = ( | |
| 117 | + questionary.confirm( | |
| 118 | + "Include infrastructure-as-code (boilerworks-opscode)?", | |
| 119 | + default=True, | |
| 120 | + ).ask() | |
| 121 | + or False | |
| 122 | + ) | |
| 123 | + | |
| 124 | + # ── Step 6: Region ──────────────────────────────────────────────────────── | |
| 125 | + region: str | None = None | |
| 126 | + if cloud: | |
| 127 | + region = questionary.text( | |
| 128 | + f"Region (e.g. {'us-east-1' if cloud == 'aws' else 'us-central1' if cloud == 'gcp' else 'eastus'}):", | |
| 129 | + ).ask() | |
| 130 | + if not region: | |
| 131 | + region = None | |
| 132 | + | |
| 133 | + # ── Step 7: Domain ──────────────────────────────────────────────────────── | |
| 134 | + domain_answer = questionary.text("Domain (optional, e.g. myapp.com):").ask() | |
| 135 | + domain = domain_answer.strip() if domain_answer and domain_answer.strip() else None | |
| 136 | + | |
| 137 | + # ── Step 8 & 9: Mobile / Web presence (Full only) ──────────────────────── | |
| 138 | + mobile = False | |
| 139 | + web_presence = False | |
| 140 | + if size == "full": | |
| 141 | + mobile = questionary.confirm("Include mobile app template?", default=False).ask() or False | |
| 142 | + web_presence = questionary.confirm("Include web presence / marketing site?", default=False).ask() or False | |
| 143 | + | |
| 144 | + # ── Step 10: Compliance ─────────────────────────────────────────────────── | |
| 145 | + compliance_answer = questionary.checkbox( | |
| 146 | + "Compliance requirements:", | |
| 147 | + choices=["soc2", "hipaa", "pci-dss", "gdpr", "none"], | |
| 148 | + ).ask() | |
| 149 | + compliance = [c for c in (compliance_answer or []) if c != "none"] | |
| 150 | + | |
| 151 | + # ── Step 11: Email provider ─────────────────────────────────────────────── | |
| 152 | + email_answer = questionary.select( | |
| 153 | + "Email provider:", | |
| 154 | + choices=["ses", "sendgrid", "mailgun", "none"], | |
| 155 | + ).ask() | |
| 156 | + email = email_answer if email_answer != "none" else None | |
| 157 | + | |
| 158 | + # ── Step 12: E2E testing ────────────────────────────────────────────────── | |
| 159 | + e2e_answer = questionary.select( | |
| 160 | + "End-to-end testing framework:", | |
| 161 | + choices=["playwright", "cypress", "none"], | |
| 162 | + ).ask() | |
| 163 | + e2e = e2e_answer if e2e_answer != "none" else None | |
| 164 | + | |
| 165 | + # ── Step 13: Summary + confirm ──────────────────────────────────────────── | |
| 166 | + summary_lines = [ | |
| 167 | + f" [bold]Project:[/bold] {project}", | |
| 168 | + f" [bold]Template:[/bold] {family} ({size})", | |
| 169 | + f" [bold]Topology:[/bold] {topology}", | |
| 170 | + f" [bold]Cloud:[/bold] {cloud or 'none'}", | |
| 171 | + f" [bold]Ops:[/bold] {'yes' if ops else 'no'}", | |
| 172 | + f" [bold]Region:[/bold] {region or '—'}", | |
| 173 | + f" [bold]Domain:[/bold] {domain or '—'}", | |
| 174 | + ] | |
| 175 | + if size == "full": | |
| 176 | + summary_lines += [ | |
| 177 | + f" [bold]Mobile:[/bold] {'yes' if mobile else 'no'}", | |
| 178 | + f" [bold]Web:[/bold] {'yes' if web_presence else 'no'}", | |
| 179 | + ] | |
| 180 | + summary_lines += [ | |
| 181 | + f" [bold]Compliance:[/bold] {', '.join(compliance) if compliance else 'none'}", | |
| 182 | + f" [bold]Email:[/bold] {email or 'none'}", | |
| 183 | + f" [bold]E2E:[/bold] {e2e or 'none'}", | |
| 184 | + ] | |
| 185 | + console.print(Panel("\n".join(summary_lines), title="[bold]Summary[/bold]", border_style="green")) | |
| 186 | + | |
| 187 | + confirmed = questionary.confirm("Write boilerworks.yaml?", default=True).ask() | |
| 188 | + if not confirmed: | |
| 189 | + console.print("[yellow]Cancelled — nothing written.[/yellow]") | |
| 190 | + sys.exit(0) | |
| 191 | + | |
| 192 | + manifest = BoilerworksManifest( | |
| 193 | + project=project, | |
| 194 | + family=family, | |
| 195 | + size=size, | |
| 196 | + topology=topology, | |
| 197 | + cloud=cloud, | |
| 198 | + ops=ops, | |
| 199 | + region=region, | |
| 200 | + domain=domain, | |
| 201 | + mobile=mobile, | |
| 202 | + web_presence=web_presence, | |
| 203 | + compliance=compliance, | |
| 204 | + services=ServicesConfig(email=email), | |
| 205 | + data=DataConfig(), | |
| 206 | + testing=TestingConfig(e2e=e2e), | |
| 207 | + ) | |
| 208 | + | |
| 209 | + out = Path(output_path) | |
| 210 | + manifest.to_file(out) | |
| 211 | + print_success(f"Written to [bold]{out}[/bold]") | |
| 212 | + console.print("\n[dim]Next:[/dim] [bold]boilerworks init[/bold]") |
| --- a/boilerworks/wizard.py | |
| +++ b/boilerworks/wizard.py | |
| @@ -0,0 +1,212 @@ | |
| --- a/boilerworks/wizard.py | |
| +++ b/boilerworks/wizard.py | |
| @@ -0,0 +1,212 @@ | |
| 1 | """Interactive setup wizard — collects answers, writes boilerworks.yaml.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import re |
| 6 | import sys |
| 7 | from pathlib import Path |
| 8 | |
| 9 | import questionary |
| 10 | from rich.panel import Panel |
| 11 | |
| 12 | from boilerworks.console import console, print_success |
| 13 | from boilerworks.manifest import BoilerworksManifest, DataConfig, ServicesConfig, TestingConfig |
| 14 | from boilerworks.registry import Registry, TemplateInfo |
| 15 | |
| 16 | _SLUG_RE = re.compile(r"^[a-z][a-z0-9-]*$") |
| 17 | |
| 18 | _SELECTION_GUIDE = """[bold]How to pick a template size:[/bold] |
| 19 | |
| 20 | [cyan]Full[/cyan] — Apps with users (login, permissions, org management) |
| 21 | [magenta]Micro[/magenta] — API-key services, microservices, workers |
| 22 | [blue]Edge[/blue] — Cloudflare Workers / Pages, globally distributed |
| 23 | |
| 24 | [bold]When to choose Full vs Micro:[/bold] |
| 25 | If it has users logging in → Full |
| 26 | If it's a service with API keys → Micro |
| 27 | If it needs global edge scale → Edge |
| 28 | """ |
| 29 | |
| 30 | |
| 31 | def _validate_slug(text: str) -> bool | str: |
| 32 | if not text: |
| 33 | return "Project name is required" |
| 34 | if not _SLUG_RE.match(text): |
| 35 | return "Must be lowercase, start with a letter, letters/digits/hyphens only (e.g. my-project)" |
| 36 | return True |
| 37 | |
| 38 | |
| 39 | def _template_choices(templates: list[TemplateInfo]) -> list[questionary.Choice]: |
| 40 | choices = [] |
| 41 | current_lang = None |
| 42 | for t in templates: |
| 43 | if t.language != current_lang: |
| 44 | current_lang = t.language |
| 45 | choices.append(questionary.Separator(f"── {t.language} ──")) |
| 46 | status_icon = {"done": "●", "building": "◐", "planned": "○"}.get(t.status, "?") |
| 47 | label = f"{status_icon} {t.name:<24} {t.description}" |
| 48 | choices.append(questionary.Choice(title=label, value=t.name)) |
| 49 | return choices |
| 50 | |
| 51 | |
| 52 | def run_wizard(output_path: str | Path = "boilerworks.yaml") -> None: |
| 53 | """Walk through the 13-step wizard and write boilerworks.yaml.""" |
| 54 | registry = Registry() |
| 55 | console.print( |
| 56 | Panel( |
| 57 | "[bold cyan]Boilerworks Setup Wizard[/bold cyan]\nAnswers are saved to [bold]boilerworks.yaml[/bold]", |
| 58 | border_style="cyan", |
| 59 | ) |
| 60 | ) |
| 61 | |
| 62 | # ── Step 1: Project name ───────────────────────────────────────────────── |
| 63 | project = questionary.text( |
| 64 | "Project name (slug format, e.g. my-app):", |
| 65 | validate=_validate_slug, |
| 66 | ).ask() |
| 67 | if project is None: |
| 68 | console.print("[yellow]Cancelled.[/yellow]") |
| 69 | sys.exit(0) |
| 70 | |
| 71 | # ── Step 2: Template size ───────────────────────────────────────────────── |
| 72 | console.print(Panel(_SELECTION_GUIDE, title="Template Guide", border_style="dim")) |
| 73 | size = questionary.select( |
| 74 | "Template size:", |
| 75 | choices=["full", "micro", "edge"], |
| 76 | ).ask() |
| 77 | if size is None: |
| 78 | console.print("[yellow]Cancelled.[/yellow]") |
| 79 | sys.exit(0) |
| 80 | |
| 81 | # ── Step 3: Template family ─────────────────────────────────────────────── |
| 82 | filtered = registry.filter_by_size(size) |
| 83 | family = questionary.select( |
| 84 | "Template family:", |
| 85 | choices=_template_choices(filtered), |
| 86 | ).ask() |
| 87 | if family is None: |
| 88 | console.print("[yellow]Cancelled.[/yellow]") |
| 89 | sys.exit(0) |
| 90 | |
| 91 | template = registry.get_by_name(family) |
| 92 | |
| 93 | # ── Step 4: Topology ────────────────────────────────────────────────────── |
| 94 | available_topologies = template.topologies if template else ["standard"] |
| 95 | if len(available_topologies) > 1: |
| 96 | topology = questionary.select( |
| 97 | "Deployment topology:", |
| 98 | choices=available_topologies, |
| 99 | ).ask() |
| 100 | else: |
| 101 | topology = available_topologies[0] |
| 102 | console.print(f" [dim]Topology: {topology} (only option for this template)[/dim]") |
| 103 | if topology is None: |
| 104 | topology = "standard" |
| 105 | |
| 106 | # ── Step 5: Cloud provider ──────────────────────────────────────────────── |
| 107 | cloud_answer = questionary.select( |
| 108 | "Cloud provider:", |
| 109 | choices=["aws", "gcp", "azure", "none"], |
| 110 | ).ask() |
| 111 | cloud = cloud_answer if cloud_answer != "none" else None |
| 112 | |
| 113 | # ── Step 5b: Ops (infra-as-code) ───────────────────────────────────────── |
| 114 | ops = False |
| 115 | if cloud: |
| 116 | ops = ( |
| 117 | questionary.confirm( |
| 118 | "Include infrastructure-as-code (boilerworks-opscode)?", |
| 119 | default=True, |
| 120 | ).ask() |
| 121 | or False |
| 122 | ) |
| 123 | |
| 124 | # ── Step 6: Region ──────────────────────────────────────────────────────── |
| 125 | region: str | None = None |
| 126 | if cloud: |
| 127 | region = questionary.text( |
| 128 | f"Region (e.g. {'us-east-1' if cloud == 'aws' else 'us-central1' if cloud == 'gcp' else 'eastus'}):", |
| 129 | ).ask() |
| 130 | if not region: |
| 131 | region = None |
| 132 | |
| 133 | # ── Step 7: Domain ──────────────────────────────────────────────────────── |
| 134 | domain_answer = questionary.text("Domain (optional, e.g. myapp.com):").ask() |
| 135 | domain = domain_answer.strip() if domain_answer and domain_answer.strip() else None |
| 136 | |
| 137 | # ── Step 8 & 9: Mobile / Web presence (Full only) ──────────────────────── |
| 138 | mobile = False |
| 139 | web_presence = False |
| 140 | if size == "full": |
| 141 | mobile = questionary.confirm("Include mobile app template?", default=False).ask() or False |
| 142 | web_presence = questionary.confirm("Include web presence / marketing site?", default=False).ask() or False |
| 143 | |
| 144 | # ── Step 10: Compliance ─────────────────────────────────────────────────── |
| 145 | compliance_answer = questionary.checkbox( |
| 146 | "Compliance requirements:", |
| 147 | choices=["soc2", "hipaa", "pci-dss", "gdpr", "none"], |
| 148 | ).ask() |
| 149 | compliance = [c for c in (compliance_answer or []) if c != "none"] |
| 150 | |
| 151 | # ── Step 11: Email provider ─────────────────────────────────────────────── |
| 152 | email_answer = questionary.select( |
| 153 | "Email provider:", |
| 154 | choices=["ses", "sendgrid", "mailgun", "none"], |
| 155 | ).ask() |
| 156 | email = email_answer if email_answer != "none" else None |
| 157 | |
| 158 | # ── Step 12: E2E testing ────────────────────────────────────────────────── |
| 159 | e2e_answer = questionary.select( |
| 160 | "End-to-end testing framework:", |
| 161 | choices=["playwright", "cypress", "none"], |
| 162 | ).ask() |
| 163 | e2e = e2e_answer if e2e_answer != "none" else None |
| 164 | |
| 165 | # ── Step 13: Summary + confirm ──────────────────────────────────────────── |
| 166 | summary_lines = [ |
| 167 | f" [bold]Project:[/bold] {project}", |
| 168 | f" [bold]Template:[/bold] {family} ({size})", |
| 169 | f" [bold]Topology:[/bold] {topology}", |
| 170 | f" [bold]Cloud:[/bold] {cloud or 'none'}", |
| 171 | f" [bold]Ops:[/bold] {'yes' if ops else 'no'}", |
| 172 | f" [bold]Region:[/bold] {region or '—'}", |
| 173 | f" [bold]Domain:[/bold] {domain or '—'}", |
| 174 | ] |
| 175 | if size == "full": |
| 176 | summary_lines += [ |
| 177 | f" [bold]Mobile:[/bold] {'yes' if mobile else 'no'}", |
| 178 | f" [bold]Web:[/bold] {'yes' if web_presence else 'no'}", |
| 179 | ] |
| 180 | summary_lines += [ |
| 181 | f" [bold]Compliance:[/bold] {', '.join(compliance) if compliance else 'none'}", |
| 182 | f" [bold]Email:[/bold] {email or 'none'}", |
| 183 | f" [bold]E2E:[/bold] {e2e or 'none'}", |
| 184 | ] |
| 185 | console.print(Panel("\n".join(summary_lines), title="[bold]Summary[/bold]", border_style="green")) |
| 186 | |
| 187 | confirmed = questionary.confirm("Write boilerworks.yaml?", default=True).ask() |
| 188 | if not confirmed: |
| 189 | console.print("[yellow]Cancelled — nothing written.[/yellow]") |
| 190 | sys.exit(0) |
| 191 | |
| 192 | manifest = BoilerworksManifest( |
| 193 | project=project, |
| 194 | family=family, |
| 195 | size=size, |
| 196 | topology=topology, |
| 197 | cloud=cloud, |
| 198 | ops=ops, |
| 199 | region=region, |
| 200 | domain=domain, |
| 201 | mobile=mobile, |
| 202 | web_presence=web_presence, |
| 203 | compliance=compliance, |
| 204 | services=ServicesConfig(email=email), |
| 205 | data=DataConfig(), |
| 206 | testing=TestingConfig(e2e=e2e), |
| 207 | ) |
| 208 | |
| 209 | out = Path(output_path) |
| 210 | manifest.to_file(out) |
| 211 | print_success(f"Written to [bold]{out}[/bold]") |
| 212 | console.print("\n[dim]Next:[/dim] [bold]boilerworks init[/bold]") |
+306
| --- a/data/templates.yaml | ||
| +++ b/data/templates.yaml | ||
| @@ -0,0 +1,306 @@ | ||
| 1 | +# Boilerworks Template Catalogue | |
| 2 | +# 26 templates across Full / Micro / Edge sizes | |
| 3 | + | |
| 4 | +# ─── Full Templates (15) ────────────────────────────────────────────────────── | |
| 5 | + | |
| 6 | +- name: django-nextjs | |
| 7 | + repo: ConflictHQ/boilerworks-django-nextjs | |
| 8 | + size: full | |
| 9 | + language: python | |
| 10 | + backend: "Django 5" | |
| 11 | + frontend: "Next.js 16" | |
| 12 | + status: done | |
| 13 | + description: "Data-heavy backends, admin-rich, rapid prototyping" | |
| 14 | + topologies: [standard, omni, api-only] | |
| 15 | + best_for: "Python teams wanting batteries-included backend with rich SPA frontend" | |
| 16 | + | |
| 17 | +- name: nestjs-nextjs | |
| 18 | + repo: ConflictHQ/boilerworks-nestjs-nextjs | |
| 19 | + size: full | |
| 20 | + language: typescript | |
| 21 | + backend: "NestJS 11" | |
| 22 | + frontend: "Next.js 16" | |
| 23 | + status: done | |
| 24 | + description: "Full TypeScript, enterprise-ish" | |
| 25 | + topologies: [standard, omni, api-only] | |
| 26 | + best_for: "TypeSbuildingt-first teams building enterprise applications" | |
| 27 | + | |
| 28 | +- nall | |
| 29 | + language: ruby | |
| 30 | + backend: "Rails 8" | |
| 31 | + frontend: "Hotwire (Turbo + Stimulus)" | |
| 32 | + status: done | |
| 33 | + description: "Marketplace, social, content, CMS" | |
| 34 | + building | |
| 35 | + description: "anting Rails-native reactive UI" | |
| 36 | + | |
| 37 | +- name: rails-nextjs | |
| 38 | + repo: ConflictHQ/boilerworks-rails-nextjs | |
| 39 | + size: full | |
| 40 | + language: ruby | |
| 41 | + backend: "Rails 8" | |
| 42 | + frontend: "Next.js 16" | |
| 43 | + status: done | |
| 44 | + description: "Rails backend, richer frontend than Hotwire" | |
| 45 | + topologies: [standard, abuilding | |
| 46 | + description: "needs exceed Hotwire" | |
| 47 | + | |
| 48 | +- name: spring-angular | |
| 49 | + repo: ConflictHQ/boilerworks-spring-angular | |
| 50 | + size: full | |
| 51 | + language: java | |
| 52 | + backenme: rails-hotwire | |
| 53 | + repo: ConflictHQ/boilerworks-rails-hotwire | |
| 54 | + size: full | |
| 55 | + language: ruby | |
| 56 | + backend: "Rails 8" | |
| 57 | + frontend: "Hotwire (Turbo + Stimulus)" | |
| 58 | + statking, or fintech" | |
| 59 | + | |
| 60 | +- name:: "Marketplace, social, content, CMS" | |
| 61 | + building | |
| 62 | + description: "anting Rails-native reactive UI" | |
| 63 | + | |
| 64 | +ipt | |
| 65 | + backend: "NestJS 11" | |
| 66 | + frontend: "Next.js 16" | |
| 67 | + status: done | |
| 68 | + descripti1andard, omni, api-only] | |
| 69 | + best_for: "TypeSbuildingt-first teams building enterprise applications" | |
| 70 | + | |
| 71 | +- name: rails-hotwire | |
| 72 | + repo: ConflictHQ/boilerworks-rails-hotwire | |
| 73 | + size: full | |
| 74 | + language: ruby | |
| 75 | + backend: "Angular 19" | |
| 76 | + status: done | |
| 77 | + description: "Enterprise, banking, fintech" | |
| 78 | + topologies: [standard] | |
| 79 | + best_for: "Java tbuilding | |
| 80 | + description: "anking, or fintech" | |
| 81 | + | |
| 82 | +- name: go-nextjs | |
| 83 | + repo: ConflictHQ/boilerworks-go-nextjs | |
| 84 | + size: full | |
| 85 | + language: go | |
| 86 | + backend: "Go + Chi" | |
| 87 | + frontend: "Next.js 16" | |
| 88 | + status: done | |
| 89 | + descre | |
| 90 | + description: "High-performance Rust microservice" | |
| 91 | + topologies: [standard] | |
| 92 | + best_for: "Maximum performance microservices in Rust" | |
| 93 | + | |
| 94 | +- name: cherrypy-micro | |
| 95 | + repo: ConflictHQ/boilerworks-cherrypy-micro | |
| 96 | + size: micro | |
| 97 | + language: python | |
| 98 | + backend: "CherryPy" | |
| 99 | + frontend:angular | |
| 100 | + repo: ConflictHQ/boilerworks-spring-angular | |
| 101 | + size: full | |
| 102 | + language: java | |
| 103 | + backend: "Spring Boot 3" | |
| 104 | + frontend: "Angular 19" | |
| 105 | + status: done | |
| 106 | + descriptze: micro | |
| 107 | + language: pyting, fintech" | |
| 108 | + topologies: [standard] | |
| 109 | + best_for: "Java tbuilding | |
| 110 | + description: "anking, or finte# Boilerworks Template Catalogue | |
| 111 | +d: "Go + Chi" | |
| 112 | + fronten/Echoteams buil an API-first approach with rich SPA" | |
| 113 | + | |
| 114 | +- name: phoenix-liveview | |
| 115 | + repo: ConflictHQ/boilerworks-phoenix-liveview | |
| 116 | + size: full | |
| 117 | + language: elixir | |
| 118 | + backend: "Phoenix 1.7" | |
| 119 | + frontend: "Live- name: rails-nextjs | |
| 120 | + repo: ConflictHQ/boilerworks-rails-nextjs | |
| 121 | + size: full | |
| 122 | + language: ruby | |
| 123 | + backend: "Rails 8" | |
| 124 | + frontend: "Next.js 16" | |
| 125 | + ze: micro | |
| 126 | + language: pyer frontend than Hotwire" | |
| 127 | + topologies: [standard, abuilding | |
| 128 | + description: "needs exceed Hotwire" | |
| 129 | + | |
| 130 | +- name: spring-angular | |
| 131 | + repo: ConflictHQ/boilerwoliveview | |
| 132 | + repo: ConflictHQ/boilerworks-phoenix-liveview | |
| 133 | + size: full | |
| 134 | + language: elixir | |
| 135 | + backend: "Phoenix 1.7" | |
| 136 | + frontend: "LiveView" | |
| 137 | + status: done | |
| 138 | + dze: micro | |
| 139 | + language: py# Boilerworks Template Catalogue | |
| 140 | +# 26 templates across Full / Micro / Edge sizes | |
| 141 | + | |
| 142 | +# ─── Full | |
| 143 | +# 26 templates across Full / Micro / Edge sizes | |
| 144 | + | |
| 145 | +# ─── Full Templates (15) ────────────────────────────────────────────────────── | |
| 146 | + | |
| 147 | +- name: django-nextjs | |
| 148 | + repo: ConTegular 19" | |
| 149 | + status: done | |
| 150 | + description: "Enterprise, banking, fintech" | |
| 151 | + topologies: [standard] | |
| 152 | + best_for: "Java tbuilding | |
| 153 | + description: "anking, or fintech" | |
| 154 | + | |
| 155 | +- name: go-nextjs | |
| 156 | + repo: ConflictHQ/boilerworks-go-nextjs | |
| 157 | + size: full | |
| 158 | + language: go | |
| 159 | + backend: "Go + Chi" | |
| 160 | + frontend: "Next.js 16" | |
| 161 | + status: done | |
| 162 | + description: "API-first Go, rich frontend" | |
| 163 | + topologies: [standard, api-only] | |
| 164 | + best_for: "Go teams wanting an Aplannedt-first teams buil an API-first approach with rich SPA" | |
| 165 | + | |
| 166 | +- name: phoenix-liveview | |
| 167 | + repo: ConflictHQ/boilerworks-phoenix-liveview | |
| 168 | + size: full | |
| 169 | + language: elixir | |
| 170 | + backend: "Phoenix 1.7" | |
| 171 | + frontend: "LiveView" | |
| 172 | + status: done | |
| 173 | + description: "Real-time, collaborative" | |
| 174 | + topologies: [standard] | |
| 175 | + best_foplanned | |
| 176 | + description: "atus: done | |
| 177 | + description: "Modern enterprise, non-Angular" | |
| 178 | + topologies: [standard, api-only] | |
| 179 | + best_for: "Java teams preferring React over Angular" | |
| 180 | + | |
| 181 | +- name: laravel-livewire | |
| 182 | + repo: ConflictHQ/boilerworks-laravel-livewire | |
| 183 | + size: full | |
| 184 | + language: php | |
| 185 | + backend: "Laravel 12" | |
| 186 | + fronteplanned | |
| 187 | + description: "St_for: "PHP teams wantn: "Server-rendered reactive PHP" | |
| 188 | + topologies: [standard] | |
| 189 | + best_for: "PHP teams wanting server-rendered reactivity without JS" | |
| 190 | + | |
| 191 | +- name: fastapi-htmx | |
| 192 | + repo: ConflictHQ/boilerworks-fastapi-htmx | |
| 193 | + size: full | |
| 194 | + language: python | |
| 195 | + backend: "FastAPI" | |
| 196 | + frontend: "Hplannedt-first teams builightweight Python, server-rendered" | |
| 197 | + topologies: [standard] | |
| 198 | + best_for: "FastAPI teams preferring server-rendered HTML" | |
| 199 | + | |
| 200 | +# ─── Micro Templates (6) ────────────────────────────────────────────────────── | |
| 201 | + | |
| 202 | +- name: django-micro | |
| 203 | + repo: ConflictHQ/boilerworks-django-micro | |
| 204 | + size: micro | |
| 205 | + language: python | |
| 206 | + backend: "Django 5 (DRF/Ninja)" | |
| 207 | + frontend: "" | |
| 208 | + status: done | |
| 209 | + description: "API-only Python service" | |
| 210 | + topologies: [standard] | |
| 211 | + best_for: "Python microservices with Django's ORM and admin" | |
| 212 | + | |
| 213 | +- name: fastapi-micro | |
| 214 | + repo: ConflictHQ/boilerworks-fastapi-micro | |
| 215 | + size: micro | |
| 216 | + language: python | |
| 217 | + backend: "FastAPI" | |
| 218 | + frontend: "" | |
| 219 | + status: done | |
| 220 | + description: "Lightweight async Python API" | |
| 221 | + topologies: [standard] | |
| 222 | + best_for: "High-performance async Python microservices" | |
| 223 | + | |
| 224 | +- name: nestjs-micro | |
| 225 | + repo: ConflictHQ/boilerworks-nestjs-micro | |
| 226 | + size: micro | |
| 227 | + language: typescript | |
| 228 | + backend: "NestJS 11" | |
| 229 | + frontend: "" | |
| 230 | + status: done | |
| 231 | + description: "API-only TypeScript service" | |
| 232 | + topologies: [standard] | |
| 233 | + best_for: "TypeScript microservices with NestJS DI and modules" | |
| 234 | + | |
| 235 | +- name: go-micro | |
| 236 | + repo: ConflictHQ/boilerworks-go-micro | |
| 237 | + size: micro | |
| 238 | + language: go | |
| 239 | + backend: "Go + Chi" | |
| 240 | + frontend: "" | |
| 241 | + status: done | |
| 242 | + description: "Lightweight Go service" | |
| 243 | + topologies: [standard] | |
| 244 | + best_for: "High-throughput Go microservices" | |
| 245 | + | |
| 246 | +- name: rust-micro | |
| 247 | + repo: ConflictHQ/boilerworks-rust-micro | |
| 248 | + size: micro | |
| 249 | + language: rust | |
| 250 | + backend: "Axum (Rust)" | |
| 251 | + frontend: "" | |
| 252 | + status: done | |
| 253 | + description: "High-performance Rust microservice" | |
| 254 | + topologies: [standard] | |
| 255 | + best_for: "Maximum performance microservices in Rust" | |
| 256 | + | |
| 257 | +- name: cherrypy-micro | |
| 258 | + repo: ConflictHQ/boilerworks-cherrypy-micro | |
| 259 | + size: micro | |
| 260 | + language: python | |
| 261 | + backend: "CherryPy" | |
| 262 | + frontend: "" | |
| 263 | + status: planned | |
| 264 | + description: "Pureor: "High-performanc───────────────────────────────────────────── | |
| 265 | + | |
| 266 | +- name: django-micro | |
| 267 | + repo: ConflictHQ/boilerworks-django-micro | |
| 268 | + size: micro | |
| 269 | + language: python | |
| 270 | + backend: "Django 5 (DRF/Ninja)" | |
| 271 | + frontend: "" | |
| 272 | + status: done | |
| 273 | + description: "API-only Python service" | |
| 274 | + topologies: [standard] | |
| 275 | + best_for: "Python microservices with Django's ORM and admin" | |
| 276 | + | |
| 277 | +- name: fastapi-micrplanned | |
| 278 | + description: "boilerworks-fastapi-micro | |
| 279 | + size: micro | |
| 280 | + language: python | |
| 281 | + backend: "FastAPI" | |
| 282 | + frontend: "" | |
| 283 | + status: done | |
| 284 | + description: "Lightweight async Python API" | |
| 285 | + topologies: [standard] | |
| 286 | + best_for: "High-performance async Python microservices" | |
| 287 | + | |
| 288 | +- name: nestjs-micro | |
| 289 | + repo: ConflictHQ/boilerworks-nestjs-micro | |
| 290 | + size: micro | |
| 291 | + language: typescript | |
| 292 | + backend: "NestJS 11" | |
| 293 | + frontend: "" | |
| 294 | + status: done | |
| 295 | + description: "API-only TypeScript service" | |
| 296 | + topologies: [standard] | |
| 297 | + best_for: "TypeScript microservices with NestJS DI and modules" | |
| 298 | + | |
| 299 | +- name: go-mplanned | |
| 300 | + description: "HQ/boilerworks-go-micro | |
| 301 | + size: micro | |
| 302 | + language: go | |
| 303 | + backend: "Go + Chi" | |
| 304 | + frontend: "" | |
| 305 | + status: done | |
| 306 | + description: " |
| --- a/data/templates.yaml | |
| +++ b/data/templates.yaml | |
| @@ -0,0 +1,306 @@ | |
| --- a/data/templates.yaml | |
| +++ b/data/templates.yaml | |
| @@ -0,0 +1,306 @@ | |
| 1 | # Boilerworks Template Catalogue |
| 2 | # 26 templates across Full / Micro / Edge sizes |
| 3 | |
| 4 | # ─── Full Templates (15) ────────────────────────────────────────────────────── |
| 5 | |
| 6 | - name: django-nextjs |
| 7 | repo: ConflictHQ/boilerworks-django-nextjs |
| 8 | size: full |
| 9 | language: python |
| 10 | backend: "Django 5" |
| 11 | frontend: "Next.js 16" |
| 12 | status: done |
| 13 | description: "Data-heavy backends, admin-rich, rapid prototyping" |
| 14 | topologies: [standard, omni, api-only] |
| 15 | best_for: "Python teams wanting batteries-included backend with rich SPA frontend" |
| 16 | |
| 17 | - name: nestjs-nextjs |
| 18 | repo: ConflictHQ/boilerworks-nestjs-nextjs |
| 19 | size: full |
| 20 | language: typescript |
| 21 | backend: "NestJS 11" |
| 22 | frontend: "Next.js 16" |
| 23 | status: done |
| 24 | description: "Full TypeScript, enterprise-ish" |
| 25 | topologies: [standard, omni, api-only] |
| 26 | best_for: "TypeSbuildingt-first teams building enterprise applications" |
| 27 | |
| 28 | - nall |
| 29 | language: ruby |
| 30 | backend: "Rails 8" |
| 31 | frontend: "Hotwire (Turbo + Stimulus)" |
| 32 | status: done |
| 33 | description: "Marketplace, social, content, CMS" |
| 34 | building |
| 35 | description: "anting Rails-native reactive UI" |
| 36 | |
| 37 | - name: rails-nextjs |
| 38 | repo: ConflictHQ/boilerworks-rails-nextjs |
| 39 | size: full |
| 40 | language: ruby |
| 41 | backend: "Rails 8" |
| 42 | frontend: "Next.js 16" |
| 43 | status: done |
| 44 | description: "Rails backend, richer frontend than Hotwire" |
| 45 | topologies: [standard, abuilding |
| 46 | description: "needs exceed Hotwire" |
| 47 | |
| 48 | - name: spring-angular |
| 49 | repo: ConflictHQ/boilerworks-spring-angular |
| 50 | size: full |
| 51 | language: java |
| 52 | backenme: rails-hotwire |
| 53 | repo: ConflictHQ/boilerworks-rails-hotwire |
| 54 | size: full |
| 55 | language: ruby |
| 56 | backend: "Rails 8" |
| 57 | frontend: "Hotwire (Turbo + Stimulus)" |
| 58 | statking, or fintech" |
| 59 | |
| 60 | - name:: "Marketplace, social, content, CMS" |
| 61 | building |
| 62 | description: "anting Rails-native reactive UI" |
| 63 | |
| 64 | ipt |
| 65 | backend: "NestJS 11" |
| 66 | frontend: "Next.js 16" |
| 67 | status: done |
| 68 | descripti1andard, omni, api-only] |
| 69 | best_for: "TypeSbuildingt-first teams building enterprise applications" |
| 70 | |
| 71 | - name: rails-hotwire |
| 72 | repo: ConflictHQ/boilerworks-rails-hotwire |
| 73 | size: full |
| 74 | language: ruby |
| 75 | backend: "Angular 19" |
| 76 | status: done |
| 77 | description: "Enterprise, banking, fintech" |
| 78 | topologies: [standard] |
| 79 | best_for: "Java tbuilding |
| 80 | description: "anking, or fintech" |
| 81 | |
| 82 | - name: go-nextjs |
| 83 | repo: ConflictHQ/boilerworks-go-nextjs |
| 84 | size: full |
| 85 | language: go |
| 86 | backend: "Go + Chi" |
| 87 | frontend: "Next.js 16" |
| 88 | status: done |
| 89 | descre |
| 90 | description: "High-performance Rust microservice" |
| 91 | topologies: [standard] |
| 92 | best_for: "Maximum performance microservices in Rust" |
| 93 | |
| 94 | - name: cherrypy-micro |
| 95 | repo: ConflictHQ/boilerworks-cherrypy-micro |
| 96 | size: micro |
| 97 | language: python |
| 98 | backend: "CherryPy" |
| 99 | frontend:angular |
| 100 | repo: ConflictHQ/boilerworks-spring-angular |
| 101 | size: full |
| 102 | language: java |
| 103 | backend: "Spring Boot 3" |
| 104 | frontend: "Angular 19" |
| 105 | status: done |
| 106 | descriptze: micro |
| 107 | language: pyting, fintech" |
| 108 | topologies: [standard] |
| 109 | best_for: "Java tbuilding |
| 110 | description: "anking, or finte# Boilerworks Template Catalogue |
| 111 | d: "Go + Chi" |
| 112 | fronten/Echoteams buil an API-first approach with rich SPA" |
| 113 | |
| 114 | - name: phoenix-liveview |
| 115 | repo: ConflictHQ/boilerworks-phoenix-liveview |
| 116 | size: full |
| 117 | language: elixir |
| 118 | backend: "Phoenix 1.7" |
| 119 | frontend: "Live- name: rails-nextjs |
| 120 | repo: ConflictHQ/boilerworks-rails-nextjs |
| 121 | size: full |
| 122 | language: ruby |
| 123 | backend: "Rails 8" |
| 124 | frontend: "Next.js 16" |
| 125 | ze: micro |
| 126 | language: pyer frontend than Hotwire" |
| 127 | topologies: [standard, abuilding |
| 128 | description: "needs exceed Hotwire" |
| 129 | |
| 130 | - name: spring-angular |
| 131 | repo: ConflictHQ/boilerwoliveview |
| 132 | repo: ConflictHQ/boilerworks-phoenix-liveview |
| 133 | size: full |
| 134 | language: elixir |
| 135 | backend: "Phoenix 1.7" |
| 136 | frontend: "LiveView" |
| 137 | status: done |
| 138 | dze: micro |
| 139 | language: py# Boilerworks Template Catalogue |
| 140 | # 26 templates across Full / Micro / Edge sizes |
| 141 | |
| 142 | # ─── Full |
| 143 | # 26 templates across Full / Micro / Edge sizes |
| 144 | |
| 145 | # ─── Full Templates (15) ────────────────────────────────────────────────────── |
| 146 | |
| 147 | - name: django-nextjs |
| 148 | repo: ConTegular 19" |
| 149 | status: done |
| 150 | description: "Enterprise, banking, fintech" |
| 151 | topologies: [standard] |
| 152 | best_for: "Java tbuilding |
| 153 | description: "anking, or fintech" |
| 154 | |
| 155 | - name: go-nextjs |
| 156 | repo: ConflictHQ/boilerworks-go-nextjs |
| 157 | size: full |
| 158 | language: go |
| 159 | backend: "Go + Chi" |
| 160 | frontend: "Next.js 16" |
| 161 | status: done |
| 162 | description: "API-first Go, rich frontend" |
| 163 | topologies: [standard, api-only] |
| 164 | best_for: "Go teams wanting an Aplannedt-first teams buil an API-first approach with rich SPA" |
| 165 | |
| 166 | - name: phoenix-liveview |
| 167 | repo: ConflictHQ/boilerworks-phoenix-liveview |
| 168 | size: full |
| 169 | language: elixir |
| 170 | backend: "Phoenix 1.7" |
| 171 | frontend: "LiveView" |
| 172 | status: done |
| 173 | description: "Real-time, collaborative" |
| 174 | topologies: [standard] |
| 175 | best_foplanned |
| 176 | description: "atus: done |
| 177 | description: "Modern enterprise, non-Angular" |
| 178 | topologies: [standard, api-only] |
| 179 | best_for: "Java teams preferring React over Angular" |
| 180 | |
| 181 | - name: laravel-livewire |
| 182 | repo: ConflictHQ/boilerworks-laravel-livewire |
| 183 | size: full |
| 184 | language: php |
| 185 | backend: "Laravel 12" |
| 186 | fronteplanned |
| 187 | description: "St_for: "PHP teams wantn: "Server-rendered reactive PHP" |
| 188 | topologies: [standard] |
| 189 | best_for: "PHP teams wanting server-rendered reactivity without JS" |
| 190 | |
| 191 | - name: fastapi-htmx |
| 192 | repo: ConflictHQ/boilerworks-fastapi-htmx |
| 193 | size: full |
| 194 | language: python |
| 195 | backend: "FastAPI" |
| 196 | frontend: "Hplannedt-first teams builightweight Python, server-rendered" |
| 197 | topologies: [standard] |
| 198 | best_for: "FastAPI teams preferring server-rendered HTML" |
| 199 | |
| 200 | # ─── Micro Templates (6) ────────────────────────────────────────────────────── |
| 201 | |
| 202 | - name: django-micro |
| 203 | repo: ConflictHQ/boilerworks-django-micro |
| 204 | size: micro |
| 205 | language: python |
| 206 | backend: "Django 5 (DRF/Ninja)" |
| 207 | frontend: "" |
| 208 | status: done |
| 209 | description: "API-only Python service" |
| 210 | topologies: [standard] |
| 211 | best_for: "Python microservices with Django's ORM and admin" |
| 212 | |
| 213 | - name: fastapi-micro |
| 214 | repo: ConflictHQ/boilerworks-fastapi-micro |
| 215 | size: micro |
| 216 | language: python |
| 217 | backend: "FastAPI" |
| 218 | frontend: "" |
| 219 | status: done |
| 220 | description: "Lightweight async Python API" |
| 221 | topologies: [standard] |
| 222 | best_for: "High-performance async Python microservices" |
| 223 | |
| 224 | - name: nestjs-micro |
| 225 | repo: ConflictHQ/boilerworks-nestjs-micro |
| 226 | size: micro |
| 227 | language: typescript |
| 228 | backend: "NestJS 11" |
| 229 | frontend: "" |
| 230 | status: done |
| 231 | description: "API-only TypeScript service" |
| 232 | topologies: [standard] |
| 233 | best_for: "TypeScript microservices with NestJS DI and modules" |
| 234 | |
| 235 | - name: go-micro |
| 236 | repo: ConflictHQ/boilerworks-go-micro |
| 237 | size: micro |
| 238 | language: go |
| 239 | backend: "Go + Chi" |
| 240 | frontend: "" |
| 241 | status: done |
| 242 | description: "Lightweight Go service" |
| 243 | topologies: [standard] |
| 244 | best_for: "High-throughput Go microservices" |
| 245 | |
| 246 | - name: rust-micro |
| 247 | repo: ConflictHQ/boilerworks-rust-micro |
| 248 | size: micro |
| 249 | language: rust |
| 250 | backend: "Axum (Rust)" |
| 251 | frontend: "" |
| 252 | status: done |
| 253 | description: "High-performance Rust microservice" |
| 254 | topologies: [standard] |
| 255 | best_for: "Maximum performance microservices in Rust" |
| 256 | |
| 257 | - name: cherrypy-micro |
| 258 | repo: ConflictHQ/boilerworks-cherrypy-micro |
| 259 | size: micro |
| 260 | language: python |
| 261 | backend: "CherryPy" |
| 262 | frontend: "" |
| 263 | status: planned |
| 264 | description: "Pureor: "High-performanc───────────────────────────────────────────── |
| 265 | |
| 266 | - name: django-micro |
| 267 | repo: ConflictHQ/boilerworks-django-micro |
| 268 | size: micro |
| 269 | language: python |
| 270 | backend: "Django 5 (DRF/Ninja)" |
| 271 | frontend: "" |
| 272 | status: done |
| 273 | description: "API-only Python service" |
| 274 | topologies: [standard] |
| 275 | best_for: "Python microservices with Django's ORM and admin" |
| 276 | |
| 277 | - name: fastapi-micrplanned |
| 278 | description: "boilerworks-fastapi-micro |
| 279 | size: micro |
| 280 | language: python |
| 281 | backend: "FastAPI" |
| 282 | frontend: "" |
| 283 | status: done |
| 284 | description: "Lightweight async Python API" |
| 285 | topologies: [standard] |
| 286 | best_for: "High-performance async Python microservices" |
| 287 | |
| 288 | - name: nestjs-micro |
| 289 | repo: ConflictHQ/boilerworks-nestjs-micro |
| 290 | size: micro |
| 291 | language: typescript |
| 292 | backend: "NestJS 11" |
| 293 | frontend: "" |
| 294 | status: done |
| 295 | description: "API-only TypeScript service" |
| 296 | topologies: [standard] |
| 297 | best_for: "TypeScript microservices with NestJS DI and modules" |
| 298 | |
| 299 | - name: go-mplanned |
| 300 | description: "HQ/boilerworks-go-micro |
| 301 | size: micro |
| 302 | language: go |
| 303 | backend: "Go + Chi" |
| 304 | frontend: "" |
| 305 | status: done |
| 306 | description: " |
+62
| --- a/pyproject.toml | ||
| +++ b/pyproject.toml | ||
| @@ -0,0 +1,62 @@ | ||
| 1 | +[build-system] | |
| 2 | +requires = ["hatchling"] | |
| 3 | +build-backend = "hatchling.build" | |
| 4 | + | |
| 5 | +[project] | |
| 6 | +name = "boilerworks" | |
| 7 | +version = "0.1.0" | |
| 8 | +description = "Production-ready project templates — assembled in seconds." | |
| 9 | +readme = "README.md" | |
| 10 | +requires-python = ">=3.12" | |
| 11 | +license = { text = "MIT" } | |
| 12 | +keywords = ["boilerplate", "templates", "scaffolding", "cli"] | |
| 13 | +classifiers = [ | |
| 14 | + "Development Status :: 3 - Alpha", | |
| 15 | + "Environment :: Console", | |
| 16 | + "Intended Audience :: Developers", | |
| 17 | + "License :: OSI Approved :: MIT License", | |
| 18 | + "Programming Language :: Python :: 3", | |
| 19 | + "Programming Language :: Python :: 3.12", | |
| 20 | + "Topic :: Software Development :: Code Generators", | |
| 21 | +] | |
| 22 | +dependencies = [ | |
| 23 | + "click>=8.1", | |
| 24 | + "questionary>=2.0", | |
| 25 | + "rich>=13.0", | |
| 26 | + "pydantic>=2.0", | |
| 27 | + "jinja2>=3.0", | |
| 28 | + "gitpython>=3.1", | |
| 29 | + "mcp[cli]>=1.0"] | |
| 30 | + | |
| 31 | +[ | |
| 32 | +[project.urls] | |
| 33 | +Homepage = "https://boilerworks.dev" | |
| 34 | +Repository = "https://github.com/ConflictHQ/boilerworks" | |
| 35 | +Issues = "https://github.com/ConflictHQ/boilerworks/issues" | |
| 36 | + | |
| 37 | +[tool.hatch.build.targets.wheel] | |
| 38 | +packages = ["boilerworks"] | |
| 39 | + | |
| 40 | +[tool.hatch.build.targets.sdist] | |
| 41 | +include = ["boilerworks/", "tests/"] | |
| 42 | + | |
| 43 | +[dependency-groups] | |
| 44 | +dev = [ | |
| 45 | + "pytest>=8.0", | |
| 46 | + "pytest-cov>=5.0", | |
| 47 | + "ruff>=0.9", | |
| 48 | +] | |
| 49 | + | |
| 50 | +[tool.ruff] | |
| 51 | +target-version = "py312" | |
| 52 | +line-length = 120 | |
| 53 | + | |
| 54 | +[tool.ruff.lint] | |
| 55 | +select = ["E", "F", "I", "W", "UP", "B", "SIM", "N"] | |
| 56 | + | |
| 57 | +[tool.pytest.ini_options] | |
| 58 | +testpaths = ["tests"] | |
| 59 | +addopts = "--cov=boilerworks --cov-report=term-missing --cov-fail-under=80" | |
| 60 | +filterwarnings = ["ignore::pytest.PytestCollectionWarning"] | |
| 61 | + | |
| 62 | +[tool.covera |
| --- a/pyproject.toml | |
| +++ b/pyproject.toml | |
| @@ -0,0 +1,62 @@ | |
| --- a/pyproject.toml | |
| +++ b/pyproject.toml | |
| @@ -0,0 +1,62 @@ | |
| 1 | [build-system] |
| 2 | requires = ["hatchling"] |
| 3 | build-backend = "hatchling.build" |
| 4 | |
| 5 | [project] |
| 6 | name = "boilerworks" |
| 7 | version = "0.1.0" |
| 8 | description = "Production-ready project templates — assembled in seconds." |
| 9 | readme = "README.md" |
| 10 | requires-python = ">=3.12" |
| 11 | license = { text = "MIT" } |
| 12 | keywords = ["boilerplate", "templates", "scaffolding", "cli"] |
| 13 | classifiers = [ |
| 14 | "Development Status :: 3 - Alpha", |
| 15 | "Environment :: Console", |
| 16 | "Intended Audience :: Developers", |
| 17 | "License :: OSI Approved :: MIT License", |
| 18 | "Programming Language :: Python :: 3", |
| 19 | "Programming Language :: Python :: 3.12", |
| 20 | "Topic :: Software Development :: Code Generators", |
| 21 | ] |
| 22 | dependencies = [ |
| 23 | "click>=8.1", |
| 24 | "questionary>=2.0", |
| 25 | "rich>=13.0", |
| 26 | "pydantic>=2.0", |
| 27 | "jinja2>=3.0", |
| 28 | "gitpython>=3.1", |
| 29 | "mcp[cli]>=1.0"] |
| 30 | |
| 31 | [ |
| 32 | [project.urls] |
| 33 | Homepage = "https://boilerworks.dev" |
| 34 | Repository = "https://github.com/ConflictHQ/boilerworks" |
| 35 | Issues = "https://github.com/ConflictHQ/boilerworks/issues" |
| 36 | |
| 37 | [tool.hatch.build.targets.wheel] |
| 38 | packages = ["boilerworks"] |
| 39 | |
| 40 | [tool.hatch.build.targets.sdist] |
| 41 | include = ["boilerworks/", "tests/"] |
| 42 | |
| 43 | [dependency-groups] |
| 44 | dev = [ |
| 45 | "pytest>=8.0", |
| 46 | "pytest-cov>=5.0", |
| 47 | "ruff>=0.9", |
| 48 | ] |
| 49 | |
| 50 | [tool.ruff] |
| 51 | target-version = "py312" |
| 52 | line-length = 120 |
| 53 | |
| 54 | [tool.ruff.lint] |
| 55 | select = ["E", "F", "I", "W", "UP", "B", "SIM", "N"] |
| 56 | |
| 57 | [tool.pytest.ini_options] |
| 58 | testpaths = ["tests"] |
| 59 | addopts = "--cov=boilerworks --cov-report=term-missing --cov-fail-under=80" |
| 60 | filterwarnings = ["ignore::pytest.PytestCollectionWarning"] |
| 61 | |
| 62 | [tool.covera |
+27
| --- a/tests/conftest.py | ||
| +++ b/tests/conftest.py | ||
| @@ -0,0 +1,27 @@ | ||
| 1 | +"""Shared pytest fixtures.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import pytest | |
| 6 | + | |
| 7 | +from boilerworks.manifest import BoilerworksManifest, DataConfig, ServicesConfig | |
| 8 | +from boilerworks.manifest import TestingConfig as TestingCfg | |
| 9 | + | |
| 10 | + | |
| 11 | +@pytest.fixture() | |
| 12 | +def valid_manifest() -> BoilerworksManifest: | |
| 13 | + return BoilerworksManifest( | |
| 14 | + project="my-app", | |
| 15 | + family="django-nextjs", | |
| 16 | + size="full", | |
| 17 | + topology="standard", | |
| 18 | + cloud="aws", | |
| 19 | + region="us-east-1", | |
| 20 | + domain="myapp.com", | |
| 21 | + mobile=False, | |
| 22 | + web_presence=False, | |
| 23 | + compliance=["soc2"], | |
| 24 | + services=ServicesConfig(email="ses"), | |
| 25 | + data=DataConfig(), | |
| 26 | + testing=TestingCfg(e2e="playwright"), | |
| 27 | + ) |
| --- a/tests/conftest.py | |
| +++ b/tests/conftest.py | |
| @@ -0,0 +1,27 @@ | |
| --- a/tests/conftest.py | |
| +++ b/tests/conftest.py | |
| @@ -0,0 +1,27 @@ | |
| 1 | """Shared pytest fixtures.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import pytest |
| 6 | |
| 7 | from boilerworks.manifest import BoilerworksManifest, DataConfig, ServicesConfig |
| 8 | from boilerworks.manifest import TestingConfig as TestingCfg |
| 9 | |
| 10 | |
| 11 | @pytest.fixture() |
| 12 | def valid_manifest() -> BoilerworksManifest: |
| 13 | return BoilerworksManifest( |
| 14 | project="my-app", |
| 15 | family="django-nextjs", |
| 16 | size="full", |
| 17 | topology="standard", |
| 18 | cloud="aws", |
| 19 | region="us-east-1", |
| 20 | domain="myapp.com", |
| 21 | mobile=False, |
| 22 | web_presence=False, |
| 23 | compliance=["soc2"], |
| 24 | services=ServicesConfig(email="ses"), |
| 25 | data=DataConfig(), |
| 26 | testing=TestingCfg(e2e="playwright"), |
| 27 | ) |
+104
| --- a/tests/test_cli.py | ||
| +++ b/tests/test_cli.py | ||
| @@ -0,0 +1,104 @@ | ||
| 1 | +"""Tests for boilerworks.cli — Click command interface.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +from click.testing import CliRunner | |
| 6 | + | |
| 7 | +from boilerworks.cli import main | |
| 8 | + | |
| 9 | + | |
| 10 | +class TestHelpOutput: | |
| 11 | + def test_main_help(self) -> None: | |
| 12 | + runner = CliRunner() | |
| 13 | + result = runner.invoke(main, ["--help"]) | |
| 14 | + assert result.exit_code == 0 | |
| 15 | + assert "setup" in result.output | |
| 16 | + assert "init" in result.output | |
| 17 | + assert "bootstrap" in result.output | |
| 18 | + assert "list" in result.output | |
| 19 | + | |
| 20 | + def test_list_help(self) -> None: | |
| 21 | + runner = CliRunner() | |
| 22 | + result = runner.invoke(main, ["list", "--help"]) | |
| 23 | + assert result.exit_code == 0 | |
| 24 | + assert "--size" in result.output | |
| 25 | + assert "--language" in result.output | |
| 26 | + assert "--status" in result.output | |
| 27 | + | |
| 28 | + def test_init_help(self) -> None: | |
| 29 | + runner = CliRunner() | |
| 30 | + result = runner.invoke(main, ["init", "--help"]) | |
| 31 | + assert result.exit_code == 0 | |
| 32 | + assert "--manifest" in result.output | |
| 33 | + assert "--output" in result.output | |
| 34 | + assert "--dry-run" in result.output | |
| 35 | + | |
| 36 | + def test_bootstrap_help(self) -> None: | |
| 37 | + runner = CliRunner() | |
| 38 | + result = runner.invoke(main, ["bootstrap", "--help"]) | |
| 39 | + assert result.exit_code == 0 | |
| 40 | + | |
| 41 | + def test_version(self) -> None: | |
| 42 | + runner = CliRunner() | |
| 43 | + result = runner.invoke(main, ["--version"]) | |
| 44 | + assert result.exit_code == 0 | |
| 45 | + assert "0.1.0" in result.output | |
| 46 | + | |
| 47 | + | |
| 48 | +class TestListCommand: | |
| 49 | + def test_list_all(self) -> None: | |
| 50 | + runner = CliRunner() | |
| 51 | + result = runner.invoke(main, ["list"]) | |
| 52 | + assert result.exit_code == 0 | |
| 53 | + assert "django-nextjs" in result.output | |
| 54 | + assert "fastapi-micro" in result.output | |
| 55 | + assert "astro-site" in result.output | |
| 56 | + | |
| 57 | + def test_list_filter_size_micro(self) -> None: | |
| 58 | + runner = CliRunner() | |
| 59 | + result = runner.invoke(main, ["list", "--size", "micro"]) | |
| 60 | + assert result.exit_code == 0 | |
| 61 | + assert "fastapi-micro" in result.output | |
| 62 | + assert "django-nextjs" not in result.output | |
| 63 | + | |
| 64 | + def test_list_filter_size_edge(self) -> None: | |
| 65 | + runner = CliRunner() | |
| 66 | + result = runner.invoke(main, ["list", "--size", "edge"]) | |
| 67 | + assert result.exit_code == 0 | |
| 68 | + assert "astro-site" in result.output | |
| 69 | + assert "django-nextjs" not in result.output | |
| 70 | + | |
| 71 | + def test_list_filter_language_python(self) -> None: | |
| 72 | + runner = CliRunner() | |
| 73 | + result = runner.invoke(main, ["list", "--language", "python"]) | |
| 74 | + assert result.exit_code == 0 | |
| 75 | + assert "django-nextjs" in result.output | |
| 76 | + assert "nestjs-nextjs" not in result.output | |
| 77 | + | |
| 78 | + def test_list_filter_size_and_language(self) -> None: | |
| 79 | + runner = CliRunner() | |
| 80 | + result = runner.invoke(main, ["list", "--size", "micro", "--language", "python"]) | |
| 81 | + assert result.exit_code == 0 | |
| 82 | + assert "fastapi-micro" in result.output | |
| 83 | + assert "nestjs-micro" not in result.output | |
| 84 | + | |
| 85 | + def test_list_filter_status_done(self) -> None: | |
| 86 | + runner = CliRunner() | |
| 87 | + result = runner.invoke(main, ["list", "--status", "done"]) | |
| 88 | + assert result.exit_code == 0 | |
| 89 | + assert "django-nextjs" in result.output | |
| 90 | + | |
| 91 | + def test_list_no_results_shows_message(self) -> None: | |
| 92 | + runner = CliRunner() | |
| 93 | + # rust + full → no results | |
| 94 | + result = runner.invoke(main, ["list", "--size", "full", "--language", "rust"]) | |
| 95 | + assert result.exit_code == 0 | |
| 96 | + assert "No templates match" in result.output | |
| 97 | + | |
| 98 | + | |
| 99 | +class TestBootstrapCommand: | |
| 100 | + def test_bootstrap_runs(self) -> None: | |
| 101 | + runner = CliRunner() | |
| 102 | + result = runner.invoke(main, ["bootstrap"]) | |
| 103 | + assert result.exit_code == 0 | |
| 104 | + assert "Bootstrap Plan" in result.output or "v1" in result.output or "v2" in result.output |
| --- a/tests/test_cli.py | |
| +++ b/tests/test_cli.py | |
| @@ -0,0 +1,104 @@ | |
| --- a/tests/test_cli.py | |
| +++ b/tests/test_cli.py | |
| @@ -0,0 +1,104 @@ | |
| 1 | """Tests for boilerworks.cli — Click command interface.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from click.testing import CliRunner |
| 6 | |
| 7 | from boilerworks.cli import main |
| 8 | |
| 9 | |
| 10 | class TestHelpOutput: |
| 11 | def test_main_help(self) -> None: |
| 12 | runner = CliRunner() |
| 13 | result = runner.invoke(main, ["--help"]) |
| 14 | assert result.exit_code == 0 |
| 15 | assert "setup" in result.output |
| 16 | assert "init" in result.output |
| 17 | assert "bootstrap" in result.output |
| 18 | assert "list" in result.output |
| 19 | |
| 20 | def test_list_help(self) -> None: |
| 21 | runner = CliRunner() |
| 22 | result = runner.invoke(main, ["list", "--help"]) |
| 23 | assert result.exit_code == 0 |
| 24 | assert "--size" in result.output |
| 25 | assert "--language" in result.output |
| 26 | assert "--status" in result.output |
| 27 | |
| 28 | def test_init_help(self) -> None: |
| 29 | runner = CliRunner() |
| 30 | result = runner.invoke(main, ["init", "--help"]) |
| 31 | assert result.exit_code == 0 |
| 32 | assert "--manifest" in result.output |
| 33 | assert "--output" in result.output |
| 34 | assert "--dry-run" in result.output |
| 35 | |
| 36 | def test_bootstrap_help(self) -> None: |
| 37 | runner = CliRunner() |
| 38 | result = runner.invoke(main, ["bootstrap", "--help"]) |
| 39 | assert result.exit_code == 0 |
| 40 | |
| 41 | def test_version(self) -> None: |
| 42 | runner = CliRunner() |
| 43 | result = runner.invoke(main, ["--version"]) |
| 44 | assert result.exit_code == 0 |
| 45 | assert "0.1.0" in result.output |
| 46 | |
| 47 | |
| 48 | class TestListCommand: |
| 49 | def test_list_all(self) -> None: |
| 50 | runner = CliRunner() |
| 51 | result = runner.invoke(main, ["list"]) |
| 52 | assert result.exit_code == 0 |
| 53 | assert "django-nextjs" in result.output |
| 54 | assert "fastapi-micro" in result.output |
| 55 | assert "astro-site" in result.output |
| 56 | |
| 57 | def test_list_filter_size_micro(self) -> None: |
| 58 | runner = CliRunner() |
| 59 | result = runner.invoke(main, ["list", "--size", "micro"]) |
| 60 | assert result.exit_code == 0 |
| 61 | assert "fastapi-micro" in result.output |
| 62 | assert "django-nextjs" not in result.output |
| 63 | |
| 64 | def test_list_filter_size_edge(self) -> None: |
| 65 | runner = CliRunner() |
| 66 | result = runner.invoke(main, ["list", "--size", "edge"]) |
| 67 | assert result.exit_code == 0 |
| 68 | assert "astro-site" in result.output |
| 69 | assert "django-nextjs" not in result.output |
| 70 | |
| 71 | def test_list_filter_language_python(self) -> None: |
| 72 | runner = CliRunner() |
| 73 | result = runner.invoke(main, ["list", "--language", "python"]) |
| 74 | assert result.exit_code == 0 |
| 75 | assert "django-nextjs" in result.output |
| 76 | assert "nestjs-nextjs" not in result.output |
| 77 | |
| 78 | def test_list_filter_size_and_language(self) -> None: |
| 79 | runner = CliRunner() |
| 80 | result = runner.invoke(main, ["list", "--size", "micro", "--language", "python"]) |
| 81 | assert result.exit_code == 0 |
| 82 | assert "fastapi-micro" in result.output |
| 83 | assert "nestjs-micro" not in result.output |
| 84 | |
| 85 | def test_list_filter_status_done(self) -> None: |
| 86 | runner = CliRunner() |
| 87 | result = runner.invoke(main, ["list", "--status", "done"]) |
| 88 | assert result.exit_code == 0 |
| 89 | assert "django-nextjs" in result.output |
| 90 | |
| 91 | def test_list_no_results_shows_message(self) -> None: |
| 92 | runner = CliRunner() |
| 93 | # rust + full → no results |
| 94 | result = runner.invoke(main, ["list", "--size", "full", "--language", "rust"]) |
| 95 | assert result.exit_code == 0 |
| 96 | assert "No templates match" in result.output |
| 97 | |
| 98 | |
| 99 | class TestBootstrapCommand: |
| 100 | def test_bootstrap_runs(self) -> None: |
| 101 | runner = CliRunner() |
| 102 | result = runner.invoke(main, ["bootstrap"]) |
| 103 | assert result.exit_code == 0 |
| 104 | assert "Bootstrap Plan" in result.output or "v1" in result.output or "v2" in result.output |
+103
| --- a/tests/test_console.py | ||
| +++ b/tests/test_console.py | ||
| @@ -0,0 +1,103 @@ | ||
| 1 | +"""Tests for boilerworks.console.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +from io import StringIO | |
| 6 | + | |
| 7 | +from rich.console import Console | |
| 8 | + | |
| 9 | +from boilerworks.console import ( | |
| 10 | + print_error, | |
| 11 | + print_info, | |
| 12 | + print_success, | |
| 13 | + print_template_detail, | |
| 14 | + print_template_table, | |
| 15 | + print_warning, | |
| 16 | +) | |
| 17 | +from boilerworks.registry import Registry, TemplateInfo | |
| 18 | + | |
| 19 | + | |
| 20 | +def _capture(fn, *args, **kwargs) -> str: | |
| 21 | + """Call fn with a capturing console and return the output.""" | |
| 22 | + buf = StringIO() | |
| 23 | + cap = Console(file=buf, highlight=False, markup=True) | |
| 24 | + # Temporarily override the module-level console | |
| 25 | + import boilerworks.console as _mod | |
| 26 | + | |
| 27 | + original = _mod.console | |
| 28 | + _mod.console = cap | |
| 29 | + try: | |
| 30 | + fn(*args, **kwargs) | |
| 31 | + finally: | |
| 32 | + _mod.console = original | |
| 33 | + return buf.getvalue() | |
| 34 | + | |
| 35 | + | |
| 36 | +class TestPrintTemplateTable: | |
| 37 | + def test_shows_all_templates(self) -> None: | |
| 38 | + registry = Registry() | |
| 39 | + templates = registry.list_all() | |
| 40 | + output = _capture(print_template_table, templates) | |
| 41 | + assert "django-nextjs" in output | |
| 42 | + assert "astro-site" in output | |
| 43 | + | |
| 44 | + def test_shows_26_count(self) -> None: | |
| 45 | + registry = Registry() | |
| 46 | + templates = registry.list_all() | |
| 47 | + output = _capture(print_template_table, templates) | |
| 48 | + assert "26" in output | |
| 49 | + | |
| 50 | + def test_empty_list_shows_message(self) -> None: | |
| 51 | + output = _capture(print_template_table, []) | |
| 52 | + assert "No templates match" in output | |
| 53 | + | |
| 54 | + def test_filtered_table(self) -> None: | |
| 55 | + registry = Registry() | |
| 56 | + templates = registry.filter_by_size("micro") | |
| 57 | + output = _capture(print_template_table, templates) | |
| 58 | + assert "fastapi-micro" in output | |
| 59 | + assert "django-nextjs" not in output | |
| 60 | + | |
| 61 | + | |
| 62 | +class TestPrintTemplateDetail: | |
| 63 | + def test_shows_template_fields(self) -> None: | |
| 64 | + registry = Registry() | |
| 65 | + template = registry.get_by_name("django-nextjs") | |
| 66 | + assert template is not None | |
| 67 | + output = _capture(print_template_detail, template) | |
| 68 | + assert "django-nextjs" in output | |
| 69 | + assert "python" in output.lower() | |
| 70 | + assert "Django" in output | |
| 71 | + | |
| 72 | + def test_template_without_frontend(self) -> None: | |
| 73 | + # Micro templates have no frontend | |
| 74 | + t = TemplateInfo( | |
| 75 | + name="test-micro", | |
| 76 | + repo="ConflictHQ/test", | |
| 77 | + size="micro", | |
| 78 | + language="python", | |
| 79 | + backend="FastAPI", | |
| 80 | + frontend="", | |
| 81 | + status="planned", | |
| 82 | + description="Test", | |
| 83 | + ) | |
| 84 | + output = _capture(print_template_detail, t) | |
| 85 | + assert "test-micro" in output | |
| 86 | + | |
| 87 | + | |
| 88 | +class TestPrintHelpers: | |
| 89 | + def test_print_success(self) -> None: | |
| 90 | + output = _capture(print_success, "All good") | |
| 91 | + assert "All good" in output | |
| 92 | + | |
| 93 | + def test_print_error(self) -> None: | |
| 94 | + output = _capture(print_error, "Something failed") | |
| 95 | + assert "Something failed" in output | |
| 96 | + | |
| 97 | + def test_print_info(self) -> None: | |
| 98 | + output = _capture(print_info, "Just FYI") | |
| 99 | + assert "Just FYI" in output | |
| 100 | + | |
| 101 | + def test_print_warning(self) -> None: | |
| 102 | + output = _capture(print_warning, "Be careful") | |
| 103 | + assert "Be careful" in output |
| --- a/tests/test_console.py | |
| +++ b/tests/test_console.py | |
| @@ -0,0 +1,103 @@ | |
| --- a/tests/test_console.py | |
| +++ b/tests/test_console.py | |
| @@ -0,0 +1,103 @@ | |
| 1 | """Tests for boilerworks.console.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from io import StringIO |
| 6 | |
| 7 | from rich.console import Console |
| 8 | |
| 9 | from boilerworks.console import ( |
| 10 | print_error, |
| 11 | print_info, |
| 12 | print_success, |
| 13 | print_template_detail, |
| 14 | print_template_table, |
| 15 | print_warning, |
| 16 | ) |
| 17 | from boilerworks.registry import Registry, TemplateInfo |
| 18 | |
| 19 | |
| 20 | def _capture(fn, *args, **kwargs) -> str: |
| 21 | """Call fn with a capturing console and return the output.""" |
| 22 | buf = StringIO() |
| 23 | cap = Console(file=buf, highlight=False, markup=True) |
| 24 | # Temporarily override the module-level console |
| 25 | import boilerworks.console as _mod |
| 26 | |
| 27 | original = _mod.console |
| 28 | _mod.console = cap |
| 29 | try: |
| 30 | fn(*args, **kwargs) |
| 31 | finally: |
| 32 | _mod.console = original |
| 33 | return buf.getvalue() |
| 34 | |
| 35 | |
| 36 | class TestPrintTemplateTable: |
| 37 | def test_shows_all_templates(self) -> None: |
| 38 | registry = Registry() |
| 39 | templates = registry.list_all() |
| 40 | output = _capture(print_template_table, templates) |
| 41 | assert "django-nextjs" in output |
| 42 | assert "astro-site" in output |
| 43 | |
| 44 | def test_shows_26_count(self) -> None: |
| 45 | registry = Registry() |
| 46 | templates = registry.list_all() |
| 47 | output = _capture(print_template_table, templates) |
| 48 | assert "26" in output |
| 49 | |
| 50 | def test_empty_list_shows_message(self) -> None: |
| 51 | output = _capture(print_template_table, []) |
| 52 | assert "No templates match" in output |
| 53 | |
| 54 | def test_filtered_table(self) -> None: |
| 55 | registry = Registry() |
| 56 | templates = registry.filter_by_size("micro") |
| 57 | output = _capture(print_template_table, templates) |
| 58 | assert "fastapi-micro" in output |
| 59 | assert "django-nextjs" not in output |
| 60 | |
| 61 | |
| 62 | class TestPrintTemplateDetail: |
| 63 | def test_shows_template_fields(self) -> None: |
| 64 | registry = Registry() |
| 65 | template = registry.get_by_name("django-nextjs") |
| 66 | assert template is not None |
| 67 | output = _capture(print_template_detail, template) |
| 68 | assert "django-nextjs" in output |
| 69 | assert "python" in output.lower() |
| 70 | assert "Django" in output |
| 71 | |
| 72 | def test_template_without_frontend(self) -> None: |
| 73 | # Micro templates have no frontend |
| 74 | t = TemplateInfo( |
| 75 | name="test-micro", |
| 76 | repo="ConflictHQ/test", |
| 77 | size="micro", |
| 78 | language="python", |
| 79 | backend="FastAPI", |
| 80 | frontend="", |
| 81 | status="planned", |
| 82 | description="Test", |
| 83 | ) |
| 84 | output = _capture(print_template_detail, t) |
| 85 | assert "test-micro" in output |
| 86 | |
| 87 | |
| 88 | class TestPrintHelpers: |
| 89 | def test_print_success(self) -> None: |
| 90 | output = _capture(print_success, "All good") |
| 91 | assert "All good" in output |
| 92 | |
| 93 | def test_print_error(self) -> None: |
| 94 | output = _capture(print_error, "Something failed") |
| 95 | assert "Something failed" in output |
| 96 | |
| 97 | def test_print_info(self) -> None: |
| 98 | output = _capture(print_info, "Just FYI") |
| 99 | assert "Just FYI" in output |
| 100 | |
| 101 | def test_print_warning(self) -> None: |
| 102 | output = _capture(print_warning, "Be careful") |
| 103 | assert "Be careful" in output |
+306
| --- a/tests/test_generator.py | ||
| +++ b/tests/test_generator.py | ||
| @@ -0,0 +1,306 @@ | ||
| 1 | +"""Tests for boilerworks.generator.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import shutil | |
| 6 | +from pathlib import Path | |
| 7 | +from unittest.mock import MagicMock, patch | |
| 8 | + | |
| 9 | +import pytest | |
| 10 | + | |
| 11 | +from boilerworks.generator import ( | |
| 12 | + _clone_and_render_ops, | |
| 13 | + _dry_run_plan, | |
| 14 | + _write_ops_config, | |
| 15 | + generate_from_manifest, | |
| 16 | +) | |
| 17 | +from boilerworks.manifest import BoilerworksManifest | |
| 18 | + | |
| 19 | + | |
| 20 | +class TestDryRun: | |
| 21 | + def test_dry_run_no_files_created(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None: | |
| 22 | + """Dry-run should print the plan without touching the filesystem.""" | |
| 23 | + project_dir = tmp_path / valid_manifest.project | |
| 24 | + _dry_run_plan(valid_manifest, tmp_path) | |
| 25 | + assert not project_dir.exists() | |
| 26 | + | |
| 27 | + def test_generate_from_manifest_dry_run(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None: | |
| 28 | + """generate_from_manifest with dry_run=True must not create any files.""" | |
| 29 | + manifest_file = tmp_path / "boilerworks.yaml" | |
| 30 | + valid_manifest.to_file(manifest_file) | |
| 31 | + | |
| 32 | + project_dir = tmp_path / valid_manifest.project | |
| 33 | + generate_from_manifest( | |
| 34 | + manifest_path=str(manifest_file), | |
| 35 | + output_dir=str(tmp_path), | |
| 36 | + dry_run=True, | |
| 37 | + ) | |
| 38 | + assert not project_dir.exists() | |
| 39 | + | |
| 40 | + def test_dry_run_with_ops_standard(self, tmp_path: Path) -> None: | |
| 41 | + """Dry-run with cloud + ops shows ops clone step (standard topology).""" | |
| 42 | + manifest = BoilerworksManifest( | |
| 43 | + project="test-app", | |
| 44 | + family="django-nextjs", | |
| 45 | + size="full", | |
| 46 | + cloud="aws", | |
| 47 | + ops=True, | |
| 48 | + topology="standard", | |
| 49 | + ) | |
| 50 | + _dry_run_plan(manifest, tmp_path) # should not raise | |
| 51 | + | |
| 52 | + def test_dry_run_with_ops_omni(self, tmp_path: Path) -> None: | |
| 53 | + """Dry-run with cloud + ops shows ops clone step (omni topology).""" | |
| 54 | + manifest = BoilerworksManifest( | |
| 55 | + project="test-app", | |
| 56 | + family="django-nextjs", | |
| 57 | + size="full", | |
| 58 | + cloud="gcp", | |
| 59 | + ops=True, | |
| 60 | + topology="omni", | |
| 61 | + ) | |
| 62 | + _dry_run_plan(manifest, tmp_path) # should not raise | |
| 63 | + | |
| 64 | + def test_dry_run_no_ops_when_flag_false(self, tmp_path: Path) -> None: | |
| 65 | + """Dry-run with cloud set but ops=False does not include ops steps.""" | |
| 66 | + manifest = BoilerworksManifest( | |
| 67 | + project="test-app", | |
| 68 | + family="django-nextjs", | |
| 69 | + size="full", | |
| 70 | + cloud="aws", | |
| 71 | + ops=False, | |
| 72 | + ) | |
| 73 | + _dry_run_plan(manifest, tmp_path) # should not raise | |
| 74 | + | |
| 75 | + def test_dry_run_shows_mobile_step(self, tmp_path: Path) -> None: | |
| 76 | + manifest = BoilerworksManifest( | |
| 77 | + project="test-app", | |
| 78 | + family="django-nextjs", | |
| 79 | + size="full", | |
| 80 | + mobile=True, | |
| 81 | + ) | |
| 82 | + _dry_run_plan(manifest, tmp_path) | |
| 83 | + | |
| 84 | + | |
| 85 | +class TestWriteOpsConfig: | |
| 86 | + def _make_ops_dir(self, tmp_path: Path, cloud: str) -> Path: | |
| 87 | + ops_dir = tmp_path / "ops" | |
| 88 | + cloud_dir = ops_dir / cloud | |
| 89 | + cloud_dir.mkdir(parents=True) | |
| 90 | + config = cloud_dir / "config.env" | |
| 91 | + config.write_text('PROJECT="boilerworks"\nAWS_REGION="us-west-2"\nOWNER="conflict"\n') | |
| 92 | + return ops_dir | |
| 93 | + | |
| 94 | + def test_aws_config_written(self, tmp_path: Path) -> None: | |
| 95 | + ops_dir = self._make_ops_dir(tmp_path, "aws") | |
| 96 | + _write_ops_config(ops_dir, "aws", "myproject", "eu-west-1", "myproject.com") | |
| 97 | + | |
| 98 | + content = (ops_dir / "aws" / "config.env").read_text() | |
| 99 | + assert 'PROJECT="myproject"' in content | |
| 100 | + assert 'AWS_REGION="eu-west-1"' in content | |
| 101 | + assert 'OWNER="myproject"' in content | |
| 102 | + assert 'DOMAIN="myproject.com"' in content | |
| 103 | + | |
| 104 | + def test_gcp_config_written(self, tmp_path: Path) -> None: | |
| 105 | + ops_dir = self._make_ops_dir(tmp_path, "gcp") | |
| 106 | + _write_ops_config(ops_dir, "gcp", "myproject", "us-central1", None) | |
| 107 | + | |
| 108 | + content = (ops_dir / "gcp" / "config.env").read_text() | |
| 109 | + assert 'PROJECT="myproject"' in content | |
| 110 | + assert 'GCP_REGION="us-central1"' in content | |
| 111 | + assert "DOMAIN" not in content | |
| 112 | + | |
| 113 | + def test_azure_config_default_region(self, tmp_path: Path) -> None: | |
| 114 | + ops_dir = self._make_ops_dir(tmp_path, "azure") | |
| 115 | + _write_ops_config(ops_dir, "azure", "myproject", None, None) | |
| 116 | + | |
| 117 | + content = (ops_dir / "azure" / "config.env").read_text() | |
| 118 | + assert 'AZURE_REGION="eastus"' in content | |
| 119 | + | |
| 120 | + def test_missing_config_file_is_noop(self, tmp_path: Path) -> None: | |
| 121 | + """If config.env doesn't exist yet, write_ops_config should not raise.""" | |
| 122 | + ops_dir = tmp_path / "ops" | |
| 123 | + ops_dir.mkdir() | |
| 124 | + (ops_dir / "aws").mkdir() | |
| 125 | + # No config.env file | |
| 126 | + _write_ops_config(ops_dir, "aws", "myproject", "us-east-1", None) | |
| 127 | + | |
| 128 | + | |
| 129 | +class TestCloneAndRenderOps: | |
| 130 | + def _fake_clone(self, src: Path) -> None: | |
| 131 | + """Create a fake clone that looks like a minimal boilerworks-opscode.""" | |
| 132 | + src.mkdir(parents=True, exist_ok=True) | |
| 133 | + (src / ".git").mkdir() | |
| 134 | + (src / "aws").mkdir() | |
| 135 | + (src / "aws" / "config.env").write_text('PROJECT="boilerworks"\nAWS_REGION="us-west-2"\nOWNER="conflict"\n') | |
| 136 | + (src / "README.md").write_text("# Boilerworks Opscode\nBoilerworks infrastructure.\n") | |
| 137 | + | |
| 138 | + def test_ops_clone_and_render_standard(self, tmp_path: Path) -> None: | |
| 139 | + """_clone_and_render_ops populates dest and renders project name.""" | |
| 140 | + ops_dest = tmp_path / "myproject-ops" | |
| 141 | + | |
| 142 | + def fake_clone(repo: str, dest: Path) -> None: | |
| 143 | + self._fake_clone(dest) | |
| 144 | + | |
| 145 | + progress = MagicMock() | |
| 146 | + progress.add_task.return_value = "task-id" | |
| 147 | + | |
| 148 | + with patch("boilerworks.generator._clone_repo", side_effect=fake_clone): | |
| 149 | + _clone_and_render_ops("myproject", "aws", "us-east-1", "myproject.com", ops_dest, progress) | |
| 150 | + | |
| 151 | + assert ops_dest.exists() | |
| 152 | + assert not (ops_dest / ".git").exists() | |
| 153 | + config_content = (ops_dest / "aws" / "config.env").read_text() | |
| 154 | + assert 'PROJECT="myproject"' in config_content | |
| 155 | + | |
| 156 | + def test_ops_clone_failure_exits(self, tmp_path: Path) -> None: | |
| 157 | + """When cloning ops fails, process exits.""" | |
| 158 | + ops_dest = tmp_path / "myproject-ops" | |
| 159 | + progress = MagicMock() | |
| 160 | + progress.add_task.return_valueself, tmp_path: Path) -> Non._clone_repo", side_effect=Ru: | |
| 161 | + f, tmp_path: Path) -> Non _clone_and_render_ops("myproNone, ops_dest, progress) | |
| 162 | + | |
| 163 | +="myproject"' in co""Tests for boilerworks.generator.""" | |
| 164 | + | |
| 165 | +from __future__ import annotations | |
| 166 | + | |
| 167 | +import shutil | |
| 168 | +from pathlib import Path | |
| 169 | +from unittest.mock import MagicMock, patch | |
| 170 | + | |
| 171 | +import pytest | |
| 172 | + | |
| 173 | +from boilerworks.generator import ( | |
| 174 | + _clone_and_render_ops, | |
| 175 | + _dry_run_plan, | |
| 176 | + _write_ops_config, | |
| 177 | + generate_from_manifest, | |
| 178 | +) | |
| 179 | +from boilerworks.manifest import BoilerworksManifest | |
| 180 | + | |
| 181 | + | |
| 182 | +class TestDryRun: | |
| 183 | + def test_dry_run_no_files_created(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None: | |
| 184 | + """Dry-run should print the plan without touching the filesystem.""" | |
| 185 | + project_dir = tmp_path / valid_manifest.project | |
| 186 | + _dry_run_plan(valid_manifest, tmp_path) | |
| 187 | + assert not project_dir.exists() | |
| 188 | + | |
| 189 | + def test_generate_from_manifest_dry_run(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None: | |
| 190 | + """generate_from_manifest with dry_run=True must not create any files.""" | |
| 191 | + manifest_file = tmp_path / "boilerworks.yaml" | |
| 192 | + valid_manifest.to_file(manifest_file) | |
| 193 | + | |
| 194 | + project_dir = tmp_path / valid_manifest.projec"""Tests for boilerworks.generator.""" | |
| 195 | + | |
| 196 | +from __future__ import ann: | |
| 197 | + "Tests foilerworkssubprocess.run"): | |
| 198 | + il | |
| 199 | +from """Tests for boilerworks.generator.""" | |
| 200 | + | |
| 201 | +from __future__ import annotations | |
| 202 | + | |
| 203 | +import shutil | |
| 204 | +fr"ath / "myapp-ops").exists() | |
| 205 | + assert not (tmp_path / "myapp-ops" / ".git").exists() | |
| 206 | + | |
| 207 | + def test_generate_omni_with_ops(self, tmp_path: PMagicMock, patch | |
| 208 | + | |
| 209 | +i"""Tests for boilerworks.g(tmp_path / "myapp-ops").exists() | |
| 210 | + assert not (tmp_path / "myapp-ops" / ".git").exists() | |
| 211 | + | |
| 212 | + def test_generate_omni_with_ops(self, tmp_path: Path) -> None: | |
| 213 | + """Omni topology: ops/ lives inside the app directory.""" | |
| 214 | + manifest = BoilerworksManifest( | |
| 215 | + project="myapp", | |
| 216 | + family="django-nextjs", | |
| 217 | + size="full", | |
| 218 | + topology="omni", | |
| 219 | + cloud="aws", | |
| 220 | + ops=True, | |
| 221 | + region="us-east-1", | |
| 222 | + ) | |
| 223 | + manifest_file = tmp_path / "boilerworks.yaml" | |
| 224 | + manifest.to_file(manifest_file) | |
| 225 | + | |
| 226 | + call_count = 0 | |
| 227 | + | |
| 228 | + def fake_clone(repo: str, dest: Path) -> None: | |
| 229 | + nonlocal call_count | |
| 230 | + call_count += 1 | |
| 231 | + if "opscode" in repo: | |
| 232 | + self._seed_opscode(dest) | |
| 233 | + else: | |
| 234 | + self._seed_template(dest) | |
| 235 | + | |
| 236 | + with self, tmp_path: Path) -> None: | |
| 237 | + """Omni topology: ops/ live: | |
| 238 | + "Tests foilerworkssubprocess.run"): | |
| 239 | + il | |
| 240 | +from """Tests for boilerworks.generator.""" | |
| 241 | + | |
| 242 | +from __future__ import annotations | |
| 243 | + | |
| 244 | +import shutil | |
| 245 | +fr"ath / "myapp-ops").exists() | |
| 246 | + assert not (tmp_path / "myapp-ops" / ".git").exists() | |
| 247 | + | |
| 248 | + def test_generate_omni_with_ops(self, tmp_path: PMagicMock, patch | |
| 249 | + | |
| 250 | +i"""Tests for boilerworks.gMagicMock, patch | |
| 251 | + | |
| 252 | +import pytest | |
| 253 | + | |
| 254 | +from boilerworks.generator import ( | |
| 255 | + _clone_and_render_ops, | |
| 256 | + _dry_run_plan, | |
| 257 | + _write_ops_config, | |
| 258 | + generate_from_manifest, | |
| 259 | +) | |
| 260 | +from boilerworks.manifest import BoilerworksManifest | |
| 261 | + | |
| 262 | + | |
| 263 | +class TestDryRun: | |
| 264 | + def test_dry_run_no_files_created(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None: | |
| 265 | + """Dry-run should print the plan without touching the filesystem.""" | |
| 266 | + project_dir = tmp_path / valid_manifest.project | |
| 267 | + _dry_run_plan(valid_manifest, tmp_path) | |
| 268 | + assert not project_dir.exists() | |
| 269 | + | |
| 270 | + def test_generate_from_manifest_dry_run(self, tmp_path: Path, valid_manifest: BoilerworksManifest) ->"""Tests for boilerworks.generator.""" | |
| 271 | + | |
| 272 | +from __future__ import ann: | |
| 273 | + "Tests foilerworkssubprocess.run"): | |
| 274 | + il | |
| 275 | +from """Tests for boilerworks.generator.""" | |
| 276 | + | |
| 277 | +from __future__ import annotations | |
| 278 | + | |
| 279 | +import shutil | |
| 280 | +from pathlib import Path | |
| 281 | +from unittest.mock import MagicMock, patch | |
| 282 | + | |
| 283 | +import pytest | |
| 284 | + | |
| 285 | +from boilerworks.generator import ( | |
| 286 | + _clone_and_render_ops, | |
| 287 | + _dry_run_plan, | |
| 288 | + _write_ops_config, | |
| 289 | + generate_from_manifest, | |
| 290 | +) | |
| 291 | +from boilerworks.manifest import BoilerworksManifest | |
| 292 | + | |
| 293 | + | |
| 294 | +class TestDryRun: | |
| 295 | + def test_dry_run_no_files_created(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None: | |
| 296 | + """Dry-run should print the plan without touching the filesystem.""" | |
| 297 | + project_dir = tmp_path / valid_manifest.project | |
| 298 | + _dry_run_plan(valid_manifest, tmp_path) | |
| 299 | + assert not project_dir.exists() | |
| 300 | + | |
| 301 | + def test_generate_from_manifest_dry_run(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None: | |
| 302 | + """generate_from_manifest with dry"""Tests for boilerworks.generator.""" | |
| 303 | + | |
| 304 | +from __future__ import ann: | |
| 305 | + "Tests foilerworkssubprocess.run"): | |
| 306 | + st.to_fi, tmp_path: Path) -> Nonshutil.rmtree(tmp_path / "m |
| --- a/tests/test_generator.py | |
| +++ b/tests/test_generator.py | |
| @@ -0,0 +1,306 @@ | |
| --- a/tests/test_generator.py | |
| +++ b/tests/test_generator.py | |
| @@ -0,0 +1,306 @@ | |
| 1 | """Tests for boilerworks.generator.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import shutil |
| 6 | from pathlib import Path |
| 7 | from unittest.mock import MagicMock, patch |
| 8 | |
| 9 | import pytest |
| 10 | |
| 11 | from boilerworks.generator import ( |
| 12 | _clone_and_render_ops, |
| 13 | _dry_run_plan, |
| 14 | _write_ops_config, |
| 15 | generate_from_manifest, |
| 16 | ) |
| 17 | from boilerworks.manifest import BoilerworksManifest |
| 18 | |
| 19 | |
| 20 | class TestDryRun: |
| 21 | def test_dry_run_no_files_created(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None: |
| 22 | """Dry-run should print the plan without touching the filesystem.""" |
| 23 | project_dir = tmp_path / valid_manifest.project |
| 24 | _dry_run_plan(valid_manifest, tmp_path) |
| 25 | assert not project_dir.exists() |
| 26 | |
| 27 | def test_generate_from_manifest_dry_run(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None: |
| 28 | """generate_from_manifest with dry_run=True must not create any files.""" |
| 29 | manifest_file = tmp_path / "boilerworks.yaml" |
| 30 | valid_manifest.to_file(manifest_file) |
| 31 | |
| 32 | project_dir = tmp_path / valid_manifest.project |
| 33 | generate_from_manifest( |
| 34 | manifest_path=str(manifest_file), |
| 35 | output_dir=str(tmp_path), |
| 36 | dry_run=True, |
| 37 | ) |
| 38 | assert not project_dir.exists() |
| 39 | |
| 40 | def test_dry_run_with_ops_standard(self, tmp_path: Path) -> None: |
| 41 | """Dry-run with cloud + ops shows ops clone step (standard topology).""" |
| 42 | manifest = BoilerworksManifest( |
| 43 | project="test-app", |
| 44 | family="django-nextjs", |
| 45 | size="full", |
| 46 | cloud="aws", |
| 47 | ops=True, |
| 48 | topology="standard", |
| 49 | ) |
| 50 | _dry_run_plan(manifest, tmp_path) # should not raise |
| 51 | |
| 52 | def test_dry_run_with_ops_omni(self, tmp_path: Path) -> None: |
| 53 | """Dry-run with cloud + ops shows ops clone step (omni topology).""" |
| 54 | manifest = BoilerworksManifest( |
| 55 | project="test-app", |
| 56 | family="django-nextjs", |
| 57 | size="full", |
| 58 | cloud="gcp", |
| 59 | ops=True, |
| 60 | topology="omni", |
| 61 | ) |
| 62 | _dry_run_plan(manifest, tmp_path) # should not raise |
| 63 | |
| 64 | def test_dry_run_no_ops_when_flag_false(self, tmp_path: Path) -> None: |
| 65 | """Dry-run with cloud set but ops=False does not include ops steps.""" |
| 66 | manifest = BoilerworksManifest( |
| 67 | project="test-app", |
| 68 | family="django-nextjs", |
| 69 | size="full", |
| 70 | cloud="aws", |
| 71 | ops=False, |
| 72 | ) |
| 73 | _dry_run_plan(manifest, tmp_path) # should not raise |
| 74 | |
| 75 | def test_dry_run_shows_mobile_step(self, tmp_path: Path) -> None: |
| 76 | manifest = BoilerworksManifest( |
| 77 | project="test-app", |
| 78 | family="django-nextjs", |
| 79 | size="full", |
| 80 | mobile=True, |
| 81 | ) |
| 82 | _dry_run_plan(manifest, tmp_path) |
| 83 | |
| 84 | |
| 85 | class TestWriteOpsConfig: |
| 86 | def _make_ops_dir(self, tmp_path: Path, cloud: str) -> Path: |
| 87 | ops_dir = tmp_path / "ops" |
| 88 | cloud_dir = ops_dir / cloud |
| 89 | cloud_dir.mkdir(parents=True) |
| 90 | config = cloud_dir / "config.env" |
| 91 | config.write_text('PROJECT="boilerworks"\nAWS_REGION="us-west-2"\nOWNER="conflict"\n') |
| 92 | return ops_dir |
| 93 | |
| 94 | def test_aws_config_written(self, tmp_path: Path) -> None: |
| 95 | ops_dir = self._make_ops_dir(tmp_path, "aws") |
| 96 | _write_ops_config(ops_dir, "aws", "myproject", "eu-west-1", "myproject.com") |
| 97 | |
| 98 | content = (ops_dir / "aws" / "config.env").read_text() |
| 99 | assert 'PROJECT="myproject"' in content |
| 100 | assert 'AWS_REGION="eu-west-1"' in content |
| 101 | assert 'OWNER="myproject"' in content |
| 102 | assert 'DOMAIN="myproject.com"' in content |
| 103 | |
| 104 | def test_gcp_config_written(self, tmp_path: Path) -> None: |
| 105 | ops_dir = self._make_ops_dir(tmp_path, "gcp") |
| 106 | _write_ops_config(ops_dir, "gcp", "myproject", "us-central1", None) |
| 107 | |
| 108 | content = (ops_dir / "gcp" / "config.env").read_text() |
| 109 | assert 'PROJECT="myproject"' in content |
| 110 | assert 'GCP_REGION="us-central1"' in content |
| 111 | assert "DOMAIN" not in content |
| 112 | |
| 113 | def test_azure_config_default_region(self, tmp_path: Path) -> None: |
| 114 | ops_dir = self._make_ops_dir(tmp_path, "azure") |
| 115 | _write_ops_config(ops_dir, "azure", "myproject", None, None) |
| 116 | |
| 117 | content = (ops_dir / "azure" / "config.env").read_text() |
| 118 | assert 'AZURE_REGION="eastus"' in content |
| 119 | |
| 120 | def test_missing_config_file_is_noop(self, tmp_path: Path) -> None: |
| 121 | """If config.env doesn't exist yet, write_ops_config should not raise.""" |
| 122 | ops_dir = tmp_path / "ops" |
| 123 | ops_dir.mkdir() |
| 124 | (ops_dir / "aws").mkdir() |
| 125 | # No config.env file |
| 126 | _write_ops_config(ops_dir, "aws", "myproject", "us-east-1", None) |
| 127 | |
| 128 | |
| 129 | class TestCloneAndRenderOps: |
| 130 | def _fake_clone(self, src: Path) -> None: |
| 131 | """Create a fake clone that looks like a minimal boilerworks-opscode.""" |
| 132 | src.mkdir(parents=True, exist_ok=True) |
| 133 | (src / ".git").mkdir() |
| 134 | (src / "aws").mkdir() |
| 135 | (src / "aws" / "config.env").write_text('PROJECT="boilerworks"\nAWS_REGION="us-west-2"\nOWNER="conflict"\n') |
| 136 | (src / "README.md").write_text("# Boilerworks Opscode\nBoilerworks infrastructure.\n") |
| 137 | |
| 138 | def test_ops_clone_and_render_standard(self, tmp_path: Path) -> None: |
| 139 | """_clone_and_render_ops populates dest and renders project name.""" |
| 140 | ops_dest = tmp_path / "myproject-ops" |
| 141 | |
| 142 | def fake_clone(repo: str, dest: Path) -> None: |
| 143 | self._fake_clone(dest) |
| 144 | |
| 145 | progress = MagicMock() |
| 146 | progress.add_task.return_value = "task-id" |
| 147 | |
| 148 | with patch("boilerworks.generator._clone_repo", side_effect=fake_clone): |
| 149 | _clone_and_render_ops("myproject", "aws", "us-east-1", "myproject.com", ops_dest, progress) |
| 150 | |
| 151 | assert ops_dest.exists() |
| 152 | assert not (ops_dest / ".git").exists() |
| 153 | config_content = (ops_dest / "aws" / "config.env").read_text() |
| 154 | assert 'PROJECT="myproject"' in config_content |
| 155 | |
| 156 | def test_ops_clone_failure_exits(self, tmp_path: Path) -> None: |
| 157 | """When cloning ops fails, process exits.""" |
| 158 | ops_dest = tmp_path / "myproject-ops" |
| 159 | progress = MagicMock() |
| 160 | progress.add_task.return_valueself, tmp_path: Path) -> Non._clone_repo", side_effect=Ru: |
| 161 | f, tmp_path: Path) -> Non _clone_and_render_ops("myproNone, ops_dest, progress) |
| 162 | |
| 163 | ="myproject"' in co""Tests for boilerworks.generator.""" |
| 164 | |
| 165 | from __future__ import annotations |
| 166 | |
| 167 | import shutil |
| 168 | from pathlib import Path |
| 169 | from unittest.mock import MagicMock, patch |
| 170 | |
| 171 | import pytest |
| 172 | |
| 173 | from boilerworks.generator import ( |
| 174 | _clone_and_render_ops, |
| 175 | _dry_run_plan, |
| 176 | _write_ops_config, |
| 177 | generate_from_manifest, |
| 178 | ) |
| 179 | from boilerworks.manifest import BoilerworksManifest |
| 180 | |
| 181 | |
| 182 | class TestDryRun: |
| 183 | def test_dry_run_no_files_created(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None: |
| 184 | """Dry-run should print the plan without touching the filesystem.""" |
| 185 | project_dir = tmp_path / valid_manifest.project |
| 186 | _dry_run_plan(valid_manifest, tmp_path) |
| 187 | assert not project_dir.exists() |
| 188 | |
| 189 | def test_generate_from_manifest_dry_run(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None: |
| 190 | """generate_from_manifest with dry_run=True must not create any files.""" |
| 191 | manifest_file = tmp_path / "boilerworks.yaml" |
| 192 | valid_manifest.to_file(manifest_file) |
| 193 | |
| 194 | project_dir = tmp_path / valid_manifest.projec"""Tests for boilerworks.generator.""" |
| 195 | |
| 196 | from __future__ import ann: |
| 197 | "Tests foilerworkssubprocess.run"): |
| 198 | il |
| 199 | from """Tests for boilerworks.generator.""" |
| 200 | |
| 201 | from __future__ import annotations |
| 202 | |
| 203 | import shutil |
| 204 | fr"ath / "myapp-ops").exists() |
| 205 | assert not (tmp_path / "myapp-ops" / ".git").exists() |
| 206 | |
| 207 | def test_generate_omni_with_ops(self, tmp_path: PMagicMock, patch |
| 208 | |
| 209 | i"""Tests for boilerworks.g(tmp_path / "myapp-ops").exists() |
| 210 | assert not (tmp_path / "myapp-ops" / ".git").exists() |
| 211 | |
| 212 | def test_generate_omni_with_ops(self, tmp_path: Path) -> None: |
| 213 | """Omni topology: ops/ lives inside the app directory.""" |
| 214 | manifest = BoilerworksManifest( |
| 215 | project="myapp", |
| 216 | family="django-nextjs", |
| 217 | size="full", |
| 218 | topology="omni", |
| 219 | cloud="aws", |
| 220 | ops=True, |
| 221 | region="us-east-1", |
| 222 | ) |
| 223 | manifest_file = tmp_path / "boilerworks.yaml" |
| 224 | manifest.to_file(manifest_file) |
| 225 | |
| 226 | call_count = 0 |
| 227 | |
| 228 | def fake_clone(repo: str, dest: Path) -> None: |
| 229 | nonlocal call_count |
| 230 | call_count += 1 |
| 231 | if "opscode" in repo: |
| 232 | self._seed_opscode(dest) |
| 233 | else: |
| 234 | self._seed_template(dest) |
| 235 | |
| 236 | with self, tmp_path: Path) -> None: |
| 237 | """Omni topology: ops/ live: |
| 238 | "Tests foilerworkssubprocess.run"): |
| 239 | il |
| 240 | from """Tests for boilerworks.generator.""" |
| 241 | |
| 242 | from __future__ import annotations |
| 243 | |
| 244 | import shutil |
| 245 | fr"ath / "myapp-ops").exists() |
| 246 | assert not (tmp_path / "myapp-ops" / ".git").exists() |
| 247 | |
| 248 | def test_generate_omni_with_ops(self, tmp_path: PMagicMock, patch |
| 249 | |
| 250 | i"""Tests for boilerworks.gMagicMock, patch |
| 251 | |
| 252 | import pytest |
| 253 | |
| 254 | from boilerworks.generator import ( |
| 255 | _clone_and_render_ops, |
| 256 | _dry_run_plan, |
| 257 | _write_ops_config, |
| 258 | generate_from_manifest, |
| 259 | ) |
| 260 | from boilerworks.manifest import BoilerworksManifest |
| 261 | |
| 262 | |
| 263 | class TestDryRun: |
| 264 | def test_dry_run_no_files_created(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None: |
| 265 | """Dry-run should print the plan without touching the filesystem.""" |
| 266 | project_dir = tmp_path / valid_manifest.project |
| 267 | _dry_run_plan(valid_manifest, tmp_path) |
| 268 | assert not project_dir.exists() |
| 269 | |
| 270 | def test_generate_from_manifest_dry_run(self, tmp_path: Path, valid_manifest: BoilerworksManifest) ->"""Tests for boilerworks.generator.""" |
| 271 | |
| 272 | from __future__ import ann: |
| 273 | "Tests foilerworkssubprocess.run"): |
| 274 | il |
| 275 | from """Tests for boilerworks.generator.""" |
| 276 | |
| 277 | from __future__ import annotations |
| 278 | |
| 279 | import shutil |
| 280 | from pathlib import Path |
| 281 | from unittest.mock import MagicMock, patch |
| 282 | |
| 283 | import pytest |
| 284 | |
| 285 | from boilerworks.generator import ( |
| 286 | _clone_and_render_ops, |
| 287 | _dry_run_plan, |
| 288 | _write_ops_config, |
| 289 | generate_from_manifest, |
| 290 | ) |
| 291 | from boilerworks.manifest import BoilerworksManifest |
| 292 | |
| 293 | |
| 294 | class TestDryRun: |
| 295 | def test_dry_run_no_files_created(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None: |
| 296 | """Dry-run should print the plan without touching the filesystem.""" |
| 297 | project_dir = tmp_path / valid_manifest.project |
| 298 | _dry_run_plan(valid_manifest, tmp_path) |
| 299 | assert not project_dir.exists() |
| 300 | |
| 301 | def test_generate_from_manifest_dry_run(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None: |
| 302 | """generate_from_manifest with dry"""Tests for boilerworks.generator.""" |
| 303 | |
| 304 | from __future__ import ann: |
| 305 | "Tests foilerworkssubprocess.run"): |
| 306 | st.to_fi, tmp_path: Path) -> Nonshutil.rmtree(tmp_path / "m |
+146
| --- a/tests/test_manifest.py | ||
| +++ b/tests/test_manifest.py | ||
| @@ -0,0 +1,146 @@ | ||
| 1 | +"""Tests for boilerworks.manifest.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import pytest | |
| 6 | +from pydantic import ValidationError | |
| 7 | + | |
| 8 | +from boilerworks.manifest import BoilerworksManifest, DataConfig, ServicesConfig | |
| 9 | +from boilerworks.manifest import TestingConfig as TestingCfg | |
| 10 | + | |
| 11 | + | |
| 12 | +def _make_manifest(**kwargs) -> BoilerworksManifest: | |
| 13 | + defaults = { | |
| 14 | + "project": "my-app", | |
| 15 | + "family": "django-nextjs", | |
| 16 | + "size": "full", | |
| 17 | + } | |
| 18 | + defaults.update(kwargs) | |
| 19 | + return BoilerworksManifest(**defaults) | |
| 20 | + | |
| 21 | + | |
| 22 | +class TestValidManifest: | |
| 23 | + def test_minimal_manifest_passes(self) -> None: | |
| 24 | + m = _make_manifest() | |
| 25 | + assert m.project == "my-app" | |
| 26 | + assert m.family == "django-nextjs" | |
| 27 | + assert m.size == "full" | |
| 28 | + | |
| 29 | + def test_defaults_are_correct(self) -> None: | |
| 30 | + m = _make_manifest() | |
| 31 | + assert m.topology == "standard" | |
| 32 | + assert m.cloud is None | |
| 33 | + assert m.region is None | |
| 34 | + assert m.domain is None | |
| 35 | + assert m.mobile is False | |
| 36 | + assert m.web_presence is False | |
| 37 | + assert m.compliance == [] | |
| 38 | + assert m.template_versions == {} | |
| 39 | + | |
| 40 | + def test_full_manifest_passes(self, valid_manifest: BoilerworksManifest) -> None: | |
| 41 | + assert valid_manifest.project == "my-app" | |
| 42 | + assert valid_manifest.cloud == "aws" | |
| 43 | + assert valid_manifest.region == "us-east-1" | |
| 44 | + | |
| 45 | + def test_micro_template(self) -> None: | |
| 46 | + m = _make_manifest(family="fastapi-micro", size="micro") | |
| 47 | + assert m.size == "micro" | |
| 48 | + | |
| 49 | + def test_edge_template(self) -> None: | |
| 50 | + m = _make_manifest(family="astro-site", size="edge") | |
| 51 | + assert m.size == "edge" | |
| 52 | + | |
| 53 | + | |
| 54 | +class TestProjectSlugValidation: | |
| 55 | + def test_valid_slugs(self) -> None: | |
| 56 | + for slug in ["my-app", "app", "my-app-v2", "a1b2c3"]: | |
| 57 | + m = _make_manifest(project=slug) | |
| 58 | + assert m.project == slug | |
| 59 | + | |
| 60 | + def test_spaces_fail(self) -> None: | |
| 61 | + with pytest.raises(ValidationError, match="project name"): | |
| 62 | + _make_manifest(project="my app") | |
| 63 | + | |
| 64 | + def test_uppercase_fails(self) -> None: | |
| 65 | + with pytest.raises(ValidationError, match="project name"): | |
| 66 | + _make_manifest(project="MyApp") | |
| 67 | + | |
| 68 | + def test_leading_digit_fails(self) -> None: | |
| 69 | + with pytest.raises(ValidationError, match="project name"): | |
| 70 | + _make_manifest(project="1app") | |
| 71 | + | |
| 72 | + def test_underscore_fails(self) -> None: | |
| 73 | + with pytest.raises(ValidationError, match="project name"): | |
| 74 | + _make_manifest(project="my_app") | |
| 75 | + | |
| 76 | + def test_empty_fails(self) -> None: | |
| 77 | + with pytest.raises(ValidationError): | |
| 78 | + _make_manifest(project="") | |
| 79 | + | |
| 80 | + | |
| 81 | +class TestFamilyValidation: | |
| 82 | + def test_unknown_family_fails(self) -> None: | |
| 83 | + with pytest.raises(ValidationError, match="unknown template family"): | |
| 84 | + _make_manifest(family="not-a-real-template") | |
| 85 | + | |
| 86 | + def test_all_known_families_pass(self) -> None: | |
| 87 | + from boilerworks.registry import Registry | |
| 88 | + | |
| 89 | + registry = Registry() | |
| 90 | + for t in registry.list_all(): | |
| 91 | + m = _make_manifest(family=t.name, size=t.size) | |
| 92 | + assert m.family == t.name | |
| 93 | + | |
| 94 | + | |
| 95 | +class TestYamlRoundtrip: | |
| 96 | + def test_to_yaml_and_back(self, valid_manifest: BoilerworksManifest) -> None: | |
| 97 | + yaml_str = valid_manifest.to_yaml() | |
| 98 | + assert "my-app" in yaml_str | |
| 99 | + assert "django-nextjs" in yaml_str | |
| 100 | + | |
| 101 | + restored = BoilerworksManifest.from_yaml(yaml_str) | |
| 102 | + assert restored.project == valid_manifest.project | |
| 103 | + assert restored.family == valid_manifest.family | |
| 104 | + assert restored.size == valid_manifest.size | |
| 105 | + assert restored.cloud == valid_manifest.cloud | |
| 106 | + assert restored.region == valid_manifest.region | |
| 107 | + | |
| 108 | + def test_to_yaml_is_valid_yaml(self, valid_manifest: BoilerworksManifest) -> None: | |
| 109 | + import yaml | |
| 110 | + | |
| 111 | + yaml_str = valid_manifest.to_yaml() | |
| 112 | + parsed = yaml.safe_load(yaml_str) | |
| 113 | + assert isinstance(parsed, dict) | |
| 114 | + assert parsed["project"] == "my-app" | |
| 115 | + | |
| 116 | + def test_from_file(self, valid_manifest: BoilerworksManifest, tmp_path) -> None: | |
| 117 | + yaml_file = tmp_path / "boilerworks.yaml" | |
| 118 | + valid_manifest.to_file(yaml_file) | |
| 119 | + | |
| 120 | + loaded = BoilerworksManifest.from_file(yaml_file) | |
| 121 | + assert loaded.project == valid_manifest.project | |
| 122 | + assert loaded.family == valid_manifest.family | |
| 123 | + | |
| 124 | + | |
| 125 | +class TestNestedModels: | |
| 126 | + def test_services_config_defaults(self) -> None: | |
| 127 | + s = ServicesConfig() | |
| 128 | + assert s.email is None | |
| 129 | + assert s.cache == "redis" | |
| 130 | + | |
| 131 | + def test_data_config_defaults(self) -> None: | |
| 132 | + d = DataConfig() | |
| 133 | + assert d.database == "postgres" | |
| 134 | + assert d.migrations is True | |
| 135 | + assert d.seed_data is True | |
| 136 | + | |
| 137 | + def test_testing_config_defaults(self) -> None: | |
| 138 | + t = TestingCfg() | |
| 139 | + assert t.e2e is None | |
| 140 | + assert t.unit is True | |
| 141 | + assert t.integration is True | |
| 142 | + | |
| 143 | + def test_services_email_options(self) -> None: | |
| 144 | + for provider in ("ses", "sendgrid", "mailgun"): | |
| 145 | + s = ServicesConfig(email=provider) | |
| 146 | + assert s.email == provider |
| --- a/tests/test_manifest.py | |
| +++ b/tests/test_manifest.py | |
| @@ -0,0 +1,146 @@ | |
| --- a/tests/test_manifest.py | |
| +++ b/tests/test_manifest.py | |
| @@ -0,0 +1,146 @@ | |
| 1 | """Tests for boilerworks.manifest.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import pytest |
| 6 | from pydantic import ValidationError |
| 7 | |
| 8 | from boilerworks.manifest import BoilerworksManifest, DataConfig, ServicesConfig |
| 9 | from boilerworks.manifest import TestingConfig as TestingCfg |
| 10 | |
| 11 | |
| 12 | def _make_manifest(**kwargs) -> BoilerworksManifest: |
| 13 | defaults = { |
| 14 | "project": "my-app", |
| 15 | "family": "django-nextjs", |
| 16 | "size": "full", |
| 17 | } |
| 18 | defaults.update(kwargs) |
| 19 | return BoilerworksManifest(**defaults) |
| 20 | |
| 21 | |
| 22 | class TestValidManifest: |
| 23 | def test_minimal_manifest_passes(self) -> None: |
| 24 | m = _make_manifest() |
| 25 | assert m.project == "my-app" |
| 26 | assert m.family == "django-nextjs" |
| 27 | assert m.size == "full" |
| 28 | |
| 29 | def test_defaults_are_correct(self) -> None: |
| 30 | m = _make_manifest() |
| 31 | assert m.topology == "standard" |
| 32 | assert m.cloud is None |
| 33 | assert m.region is None |
| 34 | assert m.domain is None |
| 35 | assert m.mobile is False |
| 36 | assert m.web_presence is False |
| 37 | assert m.compliance == [] |
| 38 | assert m.template_versions == {} |
| 39 | |
| 40 | def test_full_manifest_passes(self, valid_manifest: BoilerworksManifest) -> None: |
| 41 | assert valid_manifest.project == "my-app" |
| 42 | assert valid_manifest.cloud == "aws" |
| 43 | assert valid_manifest.region == "us-east-1" |
| 44 | |
| 45 | def test_micro_template(self) -> None: |
| 46 | m = _make_manifest(family="fastapi-micro", size="micro") |
| 47 | assert m.size == "micro" |
| 48 | |
| 49 | def test_edge_template(self) -> None: |
| 50 | m = _make_manifest(family="astro-site", size="edge") |
| 51 | assert m.size == "edge" |
| 52 | |
| 53 | |
| 54 | class TestProjectSlugValidation: |
| 55 | def test_valid_slugs(self) -> None: |
| 56 | for slug in ["my-app", "app", "my-app-v2", "a1b2c3"]: |
| 57 | m = _make_manifest(project=slug) |
| 58 | assert m.project == slug |
| 59 | |
| 60 | def test_spaces_fail(self) -> None: |
| 61 | with pytest.raises(ValidationError, match="project name"): |
| 62 | _make_manifest(project="my app") |
| 63 | |
| 64 | def test_uppercase_fails(self) -> None: |
| 65 | with pytest.raises(ValidationError, match="project name"): |
| 66 | _make_manifest(project="MyApp") |
| 67 | |
| 68 | def test_leading_digit_fails(self) -> None: |
| 69 | with pytest.raises(ValidationError, match="project name"): |
| 70 | _make_manifest(project="1app") |
| 71 | |
| 72 | def test_underscore_fails(self) -> None: |
| 73 | with pytest.raises(ValidationError, match="project name"): |
| 74 | _make_manifest(project="my_app") |
| 75 | |
| 76 | def test_empty_fails(self) -> None: |
| 77 | with pytest.raises(ValidationError): |
| 78 | _make_manifest(project="") |
| 79 | |
| 80 | |
| 81 | class TestFamilyValidation: |
| 82 | def test_unknown_family_fails(self) -> None: |
| 83 | with pytest.raises(ValidationError, match="unknown template family"): |
| 84 | _make_manifest(family="not-a-real-template") |
| 85 | |
| 86 | def test_all_known_families_pass(self) -> None: |
| 87 | from boilerworks.registry import Registry |
| 88 | |
| 89 | registry = Registry() |
| 90 | for t in registry.list_all(): |
| 91 | m = _make_manifest(family=t.name, size=t.size) |
| 92 | assert m.family == t.name |
| 93 | |
| 94 | |
| 95 | class TestYamlRoundtrip: |
| 96 | def test_to_yaml_and_back(self, valid_manifest: BoilerworksManifest) -> None: |
| 97 | yaml_str = valid_manifest.to_yaml() |
| 98 | assert "my-app" in yaml_str |
| 99 | assert "django-nextjs" in yaml_str |
| 100 | |
| 101 | restored = BoilerworksManifest.from_yaml(yaml_str) |
| 102 | assert restored.project == valid_manifest.project |
| 103 | assert restored.family == valid_manifest.family |
| 104 | assert restored.size == valid_manifest.size |
| 105 | assert restored.cloud == valid_manifest.cloud |
| 106 | assert restored.region == valid_manifest.region |
| 107 | |
| 108 | def test_to_yaml_is_valid_yaml(self, valid_manifest: BoilerworksManifest) -> None: |
| 109 | import yaml |
| 110 | |
| 111 | yaml_str = valid_manifest.to_yaml() |
| 112 | parsed = yaml.safe_load(yaml_str) |
| 113 | assert isinstance(parsed, dict) |
| 114 | assert parsed["project"] == "my-app" |
| 115 | |
| 116 | def test_from_file(self, valid_manifest: BoilerworksManifest, tmp_path) -> None: |
| 117 | yaml_file = tmp_path / "boilerworks.yaml" |
| 118 | valid_manifest.to_file(yaml_file) |
| 119 | |
| 120 | loaded = BoilerworksManifest.from_file(yaml_file) |
| 121 | assert loaded.project == valid_manifest.project |
| 122 | assert loaded.family == valid_manifest.family |
| 123 | |
| 124 | |
| 125 | class TestNestedModels: |
| 126 | def test_services_config_defaults(self) -> None: |
| 127 | s = ServicesConfig() |
| 128 | assert s.email is None |
| 129 | assert s.cache == "redis" |
| 130 | |
| 131 | def test_data_config_defaults(self) -> None: |
| 132 | d = DataConfig() |
| 133 | assert d.database == "postgres" |
| 134 | assert d.migrations is True |
| 135 | assert d.seed_data is True |
| 136 | |
| 137 | def test_testing_config_defaults(self) -> None: |
| 138 | t = TestingCfg() |
| 139 | assert t.e2e is None |
| 140 | assert t.unit is True |
| 141 | assert t.integration is True |
| 142 | |
| 143 | def test_services_email_options(self) -> None: |
| 144 | for provider in ("ses", "sendgrid", "mailgun"): |
| 145 | s = ServicesConfig(email=provider) |
| 146 | assert s.email == provider |
+70
| --- a/tests/test_registry.py | ||
| +++ b/tests/test_registry.py | ||
| @@ -0,0 +1,70 @@ | ||
| 1 | +"""Tests for boilerworks.registry.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import pytest | |
| 6 | + | |
| 7 | +from boilerworks.registry import Registry, TemplateInfo | |
| 8 | + | |
| 9 | + | |
| 10 | +@pytest.fixture(scope="module") | |
| 11 | +def registry() -> Registry: | |
| 12 | + return Registry() | |
| 13 | + | |
| 14 | + | |
| 15 | +def test_yaml_loads_without_error(registry: Registry) -> None: | |
| 16 | + templates = registry.list_all() | |
| 17 | + assert isinstance(templates, list) | |
| 18 | + assert len(templates) > 0 | |
| 19 | + | |
| 20 | + | |
| 21 | +def test_all_26_templates_present(registry: Registry) -> None: | |
| 22 | + assert len(registry.list_all()) == 26 | |
| 23 | + | |
| 24 | + | |
| 25 | +def test_templates_are_template_info_instances(registry: Registry) -> None: | |
| 26 | + for t in registry.list_all(): | |
| 27 | + assert isinstance(t, TemplateInfo) | |
| 28 | + | |
| 29 | + | |
| 30 | +def test_filter_by_size_full(registry: Registry) -> None: | |
| 31 | + full = registry.filter_by_size("full") | |
| 32 | + assert len(full) == 15 | |
| 33 | + assert all(t.size == "full" for t in full) | |
| 34 | + | |
| 35 | + | |
| 36 | +def test_filter_by_size_micro(registry: Registry) -> None: | |
| 37 | + micro = registry.filter_by_size("micro") | |
| 38 | + assert len(micro) == 6 | |
| 39 | + assert all(t.size == "micro" for t in micro) | |
| 40 | + | |
| 41 | + | |
| 42 | +def test_filter_by_size_edge(registry: Registry) -> None: | |
| 43 | + edge = registry.filter_by_size("edge") | |
| 44 | + assert len(edge) == 5 | |
| 45 | + assert all(t.siz== "edge" for t in edge) | |
| 46 | + | |
| 47 | + | |
| 48 | +def test_filter_by_language_python(registry: Registry) -> None: | |
| 49 | + python = registry.filter_by_language("python") | |
| 50 | + names = {t.name for t in python} | |
| 51 | + assert "django-nextjs" in names | |
| 52 | + assert "fastapi-micro" in names | |
| 53 | + assert "nestjs-nextjs" not in names | |
| 54 | + | |
| 55 | + | |
| 56 | +def test_filter_by_language_typescript(registry: Registry) -> None: | |
| 57 | + ts = registry.filter_by_language("typescript") | |
| 58 | + names = {t.name for t in ts} | |
| 59 | + assert "nestjs-nextjs" in names | |
| 60 | + assert "django-nextjs" not in names | |
| 61 | + | |
| 62 | + | |
| 63 | +def test_filter_by_language_go(registry: Registry) -> None: | |
| 64 | + go = registry.filter_by_language("go") | |
| 65 | + assert all(t.language == "go" for t in go) | |
| 66 | + assert len(go) >= 2 | |
| 67 | + | |
| 68 | + | |
| 69 | +def test_get_by_name_returns_correct_template(registry: Registry) -> None: | |
| 70 | + t = reg |
| --- a/tests/test_registry.py | |
| +++ b/tests/test_registry.py | |
| @@ -0,0 +1,70 @@ | |
| --- a/tests/test_registry.py | |
| +++ b/tests/test_registry.py | |
| @@ -0,0 +1,70 @@ | |
| 1 | """Tests for boilerworks.registry.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import pytest |
| 6 | |
| 7 | from boilerworks.registry import Registry, TemplateInfo |
| 8 | |
| 9 | |
| 10 | @pytest.fixture(scope="module") |
| 11 | def registry() -> Registry: |
| 12 | return Registry() |
| 13 | |
| 14 | |
| 15 | def test_yaml_loads_without_error(registry: Registry) -> None: |
| 16 | templates = registry.list_all() |
| 17 | assert isinstance(templates, list) |
| 18 | assert len(templates) > 0 |
| 19 | |
| 20 | |
| 21 | def test_all_26_templates_present(registry: Registry) -> None: |
| 22 | assert len(registry.list_all()) == 26 |
| 23 | |
| 24 | |
| 25 | def test_templates_are_template_info_instances(registry: Registry) -> None: |
| 26 | for t in registry.list_all(): |
| 27 | assert isinstance(t, TemplateInfo) |
| 28 | |
| 29 | |
| 30 | def test_filter_by_size_full(registry: Registry) -> None: |
| 31 | full = registry.filter_by_size("full") |
| 32 | assert len(full) == 15 |
| 33 | assert all(t.size == "full" for t in full) |
| 34 | |
| 35 | |
| 36 | def test_filter_by_size_micro(registry: Registry) -> None: |
| 37 | micro = registry.filter_by_size("micro") |
| 38 | assert len(micro) == 6 |
| 39 | assert all(t.size == "micro" for t in micro) |
| 40 | |
| 41 | |
| 42 | def test_filter_by_size_edge(registry: Registry) -> None: |
| 43 | edge = registry.filter_by_size("edge") |
| 44 | assert len(edge) == 5 |
| 45 | assert all(t.siz== "edge" for t in edge) |
| 46 | |
| 47 | |
| 48 | def test_filter_by_language_python(registry: Registry) -> None: |
| 49 | python = registry.filter_by_language("python") |
| 50 | names = {t.name for t in python} |
| 51 | assert "django-nextjs" in names |
| 52 | assert "fastapi-micro" in names |
| 53 | assert "nestjs-nextjs" not in names |
| 54 | |
| 55 | |
| 56 | def test_filter_by_language_typescript(registry: Registry) -> None: |
| 57 | ts = registry.filter_by_language("typescript") |
| 58 | names = {t.name for t in ts} |
| 59 | assert "nestjs-nextjs" in names |
| 60 | assert "django-nextjs" not in names |
| 61 | |
| 62 | |
| 63 | def test_filter_by_language_go(registry: Registry) -> None: |
| 64 | go = registry.filter_by_language("go") |
| 65 | assert all(t.language == "go" for t in go) |
| 66 | assert len(go) >= 2 |
| 67 | |
| 68 | |
| 69 | def test_get_by_name_returns_correct_template(registry: Registry) -> None: |
| 70 | t = reg |
+183
| --- a/tests/test_renderer.py | ||
| +++ b/tests/test_renderer.py | ||
| @@ -0,0 +1,183 @@ | ||
| 1 | +"""Tests for boilerworks.renderer.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +from pathlib import Path | |
| 6 | + | |
| 7 | +from boilerworks.renderer import ( | |
| 8 | + _SKIP_DIRS, | |
| 9 | + _SKIP_EXTENSIONS, | |
| 10 | + build_replacements, | |
| 11 | + rename_boilerworks_paths, | |
| 12 | + render_directory, | |
| 13 | + render_file, | |
| 14 | +) | |
| 15 | + | |
| 16 | + | |
| 17 | +class TestBuildReplacements: | |
| 18 | + def test_lowercase_replacement(self) -> None: | |
| 19 | + r = build_replacements("my-app") | |
| 20 | + assert r["boilerworks"] == "my-app" | |
| 21 | + | |
| 22 | + def test_uppercase_replacement(self) -> None: | |
| 23 | + r = build_replacements("my-app") | |
| 24 | + assert r["BOILERWORKS"] == "MY-APP" | |
| 25 | + | |
| 26 | + def test_title_replacement(self) -> None: | |
| 27 | + r = build_replacements("my-app") | |
| 28 | + assert r["Boilerworks"] == "My-App" | |
| 29 | + | |
| 30 | + def test_underscore_prefix_replacement(self) -> None: | |
| 31 | + r = build_replacements("my-app") | |
| 32 | + assert r["boilerworks_"] == "my_app_" | |
| 33 | + | |
| 34 | + def test_underscore_suffix_replacement(self) -> None: | |
| 35 | + r = build_replacements("my-app") | |
| 36 | + assert r["_boilerworks"] == "_my_app" | |
| 37 | + | |
| 38 | + def test_single_word_project(self) -> None: | |
| 39 | + r = build_replacements("myapp") | |
| 40 | + assert r["boilerworks"] == "myapp" | |
| 41 | + assert r["BOILERWORKS"] == "MYAPP" | |
| 42 | + | |
| 43 | + | |
| 44 | +class TestRenderFile: | |
| 45 | + def test_replaces_lowercase(self, tmp_path: Path) -> None: | |
| 46 | + f = tmp_path / "test.txt" | |
| 47 | + f.write_text("APP_NAME=boilerworks") | |
| 48 | + replacements = build_replacements("my-app") | |
| 49 | + changed = render_file(f, replacements) | |
| 50 | + assert changed is True | |
| 51 | + assert f.read_text() == "APP_NAME=my-app" | |
| 52 | + | |
| 53 | + def test_replaces_uppercase(self, tmp_path: Path) -> None: | |
| 54 | + f = tmp_path / "test.env" | |
| 55 | + f.write_text("DB_NAME=BOILERWORKS_DB") | |
| 56 | + replacements = build_replacements("my-app") | |
| 57 | + render_file(f, replacements) | |
| 58 | + assert f.read_text() == "DB_NAME=MY-APP_DB" | |
| 59 | + | |
| 60 | + def test_no_match_returns_false(self, tmp_path: Path) -> None: | |
| 61 | + f = tmp_path / "test.txt" | |
| 62 | + f.write_text("hello world") | |
| 63 | + changed = render_file(f, build_replacements("my-app")) | |
| 64 | + assert changed is False | |
| 65 | + assert f.read_text() == "hello world" | |
| 66 | + | |
| 67 | + def test_empty_file_unchanged(self, tmp_path: Path) -> None: | |
| 68 | + f = tmp_path / "empty.txt" | |
| 69 | + f.write_text("") | |
| 70 | + changed = render_file(f, build_replacements("my-app")) | |
| 71 | + assert changed is False | |
| 72 | + assert f.read_text() == "" | |
| 73 | + | |
| 74 | + def test_binary_extension_skipped(self, tmp_path: Path) -> None: | |
| 75 | + f = tmp_path / "image.png" | |
| 76 | + f.write_bytes(b"\x89PNG boilerworks") | |
| 77 | + changed = render_file(f, build_replacements("my-app")) | |
| 78 | + assert changed is False | |
| 79 | + # File content unchanged | |
| 80 | + assert b"boilerworks" in f.read_bytes() | |
| 81 | + | |
| 82 | + def test_lock_extension_skipped(self, tmp_path: Path) -> None: | |
| 83 | + f = tmp_path / "uv.lock" | |
| 84 | + f.write_text("boilerworks = 1.0") | |
| 85 | + changed = render_file(f, build_replacements("my-app")) | |
| 86 | + assert changed is False | |
| 87 | + | |
| 88 | + def test_multiple_replacements_in_one_file(self, tmp_path: Path) -> None: | |
| 89 | + f = tmp_path / "settings.py" | |
| 90 | + content = "APP = 'boilerworks'\nNAME = 'Boilerworks'\nKEY = 'BOILERWORKS'" | |
| 91 | + f.write_text(content) | |
| 92 | + render_file(f, build_replacements("cool-app")) | |
| 93 | + result = f.read_text() | |
| 94 | + assert "cool-app" in result | |
| 95 | + assert "Cool-App" in result | |
| 96 | + assert "COOL-APP" in result | |
| 97 | + assert "boilerworks" not in result | |
| 98 | + | |
| 99 | + def test_case_variant_python_module(self, tmp_path: Path) -> None: | |
| 100 | + f = tmp_path / "settings.py" | |
| 101 | + f.write_text("from boilerworks_config import settings") | |
| 102 | + replacements = build_replacements("myproject") | |
| 103 | + render_file(f, replacements) | |
| 104 | + assert f.read_text() == "from myproject_config import settings" | |
| 105 | + | |
| 106 | + | |
| 107 | +class TestRenderDirectory: | |
| 108 | + def test_renders_text_files(self, tmp_path: Path) -> None: | |
| 109 | + (tmp_path / "app.py").write_text("APP = 'boilerworks'") | |
| 110 | + (tmp_path / "README.md").write_text("# boilerworks\n") | |
| 111 | + modified = render_directory(tmp_path, build_replacements("newapp")) | |
| 112 | + assert len(modified) == 2 | |
| 113 | + | |
| 114 | + def test_skips_excluded_dirs(self, tmp_path: Path) -> None: | |
| 115 | + skip_dir = tmp_path / "node_modules" | |
| 116 | + skip_dir.mkdir() | |
| 117 | + (skip_dir / "file.js").write_text("module.exports = 'boilerworks'") | |
| 118 | + modified = render_directory(tmp_path, build_replacements("newapp")) | |
| 119 | + assert not any("node_modules" in str(p) for p in modified) | |
| 120 | + # Original file unchanged | |
| 121 | + assert "boilerworks" in (skip_dir / "file.js").read_text() | |
| 122 | + | |
| 123 | + def test_skips_git_dir(self, tmp_path: Path) -> None: | |
| 124 | + git_dir = tmp_path / ".git" | |
| 125 | + git_dir.mkdir() | |
| 126 | + (git_dir / "config").write_text("url = boilerworks") | |
| 127 | + modified = render_directory(tmp_path, build_replacements("newapp")) | |
| 128 | + assert not any(".git" in str(p) for p in modified) | |
| 129 | + | |
| 130 | + def test_skips_binary_extensions(self, tmp_path: Path) -> None: | |
| 131 | + (tmp_path / "logo.png").write_bytes(b"\x89PNG boilerworks") | |
| 132 | + (tmp_path / "app.py").write_text("# boilerworks") | |
| 133 | + modified = render_directory(tmp_path, build_replacements("newapp")) | |
| 134 | + modified_names = [p.name for p in modified] | |
| 135 | + assert "logo.png" not in modified_names | |
| 136 | + assert "app.py" in modified_names | |
| 137 | + | |
| 138 | + def test_nested_directories(self, tmp_path: Path) -> None: | |
| 139 | + subdir = tmp_path / "src" / "config" | |
| 140 | + subdir.mkdir(parents=True) | |
| 141 | + (subdir / "settings.py").write_text("APP = 'boilerworks'") | |
| 142 | + modified = render_directory(tmp_path, build_replacements("newapp")) | |
| 143 | + assert len(modified) == 1 | |
| 144 | + assert (subdir / "settings.py").read_text() == "APP = 'newapp'" | |
| 145 | + | |
| 146 | + | |
| 147 | +class TestRenameBoilerworksPaths: | |
| 148 | + def test_renames_file_with_boilerworks(self, tmp_path: Path) -> None: | |
| 149 | + f = tmp_path / "boilerworks.iml" | |
| 150 | + f.write_text("") | |
| 151 | + renames = rename_boilerworks_paths(tmp_path, "my-project") | |
| 152 | + assert len(renames) == 1 | |
| 153 | + assert not (tmp_path / "boilerworks.iml").exists() | |
| 154 | + assert (tmp_path / "my-project.iml").exists() | |
| 155 | + | |
| 156 | + def test_renames_directory_with_boilerworks(self, tmp_path: Path) -> None: | |
| 157 | + subdir = tmp_path / "boilerworks_config" | |
| 158 | + subdir.mkdir() | |
| 159 | + (subdir / "settings.py").write_text("") | |
| 160 | + rename_boilerworks_paths(tmp_path, "myproject") | |
| 161 | + assert not (tmp_path / "boilerworks_config").exists() | |
| 162 | + assert (tmp_path / "myproject_config").exists() | |
| 163 | + | |
| 164 | + def test_no_boilerworks_paths_unchanged(self, tmp_path: Path) -> None: | |
| 165 | + f = tmp_path / "other_file.txt" | |
| 166 | + f.write_text("content") | |
| 167 | + renames = rename_boilerworks_paths(tmp_path, "myproject") | |
| 168 | + assert len(renames) == 0 | |
| 169 | + assert f.exists() | |
| 170 | + | |
| 171 | + | |
| 172 | +class TestSkipSets: | |
| 173 | + def test_skip_dirs_contains_expected(self) -> None: | |
| 174 | + assert ".git" in _SKIP_DIRS | |
| 175 | + assert "node_modules" in _SKIP_DIRS | |
| 176 | + assert "__pycache__" in _SKIP_DIRS | |
| 177 | + assert ".venv" in _SKIP_DIRS | |
| 178 | + | |
| 179 | + def test_skip_extensions_contains_expected(self) -> None: | |
| 180 | + assert ".png" in _SKIP_EXTENSIONS | |
| 181 | + assert ".lock" in _SKIP_EXTENSIONS | |
| 182 | + assert ".pyc" in _SKIP_EXTENSIONS | |
| 183 | + assert ".woff" in _SKIP_EXTENSIONS |
| --- a/tests/test_renderer.py | |
| +++ b/tests/test_renderer.py | |
| @@ -0,0 +1,183 @@ | |
| --- a/tests/test_renderer.py | |
| +++ b/tests/test_renderer.py | |
| @@ -0,0 +1,183 @@ | |
| 1 | """Tests for boilerworks.renderer.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from pathlib import Path |
| 6 | |
| 7 | from boilerworks.renderer import ( |
| 8 | _SKIP_DIRS, |
| 9 | _SKIP_EXTENSIONS, |
| 10 | build_replacements, |
| 11 | rename_boilerworks_paths, |
| 12 | render_directory, |
| 13 | render_file, |
| 14 | ) |
| 15 | |
| 16 | |
| 17 | class TestBuildReplacements: |
| 18 | def test_lowercase_replacement(self) -> None: |
| 19 | r = build_replacements("my-app") |
| 20 | assert r["boilerworks"] == "my-app" |
| 21 | |
| 22 | def test_uppercase_replacement(self) -> None: |
| 23 | r = build_replacements("my-app") |
| 24 | assert r["BOILERWORKS"] == "MY-APP" |
| 25 | |
| 26 | def test_title_replacement(self) -> None: |
| 27 | r = build_replacements("my-app") |
| 28 | assert r["Boilerworks"] == "My-App" |
| 29 | |
| 30 | def test_underscore_prefix_replacement(self) -> None: |
| 31 | r = build_replacements("my-app") |
| 32 | assert r["boilerworks_"] == "my_app_" |
| 33 | |
| 34 | def test_underscore_suffix_replacement(self) -> None: |
| 35 | r = build_replacements("my-app") |
| 36 | assert r["_boilerworks"] == "_my_app" |
| 37 | |
| 38 | def test_single_word_project(self) -> None: |
| 39 | r = build_replacements("myapp") |
| 40 | assert r["boilerworks"] == "myapp" |
| 41 | assert r["BOILERWORKS"] == "MYAPP" |
| 42 | |
| 43 | |
| 44 | class TestRenderFile: |
| 45 | def test_replaces_lowercase(self, tmp_path: Path) -> None: |
| 46 | f = tmp_path / "test.txt" |
| 47 | f.write_text("APP_NAME=boilerworks") |
| 48 | replacements = build_replacements("my-app") |
| 49 | changed = render_file(f, replacements) |
| 50 | assert changed is True |
| 51 | assert f.read_text() == "APP_NAME=my-app" |
| 52 | |
| 53 | def test_replaces_uppercase(self, tmp_path: Path) -> None: |
| 54 | f = tmp_path / "test.env" |
| 55 | f.write_text("DB_NAME=BOILERWORKS_DB") |
| 56 | replacements = build_replacements("my-app") |
| 57 | render_file(f, replacements) |
| 58 | assert f.read_text() == "DB_NAME=MY-APP_DB" |
| 59 | |
| 60 | def test_no_match_returns_false(self, tmp_path: Path) -> None: |
| 61 | f = tmp_path / "test.txt" |
| 62 | f.write_text("hello world") |
| 63 | changed = render_file(f, build_replacements("my-app")) |
| 64 | assert changed is False |
| 65 | assert f.read_text() == "hello world" |
| 66 | |
| 67 | def test_empty_file_unchanged(self, tmp_path: Path) -> None: |
| 68 | f = tmp_path / "empty.txt" |
| 69 | f.write_text("") |
| 70 | changed = render_file(f, build_replacements("my-app")) |
| 71 | assert changed is False |
| 72 | assert f.read_text() == "" |
| 73 | |
| 74 | def test_binary_extension_skipped(self, tmp_path: Path) -> None: |
| 75 | f = tmp_path / "image.png" |
| 76 | f.write_bytes(b"\x89PNG boilerworks") |
| 77 | changed = render_file(f, build_replacements("my-app")) |
| 78 | assert changed is False |
| 79 | # File content unchanged |
| 80 | assert b"boilerworks" in f.read_bytes() |
| 81 | |
| 82 | def test_lock_extension_skipped(self, tmp_path: Path) -> None: |
| 83 | f = tmp_path / "uv.lock" |
| 84 | f.write_text("boilerworks = 1.0") |
| 85 | changed = render_file(f, build_replacements("my-app")) |
| 86 | assert changed is False |
| 87 | |
| 88 | def test_multiple_replacements_in_one_file(self, tmp_path: Path) -> None: |
| 89 | f = tmp_path / "settings.py" |
| 90 | content = "APP = 'boilerworks'\nNAME = 'Boilerworks'\nKEY = 'BOILERWORKS'" |
| 91 | f.write_text(content) |
| 92 | render_file(f, build_replacements("cool-app")) |
| 93 | result = f.read_text() |
| 94 | assert "cool-app" in result |
| 95 | assert "Cool-App" in result |
| 96 | assert "COOL-APP" in result |
| 97 | assert "boilerworks" not in result |
| 98 | |
| 99 | def test_case_variant_python_module(self, tmp_path: Path) -> None: |
| 100 | f = tmp_path / "settings.py" |
| 101 | f.write_text("from boilerworks_config import settings") |
| 102 | replacements = build_replacements("myproject") |
| 103 | render_file(f, replacements) |
| 104 | assert f.read_text() == "from myproject_config import settings" |
| 105 | |
| 106 | |
| 107 | class TestRenderDirectory: |
| 108 | def test_renders_text_files(self, tmp_path: Path) -> None: |
| 109 | (tmp_path / "app.py").write_text("APP = 'boilerworks'") |
| 110 | (tmp_path / "README.md").write_text("# boilerworks\n") |
| 111 | modified = render_directory(tmp_path, build_replacements("newapp")) |
| 112 | assert len(modified) == 2 |
| 113 | |
| 114 | def test_skips_excluded_dirs(self, tmp_path: Path) -> None: |
| 115 | skip_dir = tmp_path / "node_modules" |
| 116 | skip_dir.mkdir() |
| 117 | (skip_dir / "file.js").write_text("module.exports = 'boilerworks'") |
| 118 | modified = render_directory(tmp_path, build_replacements("newapp")) |
| 119 | assert not any("node_modules" in str(p) for p in modified) |
| 120 | # Original file unchanged |
| 121 | assert "boilerworks" in (skip_dir / "file.js").read_text() |
| 122 | |
| 123 | def test_skips_git_dir(self, tmp_path: Path) -> None: |
| 124 | git_dir = tmp_path / ".git" |
| 125 | git_dir.mkdir() |
| 126 | (git_dir / "config").write_text("url = boilerworks") |
| 127 | modified = render_directory(tmp_path, build_replacements("newapp")) |
| 128 | assert not any(".git" in str(p) for p in modified) |
| 129 | |
| 130 | def test_skips_binary_extensions(self, tmp_path: Path) -> None: |
| 131 | (tmp_path / "logo.png").write_bytes(b"\x89PNG boilerworks") |
| 132 | (tmp_path / "app.py").write_text("# boilerworks") |
| 133 | modified = render_directory(tmp_path, build_replacements("newapp")) |
| 134 | modified_names = [p.name for p in modified] |
| 135 | assert "logo.png" not in modified_names |
| 136 | assert "app.py" in modified_names |
| 137 | |
| 138 | def test_nested_directories(self, tmp_path: Path) -> None: |
| 139 | subdir = tmp_path / "src" / "config" |
| 140 | subdir.mkdir(parents=True) |
| 141 | (subdir / "settings.py").write_text("APP = 'boilerworks'") |
| 142 | modified = render_directory(tmp_path, build_replacements("newapp")) |
| 143 | assert len(modified) == 1 |
| 144 | assert (subdir / "settings.py").read_text() == "APP = 'newapp'" |
| 145 | |
| 146 | |
| 147 | class TestRenameBoilerworksPaths: |
| 148 | def test_renames_file_with_boilerworks(self, tmp_path: Path) -> None: |
| 149 | f = tmp_path / "boilerworks.iml" |
| 150 | f.write_text("") |
| 151 | renames = rename_boilerworks_paths(tmp_path, "my-project") |
| 152 | assert len(renames) == 1 |
| 153 | assert not (tmp_path / "boilerworks.iml").exists() |
| 154 | assert (tmp_path / "my-project.iml").exists() |
| 155 | |
| 156 | def test_renames_directory_with_boilerworks(self, tmp_path: Path) -> None: |
| 157 | subdir = tmp_path / "boilerworks_config" |
| 158 | subdir.mkdir() |
| 159 | (subdir / "settings.py").write_text("") |
| 160 | rename_boilerworks_paths(tmp_path, "myproject") |
| 161 | assert not (tmp_path / "boilerworks_config").exists() |
| 162 | assert (tmp_path / "myproject_config").exists() |
| 163 | |
| 164 | def test_no_boilerworks_paths_unchanged(self, tmp_path: Path) -> None: |
| 165 | f = tmp_path / "other_file.txt" |
| 166 | f.write_text("content") |
| 167 | renames = rename_boilerworks_paths(tmp_path, "myproject") |
| 168 | assert len(renames) == 0 |
| 169 | assert f.exists() |
| 170 | |
| 171 | |
| 172 | class TestSkipSets: |
| 173 | def test_skip_dirs_contains_expected(self) -> None: |
| 174 | assert ".git" in _SKIP_DIRS |
| 175 | assert "node_modules" in _SKIP_DIRS |
| 176 | assert "__pycache__" in _SKIP_DIRS |
| 177 | assert ".venv" in _SKIP_DIRS |
| 178 | |
| 179 | def test_skip_extensions_contains_expected(self) -> None: |
| 180 | assert ".png" in _SKIP_EXTENSIONS |
| 181 | assert ".lock" in _SKIP_EXTENSIONS |
| 182 | assert ".pyc" in _SKIP_EXTENSIONS |
| 183 | assert ".woff" in _SKIP_EXTENSIONS |
+177
| --- a/tests/test_wizard.py | ||
| +++ b/tests/test_wizard.py | ||
| @@ -0,0 +1,177 @@ | ||
| 1 | +"""Tests for boilerworks.wizard.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +from pathlib import Path | |
| 6 | +from unittest.mock import patch | |
| 7 | + | |
| 8 | +import pytest | |
| 9 | + | |
| 10 | +from boilerworks.wizard import _template_choices, _validate_slug | |
| 11 | + | |
| 12 | + | |
| 13 | +class TestValidateSlug: | |
| 14 | + def test_valid_slugs(self) -> None: | |
| 15 | + for slug in ["my-app", "app", "a1b2", "my-project-v2"]: | |
| 16 | + assert _validate_slug(slug) is True | |
| 17 | + | |
| 18 | + def test_empty_returns_message(self) -> None: | |
| 19 | + result = _validate_slug("") | |
| 20 | + assert isinstance(result, str) | |
| 21 | + assert "required" in result.lower() | |
| 22 | + | |
| 23 | + def test_spaces_returns_message(self) -> None: | |
| 24 | + result = _validate_slug("my app") | |
| 25 | + assert isinstance(result, str) | |
| 26 | + | |
| 27 | + def test_uppercase_returns_message(self) -> None: | |
| 28 | + result = _validate_slug("MyApp") | |
| 29 | + assert isinstance(result, str) | |
| 30 | + | |
| 31 | + def test_leading_digit_returns_message(self) -> None: | |
| 32 | + result = _validate_slug("1app") | |
| 33 | + assert isinstance(result, str) | |
| 34 | + | |
| 35 | + | |
| 36 | +class TestTemplateChoices: | |
| 37 | + def test_returns_choices_list(self) -> None: | |
| 38 | + from boilerworks.registry import Registry | |
| 39 | + | |
| 40 | + registry = Registry() | |
| 41 | + templates = registry.filter_by_size("full") | |
| 42 | + choices = _template_choices(templates) | |
| 43 | + assert len(choices) > 0 | |
| 44 | + | |
| 45 | + def test_separators_per_language(self) -> None: | |
| 46 | + import questionary | |
| 47 | + | |
| 48 | + from boilerworks.registry import Registry | |
| 49 | + | |
| 50 | + registry = Registry() | |
| 51 | + templates = registry.list_all() | |
| 52 | + choices = _template_choices(templates) | |
| 53 | + separators = [c for c in choices if isinstance(c, questionary.Separator)] | |
| 54 | + # Should have at least one separator per language group | |
| 55 | + assert len(separators) >= 5 | |
| 56 | + | |
| 57 | + def test_choice_values_are_template_names(self) -> None: | |
| 58 | + import questionary | |
| 59 | + | |
| 60 | + from boilerworks.registry import Registry | |
| 61 | + | |
| 62 | + registry = Registry() | |
| 63 | + templates = registry.filter_by_size("micro") | |
| 64 | + choices = _template_choices(templates) | |
| 65 | + # Separator is a subclass of Choice in questionary, so exclude them explicitly | |
| 66 | + real_choices = [ | |
| 67 | + c for c in choices if isinstance(c, questionary.Choice) and not isinstance(c, questionary.Separator) | |
| 68 | + ] | |
| 69 | + names = {c.value for c in real_choices} | |
| 70 | + expected = {t.name for t in templates} | |
| 71 | + assert names == expected | |
| 72 | + | |
| 73 | + | |
| 74 | +class TestRunWizard: | |
| 75 | + """Integration tests for run_wizard using mocked questionary.""" | |
| 76 | + | |
| 77 | + def test_wizard_writes_manifest(self, tmp_path: Path) -> None: | |
| 78 | + output_file = tmp_path / "boilerworks.yaml" | |
| 79 | + | |
| 80 | + with ( | |
| 81 | + patch("questionary.text") as mock_text, | |
| 82 | + patch("questionary.select") as mock_select, | |
| 83 | + patch("questionary.confirm") as mock_confirm, | |
| 84 | + patch("questionary.checkbox") as mock_checkbox, | |
| 85 | + ): | |
| 86 | + mock_text.return_value.ask.side_effect = [ | |
| 87 | + "my-test-app", # project name | |
| 88 | + "", # region (empty → None) | |
| 89 | + "", # domain (empty → None) | |
| 90 | + ] | |
| 91 | + mock_select.return_value.ask.side_effect = [ | |
| 92 | + "full", # size | |
| 93 | + "django-nextjs", # family | |
| 94 | + "standard", # topology | |
| 95 | + "none", # cloud | |
| 96 | + "none", # email | |
| 97 | + "none", # e2e | |
| 98 | + ] | |
| 99 | + mock_confirm.return_value.ask.side_effect = [ | |
| 100 | + False, # mobile | |
| 101 | + False, # web_presence | |
| 102 | + True, # confirm write | |
| 103 | + ] | |
| 104 | + mock_checkbox.return_value.ask.return_value = [] # compliance | |
| 105 | + | |
| 106 | + from boilerworks.wizard import run_wizard | |
| 107 | + | |
| 108 | + run_wizard(output_path=output_file) | |
| 109 | + | |
| 110 | + assert output_file.exists() | |
| 111 | + content = output_file.read_text() | |
| 112 | + assert "my-test-app" in content | |
| 113 | + assert "django-nextjs" in content | |
| 114 | + | |
| 115 | + def test_wizard_cancelled_on_project_name(self, tmp_path: Path) -> None: | |
| 116 | + output_file = tmp_path / "boilerworks.yaml" | |
| 117 | + with patch("questionary.text") as mock_text: | |
| 118 | + mock_text.return_value.ask.return_value = None # user hit Ctrl+C | |
| 119 | + from boilerworks.wizard import run_wizard | |
| 120 | + | |
| 121 | + with pytest.raises(SystemExit): | |
| 122 | + run_wizard(output_path=output_file) | |
| 123 | + | |
| 124 | + assert not output_file.exists() | |
| 125 | + | |
| 126 | + def test_wizard_cancelled_on_confirm(self, tmp_path: Path) -> None: | |
| 127 | + output_file = tmp_path / "boilerworks.yaml" | |
| 128 | + with ( | |
| 129 | + patch("questionary.text") as mock_text, | |
| 130 | + patch("questionary.select") as mock_select, | |
| 131 | + patch("questionary.confirm") as mock_confirm, | |
| 132 | + patch("questionary.checkbox") as mock_checkbox, | |
| 133 | + ): | |
| 134 | + mock_text.return_value.ask.side_effect = ["my-app", "", ""] | |
| 135 | + mock_select.return_value.ask.side_effect = ["full", "django-nextjs", "standard", "none", "none", "none"] | |
| 136 | + mock_confirm.return_value.ask.side_effect = [False, False, False] # all confirms → False | |
| 137 | + mock_checkbox.return_value.ask.return_value = [] | |
| 138 | + | |
| 139 | + from boilerworks.wizard import run_wizard | |
| 140 | + | |
| 141 | + with pytest.raises(SystemExit): | |
| 142 | + run_wizard(output_path=output_file) | |
| 143 | + | |
| 144 | + assert not output_file.exists() | |
| 145 | + | |
| 146 | + def test_wizard_with_cloud_and_region(self, tmp_path: Path) -> None: | |
| 147 | + output_file = tmp_path / "boilerworks.yaml" | |
| 148 | + with ( | |
| 149 | + patch("questionary.text") as mock_text, | |
| 150 | + patch("questionary.select") as mock_select, | |
| 151 | + patch("questionary.confirm") as mock_confirm, | |
| 152 | + patch("questionary.checkbox") as mock_checkbox, | |
| 153 | + ): | |
| 154 | + mock_text.return_value.ask.side_effect = [ | |
| 155 | + "cloud-app", # project name | |
| 156 | + "us-east-1", # region | |
| 157 | + "myapp.com", # domain | |
| 158 | + ] | |
| 159 | + # fastapi-micro has only one topology, so topology select is skipped | |
| 160 | + mock_select.return_value.ask.side_effect = [ | |
| 161 | + "micro", # size | |
| 162 | + "fastapi-micro", # family | |
| 163 | + "aws", # cloud (topology prompt skipped — single option) | |
| 164 | + "ses", # email | |
| 165 | + "playwright", # e2e | |
| 166 | + ] | |
| 167 | + mock_confirm.return_value.ask.return_value = True | |
| 168 | + mock_checkbox.return_value.ask.return_value = ["soc2"] | |
| 169 | + | |
| 170 | + from boilerworks.wizard import run_wizard | |
| 171 | + | |
| 172 | + run_wizard(output_path=output_file) | |
| 173 | + | |
| 174 | + assert output_file.exists() | |
| 175 | + content = output_file.read_text() | |
| 176 | + assert "cloud-app" in content | |
| 177 | + assert "fastapi-micro" in content |
| --- a/tests/test_wizard.py | |
| +++ b/tests/test_wizard.py | |
| @@ -0,0 +1,177 @@ | |
| --- a/tests/test_wizard.py | |
| +++ b/tests/test_wizard.py | |
| @@ -0,0 +1,177 @@ | |
| 1 | """Tests for boilerworks.wizard.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from pathlib import Path |
| 6 | from unittest.mock import patch |
| 7 | |
| 8 | import pytest |
| 9 | |
| 10 | from boilerworks.wizard import _template_choices, _validate_slug |
| 11 | |
| 12 | |
| 13 | class TestValidateSlug: |
| 14 | def test_valid_slugs(self) -> None: |
| 15 | for slug in ["my-app", "app", "a1b2", "my-project-v2"]: |
| 16 | assert _validate_slug(slug) is True |
| 17 | |
| 18 | def test_empty_returns_message(self) -> None: |
| 19 | result = _validate_slug("") |
| 20 | assert isinstance(result, str) |
| 21 | assert "required" in result.lower() |
| 22 | |
| 23 | def test_spaces_returns_message(self) -> None: |
| 24 | result = _validate_slug("my app") |
| 25 | assert isinstance(result, str) |
| 26 | |
| 27 | def test_uppercase_returns_message(self) -> None: |
| 28 | result = _validate_slug("MyApp") |
| 29 | assert isinstance(result, str) |
| 30 | |
| 31 | def test_leading_digit_returns_message(self) -> None: |
| 32 | result = _validate_slug("1app") |
| 33 | assert isinstance(result, str) |
| 34 | |
| 35 | |
| 36 | class TestTemplateChoices: |
| 37 | def test_returns_choices_list(self) -> None: |
| 38 | from boilerworks.registry import Registry |
| 39 | |
| 40 | registry = Registry() |
| 41 | templates = registry.filter_by_size("full") |
| 42 | choices = _template_choices(templates) |
| 43 | assert len(choices) > 0 |
| 44 | |
| 45 | def test_separators_per_language(self) -> None: |
| 46 | import questionary |
| 47 | |
| 48 | from boilerworks.registry import Registry |
| 49 | |
| 50 | registry = Registry() |
| 51 | templates = registry.list_all() |
| 52 | choices = _template_choices(templates) |
| 53 | separators = [c for c in choices if isinstance(c, questionary.Separator)] |
| 54 | # Should have at least one separator per language group |
| 55 | assert len(separators) >= 5 |
| 56 | |
| 57 | def test_choice_values_are_template_names(self) -> None: |
| 58 | import questionary |
| 59 | |
| 60 | from boilerworks.registry import Registry |
| 61 | |
| 62 | registry = Registry() |
| 63 | templates = registry.filter_by_size("micro") |
| 64 | choices = _template_choices(templates) |
| 65 | # Separator is a subclass of Choice in questionary, so exclude them explicitly |
| 66 | real_choices = [ |
| 67 | c for c in choices if isinstance(c, questionary.Choice) and not isinstance(c, questionary.Separator) |
| 68 | ] |
| 69 | names = {c.value for c in real_choices} |
| 70 | expected = {t.name for t in templates} |
| 71 | assert names == expected |
| 72 | |
| 73 | |
| 74 | class TestRunWizard: |
| 75 | """Integration tests for run_wizard using mocked questionary.""" |
| 76 | |
| 77 | def test_wizard_writes_manifest(self, tmp_path: Path) -> None: |
| 78 | output_file = tmp_path / "boilerworks.yaml" |
| 79 | |
| 80 | with ( |
| 81 | patch("questionary.text") as mock_text, |
| 82 | patch("questionary.select") as mock_select, |
| 83 | patch("questionary.confirm") as mock_confirm, |
| 84 | patch("questionary.checkbox") as mock_checkbox, |
| 85 | ): |
| 86 | mock_text.return_value.ask.side_effect = [ |
| 87 | "my-test-app", # project name |
| 88 | "", # region (empty → None) |
| 89 | "", # domain (empty → None) |
| 90 | ] |
| 91 | mock_select.return_value.ask.side_effect = [ |
| 92 | "full", # size |
| 93 | "django-nextjs", # family |
| 94 | "standard", # topology |
| 95 | "none", # cloud |
| 96 | "none", # email |
| 97 | "none", # e2e |
| 98 | ] |
| 99 | mock_confirm.return_value.ask.side_effect = [ |
| 100 | False, # mobile |
| 101 | False, # web_presence |
| 102 | True, # confirm write |
| 103 | ] |
| 104 | mock_checkbox.return_value.ask.return_value = [] # compliance |
| 105 | |
| 106 | from boilerworks.wizard import run_wizard |
| 107 | |
| 108 | run_wizard(output_path=output_file) |
| 109 | |
| 110 | assert output_file.exists() |
| 111 | content = output_file.read_text() |
| 112 | assert "my-test-app" in content |
| 113 | assert "django-nextjs" in content |
| 114 | |
| 115 | def test_wizard_cancelled_on_project_name(self, tmp_path: Path) -> None: |
| 116 | output_file = tmp_path / "boilerworks.yaml" |
| 117 | with patch("questionary.text") as mock_text: |
| 118 | mock_text.return_value.ask.return_value = None # user hit Ctrl+C |
| 119 | from boilerworks.wizard import run_wizard |
| 120 | |
| 121 | with pytest.raises(SystemExit): |
| 122 | run_wizard(output_path=output_file) |
| 123 | |
| 124 | assert not output_file.exists() |
| 125 | |
| 126 | def test_wizard_cancelled_on_confirm(self, tmp_path: Path) -> None: |
| 127 | output_file = tmp_path / "boilerworks.yaml" |
| 128 | with ( |
| 129 | patch("questionary.text") as mock_text, |
| 130 | patch("questionary.select") as mock_select, |
| 131 | patch("questionary.confirm") as mock_confirm, |
| 132 | patch("questionary.checkbox") as mock_checkbox, |
| 133 | ): |
| 134 | mock_text.return_value.ask.side_effect = ["my-app", "", ""] |
| 135 | mock_select.return_value.ask.side_effect = ["full", "django-nextjs", "standard", "none", "none", "none"] |
| 136 | mock_confirm.return_value.ask.side_effect = [False, False, False] # all confirms → False |
| 137 | mock_checkbox.return_value.ask.return_value = [] |
| 138 | |
| 139 | from boilerworks.wizard import run_wizard |
| 140 | |
| 141 | with pytest.raises(SystemExit): |
| 142 | run_wizard(output_path=output_file) |
| 143 | |
| 144 | assert not output_file.exists() |
| 145 | |
| 146 | def test_wizard_with_cloud_and_region(self, tmp_path: Path) -> None: |
| 147 | output_file = tmp_path / "boilerworks.yaml" |
| 148 | with ( |
| 149 | patch("questionary.text") as mock_text, |
| 150 | patch("questionary.select") as mock_select, |
| 151 | patch("questionary.confirm") as mock_confirm, |
| 152 | patch("questionary.checkbox") as mock_checkbox, |
| 153 | ): |
| 154 | mock_text.return_value.ask.side_effect = [ |
| 155 | "cloud-app", # project name |
| 156 | "us-east-1", # region |
| 157 | "myapp.com", # domain |
| 158 | ] |
| 159 | # fastapi-micro has only one topology, so topology select is skipped |
| 160 | mock_select.return_value.ask.side_effect = [ |
| 161 | "micro", # size |
| 162 | "fastapi-micro", # family |
| 163 | "aws", # cloud (topology prompt skipped — single option) |
| 164 | "ses", # email |
| 165 | "playwright", # e2e |
| 166 | ] |
| 167 | mock_confirm.return_value.ask.return_value = True |
| 168 | mock_checkbox.return_value.ask.return_value = ["soc2"] |
| 169 | |
| 170 | from boilerworks.wizard import run_wizard |
| 171 | |
| 172 | run_wizard(output_path=output_file) |
| 173 | |
| 174 | assert output_file.exists() |
| 175 | content = output_file.read_text() |
| 176 | assert "cloud-app" in content |
| 177 | assert "fastapi-micro" in content |
A
uv.lock
+575
| --- a/uv.lock | ||
| +++ b/uv.lock | ||
| @@ -0,0 +1,575 @@ | ||
| 1 | +version = 1 | |
| 2 | +revision = 3 | |
| 3 | +requires-python = ">=3.12" | |
| 4 | + | |
| 5 | +[[package]] | |
| 6 | +name = "annotated-types" | |
| 7 | +version = "0.7.0" | |
| 8 | +source = { registry = "https://pypi.org/simple" } | |
| 9 | +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } | |
| 10 | +wheels = [ | |
| 11 | + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, | |
| 12 | +] | |
| 13 | + | |
| 14 | +[[package]] | |
| 15 | +name = "boilerworks" | |
| 16 | +version = "0.1.0" | |
| 17 | +source = { editable = "." } | |
| 18 | +dependencies = [ | |
| 19 | + { name = "click" }, | |
| 20 | + { name = "gitpython" }, | |
| 21 | + { name = "jinja2" }, | |
| 22 | + { name = "pydantic" }, | |
| 23 | + { name = "pyyaml" }, | |
| 24 | + { name = "questionary" }, | |
| 25 | + { name = "rich" }, | |
| 26 | +] | |
| 27 | + | |
| 28 | +[package.dev-dependencies] | |
| 29 | +dev = [ | |
| 30 | + { name = "pytest" }, | |
| 31 | + { name = "pytest-cov" }, | |
| 32 | + { name = "ruff" }, | |
| 33 | +] | |
| 34 | + | |
| 35 | +[package.metadata] | |
| 36 | +requires-dist = [ | |
| 37 | + { name = "click", specifier = ">=8.1" }, | |
| 38 | + { name = "gitpython", specifier = ">=3.1" }, | |
| 39 | + { name = "jinja2", specifier = ">=3.0" }, | |
| 40 | + { name = "pydantic", specifier = ">=2.0" }, | |
| 41 | + { name = "pyyaml", specifier = ">=6.0" }, | |
| 42 | + { name = "questionary", specifier = ">=2.0" }, | |
| 43 | + { name = "rich", specifier = ">=13.0" }, | |
| 44 | +] | |
| 45 | + | |
| 46 | +[package.metadata.requires-dev] | |
| 47 | +dev = [ | |
| 48 | + { name = "pytest", specifier = ">=8.0" }, | |
| 49 | + { name = "pytest-cov", specifier = ">=5.0" }, | |
| 50 | + { name = "ruff", specifier = ">=0.9" }, | |
| 51 | +] | |
| 52 | + | |
| 53 | +[[package]] | |
| 54 | +name = "click" | |
| 55 | +version = "8.3.1" | |
| 56 | +source = { registry = "https://pypi.org/simple" } | |
| 57 | +dependencies = [ | |
| 58 | + { name = "colorama", marker = "sys_platform == 'win32'" }, | |
| 59 | +] | |
| 60 | +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" } | |
| 61 | +wheels = [ | |
| 62 | + { 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" }, | |
| 63 | +] | |
| 64 | + | |
| 65 | +[[package]] | |
| 66 | +name = "colorama" | |
| 67 | +version = "0.4.6" | |
| 68 | +source = { registry = "https://pypi.org/simple" } | |
| 69 | +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" } | |
| 70 | +wheels = [ | |
| 71 | + { 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" }, | |
| 72 | +] | |
| 73 | + | |
| 74 | +[[package]] | |
| 75 | +name = "coverage" | |
| 76 | +version = "7.13.5" | |
| 77 | +source = { registry = "https://pypi.org/simple" } | |
| 78 | +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } | |
| 79 | +wheels = [ | |
| 80 | + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, | |
| 81 | + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, | |
| 82 | + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, | |
| 83 | + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, | |
| 84 | + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, | |
| 85 | + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, | |
| 86 | + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, | |
| 87 | + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, | |
| 88 | + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, | |
| 89 | + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, | |
| 90 | + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, | |
| 91 | + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, | |
| 92 | + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, | |
| 93 | + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, | |
| 94 | + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, | |
| 95 | + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, | |
| 96 | + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, | |
| 97 | + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, | |
| 98 | + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, | |
| 99 | + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, | |
| 100 | + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, | |
| 101 | + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, | |
| 102 | + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, | |
| 103 | + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, | |
| 104 | + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, | |
| 105 | + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, | |
| 106 | + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, | |
| 107 | + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, | |
| 108 | + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, | |
| 109 | + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, | |
| 110 | + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, | |
| 111 | + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, | |
| 112 | + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, | |
| 113 | + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, | |
| 114 | + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, | |
| 115 | + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, | |
| 116 | + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, | |
| 117 | + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, | |
| 118 | + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, | |
| 119 | + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, | |
| 120 | + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, | |
| 121 | + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, | |
| 122 | + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, | |
| 123 | + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, | |
| 124 | + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, | |
| 125 | + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, | |
| 126 | + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, | |
| 127 | + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, | |
| 128 | + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, | |
| 129 | + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, | |
| 130 | + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, | |
| 131 | + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, | |
| 132 | + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, | |
| 133 | + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, | |
| 134 | + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, | |
| 135 | + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, | |
| 136 | + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, | |
| 137 | + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, | |
| 138 | + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, | |
| 139 | + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, | |
| 140 | + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, | |
| 141 | + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, | |
| 142 | + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, | |
| 143 | + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, | |
| 144 | + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, | |
| 145 | + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, | |
| 146 | + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, | |
| 147 | + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, | |
| 148 | + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, | |
| 149 | + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, | |
| 150 | + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, | |
| 151 | + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, | |
| 152 | + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, | |
| 153 | + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, | |
| 154 | + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, | |
| 155 | + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, | |
| 156 | +] | |
| 157 | + | |
| 158 | +[[package]] | |
| 159 | +name = "gitdb" | |
| 160 | +version = "4.0.12" | |
| 161 | +source = { registry = "https://pypi.org/simple" } | |
| 162 | +dependencies = [ | |
| 163 | + { name = "smmap" }, | |
| 164 | +] | |
| 165 | +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } | |
| 166 | +wheels = [ | |
| 167 | + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, | |
| 168 | +] | |
| 169 | + | |
| 170 | +[[package]] | |
| 171 | +name = "gitpython" | |
| 172 | +version = "3.1.46" | |
| 173 | +source = { registry = "https://pypi.org/simple" } | |
| 174 | +dependencies = [ | |
| 175 | + { name = "gitdb" }, | |
| 176 | +] | |
| 177 | +sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } | |
| 178 | +wheels = [ | |
| 179 | + { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, | |
| 180 | +] | |
| 181 | + | |
| 182 | +[[package]] | |
| 183 | +name = "iniconfig" | |
| 184 | +version = "2.3.0" | |
| 185 | +source = { registry = "https://pypi.org/simple" } | |
| 186 | +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } | |
| 187 | +wheels = [ | |
| 188 | + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, | |
| 189 | +] | |
| 190 | + | |
| 191 | +[[package]] | |
| 192 | +name = "jinja2" | |
| 193 | +version = "3.1.6" | |
| 194 | +source = { registry = "https://pypi.org/simple" } | |
| 195 | +dependencies = [ | |
| 196 | + { name = "markupsafe" }, | |
| 197 | +] | |
| 198 | +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } | |
| 199 | +wheels = [ | |
| 200 | + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, | |
| 201 | +] | |
| 202 | + | |
| 203 | +[[package]] | |
| 204 | +name = "markdown-it-py" | |
| 205 | +version = "4.0.0" | |
| 206 | +source = { registry = "https://pypi.org/simple" } | |
| 207 | +dependencies = [ | |
| 208 | + { name = "mdurl" }, | |
| 209 | +] | |
| 210 | +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } | |
| 211 | +wheels = [ | |
| 212 | + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, | |
| 213 | +] | |
| 214 | + | |
| 215 | +[[package]] | |
| 216 | +name = "markupsafe" | |
| 217 | +version = "3.0.3" | |
| 218 | +source = { registry = "https://pypi.org/simple" } | |
| 219 | +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } | |
| 220 | +wheels = [ | |
| 221 | + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, | |
| 222 | + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, | |
| 223 | + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, | |
| 224 | + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, | |
| 225 | + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, | |
| 226 | + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, | |
| 227 | + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, | |
| 228 | + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, | |
| 229 | + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, | |
| 230 | + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, | |
| 231 | + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, | |
| 232 | + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, | |
| 233 | + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, | |
| 234 | + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, | |
| 235 | + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, | |
| 236 | + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, | |
| 237 | + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, | |
| 238 | + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, | |
| 239 | + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, | |
| 240 | + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, | |
| 241 | + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, | |
| 242 | + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, | |
| 243 | + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, | |
| 244 | + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, | |
| 245 | + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, | |
| 246 | + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, | |
| 247 | + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, | |
| 248 | + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, | |
| 249 | + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, | |
| 250 | + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, | |
| 251 | + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, | |
| 252 | + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, | |
| 253 | + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, | |
| 254 | + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, | |
| 255 | + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, | |
| 256 | + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, | |
| 257 | + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, | |
| 258 | + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, | |
| 259 | + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, | |
| 260 | + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, | |
| 261 | + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, | |
| 262 | + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, | |
| 263 | + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, | |
| 264 | + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, | |
| 265 | + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, | |
| 266 | + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, | |
| 267 | + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, | |
| 268 | + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, | |
| 269 | + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, | |
| 270 | + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, | |
| 271 | + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, | |
| 272 | + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, | |
| 273 | + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, | |
| 274 | + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, | |
| 275 | + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, | |
| 276 | +] | |
| 277 | + | |
| 278 | +[[package]] | |
| 279 | +name = "mdurl" | |
| 280 | +version = "0.1.2" | |
| 281 | +source = { registry = "https://pypi.org/simple" } | |
| 282 | +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } | |
| 283 | +wheels = [ | |
| 284 | + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, | |
| 285 | +] | |
| 286 | + | |
| 287 | +[[package]] | |
| 288 | +name = "packaging" | |
| 289 | +version = "26.0" | |
| 290 | +source = { registry = "https://pypi.org/simple" } | |
| 291 | +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" } | |
| 292 | +wheels = [ | |
| 293 | + { 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" }, | |
| 294 | +] | |
| 295 | + | |
| 296 | +[[package]] | |
| 297 | +name = "pluggy" | |
| 298 | +version = "1.6.0" | |
| 299 | +source = { registry = "https://pypi.org/simple" } | |
| 300 | +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } | |
| 301 | +wheels = [ | |
| 302 | + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, | |
| 303 | +] | |
| 304 | + | |
| 305 | +[[package]] | |
| 306 | +name = "prompt-toolkit" | |
| 307 | +version = "3.0.52" | |
| 308 | +source = { registry = "https://pypi.org/simple" } | |
| 309 | +dependencies = [ | |
| 310 | + { name = "wcwidth" }, | |
| 311 | +] | |
| 312 | +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } | |
| 313 | +wheels = [ | |
| 314 | + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, | |
| 315 | +] | |
| 316 | + | |
| 317 | +[[package]] | |
| 318 | +name = "pydantic" | |
| 319 | +version = "2.12.5" | |
| 320 | +source = { registry = "https://pypi.org/simple" } | |
| 321 | +dependencies = [ | |
| 322 | + { name = "annotated-types" }, | |
| 323 | + { name = "pydantic-core" }, | |
| 324 | + { name = "typing-extensions" }, | |
| 325 | + { name = "typing-inspection" }, | |
| 326 | +] | |
| 327 | +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } | |
| 328 | +wheels = [ | |
| 329 | + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, | |
| 330 | +] | |
| 331 | + | |
| 332 | +[[package]] | |
| 333 | +name = "pydantic-core" | |
| 334 | +version = "2.41.5" | |
| 335 | +source = { registry = "https://pypi.org/simple" } | |
| 336 | +dependencies = [ | |
| 337 | + { name = "typing-extensions" }, | |
| 338 | +] | |
| 339 | +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } | |
| 340 | +wheels = [ | |
| 341 | + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, | |
| 342 | + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, | |
| 343 | + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, | |
| 344 | + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, | |
| 345 | + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, | |
| 346 | + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, | |
| 347 | + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, | |
| 348 | + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, | |
| 349 | + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, | |
| 350 | + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, | |
| 351 | + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, | |
| 352 | + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, | |
| 353 | + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, | |
| 354 | + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, | |
| 355 | + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, | |
| 356 | + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, | |
| 357 | + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, | |
| 358 | + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, | |
| 359 | + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, | |
| 360 | + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, | |
| 361 | + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, | |
| 362 | + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, | |
| 363 | + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, | |
| 364 | + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, | |
| 365 | + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, | |
| 366 | + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, | |
| 367 | + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, | |
| 368 | + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, | |
| 369 | + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, | |
| 370 | + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, | |
| 371 | + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, | |
| 372 | + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, | |
| 373 | + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, | |
| 374 | + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, | |
| 375 | + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, | |
| 376 | + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, | |
| 377 | + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, | |
| 378 | + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, | |
| 379 | + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, | |
| 380 | + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, | |
| 381 | + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, | |
| 382 | + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, | |
| 383 | + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, | |
| 384 | + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, | |
| 385 | + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, | |
| 386 | + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, | |
| 387 | + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, | |
| 388 | + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, | |
| 389 | + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, | |
| 390 | + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, | |
| 391 | + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, | |
| 392 | + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, | |
| 393 | + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, | |
| 394 | + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, | |
| 395 | + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, | |
| 396 | + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, | |
| 397 | + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, | |
| 398 | + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, | |
| 399 | + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, | |
| 400 | + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, | |
| 401 | +] | |
| 402 | + | |
| 403 | +[[package]] | |
| 404 | +name = "pygments" | |
| 405 | +version = "2.20.0" | |
| 406 | +source = { registry = "https://pypi.org/simple" } | |
| 407 | +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } | |
| 408 | +wheels = [ | |
| 409 | + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, | |
| 410 | +] | |
| 411 | + | |
| 412 | +[[package]] | |
| 413 | +name = "pytest" | |
| 414 | +version = "9.0.2" | |
| 415 | +source = { registry = "https://pypi.org/simple" } | |
| 416 | +dependencies = [ | |
| 417 | + { name = "colorama", marker = "sys_platform == 'win32'" }, | |
| 418 | + { name = "iniconfig" }, | |
| 419 | + { name = "packaging" }, | |
| 420 | + { name = "pluggy" }, | |
| 421 | + { name = "pygments" }, | |
| 422 | +] | |
| 423 | +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } | |
| 424 | +wheels = [ | |
| 425 | + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, | |
| 426 | +] | |
| 427 | + | |
| 428 | +[[package]] | |
| 429 | +name = "pytest-cov" | |
| 430 | +version = "7.1.0" | |
| 431 | +source = { registry = "https://pypi.org/simple" } | |
| 432 | +dependencies = [ | |
| 433 | + { name = "coverage" }, | |
| 434 | + { name = "pluggy" }, | |
| 435 | + { name = "pytest" }, | |
| 436 | +] | |
| 437 | +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } | |
| 438 | +wheels = [ | |
| 439 | + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, | |
| 440 | +] | |
| 441 | + | |
| 442 | +[[package]] | |
| 443 | +name = "pyyaml" | |
| 444 | +version = "6.0.3" | |
| 445 | +source = { registry = "https://pypi.org/simple" } | |
| 446 | +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" } | |
| 447 | +wheels = [ | |
| 448 | + { 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" }, | |
| 449 | + { 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" }, | |
| 450 | + { 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" }, | |
| 451 | + { 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" }, | |
| 452 | + { 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" }, | |
| 453 | + { 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" }, | |
| 454 | + { 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" }, | |
| 455 | + { 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" }, | |
| 456 | + { 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" }, | |
| 457 | + { 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" }, | |
| 458 | + { 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" }, | |
| 459 | + { 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" }, | |
| 460 | + { 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" }, | |
| 461 | + { 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" }, | |
| 462 | + { 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" }, | |
| 463 | + { 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" }, | |
| 464 | + { 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" }, | |
| 465 | + { 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" }, | |
| 466 | + { 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" }, | |
| 467 | + { 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" }, | |
| 468 | + { 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" }, | |
| 469 | + { 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" }, | |
| 470 | + { 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" }, | |
| 471 | + { 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" }, | |
| 472 | + { 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" }, | |
| 473 | + { 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" }, | |
| 474 | + { 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" }, | |
| 475 | + { 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" }, | |
| 476 | + { 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" }, | |
| 477 | + { 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" }, | |
| 478 | + { 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" }, | |
| 479 | + { 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" }, | |
| 480 | + { 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" }, | |
| 481 | + { 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" }, | |
| 482 | + { 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" }, | |
| 483 | + { 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" }, | |
| 484 | + { 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" }, | |
| 485 | + { 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" }, | |
| 486 | +] | |
| 487 | + | |
| 488 | +[[package]] | |
| 489 | +name = "questionary" | |
| 490 | +version = "2.1.1" | |
| 491 | +source = { registry = "https://pypi.org/simple" } | |
| 492 | +dependencies = [ | |
| 493 | + { name = "prompt-toolkit" }, | |
| 494 | +] | |
| 495 | +sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" } | |
| 496 | +wheels = [ | |
| 497 | + { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, | |
| 498 | +] | |
| 499 | + | |
| 500 | +[[package]] | |
| 501 | +name = "rich" | |
| 502 | +version = "14.3.3" | |
| 503 | +source = { registry = "https://pypi.org/simple" } | |
| 504 | +dependencies = [ | |
| 505 | + { name = "markdown-it-py" }, | |
| 506 | + { name = "pygments" }, | |
| 507 | +] | |
| 508 | +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } | |
| 509 | +wheels = [ | |
| 510 | + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, | |
| 511 | +] | |
| 512 | + | |
| 513 | +[[package]] | |
| 514 | +name = "ruff" | |
| 515 | +version = "0.15.8" | |
| 516 | +source = { registry = "https://pypi.org/simple" } | |
| 517 | +sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } | |
| 518 | +wheels = [ | |
| 519 | + { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, | |
| 520 | + { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, | |
| 521 | + { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, | |
| 522 | + { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, | |
| 523 | + { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, | |
| 524 | + { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, | |
| 525 | + { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, | |
| 526 | + { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, | |
| 527 | + { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, | |
| 528 | + { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, | |
| 529 | + { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, | |
| 530 | + { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, | |
| 531 | + { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, | |
| 532 | + { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, | |
| 533 | + { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, | |
| 534 | + { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, | |
| 535 | + { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, | |
| 536 | +] | |
| 537 | + | |
| 538 | +[[package]] | |
| 539 | +name = "smmap" | |
| 540 | +version = "5.0.3" | |
| 541 | +source = { registry = "https://pypi.org/simple" } | |
| 542 | +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } | |
| 543 | +wheels = [ | |
| 544 | + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, | |
| 545 | +] | |
| 546 | + | |
| 547 | +[[package]] | |
| 548 | +name = "typing-extensions" | |
| 549 | +version = "4.15.0" | |
| 550 | +source = { registry = "https://pypi.org/simple" } | |
| 551 | +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" } | |
| 552 | +wheels = [ | |
| 553 | + { 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" }, | |
| 554 | +] | |
| 555 | + | |
| 556 | +[[package]] | |
| 557 | +name = "typing-inspection" | |
| 558 | +version = "0.4.2" | |
| 559 | +source = { registry = "https://pypi.org/simple" } | |
| 560 | +dependencies = [ | |
| 561 | + { name = "typing-extensions" }, | |
| 562 | +] | |
| 563 | +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } | |
| 564 | +wheels = [ | |
| 565 | + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, | |
| 566 | +] | |
| 567 | + | |
| 568 | +[[package]] | |
| 569 | +name = "wcwidth" | |
| 570 | +version = "0.6.0" | |
| 571 | +source = { registry = "https://pypi.org/simple" } | |
| 572 | +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } | |
| 573 | +wheels = [ | |
| 574 | + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, | |
| 575 | +] |
| --- a/uv.lock | |
| +++ b/uv.lock | |
| @@ -0,0 +1,575 @@ | |
| --- a/uv.lock | |
| +++ b/uv.lock | |
| @@ -0,0 +1,575 @@ | |
| 1 | version = 1 |
| 2 | revision = 3 |
| 3 | requires-python = ">=3.12" |
| 4 | |
| 5 | [[package]] |
| 6 | name = "annotated-types" |
| 7 | version = "0.7.0" |
| 8 | source = { registry = "https://pypi.org/simple" } |
| 9 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } |
| 10 | wheels = [ |
| 11 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, |
| 12 | ] |
| 13 | |
| 14 | [[package]] |
| 15 | name = "boilerworks" |
| 16 | version = "0.1.0" |
| 17 | source = { editable = "." } |
| 18 | dependencies = [ |
| 19 | { name = "click" }, |
| 20 | { name = "gitpython" }, |
| 21 | { name = "jinja2" }, |
| 22 | { name = "pydantic" }, |
| 23 | { name = "pyyaml" }, |
| 24 | { name = "questionary" }, |
| 25 | { name = "rich" }, |
| 26 | ] |
| 27 | |
| 28 | [package.dev-dependencies] |
| 29 | dev = [ |
| 30 | { name = "pytest" }, |
| 31 | { name = "pytest-cov" }, |
| 32 | { name = "ruff" }, |
| 33 | ] |
| 34 | |
| 35 | [package.metadata] |
| 36 | requires-dist = [ |
| 37 | { name = "click", specifier = ">=8.1" }, |
| 38 | { name = "gitpython", specifier = ">=3.1" }, |
| 39 | { name = "jinja2", specifier = ">=3.0" }, |
| 40 | { name = "pydantic", specifier = ">=2.0" }, |
| 41 | { name = "pyyaml", specifier = ">=6.0" }, |
| 42 | { name = "questionary", specifier = ">=2.0" }, |
| 43 | { name = "rich", specifier = ">=13.0" }, |
| 44 | ] |
| 45 | |
| 46 | [package.metadata.requires-dev] |
| 47 | dev = [ |
| 48 | { name = "pytest", specifier = ">=8.0" }, |
| 49 | { name = "pytest-cov", specifier = ">=5.0" }, |
| 50 | { name = "ruff", specifier = ">=0.9" }, |
| 51 | ] |
| 52 | |
| 53 | [[package]] |
| 54 | name = "click" |
| 55 | version = "8.3.1" |
| 56 | source = { registry = "https://pypi.org/simple" } |
| 57 | dependencies = [ |
| 58 | { name = "colorama", marker = "sys_platform == 'win32'" }, |
| 59 | ] |
| 60 | 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" } |
| 61 | wheels = [ |
| 62 | { 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" }, |
| 63 | ] |
| 64 | |
| 65 | [[package]] |
| 66 | name = "colorama" |
| 67 | version = "0.4.6" |
| 68 | source = { registry = "https://pypi.org/simple" } |
| 69 | 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" } |
| 70 | wheels = [ |
| 71 | { 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" }, |
| 72 | ] |
| 73 | |
| 74 | [[package]] |
| 75 | name = "coverage" |
| 76 | version = "7.13.5" |
| 77 | source = { registry = "https://pypi.org/simple" } |
| 78 | sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } |
| 79 | wheels = [ |
| 80 | { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, |
| 81 | { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, |
| 82 | { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, |
| 83 | { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, |
| 84 | { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, |
| 85 | { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, |
| 86 | { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, |
| 87 | { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, |
| 88 | { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, |
| 89 | { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, |
| 90 | { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, |
| 91 | { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, |
| 92 | { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, |
| 93 | { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, |
| 94 | { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, |
| 95 | { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, |
| 96 | { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, |
| 97 | { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, |
| 98 | { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, |
| 99 | { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, |
| 100 | { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, |
| 101 | { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, |
| 102 | { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, |
| 103 | { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, |
| 104 | { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, |
| 105 | { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, |
| 106 | { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, |
| 107 | { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, |
| 108 | { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, |
| 109 | { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, |
| 110 | { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, |
| 111 | { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, |
| 112 | { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, |
| 113 | { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, |
| 114 | { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, |
| 115 | { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, |
| 116 | { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, |
| 117 | { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, |
| 118 | { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, |
| 119 | { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, |
| 120 | { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, |
| 121 | { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, |
| 122 | { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, |
| 123 | { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, |
| 124 | { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, |
| 125 | { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, |
| 126 | { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, |
| 127 | { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, |
| 128 | { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, |
| 129 | { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, |
| 130 | { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, |
| 131 | { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, |
| 132 | { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, |
| 133 | { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, |
| 134 | { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, |
| 135 | { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, |
| 136 | { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, |
| 137 | { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, |
| 138 | { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, |
| 139 | { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, |
| 140 | { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, |
| 141 | { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, |
| 142 | { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, |
| 143 | { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, |
| 144 | { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, |
| 145 | { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, |
| 146 | { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, |
| 147 | { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, |
| 148 | { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, |
| 149 | { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, |
| 150 | { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, |
| 151 | { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, |
| 152 | { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, |
| 153 | { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, |
| 154 | { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, |
| 155 | { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, |
| 156 | ] |
| 157 | |
| 158 | [[package]] |
| 159 | name = "gitdb" |
| 160 | version = "4.0.12" |
| 161 | source = { registry = "https://pypi.org/simple" } |
| 162 | dependencies = [ |
| 163 | { name = "smmap" }, |
| 164 | ] |
| 165 | sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } |
| 166 | wheels = [ |
| 167 | { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, |
| 168 | ] |
| 169 | |
| 170 | [[package]] |
| 171 | name = "gitpython" |
| 172 | version = "3.1.46" |
| 173 | source = { registry = "https://pypi.org/simple" } |
| 174 | dependencies = [ |
| 175 | { name = "gitdb" }, |
| 176 | ] |
| 177 | sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } |
| 178 | wheels = [ |
| 179 | { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, |
| 180 | ] |
| 181 | |
| 182 | [[package]] |
| 183 | name = "iniconfig" |
| 184 | version = "2.3.0" |
| 185 | source = { registry = "https://pypi.org/simple" } |
| 186 | sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } |
| 187 | wheels = [ |
| 188 | { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, |
| 189 | ] |
| 190 | |
| 191 | [[package]] |
| 192 | name = "jinja2" |
| 193 | version = "3.1.6" |
| 194 | source = { registry = "https://pypi.org/simple" } |
| 195 | dependencies = [ |
| 196 | { name = "markupsafe" }, |
| 197 | ] |
| 198 | sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } |
| 199 | wheels = [ |
| 200 | { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, |
| 201 | ] |
| 202 | |
| 203 | [[package]] |
| 204 | name = "markdown-it-py" |
| 205 | version = "4.0.0" |
| 206 | source = { registry = "https://pypi.org/simple" } |
| 207 | dependencies = [ |
| 208 | { name = "mdurl" }, |
| 209 | ] |
| 210 | sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } |
| 211 | wheels = [ |
| 212 | { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, |
| 213 | ] |
| 214 | |
| 215 | [[package]] |
| 216 | name = "markupsafe" |
| 217 | version = "3.0.3" |
| 218 | source = { registry = "https://pypi.org/simple" } |
| 219 | sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } |
| 220 | wheels = [ |
| 221 | { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, |
| 222 | { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, |
| 223 | { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, |
| 224 | { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, |
| 225 | { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, |
| 226 | { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, |
| 227 | { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, |
| 228 | { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, |
| 229 | { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, |
| 230 | { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, |
| 231 | { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, |
| 232 | { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, |
| 233 | { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, |
| 234 | { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, |
| 235 | { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, |
| 236 | { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, |
| 237 | { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, |
| 238 | { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, |
| 239 | { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, |
| 240 | { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, |
| 241 | { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, |
| 242 | { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, |
| 243 | { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, |
| 244 | { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, |
| 245 | { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, |
| 246 | { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, |
| 247 | { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, |
| 248 | { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, |
| 249 | { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, |
| 250 | { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, |
| 251 | { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, |
| 252 | { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, |
| 253 | { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, |
| 254 | { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, |
| 255 | { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, |
| 256 | { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, |
| 257 | { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, |
| 258 | { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, |
| 259 | { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, |
| 260 | { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, |
| 261 | { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, |
| 262 | { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, |
| 263 | { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, |
| 264 | { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, |
| 265 | { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, |
| 266 | { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, |
| 267 | { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, |
| 268 | { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, |
| 269 | { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, |
| 270 | { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, |
| 271 | { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, |
| 272 | { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, |
| 273 | { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, |
| 274 | { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, |
| 275 | { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, |
| 276 | ] |
| 277 | |
| 278 | [[package]] |
| 279 | name = "mdurl" |
| 280 | version = "0.1.2" |
| 281 | source = { registry = "https://pypi.org/simple" } |
| 282 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } |
| 283 | wheels = [ |
| 284 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, |
| 285 | ] |
| 286 | |
| 287 | [[package]] |
| 288 | name = "packaging" |
| 289 | version = "26.0" |
| 290 | source = { registry = "https://pypi.org/simple" } |
| 291 | 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" } |
| 292 | wheels = [ |
| 293 | { 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" }, |
| 294 | ] |
| 295 | |
| 296 | [[package]] |
| 297 | name = "pluggy" |
| 298 | version = "1.6.0" |
| 299 | source = { registry = "https://pypi.org/simple" } |
| 300 | sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } |
| 301 | wheels = [ |
| 302 | { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, |
| 303 | ] |
| 304 | |
| 305 | [[package]] |
| 306 | name = "prompt-toolkit" |
| 307 | version = "3.0.52" |
| 308 | source = { registry = "https://pypi.org/simple" } |
| 309 | dependencies = [ |
| 310 | { name = "wcwidth" }, |
| 311 | ] |
| 312 | sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } |
| 313 | wheels = [ |
| 314 | { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, |
| 315 | ] |
| 316 | |
| 317 | [[package]] |
| 318 | name = "pydantic" |
| 319 | version = "2.12.5" |
| 320 | source = { registry = "https://pypi.org/simple" } |
| 321 | dependencies = [ |
| 322 | { name = "annotated-types" }, |
| 323 | { name = "pydantic-core" }, |
| 324 | { name = "typing-extensions" }, |
| 325 | { name = "typing-inspection" }, |
| 326 | ] |
| 327 | sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } |
| 328 | wheels = [ |
| 329 | { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, |
| 330 | ] |
| 331 | |
| 332 | [[package]] |
| 333 | name = "pydantic-core" |
| 334 | version = "2.41.5" |
| 335 | source = { registry = "https://pypi.org/simple" } |
| 336 | dependencies = [ |
| 337 | { name = "typing-extensions" }, |
| 338 | ] |
| 339 | sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } |
| 340 | wheels = [ |
| 341 | { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, |
| 342 | { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, |
| 343 | { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, |
| 344 | { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, |
| 345 | { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, |
| 346 | { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, |
| 347 | { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, |
| 348 | { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, |
| 349 | { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, |
| 350 | { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, |
| 351 | { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, |
| 352 | { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, |
| 353 | { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, |
| 354 | { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, |
| 355 | { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, |
| 356 | { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, |
| 357 | { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, |
| 358 | { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, |
| 359 | { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, |
| 360 | { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, |
| 361 | { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, |
| 362 | { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, |
| 363 | { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, |
| 364 | { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, |
| 365 | { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, |
| 366 | { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, |
| 367 | { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, |
| 368 | { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, |
| 369 | { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, |
| 370 | { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, |
| 371 | { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, |
| 372 | { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, |
| 373 | { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, |
| 374 | { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, |
| 375 | { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, |
| 376 | { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, |
| 377 | { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, |
| 378 | { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, |
| 379 | { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, |
| 380 | { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, |
| 381 | { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, |
| 382 | { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, |
| 383 | { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, |
| 384 | { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, |
| 385 | { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, |
| 386 | { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, |
| 387 | { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, |
| 388 | { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, |
| 389 | { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, |
| 390 | { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, |
| 391 | { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, |
| 392 | { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, |
| 393 | { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, |
| 394 | { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, |
| 395 | { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, |
| 396 | { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, |
| 397 | { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, |
| 398 | { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, |
| 399 | { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, |
| 400 | { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, |
| 401 | ] |
| 402 | |
| 403 | [[package]] |
| 404 | name = "pygments" |
| 405 | version = "2.20.0" |
| 406 | source = { registry = "https://pypi.org/simple" } |
| 407 | sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } |
| 408 | wheels = [ |
| 409 | { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, |
| 410 | ] |
| 411 | |
| 412 | [[package]] |
| 413 | name = "pytest" |
| 414 | version = "9.0.2" |
| 415 | source = { registry = "https://pypi.org/simple" } |
| 416 | dependencies = [ |
| 417 | { name = "colorama", marker = "sys_platform == 'win32'" }, |
| 418 | { name = "iniconfig" }, |
| 419 | { name = "packaging" }, |
| 420 | { name = "pluggy" }, |
| 421 | { name = "pygments" }, |
| 422 | ] |
| 423 | sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } |
| 424 | wheels = [ |
| 425 | { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, |
| 426 | ] |
| 427 | |
| 428 | [[package]] |
| 429 | name = "pytest-cov" |
| 430 | version = "7.1.0" |
| 431 | source = { registry = "https://pypi.org/simple" } |
| 432 | dependencies = [ |
| 433 | { name = "coverage" }, |
| 434 | { name = "pluggy" }, |
| 435 | { name = "pytest" }, |
| 436 | ] |
| 437 | sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } |
| 438 | wheels = [ |
| 439 | { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, |
| 440 | ] |
| 441 | |
| 442 | [[package]] |
| 443 | name = "pyyaml" |
| 444 | version = "6.0.3" |
| 445 | source = { registry = "https://pypi.org/simple" } |
| 446 | 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" } |
| 447 | wheels = [ |
| 448 | { 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" }, |
| 449 | { 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" }, |
| 450 | { 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" }, |
| 451 | { 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" }, |
| 452 | { 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" }, |
| 453 | { 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" }, |
| 454 | { 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" }, |
| 455 | { 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" }, |
| 456 | { 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" }, |
| 457 | { 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" }, |
| 458 | { 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" }, |
| 459 | { 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" }, |
| 460 | { 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" }, |
| 461 | { 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" }, |
| 462 | { 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" }, |
| 463 | { 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" }, |
| 464 | { 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" }, |
| 465 | { 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" }, |
| 466 | { 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" }, |
| 467 | { 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" }, |
| 468 | { 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" }, |
| 469 | { 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" }, |
| 470 | { 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" }, |
| 471 | { 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" }, |
| 472 | { 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" }, |
| 473 | { 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" }, |
| 474 | { 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" }, |
| 475 | { 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" }, |
| 476 | { 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" }, |
| 477 | { 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" }, |
| 478 | { 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" }, |
| 479 | { 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" }, |
| 480 | { 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" }, |
| 481 | { 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" }, |
| 482 | { 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" }, |
| 483 | { 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" }, |
| 484 | { 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" }, |
| 485 | { 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" }, |
| 486 | ] |
| 487 | |
| 488 | [[package]] |
| 489 | name = "questionary" |
| 490 | version = "2.1.1" |
| 491 | source = { registry = "https://pypi.org/simple" } |
| 492 | dependencies = [ |
| 493 | { name = "prompt-toolkit" }, |
| 494 | ] |
| 495 | sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" } |
| 496 | wheels = [ |
| 497 | { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, |
| 498 | ] |
| 499 | |
| 500 | [[package]] |
| 501 | name = "rich" |
| 502 | version = "14.3.3" |
| 503 | source = { registry = "https://pypi.org/simple" } |
| 504 | dependencies = [ |
| 505 | { name = "markdown-it-py" }, |
| 506 | { name = "pygments" }, |
| 507 | ] |
| 508 | sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } |
| 509 | wheels = [ |
| 510 | { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, |
| 511 | ] |
| 512 | |
| 513 | [[package]] |
| 514 | name = "ruff" |
| 515 | version = "0.15.8" |
| 516 | source = { registry = "https://pypi.org/simple" } |
| 517 | sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } |
| 518 | wheels = [ |
| 519 | { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, |
| 520 | { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, |
| 521 | { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, |
| 522 | { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, |
| 523 | { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, |
| 524 | { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, |
| 525 | { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, |
| 526 | { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, |
| 527 | { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, |
| 528 | { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, |
| 529 | { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, |
| 530 | { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, |
| 531 | { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, |
| 532 | { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, |
| 533 | { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, |
| 534 | { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, |
| 535 | { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, |
| 536 | ] |
| 537 | |
| 538 | [[package]] |
| 539 | name = "smmap" |
| 540 | version = "5.0.3" |
| 541 | source = { registry = "https://pypi.org/simple" } |
| 542 | sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } |
| 543 | wheels = [ |
| 544 | { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, |
| 545 | ] |
| 546 | |
| 547 | [[package]] |
| 548 | name = "typing-extensions" |
| 549 | version = "4.15.0" |
| 550 | source = { registry = "https://pypi.org/simple" } |
| 551 | 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" } |
| 552 | wheels = [ |
| 553 | { 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" }, |
| 554 | ] |
| 555 | |
| 556 | [[package]] |
| 557 | name = "typing-inspection" |
| 558 | version = "0.4.2" |
| 559 | source = { registry = "https://pypi.org/simple" } |
| 560 | dependencies = [ |
| 561 | { name = "typing-extensions" }, |
| 562 | ] |
| 563 | sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } |
| 564 | wheels = [ |
| 565 | { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, |
| 566 | ] |
| 567 | |
| 568 | [[package]] |
| 569 | name = "wcwidth" |
| 570 | version = "0.6.0" |
| 571 | source = { registry = "https://pypi.org/simple" } |
| 572 | sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } |
| 573 | wheels = [ |
| 574 | { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, |
| 575 | ] |