diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..75fc109 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,28 @@ +# Read the Docs configuration file for hier-config-cli +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +# Build documentation in the "docs/" directory with MkDocs +mkdocs: + configuration: mkdocs.yml + fail_on_warning: false + +# Set the version of Python +build: + os: ubuntu-22.04 + tools: + python: "3.11" + jobs: + post_checkout: + # Generate any build-time dependencies or files here + - echo "Building hier-config-cli documentation" + post_install: + # Show installed packages for debugging + - pip list + +# Python dependencies required to build the docs +python: + install: + # Install Poetry + - requirements: docs/requirements.txt diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..bdf9875 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,289 @@ +# API Reference + +This page provides detailed API documentation for hier-config-cli's Python modules. + +## Command-Line Interface + +### Main CLI Group + +::: hier_config_cli.cli + options: + show_root_heading: true + show_source: true + +## Core Functions + +### Configuration Processing + +::: hier_config_cli.process_configs + options: + show_root_heading: true + show_source: true + +### Output Formatting + +::: hier_config_cli.format_output + options: + show_root_heading: true + show_source: true + +::: hier_config_cli.get_output_text + options: + show_root_heading: true + show_source: true + +### Logging + +::: hier_config_cli.setup_logging + options: + show_root_heading: true + show_source: true + +## Commands + +### Remediation Command + +::: hier_config_cli.remediation + options: + show_root_heading: true + show_source: true + +### Rollback Command + +::: hier_config_cli.rollback + options: + show_root_heading: true + show_source: true + +### Future Command + +::: hier_config_cli.future + options: + show_root_heading: true + show_source: true + +### List Platforms Command + +::: hier_config_cli.list_platforms + options: + show_root_heading: true + show_source: true + +### Version Command + +::: hier_config_cli.version + options: + show_root_heading: true + show_source: true + +## Constants + +### Platform Mapping + +The `PLATFORM_MAP` dictionary maps platform names to `hier_config.Platform` enum values: + +```python +PLATFORM_MAP = { + "ios": Platform.CISCO_IOS, + "nxos": Platform.CISCO_NXOS, + "iosxr": Platform.CISCO_XR, + "eos": Platform.ARISTA_EOS, + "junos": Platform.JUNIPER_JUNOS, + "vyos": Platform.VYOS, + "fortios": Platform.FORTINET_FORTIOS, + "generic": Platform.GENERIC, + "hp_comware5": Platform.HP_COMWARE5, + "hp_procurve": Platform.HP_PROCURVE, +} +``` + +## Type Definitions + +### Common Types + +```python +from pathlib import Path +from typing import Optional, Union + +# Configuration path types +ConfigPath = Union[str, Path] + +# Output format types +OutputFormat = Literal["text", "json", "yaml"] + +# Operation types +Operation = Literal["remediation", "rollback", "future"] +``` + +## Usage Examples + +### Programmatic Usage + +While hier-config-cli is primarily a CLI tool, you can use its functions programmatically: + +```python +from hier_config_cli import process_configs, format_output +from pathlib import Path + +# Process configurations +result, platform = process_configs( + platform_str="ios", + running_config_path="configs/running.conf", + generated_config_path="configs/intended.conf", + operation="remediation", +) + +# Format output +output = format_output( + hconfig=result, + platform=platform, + output_format="text", +) + +print(output) +``` + +### Custom Integration + +```python +import subprocess +import json +from pathlib import Path + +def generate_remediation( + platform: str, + running_config: Path, + intended_config: Path, +) -> dict: + """Generate remediation using hier-config-cli. + + Args: + platform: Platform name + running_config: Path to running config + intended_config: Path to intended config + + Returns: + Remediation data as dictionary + + Raises: + subprocess.CalledProcessError: If command fails + """ + result = subprocess.run( + [ + "hier-config-cli", + "remediation", + "--platform", platform, + "--running-config", str(running_config), + "--generated-config", str(intended_config), + "--format", "json", + ], + capture_output=True, + text=True, + check=True, + ) + + return json.loads(result.stdout) + +# Usage +remediation = generate_remediation( + platform="ios", + running_config=Path("configs/running.conf"), + intended_config=Path("configs/intended.conf"), +) + +for command in remediation["config"]: + print(command) +``` + +## Error Handling + +### Exception Types + +hier-config-cli uses `click.ClickException` for all user-facing errors: + +```python +from click import ClickException + +try: + result = process_configs( + platform_str="invalid", + running_config_path="running.conf", + generated_config_path="intended.conf", + operation="remediation", + ) +except ClickException as e: + print(f"Error: {e.message}") +``` + +### Common Exceptions + +| Exception | Cause | Message Example | +|-----------|-------|-----------------| +| `ClickException` | Unknown platform | "Unknown platform: invalid_platform" | +| `ClickException` | File not found | "Running config file not found: path/to/file" | +| `ClickException` | Permission denied | "Permission denied reading running config: path/to/file" | +| `ClickException` | Parsing error | "Error parsing configuration: [details]" | + +## Exit Codes + +| Code | Meaning | +|------|---------| +| 0 | Success | +| 1 | Error occurred | + +## Environment Variables + +hier-config-cli doesn't use environment variables directly, but respects standard Python environment variables: + +| Variable | Purpose | +|----------|---------| +| `PYTHONPATH` | Python module search path | +| `PYTHONDONTWRITEBYTECODE` | Disable .pyc files | + +## Logging + +### Log Levels + +Control logging verbosity with the `-v` flag: + +```bash +# WARNING level (default) +hier-config-cli remediation ... + +# INFO level +hier-config-cli -v remediation ... + +# DEBUG level +hier-config-cli -vv remediation ... +``` + +### Log Format + +``` +LEVEL: message +``` + +Examples: +``` +INFO: Using platform: ios +INFO: Reading running config from: running.conf +INFO: Reading generated config from: intended.conf +INFO: Parsing configurations +INFO: Generating remediation configuration +``` + +## Related Documentation + +- [Commands Reference](commands.md) - Detailed command documentation +- [Integration Guide](integration/index.md) - Integration examples +- [Development Guide](development/contributing.md) - Contributing guidelines + +## External Dependencies + +hier-config-cli depends on: + +- [hier-config](https://github.com/netdevops/hier_config) - Core configuration analysis +- [click](https://click.palletsprojects.com/) - CLI framework +- [PyYAML](https://pyyaml.org/) - YAML support + +Refer to their documentation for advanced usage and features. diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..58cdd79 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,155 @@ +# Changelog + +All notable changes to hier-config-cli will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Comprehensive documentation site with MkDocs +- Detailed integration guides for Nornir, Ansible, and CI/CD +- Development guides for contributing, testing, and code quality + +## [0.2.0] - 2024-01-XX + +### Added +- Support for FortiOS platform (requires hier-config 3.4.0+) +- Python 3.13 support +- Modern type annotations using Python 3.10+ syntax + +### Changed +- Upgraded to hier-config 3.4.0 +- Improved type checking with mypy +- Enhanced code quality with ruff linting +- Applied Python 3.10+ type annotation style + +### Fixed +- Resolved mypy type checking errors +- Fixed ruff linting issues including exception chaining +- Improved code documentation and type hints + +## [0.1.0] - 2024-01-XX + +### Added +- Initial release of hier-config-cli +- Core commands: `remediation`, `rollback`, `future`, `list-platforms`, `version` +- Support for multiple platforms: ios, nxos, iosxr, eos, junos, vyos, hp_comware5, hp_procurve, generic +- Multiple output formats: text, json, yaml +- Verbose and debug logging modes +- Comprehensive test suite with pytest +- Type safety with mypy +- Code formatting with black +- Linting with ruff +- CI/CD integration with GitHub Actions + +### Platform Support +- Cisco IOS +- Cisco NX-OS +- Cisco IOS XR +- Arista EOS +- Juniper JunOS +- VyOS +- HP Comware5 +- HP ProCurve +- Generic platform + +### Dependencies +- hier-config ^3.3.0 +- click ^8.1.7 +- pyyaml ^6.0.2 +- Python ^3.10 + +## Release Process + +### Version Numbering + +We follow [Semantic Versioning](https://semver.org/): + +- **MAJOR** version for incompatible API changes +- **MINOR** version for new functionality in a backwards compatible manner +- **PATCH** version for backwards compatible bug fixes + +### Release Checklist + +For maintainers releasing a new version: + +1. Update version in `pyproject.toml` +2. Update version in `src/hier_config_cli/__main__.py` +3. Update `CHANGELOG.md` with release notes +4. Commit changes: `git commit -m "Release v0.x.x"` +5. Create git tag: `git tag v0.x.x` +6. Push changes: `git push origin main --tags` +7. GitHub Actions will automatically publish to PyPI + +## Upgrade Guide + +### Upgrading from 0.1.x to 0.2.x + +No breaking changes. Simply upgrade: + +```bash +pip install --upgrade hier-config-cli +``` + +### Upgrading Dependencies + +If you're using hier-config-cli in your project: + +```toml +# pyproject.toml +[tool.poetry.dependencies] +hier-config-cli = "^0.2.0" +``` + +## Deprecation Policy + +- Features marked as deprecated will be removed in the next major version +- Deprecation warnings will be issued for at least one minor version before removal +- Deprecated features will be documented in the changelog + +## Future Plans + +### Planned Features + +- Configuration templates support +- Batch processing improvements +- Interactive mode +- Configuration validation rules +- Compliance reporting +- More platform support + +### Under Consideration + +- Plugin system for custom platforms +- Web UI for configuration management +- REST API server mode +- Configuration backup/restore +- Diff visualization + +## Community Contributions + +We welcome contributions! See [Contributing Guide](development/contributing.md) for details. + +### Contributors + +Thank you to all contributors who have helped improve hier-config-cli! + +- James Williams (@networktocode) - Creator and maintainer + +## Security Updates + +Security vulnerabilities are taken seriously. See [Security Policy](https://github.com/netdevops/hier-config-cli/security/policy) for reporting procedures. + +## Links + +- **GitHub Repository**: [netdevops/hier-config-cli](https://github.com/netdevops/hier-config-cli) +- **PyPI Package**: [hier-config-cli](https://pypi.org/project/hier-config-cli/) +- **Documentation**: [hier-config-cli.readthedocs.io](https://hier-config-cli.readthedocs.io/) +- **Issue Tracker**: [GitHub Issues](https://github.com/netdevops/hier-config-cli/issues) +- **Discussions**: [GitHub Discussions](https://github.com/netdevops/hier-config-cli/discussions) + +[Unreleased]: https://github.com/netdevops/hier-config-cli/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/netdevops/hier-config-cli/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/netdevops/hier-config-cli/releases/tag/v0.1.0 diff --git a/docs/commands.md b/docs/commands.md new file mode 100644 index 0000000..cf66d3e --- /dev/null +++ b/docs/commands.md @@ -0,0 +1,305 @@ +# Commands Reference + +This page provides a detailed reference for all available hier-config-cli commands. + +## Global Options + +These options are available for all commands: + +| Option | Short | Description | +|--------|-------|-------------| +| `--verbose` | `-v` | Increase verbosity. Use `-v` for INFO level, `-vv` for DEBUG level | +| `--help` | `-h` | Show help message and exit | + +## Commands + +### `remediation` + +Generates the commands needed to transform the running configuration into the intended configuration. + +**Synopsis:** +```bash +hier-config-cli remediation [OPTIONS] +``` + +**Options:** + +| Option | Required | Description | +|--------|----------|-------------| +| `--platform` | Yes | Target platform (ios, nxos, iosxr, eos, junos, vyos, fortios, etc.) | +| `--running-config` | Yes | Path to the running configuration file | +| `--generated-config` | Yes | Path to the intended/generated configuration file | +| `--format` | No | Output format: `text`, `json`, or `yaml` (default: text) | +| `--output`, `-o` | No | Write output to file instead of stdout | + +**Examples:** + +```bash +# Basic usage +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf + +# Save to file +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --output remediation.txt + +# JSON format +hier-config-cli remediation \ + --platform nxos \ + --running-config running.conf \ + --generated-config intended.conf \ + --format json + +# With verbose logging +hier-config-cli -v remediation \ + --platform iosxr \ + --running-config running.conf \ + --generated-config intended.conf +``` + +--- + +### `rollback` + +Generates the commands needed to revert from the intended configuration back to the running configuration. This is useful for preparing rollback procedures before making changes. + +**Synopsis:** +```bash +hier-config-cli rollback [OPTIONS] +``` + +**Options:** + +Same as `remediation` command. + +**Examples:** + +```bash +# Basic usage +hier-config-cli rollback \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf + +# Save to file +hier-config-cli rollback \ + --platform eos \ + --running-config running.conf \ + --generated-config intended.conf \ + --output rollback.txt + +# YAML format +hier-config-cli rollback \ + --platform junos \ + --running-config running.conf \ + --generated-config intended.conf \ + --format yaml +``` + +--- + +### `future` + +Predicts what the complete configuration will look like after applying the intended configuration to the running configuration. + +**Synopsis:** +```bash +hier-config-cli future [OPTIONS] +``` + +**Options:** + +Same as `remediation` command. + +**Examples:** + +```bash +# Basic usage +hier-config-cli future \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf + +# Save to file +hier-config-cli future \ + --platform vyos \ + --running-config running.conf \ + --generated-config intended.conf \ + --output future_state.txt + +# JSON format +hier-config-cli future \ + --platform fortios \ + --running-config running.conf \ + --generated-config intended.conf \ + --format json +``` + +--- + +### `list-platforms` + +Lists all available and supported network platforms. + +**Synopsis:** +```bash +hier-config-cli list-platforms +``` + +**Options:** + +None + +**Example:** + +```bash +hier-config-cli list-platforms +``` + +**Output:** +``` +=== Available Platforms === + eos + fortios + generic + hp_comware5 + hp_procurve + ios + iosxr + junos + nxos + vyos +``` + +--- + +### `version` + +Shows the installed version of hier-config-cli. + +**Synopsis:** +```bash +hier-config-cli version +``` + +**Options:** + +None + +**Example:** + +```bash +hier-config-cli version +``` + +**Output:** +``` +hier-config-cli version 0.2.0 +``` + +## Common Usage Patterns + +### Complete Change Workflow + +```bash +# 1. Generate remediation +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --output remediation.txt + +# 2. Generate rollback (for safety) +hier-config-cli rollback \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --output rollback.txt + +# 3. Preview future state +hier-config-cli future \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --output future.txt + +# 4. Review all files before applying changes +cat remediation.txt +cat rollback.txt +cat future.txt +``` + +### Batch Processing Multiple Devices + +```bash +# Process multiple devices +for device in router1 router2 switch1; do + hier-config-cli remediation \ + --platform ios \ + --running-config configs/${device}_running.conf \ + --generated-config configs/${device}_intended.conf \ + --output remediation/${device}_remediation.txt +done +``` + +### Using with Different Platforms + +```bash +# Cisco IOS +hier-config-cli remediation --platform ios \ + --running-config ios_running.conf \ + --generated-config ios_intended.conf + +# Cisco NX-OS +hier-config-cli remediation --platform nxos \ + --running-config nxos_running.conf \ + --generated-config nxos_intended.conf + +# Arista EOS +hier-config-cli remediation --platform eos \ + --running-config eos_running.conf \ + --generated-config eos_intended.conf + +# Juniper JunOS +hier-config-cli remediation --platform junos \ + --running-config junos_running.conf \ + --generated-config junos_intended.conf +``` + +## Exit Codes + +| Code | Description | +|------|-------------| +| 0 | Success | +| 1 | Error (file not found, invalid platform, parsing error, etc.) | + +## Error Handling + +The CLI provides clear error messages for common issues: + +**File Not Found:** +``` +Error: Running config file not found: /path/to/missing.conf +``` + +**Invalid Platform:** +``` +Error: Unknown platform: invalid_platform. Use 'list-platforms' to see available platforms. +``` + +**Permission Denied:** +``` +Error: Permission denied reading running config: /path/to/file.conf +``` + +## Tips + +1. **Use Tab Completion**: If your shell supports it, enable tab completion for easier command entry +2. **Combine with Shell Tools**: Pipe output to `less`, `grep`, or other tools for analysis +3. **Check File Paths**: Always use absolute paths or ensure you're in the correct directory +4. **Review Before Applying**: Always review generated commands before applying them to production devices diff --git a/docs/development/code-quality.md b/docs/development/code-quality.md new file mode 100644 index 0000000..4eed93e --- /dev/null +++ b/docs/development/code-quality.md @@ -0,0 +1,543 @@ +# Code Quality + +This guide covers code quality standards and tools used in hier-config-cli. + +## Overview + +hier-config-cli maintains high code quality through: + +- **Black**: Code formatting +- **Ruff**: Fast Python linting +- **mypy**: Static type checking +- **pytest**: Testing framework +- **pre-commit**: Automated checks + +## Code Formatting with Black + +[Black](https://black.readthedocs.io/) is used for consistent code formatting. + +### Configuration + +**pyproject.toml:** +```toml +[tool.black] +line-length = 100 +target-version = ["py310", "py311", "py312", "py313"] +``` + +### Usage + +```bash +# Format all code +black src/ tests/ + +# Check formatting without changes +black --check src/ tests/ + +# Show diff of changes +black --diff src/ tests/ + +# Format specific file +black src/hier_config_cli/__main__.py +``` + +### Integration + +**VS Code settings.json:** +```json +{ + "python.formatting.provider": "black", + "python.formatting.blackArgs": ["--line-length", "100"], + "editor.formatOnSave": true +} +``` + +**PyCharm:** +1. Install Black plugin +2. Settings → Tools → Black → Enable +3. Set line length to 100 + +## Linting with Ruff + +[Ruff](https://docs.astral.sh/ruff/) is an extremely fast Python linter. + +### Configuration + +**pyproject.toml:** +```toml +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [] +``` + +### Usage + +```bash +# Lint all code +ruff check src/ tests/ + +# Fix auto-fixable issues +ruff check --fix src/ tests/ + +# Show all violations +ruff check --output-format=full src/ + +# Watch for changes +ruff check --watch src/ +``` + +### Common Rules + +| Rule | Description | Example | +|------|-------------|---------| +| E501 | Line too long | Lines should be ≤100 chars | +| F401 | Unused import | Remove unused imports | +| F841 | Unused variable | Remove or use variable | +| B006 | Mutable default argument | Use `None` instead | +| UP006 | Use `list` instead of `List` | Modern type syntax | + +### Fixing Issues + +```python +# Before (E501 - line too long) +def long_function_name(param1, param2, param3, param4, param5, param6, param7): + pass + +# After +def long_function_name( + param1, param2, param3, + param4, param5, param6, + param7, +): + pass + +# Before (F401 - unused import) +import json +import os # Not used + +# After +import json + +# Before (B006 - mutable default) +def process(items=[]): + pass + +# After +def process(items=None): + if items is None: + items = [] +``` + +## Type Checking with mypy + +[mypy](https://mypy.readthedocs.io/) provides static type checking. + +### Configuration + +**pyproject.toml:** +```toml +[tool.mypy] +python_version = "3.10" +warn_return_any = false +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = false +strict_equality = true + +[[tool.mypy.overrides]] +module = [ + "hier_config.*", + "click.*", +] +ignore_missing_imports = true +``` + +### Usage + +```bash +# Type check all code +mypy src/ + +# Check specific file +mypy src/hier_config_cli/__main__.py + +# Show error codes +mypy --show-error-codes src/ + +# Generate coverage report +mypy --html-report mypy-report src/ +``` + +### Writing Type-Safe Code + +```python +from pathlib import Path +from typing import Optional, Union, List +from collections.abc import Callable + +# Function with type hints +def read_config( + path: Path, + encoding: str = "utf-8", +) -> str: + """Read configuration file. + + Args: + path: Path to config file + encoding: File encoding + + Returns: + File contents as string + + Raises: + FileNotFoundError: If file doesn't exist + """ + return path.read_text(encoding=encoding) + +# Using Optional for nullable values +def find_device(name: str) -> Optional[dict]: + """Find device by name. + + Args: + name: Device name + + Returns: + Device dict if found, None otherwise + """ + # Implementation + return None + +# Type aliases for clarity +DeviceConfig = dict[str, str] +ConfigList = list[DeviceConfig] + +def process_devices(devices: ConfigList) -> None: + """Process multiple device configurations.""" + pass + +# Callable types +ProcessFunc = Callable[[str], str] + +def apply_transform(data: str, func: ProcessFunc) -> str: + """Apply transformation function to data.""" + return func(data) +``` + +### Common Type Errors + +```python +# Error: Incompatible return type +def get_count() -> int: + return "5" # Error: expected int, got str + +# Fix +def get_count() -> int: + return 5 + +# Error: Missing type annotation +def process(data): # Error: missing type hints + return data + +# Fix +def process(data: str) -> str: + return data + +# Error: Incompatible types in assignment +value: int = "hello" # Error + +# Fix +value: str = "hello" +``` + +## Pre-commit Hooks + +[pre-commit](https://pre-commit.com/) runs checks before each commit. + +### Installation + +```bash +pip install pre-commit +pre-commit install +``` + +### Configuration + +**.pre-commit-config.yaml:** +```yaml +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-merge-conflict + + - repo: https://github.com/psf/black + rev: 24.10.0 + hooks: + - id: black + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 + hooks: + - id: ruff + args: [--fix] + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.13.0 + hooks: + - id: mypy + additional_dependencies: [types-pyyaml] + args: [--strict] +``` + +### Usage + +```bash +# Run on all files +pre-commit run --all-files + +# Run specific hook +pre-commit run black --all-files + +# Skip hooks for a commit +git commit --no-verify + +# Update hooks to latest versions +pre-commit autoupdate +``` + +## Code Review Checklist + +Before submitting code for review: + +### Functionality +- [ ] Code works as intended +- [ ] Edge cases are handled +- [ ] Error handling is appropriate +- [ ] No hardcoded values + +### Code Quality +- [ ] Black formatting applied +- [ ] No Ruff violations +- [ ] mypy type checking passes +- [ ] Meaningful variable/function names +- [ ] Code is DRY (Don't Repeat Yourself) + +### Testing +- [ ] Tests are written +- [ ] Tests pass +- [ ] Coverage is adequate +- [ ] Edge cases are tested + +### Documentation +- [ ] Docstrings are present +- [ ] Comments explain "why", not "what" +- [ ] README updated if needed +- [ ] API docs updated if needed + +### Security +- [ ] No secrets in code +- [ ] Input validation present +- [ ] No SQL injection risks +- [ ] No command injection risks + +## Continuous Integration + +### GitHub Actions Workflow + +**.github/workflows/quality.yml:** +```yaml +name: Code Quality + +on: [push, pull_request] + +jobs: + quality: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install poetry + poetry install + + - name: Check formatting with Black + run: poetry run black --check src/ tests/ + + - name: Lint with Ruff + run: poetry run ruff check src/ tests/ + + - name: Type check with mypy + run: poetry run mypy src/ + + - name: Run tests + run: poetry run pytest --cov=hier_config_cli + + - name: Check coverage threshold + run: | + poetry run pytest --cov=hier_config_cli --cov-fail-under=90 +``` + +## Quality Metrics + +### Code Coverage + +Target: **>90% coverage** + +```bash +# Generate coverage report +pytest --cov=hier_config_cli --cov-report=term-missing + +# Fail if below threshold +pytest --cov=hier_config_cli --cov-fail-under=90 +``` + +### Type Coverage + +Target: **100% type coverage** + +```bash +# Check type coverage +mypy --strict src/ +``` + +### Complexity + +Keep cyclomatic complexity low: + +```bash +# Install radon +pip install radon + +# Check complexity +radon cc src/ -a + +# Grade: A is best, F is worst +# Aim for A or B grades +``` + +## Best Practices + +### Code Organization + +```python +# Good: Organized imports +import json +import sys +from pathlib import Path +from typing import Optional + +import click +import yaml +from hier_config import HConfig + +# Module-level constants +DEFAULT_TIMEOUT = 30 +MAX_RETRIES = 3 + +# Functions and classes +def main() -> None: + pass +``` + +### Error Messages + +```python +# Good: Clear, actionable error messages +raise click.ClickException( + f"Configuration file not found: {path}\n" + f"Please check the path and try again." +) + +# Bad: Vague error message +raise Exception("Error") +``` + +### Function Length + +Keep functions focused and concise: + +```python +# Good: Single responsibility +def read_file(path: Path) -> str: + """Read file contents.""" + return path.read_text() + +def parse_config(content: str) -> dict: + """Parse configuration content.""" + return json.loads(content) + +# Bad: Too much in one function +def read_and_parse_and_validate(path: Path) -> dict: + content = path.read_text() + data = json.loads(content) + if not validate(data): + raise ValueError("Invalid") + return data +``` + +### Documentation + +```python +# Good: Clear docstring with examples +def calculate_diff( + running: str, + intended: str, +) -> list[str]: + """Calculate configuration differences. + + Args: + running: Running configuration + intended: Intended configuration + + Returns: + List of commands needed for remediation + + Example: + >>> calculate_diff("hostname old", "hostname new") + ['no hostname old', 'hostname new'] + """ + pass +``` + +## Tools Summary + +| Tool | Purpose | Command | +|------|---------|---------| +| Black | Code formatting | `black src/ tests/` | +| Ruff | Linting | `ruff check src/ tests/` | +| mypy | Type checking | `mypy src/` | +| pytest | Testing | `pytest` | +| pre-commit | Automated checks | `pre-commit run --all-files` | + +## Next Steps + +- Review [Testing Guide](testing.md) +- Read [Contributing Guidelines](contributing.md) +- Explore [Commands Reference](../commands.md) diff --git a/docs/development/contributing.md b/docs/development/contributing.md new file mode 100644 index 0000000..7c96d7f --- /dev/null +++ b/docs/development/contributing.md @@ -0,0 +1,427 @@ +# Contributing + +Thank you for your interest in contributing to hier-config-cli! This document provides guidelines and instructions for contributing. + +## Code of Conduct + +By participating in this project, you agree to maintain a respectful and inclusive environment for everyone. + +## How to Contribute + +### Reporting Bugs + +If you find a bug, please open an issue on GitHub with: + +- **Clear title** describing the issue +- **Detailed description** of the bug +- **Steps to reproduce** the issue +- **Expected behavior** vs actual behavior +- **Environment details** (OS, Python version, hier-config-cli version) +- **Sample configs** if applicable (sanitized) + +**Example:** +```markdown +## Bug Report + +### Description +hier-config-cli fails when processing FortiOS configurations with certain syntax. + +### Steps to Reproduce +1. Run: `hier-config-cli remediation --platform fortios --running-config running.conf --generated-config intended.conf` +2. Error occurs during parsing + +### Expected Behavior +Should generate remediation configuration + +### Actual Behavior +Raises parsing error: [error message] + +### Environment +- OS: Ubuntu 22.04 +- Python: 3.11.5 +- hier-config-cli: 0.2.0 +- hier-config: 3.4.0 +``` + +### Suggesting Features + +Feature requests are welcome! Please open an issue with: + +- **Clear use case** explaining why the feature would be useful +- **Detailed description** of the proposed functionality +- **Example usage** showing how it would work +- **Alternative solutions** you've considered + +### Submitting Pull Requests + +1. **Fork the repository** +2. **Create a feature branch** from `main` +3. **Make your changes** +4. **Add tests** for new functionality +5. **Update documentation** as needed +6. **Run all quality checks** (tests, linting, type checking) +7. **Submit a pull request** + +## Development Setup + +### Prerequisites + +- Python 3.10 or higher +- Poetry for dependency management +- Git + +### Initial Setup + +```bash +# Clone your fork +git clone https://github.com/YOUR_USERNAME/hier-config-cli.git +cd hier-config-cli + +# Install dependencies +poetry install + +# Activate virtual environment +poetry shell + +# Verify installation +hier-config-cli version +``` + +### Running Tests + +```bash +# Run all tests +pytest + +# Run with coverage +pytest --cov=hier_config_cli --cov-report=html + +# Run specific test file +pytest tests/test_cli.py + +# Run with verbose output +pytest -v + +# Run and watch for changes +pytest-watch +``` + +### Code Quality Checks + +Before submitting a PR, ensure all quality checks pass: + +```bash +# Format code with black +black src/ tests/ + +# Check linting with ruff +ruff check src/ tests/ + +# Fix auto-fixable issues +ruff check --fix src/ tests/ + +# Type check with mypy +mypy src/ + +# Run all checks at once +black src/ tests/ && ruff check src/ tests/ && mypy src/ && pytest +``` + +### Pre-commit Hooks + +We recommend using pre-commit hooks: + +```bash +# Install pre-commit +pip install pre-commit + +# Install hooks +pre-commit install + +# Run manually +pre-commit run --all-files +``` + +**.pre-commit-config.yaml:** +```yaml +repos: + - repo: https://github.com/psf/black + rev: 24.10.0 + hooks: + - id: black + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 + hooks: + - id: ruff + args: [--fix] + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.13.0 + hooks: + - id: mypy + additional_dependencies: [types-pyyaml] +``` + +## Coding Standards + +### Code Style + +- Follow **PEP 8** style guidelines +- Use **Black** for code formatting (line length: 100) +- Use **type hints** for all functions +- Write **docstrings** for all public functions/classes + +### Type Hints + +```python +from pathlib import Path +from typing import Optional + +def process_config( + config_path: Path, + platform: str, + output_format: str = "text", +) -> str: + """Process configuration file. + + Args: + config_path: Path to configuration file + platform: Platform name + output_format: Output format (text, json, yaml) + + Returns: + Processed configuration as string + + Raises: + FileNotFoundError: If config file doesn't exist + ValueError: If platform is unknown + """ + # Implementation + pass +``` + +### Docstring Style + +Use Google-style docstrings: + +```python +def example_function(param1: str, param2: int) -> bool: + """Brief description of function. + + Longer description if needed. Can span multiple + lines and paragraphs. + + Args: + param1: Description of param1 + param2: Description of param2 + + Returns: + Description of return value + + Raises: + ValueError: When param2 is negative + TypeError: When param1 is not a string + + Example: + >>> example_function("test", 42) + True + """ + pass +``` + +### Error Handling + +```python +import click + +# Use click.ClickException for user-facing errors +try: + config = read_file(path) +except FileNotFoundError: + raise click.ClickException( + f"Configuration file not found: {path}" + ) from None + +# Chain exceptions for debugging +except Exception as e: + raise click.ClickException( + f"Error processing configuration: {e}" + ) from e +``` + +## Writing Tests + +### Test Structure + +```python +import pytest +from pathlib import Path +from hier_config_cli import process_configs + +def test_remediation_generation(): + """Test remediation configuration generation.""" + # Arrange + platform = "ios" + running_config = "test_data/running.conf" + intended_config = "test_data/intended.conf" + + # Act + result, platform_enum = process_configs( + platform, + running_config, + intended_config, + "remediation", + ) + + # Assert + assert result is not None + assert len(list(result.all_children())) > 0 + +def test_invalid_platform(): + """Test error handling for invalid platform.""" + with pytest.raises(click.ClickException) as exc_info: + process_configs( + "invalid_platform", + "running.conf", + "intended.conf", + "remediation", + ) + + assert "Unknown platform" in str(exc_info.value) +``` + +### Test Coverage + +- Aim for **>90% code coverage** +- Test **happy paths** and **error conditions** +- Include **edge cases** +- Use **fixtures** for common test data + +```python +import pytest +from pathlib import Path + +@pytest.fixture +def test_configs(tmp_path): + """Create test configuration files.""" + running = tmp_path / "running.conf" + running.write_text("hostname test-router\n") + + intended = tmp_path / "intended.conf" + intended.write_text("hostname new-router\n") + + return { + "running": str(running), + "intended": str(intended), + } + +def test_with_fixture(test_configs): + """Test using fixture.""" + result = process_configs( + "ios", + test_configs["running"], + test_configs["intended"], + "remediation", + ) + assert result is not None +``` + +## Documentation + +### Updating Documentation + +- Update docs in the `docs/` directory +- Use Markdown format +- Include code examples +- Add links to related pages + +### Building Documentation Locally + +```bash +# Install mkdocs +pip install mkdocs mkdocs-material mkdocstrings[python] + +# Serve locally +mkdocs serve + +# Build static site +mkdocs build + +# Open in browser +# Navigate to http://127.0.0.1:8000 +``` + +### Documentation Style + +- Use **clear, concise language** +- Provide **working examples** +- Include **expected output** +- Add **troubleshooting tips** +- Link to **related documentation** + +## Pull Request Process + +### Before Submitting + +- [ ] All tests pass +- [ ] Code is formatted with Black +- [ ] No linting errors from Ruff +- [ ] Type checking passes with mypy +- [ ] Documentation is updated +- [ ] CHANGELOG.md is updated (if applicable) +- [ ] Commit messages are clear and descriptive + +### PR Description Template + +```markdown +## Description +Brief description of changes + +## Type of Change +- [ ] Bug fix +- [ ] New feature +- [ ] Breaking change +- [ ] Documentation update + +## Testing +Describe the tests you ran and their results + +## Checklist +- [ ] Tests pass locally +- [ ] Code follows style guidelines +- [ ] Documentation updated +- [ ] No new warnings +``` + +### Review Process + +1. Maintainer reviews code +2. Automated checks run (tests, linting, type checking) +3. Feedback is provided if changes needed +4. Once approved, PR is merged + +## Release Process + +(For maintainers) + +1. Update version in `pyproject.toml` and `src/hier_config_cli/__main__.py` +2. Update `CHANGELOG.md` +3. Create a git tag: `git tag v0.x.x` +4. Push tag: `git push origin v0.x.x` +5. GitHub Actions will build and publish to PyPI + +## Getting Help + +- **GitHub Issues**: [Report bugs or request features](https://github.com/netdevops/hier-config-cli/issues) +- **GitHub Discussions**: [Ask questions](https://github.com/netdevops/hier-config-cli/discussions) +- **Email**: james.williams@packetgeek.net + +## Recognition + +Contributors will be recognized in: +- CHANGELOG.md +- GitHub contributors page +- Release notes + +Thank you for contributing to hier-config-cli! diff --git a/docs/development/testing.md b/docs/development/testing.md new file mode 100644 index 0000000..9b28f3a --- /dev/null +++ b/docs/development/testing.md @@ -0,0 +1,546 @@ +# Testing + +This guide covers testing practices and procedures for hier-config-cli. + +## Test Framework + +hier-config-cli uses [pytest](https://pytest.org/) as its testing framework, along with pytest-cov for coverage reporting. + +## Running Tests + +### Basic Test Execution + +```bash +# Run all tests +pytest + +# Run with verbose output +pytest -v + +# Run specific test file +pytest tests/test_cli.py + +# Run specific test function +pytest tests/test_cli.py::test_remediation_command + +# Run tests matching a pattern +pytest -k "remediation" +``` + +### Coverage Reporting + +```bash +# Run with coverage +pytest --cov=hier_config_cli + +# Generate HTML coverage report +pytest --cov=hier_config_cli --cov-report=html + +# Open coverage report +open htmlcov/index.html # macOS +xdg-open htmlcov/index.html # Linux + +# Generate XML coverage report (for CI) +pytest --cov=hier_config_cli --cov-report=xml +``` + +### Watch Mode + +For development, use pytest-watch to automatically run tests on file changes: + +```bash +# Install pytest-watch +pip install pytest-watch + +# Run in watch mode +ptw +``` + +## Test Structure + +### Directory Layout + +``` +tests/ +├── __init__.py +├── conftest.py # Shared fixtures +├── test_cli.py # CLI command tests +├── test_config.py # Configuration processing tests +├── test_formats.py # Output format tests +└── fixtures/ # Test data + ├── configs/ + │ ├── ios/ + │ │ ├── running.conf + │ │ └── intended.conf + │ └── nxos/ + │ ├── running.conf + │ └── intended.conf + └── expected/ + └── remediation/ + └── ios_remediation.txt +``` + +### Test File Organization + +```python +"""Tests for CLI commands.""" + +import pytest +from click.testing import CliRunner +from hier_config_cli import cli + +class TestRemediationCommand: + """Tests for remediation command.""" + + def test_basic_remediation(self, test_configs): + """Test basic remediation generation.""" + pass + + def test_remediation_with_output_file(self, test_configs, tmp_path): + """Test remediation with output file.""" + pass + + def test_remediation_json_format(self, test_configs): + """Test remediation with JSON output.""" + pass + +class TestRollbackCommand: + """Tests for rollback command.""" + + def test_basic_rollback(self, test_configs): + """Test basic rollback generation.""" + pass +``` + +## Writing Tests + +### Test Fixtures + +**conftest.py:** +```python +"""Shared test fixtures.""" + +import pytest +from pathlib import Path + +@pytest.fixture +def test_data_dir(): + """Return path to test data directory.""" + return Path(__file__).parent / "fixtures" + +@pytest.fixture +def ios_configs(test_data_dir, tmp_path): + """Create temporary IOS config files.""" + # Copy test configs to temp directory + running_config = """hostname router-01 +interface GigabitEthernet0/0 + description WAN Interface + ip address 10.0.1.1 255.255.255.0 +! +""" + intended_config = """hostname router-01-updated +interface GigabitEthernet0/0 + description WAN Interface - Updated + ip address 10.0.1.1 255.255.255.0 +! +interface Vlan20 + description Guest VLAN + ip address 10.0.20.1 255.255.255.0 +! +""" + + running_file = tmp_path / "running.conf" + running_file.write_text(running_config) + + intended_file = tmp_path / "intended.conf" + intended_file.write_text(intended_config) + + return { + "platform": "ios", + "running": str(running_file), + "intended": str(intended_file), + } + +@pytest.fixture +def cli_runner(): + """Return Click CLI test runner.""" + return CliRunner() +``` + +### CLI Testing + +```python +from click.testing import CliRunner +from hier_config_cli import cli + +def test_remediation_command(cli_runner, ios_configs): + """Test remediation command.""" + result = cli_runner.invoke( + cli, + [ + "remediation", + "--platform", ios_configs["platform"], + "--running-config", ios_configs["running"], + "--generated-config", ios_configs["intended"], + ], + ) + + assert result.exit_code == 0 + assert "Remediation Configuration" in result.output + assert "hostname router-01-updated" in result.output + +def test_remediation_with_output_file(cli_runner, ios_configs, tmp_path): + """Test remediation with output file.""" + output_file = tmp_path / "remediation.txt" + + result = cli_runner.invoke( + cli, + [ + "remediation", + "--platform", ios_configs["platform"], + "--running-config", ios_configs["running"], + "--generated-config", ios_configs["intended"], + "--output", str(output_file), + ], + ) + + assert result.exit_code == 0 + assert output_file.exists() + content = output_file.read_text() + assert "hostname router-01-updated" in content + +def test_invalid_platform(cli_runner, ios_configs): + """Test error handling for invalid platform.""" + result = cli_runner.invoke( + cli, + [ + "remediation", + "--platform", "invalid", + "--running-config", ios_configs["running"], + "--generated-config", ios_configs["intended"], + ], + ) + + assert result.exit_code != 0 + assert "Unknown platform" in result.output +``` + +### Testing Output Formats + +```python +import json +import yaml + +def test_json_output(cli_runner, ios_configs): + """Test JSON output format.""" + result = cli_runner.invoke( + cli, + [ + "remediation", + "--platform", ios_configs["platform"], + "--running-config", ios_configs["running"], + "--generated-config", ios_configs["intended"], + "--format", "json", + ], + ) + + assert result.exit_code == 0 + + # Parse JSON output + # Skip the header line + json_output = "\n".join(result.output.split("\n")[1:]) + data = json.loads(json_output) + + assert "config" in data + assert isinstance(data["config"], list) + assert len(data["config"]) > 0 + +def test_yaml_output(cli_runner, ios_configs): + """Test YAML output format.""" + result = cli_runner.invoke( + cli, + [ + "remediation", + "--platform", ios_configs["platform"], + "--running-config", ios_configs["running"], + "--generated-config", ios_configs["intended"], + "--format", "yaml", + ], + ) + + assert result.exit_code == 0 + + # Parse YAML output + yaml_output = "\n".join(result.output.split("\n")[1:]) + data = yaml.safe_load(yaml_output) + + assert "config" in data + assert isinstance(data["config"], list) +``` + +### Parameterized Tests + +```python +@pytest.mark.parametrize("platform,expected", [ + ("ios", "cisco_style"), + ("nxos", "cisco_style"), + ("iosxr", "cisco_style"), + ("eos", "cisco_style"), +]) +def test_multiple_platforms(cli_runner, tmp_path, platform, expected): + """Test remediation for multiple platforms.""" + # Create test configs + running = tmp_path / "running.conf" + running.write_text("hostname old-device\n") + + intended = tmp_path / "intended.conf" + intended.write_text("hostname new-device\n") + + result = cli_runner.invoke( + cli, + [ + "remediation", + "--platform", platform, + "--running-config", str(running), + "--generated-config", str(intended), + ], + ) + + assert result.exit_code == 0 + assert "new-device" in result.output +``` + +### Testing Error Conditions + +```python +def test_missing_running_config(cli_runner, tmp_path): + """Test error when running config is missing.""" + intended = tmp_path / "intended.conf" + intended.write_text("hostname test\n") + + result = cli_runner.invoke( + cli, + [ + "remediation", + "--platform", "ios", + "--running-config", str(tmp_path / "nonexistent.conf"), + "--generated-config", str(intended), + ], + ) + + assert result.exit_code != 0 + assert "not found" in result.output.lower() + +def test_permission_error(cli_runner, ios_configs, tmp_path): + """Test error handling for permission issues.""" + import os + + # Create unreadable file + unreadable = tmp_path / "unreadable.conf" + unreadable.write_text("hostname test\n") + os.chmod(unreadable, 0o000) + + result = cli_runner.invoke( + cli, + [ + "remediation", + "--platform", "ios", + "--running-config", str(unreadable), + "--generated-config", ios_configs["intended"], + ], + ) + + # Cleanup + os.chmod(unreadable, 0o644) + + assert result.exit_code != 0 + assert "permission" in result.output.lower() +``` + +## Test Coverage Goals + +### Coverage Targets + +- **Overall**: >90% +- **Critical paths**: 100% +- **Error handling**: >95% +- **CLI commands**: >95% + +### Checking Coverage + +```bash +# Generate coverage report +pytest --cov=hier_config_cli --cov-report=term-missing + +# View detailed coverage +pytest --cov=hier_config_cli --cov-report=html +open htmlcov/index.html +``` + +### Coverage Configuration + +**pyproject.toml:** +```toml +[tool.coverage.run] +source = ["src"] +omit = ["*/tests/*", "*/__pycache__/*"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] +``` + +## Integration Tests + +### Full Workflow Tests + +```python +def test_complete_workflow(cli_runner, ios_configs, tmp_path): + """Test complete remediation workflow.""" + remediation_file = tmp_path / "remediation.txt" + rollback_file = tmp_path / "rollback.txt" + future_file = tmp_path / "future.txt" + + # Generate remediation + result = cli_runner.invoke( + cli, + [ + "remediation", + "--platform", ios_configs["platform"], + "--running-config", ios_configs["running"], + "--generated-config", ios_configs["intended"], + "--output", str(remediation_file), + ], + ) + assert result.exit_code == 0 + assert remediation_file.exists() + + # Generate rollback + result = cli_runner.invoke( + cli, + [ + "rollback", + "--platform", ios_configs["platform"], + "--running-config", ios_configs["running"], + "--generated-config", ios_configs["intended"], + "--output", str(rollback_file), + ], + ) + assert result.exit_code == 0 + assert rollback_file.exists() + + # Generate future state + result = cli_runner.invoke( + cli, + [ + "future", + "--platform", ios_configs["platform"], + "--running-config", ios_configs["running"], + "--generated-config", ios_configs["intended"], + "--output", str(future_file), + ], + ) + assert result.exit_code == 0 + assert future_file.exists() +``` + +## Performance Tests + +```python +import time + +def test_performance_large_config(cli_runner, tmp_path): + """Test performance with large configuration.""" + # Generate large config + lines = ["interface GigabitEthernet0/{}\n description Test\n".format(i) + for i in range(1000)] + + running = tmp_path / "running.conf" + running.write_text("".join(lines)) + + intended = tmp_path / "intended.conf" + intended.write_text("".join(lines) + "ntp server 192.0.2.1\n") + + start_time = time.time() + + result = cli_runner.invoke( + cli, + [ + "remediation", + "--platform", "ios", + "--running-config", str(running), + "--generated-config", str(intended), + ], + ) + + duration = time.time() - start_time + + assert result.exit_code == 0 + assert duration < 5.0 # Should complete in under 5 seconds +``` + +## CI/CD Integration + +### GitHub Actions + +**.github/workflows/test.yml:** +```yaml +name: Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.10', '3.11', '3.12'] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + pip install poetry + poetry install + + - name: Run tests + run: | + poetry run pytest --cov=hier_config_cli --cov-report=xml + + - name: Upload coverage + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml +``` + +## Best Practices + +1. **Write tests first** (TDD approach when possible) +2. **Test both success and failure paths** +3. **Use meaningful test names** that describe what is being tested +4. **Keep tests isolated** - each test should be independent +5. **Use fixtures** for common setup +6. **Mock external dependencies** when appropriate +7. **Aim for high coverage** but focus on meaningful tests +8. **Run tests before committing** +9. **Keep tests fast** - slow tests won't be run often +10. **Update tests** when changing functionality + +## Next Steps + +- Learn about [Code Quality](code-quality.md) standards +- Review [Contributing Guidelines](contributing.md) +- Explore the [Commands Reference](../commands.md) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..d92cbb4 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,82 @@ +# Hier Config CLI + +[![PyPI version](https://badge.fury.io/py/hier-config-cli.svg)](https://badge.fury.io/py/hier-config-cli) +[![Python Versions](https://img.shields.io/pypi/pyversions/hier-config-cli.svg)](https://pypi.org/project/hier-config-cli/) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![Tests](https://github.com/netdevops/hier-config-cli/workflows/test-app/badge.svg)](https://github.com/netdevops/hier-config-cli/actions) + +A powerful command-line interface tool for network configuration analysis, remediation, and rollback built on top of the [Hier Config](https://github.com/netdevops/hier_config) library. + +## Overview + +**hier-config-cli** helps network engineers analyze configuration differences, generate remediation commands, prepare rollback procedures, and predict configuration states across multiple network platforms. It's an essential tool for network automation workflows, change management, and configuration compliance. + +## Key Features + +- **Remediation Generation**: Automatically generate commands to transform running config into intended config +- **Rollback Planning**: Create rollback procedures before making changes +- **Future State Prediction**: Preview the complete configuration after applying changes +- **Multi-Platform Support**: Works with Cisco, Juniper, Arista, HP, Fortinet, VyOS, and more +- **Multiple Output Formats**: Export as text, JSON, or YAML +- **Type-Safe**: Fully typed Python code with mypy support +- **Well-Tested**: Comprehensive test suite with high code coverage +- **Detailed Logging**: Verbose and debug modes for troubleshooting + +## Use Cases + +### Change Management + +Before applying configuration changes to network devices, use hier-config-cli to: + +1. Generate the exact commands needed for remediation +2. Create rollback procedures in case of issues +3. Preview the final configuration state +4. Document changes for approval workflows + +### Configuration Compliance + +Ensure network devices match their intended configurations: + +1. Compare running configs against golden configs +2. Identify configuration drift +3. Generate remediation to bring devices into compliance + +### Network Automation + +Integrate with your automation tools: + +- **CI/CD Pipelines**: Validate configuration changes in pull requests +- **Nornir**: Scale configuration analysis across hundreds of devices +- **Ansible**: Generate dynamic remediation playbooks + +## Quick Example + +```bash +# Generate remediation commands +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf + +# Output: +# === Remediation Configuration === +# no hostname router-01 +# hostname router-01-updated +# interface GigabitEthernet0/0 +# description WAN Interface - Updated +``` + +## Get Started + +Ready to dive in? Check out the [Installation](installation.md) guide and [Quick Start](quick-start.md) tutorial. + +## Links + +- **GitHub**: [netdevops/hier-config-cli](https://github.com/netdevops/hier-config-cli) +- **PyPI**: [hier-config-cli](https://pypi.org/project/hier-config-cli/) +- **Issues**: [Report a bug](https://github.com/netdevops/hier-config-cli/issues) +- **Hier Config Library**: [netdevops/hier_config](https://github.com/netdevops/hier_config) + +## Acknowledgments + +Built on top of the excellent [Hier Config](https://github.com/netdevops/hier_config) library by James Williams and contributors. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..9e9bc67 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,142 @@ +# Installation + +## Requirements + +- Python 3.10 or higher +- pip (Python package installer) + +## Installation Methods + +### Via pip (Recommended) + +The easiest way to install hier-config-cli is using pip: + +```bash +pip install hier-config-cli +``` + +### Via Poetry + +If you're using Poetry for dependency management: + +```bash +poetry add hier-config-cli +``` + +### From Source + +For development or to get the latest unreleased features: + +```bash +# Clone the repository +git clone https://github.com/netdevops/hier-config-cli.git +cd hier-config-cli + +# Install with Poetry +poetry install + +# Activate the virtual environment +poetry shell +``` + +## Verify Installation + +After installation, verify that hier-config-cli is installed correctly: + +```bash +# Check version +hier-config-cli version + +# List available platforms +hier-config-cli list-platforms +``` + +You should see output similar to: + +``` +hier-config-cli version 0.2.0 +``` + +## Updating + +### With pip + +```bash +pip install --upgrade hier-config-cli +``` + +### With Poetry + +```bash +poetry update hier-config-cli +``` + +## Uninstalling + +### With pip + +```bash +pip uninstall hier-config-cli +``` + +### With Poetry + +```bash +poetry remove hier-config-cli +``` + +## Troubleshooting + +### Command Not Found + +If you get a "command not found" error after installation: + +1. **Check if the package is installed:** + ```bash + pip show hier-config-cli + ``` + +2. **Ensure pip's bin directory is in your PATH:** + ```bash + # On Linux/macOS + export PATH="$HOME/.local/bin:$PATH" + + # On Windows (PowerShell) + $env:Path += ";$HOME\AppData\Local\Programs\Python\Python310\Scripts" + ``` + +3. **Try running with python -m:** + ```bash + python -m hier_config_cli version + ``` + +### Permission Errors + +If you encounter permission errors during installation: + +```bash +# Use --user flag to install in user directory +pip install --user hier-config-cli +``` + +### Virtual Environments + +It's recommended to use virtual environments to avoid conflicts: + +```bash +# Create virtual environment +python -m venv venv + +# Activate it +# On Linux/macOS: +source venv/bin/activate +# On Windows: +venv\Scripts\activate + +# Install hier-config-cli +pip install hier-config-cli +``` + +## Next Steps + +Now that you have hier-config-cli installed, check out the [Quick Start](quick-start.md) guide to learn how to use it. diff --git a/docs/integration/ansible.md b/docs/integration/ansible.md new file mode 100644 index 0000000..edb0eb5 --- /dev/null +++ b/docs/integration/ansible.md @@ -0,0 +1,480 @@ +# Ansible Integration + +This guide shows how to integrate hier-config-cli with [Ansible](https://www.ansible.com/) for configuration management and network automation. + +## Overview + +Ansible is a powerful automation platform that excels at configuration management. By integrating hier-config-cli, you can: + +- Generate precise remediation commands for network devices +- Validate configuration changes before deployment +- Create automated rollback procedures +- Implement configuration compliance checks + +## Prerequisites + +```bash +# Install Ansible and hier-config-cli +pip install ansible hier-config-cli + +# For network device management +ansible-galaxy collection install ansible.netcommon +ansible-galaxy collection install cisco.ios +ansible-galaxy collection install cisco.nxos +``` + +## Basic Integration + +### Simple Playbook + +```yaml +--- +- name: Generate Configuration Remediation + hosts: network_devices + gather_facts: false + tasks: + - name: Run hier-config-cli remediation + command: > + hier-config-cli remediation + --platform {{ platform }} + --running-config /tmp/{{ inventory_hostname }}_running.conf + --generated-config /tmp/{{ inventory_hostname }}_intended.conf + --output /tmp/{{ inventory_hostname }}_remediation.txt + register: remediation_result + changed_when: false + + - name: Display remediation output + debug: + msg: "{{ remediation_result.stdout }}" +``` + +### Inventory Setup + +**inventory/hosts.ini:** +```ini +[routers] +router1 ansible_host=192.168.1.1 platform=ios +router2 ansible_host=192.168.1.2 platform=ios + +[switches] +switch1 ansible_host=192.168.1.10 platform=nxos +switch2 ansible_host=192.168.1.11 platform=nxos + +[firewalls] +firewall1 ansible_host=192.168.1.254 platform=fortios + +[network_devices:children] +routers +switches +firewalls +``` + +## Complete Workflow Playbook + +```yaml +--- +- name: Network Configuration Management with Hier Config CLI + hosts: network_devices + gather_facts: false + vars: + config_dir: "./configs" + running_config_dir: "{{ config_dir }}/running" + intended_config_dir: "{{ config_dir }}/intended" + remediation_dir: "{{ config_dir }}/remediation" + rollback_dir: "{{ config_dir }}/rollback" + + tasks: + - name: Create directory structure + delegate_to: localhost + run_once: true + file: + path: "{{ item }}" + state: directory + loop: + - "{{ running_config_dir }}" + - "{{ intended_config_dir }}" + - "{{ remediation_dir }}" + - "{{ rollback_dir }}" + + - name: Fetch running configuration + cisco.ios.ios_command: + commands: + - show running-config + register: running_config + when: platform == "ios" + + - name: Save running configuration + delegate_to: localhost + copy: + content: "{{ running_config.stdout[0] }}" + dest: "{{ running_config_dir }}/{{ inventory_hostname }}.conf" + when: running_config is defined + + - name: Generate remediation configuration + delegate_to: localhost + command: > + hier-config-cli remediation + --platform {{ platform }} + --running-config {{ running_config_dir }}/{{ inventory_hostname }}.conf + --generated-config {{ intended_config_dir }}/{{ inventory_hostname }}.conf + --output {{ remediation_dir }}/{{ inventory_hostname }}.txt + register: remediation_result + changed_when: false + + - name: Generate rollback configuration + delegate_to: localhost + command: > + hier-config-cli rollback + --platform {{ platform }} + --running-config {{ running_config_dir }}/{{ inventory_hostname }}.conf + --generated-config {{ intended_config_dir }}/{{ inventory_hostname }}.conf + --output {{ rollback_dir }}/{{ inventory_hostname }}.txt + register: rollback_result + changed_when: false + + - name: Read remediation commands + delegate_to: localhost + slurp: + src: "{{ remediation_dir }}/{{ inventory_hostname }}.txt" + register: remediation_content + + - name: Display remediation summary + debug: + msg: "Remediation for {{ inventory_hostname }} generated successfully" + + - name: Store remediation in variable + set_fact: + remediation_commands: "{{ remediation_content.content | b64decode }}" +``` + +## Custom Ansible Module + +Create a custom module for cleaner integration: + +**library/hier_config_cli.py:** +```python +#!/usr/bin/python +"""Ansible module for hier-config-cli.""" + +from ansible.module_utils.basic import AnsibleModule +import subprocess + + +def run_hier_config_cli(module): + """Run hier-config-cli command.""" + + operation = module.params['operation'] + platform = module.params['platform'] + running_config = module.params['running_config'] + generated_config = module.params['generated_config'] + output_file = module.params['output_file'] + output_format = module.params['output_format'] + + cmd = [ + 'hier-config-cli', + operation, + '--platform', platform, + '--running-config', running_config, + '--generated-config', generated_config, + '--format', output_format, + ] + + if output_file: + cmd.extend(['--output', output_file]) + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + + return { + 'changed': True, + 'stdout': result.stdout, + 'stderr': result.stderr, + 'rc': result.returncode, + } + + except subprocess.CalledProcessError as e: + module.fail_json( + msg=f"hier-config-cli failed: {e.stderr}", + rc=e.returncode, + ) + except FileNotFoundError: + module.fail_json(msg="hier-config-cli not found. Is it installed?") + + +def main(): + """Main module function.""" + module = AnsibleModule( + argument_spec={ + 'operation': { + 'required': True, + 'type': 'str', + 'choices': ['remediation', 'rollback', 'future'], + }, + 'platform': {'required': True, 'type': 'str'}, + 'running_config': {'required': True, 'type': 'path'}, + 'generated_config': {'required': True, 'type': 'path'}, + 'output_file': {'required': False, 'type': 'path'}, + 'output_format': { + 'required': False, + 'type': 'str', + 'default': 'text', + 'choices': ['text', 'json', 'yaml'], + }, + }, + supports_check_mode=False, + ) + + result = run_hier_config_cli(module) + module.exit_json(**result) + + +if __name__ == '__main__': + main() +``` + +**Using the custom module:** +```yaml +--- +- name: Use custom hier-config-cli module + hosts: network_devices + tasks: + - name: Generate remediation with custom module + hier_config_cli: + operation: remediation + platform: "{{ platform }}" + running_config: "/tmp/{{ inventory_hostname }}_running.conf" + generated_config: "/tmp/{{ inventory_hostname }}_intended.conf" + output_file: "/tmp/{{ inventory_hostname }}_remediation.txt" + output_format: text + register: result + + - name: Display result + debug: + var: result.stdout +``` + +## JSON Output Processing + +```yaml +--- +- name: Process JSON output + hosts: network_devices + tasks: + - name: Generate remediation in JSON format + command: > + hier-config-cli remediation + --platform {{ platform }} + --running-config configs/running/{{ inventory_hostname }}.conf + --generated-config configs/intended/{{ inventory_hostname }}.conf + --format json + register: remediation_json + changed_when: false + + - name: Parse JSON output + set_fact: + remediation_data: "{{ remediation_json.stdout | from_json }}" + + - name: Display configuration commands + debug: + msg: "{{ remediation_data.config }}" + + - name: Count commands + debug: + msg: "Total commands: {{ remediation_data.config | length }}" + + - name: Filter specific commands + debug: + msg: "Interface commands: {{ remediation_data.config | select('match', '^interface') | list }}" +``` + +## Change Validation Workflow + +```yaml +--- +- name: Validate Configuration Changes + hosts: network_devices + gather_facts: false + vars: + max_changes: 50 + requires_approval: true + + tasks: + - name: Generate remediation + command: > + hier-config-cli remediation + --platform {{ platform }} + --running-config configs/running/{{ inventory_hostname }}.conf + --generated-config configs/intended/{{ inventory_hostname }}.conf + --format json + register: remediation_output + delegate_to: localhost + changed_when: false + + - name: Parse remediation + set_fact: + remediation: "{{ remediation_output.stdout | from_json }}" + + - name: Check number of changes + fail: + msg: "Too many changes ({{ remediation.config | length }}). Maximum allowed: {{ max_changes }}" + when: remediation.config | length > max_changes + + - name: Display changes for review + debug: + msg: "{{ remediation.config }}" + when: requires_approval + + - name: Pause for approval + pause: + prompt: "Review changes above. Press ENTER to continue or Ctrl+C to abort" + when: requires_approval + + - name: Apply configuration + # Your configuration application logic here + debug: + msg: "Would apply {{ remediation.config | length }} commands to {{ inventory_hostname }}" +``` + +## Rollback Workflow + +```yaml +--- +- name: Configuration Rollback Workflow + hosts: network_devices + gather_facts: false + vars: + rollback_required: false + + tasks: + - name: Generate rollback configuration + command: > + hier-config-cli rollback + --platform {{ platform }} + --running-config configs/running/{{ inventory_hostname }}.conf + --generated-config configs/intended/{{ inventory_hostname }}.conf + --output configs/rollback/{{ inventory_hostname }}.txt + delegate_to: localhost + changed_when: false + + - name: Read rollback commands + slurp: + src: "configs/rollback/{{ inventory_hostname }}.txt" + register: rollback_file + delegate_to: localhost + + - name: Parse rollback commands + set_fact: + rollback_commands: "{{ rollback_file.content | b64decode }}" + + - name: Display rollback commands + debug: + msg: "{{ rollback_commands }}" + when: rollback_required + + - name: Execute rollback + # Your rollback execution logic here + debug: + msg: "Executing rollback for {{ inventory_hostname }}" + when: rollback_required +``` + +## CI/CD Integration with Ansible + +**Jenkinsfile:** +```groovy +pipeline { + agent any + + stages { + stage('Validate Configurations') { + steps { + sh ''' + ansible-playbook \ + -i inventory/hosts.ini \ + playbooks/validate_configs.yml + ''' + } + } + + stage('Generate Remediation') { + steps { + sh ''' + ansible-playbook \ + -i inventory/hosts.ini \ + playbooks/generate_remediation.yml + ''' + } + } + + stage('Review and Approve') { + steps { + input message: 'Review remediation and approve deployment?' + } + } + + stage('Deploy Changes') { + steps { + sh ''' + ansible-playbook \ + -i inventory/hosts.ini \ + playbooks/deploy_changes.yml + ''' + } + } + } + + post { + always { + archiveArtifacts artifacts: 'configs/**/*.txt', allowEmptyArchive: true + } + } +} +``` + +## Best Practices + +1. **Use delegation** (`delegate_to: localhost`) for hier-config-cli tasks +2. **Store configs in version control** for audit trails +3. **Implement approval gates** for production changes +4. **Generate rollback configs** before applying changes +5. **Use JSON format** for programmatic processing +6. **Set command limits** to prevent accidental large-scale changes +7. **Test in staging** before production deployment +8. **Keep playbooks idempotent** where possible + +## Troubleshooting + +### Command Not Found + +```yaml +- name: Check hier-config-cli installation + command: which hier-config-cli + register: hier_config_path + failed_when: hier_config_path.rc != 0 + changed_when: false +``` + +### File Permission Issues + +```yaml +- name: Ensure config directories are writable + file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - configs/running + - configs/intended + - configs/remediation +``` + +## Next Steps + +- Explore [CI/CD Integration](cicd.md) +- Learn about [Nornir Integration](nornir.md) +- Review [Commands Reference](../commands.md) diff --git a/docs/integration/cicd.md b/docs/integration/cicd.md new file mode 100644 index 0000000..38f6faa --- /dev/null +++ b/docs/integration/cicd.md @@ -0,0 +1,599 @@ +# CI/CD Integration + +Integrate hier-config-cli into your continuous integration and deployment pipelines to automate configuration validation, testing, and deployment. + +## Overview + +Using hier-config-cli in CI/CD pipelines enables: + +- **Automated validation** of configuration changes +- **Pull request checks** to catch issues early +- **Automated remediation generation** for approved changes +- **Configuration drift detection** in production +- **Compliance checking** against golden configs + +## GitHub Actions + +### Basic Workflow + +**.github/workflows/config-validation.yml:** +```yaml +name: Network Configuration Validation + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + paths: + - 'configs/**' + +jobs: + validate-configs: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install hier-config-cli + run: | + pip install hier-config-cli + + - name: Validate configurations + run: | + for device in configs/intended/*.conf; do + device_name=$(basename "$device" .conf) + echo "Validating $device_name..." + + hier-config-cli remediation \ + --platform ios \ + --running-config "configs/running/${device_name}.conf" \ + --generated-config "$device" \ + --output "remediation/${device_name}.txt" + done + + - name: Upload remediation artifacts + uses: actions/upload-artifact@v4 + with: + name: remediation-configs + path: remediation/ +``` + +### Advanced Workflow with Matrix + +**.github/workflows/multi-platform-validation.yml:** +```yaml +name: Multi-Platform Configuration Validation + +on: + pull_request: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + strategy: + matrix: + platform: [ios, nxos, iosxr, eos] + fail-fast: false + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install dependencies + run: | + pip install hier-config-cli + + - name: Generate remediation for ${{ matrix.platform }} + run: | + mkdir -p output/remediation output/rollback + + for config in configs/intended/${{ matrix.platform }}/*.conf; do + if [ -f "$config" ]; then + device=$(basename "$config" .conf) + + echo "Processing $device (${{ matrix.platform }})" + + # Generate remediation + hier-config-cli remediation \ + --platform ${{ matrix.platform }} \ + --running-config "configs/running/${{ matrix.platform }}/${device}.conf" \ + --generated-config "$config" \ + --output "output/remediation/${device}.txt" + + # Generate rollback + hier-config-cli rollback \ + --platform ${{ matrix.platform }} \ + --running-config "configs/running/${{ matrix.platform }}/${device}.conf" \ + --generated-config "$config" \ + --output "output/rollback/${device}.txt" + fi + done + + - name: Check for excessive changes + run: | + for file in output/remediation/*.txt; do + if [ -f "$file" ]; then + lines=$(wc -l < "$file") + if [ "$lines" -gt 100 ]; then + echo "::error::Too many changes in $(basename "$file"): $lines lines" + exit 1 + fi + fi + done + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: configs-${{ matrix.platform }} + path: output/ + + - name: Comment on PR + uses: actions/github-script@v7 + if: github.event_name == 'pull_request' + with: + script: | + const fs = require('fs'); + const files = fs.readdirSync('output/remediation'); + + let comment = '## Configuration Validation Results - ${{ matrix.platform }}\n\n'; + comment += `Found ${files.length} devices to update\n\n`; + + for (const file of files) { + const content = fs.readFileSync(`output/remediation/${file}`, 'utf8'); + const lines = content.split('\n').length; + comment += `- **${file}**: ${lines} configuration lines\n`; + } + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); +``` + +### Scheduled Compliance Check + +**.github/workflows/compliance-check.yml:** +```yaml +name: Configuration Compliance Check + +on: + schedule: + # Run every day at 2 AM UTC + - cron: '0 2 * * *' + workflow_dispatch: + +jobs: + compliance-check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install hier-config-cli + + - name: Fetch running configs + run: | + # This would be replaced with actual config fetching logic + # For example, using Ansible, NAPALM, or network APIs + echo "Fetching configs from devices..." + + - name: Check for drift + id: drift-check + run: | + mkdir -p drift-reports + + drift_detected=false + + for device in configs/golden/*.conf; do + device_name=$(basename "$device" .conf) + + hier-config-cli remediation \ + --platform ios \ + --running-config "configs/running/${device_name}.conf" \ + --generated-config "$device" \ + --output "drift-reports/${device_name}.txt" \ + --format json + + if [ -s "drift-reports/${device_name}.txt" ]; then + drift_detected=true + fi + done + + echo "drift_detected=$drift_detected" >> $GITHUB_OUTPUT + + - name: Create issue if drift detected + if: steps.drift-check.outputs.drift_detected == 'true' + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: 'Configuration Drift Detected', + body: 'Automated compliance check detected configuration drift. Please review the attached artifacts.', + labels: ['configuration', 'drift', 'automated'] + }); + + - name: Upload drift reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: drift-reports + path: drift-reports/ +``` + +## GitLab CI + +**.gitlab-ci.yml:** +```yaml +stages: + - validate + - review + - deploy + +variables: + PYTHON_VERSION: "3.11" + +.install_deps: &install_deps + before_script: + - pip install hier-config-cli + +validate:ios: + stage: validate + image: python:${PYTHON_VERSION} + <<: *install_deps + script: + - mkdir -p output/remediation output/rollback + - | + for config in configs/intended/ios/*.conf; do + device=$(basename "$config" .conf) + echo "Validating $device..." + + hier-config-cli remediation \ + --platform ios \ + --running-config "configs/running/ios/${device}.conf" \ + --generated-config "$config" \ + --output "output/remediation/${device}.txt" + + hier-config-cli rollback \ + --platform ios \ + --running-config "configs/running/ios/${device}.conf" \ + --generated-config "$config" \ + --output "output/rollback/${device}.txt" + done + artifacts: + paths: + - output/ + expire_in: 1 week + only: + changes: + - configs/** + +validate:nxos: + stage: validate + image: python:${PYTHON_VERSION} + <<: *install_deps + script: + - mkdir -p output/remediation output/rollback + - | + for config in configs/intended/nxos/*.conf; do + device=$(basename "$config" .conf) + echo "Validating $device..." + + hier-config-cli remediation \ + --platform nxos \ + --running-config "configs/running/nxos/${device}.conf" \ + --generated-config "$config" \ + --output "output/remediation/${device}.txt" + done + artifacts: + paths: + - output/ + expire_in: 1 week + only: + changes: + - configs/** + +review_changes: + stage: review + image: python:${PYTHON_VERSION} + script: + - | + echo "## Configuration Changes Summary" > review.md + echo "" >> review.md + + for file in output/remediation/*.txt; do + if [ -f "$file" ]; then + device=$(basename "$file" .txt) + lines=$(wc -l < "$file") + echo "- **$device**: $lines configuration lines" >> review.md + fi + done + + cat review.md + artifacts: + reports: + dotenv: review.md + dependencies: + - validate:ios + - validate:nxos + only: + - merge_requests + +deploy_configs: + stage: deploy + image: python:${PYTHON_VERSION} + script: + - echo "Deploying configurations..." + # Add deployment logic here + when: manual + only: + - main + dependencies: + - validate:ios + - validate:nxos +``` + +## Jenkins + +**Jenkinsfile:** +```groovy +pipeline { + agent { + docker { + image 'python:3.11' + } + } + + parameters { + choice( + name: 'PLATFORM', + choices: ['ios', 'nxos', 'iosxr', 'eos'], + description: 'Network platform to validate' + ) + booleanParam( + name: 'GENERATE_ROLLBACK', + defaultValue: true, + description: 'Generate rollback configurations' + ) + } + + stages { + stage('Setup') { + steps { + sh 'pip install hier-config-cli' + } + } + + stage('Validate Configurations') { + steps { + script { + sh ''' + mkdir -p output/remediation output/rollback + + for config in configs/intended/${PLATFORM}/*.conf; do + device=$(basename "$config" .conf) + echo "Validating $device..." + + hier-config-cli remediation \ + --platform ${PLATFORM} \ + --running-config "configs/running/${PLATFORM}/${device}.conf" \ + --generated-config "$config" \ + --output "output/remediation/${device}.txt" + + if [ "${GENERATE_ROLLBACK}" = "true" ]; then + hier-config-cli rollback \ + --platform ${PLATFORM} \ + --running-config "configs/running/${PLATFORM}/${device}.conf" \ + --generated-config "$config" \ + --output "output/rollback/${device}.txt" + fi + done + ''' + } + } + } + + stage('Quality Gates') { + steps { + script { + sh ''' + # Check for excessive changes + for file in output/remediation/*.txt; do + if [ -f "$file" ]; then + lines=$(wc -l < "$file") + if [ "$lines" -gt 100 ]; then + echo "ERROR: Too many changes in $(basename "$file"): $lines lines" + exit 1 + fi + fi + done + ''' + } + } + } + + stage('Generate Report') { + steps { + script { + sh ''' + echo "# Configuration Validation Report" > report.md + echo "" >> report.md + echo "Platform: ${PLATFORM}" >> report.md + echo "Date: $(date)" >> report.md + echo "" >> report.md + echo "## Devices Processed" >> report.md + echo "" >> report.md + + for file in output/remediation/*.txt; do + if [ -f "$file" ]; then + device=$(basename "$file" .txt) + lines=$(wc -l < "$file") + echo "- **$device**: $lines configuration lines" >> report.md + fi + done + ''' + } + } + } + } + + post { + always { + archiveArtifacts artifacts: 'output/**/*.txt', allowEmptyArchive: true + archiveArtifacts artifacts: 'report.md', allowEmptyArchive: true + } + success { + echo 'Configuration validation successful!' + } + failure { + echo 'Configuration validation failed!' + } + } +} +``` + +## Azure DevOps + +**azure-pipelines.yml:** +```yaml +trigger: + branches: + include: + - main + - develop + paths: + include: + - configs/** + +pool: + vmImage: 'ubuntu-latest' + +variables: + pythonVersion: '3.11' + +stages: + - stage: Validate + displayName: 'Validate Configurations' + jobs: + - job: ValidateIOS + displayName: 'Validate Cisco IOS Configs' + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '$(pythonVersion)' + + - script: | + pip install hier-config-cli + displayName: 'Install hier-config-cli' + + - script: | + mkdir -p output/remediation output/rollback + + for config in configs/intended/ios/*.conf; do + device=$(basename "$config" .conf) + echo "Processing $device..." + + hier-config-cli remediation \ + --platform ios \ + --running-config "configs/running/ios/${device}.conf" \ + --generated-config "$config" \ + --output "output/remediation/${device}.txt" + done + displayName: 'Generate Remediation' + + - task: PublishBuildArtifacts@1 + inputs: + pathToPublish: 'output' + artifactName: 'configurations' + + - job: ValidateNXOS + displayName: 'Validate Cisco NX-OS Configs' + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '$(pythonVersion)' + + - script: | + pip install hier-config-cli + displayName: 'Install hier-config-cli' + + - script: | + mkdir -p output/remediation + + for config in configs/intended/nxos/*.conf; do + device=$(basename "$config" .conf) + + hier-config-cli remediation \ + --platform nxos \ + --running-config "configs/running/nxos/${device}.conf" \ + --generated-config "$config" \ + --output "output/remediation/${device}.txt" + done + displayName: 'Generate Remediation' + + - stage: Deploy + displayName: 'Deploy Configurations' + dependsOn: Validate + condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) + jobs: + - deployment: DeployConfigs + displayName: 'Deploy to Production' + environment: 'production' + strategy: + runOnce: + deploy: + steps: + - script: | + echo "Deploying configurations..." + # Add deployment logic here + displayName: 'Deploy' +``` + +## Best Practices + +1. **Run validation on every PR** to catch issues early +2. **Use matrix builds** for multi-platform environments +3. **Set change thresholds** to prevent accidental large deployments +4. **Generate rollback configs** automatically +5. **Archive artifacts** for audit trails +6. **Use approval gates** for production deployments +7. **Implement drift detection** with scheduled jobs +8. **Add status badges** to README for visibility + +## Example Status Badges + +Add to your README.md: + +```markdown +![Config Validation](https://github.com/yourorg/yourrepo/workflows/config-validation/badge.svg) +``` + +## Next Steps + +- Learn about [Nornir Integration](nornir.md) +- Explore [Ansible Integration](ansible.md) +- Review [Commands Reference](../commands.md) diff --git a/docs/integration/index.md b/docs/integration/index.md new file mode 100644 index 0000000..d8ad9a9 --- /dev/null +++ b/docs/integration/index.md @@ -0,0 +1,250 @@ +# Integration Overview + +hier-config-cli is designed to integrate seamlessly with various automation tools and workflows. This section provides examples and best practices for integrating hier-config-cli into your network automation stack. + +## Integration Options + +hier-config-cli can be integrated into your workflows in several ways: + +### Command-Line Integration + +- Direct shell execution +- Shell scripts and batch processing +- Cron jobs for scheduled tasks + +### Automation Frameworks + +- **[Nornir](nornir.md)**: Python-based automation framework +- **[Ansible](ansible.md)**: Configuration management and automation +- **[CI/CD Pipelines](cicd.md)**: GitHub Actions, GitLab CI, Jenkins + +### Programmatic Integration + +- Subprocess calls from Python +- REST API wrappers +- Custom automation tools + +## Common Integration Patterns + +### Pre-Change Validation + +Use hier-config-cli to validate configuration changes before applying them: + +```mermaid +graph LR + A[Generate Config] --> B[Run hier-config-cli] + B --> C{Review Output} + C -->|Approved| D[Apply to Device] + C -->|Rejected| E[Revise Config] + E --> A +``` + +### Post-Change Verification + +Verify that changes were applied correctly: + +```mermaid +graph LR + A[Apply Config] --> B[Fetch Running Config] + B --> C[Run hier-config-cli] + C --> D{Differences?} + D -->|None| E[Success] + D -->|Found| F[Alert/Rollback] +``` + +### Configuration Compliance + +Continuously check for configuration drift: + +```mermaid +graph LR + A[Golden Config] --> B[Scheduled Job] + B --> C[Fetch Running Config] + C --> D[Run hier-config-cli] + D --> E{Drift Detected?} + E -->|Yes| F[Alert/Ticket] + E -->|No| G[Continue Monitoring] + G --> B +``` + +## Integration Best Practices + +### 1. Error Handling + +Always check exit codes and handle errors appropriately: + +```bash +#!/bin/bash + +if hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --output remediation.txt; then + echo "Remediation generated successfully" +else + echo "Error generating remediation" + exit 1 +fi +``` + +### 2. Logging + +Enable verbose logging for troubleshooting: + +```bash +# Add to your automation scripts +hier-config-cli -vv remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + 2>> hier-config-cli.log +``` + +### 3. File Management + +Organize configuration files systematically: + +``` +configs/ +├── running/ +│ ├── router1_running.conf +│ ├── router2_running.conf +│ └── switch1_running.conf +├── intended/ +│ ├── router1_intended.conf +│ ├── router2_intended.conf +│ └── switch1_intended.conf +└── output/ + ├── remediation/ + ├── rollback/ + └── future/ +``` + +### 4. Version Control + +Store all configurations in Git: + +```bash +# Commit intended configs +git add configs/intended/ +git commit -m "Update intended configurations" + +# Generate and commit remediation +hier-config-cli remediation \ + --platform ios \ + --running-config configs/running/router1.conf \ + --generated-config configs/intended/router1.conf \ + --output configs/output/remediation/router1.txt + +git add configs/output/remediation/router1.txt +git commit -m "Generate remediation for router1" +``` + +### 5. Parallel Processing + +Process multiple devices concurrently: + +```bash +#!/bin/bash + +# Process devices in parallel +for device in router1 router2 switch1; do + ( + hier-config-cli remediation \ + --platform ios \ + --running-config configs/running/${device}.conf \ + --generated-config configs/intended/${device}.conf \ + --output configs/output/remediation/${device}.txt + ) & +done + +# Wait for all background jobs +wait + +echo "All devices processed" +``` + +## Integration Examples + +### Quick Reference + +| Tool/Platform | Page | Use Case | +|---------------|------|----------| +| Nornir | [Nornir Integration](nornir.md) | Python-based automation at scale | +| Ansible | [Ansible Integration](ansible.md) | Configuration management | +| GitHub Actions | [CI/CD Integration](cicd.md) | Automated testing and validation | +| GitLab CI | [CI/CD Integration](cicd.md) | Pipeline integration | +| Jenkins | [CI/CD Integration](cicd.md) | Legacy CI/CD systems | + +### Simple Shell Script + +```bash +#!/bin/bash +# simple-remediation.sh + +DEVICE=$1 +PLATFORM=$2 + +hier-config-cli remediation \ + --platform ${PLATFORM} \ + --running-config configs/running/${DEVICE}.conf \ + --generated-config configs/intended/${DEVICE}.conf \ + --output configs/output/${DEVICE}_remediation.txt + +echo "Remediation generated for ${DEVICE}" +``` + +Usage: +```bash +./simple-remediation.sh router1 ios +``` + +### Python Wrapper + +```python +#!/usr/bin/env python3 +"""Simple Python wrapper for hier-config-cli.""" + +import subprocess +import sys +from pathlib import Path + +def generate_remediation(device: str, platform: str) -> bool: + """Generate remediation for a device.""" + cmd = [ + "hier-config-cli", + "remediation", + "--platform", platform, + "--running-config", f"configs/running/{device}.conf", + "--generated-config", f"configs/intended/{device}.conf", + "--output", f"configs/output/{device}_remediation.txt", + ] + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + print(f"✓ Remediation generated for {device}") + return True + except subprocess.CalledProcessError as e: + print(f"✗ Error generating remediation for {device}: {e.stderr}") + return False + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: wrapper.py ") + sys.exit(1) + + device = sys.argv[1] + platform = sys.argv[2] + + success = generate_remediation(device, platform) + sys.exit(0 if success else 1) +``` + +## Next Steps + +Explore specific integration guides: + +- **[Nornir Integration](nornir.md)**: Scale automation across many devices +- **[Ansible Integration](ansible.md)**: Integrate with Ansible playbooks +- **[CI/CD Integration](cicd.md)**: Automate validation in pipelines diff --git a/docs/integration/nornir.md b/docs/integration/nornir.md new file mode 100644 index 0000000..4ad0f4d --- /dev/null +++ b/docs/integration/nornir.md @@ -0,0 +1,436 @@ +# Nornir Integration + +[Nornir](https://nornir.readthedocs.io/) is a Python-based automation framework designed for network engineers. This guide shows how to integrate hier-config-cli with Nornir for scalable network configuration management. + +## Overview + +Nornir excels at running tasks across multiple devices in parallel, making it ideal for large-scale network automation. Combined with hier-config-cli, you can: + +- Generate remediation for hundreds of devices concurrently +- Validate configuration changes before deployment +- Create comprehensive rollback procedures +- Automate compliance checking + +## Basic Integration + +### Installation + +```bash +pip install nornir nornir-napalm nornir-utils hier-config-cli +``` + +### Simple Example + +```python +from nornir import InitNornir +from nornir.core.task import Task, Result +import subprocess + +def generate_remediation(task: Task) -> Result: + """Generate remediation configuration for a device.""" + + # Run hier-config-cli + cmd = [ + "hier-config-cli", + "remediation", + "--platform", task.host.platform, + "--running-config", f"configs/running/{task.host.name}.conf", + "--generated-config", f"configs/intended/{task.host.name}.conf", + "--output", f"configs/remediation/{task.host.name}.txt", + ] + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + ) + + if result.returncode == 0: + return Result( + host=task.host, + result=f"Remediation generated successfully", + changed=True, + ) + else: + return Result( + host=task.host, + failed=True, + result=f"Error: {result.stderr}", + ) + +# Initialize Nornir +nr = InitNornir(config_file="config.yaml") + +# Run task on all hosts +results = nr.run(task=generate_remediation) + +# Print results +for host, result in results.items(): + print(f"{host}: {result.result}") +``` + +## Complete Workflow Example + +### Directory Structure + +``` +nornir-automation/ +├── config.yaml +├── inventory/ +│ ├── hosts.yaml +│ ├── groups.yaml +│ └── defaults.yaml +├── configs/ +│ ├── running/ +│ ├── intended/ +│ ├── remediation/ +│ └── rollback/ +└── tasks/ + └── hier_config_tasks.py +``` + +### Nornir Configuration (config.yaml) + +```yaml +--- +inventory: + plugin: SimpleInventory + options: + host_file: "inventory/hosts.yaml" + group_file: "inventory/groups.yaml" + defaults_file: "inventory/defaults.yaml" + +runner: + plugin: threaded + options: + num_workers: 20 +``` + +### Inventory (inventory/hosts.yaml) + +```yaml +--- +router1: + hostname: 192.168.1.1 + platform: ios + groups: + - cisco_routers + +router2: + hostname: 192.168.1.2 + platform: ios + groups: + - cisco_routers + +switch1: + hostname: 192.168.1.10 + platform: nxos + groups: + - cisco_switches + +firewall1: + hostname: 192.168.1.254 + platform: fortios + groups: + - firewalls +``` + +### Advanced Task Module (tasks/hier_config_tasks.py) + +```python +"""Hier Config CLI tasks for Nornir.""" + +from pathlib import Path +import subprocess +from typing import Optional +from nornir.core.task import Task, Result + + +def run_hier_config_cli( + task: Task, + operation: str, + running_config_path: Optional[str] = None, + intended_config_path: Optional[str] = None, + output_path: Optional[str] = None, + output_format: str = "text", +) -> Result: + """ + Run hier-config-cli command. + + Args: + task: Nornir task object + operation: Operation to perform (remediation, rollback, future) + running_config_path: Path to running config (defaults to configs/running/{hostname}.conf) + intended_config_path: Path to intended config (defaults to configs/intended/{hostname}.conf) + output_path: Path for output file (optional) + output_format: Output format (text, json, yaml) + + Returns: + Nornir Result object + """ + # Set default paths + if not running_config_path: + running_config_path = f"configs/running/{task.host.name}.conf" + if not intended_config_path: + intended_config_path = f"configs/intended/{task.host.name}.conf" + + # Build command + cmd = [ + "hier-config-cli", + operation, + "--platform", task.host.platform, + "--running-config", running_config_path, + "--generated-config", intended_config_path, + "--format", output_format, + ] + + if output_path: + cmd.extend(["--output", output_path]) + + # Execute command + result = subprocess.run( + cmd, + capture_output=True, + text=True, + ) + + if result.returncode == 0: + return Result( + host=task.host, + result=result.stdout if not output_path else f"Output written to {output_path}", + changed=True, + ) + else: + return Result( + host=task.host, + failed=True, + result=result.stderr, + ) + + +def generate_remediation(task: Task) -> Result: + """Generate remediation configuration.""" + return run_hier_config_cli( + task, + operation="remediation", + output_path=f"configs/remediation/{task.host.name}.txt", + ) + + +def generate_rollback(task: Task) -> Result: + """Generate rollback configuration.""" + return run_hier_config_cli( + task, + operation="rollback", + output_path=f"configs/rollback/{task.host.name}.txt", + ) + + +def generate_future(task: Task) -> Result: + """Generate future configuration state.""" + return run_hier_config_cli( + task, + operation="future", + output_path=f"configs/future/{task.host.name}.txt", + ) + + +def complete_workflow(task: Task) -> Result: + """ + Run complete hier-config workflow. + + Generates: + 1. Remediation configuration + 2. Rollback configuration + 3. Future state prediction + """ + results = [] + + # Generate remediation + remediation = task.run(task=generate_remediation) + results.append(f"Remediation: {remediation.result}") + + # Generate rollback + rollback = task.run(task=generate_rollback) + results.append(f"Rollback: {rollback.result}") + + # Generate future state + future = task.run(task=generate_future) + results.append(f"Future: {future.result}") + + return Result( + host=task.host, + result="\n".join(results), + changed=True, + ) +``` + +### Main Script (main.py) + +```python +#!/usr/bin/env python3 +"""Main Nornir automation script.""" + +from nornir import InitNornir +from nornir_utils.plugins.functions import print_result +from tasks.hier_config_tasks import ( + generate_remediation, + generate_rollback, + generate_future, + complete_workflow, +) + + +def main(): + """Run main automation workflow.""" + # Initialize Nornir + nr = InitNornir(config_file="config.yaml") + + # Filter to specific platform if needed + # ios_devices = nr.filter(platform="ios") + + print("=" * 80) + print("Running Complete Hier Config Workflow") + print("=" * 80) + + # Run complete workflow on all devices + results = nr.run(task=complete_workflow) + + # Print results + print_result(results) + + # Check for failures + failed_hosts = [host for host, result in results.items() if result.failed] + + if failed_hosts: + print(f"\n❌ Failed hosts: {', '.join(failed_hosts)}") + return 1 + else: + print(f"\n✅ All {len(results)} devices processed successfully") + return 0 + + +if __name__ == "__main__": + exit(main()) +``` + +## Integration with NAPALM + +Combine hier-config-cli with NAPALM to fetch running configs: + +```python +from nornir import InitNornir +from nornir_napalm.plugins.tasks import napalm_get +from nornir.core.task import Task, Result +import subprocess +from pathlib import Path + + +def fetch_and_analyze(task: Task) -> Result: + """Fetch running config and generate remediation.""" + + # Fetch running config with NAPALM + result = task.run(task=napalm_get, getters=["config"]) + running_config = result.result["config"]["running"] + + # Save running config + running_path = f"configs/running/{task.host.name}.conf" + Path(running_path).write_text(running_config) + + # Generate remediation + cmd = [ + "hier-config-cli", + "remediation", + "--platform", task.host.platform, + "--running-config", running_path, + "--generated-config", f"configs/intended/{task.host.name}.conf", + "--output", f"configs/remediation/{task.host.name}.txt", + ] + + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + return Result( + host=task.host, + result="Config fetched and remediation generated", + changed=True, + ) + else: + return Result( + host=task.host, + failed=True, + result=result.stderr, + ) + + +# Initialize and run +nr = InitNornir(config_file="config.yaml") +results = nr.run(task=fetch_and_analyze) +``` + +## Error Handling + +```python +from nornir.core.exceptions import NornirExecutionError + + +def safe_remediation(task: Task) -> Result: + """Generate remediation with error handling.""" + try: + cmd = [ + "hier-config-cli", + "remediation", + "--platform", task.host.platform, + "--running-config", f"configs/running/{task.host.name}.conf", + "--generated-config", f"configs/intended/{task.host.name}.conf", + "--output", f"configs/remediation/{task.host.name}.txt", + ] + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + + return Result( + host=task.host, + result="Success", + changed=True, + ) + + except subprocess.CalledProcessError as e: + return Result( + host=task.host, + failed=True, + result=f"Command failed: {e.stderr}", + ) + except FileNotFoundError as e: + return Result( + host=task.host, + failed=True, + result=f"File not found: {e}", + ) + except Exception as e: + return Result( + host=task.host, + failed=True, + result=f"Unexpected error: {e}", + ) +``` + +## Best Practices + +1. **Use Nornir's filtering** to target specific device groups +2. **Leverage parallel execution** for large device counts +3. **Implement proper error handling** for production use +4. **Store configs in version control** +5. **Use NAPALM** to fetch live configs when needed +6. **Create separate tasks** for different operations +7. **Log all operations** for audit trails + +## Next Steps + +- Explore [Ansible Integration](ansible.md) +- Learn about [CI/CD Integration](cicd.md) +- Review [Commands Reference](../commands.md) diff --git a/docs/output-formats.md b/docs/output-formats.md new file mode 100644 index 0000000..873cc16 --- /dev/null +++ b/docs/output-formats.md @@ -0,0 +1,312 @@ +# Output Formats + +hier-config-cli supports multiple output formats to integrate with different workflows and tools. + +## Available Formats + +| Format | Description | Use Case | +|--------|-------------|----------| +| `text` | Plain text output (default) | Human reading, direct device application | +| `json` | JSON format | API integration, programmatic processing | +| `yaml` | YAML format | Configuration management, human-readable structured data | + +## Text Format (Default) + +The default text format outputs configuration commands in a format ready to be applied to network devices. + +**Usage:** +```bash +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --format text +``` + +**Example Output:** +``` +=== Remediation Configuration === +no hostname router-01 +hostname router-01-updated +interface GigabitEthernet0/0 + no description WAN Interface + description WAN Interface - Updated +interface Vlan20 + description Guest VLAN + ip address 10.0.20.1 255.255.255.0 +router ospf 1 + network 10.0.20.0 0.0.0.255 area 0 +ntp server 192.0.2.1 +``` + +**When to Use:** +- Directly copying commands to device CLI +- Manual review of changes +- Documentation and change records +- Generating configuration snippets + +--- + +## JSON Format + +JSON format provides structured output for programmatic processing. + +**Usage:** +```bash +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --format json +``` + +**Example Output:** +```json +{ + "config": [ + "no hostname router-01", + "hostname router-01-updated", + "interface GigabitEthernet0/0", + " no description WAN Interface", + " description WAN Interface - Updated", + "interface Vlan20", + " description Guest VLAN", + " ip address 10.0.20.1 255.255.255.0", + "router ospf 1", + " network 10.0.20.0 0.0.0.255 area 0", + "ntp server 192.0.2.1" + ] +} +``` + +**When to Use:** +- REST API integration +- Processing with JavaScript/Node.js +- Storing in document databases +- Web application integration +- CI/CD pipeline processing + +**Example - Processing with Python:** +```python +import json +import subprocess + +result = subprocess.run( + [ + "hier-config-cli", + "remediation", + "--platform", "ios", + "--running-config", "running.conf", + "--generated-config", "intended.conf", + "--format", "json", + ], + capture_output=True, + text=True, +) + +data = json.loads(result.stdout) +commands = data["config"] + +for cmd in commands: + print(f"Command: {cmd}") +``` + +**Example - Processing with jq:** +```bash +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --format json | jq '.config[]' +``` + +--- + +## YAML Format + +YAML format provides human-readable structured output. + +**Usage:** +```bash +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --format yaml +``` + +**Example Output:** +```yaml +config: +- no hostname router-01 +- hostname router-01-updated +- interface GigabitEthernet0/0 +- ' no description WAN Interface' +- ' description WAN Interface - Updated' +- interface Vlan20 +- ' description Guest VLAN' +- ' ip address 10.0.20.1 255.255.255.0' +- router ospf 1 +- ' network 10.0.20.0 0.0.0.255 area 0' +- ntp server 192.0.2.1 +``` + +**When to Use:** +- Ansible playbooks +- Configuration management systems +- Human-readable structured data +- Git-friendly diffs +- Documentation + +**Example - Using with Ansible:** +```yaml +- name: Generate and apply remediation + hosts: routers + tasks: + - name: Generate remediation configuration + command: > + hier-config-cli remediation + --platform ios + --running-config /tmp/{{ inventory_hostname }}_running.conf + --generated-config /tmp/{{ inventory_hostname }}_intended.conf + --format yaml + register: remediation_output + + - name: Parse YAML output + set_fact: + remediation_commands: "{{ remediation_output.stdout | from_yaml }}" + + - name: Display commands + debug: + var: remediation_commands.config +``` + +--- + +## Saving Output to Files + +All formats can be saved to files using the `--output` or `-o` option: + +```bash +# Save text output +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --format text \ + --output remediation.txt + +# Save JSON output +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --format json \ + --output remediation.json + +# Save YAML output +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --format yaml \ + --output remediation.yml +``` + +## Format Comparison + +### Text Format +**Pros:** +- ✅ Ready to apply directly to devices +- ✅ Easy to read +- ✅ Compact output + +**Cons:** +- ❌ No structure for programmatic parsing +- ❌ Harder to process with scripts + +### JSON Format +**Pros:** +- ✅ Easy to parse programmatically +- ✅ Wide language support +- ✅ Standard format for APIs + +**Cons:** +- ❌ More verbose +- ❌ Requires parsing before use +- ❌ Less human-readable + +### YAML Format +**Pros:** +- ✅ Human-readable +- ✅ Easy to parse +- ✅ Good for configuration management +- ✅ Git-friendly + +**Cons:** +- ❌ Requires parsing before use +- ❌ Whitespace-sensitive + +## Choosing the Right Format + +**Use Text when:** +- Manually reviewing changes +- Copying commands to device CLI +- Creating documentation +- Quick visual inspection + +**Use JSON when:** +- Building REST APIs +- Processing with web applications +- Integrating with JavaScript/Node.js +- Storing in document databases + +**Use YAML when:** +- Working with Ansible +- Need human-readable structured data +- Using configuration management tools +- Want Git-friendly diffs + +## Advanced Usage + +### Combining Formats + +Process multiple formats in a single workflow: + +```bash +# Generate text for manual review +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --format text \ + --output remediation.txt + +# Generate JSON for automation +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --format json \ + --output remediation.json + +# Generate YAML for Ansible +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --format yaml \ + --output remediation.yml +``` + +### Format Conversion + +Convert between formats using standard tools: + +```bash +# JSON to YAML +cat remediation.json | yq eval -P - > remediation.yml + +# YAML to JSON +cat remediation.yml | yq eval -o=json - > remediation.json +``` diff --git a/docs/platforms.md b/docs/platforms.md new file mode 100644 index 0000000..ba0f098 --- /dev/null +++ b/docs/platforms.md @@ -0,0 +1,260 @@ +# Supported Platforms + +hier-config-cli supports multiple network device platforms through the underlying [Hier Config](https://github.com/netdevops/hier_config) library. + +## Platform List + +| Platform | Code | Vendor | Description | +|----------|------|--------|-------------| +| Cisco IOS | `ios` | Cisco | Cisco IOS routers and switches | +| Cisco NX-OS | `nxos` | Cisco | Cisco Nexus switches | +| Cisco IOS XR | `iosxr` | Cisco | Cisco IOS XR routers | +| Arista EOS | `eos` | Arista | Arista switches | +| Juniper JunOS | `junos` | Juniper | Juniper routers and switches | +| VyOS | `vyos` | VyOS | VyOS routers | +| Fortinet FortiOS | `fortios` | Fortinet | Fortinet firewalls (v3.4.0+) | +| HP Comware5 | `hp_comware5` | HP | HP Comware5 switches | +| HP ProCurve | `hp_procurve` | HP | HP ProCurve switches | +| Generic | `generic` | N/A | Generic/unknown platform | + +## Platform-Specific Details + +### Cisco IOS (`ios`) + +**Supported Devices:** +- Catalyst switches (2960, 3560, 3750, 4500, 6500, 9000 series) +- ISR routers (1900, 2900, 3900, 4000 series) +- ASR routers (1000 series running IOS) + +**Configuration Format:** +- Hierarchical indentation +- Uses indentation to indicate parent-child relationships +- Commands like `interface`, `router`, `line` create context blocks + +**Example:** +```bash +hier-config-cli remediation \ + --platform ios \ + --running-config cisco_ios_running.conf \ + --generated-config cisco_ios_intended.conf +``` + +--- + +### Cisco NX-OS (`nxos`) + +**Supported Devices:** +- Nexus switches (3000, 5000, 6000, 7000, 9000 series) + +**Configuration Format:** +- Similar to IOS but with NX-OS specific syntax +- Support for VDC (Virtual Device Context) +- Different command structures for some features + +**Example:** +```bash +hier-config-cli remediation \ + --platform nxos \ + --running-config cisco_nxos_running.conf \ + --generated-config cisco_nxos_intended.conf +``` + +--- + +### Cisco IOS XR (`iosxr`) + +**Supported Devices:** +- ASR 9000 series +- NCS (Network Convergence System) routers +- CRS (Carrier Routing System) + +**Configuration Format:** +- Hierarchical with explicit ending (usually with `!`) +- Different syntax from IOS for some features + +**Example:** +```bash +hier-config-cli remediation \ + --platform iosxr \ + --running-config cisco_iosxr_running.conf \ + --generated-config cisco_iosxr_intended.conf +``` + +--- + +### Arista EOS (`eos`) + +**Supported Devices:** +- Arista switches (7000, 7500 series) +- Arista routers + +**Configuration Format:** +- Similar to Cisco IOS +- Some Arista-specific commands and features + +**Example:** +```bash +hier-config-cli remediation \ + --platform eos \ + --running-config arista_eos_running.conf \ + --generated-config arista_eos_intended.conf +``` + +--- + +### Juniper JunOS (`junos`) + +**Supported Devices:** +- MX Series routers +- EX Series switches +- SRX Series firewalls +- QFX Series switches + +**Configuration Format:** +- Hierarchical with curly braces `{}` +- Different syntax from Cisco-style platforms +- Set/delete command structure + +**Example:** +```bash +hier-config-cli remediation \ + --platform junos \ + --running-config juniper_junos_running.conf \ + --generated-config juniper_junos_intended.conf +``` + +**Note:** JunOS configurations use a different text representation than Cisco-style platforms. + +--- + +### VyOS (`vyos`) + +**Supported Devices:** +- VyOS routers (open-source network OS) + +**Configuration Format:** +- Hierarchical configuration +- Set/delete command structure + +**Example:** +```bash +hier-config-cli remediation \ + --platform vyos \ + --running-config vyos_running.conf \ + --generated-config vyos_intended.conf +``` + +--- + +### Fortinet FortiOS (`fortios`) + +**Supported Devices:** +- FortiGate firewalls + +**Configuration Format:** +- Hierarchical configuration with specific FortiOS syntax +- Edit/set/next command structure + +**Example:** +```bash +hier-config-cli remediation \ + --platform fortios \ + --running-config fortios_running.conf \ + --generated-config fortios_intended.conf +``` + +**Requirements:** Requires hier-config version 3.4.0 or higher. + +--- + +### HP Comware5 (`hp_comware5`) + +**Supported Devices:** +- HP switches running Comware5 OS + +**Example:** +```bash +hier-config-cli remediation \ + --platform hp_comware5 \ + --running-config hp_comware5_running.conf \ + --generated-config hp_comware5_intended.conf +``` + +--- + +### HP ProCurve (`hp_procurve`) + +**Supported Devices:** +- HP ProCurve switches + +**Example:** +```bash +hier-config-cli remediation \ + --platform hp_procurve \ + --running-config hp_procurve_running.conf \ + --generated-config hp_procurve_intended.conf +``` + +--- + +### Generic (`generic`) + +**Use Case:** +- Unknown or unsupported platforms +- Custom network devices +- Basic hierarchical configuration parsing + +**Example:** +```bash +hier-config-cli remediation \ + --platform generic \ + --running-config generic_running.conf \ + --generated-config generic_intended.conf +``` + +**Note:** Generic platform provides basic functionality but may not handle platform-specific syntax optimally. + +## Checking Available Platforms + +To see the current list of supported platforms in your installation: + +```bash +hier-config-cli list-platforms +``` + +## Platform Selection Tips + +1. **Use the specific platform** when available for best results +2. **Avoid using `generic`** unless absolutely necessary +3. **Check platform version compatibility** - some features may require specific versions +4. **Test in a lab environment** first when using a new platform + +## Adding New Platforms + +Platform support is provided by the underlying [Hier Config](https://github.com/netdevops/hier_config) library. To request a new platform: + +1. Check the [Hier Config repository](https://github.com/netdevops/hier_config) for existing support +2. Open an issue in the [hier-config-cli repository](https://github.com/netdevops/hier-config-cli/issues) +3. Provide example configurations from the target platform + +## Platform-Specific Considerations + +### Configuration Retrieval + +Different platforms have different commands for retrieving configurations: + +| Platform | Command | +|----------|---------| +| Cisco IOS/NX-OS/EOS | `show running-config` | +| Cisco IOS XR | `show running-config` | +| Juniper JunOS | `show configuration \| display set` | +| VyOS | `show configuration` | +| Fortinet FortiOS | `show full-configuration` | + +### Configuration Format + +Ensure your configuration files: +- Are saved in plain text format +- Do not include prompts or timestamps +- Are complete (not truncated) +- Match the platform's native format diff --git a/docs/quick-start.md b/docs/quick-start.md new file mode 100644 index 0000000..430e129 --- /dev/null +++ b/docs/quick-start.md @@ -0,0 +1,248 @@ +# Quick Start + +This guide will walk you through your first steps with hier-config-cli. + +## Prerequisites + +Make sure you have hier-config-cli installed. See the [Installation](installation.md) guide if you haven't already. + +## Basic Workflow + +The typical workflow with hier-config-cli involves three main operations: + +1. **Remediation**: Generate commands to transform running config into intended config +2. **Rollback**: Generate commands to revert changes (for safety) +3. **Future**: Preview the final configuration state + +## Example Configuration Files + +For this tutorial, we'll use simple Cisco IOS configurations. + +### Running Configuration (running.conf) + +``` +hostname router-01 +! +interface GigabitEthernet0/0 + description WAN Interface + ip address 10.0.1.1 255.255.255.0 +! +interface Vlan10 + description Management VLAN + ip address 10.0.10.1 255.255.255.0 +! +router ospf 1 + network 10.0.1.0 0.0.0.255 area 0 + network 10.0.10.0 0.0.0.255 area 0 +! +``` + +### Intended Configuration (intended.conf) + +``` +hostname router-01-updated +! +interface GigabitEthernet0/0 + description WAN Interface - Updated + ip address 10.0.1.1 255.255.255.0 +! +interface Vlan10 + description Management VLAN + ip address 10.0.10.1 255.255.255.0 +! +interface Vlan20 + description Guest VLAN + ip address 10.0.20.1 255.255.255.0 +! +router ospf 1 + network 10.0.1.0 0.0.0.255 area 0 + network 10.0.10.0 0.0.0.255 area 0 + network 10.0.20.0 0.0.0.255 area 0 +! +ntp server 192.0.2.1 +! +``` + +## Step 1: List Available Platforms + +First, check which platforms are supported: + +```bash +hier-config-cli list-platforms +``` + +Output: +``` +=== Available Platforms === + eos + fortios + generic + hp_comware5 + hp_procurve + ios + iosxr + junos + nxos + vyos +``` + +## Step 2: Generate Remediation + +Generate the commands needed to transform the running configuration into the intended configuration: + +```bash +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf +``` + +Output: +``` +=== Remediation Configuration === +no hostname router-01 +hostname router-01-updated +interface GigabitEthernet0/0 + no description WAN Interface + description WAN Interface - Updated +interface Vlan20 + description Guest VLAN + ip address 10.0.20.1 255.255.255.0 +router ospf 1 + network 10.0.20.0 0.0.0.255 area 0 +ntp server 192.0.2.1 +``` + +## Step 3: Generate Rollback + +Before applying changes, generate rollback commands for safety: + +```bash +hier-config-cli rollback \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf +``` + +Output: +``` +=== Rollback Configuration === +no hostname router-01-updated +hostname router-01 +interface GigabitEthernet0/0 + no description WAN Interface - Updated + description WAN Interface +no interface Vlan20 +router ospf 1 + no network 10.0.20.0 0.0.0.255 area 0 +no ntp server 192.0.2.1 +``` + +## Step 4: Preview Future State + +See what the complete configuration will look like after applying changes: + +```bash +hier-config-cli future \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf +``` + +Output: +``` +=== Future Configuration === +hostname router-01-updated +! +interface GigabitEthernet0/0 + description WAN Interface - Updated + ip address 10.0.1.1 255.255.255.0 +! +interface Vlan10 + description Management VLAN + ip address 10.0.10.1 255.255.255.0 +! +interface Vlan20 + description Guest VLAN + ip address 10.0.20.1 255.255.255.0 +! +router ospf 1 + network 10.0.1.0 0.0.0.255 area 0 + network 10.0.10.0 0.0.0.255 area 0 + network 10.0.20.0 0.0.0.255 area 0 +! +ntp server 192.0.2.1 +! +``` + +## Step 5: Save Output to File + +Save remediation commands to a file for later use: + +```bash +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --output remediation_commands.txt +``` + +Output: +``` +Remediation configuration written to: remediation_commands.txt +``` + +## Step 6: Use Different Output Formats + +### JSON Output + +```bash +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --format json +``` + +### YAML Output + +```bash +hier-config-cli remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf \ + --format yaml +``` + +## Step 7: Enable Verbose Logging + +For troubleshooting or understanding what's happening: + +```bash +# INFO level logging +hier-config-cli -v remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf + +# DEBUG level logging +hier-config-cli -vv remediation \ + --platform ios \ + --running-config running.conf \ + --generated-config intended.conf +``` + +## Best Practices + +1. **Always Generate Rollback First**: Before applying changes, always generate and save rollback commands +2. **Review Changes**: Manually review remediation commands before applying them +3. **Test in Lab**: Test configuration changes in a lab environment first +4. **Use Version Control**: Store your intended configurations in Git +5. **Document Changes**: Use the output files as documentation for change management + +## Next Steps + +- Learn about all available [Commands](commands.md) +- Explore [Supported Platforms](platforms.md) +- Check out [Integration Examples](integration/index.md) +- Read about [Output Formats](output-formats.md) diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..ee5a180 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +# Documentation build requirements for ReadTheDocs +mkdocs>=1.5.3 +mkdocs-material>=9.5.3 diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..6bf16d9 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,100 @@ +site_name: Hier Config CLI +site_description: A command-line interface tool for network configuration analysis, remediation, and rollback +site_author: James Williams +site_url: https://netdevops.github.io/hier-config-cli/ + +repo_name: netdevops/hier-config-cli +repo_url: https://github.com/netdevops/hier-config-cli +edit_uri: edit/main/docs/ + +theme: + name: material + palette: + # Palette toggle for light mode + - scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + # Palette toggle for dark mode + - scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.tabs + - navigation.sections + - navigation.top + - navigation.tracking + - search.suggest + - search.highlight + - content.tabs.link + - content.code.annotation + - content.code.copy + language: en + icon: + repo: fontawesome/brands/github + +plugins: + - search: + lang: en + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - admonition + - pymdownx.arithmatex: + generic: true + - footnotes + - pymdownx.details + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.mark + - attr_list + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.tabbed: + alternate_style: true + - toc: + permalink: true + +nav: + - Home: index.md + - Getting Started: + - Installation: installation.md + - Quick Start: quick-start.md + - User Guide: + - Commands: commands.md + - Supported Platforms: platforms.md + - Output Formats: output-formats.md + - Integration: + - Overview: integration/index.md + - Nornir: integration/nornir.md + - Ansible: integration/ansible.md + - CI/CD: integration/cicd.md + - Development: + - Contributing: development/contributing.md + - Testing: development/testing.md + - Code Quality: development/code-quality.md + - API Reference: api-reference.md + - Changelog: changelog.md + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/netdevops/hier-config-cli + - icon: fontawesome/brands/python + link: https://pypi.org/project/hier-config-cli/ + version: + provider: mike + +copyright: Copyright © 2024-2026 James Williams diff --git a/poetry.lock b/poetry.lock index 8eb4010..d173b9c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,6 +12,41 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] +[[package]] +name = "babel" +version = "2.17.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, + {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, +] + +[package.extras] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] + +[[package]] +name = "backrefs" +version = "6.1" +description = "A wrapper around re and regex that adds additional back references." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1"}, + {file = "backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7"}, + {file = "backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a"}, + {file = "backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05"}, + {file = "backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853"}, + {file = "backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0"}, + {file = "backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231"}, +] + +[package.extras] +extras = ["regex"] + [[package]] name = "black" version = "24.10.0" @@ -59,6 +94,141 @@ d = ["aiohttp (>=3.10)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "certifi" +version = "2026.1.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, + {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, +] + [[package]] name = "click" version = "8.1.7" @@ -85,7 +255,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} +markers = {main = "platform_system == \"Windows\""} [[package]] name = "coverage" @@ -223,6 +393,42 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "griffe" +version = "1.15.0" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3"}, + {file = "griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea"}, +] + +[package.dependencies] +colorama = ">=0.4" + +[package.extras] +pypi = ["pip (>=24.0)", "platformdirs (>=4.2)", "wheel (>=0.42)"] + [[package]] name = "hier-config" version = "3.4.0" @@ -238,6 +444,21 @@ files = [ [package.dependencies] pydantic = ">=2.9,<3.0" +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -250,6 +471,24 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "librt" version = "0.7.8" @@ -337,6 +576,284 @@ files = [ {file = "librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862"}, ] +[[package]] +name = "markdown" +version = "3.10.1" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3"}, + {file = "markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a"}, +] + +[package.extras] +docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python] (>=0.28.3)"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.3" +description = "Automatically link across pages in MkDocs." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9"}, + {file = "mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75"}, +] + +[package.dependencies] +Markdown = ">=3.3" +markupsafe = ">=2.0.1" +mkdocs = ">=1.1" + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mkdocs-material" +version = "9.7.1" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c"}, + {file = "mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8"}, +] + +[package.dependencies] +babel = ">=2.10" +backrefs = ">=5.7.post1" +colorama = ">=0.4" +jinja2 = ">=3.1" +markdown = ">=3.2" +mkdocs = ">=1.6" +mkdocs-material-extensions = ">=1.3" +paginate = ">=0.5" +pygments = ">=2.16" +pymdown-extensions = ">=10.2" +requests = ">=2.30" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<3)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<12.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + +[[package]] +name = "mkdocstrings" +version = "0.24.3" +description = "Automatic documentation from sources, for MkDocs." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mkdocstrings-0.24.3-py3-none-any.whl", hash = "sha256:5c9cf2a32958cd161d5428699b79c8b0988856b0d4a8c5baf8395fc1bf4087c3"}, + {file = "mkdocstrings-0.24.3.tar.gz", hash = "sha256:f327b234eb8d2551a306735436e157d0a22d45f79963c60a8b585d5f7a94c1d2"}, +] + +[package.dependencies] +click = ">=7.0" +Jinja2 = ">=2.11.1" +Markdown = ">=3.3" +MarkupSafe = ">=1.1" +mkdocs = ">=1.4" +mkdocs-autorefs = ">=0.3.1" +mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} +platformdirs = ">=2.2.0" +pymdown-extensions = ">=6.3" + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=0.5.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "1.10.0" +description = "A Python handler for mkdocstrings." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mkdocstrings_python-1.10.0-py3-none-any.whl", hash = "sha256:ba833fbd9d178a4b9d5cb2553a4df06e51dc1f51e41559a4d2398c16a6f69ecc"}, + {file = "mkdocstrings_python-1.10.0.tar.gz", hash = "sha256:71678fac657d4d2bb301eed4e4d2d91499c095fd1f8a90fa76422a87a5693828"}, +] + +[package.dependencies] +griffe = ">=0.44" +mkdocstrings = ">=0.24.2" + [[package]] name = "mypy" version = "1.19.1" @@ -423,6 +940,22 @@ files = [ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] +[[package]] +name = "paginate" +version = "0.5.7" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, + {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, +] + +[package.extras] +dev = ["pytest", "tox"] +lint = ["black"] + [[package]] name = "pathspec" version = "0.12.1" @@ -602,6 +1135,40 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pymdown-extensions" +version = "10.20.1" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pymdown_extensions-10.20.1-py3-none-any.whl", hash = "sha256:24af7feacbca56504b313b7b418c4f5e1317bb5fea60f03d57be7fcc40912aa0"}, + {file = "pymdown_extensions-10.20.1.tar.gz", hash = "sha256:e7e39c865727338d434b55f1dd8da51febcffcaebd6e1a0b9c836243f660740a"}, +] + +[package.dependencies] +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.19.1)"] + [[package]] name = "pytest" version = "8.3.4" @@ -645,13 +1212,28 @@ pytest = ">=6.2.5" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "pyyaml" version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -708,6 +1290,43 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +description = "A custom YAML tag for referencing environment variables in YAML files." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"}, + {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"}, +] + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "ruff" version = "0.8.6" @@ -736,6 +1355,18 @@ files = [ {file = "ruff-0.8.6.tar.gz", hash = "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5"}, ] +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + [[package]] name = "tomli" version = "2.2.1" @@ -803,7 +1434,68 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "urllib3" +version = "2.6.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "watchdog" +version = "6.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "75121bdb2b8a06217874d64b039b16d5dde10d61b59b70fa96029a0f38df9051" +content-hash = "bba12a7d28245e8901d829d5c27eafe2cafffb37705a145a42eb3d455ceac403" diff --git a/pyproject.toml b/pyproject.toml index 3412ced..ab14d97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,9 @@ pytest-cov = "^6.0.0" mypy = "^1.13.0" ruff = "^0.8.4" types-pyyaml = "^6.0.12" +mkdocs = "^1.5.3" +mkdocs-material = "^9.5.3" +mkdocstrings = {extras = ["python"], version = "^0.24.0"} [build-system] requires = ["poetry-core"]